diff --git a/Paintroid/src/androidTest/java/org/catrobat/paintroid/test/espresso/tools/ImportToolIntentTest.kt b/Paintroid/src/androidTest/java/org/catrobat/paintroid/test/espresso/tools/ImportToolIntentTest.kt new file mode 100644 index 0000000000..07ef66aff4 --- /dev/null +++ b/Paintroid/src/androidTest/java/org/catrobat/paintroid/test/espresso/tools/ImportToolIntentTest.kt @@ -0,0 +1,215 @@ +/* + * Paintroid: An image manipulation application for Android. + * Copyright (C) 2010-2024 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. + * + * 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.paintroid.test.espresso.tools + +import android.app.Activity +import android.app.Instrumentation +import android.content.Intent +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.graphics.PointF +import android.net.Uri +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.assertion.ViewAssertions +import androidx.test.espresso.assertion.ViewAssertions.doesNotExist +import androidx.test.espresso.intent.Intents.intending +import androidx.test.espresso.intent.matcher.IntentMatchers.hasAction +import androidx.test.espresso.intent.rule.IntentsTestRule +import androidx.test.espresso.matcher.RootMatchers.isDialog +import androidx.test.espresso.matcher.ViewMatchers +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.espresso.matcher.ViewMatchers.withText +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.rule.GrantPermissionRule +import org.catrobat.paintroid.MainActivity +import org.catrobat.paintroid.R +import org.catrobat.paintroid.contract.LayerContracts +import org.catrobat.paintroid.test.espresso.util.EspressoUtils +import org.catrobat.paintroid.test.espresso.util.MainActivityHelper +import org.catrobat.paintroid.test.espresso.util.UiInteractions +import org.catrobat.paintroid.test.espresso.util.wrappers.DrawingSurfaceInteraction +import org.catrobat.paintroid.test.espresso.util.wrappers.ToolBarViewInteraction +import org.catrobat.paintroid.test.espresso.util.wrappers.TopBarViewInteraction +import org.catrobat.paintroid.tools.ToolReference +import org.catrobat.paintroid.tools.ToolType +import org.catrobat.paintroid.tools.Workspace +import org.catrobat.paintroid.tools.implementation.MAXIMUM_BITMAP_SIZE_FACTOR +import org.catrobat.paintroid.ui.Perspective +import org.hamcrest.CoreMatchers.not +import org.junit.After +import org.junit.Assert +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import java.io.File +import java.io.FileNotFoundException +import java.io.FileOutputStream +import java.io.IOException + +@RunWith(AndroidJUnit4::class) +class ImportToolIntentTest { + + private val SAMPLE_IMAGE_NAME = "import_tool_test_sample_image.png" + + @get:Rule + var intentsTestRule = IntentsTestRule(MainActivity::class.java) + + @get:Rule + var grantPermissionRule: GrantPermissionRule = EspressoUtils.grantPermissionRulesVersionCheck() + private var toolReference: ToolReference? = null + private var displayWidth = 0 + private var displayHeight = 0 + private var initialWidth = 0 + private var initialHeight = 0 + private var maxBitmapSize = 0 + private var initialBitmapHeight = 0 + private var initialBitmapWidth = 0 + private var maxWidth = 0 + private lateinit var mainActivity: MainActivity + private lateinit var activityHelper: MainActivityHelper + private lateinit var layerModel: LayerContracts.Model + private lateinit var perspective: Perspective + private lateinit var workspace: Workspace + @Before + fun setUp() { + ToolBarViewInteraction.onToolBarView().performSelectTool(ToolType.IMPORTPNG) + mainActivity = intentsTestRule.activity + toolReference = mainActivity.toolReference + activityHelper = MainActivityHelper(mainActivity) + displayWidth = activityHelper.displayWidth + displayHeight = activityHelper.displayHeight + maxBitmapSize = displayHeight * displayWidth * MAXIMUM_BITMAP_SIZE_FACTOR.toInt() + layerModel = mainActivity.layerModel + val workingBitmap = layerModel.currentLayer!!.bitmap + initialWidth = workingBitmap.width + initialHeight = workingBitmap.height + perspective = mainActivity.perspective + workspace = mainActivity.workspace + maxWidth = maxBitmapSize / initialHeight + initialBitmapWidth = workspace.width + initialBitmapHeight = workspace.height + perspective.multiplyScale(.25f) + saveTestImage() + val imgGalleryResult = createImageGallerySetResultStub(mainActivity) + intending(hasAction(Intent.ACTION_GET_CONTENT)).respondWith(imgGalleryResult) + onView(withId(R.id.pocketpaint_dialog_import_gallery)) + .perform(click()) + } + + @After + fun tearDown() { + deleteTestImage(mainActivity) + } + + @Test + fun testEnlargeCanvas() { + var dragFrom = perspective.getSurfacePointFromCanvasPoint( + PointF(initialBitmapWidth.toFloat(), initialBitmapHeight.toFloat())) + var dragTo = perspective.getSurfacePointFromCanvasPoint( + PointF(maxWidth + 10f, initialHeight.toFloat())) + + DrawingSurfaceInteraction.onDrawingSurfaceView() + .perform(UiInteractions.swipe(dragFrom, dragTo)) + TopBarViewInteraction.onTopBarView().performClickCheckmark() + onView(withText(R.string.dialog_import_image_enlarge_image)).check( + ViewAssertions.matches(ViewMatchers.isDisplayed())) + onView(withText(R.string.pocketpaint_enlarge)).perform(click()) + Assert.assertTrue(initialBitmapHeight * initialBitmapWidth < workspace.height * workspace.width) + initialBitmapHeight = workspace.height + initialBitmapWidth = workspace.width + + TopBarViewInteraction.onTopBarView().performClickCheckmark() + onView(withText(R.string.dialog_import_image_enlarge_image)).inRoot( + not(isDialog())).check(doesNotExist()) + dragFrom = perspective.getSurfacePointFromCanvasPoint( + PointF(dragTo.x, dragTo.y)) + dragTo = perspective.getSurfacePointFromCanvasPoint( + PointF(maxWidth * 2f, initialHeight.toFloat())) + DrawingSurfaceInteraction.onDrawingSurfaceView() + .perform(UiInteractions.swipe(dragFrom, dragTo)) + TopBarViewInteraction.onTopBarView().performClickCheckmark() + + onView(withText(R.string.dialog_import_image_enlarge_image)).check( + ViewAssertions.matches(ViewMatchers.isDisplayed())) + onView(withText(R.string.pocketpaint_enlarge)).perform(click()) + onView(withText(R.string.dialog_import_image_canvas_too_large)).check( + ViewAssertions.matches(ViewMatchers.isDisplayed())) + onView(withText(R.string.pocketpaint_truncate)).perform(click()) + onView(withText(R.string.dialog_import_image_canvas_too_large)).inRoot( + not(isDialog())).check(doesNotExist()) + Assert.assertEquals( + initialBitmapHeight * initialBitmapWidth, workspace.height * workspace.width) + } + + @Test + fun testEnlargeWhenSwitchingTool() { + val dragFrom = perspective.getSurfacePointFromCanvasPoint( + PointF(initialBitmapWidth.toFloat(), initialBitmapHeight.toFloat())) + val dragTo = perspective.getSurfacePointFromCanvasPoint( + PointF(maxWidth + 10f, initialHeight.toFloat())) + + DrawingSurfaceInteraction.onDrawingSurfaceView() + .perform(UiInteractions.swipe(dragFrom, dragTo)) + ToolBarViewInteraction.onToolBarView() + .performSelectTool(ToolType.BRUSH) + + onView(withText(R.string.dialog_import_image_enlarge_image)).check( + ViewAssertions.matches(ViewMatchers.isDisplayed())) + onView(withText(R.string.pocketpaint_enlarge)).perform(click()) + Assert.assertTrue(initialBitmapHeight * initialBitmapWidth < workspace.height * workspace.width) + } + + private fun saveTestImage() { + val bm = BitmapFactory.decodeResource(intentsTestRule.activity.resources, R.drawable.pocketpaint_logo) + val dir = intentsTestRule.activity.externalCacheDir + val file = File(dir?.path, SAMPLE_IMAGE_NAME) + val outStream: FileOutputStream? + try { + outStream = FileOutputStream(file) + bm.compress(Bitmap.CompressFormat.PNG, 100, outStream) + with(outStream) { + flush() + close() + } + } catch (e: FileNotFoundException) { + throw AssertionError("Could not save temp file", e) + } catch (e: IOException) { + throw AssertionError("Could not save temp file", e) + } + } + + private fun createImageGallerySetResultStub(activity: Activity): Instrumentation.ActivityResult { + val dir = activity.externalCacheDir + val file = File(dir?.path, SAMPLE_IMAGE_NAME) + val imageUri = Uri.fromFile(file) + val resultIntent = Intent() + resultIntent.data = imageUri + resultIntent.putExtra(Intent.EXTRA_STREAM, imageUri) + return Instrumentation.ActivityResult(Activity.RESULT_OK, resultIntent) + } + + private fun deleteTestImage(activity: Activity) { + val dir = activity.externalCacheDir + val file = File(dir?.path, SAMPLE_IMAGE_NAME) + file.delete() + } +} diff --git a/Paintroid/src/androidTest/java/org/catrobat/paintroid/test/junit/tools/ImportToolTest.kt b/Paintroid/src/androidTest/java/org/catrobat/paintroid/test/junit/tools/ImportToolTest.kt index ff37e0f94d..d83bb5f72b 100644 --- a/Paintroid/src/androidTest/java/org/catrobat/paintroid/test/junit/tools/ImportToolTest.kt +++ b/Paintroid/src/androidTest/java/org/catrobat/paintroid/test/junit/tools/ImportToolTest.kt @@ -39,6 +39,7 @@ import androidx.test.espresso.IdlingRegistry import org.mockito.Mockito import org.catrobat.paintroid.ui.Perspective import android.graphics.Bitmap +import androidx.fragment.app.FragmentManager import org.catrobat.paintroid.tools.implementation.DEFAULT_BOX_RESIZE_MARGIN import org.catrobat.paintroid.tools.implementation.MAXIMUM_BORDER_RATIO import org.junit.After @@ -66,6 +67,9 @@ class ImportToolTest { @Mock private val displayMetrics: DisplayMetrics? = null + + @Mock + private lateinit var fragmentManager: FragmentManager private var drawingSurfaceWidth = 0 private var drawingSurfaceHeight = 0 private lateinit var tool: ImportTool @@ -91,7 +95,7 @@ class ImportToolTest { Mockito.`when`(workspace.scale).thenReturn(1f) Mockito.`when`(workspace.perspective).thenReturn(Perspective(20, 30)) tool = ImportTool(contextCallback, toolOptionsViewController, toolPaint, workspace, - idlingResource, commandManager, 0) + idlingResource, commandManager, 0, fragmentManager) } @After diff --git a/Paintroid/src/main/java/org/catrobat/paintroid/common/Constants.kt b/Paintroid/src/main/java/org/catrobat/paintroid/common/Constants.kt index 19b121b8b4..94ca178c34 100644 --- a/Paintroid/src/main/java/org/catrobat/paintroid/common/Constants.kt +++ b/Paintroid/src/main/java/org/catrobat/paintroid/common/Constants.kt @@ -49,6 +49,7 @@ const val ZOOM_WINDOW_ZOOM_PERCENTAGE_SHARED_PREFERENCES_TAG = "zoomwindowzoompe const val IMAGE_NUMBER_SHARED_PREFERENCES_TAG = "imagenumbertag" const val SCALE_IMAGE_FRAGMENT_TAG = "showscaleimagedialog" const val INDETERMINATE_PROGRESS_DIALOG_TAG = "indeterminateprogressdialogfragment" +const val ENLARGE_CANVAS_DIALOG_TAG = "enlargecanvasdialogfragment" const val INVALID_RESOURCE_ID = 0 const val MAX_LAYERS = 100 const val MEGABYTE_IN_BYTE = 1_048_576L diff --git a/Paintroid/src/main/java/org/catrobat/paintroid/contract/MainActivityContracts.kt b/Paintroid/src/main/java/org/catrobat/paintroid/contract/MainActivityContracts.kt index 84fef55bab..251446ab53 100644 --- a/Paintroid/src/main/java/org/catrobat/paintroid/contract/MainActivityContracts.kt +++ b/Paintroid/src/main/java/org/catrobat/paintroid/contract/MainActivityContracts.kt @@ -308,6 +308,12 @@ interface MainActivityContracts { fun checkForTemporaryFile(): Boolean fun setColorHistoryAfterLoadImage(colorHistory: ColorHistory?) + + fun truncateImportImage() + + fun enlargeCanvasImportImage(): Boolean + + fun checkSwitchingTool() } interface Model { diff --git a/Paintroid/src/main/java/org/catrobat/paintroid/dialog/ImportImageCanvasTooLargeDialog.kt b/Paintroid/src/main/java/org/catrobat/paintroid/dialog/ImportImageCanvasTooLargeDialog.kt new file mode 100644 index 0000000000..83682765a7 --- /dev/null +++ b/Paintroid/src/main/java/org/catrobat/paintroid/dialog/ImportImageCanvasTooLargeDialog.kt @@ -0,0 +1,38 @@ +/* + * Paintroid: An image manipulation application for Android. + * Copyright (C) 2010-2024 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. + * + * 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.paintroid.dialog + +import android.app.Dialog +import android.os.Bundle +import androidx.appcompat.app.AlertDialog +import org.catrobat.paintroid.R + +class ImportImageCanvasTooLargeDialog : MainActivityDialogFragment() { + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + return AlertDialog.Builder(requireContext(), R.style.PocketPaintAlertDialog) + .setTitle(R.string.dialog_import_image_title) + .setMessage(R.string.dialog_import_image_canvas_too_large) + .setNegativeButton(R.string.pocketpaint_cancel) { _, _ -> dismiss() } + .setPositiveButton(R.string.pocketpaint_truncate) { _, _ -> presenter.truncateImportImage() + presenter.checkSwitchingTool() + dismiss() } + .create() + } +} diff --git a/Paintroid/src/main/java/org/catrobat/paintroid/dialog/ImportImageEnlargeCanvasDialog.kt b/Paintroid/src/main/java/org/catrobat/paintroid/dialog/ImportImageEnlargeCanvasDialog.kt new file mode 100644 index 0000000000..fa90f6fe8b --- /dev/null +++ b/Paintroid/src/main/java/org/catrobat/paintroid/dialog/ImportImageEnlargeCanvasDialog.kt @@ -0,0 +1,42 @@ +/* + * Paintroid: An image manipulation application for Android. + * Copyright (C) 2010-2024 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. + * + * 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.paintroid.dialog + +import android.app.Dialog +import android.os.Bundle +import androidx.appcompat.app.AlertDialog +import org.catrobat.paintroid.R + +class ImportImageEnlargeCanvasDialog : MainActivityDialogFragment() { + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + return AlertDialog.Builder(requireContext(), R.style.PocketPaintAlertDialog) + .setTitle(R.string.dialog_import_image_title) + .setMessage(R.string.dialog_import_image_enlarge_image) + .setNeutralButton(R.string.pocketpaint_cancel) { _, _ -> dismiss() } + .setNegativeButton(R.string.pocketpaint_truncate) { _, _ -> presenter.truncateImportImage() + presenter.checkSwitchingTool() + dismiss() } + .setPositiveButton(R.string.pocketpaint_enlarge) { _, _ -> if (presenter.enlargeCanvasImportImage()) { + presenter.checkSwitchingTool() + } + dismiss() } + .create() + } +} diff --git a/Paintroid/src/main/java/org/catrobat/paintroid/presenter/MainActivityPresenter.kt b/Paintroid/src/main/java/org/catrobat/paintroid/presenter/MainActivityPresenter.kt index 3a4e453d29..ff10a85f06 100644 --- a/Paintroid/src/main/java/org/catrobat/paintroid/presenter/MainActivityPresenter.kt +++ b/Paintroid/src/main/java/org/catrobat/paintroid/presenter/MainActivityPresenter.kt @@ -99,6 +99,7 @@ import org.catrobat.paintroid.tools.implementation.ClippingTool import org.catrobat.paintroid.tools.implementation.LineTool import org.catrobat.paintroid.tools.implementation.DefaultToolPaint import org.catrobat.paintroid.tools.implementation.EraserTool +import org.catrobat.paintroid.tools.implementation.ImportTool import org.catrobat.paintroid.ui.LayerAdapter import java.io.File @@ -128,6 +129,7 @@ open class MainActivityPresenter( private var resetPerspectiveAfterNextCommand = false private var isExport = false private var wasImageLoaded = false + private var toolTypeToSwitch: ToolType? = null private val isImageUnchanged: Boolean get() = !commandManager.isUndoAvailable @@ -805,23 +807,29 @@ open class MainActivityPresenter( override fun toolClicked(toolType: ToolType) { idlingResource.increment() + toolTypeToSwitch = toolType bottomBarViewHolder.hide() if (toolController.toolType === toolType && toolController.hasToolOptionsView()) { toolController.toggleToolOptionsView() } else { - checkForImplicitToolApplication() - switchTool(toolType) + if (checkForImplicitToolApplication()) { + switchTool(toolType) + } } idlingResource.decrement() } - private fun checkForImplicitToolApplication() { + private fun checkForImplicitToolApplication(): Boolean { val currentTool = toolController.currentTool val currentToolType = currentTool?.toolType + if (currentToolType == ToolType.IMPORTPNG) { + return (currentTool as ImportTool).onClickOnButtonImplicit() + } if (toolController.toolList.contains(currentToolType)) { val toolToApply = currentTool as BaseToolWithShape toolToApply.onClickOnButton() } else if (currentToolType == ToolType.CLIP) (currentTool as ClippingTool).onClickOnButton() + return true } private fun switchTool(type: ToolType) { @@ -847,6 +855,7 @@ open class MainActivityPresenter( } } }.start() + toolTypeToSwitch = null } private fun setTool(toolType: ToolType) { @@ -1138,6 +1147,38 @@ open class MainActivityPresenter( setBottomNavigationColor(newPaintColor) } + override fun truncateImportImage() { + if (toolController.currentTool is ImportTool) { + val importTool = toolController.currentTool as ImportTool + importTool.addImageToCanvas() + } else { + Log.e( + MainActivity.TAG, + "truncateImportImage: Current tool is no ImportTool as required" + ) + } + } + + override fun enlargeCanvasImportImage(): Boolean { + var switchTool = false + if (toolController.currentTool is ImportTool) { + val importTool = toolController.currentTool as ImportTool + switchTool = importTool.enlargeCanvas() + } else { + Log.e( + MainActivity.TAG, + "enlargeCanvasImportImage: Current tool is no ImportTool as required" + ) + } + return switchTool + } + + override fun checkSwitchingTool() { + toolTypeToSwitch?.let { + switchTool(it) + } + } + fun checkIfClippingToolNeedsAdjustment() { if (toolController.currentTool is ClippingTool) { val clippingTool = toolController.currentTool as ClippingTool diff --git a/Paintroid/src/main/java/org/catrobat/paintroid/tools/implementation/BaseToolWithRectangleShape.kt b/Paintroid/src/main/java/org/catrobat/paintroid/tools/implementation/BaseToolWithRectangleShape.kt index 031516eb9c..199160c9e4 100644 --- a/Paintroid/src/main/java/org/catrobat/paintroid/tools/implementation/BaseToolWithRectangleShape.kt +++ b/Paintroid/src/main/java/org/catrobat/paintroid/tools/implementation/BaseToolWithRectangleShape.kt @@ -59,6 +59,9 @@ const val MAXIMUM_BORDER_RATIO = 2f @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) const val MINIMAL_BOX_SIZE = 3 +@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) +const val MAXIMUM_BITMAP_SIZE_FACTOR = 4f + const val DEFAULT_BOX_RESIZE_MARGIN = 20 const val DEFAULT_ANTIALIASING_ON = true diff --git a/Paintroid/src/main/java/org/catrobat/paintroid/tools/implementation/DefaultToolFactory.kt b/Paintroid/src/main/java/org/catrobat/paintroid/tools/implementation/DefaultToolFactory.kt index ec89b04ace..4b8ddc15b4 100644 --- a/Paintroid/src/main/java/org/catrobat/paintroid/tools/implementation/DefaultToolFactory.kt +++ b/Paintroid/src/main/java/org/catrobat/paintroid/tools/implementation/DefaultToolFactory.kt @@ -82,7 +82,8 @@ class DefaultToolFactory(mainActivity: MainActivity) : ToolFactory { workspace, idlingResource, commandManager, - DRAW_TIME_INIT + DRAW_TIME_INIT, + mainActivity.supportFragmentManager ) ToolType.PIPETTE -> PipetteTool( contextCallback, diff --git a/Paintroid/src/main/java/org/catrobat/paintroid/tools/implementation/ImportTool.kt b/Paintroid/src/main/java/org/catrobat/paintroid/tools/implementation/ImportTool.kt index 22af3cfe81..835cd3f592 100644 --- a/Paintroid/src/main/java/org/catrobat/paintroid/tools/implementation/ImportTool.kt +++ b/Paintroid/src/main/java/org/catrobat/paintroid/tools/implementation/ImportTool.kt @@ -22,13 +22,19 @@ import android.graphics.Bitmap import android.graphics.Canvas import android.graphics.PointF import android.os.Bundle +import androidx.fragment.app.FragmentManager import androidx.test.espresso.idling.CountingIdlingResource import org.catrobat.paintroid.command.CommandManager +import org.catrobat.paintroid.common.ENLARGE_CANVAS_DIALOG_TAG +import org.catrobat.paintroid.dialog.ImportImageCanvasTooLargeDialog +import org.catrobat.paintroid.dialog.ImportImageEnlargeCanvasDialog import org.catrobat.paintroid.tools.ContextCallback import org.catrobat.paintroid.tools.ToolPaint import org.catrobat.paintroid.tools.ToolType import org.catrobat.paintroid.tools.Workspace import org.catrobat.paintroid.tools.options.ToolOptionsViewController +import kotlin.math.ceil +import kotlin.math.floor import kotlin.math.max import kotlin.math.min @@ -41,7 +47,8 @@ class ImportTool( workspace: Workspace, idlingResource: CountingIdlingResource, commandManager: CommandManager, - override var drawTime: Long + override var drawTime: Long, + private val fragmentManager: FragmentManager ) : BaseToolWithRectangleShape( contextCallback, toolOptionsViewController, toolPaint, workspace, idlingResource, commandManager ) { @@ -61,6 +68,8 @@ class ImportTool( init { rotationEnabled = true + maximumBoxResolution = + metrics.widthPixels * metrics.heightPixels * MAXIMUM_BITMAP_SIZE_FACTOR } override fun drawShape(canvas: Canvas) { @@ -81,6 +90,40 @@ class ImportTool( } override fun onClickOnButton() { + if (isImageInsideBitmap()) { + addImageToCanvas() + } else { + val enlargeCanvasDialog = ImportImageEnlargeCanvasDialog() + enlargeCanvasDialog.show( + fragmentManager, + ENLARGE_CANVAS_DIALOG_TAG + ) + } + } + + fun onClickOnButtonImplicit(): Boolean { + if (isImageInsideBitmap()) { + addImageToCanvas() + return true + } + val enlargeCanvasDialog = ImportImageEnlargeCanvasDialog() + enlargeCanvasDialog.show( + fragmentManager, + ENLARGE_CANVAS_DIALOG_TAG + ) + return false + } + + fun setBitmapFromSource(bitmap: Bitmap) { + super.setBitmap(bitmap) + val maximumBorderRatioWidth = MAXIMUM_BORDER_RATIO * workspace.width + val maximumBorderRatioHeight = MAXIMUM_BORDER_RATIO * workspace.height + val minimumSize = DEFAULT_BOX_RESIZE_MARGIN.toFloat() + boxWidth = max(minimumSize, min(maximumBorderRatioWidth, bitmap.width.toFloat())) + boxHeight = max(minimumSize, min(maximumBorderRatioHeight, bitmap.height.toFloat())) + } + + fun addImageToCanvas() { drawingBitmap?.let { highlightBox() val command = commandFactory.createClipboardCommand( @@ -94,12 +137,64 @@ class ImportTool( } } - fun setBitmapFromSource(bitmap: Bitmap) { - super.setBitmap(bitmap) - val maximumBorderRatioWidth = MAXIMUM_BORDER_RATIO * workspace.width - val maximumBorderRatioHeight = MAXIMUM_BORDER_RATIO * workspace.height - val minimumSize = DEFAULT_BOX_RESIZE_MARGIN.toFloat() - boxWidth = max(minimumSize, min(maximumBorderRatioWidth, bitmap.width.toFloat())) - boxHeight = max(minimumSize, min(maximumBorderRatioHeight, bitmap.height.toFloat())) + fun enlargeCanvas(): Boolean { + val resizeCoordinateXLeft = min(0, floor(toolPosition.x - boxWidth / 2f).toInt()) + val resizeCoordinateYTop = min(0, floor(toolPosition.y - boxHeight / 2f).toInt()) + val resizeCoordinateXRight = max(workspace.width, ceil(toolPosition.x + boxWidth / 2f - 1f).toInt()) + val resizeCoordinateYBottom = max(workspace.height, ceil(toolPosition.y + boxHeight / 2f - 1f).toInt()) + val enlargeValid = areResizeBordersValid(resizeCoordinateXLeft, resizeCoordinateYTop, resizeCoordinateXRight, resizeCoordinateYBottom) + if (enlargeValid) { + val resizeCommand = commandFactory.createCropCommand( + resizeCoordinateXLeft, + resizeCoordinateYTop, + resizeCoordinateXRight, + resizeCoordinateYBottom, + maximumBoxResolution.toInt() + ) + commandManager.addCommand(resizeCommand) + if (resizeCoordinateXLeft < 0) { + toolPosition.x = boxWidth / 2f + } + if (resizeCoordinateYTop < 0) { + toolPosition.y = boxHeight / 2f + } + addImageToCanvas() + } else { + val canvasTooLargeDialog = ImportImageCanvasTooLargeDialog() + canvasTooLargeDialog.show( + fragmentManager, + ENLARGE_CANVAS_DIALOG_TAG + ) + } + return enlargeValid + } + + private fun areResizeBordersValid(resizeBoundWidthXLeft: Int, resizeBoundHeightYTop: Int, resizeBoundWidthXRight: Int, resizeBoundHeightYBottom: Int): Boolean { + if (resizeBoundWidthXRight < resizeBoundWidthXLeft || + resizeBoundHeightYTop > resizeBoundHeightYBottom + ) { + return false + } + if (resizeBoundWidthXLeft >= workspace.width || min( + resizeBoundWidthXRight, resizeBoundHeightYBottom + ) < 0 || resizeBoundHeightYTop >= workspace.height + ) { + return false + } + if (resizeBoundWidthXLeft == 0 && resizeBoundHeightYTop == 0 && resizeBoundWidthXRight == workspace.width - 1 && resizeBoundHeightYBottom == workspace.height - 1) { + return false + } + val width = resizeBoundWidthXRight - resizeBoundWidthXLeft + 1 + val height = resizeBoundHeightYBottom - resizeBoundHeightYTop + 1 + return width * height <= maximumBoxResolution + } + + private fun isImageInsideBitmap(): Boolean { + val canvasHeight = workspace.height.toFloat() + val canvasWidth = workspace.width.toFloat() + val imageTopLeft = PointF(toolPosition.x - boxWidth / 2, toolPosition.y - boxHeight / 2) + val imageBottomRight = PointF(toolPosition.x + boxWidth / 2, toolPosition.y + boxHeight / 2) + + return imageTopLeft.x >= 0 && imageTopLeft.y >= 0 && imageBottomRight.x <= canvasWidth && imageBottomRight.y <= canvasHeight } } diff --git a/Paintroid/src/main/java/org/catrobat/paintroid/tools/implementation/TransformTool.kt b/Paintroid/src/main/java/org/catrobat/paintroid/tools/implementation/TransformTool.kt index 3b97b9ef64..58f44a8c2a 100644 --- a/Paintroid/src/main/java/org/catrobat/paintroid/tools/implementation/TransformTool.kt +++ b/Paintroid/src/main/java/org/catrobat/paintroid/tools/implementation/TransformTool.kt @@ -49,8 +49,6 @@ import kotlin.math.floor import kotlin.math.max import kotlin.math.min -@VisibleForTesting -const val MAXIMUM_BITMAP_SIZE_FACTOR = 4.0f private const val START_ZOOM_FACTOR = 0.95f private const val SIDES = 4 private const val CONSTANT_1 = 10 diff --git a/Paintroid/src/main/res/values/string.xml b/Paintroid/src/main/res/values/string.xml index de2ea72fb6..a7bcce1120 100644 --- a/Paintroid/src/main/res/values/string.xml +++ b/Paintroid/src/main/res/values/string.xml @@ -231,6 +231,8 @@ Cancel Not now Rate Pocket Paint + Truncate + Enlarge Tools Current @@ -250,4 +252,7 @@ Used to display a zoomed in part of the drawing surface + The imported image will be truncated. Do you want instead to enlarge the canvas so that the whole image is imported? + + Enlarging the canvas to fit the image would exceed the maximum canvas size. Do you want instead to truncate the image?