diff --git a/ftl/core/preferences.ftl b/ftl/core/preferences.ftl
index 9be0b3e5d..d04a15ec6 100644
--- a/ftl/core/preferences.ftl
+++ b/ftl/core/preferences.ftl
@@ -58,6 +58,7 @@ preferences-appearance = Appearance
preferences-general = General
preferences-style = Style
preferences-review = Review
+preferences-answer-keys = Answer keys
preferences-distractions = Distractions
preferences-minimalist-mode = Minimalist mode
preferences-editing = Editing
@@ -71,6 +72,7 @@ preferences-import-export = Import/Export
preferences-network-timeout = Network timeout
preferences-reset-window-sizes = Reset Window Sizes
preferences-reset-window-sizes-complete = Window sizes and locations have been reset.
+preferences-shortcut-placeholder = Enter an unused shortcut key, or leave empty to disable.
## NO NEED TO TRANSLATE. This text is no longer used by Anki, and will be removed in the future.
diff --git a/qt/aqt/forms/preferences.ui b/qt/aqt/forms/preferences.ui
index f66608d45..e8c613a6e 100644
--- a/qt/aqt/forms/preferences.ui
+++ b/qt/aqt/forms/preferences.ui
@@ -245,7 +245,7 @@
preferences_review
-
+
-
diff --git a/qt/aqt/preferences.py b/qt/aqt/preferences.py
index 633a5bce6..7f643894e 100644
--- a/qt/aqt/preferences.py
+++ b/qt/aqt/preferences.py
@@ -1,6 +1,7 @@
# Copyright: Ankitects Pty Ltd and contributors
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
+import functools
import re
from typing import Any, cast
@@ -47,8 +48,39 @@ class Preferences(QDialog):
self.setup_collection()
self.setup_profile()
self.setup_global()
+ self.setup_configurable_answer_keys()
self.show()
+ def setup_configurable_answer_keys(self):
+ """
+ Create a group box in Preferences with widgets that let the user edit answer keys.
+ """
+ ease_labels = (
+ (1, tr.studying_again()),
+ (2, tr.studying_hard()),
+ (3, tr.studying_good()),
+ (4, tr.studying_easy()),
+ )
+ self.form.review_options_layout.addWidget(
+ group := QGroupBox(tr.preferences_answer_keys())
+ )
+ group.setLayout(layout := QFormLayout())
+ for ease, label in ease_labels:
+ layout.addRow(
+ label,
+ line_edit := QLineEdit(self.mw.pm.get_answer_key(ease) or ""),
+ )
+ qconnect(
+ line_edit.textChanged,
+ functools.partial(self.mw.pm.set_answer_key, ease),
+ )
+ line_edit.setValidator(
+ QRegularExpressionValidator(
+ QRegularExpression(r"^[a-z0-9\]\[=,./;\'\\-]$")
+ )
+ )
+ line_edit.setPlaceholderText(tr.preferences_shortcut_placeholder())
+
def accept(self) -> None:
# avoid exception if main window is already closed
if not self.mw.col:
diff --git a/qt/aqt/profiles.py b/qt/aqt/profiles.py
index f757355d8..fffca16bf 100644
--- a/qt/aqt/profiles.py
+++ b/qt/aqt/profiles.py
@@ -10,7 +10,7 @@ import shutil
import traceback
from enum import Enum
from pathlib import Path
-from typing import TYPE_CHECKING, Any
+from typing import TYPE_CHECKING, Any, Optional
import anki.lang
import aqt.forms
@@ -116,6 +116,8 @@ class LoadMetaResult:
class ProfileManager:
+ default_answer_keys = {ease_num: str(ease_num) for ease_num in range(1, 5)}
+
def __init__(self, base: Path) -> None: #
"base should be retrieved via ProfileMangager.get_created_base_folder"
## Settings which should be forgotten each Anki restart
@@ -537,6 +539,12 @@ create table if not exists profiles
def set_spacebar_rates_card(self, on: bool) -> None:
self.meta["spacebar_rates_card"] = on
+ def get_answer_key(self, ease: int) -> Optional[str]:
+ return self.meta.setdefault("answer_keys", self.default_answer_keys).get(ease)
+
+ def set_answer_key(self, ease: int, key: str):
+ self.meta.setdefault("answer_keys", self.default_answer_keys)[ease] = key
+
def hide_top_bar(self) -> bool:
return self.meta.get("hide_top_bar", False)
diff --git a/qt/aqt/reviewer.py b/qt/aqt/reviewer.py
index d56402236..c8bd3d963 100644
--- a/qt/aqt/reviewer.py
+++ b/qt/aqt/reviewer.py
@@ -3,6 +3,7 @@
from __future__ import annotations
+import functools
import json
import random
import re
@@ -507,14 +508,11 @@ class Reviewer:
("o", self.onOptions),
("i", self.on_card_info),
("Ctrl+Alt+i", self.on_previous_card_info),
- ("1", lambda: self._answerCard(1)),
- ("2", lambda: self._answerCard(2)),
- ("3", lambda: self._answerCard(3)),
- ("4", lambda: self._answerCard(4)),
- ("h", lambda: self._answerCard(1)),
- ("j", lambda: self._answerCard(2)),
- ("k", lambda: self._answerCard(3)),
- ("l", lambda: self._answerCard(4)),
+ *(
+ (key, functools.partial(self._answerCard, ease))
+ for ease in aqt.mw.pm.default_answer_keys
+ if (key := aqt.mw.pm.get_answer_key(ease))
+ ),
("u", self.mw.undo),
("5", self.on_pause_audio),
("6", self.on_seek_backward),