diff --git a/Paintroid/src/androidTest/java/org/catrobat/paintroid/test/espresso/tools/PixelationToolIntegrationTest.kt b/Paintroid/src/androidTest/java/org/catrobat/paintroid/test/espresso/tools/PixelationToolIntegrationTest.kt new file mode 100644 index 0000000000..a132e72c91 --- /dev/null +++ b/Paintroid/src/androidTest/java/org/catrobat/paintroid/test/espresso/tools/PixelationToolIntegrationTest.kt @@ -0,0 +1,98 @@ +package org.catrobat.paintroid.test.espresso.tools + +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.action.ViewActions.closeSoftKeyboard +import androidx.test.espresso.action.ViewActions.replaceText +import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.espresso.matcher.ViewMatchers.withText +import androidx.test.rule.ActivityTestRule +import org.catrobat.paintroid.MainActivity +import org.catrobat.paintroid.R +import org.catrobat.paintroid.contract.LayerContracts +import org.catrobat.paintroid.test.espresso.util.MainActivityHelper +import org.catrobat.paintroid.test.espresso.util.wrappers.ToolBarViewInteraction +import org.catrobat.paintroid.test.utils.ScreenshotOnFailRule +import org.catrobat.paintroid.tools.ToolReference +import org.catrobat.paintroid.tools.ToolType +import org.catrobat.paintroid.tools.implementation.MAXIMUM_BITMAP_SIZE_FACTOR +import org.catrobat.paintroid.tools.implementation.PixelTool +import org.catrobat.paintroid.ui.Perspective +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Rule +import org.junit.Test + +class PixelationToolIntegrationTest { + @get:Rule + var launchActivityRule = ActivityTestRule( + MainActivity::class.java + ) + + @get:Rule + var screenshotOnFailRule = ScreenshotOnFailRule() + private var displayWidth = 0 + private var displayHeight = 0 + private var initialWidth = 0 + private var initialHeight = 0 + private var maxBitmapSize = 0 + private lateinit var perspective: Perspective + private lateinit var layerModel: LayerContracts.Model + private lateinit var toolReference: ToolReference + private lateinit var mainActivity: MainActivity + private lateinit var activityHelper: MainActivityHelper + private lateinit var pixelTool: PixelTool + + @Before + fun setUp() { + mainActivity = launchActivityRule.activity + activityHelper = MainActivityHelper(mainActivity) + perspective = mainActivity.perspective + layerModel = mainActivity.layerModel + toolReference = mainActivity.toolReference + displayWidth = activityHelper.displayWidth + displayHeight = activityHelper.displayHeight + + maxBitmapSize = displayHeight * displayWidth * MAXIMUM_BITMAP_SIZE_FACTOR.toInt() + val workingBitmap = layerModel.currentLayer!!.bitmap + initialWidth = workingBitmap.width + initialHeight = workingBitmap.height + ToolBarViewInteraction.onToolBarView() + .performSelectTool(ToolType.PIXEL) + pixelTool = toolReference.tool as PixelTool + } + + @Test + fun inputeTest() { + var width = 80.0f + var height = 49.0f + onView(withId(R.id.pocketpaint_pixel_width_value)) + .check(matches(isDisplayed())) + .perform(replaceText(width.toString()), closeSoftKeyboard()) + .check(matches(withText(width.toString()))) + + // Check and set height value + onView(withId(R.id.pocketpaint_pixel_height_value)) + .check(matches(isDisplayed())) + .perform(replaceText(height.toString()), closeSoftKeyboard()) + .check(matches(withText(height.toString()))) + + // Set SeekBar position for color (e.g., halfway) + /* onView(withId(R.id.pocketpaint_pixel_color_seekbar)) + .perform(swipeRight()) // Swipe to change SeekBar's position. + + onView(withId(R.id.pocketpaint_transform_pixel_color_text)) + .check(matches(isDisplayed())) + .perform(replaceText(collor.toString()), closeSoftKeyboard()) + .check(matches(withText(collor.toString())))*/ + + // Optionally click the apply button + onView(withId(R.id.pocketpaint_pixel_apply_button)) + .perform(click()) + assertEquals(pixelTool.numPixelWidth, width) + assertEquals(pixelTool.numPixelHeight, height) + // assertEquals(pixelTool.numCollors.toInt(),collor ) + } +} diff --git a/Paintroid/src/main/java/org/catrobat/paintroid/controller/ToolController.kt b/Paintroid/src/main/java/org/catrobat/paintroid/controller/ToolController.kt index 57c3ed6d26..1aded7adfd 100644 --- a/Paintroid/src/main/java/org/catrobat/paintroid/controller/ToolController.kt +++ b/Paintroid/src/main/java/org/catrobat/paintroid/controller/ToolController.kt @@ -35,7 +35,8 @@ interface ToolController { ToolType.TRANSFORM, ToolType.IMPORTPNG, ToolType.SHAPE, - ToolType.LINE + ToolType.LINE, + ToolType.PIXEL ) fun setOnColorPickedListener(onColorPickedListener: OnColorPickedListener) 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 0a32cf8088..c90685081e 100644 --- a/Paintroid/src/main/java/org/catrobat/paintroid/tools/ToolType.kt +++ b/Paintroid/src/main/java/org/catrobat/paintroid/tools/ToolType.kt @@ -214,6 +214,15 @@ enum class ToolType( R.id.pocketpaint_tools_clipping, INVALID_RESOURCE_ID, true + ), + PIXEL( + R.string.button_pixel, + R.string.help_content_text, + R.drawable.ic_pocketpaint_tool_pixel, + EnumSet.of(StateChange.ALL), + R.id.pocketpaint_tools_pixel, + INVALID_RESOURCE_ID, + true ); fun shouldReactToStateChange(stateChange: StateChange): Boolean = 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..2b6ef50b22 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 @@ -30,12 +30,13 @@ import org.catrobat.paintroid.tools.ToolType import org.catrobat.paintroid.tools.Workspace import org.catrobat.paintroid.tools.options.ToolOptionsViewController import org.catrobat.paintroid.ui.tools.DefaultBrushToolOptionsView -import org.catrobat.paintroid.ui.tools.DefaultFillToolOptionsView -import org.catrobat.paintroid.ui.tools.DefaultShapeToolOptionsView -import org.catrobat.paintroid.ui.tools.DefaultSprayToolOptionsView import org.catrobat.paintroid.ui.tools.DefaultClipboardToolOptionsView -import org.catrobat.paintroid.ui.tools.DefaultTextToolOptionsView +import org.catrobat.paintroid.ui.tools.DefaultShapeToolOptionsView +import org.catrobat.paintroid.ui.tools.DefaultFillToolOptionsView import org.catrobat.paintroid.ui.tools.DefaultTransformToolOptionsView +import org.catrobat.paintroid.ui.tools.DefaultTextToolOptionsView +import org.catrobat.paintroid.ui.tools.DefaultSprayToolOptionsView +import org.catrobat.paintroid.ui.tools.DefaultPixelToolOptionsView import org.catrobat.paintroid.ui.tools.DefaultSmudgeToolOptionsView private const val DRAW_TIME_INIT: Long = 30_000_000 @@ -203,6 +204,16 @@ class DefaultToolFactory(mainActivity: MainActivity) : ToolFactory { DRAW_TIME_INIT, mainActivity ) + ToolType.PIXEL -> PixelTool( + DefaultPixelToolOptionsView(toolLayout), + contextCallback, + toolOptionsViewController, + toolPaint, + workspace, + idlingResource, + commandManager, + DRAW_TIME_INIT + ) else -> BrushTool( DefaultBrushToolOptionsView(toolLayout), contextCallback, diff --git a/Paintroid/src/main/java/org/catrobat/paintroid/tools/implementation/PixelTool.kt b/Paintroid/src/main/java/org/catrobat/paintroid/tools/implementation/PixelTool.kt new file mode 100644 index 0000000000..96b59c695c --- /dev/null +++ b/Paintroid/src/main/java/org/catrobat/paintroid/tools/implementation/PixelTool.kt @@ -0,0 +1,81 @@ +package org.catrobat.paintroid.tools.implementation + +import android.graphics.Bitmap +import android.graphics.PointF +import androidx.annotation.VisibleForTesting +import androidx.test.espresso.idling.CountingIdlingResource +import org.catrobat.paintroid.command.CommandManager +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.PixelationToolOptionsView +import org.catrobat.paintroid.tools.options.ToolOptionsViewController +import org.catrobat.paintroid.ui.tools.DefaultPixelToolOptionsView + +class PixelTool( + pixelToolOptionsViewParam: PixelationToolOptionsView, + contextCallback: ContextCallback, + toolOptionsViewController: ToolOptionsViewController, + toolPaint: ToolPaint, + workspace: Workspace, + idlingResource: CountingIdlingResource, + commandManager: CommandManager, + override var drawTime: Long +) : BaseToolWithRectangleShape(contextCallback, toolOptionsViewController, toolPaint, workspace, idlingResource, commandManager) { + private val pixelToolOptionsView: PixelationToolOptionsView + @VisibleForTesting + @JvmField + var numPixelHeight = DefaultPixelToolOptionsView.defaultHeight + + @VisibleForTesting + @JvmField + var numPixelWidth = DefaultPixelToolOptionsView.defaulWidth + + @VisibleForTesting + @JvmField + var numCollors = DefaultPixelToolOptionsView.defaultCollor + + init { + boxHeight = workspace.height.toFloat() + boxWidth = workspace.width.toFloat() + toolPosition.x = boxWidth / 2f + toolPosition.y = boxHeight / 2f + this.pixelToolOptionsView = pixelToolOptionsViewParam + setBitmap(Bitmap.createBitmap(boxWidth.toInt(), boxHeight.toInt(), Bitmap.Config.ARGB_8888)) + toolOptionsViewController.showDelayed() + this.pixelToolOptionsView.setPixelPreviewListener(object : PixelationToolOptionsView.OnPixelationPreviewListener { + override fun setPixelWidth(widthPixels: Float) { + this@PixelTool.numPixelWidth = widthPixels + } + + override fun setPixelHeight(heightPixels: Float) { + this@PixelTool.numPixelHeight = heightPixels + } + + override fun setNumCollor(collorNum: Float) { + this@PixelTool.numCollors = collorNum + } + }) + } + + override val toolType: ToolType + get() = ToolType.PIXEL + + override fun handleUpAnimations(coordinate: PointF?) { + super.handleUp(coordinate) + } + + override fun handleDownAnimations(coordinate: PointF?) { + super.handleDown(coordinate) + } + + override fun toolPositionCoordinates(coordinate: PointF): PointF = coordinate + + // is the checkmark to run the programm + override fun onClickOnButton() { + // test if the ui works good then shoudl be enought for the 30.8 + } + + override fun resetInternalState() = Unit +} diff --git a/Paintroid/src/main/java/org/catrobat/paintroid/tools/options/PixelationToolOptionsView.kt b/Paintroid/src/main/java/org/catrobat/paintroid/tools/options/PixelationToolOptionsView.kt new file mode 100644 index 0000000000..7fa8cef471 --- /dev/null +++ b/Paintroid/src/main/java/org/catrobat/paintroid/tools/options/PixelationToolOptionsView.kt @@ -0,0 +1,14 @@ +package org.catrobat.paintroid.tools.options + +interface PixelationToolOptionsView { + + fun setPixelPreviewListener(onPixelationPreviewListener: OnPixelationPreviewListener) + + interface OnPixelationPreviewListener { + fun setPixelWidth(widthPixels: Float) + + fun setPixelHeight(heightPixels: Float) + + fun setNumCollor(collorNum: Float) + } +} diff --git a/Paintroid/src/main/java/org/catrobat/paintroid/ui/tools/DefaultPixelToolOptionsView.kt b/Paintroid/src/main/java/org/catrobat/paintroid/ui/tools/DefaultPixelToolOptionsView.kt new file mode 100644 index 0000000000..48c646d02c --- /dev/null +++ b/Paintroid/src/main/java/org/catrobat/paintroid/ui/tools/DefaultPixelToolOptionsView.kt @@ -0,0 +1,137 @@ +package org.catrobat.paintroid.ui.tools + +import android.annotation.SuppressLint +import android.text.Editable +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.SeekBar +import android.widget.SeekBar.OnSeekBarChangeListener +import androidx.annotation.VisibleForTesting +import androidx.appcompat.widget.AppCompatEditText +import androidx.appcompat.widget.AppCompatSeekBar +import org.catrobat.paintroid.R +import org.catrobat.paintroid.tools.helper.DefaultNumberRangeFilter +import org.catrobat.paintroid.tools.options.PixelationToolOptionsView.OnPixelationPreviewListener +import org.catrobat.paintroid.tools.options.PixelationToolOptionsView +import java.lang.NumberFormatException +import java.text.NumberFormat +import java.text.ParseException +import java.util.Locale + +private const val MIN_COLOR = 1 +private const val MAX_COLOR = 40 + +@SuppressLint("NotImplementedDeclaration") +class DefaultPixelToolOptionsView(rootView: ViewGroup) : PixelationToolOptionsView { + + private val pixelNumWidth: AppCompatEditText + private val pixelNumHeight: AppCompatEditText + private var colorNumText: AppCompatEditText + // private val thisLayer : Chip + // private val currentView = rootView + private var pixelChangedListener: OnPixelationPreviewListener? = null + private var pixelNumWidthWatcher: PixelToolNumTextWatcher + private var pixelNumHeightWatcher: PixelToolNumTextWatcher + private var colorNumBar: AppCompatSeekBar + + companion object { + private val TAG = DefaultPixelToolOptionsView::class.java.simpleName + const val defaulWidth = 40f + const val defaultHeight = 60f + const val defaultCollor = 20f + } + + init { + val inflater = LayoutInflater.from(rootView.context) + val pixelView = inflater.inflate(R.layout.dialog_pocketpaint_pixel, rootView, true) + colorNumBar = pixelView.findViewById(R.id.pocketpaint_pixel_color_seekbar) + colorNumText = pixelView.findViewById(R.id.pocketpaint_transform_pixel_color_text) + colorNumBar.progress = defaultCollor.toInt() + colorNumText.setText(String.format(Locale.getDefault(), "%d", colorNumBar.progress)) + pixelNumWidth = pixelView.findViewById(R.id.pocketpaint_pixel_width_value) + pixelNumHeight = pixelView.findViewById(R.id.pocketpaint_pixel_height_value) + pixelNumWidth.setText(defaulWidth.toString()) + pixelNumHeight.setText(defaultHeight.toString()) + + pixelNumWidthWatcher = object : PixelToolNumTextWatcher() { + override fun setValue(value: Float) { + pixelChangedListener?.setPixelWidth(value) + } + } + pixelNumHeightWatcher = object : PixelToolNumTextWatcher() { + override fun setValue(value: Float) { + pixelChangedListener?.setPixelHeight(value) + } + } + pixelNumWidth.addTextChangedListener(pixelNumWidthWatcher) + pixelNumHeight.addTextChangedListener(pixelNumHeightWatcher) + colorNumText.filters = arrayOf(DefaultNumberRangeFilter(MIN_COLOR, MAX_COLOR)) + colorNumText.setText(String.format(Locale.getDefault(), "%d", colorNumBar.progress)) + colorNumText.addTextChangedListener(object : TextWatcher { + override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) = + Unit + + override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) = + Unit + + override fun afterTextChanged(editable: Editable) { + val percentageTextString = colorNumText.text.toString() + val percentageTextInt: Int = try { + percentageTextString.toInt() + } catch (exp: NumberFormatException) { + exp.localizedMessage?.let { + Log.d(TAG, it) + } + MIN_COLOR + } + colorNumBar.progress = percentageTextInt + colorNumText.setSelection(colorNumText.length()) + } + }) + colorNumBar.setOnSeekBarChangeListener(object : OnSeekBarChangeListener { + override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) { + if (progress == 0) { + return + } + colorNumText.setText(String.format(Locale.getDefault(), "%d", progress)) + } + override fun onStartTrackingTouch(seekBar: SeekBar) = Unit + + override fun onStopTrackingTouch(seekBar: SeekBar) { + colorNumText.setText(String.format(Locale.getDefault(), "%d", seekBar.progress)) + } + }) + pixelView.findViewById(R.id.pocketpaint_pixel_apply_button) + .setOnClickListener { + + pixelChangedListener?.setNumCollor(colorNumBar.progress.toFloat()) + } + } + + override fun setPixelPreviewListener(onPixelationPreviewListener: PixelationToolOptionsView.OnPixelationPreviewListener) { + this.pixelChangedListener = onPixelationPreviewListener + } + + abstract class PixelToolNumTextWatcher : TextWatcher { + protected abstract fun setValue(value: Float) + override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) = Unit + + override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) = Unit + override fun afterTextChanged(editable: Editable) { + var str = editable.toString() + if (str.isEmpty()) { + str = MAX_COLOR.toString() + } + try { + val value = NumberFormat.getIntegerInstance().parse(str)?.toFloat() + value?.let { setValue(it) } + } catch (e: ParseException) { + e.message?.let { Log.e(TAG, it) } + } + } + } +} diff --git a/Paintroid/src/main/java/org/catrobat/paintroid/ui/zoomwindow/DefaultZoomWindowController.kt b/Paintroid/src/main/java/org/catrobat/paintroid/ui/zoomwindow/DefaultZoomWindowController.kt index 4f2f3d8e9c..713bd1d8f6 100644 --- a/Paintroid/src/main/java/org/catrobat/paintroid/ui/zoomwindow/DefaultZoomWindowController.kt +++ b/Paintroid/src/main/java/org/catrobat/paintroid/ui/zoomwindow/DefaultZoomWindowController.kt @@ -221,6 +221,7 @@ class DefaultZoomWindowController } override fun checkIfToolCompatibleWithZoomWindow(tool: Tool?): Constants { + return when (tool?.toolType?.name) { ToolType.HAND.name, ToolType.FILL.name, @@ -228,6 +229,7 @@ class DefaultZoomWindowController ToolType.TRANSFORM.name, ToolType.IMPORTPNG.name, ToolType.SHAPE.name, + ToolType.PIXEL.name, ToolType.TEXT.name -> Constants.NOT_COMPATIBLE ToolType.LINE.name, ToolType.CURSOR.name, diff --git a/Paintroid/src/main/res/drawable/ic_pocketpaint_tool_pixel.xml b/Paintroid/src/main/res/drawable/ic_pocketpaint_tool_pixel.xml new file mode 100644 index 0000000000..fd487bddbc --- /dev/null +++ b/Paintroid/src/main/res/drawable/ic_pocketpaint_tool_pixel.xml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + diff --git a/Paintroid/src/main/res/layout/dialog_pocketpaint_pixel.xml b/Paintroid/src/main/res/layout/dialog_pocketpaint_pixel.xml new file mode 100644 index 0000000000..e512d0b85b --- /dev/null +++ b/Paintroid/src/main/res/layout/dialog_pocketpaint_pixel.xml @@ -0,0 +1,161 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +