From a29374617e4e207b54319e97d975922c9903c8f3 Mon Sep 17 00:00:00 2001 From: Brayan Oliveira <69634269+brayandso@users.noreply.github.com> Date: Sun, 15 Dec 2024 13:43:45 -0300 Subject: [PATCH 01/29] refactor: remove MappableBinding.Version unused. Something similar may be used in the future, but it will likely be different --- .../java/com/ichi2/anki/reviewer/MappableBinding.kt | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/reviewer/MappableBinding.kt b/AnkiDroid/src/main/java/com/ichi2/anki/reviewer/MappableBinding.kt index 51fec49cbfa0..c9cdda5f0a17 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/reviewer/MappableBinding.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/reviewer/MappableBinding.kt @@ -165,11 +165,6 @@ class MappableBinding( } } - /** the serialisation version */ - enum class Version { - ONE, - } - companion object { const val PREF_SEPARATOR = '|' @@ -185,12 +180,8 @@ class MappableBinding( .mapNotNull { it.toPreferenceString() } .joinToString(prefix = "1/", separator = PREF_SEPARATOR.toString()) - @Suppress("UNUSED_PARAMETER") @CheckResult - fun fromString( - s: String, - v: Version = Version.ONE, - ): MappableBinding? { + fun fromString(s: String): MappableBinding? { if (s.isEmpty()) { return null } From 216de0bd6a1c61773d9658a6828e4f87edbfaf2e Mon Sep 17 00:00:00 2001 From: Brayan Oliveira <69634269+brayandso@users.noreply.github.com> Date: Tue, 24 Dec 2024 06:24:46 -0300 Subject: [PATCH 02/29] refactor: MappableBinding This makes `MappableBinding` abstract and moves the logic of MappableBinding::Screen to `MappableBinding` subclasses. That reduces boilerplate and simplifies the code a bit --- .../ichi2/anki/cardviewer/ViewerCommand.kt | 21 +- .../ichi2/anki/reviewer/MappableBinding.kt | 201 ++++++++---------- .../ichi2/anki/reviewer/PeripheralKeymap.kt | 8 +- .../servicelayer/PreferenceUpgradeService.kt | 4 +- .../ichi2/preferences/ControlPreference.kt | 26 +-- .../ichi2/anki/ReviewerKeyboardInputTest.kt | 6 +- .../com/ichi2/anki/ReviewerNoParamTest.kt | 6 +- .../anki/cardviewer/GestureProcessorTest.kt | 5 +- .../ichi2/anki/reviewer/BindingAndroidTest.kt | 5 +- .../anki/reviewer/MappableBindingTest.kt | 2 +- .../UpgradeGesturesToControlsTest.kt | 8 +- .../com/ichi2/ui/BindingPreferenceTest.kt | 12 +- 12 files changed, 126 insertions(+), 178 deletions(-) diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/cardviewer/ViewerCommand.kt b/AnkiDroid/src/main/java/com/ichi2/anki/cardviewer/ViewerCommand.kt index 08f9c73ae4d8..e8635969449f 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/cardviewer/ViewerCommand.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/cardviewer/ViewerCommand.kt @@ -28,7 +28,7 @@ import com.ichi2.anki.reviewer.CardSide import com.ichi2.anki.reviewer.MappableBinding import com.ichi2.anki.reviewer.MappableBinding.Companion.fromPreference import com.ichi2.anki.reviewer.MappableBinding.Companion.toPreferenceString -import com.ichi2.anki.reviewer.MappableBinding.Screen +import com.ichi2.anki.reviewer.ReviewerBinding /** Abstraction: Discuss moving many of these to 'Reviewer' */ enum class ViewerCommand( @@ -137,17 +137,6 @@ enum class ViewerCommand( // If we use the serialised format, then this adds additional coupling to the properties. val defaultValue: List get() { - // all of the default commands are currently for the Reviewer - fun keyCode( - keycode: Int, - side: CardSide, - modifierKeys: ModifierKeys = ModifierKeys.none(), - ) = keyCode(keycode, Screen.Reviewer(side), modifierKeys) - - fun unicode( - c: Char, - side: CardSide, - ) = unicode(c, Screen.Reviewer(side)) return when (this) { FLIP_OR_ANSWER_EASE1 -> listOf( @@ -256,14 +245,14 @@ enum class ViewerCommand( private fun keyCode( keycode: Int, - screen: Screen, + side: CardSide, keys: ModifierKeys = ModifierKeys.none(), - ): MappableBinding = MappableBinding(keyCode(keys, keycode), screen) + ): ReviewerBinding = ReviewerBinding(keyCode(keys, keycode), side) private fun unicode( c: Char, - screen: Screen, - ): MappableBinding = MappableBinding(unicode(c), screen) + side: CardSide, + ): ReviewerBinding = ReviewerBinding(unicode(c), side) fun interface CommandProcessor { /** diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/reviewer/MappableBinding.kt b/AnkiDroid/src/main/java/com/ichi2/anki/reviewer/MappableBinding.kt index c9cdda5f0a17..45899e8a75d0 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/reviewer/MappableBinding.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/reviewer/MappableBinding.kt @@ -36,9 +36,9 @@ import java.util.Objects * Also defines equality over bindings. * https://stackoverflow.com/questions/5453226/java-need-a-hash-map-where-one-supplies-a-function-to-do-the-hashing */ -class MappableBinding( +@Suppress("EqualsOrHashCode") +sealed class MappableBinding( val binding: Binding, - val screen: Screen, ) { val isKey: Boolean get() = binding is KeyBinding @@ -47,39 +47,34 @@ class MappableBinding( if (other == null) return false val otherBinding = (other as MappableBinding).binding - val bindingEquals = - when { - binding is KeyCode && otherBinding is KeyCode -> binding.keycode == otherBinding.keycode && modifierEquals(otherBinding) - binding is UnicodeCharacter && otherBinding is UnicodeCharacter -> { - binding.unicodeCharacter == otherBinding.unicodeCharacter && - modifierEquals(otherBinding) - } - binding is GestureInput && otherBinding is GestureInput -> binding.gesture == otherBinding.gesture - binding is AxisButtonBinding && otherBinding is AxisButtonBinding -> { - binding.axis == otherBinding.axis && binding.threshold == otherBinding.threshold - } - else -> false + + return when { + binding is KeyCode && otherBinding is KeyCode -> binding.keycode == otherBinding.keycode && modifierEquals(otherBinding) + binding is UnicodeCharacter && otherBinding is UnicodeCharacter -> { + binding.unicodeCharacter == otherBinding.unicodeCharacter && + modifierEquals(otherBinding) } - if (!bindingEquals) { - return false + binding is GestureInput && otherBinding is GestureInput -> binding.gesture == otherBinding.gesture + binding is AxisButtonBinding && otherBinding is AxisButtonBinding -> { + binding.axis == otherBinding.axis && binding.threshold == otherBinding.threshold + } + else -> false } - - return screen.screenEquals(other.screen) } - override fun hashCode(): Int { - // don't include the modifierKeys or mSide - val bindingHash = - when (binding) { - is KeyCode -> binding.keycode - is UnicodeCharacter -> binding.unicodeCharacter - is GestureInput -> binding.gesture - is AxisButtonBinding -> hash(binding.axis.motionEventValue, binding.threshold.toInt()) - else -> 0 - } - return Objects.hash(bindingHash, screen.prefix) + protected fun getBindingHash(): Any { + // don't include the modifierKeys + return when (binding) { + is KeyCode -> binding.keycode + is UnicodeCharacter -> binding.unicodeCharacter + is GestureInput -> binding.gesture + is AxisButtonBinding -> hash(binding.axis.motionEventValue, binding.threshold.toInt()) + else -> 0 + } } + abstract override fun hashCode(): Int + private fun modifierEquals(otherBinding: KeyBinding): Boolean { // equals allowing subclasses val keys = otherBinding.modifierKeys @@ -90,90 +85,13 @@ class MappableBinding( // allow subclasses to work - a subclass which overrides shiftMatches will return true on one of the tests } - fun toDisplayString(context: Context): String = screen.toDisplayString(context, binding) - - fun toPreferenceString(): String? = screen.toPreferenceString(binding) - - abstract class Screen private constructor( - val prefix: Char, - ) { - abstract fun toPreferenceString(binding: Binding): String? - - abstract fun toDisplayString( - context: Context, - binding: Binding, - ): String - - abstract fun screenEquals(otherScreen: Screen): Boolean + abstract fun toDisplayString(context: Context): String - class Reviewer( - val side: CardSide, - ) : Screen('r') { - override fun toPreferenceString(binding: Binding): String? { - if (!binding.isValid) { - return null - } - val s = StringBuilder() - s.append(prefix) - s.append(binding.toString()) - // don't serialise problematic bindings - if (s.isEmpty()) { - return null - } - when (side) { - CardSide.QUESTION -> s.append('0') - CardSide.ANSWER -> s.append('1') - CardSide.BOTH -> s.append('2') - } - return s.toString() - } - - override fun toDisplayString( - context: Context, - binding: Binding, - ): String { - val formatString = - when (side) { - CardSide.QUESTION -> context.getString(R.string.display_binding_card_side_question) - CardSide.ANSWER -> context.getString(R.string.display_binding_card_side_answer) - CardSide.BOTH -> context.getString(R.string.display_binding_card_side_both) // intentionally no prefix - } - return String.format(formatString, binding.toDisplayString(context)) - } - - override fun screenEquals(otherScreen: Screen): Boolean { - val other: Reviewer = otherScreen as? Reviewer ?: return false - - return side === CardSide.BOTH || - other.side === CardSide.BOTH || - side === other.side - } - - companion object { - fun fromString(s: String): MappableBinding { - val binding = s.substring(0, s.length - 1) - val b = Binding.fromString(binding) - val side = - when (s[s.length - 1]) { - '0' -> CardSide.QUESTION - '1' -> CardSide.ANSWER - else -> CardSide.BOTH - } - return MappableBinding(b, Reviewer(side)) - } - } - } - } + abstract fun toPreferenceString(): String? companion object { const val PREF_SEPARATOR = '|' - @CheckResult - fun fromGesture( - gesture: Gesture, - screen: (CardSide) -> Screen, - ): MappableBinding = MappableBinding(GestureInput(gesture), screen(CardSide.BOTH)) - @CheckResult fun List.toPreferenceString(): String = this @@ -188,7 +106,7 @@ class MappableBinding( return try { // the prefix of the serialized when (s[0]) { - 'r' -> Screen.Reviewer.fromString(s.substring(1)) + 'r' -> ReviewerBinding.fromString(s.substring(1)) else -> null } } catch (e: Exception) { @@ -232,6 +150,65 @@ class MappableBinding( } } -@Suppress("UnusedReceiverParameter") -val ViewerCommand.screenBuilder: (CardSide) -> MappableBinding.Screen - get() = { it -> MappableBinding.Screen.Reviewer(it) } +class ReviewerBinding( + binding: Binding, + val side: CardSide, +) : MappableBinding(binding) { + override fun equals(other: Any?): Boolean { + if (!super.equals(other)) return false + if (other !is ReviewerBinding) return false + + return side === CardSide.BOTH || + other.side === CardSide.BOTH || + side === other.side + } + + override fun hashCode(): Int = Objects.hash(getBindingHash(), 'r') + + override fun toPreferenceString(): String? { + if (!binding.isValid) { + return null + } + val s = + StringBuilder() + .append('r') + .append(binding.toString()) + // don't serialise problematic bindings + if (s.isEmpty()) { + return null + } + when (side) { + CardSide.QUESTION -> s.append('0') + CardSide.ANSWER -> s.append('1') + CardSide.BOTH -> s.append('2') + } + return s.toString() + } + + override fun toDisplayString(context: Context): String { + val formatString = + when (side) { + CardSide.QUESTION -> context.getString(R.string.display_binding_card_side_question) + CardSide.ANSWER -> context.getString(R.string.display_binding_card_side_answer) + CardSide.BOTH -> context.getString(R.string.display_binding_card_side_both) // intentionally no prefix + } + return String.format(formatString, binding.toDisplayString(context)) + } + + companion object { + fun fromString(s: String): MappableBinding { + val binding = s.substring(0, s.length - 1) + val b = Binding.fromString(binding) + val side = + when (s[s.length - 1]) { + '0' -> CardSide.QUESTION + '1' -> CardSide.ANSWER + else -> CardSide.BOTH + } + return ReviewerBinding(b, side) + } + + @CheckResult + fun fromGesture(gesture: Gesture): ReviewerBinding = ReviewerBinding(GestureInput(gesture), CardSide.BOTH) + } +} diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/reviewer/PeripheralKeymap.kt b/AnkiDroid/src/main/java/com/ichi2/anki/reviewer/PeripheralKeymap.kt index 6b0081c26361..5330b06bedf0 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/reviewer/PeripheralKeymap.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/reviewer/PeripheralKeymap.kt @@ -24,7 +24,6 @@ import com.ichi2.anki.preferences.sharedPrefs import com.ichi2.anki.reviewer.Binding.Companion.possibleKeyBindings import com.ichi2.anki.reviewer.CardSide.Companion.fromAnswer import com.ichi2.anki.reviewer.MappableBinding.Companion.fromPreference -import com.ichi2.anki.reviewer.MappableBinding.Screen /** Accepts peripheral input, mapping via various keybinding strategies, * and converting them to commands for the Reviewer. */ @@ -32,7 +31,7 @@ class PeripheralKeymap( reviewerUi: ReviewerUi, commandProcessor: ViewerCommand.CommandProcessor, ) { - private val keyMap: KeyMap = KeyMap(commandProcessor, reviewerUi) { Screen.Reviewer(it) } + private val keyMap: KeyMap = KeyMap(commandProcessor, reviewerUi) private var hasSetup = false fun setup() { @@ -53,7 +52,7 @@ class PeripheralKeymap( ) { val bindings = fromPreference(preferences, command) - .filter { it.screen is Screen.Reviewer } + .filterIsInstance() for (b in bindings) { if (!b.isKey) { continue @@ -81,7 +80,6 @@ class PeripheralKeymap( class KeyMap( private val processor: ViewerCommand.CommandProcessor, private val reviewerUI: ReviewerUi, - private val screenBuilder: (CardSide) -> Screen, ) { val bindingMap = HashMap() @@ -94,7 +92,7 @@ class PeripheralKeymap( val bindings = possibleKeyBindings(event!!) val side = fromAnswer(reviewerUI.isDisplayingAnswer) for (b in bindings) { - val binding = MappableBinding(b, screenBuilder(side)) + val binding = ReviewerBinding(b, side) val command = bindingMap[binding] ?: continue ret = ret or processor.executeCommand(command, fromGesture = null) } diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/servicelayer/PreferenceUpgradeService.kt b/AnkiDroid/src/main/java/com/ichi2/anki/servicelayer/PreferenceUpgradeService.kt index 377411a05cd5..c8b1362f7146 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/servicelayer/PreferenceUpgradeService.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/servicelayer/PreferenceUpgradeService.kt @@ -36,7 +36,7 @@ import com.ichi2.anki.reviewer.CardSide import com.ichi2.anki.reviewer.FullScreenMode import com.ichi2.anki.reviewer.MappableBinding import com.ichi2.anki.reviewer.MappableBinding.Companion.toPreferenceString -import com.ichi2.anki.reviewer.screenBuilder +import com.ichi2.anki.reviewer.ReviewerBinding import com.ichi2.libanki.Consts import com.ichi2.utils.HashUtil.hashSetInit import timber.log.Timber @@ -385,7 +385,7 @@ object PreferenceUpgradeService { Timber.i("Moving preference from '%s' to '%s'", oldGesturePreferenceKey, command.preferenceKey) // add to the binding_COMMANDNAME preference - val mappableBinding = MappableBinding(binding, command.screenBuilder(CardSide.BOTH)) + val mappableBinding = ReviewerBinding(binding, CardSide.BOTH) command.addBindingAtEnd(preferences, mappableBinding) } } diff --git a/AnkiDroid/src/main/java/com/ichi2/preferences/ControlPreference.kt b/AnkiDroid/src/main/java/com/ichi2/preferences/ControlPreference.kt index b1dc32c9263b..04e27d35e7b8 100644 --- a/AnkiDroid/src/main/java/com/ichi2/preferences/ControlPreference.kt +++ b/AnkiDroid/src/main/java/com/ichi2/preferences/ControlPreference.kt @@ -33,10 +33,8 @@ import com.ichi2.anki.dialogs.WarningDisplay import com.ichi2.anki.preferences.sharedPrefs import com.ichi2.anki.reviewer.CardSide import com.ichi2.anki.reviewer.MappableBinding -import com.ichi2.anki.reviewer.MappableBinding.Companion.fromGesture import com.ichi2.anki.reviewer.MappableBinding.Companion.toPreferenceString -import com.ichi2.anki.reviewer.MappableBinding.Screen -import com.ichi2.anki.reviewer.screenBuilder +import com.ichi2.anki.reviewer.ReviewerBinding import com.ichi2.anki.showThemedToast import com.ichi2.ui.AxisPicker import com.ichi2.ui.KeyPicker @@ -75,9 +73,6 @@ class ControlPreference : ListPreference { @Suppress("unused") constructor(context: Context) : super(context) - val screenBuilder: (CardSide) -> Screen - get() = ViewerCommand.fromPreferenceKey(key).screenBuilder - private fun refreshEntries() { val entryTitles: MutableList = ArrayList() val entryIndices: MutableList = ArrayList() @@ -125,11 +120,7 @@ class ControlPreference : ListPreference { positiveButton(R.string.dialog_ok) { val gesture = gesturePicker.getGesture() ?: return@positiveButton - val mappableBinding = - fromGesture( - gesture, - screenBuilder, - ) + val mappableBinding = ReviewerBinding.fromGesture(gesture) if (bindingIsUsedOnAnotherCommand(mappableBinding)) { showDialogToReplaceBinding(mappableBinding, context.getString(R.string.binding_replace_gesture), it) } else { @@ -141,7 +132,7 @@ class ControlPreference : ListPreference { customView(view = gesturePicker) gesturePicker.onGestureChanged { gesture -> - warnIfBindingIsUsed(fromGesture(gesture, screenBuilder), gesturePicker) + warnIfBindingIsUsed(ReviewerBinding.fromGesture(gesture), gesturePicker) } } } @@ -155,10 +146,7 @@ class ControlPreference : ListPreference { // When the user presses a key keyPicker.setBindingChangedListener { binding -> val mappableBinding = - MappableBinding( - binding, - screenBuilder(CardSide.BOTH), - ) + ReviewerBinding(binding, CardSide.BOTH) warnIfBindingIsUsed(mappableBinding, keyPicker) } @@ -166,7 +154,7 @@ class ControlPreference : ListPreference { val binding = keyPicker.getBinding() ?: return@positiveButton // Use CardSide.BOTH as placeholder just to check if binding exists CardSideSelectionDialog.displayInstance(context) { side -> - val mappableBinding = MappableBinding(binding, screenBuilder(side)) + val mappableBinding = ReviewerBinding(binding, side) if (bindingIsUsedOnAnotherCommand(mappableBinding)) { showDialogToReplaceBinding(mappableBinding, context.getString(R.string.binding_replace_key), it) } else { @@ -204,10 +192,10 @@ class ControlPreference : ListPreference { .create() axisPicker.setBindingChangedListener { binding -> - showToastIfBindingIsUsed(MappableBinding(binding, screenBuilder(CardSide.BOTH))) + showToastIfBindingIsUsed(ReviewerBinding(binding, CardSide.BOTH)) // Use CardSide.BOTH as placeholder just to check if binding exists CardSideSelectionDialog.displayInstance(context) { side -> - val mappableBinding = MappableBinding(binding, screenBuilder(side)) + val mappableBinding = ReviewerBinding(binding, side) if (bindingIsUsedOnAnotherCommand(mappableBinding)) { showDialogToReplaceBinding(mappableBinding, context.getString(R.string.binding_replace_key), dialog) } else { diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/ReviewerKeyboardInputTest.kt b/AnkiDroid/src/test/java/com/ichi2/anki/ReviewerKeyboardInputTest.kt index c4ff6b5ebad5..8ee3b65519b5 100644 --- a/AnkiDroid/src/test/java/com/ichi2/anki/ReviewerKeyboardInputTest.kt +++ b/AnkiDroid/src/test/java/com/ichi2/anki/ReviewerKeyboardInputTest.kt @@ -44,7 +44,7 @@ import com.ichi2.anki.cardviewer.ViewerCommand import com.ichi2.anki.reviewer.Binding.Companion.keyCode import com.ichi2.anki.reviewer.Binding.ModifierKeys import com.ichi2.anki.reviewer.CardSide -import com.ichi2.anki.reviewer.MappableBinding +import com.ichi2.anki.reviewer.ReviewerBinding import com.ichi2.libanki.Card import kotlinx.coroutines.Job import org.hamcrest.MatcherAssert.assertThat @@ -182,7 +182,7 @@ class ReviewerKeyboardInputTest : RobolectricTest() { fun pressingZShouldUndoIfAvailable() { ViewerCommand.UNDO.addBinding( sharedPrefs(), - MappableBinding(keyCode(KEYCODE_Z, ModifierKeys.none()), MappableBinding.Screen.Reviewer(CardSide.BOTH)), + ReviewerBinding(keyCode(KEYCODE_Z, ModifierKeys.none()), CardSide.BOTH), ) val underTest = KeyboardInputTestReviewer.displayingAnswer().withUndoAvailable(true) underTest.handleAndroidKeyPress(KEYCODE_Z) @@ -193,7 +193,7 @@ class ReviewerKeyboardInputTest : RobolectricTest() { fun pressingZShouldNotUndoIfNotAvailable() { ViewerCommand.UNDO.addBinding( sharedPrefs(), - MappableBinding(keyCode(KEYCODE_Z, ModifierKeys.none()), MappableBinding.Screen.Reviewer(CardSide.BOTH)), + ReviewerBinding(keyCode(KEYCODE_Z, ModifierKeys.none()), CardSide.BOTH), ) val underTest = KeyboardInputTestReviewer.displayingAnswer().withUndoAvailable(false) underTest.handleUnicodeKeyPress('z') diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/ReviewerNoParamTest.kt b/AnkiDroid/src/test/java/com/ichi2/anki/ReviewerNoParamTest.kt index e5c1968a9182..d640e88169e0 100644 --- a/AnkiDroid/src/test/java/com/ichi2/anki/ReviewerNoParamTest.kt +++ b/AnkiDroid/src/test/java/com/ichi2/anki/ReviewerNoParamTest.kt @@ -33,7 +33,7 @@ import com.ichi2.anki.reviewer.FullScreenMode import com.ichi2.anki.reviewer.FullScreenMode.Companion.setPreference import com.ichi2.anki.reviewer.MappableBinding import com.ichi2.anki.reviewer.MappableBinding.Companion.toPreferenceString -import com.ichi2.anki.reviewer.MappableBinding.Screen +import com.ichi2.anki.reviewer.ReviewerBinding import com.ichi2.libanki.Consts import com.ichi2.libanki.DeckId import com.ichi2.testutils.common.Flaky @@ -316,9 +316,7 @@ class ReviewerNoParamTest : RobolectricTest() { val prefs = targetContext.sharedPrefs() ViewerCommand.FLIP_OR_ANSWER_EASE1.addBinding( prefs, - MappableBinding.fromGesture(gesture) { - Screen.Reviewer(it) - }, + ReviewerBinding.fromGesture(gesture), ) } diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/cardviewer/GestureProcessorTest.kt b/AnkiDroid/src/test/java/com/ichi2/anki/cardviewer/GestureProcessorTest.kt index 721e9863ffe3..a82311dab705 100644 --- a/AnkiDroid/src/test/java/com/ichi2/anki/cardviewer/GestureProcessorTest.kt +++ b/AnkiDroid/src/test/java/com/ichi2/anki/cardviewer/GestureProcessorTest.kt @@ -18,9 +18,8 @@ package com.ichi2.anki.cardviewer import android.content.SharedPreferences import android.view.ViewConfiguration import com.ichi2.anki.AnkiDroidApp -import com.ichi2.anki.reviewer.MappableBinding import com.ichi2.anki.reviewer.MappableBinding.Companion.toPreferenceString -import com.ichi2.anki.reviewer.screenBuilder +import com.ichi2.anki.reviewer.ReviewerBinding import io.mockk.every import io.mockk.mockk import io.mockk.mockkStatic @@ -53,7 +52,7 @@ class GestureProcessorTest : ViewerCommand.CommandProcessor { fun integrationTest() { val prefs = mockk(relaxed = true) every { prefs.getString(ViewerCommand.SHOW_ANSWER.preferenceKey, null) } returns - listOf(MappableBinding.fromGesture(Gesture.TAP_CENTER, ViewerCommand.SHOW_ANSWER.screenBuilder)) + listOf(ReviewerBinding.fromGesture(Gesture.TAP_CENTER)) .toPreferenceString() every { prefs.getBoolean("gestureCornerTouch", any()) } returns true sut.init(prefs) diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/reviewer/BindingAndroidTest.kt b/AnkiDroid/src/test/java/com/ichi2/anki/reviewer/BindingAndroidTest.kt index 857c5d8ed7bd..97a4f7ae2f2a 100644 --- a/AnkiDroid/src/test/java/com/ichi2/anki/reviewer/BindingAndroidTest.kt +++ b/AnkiDroid/src/test/java/com/ichi2/anki/reviewer/BindingAndroidTest.kt @@ -23,7 +23,6 @@ import com.ichi2.anki.cardviewer.Gesture import com.ichi2.anki.reviewer.Binding.ModifierKeys.Companion.alt import com.ichi2.anki.reviewer.Binding.ModifierKeys.Companion.ctrl import com.ichi2.anki.reviewer.Binding.ModifierKeys.Companion.shift -import com.ichi2.anki.reviewer.MappableBinding.Screen.Reviewer import org.junit.Assert.assertEquals import org.junit.Test import org.junit.runner.RunWith @@ -83,8 +82,8 @@ class BindingAndroidTest : RobolectricTest() { fst: Binding, snd: Binding, ) { - val first = MappableBinding(fst, Reviewer(CardSide.BOTH)) - val second = MappableBinding(snd, Reviewer(CardSide.BOTH)) + val first = ReviewerBinding(fst, CardSide.BOTH) + val second = ReviewerBinding(snd, CardSide.BOTH) assertEquals(first, second) } } diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/reviewer/MappableBindingTest.kt b/AnkiDroid/src/test/java/com/ichi2/anki/reviewer/MappableBindingTest.kt index 9a1f0235919f..6e118328bec3 100644 --- a/AnkiDroid/src/test/java/com/ichi2/anki/reviewer/MappableBindingTest.kt +++ b/AnkiDroid/src/test/java/com/ichi2/anki/reviewer/MappableBindingTest.kt @@ -54,5 +54,5 @@ class MappableBindingTest { @Suppress("SameParameterValue") private fun unicodeCharacter(char: Char) = fromBinding(BindingTest.unicodeCharacter(char)) - private fun fromBinding(binding: Binding): Any = MappableBinding(binding, MappableBinding.Screen.Reviewer(CardSide.BOTH)) + private fun fromBinding(binding: Binding): Any = ReviewerBinding(binding, CardSide.BOTH) } diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/servicemodel/UpgradeGesturesToControlsTest.kt b/AnkiDroid/src/test/java/com/ichi2/anki/servicemodel/UpgradeGesturesToControlsTest.kt index ad5e6a628e6d..c0143e793589 100644 --- a/AnkiDroid/src/test/java/com/ichi2/anki/servicemodel/UpgradeGesturesToControlsTest.kt +++ b/AnkiDroid/src/test/java/com/ichi2/anki/servicemodel/UpgradeGesturesToControlsTest.kt @@ -22,7 +22,7 @@ import com.ichi2.anki.cardviewer.ViewerCommand import com.ichi2.anki.reviewer.Binding.Companion.keyCode import com.ichi2.anki.reviewer.CardSide import com.ichi2.anki.reviewer.MappableBinding -import com.ichi2.anki.reviewer.MappableBinding.Screen.Reviewer +import com.ichi2.anki.reviewer.ReviewerBinding import com.ichi2.anki.servicelayer.PreferenceUpgradeService.PreferenceUpgrade.Companion.UPGRADE_VERSION_PREF_KEY import com.ichi2.anki.servicelayer.PreferenceUpgradeService.PreferenceUpgrade.UpgradeGesturesToControls import org.hamcrest.CoreMatchers.equalTo @@ -88,7 +88,7 @@ class UpgradeGesturesToControlsTest( val binding = fromPreference.first() assertThat("should be a key binding", binding.isKey, equalTo(true)) - assertThat("binding should match", binding, equalTo(MappableBinding(keyCode(testData.keyCode), Reviewer(CardSide.BOTH)))) + assertThat("binding should match", binding, equalTo(ReviewerBinding(keyCode(testData.keyCode), CardSide.BOTH))) } @Test @@ -203,8 +203,8 @@ class UpgradeGesturesToControlsTest( val oldCommandPreferenceStrings: HashMap = hashMapOf(*UpgradeGesturesToControls().oldCommandValues.map { Pair(it.value, it.key.toString()) }.toTypedArray()) - private val volume_up_binding = MappableBinding(keyCode(KEYCODE_VOLUME_UP), Reviewer(CardSide.BOTH)) - private val volume_down_binding = MappableBinding(keyCode(KEYCODE_VOLUME_DOWN), Reviewer(CardSide.BOTH)) + private val volume_up_binding = ReviewerBinding(keyCode(KEYCODE_VOLUME_UP), CardSide.BOTH) + private val volume_down_binding = ReviewerBinding(keyCode(KEYCODE_VOLUME_DOWN), CardSide.BOTH) @JvmStatic @ParameterizedRobolectricTestRunner.Parameters(name = "{index}: isValid({0})={1}") diff --git a/AnkiDroid/src/test/java/com/ichi2/ui/BindingPreferenceTest.kt b/AnkiDroid/src/test/java/com/ichi2/ui/BindingPreferenceTest.kt index e54da03ba4bf..a8d1d5d35943 100644 --- a/AnkiDroid/src/test/java/com/ichi2/ui/BindingPreferenceTest.kt +++ b/AnkiDroid/src/test/java/com/ichi2/ui/BindingPreferenceTest.kt @@ -22,7 +22,7 @@ import com.ichi2.anki.reviewer.Binding import com.ichi2.anki.reviewer.CardSide import com.ichi2.anki.reviewer.MappableBinding import com.ichi2.anki.reviewer.MappableBinding.Companion.toPreferenceString -import com.ichi2.anki.reviewer.MappableBinding.Screen.Reviewer +import com.ichi2.anki.reviewer.ReviewerBinding import org.junit.Assert.assertEquals import org.junit.Test import org.junit.runner.RunWith @@ -49,11 +49,11 @@ class BindingPreferenceTest { private fun getSampleBindings(): List = listOf( - MappableBinding(Binding.unicode('a'), Reviewer(CardSide.BOTH)), - MappableBinding(Binding.unicode(' '), Reviewer(CardSide.ANSWER)), + ReviewerBinding(Binding.unicode('a'), CardSide.BOTH), + ReviewerBinding(Binding.unicode(' '), CardSide.ANSWER), // this one is important: ensure that "|" as a unicode char can't be used - MappableBinding(Binding.unicode(Binding.FORBIDDEN_UNICODE_CHAR), Reviewer(CardSide.QUESTION)), - MappableBinding(Binding.gesture(Gesture.LONG_TAP), Reviewer(CardSide.BOTH)), - MappableBinding(Binding.keyCode(12), Reviewer(CardSide.BOTH)), + ReviewerBinding(Binding.unicode(Binding.FORBIDDEN_UNICODE_CHAR), CardSide.QUESTION), + ReviewerBinding(Binding.gesture(Gesture.LONG_TAP), CardSide.BOTH), + ReviewerBinding(Binding.keyCode(12), CardSide.BOTH), ) } From 97b61ea98b7fb65a4259f9d1cabf741764a23061 Mon Sep 17 00:00:00 2001 From: Brayan Oliveira <69634269+brayandso@users.noreply.github.com> Date: Tue, 24 Dec 2024 15:43:13 -0300 Subject: [PATCH 03/29] ta dificil --- .../ichi2/anki/cardviewer/ViewerCommand.kt | 10 +- .../preferences/ControlsSettingsFragment.kt | 4 +- .../anki/preferences/SettingsFragment.kt | 4 +- .../ichi2/anki/reviewer/MappableBinding.kt | 5 + .../ichi2/preferences/ControlPreference.kt | 3 +- .../ichi2/preferences/ControlPreference2.kt | 249 ++++++++++++++++++ .../src/main/res/drawable/dotted_line.xml | 9 + .../src/main/res/drawable/ic_keyboard.xml | 5 + .../src/main/res/drawable/ic_videogame.xml | 5 + .../main/res/layout/control_preference.xml | 84 ++++++ .../layout/control_preference_list_item.xml | 11 + .../src/main/res/xml/preferences_controls.xml | 11 +- 12 files changed, 387 insertions(+), 13 deletions(-) create mode 100644 AnkiDroid/src/main/java/com/ichi2/preferences/ControlPreference2.kt create mode 100644 AnkiDroid/src/main/res/drawable/dotted_line.xml create mode 100644 AnkiDroid/src/main/res/drawable/ic_keyboard.xml create mode 100644 AnkiDroid/src/main/res/drawable/ic_videogame.xml create mode 100644 AnkiDroid/src/main/res/layout/control_preference.xml create mode 100644 AnkiDroid/src/main/res/layout/control_preference_list_item.xml diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/cardviewer/ViewerCommand.kt b/AnkiDroid/src/main/java/com/ichi2/anki/cardviewer/ViewerCommand.kt index e8635969449f..6d07dca77cbb 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/cardviewer/ViewerCommand.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/cardviewer/ViewerCommand.kt @@ -30,10 +30,14 @@ import com.ichi2.anki.reviewer.MappableBinding.Companion.fromPreference import com.ichi2.anki.reviewer.MappableBinding.Companion.toPreferenceString import com.ichi2.anki.reviewer.ReviewerBinding +interface ScreenAction { + fun getBindings(prefs: SharedPreferences): List +} + /** Abstraction: Discuss moving many of these to 'Reviewer' */ enum class ViewerCommand( val resourceId: Int, -) { +) : ScreenAction { SHOW_ANSWER(R.string.show_answer), FLIP_OR_ANSWER_EASE1(R.string.answer_again), FLIP_OR_ANSWER_EASE2(R.string.answer_hard), @@ -134,8 +138,10 @@ enum class ViewerCommand( preferences.edit { putString(preferenceKey, newValue) } } + override fun getBindings(prefs: SharedPreferences): List = defaultValue + // If we use the serialised format, then this adds additional coupling to the properties. - val defaultValue: List + val defaultValue: List get() { return when (this) { FLIP_OR_ANSWER_EASE1 -> diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/preferences/ControlsSettingsFragment.kt b/AnkiDroid/src/main/java/com/ichi2/anki/preferences/ControlsSettingsFragment.kt index 6e223c9d3dc8..57817e8ebee7 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/preferences/ControlsSettingsFragment.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/preferences/ControlsSettingsFragment.kt @@ -40,10 +40,10 @@ class ControlsSettingsFragment : SettingsFragment() { .filter { pref -> pref.value == null } .forEach { pref -> pref.value = commands[pref.key]?.defaultValue?.toPreferenceString() } - setDynamicTitle() + setTitlesFromBackend() } - private fun setDynamicTitle() { + private fun setTitlesFromBackend() { findPreference(getString(R.string.reschedule_command_key))?.let { val preferenceTitle = TR.actionsSetDueDate().toSentenceCase(R.string.sentence_set_due_date) it.title = preferenceTitle diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/preferences/SettingsFragment.kt b/AnkiDroid/src/main/java/com/ichi2/anki/preferences/SettingsFragment.kt index 5fc8a8053455..e2fb7b5926e6 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/preferences/SettingsFragment.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/preferences/SettingsFragment.kt @@ -92,7 +92,7 @@ abstract class SettingsFragment : (preference as? DialogFragmentProvider)?.makeDialogFragment() ?: return super.onDisplayPreferenceDialog(preference) Timber.d("displaying custom preference: ${dialogFragment::class.simpleName}") - dialogFragment.arguments = bundleOf("key" to preference.key) + dialogFragment.arguments = bundleOf(PREF_DIALOG_KEY to preference.key) dialogFragment.setTargetFragment(this, 0) dialogFragment.show(parentFragmentManager, "androidx.preference.PreferenceFragment.DIALOG") } @@ -127,6 +127,8 @@ abstract class SettingsFragment : } companion object { + const val PREF_DIALOG_KEY = "key" + /** * Converts a preference value to a numeric number that * can be reported to analytics, since analytics events only accept diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/reviewer/MappableBinding.kt b/AnkiDroid/src/main/java/com/ichi2/anki/reviewer/MappableBinding.kt index 45899e8a75d0..1641baf41f97 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/reviewer/MappableBinding.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/reviewer/MappableBinding.kt @@ -21,6 +21,7 @@ import android.content.SharedPreferences import androidx.annotation.CheckResult import com.ichi2.anki.R import com.ichi2.anki.cardviewer.Gesture +import com.ichi2.anki.cardviewer.ScreenAction import com.ichi2.anki.cardviewer.ViewerCommand import com.ichi2.anki.reviewer.Binding.AxisButtonBinding import com.ichi2.anki.reviewer.Binding.GestureInput @@ -31,6 +32,10 @@ import com.ichi2.utils.hash import timber.log.Timber import java.util.Objects +interface BindingProcessor> { + fun executeAction(action: A) +} + /** * Binding + additional contextual information * Also defines equality over bindings. diff --git a/AnkiDroid/src/main/java/com/ichi2/preferences/ControlPreference.kt b/AnkiDroid/src/main/java/com/ichi2/preferences/ControlPreference.kt index 04e27d35e7b8..619fe10409e3 100644 --- a/AnkiDroid/src/main/java/com/ichi2/preferences/ControlPreference.kt +++ b/AnkiDroid/src/main/java/com/ichi2/preferences/ControlPreference.kt @@ -145,8 +145,7 @@ class ControlPreference : ListPreference { // When the user presses a key keyPicker.setBindingChangedListener { binding -> - val mappableBinding = - ReviewerBinding(binding, CardSide.BOTH) + val mappableBinding = ReviewerBinding(binding, CardSide.BOTH) warnIfBindingIsUsed(mappableBinding, keyPicker) } diff --git a/AnkiDroid/src/main/java/com/ichi2/preferences/ControlPreference2.kt b/AnkiDroid/src/main/java/com/ichi2/preferences/ControlPreference2.kt new file mode 100644 index 000000000000..cb7d86750c89 --- /dev/null +++ b/AnkiDroid/src/main/java/com/ichi2/preferences/ControlPreference2.kt @@ -0,0 +1,249 @@ +/* + * Copyright (c) 2024 Brayan Oliveira + * + * 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.preferences + +import android.app.Dialog +import android.content.Context +import android.content.SharedPreferences +import android.os.Bundle +import android.text.TextUtils +import android.util.AttributeSet +import android.view.View +import android.widget.ArrayAdapter +import android.widget.ListView +import androidx.appcompat.app.AlertDialog +import androidx.core.view.isVisible +import androidx.fragment.app.DialogFragment +import androidx.preference.DialogPreference +import androidx.preference.PreferenceFragmentCompat +import com.ichi2.anki.R +import com.ichi2.anki.cardviewer.GestureProcessor +import com.ichi2.anki.cardviewer.ViewerCommand +import com.ichi2.anki.dialogs.CardSideSelectionDialog +import com.ichi2.anki.dialogs.GestureSelectionDialogUtils +import com.ichi2.anki.dialogs.GestureSelectionDialogUtils.onGestureChanged +import com.ichi2.anki.dialogs.KeySelectionDialogUtils +import com.ichi2.anki.preferences.SettingsFragment +import com.ichi2.anki.preferences.requirePreference +import com.ichi2.anki.reviewer.Binding +import com.ichi2.anki.reviewer.MappableBinding +import com.ichi2.anki.reviewer.MappableBinding.Companion.toPreferenceString +import com.ichi2.anki.reviewer.ReviewerBinding +import com.ichi2.anki.utils.ext.sharedPrefs +import com.ichi2.ui.AxisPicker +import com.ichi2.ui.KeyPicker +import com.ichi2.utils.create +import com.ichi2.utils.customView +import com.ichi2.utils.negativeButton +import com.ichi2.utils.positiveButton +import com.ichi2.utils.show + +/** + * A preference which allows mapping of inputs to actions (example: keys -> commands) + * + * This is implemented as a List, the elements allow the user to either add, or + * remove previously mapped keys + */ +abstract class ControlPreference2 + @JvmOverloads + constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = androidx.preference.R.attr.preferenceStyle, + defStyleRes: Int = androidx.preference.R.style.Preference_DialogPreference, + ) : DialogPreference(context, attrs, defStyleAttr, defStyleRes), + DialogFragmentProvider { + var value: String = "" + set(value) { + val changed = !TextUtils.equals(field, value) + if (changed) { + field = value + persistString(value) + notifyChanged() + } + } + + val action: ViewerCommand = ViewerCommand.USER_ACTION_1 + + override fun getSummary(): CharSequence { + val prefValue = sharedPreferences?.getString(key, null) ?: return "" + val bindings = MappableBinding.fromPreferenceString(prefValue) + return bindings.joinToString(", ") { it.toDisplayString(context) } + } + + override fun makeDialogFragment(): DialogFragment = ControlPreferenceDialogFragment() + } + +class ControlPreferenceDialogFragment : DialogFragment() { + private lateinit var preference: ControlPreference2 + private lateinit var preferences: SharedPreferences + private var title: CharSequence? = null + + @Suppress("DEPRECATION") + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val key = requireArguments().getString(SettingsFragment.PREF_DIALOG_KEY)!! + preference = (targetFragment as PreferenceFragmentCompat).requirePreference(key) + preferences = preference.sharedPreferences!! + title = preference.title + } + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val view = requireActivity().layoutInflater.inflate(R.layout.control_preference, null) + + setupAddBindingDialogs(view) + setupRemovalEntries(view) + + return AlertDialog.Builder(requireContext()).create { + setTitle(title) + customView(view) + negativeButton(R.string.dialog_cancel) + } + } + + private fun setupAddBindingDialogs(view: View) { + view.findViewById(R.id.add_gesture).apply { + setOnClickListener { + showGesturePickerDialog() + dismiss() + } + isVisible = sharedPrefs().getBoolean(GestureProcessor.PREF_KEY, false) + } + + view.findViewById(R.id.add_key).setOnClickListener { + showKeyPickerDialog() + dismiss() + } + + view.findViewById(R.id.add_axis).setOnClickListener { + showAddAxisDialog() + dismiss() + } + } + + private fun setupRemovalEntries(view: View) { + val listView = view.findViewById(R.id.list_view) + val bindings = MappableBinding.fromPreferenceString(preferences.getString(preference.key, "")) + if (bindings.isEmpty()) { + listView.isVisible = false + return + } + val titles = + bindings.map { + getString(R.string.binding_remove_binding, it.toDisplayString(requireContext())) + } + listView.apply { + adapter = ArrayAdapter(requireContext(), R.layout.control_preference_list_item, titles) + setOnItemClickListener { _, _, index, _ -> + bindings.removeAt(index) + preference.value = bindings.toPreferenceString() + dismiss() + } + } + } + + private fun showGesturePickerDialog() { + AlertDialog.Builder(requireContext()).show { + setTitle(title) + val gesturePicker = GestureSelectionDialogUtils.getGesturePicker(context) + positiveButton(R.string.dialog_ok) { + val gesture = gesturePicker.getGesture() ?: return@positiveButton + if (preference.action is ViewerCommand) { + val mappableBinding = ReviewerBinding.fromGesture(gesture) + addMappableBinding(mappableBinding) + it.dismiss() + } + } + negativeButton(R.string.dialog_cancel) { it.dismiss() } + customView(view = gesturePicker) + gesturePicker.onGestureChanged { _ -> + } + } + } + + private fun showKeyPickerDialog() { + AlertDialog.Builder(requireContext()).show { + val keyPicker: KeyPicker = KeyPicker.inflate(context) + customView(view = keyPicker.rootLayout) + setTitle(title) + + // When the user presses a key + keyPicker.setBindingChangedListener { _ -> + } + positiveButton(R.string.dialog_ok) { + val binding = keyPicker.getBinding() ?: return@positiveButton + when (preference.action) { + is ViewerCommand -> { + CardSideSelectionDialog.displayInstance(context) { side -> + val currentCommand = getViewerCommandAssociatedTo(binding) + if (currentCommand != null && currentCommand != preference.action) { + it.dismiss() + } else { + val reviewerBinding = ReviewerBinding(binding, side) + addMappableBinding(reviewerBinding) + it.dismiss() + } + } + } + else -> {} + } + } + negativeButton(R.string.dialog_cancel) { it.dismiss() } + keyPicker.setKeycodeValidation(KeySelectionDialogUtils.disallowModifierKeyCodes()) + } + } + + private fun showAddAxisDialog() { + val axisPicker: AxisPicker = AxisPicker.inflate(requireContext()) + + val dialog = + AlertDialog + .Builder(requireContext()) + .customView(view = axisPicker.rootLayout) + .setTitle(title) + .negativeButton(R.string.dialog_cancel) { it.dismiss() } + .create() + + axisPicker.setBindingChangedListener { _ -> + } + + dialog.show() + } + + private fun addMappableBinding(mappableBinding: MappableBinding) { + val bindings = preference.action.getBindings(preferences).toMutableList() + bindings.add(mappableBinding as ReviewerBinding) + preference.value = bindings.toPreferenceString() + } + + private fun getViewerCommandAssociatedTo(binding: Binding): ViewerCommand? { + val mappings = ViewerCommand.entries.associateWith { it.getBindings(preferences) } + return mappings.entries + .firstOrNull { x -> + x.value.any { reviewerBinding -> reviewerBinding.binding == binding } + }?.key + } +} + +class ReviewerPreference + @JvmOverloads + constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = androidx.preference.R.attr.preferenceStyle, + defStyleRes: Int = androidx.preference.R.style.Preference_DialogPreference, + ) : ControlPreference2(context, attrs, defStyleAttr, defStyleRes) diff --git a/AnkiDroid/src/main/res/drawable/dotted_line.xml b/AnkiDroid/src/main/res/drawable/dotted_line.xml new file mode 100644 index 000000000000..281676c2be30 --- /dev/null +++ b/AnkiDroid/src/main/res/drawable/dotted_line.xml @@ -0,0 +1,9 @@ + + + + diff --git a/AnkiDroid/src/main/res/drawable/ic_keyboard.xml b/AnkiDroid/src/main/res/drawable/ic_keyboard.xml new file mode 100644 index 000000000000..121be9b643e8 --- /dev/null +++ b/AnkiDroid/src/main/res/drawable/ic_keyboard.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/AnkiDroid/src/main/res/drawable/ic_videogame.xml b/AnkiDroid/src/main/res/drawable/ic_videogame.xml new file mode 100644 index 000000000000..6e78a4a0f20f --- /dev/null +++ b/AnkiDroid/src/main/res/drawable/ic_videogame.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/AnkiDroid/src/main/res/layout/control_preference.xml b/AnkiDroid/src/main/res/layout/control_preference.xml new file mode 100644 index 000000000000..e0f1043d2f58 --- /dev/null +++ b/AnkiDroid/src/main/res/layout/control_preference.xml @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/AnkiDroid/src/main/res/layout/control_preference_list_item.xml b/AnkiDroid/src/main/res/layout/control_preference_list_item.xml new file mode 100644 index 000000000000..39e717317502 --- /dev/null +++ b/AnkiDroid/src/main/res/layout/control_preference_list_item.xml @@ -0,0 +1,11 @@ + + diff --git a/AnkiDroid/src/main/res/xml/preferences_controls.xml b/AnkiDroid/src/main/res/xml/preferences_controls.xml index 47433f2b0cbc..b2ecf735ba11 100644 --- a/AnkiDroid/src/main/res/xml/preferences_controls.xml +++ b/AnkiDroid/src/main/res/xml/preferences_controls.xml @@ -49,25 +49,24 @@ android:valueFrom="20" android:valueTo="180" app:displayValue="true"/> - - - - - - From de9b3ed70655e96881f7534b945eb8b2ecea6ad0 Mon Sep 17 00:00:00 2001 From: Brayan Oliveira <69634269+brayandso@users.noreply.github.com> Date: Tue, 24 Dec 2024 15:50:35 -0300 Subject: [PATCH 04/29] ta dificil --- .../ichi2/preferences/ControlPreference2.kt | 164 +++++++++--------- .../src/main/res/xml/preferences_controls.xml | 10 +- 2 files changed, 84 insertions(+), 90 deletions(-) diff --git a/AnkiDroid/src/main/java/com/ichi2/preferences/ControlPreference2.kt b/AnkiDroid/src/main/java/com/ichi2/preferences/ControlPreference2.kt index cb7d86750c89..0c0a83d84cdc 100644 --- a/AnkiDroid/src/main/java/com/ichi2/preferences/ControlPreference2.kt +++ b/AnkiDroid/src/main/java/com/ichi2/preferences/ControlPreference2.kt @@ -38,6 +38,7 @@ import com.ichi2.anki.dialogs.GestureSelectionDialogUtils.onGestureChanged import com.ichi2.anki.dialogs.KeySelectionDialogUtils import com.ichi2.anki.preferences.SettingsFragment import com.ichi2.anki.preferences.requirePreference +import com.ichi2.anki.preferences.sharedPrefs import com.ichi2.anki.reviewer.Binding import com.ichi2.anki.reviewer.MappableBinding import com.ichi2.anki.reviewer.MappableBinding.Companion.toPreferenceString @@ -85,6 +86,81 @@ abstract class ControlPreference2 } override fun makeDialogFragment(): DialogFragment = ControlPreferenceDialogFragment() + + fun addMappableBinding(mappableBinding: MappableBinding) { + val bindings = action.getBindings(context.sharedPrefs()).toMutableList() + bindings.add(mappableBinding as ReviewerBinding) + value = bindings.toPreferenceString() + } + + fun showGesturePickerDialog() { + AlertDialog.Builder(context).show { + setTitle(title) + val gesturePicker = GestureSelectionDialogUtils.getGesturePicker(context) + positiveButton(R.string.dialog_ok) { + val gesture = gesturePicker.getGesture() ?: return@positiveButton + val mappableBinding = ReviewerBinding.fromGesture(gesture) + addMappableBinding(mappableBinding) + it.dismiss() + } + negativeButton(R.string.dialog_cancel) { it.dismiss() } + customView(view = gesturePicker) + gesturePicker.onGestureChanged { _ -> + } + } + } + + fun showKeyPickerDialog() { + AlertDialog.Builder(context).show { + val keyPicker: KeyPicker = KeyPicker.inflate(context) + customView(view = keyPicker.rootLayout) + setTitle(title) + + // When the user presses a key + keyPicker.setBindingChangedListener { _ -> + } + positiveButton(R.string.dialog_ok) { + val binding = keyPicker.getBinding() ?: return@positiveButton + CardSideSelectionDialog.displayInstance(context) { side -> + val currentCommand = getViewerCommandAssociatedTo(binding) + if (currentCommand != null && currentCommand != action) { + it.dismiss() + } else { + val reviewerBinding = ReviewerBinding(binding, side) + addMappableBinding(reviewerBinding) + it.dismiss() + } + } + } + negativeButton(R.string.dialog_cancel) { it.dismiss() } + keyPicker.setKeycodeValidation(KeySelectionDialogUtils.disallowModifierKeyCodes()) + } + } + + fun showAddAxisDialog() { + val axisPicker: AxisPicker = AxisPicker.inflate(context) + + val dialog = + AlertDialog + .Builder(context) + .customView(view = axisPicker.rootLayout) + .setTitle(title) + .negativeButton(R.string.dialog_cancel) { it.dismiss() } + .create() + + axisPicker.setBindingChangedListener { _ -> + } + + dialog.show() + } + + private fun getViewerCommandAssociatedTo(binding: Binding): ViewerCommand? { + val mappings = ViewerCommand.entries.associateWith { it.getBindings(sharedPreferences!!) } + return mappings.entries + .firstOrNull { x -> + x.value.any { reviewerBinding -> reviewerBinding.binding == binding } + }?.key + } } class ControlPreferenceDialogFragment : DialogFragment() { @@ -118,19 +194,19 @@ class ControlPreferenceDialogFragment : DialogFragment() { private fun setupAddBindingDialogs(view: View) { view.findViewById(R.id.add_gesture).apply { setOnClickListener { - showGesturePickerDialog() + preference.showGesturePickerDialog() dismiss() } isVisible = sharedPrefs().getBoolean(GestureProcessor.PREF_KEY, false) } view.findViewById(R.id.add_key).setOnClickListener { - showKeyPickerDialog() + preference.showKeyPickerDialog() dismiss() } view.findViewById(R.id.add_axis).setOnClickListener { - showAddAxisDialog() + preference.showAddAxisDialog() dismiss() } } @@ -155,88 +231,6 @@ class ControlPreferenceDialogFragment : DialogFragment() { } } } - - private fun showGesturePickerDialog() { - AlertDialog.Builder(requireContext()).show { - setTitle(title) - val gesturePicker = GestureSelectionDialogUtils.getGesturePicker(context) - positiveButton(R.string.dialog_ok) { - val gesture = gesturePicker.getGesture() ?: return@positiveButton - if (preference.action is ViewerCommand) { - val mappableBinding = ReviewerBinding.fromGesture(gesture) - addMappableBinding(mappableBinding) - it.dismiss() - } - } - negativeButton(R.string.dialog_cancel) { it.dismiss() } - customView(view = gesturePicker) - gesturePicker.onGestureChanged { _ -> - } - } - } - - private fun showKeyPickerDialog() { - AlertDialog.Builder(requireContext()).show { - val keyPicker: KeyPicker = KeyPicker.inflate(context) - customView(view = keyPicker.rootLayout) - setTitle(title) - - // When the user presses a key - keyPicker.setBindingChangedListener { _ -> - } - positiveButton(R.string.dialog_ok) { - val binding = keyPicker.getBinding() ?: return@positiveButton - when (preference.action) { - is ViewerCommand -> { - CardSideSelectionDialog.displayInstance(context) { side -> - val currentCommand = getViewerCommandAssociatedTo(binding) - if (currentCommand != null && currentCommand != preference.action) { - it.dismiss() - } else { - val reviewerBinding = ReviewerBinding(binding, side) - addMappableBinding(reviewerBinding) - it.dismiss() - } - } - } - else -> {} - } - } - negativeButton(R.string.dialog_cancel) { it.dismiss() } - keyPicker.setKeycodeValidation(KeySelectionDialogUtils.disallowModifierKeyCodes()) - } - } - - private fun showAddAxisDialog() { - val axisPicker: AxisPicker = AxisPicker.inflate(requireContext()) - - val dialog = - AlertDialog - .Builder(requireContext()) - .customView(view = axisPicker.rootLayout) - .setTitle(title) - .negativeButton(R.string.dialog_cancel) { it.dismiss() } - .create() - - axisPicker.setBindingChangedListener { _ -> - } - - dialog.show() - } - - private fun addMappableBinding(mappableBinding: MappableBinding) { - val bindings = preference.action.getBindings(preferences).toMutableList() - bindings.add(mappableBinding as ReviewerBinding) - preference.value = bindings.toPreferenceString() - } - - private fun getViewerCommandAssociatedTo(binding: Binding): ViewerCommand? { - val mappings = ViewerCommand.entries.associateWith { it.getBindings(preferences) } - return mappings.entries - .firstOrNull { x -> - x.value.any { reviewerBinding -> reviewerBinding.binding == binding } - }?.key - } } class ReviewerPreference diff --git a/AnkiDroid/src/main/res/xml/preferences_controls.xml b/AnkiDroid/src/main/res/xml/preferences_controls.xml index b2ecf735ba11..548e6a2e5dbd 100644 --- a/AnkiDroid/src/main/res/xml/preferences_controls.xml +++ b/AnkiDroid/src/main/res/xml/preferences_controls.xml @@ -50,23 +50,23 @@ android:valueTo="180" app:displayValue="true"/> - - - - - From 36b9c4955d7108b0cb468ad8a4bce7a882f4c343 Mon Sep 17 00:00:00 2001 From: Brayan Oliveira <69634269+brayandso@users.noreply.github.com> Date: Tue, 24 Dec 2024 15:52:20 -0300 Subject: [PATCH 05/29] ag --- .../ichi2/preferences/ControlPreference2.kt | 175 ++++++++++-------- 1 file changed, 93 insertions(+), 82 deletions(-) diff --git a/AnkiDroid/src/main/java/com/ichi2/preferences/ControlPreference2.kt b/AnkiDroid/src/main/java/com/ichi2/preferences/ControlPreference2.kt index 0c0a83d84cdc..ed44c5eca214 100644 --- a/AnkiDroid/src/main/java/com/ichi2/preferences/ControlPreference2.kt +++ b/AnkiDroid/src/main/java/com/ichi2/preferences/ControlPreference2.kt @@ -58,111 +58,122 @@ import com.ichi2.utils.show * This is implemented as a List, the elements allow the user to either add, or * remove previously mapped keys */ -abstract class ControlPreference2 - @JvmOverloads +abstract class ControlPreference2 : + DialogPreference, + DialogFragmentProvider { + @Suppress("unused") constructor( context: Context, - attrs: AttributeSet? = null, - defStyleAttr: Int = androidx.preference.R.attr.preferenceStyle, - defStyleRes: Int = androidx.preference.R.style.Preference_DialogPreference, - ) : DialogPreference(context, attrs, defStyleAttr, defStyleRes), - DialogFragmentProvider { - var value: String = "" - set(value) { - val changed = !TextUtils.equals(field, value) - if (changed) { - field = value - persistString(value) - notifyChanged() - } + attrs: AttributeSet?, + defStyleAttr: Int, + defStyleRes: Int, + ) : super(context, attrs, defStyleAttr, defStyleRes) + + @Suppress("unused") + constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) + + @Suppress("unused") + constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) + + @Suppress("unused") + constructor(context: Context) : super(context) + + var value: String = "" + set(value) { + val changed = !TextUtils.equals(field, value) + if (changed) { + field = value + persistString(value) + notifyChanged() } + } - val action: ViewerCommand = ViewerCommand.USER_ACTION_1 + val action: ViewerCommand = ViewerCommand.USER_ACTION_1 - override fun getSummary(): CharSequence { - val prefValue = sharedPreferences?.getString(key, null) ?: return "" - val bindings = MappableBinding.fromPreferenceString(prefValue) - return bindings.joinToString(", ") { it.toDisplayString(context) } - } + override fun getSummary(): CharSequence { + val prefValue = sharedPreferences?.getString(key, null) ?: return "" + val bindings = MappableBinding.fromPreferenceString(prefValue) + return bindings.joinToString(", ") { it.toDisplayString(context) } + } - override fun makeDialogFragment(): DialogFragment = ControlPreferenceDialogFragment() + override fun makeDialogFragment(): DialogFragment = ControlPreferenceDialogFragment() - fun addMappableBinding(mappableBinding: MappableBinding) { - val bindings = action.getBindings(context.sharedPrefs()).toMutableList() - bindings.add(mappableBinding as ReviewerBinding) - value = bindings.toPreferenceString() - } + fun addMappableBinding(mappableBinding: MappableBinding) { + val bindings = action.getBindings(context.sharedPrefs()).toMutableList() + bindings.add(mappableBinding as ReviewerBinding) + value = bindings.toPreferenceString() + } - fun showGesturePickerDialog() { - AlertDialog.Builder(context).show { - setTitle(title) - val gesturePicker = GestureSelectionDialogUtils.getGesturePicker(context) - positiveButton(R.string.dialog_ok) { - val gesture = gesturePicker.getGesture() ?: return@positiveButton - val mappableBinding = ReviewerBinding.fromGesture(gesture) - addMappableBinding(mappableBinding) - it.dismiss() - } - negativeButton(R.string.dialog_cancel) { it.dismiss() } - customView(view = gesturePicker) - gesturePicker.onGestureChanged { _ -> - } + fun showGesturePickerDialog() { + AlertDialog.Builder(context).show { + setTitle(title) + val gesturePicker = GestureSelectionDialogUtils.getGesturePicker(context) + positiveButton(R.string.dialog_ok) { + val gesture = gesturePicker.getGesture() ?: return@positiveButton + val mappableBinding = ReviewerBinding.fromGesture(gesture) + addMappableBinding(mappableBinding) + it.dismiss() + } + negativeButton(R.string.dialog_cancel) { it.dismiss() } + customView(view = gesturePicker) + gesturePicker.onGestureChanged { _ -> } } + } - fun showKeyPickerDialog() { - AlertDialog.Builder(context).show { - val keyPicker: KeyPicker = KeyPicker.inflate(context) - customView(view = keyPicker.rootLayout) - setTitle(title) + fun showKeyPickerDialog() { + AlertDialog.Builder(context).show { + val keyPicker: KeyPicker = KeyPicker.inflate(context) + customView(view = keyPicker.rootLayout) + setTitle(title) - // When the user presses a key - keyPicker.setBindingChangedListener { _ -> - } - positiveButton(R.string.dialog_ok) { - val binding = keyPicker.getBinding() ?: return@positiveButton - CardSideSelectionDialog.displayInstance(context) { side -> - val currentCommand = getViewerCommandAssociatedTo(binding) - if (currentCommand != null && currentCommand != action) { - it.dismiss() - } else { - val reviewerBinding = ReviewerBinding(binding, side) - addMappableBinding(reviewerBinding) - it.dismiss() - } + // When the user presses a key + keyPicker.setBindingChangedListener { _ -> + } + positiveButton(R.string.dialog_ok) { + val binding = keyPicker.getBinding() ?: return@positiveButton + CardSideSelectionDialog.displayInstance(context) { side -> + val currentCommand = getViewerCommandAssociatedTo(binding) + if (currentCommand != null && currentCommand != action) { + it.dismiss() + } else { + val reviewerBinding = ReviewerBinding(binding, side) + addMappableBinding(reviewerBinding) + it.dismiss() } } - negativeButton(R.string.dialog_cancel) { it.dismiss() } - keyPicker.setKeycodeValidation(KeySelectionDialogUtils.disallowModifierKeyCodes()) } + negativeButton(R.string.dialog_cancel) { it.dismiss() } + keyPicker.setKeycodeValidation(KeySelectionDialogUtils.disallowModifierKeyCodes()) } + } - fun showAddAxisDialog() { - val axisPicker: AxisPicker = AxisPicker.inflate(context) - - val dialog = - AlertDialog - .Builder(context) - .customView(view = axisPicker.rootLayout) - .setTitle(title) - .negativeButton(R.string.dialog_cancel) { it.dismiss() } - .create() + fun showAddAxisDialog() { + val axisPicker: AxisPicker = AxisPicker.inflate(context) - axisPicker.setBindingChangedListener { _ -> - } + val dialog = + AlertDialog + .Builder(context) + .customView(view = axisPicker.rootLayout) + .setTitle(title) + .negativeButton(R.string.dialog_cancel) { it.dismiss() } + .create() - dialog.show() + axisPicker.setBindingChangedListener { _ -> } - private fun getViewerCommandAssociatedTo(binding: Binding): ViewerCommand? { - val mappings = ViewerCommand.entries.associateWith { it.getBindings(sharedPreferences!!) } - return mappings.entries - .firstOrNull { x -> - x.value.any { reviewerBinding -> reviewerBinding.binding == binding } - }?.key - } + dialog.show() } + private fun getViewerCommandAssociatedTo(binding: Binding): ViewerCommand? { + val mappings = ViewerCommand.entries.associateWith { it.getBindings(sharedPreferences!!) } + return mappings.entries + .firstOrNull { x -> + x.value.any { reviewerBinding -> reviewerBinding.binding == binding } + }?.key + } +} + class ControlPreferenceDialogFragment : DialogFragment() { private lateinit var preference: ControlPreference2 private lateinit var preferences: SharedPreferences From 0e7fd4d67a64415f4d5fee6c3efa66180264793d Mon Sep 17 00:00:00 2001 From: Brayan Oliveira <69634269+brayandso@users.noreply.github.com> Date: Tue, 24 Dec 2024 18:42:59 -0300 Subject: [PATCH 06/29] foi --- .../ichi2/anki/cardviewer/ViewerCommand.kt | 14 +- .../preferences/ControlsSettingsFragment.kt | 6 + .../ichi2/anki/reviewer/MappableBinding.kt | 29 +++- .../ichi2/preferences/ControlPreference2.kt | 152 ++++++++---------- .../preferences/ReviewerControlPreference.kt | 105 ++++++++++++ .../src/main/res/drawable/dotted_line.xml | 4 +- .../main/res/drawable/ic_remove_outline.xml | 5 + .../main/res/layout/control_preference.xml | 26 ++- .../layout/control_preference_list_item.xml | 8 +- .../src/main/res/xml/preferences_controls.xml | 10 +- 10 files changed, 245 insertions(+), 114 deletions(-) create mode 100644 AnkiDroid/src/main/java/com/ichi2/preferences/ReviewerControlPreference.kt create mode 100644 AnkiDroid/src/main/res/drawable/ic_remove_outline.xml diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/cardviewer/ViewerCommand.kt b/AnkiDroid/src/main/java/com/ichi2/anki/cardviewer/ViewerCommand.kt index 6d07dca77cbb..5dc336b71dac 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/cardviewer/ViewerCommand.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/cardviewer/ViewerCommand.kt @@ -17,6 +17,7 @@ package com.ichi2.anki.cardviewer import android.content.SharedPreferences import android.view.KeyEvent +import androidx.annotation.LayoutRes import androidx.core.content.edit import com.ichi2.anki.R import com.ichi2.anki.reviewer.Binding.Companion.keyCode @@ -31,6 +32,10 @@ import com.ichi2.anki.reviewer.MappableBinding.Companion.toPreferenceString import com.ichi2.anki.reviewer.ReviewerBinding interface ScreenAction { + @get:LayoutRes + val nameRes: Int + val preferenceKey: String + fun getBindings(prefs: SharedPreferences): List } @@ -95,7 +100,7 @@ enum class ViewerCommand( fun fromPreferenceKey(key: String) = entries.first { it.preferenceKey == key } } - val preferenceKey: String + override val preferenceKey: String get() = "binding_$name" fun addBinding( @@ -138,7 +143,12 @@ enum class ViewerCommand( preferences.edit { putString(preferenceKey, newValue) } } - override fun getBindings(prefs: SharedPreferences): List = defaultValue + override fun getBindings(prefs: SharedPreferences): List { + val prefValue = prefs.getString(preferenceKey, null) ?: return defaultValue + return ReviewerBinding.fromPreferenceString(prefValue) + } + + override val nameRes: Int get() = resourceId // If we use the serialised format, then this adds additional coupling to the properties. val defaultValue: List diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/preferences/ControlsSettingsFragment.kt b/AnkiDroid/src/main/java/com/ichi2/anki/preferences/ControlsSettingsFragment.kt index 57817e8ebee7..b7b1c619b824 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/preferences/ControlsSettingsFragment.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/preferences/ControlsSettingsFragment.kt @@ -23,6 +23,7 @@ import com.ichi2.anki.reviewer.MappableBinding.Companion.toPreferenceString import com.ichi2.anki.ui.internationalization.toSentenceCase import com.ichi2.annotations.NeedsTest import com.ichi2.preferences.ControlPreference +import com.ichi2.preferences.ReviewerControlPreference class ControlsSettingsFragment : SettingsFragment() { override val preferenceResource: Int @@ -40,6 +41,11 @@ class ControlsSettingsFragment : SettingsFragment() { .filter { pref -> pref.value == null } .forEach { pref -> pref.value = commands[pref.key]?.defaultValue?.toPreferenceString() } + allPreferences() + .filterIsInstance() + .filter { pref -> pref.getValue() == null } + .forEach { pref -> commands[pref.key]?.defaultValue?.toPreferenceString()?.let { pref.setValue(it) } } + setTitlesFromBackend() } diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/reviewer/MappableBinding.kt b/AnkiDroid/src/main/java/com/ichi2/anki/reviewer/MappableBinding.kt index 1641baf41f97..0a3aa50776fe 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/reviewer/MappableBinding.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/reviewer/MappableBinding.kt @@ -120,6 +120,23 @@ sealed class MappableBinding( } } + @CheckResult + fun getPreferenceBindingStrings(string: String): List { + if (string.isEmpty()) return emptyList() + try { + val version = string.takeWhile { x -> x != '/' } + val remainder = string.substring(version.length + 1) // skip the / + if (version != "1") { + Timber.w("cannot handle version '$version'") + return emptyList() + } + return remainder.split(PREF_SEPARATOR) + } catch (e: Exception) { + Timber.w(e, "Failed to deserialize preference") + return emptyList() + } + } + @CheckResult fun fromPreferenceString(string: String?): MutableList { if (string.isNullOrEmpty()) return ArrayList() @@ -201,7 +218,7 @@ class ReviewerBinding( } companion object { - fun fromString(s: String): MappableBinding { + fun fromString(s: String): ReviewerBinding { val binding = s.substring(0, s.length - 1) val b = Binding.fromString(binding) val side = @@ -213,6 +230,16 @@ class ReviewerBinding( return ReviewerBinding(b, side) } + fun fromPreferenceString(prefString: String?): List { + try { + if (prefString.isNullOrEmpty()) return emptyList() + val strings = getPreferenceBindingStrings(prefString) + return strings.map { fromString(it.substring(1)) } + } catch (_: Throwable) { + return emptyList() + } + } + @CheckResult fun fromGesture(gesture: Gesture): ReviewerBinding = ReviewerBinding(GestureInput(gesture), CardSide.BOTH) } diff --git a/AnkiDroid/src/main/java/com/ichi2/preferences/ControlPreference2.kt b/AnkiDroid/src/main/java/com/ichi2/preferences/ControlPreference2.kt index ed44c5eca214..1ffb29f80e42 100644 --- a/AnkiDroid/src/main/java/com/ichi2/preferences/ControlPreference2.kt +++ b/AnkiDroid/src/main/java/com/ichi2/preferences/ControlPreference2.kt @@ -1,4 +1,5 @@ /* + * Copyright (c) 2021 David Allison * Copyright (c) 2024 Brayan Oliveira * * This program is free software; you can redistribute it and/or modify it under @@ -17,12 +18,12 @@ package com.ichi2.preferences import android.app.Dialog import android.content.Context -import android.content.SharedPreferences import android.os.Bundle import android.text.TextUtils import android.util.AttributeSet import android.view.View import android.widget.ArrayAdapter +import android.widget.LinearLayout import android.widget.ListView import androidx.appcompat.app.AlertDialog import androidx.core.view.isVisible @@ -30,19 +31,17 @@ import androidx.fragment.app.DialogFragment import androidx.preference.DialogPreference import androidx.preference.PreferenceFragmentCompat import com.ichi2.anki.R +import com.ichi2.anki.cardviewer.Gesture import com.ichi2.anki.cardviewer.GestureProcessor -import com.ichi2.anki.cardviewer.ViewerCommand -import com.ichi2.anki.dialogs.CardSideSelectionDialog import com.ichi2.anki.dialogs.GestureSelectionDialogUtils import com.ichi2.anki.dialogs.GestureSelectionDialogUtils.onGestureChanged import com.ichi2.anki.dialogs.KeySelectionDialogUtils +import com.ichi2.anki.dialogs.WarningDisplay import com.ichi2.anki.preferences.SettingsFragment import com.ichi2.anki.preferences.requirePreference -import com.ichi2.anki.preferences.sharedPrefs import com.ichi2.anki.reviewer.Binding import com.ichi2.anki.reviewer.MappableBinding import com.ichi2.anki.reviewer.MappableBinding.Companion.toPreferenceString -import com.ichi2.anki.reviewer.ReviewerBinding import com.ichi2.anki.utils.ext.sharedPrefs import com.ichi2.ui.AxisPicker import com.ichi2.ui.KeyPicker @@ -58,7 +57,7 @@ import com.ichi2.utils.show * This is implemented as a List, the elements allow the user to either add, or * remove previously mapped keys */ -abstract class ControlPreference2 : +abstract class ControlPreference2 : DialogPreference, DialogFragmentProvider { @Suppress("unused") @@ -78,45 +77,47 @@ abstract class ControlPreference2 : @Suppress("unused") constructor(context: Context) : super(context) - var value: String = "" - set(value) { - val changed = !TextUtils.equals(field, value) - if (changed) { - field = value - persistString(value) - notifyChanged() - } - } + abstract fun getMappableBindings(): List - val action: ViewerCommand = ViewerCommand.USER_ACTION_1 + abstract fun onKeySelected(binding: Binding) - override fun getSummary(): CharSequence { - val prefValue = sharedPreferences?.getString(key, null) ?: return "" - val bindings = MappableBinding.fromPreferenceString(prefValue) - return bindings.joinToString(", ") { it.toDisplayString(context) } - } + abstract fun onGestureSelected(gesture: Gesture) - override fun makeDialogFragment(): DialogFragment = ControlPreferenceDialogFragment() + abstract fun onAxisSelected(binding: Binding) - fun addMappableBinding(mappableBinding: MappableBinding) { - val bindings = action.getBindings(context.sharedPrefs()).toMutableList() - bindings.add(mappableBinding as ReviewerBinding) - value = bindings.toPreferenceString() + /** @return whether the binding is used in another action */ + abstract fun warnIfUsed( + binding: Binding, + warningDisplay: WarningDisplay?, + ): Boolean + + fun getValue(): String? = getPersistedString(null) + + fun setValue(value: String) { + if (!TextUtils.equals(getValue(), value)) { + persistString(value) + notifyChanged() + } } + override fun getSummary(): CharSequence = getMappableBindings().joinToString(", ") { it.toDisplayString(context) } + + override fun makeDialogFragment(): DialogFragment = ControlPreferenceDialogFragment() + fun showGesturePickerDialog() { AlertDialog.Builder(context).show { setTitle(title) + setIcon(icon) val gesturePicker = GestureSelectionDialogUtils.getGesturePicker(context) positiveButton(R.string.dialog_ok) { val gesture = gesturePicker.getGesture() ?: return@positiveButton - val mappableBinding = ReviewerBinding.fromGesture(gesture) - addMappableBinding(mappableBinding) + onGestureSelected(gesture) it.dismiss() } negativeButton(R.string.dialog_cancel) { it.dismiss() } customView(view = gesturePicker) - gesturePicker.onGestureChanged { _ -> + gesturePicker.onGestureChanged { gesture -> + warnIfUsedOrClearWarning(Binding.GestureInput(gesture), gesturePicker) } } } @@ -126,22 +127,16 @@ abstract class ControlPreference2 : val keyPicker: KeyPicker = KeyPicker.inflate(context) customView(view = keyPicker.rootLayout) setTitle(title) + setIcon(icon) // When the user presses a key - keyPicker.setBindingChangedListener { _ -> + keyPicker.setBindingChangedListener { binding -> + warnIfUsedOrClearWarning(binding, keyPicker) } positiveButton(R.string.dialog_ok) { val binding = keyPicker.getBinding() ?: return@positiveButton - CardSideSelectionDialog.displayInstance(context) { side -> - val currentCommand = getViewerCommandAssociatedTo(binding) - if (currentCommand != null && currentCommand != action) { - it.dismiss() - } else { - val reviewerBinding = ReviewerBinding(binding, side) - addMappableBinding(reviewerBinding) - it.dismiss() - } - } + onKeySelected(binding) + it.dismiss() } negativeButton(R.string.dialog_cancel) { it.dismiss() } keyPicker.setKeycodeValidation(KeySelectionDialogUtils.disallowModifierKeyCodes()) @@ -149,54 +144,53 @@ abstract class ControlPreference2 : } fun showAddAxisDialog() { - val axisPicker: AxisPicker = AxisPicker.inflate(context) - - val dialog = - AlertDialog - .Builder(context) - .customView(view = axisPicker.rootLayout) - .setTitle(title) - .negativeButton(R.string.dialog_cancel) { it.dismiss() } - .create() - - axisPicker.setBindingChangedListener { _ -> + val axisPicker = + AxisPicker.inflate(context).apply { + setBindingChangedListener { binding -> + warnIfUsedOrClearWarning(binding, warningDisplay = null) + onAxisSelected(binding) + } + } + AlertDialog.Builder(context).show { + customView(view = axisPicker.rootLayout) + setTitle(title) + setIcon(icon) + negativeButton(R.string.dialog_cancel) { it.dismiss() } } - - dialog.show() } - private fun getViewerCommandAssociatedTo(binding: Binding): ViewerCommand? { - val mappings = ViewerCommand.entries.associateWith { it.getBindings(sharedPreferences!!) } - return mappings.entries - .firstOrNull { x -> - x.value.any { reviewerBinding -> reviewerBinding.binding == binding } - }?.key + private fun warnIfUsedOrClearWarning( + binding: Binding, + warningDisplay: WarningDisplay?, + ) { + if (!warnIfUsed(binding, warningDisplay)) { + warningDisplay?.clearWarning() + } } } -class ControlPreferenceDialogFragment : DialogFragment() { - private lateinit var preference: ControlPreference2 - private lateinit var preferences: SharedPreferences - private var title: CharSequence? = null +class ControlPreferenceDialogFragment : DialogFragment() { + private lateinit var preference: ControlPreference2 - @Suppress("DEPRECATION") + @Suppress("DEPRECATION") // targetFragment override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - val key = requireArguments().getString(SettingsFragment.PREF_DIALOG_KEY)!! + val key = + requireNotNull(requireArguments().getString(SettingsFragment.PREF_DIALOG_KEY)) { + "ControlPreferenceDialogFragment must have a 'key' argument leading to its preference" + } preference = (targetFragment as PreferenceFragmentCompat).requirePreference(key) - preferences = preference.sharedPreferences!! - title = preference.title } override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { val view = requireActivity().layoutInflater.inflate(R.layout.control_preference, null) setupAddBindingDialogs(view) - setupRemovalEntries(view) + setupRemoveControlEntries(view) return AlertDialog.Builder(requireContext()).create { - setTitle(title) + setTitle(preference.title) customView(view) negativeButton(R.string.dialog_cancel) } @@ -222,33 +216,23 @@ class ControlPreferenceDialogFragment : DialogFragment() { } } - private fun setupRemovalEntries(view: View) { - val listView = view.findViewById(R.id.list_view) - val bindings = MappableBinding.fromPreferenceString(preferences.getString(preference.key, "")) + private fun setupRemoveControlEntries(view: View) { + val bindings = preference.getMappableBindings().toMutableList() if (bindings.isEmpty()) { - listView.isVisible = false + view.findViewById(R.id.remove_layout).isVisible = false return } val titles = bindings.map { getString(R.string.binding_remove_binding, it.toDisplayString(requireContext())) } - listView.apply { + view.findViewById(R.id.list_view).apply { adapter = ArrayAdapter(requireContext(), R.layout.control_preference_list_item, titles) setOnItemClickListener { _, _, index, _ -> bindings.removeAt(index) - preference.value = bindings.toPreferenceString() + preference.setValue(bindings.toPreferenceString()) dismiss() } } } } - -class ReviewerPreference - @JvmOverloads - constructor( - context: Context, - attrs: AttributeSet? = null, - defStyleAttr: Int = androidx.preference.R.attr.preferenceStyle, - defStyleRes: Int = androidx.preference.R.style.Preference_DialogPreference, - ) : ControlPreference2(context, attrs, defStyleAttr, defStyleRes) diff --git a/AnkiDroid/src/main/java/com/ichi2/preferences/ReviewerControlPreference.kt b/AnkiDroid/src/main/java/com/ichi2/preferences/ReviewerControlPreference.kt new file mode 100644 index 000000000000..4b71191d0ae1 --- /dev/null +++ b/AnkiDroid/src/main/java/com/ichi2/preferences/ReviewerControlPreference.kt @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2024 Brayan Oliveira + * + * 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.preferences + +import android.content.Context +import android.util.AttributeSet +import com.ichi2.anki.R +import com.ichi2.anki.cardviewer.Gesture +import com.ichi2.anki.cardviewer.ViewerCommand +import com.ichi2.anki.dialogs.CardSideSelectionDialog +import com.ichi2.anki.dialogs.WarningDisplay +import com.ichi2.anki.preferences.sharedPrefs +import com.ichi2.anki.reviewer.Binding +import com.ichi2.anki.reviewer.CardSide +import com.ichi2.anki.reviewer.MappableBinding.Companion.toPreferenceString +import com.ichi2.anki.reviewer.ReviewerBinding +import com.ichi2.anki.showThemedToast + +class ReviewerControlPreference : ControlPreference2 { + @Suppress("unused") + constructor( + context: Context, + attrs: AttributeSet?, + defStyleAttr: Int, + defStyleRes: Int, + ) : super(context, attrs, defStyleAttr, defStyleRes) + + @Suppress("unused") + constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) + + @Suppress("unused") + constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) + + @Suppress("unused") + constructor(context: Context) : super(context) + + override fun getMappableBindings(): List = ReviewerBinding.fromPreferenceString(getValue()) + + override fun onKeySelected(binding: Binding) { + CardSideSelectionDialog.displayInstance(context) { side -> + addBinding(binding, side) + } + } + + override fun onGestureSelected(gesture: Gesture) { + addBinding(Binding.GestureInput(gesture), CardSide.BOTH) + } + + override fun onAxisSelected(binding: Binding) { + CardSideSelectionDialog.displayInstance(context) { side -> + addBinding(binding, side) + } + } + + override fun warnIfUsed( + binding: Binding, + warningDisplay: WarningDisplay?, + ): Boolean { + val prefs = context.sharedPrefs() + val mappableBinding = ReviewerBinding(binding, CardSide.BOTH) + val actionsMap = + ViewerCommand.entries + .associateWith { a -> a.getBindings(prefs) } + .filterValues { it.isNotEmpty() } + val commandWithBinding = + actionsMap.entries + .firstOrNull { + it.value.any { b -> b == mappableBinding } + }?.key ?: return false + + if (commandWithBinding.preferenceKey == key) return false + + val actionTitle = context.getString(commandWithBinding.nameRes) + val warning = context.getString(R.string.bindings_already_bound, actionTitle) + if (warningDisplay != null) { + warningDisplay.setWarning(warning) + } else { + showThemedToast(context, warning, true) + } + return true + } + + private fun addBinding( + binding: Binding, + side: CardSide, + ) { + val reviewerBinding = ReviewerBinding(binding, side) + val bindings = ReviewerBinding.fromPreferenceString(getValue() ?: "").toMutableList() + bindings.add(reviewerBinding) + setValue(bindings.toPreferenceString()) + } +} diff --git a/AnkiDroid/src/main/res/drawable/dotted_line.xml b/AnkiDroid/src/main/res/drawable/dotted_line.xml index 281676c2be30..c44a6594f76a 100644 --- a/AnkiDroid/src/main/res/drawable/dotted_line.xml +++ b/AnkiDroid/src/main/res/drawable/dotted_line.xml @@ -2,8 +2,8 @@ + android:dashGap="12dp" /> diff --git a/AnkiDroid/src/main/res/drawable/ic_remove_outline.xml b/AnkiDroid/src/main/res/drawable/ic_remove_outline.xml new file mode 100644 index 000000000000..2326cd6414a0 --- /dev/null +++ b/AnkiDroid/src/main/res/drawable/ic_remove_outline.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/AnkiDroid/src/main/res/layout/control_preference.xml b/AnkiDroid/src/main/res/layout/control_preference.xml index e0f1043d2f58..f34f202c5e3a 100644 --- a/AnkiDroid/src/main/res/layout/control_preference.xml +++ b/AnkiDroid/src/main/res/layout/control_preference.xml @@ -1,8 +1,11 @@ + android:orientation="vertical" + tools:context="com.ichi2.preferences.ControlPreference2" + > - - - - - - - - - - - + android:layout_marginHorizontal="32dp" + android:layout_marginVertical="4dp"/> - + android:textAppearance="?attr/textAppearanceBodyLarge" + tools:text="Remove ‘\u2328 Space’" + android:drawableStart="@drawable/ic_remove_outline" + android:drawablePadding="16dp"/> diff --git a/AnkiDroid/src/main/res/xml/preferences_controls.xml b/AnkiDroid/src/main/res/xml/preferences_controls.xml index 548e6a2e5dbd..0c6c086600b7 100644 --- a/AnkiDroid/src/main/res/xml/preferences_controls.xml +++ b/AnkiDroid/src/main/res/xml/preferences_controls.xml @@ -50,23 +50,23 @@ android:valueTo="180" app:displayValue="true"/> - - - - - From a10f0e975dfaaf809aeab93aa22415481606718e Mon Sep 17 00:00:00 2001 From: Brayan Oliveira <69634269+brayandso@users.noreply.github.com> Date: Tue, 24 Dec 2024 19:04:33 -0300 Subject: [PATCH 07/29] refactor: use paddingRelative in AlertDialog.customView --- .../src/main/java/com/ichi2/anki/ModelFieldEditor.kt | 6 +++--- .../anki/dialogs/customstudy/CustomStudyDialog.kt | 2 +- .../java/com/ichi2/anki/notetype/AddNewNotesType.kt | 2 +- .../main/java/com/ichi2/utils/AlertDialogFacade.kt | 12 +++++++++--- 4 files changed, 14 insertions(+), 8 deletions(-) diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/ModelFieldEditor.kt b/AnkiDroid/src/main/java/com/ichi2/anki/ModelFieldEditor.kt index ecbc96635c50..c3fdd65bacf0 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/ModelFieldEditor.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/ModelFieldEditor.kt @@ -176,7 +176,7 @@ class ModelFieldEditor : fieldNameInput?.let { fieldNameInput -> fieldNameInput.isSingleLine = true AlertDialog.Builder(this).show { - customView(view = fieldNameInput, paddingLeft = 64, paddingRight = 64, paddingTop = 32) + customView(view = fieldNameInput, paddingStart = 64, paddingEnd = 64, paddingTop = 32) title(R.string.model_field_editor_add) positiveButton(R.string.dialog_ok) { // Name is valid, now field is added @@ -303,7 +303,7 @@ class ModelFieldEditor : fieldNameInput.setText(fieldsLabels[currentPos]) fieldNameInput.setSelection(fieldNameInput.text!!.length) AlertDialog.Builder(this).show { - customView(view = fieldNameInput, paddingLeft = 64, paddingRight = 64, paddingTop = 32) + customView(view = fieldNameInput, paddingStart = 64, paddingEnd = 64, paddingTop = 32) title(R.string.model_field_editor_rename) positiveButton(R.string.rename) { if (uniqueName(fieldNameInput) == null) { @@ -347,7 +347,7 @@ class ModelFieldEditor : fieldNameInput?.let { fieldNameInput -> fieldNameInput.setRawInputType(InputType.TYPE_CLASS_NUMBER) AlertDialog.Builder(this).show { - customView(view = fieldNameInput, paddingLeft = 64, paddingRight = 64, paddingTop = 32) + customView(view = fieldNameInput, paddingStart = 64, paddingEnd = 64, paddingTop = 32) title(text = String.format(resources.getString(R.string.model_field_editor_reposition), 1, fieldsLabels.size)) positiveButton(R.string.dialog_ok) { val newPosition = fieldNameInput.text.toString() diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/dialogs/customstudy/CustomStudyDialog.kt b/AnkiDroid/src/main/java/com/ichi2/anki/dialogs/customstudy/CustomStudyDialog.kt index f1e31c5bd11b..22104b2fbc23 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/dialogs/customstudy/CustomStudyDialog.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/dialogs/customstudy/CustomStudyDialog.kt @@ -243,7 +243,7 @@ class CustomStudyDialog( val dialog = AlertDialog .Builder(requireActivity()) - .customView(view = v, paddingLeft = 64, paddingRight = 64, paddingTop = 32, paddingBottom = 32) + .customView(view = v, paddingStart = 64, paddingEnd = 64, paddingTop = 32, paddingBottom = 32) .positiveButton(R.string.dialog_ok) { // Get the value selected by user val n = diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/notetype/AddNewNotesType.kt b/AnkiDroid/src/main/java/com/ichi2/anki/notetype/AddNewNotesType.kt index 33791b10ded6..3cb0a3804cd9 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/notetype/AddNewNotesType.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/notetype/AddNewNotesType.kt @@ -78,7 +78,7 @@ class AddNewNotesType( AlertDialog .Builder(activity) .apply { - customView(dialogView, paddingLeft = 32, paddingRight = 32, paddingTop = 64, paddingBottom = 64) + customView(dialogView, paddingStart = 32, paddingEnd = 32, paddingTop = 64, paddingBottom = 64) positiveButton(R.string.dialog_ok) { _ -> val newName = dialogView.findViewById(R.id.notetype_new_name).text.toString() diff --git a/AnkiDroid/src/main/java/com/ichi2/utils/AlertDialogFacade.kt b/AnkiDroid/src/main/java/com/ichi2/utils/AlertDialogFacade.kt index 3d9fb53f9ed0..d58ef5173891 100644 --- a/AnkiDroid/src/main/java/com/ichi2/utils/AlertDialogFacade.kt +++ b/AnkiDroid/src/main/java/com/ichi2/utils/AlertDialogFacade.kt @@ -198,12 +198,18 @@ fun AlertDialog.getCheckBoxPrompt(): CheckBox = "CheckBox prompt is not available. Forgot to call AlertDialog.Builder.checkBoxPrompt()?" } +/** + * @param paddingStart the start padding in pixels + * @param paddingTop the top padding in pixels + * @param paddingEnd the end padding in pixels + * @param paddingBottom the bottom padding in pixels + */ fun AlertDialog.Builder.customView( view: View, paddingTop: Int = 0, paddingBottom: Int = 0, - paddingLeft: Int = 0, - paddingRight: Int = 0, + paddingStart: Int = 0, + paddingEnd: Int = 0, ): AlertDialog.Builder { val container = FrameLayout(context) @@ -213,7 +219,7 @@ fun AlertDialog.Builder.customView( FrameLayout.LayoutParams.WRAP_CONTENT, ) - container.setPadding(paddingLeft, paddingTop, paddingRight, paddingBottom) + container.setPaddingRelative(paddingStart, paddingTop, paddingEnd, paddingBottom) container.addView(view, containerParams) setView(container) From 6fa1f56823a01f1ae909f4fe32f766c5f49b186a Mon Sep 17 00:00:00 2001 From: Brayan Oliveira <69634269+brayandso@users.noreply.github.com> Date: Tue, 24 Dec 2024 19:06:57 -0300 Subject: [PATCH 08/29] indo --- .../preferences/ControlsSettingsFragment.kt | 30 +++---- .../ichi2/preferences/ControlPreference2.kt | 2 +- .../src/main/res/xml/preferences_controls.xml | 88 +++++++++---------- 3 files changed, 57 insertions(+), 63 deletions(-) diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/preferences/ControlsSettingsFragment.kt b/AnkiDroid/src/main/java/com/ichi2/anki/preferences/ControlsSettingsFragment.kt index b7b1c619b824..788f31f66abc 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/preferences/ControlsSettingsFragment.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/preferences/ControlsSettingsFragment.kt @@ -16,13 +16,13 @@ package com.ichi2.anki.preferences import androidx.annotation.StringRes +import androidx.preference.Preference import com.ichi2.anki.CollectionManager.TR import com.ichi2.anki.R import com.ichi2.anki.cardviewer.ViewerCommand import com.ichi2.anki.reviewer.MappableBinding.Companion.toPreferenceString import com.ichi2.anki.ui.internationalization.toSentenceCase import com.ichi2.annotations.NeedsTest -import com.ichi2.preferences.ControlPreference import com.ichi2.preferences.ReviewerControlPreference class ControlsSettingsFragment : SettingsFragment() { @@ -36,11 +36,6 @@ class ControlsSettingsFragment : SettingsFragment() { val commands = ViewerCommand.entries.associateBy { it.preferenceKey } // set defaultValue in the prefs creation. // if a preference is empty, it has a value like "1/" - allPreferences() - .filterIsInstance() - .filter { pref -> pref.value == null } - .forEach { pref -> pref.value = commands[pref.key]?.defaultValue?.toPreferenceString() } - allPreferences() .filterIsInstance() .filter { pref -> pref.getValue() == null } @@ -50,39 +45,38 @@ class ControlsSettingsFragment : SettingsFragment() { } private fun setTitlesFromBackend() { - findPreference(getString(R.string.reschedule_command_key))?.let { + findPreference(getString(R.string.reschedule_command_key))?.let { val preferenceTitle = TR.actionsSetDueDate().toSentenceCase(R.string.sentence_set_due_date) it.title = preferenceTitle - it.dialogTitle = preferenceTitle } - findPreference(getString(R.string.toggle_whiteboard_command_key))?.let { + findPreference(getString(R.string.toggle_whiteboard_command_key))?.let { it.title = getString(R.string.gesture_toggle_whiteboard).toSentenceCase(R.string.sentence_gesture_toggle_whiteboard) } - findPreference(getString(R.string.abort_and_sync_command_key))?.let { + findPreference(getString(R.string.abort_and_sync_command_key))?.let { it.title = getString(R.string.gesture_abort_sync).toSentenceCase(R.string.sentence_gesture_abort_sync) } - findPreference(getString(R.string.flag_red_command_key))?.let { + findPreference(getString(R.string.flag_red_command_key))?.let { it.title = getString(R.string.gesture_flag_red).toSentenceCase(R.string.sentence_gesture_flag_red) } - findPreference(getString(R.string.flag_orange_command_key))?.let { + findPreference(getString(R.string.flag_orange_command_key))?.let { it.title = getString(R.string.gesture_flag_orange).toSentenceCase(R.string.sentence_gesture_flag_orange) } - findPreference(getString(R.string.flag_green_command_key))?.let { + findPreference(getString(R.string.flag_green_command_key))?.let { it.title = getString(R.string.gesture_flag_green).toSentenceCase(R.string.sentence_gesture_flag_green) } - findPreference(getString(R.string.flag_blue_command_key))?.let { + findPreference(getString(R.string.flag_blue_command_key))?.let { it.title = getString(R.string.gesture_flag_blue).toSentenceCase(R.string.sentence_gesture_flag_blue) } - findPreference(getString(R.string.flag_pink_command_key))?.let { + findPreference(getString(R.string.flag_pink_command_key))?.let { it.title = getString(R.string.gesture_flag_pink).toSentenceCase(R.string.sentence_gesture_flag_pink) } - findPreference(getString(R.string.flag_turquoise_command_key))?.let { + findPreference(getString(R.string.flag_turquoise_command_key))?.let { it.title = getString(R.string.gesture_flag_turquoise).toSentenceCase(R.string.sentence_gesture_flag_turquoise) } - findPreference(getString(R.string.flag_purple_command_key))?.let { + findPreference(getString(R.string.flag_purple_command_key))?.let { it.title = getString(R.string.gesture_flag_purple).toSentenceCase(R.string.sentence_gesture_flag_purple) } - findPreference(getString(R.string.remove_flag_command_key))?.let { + findPreference(getString(R.string.remove_flag_command_key))?.let { it.title = getString(R.string.gesture_flag_remove).toSentenceCase(R.string.sentence_gesture_flag_remove) } } diff --git a/AnkiDroid/src/main/java/com/ichi2/preferences/ControlPreference2.kt b/AnkiDroid/src/main/java/com/ichi2/preferences/ControlPreference2.kt index 1ffb29f80e42..3986d814e45c 100644 --- a/AnkiDroid/src/main/java/com/ichi2/preferences/ControlPreference2.kt +++ b/AnkiDroid/src/main/java/com/ichi2/preferences/ControlPreference2.kt @@ -191,7 +191,7 @@ class ControlPreferenceDialogFragment : DialogFragment() { return AlertDialog.Builder(requireContext()).create { setTitle(preference.title) - customView(view) + customView(view, paddingTop = 32) negativeButton(R.string.dialog_cancel) } } diff --git a/AnkiDroid/src/main/res/xml/preferences_controls.xml b/AnkiDroid/src/main/res/xml/preferences_controls.xml index 0c6c086600b7..4005303c20a5 100644 --- a/AnkiDroid/src/main/res/xml/preferences_controls.xml +++ b/AnkiDroid/src/main/res/xml/preferences_controls.xml @@ -73,42 +73,42 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Date: Tue, 24 Dec 2024 19:13:49 -0300 Subject: [PATCH 09/29] tetra --- .../src/main/java/com/ichi2/preferences/ControlPreference2.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/AnkiDroid/src/main/java/com/ichi2/preferences/ControlPreference2.kt b/AnkiDroid/src/main/java/com/ichi2/preferences/ControlPreference2.kt index 3986d814e45c..ff693c777472 100644 --- a/AnkiDroid/src/main/java/com/ichi2/preferences/ControlPreference2.kt +++ b/AnkiDroid/src/main/java/com/ichi2/preferences/ControlPreference2.kt @@ -191,7 +191,8 @@ class ControlPreferenceDialogFragment : DialogFragment() { return AlertDialog.Builder(requireContext()).create { setTitle(preference.title) - customView(view, paddingTop = 32) + setIcon(preference.icon) + customView(view, paddingTop = 24) negativeButton(R.string.dialog_cancel) } } From 9fdb05d206ebec70db1378974069d9e8065bc857 Mon Sep 17 00:00:00 2001 From: Brayan Oliveira <69634269+brayandso@users.noreply.github.com> Date: Wed, 25 Dec 2024 05:43:29 -0300 Subject: [PATCH 10/29] aregesturesEnabled --- .../java/com/ichi2/preferences/ControlPreference2.kt | 12 +++++++----- .../ichi2/preferences/ReviewerControlPreference.kt | 4 ++++ 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/AnkiDroid/src/main/java/com/ichi2/preferences/ControlPreference2.kt b/AnkiDroid/src/main/java/com/ichi2/preferences/ControlPreference2.kt index ff693c777472..2c6ae35dec73 100644 --- a/AnkiDroid/src/main/java/com/ichi2/preferences/ControlPreference2.kt +++ b/AnkiDroid/src/main/java/com/ichi2/preferences/ControlPreference2.kt @@ -81,10 +81,12 @@ abstract class ControlPreference2 : abstract fun onKeySelected(binding: Binding) - abstract fun onGestureSelected(gesture: Gesture) - abstract fun onAxisSelected(binding: Binding) + abstract val areGesturesEnabled: Boolean + + protected open fun onGestureSelected(gesture: Gesture) {} + /** @return whether the binding is used in another action */ abstract fun warnIfUsed( binding: Binding, @@ -102,7 +104,7 @@ abstract class ControlPreference2 : override fun getSummary(): CharSequence = getMappableBindings().joinToString(", ") { it.toDisplayString(context) } - override fun makeDialogFragment(): DialogFragment = ControlPreferenceDialogFragment() + override fun makeDialogFragment(): DialogFragment = ControlPreferenceDialogFragment() fun showGesturePickerDialog() { AlertDialog.Builder(context).show { @@ -169,8 +171,8 @@ abstract class ControlPreference2 : } } -class ControlPreferenceDialogFragment : DialogFragment() { - private lateinit var preference: ControlPreference2 +class ControlPreferenceDialogFragment : DialogFragment() { + private lateinit var preference: ControlPreference2 @Suppress("DEPRECATION") // targetFragment override fun onCreate(savedInstanceState: Bundle?) { diff --git a/AnkiDroid/src/main/java/com/ichi2/preferences/ReviewerControlPreference.kt b/AnkiDroid/src/main/java/com/ichi2/preferences/ReviewerControlPreference.kt index 4b71191d0ae1..43cd5438a269 100644 --- a/AnkiDroid/src/main/java/com/ichi2/preferences/ReviewerControlPreference.kt +++ b/AnkiDroid/src/main/java/com/ichi2/preferences/ReviewerControlPreference.kt @@ -19,6 +19,7 @@ import android.content.Context import android.util.AttributeSet import com.ichi2.anki.R import com.ichi2.anki.cardviewer.Gesture +import com.ichi2.anki.cardviewer.GestureProcessor import com.ichi2.anki.cardviewer.ViewerCommand import com.ichi2.anki.dialogs.CardSideSelectionDialog import com.ichi2.anki.dialogs.WarningDisplay @@ -47,6 +48,9 @@ class ReviewerControlPreference : ControlPreference2 { @Suppress("unused") constructor(context: Context) : super(context) + override val areGesturesEnabled: Boolean + get() = sharedPreferences?.getBoolean(GestureProcessor.PREF_KEY, false) ?: false + override fun getMappableBindings(): List = ReviewerBinding.fromPreferenceString(getValue()) override fun onKeySelected(binding: Binding) { From fe1b49e65f52db91bd6d995bfd22acaf6de2a5fa Mon Sep 17 00:00:00 2001 From: Brayan Oliveira <69634269+brayandso@users.noreply.github.com> Date: Wed, 25 Dec 2024 05:46:45 -0300 Subject: [PATCH 11/29] alguns usos do MappableBinding --- .../src/main/java/com/ichi2/anki/reviewer/MappableBinding.kt | 2 +- .../com/ichi2/anki/servicelayer/PreferenceUpgradeService.kt | 5 ++--- .../src/test/java/com/ichi2/anki/ReviewerNoParamTest.kt | 4 ++-- .../src/test/java/com/ichi2/ui/BindingPreferenceTest.kt | 2 +- 4 files changed, 6 insertions(+), 7 deletions(-) diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/reviewer/MappableBinding.kt b/AnkiDroid/src/main/java/com/ichi2/anki/reviewer/MappableBinding.kt index 0a3aa50776fe..2ab809535138 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/reviewer/MappableBinding.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/reviewer/MappableBinding.kt @@ -233,7 +233,7 @@ class ReviewerBinding( fun fromPreferenceString(prefString: String?): List { try { if (prefString.isNullOrEmpty()) return emptyList() - val strings = getPreferenceBindingStrings(prefString) + val strings = getPreferenceBindingStrings(prefString) // TODO return strings.map { fromString(it.substring(1)) } } catch (_: Throwable) { return emptyList() diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/servicelayer/PreferenceUpgradeService.kt b/AnkiDroid/src/main/java/com/ichi2/anki/servicelayer/PreferenceUpgradeService.kt index c8b1362f7146..706cd1c73904 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/servicelayer/PreferenceUpgradeService.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/servicelayer/PreferenceUpgradeService.kt @@ -34,7 +34,6 @@ import com.ichi2.anki.reviewer.Binding import com.ichi2.anki.reviewer.Binding.Companion.keyCode import com.ichi2.anki.reviewer.CardSide import com.ichi2.anki.reviewer.FullScreenMode -import com.ichi2.anki.reviewer.MappableBinding import com.ichi2.anki.reviewer.MappableBinding.Companion.toPreferenceString import com.ichi2.anki.reviewer.ReviewerBinding import com.ichi2.libanki.Consts @@ -483,8 +482,8 @@ object PreferenceUpgradeService { val destinyPrefValue = preferences.getString(destinyPrefKey, null) val joinedBindings = - MappableBinding.fromPreferenceString(destinyPrefValue) + - MappableBinding.fromPreferenceString(sourcePrefValue) + ReviewerBinding.fromPreferenceString(destinyPrefValue) + + ReviewerBinding.fromPreferenceString(sourcePrefValue) preferences.edit { putString(destinyPrefKey, joinedBindings.toPreferenceString()) remove(sourcePrefKey) diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/ReviewerNoParamTest.kt b/AnkiDroid/src/test/java/com/ichi2/anki/ReviewerNoParamTest.kt index d640e88169e0..9b1e963bafd3 100644 --- a/AnkiDroid/src/test/java/com/ichi2/anki/ReviewerNoParamTest.kt +++ b/AnkiDroid/src/test/java/com/ichi2/anki/ReviewerNoParamTest.kt @@ -300,8 +300,8 @@ class ReviewerNoParamTest : RobolectricTest() { for (mappableBinding in MappableBinding.fromPreference(prefs, command)) { val gestureBinding = mappableBinding.binding as? Binding.GestureInput? ?: continue if (gestureBinding.gesture in gestures) { - val bindings: MutableList = - MappableBinding.fromPreferenceString(command.preferenceKey) + val bindings: MutableList = + ReviewerBinding.fromPreferenceString(command.preferenceKey).toMutableList() bindings.remove(mappableBinding) prefs.edit { putString(command.preferenceKey, bindings.toPreferenceString()) diff --git a/AnkiDroid/src/test/java/com/ichi2/ui/BindingPreferenceTest.kt b/AnkiDroid/src/test/java/com/ichi2/ui/BindingPreferenceTest.kt index a8d1d5d35943..0cddd8f1a517 100644 --- a/AnkiDroid/src/test/java/com/ichi2/ui/BindingPreferenceTest.kt +++ b/AnkiDroid/src/test/java/com/ichi2/ui/BindingPreferenceTest.kt @@ -33,7 +33,7 @@ class BindingPreferenceTest { fun serialization_deserialization_returns_same_result() { val str = getSampleBindings().toPreferenceString() - val again = MappableBinding.fromPreferenceString(str) + val again = ReviewerBinding.fromPreferenceString(str) assertEquals(str, again.toPreferenceString()) } From be9ceb09b852f4cb28c9e93bc448565ecc5c31a8 Mon Sep 17 00:00:00 2001 From: Brayan Oliveira <69634269+brayandso@users.noreply.github.com> Date: Wed, 25 Dec 2024 05:57:19 -0300 Subject: [PATCH 12/29] consertado ReviewerBinding fromString --- .../ichi2/anki/reviewer/MappableBinding.kt | 37 +++++++------------ 1 file changed, 14 insertions(+), 23 deletions(-) diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/reviewer/MappableBinding.kt b/AnkiDroid/src/main/java/com/ichi2/anki/reviewer/MappableBinding.kt index 2ab809535138..a0d4c06434c9 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/reviewer/MappableBinding.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/reviewer/MappableBinding.kt @@ -96,12 +96,13 @@ sealed class MappableBinding( companion object { const val PREF_SEPARATOR = '|' + private const val VERSION_PREFIX = "1/" @CheckResult fun List.toPreferenceString(): String = this .mapNotNull { it.toPreferenceString() } - .joinToString(prefix = "1/", separator = PREF_SEPARATOR.toString()) + .joinToString(prefix = VERSION_PREFIX, separator = PREF_SEPARATOR.toString()) @CheckResult fun fromString(s: String): MappableBinding? { @@ -123,18 +124,11 @@ sealed class MappableBinding( @CheckResult fun getPreferenceBindingStrings(string: String): List { if (string.isEmpty()) return emptyList() - try { - val version = string.takeWhile { x -> x != '/' } - val remainder = string.substring(version.length + 1) // skip the / - if (version != "1") { - Timber.w("cannot handle version '$version'") - return emptyList() - } - return remainder.split(PREF_SEPARATOR) - } catch (e: Exception) { - Timber.w(e, "Failed to deserialize preference") + if (!string.startsWith(VERSION_PREFIX)) { + Timber.w("cannot handle version of string %s", string) return emptyList() } + return string.substring(VERSION_PREFIX.length).split(PREF_SEPARATOR) } @CheckResult @@ -218,26 +212,23 @@ class ReviewerBinding( } companion object { - fun fromString(s: String): ReviewerBinding { - val binding = s.substring(0, s.length - 1) - val b = Binding.fromString(binding) + fun fromString(string: String): ReviewerBinding? { + if (string.isEmpty()) return null + val bindingString = string.substring(0, string.length - 1) + val binding = Binding.fromString(bindingString) val side = - when (s[s.length - 1]) { + when (string.last()) { '0' -> CardSide.QUESTION '1' -> CardSide.ANSWER else -> CardSide.BOTH } - return ReviewerBinding(b, side) + return ReviewerBinding(binding, side) } fun fromPreferenceString(prefString: String?): List { - try { - if (prefString.isNullOrEmpty()) return emptyList() - val strings = getPreferenceBindingStrings(prefString) // TODO - return strings.map { fromString(it.substring(1)) } - } catch (_: Throwable) { - return emptyList() - } + if (prefString.isNullOrEmpty()) return emptyList() + val strings = getPreferenceBindingStrings(prefString) // TODO + return strings.mapNotNull { fromString(it.substring(1)) } } @CheckResult From 7af144a396aecd04a55263c23b83dc0f3bcbb954 Mon Sep 17 00:00:00 2001 From: Brayan Oliveira <69634269+brayandso@users.noreply.github.com> Date: Wed, 25 Dec 2024 06:23:55 -0300 Subject: [PATCH 13/29] iendo --- .../ichi2/anki/reviewer/MappableBinding.kt | 37 +++++++++---------- .../preferences/ReviewerControlPreference.kt | 5 ++- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/reviewer/MappableBinding.kt b/AnkiDroid/src/main/java/com/ichi2/anki/reviewer/MappableBinding.kt index a0d4c06434c9..dfe0ae1bff82 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/reviewer/MappableBinding.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/reviewer/MappableBinding.kt @@ -128,7 +128,7 @@ sealed class MappableBinding( Timber.w("cannot handle version of string %s", string) return emptyList() } - return string.substring(VERSION_PREFIX.length).split(PREF_SEPARATOR) + return string.substring(VERSION_PREFIX.length).split(PREF_SEPARATOR).filter { it.isNotEmpty() } } @CheckResult @@ -179,24 +179,15 @@ class ReviewerBinding( side === other.side } - override fun hashCode(): Int = Objects.hash(getBindingHash(), 'r') + override fun hashCode(): Int = Objects.hash(getBindingHash(), PREFIX) override fun toPreferenceString(): String? { - if (!binding.isValid) { - return null - } - val s = - StringBuilder() - .append('r') - .append(binding.toString()) - // don't serialise problematic bindings - if (s.isEmpty()) { - return null - } + if (!binding.isValid) return null + val s = StringBuilder().append(PREFIX).append(binding.toString()) when (side) { - CardSide.QUESTION -> s.append('0') - CardSide.ANSWER -> s.append('1') - CardSide.BOTH -> s.append('2') + CardSide.QUESTION -> s.append(QUESTION_SUFFIX) + CardSide.ANSWER -> s.append(ANSWER_SUFFIX) + CardSide.BOTH -> s.append(QUESTION_AND_ANSWER_SUFFIX) } return s.toString() } @@ -212,14 +203,19 @@ class ReviewerBinding( } companion object { + private const val PREFIX = "r" + private const val QUESTION_SUFFIX = '0' + private const val ANSWER_SUFFIX = '1' + private const val QUESTION_AND_ANSWER_SUFFIX = '2' + fun fromString(string: String): ReviewerBinding? { if (string.isEmpty()) return null val bindingString = string.substring(0, string.length - 1) val binding = Binding.fromString(bindingString) val side = when (string.last()) { - '0' -> CardSide.QUESTION - '1' -> CardSide.ANSWER + QUESTION_SUFFIX -> CardSide.QUESTION + ANSWER_SUFFIX -> CardSide.ANSWER else -> CardSide.BOTH } return ReviewerBinding(binding, side) @@ -228,7 +224,10 @@ class ReviewerBinding( fun fromPreferenceString(prefString: String?): List { if (prefString.isNullOrEmpty()) return emptyList() val strings = getPreferenceBindingStrings(prefString) // TODO - return strings.mapNotNull { fromString(it.substring(1)) } + return strings.mapNotNull { + if (it.isEmpty()) return@mapNotNull null + fromString(it.substring(1)) + } } @CheckResult diff --git a/AnkiDroid/src/main/java/com/ichi2/preferences/ReviewerControlPreference.kt b/AnkiDroid/src/main/java/com/ichi2/preferences/ReviewerControlPreference.kt index 43cd5438a269..17f772098fa5 100644 --- a/AnkiDroid/src/main/java/com/ichi2/preferences/ReviewerControlPreference.kt +++ b/AnkiDroid/src/main/java/com/ichi2/preferences/ReviewerControlPreference.kt @@ -102,8 +102,9 @@ class ReviewerControlPreference : ControlPreference2 { side: CardSide, ) { val reviewerBinding = ReviewerBinding(binding, side) - val bindings = ReviewerBinding.fromPreferenceString(getValue() ?: "").toMutableList() + val bindings = ReviewerBinding.fromPreferenceString(getValue()).toMutableList() bindings.add(reviewerBinding) - setValue(bindings.toPreferenceString()) + val newPrefValue = bindings.toPreferenceString() + setValue(newPrefValue) } } From 83e4db96c83415eba8f0abc9b42842b2cb813a5c Mon Sep 17 00:00:00 2001 From: Brayan Oliveira <69634269+brayandso@users.noreply.github.com> Date: Wed, 25 Dec 2024 06:31:15 -0300 Subject: [PATCH 14/29] fazer essa na retirada da Controlpreference --- .../com/ichi2/anki/reviewer/MotionEventHandler.kt | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/reviewer/MotionEventHandler.kt b/AnkiDroid/src/main/java/com/ichi2/anki/reviewer/MotionEventHandler.kt index 0b04b064a4c4..6d31e7ac5439 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/reviewer/MotionEventHandler.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/reviewer/MotionEventHandler.kt @@ -22,6 +22,7 @@ import com.ichi2.anki.AbstractFlashcardViewer import com.ichi2.anki.AnkiDroidApp import com.ichi2.anki.cardviewer.ViewerCommand import com.ichi2.anki.preferences.sharedPrefs +import com.ichi2.anki.reviewer.MappableBinding.Companion.fromPreference import com.ichi2.compat.CompatHelper import timber.log.Timber @@ -112,14 +113,20 @@ class MotionEventHandler( return MotionEventHandler(viewer, handlers) } - private fun getAxisButtonBindings(context: Context) = - sequence { - for ((command, bindings) in MappableBinding.allMappings(context.sharedPrefs())) { + private fun getAxisButtonBindings(context: Context): Sequence { + val prefs = context.sharedPrefs() + val mappings = + ViewerCommand.entries.map { + Pair(it, fromPreference(prefs, it)) + } + return sequence { + for ((command, bindings) in mappings) { for (binding in bindings.map { it.binding }.filterIsInstance()) { yield(SingleAxisDetector(command, binding)) } } } + } } } From 3165197c196230f86a09973f063b84e33af837b3 Mon Sep 17 00:00:00 2001 From: Brayan Oliveira <69634269+brayandso@users.noreply.github.com> Date: Wed, 25 Dec 2024 06:36:57 -0300 Subject: [PATCH 15/29] removida fromPreference --- .../ichi2/anki/cardviewer/GestureProcessor.kt | 3 +-- .../ichi2/anki/cardviewer/ViewerCommand.kt | 3 +-- .../ichi2/anki/reviewer/MappableBinding.kt | 23 ++++++------------- .../ichi2/anki/reviewer/MotionEventHandler.kt | 3 +-- .../ichi2/anki/reviewer/PeripheralKeymap.kt | 5 +--- .../com/ichi2/anki/ReviewerNoParamTest.kt | 3 +-- .../UpgradeGesturesToControlsTest.kt | 10 ++++---- 7 files changed, 17 insertions(+), 33 deletions(-) diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/cardviewer/GestureProcessor.kt b/AnkiDroid/src/main/java/com/ichi2/anki/cardviewer/GestureProcessor.kt index 264901f63144..03a272a0b86d 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/cardviewer/GestureProcessor.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/cardviewer/GestureProcessor.kt @@ -18,7 +18,6 @@ package com.ichi2.anki.cardviewer import android.content.SharedPreferences import com.ichi2.anki.reviewer.Binding import com.ichi2.anki.reviewer.GestureMapper -import com.ichi2.anki.reviewer.MappableBinding class GestureProcessor( private val processor: ViewerCommand.CommandProcessor?, @@ -58,7 +57,7 @@ class GestureProcessor( val associatedCommands = HashMap() for (command in ViewerCommand.entries) { - for (mappableBinding in MappableBinding.fromPreference(preferences, command)) { + for (mappableBinding in command.getBindings(preferences)) { if (mappableBinding.binding is Binding.GestureInput) { associatedCommands[mappableBinding.binding.gesture] = command } diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/cardviewer/ViewerCommand.kt b/AnkiDroid/src/main/java/com/ichi2/anki/cardviewer/ViewerCommand.kt index 5dc336b71dac..095deb9284b4 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/cardviewer/ViewerCommand.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/cardviewer/ViewerCommand.kt @@ -27,7 +27,6 @@ import com.ichi2.anki.reviewer.Binding.ModifierKeys.Companion.ctrl import com.ichi2.anki.reviewer.Binding.ModifierKeys.Companion.shift import com.ichi2.anki.reviewer.CardSide import com.ichi2.anki.reviewer.MappableBinding -import com.ichi2.anki.reviewer.MappableBinding.Companion.fromPreference import com.ichi2.anki.reviewer.MappableBinding.Companion.toPreferenceString import com.ichi2.anki.reviewer.ReviewerBinding @@ -137,7 +136,7 @@ enum class ViewerCommand( binding: MappableBinding, performAdd: (MutableList, MappableBinding) -> Boolean, ) { - val bindings: MutableList = fromPreference(preferences, this) + val bindings: MutableList = this.getBindings(preferences).toMutableList() performAdd(bindings, binding) val newValue: String = bindings.toPreferenceString() preferences.edit { putString(preferenceKey, newValue) } diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/reviewer/MappableBinding.kt b/AnkiDroid/src/main/java/com/ichi2/anki/reviewer/MappableBinding.kt index dfe0ae1bff82..863e9c4a114b 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/reviewer/MappableBinding.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/reviewer/MappableBinding.kt @@ -148,20 +148,11 @@ sealed class MappableBinding( } } - @CheckResult - fun fromPreference( - prefs: SharedPreferences, - command: ViewerCommand, - ): MutableList { - val value = prefs.getString(command.preferenceKey, null) ?: return command.defaultValue.toMutableList() - return fromPreferenceString(value) - } - @CheckResult fun allMappings(prefs: SharedPreferences): MutableList>> = ViewerCommand.entries .map { - Pair(it, fromPreference(prefs, it)) + Pair(it, it.getBindings(prefs).toMutableList()) }.toMutableList() } } @@ -210,7 +201,10 @@ class ReviewerBinding( fun fromString(string: String): ReviewerBinding? { if (string.isEmpty()) return null - val bindingString = string.substring(0, string.length - 1) + val bindingString = + StringBuilder(string) + .substring(0, string.length - 1) + .removePrefix(PREFIX) val binding = Binding.fromString(bindingString) val side = when (string.last()) { @@ -223,11 +217,8 @@ class ReviewerBinding( fun fromPreferenceString(prefString: String?): List { if (prefString.isNullOrEmpty()) return emptyList() - val strings = getPreferenceBindingStrings(prefString) // TODO - return strings.mapNotNull { - if (it.isEmpty()) return@mapNotNull null - fromString(it.substring(1)) - } + val strings = getPreferenceBindingStrings(prefString) + return strings.mapNotNull { fromString(it) } } @CheckResult diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/reviewer/MotionEventHandler.kt b/AnkiDroid/src/main/java/com/ichi2/anki/reviewer/MotionEventHandler.kt index 6d31e7ac5439..ba0af455e4bd 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/reviewer/MotionEventHandler.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/reviewer/MotionEventHandler.kt @@ -22,7 +22,6 @@ import com.ichi2.anki.AbstractFlashcardViewer import com.ichi2.anki.AnkiDroidApp import com.ichi2.anki.cardviewer.ViewerCommand import com.ichi2.anki.preferences.sharedPrefs -import com.ichi2.anki.reviewer.MappableBinding.Companion.fromPreference import com.ichi2.compat.CompatHelper import timber.log.Timber @@ -117,7 +116,7 @@ class MotionEventHandler( val prefs = context.sharedPrefs() val mappings = ViewerCommand.entries.map { - Pair(it, fromPreference(prefs, it)) + Pair(it, it.getBindings(prefs)) } return sequence { for ((command, bindings) in mappings) { diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/reviewer/PeripheralKeymap.kt b/AnkiDroid/src/main/java/com/ichi2/anki/reviewer/PeripheralKeymap.kt index 5330b06bedf0..f3564a98bc2b 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/reviewer/PeripheralKeymap.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/reviewer/PeripheralKeymap.kt @@ -23,7 +23,6 @@ import com.ichi2.anki.cardviewer.ViewerCommand import com.ichi2.anki.preferences.sharedPrefs import com.ichi2.anki.reviewer.Binding.Companion.possibleKeyBindings import com.ichi2.anki.reviewer.CardSide.Companion.fromAnswer -import com.ichi2.anki.reviewer.MappableBinding.Companion.fromPreference /** Accepts peripheral input, mapping via various keybinding strategies, * and converting them to commands for the Reviewer. */ @@ -50,9 +49,7 @@ class PeripheralKeymap( command: ViewerCommand, preferences: SharedPreferences, ) { - val bindings = - fromPreference(preferences, command) - .filterIsInstance() + val bindings = command.getBindings(preferences) for (b in bindings) { if (!b.isKey) { continue diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/ReviewerNoParamTest.kt b/AnkiDroid/src/test/java/com/ichi2/anki/ReviewerNoParamTest.kt index 9b1e963bafd3..bf5076c6a981 100644 --- a/AnkiDroid/src/test/java/com/ichi2/anki/ReviewerNoParamTest.kt +++ b/AnkiDroid/src/test/java/com/ichi2/anki/ReviewerNoParamTest.kt @@ -31,7 +31,6 @@ import com.ichi2.anki.preferences.sharedPrefs import com.ichi2.anki.reviewer.Binding import com.ichi2.anki.reviewer.FullScreenMode import com.ichi2.anki.reviewer.FullScreenMode.Companion.setPreference -import com.ichi2.anki.reviewer.MappableBinding import com.ichi2.anki.reviewer.MappableBinding.Companion.toPreferenceString import com.ichi2.anki.reviewer.ReviewerBinding import com.ichi2.libanki.Consts @@ -297,7 +296,7 @@ class ReviewerNoParamTest : RobolectricTest() { private fun disableGestures(vararg gestures: Gesture) { val prefs = targetContext.sharedPrefs() for (command in ViewerCommand.entries) { - for (mappableBinding in MappableBinding.fromPreference(prefs, command)) { + for (mappableBinding in command.getBindings(prefs)) { val gestureBinding = mappableBinding.binding as? Binding.GestureInput? ?: continue if (gestureBinding.gesture in gestures) { val bindings: MutableList = diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/servicemodel/UpgradeGesturesToControlsTest.kt b/AnkiDroid/src/test/java/com/ichi2/anki/servicemodel/UpgradeGesturesToControlsTest.kt index c0143e793589..5270c5f85dbc 100644 --- a/AnkiDroid/src/test/java/com/ichi2/anki/servicemodel/UpgradeGesturesToControlsTest.kt +++ b/AnkiDroid/src/test/java/com/ichi2/anki/servicemodel/UpgradeGesturesToControlsTest.kt @@ -71,7 +71,7 @@ class UpgradeGesturesToControlsTest( assertThat(prefs.contains(testData.affectedPreferenceKey), equalTo(true)) assertThat(prefs.contains(testData.unaffectedPreferenceKey), equalTo(false)) - assertThat("example command should have no defaults", MappableBinding.fromPreference(prefs, command), empty()) + assertThat("example command should have no defaults", command.getBindings(prefs), empty()) upgradeAllGestures() @@ -83,7 +83,7 @@ class UpgradeGesturesToControlsTest( assertThat("legacy preference removed", prefs.contains(testData.affectedPreferenceKey), equalTo(false)) assertThat("new preference added", prefs.contains(command.preferenceKey), equalTo(true)) - val fromPreference = MappableBinding.fromPreference(prefs, command) + val fromPreference = command.getBindings(prefs) assertThat(fromPreference, hasSize(1)) val binding = fromPreference.first() @@ -102,7 +102,7 @@ class UpgradeGesturesToControlsTest( assertThat(prefs.contains(testData.affectedPreferenceKey), equalTo(true)) assertThat(prefs.contains(testData.unaffectedPreferenceKey), equalTo(false)) assertThat("new preference does not exist", prefs.contains(command.preferenceKey), equalTo(false)) - val previousCommands = MappableBinding.fromPreference(prefs, command) + val previousCommands = command.getBindings(prefs) assertThat("example command should have defaults", previousCommands, not(empty())) upgradeAllGestures() @@ -115,7 +115,7 @@ class UpgradeGesturesToControlsTest( assertThat("legacy preference removed", prefs.contains(testData.affectedPreferenceKey), equalTo(false)) assertThat("new preference exists", prefs.contains(command.preferenceKey), equalTo(true)) - val currentCommands = MappableBinding.fromPreference(prefs, command) + val currentCommands = command.getBindings(prefs) assertThat("a binding was added to '${command.preferenceKey}'", currentCommands, hasSize(previousCommands.size + 1)) // ensure that the order was not changed - the last element is not included in the zip @@ -142,7 +142,7 @@ class UpgradeGesturesToControlsTest( assertThat(prefs.contains(testData.affectedPreferenceKey), equalTo(true)) assertThat(prefs.contains(testData.unaffectedPreferenceKey), equalTo(false)) assertThat("new preference exists", prefs.contains(command.preferenceKey), equalTo(true)) - val previousCommands = MappableBinding.fromPreference(prefs, command) + val previousCommands = command.getBindings(prefs) assertThat("example command should have defaults", previousCommands, hasSize(2)) assertThat(previousCommands.first(), equalTo(testData.binding)) From 6c824f0a55a7f13241f69164530b1f2e32432f6f Mon Sep 17 00:00:00 2001 From: Brayan Oliveira <69634269+brayandso@users.noreply.github.com> Date: Wed, 25 Dec 2024 06:38:52 -0300 Subject: [PATCH 16/29] removida controlPreference --- .../ichi2/anki/reviewer/MappableBinding.kt | 76 +--- .../ichi2/preferences/ControlPreference.kt | 342 ++++++++---------- .../ichi2/preferences/ControlPreference2.kt | 241 ------------ .../preferences/ReviewerControlPreference.kt | 2 +- .../main/res/layout/control_preference.xml | 2 +- .../src/main/res/values/10-preferences.xml | 1 - 6 files changed, 164 insertions(+), 500 deletions(-) delete mode 100644 AnkiDroid/src/main/java/com/ichi2/preferences/ControlPreference2.kt diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/reviewer/MappableBinding.kt b/AnkiDroid/src/main/java/com/ichi2/anki/reviewer/MappableBinding.kt index 863e9c4a114b..d8413844cc93 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/reviewer/MappableBinding.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/reviewer/MappableBinding.kt @@ -17,12 +17,10 @@ package com.ichi2.anki.reviewer import android.content.Context -import android.content.SharedPreferences import androidx.annotation.CheckResult import com.ichi2.anki.R import com.ichi2.anki.cardviewer.Gesture import com.ichi2.anki.cardviewer.ScreenAction -import com.ichi2.anki.cardviewer.ViewerCommand import com.ichi2.anki.reviewer.Binding.AxisButtonBinding import com.ichi2.anki.reviewer.Binding.GestureInput import com.ichi2.anki.reviewer.Binding.KeyBinding @@ -104,23 +102,6 @@ sealed class MappableBinding( .mapNotNull { it.toPreferenceString() } .joinToString(prefix = VERSION_PREFIX, separator = PREF_SEPARATOR.toString()) - @CheckResult - fun fromString(s: String): MappableBinding? { - if (s.isEmpty()) { - return null - } - return try { - // the prefix of the serialized - when (s[0]) { - 'r' -> ReviewerBinding.fromString(s.substring(1)) - else -> null - } - } catch (e: Exception) { - Timber.w(e, "failed to deserialize binding") - null - } - } - @CheckResult fun getPreferenceBindingStrings(string: String): List { if (string.isEmpty()) return emptyList() @@ -130,30 +111,6 @@ sealed class MappableBinding( } return string.substring(VERSION_PREFIX.length).split(PREF_SEPARATOR).filter { it.isNotEmpty() } } - - @CheckResult - fun fromPreferenceString(string: String?): MutableList { - if (string.isNullOrEmpty()) return ArrayList() - try { - val version = string.takeWhile { x -> x != '/' } - val remainder = string.substring(version.length + 1) // skip the / - if (version != "1") { - Timber.w("cannot handle version '$version'") - return ArrayList() - } - return remainder.split(PREF_SEPARATOR).mapNotNull { fromString(it) }.toMutableList() - } catch (e: Exception) { - Timber.w(e, "Failed to deserialize preference") - return ArrayList() - } - } - - @CheckResult - fun allMappings(prefs: SharedPreferences): MutableList>> = - ViewerCommand.entries - .map { - Pair(it, it.getBindings(prefs).toMutableList()) - }.toMutableList() } } @@ -199,24 +156,25 @@ class ReviewerBinding( private const val ANSWER_SUFFIX = '1' private const val QUESTION_AND_ANSWER_SUFFIX = '2' - fun fromString(string: String): ReviewerBinding? { - if (string.isEmpty()) return null - val bindingString = - StringBuilder(string) - .substring(0, string.length - 1) - .removePrefix(PREFIX) - val binding = Binding.fromString(bindingString) - val side = - when (string.last()) { - QUESTION_SUFFIX -> CardSide.QUESTION - ANSWER_SUFFIX -> CardSide.ANSWER - else -> CardSide.BOTH - } - return ReviewerBinding(binding, side) - } - fun fromPreferenceString(prefString: String?): List { if (prefString.isNullOrEmpty()) return emptyList() + + fun fromString(string: String): ReviewerBinding? { + if (string.isEmpty()) return null + val bindingString = + StringBuilder(string) + .substring(0, string.length - 1) + .removePrefix(PREFIX) + val binding = Binding.fromString(bindingString) + val side = + when (string.last()) { + QUESTION_SUFFIX -> CardSide.QUESTION + ANSWER_SUFFIX -> CardSide.ANSWER + else -> CardSide.BOTH + } + return ReviewerBinding(binding, side) + } + val strings = getPreferenceBindingStrings(prefString) return strings.mapNotNull { fromString(it) } } diff --git a/AnkiDroid/src/main/java/com/ichi2/preferences/ControlPreference.kt b/AnkiDroid/src/main/java/com/ichi2/preferences/ControlPreference.kt index 619fe10409e3..f6e7d382ce91 100644 --- a/AnkiDroid/src/main/java/com/ichi2/preferences/ControlPreference.kt +++ b/AnkiDroid/src/main/java/com/ichi2/preferences/ControlPreference.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2021 David Allison + * Copyright (c) 2024 Brayan Oliveira * * 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 @@ -13,49 +14,52 @@ * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ - package com.ichi2.preferences -import android.annotation.SuppressLint +import android.app.Dialog import android.content.Context -import android.content.DialogInterface +import android.os.Bundle +import android.text.TextUtils import android.util.AttributeSet +import android.view.View +import android.widget.ArrayAdapter +import android.widget.LinearLayout +import android.widget.ListView import androidx.appcompat.app.AlertDialog -import androidx.preference.ListPreference +import androidx.core.view.isVisible +import androidx.fragment.app.DialogFragment +import androidx.preference.DialogPreference +import androidx.preference.PreferenceFragmentCompat import com.ichi2.anki.R +import com.ichi2.anki.cardviewer.Gesture import com.ichi2.anki.cardviewer.GestureProcessor -import com.ichi2.anki.cardviewer.ViewerCommand -import com.ichi2.anki.dialogs.CardSideSelectionDialog import com.ichi2.anki.dialogs.GestureSelectionDialogUtils import com.ichi2.anki.dialogs.GestureSelectionDialogUtils.onGestureChanged import com.ichi2.anki.dialogs.KeySelectionDialogUtils import com.ichi2.anki.dialogs.WarningDisplay -import com.ichi2.anki.preferences.sharedPrefs -import com.ichi2.anki.reviewer.CardSide +import com.ichi2.anki.preferences.SettingsFragment +import com.ichi2.anki.preferences.requirePreference +import com.ichi2.anki.reviewer.Binding import com.ichi2.anki.reviewer.MappableBinding import com.ichi2.anki.reviewer.MappableBinding.Companion.toPreferenceString -import com.ichi2.anki.reviewer.ReviewerBinding -import com.ichi2.anki.showThemedToast +import com.ichi2.anki.utils.ext.sharedPrefs import com.ichi2.ui.AxisPicker import com.ichi2.ui.KeyPicker +import com.ichi2.utils.create import com.ichi2.utils.customView -import com.ichi2.utils.message import com.ichi2.utils.negativeButton import com.ichi2.utils.positiveButton import com.ichi2.utils.show -import com.ichi2.utils.title /** * A preference which allows mapping of inputs to actions (example: keys -> commands) * * This is implemented as a List, the elements allow the user to either add, or * remove previously mapped keys - * - * Future: - * * Allow mapping gestures here - * * Allow maps other than the reviewer */ -class ControlPreference : ListPreference { +abstract class ControlPreference : + DialogPreference, + DialogFragmentProvider { @Suppress("unused") constructor( context: Context, @@ -73,221 +77,165 @@ class ControlPreference : ListPreference { @Suppress("unused") constructor(context: Context) : super(context) - private fun refreshEntries() { - val entryTitles: MutableList = ArrayList() - val entryIndices: MutableList = ArrayList() - // negative indices are "add" - entryTitles.add(context.getString(R.string.binding_add_key)) - entryIndices.add(ADD_KEY_INDEX) - // Add a joystick/motion controller - entryTitles.add(context.getString(R.string.binding_add_axis)) - entryIndices.add(ADD_AXIS_INDEX) - // Put "Add gesture" option if gestures are enabled - if (context.sharedPrefs().getBoolean(GestureProcessor.PREF_KEY, false)) { - entryTitles.add(context.getString(R.string.binding_add_gesture)) - entryIndices.add(ADD_GESTURE_INDEX) - } - // 0 and above are "delete" actions for already mapped preferences - for ((i, binding) in MappableBinding.fromPreferenceString(value).withIndex()) { - entryTitles.add(context.getString(R.string.binding_remove_binding, binding.toDisplayString(context))) - entryIndices.add(i) - } - entries = entryTitles.toTypedArray() - entryValues = entryIndices.map { it.toString() }.toTypedArray() - } + abstract fun getMappableBindings(): List - override fun onClick() { - refreshEntries() - super.onClick() - } + abstract fun onKeySelected(binding: Binding) - /** The summary that appears on the preference */ - override fun getSummary(): CharSequence = - MappableBinding - .fromPreferenceString(value) - .joinToString(", ") { it.toDisplayString(context) } + abstract fun onAxisSelected(binding: Binding) - /** Called when an element is selected in the ListView */ - @SuppressLint("CheckResult") - override fun callChangeListener(newValue: Any?): Boolean { - when (val index: Int = (newValue as String).toInt()) { - ADD_GESTURE_INDEX -> { - val actionName = title - AlertDialog.Builder(context).show { - title(text = actionName.toString()) + abstract val areGesturesEnabled: Boolean - val gesturePicker = GestureSelectionDialogUtils.getGesturePicker(context) + protected open fun onGestureSelected(gesture: Gesture) {} - positiveButton(R.string.dialog_ok) { - val gesture = gesturePicker.getGesture() ?: return@positiveButton - val mappableBinding = ReviewerBinding.fromGesture(gesture) - if (bindingIsUsedOnAnotherCommand(mappableBinding)) { - showDialogToReplaceBinding(mappableBinding, context.getString(R.string.binding_replace_gesture), it) - } else { - addBinding(mappableBinding) - it.dismiss() - } - } - negativeButton(R.string.dialog_cancel) { it.dismiss() } - customView(view = gesturePicker) + /** @return whether the binding is used in another action */ + abstract fun warnIfUsed( + binding: Binding, + warningDisplay: WarningDisplay?, + ): Boolean - gesturePicker.onGestureChanged { gesture -> - warnIfBindingIsUsed(ReviewerBinding.fromGesture(gesture), gesturePicker) - } - } - } - ADD_KEY_INDEX -> { - val actionName = title - AlertDialog.Builder(context).show { - val keyPicker: KeyPicker = KeyPicker.inflate(context) - customView(view = keyPicker.rootLayout) - title(text = actionName.toString()) + fun getValue(): String? = getPersistedString(null) + + fun setValue(value: String) { + if (!TextUtils.equals(getValue(), value)) { + persistString(value) + notifyChanged() + } + } - // When the user presses a key - keyPicker.setBindingChangedListener { binding -> - val mappableBinding = ReviewerBinding(binding, CardSide.BOTH) - warnIfBindingIsUsed(mappableBinding, keyPicker) - } + override fun getSummary(): CharSequence = getMappableBindings().joinToString(", ") { it.toDisplayString(context) } - positiveButton(R.string.dialog_ok) { - val binding = keyPicker.getBinding() ?: return@positiveButton - // Use CardSide.BOTH as placeholder just to check if binding exists - CardSideSelectionDialog.displayInstance(context) { side -> - val mappableBinding = ReviewerBinding(binding, side) - if (bindingIsUsedOnAnotherCommand(mappableBinding)) { - showDialogToReplaceBinding(mappableBinding, context.getString(R.string.binding_replace_key), it) - } else { - addBinding(mappableBinding) - it.dismiss() - } - } - } - negativeButton(R.string.dialog_cancel) { it.dismiss() } + override fun makeDialogFragment(): DialogFragment = ControlPreferenceDialogFragment() - keyPicker.setKeycodeValidation(KeySelectionDialogUtils.disallowModifierKeyCodes()) - } + fun showGesturePickerDialog() { + AlertDialog.Builder(context).show { + setTitle(title) + setIcon(icon) + val gesturePicker = GestureSelectionDialogUtils.getGesturePicker(context) + positiveButton(R.string.dialog_ok) { + val gesture = gesturePicker.getGesture() ?: return@positiveButton + onGestureSelected(gesture) + it.dismiss() } - ADD_AXIS_INDEX -> displayAddAxisDialog() - else -> { - val bindings: MutableList = MappableBinding.fromPreferenceString(value) - bindings.removeAt(index) - value = bindings.toPreferenceString() + negativeButton(R.string.dialog_cancel) { it.dismiss() } + customView(view = gesturePicker) + gesturePicker.onGestureChanged { gesture -> + warnIfUsedOrClearWarning(Binding.GestureInput(gesture), gesturePicker) } } - // don't persist the value - return false } - @SuppressLint("CheckResult") // noAutoDismiss - private fun displayAddAxisDialog() { - val actionName = title - val axisPicker: AxisPicker = AxisPicker.inflate(context) - val dialog = - AlertDialog - .Builder(context) - .customView(view = axisPicker.rootLayout) - .title(text = actionName.toString()) - .negativeButton(R.string.dialog_cancel) { it.dismiss() } - .create() + fun showKeyPickerDialog() { + AlertDialog.Builder(context).show { + val keyPicker: KeyPicker = KeyPicker.inflate(context) + customView(view = keyPicker.rootLayout) + setTitle(title) + setIcon(icon) + + // When the user presses a key + keyPicker.setBindingChangedListener { binding -> + warnIfUsedOrClearWarning(binding, keyPicker) + } + positiveButton(R.string.dialog_ok) { + val binding = keyPicker.getBinding() ?: return@positiveButton + onKeySelected(binding) + it.dismiss() + } + negativeButton(R.string.dialog_cancel) { it.dismiss() } + keyPicker.setKeycodeValidation(KeySelectionDialogUtils.disallowModifierKeyCodes()) + } + } - axisPicker.setBindingChangedListener { binding -> - showToastIfBindingIsUsed(ReviewerBinding(binding, CardSide.BOTH)) - // Use CardSide.BOTH as placeholder just to check if binding exists - CardSideSelectionDialog.displayInstance(context) { side -> - val mappableBinding = ReviewerBinding(binding, side) - if (bindingIsUsedOnAnotherCommand(mappableBinding)) { - showDialogToReplaceBinding(mappableBinding, context.getString(R.string.binding_replace_key), dialog) - } else { - addBinding(mappableBinding) - dialog.dismiss() + fun showAddAxisDialog() { + val axisPicker = + AxisPicker.inflate(context).apply { + setBindingChangedListener { binding -> + warnIfUsedOrClearWarning(binding, warningDisplay = null) + onAxisSelected(binding) } } + AlertDialog.Builder(context).show { + customView(view = axisPicker.rootLayout) + setTitle(title) + setIcon(icon) + negativeButton(R.string.dialog_cancel) { it.dismiss() } } - - dialog.show() } - /** - * Return if another command uses - */ - private fun bindingIsUsedOnAnotherCommand(binding: MappableBinding): Boolean = getCommandWithBindingExceptThis(binding) != null - - private fun warnIfBindingIsUsed( - binding: MappableBinding, - warningDisplay: WarningDisplay, + private fun warnIfUsedOrClearWarning( + binding: Binding, + warningDisplay: WarningDisplay?, ) { - getCommandWithBindingExceptThis(binding)?.let { - val name = context.getString(it.resourceId) - val warning = context.getString(R.string.bindings_already_bound, name) - warningDisplay.setWarning(warning) - } ?: warningDisplay.clearWarning() + if (!warnIfUsed(binding, warningDisplay)) { + warningDisplay?.clearWarning() + } } +} + +class ControlPreferenceDialogFragment : DialogFragment() { + private lateinit var preference: ControlPreference - /** Displays a warning to the user if the provided binding couldn't be used */ - private fun showToastIfBindingIsUsed(binding: MappableBinding) { - val bindingCommand = - getCommandWithBindingExceptThis(binding) - ?: return + @Suppress("DEPRECATION") // targetFragment + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) - val commandName = context.getString(bindingCommand.resourceId) - val text = context.getString(R.string.bindings_already_bound, commandName) - showThemedToast(context, text, true) + val key = + requireNotNull(requireArguments().getString(SettingsFragment.PREF_DIALOG_KEY)) { + "ControlPreferenceDialogFragment must have a 'key' argument leading to its preference" + } + preference = (targetFragment as PreferenceFragmentCompat).requirePreference(key) } - /** @return command where the binding is mapped excluding the current command */ - private fun getCommandWithBindingExceptThis(binding: MappableBinding): ViewerCommand? = - MappableBinding - .allMappings(context.sharedPrefs()) - // filter to the commands which have a binding matching this one except this - .firstOrNull { x -> x.second.any { cmdBinding -> cmdBinding == binding } && x.first.preferenceKey != key } - ?.first + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val view = requireActivity().layoutInflater.inflate(R.layout.control_preference, null) + + setupAddBindingDialogs(view) + setupRemoveControlEntries(view) - private fun addBinding(binding: MappableBinding) { - val bindings = MappableBinding.fromPreferenceString(value) - // by removing the binding, we ensure it's now at the start of the list - bindings.remove(binding) - bindings.add(0, binding) - value = bindings.toPreferenceString() + return AlertDialog.Builder(requireContext()).create { + setTitle(preference.title) + setIcon(preference.icon) + customView(view, paddingTop = 24) + negativeButton(R.string.dialog_cancel) + } } - /** - * Remove binding from all control preferences other than this one - */ - private fun clearBinding(binding: MappableBinding) { - for (command in ViewerCommand.entries) { - val commandPreference = - preferenceManager.findPreference(command.preferenceKey) - ?: continue - val bindings = MappableBinding.fromPreferenceString(commandPreference.value) - if (binding in bindings) { - bindings.remove(binding) - commandPreference.value = bindings.toPreferenceString() + private fun setupAddBindingDialogs(view: View) { + view.findViewById(R.id.add_gesture).apply { + setOnClickListener { + preference.showGesturePickerDialog() + dismiss() } + isVisible = sharedPrefs().getBoolean(GestureProcessor.PREF_KEY, false) } - } - private fun showDialogToReplaceBinding( - binding: MappableBinding, - title: String, - parentDialog: DialogInterface, - ) { - val commandName = context.getString(getCommandWithBindingExceptThis(binding)!!.resourceId) + view.findViewById(R.id.add_key).setOnClickListener { + preference.showKeyPickerDialog() + dismiss() + } - AlertDialog.Builder(context).show { - title(text = title) - message(text = context.getString(R.string.bindings_already_bound, commandName)) - positiveButton(R.string.dialog_positive_replace) { - clearBinding(binding) - addBinding(binding) - parentDialog.dismiss() - } - negativeButton(R.string.dialog_cancel) + view.findViewById(R.id.add_axis).setOnClickListener { + preference.showAddAxisDialog() + dismiss() } } - companion object { - private const val ADD_AXIS_INDEX = -3 - private const val ADD_KEY_INDEX = -2 - private const val ADD_GESTURE_INDEX = -1 + private fun setupRemoveControlEntries(view: View) { + val bindings = preference.getMappableBindings().toMutableList() + if (bindings.isEmpty()) { + view.findViewById(R.id.remove_layout).isVisible = false + return + } + val titles = + bindings.map { + getString(R.string.binding_remove_binding, it.toDisplayString(requireContext())) + } + view.findViewById(R.id.list_view).apply { + adapter = ArrayAdapter(requireContext(), R.layout.control_preference_list_item, titles) + setOnItemClickListener { _, _, index, _ -> + bindings.removeAt(index) + preference.setValue(bindings.toPreferenceString()) + dismiss() + } + } } } diff --git a/AnkiDroid/src/main/java/com/ichi2/preferences/ControlPreference2.kt b/AnkiDroid/src/main/java/com/ichi2/preferences/ControlPreference2.kt deleted file mode 100644 index 2c6ae35dec73..000000000000 --- a/AnkiDroid/src/main/java/com/ichi2/preferences/ControlPreference2.kt +++ /dev/null @@ -1,241 +0,0 @@ -/* - * Copyright (c) 2021 David Allison - * Copyright (c) 2024 Brayan Oliveira - * - * 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.preferences - -import android.app.Dialog -import android.content.Context -import android.os.Bundle -import android.text.TextUtils -import android.util.AttributeSet -import android.view.View -import android.widget.ArrayAdapter -import android.widget.LinearLayout -import android.widget.ListView -import androidx.appcompat.app.AlertDialog -import androidx.core.view.isVisible -import androidx.fragment.app.DialogFragment -import androidx.preference.DialogPreference -import androidx.preference.PreferenceFragmentCompat -import com.ichi2.anki.R -import com.ichi2.anki.cardviewer.Gesture -import com.ichi2.anki.cardviewer.GestureProcessor -import com.ichi2.anki.dialogs.GestureSelectionDialogUtils -import com.ichi2.anki.dialogs.GestureSelectionDialogUtils.onGestureChanged -import com.ichi2.anki.dialogs.KeySelectionDialogUtils -import com.ichi2.anki.dialogs.WarningDisplay -import com.ichi2.anki.preferences.SettingsFragment -import com.ichi2.anki.preferences.requirePreference -import com.ichi2.anki.reviewer.Binding -import com.ichi2.anki.reviewer.MappableBinding -import com.ichi2.anki.reviewer.MappableBinding.Companion.toPreferenceString -import com.ichi2.anki.utils.ext.sharedPrefs -import com.ichi2.ui.AxisPicker -import com.ichi2.ui.KeyPicker -import com.ichi2.utils.create -import com.ichi2.utils.customView -import com.ichi2.utils.negativeButton -import com.ichi2.utils.positiveButton -import com.ichi2.utils.show - -/** - * A preference which allows mapping of inputs to actions (example: keys -> commands) - * - * This is implemented as a List, the elements allow the user to either add, or - * remove previously mapped keys - */ -abstract class ControlPreference2 : - DialogPreference, - DialogFragmentProvider { - @Suppress("unused") - constructor( - context: Context, - attrs: AttributeSet?, - defStyleAttr: Int, - defStyleRes: Int, - ) : super(context, attrs, defStyleAttr, defStyleRes) - - @Suppress("unused") - constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) - - @Suppress("unused") - constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) - - @Suppress("unused") - constructor(context: Context) : super(context) - - abstract fun getMappableBindings(): List - - abstract fun onKeySelected(binding: Binding) - - abstract fun onAxisSelected(binding: Binding) - - abstract val areGesturesEnabled: Boolean - - protected open fun onGestureSelected(gesture: Gesture) {} - - /** @return whether the binding is used in another action */ - abstract fun warnIfUsed( - binding: Binding, - warningDisplay: WarningDisplay?, - ): Boolean - - fun getValue(): String? = getPersistedString(null) - - fun setValue(value: String) { - if (!TextUtils.equals(getValue(), value)) { - persistString(value) - notifyChanged() - } - } - - override fun getSummary(): CharSequence = getMappableBindings().joinToString(", ") { it.toDisplayString(context) } - - override fun makeDialogFragment(): DialogFragment = ControlPreferenceDialogFragment() - - fun showGesturePickerDialog() { - AlertDialog.Builder(context).show { - setTitle(title) - setIcon(icon) - val gesturePicker = GestureSelectionDialogUtils.getGesturePicker(context) - positiveButton(R.string.dialog_ok) { - val gesture = gesturePicker.getGesture() ?: return@positiveButton - onGestureSelected(gesture) - it.dismiss() - } - negativeButton(R.string.dialog_cancel) { it.dismiss() } - customView(view = gesturePicker) - gesturePicker.onGestureChanged { gesture -> - warnIfUsedOrClearWarning(Binding.GestureInput(gesture), gesturePicker) - } - } - } - - fun showKeyPickerDialog() { - AlertDialog.Builder(context).show { - val keyPicker: KeyPicker = KeyPicker.inflate(context) - customView(view = keyPicker.rootLayout) - setTitle(title) - setIcon(icon) - - // When the user presses a key - keyPicker.setBindingChangedListener { binding -> - warnIfUsedOrClearWarning(binding, keyPicker) - } - positiveButton(R.string.dialog_ok) { - val binding = keyPicker.getBinding() ?: return@positiveButton - onKeySelected(binding) - it.dismiss() - } - negativeButton(R.string.dialog_cancel) { it.dismiss() } - keyPicker.setKeycodeValidation(KeySelectionDialogUtils.disallowModifierKeyCodes()) - } - } - - fun showAddAxisDialog() { - val axisPicker = - AxisPicker.inflate(context).apply { - setBindingChangedListener { binding -> - warnIfUsedOrClearWarning(binding, warningDisplay = null) - onAxisSelected(binding) - } - } - AlertDialog.Builder(context).show { - customView(view = axisPicker.rootLayout) - setTitle(title) - setIcon(icon) - negativeButton(R.string.dialog_cancel) { it.dismiss() } - } - } - - private fun warnIfUsedOrClearWarning( - binding: Binding, - warningDisplay: WarningDisplay?, - ) { - if (!warnIfUsed(binding, warningDisplay)) { - warningDisplay?.clearWarning() - } - } -} - -class ControlPreferenceDialogFragment : DialogFragment() { - private lateinit var preference: ControlPreference2 - - @Suppress("DEPRECATION") // targetFragment - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - val key = - requireNotNull(requireArguments().getString(SettingsFragment.PREF_DIALOG_KEY)) { - "ControlPreferenceDialogFragment must have a 'key' argument leading to its preference" - } - preference = (targetFragment as PreferenceFragmentCompat).requirePreference(key) - } - - override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { - val view = requireActivity().layoutInflater.inflate(R.layout.control_preference, null) - - setupAddBindingDialogs(view) - setupRemoveControlEntries(view) - - return AlertDialog.Builder(requireContext()).create { - setTitle(preference.title) - setIcon(preference.icon) - customView(view, paddingTop = 24) - negativeButton(R.string.dialog_cancel) - } - } - - private fun setupAddBindingDialogs(view: View) { - view.findViewById(R.id.add_gesture).apply { - setOnClickListener { - preference.showGesturePickerDialog() - dismiss() - } - isVisible = sharedPrefs().getBoolean(GestureProcessor.PREF_KEY, false) - } - - view.findViewById(R.id.add_key).setOnClickListener { - preference.showKeyPickerDialog() - dismiss() - } - - view.findViewById(R.id.add_axis).setOnClickListener { - preference.showAddAxisDialog() - dismiss() - } - } - - private fun setupRemoveControlEntries(view: View) { - val bindings = preference.getMappableBindings().toMutableList() - if (bindings.isEmpty()) { - view.findViewById(R.id.remove_layout).isVisible = false - return - } - val titles = - bindings.map { - getString(R.string.binding_remove_binding, it.toDisplayString(requireContext())) - } - view.findViewById(R.id.list_view).apply { - adapter = ArrayAdapter(requireContext(), R.layout.control_preference_list_item, titles) - setOnItemClickListener { _, _, index, _ -> - bindings.removeAt(index) - preference.setValue(bindings.toPreferenceString()) - dismiss() - } - } - } -} diff --git a/AnkiDroid/src/main/java/com/ichi2/preferences/ReviewerControlPreference.kt b/AnkiDroid/src/main/java/com/ichi2/preferences/ReviewerControlPreference.kt index 17f772098fa5..d600b02ec6d7 100644 --- a/AnkiDroid/src/main/java/com/ichi2/preferences/ReviewerControlPreference.kt +++ b/AnkiDroid/src/main/java/com/ichi2/preferences/ReviewerControlPreference.kt @@ -30,7 +30,7 @@ import com.ichi2.anki.reviewer.MappableBinding.Companion.toPreferenceString import com.ichi2.anki.reviewer.ReviewerBinding import com.ichi2.anki.showThemedToast -class ReviewerControlPreference : ControlPreference2 { +class ReviewerControlPreference : ControlPreference { @Suppress("unused") constructor( context: Context, diff --git a/AnkiDroid/src/main/res/layout/control_preference.xml b/AnkiDroid/src/main/res/layout/control_preference.xml index f34f202c5e3a..5ea6cd785c9f 100644 --- a/AnkiDroid/src/main/res/layout/control_preference.xml +++ b/AnkiDroid/src/main/res/layout/control_preference.xml @@ -4,7 +4,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" - tools:context="com.ichi2.preferences.ControlPreference2" + tools:context="com.ichi2.preferences.ControlPreference" > Add gesture Replace gesture Add key - Replace key Press a key Add joystick/motion controller Move a joystick/motion controller From 472a17320294c39e9005e918fe314df19e64a85e Mon Sep 17 00:00:00 2001 From: Brayan Oliveira <69634269+brayandso@users.noreply.github.com> Date: Wed, 25 Dec 2024 12:28:27 -0300 Subject: [PATCH 17/29] =?UTF-8?q?t=C3=A1=20dificil?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/java/com/ichi2/anki/Reviewer.kt | 25 ++++---- .../java/com/ichi2/anki/reviewer/Binding.kt | 30 +++++++++- .../ichi2/anki/reviewer/MappableBinding.kt | 4 +- .../com/ichi2/anki/reviewer/ScreenKeyMap.kt | 57 +++++++++++++++++++ 4 files changed, 96 insertions(+), 20 deletions(-) create mode 100644 AnkiDroid/src/main/java/com/ichi2/anki/reviewer/ScreenKeyMap.kt diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/Reviewer.kt b/AnkiDroid/src/main/java/com/ichi2/anki/Reviewer.kt index d51534bc89dc..2883dcaedf66 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/Reviewer.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/Reviewer.kt @@ -79,12 +79,14 @@ import com.ichi2.anki.reviewer.AnswerButtons.Companion.getBackgroundColors import com.ichi2.anki.reviewer.AnswerButtons.Companion.getTextColors import com.ichi2.anki.reviewer.AnswerTimer import com.ichi2.anki.reviewer.AutomaticAnswerAction +import com.ichi2.anki.reviewer.BindingProcessor import com.ichi2.anki.reviewer.CardMarker import com.ichi2.anki.reviewer.FullScreenMode import com.ichi2.anki.reviewer.FullScreenMode.Companion.fromPreference import com.ichi2.anki.reviewer.FullScreenMode.Companion.isFullScreenReview -import com.ichi2.anki.reviewer.PeripheralKeymap +import com.ichi2.anki.reviewer.ReviewerBinding import com.ichi2.anki.reviewer.ReviewerUi +import com.ichi2.anki.reviewer.ScreenKeyMap import com.ichi2.anki.scheduling.ForgetCardsDialog import com.ichi2.anki.scheduling.SetDueDateDialog import com.ichi2.anki.scheduling.registerOnForgetHandler @@ -133,7 +135,8 @@ import kotlin.coroutines.resume @NeedsTest("#14709: Timebox shouldn't appear instantly when the Reviewer is opened") open class Reviewer : AbstractFlashcardViewer(), - ReviewerUi { + ReviewerUi, + BindingProcessor { private var queueState: CurrentQueueState? = null private val customSchedulingKey = TimeManager.time.intTimeMS().toString() private var hasDrawerSwipeConflicts = false @@ -197,7 +200,7 @@ open class Reviewer : private lateinit var toolbar: Toolbar @VisibleForTesting - protected val processor = PeripheralKeymap(this, this) + protected lateinit var processor: ScreenKeyMap private val addNoteLauncher = registerForActivityResult( @@ -222,6 +225,7 @@ open class Reviewer : textBarReview = findViewById(R.id.review_number) toolbar = findViewById(R.id.toolbar) micToolBarLayer = findViewById(R.id.mic_tool_bar_layer) + processor = ScreenKeyMap(sharedPrefs(), ViewerCommand.entries, this) if (sharedPrefs().getString("answerButtonPosition", "bottom") == "bottom" && !navBarNeedsScrim) { setNavigationBarColor(R.attr.showAnswerColor) } @@ -912,22 +916,12 @@ open class Reviewer : if (answerFieldIsFocused()) { return super.onKeyDown(keyCode, event) } - if (processor.onKeyDown(keyCode, event) || super.onKeyDown(keyCode, event)) { + if (processor.onKeyDown(event) || super.onKeyDown(keyCode, event)) { return true } return false } - override fun onKeyUp( - keyCode: Int, - event: KeyEvent, - ): Boolean = - if (processor.onKeyUp(keyCode, event)) { - true - } else { - super.onKeyUp(keyCode, event) - } - override fun onGenericMotionEvent(event: MotionEvent?): Boolean { if (motionEventHandler.onGenericMotionEvent(event)) { return true @@ -983,7 +977,6 @@ open class Reviewer : val preferences = super.restorePreferences() prefHideDueCount = preferences.getBoolean("hideDueCount", false) prefShowETA = preferences.getBoolean("showETA", false) - processor.setup() prefFullscreenReview = isFullScreenReview(preferences) actionButtons.setup(preferences) return preferences @@ -1671,4 +1664,6 @@ open class Reviewer : /** Default (500ms) time for action snackbars, such as undo, bury and suspend */ const val ACTION_SNACKBAR_TIME = 500 } + + override fun executeAction(action: ViewerCommand): Boolean = executeCommand(action, null) } diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/reviewer/Binding.kt b/AnkiDroid/src/main/java/com/ichi2/anki/reviewer/Binding.kt index 8a9e923cdb99..5a61e06b7f49 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/reviewer/Binding.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/reviewer/Binding.kt @@ -23,6 +23,7 @@ import com.ichi2.anki.utils.ext.ifNotZero import com.ichi2.utils.StringUtil import com.ichi2.utils.lastIndexOfOrNull import timber.log.Timber +import java.util.Objects sealed interface Binding { data class GestureInput( @@ -35,6 +36,10 @@ sealed interface Binding { append(GESTURE_PREFIX) append(gesture) } + + override fun equals(other: Any?): Boolean = other is GestureInput && gesture == other.gesture + + override fun hashCode(): Int = Objects.hash(gesture) } /** @@ -85,6 +90,15 @@ sealed interface Binding { else -> KEY_PREFIX.toString() } + override fun equals(other: Any?): Boolean { + if (other !is KeyCode) return false + if (keycode != other.keycode) return false + if (modifierKeys != other.modifierKeys) return false + return true + } + + override fun hashCode(): Int = Objects.hash(keycode, modifierKeys) + override fun toDisplayString(context: Context): String = buildString { append(getKeyCodePrefix()) @@ -121,6 +135,10 @@ sealed interface Binding { append(modifierKeys.toString()) append(unicodeCharacter) } + + override fun equals(other: Any?): Boolean = super.equals(other) + + override fun hashCode(): Int = Objects.hash(unicodeCharacter, modifierKeys) } data object UnknownBinding : Binding { @@ -168,6 +186,13 @@ sealed interface Binding { if (shift) append("Shift+") } + override fun equals(other: Any?): Boolean { + if (other !is ModifierKeys) return false + return semiStructuralEquals(other) + } + + override fun hashCode(): Int = Objects.hash(shift, ctrl, alt, semiStructuralEquals(this)) + fun semiStructuralEquals(keys: ModifierKeys): Boolean { if (this.alt != keys.alt || this.ctrl != keys.ctrl) { return false @@ -250,9 +275,9 @@ sealed interface Binding { /** * This returns multiple bindings due to the "default" implementation not knowing what the keycode for a button is */ - fun possibleKeyBindings(event: KeyEvent): List { + fun possibleKeyBindings(event: KeyEvent): Set { val modifiers = ModifierKeys(event.isShiftPressed, event.isCtrlPressed, event.isAltPressed) - val ret: MutableList = ArrayList() + val ret = mutableSetOf() event.keyCode.ifNotZero { keyCode -> ret.add(keyCode(keyCode, modifiers)) } // passing in metaState: 0 means that Ctrl+1 returns '1' instead of '\0' @@ -267,7 +292,6 @@ sealed interface Binding { Timber.w(e) } } - return ret } diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/reviewer/MappableBinding.kt b/AnkiDroid/src/main/java/com/ichi2/anki/reviewer/MappableBinding.kt index d8413844cc93..71376297d5fd 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/reviewer/MappableBinding.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/reviewer/MappableBinding.kt @@ -30,8 +30,8 @@ import com.ichi2.utils.hash import timber.log.Timber import java.util.Objects -interface BindingProcessor> { - fun executeAction(action: A) +fun interface BindingProcessor> { + fun executeAction(action: A): Boolean } /** diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/reviewer/ScreenKeyMap.kt b/AnkiDroid/src/main/java/com/ichi2/anki/reviewer/ScreenKeyMap.kt new file mode 100644 index 000000000000..92a752ee2129 --- /dev/null +++ b/AnkiDroid/src/main/java/com/ichi2/anki/reviewer/ScreenKeyMap.kt @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2020 David Allison + * Copyright (c) 2024 Brayan Oliveira + * + * 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.reviewer + +import android.content.SharedPreferences +import android.view.KeyEvent +import com.ichi2.anki.cardviewer.ScreenAction + +class ScreenKeyMap>( + sharedPrefs: SharedPreferences, + actions: List, + private val processor: BindingProcessor, +) { + private val bindingMap = HashMap() + + init { + for (action in actions) { + val bindings = action.getBindings(sharedPrefs) + for (binding in bindings) { + bindingMap[binding] = action + } + } + } + + fun onKeyDown(event: KeyEvent): Boolean { + if (event.repeatCount > 0) { + return false + } + var ret = false + val possibleKeyBindings = Binding.possibleKeyBindings(event) + for ((mappableBinding, action) in bindingMap) { + if (possibleKeyBindings.contains(mappableBinding.binding)) { + ret = ret or processor.executeAction(action) + } + } +// val mds = bindingMap.filterKeys { m -> possibleKeyBindings.any { it == m.binding } } +// for ((mappableBinding, action) in mds) { +// ret = ret or processor.executeAction(action) +// } + return ret + } +} From 83cf35b34a08330b2f4030a2c3113dee7f652211 Mon Sep 17 00:00:00 2001 From: Brayan Oliveira <69634269+brayandso@users.noreply.github.com> Date: Wed, 25 Dec 2024 12:42:16 -0300 Subject: [PATCH 18/29] summertime sadness --- .../src/main/java/com/ichi2/anki/Reviewer.kt | 5 ++++- .../ichi2/anki/reviewer/MappableBinding.kt | 5 ++++- .../com/ichi2/anki/reviewer/ScreenKeyMap.kt | 22 ++++++++----------- 3 files changed, 17 insertions(+), 15 deletions(-) diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/Reviewer.kt b/AnkiDroid/src/main/java/com/ichi2/anki/Reviewer.kt index 2883dcaedf66..28218284fb83 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/Reviewer.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/Reviewer.kt @@ -1665,5 +1665,8 @@ open class Reviewer : const val ACTION_SNACKBAR_TIME = 500 } - override fun executeAction(action: ViewerCommand): Boolean = executeCommand(action, null) + override fun executeAction( + action: ViewerCommand, + forBinding: ReviewerBinding, + ): Boolean = executeCommand(action, null) } diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/reviewer/MappableBinding.kt b/AnkiDroid/src/main/java/com/ichi2/anki/reviewer/MappableBinding.kt index 71376297d5fd..0f0b2fd76500 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/reviewer/MappableBinding.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/reviewer/MappableBinding.kt @@ -31,7 +31,10 @@ import timber.log.Timber import java.util.Objects fun interface BindingProcessor> { - fun executeAction(action: A): Boolean + fun executeAction( + action: A, + forBinding: B, + ): Boolean } /** diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/reviewer/ScreenKeyMap.kt b/AnkiDroid/src/main/java/com/ichi2/anki/reviewer/ScreenKeyMap.kt index 92a752ee2129..ce1edda8bb6f 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/reviewer/ScreenKeyMap.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/reviewer/ScreenKeyMap.kt @@ -26,13 +26,13 @@ class ScreenKeyMap>( actions: List, private val processor: BindingProcessor, ) { - private val bindingMap = HashMap() + private val bindingMap = HashMap>() init { for (action in actions) { - val bindings = action.getBindings(sharedPrefs) - for (binding in bindings) { - bindingMap[binding] = action + val mappableBindings = action.getBindings(sharedPrefs) + for (mappableBinding in mappableBindings) { + bindingMap[mappableBinding.binding] = mappableBinding to action } } } @@ -42,16 +42,12 @@ class ScreenKeyMap>( return false } var ret = false - val possibleKeyBindings = Binding.possibleKeyBindings(event) - for ((mappableBinding, action) in bindingMap) { - if (possibleKeyBindings.contains(mappableBinding.binding)) { - ret = ret or processor.executeAction(action) - } + val bindings = Binding.possibleKeyBindings(event) + for (binding in bindings) { + val (mappableBinding, action) = bindingMap[binding] ?: continue + ret = ret or processor.executeAction(action, mappableBinding) } -// val mds = bindingMap.filterKeys { m -> possibleKeyBindings.any { it == m.binding } } -// for ((mappableBinding, action) in mds) { -// ret = ret or processor.executeAction(action) -// } + return ret } } From 3792feaef1bbc5299c28ec13dd758d5f7e4fbc22 Mon Sep 17 00:00:00 2001 From: Brayan Oliveira <69634269+brayandso@users.noreply.github.com> Date: Wed, 25 Dec 2024 13:22:19 -0300 Subject: [PATCH 19/29] =?UTF-8?q?deus=20=C3=A9=20bom?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../preferences/ControlsSettingsFragment.kt | 66 ++++- AnkiDroid/src/main/res/layout/tab_layout.xml | 8 + AnkiDroid/src/main/res/values/preferences.xml | 1 + .../src/main/res/xml/preferences_controls.xml | 275 +---------------- .../res/xml/preferences_reviewer_controls.xml | 277 ++++++++++++++++++ 5 files changed, 352 insertions(+), 275 deletions(-) create mode 100644 AnkiDroid/src/main/res/layout/tab_layout.xml create mode 100644 AnkiDroid/src/main/res/xml/preferences_reviewer_controls.xml diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/preferences/ControlsSettingsFragment.kt b/AnkiDroid/src/main/java/com/ichi2/anki/preferences/ControlsSettingsFragment.kt index 788f31f66abc..a73e5d761ce5 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/preferences/ControlsSettingsFragment.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/preferences/ControlsSettingsFragment.kt @@ -15,35 +15,85 @@ */ package com.ichi2.anki.preferences +import android.os.Bundle +import android.view.View import androidx.annotation.StringRes +import androidx.annotation.XmlRes import androidx.preference.Preference +import androidx.preference.get +import com.google.android.material.tabs.TabLayout import com.ichi2.anki.CollectionManager.TR import com.ichi2.anki.R import com.ichi2.anki.cardviewer.ViewerCommand import com.ichi2.anki.reviewer.MappableBinding.Companion.toPreferenceString import com.ichi2.anki.ui.internationalization.toSentenceCase import com.ichi2.annotations.NeedsTest -import com.ichi2.preferences.ReviewerControlPreference +import com.ichi2.preferences.ControlPreference +import timber.log.Timber -class ControlsSettingsFragment : SettingsFragment() { +class ControlsSettingsFragment : + SettingsFragment(), + TabLayout.OnTabSelectedListener { override val preferenceResource: Int get() = R.xml.preferences_controls override val analyticsScreenNameConstant: String get() = "prefs.controls" + private var staticPreferencesCount: Int = 0 + @NeedsTest("Keys and titles in the XML layout are the same of the ViewerCommands") override fun initSubscreen() { + requirePreference(R.string.pref_controls_tab_layout_key).setViewId(R.id.tab_layout) + staticPreferencesCount = preferenceScreen.preferenceCount + } + + override fun onViewCreated( + view: View, + savedInstanceState: Bundle?, + ) { + super.onViewCreated(view, savedInstanceState) + + listView.post { + val tabLayout = listView.findViewById(R.id.tab_layout) + setupTabLayout(tabLayout) + } + } + + private fun setupTabLayout(tabLayout: TabLayout) { + tabLayout.addOnTabSelectedListener(this) + for (screen in ControlPreferenceScreen.entries) { + val tab = + tabLayout.newTab().apply { + setText(screen.titleRes) + } + tabLayout.addTab(tab) + } + } + + private fun getScreen(tab: TabLayout.Tab): ControlPreferenceScreen = ControlPreferenceScreen.entries[tab.position] + + override fun onTabSelected(tab: TabLayout.Tab) { + val screen = getScreen(tab) + Timber.v("Selected tab %d - %s", tab.position, screen.name) + addPreferencesFromResource(screen.xmlRes) + val commands = ViewerCommand.entries.associateBy { it.preferenceKey } // set defaultValue in the prefs creation. // if a preference is empty, it has a value like "1/" allPreferences() - .filterIsInstance() + .filterIsInstance>() .filter { pref -> pref.getValue() == null } .forEach { pref -> commands[pref.key]?.defaultValue?.toPreferenceString()?.let { pref.setValue(it) } } + } - setTitlesFromBackend() + override fun onTabUnselected(tab: TabLayout.Tab?) { + for (i in staticPreferencesCount until preferenceScreen.preferenceCount) { + preferenceScreen.removePreference(preferenceScreen[staticPreferencesCount]) + } } + override fun onTabReselected(tab: TabLayout.Tab?) = Unit + private fun setTitlesFromBackend() { findPreference(getString(R.string.reschedule_command_key))?.let { val preferenceTitle = TR.actionsSetDueDate().toSentenceCase(R.string.sentence_set_due_date) @@ -85,3 +135,11 @@ class ControlsSettingsFragment : SettingsFragment() { @StringRes resId: Int, ): String = this.toSentenceCase(this@ControlsSettingsFragment, resId) } + +enum class ControlPreferenceScreen( + @XmlRes val xmlRes: Int, + @StringRes val titleRes: Int, +) { + REVIEWER(R.xml.preferences_reviewer_controls, R.string.pref_cat_reviewer), + PREVIEWER(R.xml.preferences_accessibility, R.string.accessibility), +} diff --git a/AnkiDroid/src/main/res/layout/tab_layout.xml b/AnkiDroid/src/main/res/layout/tab_layout.xml new file mode 100644 index 000000000000..f2221a4291bd --- /dev/null +++ b/AnkiDroid/src/main/res/layout/tab_layout.xml @@ -0,0 +1,8 @@ + + \ No newline at end of file diff --git a/AnkiDroid/src/main/res/values/preferences.xml b/AnkiDroid/src/main/res/values/preferences.xml index e150e4335d35..229f7391d151 100644 --- a/AnkiDroid/src/main/res/values/preferences.xml +++ b/AnkiDroid/src/main/res/values/preferences.xml @@ -83,6 +83,7 @@ gestureCornerTouch gestureFullScreenNavigationDrawer swipeSensitivity + controlsTabLayout binding_SHOW_ANSWER binding_FLIP_OR_ANSWER_EASE1 binding_FLIP_OR_ANSWER_EASE2 diff --git a/AnkiDroid/src/main/res/xml/preferences_controls.xml b/AnkiDroid/src/main/res/xml/preferences_controls.xml index 4005303c20a5..5cacf376da1c 100644 --- a/AnkiDroid/src/main/res/xml/preferences_controls.xml +++ b/AnkiDroid/src/main/res/xml/preferences_controls.xml @@ -14,11 +14,8 @@ ~ You should have received a copy of the GNU General Public License along with ~ this program. If not, see . --> - - @@ -49,274 +46,10 @@ android:valueFrom="20" android:valueTo="180" app:displayValue="true"/> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - - - - - - \ No newline at end of file diff --git a/AnkiDroid/src/main/res/xml/preferences_reviewer_controls.xml b/AnkiDroid/src/main/res/xml/preferences_reviewer_controls.xml new file mode 100644 index 000000000000..b14e6e73d419 --- /dev/null +++ b/AnkiDroid/src/main/res/xml/preferences_reviewer_controls.xml @@ -0,0 +1,277 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From 49d39638386a86f4cfebf8ae9e300713d22b83cb Mon Sep 17 00:00:00 2001 From: Brayan Oliveira <69634269+brayandso@users.noreply.github.com> Date: Wed, 25 Dec 2024 14:48:07 -0300 Subject: [PATCH 20/29] previewer actions --- .../ichi2/anki/cardviewer/ViewerCommand.kt | 4 +- .../preferences/ControlsSettingsFragment.kt | 20 +++- .../ichi2/anki/previewer/PreviewerFragment.kt | 72 ++++++++------- .../ichi2/anki/reviewer/MappableBinding.kt | 82 ++++++++++++++++- .../ichi2/preferences/ControlPreference.kt | 6 +- .../preferences/PreviewerControlPreference.kt | 91 +++++++++++++++++++ .../preferences/ReviewerControlPreference.kt | 7 +- AnkiDroid/src/main/res/layout/previewer.xml | 8 +- AnkiDroid/src/main/res/values/02-strings.xml | 2 + .../src/main/res/values/10-preferences.xml | 1 + AnkiDroid/src/main/res/values/preferences.xml | 15 +++ .../xml/preferences_previewer_controls.xml | 81 +++++++++++++++++ 12 files changed, 337 insertions(+), 52 deletions(-) create mode 100644 AnkiDroid/src/main/java/com/ichi2/preferences/PreviewerControlPreference.kt create mode 100644 AnkiDroid/src/main/res/xml/preferences_previewer_controls.xml diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/cardviewer/ViewerCommand.kt b/AnkiDroid/src/main/java/com/ichi2/anki/cardviewer/ViewerCommand.kt index 095deb9284b4..4565890ae64c 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/cardviewer/ViewerCommand.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/cardviewer/ViewerCommand.kt @@ -32,7 +32,7 @@ import com.ichi2.anki.reviewer.ReviewerBinding interface ScreenAction { @get:LayoutRes - val nameRes: Int + val titleRes: Int val preferenceKey: String fun getBindings(prefs: SharedPreferences): List @@ -147,7 +147,7 @@ enum class ViewerCommand( return ReviewerBinding.fromPreferenceString(prefValue) } - override val nameRes: Int get() = resourceId + override val titleRes: Int get() = resourceId // If we use the serialised format, then this adds additional coupling to the properties. val defaultValue: List diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/preferences/ControlsSettingsFragment.kt b/AnkiDroid/src/main/java/com/ichi2/anki/preferences/ControlsSettingsFragment.kt index a73e5d761ce5..2a0d13f7cc9c 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/preferences/ControlsSettingsFragment.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/preferences/ControlsSettingsFragment.kt @@ -24,9 +24,12 @@ import androidx.preference.get import com.google.android.material.tabs.TabLayout import com.ichi2.anki.CollectionManager.TR import com.ichi2.anki.R +import com.ichi2.anki.cardviewer.ScreenAction import com.ichi2.anki.cardviewer.ViewerCommand import com.ichi2.anki.reviewer.MappableBinding.Companion.toPreferenceString +import com.ichi2.anki.reviewer.PreviewerAction import com.ichi2.anki.ui.internationalization.toSentenceCase +import com.ichi2.anki.utils.ext.sharedPrefs import com.ichi2.annotations.NeedsTest import com.ichi2.preferences.ControlPreference import timber.log.Timber @@ -77,18 +80,20 @@ class ControlsSettingsFragment : Timber.v("Selected tab %d - %s", tab.position, screen.name) addPreferencesFromResource(screen.xmlRes) - val commands = ViewerCommand.entries.associateBy { it.preferenceKey } + val commands = screen.getActions().associateBy { it.preferenceKey } // set defaultValue in the prefs creation. // if a preference is empty, it has a value like "1/" + val prefs = sharedPrefs() allPreferences() .filterIsInstance>() .filter { pref -> pref.getValue() == null } - .forEach { pref -> commands[pref.key]?.defaultValue?.toPreferenceString()?.let { pref.setValue(it) } } + .forEach { pref -> commands[pref.key]?.getBindings(prefs)?.toPreferenceString()?.let { pref.setValue(it) } } } override fun onTabUnselected(tab: TabLayout.Tab?) { for (i in staticPreferencesCount until preferenceScreen.preferenceCount) { - preferenceScreen.removePreference(preferenceScreen[staticPreferencesCount]) + val pref = preferenceScreen[staticPreferencesCount] + preferenceScreen.removePreference(pref) } } @@ -141,5 +146,12 @@ enum class ControlPreferenceScreen( @StringRes val titleRes: Int, ) { REVIEWER(R.xml.preferences_reviewer_controls, R.string.pref_cat_reviewer), - PREVIEWER(R.xml.preferences_accessibility, R.string.accessibility), + PREVIEWER(R.xml.preferences_previewer_controls, R.string.card_editor_preview_card), + ; + + fun getActions(): List> = + when (this) { + REVIEWER -> ViewerCommand.entries + PREVIEWER -> PreviewerAction.entries + } } diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/previewer/PreviewerFragment.kt b/AnkiDroid/src/main/java/com/ichi2/anki/previewer/PreviewerFragment.kt index b16e093b4b12..a8cc65bcf753 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/previewer/PreviewerFragment.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/previewer/PreviewerFragment.kt @@ -41,6 +41,10 @@ import com.ichi2.anki.Flag import com.ichi2.anki.R import com.ichi2.anki.browser.PreviewerIdsFile import com.ichi2.anki.cardviewer.CardMediaPlayer +import com.ichi2.anki.reviewer.BindingProcessor +import com.ichi2.anki.reviewer.PreviewerAction +import com.ichi2.anki.reviewer.PreviewerBinding +import com.ichi2.anki.reviewer.ScreenKeyMap import com.ichi2.anki.snackbar.BaseSnackbarBuilderProvider import com.ichi2.anki.snackbar.SnackbarBuilder import com.ichi2.anki.utils.ext.sharedPrefs @@ -53,7 +57,8 @@ class PreviewerFragment : CardViewerFragment(R.layout.previewer), Toolbar.OnMenuItemClickListener, BaseSnackbarBuilderProvider, - DispatchKeyEventListener { + DispatchKeyEventListener, + BindingProcessor { override val viewModel: PreviewerViewModel by viewModels { val previewerIdsFile = requireNotNull(BundleCompat.getParcelable(requireArguments(), CARD_IDS_FILE_ARG, PreviewerIdsFile::class.java)) { @@ -76,6 +81,8 @@ class PreviewerFragment : } } + private lateinit var keyMap: ScreenKeyMap + override fun onViewCreated( view: View, savedInstanceState: Bundle?, @@ -181,6 +188,8 @@ class PreviewerFragment : if (sharedPrefs().getBoolean("safeDisplay", false)) { view.findViewById(R.id.webview_container).elevation = 0F } + + keyMap = ScreenKeyMap(sharedPrefs(), PreviewerAction.entries, this) } private fun setupFlagMenu(menu: Menu) { @@ -211,6 +220,33 @@ class PreviewerFragment : return true } + override fun executeAction( + action: PreviewerAction, + forBinding: PreviewerBinding, + ): Boolean { + when (action) { + PreviewerAction.MARK -> viewModel.toggleMark() + PreviewerAction.EDIT -> editCard() + PreviewerAction.TOGGLE_BACKSIDE_ONLY -> viewModel.toggleBackSideOnly() + PreviewerAction.REPLAY_AUDIO -> viewModel.replayAudios() + PreviewerAction.TOGGLE_FLAG_RED -> viewModel.toggleFlag(Flag.RED) + PreviewerAction.TOGGLE_FLAG_ORANGE -> viewModel.toggleFlag(Flag.ORANGE) + PreviewerAction.TOGGLE_FLAG_GREEN -> viewModel.toggleFlag(Flag.GREEN) + PreviewerAction.TOGGLE_FLAG_BLUE -> viewModel.toggleFlag(Flag.BLUE) + PreviewerAction.TOGGLE_FLAG_PINK -> viewModel.toggleFlag(Flag.PINK) + PreviewerAction.TOGGLE_FLAG_TURQUOISE -> viewModel.toggleFlag(Flag.TURQUOISE) + PreviewerAction.TOGGLE_FLAG_PURPLE -> viewModel.toggleFlag(Flag.PURPLE) + PreviewerAction.UNSET_FLAG -> viewModel.setFlag(Flag.NONE) + PreviewerAction.BACK -> { + requireView().findViewById(R.id.show_previous).performClickIfEnabled() + } + PreviewerAction.NEXT -> { + requireView().findViewById(R.id.show_next).performClickIfEnabled() + } + } + return true + } + private fun setBackSideOnlyButtonIcon( menu: Menu, isBackSideOnly: Boolean, @@ -240,39 +276,7 @@ class PreviewerFragment : override fun dispatchKeyEvent(event: KeyEvent): Boolean { if (event.action != KeyEvent.ACTION_DOWN) return false - - if (event.isCtrlPressed) { - when (event.keyCode) { - KeyEvent.KEYCODE_1 -> viewModel.toggleFlag(Flag.RED) - KeyEvent.KEYCODE_2 -> viewModel.toggleFlag(Flag.ORANGE) - KeyEvent.KEYCODE_3 -> viewModel.toggleFlag(Flag.GREEN) - KeyEvent.KEYCODE_4 -> viewModel.toggleFlag(Flag.BLUE) - KeyEvent.KEYCODE_5 -> viewModel.toggleFlag(Flag.PINK) - KeyEvent.KEYCODE_6 -> viewModel.toggleFlag(Flag.TURQUOISE) - KeyEvent.KEYCODE_7 -> viewModel.toggleFlag(Flag.PURPLE) - else -> return false - } - return true - } - - when (event.unicodeChar.toChar()) { - '*' -> { - viewModel.toggleMark() - return true - } - } - - when (event.keyCode) { - KeyEvent.KEYCODE_DPAD_LEFT -> { - requireView().findViewById(R.id.show_previous).performClickIfEnabled() - } - KeyEvent.KEYCODE_DPAD_RIGHT -> { - requireView().findViewById(R.id.show_next).performClickIfEnabled() - } - KeyEvent.KEYCODE_R -> viewModel.replayAudios() - else -> return false - } - return true + return keyMap.onKeyDown(event) } companion object { diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/reviewer/MappableBinding.kt b/AnkiDroid/src/main/java/com/ichi2/anki/reviewer/MappableBinding.kt index 0f0b2fd76500..2c18faa12cc9 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/reviewer/MappableBinding.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/reviewer/MappableBinding.kt @@ -17,14 +17,19 @@ package com.ichi2.anki.reviewer import android.content.Context +import android.content.SharedPreferences +import android.view.KeyEvent import androidx.annotation.CheckResult import com.ichi2.anki.R import com.ichi2.anki.cardviewer.Gesture import com.ichi2.anki.cardviewer.ScreenAction import com.ichi2.anki.reviewer.Binding.AxisButtonBinding +import com.ichi2.anki.reviewer.Binding.Companion.keyCode +import com.ichi2.anki.reviewer.Binding.Companion.unicode import com.ichi2.anki.reviewer.Binding.GestureInput import com.ichi2.anki.reviewer.Binding.KeyBinding import com.ichi2.anki.reviewer.Binding.KeyCode +import com.ichi2.anki.reviewer.Binding.ModifierKeys.Companion.ctrl import com.ichi2.anki.reviewer.Binding.UnicodeCharacter import com.ichi2.utils.hash import timber.log.Timber @@ -130,7 +135,7 @@ class ReviewerBinding( side === other.side } - override fun hashCode(): Int = Objects.hash(getBindingHash(), PREFIX) + override fun hashCode(): Int = Objects.hash(getBindingHash(), PREFIX, side) override fun toPreferenceString(): String? { if (!binding.isValid) return null @@ -186,3 +191,78 @@ class ReviewerBinding( fun fromGesture(gesture: Gesture): ReviewerBinding = ReviewerBinding(GestureInput(gesture), CardSide.BOTH) } } + +class PreviewerBinding( + binding: Binding, +) : MappableBinding(binding) { + override fun toDisplayString(context: Context): String = binding.toDisplayString(context) + + override fun toPreferenceString(): String = binding.toString() + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + if (!super.equals(other)) return false + return true + } + + override fun hashCode(): Int = Objects.hash(binding, 'p') + + companion object { + fun fromPreferenceValue(prefValue: String?): List { + if (prefValue.isNullOrEmpty()) return emptyList() + return getPreferenceBindingStrings(prefValue).map { + val binding = Binding.fromString(it) + PreviewerBinding(binding) + } + } + } +} + +enum class PreviewerAction( + override val titleRes: Int, +) : ScreenAction { + BACK(R.string.previewer_back), + NEXT(R.string.previewer_next), + MARK(R.string.menu_mark_note), + EDIT(R.string.cardeditor_title_edit_card), + TOGGLE_BACKSIDE_ONLY(R.string.toggle_backside_only), + REPLAY_AUDIO(R.string.replay_audio), + TOGGLE_FLAG_RED(R.string.gesture_flag_red), + TOGGLE_FLAG_ORANGE(R.string.gesture_flag_orange), + TOGGLE_FLAG_GREEN(R.string.gesture_flag_green), + TOGGLE_FLAG_BLUE(R.string.gesture_flag_blue), + TOGGLE_FLAG_PINK(R.string.gesture_flag_pink), + TOGGLE_FLAG_TURQUOISE(R.string.gesture_flag_turquoise), + TOGGLE_FLAG_PURPLE(R.string.gesture_flag_purple), + UNSET_FLAG(R.string.gesture_flag_purple), + ; + + override val preferenceKey = "previewer_$name" + + override fun getBindings(prefs: SharedPreferences): List { + val prefValue = prefs.getString(preferenceKey, null) ?: return defaultBindings + return PreviewerBinding.fromPreferenceValue(prefValue) + } + + private val defaultBindings: List get() { + val binding = + when (this) { + BACK -> keyCode(KeyEvent.KEYCODE_DPAD_LEFT) + NEXT -> keyCode(KeyEvent.KEYCODE_DPAD_RIGHT) + MARK -> unicode('*') + REPLAY_AUDIO -> keyCode(KeyEvent.KEYCODE_R) + EDIT -> keyCode(KeyEvent.KEYCODE_E) + TOGGLE_BACKSIDE_ONLY -> keyCode(KeyEvent.KEYCODE_B) + TOGGLE_FLAG_RED -> keyCode(KeyEvent.KEYCODE_1, ctrl()) + TOGGLE_FLAG_ORANGE -> keyCode(KeyEvent.KEYCODE_2, ctrl()) + TOGGLE_FLAG_GREEN -> keyCode(KeyEvent.KEYCODE_3, ctrl()) + TOGGLE_FLAG_BLUE -> keyCode(KeyEvent.KEYCODE_4, ctrl()) + TOGGLE_FLAG_PINK -> keyCode(KeyEvent.KEYCODE_5, ctrl()) + TOGGLE_FLAG_TURQUOISE -> keyCode(KeyEvent.KEYCODE_6, ctrl()) + TOGGLE_FLAG_PURPLE -> keyCode(KeyEvent.KEYCODE_7, ctrl()) + UNSET_FLAG -> return emptyList() + } + return listOf(PreviewerBinding(binding)) + } +} diff --git a/AnkiDroid/src/main/java/com/ichi2/preferences/ControlPreference.kt b/AnkiDroid/src/main/java/com/ichi2/preferences/ControlPreference.kt index f6e7d382ce91..bf46ed5be5fd 100644 --- a/AnkiDroid/src/main/java/com/ichi2/preferences/ControlPreference.kt +++ b/AnkiDroid/src/main/java/com/ichi2/preferences/ControlPreference.kt @@ -31,7 +31,6 @@ import androidx.fragment.app.DialogFragment import androidx.preference.DialogPreference import androidx.preference.PreferenceFragmentCompat import com.ichi2.anki.R -import com.ichi2.anki.cardviewer.Gesture import com.ichi2.anki.cardviewer.GestureProcessor import com.ichi2.anki.dialogs.GestureSelectionDialogUtils import com.ichi2.anki.dialogs.GestureSelectionDialogUtils.onGestureChanged @@ -85,7 +84,7 @@ abstract class ControlPreference : abstract val areGesturesEnabled: Boolean - protected open fun onGestureSelected(gesture: Gesture) {} + protected open fun onGestureSelected(binding: Binding) {} /** @return whether the binding is used in another action */ abstract fun warnIfUsed( @@ -113,7 +112,8 @@ abstract class ControlPreference : val gesturePicker = GestureSelectionDialogUtils.getGesturePicker(context) positiveButton(R.string.dialog_ok) { val gesture = gesturePicker.getGesture() ?: return@positiveButton - onGestureSelected(gesture) + val binding = Binding.GestureInput(gesture) + onGestureSelected(binding) it.dismiss() } negativeButton(R.string.dialog_cancel) { it.dismiss() } diff --git a/AnkiDroid/src/main/java/com/ichi2/preferences/PreviewerControlPreference.kt b/AnkiDroid/src/main/java/com/ichi2/preferences/PreviewerControlPreference.kt new file mode 100644 index 000000000000..45f4b7a94225 --- /dev/null +++ b/AnkiDroid/src/main/java/com/ichi2/preferences/PreviewerControlPreference.kt @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2024 Brayan Oliveira + * + * 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.preferences + +import android.content.Context +import android.util.AttributeSet +import com.ichi2.anki.R +import com.ichi2.anki.dialogs.WarningDisplay +import com.ichi2.anki.preferences.sharedPrefs +import com.ichi2.anki.reviewer.Binding +import com.ichi2.anki.reviewer.MappableBinding.Companion.toPreferenceString +import com.ichi2.anki.reviewer.PreviewerAction +import com.ichi2.anki.reviewer.PreviewerBinding +import com.ichi2.anki.showThemedToast + +class PreviewerControlPreference : ControlPreference { + @Suppress("unused") + constructor( + context: Context, + attrs: AttributeSet?, + defStyleAttr: Int, + defStyleRes: Int, + ) : super(context, attrs, defStyleAttr, defStyleRes) + + @Suppress("unused") + constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) + + @Suppress("unused") + constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) + + @Suppress("unused") + constructor(context: Context) : super(context) + + override val areGesturesEnabled: Boolean = false + + override fun getMappableBindings(): List = PreviewerBinding.fromPreferenceValue(getValue()) + + override fun onKeySelected(binding: Binding) = addBinding(binding) + + override fun onGestureSelected(binding: Binding) = addBinding(binding) + + override fun onAxisSelected(binding: Binding) = addBinding(binding) + + override fun warnIfUsed( + binding: Binding, + warningDisplay: WarningDisplay?, + ): Boolean { + val prefs = context.sharedPrefs() + val mappableBinding = PreviewerBinding(binding) + val actionsMap = + PreviewerAction.entries + .associateWith { a -> a.getBindings(prefs) } + .filterValues { it.isNotEmpty() } + val commandWithBinding = + actionsMap.entries + .firstOrNull { + it.value.any { b -> b == mappableBinding } + }?.key ?: return false + + if (commandWithBinding.preferenceKey == key) return false + + val actionTitle = context.getString(commandWithBinding.titleRes) + val warning = context.getString(R.string.bindings_already_bound, actionTitle) + if (warningDisplay != null) { + warningDisplay.setWarning(warning) + } else { + showThemedToast(context, warning, true) + } + return true + } + + private fun addBinding(binding: Binding) { + val previewerBinding = PreviewerBinding(binding) + val bindings = getMappableBindings().toMutableList() + bindings.add(previewerBinding) + setValue(bindings.toPreferenceString()) + } +} diff --git a/AnkiDroid/src/main/java/com/ichi2/preferences/ReviewerControlPreference.kt b/AnkiDroid/src/main/java/com/ichi2/preferences/ReviewerControlPreference.kt index d600b02ec6d7..982bf834f1b2 100644 --- a/AnkiDroid/src/main/java/com/ichi2/preferences/ReviewerControlPreference.kt +++ b/AnkiDroid/src/main/java/com/ichi2/preferences/ReviewerControlPreference.kt @@ -18,7 +18,6 @@ package com.ichi2.preferences import android.content.Context import android.util.AttributeSet import com.ichi2.anki.R -import com.ichi2.anki.cardviewer.Gesture import com.ichi2.anki.cardviewer.GestureProcessor import com.ichi2.anki.cardviewer.ViewerCommand import com.ichi2.anki.dialogs.CardSideSelectionDialog @@ -59,9 +58,7 @@ class ReviewerControlPreference : ControlPreference { } } - override fun onGestureSelected(gesture: Gesture) { - addBinding(Binding.GestureInput(gesture), CardSide.BOTH) - } + override fun onGestureSelected(binding: Binding) = addBinding(binding, CardSide.BOTH) override fun onAxisSelected(binding: Binding) { CardSideSelectionDialog.displayInstance(context) { side -> @@ -87,7 +84,7 @@ class ReviewerControlPreference : ControlPreference { if (commandWithBinding.preferenceKey == key) return false - val actionTitle = context.getString(commandWithBinding.nameRes) + val actionTitle = context.getString(commandWithBinding.titleRes) val warning = context.getString(R.string.bindings_already_bound, actionTitle) if (warningDisplay != null) { warningDisplay.setWarning(warning) diff --git a/AnkiDroid/src/main/res/layout/previewer.xml b/AnkiDroid/src/main/res/layout/previewer.xml index 6d89b4529ec6..ad9a79c40bfa 100644 --- a/AnkiDroid/src/main/res/layout/previewer.xml +++ b/AnkiDroid/src/main/res/layout/previewer.xml @@ -74,7 +74,8 @@ app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/slider" - app:iconSize="32dp" /> + app:iconSize="32dp" + android:contentDescription="@string/previewer_back"/> + android:gravity="center_horizontal"/> + app:iconSize="32dp" + android:contentDescription="@string/previewer_next"/> \ No newline at end of file diff --git a/AnkiDroid/src/main/res/values/02-strings.xml b/AnkiDroid/src/main/res/values/02-strings.xml index fba8a26862f4..45dbdd18d456 100644 --- a/AnkiDroid/src/main/res/values/02-strings.xml +++ b/AnkiDroid/src/main/res/values/02-strings.xml @@ -389,6 +389,8 @@ opening the system text to speech settings fails"> Reposition + Next + Back Record Stop diff --git a/AnkiDroid/src/main/res/values/10-preferences.xml b/AnkiDroid/src/main/res/values/10-preferences.xml index 8a9c7ff9b4b1..4f145161871a 100644 --- a/AnkiDroid/src/main/res/values/10-preferences.xml +++ b/AnkiDroid/src/main/res/values/10-preferences.xml @@ -257,6 +257,7 @@ Move a joystick/motion controller User actions Trigger JavaScript from the review screen + Toggle backside only User action 1 User action 2 User action 3 diff --git a/AnkiDroid/src/main/res/values/preferences.xml b/AnkiDroid/src/main/res/values/preferences.xml index 229f7391d151..1faf0b0ffef7 100644 --- a/AnkiDroid/src/main/res/values/preferences.xml +++ b/AnkiDroid/src/main/res/values/preferences.xml @@ -134,6 +134,21 @@ binding_USER_ACTION_8 binding_USER_ACTION_9 binding_TOGGLE_AUTO_ADVANCE + + previewer_NEXT + previewer_BACK + previewer_MARK + previewer_EDIT + previewer_EDIT + previewer_BACKSIDE_ONLY + previewer_TOGGLE_FLAG_RED + previewer_TOGGLE_FLAG_ORANGE + previewer_TOGGLE_FLAG_GREEN + previewer_TOGGLE_FLAG_BLUE + previewer_TOGGLE_FLAG_PINK + previewer_TOGGLE_FLAG_TURQUOISE + previewer_TOGGLE_FLAG_PURPLE + previewer_UNSET_FLAG accessibilityScreen cardZoom diff --git a/AnkiDroid/src/main/res/xml/preferences_previewer_controls.xml b/AnkiDroid/src/main/res/xml/preferences_previewer_controls.xml new file mode 100644 index 000000000000..4ce441586971 --- /dev/null +++ b/AnkiDroid/src/main/res/xml/preferences_previewer_controls.xml @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From 76a659b126a21bb9b31cbb78d83ed7df12bfa8e4 Mon Sep 17 00:00:00 2001 From: Brayan Oliveira <69634269+brayandso@users.noreply.github.com> Date: Wed, 25 Dec 2024 14:53:36 -0300 Subject: [PATCH 21/29] fixup --- AnkiDroid/src/main/java/com/ichi2/anki/Reviewer.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/Reviewer.kt b/AnkiDroid/src/main/java/com/ichi2/anki/Reviewer.kt index 28218284fb83..42ea681d28bf 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/Reviewer.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/Reviewer.kt @@ -81,6 +81,7 @@ import com.ichi2.anki.reviewer.AnswerTimer import com.ichi2.anki.reviewer.AutomaticAnswerAction import com.ichi2.anki.reviewer.BindingProcessor import com.ichi2.anki.reviewer.CardMarker +import com.ichi2.anki.reviewer.CardSide import com.ichi2.anki.reviewer.FullScreenMode import com.ichi2.anki.reviewer.FullScreenMode.Companion.fromPreference import com.ichi2.anki.reviewer.FullScreenMode.Companion.isFullScreenReview @@ -1668,5 +1669,8 @@ open class Reviewer : override fun executeAction( action: ViewerCommand, forBinding: ReviewerBinding, - ): Boolean = executeCommand(action, null) + ): Boolean { + if (CardSide.fromAnswer(isDisplayingAnswer) != forBinding.side) return false + return executeCommand(action, null) + } } From b01b109f9edc762cd258041e37c472a9acbdcb69 Mon Sep 17 00:00:00 2001 From: Brayan Oliveira <69634269+brayandso@users.noreply.github.com> Date: Wed, 25 Dec 2024 15:14:30 -0300 Subject: [PATCH 22/29] new reviewer --- .../src/main/java/com/ichi2/anki/Reviewer.kt | 6 +- .../anki/preferences/reviewer/ViewerAction.kt | 79 ++++++++++++++++++- .../ichi2/anki/previewer/PreviewerFragment.kt | 4 +- .../ichi2/anki/reviewer/MappableBinding.kt | 4 +- .../com/ichi2/anki/reviewer/ScreenKeyMap.kt | 3 +- .../ui/windows/reviewer/ReviewerFragment.kt | 41 ++++++++-- 6 files changed, 118 insertions(+), 19 deletions(-) diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/Reviewer.kt b/AnkiDroid/src/main/java/com/ichi2/anki/Reviewer.kt index 42ea681d28bf..d4b4a3fdd55e 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/Reviewer.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/Reviewer.kt @@ -1666,11 +1666,11 @@ open class Reviewer : const val ACTION_SNACKBAR_TIME = 500 } - override fun executeAction( + override fun processAction( action: ViewerCommand, - forBinding: ReviewerBinding, + binding: ReviewerBinding, ): Boolean { - if (CardSide.fromAnswer(isDisplayingAnswer) != forBinding.side) return false + if (binding.side != CardSide.BOTH && CardSide.fromAnswer(isDisplayingAnswer) != binding.side) return false return executeCommand(action, null) } } diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/preferences/reviewer/ViewerAction.kt b/AnkiDroid/src/main/java/com/ichi2/anki/preferences/reviewer/ViewerAction.kt index a064ba4ace27..ba9855b9276b 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/preferences/reviewer/ViewerAction.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/preferences/reviewer/ViewerAction.kt @@ -15,14 +15,22 @@ */ package com.ichi2.anki.preferences.reviewer +import android.content.SharedPreferences +import android.view.KeyEvent import androidx.annotation.DrawableRes import androidx.annotation.IdRes import androidx.annotation.StringRes import com.ichi2.anki.Flag import com.ichi2.anki.R +import com.ichi2.anki.cardviewer.ScreenAction import com.ichi2.anki.preferences.reviewer.MenuDisplayType.ALWAYS import com.ichi2.anki.preferences.reviewer.MenuDisplayType.DISABLED import com.ichi2.anki.preferences.reviewer.MenuDisplayType.MENU_ONLY +import com.ichi2.anki.reviewer.Binding +import com.ichi2.anki.reviewer.Binding.ModifierKeys +import com.ichi2.anki.reviewer.Binding.ModifierKeys.Companion.ctrl +import com.ichi2.anki.reviewer.CardSide +import com.ichi2.anki.reviewer.ReviewerBinding /** * @param menuId menu Id of the action @@ -34,10 +42,10 @@ import com.ichi2.anki.preferences.reviewer.MenuDisplayType.MENU_ONLY enum class ViewerAction( @IdRes val menuId: Int, @DrawableRes val drawableRes: Int?, - @StringRes val titleRes: Int = R.string.empty_string, + @StringRes override val titleRes: Int = R.string.empty_string, val defaultDisplayType: MenuDisplayType? = null, val parentMenu: ViewerAction? = null, -) { +) : ScreenAction { // Always UNDO(R.id.action_undo, R.drawable.ic_undo_white, R.string.undo, ALWAYS), @@ -45,7 +53,7 @@ enum class ViewerAction( REDO(R.id.action_redo, R.drawable.ic_redo, R.string.redo, MENU_ONLY), FLAG_MENU(R.id.action_flag, R.drawable.ic_flag_transparent, R.string.menu_flag, MENU_ONLY), MARK(R.id.action_mark, R.drawable.ic_star, R.string.menu_mark_note, MENU_ONLY), - EDIT_NOTE(R.id.action_edit_note, R.drawable.ic_mode_edit_white, R.string.cardeditor_title_edit_card, MENU_ONLY), + EDIT(R.id.action_edit_note, R.drawable.ic_mode_edit_white, R.string.cardeditor_title_edit_card, MENU_ONLY), BURY_MENU(R.id.action_bury, R.drawable.ic_flip_to_back_white, R.string.menu_bury, MENU_ONLY), SUSPEND_MENU(R.id.action_suspend, R.drawable.ic_suspend, R.string.menu_suspend, MENU_ONLY), DELETE(R.id.action_delete, R.drawable.ic_delete_white, R.string.menu_delete_note, MENU_ONLY), @@ -79,8 +87,73 @@ enum class ViewerAction( FLAG_PURPLE(Flag.PURPLE.id, Flag.PURPLE.drawableRes, parentMenu = FLAG_MENU), ; + override val preferenceKey: String get() = "binding_$name" + + override fun getBindings(prefs: SharedPreferences): List { + val prefValue = prefs.getString(preferenceKey, null) ?: return defaultBindings + return ReviewerBinding.fromPreferenceString(prefValue) + } + + private val defaultBindings: List get() = + when (this) { + UNDO -> listOf(keycode(KeyEvent.KEYCODE_Z, ctrl())) + REDO -> listOf(keycode(KeyEvent.KEYCODE_Z, ModifierKeys(shift = true, ctrl = true, alt = false))) + MARK -> listOf(unicode('*')) + EDIT -> listOf(keycode(KeyEvent.KEYCODE_E)) + ADD_NOTE -> listOf(keycode(KeyEvent.KEYCODE_A)) + BURY_NOTE -> listOf(unicode('=')) + BURY_CARD -> listOf(unicode('-')) + SUSPEND_NOTE -> listOf(unicode('!')) + SUSPEND_CARD -> listOf(unicode('@')) + // No default gestures + DELETE, + CARD_INFO, + USER_ACTION_1, + USER_ACTION_2, + USER_ACTION_3, + USER_ACTION_4, + USER_ACTION_5, + USER_ACTION_6, + USER_ACTION_7, + USER_ACTION_8, + USER_ACTION_9, + // Menu flag actions. They set the flag, but don't toggle it + UNSET_FLAG, + FLAG_RED, + FLAG_ORANGE, + FLAG_BLUE, + FLAG_GREEN, + FLAG_PINK, + FLAG_TURQUOISE, + FLAG_PURPLE, + // Menu only + DECK_OPTIONS, + BURY_MENU, + SUSPEND_MENU, + FLAG_MENU, + -> emptyList() + } + fun isSubMenu() = ViewerAction.entries.any { it.parentMenu == this } + private fun keycode( + keycode: Int, + keys: ModifierKeys = ModifierKeys.none(), + side: CardSide = CardSide.BOTH, + ): ReviewerBinding { + val binding = Binding.keyCode(keycode, keys) + return ReviewerBinding(binding = binding, side = side) + } + + private fun unicode( + unicodeChar: Char, + keys: ModifierKeys = ModifierKeys.none(), + side: CardSide = CardSide.BOTH, + ): ReviewerBinding { + val binding = Binding.unicode(unicodeChar, keys) + return ReviewerBinding(binding = binding, side = side) + } + companion object { fun fromId( @IdRes id: Int, diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/previewer/PreviewerFragment.kt b/AnkiDroid/src/main/java/com/ichi2/anki/previewer/PreviewerFragment.kt index a8cc65bcf753..a9db72c18de2 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/previewer/PreviewerFragment.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/previewer/PreviewerFragment.kt @@ -220,9 +220,9 @@ class PreviewerFragment : return true } - override fun executeAction( + override fun processAction( action: PreviewerAction, - forBinding: PreviewerBinding, + binding: PreviewerBinding, ): Boolean { when (action) { PreviewerAction.MARK -> viewModel.toggleMark() diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/reviewer/MappableBinding.kt b/AnkiDroid/src/main/java/com/ichi2/anki/reviewer/MappableBinding.kt index 2c18faa12cc9..7f0cdb6f87d3 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/reviewer/MappableBinding.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/reviewer/MappableBinding.kt @@ -36,9 +36,9 @@ import timber.log.Timber import java.util.Objects fun interface BindingProcessor> { - fun executeAction( + fun processAction( action: A, - forBinding: B, + binding: B, ): Boolean } diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/reviewer/ScreenKeyMap.kt b/AnkiDroid/src/main/java/com/ichi2/anki/reviewer/ScreenKeyMap.kt index ce1edda8bb6f..b84892ad939c 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/reviewer/ScreenKeyMap.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/reviewer/ScreenKeyMap.kt @@ -45,9 +45,8 @@ class ScreenKeyMap>( val bindings = Binding.possibleKeyBindings(event) for (binding in bindings) { val (mappableBinding, action) = bindingMap[binding] ?: continue - ret = ret or processor.executeAction(action, mappableBinding) + ret = ret or processor.processAction(action, mappableBinding) } - return ret } } diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/ui/windows/reviewer/ReviewerFragment.kt b/AnkiDroid/src/main/java/com/ichi2/anki/ui/windows/reviewer/ReviewerFragment.kt index f6c1cdaedbb5..649e7f6fed9d 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/ui/windows/reviewer/ReviewerFragment.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/ui/windows/reviewer/ReviewerFragment.kt @@ -20,6 +20,7 @@ import android.content.Intent import android.os.Bundle import android.text.SpannableString import android.text.style.UnderlineSpan +import android.view.KeyEvent import android.view.Menu import android.view.MenuItem import android.view.View @@ -43,6 +44,7 @@ import com.google.android.material.button.MaterialButton import com.google.android.material.textview.MaterialTextView import com.ichi2.anki.AbstractFlashcardViewer.Companion.RESULT_NO_MORE_CARDS import com.ichi2.anki.CollectionManager +import com.ichi2.anki.DispatchKeyEventListener import com.ichi2.anki.Flag import com.ichi2.anki.NoteEditor import com.ichi2.anki.R @@ -57,7 +59,7 @@ import com.ichi2.anki.preferences.reviewer.ViewerAction.BURY_NOTE import com.ichi2.anki.preferences.reviewer.ViewerAction.CARD_INFO import com.ichi2.anki.preferences.reviewer.ViewerAction.DECK_OPTIONS import com.ichi2.anki.preferences.reviewer.ViewerAction.DELETE -import com.ichi2.anki.preferences.reviewer.ViewerAction.EDIT_NOTE +import com.ichi2.anki.preferences.reviewer.ViewerAction.EDIT import com.ichi2.anki.preferences.reviewer.ViewerAction.FLAG_BLUE import com.ichi2.anki.preferences.reviewer.ViewerAction.FLAG_GREEN import com.ichi2.anki.preferences.reviewer.ViewerAction.FLAG_MENU @@ -84,6 +86,10 @@ import com.ichi2.anki.preferences.reviewer.ViewerAction.USER_ACTION_8 import com.ichi2.anki.preferences.reviewer.ViewerAction.USER_ACTION_9 import com.ichi2.anki.previewer.CardViewerActivity import com.ichi2.anki.previewer.CardViewerFragment +import com.ichi2.anki.reviewer.BindingProcessor +import com.ichi2.anki.reviewer.CardSide +import com.ichi2.anki.reviewer.ReviewerBinding +import com.ichi2.anki.reviewer.ScreenKeyMap import com.ichi2.anki.snackbar.BaseSnackbarBuilderProvider import com.ichi2.anki.snackbar.SnackbarBuilder import com.ichi2.anki.snackbar.showSnackbar @@ -98,7 +104,9 @@ import kotlinx.coroutines.launch class ReviewerFragment : CardViewerFragment(R.layout.reviewer2), BaseSnackbarBuilderProvider, - ActionMenuView.OnMenuItemClickListener { + ActionMenuView.OnMenuItemClickListener, + BindingProcessor, + DispatchKeyEventListener { override val viewModel: ReviewerViewModel by viewModels { ReviewerViewModel.factory(CardMediaPlayer()) } @@ -110,6 +118,8 @@ class ReviewerFragment : anchorView = this@ReviewerFragment.view?.findViewById(R.id.buttons_area) } + private lateinit var keyMap: ScreenKeyMap + override fun onStop() { super.onStop() if (!requireActivity().isChangingConfigurations) { @@ -122,6 +132,7 @@ class ReviewerFragment : savedInstanceState: Bundle?, ) { super.onViewCreated(view, savedInstanceState) + keyMap = ScreenKeyMap(sharedPrefs(), ViewerAction.entries, this) view.findViewById(R.id.toolbar).apply { setNavigationOnClickListener { requireActivity().onBackPressedDispatcher.onBackPressed() } @@ -154,15 +165,12 @@ class ReviewerFragment : } } - // TODO - override fun onMenuItemClick(item: MenuItem): Boolean { - if (item.hasSubMenu()) return false - val action = ViewerAction.fromId(item.itemId) + private fun executeAction(action: ViewerAction): Boolean { when (action) { ADD_NOTE -> launchAddNote() CARD_INFO -> launchCardInfo() DECK_OPTIONS -> launchDeckOptions() - EDIT_NOTE -> launchEditNote() + EDIT -> launchEditNote() DELETE -> viewModel.deleteNote() MARK -> viewModel.toggleMark() REDO -> viewModel.redo() @@ -195,6 +203,25 @@ class ReviewerFragment : return true } + override fun dispatchKeyEvent(event: KeyEvent): Boolean { + if (event.action != KeyEvent.ACTION_DOWN) return false + return keyMap.onKeyDown(event) + } + + override fun processAction( + action: ViewerAction, + binding: ReviewerBinding, + ): Boolean { + if (binding.side != CardSide.BOTH && CardSide.fromAnswer(viewModel.showingAnswer.value) != binding.side) return false + return executeAction(action) + } + + override fun onMenuItemClick(item: MenuItem): Boolean { + if (item.hasSubMenu()) return false + val action = ViewerAction.fromId(item.itemId) + return executeAction(action) + } + private fun setupAnswerButtons(view: View) { val hideAnswerButtons = sharedPrefs().getBoolean(getString(R.string.hide_answer_buttons_key), false) if (hideAnswerButtons) { From bf82bab255d95d4bea29926a77e4acdac28225f8 Mon Sep 17 00:00:00 2001 From: Brayan Oliveira <69634269+brayandso@users.noreply.github.com> Date: Wed, 25 Dec 2024 15:34:03 -0300 Subject: [PATCH 23/29] fixes --- AnkiDroid/src/main/res/values/02-strings.xml | 2 +- AnkiDroid/src/main/res/values/18-standard-models.xml | 2 +- .../src/test/java/com/ichi2/anki/ReviewerKeyboardInputTest.kt | 2 -- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/AnkiDroid/src/main/res/values/02-strings.xml b/AnkiDroid/src/main/res/values/02-strings.xml index 45dbdd18d456..2f6cf9ff1f36 100644 --- a/AnkiDroid/src/main/res/values/02-strings.xml +++ b/AnkiDroid/src/main/res/values/02-strings.xml @@ -395,7 +395,7 @@ opening the system text to speech settings fails"> Record Stop Play - Next + Next Cannot Delete Card Type diff --git a/AnkiDroid/src/main/res/values/18-standard-models.xml b/AnkiDroid/src/main/res/values/18-standard-models.xml index 9f4fe8d83be1..1053822f0720 100644 --- a/AnkiDroid/src/main/res/values/18-standard-models.xml +++ b/AnkiDroid/src/main/res/values/18-standard-models.xml @@ -3,5 +3,5 @@ Front - Back + Back diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/ReviewerKeyboardInputTest.kt b/AnkiDroid/src/test/java/com/ichi2/anki/ReviewerKeyboardInputTest.kt index 8ee3b65519b5..df08a06bf34a 100644 --- a/AnkiDroid/src/test/java/com/ichi2/anki/ReviewerKeyboardInputTest.kt +++ b/AnkiDroid/src/test/java/com/ichi2/anki/ReviewerKeyboardInputTest.kt @@ -447,7 +447,6 @@ class ReviewerKeyboardInputTest : RobolectricTest() { fun displayingAnswer(): KeyboardInputTestReviewer { val keyboardInputTestReviewer = KeyboardInputTestReviewer() displayAnswer = true - keyboardInputTestReviewer.processor.setup() return keyboardInputTestReviewer } @@ -455,7 +454,6 @@ class ReviewerKeyboardInputTest : RobolectricTest() { fun displayingQuestion(): KeyboardInputTestReviewer { val keyboardInputTestReviewer = KeyboardInputTestReviewer() displayAnswer = false - keyboardInputTestReviewer.processor.setup() return keyboardInputTestReviewer } } From de904a847bbe344f6cd680dd3ad913bc156ae405 Mon Sep 17 00:00:00 2001 From: Brayan Oliveira <69634269+brayandso@users.noreply.github.com> Date: Wed, 25 Dec 2024 15:46:53 -0300 Subject: [PATCH 24/29] fixes2 --- .../com/ichi2/anki/preferences/ControlsSettingsFragment.kt | 2 +- .../src/main/java/com/ichi2/preferences/ControlPreference.kt | 4 +--- AnkiDroid/src/main/res/values/10-preferences.xml | 3 +-- AnkiDroid/src/main/res/values/preferences.xml | 2 +- AnkiDroid/src/main/res/xml/preferences_previewer_controls.xml | 2 +- 5 files changed, 5 insertions(+), 8 deletions(-) diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/preferences/ControlsSettingsFragment.kt b/AnkiDroid/src/main/java/com/ichi2/anki/preferences/ControlsSettingsFragment.kt index 2a0d13f7cc9c..6b607873aeff 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/preferences/ControlsSettingsFragment.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/preferences/ControlsSettingsFragment.kt @@ -145,7 +145,7 @@ enum class ControlPreferenceScreen( @XmlRes val xmlRes: Int, @StringRes val titleRes: Int, ) { - REVIEWER(R.xml.preferences_reviewer_controls, R.string.pref_cat_reviewer), + REVIEWER(R.xml.preferences_reviewer_controls, R.string.review), PREVIEWER(R.xml.preferences_previewer_controls, R.string.card_editor_preview_card), ; diff --git a/AnkiDroid/src/main/java/com/ichi2/preferences/ControlPreference.kt b/AnkiDroid/src/main/java/com/ichi2/preferences/ControlPreference.kt index bf46ed5be5fd..fac7f178ae78 100644 --- a/AnkiDroid/src/main/java/com/ichi2/preferences/ControlPreference.kt +++ b/AnkiDroid/src/main/java/com/ichi2/preferences/ControlPreference.kt @@ -31,7 +31,6 @@ import androidx.fragment.app.DialogFragment import androidx.preference.DialogPreference import androidx.preference.PreferenceFragmentCompat import com.ichi2.anki.R -import com.ichi2.anki.cardviewer.GestureProcessor import com.ichi2.anki.dialogs.GestureSelectionDialogUtils import com.ichi2.anki.dialogs.GestureSelectionDialogUtils.onGestureChanged import com.ichi2.anki.dialogs.KeySelectionDialogUtils @@ -41,7 +40,6 @@ import com.ichi2.anki.preferences.requirePreference import com.ichi2.anki.reviewer.Binding import com.ichi2.anki.reviewer.MappableBinding import com.ichi2.anki.reviewer.MappableBinding.Companion.toPreferenceString -import com.ichi2.anki.utils.ext.sharedPrefs import com.ichi2.ui.AxisPicker import com.ichi2.ui.KeyPicker import com.ichi2.utils.create @@ -205,7 +203,7 @@ class ControlPreferenceDialogFragment : DialogFragment() { preference.showGesturePickerDialog() dismiss() } - isVisible = sharedPrefs().getBoolean(GestureProcessor.PREF_KEY, false) + isVisible = preference.areGesturesEnabled } view.findViewById(R.id.add_key).setOnClickListener { diff --git a/AnkiDroid/src/main/res/values/10-preferences.xml b/AnkiDroid/src/main/res/values/10-preferences.xml index 4f145161871a..b57095d02405 100644 --- a/AnkiDroid/src/main/res/values/10-preferences.xml +++ b/AnkiDroid/src/main/res/values/10-preferences.xml @@ -174,7 +174,7 @@ Reset Third-party API apps See a list of applications which make use of the AnkiDroid API such as dictionaries, utilities. - + Review Reviewer Show deck title @@ -250,7 +250,6 @@ Add gesture - Replace gesture Add key Press a key Add joystick/motion controller diff --git a/AnkiDroid/src/main/res/values/preferences.xml b/AnkiDroid/src/main/res/values/preferences.xml index 1faf0b0ffef7..7c5b8e6f643b 100644 --- a/AnkiDroid/src/main/res/values/preferences.xml +++ b/AnkiDroid/src/main/res/values/preferences.xml @@ -139,7 +139,7 @@ previewer_BACK previewer_MARK previewer_EDIT - previewer_EDIT + previewer_REPLAY_AUDIO previewer_BACKSIDE_ONLY previewer_TOGGLE_FLAG_RED previewer_TOGGLE_FLAG_ORANGE diff --git a/AnkiDroid/src/main/res/xml/preferences_previewer_controls.xml b/AnkiDroid/src/main/res/xml/preferences_previewer_controls.xml index 4ce441586971..f4367e251a34 100644 --- a/AnkiDroid/src/main/res/xml/preferences_previewer_controls.xml +++ b/AnkiDroid/src/main/res/xml/preferences_previewer_controls.xml @@ -20,7 +20,7 @@ android:icon="@drawable/ic_star_border_white" /> From 2dc26ea423748f447e526f9b61f90cd0c86b10fc Mon Sep 17 00:00:00 2001 From: Brayan Oliveira <69634269+brayandso@users.noreply.github.com> Date: Wed, 25 Dec 2024 15:59:31 -0300 Subject: [PATCH 25/29] perf improvements --- .../anki/preferences/ControlsSettingsFragment.kt | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/preferences/ControlsSettingsFragment.kt b/AnkiDroid/src/main/java/com/ichi2/anki/preferences/ControlsSettingsFragment.kt index 6b607873aeff..60e1ac0e3d4c 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/preferences/ControlsSettingsFragment.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/preferences/ControlsSettingsFragment.kt @@ -42,12 +42,17 @@ class ControlsSettingsFragment : override val analyticsScreenNameConstant: String get() = "prefs.controls" + /** + * The number of preferences defined statically at the [preferenceResource]. + * It is used to keep them while trying to remove all of the others in [onTabUnselected]. + */ private var staticPreferencesCount: Int = 0 @NeedsTest("Keys and titles in the XML layout are the same of the ViewerCommands") override fun initSubscreen() { requirePreference(R.string.pref_controls_tab_layout_key).setViewId(R.id.tab_layout) staticPreferencesCount = preferenceScreen.preferenceCount + addPreferencesFromResource(ControlPreferenceScreen.entries.first().xmlRes) } override fun onViewCreated( @@ -63,14 +68,11 @@ class ControlsSettingsFragment : } private fun setupTabLayout(tabLayout: TabLayout) { - tabLayout.addOnTabSelectedListener(this) for (screen in ControlPreferenceScreen.entries) { - val tab = - tabLayout.newTab().apply { - setText(screen.titleRes) - } + val tab = tabLayout.newTab().setText(screen.titleRes) tabLayout.addTab(tab) } + tabLayout.addOnTabSelectedListener(this) } private fun getScreen(tab: TabLayout.Tab): ControlPreferenceScreen = ControlPreferenceScreen.entries[tab.position] From 398f3bff43585a1e1bddfd6a02db35080c69de25 Mon Sep 17 00:00:00 2001 From: Brayan Oliveira <69634269+brayandso@users.noreply.github.com> Date: Wed, 25 Dec 2024 16:01:50 -0300 Subject: [PATCH 26/29] fixeeeess --- .../preferences/ControlsSettingsFragment.kt | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/preferences/ControlsSettingsFragment.kt b/AnkiDroid/src/main/java/com/ichi2/anki/preferences/ControlsSettingsFragment.kt index 60e1ac0e3d4c..2177d919e519 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/preferences/ControlsSettingsFragment.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/preferences/ControlsSettingsFragment.kt @@ -52,7 +52,9 @@ class ControlsSettingsFragment : override fun initSubscreen() { requirePreference(R.string.pref_controls_tab_layout_key).setViewId(R.id.tab_layout) staticPreferencesCount = preferenceScreen.preferenceCount - addPreferencesFromResource(ControlPreferenceScreen.entries.first().xmlRes) + val initialScreen = ControlPreferenceScreen.entries.first() + addPreferencesFromResource(initialScreen.xmlRes) + setControlPreferencesDefaultValues(initialScreen) } override fun onViewCreated( @@ -77,19 +79,21 @@ class ControlsSettingsFragment : private fun getScreen(tab: TabLayout.Tab): ControlPreferenceScreen = ControlPreferenceScreen.entries[tab.position] - override fun onTabSelected(tab: TabLayout.Tab) { - val screen = getScreen(tab) - Timber.v("Selected tab %d - %s", tab.position, screen.name) - addPreferencesFromResource(screen.xmlRes) - + private fun setControlPreferencesDefaultValues(screen: ControlPreferenceScreen) { val commands = screen.getActions().associateBy { it.preferenceKey } - // set defaultValue in the prefs creation. - // if a preference is empty, it has a value like "1/" val prefs = sharedPrefs() allPreferences() .filterIsInstance>() .filter { pref -> pref.getValue() == null } .forEach { pref -> commands[pref.key]?.getBindings(prefs)?.toPreferenceString()?.let { pref.setValue(it) } } + setTitlesFromBackend() + } + + override fun onTabSelected(tab: TabLayout.Tab) { + val screen = getScreen(tab) + Timber.v("Selected tab %d - %s", tab.position, screen.name) + addPreferencesFromResource(screen.xmlRes) + setControlPreferencesDefaultValues(screen) } override fun onTabUnselected(tab: TabLayout.Tab?) { From 4ab55c60ec1405037fcc81b97b5d9af702bae29a Mon Sep 17 00:00:00 2001 From: Brayan Oliveira <69634269+brayandso@users.noreply.github.com> Date: Wed, 25 Dec 2024 16:16:39 -0300 Subject: [PATCH 27/29] fixeeeess --- AnkiDroid/src/main/java/com/ichi2/anki/Reviewer.kt | 6 ++++-- .../src/main/java/com/ichi2/anki/reviewer/Binding.kt | 3 ++- .../test/java/com/ichi2/anki/ReviewerKeyboardInputTest.kt | 8 ++++++++ 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/Reviewer.kt b/AnkiDroid/src/main/java/com/ichi2/anki/Reviewer.kt index d4b4a3fdd55e..edffd73bcf84 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/Reviewer.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/Reviewer.kt @@ -79,6 +79,7 @@ import com.ichi2.anki.reviewer.AnswerButtons.Companion.getBackgroundColors import com.ichi2.anki.reviewer.AnswerButtons.Companion.getTextColors import com.ichi2.anki.reviewer.AnswerTimer import com.ichi2.anki.reviewer.AutomaticAnswerAction +import com.ichi2.anki.reviewer.Binding import com.ichi2.anki.reviewer.BindingProcessor import com.ichi2.anki.reviewer.CardMarker import com.ichi2.anki.reviewer.CardSide @@ -201,7 +202,7 @@ open class Reviewer : private lateinit var toolbar: Toolbar @VisibleForTesting - protected lateinit var processor: ScreenKeyMap + protected open lateinit var processor: ScreenKeyMap private val addNoteLauncher = registerForActivityResult( @@ -1671,6 +1672,7 @@ open class Reviewer : binding: ReviewerBinding, ): Boolean { if (binding.side != CardSide.BOTH && CardSide.fromAnswer(isDisplayingAnswer) != binding.side) return false - return executeCommand(action, null) + val gesture = (binding.binding as? Binding.GestureInput)?.gesture + return executeCommand(action, gesture) } } diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/reviewer/Binding.kt b/AnkiDroid/src/main/java/com/ichi2/anki/reviewer/Binding.kt index 5a61e06b7f49..4f230c7dc8df 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/reviewer/Binding.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/reviewer/Binding.kt @@ -136,7 +136,8 @@ sealed interface Binding { append(unicodeCharacter) } - override fun equals(other: Any?): Boolean = super.equals(other) + override fun equals(other: Any?): Boolean = + (other is UnicodeCharacter && unicodeCharacter == other.unicodeCharacter && modifierKeys == other.modifierKeys) override fun hashCode(): Int = Objects.hash(unicodeCharacter, modifierKeys) } diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/ReviewerKeyboardInputTest.kt b/AnkiDroid/src/test/java/com/ichi2/anki/ReviewerKeyboardInputTest.kt index df08a06bf34a..867dc25a3aec 100644 --- a/AnkiDroid/src/test/java/com/ichi2/anki/ReviewerKeyboardInputTest.kt +++ b/AnkiDroid/src/test/java/com/ichi2/anki/ReviewerKeyboardInputTest.kt @@ -45,6 +45,7 @@ import com.ichi2.anki.reviewer.Binding.Companion.keyCode import com.ichi2.anki.reviewer.Binding.ModifierKeys import com.ichi2.anki.reviewer.CardSide import com.ichi2.anki.reviewer.ReviewerBinding +import com.ichi2.anki.reviewer.ScreenKeyMap import com.ichi2.libanki.Card import kotlinx.coroutines.Job import org.hamcrest.MatcherAssert.assertThat @@ -56,6 +57,10 @@ import org.mockito.Mockito import org.mockito.kotlin.whenever import timber.log.Timber +// ReviewerKeyboardInputTest +// PreferencesAnalyticsTest +// PeripheralKeymapTest + @RunWith(AndroidJUnit4::class) class ReviewerKeyboardInputTest : RobolectricTest() { @Test @@ -250,6 +255,9 @@ class ReviewerKeyboardInputTest : RobolectricTest() { var replayAudioCalled = false private set + override var processor: ScreenKeyMap = + ScreenKeyMap(sharedPrefs(), ViewerCommand.entries, this) + private val cardFlips = mutableListOf() override val isDrawerOpen: Boolean get() = false From a711700e9f6922920dd3ec4104cd76d966cc5662 Mon Sep 17 00:00:00 2001 From: Brayan Oliveira <69634269+brayandso@users.noreply.github.com> Date: Wed, 25 Dec 2024 16:31:32 -0300 Subject: [PATCH 28/29] =?UTF-8?q?foi=20ser=C3=A1=3F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../anki/reviewer/PeripheralKeymapTest.kt | 57 --------- .../ichi2/anki/analytics/UsageAnalytics.kt | 15 +++ .../ichi2/anki/reviewer/PeripheralKeymap.kt | 108 ------------------ .../analytics/PreferencesAnalyticsTest.kt | 2 + .../anki/preferences/PreferenceTestUtils.kt | 18 ++- .../anki/reviewer/PeripheralKeymapTest.kt | 53 --------- 6 files changed, 30 insertions(+), 223 deletions(-) delete mode 100644 AnkiDroid/src/androidTest/java/com/ichi2/anki/reviewer/PeripheralKeymapTest.kt delete mode 100644 AnkiDroid/src/main/java/com/ichi2/anki/reviewer/PeripheralKeymap.kt delete mode 100644 AnkiDroid/src/test/java/com/ichi2/anki/reviewer/PeripheralKeymapTest.kt diff --git a/AnkiDroid/src/androidTest/java/com/ichi2/anki/reviewer/PeripheralKeymapTest.kt b/AnkiDroid/src/androidTest/java/com/ichi2/anki/reviewer/PeripheralKeymapTest.kt deleted file mode 100644 index c148563b8f8b..000000000000 --- a/AnkiDroid/src/androidTest/java/com/ichi2/anki/reviewer/PeripheralKeymapTest.kt +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright (c) 2020 David Allison - * - * 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.reviewer - -import android.view.KeyEvent -import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.ichi2.anki.cardviewer.Gesture -import com.ichi2.anki.cardviewer.ViewerCommand -import com.ichi2.anki.tests.InstrumentedTest -import com.ichi2.anki.testutil.MockReviewerUi -import org.hamcrest.MatcherAssert.assertThat -import org.hamcrest.Matchers.equalTo -import org.hamcrest.Matchers.hasSize -import org.junit.Test -import org.junit.runner.RunWith - -@RunWith(AndroidJUnit4::class) -class PeripheralKeymapTest : InstrumentedTest() { - @Test - fun testNumpadAction() { - // #7736 Ensures that a numpad key is passed through (mostly testing num lock) - val processed: MutableList = ArrayList() - - val peripheralKeymap = - PeripheralKeymap(MockReviewerUi.displayingAnswer()) { e: ViewerCommand, _: Gesture? -> processed.add(e) } - peripheralKeymap.setup() - - peripheralKeymap.onKeyDown( - KeyEvent.KEYCODE_NUMPAD_1, - getNumpadEvent(KeyEvent.KEYCODE_NUMPAD_1), - ) - peripheralKeymap.onKeyUp( - KeyEvent.KEYCODE_NUMPAD_1, - getNumpadEvent(KeyEvent.KEYCODE_NUMPAD_1), - ) - assertThat>(processed, hasSize(1)) - assertThat( - processed[0], - equalTo(ViewerCommand.FLIP_OR_ANSWER_EASE1), - ) - } - - private fun getNumpadEvent(keycode: Int): KeyEvent = KeyEvent(0, 0, KeyEvent.ACTION_UP, keycode, 0, KeyEvent.META_NUM_LOCK_ON) -} diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/analytics/UsageAnalytics.kt b/AnkiDroid/src/main/java/com/ichi2/anki/analytics/UsageAnalytics.kt index 8c90f87da734..25d85a6006ed 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/analytics/UsageAnalytics.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/analytics/UsageAnalytics.kt @@ -567,6 +567,21 @@ object UsageAnalytics { "binding_USER_ACTION_7", "binding_USER_ACTION_8", "binding_USER_ACTION_9", + // ******************************** Controls - Previewer ******************************* + "previewer_NEXT", + "previewer_BACK", + "previewer_MARK", + "previewer_EDIT", + "previewer_REPLAY_AUDIO", + "previewer_BACKSIDE_ONLY", + "previewer_TOGGLE_FLAG_RED", + "previewer_TOGGLE_FLAG_ORANGE", + "previewer_TOGGLE_FLAG_GREEN", + "previewer_TOGGLE_FLAG_BLUE", + "previewer_TOGGLE_FLAG_PINK", + "previewer_TOGGLE_FLAG_TURQUOISE", + "previewer_TOGGLE_FLAG_PURPLE", + "previewer_UNSET_FLAG", // ******************************** Accessibility ****************************************** "cardZoom", "imageZoom", diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/reviewer/PeripheralKeymap.kt b/AnkiDroid/src/main/java/com/ichi2/anki/reviewer/PeripheralKeymap.kt deleted file mode 100644 index f3564a98bc2b..000000000000 --- a/AnkiDroid/src/main/java/com/ichi2/anki/reviewer/PeripheralKeymap.kt +++ /dev/null @@ -1,108 +0,0 @@ -/* - Copyright (c) 2020 David Allison - - 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.reviewer - -import android.content.SharedPreferences -import android.view.KeyEvent -import com.ichi2.anki.AnkiDroidApp -import com.ichi2.anki.cardviewer.ViewerCommand -import com.ichi2.anki.preferences.sharedPrefs -import com.ichi2.anki.reviewer.Binding.Companion.possibleKeyBindings -import com.ichi2.anki.reviewer.CardSide.Companion.fromAnswer - -/** Accepts peripheral input, mapping via various keybinding strategies, - * and converting them to commands for the Reviewer. */ -class PeripheralKeymap( - reviewerUi: ReviewerUi, - commandProcessor: ViewerCommand.CommandProcessor, -) { - private val keyMap: KeyMap = KeyMap(commandProcessor, reviewerUi) - private var hasSetup = false - - fun setup() { - val preferences = AnkiDroidApp.instance.sharedPrefs() - setup(preferences) - } - - fun setup(preferences: SharedPreferences) { - for (command in ViewerCommand.entries) { - add(command, preferences) - } - hasSetup = true - } - - private fun add( - command: ViewerCommand, - preferences: SharedPreferences, - ) { - val bindings = command.getBindings(preferences) - for (b in bindings) { - if (!b.isKey) { - continue - } - keyMap[b] = command - } - } - - fun onKeyDown( - keyCode: Int, - event: KeyEvent, - ): Boolean = - if (!hasSetup || event.repeatCount > 0) { - false - } else { - keyMap.onKeyDown(keyCode, event) - } - - @Suppress("UNUSED_PARAMETER") - fun onKeyUp( - keyCode: Int, - event: KeyEvent?, - ): Boolean = false - - class KeyMap( - private val processor: ViewerCommand.CommandProcessor, - private val reviewerUI: ReviewerUi, - ) { - val bindingMap = HashMap() - - @Suppress("UNUSED_PARAMETER") - fun onKeyDown( - keyCode: Int, - event: KeyEvent?, - ): Boolean { - var ret = false - val bindings = possibleKeyBindings(event!!) - val side = fromAnswer(reviewerUI.isDisplayingAnswer) - for (b in bindings) { - val binding = ReviewerBinding(b, side) - val command = bindingMap[binding] ?: continue - ret = ret or processor.executeCommand(command, fromGesture = null) - } - return ret - } - - operator fun set( - key: MappableBinding, - value: ViewerCommand, - ) { - bindingMap[key] = value - } - - operator fun get(key: MappableBinding): ViewerCommand? = bindingMap[key] - } -} diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/analytics/PreferencesAnalyticsTest.kt b/AnkiDroid/src/test/java/com/ichi2/anki/analytics/PreferencesAnalyticsTest.kt index 3154cd924731..d90438335dbf 100644 --- a/AnkiDroid/src/test/java/com/ichi2/anki/analytics/PreferencesAnalyticsTest.kt +++ b/AnkiDroid/src/test/java/com/ichi2/anki/analytics/PreferencesAnalyticsTest.kt @@ -75,6 +75,8 @@ class PreferencesAnalyticsTest : RobolectricTest() { "widgetVibrate", // Blink light "widgetBlink", + // Special views + "controlsTabLayout", // potential personal data "syncAccount", "syncBaseUrl", diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/preferences/PreferenceTestUtils.kt b/AnkiDroid/src/test/java/com/ichi2/anki/preferences/PreferenceTestUtils.kt index 9a8766d989e3..64d1e3b121bb 100644 --- a/AnkiDroid/src/test/java/com/ichi2/anki/preferences/PreferenceTestUtils.kt +++ b/AnkiDroid/src/test/java/com/ichi2/anki/preferences/PreferenceTestUtils.kt @@ -113,11 +113,19 @@ object PreferenceTestUtils { @XmlRes xml: Int, ): List = getAttrFromXml(context, xml, "key").map { attrValueToString(it, context) } - fun getAllPreferenceKeys(context: Context): Set = - getAllPreferencesFragments(context) - .filterIsInstance() - .map { it.preferenceResource } - .flatMapTo(hashSetOf()) { getKeysFromXml(context, it) } + fun getAllPreferenceKeys(context: Context): Set { + val controlPreferencesKeys = + ControlPreferenceScreen.entries.flatMap { + getKeysFromXml(context, it.xmlRes) + } + val staticPreferencesKeys = + getAllPreferencesFragments(context) + .filterIsInstance() + .map { it.preferenceResource } + .flatMapTo(hashSetOf()) { getKeysFromXml(context, it) } + + return staticPreferencesKeys + controlPreferencesKeys + } fun getAllCustomButtonKeys(context: Context): Set { val keys = getKeysFromXml(context, R.xml.preferences_custom_buttons).toMutableSet() diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/reviewer/PeripheralKeymapTest.kt b/AnkiDroid/src/test/java/com/ichi2/anki/reviewer/PeripheralKeymapTest.kt deleted file mode 100644 index 8e610325b37e..000000000000 --- a/AnkiDroid/src/test/java/com/ichi2/anki/reviewer/PeripheralKeymapTest.kt +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright (c) 2020 David Allison - * - * 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.reviewer - -import android.view.KeyEvent -import com.github.ivanshafran.sharedpreferencesmock.SPMockBuilder -import com.ichi2.anki.cardviewer.ViewerCommand -import org.hamcrest.MatcherAssert.assertThat -import org.hamcrest.Matchers.equalTo -import org.hamcrest.Matchers.hasSize -import org.junit.Test -import org.mockito.Mockito.mock -import org.mockito.kotlin.whenever - -class PeripheralKeymapTest { - @Test - fun flagAndAnswerDoNotConflict() { - val processed: MutableList = ArrayList() - - val peripheralKeymap = PeripheralKeymap(MockReviewerUi()) { e: ViewerCommand, _ -> processed.add(e) } - peripheralKeymap.setup(SPMockBuilder().createSharedPreferences()) - val event = mock(KeyEvent::class.java) - whenever(event.unicodeChar).thenReturn(0) - whenever(event.isCtrlPressed).thenReturn(true) - whenever(event.getUnicodeChar(0)).thenReturn(49) - whenever(event.keyCode).thenReturn(KeyEvent.KEYCODE_1) - - assertThat(event.unicodeChar.toChar(), equalTo('\u0000')) - assertThat(event.getUnicodeChar(0).toChar(), equalTo('1')) - peripheralKeymap.onKeyDown(KeyEvent.KEYCODE_1, event) - - assertThat>(processed, hasSize(1)) - assertThat(processed[0], equalTo(ViewerCommand.TOGGLE_FLAG_RED)) - } - - private class MockReviewerUi : ReviewerUi { - override val isDisplayingAnswer: Boolean - get() = false - } -} From da0d22164e4afe4b052af560da1233e5feae21ec Mon Sep 17 00:00:00 2001 From: Brayan Oliveira <69634269+brayandso@users.noreply.github.com> Date: Wed, 25 Dec 2024 16:38:34 -0300 Subject: [PATCH 29/29] remove Dotted line --- .../ichi2/preferences/ControlPreference.kt | 6 ++--- .../src/main/res/drawable/dotted_line.xml | 9 -------- .../main/res/layout/control_preference.xml | 23 ++++--------------- 3 files changed, 7 insertions(+), 31 deletions(-) delete mode 100644 AnkiDroid/src/main/res/drawable/dotted_line.xml diff --git a/AnkiDroid/src/main/java/com/ichi2/preferences/ControlPreference.kt b/AnkiDroid/src/main/java/com/ichi2/preferences/ControlPreference.kt index fac7f178ae78..c2ecf1dd3fdc 100644 --- a/AnkiDroid/src/main/java/com/ichi2/preferences/ControlPreference.kt +++ b/AnkiDroid/src/main/java/com/ichi2/preferences/ControlPreference.kt @@ -23,7 +23,6 @@ import android.text.TextUtils import android.util.AttributeSet import android.view.View import android.widget.ArrayAdapter -import android.widget.LinearLayout import android.widget.ListView import androidx.appcompat.app.AlertDialog import androidx.core.view.isVisible @@ -219,15 +218,16 @@ class ControlPreferenceDialogFragment : DialogFragment() { private fun setupRemoveControlEntries(view: View) { val bindings = preference.getMappableBindings().toMutableList() + val listView = view.findViewById(R.id.list_view) if (bindings.isEmpty()) { - view.findViewById(R.id.remove_layout).isVisible = false + listView.isVisible = false return } val titles = bindings.map { getString(R.string.binding_remove_binding, it.toDisplayString(requireContext())) } - view.findViewById(R.id.list_view).apply { + listView.apply { adapter = ArrayAdapter(requireContext(), R.layout.control_preference_list_item, titles) setOnItemClickListener { _, _, index, _ -> bindings.removeAt(index) diff --git a/AnkiDroid/src/main/res/drawable/dotted_line.xml b/AnkiDroid/src/main/res/drawable/dotted_line.xml deleted file mode 100644 index c44a6594f76a..000000000000 --- a/AnkiDroid/src/main/res/drawable/dotted_line.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - diff --git a/AnkiDroid/src/main/res/layout/control_preference.xml b/AnkiDroid/src/main/res/layout/control_preference.xml index 5ea6cd785c9f..a82d809f7383 100644 --- a/AnkiDroid/src/main/res/layout/control_preference.xml +++ b/AnkiDroid/src/main/res/layout/control_preference.xml @@ -52,25 +52,10 @@ android:textAppearance="?attr/textAppearanceBodyLarge" /> - - - - - - - + android:divider="@null" + /> \ No newline at end of file