From 520c87e325c86973e13138edc97a3bc63a4bfc5f Mon Sep 17 00:00:00 2001 From: Michael Tiefnig Date: Mon, 20 Jun 2022 10:22:25 +0200 Subject: [PATCH] PAINTROID-432 Implemented new "clipping" tool where user can mark one or several areas and then apply the tool which erases everything except the marked areas. --- .../test/espresso/LayerIntegrationTest.kt | 1 + .../tools/ClippingToolIntegrationTest.kt | 319 ++++++++++++++++++ .../org/catrobat/paintroid/MainActivity.kt | 13 +- .../paintroid/command/CommandFactory.kt | 13 +- .../paintroid/command/CommandManager.kt | 10 + .../implementation/AsyncCommandManager.kt | 30 ++ .../command/implementation/ClippingCommand.kt | 36 ++ .../implementation/DefaultCommandFactory.kt | 20 +- .../implementation/DefaultCommandManager.kt | 41 ++- .../ClippingCommandSerializer.kt | 27 ++ .../CommandSerializationUtilities.kt | 2 + .../controller/DefaultToolController.kt | 28 +- .../paintroid/presenter/LayerPresenter.kt | 27 ++ .../presenter/MainActivityPresenter.kt | 84 ++++- .../org/catrobat/paintroid/tools/ToolType.kt | 9 + .../org/catrobat/paintroid/tools/Workspace.kt | 2 +- .../tools/implementation/BaseTool.kt | 8 +- .../tools/implementation/BaseToolWithShape.kt | 1 + .../tools/implementation/BrushTool.kt | 5 +- .../tools/implementation/ClippingTool.kt | 187 ++++++++++ .../implementation/DefaultToolFactory.kt | 15 +- .../tools/implementation/DefaultToolPaint.kt | 1 + .../tools/implementation/DefaultWorkspace.kt | 2 +- .../tools/options/BrushToolOptionsView.kt | 2 + .../paintroid/ui/MainActivityNavigator.kt | 10 +- .../ui/tools/DefaultBrushToolOptionsView.kt | 27 +- .../ui/tools/DefaultSmudgeToolOptionsView.kt | 4 + .../drawable/ic_pocketpaint_tool_clipping.xml | 8 + .../pocketpaint_layout_bottom_bar.xml | 13 + .../layout/pocketpaint_layout_bottom_bar.xml | 13 + .../pocketpaint_layout_help_bottom_bar.xml | 13 + Paintroid/src/main/res/values/string.xml | 2 + 32 files changed, 945 insertions(+), 28 deletions(-) create mode 100644 Paintroid/src/androidTest/java/org/catrobat/paintroid/test/espresso/tools/ClippingToolIntegrationTest.kt create mode 100644 Paintroid/src/main/java/org/catrobat/paintroid/command/implementation/ClippingCommand.kt create mode 100644 Paintroid/src/main/java/org/catrobat/paintroid/command/serialization/ClippingCommandSerializer.kt create mode 100644 Paintroid/src/main/java/org/catrobat/paintroid/tools/implementation/ClippingTool.kt create mode 100644 Paintroid/src/main/res/drawable/ic_pocketpaint_tool_clipping.xml diff --git a/Paintroid/src/androidTest/java/org/catrobat/paintroid/test/espresso/LayerIntegrationTest.kt b/Paintroid/src/androidTest/java/org/catrobat/paintroid/test/espresso/LayerIntegrationTest.kt index 8a06ba65ac..bfdafb7bd8 100644 --- a/Paintroid/src/androidTest/java/org/catrobat/paintroid/test/espresso/LayerIntegrationTest.kt +++ b/Paintroid/src/androidTest/java/org/catrobat/paintroid/test/espresso/LayerIntegrationTest.kt @@ -740,6 +740,7 @@ class LayerIntegrationTest { TopBarViewInteraction.onTopBarView() .performOpenMoreOptions() onView(withText(R.string.menu_load_image)).perform(click()) + onView(withText(R.string.menu_replace_image)).perform(click()) Intents.release() onView(withText(R.string.dialog_warning_new_image)).check(ViewAssertions.doesNotExist()) onView(withText(R.string.pocketpaint_ok)).perform(click()) diff --git a/Paintroid/src/androidTest/java/org/catrobat/paintroid/test/espresso/tools/ClippingToolIntegrationTest.kt b/Paintroid/src/androidTest/java/org/catrobat/paintroid/test/espresso/tools/ClippingToolIntegrationTest.kt new file mode 100644 index 0000000000..5e001b5eee --- /dev/null +++ b/Paintroid/src/androidTest/java/org/catrobat/paintroid/test/espresso/tools/ClippingToolIntegrationTest.kt @@ -0,0 +1,319 @@ +package org.catrobat.paintroid.test.espresso.tools + +import android.graphics.Color +import android.graphics.PointF +import androidx.test.espresso.IdlingRegistry +import androidx.test.espresso.idling.CountingIdlingResource +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.rule.ActivityTestRule +import org.catrobat.paintroid.MainActivity +import org.catrobat.paintroid.test.espresso.util.BitmapLocationProvider +import org.catrobat.paintroid.test.espresso.util.DrawingSurfaceLocationProvider +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.LayerMenuViewInteraction +import org.catrobat.paintroid.test.espresso.util.wrappers.ToolBarViewInteraction +import org.catrobat.paintroid.test.espresso.util.wrappers.ToolPropertiesInteraction.onToolProperties +import org.catrobat.paintroid.test.espresso.util.wrappers.TopBarViewInteraction +import org.catrobat.paintroid.test.utils.ScreenshotOnFailRule +import org.catrobat.paintroid.tools.ToolReference +import org.catrobat.paintroid.tools.ToolType +import org.catrobat.paintroid.tools.Workspace +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class ClippingToolIntegrationTest { + @get:Rule + var launchActivityRule: ActivityTestRule = ActivityTestRule( + MainActivity::class.java + ) + + @get:Rule + var screenshotOnFailRule = ScreenshotOnFailRule() + + private lateinit var idlingResource: CountingIdlingResource + private lateinit var toolReference: ToolReference + private lateinit var mainActivity: MainActivity + private lateinit var workspace: Workspace + private lateinit var middle: PointF + private lateinit var middleLeft: PointF + private lateinit var middleTop: PointF + private lateinit var middleBot: PointF + private lateinit var middleRight: PointF + + private lateinit var middlePoint1: PointF + private lateinit var middlePoint2: PointF + private lateinit var middlePoint3: PointF + private lateinit var middlePoint4: PointF + + @Before + fun setUp() { + mainActivity = launchActivityRule.activity + idlingResource = mainActivity.idlingResource + IdlingRegistry.getInstance().register(idlingResource) + workspace = mainActivity.workspace + toolReference = mainActivity.toolReference + middle = PointF((workspace.width / 2).toFloat(), (workspace.height / 2).toFloat()) + middleLeft = PointF(middle.x - workspace.width / 4, middle.y) + middleTop = PointF(middle.x, middle.y - workspace.height / 4) + middleBot = PointF(middle.x, middle.y + workspace.height / 4) + middleRight = PointF(middle.x + workspace.width / 4, middle.y) + + middlePoint1 = PointF(middle.x - workspace.width / 4, middle.y - workspace.height / 4) + middlePoint2 = PointF(middle.x + workspace.width / 4, middle.y - workspace.height / 4) + middlePoint3 = PointF(middle.x + workspace.width / 4, middle.y + workspace.height / 4) + middlePoint4 = PointF(middle.x - workspace.width / 4, middle.y + workspace.height / 4) + + ToolBarViewInteraction.onToolBarView() + .performSelectTool(ToolType.FILL) + } + + @After + fun tearDown() { + IdlingRegistry.getInstance().unregister(idlingResource) + } + + @Test + fun testClipOnBlackBitmap() { + onToolProperties().setColor(Color.BLACK) + + DrawingSurfaceInteraction.onDrawingSurfaceView() + .checkPixelColor(Color.TRANSPARENT, BitmapLocationProvider.MIDDLE) + + DrawingSurfaceInteraction.onDrawingSurfaceView() + .perform(UiInteractions.touchAt(DrawingSurfaceLocationProvider.MIDDLE)) + + DrawingSurfaceInteraction.onDrawingSurfaceView() + .checkPixelColor(Color.BLACK, BitmapLocationProvider.MIDDLE) + + ToolBarViewInteraction.onToolBarView() + .performSelectTool(ToolType.CLIP) + + onToolProperties().setColor(Color.YELLOW) + + toolReference.tool?.handleDown(middleLeft) + toolReference.tool?.handleMove(middlePoint1) + toolReference.tool?.handleMove(middleTop) + toolReference.tool?.handleMove(middlePoint2) + toolReference.tool?.handleMove(middleRight) + toolReference.tool?.handleMove(middlePoint3) + toolReference.tool?.handleMove(middleBot) + toolReference.tool?.handleMove(middlePoint4) + toolReference.tool?.handleUp(middleLeft) + + TopBarViewInteraction.onTopBarView() + .performClickCheckmark() + + val inAreaX = middle.x - 10 + val inAreaY = middle.y - 10 + + val outOfAreaX = workspace.width - 10 + val outOfAreaY = workspace.height - 10 + + val colorInArea = workspace.bitmapOfCurrentLayer?.getPixel(inAreaX.toInt(), inAreaY.toInt()) + val colorOutOfArea = workspace.bitmapOfCurrentLayer?.getPixel(outOfAreaX, outOfAreaY) + + assertEquals(colorInArea, Color.BLACK) + assertEquals(colorOutOfArea, Color.TRANSPARENT) + } + + @Test + fun testClipOnBlackBitmapOnlyAppliedOnCurrentLayer() { + onToolProperties().setColor(Color.BLACK) + + DrawingSurfaceInteraction.onDrawingSurfaceView() + .checkPixelColor(Color.TRANSPARENT, BitmapLocationProvider.MIDDLE) + + DrawingSurfaceInteraction.onDrawingSurfaceView() + .perform(UiInteractions.touchAt(DrawingSurfaceLocationProvider.MIDDLE)) + + DrawingSurfaceInteraction.onDrawingSurfaceView() + .checkPixelColor(Color.BLACK, BitmapLocationProvider.MIDDLE) + + onToolProperties().setColor(Color.YELLOW) + + LayerMenuViewInteraction.onLayerMenuView() + .performOpen() + .performAddLayer() + .performSelectLayer(0) + .performClose() + + DrawingSurfaceInteraction.onDrawingSurfaceView() + .checkPixelColor(Color.TRANSPARENT, BitmapLocationProvider.MIDDLE) + + DrawingSurfaceInteraction.onDrawingSurfaceView() + .perform(UiInteractions.touchAt(DrawingSurfaceLocationProvider.MIDDLE)) + + DrawingSurfaceInteraction.onDrawingSurfaceView() + .checkPixelColor(Color.YELLOW, BitmapLocationProvider.MIDDLE) + + ToolBarViewInteraction.onToolBarView() + .performSelectTool(ToolType.CLIP) + + toolReference.tool?.handleDown(middleLeft) + toolReference.tool?.handleMove(middlePoint1) + toolReference.tool?.handleMove(middleTop) + toolReference.tool?.handleMove(middlePoint2) + toolReference.tool?.handleMove(middleRight) + toolReference.tool?.handleMove(middlePoint3) + toolReference.tool?.handleMove(middleBot) + toolReference.tool?.handleMove(middlePoint4) + toolReference.tool?.handleUp(middleLeft) + + TopBarViewInteraction.onTopBarView() + .performClickCheckmark() + + val inAreaX = middle.x - 10 + val inAreaY = middle.y - 10 + + val outOfAreaX = workspace.width - 10 + val outOfAreaY = workspace.height - 10 + + val colorInAreaCurrentLayer = workspace.bitmapOfCurrentLayer?.getPixel(inAreaX.toInt(), inAreaY.toInt()) + val colorOutOfAreaCurrentLayer = workspace.bitmapOfCurrentLayer?.getPixel(outOfAreaX, outOfAreaY) + + val colorInAreaSecondLayer = workspace.bitmapLisOfAllLayers[1]?.getPixel(inAreaX.toInt(), inAreaY.toInt()) + val colorOutOfAreaSecondLayer = workspace.bitmapLisOfAllLayers[1]?.getPixel(outOfAreaX, outOfAreaY) + + assertEquals(colorInAreaCurrentLayer, Color.YELLOW) + assertEquals(colorOutOfAreaCurrentLayer, Color.TRANSPARENT) + assertEquals(colorInAreaSecondLayer, Color.BLACK) + assertEquals(colorOutOfAreaSecondLayer, Color.BLACK) + } + + @Test + fun testIfPathGetsDrawnWhenUsingClippingTool() { + DrawingSurfaceInteraction.onDrawingSurfaceView() + .checkPixelColor(Color.TRANSPARENT, BitmapLocationProvider.MIDDLE) + + ToolBarViewInteraction.onToolBarView() + .performSelectTool(ToolType.CLIP) + + onToolProperties().setColor(Color.BLACK) + + onToolProperties().setStrokeWidth(20f) + + toolReference.tool?.handleDown(middleTop) + toolReference.tool?.handleMove(middleLeft) + toolReference.tool?.handleMove(middleRight) + toolReference.tool?.handleUp(middleTop) + + LayerMenuViewInteraction.onLayerMenuView() + .performOpen() + .performClose() + + val bitmapColor = workspace.bitmapOfCurrentLayer?.getPixel(middle.x.toInt(), middle.y.toInt()) + assertEquals(bitmapColor, Color.BLACK) + } + + @Test + fun testClipBlackBitmapAndThenUndo() { + onToolProperties().setColor(Color.BLACK) + + DrawingSurfaceInteraction.onDrawingSurfaceView() + .checkPixelColor(Color.TRANSPARENT, BitmapLocationProvider.MIDDLE) + + DrawingSurfaceInteraction.onDrawingSurfaceView() + .perform(UiInteractions.touchAt(DrawingSurfaceLocationProvider.MIDDLE)) + + DrawingSurfaceInteraction.onDrawingSurfaceView() + .checkPixelColor(Color.BLACK, BitmapLocationProvider.MIDDLE) + + ToolBarViewInteraction.onToolBarView() + .performSelectTool(ToolType.CLIP) + + onToolProperties().setColor(Color.YELLOW) + + toolReference.tool?.handleDown(middleLeft) + toolReference.tool?.handleMove(middlePoint1) + toolReference.tool?.handleMove(middleTop) + toolReference.tool?.handleMove(middlePoint2) + toolReference.tool?.handleMove(middleRight) + toolReference.tool?.handleMove(middlePoint3) + toolReference.tool?.handleMove(middleBot) + toolReference.tool?.handleMove(middlePoint4) + toolReference.tool?.handleUp(middleLeft) + + TopBarViewInteraction.onTopBarView() + .performClickCheckmark() + + val inAreaX = middle.x - 10 + val inAreaY = middle.y - 10 + + val outOfAreaX = workspace.width - 10 + val outOfAreaY = workspace.height - 10 + + val colorInArea = workspace.bitmapOfCurrentLayer?.getPixel(inAreaX.toInt(), inAreaY.toInt()) + val colorOutOfArea = workspace.bitmapOfCurrentLayer?.getPixel(outOfAreaX, outOfAreaY) + + assertEquals(colorInArea, Color.BLACK) + assertEquals(colorOutOfArea, Color.TRANSPARENT) + + TopBarViewInteraction.onTopBarView() + .performUndo() + + val colorOutOfAreaAfterUndo = workspace.bitmapOfCurrentLayer?.getPixel(outOfAreaX, outOfAreaY) + assertEquals(colorOutOfAreaAfterUndo, Color.BLACK) + } + + @Test + fun testClipBlackBitmapAndThenUndoAndRedo() { + onToolProperties().setColor(Color.BLACK) + + DrawingSurfaceInteraction.onDrawingSurfaceView() + .checkPixelColor(Color.TRANSPARENT, BitmapLocationProvider.MIDDLE) + + DrawingSurfaceInteraction.onDrawingSurfaceView() + .perform(UiInteractions.touchAt(DrawingSurfaceLocationProvider.MIDDLE)) + + DrawingSurfaceInteraction.onDrawingSurfaceView() + .checkPixelColor(Color.BLACK, BitmapLocationProvider.MIDDLE) + + ToolBarViewInteraction.onToolBarView() + .performSelectTool(ToolType.CLIP) + + onToolProperties().setColor(Color.YELLOW) + + toolReference.tool?.handleDown(middleLeft) + toolReference.tool?.handleMove(middlePoint1) + toolReference.tool?.handleMove(middleTop) + toolReference.tool?.handleMove(middlePoint2) + toolReference.tool?.handleMove(middleRight) + toolReference.tool?.handleMove(middlePoint3) + toolReference.tool?.handleMove(middleBot) + toolReference.tool?.handleMove(middlePoint4) + toolReference.tool?.handleUp(middleLeft) + + TopBarViewInteraction.onTopBarView() + .performClickCheckmark() + + val inAreaX = middle.x - 10 + val inAreaY = middle.y - 10 + + val outOfAreaX = workspace.width - 10 + val outOfAreaY = workspace.height - 10 + + val colorInArea = workspace.bitmapOfCurrentLayer?.getPixel(inAreaX.toInt(), inAreaY.toInt()) + val colorOutOfArea = workspace.bitmapOfCurrentLayer?.getPixel(outOfAreaX, outOfAreaY) + + assertEquals(colorInArea, Color.BLACK) + assertEquals(colorOutOfArea, Color.TRANSPARENT) + + TopBarViewInteraction.onTopBarView() + .performUndo() + + val colorOutOfAreaAfterUndo = workspace.bitmapOfCurrentLayer?.getPixel(outOfAreaX, outOfAreaY) + assertEquals(colorOutOfAreaAfterUndo, Color.BLACK) + + TopBarViewInteraction.onTopBarView() + .performRedo() + + val colorOutOfAreaAfterRedo = workspace.bitmapOfCurrentLayer?.getPixel(outOfAreaX, outOfAreaY) + assertEquals(colorOutOfAreaAfterRedo, Color.TRANSPARENT) + } +} diff --git a/Paintroid/src/main/java/org/catrobat/paintroid/MainActivity.kt b/Paintroid/src/main/java/org/catrobat/paintroid/MainActivity.kt index 7a54871980..90aeeb9195 100644 --- a/Paintroid/src/main/java/org/catrobat/paintroid/MainActivity.kt +++ b/Paintroid/src/main/java/org/catrobat/paintroid/MainActivity.kt @@ -76,6 +76,7 @@ import org.catrobat.paintroid.tools.ToolReference import org.catrobat.paintroid.tools.ToolType import org.catrobat.paintroid.tools.Workspace import org.catrobat.paintroid.tools.implementation.BaseToolWithShape +import org.catrobat.paintroid.tools.implementation.ClippingTool import org.catrobat.paintroid.tools.implementation.DefaultContextCallback import org.catrobat.paintroid.tools.implementation.DefaultToolFactory import org.catrobat.paintroid.tools.implementation.DefaultToolPaint @@ -447,7 +448,7 @@ class MainActivity : AppCompatActivity(), MainView, CommandListener { defaultToolController = DefaultToolController( toolReference, toolOptionsViewController, - DefaultToolFactory(), + DefaultToolFactory(this), commandManager, workspace, idlingResource, @@ -536,9 +537,15 @@ class MainActivity : AppCompatActivity(), MainView, CommandListener { topBar.checkmarkButton.setOnClickListener { if (toolReference.tool?.toolType?.name.equals(ToolType.TRANSFORM.name)) { (toolReference.tool as TransformTool).checkMarkClicked = true + val tool = toolReference.tool as BaseToolWithShape? + tool?.onClickOnButton() + } else if (toolReference.tool?.toolType?.name.equals(ToolType.CLIP.name)) { + val tool = toolReference.tool as ClippingTool? + tool?.onClickOnButton() + } else { + val tool = toolReference.tool as BaseToolWithShape? + tool?.onClickOnButton() } - val tool = toolReference.tool as BaseToolWithShape? - tool?.onClickOnButton() } topBar.plusButton.setOnClickListener { val tool = toolReference.tool as LineTool diff --git a/Paintroid/src/main/java/org/catrobat/paintroid/command/CommandFactory.kt b/Paintroid/src/main/java/org/catrobat/paintroid/command/CommandFactory.kt index 0b59aecbd0..485cf8f7f6 100644 --- a/Paintroid/src/main/java/org/catrobat/paintroid/command/CommandFactory.kt +++ b/Paintroid/src/main/java/org/catrobat/paintroid/command/CommandFactory.kt @@ -76,7 +76,13 @@ interface CommandFactory { fun createPathCommand(paint: Paint, path: SerializablePath): Command - fun createSmudgePathCommand(bitmap: Bitmap, pointPath: MutableList, maxPressure: Float, maxSize: Float, minSize: Float): Command + fun createSmudgePathCommand( + bitmap: Bitmap, + pointPath: MutableList, + maxPressure: Float, + maxSize: Float, + minSize: Float + ): Command fun createTextToolCommand( multilineText: Array, @@ -109,4 +115,9 @@ interface CommandFactory { ): Command fun createColorChangedCommand(toolReference: ToolReference, context: Context, color: Int): Command + + fun createClippingCommand( + bitmap: Bitmap, + pathBitmap: Bitmap + ): Command } diff --git a/Paintroid/src/main/java/org/catrobat/paintroid/command/CommandManager.kt b/Paintroid/src/main/java/org/catrobat/paintroid/command/CommandManager.kt index 818f1beaa0..963c3b8c70 100644 --- a/Paintroid/src/main/java/org/catrobat/paintroid/command/CommandManager.kt +++ b/Paintroid/src/main/java/org/catrobat/paintroid/command/CommandManager.kt @@ -32,6 +32,8 @@ interface CommandManager { fun addCommand(command: Command?) + fun addCommandWithoutUndo(command: Command?) + fun setInitialStateCommand(command: Command) fun loadCommandsCatrobatImage(model: CommandManagerModel?) @@ -54,6 +56,14 @@ interface CommandManager { fun getCommandManagerModelForCatrobatImage(): CommandManagerModel? + fun adjustUndoListForClippingTool() + + fun undoInClippingTool() + + fun popFirstCommandInUndo() + + fun popFirstCommandInRedo() + interface CommandListener { fun commandPostExecute() } diff --git a/Paintroid/src/main/java/org/catrobat/paintroid/command/implementation/AsyncCommandManager.kt b/Paintroid/src/main/java/org/catrobat/paintroid/command/implementation/AsyncCommandManager.kt index 2cde4aafe0..762cf17840 100644 --- a/Paintroid/src/main/java/org/catrobat/paintroid/command/implementation/AsyncCommandManager.kt +++ b/Paintroid/src/main/java/org/catrobat/paintroid/command/implementation/AsyncCommandManager.kt @@ -64,6 +64,20 @@ open class AsyncCommandManager( if (!shuttingDown) { synchronized(layerModel) { commandManager.addCommand(command) } } + withContext(Dispatchers.Main) { + commandManager.adjustUndoListForClippingTool() + notifyCommandPostExecute() + } + } + } + } + + override fun addCommandWithoutUndo(command: Command?) { + CoroutineScope(Dispatchers.Default).launch { + mutex.withLock { + if (!shuttingDown) { + synchronized(layerModel) { commandManager.addCommandWithoutUndo(command) } + } withContext(Dispatchers.Main) { notifyCommandPostExecute() } @@ -138,6 +152,22 @@ open class AsyncCommandManager( synchronized(layerModel) { commandManager.setInitialStateCommand(command) } } + override fun adjustUndoListForClippingTool() { + synchronized(layerModel) { commandManager.adjustUndoListForClippingTool() } + } + + override fun undoInClippingTool() { + synchronized(layerModel) { commandManager.undoInClippingTool() } + } + + override fun popFirstCommandInUndo() { + synchronized(layerModel) { commandManager.popFirstCommandInUndo() } + } + + override fun popFirstCommandInRedo() { + synchronized(layerModel) { commandManager.popFirstCommandInRedo() } + } + private fun manageUndoAndRedo(callFunction: () -> Unit, condition: Boolean) { CoroutineScope(Dispatchers.Default).launch { mutex.withLock { diff --git a/Paintroid/src/main/java/org/catrobat/paintroid/command/implementation/ClippingCommand.kt b/Paintroid/src/main/java/org/catrobat/paintroid/command/implementation/ClippingCommand.kt new file mode 100644 index 0000000000..2ebf7f70e8 --- /dev/null +++ b/Paintroid/src/main/java/org/catrobat/paintroid/command/implementation/ClippingCommand.kt @@ -0,0 +1,36 @@ +package org.catrobat.paintroid.command.implementation + +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.graphics.PorterDuff +import android.graphics.PorterDuffXfermode +import android.graphics.Rect +import org.catrobat.paintroid.command.Command +import org.catrobat.paintroid.contract.LayerContracts + +class ClippingCommand(bitmap: Bitmap, pathBitmap: Bitmap) : Command { + + var bitmap: Bitmap? = bitmap.copy(bitmap.config, true); private set + var pathBitmap: Bitmap? = pathBitmap.copy(pathBitmap.config, true); private set + + override fun run(canvas: Canvas, layerModel: LayerContracts.Model) { + val bitmapToDraw = bitmap + bitmapToDraw ?: return + val wholeRect = Rect(0, 0, bitmapToDraw.width, bitmapToDraw.height) + layerModel.currentLayer?.bitmap?.eraseColor(Color.TRANSPARENT) + val paint = Paint() + with(canvas) { + save() + pathBitmap?.let { drawBitmap(it, null, wholeRect, null) } + paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_IN) + bitmap?.let { drawBitmap(it, null, wholeRect, paint) } + restore() + } + } + override fun freeResources() { + bitmap?.recycle() + pathBitmap?.recycle() + } +} diff --git a/Paintroid/src/main/java/org/catrobat/paintroid/command/implementation/DefaultCommandFactory.kt b/Paintroid/src/main/java/org/catrobat/paintroid/command/implementation/DefaultCommandFactory.kt index d00ef70f03..dcaab26a13 100644 --- a/Paintroid/src/main/java/org/catrobat/paintroid/command/implementation/DefaultCommandFactory.kt +++ b/Paintroid/src/main/java/org/catrobat/paintroid/command/implementation/DefaultCommandFactory.kt @@ -128,7 +128,13 @@ class DefaultCommandFactory : CommandFactory { commonFactory.createSerializablePath(path) ) - override fun createSmudgePathCommand(bitmap: Bitmap, pointPath: MutableList, maxPressure: Float, maxSize: Float, minSize: Float): Command { + override fun createSmudgePathCommand( + bitmap: Bitmap, + pointPath: MutableList, + maxPressure: Float, + maxSize: Float, + minSize: Float + ): Command { val copy = mutableListOf() pointPath.forEach { @@ -170,6 +176,12 @@ class DefaultCommandFactory : CommandFactory { boxRotation ) + override fun createClippingCommand(bitmap: Bitmap, pathBitmap: Bitmap): Command = + ClippingCommand( + bitmap, + pathBitmap + ) + override fun createSprayCommand(sprayedPoints: FloatArray, paint: Paint): Command = SprayCommand(sprayedPoints, paint) @@ -180,6 +192,10 @@ class DefaultCommandFactory : CommandFactory { boxRotation: Float ): Command = CutCommand(toPoint(toolPosition), boxWidth, boxHeight, boxRotation) - override fun createColorChangedCommand(toolReference: ToolReference, context: Context, color: Int): Command = + override fun createColorChangedCommand( + toolReference: ToolReference, + context: Context, + color: Int + ): Command = ColorChangedCommand(toolReference, context, color) } diff --git a/Paintroid/src/main/java/org/catrobat/paintroid/command/implementation/DefaultCommandManager.kt b/Paintroid/src/main/java/org/catrobat/paintroid/command/implementation/DefaultCommandManager.kt index ceef7aea82..5fbe13327b 100644 --- a/Paintroid/src/main/java/org/catrobat/paintroid/command/implementation/DefaultCommandManager.kt +++ b/Paintroid/src/main/java/org/catrobat/paintroid/command/implementation/DefaultCommandManager.kt @@ -30,6 +30,8 @@ import java.util.Collections import java.util.Deque import kotlin.collections.ArrayList +const val FIVE = 5 + class DefaultCommandManager( private val commonFactory: CommonFactory, private val layerModel: LayerContracts.Model @@ -78,6 +80,12 @@ class DefaultCommandManager( notifyCommandExecuted() } + override fun addCommandWithoutUndo(command: Command?) { + redoCommandList.clear() + executeCommand(command) + notifyCommandExecuted() + } + private fun executeCommand(command: Command?) { val currentLayer = layerModel.currentLayer val canvas = commonFactory.createCanvas() @@ -196,7 +204,6 @@ class DefaultCommandManager( undoCommandList.clear() redoCommandList.clear() layerModel.reset() - if (initialStateCommand != null) { val canvas = commonFactory.createCanvas() initialStateCommand?.run(canvas, layerModel) @@ -314,10 +321,40 @@ class DefaultCommandManager( override fun getCommandManagerModelForCatrobatImage(): CommandManagerModel? { var adaptedModel: CommandManagerModel? = null - commandManagerModel?.let { it1 -> adaptedModel = CommandManagerModel(it1.initialCommand, it1.commands.filter { it !is ColorChangedCommand }.toMutableList()) } + commandManagerModel?.let { it1 -> + adaptedModel = CommandManagerModel( + it1.initialCommand, + it1.commands.filter { + it !is ColorChangedCommand + }.toMutableList() + ) + } return adaptedModel } + override fun adjustUndoListForClippingTool() { + val commandName = undoCommandList.first.toString().split(".", "@")[FIVE] + if (commandName == ClippingCommand::class.java.simpleName) { + val clippingCommand = undoCommandList.pop() + undoCommandList.pop() + undoCommandList.addFirst(clippingCommand) + } + } + + override fun undoInClippingTool() { + val command = undoCommandList.pop() + handleUndo(command) + notifyCommandExecuted() + } + + override fun popFirstCommandInUndo() { + undoCommandList.pop() + } + + override fun popFirstCommandInRedo() { + redoCommandList.pop() + } + override fun setInitialStateCommand(command: Command) { initialStateCommand = command } diff --git a/Paintroid/src/main/java/org/catrobat/paintroid/command/serialization/ClippingCommandSerializer.kt b/Paintroid/src/main/java/org/catrobat/paintroid/command/serialization/ClippingCommandSerializer.kt new file mode 100644 index 0000000000..e6f4ab26c1 --- /dev/null +++ b/Paintroid/src/main/java/org/catrobat/paintroid/command/serialization/ClippingCommandSerializer.kt @@ -0,0 +1,27 @@ +package org.catrobat.paintroid.command.serialization + +import android.graphics.Bitmap +import com.esotericsoftware.kryo.Kryo +import com.esotericsoftware.kryo.io.Input +import com.esotericsoftware.kryo.io.Output +import org.catrobat.paintroid.command.implementation.ClippingCommand + +class ClippingCommandSerializer(version: Int) : VersionSerializer(version) { + override fun write(kryo: Kryo, output: Output, command: ClippingCommand) { + with(kryo) { + writeObject(output, command.bitmap) + writeObject(output, command.pathBitmap) + } + } + + override fun read(kryo: Kryo, input: Input, type: Class): ClippingCommand = + super.handleVersions(this, kryo, input, type) + + override fun readCurrentVersion(kryo: Kryo, input: Input, type: Class): ClippingCommand { + return with(kryo) { + val bitmap = readObject(input, Bitmap::class.java) + val pathBitmap = readObject(input, Bitmap::class.java) + ClippingCommand(bitmap, pathBitmap) + } + } +} diff --git a/Paintroid/src/main/java/org/catrobat/paintroid/command/serialization/CommandSerializationUtilities.kt b/Paintroid/src/main/java/org/catrobat/paintroid/command/serialization/CommandSerializationUtilities.kt index 36add4edf4..a8a290ff43 100644 --- a/Paintroid/src/main/java/org/catrobat/paintroid/command/serialization/CommandSerializationUtilities.kt +++ b/Paintroid/src/main/java/org/catrobat/paintroid/command/serialization/CommandSerializationUtilities.kt @@ -39,6 +39,7 @@ import com.esotericsoftware.kryo.io.Input import com.esotericsoftware.kryo.io.Output import org.catrobat.paintroid.command.Command import org.catrobat.paintroid.command.CommandManager +import org.catrobat.paintroid.command.implementation.ClippingCommand import org.catrobat.paintroid.command.implementation.AddEmptyLayerCommand import org.catrobat.paintroid.command.implementation.CompositeCommand import org.catrobat.paintroid.command.implementation.CropCommand @@ -141,6 +142,7 @@ class CommandSerializationUtilities(private val activityContext: Context, privat put(SerializablePath.Cube::class.java, SerializablePath.PathActionCubeSerializer(version)) put(Bitmap::class.java, BitmapSerializer(version)) put(SmudgePathCommand::class.java, SmudgePathCommandSerializer(version)) + put(ClippingCommand::class.java, ClippingCommandSerializer(version)) } } diff --git a/Paintroid/src/main/java/org/catrobat/paintroid/controller/DefaultToolController.kt b/Paintroid/src/main/java/org/catrobat/paintroid/controller/DefaultToolController.kt index 82ff23d113..f4227f0ca4 100644 --- a/Paintroid/src/main/java/org/catrobat/paintroid/controller/DefaultToolController.kt +++ b/Paintroid/src/main/java/org/catrobat/paintroid/controller/DefaultToolController.kt @@ -19,6 +19,7 @@ package org.catrobat.paintroid.controller import android.graphics.Bitmap +import android.graphics.PointF import android.os.Bundle import android.view.View import androidx.test.espresso.idling.CountingIdlingResource @@ -33,6 +34,7 @@ import org.catrobat.paintroid.tools.ToolReference import org.catrobat.paintroid.tools.ToolType import org.catrobat.paintroid.tools.Workspace import org.catrobat.paintroid.tools.implementation.BaseToolWithShape +import org.catrobat.paintroid.tools.implementation.ClippingTool import org.catrobat.paintroid.tools.implementation.ImportTool import org.catrobat.paintroid.tools.implementation.LineTool import org.catrobat.paintroid.tools.options.ToolOptionsViewController @@ -72,6 +74,8 @@ class DefaultToolController( private fun createAndSetupTool(toolType: ToolType): Tool { if (toolType != ToolType.HAND) { toolOptionsViewController.removeToolViews() + } else if (toolType != ToolType.CLIP) { + toolOptionsViewController.removeToolViews() } if (toolList.contains(toolType)) { toolOptionsViewController.showCheckmark() @@ -124,11 +128,14 @@ class DefaultToolController( private fun switchTool(tool: Tool, backPressed: Boolean) { val currentTool = toolReference.tool val currentToolType = currentTool?.toolType - if (toolList.contains(currentToolType) && !backPressed) { - val toolToApply = currentTool as BaseToolWithShape - toolToApply.onClickOnButton() + if (toolList.contains(currentToolType)) { + if (!backPressed) { + val toolToApply = currentTool as BaseToolWithShape + toolToApply.onClickOnButton() + } + } else if (currentToolType == ToolType.CLIP) { + adjustClippingTool(backPressed) } - currentToolType?.let { hidePlusIfShown(it) } if (currentTool?.toolType == tool.toolType) { @@ -140,6 +147,19 @@ class DefaultToolController( workspace.invalidate() } + private fun adjustClippingTool(backPressed: Boolean) { + val clippingTool = currentTool as ClippingTool + if (backPressed) { + if (clippingTool.areaClosed) { + clippingTool.handleDown(PointF(0f, 0f)) + clippingTool.wasRecentlyApplied = true + clippingTool.resetInternalState(StateChange.NEW_IMAGE_LOADED) + } + } else { + clippingTool.onClickOnButton() + } + } + private fun hidePlusIfShown(currentToolType: ToolType) { if (currentToolType == ToolType.LINE && null != LineTool.topBarViewHolder) { val visibility = LineTool.topBarViewHolder?.plusButton?.visibility == View.VISIBLE diff --git a/Paintroid/src/main/java/org/catrobat/paintroid/presenter/LayerPresenter.kt b/Paintroid/src/main/java/org/catrobat/paintroid/presenter/LayerPresenter.kt index 8d6713a831..c28aeebf39 100644 --- a/Paintroid/src/main/java/org/catrobat/paintroid/presenter/LayerPresenter.kt +++ b/Paintroid/src/main/java/org/catrobat/paintroid/presenter/LayerPresenter.kt @@ -18,6 +18,7 @@ */ package org.catrobat.paintroid.presenter +import android.graphics.PointF import android.util.Log import android.view.View import android.widget.Toast @@ -28,6 +29,7 @@ import org.catrobat.paintroid.common.MAX_LAYERS import org.catrobat.paintroid.contract.LayerContracts import org.catrobat.paintroid.controller.DefaultToolController import org.catrobat.paintroid.tools.ToolType +import org.catrobat.paintroid.tools.implementation.ClippingTool import org.catrobat.paintroid.tools.implementation.LineTool import org.catrobat.paintroid.ui.DrawingSurface import org.catrobat.paintroid.ui.dragndrop.DragAndDropPresenter @@ -83,6 +85,23 @@ class LayerPresenter( } } + private fun checkIfClippingToolInUse(): Boolean { + if (defaultToolController?.currentTool?.toolType == ToolType.CLIP) { + val clippingTool = defaultToolController?.currentTool as ClippingTool + clippingTool.wasRecentlyApplied = true + if (clippingTool.areaClosed) { + clippingTool.handleDown(PointF(0f, 0f)) + clippingTool.initialEventCoordinate = null + clippingTool.previousEventCoordinate = null + clippingTool.pathToDraw.rewind() + clippingTool.pointArray.clear() + } + return true + } else { + return false + } + } + override fun setAdapter(layerAdapter: LayerContracts.Adapter) { this.adapter = layerAdapter } @@ -141,7 +160,9 @@ class LayerPresenter( override fun addLayer() { if (layerCount < MAX_LAYERS) { checkIfLineToolInUse() + val clippingToolInUse = checkIfClippingToolInUse() commandManager.addCommand(commandFactory.createAddEmptyLayerCommand()) + if (clippingToolInUse) (defaultToolController?.currentTool as ClippingTool).copyBitmapOfCurrentLayer() } else { navigator.showToast(R.string.layer_too_many_layers, Toast.LENGTH_SHORT) } @@ -150,9 +171,11 @@ class LayerPresenter( override fun removeLayer() { if (layerCount > 1) { checkIfLineToolInUse() + val clippingToolInUse = checkIfClippingToolInUse() val layerToDelete = model.currentLayer ?: return val index = model.getLayerIndexOf(layerToDelete) commandManager.addCommand(commandFactory.createRemoveLayerCommand(index)) + if (clippingToolInUse) (defaultToolController?.currentTool as ClippingTool).copyBitmapOfCurrentLayer() } } @@ -197,11 +220,13 @@ class LayerPresenter( checkIfLineToolInUse() layers.getOrNull(mergeWith)?.let { actualLayer -> val actualPosition = model.getLayerIndexOf(actualLayer) + val clippingToolInUse = checkIfClippingToolInUse() if (position != actualPosition && actualPosition > -1) { commandManager.addCommand( commandFactory.createMergeLayersCommand(position, actualPosition) ) navigator.showToast(R.string.layer_merged, Toast.LENGTH_SHORT) + if (clippingToolInUse) (defaultToolController?.currentTool as ClippingTool).copyBitmapOfCurrentLayer() } } } @@ -210,6 +235,7 @@ class LayerPresenter( if (position != swapWith) { checkIfLineToolInUse() commandManager.addCommand(commandFactory.createReorderLayersCommand(position, swapWith)) + checkIfClippingToolInUse() } } @@ -253,6 +279,7 @@ class LayerPresenter( if (position != model.currentLayer?.let { model.getLayerIndexOf(it) }) { checkIfLineToolInUse() commandManager.addCommand(commandFactory.createSelectLayerCommand(position)) + checkIfClippingToolInUse() } } 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 2fc89139b0..d5e6a75c64 100644 --- a/Paintroid/src/main/java/org/catrobat/paintroid/presenter/MainActivityPresenter.kt +++ b/Paintroid/src/main/java/org/catrobat/paintroid/presenter/MainActivityPresenter.kt @@ -28,6 +28,8 @@ import android.content.pm.PackageManager import android.database.Cursor import android.graphics.Bitmap import android.graphics.Color +import android.graphics.Paint +import android.graphics.PointF import android.net.Uri import android.os.Environment import android.provider.DocumentsContract @@ -80,8 +82,10 @@ import org.catrobat.paintroid.iotasks.CreateFile.CreateFileCallback import org.catrobat.paintroid.iotasks.LoadImage.LoadImageCallback import org.catrobat.paintroid.iotasks.SaveImage.SaveImageCallback import org.catrobat.paintroid.model.CommandManagerModel +import org.catrobat.paintroid.tools.Tool import org.catrobat.paintroid.tools.ToolType import org.catrobat.paintroid.tools.Workspace +import org.catrobat.paintroid.tools.implementation.ClippingTool import org.catrobat.paintroid.tools.implementation.LineTool import org.catrobat.paintroid.ui.LayerAdapter import org.catrobat.paintroid.ui.Perspective @@ -133,12 +137,17 @@ open class MainActivityPresenter( return sharedPreferences.preferenceImageNumber } + var clippingToolInUseAndUndoRedoClicked = false + var clippingToolPaint = Paint() + override fun replaceImageClicked() { + checkIfClippingToolNeedsAdjustment() switchBetweenVersions(PERMISSION_REQUEST_CODE_REPLACE_PICTURE, false) setFirstCheckBoxInLayerMenu() } override fun addImageToCurrentLayerClicked() { + checkIfClippingToolNeedsAdjustment() setTool(ToolType.IMPORTPNG) switchBetweenVersions(PERMISSION_REQUEST_CODE_IMPORT_PICTURE) } @@ -156,11 +165,13 @@ open class MainActivityPresenter( } override fun loadNewImage() { + checkIfClippingToolNeedsAdjustment() navigator.startLoadImageActivity(REQUEST_CODE_LOAD_PICTURE) setFirstCheckBoxInLayerMenu() } override fun newImageClicked() { + checkIfClippingToolNeedsAdjustment() if (isImageUnchanged || model.isSaved) { onNewImage() setFirstCheckBoxInLayerMenu() @@ -171,6 +182,7 @@ open class MainActivityPresenter( } override fun saveBeforeNewImage() { + checkIfClippingToolNeedsAdjustment() navigator.showSaveImageInformationDialogWhenStandalone( PERMISSION_EXTERNAL_STORAGE_SAVE_CONFIRMED_NEW_EMPTY, imageNumber, @@ -193,6 +205,7 @@ open class MainActivityPresenter( } override fun saveBeforeFinish() { + checkIfClippingToolNeedsAdjustment() navigator.showSaveImageInformationDialogWhenStandalone( PERMISSION_EXTERNAL_STORAGE_SAVE_CONFIRMED_FINISH, imageNumber, @@ -217,6 +230,8 @@ open class MainActivityPresenter( } override fun shareImageClicked() { + checkIfClippingToolNeedsAdjustment() + view.refreshDrawingSurface() val bitmap: Bitmap? = workspace.bitmapOfAllLayers navigator.startShareImageActivity(bitmap) } @@ -524,14 +539,19 @@ open class MainActivityPresenter( } override fun saveImageConfirmClicked(requestCode: Int, uri: Uri?) { + checkIfClippingToolNeedsAdjustment() + view.refreshDrawingSurface() interactor.saveImage(this, requestCode, workspace, uri, context) } override fun saveCopyConfirmClicked(requestCode: Int) { + checkIfClippingToolNeedsAdjustment() + view.refreshDrawingSurface() interactor.saveCopy(this, requestCode, workspace, context) } override fun undoClicked() { + idlingResource.increment() if (view.isKeyboardShown) { view.hideKeyboard() } else { @@ -539,13 +559,22 @@ open class MainActivityPresenter( if (toolController.currentTool is LineTool) { (toolController.currentTool as LineTool).undoChangePaintColor(Color.BLACK) } else { - toolController.currentTool?.changePaintColor(Color.BLACK) - commandManager.undo() + if (toolController.currentTool is ClippingTool) { + val clippingTool = toolController.currentTool as ClippingTool + clippingToolPaint = clippingTool.drawPaint + commandManager.undo() + clippingToolInUseAndUndoRedoClicked = true + } else { + toolController.currentTool?.changePaintColor(Color.BLACK) + commandManager.undo() + } } } + idlingResource.decrement() } override fun redoClicked() { + idlingResource.increment() if (view.isKeyboardShown) { view.hideKeyboard() } else { @@ -553,8 +582,12 @@ open class MainActivityPresenter( (toolController.currentTool as LineTool).redoLineTool() } else { commandManager.redo() + if (toolController.currentTool is ClippingTool) { + clippingToolInUseAndUndoRedoClicked = true + } } } + idlingResource.decrement() } override fun showColorPickerClicked() { @@ -562,6 +595,7 @@ open class MainActivityPresenter( } override fun showLayerMenuClicked() { + idlingResource.increment() layerAdapter?.apply { for (i in 0 until count) { val currentHolder = getViewHolderAt(i) @@ -573,6 +607,7 @@ open class MainActivityPresenter( } } drawerLayoutViewHolder.openDrawer(Gravity.END) + idlingResource.decrement() } override fun onCommandPostExecute() { @@ -582,11 +617,31 @@ open class MainActivityPresenter( workspace.resetPerspective() } model.isSaved = false + if (clippingToolInUseAndUndoRedoClicked) { + adjustClippingToolPostCommandExecute() + } toolController.resetToolInternalState() view.refreshDrawingSurface() refreshTopBarButtons() } + fun adjustClippingToolPostCommandExecute() { + val clippingTool = toolController.currentTool as ClippingTool + if (clippingTool.areaClosed) { + commandManager.popFirstCommandInRedo() + } + clippingTool.areaClosed = false + clippingTool.pathToDraw.rewind() + clippingTool.pointArray.clear() + clippingTool.initialEventCoordinate = null + clippingTool.previousEventCoordinate = null + clippingTool.changePaintColor(clippingToolPaint.color) + clippingTool.mainActivity.bottomNavigationViewHolder + .setColorButtonColor(clippingToolPaint.color) + (toolController.currentTool as ClippingTool).wasRecentlyApplied = true + clippingToolInUseAndUndoRedoClicked = false + } + override fun setBottomNavigationColor(color: Int) { bottomNavigationViewHolder.setColorButtonColor(color) } @@ -715,6 +770,8 @@ open class MainActivityPresenter( toolController.switchTool(type, backPressed) if (type === ToolType.IMPORTPNG) { showImportDialog() + } else if (type == ToolType.CLIP) { + (toolController.currentTool as ClippingTool).copyBitmapOfCurrentLayer() } } @@ -985,6 +1042,29 @@ open class MainActivityPresenter( override fun checkForTemporaryFile(): Boolean = FileIO.checkForTemporaryFile(internalMemoryPath) + fun checkIfClippingToolNeedsAdjustment() { + if (toolController.currentTool is ClippingTool) { + val clippingTool = toolController.currentTool as ClippingTool + if (clippingTool.areaClosed) { + clippingTool.handleDown( + clippingTool.initialEventCoordinate?.x?.let { + clippingTool.initialEventCoordinate?.y?.let { it1 -> + PointF( + it, + it1 + ) + } + } + ) + (toolController.currentTool as ClippingTool).wasRecentlyApplied = true + clippingTool.resetInternalState(Tool.StateChange.NEW_IMAGE_LOADED) + } else { + (toolController.currentTool as ClippingTool).wasRecentlyApplied = true + clippingTool.resetInternalState(Tool.StateChange.NEW_IMAGE_LOADED) + } + } + } + companion object { @JvmStatic fun getPathFromUri(context: Context, uri: Uri): String { diff --git a/Paintroid/src/main/java/org/catrobat/paintroid/tools/ToolType.kt b/Paintroid/src/main/java/org/catrobat/paintroid/tools/ToolType.kt index 33c1b9a12e..1e5213e06c 100644 --- a/Paintroid/src/main/java/org/catrobat/paintroid/tools/ToolType.kt +++ b/Paintroid/src/main/java/org/catrobat/paintroid/tools/ToolType.kt @@ -205,6 +205,15 @@ enum class ToolType( R.id.pocketpaint_tools_smudge, INVALID_RESOURCE_ID, true + ), + CLIP( + R.string.button_clip, + R.string.help_content_clip, + R.drawable.ic_pocketpaint_tool_clipping, + EnumSet.of(StateChange.RESET_INTERNAL_STATE), + R.id.pocketpaint_tools_clipping, + INVALID_RESOURCE_ID, + true ); fun shouldReactToStateChange(stateChange: StateChange): Boolean = diff --git a/Paintroid/src/main/java/org/catrobat/paintroid/tools/Workspace.kt b/Paintroid/src/main/java/org/catrobat/paintroid/tools/Workspace.kt index 709e62b3d5..65fa49e058 100644 --- a/Paintroid/src/main/java/org/catrobat/paintroid/tools/Workspace.kt +++ b/Paintroid/src/main/java/org/catrobat/paintroid/tools/Workspace.kt @@ -31,7 +31,7 @@ interface Workspace { val surfaceHeight: Int val bitmapOfAllLayers: Bitmap? val bitmapLisOfAllLayers: List - val bitmapOfCurrentLayer: Bitmap? + var bitmapOfCurrentLayer: Bitmap? val currentLayerIndex: Int val scaleForCenterBitmap: Float var scale: Float diff --git a/Paintroid/src/main/java/org/catrobat/paintroid/tools/implementation/BaseTool.kt b/Paintroid/src/main/java/org/catrobat/paintroid/tools/implementation/BaseTool.kt index e553f702f4..95f722be7f 100644 --- a/Paintroid/src/main/java/org/catrobat/paintroid/tools/implementation/BaseTool.kt +++ b/Paintroid/src/main/java/org/catrobat/paintroid/tools/implementation/BaseTool.kt @@ -41,8 +41,7 @@ import org.catrobat.paintroid.tools.options.ToolOptionsViewController abstract class BaseTool( @JvmField open var contextCallback: ContextCallback, - @JvmField - protected var toolOptionsViewController: ToolOptionsViewController, + @JvmField var toolOptionsViewController: ToolOptionsViewController, @JvmField protected var toolPaint: ToolPaint, @JvmField @@ -59,7 +58,7 @@ abstract class BaseTool( protected var scrollBehavior: ScrollBehavior @JvmField - protected var previousEventCoordinate: PointF? + var previousEventCoordinate: PointF? @JvmField protected var commandFactory: CommandFactory = DefaultCommandFactory() @@ -69,6 +68,9 @@ abstract class BaseTool( scrollBehavior = PointScrollBehavior(scrollTolerance) movedDistance = PointF(0f, 0f) previousEventCoordinate = PointF(0f, 0f) + if (toolPaint != null && toolPaint.paint != null && toolPaint.paint.pathEffect != null) { + toolPaint.paint.pathEffect = null + } } override fun onSaveInstanceState(bundle: Bundle?) = Unit diff --git a/Paintroid/src/main/java/org/catrobat/paintroid/tools/implementation/BaseToolWithShape.kt b/Paintroid/src/main/java/org/catrobat/paintroid/tools/implementation/BaseToolWithShape.kt index d053dbb145..5d5bea966c 100644 --- a/Paintroid/src/main/java/org/catrobat/paintroid/tools/implementation/BaseToolWithShape.kt +++ b/Paintroid/src/main/java/org/catrobat/paintroid/tools/implementation/BaseToolWithShape.kt @@ -84,6 +84,7 @@ abstract class BaseToolWithShape @SuppressLint("VisibleForTests") constructor( } linePaint = Paint() linePaint.color = primaryShapeColor + linePaint.pathEffect = null } abstract fun drawShape(canvas: Canvas) diff --git a/Paintroid/src/main/java/org/catrobat/paintroid/tools/implementation/BrushTool.kt b/Paintroid/src/main/java/org/catrobat/paintroid/tools/implementation/BrushTool.kt index ca60ebc05a..154a6a5ce8 100644 --- a/Paintroid/src/main/java/org/catrobat/paintroid/tools/implementation/BrushTool.kt +++ b/Paintroid/src/main/java/org/catrobat/paintroid/tools/implementation/BrushTool.kt @@ -64,13 +64,14 @@ open class BrushTool( @VisibleForTesting @JvmField var pathToDraw: SerializablePath = SerializablePath() - private var initialEventCoordinate: PointF? = null + var initialEventCoordinate: PointF? = null private var pathInsideBitmap = false private val drawToolMovedDistance = PointF(0f, 0f) - private val pointArray = mutableListOf() + val pointArray = mutableListOf() init { + toolOptionsViewController.enable() pathToDraw.incReserve(1) brushToolOptionsView.setBrushChangedListener(CommonBrushChangedListener(this)) brushToolOptionsView.setBrushPreviewListener( diff --git a/Paintroid/src/main/java/org/catrobat/paintroid/tools/implementation/ClippingTool.kt b/Paintroid/src/main/java/org/catrobat/paintroid/tools/implementation/ClippingTool.kt new file mode 100644 index 0000000000..59b74eeb46 --- /dev/null +++ b/Paintroid/src/main/java/org/catrobat/paintroid/tools/implementation/ClippingTool.kt @@ -0,0 +1,187 @@ +package org.catrobat.paintroid.tools.implementation + +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.DashPathEffect +import android.graphics.Paint +import android.graphics.PointF +import androidx.test.espresso.idling.CountingIdlingResource +import org.catrobat.paintroid.MainActivity +import org.catrobat.paintroid.command.CommandManager +import org.catrobat.paintroid.tools.ContextCallback +import org.catrobat.paintroid.tools.Tool +import org.catrobat.paintroid.tools.ToolPaint +import org.catrobat.paintroid.tools.ToolType +import org.catrobat.paintroid.tools.Workspace +import org.catrobat.paintroid.tools.common.CommonBrushChangedListener +import org.catrobat.paintroid.tools.common.CommonBrushPreviewListener +import org.catrobat.paintroid.tools.options.BrushToolOptionsView +import org.catrobat.paintroid.tools.options.ToolOptionsViewController + +const val TWO = 2f +const val TWO_AND_A_HALF = 2.5f +const val CONTOUR_SIZE = 5 + +class ClippingTool( + brushToolOptionsView: BrushToolOptionsView, + contextCallback: ContextCallback, + toolOptionsViewController: ToolOptionsViewController, + toolPaint: ToolPaint, + workspace: Workspace, + idlingResource: CountingIdlingResource, + commandManager: CommandManager, + drawTime: Long, + var mainActivity: MainActivity +) : BrushTool( + brushToolOptionsView, + contextCallback, + toolOptionsViewController, + toolPaint, + workspace, + idlingResource, + commandManager, + drawTime +) { + private var newBitmap: Bitmap? = null + var wasRecentlyApplied: Boolean = false + var areaClosed = false + var clippingPaint = Paint() + private val pathLineLength: Float + get() = toolPaint.paint.strokeWidth * TWO_AND_A_HALF + private val pathGapLength: Float + get() = toolPaint.paint.strokeWidth * TWO + override val bitmapPaint: Paint + get() = toolPaint.paint + + override val previewPaint: Paint + get() = toolPaint.previewPaint + + override val toolType: ToolType + get() = ToolType.CLIP + + init { + toolPaint.paint.strokeWidth = STROKE_10 + toolPaint.paint.style = Paint.Style.STROKE + toolPaint.paint.pathEffect = DashPathEffect(floatArrayOf(pathLineLength, pathGapLength), 0f) + brushToolOptionsView.setBrushChangedListener(CommonBrushChangedListener(this)) + brushToolOptionsView.setBrushPreviewListener( + CommonBrushPreviewListener( + toolPaint, + toolType + ) + ) + brushToolOptionsView.setCurrentPaint(toolPaint.paint) + brushToolOptionsView.invalidate() + toolOptionsViewController.showCheckmark() + brushToolOptionsView.hideCaps() + copyBitmapOfCurrentLayer() + } + + override fun draw(canvas: Canvas) { + clippingPaint = toolPaint.previewPaint + clippingPaint.pathEffect = toolPaint.paint.pathEffect + clippingPaint.color = if (toolPaint.previewColor == Color.BLACK) Color.WHITE else Color.BLACK + clippingPaint.strokeWidth = toolPaint.previewPaint.strokeWidth + CONTOUR_SIZE + idlingResource.increment() + canvas.run { + save() + clipRect(0, 0, workspace.width, workspace.height) + drawPath(pathToDraw, clippingPaint) + drawPath(pathToDraw, previewPaint) + restore() + } + idlingResource.decrement() + } + + fun copyBitmapOfCurrentLayer() { + if (workspace.bitmapOfCurrentLayer != null) { + newBitmap = workspace.bitmapOfCurrentLayer?.copy(workspace.bitmapOfCurrentLayer?.config, true) + } + } + + override fun handleDown(coordinate: PointF?): Boolean { + if (areaClosed) { + super.resetInternalState() + areaClosed = false + commandManager.undoInClippingTool() + changePaintColor(toolPaint.previewPaint.color) + brushToolOptionsView.setCurrentPaint(toolPaint.paint) + brushToolOptionsView.invalidate() + mainActivity.bottomNavigationViewHolder.setColorButtonColor(toolPaint.previewColor) + } + return super.handleDown(coordinate) + } + + override fun handleUp(coordinate: PointF?): Boolean { + val tempPoint = initialEventCoordinate + if (previousEventCoordinate == initialEventCoordinate) { + super.resetInternalState() + return false + } + if (!areaClosed && coordinate != null && tempPoint != null) { + pathToDraw.incReserve(1) + pathToDraw.quadTo(coordinate.x, coordinate.y, tempPoint.x, tempPoint.y) + pointArray.add(PointF(coordinate.x, coordinate.y)) + areaClosed = true + } + return super.handleUp(coordinate) + } + + fun onClickOnButton() { + idlingResource.increment() + if (areaClosed) { + val pathBitmap = + newBitmap?.config?.let { + newBitmap?.width?.let { it1 -> + newBitmap?.height?.let { it2 -> + Bitmap.createBitmap( + it1, it2, + it + ) + } + } + } + val canvas: Canvas? + val paint = Paint() + paint.color = Color.BLACK + paint.style = Paint.Style.FILL + if (pathBitmap != null) { + canvas = Canvas(pathBitmap) + canvas.drawPath(pathToDraw, paint) + } + + newBitmap?.let { + val command = pathBitmap?.let { it1 -> + commandFactory.createClippingCommand( + it, + it1 + ) + } + commandManager.addCommand(command) + commandManager.adjustUndoListForClippingTool() + } + areaClosed = false + wasRecentlyApplied = true + pathToDraw.rewind() + } + idlingResource.decrement() + } + + override fun resetInternalState(stateChange: Tool.StateChange) { + if (stateChange == Tool.StateChange.NEW_IMAGE_LOADED) { + areaClosed = false + super.resetInternalState() + copyBitmapOfCurrentLayer() + } else if (stateChange == Tool.StateChange.RESET_INTERNAL_STATE && wasRecentlyApplied) { + copyBitmapOfCurrentLayer() + wasRecentlyApplied = false + } + } + + override fun changePaintStrokeWidth(strokeWidth: Int) { + toolPaint.paint.pathEffect = DashPathEffect(floatArrayOf(pathLineLength, pathGapLength), 0f) + previewPaint.pathEffect = toolPaint.paint.pathEffect + super.changePaintStrokeWidth(strokeWidth) + } +} 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 b61c585a2f..6cfa52006e 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 @@ -19,6 +19,7 @@ package org.catrobat.paintroid.tools.implementation import androidx.test.espresso.idling.CountingIdlingResource +import org.catrobat.paintroid.MainActivity import org.catrobat.paintroid.colorpicker.OnColorPickedListener import org.catrobat.paintroid.command.CommandManager import org.catrobat.paintroid.tools.ContextCallback @@ -40,7 +41,8 @@ import org.catrobat.paintroid.ui.tools.DefaultSmudgeToolOptionsView private const val DRAW_TIME_INIT: Long = 30_000_000 @SuppressWarnings("LongMethod") -class DefaultToolFactory : ToolFactory { +class DefaultToolFactory(mainActivity: MainActivity) : ToolFactory { + var mainActivity: MainActivity = mainActivity override fun createTool( toolType: ToolType, toolOptionsViewController: ToolOptionsViewController, @@ -189,6 +191,17 @@ class DefaultToolFactory : ToolFactory { idlingResource, commandManager, ) + ToolType.CLIP -> ClippingTool( + DefaultBrushToolOptionsView(toolLayout), + contextCallback, + toolOptionsViewController, + toolPaint, + workspace, + idlingResource, + commandManager, + DRAW_TIME_INIT, + mainActivity + ) else -> BrushTool( DefaultBrushToolOptionsView(toolLayout), contextCallback, diff --git a/Paintroid/src/main/java/org/catrobat/paintroid/tools/implementation/DefaultToolPaint.kt b/Paintroid/src/main/java/org/catrobat/paintroid/tools/implementation/DefaultToolPaint.kt index b3701d5d10..230792c5d9 100644 --- a/Paintroid/src/main/java/org/catrobat/paintroid/tools/implementation/DefaultToolPaint.kt +++ b/Paintroid/src/main/java/org/catrobat/paintroid/tools/implementation/DefaultToolPaint.kt @@ -31,6 +31,7 @@ import org.catrobat.paintroid.R import org.catrobat.paintroid.tools.ToolPaint const val STROKE_25 = 25f +const val STROKE_10 = 10f class DefaultToolPaint(private val context: Context) : ToolPaint { private val bitmapPaint = Paint().apply { diff --git a/Paintroid/src/main/java/org/catrobat/paintroid/tools/implementation/DefaultWorkspace.kt b/Paintroid/src/main/java/org/catrobat/paintroid/tools/implementation/DefaultWorkspace.kt index e90a073a2e..e7bb365b22 100644 --- a/Paintroid/src/main/java/org/catrobat/paintroid/tools/implementation/DefaultWorkspace.kt +++ b/Paintroid/src/main/java/org/catrobat/paintroid/tools/implementation/DefaultWorkspace.kt @@ -51,7 +51,7 @@ class DefaultWorkspace( override val bitmapLisOfAllLayers: List get() = LayerModel.getBitmapListOfAllLayers(layerModel.layers) - override val bitmapOfCurrentLayer: Bitmap? + override var bitmapOfCurrentLayer: Bitmap? = null get() = layerModel.currentLayer?.bitmap?.let { Bitmap.createBitmap(it) } override val currentLayerIndex: Int diff --git a/Paintroid/src/main/java/org/catrobat/paintroid/tools/options/BrushToolOptionsView.kt b/Paintroid/src/main/java/org/catrobat/paintroid/tools/options/BrushToolOptionsView.kt index c695601643..f76c009b15 100644 --- a/Paintroid/src/main/java/org/catrobat/paintroid/tools/options/BrushToolOptionsView.kt +++ b/Paintroid/src/main/java/org/catrobat/paintroid/tools/options/BrushToolOptionsView.kt @@ -32,6 +32,8 @@ interface BrushToolOptionsView { fun setBrushPreviewListener(onBrushPreviewListener: OnBrushPreviewListener) + fun hideCaps() + interface OnBrushChangedListener { fun setCap(strokeCap: Cap) diff --git a/Paintroid/src/main/java/org/catrobat/paintroid/ui/MainActivityNavigator.kt b/Paintroid/src/main/java/org/catrobat/paintroid/ui/MainActivityNavigator.kt index 5043a2781f..04931cb1b9 100644 --- a/Paintroid/src/main/java/org/catrobat/paintroid/ui/MainActivityNavigator.kt +++ b/Paintroid/src/main/java/org/catrobat/paintroid/ui/MainActivityNavigator.kt @@ -87,6 +87,7 @@ import org.catrobat.paintroid.dialog.SaveBeforeNewImageDialog import org.catrobat.paintroid.dialog.SaveInformationDialog import org.catrobat.paintroid.dialog.ScaleImageOnLoadDialog import org.catrobat.paintroid.tools.ToolReference +import org.catrobat.paintroid.tools.ToolType import org.catrobat.paintroid.ui.fragments.CatroidMediaGalleryFragment import org.catrobat.paintroid.ui.fragments.CatroidMediaGalleryFragment.MediaGalleryListener @@ -132,8 +133,13 @@ class MainActivityNavigator( private fun setupColorPickerDialogListeners(dialog: ColorPickerDialog) { dialog.addOnColorPickedListener(object : OnColorPickedListener { override fun colorChanged(color: Int) { - val command = commandFactory.createColorChangedCommand(toolReference, mainActivity, color) - mainActivity.commandManager.addCommand(command) + if (toolReference.tool?.toolType != ToolType.CLIP) { + val command = commandFactory.createColorChangedCommand(toolReference, mainActivity, color) + mainActivity.commandManager.addCommand(command) + } else { + val command = commandFactory.createColorChangedCommand(toolReference, mainActivity, color) + mainActivity.commandManager.addCommandWithoutUndo(command) + } } }) mainActivity.presenter.bitmap?.let { dialog.setBitmap(it) } diff --git a/Paintroid/src/main/java/org/catrobat/paintroid/ui/tools/DefaultBrushToolOptionsView.kt b/Paintroid/src/main/java/org/catrobat/paintroid/ui/tools/DefaultBrushToolOptionsView.kt index 74f800271d..b0955fefab 100644 --- a/Paintroid/src/main/java/org/catrobat/paintroid/ui/tools/DefaultBrushToolOptionsView.kt +++ b/Paintroid/src/main/java/org/catrobat/paintroid/ui/tools/DefaultBrushToolOptionsView.kt @@ -25,6 +25,7 @@ import android.text.InputFilter import android.text.TextWatcher import android.util.Log import android.view.LayoutInflater +import android.view.View import android.view.ViewGroup import android.widget.EditText import android.widget.SeekBar @@ -44,12 +45,14 @@ private const val MIN_VAL = 1 private const val MAX_VAL = 100 class DefaultBrushToolOptionsView(rootView: ViewGroup) : BrushToolOptionsView { - private val brushSizeText: EditText - private val brushWidthSeekBar: SeekBar - private val buttonCircle: Chip + private var brushSizeText: EditText + private var brushWidthSeekBar: SeekBar + private var buttonCircle: Chip private val buttonRect: Chip - private val brushToolPreview: BrushToolPreview + private var brushToolPreview: BrushToolPreview private var brushChangedListener: OnBrushChangedListener? = null + private var currentView = rootView + private val capsView: View companion object { private val TAG = DefaultBrushToolOptionsView::class.java.simpleName @@ -65,6 +68,7 @@ class DefaultBrushToolOptionsView(rootView: ViewGroup) : BrushToolOptionsView { brushWidthSeekBar.setOnSeekBarChangeListener(OnBrushChangedWidthSeekBarListener()) brushSizeText = findViewById(R.id.pocketpaint_stroke_width_width_text) brushToolPreview = findViewById(R.id.pocketpaint_brush_tool_preview) + capsView = findViewById(R.id.pocketpaint_stroke_types) } brushSizeText.filters = arrayOf(DefaultNumberRangeFilter(MIN_VAL, MAX_VAL)) buttonCircle.setOnClickListener { onCircleButtonClicked() } @@ -97,6 +101,10 @@ class DefaultBrushToolOptionsView(rootView: ViewGroup) : BrushToolOptionsView { invalidate() } + override fun hideCaps() { + capsView.visibility = View.GONE + } + private fun onCircleButtonClicked() { updateStrokeCap(Cap.ROUND) buttonCircle.isSelected = true @@ -104,6 +112,17 @@ class DefaultBrushToolOptionsView(rootView: ViewGroup) : BrushToolOptionsView { invalidate() } + fun adjustOptionsView() { + val inflater = LayoutInflater.from(currentView.context) + val brushPickerView = inflater.inflate(R.layout.dialog_pocketpaint_stroke, currentView, true) + brushPickerView.apply { + brushWidthSeekBar = findViewById(R.id.pocketpaint_stroke_width_seek_bar) + brushWidthSeekBar.setOnSeekBarChangeListener(OnBrushChangedWidthSeekBarListener()) + brushSizeText = findViewById(R.id.pocketpaint_stroke_width_width_text) + brushToolPreview = findViewById(R.id.pocketpaint_brush_tool_preview) + } + } + override fun setCurrentPaint(paint: Paint) { if (paint.strokeCap == Cap.ROUND) { buttonCircle.isSelected = true diff --git a/Paintroid/src/main/java/org/catrobat/paintroid/ui/tools/DefaultSmudgeToolOptionsView.kt b/Paintroid/src/main/java/org/catrobat/paintroid/ui/tools/DefaultSmudgeToolOptionsView.kt index 56f038006b..4c3ca2bd69 100644 --- a/Paintroid/src/main/java/org/catrobat/paintroid/ui/tools/DefaultSmudgeToolOptionsView.kt +++ b/Paintroid/src/main/java/org/catrobat/paintroid/ui/tools/DefaultSmudgeToolOptionsView.kt @@ -186,6 +186,10 @@ class DefaultSmudgeToolOptionsView(rootView: ViewGroup) : SmudgeToolOptionsView brushToolPreview.invalidate() } + override fun hideCaps() { + // Should never be reached + } + private fun updateStrokeWidthChange(strokeWidth: Int) { brushChangedListener?.setStrokeWidth(strokeWidth) } diff --git a/Paintroid/src/main/res/drawable/ic_pocketpaint_tool_clipping.xml b/Paintroid/src/main/res/drawable/ic_pocketpaint_tool_clipping.xml new file mode 100644 index 0000000000..0dc786b6e7 --- /dev/null +++ b/Paintroid/src/main/res/drawable/ic_pocketpaint_tool_clipping.xml @@ -0,0 +1,8 @@ + + + \ No newline at end of file diff --git a/Paintroid/src/main/res/layout-land/pocketpaint_layout_bottom_bar.xml b/Paintroid/src/main/res/layout-land/pocketpaint_layout_bottom_bar.xml index fdbc8a0307..3c4e254be4 100644 --- a/Paintroid/src/main/res/layout-land/pocketpaint_layout_bottom_bar.xml +++ b/Paintroid/src/main/res/layout-land/pocketpaint_layout_bottom_bar.xml @@ -229,5 +229,18 @@ style="@style/PocketPaintToolSelectionButtonTextView" android:text="@string/button_smudge"/> + + + + + diff --git a/Paintroid/src/main/res/layout/pocketpaint_layout_bottom_bar.xml b/Paintroid/src/main/res/layout/pocketpaint_layout_bottom_bar.xml index db3232b032..f698b405e5 100644 --- a/Paintroid/src/main/res/layout/pocketpaint_layout_bottom_bar.xml +++ b/Paintroid/src/main/res/layout/pocketpaint_layout_bottom_bar.xml @@ -234,5 +234,18 @@ style="@style/PocketPaintToolSelectionButtonTextView" android:text="@string/button_smudge"/> + + + + + diff --git a/Paintroid/src/main/res/layout/pocketpaint_layout_help_bottom_bar.xml b/Paintroid/src/main/res/layout/pocketpaint_layout_help_bottom_bar.xml index 94173f0513..a59fe5b547 100644 --- a/Paintroid/src/main/res/layout/pocketpaint_layout_help_bottom_bar.xml +++ b/Paintroid/src/main/res/layout/pocketpaint_layout_help_bottom_bar.xml @@ -265,5 +265,18 @@ android:text="@string/button_smudge" android:textColor="@color/pocketpaint_color_picker_white"/> + + + + + diff --git a/Paintroid/src/main/res/values/string.xml b/Paintroid/src/main/res/values/string.xml index 00894169a6..4be0a6259d 100644 --- a/Paintroid/src/main/res/values/string.xml +++ b/Paintroid/src/main/res/values/string.xml @@ -39,6 +39,7 @@ Watercolor Spray can Smudge + Clip area Apply Checkmark Connect Line Segment @@ -83,6 +84,7 @@ Move your finger to move the canvas. Move your finger on the image to create a spray can pattern. Move your finger on the image on different drawings to smudge them. + Mark area which should not be erased. Back to project Quit Save changes?