-
-
Notifications
You must be signed in to change notification settings - Fork 5
/
gui.py
366 lines (312 loc) · 13.6 KB
/
gui.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
# Copyright: Ren Tatsumoto <tatsu at autistici.org>
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
from gettext import gettext as _
from typing import Optional
from aqt import mw
from aqt.qt import *
from aqt.utils import restoreGeom, saveGeom
from .ajt_common.about_menu import menu_root_entry
from .ajt_common.consts import ADDON_SERIES
from .ajt_common.enum_select_combo import EnumSelectCombo
from .ajt_common.grab_key import ShortCutGrabButton
from .ajt_common.monospace_line_edit import MonoSpaceLineEdit
from .ajt_common.utils import ui_translate
from .ajt_common.widget_placement import place_widgets_in_grid
from .config import FlexibleGradingConfig, RemainingCountType, config
from .consts import ADDON_NAME, HTML_COLORS_LINK, SCHED_NAG_MSG
as_label = ui_translate
class ColorEdit(MonoSpaceLineEdit):
font_size = 14
min_height = 24
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
color_regex = QRegularExpression(r"^#?\w+$")
color_validator = QRegularExpressionValidator(color_regex, self)
self.setValidator(color_validator)
self.setPlaceholderText("HTML color code")
class ColorEditPicker(QWidget):
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
self._edit = ColorEdit()
self.setLayout(layout := QHBoxLayout())
layout.setContentsMargins(0, 0, 0, 0)
layout.addWidget(self._edit)
layout.addWidget(b := QPushButton(_("Pick")))
b.setMinimumSize(32, 16)
b.setBaseSize(32, 22)
qconnect(b.clicked, self.choose_color)
def choose_color(self) -> None:
color = QColorDialog.getColor(initial=QColor.fromString(self._edit.text()))
if color.isValid():
self._edit.setText(color.name())
def setText(self, text: str) -> None:
return self._edit.setText(text)
def text(self) -> str:
return self._edit.text()
class SimpleKeyEdit(MonoSpaceLineEdit):
font_size = 14
min_height = 24
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
key_regex = QRegularExpression(r'^[-a-z0-9:;<>=?@~|`_/&!#$%^*(){}"+\]\[\\\']?$')
key_validator = QRegularExpressionValidator(key_regex, self)
self.setValidator(key_validator)
self.setPlaceholderText("Key letter")
self.setToolTip("If a key is taken by something else, it will refuse to work.\nLeave empty to disable.")
class ScrollAmountSpinBox(QSpinBox):
_default_allowed_range: tuple[int, int] = (10, 1000)
_single_step_amount: int = 10
def __init__(self, parent=None, initial_value: Optional[int] = None) -> None:
super().__init__(parent)
self.setRange(*self._default_allowed_range)
self.setSingleStep(self._single_step_amount)
if initial_value:
self.setValue(initial_value)
def make_color_line_edits() -> dict[str, ColorEditPicker]:
d = {}
for label in config.colors:
d[label] = ColorEditPicker()
return d
def make_answer_key_edits() -> dict[str, QLineEdit]:
d = {}
for label, button_key in config.buttons.items():
d[label] = SimpleKeyEdit(button_key)
return d
def make_toggleables() -> dict[str, QCheckBox]:
"""
Automatically create QCheckBox instances for config keys with bool values.
to avoid creating them by hand for each key.
"""
d = {}
for toggleable in config.bool_keys():
if toggleable == "color_buttons":
# handled separately by a checkable groupbox
continue
d[toggleable] = QCheckBox(as_label(toggleable))
return d
def make_scroll_shortcut_edits() -> dict[str, ShortCutGrabButton]:
return {key: ShortCutGrabButton() for key in config.scroll.keys()}
OK_AND_CANCEL = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
class SettingsMenuUI(QDialog):
name = f"{ADDON_SERIES} {ADDON_NAME} Settings Dialog"
_n_columns = 2
_scroll_shortcut_edits: dict[str, ShortCutGrabButton]
_colors: dict[str, ColorEditPicker]
_answer_keys: dict[str, QLineEdit]
_toggleables: dict[str, QCheckBox]
_color_buttons_gbox: QGroupBox # if unchecked, buttons are not painted.
_button_box: QDialogButtonBox
_restore_settings_button: QPushButton
_scroll_amount_spin: QSpinBox
_remaining_count_combo: EnumSelectCombo
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
self.setWindowTitle(f"{ADDON_SERIES} {ADDON_NAME}")
self.setMinimumSize(640, 540)
self._colors = make_color_line_edits()
self._answer_keys = make_answer_key_edits()
self._toggleables = make_toggleables()
self._scroll_shortcut_edits = make_scroll_shortcut_edits()
self._color_buttons_gbox = QGroupBox("Color buttons")
self._scroll_amount_spin = ScrollAmountSpinBox()
self._remaining_count_combo = EnumSelectCombo(enum_type=RemainingCountType)
self._button_box = QDialogButtonBox(OK_AND_CANCEL, parent=self)
self._restore_settings_button = self._button_box.addButton(
_("Restore &Defaults"), QDialogButtonBox.ButtonRole.ResetRole
)
self.setup_layout()
self.add_tooltips()
def setup_layout(self) -> None:
layout = QVBoxLayout(self)
layout.addLayout(self.make_settings_layout())
layout.addWidget(self._button_box)
self.setLayout(layout)
def make_settings_layout(self) -> QLayout:
layout = QGridLayout()
# row, col, row-span, col-span
# Color buttons (pick colors)
layout.addWidget(self.make_button_colors_group(), 0, 0, 1, 1)
# Keys (assign letters)
layout.addWidget(self.make_shortcuts_group(), 0, 1, 1, 1)
# Buttons (remove, prevent clicks)
layout.addWidget(self.make_buttons_group(), 1, 0, 1, 1)
# Features (disable/enable pass-fail, flexible grading, etc.)
layout.addWidget(self.make_features_group(), 1, 1, 1, 1)
# Zoom behavior
layout.addWidget(self.make_zoom_group(), 2, 0, 1, 1)
# Scroll shortcuts and scroll amount
layout.addWidget(self.make_scroll_group(), 2, 1, 1, 1)
return layout
@staticmethod
def make_colors_link() -> QLabel:
label = QLabel()
label.setText(
f'For the list of colors, see <a style="color: SteelBlue;" href="{HTML_COLORS_LINK}">w3schools.com</a>.'
)
label.setOpenExternalLinks(True)
return label
def make_button_colors_group(self) -> QGroupBox:
gbox = self._color_buttons_gbox
gbox.setCheckable(True)
form = QFormLayout()
for key, lineedit in self._colors.items():
form.addRow(as_label(key), lineedit)
form.addWidget(self.make_colors_link())
gbox.setLayout(form)
return gbox
def make_shortcuts_group(self) -> QGroupBox:
gbox = QGroupBox("Keys")
gbox.setCheckable(False)
form = QFormLayout()
for key, key_edit in self._answer_keys.items():
form.addRow(as_label(key), key_edit)
gbox.setLayout(form)
return gbox
def make_buttons_group(self) -> QGroupBox:
keys = (
"remove_buttons",
"prevent_clicks",
"hide_button_times",
)
gbox = QGroupBox("Buttons")
gbox.setCheckable(False)
gbox.setLayout(
place_widgets_in_grid(
(self._toggleables[key] for key in keys),
n_columns=self._n_columns,
)
)
return gbox
def make_features_group(self) -> QGroupBox:
keys = (
"pass_fail",
"flexible_grading",
"show_last_review",
"show_reps_done_today",
"press_answer_key_to_flip_card",
)
gbox = QGroupBox("Features")
gbox.setCheckable(False)
form = QFormLayout()
form.addRow(
place_widgets_in_grid(
(self._toggleables[key] for key in keys),
n_columns=self._n_columns,
)
)
form.addRow("Show remaining count:", self._remaining_count_combo)
gbox.setLayout(form)
return gbox
def make_zoom_group(self) -> QGroupBox:
keys = (
"set_zoom_shortcuts",
"remember_zoom_level",
"tooltip_on_zoom_change",
)
gbox = QGroupBox("Zoom")
gbox.setCheckable(False)
gbox.setLayout(
place_widgets_in_grid(
(self._toggleables[key] for key in keys),
n_columns=self._n_columns,
)
)
return gbox
def make_scroll_group(self) -> QGroupBox:
gbox = QGroupBox("Scroll")
gbox.setCheckable(False)
form = QFormLayout()
for scroll_direction, key_edit_widget in self._scroll_shortcut_edits.items():
form.addRow(as_label(scroll_direction), key_edit_widget)
form.addRow("Scroll amount", self._scroll_amount_spin)
gbox.setLayout(form)
return gbox
def add_tooltips(self) -> None:
self._toggleables["pass_fail"].setToolTip('"Hard" and "Easy" buttons will be hidden.')
self._toggleables["flexible_grading"].setToolTip(
"Grade cards from their front side\nwithout having to reveal the answer."
)
self._toggleables["remove_buttons"].setToolTip(
"Remove answer buttons.\nOnly the corresponding intervals will be visible."
)
self._toggleables["hide_button_times"].setToolTip(
"Remove text shown above each answer button\n"
"that tells you the future interval of the card\n"
"if you press the button."
)
self._toggleables["prevent_clicks"].setToolTip(
"Make answer buttons disabled.\n" "Disabled buttons are visible but unusable and un-clickable."
)
self._toggleables["show_last_review"].setToolTip("Print the result of the last review on the toolbar.")
self._toggleables["set_zoom_shortcuts"].setToolTip("Change zoom value by pressing Ctrl+Plus and Ctrl+Minus.")
self._toggleables["remember_zoom_level"].setToolTip("Remember last zoom level and restore it on state change.")
self._toggleables["tooltip_on_zoom_change"].setToolTip("Show a tooltip when zoom level changes.")
self._remaining_count_combo.setToolTip(
"Default: 'New', 'Learn', and 'Due' counters are separate.\n"
"Single: Show a single number of cards left to review.\n"
"None: Completely turn off the indicator\nthat tells you how many cards are left."
)
self._toggleables["press_answer_key_to_flip_card"].setToolTip(
"Answer keys ('h', 'j', 'k', 'l' by default) will be used\n"
"to reveal the back side, similarly to the Space bar."
)
self._toggleables["show_reps_done_today"].setToolTip(
"Print the number of reviews done today on the bottom bar."
)
class SettingsMenuDialog(SettingsMenuUI):
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
self.connect_buttons()
if mw.col.schedVer() < 2:
self.layout().addWidget(QLabel(SCHED_NAG_MSG))
self.restore_values(config)
restoreGeom(self, self.name)
def restore_values(self, cm: FlexibleGradingConfig) -> None:
self._color_buttons_gbox.setChecked(cm["color_buttons"])
for key, checkbox in self._toggleables.items():
checkbox.setChecked(cm[key])
for label, color_text in cm.colors.items():
self._colors[label].setText(color_text)
for label, key_letter in cm.buttons.items():
self._answer_keys[label].setText(key_letter)
for scroll_direction, shortcut_str in cm.scroll.items():
self._scroll_shortcut_edits[scroll_direction].setValue(shortcut_str)
self._scroll_amount_spin.setValue(config.scroll_amount)
self._remaining_count_combo.setCurrentName(config.remaining_count_type)
def connect_buttons(self) -> None:
qconnect(
self._restore_settings_button.clicked,
lambda: self.restore_values(FlexibleGradingConfig(default=True)),
)
qconnect(self._button_box.accepted, self.accept)
qconnect(self._button_box.rejected, self.reject)
def accept(self) -> None:
config["color_buttons"] = self._color_buttons_gbox.isChecked()
for label, lineedit in self._colors.items():
config.set_color(label, lineedit.text())
for label, lineedit in self._answer_keys.items():
config.set_key(label, lineedit.text())
for key, checkbox in self._toggleables.items():
config[key] = checkbox.isChecked()
for scroll_direction, key_edit_widget in self._scroll_shortcut_edits.items():
config.scroll[scroll_direction] = key_edit_widget.value()
config.scroll_amount = self._scroll_amount_spin.value()
config.remaining_count_type = self._remaining_count_combo.currentData()
config.write_config()
return super().accept()
def done(self, *args, **kwargs) -> None:
saveGeom(self, self.name)
return super().done(*args, **kwargs)
def on_open_settings() -> None:
assert mw
if mw.state != "deckBrowser":
mw.moveToState("deckBrowser")
dialog = SettingsMenuDialog(mw)
dialog.exec()
def setup_settings_action(parent: QWidget) -> QAction:
action_settings = QAction(f"{ADDON_NAME} Options...", parent)
qconnect(action_settings.triggered, on_open_settings)
return action_settings
def main() -> None:
root_menu = menu_root_entry()
root_menu.addAction(setup_settings_action(root_menu))