-
-
Notifications
You must be signed in to change notification settings - Fork 5
/
bottom_toolbar.py
187 lines (142 loc) · 7.33 KB
/
bottom_toolbar.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
# Copyright: Ren Tatsumoto <tatsu at autistici.org>
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import json
import re
from collections.abc import Sequence
from typing import Callable
from anki.cards import Card
from anki.hooks import wrap
from anki.scheduler.v3 import Scheduler as V3Scheduler
from aqt import gui_hooks, tr
from aqt.reviewer import Reviewer
from .config import config
_ans_buttons_default = Reviewer._answerButtons
def only_pass_fail(buttons: tuple, default_ease: int) -> tuple[tuple[int, str], ...]:
def is_again_or_good(ease: int, _label: str) -> bool:
return ease in (1, default_ease)
return tuple(button for button in buttons if is_again_or_good(*button))
def apply_label_colors(buttons: tuple, default_ease: int) -> tuple[tuple[int, str], ...]:
def color_label(ease: int, label: str) -> tuple[int, str]:
return ease, f'<font color="{config.get_ease_color(ease, default_ease)}">{label}</font>'
return tuple(color_label(*button) for button in buttons)
def filter_answer_buttons(buttons: tuple, self: Reviewer, _: Card) -> tuple[tuple[int, str], ...]:
# Called by _answerButtonList, before _answerButtons gets called
if config["pass_fail"] is True:
buttons = only_pass_fail(buttons, self._defaultEase())
if config["color_buttons"] is True:
buttons = apply_label_colors(buttons, self._defaultEase())
return buttons
def make_buttonless_ease_row(self: Reviewer, front: bool = False) -> str:
"""Returns ease row html when config.remove_buttons is true"""
def get_button_times() -> Sequence[str]:
# Note: Anki devs removed all schedulers before v3.
assert self._v3
assert isinstance(self.mw.col.sched, V3Scheduler)
return self.mw.col.sched.describe_next_states(self._v3.states)
def text_for_ease(ease: int, label: str) -> str:
"""Returns html with button-time text for the specified Ease."""
# Get button time from the default function,
# but remove `class="nobold"` since it introduces `position: absolute`
# which prevents the text from being visible when there is no button.
if config["hide_button_times"]:
html = f"""<span>{label}</span>"""
else:
html = self._buttonTime(ease, v3_labels=get_button_times()).replace('class="nobold"', "")
if config["color_buttons"] is True:
html = html.replace(
"<span",
f'<span style="color: {config.get_ease_color(ease, self._defaultEase())};"',
)
return html
def stat_txt() -> str:
"""
Returns html showing remaining cards, e.g. 10+70+108 (new+learn+review).
Note that if the "remaining_count_type" option is set to anything other than "default",
the HTML will be modified.
"""
return f'<div class="ajt__stat_txt">{self._remaining()}</div>'
ease_row: list[str] = []
if front is False or config["flexible_grading"] is True:
ease_row.extend(text_for_ease(ease, label) for ease, label in self._answerButtonList())
if front is True:
ease_row.insert(len(ease_row) // 2, stat_txt())
return f'<div class="ajt__ease_row">{"".join(ease_row)}</div>'
def disable_buttons(html: str) -> str:
return html.replace("<button", "<button disabled")
def make_backside_answer_buttons(self: Reviewer, _old: Callable) -> str:
if config["remove_buttons"] is True:
return make_buttonless_ease_row(self)
elif config["prevent_clicks"] is True:
return disable_buttons(_old(self))
else:
return _old(self)
def make_show_ans_table_cell(self: Reviewer):
"""Creates html code with a table data-cell holding the "Show answer" button."""
def make_show_ans_button() -> str:
"""Copypasted from Reviewer._showAnswerButton, removed id to fix margin-bottom."""
return """
<button title="{}" onclick='pycmd("ans");'>{}<span class=stattxt>{}</span></button>
""".format(
tr.actions_shortcut_key(val=tr.studying_space()),
tr.studying_show_answer(),
self._remaining(),
)
return f"<td align=center>{make_show_ans_button()}</td>"
def calc_middle_insert_pos(buttons_html_table: str) -> int:
cell_positions = [m.start() for m in re.finditer(r"<td", buttons_html_table)]
return cell_positions[: len(cell_positions) // 2 + 1][-1]
def make_flexible_front_row(self: Reviewer) -> str:
ans_buttons = _ans_buttons_default(self)
insert_pos = calc_middle_insert_pos(ans_buttons)
html = ans_buttons[:insert_pos] + make_show_ans_table_cell(self) + ans_buttons[insert_pos:]
return html
def make_frontside_answer_buttons(self: Reviewer) -> None:
html = None
if config["remove_buttons"] is True:
html = make_buttonless_ease_row(self, front=True)
elif config["flexible_grading"] is True:
html = make_flexible_front_row(self)
if config["prevent_clicks"] is True:
html = disable_buttons(html)
if html is not None:
self.bottom.web.eval("showAnswer(%s);" % json.dumps(html))
self.bottom.web.adjustHeightToFit()
def edit_bottom_html(self: Reviewer, _old: Callable) -> str:
html = _old(self)
if config["remove_buttons"] is True:
html = (
# Shrink the "Edit" button on the left and the "More" button on the right.
# Change class name of the seconds passed counter.
html.replace("<button ", '<div class="ajt__corner_button" ')
.replace("</button>", "</div>")
.replace(" class=stat>", " class=ajt__corner_stat>")
.replace(" class=stattxt>", " class=ajt__time_remaining>")
.replace(" id=innertable", ' id="innertable" class="ajt__innertable"')
)
if config["prevent_clicks"] is True:
html = disable_buttons(html)
return html
def edit_button_time(self: Reviewer, ease: int, v3_labels: Sequence[str], _old: Callable):
if config["hide_button_times"]:
return ""
return _old(self, ease, v3_labels)
def main():
# (*) Create html layout for the answer buttons on the back side.
# Buttons are either removed, disabled or left unchanged depending on config options.
# noinspection PyProtectedMember
Reviewer._answerButtons = wrap(Reviewer._answerButtons, make_backside_answer_buttons, "around")
# Wrap front side button(s).
# noinspection PyProtectedMember
Reviewer._showAnswerButton = wrap(Reviewer._showAnswerButton, make_frontside_answer_buttons, "after")
# Edit (ease, label) tuples which are used to create answer buttons.
# If `color_buttons` is true, labels are colored.
# If `pass_fail` is true, "Hard" and "Easy" buttons are removed.
# This func gets called inside _answerButtonList, which itself gets called inside _answerButtons (*)
gui_hooks.reviewer_will_init_answer_buttons.append(filter_answer_buttons)
# Edit the "Edit" and "More" buttons which are shown in the Reviewer.
# If the user chooses to remove buttons, convert them to <div>s to free vertical space.
# noinspection PyProtectedMember
Reviewer._bottomHTML = wrap(Reviewer._bottomHTML, edit_bottom_html, "around")
# Edit the text shown above answer buttons. Remove button times if the user wants to.
# noinspection PyProtectedMember
Reviewer._buttonTime = wrap(Reviewer._buttonTime, edit_button_time, "around")