Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(new reviewer): Keybinds #17928

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,9 @@ 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.preferences.sharedPrefs
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
Expand All @@ -34,16 +33,11 @@ class PeripheralKeymapTest : InstrumentedTest() {
// #7736 Ensures that a numpad key is passed through (mostly testing num lock)
val processed: MutableList<ViewerCommand> = ArrayList()

val sharedPrefs = testContext.sharedPrefs()
val peripheralKeymap =
PeripheralKeymap(MockReviewerUi.displayingAnswer()) { e: ViewerCommand, _: Gesture? -> processed.add(e) }
peripheralKeymap.setup()
PeripheralKeymap(sharedPrefs, ViewerCommand.entries) { e: ViewerCommand, _ -> processed.add(e) }

peripheralKeymap.onKeyDown(
KeyEvent.KEYCODE_NUMPAD_1,
getNumpadEvent(KeyEvent.KEYCODE_NUMPAD_1),
)
peripheralKeymap.onKeyUp(
KeyEvent.KEYCODE_NUMPAD_1,
getNumpadEvent(KeyEvent.KEYCODE_NUMPAD_1),
)
assertThat<List<ViewerCommand>>(processed, hasSize(1))
Expand Down
16 changes: 16 additions & 0 deletions AnkiDroid/src/main/assets/scripts/ankidroid.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,19 @@ globalThis.ankidroid.userAction = function (number) {
alert(e);
}
};

globalThis.ankidroid.showHint = function () {
var hints = document.querySelectorAll("a.hint");
for (var i = 0; i < hints.length; i++) {
if (hints[i].style.display != "none") {
hints[i].click();
break;
}
}
};

globalThis.ankidroid.showAllHints = function () {
document.querySelectorAll("a.hint").forEach(el => {
el.click();
});
};
32 changes: 18 additions & 14 deletions AnkiDroid/src/main/java/com/ichi2/anki/Reviewer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -80,11 +80,15 @@ 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
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.scheduling.ForgetCardsDialog
import com.ichi2.anki.scheduling.SetDueDateDialog
Expand Down Expand Up @@ -132,7 +136,8 @@ import kotlin.coroutines.resume
@NeedsTest("#14709: Timebox shouldn't appear instantly when the Reviewer is opened")
open class Reviewer :
AbstractFlashcardViewer(),
ReviewerUi {
ReviewerUi,
BindingProcessor<ReviewerBinding, ViewerCommand> {
private var queueState: CurrentQueueState? = null
private val customSchedulingKey = TimeManager.time.intTimeMS().toString()
private var hasDrawerSwipeConflicts = false
Expand Down Expand Up @@ -196,7 +201,7 @@ open class Reviewer :
private lateinit var toolbar: Toolbar

@VisibleForTesting
protected val processor = PeripheralKeymap(this, this)
protected open lateinit var processor: PeripheralKeymap<ReviewerBinding, ViewerCommand>

private val addNoteLauncher =
registerForActivityResult(
Expand All @@ -221,6 +226,7 @@ open class Reviewer :
textBarReview = findViewById(R.id.review_number)
toolbar = findViewById(R.id.toolbar)
micToolBarLayer = findViewById(R.id.mic_tool_bar_layer)
processor = PeripheralKeymap(sharedPrefs(), ViewerCommand.entries, this)
if (sharedPrefs().getString("answerButtonPosition", "bottom") == "bottom" && !navBarNeedsScrim) {
setNavigationBarColor(R.attr.showAnswerColor)
}
Expand Down Expand Up @@ -906,22 +912,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
Expand Down Expand Up @@ -977,7 +973,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
Expand Down Expand Up @@ -1670,4 +1665,13 @@ open class Reviewer :
Intent(context, Reviewer::class.java)
}
}

override fun processAction(
action: ViewerCommand,
binding: ReviewerBinding,
): Boolean {
if (binding.side != CardSide.BOTH && CardSide.fromAnswer(isDisplayingAnswer) != binding.side) return false
val gesture = (binding.binding as? Binding.GestureInput)?.gesture
return executeCommand(action, gesture)
}
}
14 changes: 10 additions & 4 deletions AnkiDroid/src/main/java/com/ichi2/anki/cardviewer/ViewerCommand.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,19 @@
*/
package com.ichi2.anki.cardviewer

import android.content.SharedPreferences
import android.view.KeyEvent
import com.ichi2.anki.reviewer.Binding.Companion.keyCode
import com.ichi2.anki.reviewer.Binding.Companion.unicode
import com.ichi2.anki.reviewer.Binding.ModifierKeys
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.MappableAction
import com.ichi2.anki.reviewer.ReviewerBinding

/** Abstraction: Discuss moving many of these to 'Reviewer' */
enum class ViewerCommand {
enum class ViewerCommand : MappableAction<ReviewerBinding> {
SHOW_ANSWER,
FLIP_OR_ANSWER_EASE1,
FLIP_OR_ANSWER_EASE2,
Expand Down Expand Up @@ -78,11 +79,16 @@ enum class ViewerCommand {
USER_ACTION_9,
;

val preferenceKey: String
override val preferenceKey: String
get() = "binding_$name"

override fun getBindings(prefs: SharedPreferences): List<ReviewerBinding> {
val prefValue = prefs.getString(preferenceKey, null) ?: return defaultValue
return ReviewerBinding.fromPreferenceString(prefValue)
}

// If we use the serialised format, then this adds additional coupling to the properties.
val defaultValue: List<MappableBinding>
val defaultValue: List<ReviewerBinding>
get() {
return when (this) {
FLIP_OR_ANSWER_EASE1 ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
*/
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
Expand All @@ -23,6 +25,13 @@ import com.ichi2.anki.R
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.Binding.ModifierKeys.Companion.shift
import com.ichi2.anki.reviewer.CardSide
import com.ichi2.anki.reviewer.MappableAction
import com.ichi2.anki.reviewer.ReviewerBinding

/**
* @param menuId menu Id of the action
Expand All @@ -32,12 +41,12 @@ import com.ichi2.anki.preferences.reviewer.MenuDisplayType.MENU_ONLY
* or if the item has a [parentMenu].
*/
enum class ViewerAction(
@IdRes val menuId: Int,
@DrawableRes val drawableRes: Int?,
@IdRes val menuId: Int = 0,
@DrawableRes val drawableRes: Int? = null,
@StringRes val titleRes: Int = R.string.empty_string,
val defaultDisplayType: MenuDisplayType? = null,
val parentMenu: ViewerAction? = null,
) {
) : MappableAction<ReviewerBinding> {
// Always
UNDO(R.id.action_undo, R.drawable.ic_undo_white, R.string.undo, ALWAYS),

Expand Down Expand Up @@ -78,10 +87,118 @@ enum class ViewerAction(
FLAG_PINK(Flag.PINK.id, Flag.PINK.drawableRes, parentMenu = FLAG_MENU),
FLAG_TURQUOISE(Flag.TURQUOISE.id, Flag.TURQUOISE.drawableRes, parentMenu = FLAG_MENU),
FLAG_PURPLE(Flag.PURPLE.id, Flag.PURPLE.drawableRes, parentMenu = FLAG_MENU),

// Command only
SHOW_ANSWER,
FLIP_OR_ANSWER_EASE1,
FLIP_OR_ANSWER_EASE2,
FLIP_OR_ANSWER_EASE3,
FLIP_OR_ANSWER_EASE4,
SHOW_HINT,
SHOW_ALL_HINTS,
EXIT,
;

override val preferenceKey: String get() = "binding_$name"

override fun getBindings(prefs: SharedPreferences): List<ReviewerBinding> {
val prefValue = prefs.getString(preferenceKey, null) ?: return defaultBindings
return ReviewerBinding.fromPreferenceString(prefValue)
}

private val defaultBindings: List<ReviewerBinding> 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('@'))
TOGGLE_AUTO_ADVANCE -> listOf(keycode(KeyEvent.KEYCODE_A, shift()))
SHOW_HINT -> listOf(keycode(KeyEvent.KEYCODE_H))
SHOW_ALL_HINTS -> listOf(keycode(KeyEvent.KEYCODE_G))
FLIP_OR_ANSWER_EASE1 ->
listOf(
keycode(KeyEvent.KEYCODE_BUTTON_Y),
keycode(KeyEvent.KEYCODE_1, side = CardSide.ANSWER),
keycode(KeyEvent.KEYCODE_NUMPAD_1, side = CardSide.ANSWER),
)
FLIP_OR_ANSWER_EASE2 ->
listOf(
keycode(KeyEvent.KEYCODE_BUTTON_X),
keycode(KeyEvent.KEYCODE_2, side = CardSide.ANSWER),
keycode(KeyEvent.KEYCODE_NUMPAD_2, side = CardSide.ANSWER),
)
FLIP_OR_ANSWER_EASE3 ->
listOf(
keycode(KeyEvent.KEYCODE_BUTTON_B),
keycode(KeyEvent.KEYCODE_3, side = CardSide.ANSWER),
keycode(KeyEvent.KEYCODE_NUMPAD_3, side = CardSide.ANSWER),
keycode(KeyEvent.KEYCODE_DPAD_CENTER),
keycode(KeyEvent.KEYCODE_SPACE, side = CardSide.BOTH),
keycode(KeyEvent.KEYCODE_ENTER, side = CardSide.ANSWER),
keycode(KeyEvent.KEYCODE_NUMPAD_ENTER, side = CardSide.ANSWER),
)
FLIP_OR_ANSWER_EASE4 ->
listOf(
keycode(KeyEvent.KEYCODE_BUTTON_A),
keycode(KeyEvent.KEYCODE_4, side = CardSide.ANSWER),
keycode(KeyEvent.KEYCODE_NUMPAD_4, side = CardSide.ANSWER),
)
// No default gestures
SHOW_ANSWER,
DELETE,
CARD_INFO,
EXIT,
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,
Expand Down
14 changes: 14 additions & 0 deletions AnkiDroid/src/main/java/com/ichi2/anki/reviewer/MappableBinding.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package com.ichi2.anki.reviewer

import android.content.Context
import android.content.SharedPreferences
import androidx.annotation.CheckResult
import com.ichi2.anki.reviewer.Binding.KeyBinding
import com.ichi2.utils.hash
Expand Down Expand Up @@ -73,3 +74,16 @@ open class MappableBinding(
}
}
}

interface MappableAction<B : MappableBinding> {
val preferenceKey: String

fun getBindings(prefs: SharedPreferences): List<B>
}

fun interface BindingProcessor<B : MappableBinding, A : MappableAction<B>> {
fun processAction(
action: A,
binding: B,
): Boolean
}
Loading