Skip to content

Commit

Permalink
refactor: MappableBinding
Browse files Browse the repository at this point in the history
This makes `MappableBinding` abstract and moves the logic of MappableBinding::Screen to `MappableBinding` subclasses.

That reduces boilerplate and simplifies the code a bit
  • Loading branch information
BrayanDSO committed Dec 24, 2024
1 parent a293746 commit 43e803d
Show file tree
Hide file tree
Showing 6 changed files with 105 additions and 152 deletions.
21 changes: 5 additions & 16 deletions AnkiDroid/src/main/java/com/ichi2/anki/cardviewer/ViewerCommand.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -137,17 +137,6 @@ enum class ViewerCommand(
// If we use the serialised format, then this adds additional coupling to the properties.
val defaultValue: List<MappableBinding>
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(
Expand Down Expand Up @@ -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 {
/**
Expand Down
197 changes: 87 additions & 110 deletions AnkiDroid/src/main/java/com/ichi2/anki/reviewer/MappableBinding.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand All @@ -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<MappableBinding>.toPreferenceString(): String =
Expand All @@ -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) {
Expand Down Expand Up @@ -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)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,14 @@ 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. */
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() {
Expand All @@ -53,7 +52,7 @@ class PeripheralKeymap(
) {
val bindings =
fromPreference(preferences, command)
.filter { it.screen is Screen.Reviewer }
.filterIsInstance<ReviewerBinding>()
for (b in bindings) {
if (!b.isKey) {
continue
Expand Down Expand Up @@ -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<MappableBinding, ViewerCommand>()

Expand All @@ -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)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
}
Expand Down
Loading

0 comments on commit 43e803d

Please sign in to comment.