diff --git a/AnkiDroid/src/androidTest/java/com/ichi2/anki/reviewer/PeripheralKeymapTest.kt b/AnkiDroid/src/androidTest/java/com/ichi2/anki/reviewer/PeripheralKeymapTest.kt index c148563b8f8b..79a4056a5e55 100644 --- a/AnkiDroid/src/androidTest/java/com/ichi2/anki/reviewer/PeripheralKeymapTest.kt +++ b/AnkiDroid/src/androidTest/java/com/ichi2/anki/reviewer/PeripheralKeymapTest.kt @@ -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 @@ -34,16 +33,11 @@ class PeripheralKeymapTest : InstrumentedTest() { // #7736 Ensures that a numpad key is passed through (mostly testing num lock) val processed: MutableList = 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>(processed, hasSize(1)) diff --git a/AnkiDroid/src/main/assets/scripts/ankidroid.js b/AnkiDroid/src/main/assets/scripts/ankidroid.js index a2dd0210bdbd..ba4659419d30 100644 --- a/AnkiDroid/src/main/assets/scripts/ankidroid.js +++ b/AnkiDroid/src/main/assets/scripts/ankidroid.js @@ -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(); + }); +}; diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/Reviewer.kt b/AnkiDroid/src/main/java/com/ichi2/anki/Reviewer.kt index e802da862551..8e0b3b1a681d 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/Reviewer.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/Reviewer.kt @@ -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 @@ -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 { private var queueState: CurrentQueueState? = null private val customSchedulingKey = TimeManager.time.intTimeMS().toString() private var hasDrawerSwipeConflicts = false @@ -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 private val addNoteLauncher = registerForActivityResult( @@ -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) } @@ -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 @@ -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 @@ -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) + } } 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 526bdc9164b9..a3273bd23f8e 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/cardviewer/ViewerCommand.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/cardviewer/ViewerCommand.kt @@ -15,6 +15,7 @@ */ 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 @@ -22,11 +23,11 @@ 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 { SHOW_ANSWER, FLIP_OR_ANSWER_EASE1, FLIP_OR_ANSWER_EASE2, @@ -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 { + 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 + val defaultValue: List get() { return when (this) { FLIP_OR_ANSWER_EASE1 -> 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 9f9038e9ed53..a0f38966e42d 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,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 @@ -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 @@ -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 { // Always UNDO(R.id.action_undo, R.drawable.ic_undo_white, R.string.undo, ALWAYS), @@ -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 { + 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('@')) + 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, 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 8250e5663eb6..5c9110a0873b 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/reviewer/MappableBinding.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/reviewer/MappableBinding.kt @@ -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 @@ -73,3 +74,16 @@ open class MappableBinding( } } } + +interface MappableAction { + val preferenceKey: String + + fun getBindings(prefs: SharedPreferences): List +} + +fun interface BindingProcessor> { + fun processAction( + action: A, + binding: B, + ): Boolean +} 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 99d5f0936d46..b15c17bfefd6 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/reviewer/PeripheralKeymap.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/reviewer/PeripheralKeymap.kt @@ -18,94 +18,39 @@ 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, +class PeripheralKeymap>( + sharedPrefs: SharedPreferences, + actions: List, + private var processor: BindingProcessor? = null, ) { - 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 = - ReviewerBinding - .fromPreference(preferences, command) - .filterIsInstance() - for (b in bindings) { - if (!b.isKey) { - continue + private val bindingMap = HashMap>() + + init { + for (action in actions) { + val mappableBindings = action.getBindings(sharedPrefs) + for (mappableBinding in mappableBindings) { + if (!mappableBinding.isKey) continue + bindingMap[mappableBinding.binding] = mappableBinding to action } - 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() + fun setProcessor(processor: BindingProcessor) { + this.processor = processor + } - @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 + fun onKeyDown(event: KeyEvent): Boolean { + if (event.repeatCount > 0) { + return false } - - operator fun set( - key: MappableBinding, - value: ViewerCommand, - ) { - bindingMap[key] = value + val bindings = possibleKeyBindings(event) + for (binding in bindings) { + val (mappableBinding, action) = bindingMap[binding] ?: continue + if (processor?.processAction(action, mappableBinding) == true) return true } - - operator fun get(key: MappableBinding): ViewerCommand? = bindingMap[key] + return false } } 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 a8feeef1db31..9b7b76e159ce 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 @@ -49,50 +50,29 @@ import com.google.android.material.card.MaterialCardView import com.google.android.material.shape.ShapeAppearanceModel import com.google.android.material.textfield.TextInputEditText 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.Flag +import com.ichi2.anki.DispatchKeyEventListener import com.ichi2.anki.NoteEditor import com.ichi2.anki.R import com.ichi2.anki.cardviewer.CardMediaPlayer import com.ichi2.anki.noteeditor.NoteEditorLauncher +import com.ichi2.anki.pages.CardInfoDestination +import com.ichi2.anki.pages.DeckOptionsDestination import com.ichi2.anki.preferences.reviewer.ReviewerMenuView import com.ichi2.anki.preferences.reviewer.ViewerAction -import com.ichi2.anki.preferences.reviewer.ViewerAction.ADD_NOTE import com.ichi2.anki.preferences.reviewer.ViewerAction.BURY_CARD import com.ichi2.anki.preferences.reviewer.ViewerAction.BURY_MENU 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 -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 -import com.ichi2.anki.preferences.reviewer.ViewerAction.FLAG_ORANGE -import com.ichi2.anki.preferences.reviewer.ViewerAction.FLAG_PINK -import com.ichi2.anki.preferences.reviewer.ViewerAction.FLAG_PURPLE -import com.ichi2.anki.preferences.reviewer.ViewerAction.FLAG_RED -import com.ichi2.anki.preferences.reviewer.ViewerAction.FLAG_TURQUOISE import com.ichi2.anki.preferences.reviewer.ViewerAction.MARK import com.ichi2.anki.preferences.reviewer.ViewerAction.REDO import com.ichi2.anki.preferences.reviewer.ViewerAction.SUSPEND_CARD import com.ichi2.anki.preferences.reviewer.ViewerAction.SUSPEND_MENU import com.ichi2.anki.preferences.reviewer.ViewerAction.SUSPEND_NOTE -import com.ichi2.anki.preferences.reviewer.ViewerAction.TOGGLE_AUTO_ADVANCE import com.ichi2.anki.preferences.reviewer.ViewerAction.UNDO -import com.ichi2.anki.preferences.reviewer.ViewerAction.UNSET_FLAG -import com.ichi2.anki.preferences.reviewer.ViewerAction.USER_ACTION_1 -import com.ichi2.anki.preferences.reviewer.ViewerAction.USER_ACTION_2 -import com.ichi2.anki.preferences.reviewer.ViewerAction.USER_ACTION_3 -import com.ichi2.anki.preferences.reviewer.ViewerAction.USER_ACTION_4 -import com.ichi2.anki.preferences.reviewer.ViewerAction.USER_ACTION_5 -import com.ichi2.anki.preferences.reviewer.ViewerAction.USER_ACTION_6 -import com.ichi2.anki.preferences.reviewer.ViewerAction.USER_ACTION_7 -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.PeripheralKeymap import com.ichi2.anki.settings.Prefs import com.ichi2.anki.settings.enums.FrameStyle import com.ichi2.anki.settings.enums.HideSystemBars @@ -106,14 +86,14 @@ import com.ichi2.anki.utils.ext.removeSubMenu import com.ichi2.anki.utils.ext.sharedPrefs import com.ichi2.anki.utils.ext.window import com.ichi2.libanki.sched.Counts -import kotlinx.coroutines.launch class ReviewerFragment : CardViewerFragment(R.layout.reviewer2), BaseSnackbarBuilderProvider, - ActionMenuView.OnMenuItemClickListener { + ActionMenuView.OnMenuItemClickListener, + DispatchKeyEventListener { override val viewModel: ReviewerViewModel by viewModels { - ReviewerViewModel.factory(CardMediaPlayer()) + ReviewerViewModel.factory(CardMediaPlayer(), PeripheralKeymap(sharedPrefs(), ViewerAction.entries)) } override val webView: WebView @@ -159,12 +139,10 @@ class ReviewerFragment : showSnackbar(message, duration = 500) } - viewModel.isQueueFinishedFlow.collectIn(lifecycleScope) { isQueueFinished -> - if (isQueueFinished) { - requireActivity().run { - setResult(RESULT_NO_MORE_CARDS) - finish() - } + viewModel.finishResultFlow.collectIn(lifecycleScope) { result -> + requireActivity().run { + setResult(result) + finish() } } @@ -177,48 +155,16 @@ class ReviewerFragment : viewModel.showingAnswer.collectIn(lifecycleScope) { resetZoom() } - } - // TODO - override fun onMenuItemClick(item: MenuItem): Boolean { - if (item.hasSubMenu()) return false - val action = ViewerAction.fromId(item.itemId) - when (action) { - ADD_NOTE -> launchAddNote() - CARD_INFO -> launchCardInfo() - DECK_OPTIONS -> launchDeckOptions() - EDIT -> launchEditNote() - DELETE -> viewModel.deleteNote() - MARK -> viewModel.toggleMark() - REDO -> viewModel.redo() - UNDO -> viewModel.undo() - TOGGLE_AUTO_ADVANCE -> viewModel.toggleAutoAdvance() - BURY_NOTE -> viewModel.buryNote() - BURY_CARD -> viewModel.buryCard() - SUSPEND_NOTE -> viewModel.suspendNote() - SUSPEND_CARD -> viewModel.suspendCard() - UNSET_FLAG -> viewModel.setFlag(Flag.NONE) - FLAG_RED -> viewModel.setFlag(Flag.RED) - FLAG_ORANGE -> viewModel.setFlag(Flag.ORANGE) - FLAG_BLUE -> viewModel.setFlag(Flag.BLUE) - FLAG_GREEN -> viewModel.setFlag(Flag.GREEN) - FLAG_PINK -> viewModel.setFlag(Flag.PINK) - FLAG_TURQUOISE -> viewModel.setFlag(Flag.TURQUOISE) - FLAG_PURPLE -> viewModel.setFlag(Flag.PURPLE) - USER_ACTION_1 -> viewModel.userAction(1) - USER_ACTION_2 -> viewModel.userAction(2) - USER_ACTION_3 -> viewModel.userAction(3) - USER_ACTION_4 -> viewModel.userAction(4) - USER_ACTION_5 -> viewModel.userAction(5) - USER_ACTION_6 -> viewModel.userAction(6) - USER_ACTION_7 -> viewModel.userAction(7) - USER_ACTION_8 -> viewModel.userAction(8) - USER_ACTION_9 -> viewModel.userAction(9) - SUSPEND_MENU -> viewModel.suspendCard() - BURY_MENU -> viewModel.buryCard() - FLAG_MENU -> return false + viewModel.destinationFlow.collectIn(lifecycleScope) { destination -> + val intent = destination.toIntent(requireContext()) + when (destination) { + is NoteEditorLauncher.EditNoteFromPreviewer -> noteEditorLauncher.launch(intent) + is NoteEditorLauncher.AddNote -> noteEditorLauncher.launch(intent) + is DeckOptionsDestination -> deckOptionsLauncher.launch(intent) + is CardInfoDestination -> startActivity(intent) + } } - return true } private fun setupTypeAnswer(view: View) { @@ -267,6 +213,10 @@ class ReviewerFragment : webView.settings.loadWithOverviewMode = true } + override fun dispatchKeyEvent(event: KeyEvent): Boolean = viewModel.onKeyDown(event) + + override fun onMenuItemClick(item: MenuItem): Boolean = viewModel.onMenuItemClick(item) + private fun setupAnswerButtons(view: View) { val prefs = sharedPrefs() val answerButtonsLayout = view.findViewById(R.id.answer_buttons) @@ -517,37 +467,11 @@ class ReviewerFragment : } } - private fun launchEditNote() { - lifecycleScope.launch { - val intent = viewModel.getEditNoteDestination().toIntent(requireContext()) - noteEditorLauncher.launch(intent) - } - } - - private fun launchAddNote() { - val intent = NoteEditorLauncher.AddNoteFromReviewer().toIntent(requireContext()) - noteEditorLauncher.launch(intent) - } - - private fun launchCardInfo() { - lifecycleScope.launch { - val intent = viewModel.getCardInfoDestination().toIntent(requireContext()) - startActivity(intent) - } - } - private val deckOptionsLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { viewModel.refreshCard() } - private fun launchDeckOptions() { - lifecycleScope.launch { - val intent = viewModel.getDeckOptionsDestination().toIntent(requireContext()) - deckOptionsLauncher.launch(intent) - } - } - companion object { fun getIntent(context: Context): Intent = CardViewerActivity.getIntent(context, ReviewerFragment::class) } diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/ui/windows/reviewer/ReviewerViewModel.kt b/AnkiDroid/src/main/java/com/ichi2/anki/ui/windows/reviewer/ReviewerViewModel.kt index 7b9637a0a39c..66d93efc9df0 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/ui/windows/reviewer/ReviewerViewModel.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/ui/windows/reviewer/ReviewerViewModel.kt @@ -16,6 +16,8 @@ package com.ichi2.anki.ui.windows.reviewer import android.text.style.RelativeSizeSpan +import android.view.KeyEvent +import android.view.MenuItem import androidx.core.text.buildSpannedString import androidx.core.text.inSpans import androidx.lifecycle.ViewModelProvider @@ -23,6 +25,8 @@ import androidx.lifecycle.viewmodel.initializer import androidx.lifecycle.viewmodel.viewModelFactory import anki.collection.OpChanges import anki.frontend.SetSchedulingStatesRequest +import com.ichi2.anki.AbstractFlashcardViewer +import com.ichi2.anki.AbstractFlashcardViewer.Companion.RESULT_NO_MORE_CARDS import com.ichi2.anki.CollectionManager import com.ichi2.anki.CollectionManager.withCol import com.ichi2.anki.Ease @@ -36,14 +40,19 @@ import com.ichi2.anki.pages.AnkiServer import com.ichi2.anki.pages.CardInfoDestination import com.ichi2.anki.pages.DeckOptionsDestination import com.ichi2.anki.preferences.getShowIntervalOnButtons +import com.ichi2.anki.preferences.reviewer.ViewerAction import com.ichi2.anki.previewer.CardViewerViewModel import com.ichi2.anki.previewer.TypeAnswer +import com.ichi2.anki.reviewer.BindingProcessor import com.ichi2.anki.reviewer.CardSide +import com.ichi2.anki.reviewer.PeripheralKeymap +import com.ichi2.anki.reviewer.ReviewerBinding import com.ichi2.anki.servicelayer.MARKED_TAG import com.ichi2.anki.servicelayer.NoteService import com.ichi2.anki.servicelayer.isBuryNoteAvailable import com.ichi2.anki.servicelayer.isSuspendNoteAvailable import com.ichi2.anki.ui.windows.reviewer.autoadvance.AutoAdvance +import com.ichi2.anki.utils.Destination import com.ichi2.libanki.ChangeManager import com.ichi2.libanki.redo import com.ichi2.libanki.sched.Counts @@ -59,8 +68,10 @@ import kotlinx.coroutines.flow.MutableStateFlow class ReviewerViewModel( cardMediaPlayer: CardMediaPlayer, + private val keyMap: PeripheralKeymap, ) : CardViewerViewModel(cardMediaPlayer), - ChangeManager.Subscriber { + ChangeManager.Subscriber, + BindingProcessor { private var queueState: Deferred = asyncIO { // this assumes that the Reviewer won't be launched if there isn't a queueState @@ -70,7 +81,7 @@ class ReviewerViewModel( asyncIO { queueState.await()!!.topCard } - var isQueueFinishedFlow = MutableSharedFlow() + var finishResultFlow = MutableSharedFlow() val isMarkedFlow = MutableStateFlow(false) val flagFlow = MutableStateFlow(Flag.NONE) val actionFeedbackFlow = MutableSharedFlow() @@ -80,6 +91,7 @@ class ReviewerViewModel( val redoLabelFlow = MutableStateFlow(null) val countsFlow = MutableStateFlow(Counts() to Counts.Queue.NEW) val typeAnswerFlow = MutableStateFlow(null) + val destinationFlow = MutableSharedFlow() override val server = AnkiServer(this).also { it.start() } private val stateMutationKey = TimeManager.time.intTimeMS().toString() @@ -109,6 +121,7 @@ class ReviewerViewModel( } init { + keyMap.setProcessor(this) ChangeManager.subscribe(this) launchCatchingIO { updateUndoAndRedoLabels() @@ -175,30 +188,36 @@ class ReviewerViewModel( fun answerEasy() = answerCard(Ease.EASY) - fun toggleMark() { - launchCatchingIO { - val card = currentCard.await() - val note = withCol { card.note(this@withCol) } - NoteService.toggleMark(note) - isMarkedFlow.emit(NoteService.isMarked(note)) - } + fun onKeyDown(event: KeyEvent): Boolean { + if (event.action != KeyEvent.ACTION_DOWN) return false + return keyMap.onKeyDown(event) } - fun setFlag(flag: Flag) { - launchCatchingIO { - val card = currentCard.await() - undoableOp { - setUserFlagForCards(listOf(card.id), flag) - } - flagFlow.emit(flag) + private suspend fun toggleMark() { + val card = currentCard.await() + val note = withCol { card.note(this@withCol) } + NoteService.toggleMark(note) + isMarkedFlow.emit(NoteService.isMarked(note)) + } + + private suspend fun setFlag(flag: Flag) { + val card = currentCard.await() + undoableOp { + setUserFlagForCards(listOf(card.id), flag) } + flagFlow.emit(flag) } fun onStateMutationCallback() { statesMutated = true } - suspend fun getEditNoteDestination(): NoteEditorLauncher = NoteEditorLauncher.EditNoteFromPreviewer(currentCard.await().id) + private suspend fun emitEditNoteDestination() { + val destination = NoteEditorLauncher.EditNoteFromPreviewer(currentCard.await().id) + destinationFlow.emit(destination) + } + + private suspend fun emitAddNoteDestination() = destinationFlow.emit(NoteEditorLauncher.AddNoteFromReviewer()) fun refreshCard() { launchCatchingIO { @@ -206,137 +225,119 @@ class ReviewerViewModel( } } - suspend fun getCardInfoDestination(): CardInfoDestination = CardInfoDestination(currentCard.await().id) + private suspend fun emitCardInfoDestination() { + val destination = CardInfoDestination(currentCard.await().id) + destinationFlow.emit(destination) + } - suspend fun getDeckOptionsDestination(): DeckOptionsDestination { + private suspend fun emitDeckOptionsDestination() { val deckId = withCol { decks.getCurrentId() } val isFiltered = withCol { decks.isFiltered(deckId) } - return DeckOptionsDestination(deckId, isFiltered) - } - - fun deleteNote() { - launchCatchingIO { - val cardId = currentCard.await().id - val noteCount = - undoableOp { - removeNotes(cids = listOf(cardId)) - }.count - actionFeedbackFlow.emit(CollectionManager.TR.browsingCardsDeleted(noteCount)) - updateCurrentCard() - } + val destination = DeckOptionsDestination(deckId, isFiltered) + destinationFlow.emit(destination) } - fun buryCard() { - launchCatchingIO { - val cardId = currentCard.await().id - val noteCount = - undoableOp { - sched.buryCards(cids = listOf(cardId)) - }.count - actionFeedbackFlow.emit(CollectionManager.TR.studyingCardsBuried(noteCount)) - updateCurrentCard() - } + private suspend fun deleteNote() { + val cardId = currentCard.await().id + val noteCount = + undoableOp { + removeNotes(cids = listOf(cardId)) + }.count + actionFeedbackFlow.emit(CollectionManager.TR.browsingCardsDeleted(noteCount)) + updateCurrentCard() } - fun buryNote() { - launchCatchingIO { - val noteId = currentCard.await().nid - val noteCount = - undoableOp { - sched.buryNotes(nids = listOf(noteId)) - }.count - actionFeedbackFlow.emit(CollectionManager.TR.studyingCardsBuried(noteCount)) - updateCurrentCard() - } + suspend fun buryCard() { + val cardId = currentCard.await().id + val noteCount = + undoableOp { + sched.buryCards(cids = listOf(cardId)) + }.count + actionFeedbackFlow.emit(CollectionManager.TR.studyingCardsBuried(noteCount)) + updateCurrentCard() } - fun suspendCard() { - launchCatchingIO { - val cardId = currentCard.await().id + private suspend fun buryNote() { + val noteId = currentCard.await().nid + val noteCount = undoableOp { - sched.suspendCards(ids = listOf(cardId)) + sched.buryNotes(nids = listOf(noteId)) }.count - actionFeedbackFlow.emit(CollectionManager.TR.studyingCardSuspended()) - updateCurrentCard() - } + actionFeedbackFlow.emit(CollectionManager.TR.studyingCardsBuried(noteCount)) + updateCurrentCard() } - fun suspendNote() { - launchCatchingIO { - val noteId = currentCard.await().nid - undoableOp { - sched.suspendNotes(ids = listOf(noteId)) - } - actionFeedbackFlow.emit(CollectionManager.TR.studyingNoteSuspended()) - updateCurrentCard() - } + private suspend fun suspendCard() { + val cardId = currentCard.await().id + undoableOp { + sched.suspendCards(ids = listOf(cardId)) + }.count + actionFeedbackFlow.emit(CollectionManager.TR.studyingCardSuspended()) + updateCurrentCard() } - fun undo() { - launchCatchingIO { - val changes = - undoableOp { - undo() - } - val message = - if (changes.operation.isEmpty()) { - CollectionManager.TR.actionsNothingToUndo() - } else { - CollectionManager.TR.undoActionUndone(changes.operation) - } - actionFeedbackFlow.emit(message) - updateCurrentCard() + private suspend fun suspendNote() { + val noteId = currentCard.await().nid + undoableOp { + sched.suspendNotes(ids = listOf(noteId)) } + actionFeedbackFlow.emit(CollectionManager.TR.studyingNoteSuspended()) + updateCurrentCard() } - fun redo() { - launchCatchingIO { - val changes = - undoableOp { - redo() - } - val message = - if (changes.operation.isEmpty()) { - CollectionManager.TR.actionsNothingToRedo() - } else { - CollectionManager.TR.undoRedoAction(changes.operation) - } - actionFeedbackFlow.emit(message) - updateCurrentCard() - } + private suspend fun undo() { + val changes = + undoableOp { + undo() + } + val message = + if (changes.operation.isEmpty()) { + CollectionManager.TR.actionsNothingToUndo() + } else { + CollectionManager.TR.undoActionUndone(changes.operation) + } + actionFeedbackFlow.emit(message) + updateCurrentCard() } - fun userAction( + private suspend fun redo() { + val changes = undoableOp { redo() } + val message = + if (changes.operation.isEmpty()) { + CollectionManager.TR.actionsNothingToRedo() + } else { + CollectionManager.TR.undoRedoAction(changes.operation) + } + actionFeedbackFlow.emit(message) + updateCurrentCard() + } + + private suspend fun userAction( @Reviewer.UserAction number: Int, ) { - launchCatchingIO { - eval.emit("javascript: ankidroid.userAction($number);") - } + eval.emit("javascript: ankidroid.userAction($number);") } fun stopAutoAdvance() { autoAdvance.cancelQuestionAndAnswerActionJobs() } - fun toggleAutoAdvance() { - launchCatchingIO { - autoAdvance.isEnabled = !autoAdvance.isEnabled - - val message = - if (autoAdvance.isEnabled) { - CollectionManager.TR.actionsAutoAdvanceActivated() - } else { - CollectionManager.TR.actionsAutoAdvanceDeactivated() - } - actionFeedbackFlow.emit(message) - - if (autoAdvance.shouldWaitForAudio() && cardMediaPlayer.isPlaying) return@launchCatchingIO - - if (showingAnswer.value) { - autoAdvance.onShowAnswer() + private suspend fun toggleAutoAdvance() { + autoAdvance.isEnabled = !autoAdvance.isEnabled + val message = + if (autoAdvance.isEnabled) { + CollectionManager.TR.actionsAutoAdvanceActivated() } else { - autoAdvance.onShowQuestion() + CollectionManager.TR.actionsAutoAdvanceDeactivated() } + actionFeedbackFlow.emit(message) + + if (autoAdvance.shouldWaitForAudio() && cardMediaPlayer.isPlaying) return + + if (showingAnswer.value) { + autoAdvance.onShowAnswer() + } else { + autoAdvance.onShowQuestion() } } @@ -409,7 +410,7 @@ class ReviewerViewModel( private fun answerCard(ease: Ease) { launchCatchingIO { queueState.await()?.let { - undoableOp { sched.answerCard(it, ease) } + undoableOp { sched.answerCard(it, ease) } updateCurrentCard() } } @@ -435,7 +436,7 @@ class ReviewerViewModel( } val state = queueState.await() if (state == null) { - isQueueFinishedFlow.emit(true) + finishResultFlow.emit(RESULT_NO_MORE_CARDS) return } @@ -478,6 +479,78 @@ class ReviewerViewModel( answerButtonsNextTimeFlow.emit(nextTimes) } + private fun flipOrAnswer(ease: Ease) { + if (showingAnswer.value) { + answerCard(ease) + } else { + onShowAnswer() + } + } + + private fun executeAction(action: ViewerAction) { + launchCatchingIO { + when (action) { + ViewerAction.ADD_NOTE -> emitAddNoteDestination() + ViewerAction.CARD_INFO -> emitCardInfoDestination() + ViewerAction.DECK_OPTIONS -> emitDeckOptionsDestination() + ViewerAction.EDIT -> emitEditNoteDestination() + ViewerAction.DELETE -> deleteNote() + ViewerAction.MARK -> toggleMark() + ViewerAction.REDO -> redo() + ViewerAction.UNDO -> undo() + ViewerAction.TOGGLE_AUTO_ADVANCE -> toggleAutoAdvance() + ViewerAction.BURY_NOTE -> buryNote() + ViewerAction.BURY_CARD -> buryCard() + ViewerAction.SUSPEND_NOTE -> suspendNote() + ViewerAction.SUSPEND_CARD -> suspendCard() + ViewerAction.UNSET_FLAG -> setFlag(Flag.NONE) + ViewerAction.FLAG_RED -> setFlag(Flag.RED) + ViewerAction.FLAG_ORANGE -> setFlag(Flag.ORANGE) + ViewerAction.FLAG_BLUE -> setFlag(Flag.BLUE) + ViewerAction.FLAG_GREEN -> setFlag(Flag.GREEN) + ViewerAction.FLAG_PINK -> setFlag(Flag.PINK) + ViewerAction.FLAG_TURQUOISE -> setFlag(Flag.TURQUOISE) + ViewerAction.FLAG_PURPLE -> setFlag(Flag.PURPLE) + ViewerAction.SHOW_ANSWER -> if (!showingAnswer.value) onShowAnswer() + ViewerAction.FLIP_OR_ANSWER_EASE1 -> flipOrAnswer(Ease.AGAIN) + ViewerAction.FLIP_OR_ANSWER_EASE2 -> flipOrAnswer(Ease.HARD) + ViewerAction.FLIP_OR_ANSWER_EASE3 -> flipOrAnswer(Ease.GOOD) + ViewerAction.FLIP_OR_ANSWER_EASE4 -> flipOrAnswer(Ease.EASY) + ViewerAction.SHOW_HINT -> eval.emit("ankidroid.showHint()") + ViewerAction.SHOW_ALL_HINTS -> eval.emit("ankidroid.showAllHints()") + ViewerAction.EXIT -> finishResultFlow.emit(AbstractFlashcardViewer.RESULT_DEFAULT) + ViewerAction.USER_ACTION_1 -> userAction(1) + ViewerAction.USER_ACTION_2 -> userAction(2) + ViewerAction.USER_ACTION_3 -> userAction(3) + ViewerAction.USER_ACTION_4 -> userAction(4) + ViewerAction.USER_ACTION_5 -> userAction(5) + ViewerAction.USER_ACTION_6 -> userAction(6) + ViewerAction.USER_ACTION_7 -> userAction(7) + ViewerAction.USER_ACTION_8 -> userAction(8) + ViewerAction.USER_ACTION_9 -> userAction(9) + ViewerAction.SUSPEND_MENU -> suspendCard() + ViewerAction.BURY_MENU -> buryCard() + ViewerAction.FLAG_MENU -> {} + } + } + } + + override fun processAction( + action: ViewerAction, + binding: ReviewerBinding, + ): Boolean { + if (binding.side != CardSide.BOTH && CardSide.fromAnswer(showingAnswer.value) != binding.side) return false + executeAction(action) + return true + } + + fun onMenuItemClick(item: MenuItem): Boolean { + if (item.hasSubMenu()) return false + val action = ViewerAction.fromId(item.itemId) + executeAction(action) + return true + } + override fun opExecuted( changes: OpChanges, handler: Any?, @@ -486,10 +559,13 @@ class ReviewerViewModel( } companion object { - fun factory(soundPlayer: CardMediaPlayer): ViewModelProvider.Factory = + fun factory( + soundPlayer: CardMediaPlayer, + keyMap: PeripheralKeymap, + ): ViewModelProvider.Factory = viewModelFactory { initializer { - ReviewerViewModel(soundPlayer) + ReviewerViewModel(soundPlayer, keyMap) } } diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/ReviewerKeyboardInputTest.kt b/AnkiDroid/src/test/java/com/ichi2/anki/ReviewerKeyboardInputTest.kt index 69b2b12d9d70..725f0d5da7e5 100644 --- a/AnkiDroid/src/test/java/com/ichi2/anki/ReviewerKeyboardInputTest.kt +++ b/AnkiDroid/src/test/java/com/ichi2/anki/ReviewerKeyboardInputTest.kt @@ -44,6 +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.PeripheralKeymap import com.ichi2.anki.reviewer.ReviewerBinding import com.ichi2.anki.utils.ext.addBinding import com.ichi2.libanki.Card @@ -259,6 +260,9 @@ class ReviewerKeyboardInputTest : RobolectricTest() { displayAnswer = true } + override var processor: PeripheralKeymap = + PeripheralKeymap(sharedPrefs(), ViewerCommand.entries, this) + override fun answerFieldIsFocused(): Boolean = focusTextField override fun displayCardAnswer() { @@ -448,7 +452,6 @@ class ReviewerKeyboardInputTest : RobolectricTest() { fun displayingAnswer(): KeyboardInputTestReviewer { val keyboardInputTestReviewer = KeyboardInputTestReviewer() displayAnswer = true - keyboardInputTestReviewer.processor.setup() return keyboardInputTestReviewer } @@ -456,7 +459,6 @@ class ReviewerKeyboardInputTest : RobolectricTest() { fun displayingQuestion(): KeyboardInputTestReviewer { val keyboardInputTestReviewer = KeyboardInputTestReviewer() displayAnswer = false - keyboardInputTestReviewer.processor.setup() return keyboardInputTestReviewer } } diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/reviewer/PeripheralKeymapTest.kt b/AnkiDroid/src/test/java/com/ichi2/anki/reviewer/PeripheralKeymapTest.kt index 8e610325b37e..6fcb2ccd3da4 100644 --- a/AnkiDroid/src/test/java/com/ichi2/anki/reviewer/PeripheralKeymapTest.kt +++ b/AnkiDroid/src/test/java/com/ichi2/anki/reviewer/PeripheralKeymapTest.kt @@ -30,8 +30,8 @@ class PeripheralKeymapTest { fun flagAndAnswerDoNotConflict() { val processed: MutableList = ArrayList() - val peripheralKeymap = PeripheralKeymap(MockReviewerUi()) { e: ViewerCommand, _ -> processed.add(e) } - peripheralKeymap.setup(SPMockBuilder().createSharedPreferences()) + val sharedPrefs = SPMockBuilder().createSharedPreferences() + val peripheralKeymap = PeripheralKeymap(sharedPrefs, ViewerCommand.entries) { e: ViewerCommand, _ -> processed.add(e) } val event = mock(KeyEvent::class.java) whenever(event.unicodeChar).thenReturn(0) whenever(event.isCtrlPressed).thenReturn(true) @@ -40,14 +40,9 @@ class PeripheralKeymapTest { assertThat(event.unicodeChar.toChar(), equalTo('\u0000')) assertThat(event.getUnicodeChar(0).toChar(), equalTo('1')) - peripheralKeymap.onKeyDown(KeyEvent.KEYCODE_1, event) + peripheralKeymap.onKeyDown(event) assertThat>(processed, hasSize(1)) assertThat(processed[0], equalTo(ViewerCommand.TOGGLE_FLAG_RED)) } - - private class MockReviewerUi : ReviewerUi { - override val isDisplayingAnswer: Boolean - get() = false - } }