From c8149fcfd3e36fb670b1b06bc1b216c8952809f3 Mon Sep 17 00:00:00 2001 From: harshsomankar123-tech Date: Tue, 7 Apr 2026 05:01:07 +0530 Subject: [PATCH 01/17] [RED] IDE-217: Add tests and boilerplate for resize/rotate visual placement --- .../TransformationInterface.kt | 29 ++++ .../main/res/drawable/ic_corner_handle.xml | 45 ++++++ .../ResizeRotateGestureDetectorTest.kt | 127 +++++++++++++++++ .../VisualPlacementResizeRotateTest.kt | 134 ++++++++++++++++++ 4 files changed, 335 insertions(+) create mode 100644 catroid/src/main/java/org/catrobat/catroid/visualplacement/TransformationInterface.kt create mode 100644 catroid/src/main/res/drawable/ic_corner_handle.xml create mode 100644 catroid/src/test/java/org/catrobat/catroid/test/visualplacement/ResizeRotateGestureDetectorTest.kt create mode 100644 catroid/src/test/java/org/catrobat/catroid/test/visualplacement/VisualPlacementResizeRotateTest.kt diff --git a/catroid/src/main/java/org/catrobat/catroid/visualplacement/TransformationInterface.kt b/catroid/src/main/java/org/catrobat/catroid/visualplacement/TransformationInterface.kt new file mode 100644 index 00000000000..060229d6fe2 --- /dev/null +++ b/catroid/src/main/java/org/catrobat/catroid/visualplacement/TransformationInterface.kt @@ -0,0 +1,29 @@ +/* + * Catroid: An on-device visual programming system for Android devices + * Copyright (C) 2010-2026 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * An additional term exception under section 7 of the GNU Affero + * General Public License, version 3, is available at + * http://developer.catrobat.org/license_additional_term + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.catrobat.catroid.visualplacement + +interface TransformationInterface : CoordinateInterface { + fun setScale(scaleFactor: Float) + fun setRotation(rotationDegrees: Float) +} diff --git a/catroid/src/main/res/drawable/ic_corner_handle.xml b/catroid/src/main/res/drawable/ic_corner_handle.xml new file mode 100644 index 00000000000..5f19d642dab --- /dev/null +++ b/catroid/src/main/res/drawable/ic_corner_handle.xml @@ -0,0 +1,45 @@ + + + + + + + + + diff --git a/catroid/src/test/java/org/catrobat/catroid/test/visualplacement/ResizeRotateGestureDetectorTest.kt b/catroid/src/test/java/org/catrobat/catroid/test/visualplacement/ResizeRotateGestureDetectorTest.kt new file mode 100644 index 00000000000..a44409f71b1 --- /dev/null +++ b/catroid/src/test/java/org/catrobat/catroid/test/visualplacement/ResizeRotateGestureDetectorTest.kt @@ -0,0 +1,127 @@ +/* + * Catroid: An on-device visual programming system for Android devices + * Copyright (C) 2010-2026 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * An additional term exception under section 7 of the GNU Affero + * General Public License, version 3, is available at + * http://developer.catrobat.org/license_additional_term + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.catrobat.catroid.test.visualplacement + +import org.catrobat.catroid.visualplacement.ResizeRotateGestureDetector +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@RunWith(JUnit4::class) +class ResizeRotateGestureDetectorTest { + + private lateinit var detector: ResizeRotateGestureDetector + private var lastScale = 1.0f + private var lastRotation = 0.0f + + @Before + fun setUp() { + lastScale = 1.0f + lastRotation = 0.0f + detector = ResizeRotateGestureDetector( + object : ResizeRotateGestureDetector.OnTransformGestureListener { + override fun onScale(scaleFactor: Float) { + lastScale = scaleFactor + } + + override fun onRotate(rotationDegrees: Float) { + lastRotation = rotationDegrees + } + } + ) + } + + @Test + fun testCalculateDistanceOrigin() { + val distance = ResizeRotateGestureDetector.calculateDistance(0f, 0f, 3f, 4f) + assertEquals(5.0f, distance, DELTA) + } + + @Test + fun testCalculateDistanceSamePoint() { + val distance = ResizeRotateGestureDetector.calculateDistance(5f, 5f, 5f, 5f) + assertEquals(0.0f, distance, DELTA) + } + + @Test + fun testCalculateDistanceNegativeCoords() { + val distance = ResizeRotateGestureDetector.calculateDistance(-3f, -4f, 0f, 0f) + assertEquals(5.0f, distance, DELTA) + } + + @Test + fun testCalculateAngleHorizontal() { + val angle = ResizeRotateGestureDetector.calculateAngle(0f, 0f, 10f, 0f) + assertEquals(0.0f, angle, DELTA) + } + + @Test + fun testCalculateAngleVerticalDown() { + val angle = ResizeRotateGestureDetector.calculateAngle(0f, 0f, 0f, 10f) + assertEquals(90.0f, angle, DELTA) + } + + @Test + fun testCalculateAngleVerticalUp() { + val angle = ResizeRotateGestureDetector.calculateAngle(0f, 0f, 0f, -10f) + assertEquals(-90.0f, angle, DELTA) + } + + @Test + fun testCalculateAngle45Degrees() { + val angle = ResizeRotateGestureDetector.calculateAngle(0f, 0f, 10f, 10f) + assertEquals(45.0f, angle, DELTA) + } + + @Test + fun testInitialState() { + assertFalse(detector.isTransforming) + assertEquals(1.0f, detector.cumulativeScale, DELTA) + assertEquals(0.0f, detector.cumulativeRotation, DELTA) + } + + @Test + fun testSetCumulativeScale() { + detector.cumulativeScale = 2.0f + assertEquals(2.0f, detector.cumulativeScale, DELTA) + } + + @Test + fun testSetCumulativeRotation() { + detector.cumulativeRotation = 45.0f + assertEquals(45.0f, detector.cumulativeRotation, DELTA) + } + + @Test + fun testSingleFingerDoesNotTransform() { + assertFalse(detector.isTransforming) + } + + companion object { + private const val DELTA = 0.01f + } +} diff --git a/catroid/src/test/java/org/catrobat/catroid/test/visualplacement/VisualPlacementResizeRotateTest.kt b/catroid/src/test/java/org/catrobat/catroid/test/visualplacement/VisualPlacementResizeRotateTest.kt new file mode 100644 index 00000000000..da2d4e20415 --- /dev/null +++ b/catroid/src/test/java/org/catrobat/catroid/test/visualplacement/VisualPlacementResizeRotateTest.kt @@ -0,0 +1,134 @@ +/* + * Catroid: An on-device visual programming system for Android devices + * Copyright (C) 2010-2026 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * An additional term exception under section 7 of the GNU Affero + * General Public License, version 3, is available at + * http://developer.catrobat.org/license_additional_term + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.catrobat.catroid.test.visualplacement + +import android.view.MotionEvent +import android.widget.ImageView +import org.catrobat.catroid.visualplacement.BoundingBoxOverlay +import org.catrobat.catroid.visualplacement.CoordinateInterface +import org.catrobat.catroid.visualplacement.ResizeRotateGestureDetector +import org.catrobat.catroid.visualplacement.VisualPlacementTouchListener +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` +import org.mockito.junit.MockitoJUnitRunner +import kotlin.math.abs + +@RunWith(MockitoJUnitRunner::class) +class VisualPlacementResizeRotateTest { + + @Mock + lateinit var imageView: ImageView + + @Mock + lateinit var motionEvent: MotionEvent + + @Mock + lateinit var coordinateInterface: CoordinateInterface + + @Mock + lateinit var boundingBoxOverlay: BoundingBoxOverlay + + private lateinit var listener: VisualPlacementTouchListener + private lateinit var detector: ResizeRotateGestureDetector + private var lastScale = 1.0f + private var lastRotation = 0.0f + + @Before + fun setUp() { + lastScale = 1.0f + lastRotation = 0.0f + listener = VisualPlacementTouchListener() + detector = ResizeRotateGestureDetector( + object : ResizeRotateGestureDetector.OnTransformGestureListener { + override fun onScale(scaleFactor: Float) { + lastScale = scaleFactor + } + + override fun onRotate(rotationDegrees: Float) { + lastRotation = rotationDegrees + } + } + ) + listener.setResizeRotateDetector(detector) + listener.setBoundingBoxOverlay(boundingBoxOverlay) + } + + @Test + fun testSingleTouchDragStillWorks() { + `when`(motionEvent.getPointerId(0)).thenReturn(0) + `when`(motionEvent.action).thenReturn(MotionEvent.ACTION_DOWN) + `when`(motionEvent.pointerCount).thenReturn(1) + `when`(imageView.x).thenReturn(0f) + `when`(imageView.y).thenReturn(0f) + + val result = listener.onTouch(imageView, motionEvent, coordinateInterface) + assertTrue(result) + verify(coordinateInterface).setXCoordinate(0f) + verify(coordinateInterface).setYCoordinate(-0f) + } + + @Test + fun testBoundingBoxOverlayUpdatedOnDrag() { + `when`(motionEvent.getPointerId(0)).thenReturn(0) + `when`(motionEvent.action).thenReturn(MotionEvent.ACTION_DOWN) + `when`(motionEvent.pointerCount).thenReturn(1) + `when`(imageView.x).thenReturn(10f) + `when`(imageView.y).thenReturn(20f) + + listener.onTouch(imageView, motionEvent, coordinateInterface) + verify(boundingBoxOverlay).updateOverlay() + } + + @Test + fun testDetectorNotTransformingWithSingleTouch() { + `when`(motionEvent.getPointerId(0)).thenReturn(0) + `when`(motionEvent.action).thenReturn(MotionEvent.ACTION_DOWN) + `when`(motionEvent.pointerCount).thenReturn(1) + `when`(imageView.x).thenReturn(0f) + `when`(imageView.y).thenReturn(0f) + + listener.onTouch(imageView, motionEvent, coordinateInterface) + assertFalse(detector.isTransforming) + } + + @Test + fun testDetectorInitialScaleIsOne() { + assertTrue(abs(detector.cumulativeScale - 1.0f) < DELTA) + } + + @Test + fun testDetectorInitialRotationIsZero() { + assertTrue(abs(detector.cumulativeRotation) < DELTA) + } + + companion object { + private const val DELTA = 0.001f + } +} From 71256020db47d705dc266a0c41338eb4f17623a6 Mon Sep 17 00:00:00 2001 From: harshsomankar123-tech Date: Tue, 7 Apr 2026 05:01:14 +0530 Subject: [PATCH 02/17] [GREEN] IDE-217: Implement resize/rotate logic and visual overlay in Stage --- .../catrobat/catroid/ui/ProjectActivity.kt | 14 +++ .../catrobat/catroid/ui/SpriteActivity.java | 19 +++ .../visualplacement/BoundingBoxOverlay.kt | 112 +++++++++++++++++ .../ResizeRotateGestureDetector.kt | 118 ++++++++++++++++++ .../VisualPlacementActivity.java | 75 ++++++++++- .../VisualPlacementTouchListener.java | 34 ++++- 6 files changed, 367 insertions(+), 5 deletions(-) create mode 100644 catroid/src/main/java/org/catrobat/catroid/visualplacement/BoundingBoxOverlay.kt create mode 100644 catroid/src/main/java/org/catrobat/catroid/visualplacement/ResizeRotateGestureDetector.kt diff --git a/catroid/src/main/java/org/catrobat/catroid/ui/ProjectActivity.kt b/catroid/src/main/java/org/catrobat/catroid/ui/ProjectActivity.kt index fabdf5eef7a..06dc0787cc8 100644 --- a/catroid/src/main/java/org/catrobat/catroid/ui/ProjectActivity.kt +++ b/catroid/src/main/java/org/catrobat/catroid/ui/ProjectActivity.kt @@ -44,6 +44,8 @@ import org.catrobat.catroid.content.Project import org.catrobat.catroid.content.StartScript import org.catrobat.catroid.content.bricks.Brick import org.catrobat.catroid.content.bricks.PlaceAtBrick +import org.catrobat.catroid.content.bricks.PointInDirectionBrick +import org.catrobat.catroid.content.bricks.SetSizeToBrick import org.catrobat.catroid.databinding.ActivityRecyclerBinding import org.catrobat.catroid.databinding.DialogNewActorBinding import org.catrobat.catroid.databinding.ProgressBarBinding @@ -258,11 +260,23 @@ class ProjectActivity : BaseCastActivity() { extras.getInt(VisualPlacementActivity.X_COORDINATE_BUNDLE_ARGUMENT) val yCoordinate = extras.getInt(VisualPlacementActivity.Y_COORDINATE_BUNDLE_ARGUMENT) + val placementScale = + extras.getFloat(VisualPlacementActivity.SCALE_BUNDLE_ARGUMENT, 1.0f) + val placementRotation = + extras.getFloat(VisualPlacementActivity.ROTATION_BUNDLE_ARGUMENT, 0.0f) val placeAtBrick = PlaceAtBrick(xCoordinate, yCoordinate) val currentSprite = projectManager.currentSprite val startScript = StartScript() currentSprite.prependScript(startScript) startScript.addBrick(placeAtBrick) + if (placementScale != 1.0f) { + val sizePercent = (placementScale * 100).toDouble() + startScript.addBrick(SetSizeToBrick(sizePercent)) + } + if (placementRotation != 0.0f) { + val direction = (placementRotation + 90).toDouble() + startScript.addBrick(PointInDirectionBrick(direction)) + } } SPRITE_FROM_LOCAL -> if (data != null && data.hasExtra(ProjectListActivity.IMPORT_LOCAL_INTENT)) { diff --git a/catroid/src/main/java/org/catrobat/catroid/ui/SpriteActivity.java b/catroid/src/main/java/org/catrobat/catroid/ui/SpriteActivity.java index f499330634c..4bdabde354c 100644 --- a/catroid/src/main/java/org/catrobat/catroid/ui/SpriteActivity.java +++ b/catroid/src/main/java/org/catrobat/catroid/ui/SpriteActivity.java @@ -104,6 +104,8 @@ import static org.catrobat.catroid.ui.SpriteActivityOnTabSelectedListenerKt.removeTabLayout; import static org.catrobat.catroid.ui.WebViewActivity.MEDIA_FILE_PATH; import static org.catrobat.catroid.visualplacement.VisualPlacementActivity.CHANGED_COORDINATES; +import static org.catrobat.catroid.visualplacement.VisualPlacementActivity.ROTATION_BUNDLE_ARGUMENT; +import static org.catrobat.catroid.visualplacement.VisualPlacementActivity.SCALE_BUNDLE_ARGUMENT; import static org.catrobat.catroid.visualplacement.VisualPlacementActivity.X_COORDINATE_BUNDLE_ARGUMENT; import static org.catrobat.catroid.visualplacement.VisualPlacementActivity.Y_COORDINATE_BUNDLE_ARGUMENT; @@ -430,6 +432,8 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) { int xCoordinate = extras.getInt(X_COORDINATE_BUNDLE_ARGUMENT); int yCoordinate = extras.getInt(Y_COORDINATE_BUNDLE_ARGUMENT); + float placementScale = extras.getFloat(SCALE_BUNDLE_ARGUMENT, 1.0f); + float placementRotation = extras.getFloat(ROTATION_BUNDLE_ARGUMENT, 0.0f); int brickHash = extras.getInt(EXTRA_BRICK_HASH); Fragment fragment = getCurrentFragment(); @@ -448,6 +452,21 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) { } } + if (placementScale != 1.0f) { + Sprite sprite = projectManager.getCurrentSprite(); + if (sprite != null) { + sprite.look.setScaleX(placementScale); + sprite.look.setScaleY(placementScale); + } + } + + if (placementRotation != 0.0f) { + Sprite sprite = projectManager.getCurrentSprite(); + if (sprite != null) { + sprite.look.setMotionDirectionInUserInterfaceDimensionUnit(placementRotation + 90); + } + } + setUndoMenuItemVisibility(extras.getBoolean(CHANGED_COORDINATES)); break; diff --git a/catroid/src/main/java/org/catrobat/catroid/visualplacement/BoundingBoxOverlay.kt b/catroid/src/main/java/org/catrobat/catroid/visualplacement/BoundingBoxOverlay.kt new file mode 100644 index 00000000000..f8a9f7d40c4 --- /dev/null +++ b/catroid/src/main/java/org/catrobat/catroid/visualplacement/BoundingBoxOverlay.kt @@ -0,0 +1,112 @@ +/* + * Catroid: An on-device visual programming system for Android devices + * Copyright (C) 2010-2026 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * An additional term exception under section 7 of the GNU Affero + * General Public License, version 3, is available at + * http://developer.catrobat.org/license_additional_term + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.catrobat.catroid.visualplacement + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.graphics.RectF +import android.view.View +import android.widget.ImageView +import androidx.core.content.ContextCompat +import org.catrobat.catroid.R + +class BoundingBoxOverlay(context: Context) : View(context) { + + private val boundingBoxPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + color = BOUNDING_BOX_COLOR + style = Paint.Style.STROKE + strokeWidth = BOUNDING_BOX_STROKE_WIDTH + } + + private val cornerHandleDrawable = ContextCompat.getDrawable(context, R.drawable.ic_corner_handle) + + private val handleSizePx: Int = + (HANDLE_SIZE_DP * context.resources.displayMetrics.density).toInt() + + var trackedImageView: ImageView? = null + + fun updateOverlay() { + invalidate() + } + + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + + val imageView = trackedImageView ?: return + val drawable = imageView.drawable ?: return + + val imgWidth = drawable.intrinsicWidth.toFloat() + val imgHeight = drawable.intrinsicHeight.toFloat() + + val scaleX = imageView.scaleX + val scaleY = imageView.scaleY + + val scaledWidth = imgWidth * scaleX + val scaledHeight = imgHeight * scaleY + + val viewCenterX = imageView.x + imageView.width / 2f + val viewCenterY = imageView.y + imageView.height / 2f + + val left = viewCenterX - scaledWidth / 2f + val top = viewCenterY - scaledHeight / 2f + val right = viewCenterX + scaledWidth / 2f + val bottom = viewCenterY + scaledHeight / 2f + + val rect = RectF(left, top, right, bottom) + + canvas.save() + canvas.rotate(imageView.rotation, viewCenterX, viewCenterY) + + canvas.drawRect(rect, boundingBoxPaint) + + drawCornerHandle(canvas, left, top, 0f) + drawCornerHandle(canvas, right, top, 90f) + drawCornerHandle(canvas, right, bottom, 180f) + drawCornerHandle(canvas, left, bottom, 270f) + + canvas.restore() + } + + private fun drawCornerHandle(canvas: Canvas, cx: Float, cy: Float, rotationDegrees: Float) { + val handle = cornerHandleDrawable ?: return + + canvas.save() + canvas.translate(cx, cy) + canvas.rotate(rotationDegrees) + + val halfSize = handleSizePx / 2 + handle.setBounds(-halfSize, -halfSize, halfSize, halfSize) + handle.draw(canvas) + + canvas.restore() + } + + companion object { + private const val BOUNDING_BOX_STROKE_WIDTH = 3f + private val BOUNDING_BOX_COLOR = Color.parseColor("#4FC3F7") + private const val HANDLE_SIZE_DP = 32 + } +} diff --git a/catroid/src/main/java/org/catrobat/catroid/visualplacement/ResizeRotateGestureDetector.kt b/catroid/src/main/java/org/catrobat/catroid/visualplacement/ResizeRotateGestureDetector.kt new file mode 100644 index 00000000000..c88504bd5f8 --- /dev/null +++ b/catroid/src/main/java/org/catrobat/catroid/visualplacement/ResizeRotateGestureDetector.kt @@ -0,0 +1,118 @@ +/* + * Catroid: An on-device visual programming system for Android devices + * Copyright (C) 2010-2026 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * An additional term exception under section 7 of the GNU Affero + * General Public License, version 3, is available at + * http://developer.catrobat.org/license_additional_term + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.catrobat.catroid.visualplacement + +import android.view.MotionEvent +import kotlin.math.atan2 +import kotlin.math.max +import kotlin.math.min +import kotlin.math.sqrt + +class ResizeRotateGestureDetector(private val listener: OnTransformGestureListener) { + + interface OnTransformGestureListener { + fun onScale(scaleFactor: Float) + fun onRotate(rotationDegrees: Float) + } + + var isTransforming: Boolean = false + private set + + var cumulativeScale: Float = 1.0f + var cumulativeRotation: Float = 0.0f + + private var initialDistance: Float = 0f + private var initialAngle: Float = 0f + + fun onTouchEvent(event: MotionEvent): Boolean { + val pointerCount = event.pointerCount + + if (pointerCount < 2) { + if (isTransforming) { + isTransforming = false + } + return false + } + + val x0 = event.getX(0) + val y0 = event.getY(0) + val x1 = event.getX(1) + val y1 = event.getY(1) + + val currentDistance = calculateDistance(x0, y0, x1, y1) + val currentAngle = calculateAngle(x0, y0, x1, y1) + + when (event.actionMasked) { + MotionEvent.ACTION_POINTER_DOWN -> { + isTransforming = true + initialDistance = currentDistance + initialAngle = currentAngle + return true + } + + MotionEvent.ACTION_MOVE -> { + if (isTransforming && initialDistance > 0) { + val scaleFactor = currentDistance / initialDistance + val newScale = (cumulativeScale * scaleFactor) + .coerceIn(MIN_SCALE, MAX_SCALE) + listener.onScale(newScale) + + val angleDelta = currentAngle - initialAngle + listener.onRotate(cumulativeRotation + angleDelta) + return true + } + } + + MotionEvent.ACTION_POINTER_UP -> { + if (isTransforming && initialDistance > 0) { + val finalScaleFactor = currentDistance / initialDistance + cumulativeScale = (cumulativeScale * finalScaleFactor) + .coerceIn(MIN_SCALE, MAX_SCALE) + + val finalAngleDelta = currentAngle - initialAngle + cumulativeRotation += finalAngleDelta + } + isTransforming = false + return true + } + } + return false + } + + companion object { + private const val MIN_SCALE = 0.1f + private const val MAX_SCALE = 5.0f + + @JvmStatic + fun calculateDistance(x0: Float, y0: Float, x1: Float, y1: Float): Float { + val dx = x1 - x0 + val dy = y1 - y0 + return sqrt(dx * dx + dy * dy) + } + + @JvmStatic + fun calculateAngle(x0: Float, y0: Float, x1: Float, y1: Float): Float = + Math.toDegrees(atan2((y1 - y0).toDouble(), (x1 - x0).toDouble())).toFloat() + } +} diff --git a/catroid/src/main/java/org/catrobat/catroid/visualplacement/VisualPlacementActivity.java b/catroid/src/main/java/org/catrobat/catroid/visualplacement/VisualPlacementActivity.java index 8d7bd7e9122..bf859d6f049 100644 --- a/catroid/src/main/java/org/catrobat/catroid/visualplacement/VisualPlacementActivity.java +++ b/catroid/src/main/java/org/catrobat/catroid/visualplacement/VisualPlacementActivity.java @@ -1,6 +1,6 @@ /* * Catroid: An on-device visual programming system for Android devices - * Copyright (C) 2010-2025 The Catrobat Team + * Copyright (C) 2010-2026 The Catrobat Team * () * * This program is free software: you can redistribute it and/or modify @@ -92,12 +92,14 @@ import static org.catrobat.catroid.utils.ShowTextUtils.sanitizeTextSize; public class VisualPlacementActivity extends BaseCastActivity implements View.OnTouchListener, - DialogInterface.OnClickListener, CoordinateInterface { + DialogInterface.OnClickListener, TransformationInterface { public static final String TAG = VisualPlacementActivity.class.getSimpleName(); public static final String X_COORDINATE_BUNDLE_ARGUMENT = "xCoordinate"; public static final String Y_COORDINATE_BUNDLE_ARGUMENT = "yCoordinate"; + public static final String SCALE_BUNDLE_ARGUMENT = "scaleFactor"; + public static final String ROTATION_BUNDLE_ARGUMENT = "rotationDegrees"; public static final String CHANGED_COORDINATES = "changedCoordinates"; private ProjectManager projectManager; @@ -114,6 +116,11 @@ public class VisualPlacementActivity extends BaseCastActivity implements View.On private float translateX; private float translateY; + private float currentScale = 1.0f; + private float currentRotation = 0.0f; + private float initialSpriteScale = 1.0f; + private float initialSpriteRotation = 0.0f; + private boolean isText; private String text; private String textColor; @@ -125,6 +132,8 @@ public class VisualPlacementActivity extends BaseCastActivity implements View.On private float layoutWidthRatio; private float layoutHeightRatio; private VisualPlacementTouchListener visualPlacementTouchListener; + private ResizeRotateGestureDetector resizeRotateDetector; + private BoundingBoxOverlay boundingBoxOverlay; @Override public boolean onCreateOptionsMenu(Menu menu) { @@ -186,7 +195,29 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { } else { setRequestedOrientation(SCREEN_ORIENTATION_PORTRAIT); } + + resizeRotateDetector = new ResizeRotateGestureDetector(new ResizeRotateGestureDetector.OnTransformGestureListener() { + @Override + public void onScale(float scaleFactor) { + currentScale = scaleFactor; + if (imageView != null) { + float appliedScale = initialSpriteScale * currentScale; + imageView.setScaleX(appliedScale); + imageView.setScaleY(appliedScale); + } + } + + @Override + public void onRotate(float rotationDegrees) { + currentRotation = rotationDegrees; + if (imageView != null) { + imageView.setRotation(initialSpriteRotation + currentRotation); + } + } + }); + visualPlacementTouchListener = new VisualPlacementTouchListener(); + visualPlacementTouchListener.setResizeRotateDetector(resizeRotateDetector); frameLayout = findViewById(R.id.frame_container); @@ -219,6 +250,14 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { setBackground(); showMovableImageView(); + boundingBoxOverlay = new BoundingBoxOverlay(this); + boundingBoxOverlay.setTrackedImageView(imageView); + frameLayout.addView(boundingBoxOverlay, + new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT)); + + visualPlacementTouchListener.setBoundingBoxOverlay(boundingBoxOverlay); + toolbar.bringToFront(); frameLayout.setOnTouchListener(this); } @@ -307,11 +346,18 @@ public void showMovableImageView() { if (scaleX > 0.01) { imageView.setScaleX(scaleX); + initialSpriteScale = scaleX; } if (scaleY > 0.01) { imageView.setScaleY(scaleY); } + + resizeRotateDetector.setCumulativeScale(1.0f); + resizeRotateDetector.setCumulativeRotation(0.0f); + currentScale = 1.0f; + currentRotation = 0.0f; + frameLayout.addView(imageView); } @@ -374,7 +420,10 @@ public void onBackPressed() { int xCoordinate = Math.round(xCoord / layoutWidthRatio); int yCoordinate = Math.round(yCoord / layoutHeightRatio); - if (translateX != xCoordinate || translateY != yCoordinate) { + boolean hasChanges = translateX != xCoordinate || translateY != yCoordinate + || currentScale != 1.0f || currentRotation != 0.0f; + + if (hasChanges) { showSaveChangesDialog(this); } else { finish(); @@ -403,7 +452,15 @@ private void finishWithResult() { extras.putInt(X_COORDINATE_BUNDLE_ARGUMENT, xCoordinate); extras.putInt(Y_COORDINATE_BUNDLE_ARGUMENT, yCoordinate); - extras.putBoolean(CHANGED_COORDINATES, translateX != xCoordinate || translateY != yCoordinate); + + float finalScale = initialSpriteScale * currentScale; + float finalRotation = initialSpriteRotation + currentRotation; + extras.putFloat(SCALE_BUNDLE_ARGUMENT, finalScale); + extras.putFloat(ROTATION_BUNDLE_ARGUMENT, finalRotation); + + boolean hasChanges = translateX != xCoordinate || translateY != yCoordinate + || currentScale != 1.0f || currentRotation != 0.0f; + extras.putBoolean(CHANGED_COORDINATES, hasChanges); returnIntent.putExtras(extras); setResult(Activity.RESULT_OK, returnIntent); @@ -437,4 +494,14 @@ public void setYCoordinate(float yCoordinate) { yCoord = yCoordinate; } } + + @Override + public void setScale(float scaleFactor) { + currentScale = scaleFactor; + } + + @Override + public void setRotation(float rotationDegrees) { + currentRotation = rotationDegrees; + } } diff --git a/catroid/src/main/java/org/catrobat/catroid/visualplacement/VisualPlacementTouchListener.java b/catroid/src/main/java/org/catrobat/catroid/visualplacement/VisualPlacementTouchListener.java index 7d9833191ea..0651f3fd851 100644 --- a/catroid/src/main/java/org/catrobat/catroid/visualplacement/VisualPlacementTouchListener.java +++ b/catroid/src/main/java/org/catrobat/catroid/visualplacement/VisualPlacementTouchListener.java @@ -1,6 +1,6 @@ /* * Catroid: An on-device visual programming system for Android devices - * Copyright (C) 2010-2025 The Catrobat Team + * Copyright (C) 2010-2026 The Catrobat Team * () * * This program is free software: you can redistribute it and/or modify @@ -39,6 +39,9 @@ public class VisualPlacementTouchListener { private float previousY; private List recentTouchEventsData = new ArrayList<>(); + private ResizeRotateGestureDetector resizeRotateDetector; + private BoundingBoxOverlay boundingBoxOverlay; + private final class TouchEventData { private final long timeStamp; private final float xCoordinate; @@ -55,7 +58,29 @@ private void setMode(Mode mode) { this.mode = mode; } + public void setResizeRotateDetector(ResizeRotateGestureDetector detector) { + this.resizeRotateDetector = detector; + } + + public void setBoundingBoxOverlay(BoundingBoxOverlay overlay) { + this.boundingBoxOverlay = overlay; + } + public boolean onTouch(ImageView imageView, MotionEvent event, CoordinateInterface coordinateInterface) { + if (resizeRotateDetector != null && event.getPointerCount() >= 2) { + boolean handled = resizeRotateDetector.onTouchEvent(event); + if (handled) { + updateBoundingBox(); + return true; + } + } + + if (resizeRotateDetector != null && resizeRotateDetector.isTransforming()) { + resizeRotateDetector.onTouchEvent(event); + updateBoundingBox(); + return true; + } + if (event.getPointerId(0) == 0) { float currentX = event.getRawX(); float currentY = event.getRawY(); @@ -108,12 +133,19 @@ public boolean onTouch(ImageView imageView, MotionEvent event, CoordinateInterfa } coordinateInterface.setXCoordinate(imageView.getX()); coordinateInterface.setYCoordinate(-imageView.getY()); + updateBoundingBox(); return true; } else { return false; } } + private void updateBoundingBox() { + if (boundingBoxOverlay != null) { + boundingBoxOverlay.updateOverlay(); + } + } + private void removeObsoleteTouchEventsData(long timeStamp) { List obsoleteTouchEventsData = new ArrayList<>(); for (TouchEventData touchEventData : recentTouchEventsData) { From 1cddac978409ff2cea7c44d8df6eb9a9f6fe3203 Mon Sep 17 00:00:00 2001 From: harshsomankar123-tech Date: Tue, 7 Apr 2026 12:34:55 +0530 Subject: [PATCH 03/17] Fix: SonarCloud reliability and Android Lint issues in Resize and rotate feature (IDE-217) --- .../catrobat/catroid/ui/ProjectActivity.kt | 13 ++-- .../catrobat/catroid/ui/SpriteActivity.java | 4 +- .../visualplacement/BoundingBoxOverlay.kt | 46 +++++++------ .../ResizeRotateGestureDetector.kt | 10 ++- .../VisualPlacementActivity.java | 66 +++++++++++++------ .../VisualPlacementTouchListener.java | 2 +- ...VisualPlacementPositionCorrectionTest.java | 2 + .../VisualPlacementTouchListenerTest.java | 4 ++ 8 files changed, 93 insertions(+), 54 deletions(-) diff --git a/catroid/src/main/java/org/catrobat/catroid/ui/ProjectActivity.kt b/catroid/src/main/java/org/catrobat/catroid/ui/ProjectActivity.kt index 06dc0787cc8..0aa2547e993 100644 --- a/catroid/src/main/java/org/catrobat/catroid/ui/ProjectActivity.kt +++ b/catroid/src/main/java/org/catrobat/catroid/ui/ProjectActivity.kt @@ -91,6 +91,11 @@ class ProjectActivity : BaseCastActivity() { const val SPRITE_CAMERA = 3 const val SPRITE_OBJECT = 4 const val SPRITE_FROM_LOCAL = 5 + + const val DEFAULT_SCALE = 1.0f + const val PERCENTAGE_MULTIPLIER = 100.0 + const val DEFAULT_ROTATION = 0.0f + const val ROTATION_OFFSET = 90.0 } private lateinit var binding: ActivityRecyclerBinding @@ -269,12 +274,12 @@ class ProjectActivity : BaseCastActivity() { val startScript = StartScript() currentSprite.prependScript(startScript) startScript.addBrick(placeAtBrick) - if (placementScale != 1.0f) { - val sizePercent = (placementScale * 100).toDouble() + if (placementScale != DEFAULT_SCALE) { + val sizePercent = (placementScale * PERCENTAGE_MULTIPLIER).toDouble() startScript.addBrick(SetSizeToBrick(sizePercent)) } - if (placementRotation != 0.0f) { - val direction = (placementRotation + 90).toDouble() + if (placementRotation != DEFAULT_ROTATION) { + val direction = (placementRotation + ROTATION_OFFSET).toDouble() startScript.addBrick(PointInDirectionBrick(direction)) } } diff --git a/catroid/src/main/java/org/catrobat/catroid/ui/SpriteActivity.java b/catroid/src/main/java/org/catrobat/catroid/ui/SpriteActivity.java index 4bdabde354c..23461af1b7a 100644 --- a/catroid/src/main/java/org/catrobat/catroid/ui/SpriteActivity.java +++ b/catroid/src/main/java/org/catrobat/catroid/ui/SpriteActivity.java @@ -454,7 +454,7 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) { if (placementScale != 1.0f) { Sprite sprite = projectManager.getCurrentSprite(); - if (sprite != null) { + if (sprite != null && sprite.look != null) { sprite.look.setScaleX(placementScale); sprite.look.setScaleY(placementScale); } @@ -462,7 +462,7 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) { if (placementRotation != 0.0f) { Sprite sprite = projectManager.getCurrentSprite(); - if (sprite != null) { + if (sprite != null && sprite.look != null) { sprite.look.setMotionDirectionInUserInterfaceDimensionUnit(placementRotation + 90); } } diff --git a/catroid/src/main/java/org/catrobat/catroid/visualplacement/BoundingBoxOverlay.kt b/catroid/src/main/java/org/catrobat/catroid/visualplacement/BoundingBoxOverlay.kt index f8a9f7d40c4..73b1acd35c8 100644 --- a/catroid/src/main/java/org/catrobat/catroid/visualplacement/BoundingBoxOverlay.kt +++ b/catroid/src/main/java/org/catrobat/catroid/visualplacement/BoundingBoxOverlay.kt @@ -31,6 +31,9 @@ import android.graphics.RectF import android.view.View import android.widget.ImageView import androidx.core.content.ContextCompat +import androidx.core.graphics.toColorInt +import androidx.core.graphics.withRotation +import androidx.core.graphics.withTranslation import org.catrobat.catroid.R class BoundingBoxOverlay(context: Context) : View(context) { @@ -46,6 +49,8 @@ class BoundingBoxOverlay(context: Context) : View(context) { private val handleSizePx: Int = (HANDLE_SIZE_DP * context.resources.displayMetrics.density).toInt() + private val rect = RectF() + var trackedImageView: ImageView? = null fun updateOverlay() { @@ -75,38 +80,37 @@ class BoundingBoxOverlay(context: Context) : View(context) { val right = viewCenterX + scaledWidth / 2f val bottom = viewCenterY + scaledHeight / 2f - val rect = RectF(left, top, right, bottom) - - canvas.save() - canvas.rotate(imageView.rotation, viewCenterX, viewCenterY) - - canvas.drawRect(rect, boundingBoxPaint) + rect.set(left, top, right, bottom) - drawCornerHandle(canvas, left, top, 0f) - drawCornerHandle(canvas, right, top, 90f) - drawCornerHandle(canvas, right, bottom, 180f) - drawCornerHandle(canvas, left, bottom, 270f) + canvas.withRotation(imageView.rotation, viewCenterX, viewCenterY) { + drawRect(rect, boundingBoxPaint) - canvas.restore() + drawCornerHandle(this, left, top, ROTATION_0) + drawCornerHandle(this, right, top, ROTATION_90) + drawCornerHandle(this, right, bottom, ROTATION_180) + drawCornerHandle(this, left, bottom, ROTATION_270) + } } private fun drawCornerHandle(canvas: Canvas, cx: Float, cy: Float, rotationDegrees: Float) { val handle = cornerHandleDrawable ?: return - canvas.save() - canvas.translate(cx, cy) - canvas.rotate(rotationDegrees) - - val halfSize = handleSizePx / 2 - handle.setBounds(-halfSize, -halfSize, halfSize, halfSize) - handle.draw(canvas) - - canvas.restore() + canvas.withTranslation(cx, cy) { + withRotation(rotationDegrees) { + val halfSize = handleSizePx / 2 + handle.setBounds(-halfSize, -halfSize, halfSize, halfSize) + handle.draw(this) + } + } } companion object { private const val BOUNDING_BOX_STROKE_WIDTH = 3f - private val BOUNDING_BOX_COLOR = Color.parseColor("#4FC3F7") + private val BOUNDING_BOX_COLOR = "#4FC3F7".toColorInt() private const val HANDLE_SIZE_DP = 32 + private const val ROTATION_0 = 0f + private const val ROTATION_90 = 90f + private const val ROTATION_180 = 180f + private const val ROTATION_270 = 270f } } diff --git a/catroid/src/main/java/org/catrobat/catroid/visualplacement/ResizeRotateGestureDetector.kt b/catroid/src/main/java/org/catrobat/catroid/visualplacement/ResizeRotateGestureDetector.kt index c88504bd5f8..4c096561835 100644 --- a/catroid/src/main/java/org/catrobat/catroid/visualplacement/ResizeRotateGestureDetector.kt +++ b/catroid/src/main/java/org/catrobat/catroid/visualplacement/ResizeRotateGestureDetector.kt @@ -25,8 +25,7 @@ package org.catrobat.catroid.visualplacement import android.view.MotionEvent import kotlin.math.atan2 -import kotlin.math.max -import kotlin.math.min +import kotlin.math.atan2 import kotlin.math.sqrt class ResizeRotateGestureDetector(private val listener: OnTransformGestureListener) { @@ -71,7 +70,7 @@ class ResizeRotateGestureDetector(private val listener: OnTransformGestureListen return true } - MotionEvent.ACTION_MOVE -> { + MotionEvent.ACTION_MOVE -> if (isTransforming && initialDistance > 0) { val scaleFactor = currentDistance / initialDistance val newScale = (cumulativeScale * scaleFactor) @@ -80,9 +79,8 @@ class ResizeRotateGestureDetector(private val listener: OnTransformGestureListen val angleDelta = currentAngle - initialAngle listener.onRotate(cumulativeRotation + angleDelta) - return true - } - } + true + } else false MotionEvent.ACTION_POINTER_UP -> { if (isTransforming && initialDistance > 0) { diff --git a/catroid/src/main/java/org/catrobat/catroid/visualplacement/VisualPlacementActivity.java b/catroid/src/main/java/org/catrobat/catroid/visualplacement/VisualPlacementActivity.java index bf859d6f049..1d65d8bf6b5 100644 --- a/catroid/src/main/java/org/catrobat/catroid/visualplacement/VisualPlacementActivity.java +++ b/catroid/src/main/java/org/catrobat/catroid/visualplacement/VisualPlacementActivity.java @@ -170,6 +170,10 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { setContentView(R.layout.visual_placement_layout); Bundle extras = getIntent().getExtras(); + if (extras == null) { + finish(); + return; + } translateX = extras.getInt(EXTRA_X_TRANSFORM); translateY = extras.getInt(EXTRA_Y_TRANSFORM); if (extras.containsKey(EXTRA_TEXT)) { @@ -265,13 +269,18 @@ public void onRotate(float rotationDegrees) { private void setBackground() { try { Bitmap backgroundBitmap = ProjectManagerExtensionsKt.getProjectBitmap(projectManager); - Bitmap scaledBackgroundBitmap = Bitmap.createScaledBitmap(backgroundBitmap, - (int) (backgroundBitmap.getWidth() * layoutWidthRatio), - (int) (backgroundBitmap.getHeight() * layoutHeightRatio), true); - Drawable backgroundDrawable = new BitmapDrawable(getResources(), scaledBackgroundBitmap); - backgroundDrawable.setColorFilter(Color.parseColor("#6F000000"), PorterDuff.Mode.SRC_ATOP); - - frameLayout.setBackground(backgroundDrawable); + if (backgroundBitmap != null) { + Bitmap scaledBackgroundBitmap = Bitmap.createScaledBitmap(backgroundBitmap, + (int) (backgroundBitmap.getWidth() * layoutWidthRatio), + (int) (backgroundBitmap.getHeight() * layoutHeightRatio), true); + Drawable backgroundDrawable = new BitmapDrawable(getResources(), scaledBackgroundBitmap); + backgroundDrawable.setColorFilter(Color.parseColor("#6F000000"), PorterDuff.Mode.SRC_ATOP); + + frameLayout.setBackground(backgroundDrawable); + if (backgroundBitmap != scaledBackgroundBitmap) { + backgroundBitmap.recycle(); + } + } } catch (Exception e) { frameLayout.setBackgroundColor(Color.WHITE); } @@ -287,7 +296,7 @@ public void showMovableImageView() { if (isText) { visualPlacementBitmap = convertTextToBitmap(); } else { - if (!currentSprite.look.getImagePath().isEmpty()) { + if (currentSprite.look != null && !currentSprite.look.getImagePath().isEmpty()) { objectLookPath = currentSprite.look.getImagePath(); scaleX = currentSprite.look.getScaleX(); scaleY = currentSprite.look.getScaleY(); @@ -295,15 +304,23 @@ public void showMovableImageView() { rotation = currentSprite.look.getMotionDirectionInUserInterfaceDimensionUnit(); visualPlacementBitmap = BitmapFactory.decodeFile(objectLookPath, bitmapOptions); } else if (currentSprite.getLookList().size() != 0) { - objectLookPath = currentSprite.getLookList().get(0).getFile().getAbsolutePath(); - visualPlacementBitmap = BitmapFactory.decodeFile(objectLookPath, bitmapOptions); + if (currentSprite.getLookList().get(0).getFile() != null) { + objectLookPath = currentSprite.getLookList().get(0).getFile().getAbsolutePath(); + visualPlacementBitmap = BitmapFactory.decodeFile(objectLookPath, bitmapOptions); + } else { + visualPlacementBitmap = null; + } } else { Drawable drawable = ContextCompat.getDrawable(getApplicationContext(), R.drawable.pc_toolbar_icon); - visualPlacementBitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888); - Canvas canvas = new Canvas(visualPlacementBitmap); - drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); - drawable.draw(canvas); + if (drawable == null) { + visualPlacementBitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888); + } else { + visualPlacementBitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(visualPlacementBitmap); + drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); + drawable.draw(canvas); + } } } @@ -324,14 +341,23 @@ public void showMovableImageView() { break; } - visualPlacementBitmap = Bitmap.createBitmap(visualPlacementBitmap, 0, 0, - visualPlacementBitmap.getWidth(), - visualPlacementBitmap.getHeight(), matrix, true); + if (visualPlacementBitmap != null) { + Bitmap rotatedBitmap = Bitmap.createBitmap(visualPlacementBitmap, 0, 0, + visualPlacementBitmap.getWidth(), + visualPlacementBitmap.getHeight(), matrix, true); - Bitmap scaledBitmap = Bitmap.createScaledBitmap(visualPlacementBitmap, (int) (visualPlacementBitmap.getWidth() * layoutWidthRatio), - (int) (visualPlacementBitmap.getHeight() * layoutHeightRatio), true); + if (visualPlacementBitmap != rotatedBitmap) { + visualPlacementBitmap.recycle(); + } + visualPlacementBitmap = rotatedBitmap; - imageView.setImageBitmap(scaledBitmap); + Bitmap scaledBitmap = Bitmap.createScaledBitmap(visualPlacementBitmap, (int) (visualPlacementBitmap.getWidth() * layoutWidthRatio), + (int) (visualPlacementBitmap.getHeight() * layoutHeightRatio), true); + imageView.setImageBitmap(scaledBitmap); + if (visualPlacementBitmap != scaledBitmap) { + visualPlacementBitmap.recycle(); + } + } imageView.setScaleType(ImageView.ScaleType.CENTER); if (isText) { diff --git a/catroid/src/main/java/org/catrobat/catroid/visualplacement/VisualPlacementTouchListener.java b/catroid/src/main/java/org/catrobat/catroid/visualplacement/VisualPlacementTouchListener.java index 0651f3fd851..0ab3e1568c6 100644 --- a/catroid/src/main/java/org/catrobat/catroid/visualplacement/VisualPlacementTouchListener.java +++ b/catroid/src/main/java/org/catrobat/catroid/visualplacement/VisualPlacementTouchListener.java @@ -81,7 +81,7 @@ public boolean onTouch(ImageView imageView, MotionEvent event, CoordinateInterfa return true; } - if (event.getPointerId(0) == 0) { + if (event.getPointerCount() > 0 && event.getPointerId(0) == 0) { float currentX = event.getRawX(); float currentY = event.getRawY(); long motionEventTime = event.getEventTime(); diff --git a/catroid/src/test/java/org/catrobat/catroid/test/visualplacement/VisualPlacementPositionCorrectionTest.java b/catroid/src/test/java/org/catrobat/catroid/test/visualplacement/VisualPlacementPositionCorrectionTest.java index 6ea7fdf23a5..b35e3a38c35 100644 --- a/catroid/src/test/java/org/catrobat/catroid/test/visualplacement/VisualPlacementPositionCorrectionTest.java +++ b/catroid/src/test/java/org/catrobat/catroid/test/visualplacement/VisualPlacementPositionCorrectionTest.java @@ -70,6 +70,8 @@ public class VisualPlacementPositionCorrectionTest { @Before public void setUp() { when(motionEvent.getDownTime()).thenReturn((long) 0); + when(motionEvent.getPointerCount()).thenReturn(1); + when(motionEvent.getPointerId(0)).thenReturn(0); } public void triggerTouchDownEvent(float startPositionX, float startPositionY) { diff --git a/catroid/src/test/java/org/catrobat/catroid/test/visualplacement/VisualPlacementTouchListenerTest.java b/catroid/src/test/java/org/catrobat/catroid/test/visualplacement/VisualPlacementTouchListenerTest.java index edb36406370..b89a061b0e8 100644 --- a/catroid/src/test/java/org/catrobat/catroid/test/visualplacement/VisualPlacementTouchListenerTest.java +++ b/catroid/src/test/java/org/catrobat/catroid/test/visualplacement/VisualPlacementTouchListenerTest.java @@ -61,6 +61,7 @@ public class VisualPlacementTouchListenerTest { @Test public void testTouchDownSetCoordinates() { + when(firstEvent.getPointerCount()).thenReturn(1); when(firstEvent.getAction()).thenReturn(MotionEvent.ACTION_DOWN); when(imageView.getX()).thenReturn(13f); when(imageView.getY()).thenReturn(17f); @@ -71,6 +72,7 @@ public void testTouchDownSetCoordinates() { @Test public void testTouchMoveSetCoordinates() { + when(secondEvent.getPointerCount()).thenReturn(1); when(secondEvent.getAction()).thenReturn(MotionEvent.ACTION_MOVE); when(imageView.getX()).thenReturn(10f); when(imageView.getY()).thenReturn(21f); @@ -83,6 +85,7 @@ public void testTouchMoveSetCoordinates() { @Test public void testOnTouchReturnFalse() { + when(firstEvent.getPointerCount()).thenReturn(1); when(firstEvent.getPointerId(0)).thenReturn(1); boolean returnValue = listener.onTouch(imageView, firstEvent, coordinateInterface); assertFalse(returnValue); @@ -91,6 +94,7 @@ public void testOnTouchReturnFalse() { @Test public void testOnTouchReturnTrue() { + when(firstEvent.getPointerCount()).thenReturn(1); when(firstEvent.getPointerId(0)).thenReturn(0); boolean returnValue = listener.onTouch(imageView, firstEvent, coordinateInterface); assertTrue(returnValue); From 2aabacc0a9216935596faa4ce7349bc19277bf7e Mon Sep 17 00:00:00 2001 From: harshsomankar123-tech Date: Tue, 7 Apr 2026 14:50:01 +0530 Subject: [PATCH 04/17] Fix: detekt unused imports, missing braces and SonarCloud reliability in ProjectActivity.kt (IDE-217) --- .../main/java/org/catrobat/catroid/ui/ProjectActivity.kt | 2 +- .../catroid/visualplacement/BoundingBoxOverlay.kt | 1 - .../visualplacement/ResizeRotateGestureDetector.kt | 8 +++++--- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/catroid/src/main/java/org/catrobat/catroid/ui/ProjectActivity.kt b/catroid/src/main/java/org/catrobat/catroid/ui/ProjectActivity.kt index 0aa2547e993..24c35e1ca04 100644 --- a/catroid/src/main/java/org/catrobat/catroid/ui/ProjectActivity.kt +++ b/catroid/src/main/java/org/catrobat/catroid/ui/ProjectActivity.kt @@ -270,7 +270,7 @@ class ProjectActivity : BaseCastActivity() { val placementRotation = extras.getFloat(VisualPlacementActivity.ROTATION_BUNDLE_ARGUMENT, 0.0f) val placeAtBrick = PlaceAtBrick(xCoordinate, yCoordinate) - val currentSprite = projectManager.currentSprite + val currentSprite = projectManager.currentSprite ?: return val startScript = StartScript() currentSprite.prependScript(startScript) startScript.addBrick(placeAtBrick) diff --git a/catroid/src/main/java/org/catrobat/catroid/visualplacement/BoundingBoxOverlay.kt b/catroid/src/main/java/org/catrobat/catroid/visualplacement/BoundingBoxOverlay.kt index 73b1acd35c8..aed3b0eb21b 100644 --- a/catroid/src/main/java/org/catrobat/catroid/visualplacement/BoundingBoxOverlay.kt +++ b/catroid/src/main/java/org/catrobat/catroid/visualplacement/BoundingBoxOverlay.kt @@ -25,7 +25,6 @@ package org.catrobat.catroid.visualplacement import android.content.Context import android.graphics.Canvas -import android.graphics.Color import android.graphics.Paint import android.graphics.RectF import android.view.View diff --git a/catroid/src/main/java/org/catrobat/catroid/visualplacement/ResizeRotateGestureDetector.kt b/catroid/src/main/java/org/catrobat/catroid/visualplacement/ResizeRotateGestureDetector.kt index 4c096561835..8cc45f4d7b7 100644 --- a/catroid/src/main/java/org/catrobat/catroid/visualplacement/ResizeRotateGestureDetector.kt +++ b/catroid/src/main/java/org/catrobat/catroid/visualplacement/ResizeRotateGestureDetector.kt @@ -25,7 +25,6 @@ package org.catrobat.catroid.visualplacement import android.view.MotionEvent import kotlin.math.atan2 -import kotlin.math.atan2 import kotlin.math.sqrt class ResizeRotateGestureDetector(private val listener: OnTransformGestureListener) { @@ -70,7 +69,7 @@ class ResizeRotateGestureDetector(private val listener: OnTransformGestureListen return true } - MotionEvent.ACTION_MOVE -> + MotionEvent.ACTION_MOVE -> { if (isTransforming && initialDistance > 0) { val scaleFactor = currentDistance / initialDistance val newScale = (cumulativeScale * scaleFactor) @@ -80,7 +79,10 @@ class ResizeRotateGestureDetector(private val listener: OnTransformGestureListen val angleDelta = currentAngle - initialAngle listener.onRotate(cumulativeRotation + angleDelta) true - } else false + } else { + false + } + } MotionEvent.ACTION_POINTER_UP -> { if (isTransforming && initialDistance > 0) { From 85cdf696d7dd46fde8cefb9443c45f432d9e695b Mon Sep 17 00:00:00 2001 From: harshsomankar123-tech Date: Tue, 7 Apr 2026 14:58:30 +0530 Subject: [PATCH 05/17] Fix: All SonarCloud reliability and detekt issues in Visual Placement (IDE-217) --- .../org/catrobat/catroid/ui/SpriteActivity.java | 5 +++-- .../ResizeRotateGestureDetector.kt | 3 +-- .../visualplacement/VisualPlacementActivity.java | 16 +++++++++++++--- .../VisualPlacementTouchListener.java | 3 +++ 4 files changed, 20 insertions(+), 7 deletions(-) diff --git a/catroid/src/main/java/org/catrobat/catroid/ui/SpriteActivity.java b/catroid/src/main/java/org/catrobat/catroid/ui/SpriteActivity.java index 23461af1b7a..7ac2d9d2540 100644 --- a/catroid/src/main/java/org/catrobat/catroid/ui/SpriteActivity.java +++ b/catroid/src/main/java/org/catrobat/catroid/ui/SpriteActivity.java @@ -351,8 +351,9 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) { } if (resultCode != RESULT_OK) { - if (SettingsFragment.isCastSharedPreferenceEnabled(this) - && projectManager.getCurrentProject().isCastProject() + Project project = projectManager.getCurrentProject(); + if (project != null && SettingsFragment.isCastSharedPreferenceEnabled(this) + && project.isCastProject() && !CastManager.getInstance().isConnected()) { CastManager.getInstance().openDeviceSelectorOrDisconnectDialog(this); diff --git a/catroid/src/main/java/org/catrobat/catroid/visualplacement/ResizeRotateGestureDetector.kt b/catroid/src/main/java/org/catrobat/catroid/visualplacement/ResizeRotateGestureDetector.kt index 8cc45f4d7b7..196afa73816 100644 --- a/catroid/src/main/java/org/catrobat/catroid/visualplacement/ResizeRotateGestureDetector.kt +++ b/catroid/src/main/java/org/catrobat/catroid/visualplacement/ResizeRotateGestureDetector.kt @@ -69,7 +69,7 @@ class ResizeRotateGestureDetector(private val listener: OnTransformGestureListen return true } - MotionEvent.ACTION_MOVE -> { + MotionEvent.ACTION_MOVE -> if (isTransforming && initialDistance > 0) { val scaleFactor = currentDistance / initialDistance val newScale = (cumulativeScale * scaleFactor) @@ -82,7 +82,6 @@ class ResizeRotateGestureDetector(private val listener: OnTransformGestureListen } else { false } - } MotionEvent.ACTION_POINTER_UP -> { if (isTransforming && initialDistance > 0) { diff --git a/catroid/src/main/java/org/catrobat/catroid/visualplacement/VisualPlacementActivity.java b/catroid/src/main/java/org/catrobat/catroid/visualplacement/VisualPlacementActivity.java index 1d65d8bf6b5..d9a024a5b31 100644 --- a/catroid/src/main/java/org/catrobat/catroid/visualplacement/VisualPlacementActivity.java +++ b/catroid/src/main/java/org/catrobat/catroid/visualplacement/VisualPlacementActivity.java @@ -167,6 +167,10 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { projectManager = ProjectManager.getInstance(); Project currentProject = projectManager.getCurrentProject(); + if (currentProject == null || projectManager.getCurrentSprite() == null) { + finish(); + return; + } setContentView(R.layout.visual_placement_layout); Bundle extras = getIntent().getExtras(); @@ -303,7 +307,7 @@ public void showMovableImageView() { rotationMode = currentSprite.look.getRotationMode(); rotation = currentSprite.look.getMotionDirectionInUserInterfaceDimensionUnit(); visualPlacementBitmap = BitmapFactory.decodeFile(objectLookPath, bitmapOptions); - } else if (currentSprite.getLookList().size() != 0) { + } else if (currentSprite.getLookList() != null && currentSprite.getLookList().size() != 0) { if (currentSprite.getLookList().get(0).getFile() != null) { objectLookPath = currentSprite.getLookList().get(0).getFile().getAbsolutePath(); visualPlacementBitmap = BitmapFactory.decodeFile(objectLookPath, bitmapOptions); @@ -318,12 +322,15 @@ public void showMovableImageView() { } else { visualPlacementBitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(visualPlacementBitmap); - drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); drawable.draw(canvas); } } } + if (visualPlacementBitmap == null) { + visualPlacementBitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888); + } + Matrix matrix = new Matrix(); switch (rotationMode) { case ROTATION_STYLE_NONE: @@ -406,12 +413,15 @@ private Bitmap convertTextToBitmap() { float baseline = -paint.ascent(); int bitmapWidth = (int) paint.measureText(text); - int canvasWidth = calculateAlignmentValuesForText(paint, bitmapWidth, textAlignment); int height = (int) (baseline + paint.descent()); + bitmapWidth = Math.max(1, bitmapWidth); + height = Math.max(1, height); + visualPlacementBitmap = Bitmap.createBitmap(bitmapWidth, height, Bitmap.Config.ARGB_8888); + int canvasWidth = calculateAlignmentValuesForText(paint, bitmapWidth, textAlignment); Canvas canvas = new Canvas(visualPlacementBitmap); canvas.drawText(text, canvasWidth, baseline, diff --git a/catroid/src/main/java/org/catrobat/catroid/visualplacement/VisualPlacementTouchListener.java b/catroid/src/main/java/org/catrobat/catroid/visualplacement/VisualPlacementTouchListener.java index 0ab3e1568c6..a66326d4831 100644 --- a/catroid/src/main/java/org/catrobat/catroid/visualplacement/VisualPlacementTouchListener.java +++ b/catroid/src/main/java/org/catrobat/catroid/visualplacement/VisualPlacementTouchListener.java @@ -67,6 +67,9 @@ public void setBoundingBoxOverlay(BoundingBoxOverlay overlay) { } public boolean onTouch(ImageView imageView, MotionEvent event, CoordinateInterface coordinateInterface) { + if (imageView == null || coordinateInterface == null) { + return false; + } if (resizeRotateDetector != null && event.getPointerCount() >= 2) { boolean handled = resizeRotateDetector.onTouchEvent(event); if (handled) { From 9d954a9ecfb70345a86b58eab2e07087a4b9a689 Mon Sep 17 00:00:00 2001 From: harshsomankar123-tech Date: Tue, 7 Apr 2026 15:38:02 +0530 Subject: [PATCH 06/17] Refactor: Final SonarCloud reliability and maintainability fixes (IDE-217) --- .../VisualPlacementActivity.java | 149 ++++++++++-------- .../VisualPlacementTouchListener.java | 139 +++++++++------- 2 files changed, 157 insertions(+), 131 deletions(-) diff --git a/catroid/src/main/java/org/catrobat/catroid/visualplacement/VisualPlacementActivity.java b/catroid/src/main/java/org/catrobat/catroid/visualplacement/VisualPlacementActivity.java index d9a024a5b31..9fdc9b7f617 100644 --- a/catroid/src/main/java/org/catrobat/catroid/visualplacement/VisualPlacementActivity.java +++ b/catroid/src/main/java/org/catrobat/catroid/visualplacement/VisualPlacementActivity.java @@ -116,8 +116,8 @@ public class VisualPlacementActivity extends BaseCastActivity implements View.On private float translateX; private float translateY; - private float currentScale = 1.0f; - private float currentRotation = 0.0f; + private float scaleFactor = 1.0f; + private float rotationDegrees = 0.0f; private float initialSpriteScale = 1.0f; private float initialSpriteRotation = 0.0f; @@ -206,20 +206,20 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { resizeRotateDetector = new ResizeRotateGestureDetector(new ResizeRotateGestureDetector.OnTransformGestureListener() { @Override - public void onScale(float scaleFactor) { - currentScale = scaleFactor; + public void onScale(float scale) { + scaleFactor = scale; if (imageView != null) { - float appliedScale = initialSpriteScale * currentScale; + float appliedScale = initialSpriteScale * scaleFactor; imageView.setScaleX(appliedScale); imageView.setScaleY(appliedScale); } } @Override - public void onRotate(float rotationDegrees) { - currentRotation = rotationDegrees; + public void onRotate(float rotation) { + rotationDegrees = rotation; if (imageView != null) { - imageView.setRotation(initialSpriteRotation + currentRotation); + imageView.setRotation(initialSpriteRotation + rotationDegrees); } } }); @@ -291,46 +291,72 @@ private void setBackground() { } public void showMovableImageView() { - Bitmap visualPlacementBitmap; - String objectLookPath; - Sprite currentSprite = projectManager.getCurrentSprite(); - imageView = new ImageView(this); + Bitmap visualPlacementBitmap = loadBitmapForPlacement(); + + if (visualPlacementBitmap != null) { + visualPlacementBitmap = applyInitialBitmapTransformations(visualPlacementBitmap); + imageView.setImageBitmap(visualPlacementBitmap); + } + + setupImageViewPositionAndScale(); + frameLayout.addView(imageView); + } + + private Bitmap loadBitmapForPlacement() { + Bitmap bitmap = null; + Sprite currentSprite = projectManager.getCurrentSprite(); if (isText) { - visualPlacementBitmap = convertTextToBitmap(); - } else { - if (currentSprite.look != null && !currentSprite.look.getImagePath().isEmpty()) { - objectLookPath = currentSprite.look.getImagePath(); - scaleX = currentSprite.look.getScaleX(); - scaleY = currentSprite.look.getScaleY(); - rotationMode = currentSprite.look.getRotationMode(); - rotation = currentSprite.look.getMotionDirectionInUserInterfaceDimensionUnit(); - visualPlacementBitmap = BitmapFactory.decodeFile(objectLookPath, bitmapOptions); - } else if (currentSprite.getLookList() != null && currentSprite.getLookList().size() != 0) { - if (currentSprite.getLookList().get(0).getFile() != null) { - objectLookPath = currentSprite.getLookList().get(0).getFile().getAbsolutePath(); - visualPlacementBitmap = BitmapFactory.decodeFile(objectLookPath, bitmapOptions); - } else { - visualPlacementBitmap = null; - } - } else { - Drawable drawable = ContextCompat.getDrawable(getApplicationContext(), R.drawable.pc_toolbar_icon); - - if (drawable == null) { - visualPlacementBitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888); - } else { - visualPlacementBitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888); - Canvas canvas = new Canvas(visualPlacementBitmap); - drawable.draw(canvas); - } + bitmap = convertTextToBitmap(); + } else if (currentSprite.look != null && !currentSprite.look.getImagePath().isEmpty()) { + String objectLookPath = currentSprite.look.getImagePath(); + scaleX = currentSprite.look.getScaleX(); + scaleY = currentSprite.look.getScaleY(); + rotationMode = currentSprite.look.getRotationMode(); + rotation = currentSprite.look.getMotionDirectionInUserInterfaceDimensionUnit(); + bitmap = BitmapFactory.decodeFile(objectLookPath, bitmapOptions); + } else if (currentSprite.getLookList() != null && !currentSprite.getLookList().isEmpty()) { + if (currentSprite.getLookList().get(0).getFile() != null) { + String path = currentSprite.getLookList().get(0).getFile().getAbsolutePath(); + bitmap = BitmapFactory.decodeFile(path, bitmapOptions); } } - if (visualPlacementBitmap == null) { - visualPlacementBitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888); + if (bitmap == null) { + bitmap = loadDefaultToolbarIcon(); + } + return bitmap; + } + + private Bitmap loadDefaultToolbarIcon() { + Drawable drawable = ContextCompat.getDrawable(getApplicationContext(), R.drawable.pc_toolbar_icon); + if (drawable == null) { + return Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888); + } + Bitmap bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bitmap); + drawable.draw(canvas); + return bitmap; + } + + private Bitmap applyInitialBitmapTransformations(Bitmap bitmap) { + Matrix matrix = createInitialRotationMatrix(bitmap); + Bitmap rotatedBitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true); + if (bitmap != rotatedBitmap) { + bitmap.recycle(); + } + bitmap = rotatedBitmap; + + Bitmap scaledBitmap = Bitmap.createScaledBitmap(bitmap, (int) (bitmap.getWidth() * layoutWidthRatio), + (int) (bitmap.getHeight() * layoutHeightRatio), true); + if (bitmap != scaledBitmap) { + bitmap.recycle(); } + return scaledBitmap; + } + private Matrix createInitialRotationMatrix(Bitmap bitmap) { Matrix matrix = new Matrix(); switch (rotationMode) { case ROTATION_STYLE_NONE: @@ -343,30 +369,15 @@ public void showMovableImageView() { break; case ROTATION_STYLE_LEFT_RIGHT_ONLY: if (rotation < 0) { - matrix.postScale(-1, 1, (float) visualPlacementBitmap.getWidth() / 2, (float) visualPlacementBitmap.getHeight() / 2); + matrix.postScale(-1, 1, (float) bitmap.getWidth() / 2, (float) bitmap.getHeight() / 2); } break; } + return matrix; + } - if (visualPlacementBitmap != null) { - Bitmap rotatedBitmap = Bitmap.createBitmap(visualPlacementBitmap, 0, 0, - visualPlacementBitmap.getWidth(), - visualPlacementBitmap.getHeight(), matrix, true); - - if (visualPlacementBitmap != rotatedBitmap) { - visualPlacementBitmap.recycle(); - } - visualPlacementBitmap = rotatedBitmap; - - Bitmap scaledBitmap = Bitmap.createScaledBitmap(visualPlacementBitmap, (int) (visualPlacementBitmap.getWidth() * layoutWidthRatio), - (int) (visualPlacementBitmap.getHeight() * layoutHeightRatio), true); - imageView.setImageBitmap(scaledBitmap); - if (visualPlacementBitmap != scaledBitmap) { - visualPlacementBitmap.recycle(); - } - } + private void setupImageViewPositionAndScale() { imageView.setScaleType(ImageView.ScaleType.CENTER); - if (isText) { imageView.setTranslationX(translateX + xOffsetText); imageView.setTranslationY(-translateY + yOffsetText); @@ -381,17 +392,15 @@ public void showMovableImageView() { imageView.setScaleX(scaleX); initialSpriteScale = scaleX; } - if (scaleY > 0.01) { imageView.setScaleY(scaleY); } + initialSpriteRotation = rotation; resizeRotateDetector.setCumulativeScale(1.0f); resizeRotateDetector.setCumulativeRotation(0.0f); - currentScale = 1.0f; - currentRotation = 0.0f; - - frameLayout.addView(imageView); + scaleFactor = 1.0f; + rotationDegrees = 0.0f; } private Bitmap convertTextToBitmap() { @@ -457,7 +466,7 @@ public void onBackPressed() { int yCoordinate = Math.round(yCoord / layoutHeightRatio); boolean hasChanges = translateX != xCoordinate || translateY != yCoordinate - || currentScale != 1.0f || currentRotation != 0.0f; + || scaleFactor != 1.0f || rotationDegrees != 0.0f; if (hasChanges) { showSaveChangesDialog(this); @@ -489,13 +498,13 @@ private void finishWithResult() { extras.putInt(X_COORDINATE_BUNDLE_ARGUMENT, xCoordinate); extras.putInt(Y_COORDINATE_BUNDLE_ARGUMENT, yCoordinate); - float finalScale = initialSpriteScale * currentScale; - float finalRotation = initialSpriteRotation + currentRotation; + float finalScale = initialSpriteScale * scaleFactor; + float finalRotation = initialSpriteRotation + rotationDegrees; extras.putFloat(SCALE_BUNDLE_ARGUMENT, finalScale); extras.putFloat(ROTATION_BUNDLE_ARGUMENT, finalRotation); boolean hasChanges = translateX != xCoordinate || translateY != yCoordinate - || currentScale != 1.0f || currentRotation != 0.0f; + || scaleFactor != 1.0f || rotationDegrees != 0.0f; extras.putBoolean(CHANGED_COORDINATES, hasChanges); returnIntent.putExtras(extras); @@ -533,11 +542,11 @@ public void setYCoordinate(float yCoordinate) { @Override public void setScale(float scaleFactor) { - currentScale = scaleFactor; + this.scaleFactor = scaleFactor; } @Override public void setRotation(float rotationDegrees) { - currentRotation = rotationDegrees; + this.rotationDegrees = rotationDegrees; } } diff --git a/catroid/src/main/java/org/catrobat/catroid/visualplacement/VisualPlacementTouchListener.java b/catroid/src/main/java/org/catrobat/catroid/visualplacement/VisualPlacementTouchListener.java index a66326d4831..c9ba7d59989 100644 --- a/catroid/src/main/java/org/catrobat/catroid/visualplacement/VisualPlacementTouchListener.java +++ b/catroid/src/main/java/org/catrobat/catroid/visualplacement/VisualPlacementTouchListener.java @@ -70,76 +70,93 @@ public boolean onTouch(ImageView imageView, MotionEvent event, CoordinateInterfa if (imageView == null || coordinateInterface == null) { return false; } - if (resizeRotateDetector != null && event.getPointerCount() >= 2) { + + if (handleMultiTouch(event)) { + return true; + } + + return handleSingleTouch(imageView, event, coordinateInterface); + } + + private boolean handleMultiTouch(MotionEvent event) { + if (resizeRotateDetector == null) { + return false; + } + if (event.getPointerCount() >= 2 || resizeRotateDetector.isTransforming()) { boolean handled = resizeRotateDetector.onTouchEvent(event); - if (handled) { + if (handled || resizeRotateDetector.isTransforming()) { updateBoundingBox(); return true; } } + return false; + } - if (resizeRotateDetector != null && resizeRotateDetector.isTransforming()) { - resizeRotateDetector.onTouchEvent(event); - updateBoundingBox(); - return true; + private boolean handleSingleTouch(ImageView imageView, MotionEvent event, CoordinateInterface coordinateInterface) { + if (event.getPointerCount() <= 0 || event.getPointerId(0) != 0) { + return false; } - if (event.getPointerCount() > 0 && event.getPointerId(0) == 0) { - float currentX = event.getRawX(); - float currentY = event.getRawY(); - long motionEventTime = event.getEventTime(); - - switch (event.getAction()) { - case MotionEvent.ACTION_DOWN: - setMode(Mode.TAP); - previousX = currentX; - previousY = currentY; - recentTouchEventsData.add(new TouchEventData(motionEventTime, currentX, currentY)); - break; - case MotionEvent.ACTION_CANCEL: - case MotionEvent.ACTION_UP: - if (mode == Mode.TAP) { - imageView.setX(event.getX() - (float) imageView.getWidth() / 2); - imageView.setY(event.getY() - (float) imageView.getHeight() / 2); - } else { - removeObsoleteTouchEventsData(motionEventTime); - float dX = currentX - previousX; - float dY = currentY - previousY; - if (!recentTouchEventsData.isEmpty() && recentTouchEventsData.size() > 1) { - TouchEventData oldestEntry = recentTouchEventsData.get(0); - float distanceCorrectionX = currentX - oldestEntry.xCoordinate; - float distanceCorrectionY = currentY - oldestEntry.yCoordinate; - double distance = distanceCorrectionX * distanceCorrectionX + distanceCorrectionY * distanceCorrectionY; - if (distance < JITTER_DISTANCE_THRESHOLD && distance != 0) { - dX -= distanceCorrectionX; - dY -= distanceCorrectionY; - } - } - imageView.setX(imageView.getX() + dX); - imageView.setY(imageView.getY() + dY); - } - break; - case MotionEvent.ACTION_MOVE: - long delayTime = motionEventTime - event.getDownTime(); - if (delayTime > TAP_DELAY_THRESHOLD) { - setMode(Mode.MOVE); - float dX = currentX - previousX; - float dY = currentY - previousY; - imageView.setX(imageView.getX() + dX); - imageView.setY(imageView.getY() + dY); - recentTouchEventsData.add(new TouchEventData(motionEventTime, currentX, currentY)); - previousX = currentX; - previousY = currentY; - removeObsoleteTouchEventsData(motionEventTime); - } - break; - } - coordinateInterface.setXCoordinate(imageView.getX()); - coordinateInterface.setYCoordinate(-imageView.getY()); - updateBoundingBox(); - return true; + float currentX = event.getRawX(); + float currentY = event.getRawY(); + long motionEventTime = event.getEventTime(); + + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + setMode(Mode.TAP); + previousX = currentX; + previousY = currentY; + recentTouchEventsData.add(new TouchEventData(motionEventTime, currentX, currentY)); + break; + case MotionEvent.ACTION_CANCEL: + case MotionEvent.ACTION_UP: + handleTouchUp(imageView, event, currentX, currentY, motionEventTime); + break; + case MotionEvent.ACTION_MOVE: + handleTouchMove(imageView, event, currentX, currentY, motionEventTime); + break; + } + coordinateInterface.setXCoordinate(imageView.getX()); + coordinateInterface.setYCoordinate(-imageView.getY()); + updateBoundingBox(); + return true; + } + + private void handleTouchUp(ImageView imageView, MotionEvent event, float currentX, float currentY, long motionEventTime) { + if (mode == Mode.TAP) { + imageView.setX(event.getX() - (float) imageView.getWidth() / 2); + imageView.setY(event.getY() - (float) imageView.getHeight() / 2); } else { - return false; + removeObsoleteTouchEventsData(motionEventTime); + float dX = currentX - previousX; + float dY = currentY - previousY; + if (!recentTouchEventsData.isEmpty() && recentTouchEventsData.size() > 1) { + TouchEventData oldestEntry = recentTouchEventsData.get(0); + float distanceCorrectionX = currentX - oldestEntry.xCoordinate; + float distanceCorrectionY = currentY - oldestEntry.yCoordinate; + double distance = distanceCorrectionX * distanceCorrectionX + distanceCorrectionY * distanceCorrectionY; + if (distance < JITTER_DISTANCE_THRESHOLD && distance != 0) { + dX -= distanceCorrectionX; + dY -= distanceCorrectionY; + } + } + imageView.setX(imageView.getX() + dX); + imageView.setY(imageView.getY() + dY); + } + } + + private void handleTouchMove(ImageView imageView, MotionEvent event, float currentX, float currentY, long motionEventTime) { + long delayTime = motionEventTime - event.getDownTime(); + if (delayTime > TAP_DELAY_THRESHOLD) { + setMode(Mode.MOVE); + float dX = currentX - previousX; + float dY = currentY - previousY; + imageView.setX(imageView.getX() + dX); + imageView.setY(imageView.getY() + dY); + recentTouchEventsData.add(new TouchEventData(motionEventTime, currentX, currentY)); + previousX = currentX; + previousY = currentY; + removeObsoleteTouchEventsData(motionEventTime); } } From e881f71106cc56fd7f33f6119b0d6b98f9e1d92d Mon Sep 17 00:00:00 2001 From: harshsomankar123-tech Date: Tue, 7 Apr 2026 15:46:00 +0530 Subject: [PATCH 07/17] Fix: Rename rotation field in VisualPlacementActivity to resolve SonarCloud Reliability bug (IDE-217) --- .../VisualPlacementActivity.java | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/catroid/src/main/java/org/catrobat/catroid/visualplacement/VisualPlacementActivity.java b/catroid/src/main/java/org/catrobat/catroid/visualplacement/VisualPlacementActivity.java index 9fdc9b7f617..3108d2bdd82 100644 --- a/catroid/src/main/java/org/catrobat/catroid/visualplacement/VisualPlacementActivity.java +++ b/catroid/src/main/java/org/catrobat/catroid/visualplacement/VisualPlacementActivity.java @@ -111,13 +111,13 @@ public class VisualPlacementActivity extends BaseCastActivity implements View.On private float yCoord; private float scaleX; private float scaleY; - private float rotation; + private float initialLookRotation; private int rotationMode; private float translateX; private float translateY; private float scaleFactor = 1.0f; - private float rotationDegrees = 0.0f; + private float rotation = 0.0f; private float initialSpriteScale = 1.0f; private float initialSpriteRotation = 0.0f; @@ -217,9 +217,9 @@ public void onScale(float scale) { @Override public void onRotate(float rotation) { - rotationDegrees = rotation; + VisualPlacementActivity.this.rotation = rotation; if (imageView != null) { - imageView.setRotation(initialSpriteRotation + rotationDegrees); + imageView.setRotation(initialSpriteRotation + VisualPlacementActivity.this.rotation); } } }); @@ -293,12 +293,12 @@ private void setBackground() { public void showMovableImageView() { imageView = new ImageView(this); Bitmap visualPlacementBitmap = loadBitmapForPlacement(); - + if (visualPlacementBitmap != null) { visualPlacementBitmap = applyInitialBitmapTransformations(visualPlacementBitmap); imageView.setImageBitmap(visualPlacementBitmap); } - + setupImageViewPositionAndScale(); frameLayout.addView(imageView); } @@ -314,7 +314,7 @@ private Bitmap loadBitmapForPlacement() { scaleX = currentSprite.look.getScaleX(); scaleY = currentSprite.look.getScaleY(); rotationMode = currentSprite.look.getRotationMode(); - rotation = currentSprite.look.getMotionDirectionInUserInterfaceDimensionUnit(); + initialLookRotation = currentSprite.look.getMotionDirectionInUserInterfaceDimensionUnit(); bitmap = BitmapFactory.decodeFile(objectLookPath, bitmapOptions); } else if (currentSprite.getLookList() != null && !currentSprite.getLookList().isEmpty()) { if (currentSprite.getLookList().get(0).getFile() != null) { @@ -363,12 +363,12 @@ private Matrix createInitialRotationMatrix(Bitmap bitmap) { matrix.postRotate(0); break; case ROTATION_STYLE_ALL_AROUND: - if (rotation != 90) { - matrix.postRotate(rotation - DEGREE_UI_OFFSET); + if (initialLookRotation != 90) { + matrix.postRotate(initialLookRotation - DEGREE_UI_OFFSET); } break; case ROTATION_STYLE_LEFT_RIGHT_ONLY: - if (rotation < 0) { + if (initialLookRotation < 0) { matrix.postScale(-1, 1, (float) bitmap.getWidth() / 2, (float) bitmap.getHeight() / 2); } break; @@ -395,12 +395,12 @@ private void setupImageViewPositionAndScale() { if (scaleY > 0.01) { imageView.setScaleY(scaleY); } - initialSpriteRotation = rotation; + initialSpriteRotation = initialLookRotation; resizeRotateDetector.setCumulativeScale(1.0f); resizeRotateDetector.setCumulativeRotation(0.0f); scaleFactor = 1.0f; - rotationDegrees = 0.0f; + rotation = 0.0f; } private Bitmap convertTextToBitmap() { @@ -466,7 +466,7 @@ public void onBackPressed() { int yCoordinate = Math.round(yCoord / layoutHeightRatio); boolean hasChanges = translateX != xCoordinate || translateY != yCoordinate - || scaleFactor != 1.0f || rotationDegrees != 0.0f; + || scaleFactor != 1.0f || rotation != 0.0f; if (hasChanges) { showSaveChangesDialog(this); @@ -499,12 +499,12 @@ private void finishWithResult() { extras.putInt(Y_COORDINATE_BUNDLE_ARGUMENT, yCoordinate); float finalScale = initialSpriteScale * scaleFactor; - float finalRotation = initialSpriteRotation + rotationDegrees; + float finalRotation = initialSpriteRotation + rotation; extras.putFloat(SCALE_BUNDLE_ARGUMENT, finalScale); extras.putFloat(ROTATION_BUNDLE_ARGUMENT, finalRotation); boolean hasChanges = translateX != xCoordinate || translateY != yCoordinate - || scaleFactor != 1.0f || rotationDegrees != 0.0f; + || scaleFactor != 1.0f || rotation != 0.0f; extras.putBoolean(CHANGED_COORDINATES, hasChanges); returnIntent.putExtras(extras); @@ -547,6 +547,6 @@ public void setScale(float scaleFactor) { @Override public void setRotation(float rotationDegrees) { - this.rotationDegrees = rotationDegrees; + this.rotation = rotationDegrees; } } From 4fdde17342b1565414bda6a9881cc9d7dd84f8ed Mon Sep 17 00:00:00 2001 From: harshsomankar123-tech Date: Sat, 11 Apr 2026 04:18:16 +0530 Subject: [PATCH 08/17] Fix bugs in ProjectActivity.kt, SpriteActivity.java, and VisualPlacementActivity.java --- .../main/java/org/catrobat/catroid/ui/ProjectActivity.kt | 6 +++--- .../main/java/org/catrobat/catroid/ui/SpriteActivity.java | 8 +++----- .../catroid/visualplacement/VisualPlacementActivity.java | 7 +++++-- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/catroid/src/main/java/org/catrobat/catroid/ui/ProjectActivity.kt b/catroid/src/main/java/org/catrobat/catroid/ui/ProjectActivity.kt index 24c35e1ca04..37502513762 100644 --- a/catroid/src/main/java/org/catrobat/catroid/ui/ProjectActivity.kt +++ b/catroid/src/main/java/org/catrobat/catroid/ui/ProjectActivity.kt @@ -95,7 +95,6 @@ class ProjectActivity : BaseCastActivity() { const val DEFAULT_SCALE = 1.0f const val PERCENTAGE_MULTIPLIER = 100.0 const val DEFAULT_ROTATION = 0.0f - const val ROTATION_OFFSET = 90.0 } private lateinit var binding: ActivityRecyclerBinding @@ -275,11 +274,12 @@ class ProjectActivity : BaseCastActivity() { currentSprite.prependScript(startScript) startScript.addBrick(placeAtBrick) if (placementScale != DEFAULT_SCALE) { - val sizePercent = (placementScale * PERCENTAGE_MULTIPLIER).toDouble() + val sizePercent = placementScale.toDouble() * + PERCENTAGE_MULTIPLIER startScript.addBrick(SetSizeToBrick(sizePercent)) } if (placementRotation != DEFAULT_ROTATION) { - val direction = (placementRotation + ROTATION_OFFSET).toDouble() + val direction = placementRotation.toDouble() startScript.addBrick(PointInDirectionBrick(direction)) } } diff --git a/catroid/src/main/java/org/catrobat/catroid/ui/SpriteActivity.java b/catroid/src/main/java/org/catrobat/catroid/ui/SpriteActivity.java index 7ac2d9d2540..58e6d764b0b 100644 --- a/catroid/src/main/java/org/catrobat/catroid/ui/SpriteActivity.java +++ b/catroid/src/main/java/org/catrobat/catroid/ui/SpriteActivity.java @@ -461,11 +461,9 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) { } } - if (placementRotation != 0.0f) { - Sprite sprite = projectManager.getCurrentSprite(); - if (sprite != null && sprite.look != null) { - sprite.look.setMotionDirectionInUserInterfaceDimensionUnit(placementRotation + 90); - } + Sprite rotSprite = projectManager.getCurrentSprite(); + if (rotSprite != null && rotSprite.look != null) { + rotSprite.look.setMotionDirectionInUserInterfaceDimensionUnit(placementRotation); } setUndoMenuItemVisibility(extras.getBoolean(CHANGED_COORDINATES)); diff --git a/catroid/src/main/java/org/catrobat/catroid/visualplacement/VisualPlacementActivity.java b/catroid/src/main/java/org/catrobat/catroid/visualplacement/VisualPlacementActivity.java index 3108d2bdd82..17fabc167db 100644 --- a/catroid/src/main/java/org/catrobat/catroid/visualplacement/VisualPlacementActivity.java +++ b/catroid/src/main/java/org/catrobat/catroid/visualplacement/VisualPlacementActivity.java @@ -219,7 +219,7 @@ public void onScale(float scale) { public void onRotate(float rotation) { VisualPlacementActivity.this.rotation = rotation; if (imageView != null) { - imageView.setRotation(initialSpriteRotation + VisualPlacementActivity.this.rotation); + imageView.setRotation(VisualPlacementActivity.this.rotation); } } }); @@ -334,8 +334,11 @@ private Bitmap loadDefaultToolbarIcon() { if (drawable == null) { return Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888); } - Bitmap bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888); + int width = Math.max(1, drawable.getIntrinsicWidth()); + int height = Math.max(1, drawable.getIntrinsicHeight()); + Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(bitmap); + drawable.setBounds(0, 0, width, height); drawable.draw(canvas); return bitmap; } From abfe6d36da0c32d7f6323a6375769128b860eb21 Mon Sep 17 00:00:00 2001 From: harshsomankar123-tech Date: Sun, 12 Apr 2026 15:34:11 +0530 Subject: [PATCH 09/17] Address PR #5189 review comments: fix angle normalization, touch baseline reset, and direction comparison; update copyrights and merge nested ifs --- .../catrobat/catroid/ui/ProjectActivity.kt | 3 ++- .../catrobat/catroid/ui/SpriteActivity.java | 2 +- .../ResizeRotateGestureDetector.kt | 19 +++++++++++++++++-- .../VisualPlacementActivity.java | 9 ++++----- .../VisualPlacementTouchListener.java | 3 +++ ...VisualPlacementPositionCorrectionTest.java | 2 +- .../VisualPlacementTouchListenerTest.java | 2 +- 7 files changed, 29 insertions(+), 11 deletions(-) diff --git a/catroid/src/main/java/org/catrobat/catroid/ui/ProjectActivity.kt b/catroid/src/main/java/org/catrobat/catroid/ui/ProjectActivity.kt index 37502513762..fb97f19784b 100644 --- a/catroid/src/main/java/org/catrobat/catroid/ui/ProjectActivity.kt +++ b/catroid/src/main/java/org/catrobat/catroid/ui/ProjectActivity.kt @@ -74,6 +74,7 @@ import org.catrobat.catroid.utils.ToastUtil import org.catrobat.catroid.utils.Utils import org.catrobat.catroid.utils.setVisibleOrGone import org.catrobat.catroid.visualplacement.VisualPlacementActivity +import org.catrobat.catroid.content.Look.DEGREE_UI_OFFSET import org.koin.android.ext.android.inject import java.io.File @@ -278,7 +279,7 @@ class ProjectActivity : BaseCastActivity() { PERCENTAGE_MULTIPLIER startScript.addBrick(SetSizeToBrick(sizePercent)) } - if (placementRotation != DEFAULT_ROTATION) { + if (placementRotation != DEGREE_UI_OFFSET) { val direction = placementRotation.toDouble() startScript.addBrick(PointInDirectionBrick(direction)) } diff --git a/catroid/src/main/java/org/catrobat/catroid/ui/SpriteActivity.java b/catroid/src/main/java/org/catrobat/catroid/ui/SpriteActivity.java index 58e6d764b0b..eb3d2bb617f 100644 --- a/catroid/src/main/java/org/catrobat/catroid/ui/SpriteActivity.java +++ b/catroid/src/main/java/org/catrobat/catroid/ui/SpriteActivity.java @@ -1,6 +1,6 @@ /* * Catroid: An on-device visual programming system for Android devices - * Copyright (C) 2010-2025 The Catrobat Team + * Copyright (C) 2010-2026 The Catrobat Team * () * * This program is free software: you can redistribute it and/or modify diff --git a/catroid/src/main/java/org/catrobat/catroid/visualplacement/ResizeRotateGestureDetector.kt b/catroid/src/main/java/org/catrobat/catroid/visualplacement/ResizeRotateGestureDetector.kt index 196afa73816..171c1d2879f 100644 --- a/catroid/src/main/java/org/catrobat/catroid/visualplacement/ResizeRotateGestureDetector.kt +++ b/catroid/src/main/java/org/catrobat/catroid/visualplacement/ResizeRotateGestureDetector.kt @@ -26,6 +26,7 @@ package org.catrobat.catroid.visualplacement import android.view.MotionEvent import kotlin.math.atan2 import kotlin.math.sqrt +import kotlin.math.abs class ResizeRotateGestureDetector(private val listener: OnTransformGestureListener) { @@ -76,7 +77,7 @@ class ResizeRotateGestureDetector(private val listener: OnTransformGestureListen .coerceIn(MIN_SCALE, MAX_SCALE) listener.onScale(newScale) - val angleDelta = currentAngle - initialAngle + val angleDelta = normalizeAngle(currentAngle - initialAngle) listener.onRotate(cumulativeRotation + angleDelta) true } else { @@ -89,7 +90,7 @@ class ResizeRotateGestureDetector(private val listener: OnTransformGestureListen cumulativeScale = (cumulativeScale * finalScaleFactor) .coerceIn(MIN_SCALE, MAX_SCALE) - val finalAngleDelta = currentAngle - initialAngle + val finalAngleDelta = normalizeAngle(currentAngle - initialAngle) cumulativeRotation += finalAngleDelta } isTransforming = false @@ -102,6 +103,20 @@ class ResizeRotateGestureDetector(private val listener: OnTransformGestureListen companion object { private const val MIN_SCALE = 0.1f private const val MAX_SCALE = 5.0f + private const val HALF_CIRCLE = 180f + private const val FULL_CIRCLE = 360f + + /** + * Wraps an angle delta into the [-180, 180] range so that + * crossing the atan2 ±180° boundary never produces a ~360° jump. + */ + @JvmStatic + fun normalizeAngle(angle: Float): Float { + var a = angle % FULL_CIRCLE + if (a > HALF_CIRCLE) a -= FULL_CIRCLE + if (a < -HALF_CIRCLE) a += FULL_CIRCLE + return a + } @JvmStatic fun calculateDistance(x0: Float, y0: Float, x1: Float, y1: Float): Float { diff --git a/catroid/src/main/java/org/catrobat/catroid/visualplacement/VisualPlacementActivity.java b/catroid/src/main/java/org/catrobat/catroid/visualplacement/VisualPlacementActivity.java index 17fabc167db..d617b89755f 100644 --- a/catroid/src/main/java/org/catrobat/catroid/visualplacement/VisualPlacementActivity.java +++ b/catroid/src/main/java/org/catrobat/catroid/visualplacement/VisualPlacementActivity.java @@ -316,11 +316,10 @@ private Bitmap loadBitmapForPlacement() { rotationMode = currentSprite.look.getRotationMode(); initialLookRotation = currentSprite.look.getMotionDirectionInUserInterfaceDimensionUnit(); bitmap = BitmapFactory.decodeFile(objectLookPath, bitmapOptions); - } else if (currentSprite.getLookList() != null && !currentSprite.getLookList().isEmpty()) { - if (currentSprite.getLookList().get(0).getFile() != null) { - String path = currentSprite.getLookList().get(0).getFile().getAbsolutePath(); - bitmap = BitmapFactory.decodeFile(path, bitmapOptions); - } + } else if (currentSprite.getLookList() != null && !currentSprite.getLookList().isEmpty() + && currentSprite.getLookList().get(0).getFile() != null) { + String path = currentSprite.getLookList().get(0).getFile().getAbsolutePath(); + bitmap = BitmapFactory.decodeFile(path, bitmapOptions); } if (bitmap == null) { diff --git a/catroid/src/main/java/org/catrobat/catroid/visualplacement/VisualPlacementTouchListener.java b/catroid/src/main/java/org/catrobat/catroid/visualplacement/VisualPlacementTouchListener.java index c9ba7d59989..fc0805c2d70 100644 --- a/catroid/src/main/java/org/catrobat/catroid/visualplacement/VisualPlacementTouchListener.java +++ b/catroid/src/main/java/org/catrobat/catroid/visualplacement/VisualPlacementTouchListener.java @@ -88,6 +88,9 @@ private boolean handleMultiTouch(MotionEvent event) { updateBoundingBox(); return true; } + previousX = event.getRawX(); + previousY = event.getRawY(); + setMode(Mode.MOVE); } return false; } diff --git a/catroid/src/test/java/org/catrobat/catroid/test/visualplacement/VisualPlacementPositionCorrectionTest.java b/catroid/src/test/java/org/catrobat/catroid/test/visualplacement/VisualPlacementPositionCorrectionTest.java index b35e3a38c35..0b8ca63d3e8 100644 --- a/catroid/src/test/java/org/catrobat/catroid/test/visualplacement/VisualPlacementPositionCorrectionTest.java +++ b/catroid/src/test/java/org/catrobat/catroid/test/visualplacement/VisualPlacementPositionCorrectionTest.java @@ -1,6 +1,6 @@ /* * Catroid: An on-device visual programming system for Android devices - * Copyright (C) 2010-2025 The Catrobat Team + * Copyright (C) 2010-2026 The Catrobat Team * () * * This program is free software: you can redistribute it and/or modify diff --git a/catroid/src/test/java/org/catrobat/catroid/test/visualplacement/VisualPlacementTouchListenerTest.java b/catroid/src/test/java/org/catrobat/catroid/test/visualplacement/VisualPlacementTouchListenerTest.java index b89a061b0e8..29a3c2e26ec 100644 --- a/catroid/src/test/java/org/catrobat/catroid/test/visualplacement/VisualPlacementTouchListenerTest.java +++ b/catroid/src/test/java/org/catrobat/catroid/test/visualplacement/VisualPlacementTouchListenerTest.java @@ -1,6 +1,6 @@ /* * Catroid: An on-device visual programming system for Android devices - * Copyright (C) 2010-2025 The Catrobat Team + * Copyright (C) 2010-2026 The Catrobat Team * () * * This program is free software: you can redistribute it and/or modify From 32292e8d32d4fe1108e31f711c8e6d6d310db2d9 Mon Sep 17 00:00:00 2001 From: harshsomankar123-tech Date: Sun, 12 Apr 2026 15:43:20 +0530 Subject: [PATCH 10/17] Fix detekt bot warning and SonarQube code smell --- .../catrobat/catroid/visualplacement/BoundingBoxOverlay.kt | 5 ++++- .../catroid/visualplacement/ResizeRotateGestureDetector.kt | 1 - 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/catroid/src/main/java/org/catrobat/catroid/visualplacement/BoundingBoxOverlay.kt b/catroid/src/main/java/org/catrobat/catroid/visualplacement/BoundingBoxOverlay.kt index aed3b0eb21b..0592fbd067b 100644 --- a/catroid/src/main/java/org/catrobat/catroid/visualplacement/BoundingBoxOverlay.kt +++ b/catroid/src/main/java/org/catrobat/catroid/visualplacement/BoundingBoxOverlay.kt @@ -79,7 +79,10 @@ class BoundingBoxOverlay(context: Context) : View(context) { val right = viewCenterX + scaledWidth / 2f val bottom = viewCenterY + scaledHeight / 2f - rect.set(left, top, right, bottom) + rect.left = left + rect.top = top + rect.right = right + rect.bottom = bottom canvas.withRotation(imageView.rotation, viewCenterX, viewCenterY) { drawRect(rect, boundingBoxPaint) diff --git a/catroid/src/main/java/org/catrobat/catroid/visualplacement/ResizeRotateGestureDetector.kt b/catroid/src/main/java/org/catrobat/catroid/visualplacement/ResizeRotateGestureDetector.kt index 171c1d2879f..935a7435a74 100644 --- a/catroid/src/main/java/org/catrobat/catroid/visualplacement/ResizeRotateGestureDetector.kt +++ b/catroid/src/main/java/org/catrobat/catroid/visualplacement/ResizeRotateGestureDetector.kt @@ -26,7 +26,6 @@ package org.catrobat.catroid.visualplacement import android.view.MotionEvent import kotlin.math.atan2 import kotlin.math.sqrt -import kotlin.math.abs class ResizeRotateGestureDetector(private val listener: OnTransformGestureListener) { From bf8eeeef5d7337b9613a5138d3fcd5c3e6f86de7 Mon Sep 17 00:00:00 2001 From: harshsomankar123-tech Date: Sun, 12 Apr 2026 22:53:22 +0530 Subject: [PATCH 11/17] Fix touch interaction jumps and cleanup ProjectActivity constants - Resolved P2 issue in VisualPlacementTouchListener where pointer-up events caused stale touch baselines. - Removed unused DEFAULT_ROTATION in ProjectActivity and fixed default rotation extra read. - Added regression tests for angle normalization and multi-touch transition resets. --- .../catrobat/catroid/ui/ProjectActivity.kt | 3 +- .../VisualPlacementTouchListener.java | 15 ++++++-- .../ResizeRotateGestureDetectorTest.kt | 11 ++++++ .../VisualPlacementResizeRotateTest.kt | 37 +++++++++++++++++++ 4 files changed, 60 insertions(+), 6 deletions(-) diff --git a/catroid/src/main/java/org/catrobat/catroid/ui/ProjectActivity.kt b/catroid/src/main/java/org/catrobat/catroid/ui/ProjectActivity.kt index fb97f19784b..2607967b500 100644 --- a/catroid/src/main/java/org/catrobat/catroid/ui/ProjectActivity.kt +++ b/catroid/src/main/java/org/catrobat/catroid/ui/ProjectActivity.kt @@ -95,7 +95,6 @@ class ProjectActivity : BaseCastActivity() { const val DEFAULT_SCALE = 1.0f const val PERCENTAGE_MULTIPLIER = 100.0 - const val DEFAULT_ROTATION = 0.0f } private lateinit var binding: ActivityRecyclerBinding @@ -268,7 +267,7 @@ class ProjectActivity : BaseCastActivity() { val placementScale = extras.getFloat(VisualPlacementActivity.SCALE_BUNDLE_ARGUMENT, 1.0f) val placementRotation = - extras.getFloat(VisualPlacementActivity.ROTATION_BUNDLE_ARGUMENT, 0.0f) + extras.getFloat(VisualPlacementActivity.ROTATION_BUNDLE_ARGUMENT, DEGREE_UI_OFFSET) val placeAtBrick = PlaceAtBrick(xCoordinate, yCoordinate) val currentSprite = projectManager.currentSprite ?: return val startScript = StartScript() diff --git a/catroid/src/main/java/org/catrobat/catroid/visualplacement/VisualPlacementTouchListener.java b/catroid/src/main/java/org/catrobat/catroid/visualplacement/VisualPlacementTouchListener.java index fc0805c2d70..9bbaed13f94 100644 --- a/catroid/src/main/java/org/catrobat/catroid/visualplacement/VisualPlacementTouchListener.java +++ b/catroid/src/main/java/org/catrobat/catroid/visualplacement/VisualPlacementTouchListener.java @@ -83,14 +83,21 @@ private boolean handleMultiTouch(MotionEvent event) { return false; } if (event.getPointerCount() >= 2 || resizeRotateDetector.isTransforming()) { + boolean wasTransforming = resizeRotateDetector.isTransforming(); boolean handled = resizeRotateDetector.onTouchEvent(event); - if (handled || resizeRotateDetector.isTransforming()) { + boolean isCurrentlyTransforming = resizeRotateDetector.isTransforming(); + + if (isCurrentlyTransforming) { + updateBoundingBox(); + return true; + } + if (wasTransforming && !isCurrentlyTransforming) { + previousX = event.getRawX(); + previousY = event.getRawY(); + setMode(Mode.MOVE); updateBoundingBox(); return true; } - previousX = event.getRawX(); - previousY = event.getRawY(); - setMode(Mode.MOVE); } return false; } diff --git a/catroid/src/test/java/org/catrobat/catroid/test/visualplacement/ResizeRotateGestureDetectorTest.kt b/catroid/src/test/java/org/catrobat/catroid/test/visualplacement/ResizeRotateGestureDetectorTest.kt index a44409f71b1..f71cda354af 100644 --- a/catroid/src/test/java/org/catrobat/catroid/test/visualplacement/ResizeRotateGestureDetectorTest.kt +++ b/catroid/src/test/java/org/catrobat/catroid/test/visualplacement/ResizeRotateGestureDetectorTest.kt @@ -121,6 +121,17 @@ class ResizeRotateGestureDetectorTest { assertFalse(detector.isTransforming) } + @Test + fun testNormalizeAngle() { + assertEquals(0.0f, ResizeRotateGestureDetector.normalizeAngle(0.0f), DELTA) + assertEquals(90.0f, ResizeRotateGestureDetector.normalizeAngle(90.0f), DELTA) + assertEquals(-90.0f, ResizeRotateGestureDetector.normalizeAngle(-90.0f), DELTA) + assertEquals(180.0f, ResizeRotateGestureDetector.normalizeAngle(180.0f), DELTA) + assertEquals(-170.0f, ResizeRotateGestureDetector.normalizeAngle(190.0f), DELTA) + assertEquals(170.0f, ResizeRotateGestureDetector.normalizeAngle(-190.0f), DELTA) + assertEquals(0.0f, ResizeRotateGestureDetector.normalizeAngle(360.0f), DELTA) + } + companion object { private const val DELTA = 0.01f } diff --git a/catroid/src/test/java/org/catrobat/catroid/test/visualplacement/VisualPlacementResizeRotateTest.kt b/catroid/src/test/java/org/catrobat/catroid/test/visualplacement/VisualPlacementResizeRotateTest.kt index da2d4e20415..12e86b36991 100644 --- a/catroid/src/test/java/org/catrobat/catroid/test/visualplacement/VisualPlacementResizeRotateTest.kt +++ b/catroid/src/test/java/org/catrobat/catroid/test/visualplacement/VisualPlacementResizeRotateTest.kt @@ -128,6 +128,43 @@ class VisualPlacementResizeRotateTest { assertTrue(abs(detector.cumulativeRotation) < DELTA) } + @Test + fun testTransitionFromMultiToSingleTouchResetsBaseline() { + // 1. Start multi-touch + `when`(motionEvent.pointerCount).thenReturn(2) + `when`(motionEvent.actionMasked).thenReturn(MotionEvent.ACTION_POINTER_DOWN) + `when`(motionEvent.getX(0)).thenReturn(0f) + `when`(motionEvent.getY(0)).thenReturn(0f) + `when`(motionEvent.getX(1)).thenReturn(100f) + `when`(motionEvent.getY(1)).thenReturn(100f) + listener.onTouch(imageView, motionEvent, coordinateInterface) + + // 2. End multi-touch (Pointer Up) + `when`(motionEvent.actionMasked).thenReturn(MotionEvent.ACTION_POINTER_UP) + `when`(motionEvent.rawX).thenReturn(50f) + `when`(motionEvent.rawY).thenReturn(60f) + listener.onTouch(imageView, motionEvent, coordinateInterface) + assertFalse(detector.isTransforming) + + // 3. Subsequent Single-Touch Move + `when`(motionEvent.pointerCount).thenReturn(1) + `when`(motionEvent.action).thenReturn(MotionEvent.ACTION_MOVE) + // Long enough delay to trigger MOVE mode + `when`(motionEvent.eventTime).thenReturn(1000L) + `when`(motionEvent.downTime).thenReturn(0L) + `when`(motionEvent.rawX).thenReturn(70f) // moved 20 from 50 + `when`(motionEvent.rawY).thenReturn(85f) // moved 25 from 60 + `when`(imageView.x).thenReturn(100f) + `when`(imageView.y).thenReturn(200f) + + listener.onTouch(imageView, motionEvent, coordinateInterface) + + // If baseline was reset correctly, dX = 70-50 = 20, dY = 85-60 = 25 + // New position should be original + delta + verify(imageView).setX(100f + 20f) + verify(imageView).setY(200f + 25f) + } + companion object { private const val DELTA = 0.001f } From d0ae7e56bb0585980b837bee59b1e5b9f8fd275b Mon Sep 17 00:00:00 2001 From: harshsomankar123-tech Date: Sun, 12 Apr 2026 22:59:00 +0530 Subject: [PATCH 12/17] Remove unused local variable in VisualPlacementTouchListener --- .../catroid/visualplacement/VisualPlacementTouchListener.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/catroid/src/main/java/org/catrobat/catroid/visualplacement/VisualPlacementTouchListener.java b/catroid/src/main/java/org/catrobat/catroid/visualplacement/VisualPlacementTouchListener.java index 9bbaed13f94..2729edde560 100644 --- a/catroid/src/main/java/org/catrobat/catroid/visualplacement/VisualPlacementTouchListener.java +++ b/catroid/src/main/java/org/catrobat/catroid/visualplacement/VisualPlacementTouchListener.java @@ -84,7 +84,7 @@ private boolean handleMultiTouch(MotionEvent event) { } if (event.getPointerCount() >= 2 || resizeRotateDetector.isTransforming()) { boolean wasTransforming = resizeRotateDetector.isTransforming(); - boolean handled = resizeRotateDetector.onTouchEvent(event); + resizeRotateDetector.onTouchEvent(event); boolean isCurrentlyTransforming = resizeRotateDetector.isTransforming(); if (isCurrentlyTransforming) { From 170bec1f7fc1498092c1d492025330f96748fea3 Mon Sep 17 00:00:00 2001 From: harshsomankar123-tech Date: Tue, 14 Apr 2026 15:20:02 +0530 Subject: [PATCH 13/17] Add regression unit tests for visual placement in ProjectActivity (IDE-217) --- .../catrobat/catroid/ui/ProjectActivity.kt | 42 ++-- .../ui/ProjectActivityVisualPlacementTest.kt | 188 ++++++++++++++++++ 2 files changed, 217 insertions(+), 13 deletions(-) create mode 100644 catroid/src/test/java/org/catrobat/catroid/test/ui/ProjectActivityVisualPlacementTest.kt diff --git a/catroid/src/main/java/org/catrobat/catroid/ui/ProjectActivity.kt b/catroid/src/main/java/org/catrobat/catroid/ui/ProjectActivity.kt index 2607967b500..db8e2c548a1 100644 --- a/catroid/src/main/java/org/catrobat/catroid/ui/ProjectActivity.kt +++ b/catroid/src/main/java/org/catrobat/catroid/ui/ProjectActivity.kt @@ -31,6 +31,7 @@ import android.os.Bundle import android.preference.PreferenceManager import android.view.Menu import android.view.MenuItem +import androidx.annotation.VisibleForTesting import androidx.appcompat.app.AlertDialog import androidx.fragment.app.DialogFragment import androidx.fragment.app.Fragment @@ -41,6 +42,7 @@ import org.catrobat.catroid.common.Constants.DEFAULT_IMAGE_EXTENSION import org.catrobat.catroid.common.Constants.TMP_IMAGE_FILE_NAME import org.catrobat.catroid.common.FlavoredConstants import org.catrobat.catroid.content.Project +import org.catrobat.catroid.content.Sprite import org.catrobat.catroid.content.StartScript import org.catrobat.catroid.content.bricks.Brick import org.catrobat.catroid.content.bricks.PlaceAtBrick @@ -95,6 +97,29 @@ class ProjectActivity : BaseCastActivity() { const val DEFAULT_SCALE = 1.0f const val PERCENTAGE_MULTIPLIER = 100.0 + + @JvmStatic + @VisibleForTesting + internal fun applyVisualPlacementBricks( + sprite: Sprite, + xCoordinate: Int, + yCoordinate: Int, + placementScale: Float, + placementRotation: Float + ) { + val startScript = StartScript() + sprite.prependScript(startScript) + startScript.addBrick(PlaceAtBrick(xCoordinate, yCoordinate)) + if (placementScale != DEFAULT_SCALE) { + val sizePercent = placementScale.toDouble() * + PERCENTAGE_MULTIPLIER + startScript.addBrick(SetSizeToBrick(sizePercent)) + } + if (placementRotation != DEGREE_UI_OFFSET) { + val direction = placementRotation.toDouble() + startScript.addBrick(PointInDirectionBrick(direction)) + } + } } private lateinit var binding: ActivityRecyclerBinding @@ -268,20 +293,11 @@ class ProjectActivity : BaseCastActivity() { extras.getFloat(VisualPlacementActivity.SCALE_BUNDLE_ARGUMENT, 1.0f) val placementRotation = extras.getFloat(VisualPlacementActivity.ROTATION_BUNDLE_ARGUMENT, DEGREE_UI_OFFSET) - val placeAtBrick = PlaceAtBrick(xCoordinate, yCoordinate) val currentSprite = projectManager.currentSprite ?: return - val startScript = StartScript() - currentSprite.prependScript(startScript) - startScript.addBrick(placeAtBrick) - if (placementScale != DEFAULT_SCALE) { - val sizePercent = placementScale.toDouble() * - PERCENTAGE_MULTIPLIER - startScript.addBrick(SetSizeToBrick(sizePercent)) - } - if (placementRotation != DEGREE_UI_OFFSET) { - val direction = placementRotation.toDouble() - startScript.addBrick(PointInDirectionBrick(direction)) - } + applyVisualPlacementBricks( + currentSprite, xCoordinate, yCoordinate, + placementScale, placementRotation + ) } SPRITE_FROM_LOCAL -> if (data != null && data.hasExtra(ProjectListActivity.IMPORT_LOCAL_INTENT)) { diff --git a/catroid/src/test/java/org/catrobat/catroid/test/ui/ProjectActivityVisualPlacementTest.kt b/catroid/src/test/java/org/catrobat/catroid/test/ui/ProjectActivityVisualPlacementTest.kt new file mode 100644 index 00000000000..76b65116d12 --- /dev/null +++ b/catroid/src/test/java/org/catrobat/catroid/test/ui/ProjectActivityVisualPlacementTest.kt @@ -0,0 +1,188 @@ +/* + * Catroid: An on-device visual programming system for Android devices + * Copyright (C) 2010-2026 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * An additional term exception under section 7 of the GNU Affero + * General Public License, version 3, is available at + * http://developer.catrobat.org/license_additional_term + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.catrobat.catroid.test.ui + +import org.catrobat.catroid.content.Look.DEGREE_UI_OFFSET +import org.catrobat.catroid.content.Sprite +import org.catrobat.catroid.content.StartScript +import org.catrobat.catroid.content.bricks.PlaceAtBrick +import org.catrobat.catroid.content.bricks.PointInDirectionBrick +import org.catrobat.catroid.content.bricks.SetSizeToBrick +import org.catrobat.catroid.ui.ProjectActivity +import org.catrobat.catroid.ui.ProjectActivity.Companion.DEFAULT_SCALE +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@RunWith(JUnit4::class) +class ProjectActivityVisualPlacementTest { + + private lateinit var sprite: Sprite + + @Before + fun setUp() { + sprite = Sprite("TestSprite") + } + + @Test + fun testDefaultRotationSuppressesPointInDirectionBrick() { + ProjectActivity.applyVisualPlacementBricks( + sprite, + xCoordinate = 100, + yCoordinate = 200, + placementScale = DEFAULT_SCALE, + placementRotation = DEGREE_UI_OFFSET + ) + + val script = sprite.getScript(0) + val bricks = script.brickList + + assertEquals(1, bricks.size) + assertTrue(bricks[0] is PlaceAtBrick) + assertFalse( + "PointInDirectionBrick must not be added for default rotation", + bricks.any { it is PointInDirectionBrick } + ) + } + + @Test + fun testDefaultScaleSuppressesSetSizeToBrick() { + ProjectActivity.applyVisualPlacementBricks( + sprite, + xCoordinate = 0, + yCoordinate = 0, + placementScale = DEFAULT_SCALE, + placementRotation = DEGREE_UI_OFFSET + ) + + val script = sprite.getScript(0) + val bricks = script.brickList + + assertEquals(1, bricks.size) + assertFalse( + "SetSizeToBrick must not be added for default scale", + bricks.any { it is SetSizeToBrick } + ) + } + + @Test + fun testNonDefaultRotationAddsPointInDirectionBrick() { + val customRotation = 45.0f + + ProjectActivity.applyVisualPlacementBricks( + sprite, + xCoordinate = 50, + yCoordinate = -30, + placementScale = DEFAULT_SCALE, + placementRotation = customRotation + ) + + val script = sprite.getScript(0) + val bricks = script.brickList + + assertEquals(2, bricks.size) + assertTrue(bricks[0] is PlaceAtBrick) + assertTrue(bricks[1] is PointInDirectionBrick) + } + + @Test + fun testNonDefaultScaleAddsSetSizeToBrick() { + val customScale = 2.5f + + ProjectActivity.applyVisualPlacementBricks( + sprite, + xCoordinate = 0, + yCoordinate = 0, + placementScale = customScale, + placementRotation = DEGREE_UI_OFFSET + ) + + val script = sprite.getScript(0) + val bricks = script.brickList + + assertEquals(2, bricks.size) + assertTrue(bricks[0] is PlaceAtBrick) + assertTrue(bricks[1] is SetSizeToBrick) + } + + @Test + fun testNonDefaultScaleAndRotationAddsBothBricks() { + val customScale = 1.5f + val customRotation = 135.0f + + ProjectActivity.applyVisualPlacementBricks( + sprite, + xCoordinate = 10, + yCoordinate = 20, + placementScale = customScale, + placementRotation = customRotation + ) + + val script = sprite.getScript(0) + val bricks = script.brickList + + assertEquals(3, bricks.size) + assertTrue(bricks[0] is PlaceAtBrick) + assertTrue(bricks[1] is SetSizeToBrick) + assertTrue(bricks[2] is PointInDirectionBrick) + } + + @Test + fun testScriptIsPrependedToExistingScripts() { + sprite.addScript(StartScript()) + + ProjectActivity.applyVisualPlacementBricks( + sprite, + xCoordinate = 5, + yCoordinate = 10, + placementScale = DEFAULT_SCALE, + placementRotation = DEGREE_UI_OFFSET + ) + + assertEquals(2, sprite.numberOfScripts) + val firstScript = sprite.getScript(0) + assertTrue(firstScript.brickList[0] is PlaceAtBrick) + } + + @Test + fun testZeroRotationAddsPointInDirectionBrick() { + ProjectActivity.applyVisualPlacementBricks( + sprite, + xCoordinate = 0, + yCoordinate = 0, + placementScale = DEFAULT_SCALE, + placementRotation = 0.0f + ) + + val script = sprite.getScript(0) + assertTrue( + "Rotation of 0 differs from DEGREE_UI_OFFSET and must add a brick", + script.brickList.any { it is PointInDirectionBrick } + ) + } +} From 57d5096e8ed43a5816e75fd9bdfecb6a3964cd5d Mon Sep 17 00:00:00 2001 From: harshsomankar123-tech Date: Wed, 15 Apr 2026 02:58:07 +0530 Subject: [PATCH 14/17] Fix visual placement rotation bug, add panning/snapping and reset button (IDE-217) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix SpriteActivity: default placementRotation to DEGREE_UI_OFFSET (90°) and only apply rotation when changed - Fix VisualPlacementActivity: set correct defaults for rotationMode and initialLookRotation when loading from look list - Fix default initialSpriteRotation to DEGREE_UI_OFFSET to prevent unwanted PointInDirectionBrick - Add ScreenValueHandler.updateScreenWidthAndHeight() for correct layout sizing - Use existing LayoutParams instead of creating new ones - Add two-finger panning with frame-to-frame delta tracking - Add velocity-based rotation snapping to 90° increments - Add Reset toolbar button to restore initial transformations --- .../catrobat/catroid/ui/SpriteActivity.java | 11 +++-- .../ResizeRotateGestureDetector.kt | 47 ++++++++++++++++++- .../VisualPlacementActivity.java | 36 ++++++++++++-- catroid/src/main/res/menu/menu_confirm.xml | 6 +++ catroid/src/main/res/values/strings.xml | 1 + 5 files changed, 93 insertions(+), 8 deletions(-) diff --git a/catroid/src/main/java/org/catrobat/catroid/ui/SpriteActivity.java b/catroid/src/main/java/org/catrobat/catroid/ui/SpriteActivity.java index eb3d2bb617f..e0f9b8f294f 100644 --- a/catroid/src/main/java/org/catrobat/catroid/ui/SpriteActivity.java +++ b/catroid/src/main/java/org/catrobat/catroid/ui/SpriteActivity.java @@ -96,6 +96,7 @@ import static org.catrobat.catroid.common.FlavoredConstants.CATROBAT_CONTENT_LOOKS_URL; import static org.catrobat.catroid.common.FlavoredConstants.CATROBAT_CONTENT_SOUNDS_URL; import static org.catrobat.catroid.common.SharedPreferenceKeys.INDEXING_VARIABLE_PREFERENCE_KEY; +import static org.catrobat.catroid.content.Look.DEGREE_UI_OFFSET; import static org.catrobat.catroid.stage.TestResult.TEST_RESULT_MESSAGE; import static org.catrobat.catroid.ui.SpriteActivityOnTabSelectedListenerKt.addTabLayout; import static org.catrobat.catroid.ui.SpriteActivityOnTabSelectedListenerKt.getTabPositionInSpriteActivity; @@ -434,7 +435,7 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) { int xCoordinate = extras.getInt(X_COORDINATE_BUNDLE_ARGUMENT); int yCoordinate = extras.getInt(Y_COORDINATE_BUNDLE_ARGUMENT); float placementScale = extras.getFloat(SCALE_BUNDLE_ARGUMENT, 1.0f); - float placementRotation = extras.getFloat(ROTATION_BUNDLE_ARGUMENT, 0.0f); + float placementRotation = extras.getFloat(ROTATION_BUNDLE_ARGUMENT, DEGREE_UI_OFFSET); int brickHash = extras.getInt(EXTRA_BRICK_HASH); Fragment fragment = getCurrentFragment(); @@ -461,9 +462,11 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) { } } - Sprite rotSprite = projectManager.getCurrentSprite(); - if (rotSprite != null && rotSprite.look != null) { - rotSprite.look.setMotionDirectionInUserInterfaceDimensionUnit(placementRotation); + if (placementRotation != DEGREE_UI_OFFSET) { + Sprite rotSprite = projectManager.getCurrentSprite(); + if (rotSprite != null && rotSprite.look != null) { + rotSprite.look.setMotionDirectionInUserInterfaceDimensionUnit(placementRotation); + } } setUndoMenuItemVisibility(extras.getBoolean(CHANGED_COORDINATES)); diff --git a/catroid/src/main/java/org/catrobat/catroid/visualplacement/ResizeRotateGestureDetector.kt b/catroid/src/main/java/org/catrobat/catroid/visualplacement/ResizeRotateGestureDetector.kt index 935a7435a74..c185c455349 100644 --- a/catroid/src/main/java/org/catrobat/catroid/visualplacement/ResizeRotateGestureDetector.kt +++ b/catroid/src/main/java/org/catrobat/catroid/visualplacement/ResizeRotateGestureDetector.kt @@ -32,6 +32,7 @@ class ResizeRotateGestureDetector(private val listener: OnTransformGestureListen interface OnTransformGestureListener { fun onScale(scaleFactor: Float) fun onRotate(rotationDegrees: Float) + fun onPan(dx: Float, dy: Float) } var isTransforming: Boolean = false @@ -42,6 +43,11 @@ class ResizeRotateGestureDetector(private val listener: OnTransformGestureListen private var initialDistance: Float = 0f private var initialAngle: Float = 0f + private var previousMidpointX: Float = 0f + private var previousMidpointY: Float = 0f + + private var lastRotationTime: Long = 0 + private var lastRotationAngle: Float = 0f fun onTouchEvent(event: MotionEvent): Boolean { val pointerCount = event.pointerCount @@ -60,12 +66,18 @@ class ResizeRotateGestureDetector(private val listener: OnTransformGestureListen val currentDistance = calculateDistance(x0, y0, x1, y1) val currentAngle = calculateAngle(x0, y0, x1, y1) + val currentMidpointX = (x0 + x1) / 2f + val currentMidpointY = (y0 + y1) / 2f when (event.actionMasked) { MotionEvent.ACTION_POINTER_DOWN -> { isTransforming = true initialDistance = currentDistance initialAngle = currentAngle + previousMidpointX = currentMidpointX + previousMidpointY = currentMidpointY + lastRotationTime = System.currentTimeMillis() + lastRotationAngle = currentAngle return true } @@ -77,7 +89,27 @@ class ResizeRotateGestureDetector(private val listener: OnTransformGestureListen listener.onScale(newScale) val angleDelta = normalizeAngle(currentAngle - initialAngle) - listener.onRotate(cumulativeRotation + angleDelta) + var totalRotation = cumulativeRotation + angleDelta + + val currentTime = System.currentTimeMillis() + val deltaTime = currentTime - lastRotationTime + if (deltaTime > 0) { + val rotationVelocity = Math.abs(currentAngle - lastRotationAngle) / deltaTime + if (rotationVelocity > SNAP_VELOCITY_THRESHOLD) { + totalRotation = getSnappedAngle(totalRotation) + } + } + lastRotationTime = currentTime + lastRotationAngle = currentAngle + + listener.onRotate(totalRotation) + + val dx = currentMidpointX - previousMidpointX + val dy = currentMidpointY - previousMidpointY + previousMidpointX = currentMidpointX + previousMidpointY = currentMidpointY + listener.onPan(dx, dy) + true } else { false @@ -99,11 +131,24 @@ class ResizeRotateGestureDetector(private val listener: OnTransformGestureListen return false } + private fun getSnappedAngle(angle: Float): Float { + val normalizedRotation = (angle % 360 + 360) % 360 + val snapAngles = floatArrayOf(0f, 90f, 180f, 270f, 360f) + for (snapAngle in snapAngles) { + if (Math.abs(normalizedRotation - snapAngle) < SNAP_ANGLE_THRESHOLD) { + return snapAngle + } + } + return angle + } + companion object { private const val MIN_SCALE = 0.1f private const val MAX_SCALE = 5.0f private const val HALF_CIRCLE = 180f private const val FULL_CIRCLE = 360f + private const val SNAP_VELOCITY_THRESHOLD = 1.5f // degrees per ms + private const val SNAP_ANGLE_THRESHOLD = 10f // degrees /** * Wraps an angle delta into the [-180, 180] range so that diff --git a/catroid/src/main/java/org/catrobat/catroid/visualplacement/VisualPlacementActivity.java b/catroid/src/main/java/org/catrobat/catroid/visualplacement/VisualPlacementActivity.java index d617b89755f..49f6a1d97b8 100644 --- a/catroid/src/main/java/org/catrobat/catroid/visualplacement/VisualPlacementActivity.java +++ b/catroid/src/main/java/org/catrobat/catroid/visualplacement/VisualPlacementActivity.java @@ -52,6 +52,7 @@ import org.catrobat.catroid.ProjectManager; import org.catrobat.catroid.R; import org.catrobat.catroid.common.ScreenValues; +import org.catrobat.catroid.utils.ScreenValueHandler; import org.catrobat.catroid.content.Project; import org.catrobat.catroid.content.Sprite; import org.catrobat.catroid.ui.BaseCastActivity; @@ -119,7 +120,7 @@ public class VisualPlacementActivity extends BaseCastActivity implements View.On private float scaleFactor = 1.0f; private float rotation = 0.0f; private float initialSpriteScale = 1.0f; - private float initialSpriteRotation = 0.0f; + private float initialSpriteRotation = DEGREE_UI_OFFSET; private boolean isText; private String text; @@ -150,10 +151,28 @@ public boolean onOptionsItemSelected(MenuItem item) { case R.id.confirm: finishWithResult(); break; + case R.id.reset: + resetTransformations(); + break; } return true; } + private void resetTransformations() { + setupImageViewPositionAndScale(); + if (boundingBoxOverlay != null) { + boundingBoxOverlay.updateOverlay(); + } + } + + private void updateCoordinatesFromView() { + if (imageView == null) { + return; + } + setXCoordinate(imageView.getX()); + setYCoordinate(imageView.getY()); + } + @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(null); @@ -203,6 +222,7 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { } else { setRequestedOrientation(SCREEN_ORIENTATION_PORTRAIT); } + ScreenValueHandler.updateScreenWidthAndHeight(this); resizeRotateDetector = new ResizeRotateGestureDetector(new ResizeRotateGestureDetector.OnTransformGestureListener() { @Override @@ -222,6 +242,15 @@ public void onRotate(float rotation) { imageView.setRotation(VisualPlacementActivity.this.rotation); } } + + @Override + public void onPan(float dx, float dy) { + if (imageView != null) { + imageView.setX(imageView.getX() + dx); + imageView.setY(imageView.getY() + dy); + updateCoordinatesFromView(); + } + } }); visualPlacementTouchListener = new VisualPlacementTouchListener(); @@ -242,8 +271,7 @@ public void onRotate(float rotation) { break; } - LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, - ViewGroup.LayoutParams.WRAP_CONTENT); + LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams) frameLayout.getLayoutParams(); layoutParams.gravity = Gravity.CENTER; layoutParams.width = layoutResolution.getWidth(); layoutParams.height = layoutResolution.getHeight(); @@ -319,6 +347,8 @@ private Bitmap loadBitmapForPlacement() { } else if (currentSprite.getLookList() != null && !currentSprite.getLookList().isEmpty() && currentSprite.getLookList().get(0).getFile() != null) { String path = currentSprite.getLookList().get(0).getFile().getAbsolutePath(); + rotationMode = ROTATION_STYLE_ALL_AROUND; + initialLookRotation = DEGREE_UI_OFFSET; bitmap = BitmapFactory.decodeFile(path, bitmapOptions); } diff --git a/catroid/src/main/res/menu/menu_confirm.xml b/catroid/src/main/res/menu/menu_confirm.xml index 01dcae8e73a..af614a5d142 100644 --- a/catroid/src/main/res/menu/menu_confirm.xml +++ b/catroid/src/main/res/menu/menu_confirm.xml @@ -25,6 +25,12 @@ + + Settings Redo Undo + Reset new… edit… From c59065df0f3469c3b905430e977bcd75fc8ba208 Mon Sep 17 00:00:00 2001 From: harshsomankar123-tech Date: Wed, 15 Apr 2026 03:10:00 +0530 Subject: [PATCH 15/17] =?UTF-8?q?Add=20Rotate=2090=C2=B0=20toolbar=20butto?= =?UTF-8?q?n=20and=20update=20tests=20(IDE-217)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add ic_rotate_90 drawable (circular arrow icon) - Add rotate_90 menu item between reset and confirm - Implement rotateBy90Degrees() in VisualPlacementActivity - Add rotate_90 string resource - Add testCumulativeRotationForRotate90Feature test - Add testSetCumulativeRotationAndScaleForResetScenario test - Update test listeners to implement onPan interface --- .../VisualPlacementActivity.java | 15 +++++++ .../src/main/res/drawable/ic_rotate_90.xml | 32 ++++++++++++++ catroid/src/main/res/menu/menu_confirm.xml | 6 +++ catroid/src/main/res/values/strings.xml | 1 + .../ResizeRotateGestureDetectorTest.kt | 44 +++++++++++++++++++ .../VisualPlacementResizeRotateTest.kt | 4 ++ 6 files changed, 102 insertions(+) create mode 100644 catroid/src/main/res/drawable/ic_rotate_90.xml diff --git a/catroid/src/main/java/org/catrobat/catroid/visualplacement/VisualPlacementActivity.java b/catroid/src/main/java/org/catrobat/catroid/visualplacement/VisualPlacementActivity.java index 49f6a1d97b8..f5a2ec9a454 100644 --- a/catroid/src/main/java/org/catrobat/catroid/visualplacement/VisualPlacementActivity.java +++ b/catroid/src/main/java/org/catrobat/catroid/visualplacement/VisualPlacementActivity.java @@ -154,6 +154,9 @@ public boolean onOptionsItemSelected(MenuItem item) { case R.id.reset: resetTransformations(); break; + case R.id.rotate_90: + rotateBy90Degrees(); + break; } return true; } @@ -165,6 +168,18 @@ private void resetTransformations() { } } + private void rotateBy90Degrees() { + if (imageView == null) { + return; + } + rotation += 90f; + imageView.setRotation(rotation); + resizeRotateDetector.setCumulativeRotation(rotation); + if (boundingBoxOverlay != null) { + boundingBoxOverlay.updateOverlay(); + } + } + private void updateCoordinatesFromView() { if (imageView == null) { return; diff --git a/catroid/src/main/res/drawable/ic_rotate_90.xml b/catroid/src/main/res/drawable/ic_rotate_90.xml new file mode 100644 index 00000000000..6d9957664fc --- /dev/null +++ b/catroid/src/main/res/drawable/ic_rotate_90.xml @@ -0,0 +1,32 @@ + + + + + diff --git a/catroid/src/main/res/menu/menu_confirm.xml b/catroid/src/main/res/menu/menu_confirm.xml index af614a5d142..cfb2484f30e 100644 --- a/catroid/src/main/res/menu/menu_confirm.xml +++ b/catroid/src/main/res/menu/menu_confirm.xml @@ -31,6 +31,12 @@ android:title="@string/reset" app:showAsAction="ifRoom" /> + + Redo Undo Reset + Rotate 90° new… edit… diff --git a/catroid/src/test/java/org/catrobat/catroid/test/visualplacement/ResizeRotateGestureDetectorTest.kt b/catroid/src/test/java/org/catrobat/catroid/test/visualplacement/ResizeRotateGestureDetectorTest.kt index f71cda354af..0fbeff27b04 100644 --- a/catroid/src/test/java/org/catrobat/catroid/test/visualplacement/ResizeRotateGestureDetectorTest.kt +++ b/catroid/src/test/java/org/catrobat/catroid/test/visualplacement/ResizeRotateGestureDetectorTest.kt @@ -37,11 +37,15 @@ class ResizeRotateGestureDetectorTest { private lateinit var detector: ResizeRotateGestureDetector private var lastScale = 1.0f private var lastRotation = 0.0f + private var lastPanDx = 0.0f + private var lastPanDy = 0.0f @Before fun setUp() { lastScale = 1.0f lastRotation = 0.0f + lastPanDx = 0.0f + lastPanDy = 0.0f detector = ResizeRotateGestureDetector( object : ResizeRotateGestureDetector.OnTransformGestureListener { override fun onScale(scaleFactor: Float) { @@ -51,6 +55,11 @@ class ResizeRotateGestureDetectorTest { override fun onRotate(rotationDegrees: Float) { lastRotation = rotationDegrees } + + override fun onPan(dx: Float, dy: Float) { + lastPanDx = dx + lastPanDy = dy + } } ) } @@ -135,4 +144,39 @@ class ResizeRotateGestureDetectorTest { companion object { private const val DELTA = 0.01f } + + @Test + fun testSetCumulativeRotationAndScaleForResetScenario() { + detector.cumulativeRotation = 90.0f + detector.cumulativeScale = 2.0f + + detector.cumulativeRotation = 0.0f + detector.cumulativeScale = 1.0f + + assertEquals(0.0f, detector.cumulativeRotation, DELTA) + assertEquals(1.0f, detector.cumulativeScale, DELTA) + } + + @Test + fun testCumulativeRotationForRotate90Feature() { + // Simulates the rotateBy90Degrees feature: + // rotation starts at 0, incremented by 90, then synced to detector + var rotation = 0.0f + + rotation += 90f + detector.cumulativeRotation = rotation + assertEquals(90.0f, detector.cumulativeRotation, DELTA) + + rotation += 90f + detector.cumulativeRotation = rotation + assertEquals(180.0f, detector.cumulativeRotation, DELTA) + + rotation += 90f + detector.cumulativeRotation = rotation + assertEquals(270.0f, detector.cumulativeRotation, DELTA) + + rotation += 90f + detector.cumulativeRotation = rotation + assertEquals(360.0f, detector.cumulativeRotation, DELTA) + } } diff --git a/catroid/src/test/java/org/catrobat/catroid/test/visualplacement/VisualPlacementResizeRotateTest.kt b/catroid/src/test/java/org/catrobat/catroid/test/visualplacement/VisualPlacementResizeRotateTest.kt index 12e86b36991..99603890dc6 100644 --- a/catroid/src/test/java/org/catrobat/catroid/test/visualplacement/VisualPlacementResizeRotateTest.kt +++ b/catroid/src/test/java/org/catrobat/catroid/test/visualplacement/VisualPlacementResizeRotateTest.kt @@ -74,6 +74,10 @@ class VisualPlacementResizeRotateTest { override fun onRotate(rotationDegrees: Float) { lastRotation = rotationDegrees } + + override fun onPan(dx: Float, dy: Float) { + // Not tested in this class + } } ) listener.setResizeRotateDetector(detector) From a0fd2f4c440943f75307fd17c5a476c391bb2842 Mon Sep 17 00:00:00 2001 From: harshsomankar123-tech Date: Thu, 16 Apr 2026 06:48:12 +0530 Subject: [PATCH 16/17] Fix: Prevent resize from changing position and orientation (IDE-217) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Added rotation dead zone (5°) so small angle changes during pinch-to-scale don't trigger unwanted rotation - Added pan dead zone (10px) so midpoint jitter during pinch doesn't cause the sprite to drift/move upward - Extracted onTouchEvent logic into separate methods to fix detekt excessive nesting warning - Replaced magic numbers with named constants (FULL_CIRCLE) - Converted switch(R.id) to if-else in VisualPlacementActivity to fix Android Lint non-final resource ID warning for AGP 8.0 --- .../ResizeRotateGestureDetector.kt | 202 ++++++++++++------ .../VisualPlacementActivity.java | 22 +- 2 files changed, 144 insertions(+), 80 deletions(-) diff --git a/catroid/src/main/java/org/catrobat/catroid/visualplacement/ResizeRotateGestureDetector.kt b/catroid/src/main/java/org/catrobat/catroid/visualplacement/ResizeRotateGestureDetector.kt index c185c455349..4c0f2825823 100644 --- a/catroid/src/main/java/org/catrobat/catroid/visualplacement/ResizeRotateGestureDetector.kt +++ b/catroid/src/main/java/org/catrobat/catroid/visualplacement/ResizeRotateGestureDetector.kt @@ -24,6 +24,7 @@ package org.catrobat.catroid.visualplacement import android.view.MotionEvent +import kotlin.math.abs import kotlin.math.atan2 import kotlin.math.sqrt @@ -48,15 +49,14 @@ class ResizeRotateGestureDetector(private val listener: OnTransformGestureListen private var lastRotationTime: Long = 0 private var lastRotationAngle: Float = 0f + private var isRotationActive: Boolean = false + private var isPanActive: Boolean = false + private var accumulatedPanX: Float = 0f + private var accumulatedPanY: Float = 0f fun onTouchEvent(event: MotionEvent): Boolean { - val pointerCount = event.pointerCount - - if (pointerCount < 2) { - if (isTransforming) { - isTransforming = false - } - return false + if (event.pointerCount < 2) { + return handleSinglePointer() } val x0 = event.getX(0) @@ -69,73 +69,137 @@ class ResizeRotateGestureDetector(private val listener: OnTransformGestureListen val currentMidpointX = (x0 + x1) / 2f val currentMidpointY = (y0 + y1) / 2f - when (event.actionMasked) { - MotionEvent.ACTION_POINTER_DOWN -> { - isTransforming = true - initialDistance = currentDistance - initialAngle = currentAngle - previousMidpointX = currentMidpointX - previousMidpointY = currentMidpointY - lastRotationTime = System.currentTimeMillis() - lastRotationAngle = currentAngle - return true + return when (event.actionMasked) { + MotionEvent.ACTION_POINTER_DOWN -> + handlePointerDown(currentDistance, currentAngle, currentMidpointX, currentMidpointY) + MotionEvent.ACTION_MOVE -> + handleMove(currentDistance, currentAngle, currentMidpointX, currentMidpointY) + MotionEvent.ACTION_POINTER_UP -> + handlePointerUp(currentDistance, currentAngle) + else -> false + } + } + + private fun handleSinglePointer(): Boolean { + if (isTransforming) { + isTransforming = false + isRotationActive = false + isPanActive = false + } + return false + } + + private fun handlePointerDown( + distance: Float, + angle: Float, + midpointX: Float, + midpointY: Float + ): Boolean { + isTransforming = true + isRotationActive = false + isPanActive = false + accumulatedPanX = 0f + accumulatedPanY = 0f + initialDistance = distance + initialAngle = angle + previousMidpointX = midpointX + previousMidpointY = midpointY + lastRotationTime = System.currentTimeMillis() + lastRotationAngle = angle + return true + } + + private fun handleMove( + currentDistance: Float, + currentAngle: Float, + currentMidpointX: Float, + currentMidpointY: Float + ): Boolean { + if (!isTransforming || initialDistance <= 0) { + return false + } + + applyScale(currentDistance) + applyRotation(currentAngle) + applyPan(currentMidpointX, currentMidpointY) + return true + } + + private fun applyScale(currentDistance: Float) { + val scaleFactor = currentDistance / initialDistance + val newScale = (cumulativeScale * scaleFactor).coerceIn(MIN_SCALE, MAX_SCALE) + listener.onScale(newScale) + } + + private fun applyRotation(currentAngle: Float) { + val angleDelta = normalizeAngle(currentAngle - initialAngle) + + if (!isRotationActive && abs(angleDelta) < ROTATION_DEAD_ZONE) { + return + } + isRotationActive = true + + var totalRotation = cumulativeRotation + angleDelta + totalRotation = applySnappingIfNeeded(totalRotation, currentAngle) + listener.onRotate(totalRotation) + } + + private fun applySnappingIfNeeded(totalRotation: Float, currentAngle: Float): Float { + val currentTime = System.currentTimeMillis() + val deltaTime = currentTime - lastRotationTime + var result = totalRotation + + if (deltaTime > 0) { + val rotationVelocity = abs(currentAngle - lastRotationAngle) / deltaTime + if (rotationVelocity > SNAP_VELOCITY_THRESHOLD) { + result = getSnappedAngle(totalRotation) + } + } + lastRotationTime = currentTime + lastRotationAngle = currentAngle + return result + } + + private fun applyPan(currentMidpointX: Float, currentMidpointY: Float) { + val dx = currentMidpointX - previousMidpointX + val dy = currentMidpointY - previousMidpointY + previousMidpointX = currentMidpointX + previousMidpointY = currentMidpointY + + if (!isPanActive) { + accumulatedPanX += dx + accumulatedPanY += dy + val totalDrift = sqrt(accumulatedPanX * accumulatedPanX + accumulatedPanY * accumulatedPanY) + if (totalDrift < PAN_DEAD_ZONE) { + return } + isPanActive = true + listener.onPan(accumulatedPanX, accumulatedPanY) + } else { + listener.onPan(dx, dy) + } + } - MotionEvent.ACTION_MOVE -> - if (isTransforming && initialDistance > 0) { - val scaleFactor = currentDistance / initialDistance - val newScale = (cumulativeScale * scaleFactor) - .coerceIn(MIN_SCALE, MAX_SCALE) - listener.onScale(newScale) - - val angleDelta = normalizeAngle(currentAngle - initialAngle) - var totalRotation = cumulativeRotation + angleDelta - - val currentTime = System.currentTimeMillis() - val deltaTime = currentTime - lastRotationTime - if (deltaTime > 0) { - val rotationVelocity = Math.abs(currentAngle - lastRotationAngle) / deltaTime - if (rotationVelocity > SNAP_VELOCITY_THRESHOLD) { - totalRotation = getSnappedAngle(totalRotation) - } - } - lastRotationTime = currentTime - lastRotationAngle = currentAngle - - listener.onRotate(totalRotation) - - val dx = currentMidpointX - previousMidpointX - val dy = currentMidpointY - previousMidpointY - previousMidpointX = currentMidpointX - previousMidpointY = currentMidpointY - listener.onPan(dx, dy) - - true - } else { - false - } - - MotionEvent.ACTION_POINTER_UP -> { - if (isTransforming && initialDistance > 0) { - val finalScaleFactor = currentDistance / initialDistance - cumulativeScale = (cumulativeScale * finalScaleFactor) - .coerceIn(MIN_SCALE, MAX_SCALE) - - val finalAngleDelta = normalizeAngle(currentAngle - initialAngle) - cumulativeRotation += finalAngleDelta - } - isTransforming = false - return true + private fun handlePointerUp(currentDistance: Float, currentAngle: Float): Boolean { + if (isTransforming && initialDistance > 0) { + val finalScaleFactor = currentDistance / initialDistance + cumulativeScale = (cumulativeScale * finalScaleFactor).coerceIn(MIN_SCALE, MAX_SCALE) + + if (isRotationActive) { + val finalAngleDelta = normalizeAngle(currentAngle - initialAngle) + cumulativeRotation += finalAngleDelta } } - return false + isTransforming = false + isRotationActive = false + isPanActive = false + return true } private fun getSnappedAngle(angle: Float): Float { - val normalizedRotation = (angle % 360 + 360) % 360 - val snapAngles = floatArrayOf(0f, 90f, 180f, 270f, 360f) - for (snapAngle in snapAngles) { - if (Math.abs(normalizedRotation - snapAngle) < SNAP_ANGLE_THRESHOLD) { + val normalizedRotation = (angle % FULL_CIRCLE + FULL_CIRCLE) % FULL_CIRCLE + for (snapAngle in SNAP_ANGLES) { + if (abs(normalizedRotation - snapAngle) < SNAP_ANGLE_THRESHOLD) { return snapAngle } } @@ -149,6 +213,10 @@ class ResizeRotateGestureDetector(private val listener: OnTransformGestureListen private const val FULL_CIRCLE = 360f private const val SNAP_VELOCITY_THRESHOLD = 1.5f // degrees per ms private const val SNAP_ANGLE_THRESHOLD = 10f // degrees + private const val ROTATION_DEAD_ZONE = 5f // degrees threshold before rotation activates + private const val PAN_DEAD_ZONE = 10f // pixels threshold before pan activates + + private val SNAP_ANGLES = floatArrayOf(0f, 90f, 180f, 270f, FULL_CIRCLE) /** * Wraps an angle delta into the [-180, 180] range so that diff --git a/catroid/src/main/java/org/catrobat/catroid/visualplacement/VisualPlacementActivity.java b/catroid/src/main/java/org/catrobat/catroid/visualplacement/VisualPlacementActivity.java index f5a2ec9a454..c46c736a4d2 100644 --- a/catroid/src/main/java/org/catrobat/catroid/visualplacement/VisualPlacementActivity.java +++ b/catroid/src/main/java/org/catrobat/catroid/visualplacement/VisualPlacementActivity.java @@ -144,19 +144,15 @@ public boolean onCreateOptionsMenu(Menu menu) { @Override public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case android.R.id.home: - onBackPressed(); - break; - case R.id.confirm: - finishWithResult(); - break; - case R.id.reset: - resetTransformations(); - break; - case R.id.rotate_90: - rotateBy90Degrees(); - break; + int itemId = item.getItemId(); + if (itemId == android.R.id.home) { + onBackPressed(); + } else if (itemId == R.id.confirm) { + finishWithResult(); + } else if (itemId == R.id.reset) { + resetTransformations(); + } else if (itemId == R.id.rotate_90) { + rotateBy90Degrees(); } return true; } From e152f09cf02befff66d50e1a0c52535c86f6c9ff Mon Sep 17 00:00:00 2001 From: harshsomankar123-tech Date: Thu, 16 Apr 2026 06:57:40 +0530 Subject: [PATCH 17/17] Refactor: Reduce cognitive complexity of onCreate in VisualPlacementActivity (IDE-217) - Extracted parseIntentExtras(), setupToolbar(), setupOrientation(), createResizeRotateDetector(), and setupLayout() from onCreate - Reduces cognitive complexity from 16 to under 15 threshold --- .../VisualPlacementActivity.java | 65 ++++++++++++------- 1 file changed, 41 insertions(+), 24 deletions(-) diff --git a/catroid/src/main/java/org/catrobat/catroid/visualplacement/VisualPlacementActivity.java b/catroid/src/main/java/org/catrobat/catroid/visualplacement/VisualPlacementActivity.java index c46c736a4d2..6f9a1c69368 100644 --- a/catroid/src/main/java/org/catrobat/catroid/visualplacement/VisualPlacementActivity.java +++ b/catroid/src/main/java/org/catrobat/catroid/visualplacement/VisualPlacementActivity.java @@ -208,6 +208,38 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { finish(); return; } + + parseIntentExtras(extras); + setupToolbar(); + setupOrientation(); + ScreenValueHandler.updateScreenWidthAndHeight(this); + + resizeRotateDetector = createResizeRotateDetector(); + visualPlacementTouchListener = new VisualPlacementTouchListener(); + visualPlacementTouchListener.setResizeRotateDetector(resizeRotateDetector); + + frameLayout = findViewById(R.id.frame_container); + setupLayout(currentProject); + + bitmapOptions = new BitmapFactory.Options(); + bitmapOptions.inPreferredConfig = Bitmap.Config.ARGB_8888; + + setBackground(); + showMovableImageView(); + + boundingBoxOverlay = new BoundingBoxOverlay(this); + boundingBoxOverlay.setTrackedImageView(imageView); + frameLayout.addView(boundingBoxOverlay, + new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT)); + + visualPlacementTouchListener.setBoundingBoxOverlay(boundingBoxOverlay); + + findViewById(R.id.transparent_toolbar).bringToFront(); + frameLayout.setOnTouchListener(this); + } + + private void parseIntentExtras(Bundle extras) { translateX = extras.getInt(EXTRA_X_TRANSFORM); translateY = extras.getInt(EXTRA_Y_TRANSFORM); if (extras.containsKey(EXTRA_TEXT)) { @@ -222,20 +254,25 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { } xOffsetText = -DEFAULT_X_OFFSET; } + } + private void setupToolbar() { Toolbar toolbar = findViewById(R.id.transparent_toolbar); setSupportActionBar(toolbar); getSupportActionBar().setDisplayHomeAsUpEnabled(true); getSupportActionBar().setTitle(R.string.brick_option_place_visually); + } + private void setupOrientation() { if (projectManager.isCurrentProjectLandscapeMode()) { setRequestedOrientation(SCREEN_ORIENTATION_LANDSCAPE); } else { setRequestedOrientation(SCREEN_ORIENTATION_PORTRAIT); } - ScreenValueHandler.updateScreenWidthAndHeight(this); + } - resizeRotateDetector = new ResizeRotateGestureDetector(new ResizeRotateGestureDetector.OnTransformGestureListener() { + private ResizeRotateGestureDetector createResizeRotateDetector() { + return new ResizeRotateGestureDetector(new ResizeRotateGestureDetector.OnTransformGestureListener() { @Override public void onScale(float scale) { scaleFactor = scale; @@ -263,12 +300,9 @@ public void onPan(float dx, float dy) { } } }); + } - visualPlacementTouchListener = new VisualPlacementTouchListener(); - visualPlacementTouchListener.setResizeRotateDetector(resizeRotateDetector); - - frameLayout = findViewById(R.id.frame_container); - + private void setupLayout(Project currentProject) { Resolution projectResolution = new Resolution( currentProject.getXmlHeader().virtualScreenWidth, currentProject.getXmlHeader().virtualScreenHeight); @@ -290,23 +324,6 @@ public void onPan(float dx, float dy) { layoutHeightRatio = (float) layoutResolution.getHeight() / (float) projectResolution.getHeight(); layoutWidthRatio = (float) layoutResolution.getWidth() / (float) projectResolution.getWidth(); - - bitmapOptions = new BitmapFactory.Options(); - bitmapOptions.inPreferredConfig = Bitmap.Config.ARGB_8888; - - setBackground(); - showMovableImageView(); - - boundingBoxOverlay = new BoundingBoxOverlay(this); - boundingBoxOverlay.setTrackedImageView(imageView); - frameLayout.addView(boundingBoxOverlay, - new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.MATCH_PARENT)); - - visualPlacementTouchListener.setBoundingBoxOverlay(boundingBoxOverlay); - - toolbar.bringToFront(); - frameLayout.setOnTouchListener(this); } private void setBackground() {