diff --git a/AnkiDroid/build.gradle b/AnkiDroid/build.gradle index 0af8fa573362..9bcceb60d8ae 100644 --- a/AnkiDroid/build.gradle +++ b/AnkiDroid/build.gradle @@ -421,7 +421,7 @@ dependencies { implementation libs.jsoup implementation libs.java.semver // For AnkiDroid JS API Versioning implementation libs.drakeet.drawer - implementation libs.colorpicker + implementation libs.skydoves.colorpickerview implementation libs.kotlin.reflect implementation libs.kotlin.test implementation libs.search.preference diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/Whiteboard.kt b/AnkiDroid/src/main/java/com/ichi2/anki/Whiteboard.kt index 51423ee3408b..9c3a327d036a 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/Whiteboard.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/Whiteboard.kt @@ -45,10 +45,10 @@ import com.ichi2.anki.common.time.Time import com.ichi2.anki.common.time.getTimestamp import com.ichi2.anki.dialogs.WhiteBoardWidthDialog import com.ichi2.anki.preferences.sharedPrefs +import com.ichi2.anki.ui.windows.reviewer.whiteboard.showColorPickerDialog import com.ichi2.compat.CompatHelper import com.ichi2.themes.Themes.currentTheme import com.ichi2.utils.DisplayUtils.getDisplayDimensions -import com.mrudultora.colorpicker.ColorPickerPopUp import timber.log.Timber import java.io.FileNotFoundException import kotlin.math.abs @@ -395,22 +395,7 @@ class Whiteboard( penColor = yellowPenColor } R.id.pen_color_custom -> { - ColorPickerPopUp(context).run { - setShowAlpha(true) - setDefaultColor(penColor) - setOnPickColorListener( - object : ColorPickerPopUp.OnPickColorListener { - override fun onColorPicked(color: Int) { - penColor = color - } - - override fun onCancel() { - // unused - } - }, - ) - show() - } + context.showColorPickerDialog(penColor) { penColor = it } } R.id.stroke_width -> { handleWidthChangeDialog() diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/ui/windows/reviewer/whiteboard/ColorPickerDialog.kt b/AnkiDroid/src/main/java/com/ichi2/anki/ui/windows/reviewer/whiteboard/ColorPickerDialog.kt new file mode 100644 index 000000000000..3cf0ec8d6091 --- /dev/null +++ b/AnkiDroid/src/main/java/com/ichi2/anki/ui/windows/reviewer/whiteboard/ColorPickerDialog.kt @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2025 Divyansh Kushwaha + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ + +package com.ichi2.anki.ui.windows.reviewer.whiteboard + +import android.content.Context +import com.ichi2.anki.R +import com.ichi2.utils.Dp +import com.ichi2.utils.dp +import com.ichi2.utils.negativeButton +import com.ichi2.utils.show +import com.skydoves.colorpickerview.AlphaTileView +import com.skydoves.colorpickerview.ColorEnvelope +import com.skydoves.colorpickerview.ColorPickerDialog +import com.skydoves.colorpickerview.flag.FlagView +import com.skydoves.colorpickerview.listeners.ColorEnvelopeListener +import com.skydoves.colorpickerview.sliders.AlphaSlideBar +import com.skydoves.colorpickerview.sliders.BrightnessSlideBar + +/** + * Shows a customizable color picker dialog with alpha and brightness controls. + * + * @receiver The Android context used to create the dialog. + * @param initialColor The initial color to display in the picker as an `AARRGGBB` integer. + * @param onColorSelected Callback invoked when the user confirms their color selection. + * Receives the selected color as an `AARRGGBB` integer. + */ +fun Context.showColorPickerDialog( + initialColor: Int, + onColorSelected: (Int) -> Unit, +) { + ColorPickerDialog + .Builder(this) + .show { + // Use post() so setInitialColor() runs after the view is laid out. + // This ensures the BrightnessSlideBar is fully initialized before applying + // the initial color. Calling it too early causes the slider to use its + // default position, resulting in an incorrect displayed color. + colorPickerView.post { colorPickerView.setInitialColor(initialColor) } + // Bubble showing the selected color + colorPickerView.flagView = BubbleFlag(this@showColorPickerDialog) + setPositiveButton( + R.string.dialog_ok, + ColorEnvelopeListener { envelope, _ -> + envelope?.color?.let(onColorSelected) + }, + ) + setTitle(R.string.choose_color) + negativeButton(R.string.dialog_cancel) + setBottomSpace(12.dp) + } +} + +private class BubbleFlag( + context: Context?, +) : FlagView(context, R.layout.colorpicker_flag_bubble) { + val alphaTileView: AlphaTileView = findViewById(R.id.flag_color_layout) + + override fun onRefresh(colorEnvelope: ColorEnvelope) { + alphaTileView.setPaintColor(colorEnvelope.color) + } + + override fun onFlipped(isFlipped: Boolean?) { + } +} + +/** + * Sets the margin of the bottom. this space is visible when [AlphaSlideBar] or + * [BrightnessSlideBar] is attached. + * + * @param bottomSpace the bottom margin in display pixels. + */ +private fun ColorPickerDialog.Builder.setBottomSpace(bottomSpace: Dp) = this.setBottomSpace(bottomSpace.dp.toInt()) diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/ui/windows/reviewer/whiteboard/WhiteboardFragment.kt b/AnkiDroid/src/main/java/com/ichi2/anki/ui/windows/reviewer/whiteboard/WhiteboardFragment.kt index 5273e8b1194e..b5626a9bf08c 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/ui/windows/reviewer/whiteboard/WhiteboardFragment.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/ui/windows/reviewer/whiteboard/WhiteboardFragment.kt @@ -46,7 +46,6 @@ import com.ichi2.themes.Themes import com.ichi2.utils.dp import com.ichi2.utils.increaseHorizontalPaddingOfMenuIcons import com.ichi2.utils.toRGBAHex -import com.mrudultora.colorpicker.ColorPickerPopUp import dev.androidbroadcast.vbpd.viewBinding import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.launchIn @@ -273,21 +272,11 @@ class WhiteboardFragment : * Shows a popup for adding a new brush color. */ private fun showAddColorDialog() { - ColorPickerPopUp(context).run { - setShowAlpha(true) - setDefaultColor(viewModel.brushColor.value) - setOnPickColorListener( - object : ColorPickerPopUp.OnPickColorListener { - override fun onColorPicked(color: Int) { - Timber.i("Added brush with color ${color.toRGBAHex()}") - viewModel.addBrush(color) - } - - override fun onCancel() {} - }, - ) - show() - } + requireContext() + .showColorPickerDialog(viewModel.brushColor.value) { color -> + Timber.i("Added brush with color ${color.toRGBAHex()}") + viewModel.addBrush(color) + } } /** @@ -366,19 +355,11 @@ class WhiteboardFragment : * Shows a color picker popup to change the active brush's color. */ private fun showChangeColorDialog() { - ColorPickerPopUp(requireContext()) - .setShowAlpha(true) - .setDefaultColor(viewModel.brushColor.value) - .setOnPickColorListener( - object : ColorPickerPopUp.OnPickColorListener { - override fun onColorPicked(color: Int) { - viewModel.updateBrushColor(color) - brushConfigPopup?.dismiss() - } - - override fun onCancel() {} - }, - ).show() + requireContext() + .showColorPickerDialog(viewModel.brushColor.value) { color -> + viewModel.updateBrushColor(color) + brushConfigPopup?.dismiss() + } } /** diff --git a/AnkiDroid/src/main/java/com/ichi2/utils/AlertDialogFacade.kt b/AnkiDroid/src/main/java/com/ichi2/utils/AlertDialogFacade.kt index f6285fbb01ad..70e9bab974d7 100644 --- a/AnkiDroid/src/main/java/com/ichi2/utils/AlertDialogFacade.kt +++ b/AnkiDroid/src/main/java/com/ichi2/utils/AlertDialogFacade.kt @@ -144,9 +144,9 @@ fun AlertDialog.Builder.cancelable(cancelable: Boolean): AlertDialog.Builder = t * Executes the provided block, then creates an [AlertDialog] with the arguments supplied * and immediately displays the dialog */ -inline fun AlertDialog.Builder.show( +inline fun T.show( enableEnterKeyHandler: Boolean = false, // Make it opt-in - block: AlertDialog.Builder.() -> Unit, + block: T.() -> Unit, ): AlertDialog { this.apply { block() } val dialog = this.show() @@ -187,7 +187,7 @@ fun AlertDialog.Builder.createAndApply(block: AlertDialog.() -> Unit): AlertDial /** * Executes [block] on the [AlertDialog.Builder] instance and returns the initialized [AlertDialog]. */ -fun AlertDialog.Builder.create(block: AlertDialog.Builder.() -> Unit): AlertDialog { +fun T.create(block: T.() -> Unit): AlertDialog { block() return create() } diff --git a/AnkiDroid/src/main/res/drawable/pin_shape.xml b/AnkiDroid/src/main/res/drawable/pin_shape.xml new file mode 100644 index 000000000000..cbf9f1f311f8 --- /dev/null +++ b/AnkiDroid/src/main/res/drawable/pin_shape.xml @@ -0,0 +1,29 @@ + + + + + diff --git a/AnkiDroid/src/main/res/layout/colorpicker_flag_bubble.xml b/AnkiDroid/src/main/res/layout/colorpicker_flag_bubble.xml new file mode 100644 index 000000000000..781a0e628a6e --- /dev/null +++ b/AnkiDroid/src/main/res/layout/colorpicker_flag_bubble.xml @@ -0,0 +1,31 @@ + + + + + + diff --git a/AnkiDroid/src/main/res/values/03-dialogs.xml b/AnkiDroid/src/main/res/values/03-dialogs.xml index cf71998757d6..ab7e128db541 100644 --- a/AnkiDroid/src/main/res/values/03-dialogs.xml +++ b/AnkiDroid/src/main/res/values/03-dialogs.xml @@ -268,4 +268,6 @@ also changes the interval of the card" The text to speech engine %1$s does not support the following language: %2$s Change engine Voice options + + Choose color diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8618570d8882..afff7dd0c10f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -62,7 +62,8 @@ androidxWork = "2.11.0" ankiBackend = '0.1.62-anki25.09.2' autoService = "1.1.1" autoServiceAnnotations = "1.1.1" -colorpicker = "1.2.0" +# https://github.com/skydoves/ColorPickerView/releases +colorPickerView = "2.3.0" # https://commons.apache.org/proper/commons-collections/changes.html commonsCollections4 = "4.5.0" # https://commons.apache.org/proper/commons-compress/changes-report.html @@ -143,7 +144,7 @@ androidx-recyclerview = { module = "androidx.recyclerview:recyclerview", version auto-service = { module = "com.google.auto.service:auto-service", version.ref = "autoService" } auto-service-annotations = { module = "com.google.auto.service:auto-service-annotations", version.ref = "autoServiceAnnotations" } jetbrains-annotations = { module = "org.jetbrains:annotations", version.ref = "jetbrainsAnnotations" } -colorpicker = { module = "com.github.mrudultora:Colorpicker", version.ref = "colorpicker" } +skydoves-colorpickerview = { module = "com.github.skydoves:colorpickerview", version.ref = "colorPickerView" } commons-io = { module = "commons-io:commons-io", version.ref = "commonsIo" } commons-collections4 = { module = "org.apache.commons:commons-collections4", version.ref = "commonsCollections4" } commons-compress = { module = "org.apache.commons:commons-compress", version.ref = "commonsCompress" }