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..57dcb6fdc32a 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") +abstract 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,89 +85,15 @@ 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 - - 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 - } + abstract fun toDisplayString(context: Context): String - 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)) + fun fromGesture(gesture: Gesture): ReviewerBinding = ReviewerBinding(GestureInput(gesture), CardSide.BOTH) @CheckResult fun List.toPreferenceString(): String = @@ -188,7 +109,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 +153,62 @@ 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) + } + } +} 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..d4473648d212 100644 --- a/AnkiDroid/src/main/java/com/ichi2/preferences/ControlPreference.kt +++ b/AnkiDroid/src/main/java/com/ichi2/preferences/ControlPreference.kt @@ -35,8 +35,7 @@ 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 +74,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() @@ -126,10 +122,7 @@ class ControlPreference : ListPreference { positiveButton(R.string.dialog_ok) { val gesture = gesturePicker.getGesture() ?: return@positiveButton val mappableBinding = - fromGesture( - gesture, - screenBuilder, - ) + fromGesture(gesture) if (bindingIsUsedOnAnotherCommand(mappableBinding)) { showDialogToReplaceBinding(mappableBinding, context.getString(R.string.binding_replace_gesture), it) } else { @@ -141,7 +134,7 @@ class ControlPreference : ListPreference { customView(view = gesturePicker) gesturePicker.onGestureChanged { gesture -> - warnIfBindingIsUsed(fromGesture(gesture, screenBuilder), gesturePicker) + warnIfBindingIsUsed(fromGesture(gesture), gesturePicker) } } } @@ -155,10 +148,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 +156,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 +194,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/cardviewer/GestureProcessorTest.kt b/AnkiDroid/src/test/java/com/ichi2/anki/cardviewer/GestureProcessorTest.kt index 721e9863ffe3..e2179e081c48 100644 --- a/AnkiDroid/src/test/java/com/ichi2/anki/cardviewer/GestureProcessorTest.kt +++ b/AnkiDroid/src/test/java/com/ichi2/anki/cardviewer/GestureProcessorTest.kt @@ -20,7 +20,6 @@ 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 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(MappableBinding.fromGesture(Gesture.TAP_CENTER)) .toPreferenceString() every { prefs.getBoolean("gestureCornerTouch", any()) } returns true sut.init(prefs)