80 |
81 |
Anki
82 |
83 |
84 |
89 |
90 |
91 |
92 |
93 | %(stats)s
94 | %(countwarn)s
95 |
96 |
97 |
98 |
99 |
100 |
101 | """
102 |
--------------------------------------------------------------------------------
/redesign/actions_and_settings.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime
2 |
3 | from PyQt5.QtCore import QTimer
4 | from PyQt5.QtGui import QColor
5 | from PyQt5.QtWidgets import QColorDialog
6 |
7 | from .internals import Setting, MenuAction, alert
8 | from .color_map import ColorMapWindow
9 | from .mode import ModeWindow
10 | from .selector import StylersSelectorWindow
11 |
12 |
13 | class UserColorMap(Setting, MenuAction):
14 | value = {'#000000': 'white'}
15 | window = None
16 | label = 'Customize colors on cards'
17 |
18 | def action(self):
19 | from aqt import mw as main_window
20 | if not self.window:
21 | # self.value is mutable, any modifications done by ColorMapWindow
22 | # will be done on the value of this singleton class object
23 | self.window = ColorMapWindow(
24 | main_window,
25 | self.value,
26 | on_update=self.on_colors_changed
27 | )
28 | self.window.show()
29 |
30 | def on_colors_changed(self):
31 | self.app.refresh()
32 |
33 |
34 |
35 |
36 | class InvertImage(Setting, MenuAction):
37 | """Toggles image inversion.
38 |
39 | To learn how images are inverted check also append_to_styles().
40 | """
41 |
42 | # setting
43 | value = False
44 |
45 | # menu action
46 | label = '&Invert images'
47 | checkable = True
48 |
49 | def action(self):
50 | self.value = not self.value
51 | self.app.refresh()
52 |
53 |
54 |
55 |
56 | class InvertLatex(Setting, MenuAction):
57 | """Toggles latex inversion.
58 |
59 | Latex formulas are nothing more than images with class "latex".
60 | To learn how formulas are inverted check also append_to_styles().
61 | """
62 | value = False
63 | label = 'Invert &latex'
64 | checkable = True
65 |
66 | def action(self):
67 | self.value = not self.value
68 | self.app.refresh()
69 |
70 |
71 |
72 |
73 | class TransparentLatex(Setting, MenuAction):
74 | """Toggles transparent latex generation.
75 |
76 | See make_latex_transparent() for details.
77 | """
78 | value = False
79 | label = 'Force transparent latex'
80 | checkable = True
81 |
82 | def action(self):
83 | self.value = not self.value
84 | if self.value:
85 | self.make_latex_transparent()
86 |
87 | def make_latex_transparent(self):
88 | """Overwrite latex generation commands to use transparent images.
89 |
90 | Already generated latex images won't be affected;
91 | delete those manually from your media folder in order
92 | to regenerate images in transparent version.
93 | """
94 |
95 | commands = self.get_commands()
96 |
97 | for command in commands:
98 | command[1] = [
99 | "dvipng",
100 | "-D", "200",
101 | "-T", "tight",
102 | "-bg", "Transparent",
103 | "-z", "9", # use maximal PNG compression
104 | "tmp.dvi",
105 | "-o", "tmp.png"
106 | ]
107 |
108 | @staticmethod
109 | def get_commands():
110 | from anki.latex import pngCommands
111 | from anki.latex import svgCommands
112 | commands = []
113 | commands.extend([pngCommands, svgCommands])
114 | return commands
115 |
116 | def on_load(self):
117 | if self.value:
118 | self.make_latex_transparent()
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 | # COLORS
128 | # 3 Sections:
129 | # Section 1 is default colors with the names of the colors used throughout the addon (color_t, color_b, etc.).
130 | # Section 2 is light mode colors. "LightMode" can be selected in View menu by user to switch to these colors.
131 | # Section 3 is dark mode colors. "DarkMode" can be selected in View menu by user to switch to these colors.
132 | class ColorAction(Setting, MenuAction):
133 | def action(self):
134 | qt_color_old = QColor(self.value)
135 | qt_color = QColorDialog.getColor(qt_color_old)
136 | if qt_color.isValid():
137 | self.value = qt_color.name()
138 | self.app.refresh()
139 |
140 | class TextColor(ColorAction):
141 | """
142 | Open color picker and set chosen color to text (in content)
143 | """
144 | name = 'color_t'
145 | value = '#000000'
146 | label = 'Set &text color'
147 |
148 | class BackgroundColor(ColorAction):
149 | """
150 | Open color picker and set chosen color to background (of main window content)
151 | """
152 | name = 'color_b'
153 | value = '#fafafa'
154 | label = 'Set &background color'
155 |
156 | class CardColor(ColorAction):
157 | """
158 | Open color picker and set chosen color to background (of content)
159 | """
160 | name = 'color_c'
161 | value = '#ffffff'
162 | label = 'Set &card color'
163 |
164 | class PrimaryColor(ColorAction):
165 | """
166 | Open color picker and set chosen color to primary color (of buttons and more)
167 | """
168 | name = 'color_p'
169 | value = '#2196f3'
170 | label = 'Set &primary color'
171 |
172 | class AuxiliaryBackgroundColor(ColorAction):
173 | """
174 | Open color picker and set chosen color to auxiliary background (of content)
175 | """
176 | name = 'color_s'
177 | value = '#00ff00'
178 | label = 'Set &auxiliary background color'
179 |
180 | # TODO: include in menu
181 | class ActiveBackgroundColor(ColorAction):
182 | """
183 | Open color picker and set chosen color to auxiliary background (of content)
184 | """
185 | name = 'color_a'
186 | value = '#2196f3'
187 | label = 'Set active color'
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 | # LIGHT MODE COLORS
197 | class TextColor(ColorAction):
198 | name = 'color_t_light'
199 | value = '#000000'
200 | label = 'Set &text color'
201 |
202 | class BackgroundColor(ColorAction):
203 | name = 'color_b_light'
204 | value = '#fafafa'
205 | label = 'Set &background color'
206 |
207 | class CardColor(ColorAction):
208 | name = 'color_c_light'
209 | value = '#ffffff'
210 | label = 'Set &card color'
211 |
212 | class PrimaryColor(ColorAction):
213 | name = 'color_p_light'
214 | value = '#2196f3'
215 | label = 'Set &primary color'
216 |
217 | class AuxiliaryBackgroundColor(ColorAction):
218 | name = 'color_s_light'
219 | value = '#00ff00'
220 | label = 'Set &auxiliary background color'
221 |
222 | class ActiveBackgroundColor(ColorAction):
223 | name = 'color_a_light'
224 | value = '#2196f3'
225 | label = 'Set active color'
226 |
227 |
228 | class LightColors(MenuAction):
229 | """Reset colors"""
230 | label = '&Light mode'
231 |
232 | def action(self):
233 | self.app.config.color_p = self.app.config.color_p_light
234 | self.app.config.color_b = self.app.config.color_b_light
235 | self.app.config.color_c = self.app.config.color_c_light
236 | self.app.config.color_t = self.app.config.color_t_light
237 | self.app.refresh()
238 |
239 |
240 |
241 |
242 |
243 | # DARK MODE COLORS
244 | class TextColor(ColorAction):
245 | name = 'color_t_dark'
246 | value = 'rgba(255,255,255,0.7)'
247 | label = 'Set &text color'
248 |
249 | class BackgroundColor(ColorAction):
250 | name = 'color_b_dark'
251 | value = '#312e42'
252 | label = 'Set &background color'
253 |
254 | class CardColor(ColorAction):
255 | name = 'color_c_dark'
256 | value = '#44405a'
257 | label = 'Set &card color'
258 |
259 | class PrimaryColor(ColorAction):
260 | name = 'color_p_dark'
261 | value = '#00b79f' # Blue: #2196f3, Red: #ef5350, Teal: #00b79f
262 | label = 'Set &primary color'
263 |
264 | class AuxiliaryBackgroundColor(ColorAction):
265 | name = 'color_s_dark'
266 | value = '#00ffff'
267 | label = 'Set &auxiliary background color'
268 |
269 | class ActiveBackgroundColor(ColorAction):
270 | name = 'color_a_dark'
271 | value = '#2196f3'
272 | label = 'Set active color'
273 |
274 | class DarkColors(MenuAction):
275 | label = '&Dark mode'
276 |
277 | def action(self):
278 | self.app.config.color_p = self.app.config.color_p_dark
279 | self.app.config.color_b = self.app.config.color_b_dark
280 | self.app.config.color_c = self.app.config.color_c_dark
281 | self.app.config.color_t = self.app.config.color_t_dark
282 | self.app.refresh()
283 |
284 |
285 |
286 |
287 |
288 |
289 |
290 |
291 | class About(MenuAction):
292 | """Show "about" window"""
293 | label = '&About...'
294 |
295 | def action(self):
296 | self.app.about()
297 |
298 |
299 |
300 |
301 | class EnableInDialogs(Setting, MenuAction):
302 | """Switch for night mode in dialogs"""
303 | value = True
304 | label = 'Enable in &dialogs'
305 | checkable = True
306 |
307 | def action(self):
308 | self.value = not self.value
309 |
310 |
311 |
312 | class StyleScrollBars(Setting, MenuAction):
313 | value = True
314 | label = 'Enable new scrollbars'
315 | checkable = True
316 |
317 | def action(self):
318 | self.value = not self.value
319 | self.app.refresh()
320 |
321 |
322 |
323 |
324 | class ModeSettings(Setting, MenuAction):
325 | value = {
326 | 'mode': 'manual',
327 | 'start_at': '21:30',
328 | 'end_at': '07:30'
329 | }
330 | window = None
331 | label = 'Start automatically'
332 | checkable = True
333 |
334 | @property
335 | def is_checked(self):
336 | return self.mode == 'auto'
337 |
338 | @property
339 | def mode(self):
340 | return self.value['mode']
341 |
342 | def action(self):
343 | from aqt import mw as main_window
344 |
345 | if not self.window:
346 | # self.value is mutable, any modifications done by ColorMapWindow
347 | # will be done on the value of this singleton class object
348 | self.window = ModeWindow(
349 | main_window,
350 | self.value,
351 | on_update=self.update
352 | )
353 | self.window.show()
354 | self.app.update_menu()
355 |
356 | def update(self):
357 | self.app.refresh()
358 |
359 | @property
360 | def is_active(self):
361 | current_time = datetime.now().time()
362 | start = self.time('start_at')
363 | end = self.time('end_at')
364 | if end > start:
365 | return start <= current_time <= end
366 | else:
367 | return start <= current_time or current_time <= end
368 |
369 | def time(self, which):
370 | return datetime.strptime(self.value[which], '%H:%M').time()
371 |
372 |
373 |
374 |
375 | class EnableNightMode(Setting, MenuAction):
376 | """Switch night mode"""
377 | value = False
378 | label = '&Enable redesign'
379 | shortcut = 'Ctrl+n'
380 | checkable = True
381 |
382 | require = {
383 | ModeSettings,
384 | # 'StateSettings' (circular dependency)
385 | }
386 |
387 | def action(self):
388 | self.value = not self.value
389 |
390 | if self.mode_settings.mode != 'manual':
391 | alert(
392 | 'Automatic Redesign has been disabled. '
393 | '(You pressed "ctrl+n" or switched a toggle in the menu). '
394 | 'Now you can toggle Night Mode manually '
395 | 'or re-enable the Automatic Night Mode in the menu. '
396 | )
397 | self.mode_settings.value['mode'] = 'manual'
398 |
399 | success = self.app.refresh()
400 |
401 | if not success:
402 | self.value = not self.value
403 |
404 | self.app.config.state_on.update_state()
405 |
406 |
407 |
408 |
409 | class StateSetting(Setting):
410 | """Stores the last state of application.
411 |
412 | The state after start-up is determined programmatically;
413 | the value set during configuration loading will be ignored.
414 | """
415 | name = 'state_on'
416 | state = None
417 |
418 | require = {
419 | ModeSettings,
420 | EnableNightMode
421 | }
422 |
423 | @property
424 | def value(self):
425 | if self.mode_settings.mode == 'manual':
426 | return self.enable_night_mode.value
427 | else:
428 | return self.mode_settings.is_active
429 |
430 | @value.setter
431 | def value(self, value):
432 | pass
433 |
434 | def __init__(self, *args, **kwargs):
435 | super().__init__(*args, **kwargs)
436 | # check the state every 60 seconds
437 | # (maybe a bit suboptimal, but the most reliable)
438 | from aqt import mw as main_window
439 | self.timer = QTimer(main_window)
440 | self.timer.setInterval(60 * 100) # 1000 milliseconds
441 | self.timer.timeout.connect(self.maybe_enable_maybe_disable)
442 |
443 | def on_load(self):
444 | if self.value:
445 | self.app.on()
446 |
447 | self.update_state()
448 | self.timer.start()
449 |
450 | def on_save(self):
451 | self.timer.stop()
452 |
453 | def maybe_enable_maybe_disable(self):
454 | if self.value != self.state:
455 | self.app.refresh()
456 | self.update_state()
457 |
458 | def update_state(self):
459 | self.state = self.value
460 |
461 |
462 |
463 |
464 | class DisabledStylers(Setting, MenuAction):
465 |
466 | value = set()
467 | window = None
468 | label = 'Choose what to style'
469 |
470 | def action(self):
471 | from aqt import mw as main_window
472 |
473 | if not self.window:
474 | self.window = StylersSelectorWindow(
475 | main_window,
476 | self.value,
477 | self.app.styles.stylers,
478 | on_update=self.update
479 | )
480 | self.window.show()
481 |
482 | def update(self):
483 | self.app.refresh(reload=True)
484 |
--------------------------------------------------------------------------------
/redesign/color_map.py:
--------------------------------------------------------------------------------
1 | from PyQt5.QtCore import Qt, pyqtSlot as slot
2 | from PyQt5.QtGui import QColor
3 | from PyQt5.QtWidgets import QWidget, QLabel, QGridLayout, QPushButton, QColorDialog, QHBoxLayout, QVBoxLayout
4 |
5 | from .internals import alert
6 | from .gui import create_button, remove_layout, AddonDialog
7 | from .languages import _
8 |
9 |
10 | class ColorSwatch(QPushButton):
11 |
12 | def __init__(self, parent, color=None, on_color_change=None, name='Color', verify_colors=False):
13 | """
14 |
15 | Args:
16 | parent: a parent Qt instance
17 | color: the color name or hexadecimal code (in form of a string)
18 | on_color_change: a function or method taking old color and a new one
19 | verify_colors: should the parent be asked if the color is acceptable?
20 | to verify a color parent.is_acceptable(color) will be invoked
21 | """
22 | QPushButton.__init__(self, color, parent)
23 | self.verify_colors = verify_colors
24 | self.parent = parent
25 | self.color = color
26 | self.name = name
27 | self.callback = on_color_change
28 |
29 | if color:
30 | self.set_color(color)
31 | else:
32 | self.setText(_('(Not specified)'))
33 |
34 | self.clicked.connect(self.pick_color)
35 |
36 | def set_color(self, color):
37 | self.color = color
38 | self.setText(color)
39 | self.setStyleSheet(f'background-color: {self.color}; color: {self.text_color}')
40 |
41 | @property
42 | def text_color(self):
43 | return 'black' if self.qt_color.lightness() > 127 else 'white'
44 |
45 | @property
46 | def qt_color(self):
47 | return QColor(self.color)
48 |
49 | @slot()
50 | def pick_color(self, qt_color=None):
51 | if not qt_color:
52 | qt_color = self.qt_color
53 |
54 | old_color = self.color
55 | qt_color = QColorDialog.getColor(
56 | qt_color,
57 | parent=self,
58 | title=_('Select %s') % _(self.name)
59 | )
60 |
61 | if qt_color.isValid():
62 | color = qt_color.name()
63 | if self.verify_colors and not self.parent.is_acceptable(color):
64 | alert(_('This color (%s) is already mapped. Please select a different one.') % color)
65 | return self.pick_color(qt_color=qt_color)
66 |
67 | self.set_color(color)
68 | self.callback(old_color, self.color)
69 |
70 |
71 | class ColorMapping(QWidget):
72 |
73 | def __init__(self, parent, normal_color, night_color):
74 | """
75 |
76 | Args:
77 | parent: ColorMapWindow instance
78 | normal_color: name or code of code to use in normal mode
79 | night_color: name or code of code to use in night mode
80 | """
81 | QWidget.__init__(self, parent)
82 | self.parent = parent
83 | self.normal = ColorSwatch(self, normal_color, self.update_normal, 'Normal Mode Color', verify_colors=True)
84 | self.night = ColorSwatch(self, night_color, self.update_night, 'Night Mode Color')
85 | self.grid = QGridLayout()
86 | self.fill_layout()
87 | self.setLayout(self.grid)
88 |
89 | def fill_layout(self):
90 | remove = create_button('Remove', self.remove)
91 | grid = self.grid
92 | grid.addWidget(self.normal, 0, 1, 1, 3)
93 | arrow = QLabel('→')
94 | arrow.setAlignment(Qt.AlignCenter)
95 | grid.addWidget(arrow, 0, 4)
96 | grid.addWidget(self.night, 0, 5, 1, 3)
97 | grid.addWidget(remove, 0, 8)
98 |
99 | @slot()
100 | def remove(self):
101 | self.parent.update(self.normal.color, None, None)
102 | remove_layout(self.grid)
103 | self.parent.mappings.removeWidget(self)
104 | self.deleteLater()
105 |
106 | def update_normal(self, old, new):
107 | night = self.night.color
108 | self.parent.update(old, new, night)
109 |
110 | def update_night(self, old, new):
111 | normal = self.normal.color
112 | self.parent.update(normal, normal, new)
113 |
114 | def is_acceptable(self, color):
115 | return self.parent.is_acceptable(color)
116 |
117 |
118 | class ColorMapWindow(AddonDialog):
119 |
120 | def __init__(self, parent, color_map, title='Customize color swapping', on_update=None):
121 | super().__init__(self, parent, Qt.Window)
122 | self.on_update = on_update
123 | self.color_map = color_map
124 |
125 | self.init_ui(title)
126 |
127 | def init_ui(self, title):
128 | self.setWindowTitle(_(title))
129 |
130 | btn_add_mapping = create_button('+ Add color mapping', self.on_add)
131 | btn_close = create_button('Close', self.close)
132 |
133 | buttons = QHBoxLayout()
134 |
135 | buttons.addWidget(btn_close)
136 | buttons.addWidget(btn_add_mapping)
137 | buttons.setAlignment(Qt.AlignBottom)
138 |
139 | body = QVBoxLayout()
140 | body.setAlignment(Qt.AlignTop)
141 |
142 | header = QLabel(_(
143 | 'Specify how particular colors on your cards '
144 | 'should be swapped when the redesign is on.'
145 | ))
146 | header.setAlignment(Qt.AlignCenter)
147 |
148 | mappings = QVBoxLayout()
149 | mappings.setAlignment(Qt.AlignTop)
150 |
151 | for normal_color, night_color in self.color_map.items():
152 | mapping = ColorMapping(self, normal_color, night_color)
153 | mappings.addWidget(mapping)
154 |
155 | self.mappings = mappings
156 |
157 | body.addWidget(header)
158 | body.addLayout(mappings)
159 | body.addStretch(1)
160 | body.addLayout(buttons)
161 | self.setLayout(body)
162 |
163 | self.setGeometry(300, 300, 350, 300)
164 | self.show()
165 |
166 | @slot()
167 | def on_add(self):
168 | mapping = ColorMapping(self, None, None)
169 | self.mappings.addWidget(mapping)
170 | mapping.normal.pick_color()
171 | mapping.night.pick_color()
172 |
173 | def is_acceptable(self, color):
174 | return color not in self.color_map
175 |
176 | def update(self, old_key, new_key, new_value):
177 | if old_key:
178 | del self.color_map[old_key]
179 | if new_key:
180 | self.color_map[new_key] = new_value
181 | if self.on_update:
182 | self.on_update()
183 |
--------------------------------------------------------------------------------
/redesign/config.py:
--------------------------------------------------------------------------------
1 | from aqt import mw
2 | from .internals import Setting
3 |
4 |
5 | class Config:
6 |
7 | def __init__(self, app, prefix=''):
8 | self.app = app
9 | self.prefix = prefix
10 | self.settings = {}
11 |
12 | # has to be separately from __init__ to avoid circular reference
13 | def init_settings(self):
14 | for setting_class in Setting.members:
15 | setting = setting_class(self.app)
16 | self.settings[setting.name] = setting
17 |
18 | def __getattr__(self, attr):
19 | return self.settings[attr]
20 |
21 | def stored_name(self, name):
22 | return self.prefix + name
23 |
24 | def load(self):
25 | for name, setting in self.settings.items():
26 | key = self.stored_name(name)
27 | value = mw.pm.profile.get(key, setting.default_value)
28 |
29 | setting.value = value
30 |
31 | for setting in self.settings.values():
32 | setting.on_load()
33 |
34 | def save(self):
35 | """
36 | Saves configurable variables into profile, so they can
37 | be used to restore previous state after Anki restart.
38 | """
39 | for name, setting in self.settings.items():
40 | key = self.stored_name(name)
41 | mw.pm.profile[key] = setting.value
42 |
43 | for setting in self.settings.values():
44 | setting.on_save()
45 |
46 |
47 | class ConfigValueGetter:
48 |
49 | def __init__(self, config):
50 | self.config = config
51 |
52 | def __getattr__(self, attr):
53 | setting = getattr(self.config, attr)
54 | return setting.value
55 |
--------------------------------------------------------------------------------
/redesign/css_class.py:
--------------------------------------------------------------------------------
1 | def inject_css_class(state: bool, html: str):
2 | if state:
3 | javascript = """
4 | function add_anki_redesign_class(){
5 | current_classes = document.body.className;
6 | if(current_classes.indexOf("anki_redesign") == -1)
7 | {
8 | document.body.className += " anki_redesign";
9 | }
10 | }
11 | // explanation of setTimeout use:
12 | // callback defined in _showQuestion of reviewer.js would otherwise overwrite
13 | // the newly set body class; in order to prevent that the function execution
14 | // is being placed on the end of execution queue (hence time = 0)
15 | setTimeout(add_anki_redesign_class, 0)
16 | """
17 | else:
18 | javascript = """
19 | function remove_anki_redesign_class(){
20 | current_classes = document.body.className;
21 | if(current_classes.indexOf("anki_redesign") != -1)
22 | {
23 | document.body.className = current_classes.replace("anki_redesign","");
24 | }
25 | }
26 | setTimeout(remove_anki_redesign_class, 0)
27 | """
28 | # script on the beginning of the HTML so it will always be
29 | # before any user-defined, potentially malformed HTML
30 | html = f"" + html
31 | return html
32 |
--------------------------------------------------------------------------------
/redesign/gui.py:
--------------------------------------------------------------------------------
1 | from PyQt5.QtWidgets import QPushButton, QDialog
2 |
3 | from .languages import _
4 |
5 |
6 | class AddonDialog(QDialog):
7 | def __init__(self, *args, **kwargs):
8 | QDialog.__init__(*args, **kwargs)
9 |
10 |
11 | def create_button(name, callback=None):
12 | button = QPushButton(_(name))
13 | if callback:
14 | button.clicked.connect(callback)
15 | return button
16 |
17 |
18 | def iterate_widgets(layout):
19 | for i in reversed(range(layout.count())):
20 | yield layout.itemAt(i).widget()
21 |
22 |
23 | def remove_layout(layout):
24 | for widget in iterate_widgets(layout):
25 | layout.removeWidget(widget)
26 | widget.deleteLater()
27 |
28 |
--------------------------------------------------------------------------------
/redesign/icons.py:
--------------------------------------------------------------------------------
1 | from os import makedirs
2 | from os.path import isfile, dirname, abspath, join
3 | from PyQt5.QtGui import QIcon, QPixmap
4 | from PyQt5.QtWidgets import QStyle
5 |
6 |
7 | def inverted_icon(icon, width=32, height=32, as_image=False):
8 | pixmap = icon.pixmap(width, height)
9 | image = pixmap.toImage()
10 | # image.invertPixels()
11 | if as_image:
12 | return image
13 | new_icon = QIcon(QPixmap.fromImage(image))
14 | return new_icon
15 |
16 |
17 | class Icons:
18 |
19 | paths = {}
20 |
21 | def __init__(self, mw):
22 |
23 | add_on_path = dirname(abspath(__file__))
24 | add_on_resources = join(add_on_path, 'user_files')
25 | icons_path = join(add_on_resources, 'icons')
26 | makedirs(icons_path, exist_ok=True)
27 |
28 | icon_path = join(icons_path, 'arrow.png')
29 |
30 | if not isfile(icon_path):
31 | down_arrow_icon = mw.style().standardIcon(QStyle.SP_ArrowDown)
32 | image = inverted_icon(down_arrow_icon, width=16, height=16, as_image=True)
33 | image.save(icon_path)
34 |
35 | arrow_path = icon_path.replace('\\', '/')
36 |
37 | where_to_look_for_arrow_icon = [
38 | '/usr/share/icons/Adwaita/scalable/actions/pan-down-symbolic.svg',
39 | '/usr/share/icons/gnome/scalable/actions/go-down-symbolic.svg',
40 | '/usr/share/icons/ubuntu-mobile/actions/scalable/dropdown-menu.svg',
41 | '/usr/share/icons/Humanity/actions/16/down.svg',
42 | '/usr/share/icons/Humanity/actions/16/go-down.svg',
43 | '/usr/share/icons/Humanity/actions/16/stock_down.svg',
44 | '/usr/share/icons/nuvola/16x16/actions/arrow-down.png',
45 | '/usr/share/icons/default.kde4/16x16/actions/arrow-down.png'
46 | ]
47 |
48 | for path in where_to_look_for_arrow_icon:
49 | if isfile(path):
50 | arrow_path = path
51 | break
52 |
53 | self.paths['arrow'] = arrow_path
54 |
55 | @property
56 | def arrow(self):
57 | return self.paths['arrow']
58 |
--------------------------------------------------------------------------------
/redesign/internals.py:
--------------------------------------------------------------------------------
1 | import re
2 | from PyQt5 import QtCore
3 | from abc import abstractmethod, ABCMeta
4 | from inspect import isclass
5 | from types import MethodType
6 |
7 | from anki.hooks import wrap
8 | from anki.lang import _
9 | from aqt.utils import showWarning
10 |
11 |
12 | try:
13 | from_utf8 = QtCore.QString.fromUtf8
14 | except AttributeError:
15 | from_utf8 = lambda s: s
16 |
17 |
18 | def alert(info):
19 | showWarning(_(info))
20 |
21 |
22 | class PropertyDescriptor:
23 | def __init__(self, value=None):
24 | self.value = value
25 |
26 | def __get__(self, obj, obj_type):
27 | return self.value(obj)
28 |
29 | def __set__(self, obj, value):
30 | self.value = value
31 |
32 |
33 | class css(PropertyDescriptor):
34 | is_css = True
35 |
36 |
37 | def abstract_property(func):
38 | return property(abstractmethod(func))
39 |
40 |
41 | def snake_case(camel_case):
42 | return re.sub('(?!^)([A-Z]+)', r'_\1', camel_case).lower()
43 |
44 |
45 | class AbstractRegisteringType(ABCMeta):
46 |
47 | def __init__(cls, name, bases, attributes):
48 | super().__init__(name, bases, attributes)
49 |
50 | if not hasattr(cls, 'members'):
51 | cls.members = set()
52 |
53 | cls.members.add(cls)
54 | cls.members -= set(bases)
55 |
56 |
57 | class SnakeNameMixin:
58 |
59 | @property
60 | def name(self):
61 | """Nice looking internal identifier."""
62 |
63 | return snake_case(
64 | self.__class__.__name__
65 | if hasattr(self, '__class__')
66 | else self.__name__
67 | )
68 |
69 |
70 | class MenuAction(SnakeNameMixin, metaclass=AbstractRegisteringType):
71 |
72 | def __init__(self, app):
73 | self.app = app
74 |
75 | @abstract_property
76 | def label(self):
77 | """Text to be shown on menu entry.
78 |
79 | Use ampersand ('&') to set that the following
80 | character as a menu shortcut for this action.
81 |
82 | Use double ampersand ('&&') to display '&'.
83 | """
84 | pass
85 |
86 | @property
87 | def checkable(self):
88 | """Add 'checked' sign to menu item when active"""
89 | return False
90 |
91 | @property
92 | def shortcut(self):
93 | """Global shortcut for this menu action.
94 |
95 | The shortcut should be given as a string, like:
96 | shortcut = 'Ctrl+n'
97 | """
98 | return None
99 |
100 | @abstractmethod
101 | def action(self):
102 | """Callback for menu entry clicking/selection"""
103 | pass
104 |
105 | @property
106 | def is_checked(self):
107 | """Should the menu item be checked (assuming that checkable is True)"""
108 | return bool(self.value)
109 |
110 |
111 | def singleton_creator(old_creator):
112 | def one_to_rule_them_all(cls, *args, **kwargs):
113 | if not cls.instance:
114 | cls.instance = old_creator(cls)
115 | return cls.instance
116 | return one_to_rule_them_all
117 |
118 |
119 | class SingletonMetaclass(AbstractRegisteringType):
120 |
121 | def __init__(cls, name, bases, attributes):
122 | super().__init__(name, bases, attributes)
123 |
124 | # singleton
125 | cls.instance = None
126 | old_creator = cls.__new__
127 | cls.__new__ = singleton_creator(old_creator)
128 |
129 |
130 | class RequiringMixin:
131 |
132 | require = set()
133 | dependencies = {}
134 |
135 | def __init__(self, app):
136 | for requirement in self.require:
137 | instance = requirement(app)
138 | key = instance.name
139 | self.dependencies[key] = instance
140 |
141 | def __getattr__(self, attr):
142 | if attr in self.dependencies:
143 | return self.dependencies[attr]
144 |
145 |
146 | class Setting(RequiringMixin, SnakeNameMixin, metaclass=SingletonMetaclass):
147 |
148 | def __init__(self, app):
149 | RequiringMixin.__init__(self, app)
150 | self.default_value = self.value
151 | self.app = app
152 |
153 | @abstract_property
154 | def value(self):
155 | """Default value of a setting"""
156 | pass
157 |
158 | def on_load(self):
159 | """Callback called after loading of initial value"""
160 | pass
161 |
162 | def on_save(self):
163 | pass
164 |
165 | def reset(self):
166 | if hasattr(self, 'default_value'):
167 | self.value = self.default_value
168 |
169 |
170 | def decorate_or_call(operator):
171 | def outer_decorator(method_or_value):
172 | if callable(method_or_value):
173 | method = method_or_value
174 |
175 | def decorated(*args, **kwargs):
176 | return operator(method(*args, **kwargs))
177 | return decorated
178 | else:
179 | return operator(method_or_value)
180 | return outer_decorator
181 |
182 |
183 | @decorate_or_call
184 | def style_tag(some_css):
185 | return ''
186 |
187 |
188 | @decorate_or_call
189 | def percent_escaped(text):
190 | return text.replace('%', '%%')
191 |
192 |
193 | class StylerMetaclass(AbstractRegisteringType):
194 | """
195 | Makes classes: singletons, work with:
196 | wraps,
197 | appends_in_night_mode,
198 | replaces_in_night_mode
199 | decorators
200 | """
201 |
202 | def __init__(cls, name, bases, attributes):
203 | super().__init__(name, bases, attributes)
204 |
205 | # singleton
206 | cls.instance = None
207 | old_creator = cls.__new__
208 | cls.__new__ = singleton_creator(old_creator)
209 |
210 | # additions and replacements
211 | cls.additions = {}
212 | cls.replacements = {}
213 |
214 | target = attributes.get('target', None)
215 |
216 | def callback_maker(wrapper):
217 | def raw_new(*args, **kwargs):
218 | return wrapper(cls.instance, *args, **kwargs)
219 | return raw_new
220 |
221 | for key, attr in attributes.items():
222 |
223 | if key == 'init':
224 | key = '__init__'
225 | if hasattr(attr, 'wraps'):
226 |
227 | if not target:
228 | raise Exception(f'Asked to wrap "{key}" but target of {name} not defined')
229 |
230 | original = getattr(target, key)
231 |
232 | if type(original) is MethodType:
233 | original = original.__func__
234 |
235 | new = wrap(original, callback_maker(attr), attr.position)
236 |
237 | # for classes, just add the new function, it will be bound later,
238 | # but instances need some more work: we need to bind!
239 | if not isclass(target):
240 | new = MethodType(new, target)
241 |
242 | cls.replacements[key] = new
243 |
244 | if hasattr(attr, 'appends_in_night_mode'):
245 | if not target:
246 | raise Exception(f'Asked to replace "{key}" but target of {name} not defined')
247 | cls.additions[key] = attr
248 | if hasattr(attr, 'replaces_in_night_mode'):
249 | if not target:
250 | raise Exception(f'Asked to replace "{key}" but target of {name} not defined')
251 | cls.replacements[key] = attr
252 |
253 | # TODO: invoke and cache css?
254 | if hasattr(attr, 'is_css'):
255 | pass
256 |
257 |
258 | def wraps(method=None, position='after'):
259 | """Decorator for methods extending Anki QT methods.
260 |
261 | Args:
262 | method: a function method to be wrapped
263 | position: after, before or around
264 | """
265 |
266 | if not method:
267 | def wraps_inner(func):
268 | return wraps(method=func, position=position)
269 | return wraps_inner
270 |
271 | method.wraps = True
272 | method.position = position
273 |
274 | return method
275 |
276 |
277 | class appends_in_night_mode(PropertyDescriptor):
278 | appends_in_night_mode = True
279 |
280 |
281 | class replaces_in_night_mode(PropertyDescriptor):
282 | replaces_in_night_mode = True
283 |
284 |
285 | def move_args_to_kwargs(original_function, args, kwargs):
286 | args = list(args)
287 |
288 | import inspect
289 |
290 | signature = inspect.signature(original_function)
291 | i = 0
292 | for name, parameter in signature.parameters.items():
293 | if i >= len(args):
294 | break
295 | if parameter.default is not inspect._empty:
296 | value = args.pop(i)
297 | kwargs[name] = value
298 | else:
299 | i += 1
300 | return args, kwargs
301 |
302 |
--------------------------------------------------------------------------------
/redesign/languages.py:
--------------------------------------------------------------------------------
1 | import gettext
2 | from os import path
3 |
4 | from anki.lang import getLang, _ as fallback_translation
5 |
6 | lang = getLang()
7 | this_dir = path.dirname(path.abspath(__file__))
8 | locale_dir = path.join(this_dir, 'locale')
9 | trans = gettext.translation('Anki-Night-Mode', locale_dir, languages=[lang], fallback=True)
10 | # See: http://www.loc.gov/standards/iso639-2/php/code_list.php for language codes
11 |
12 |
13 | def _(text):
14 | try:
15 | return trans.gettext(text)
16 | except Exception as e:
17 | print(e)
18 | return fallback_translation(text)
19 |
--------------------------------------------------------------------------------
/redesign/menu.py:
--------------------------------------------------------------------------------
1 | from PyQt5.QtGui import QKeySequence
2 | from PyQt5.QtWidgets import QAction, QMenu
3 |
4 | from aqt import mw
5 |
6 | from .languages import _
7 |
8 |
9 |
10 |
11 | def get_or_create_menu(attribute_name, label):
12 |
13 | if not hasattr(mw, attribute_name):
14 | menu = QMenu(_(label), mw)
15 | setattr(mw, attribute_name, menu)
16 |
17 | mw.form.menubar.insertMenu(
18 | mw.form.menuTools.menuAction(),
19 | menu
20 | )
21 | else:
22 | menu = getattr(mw, attribute_name)
23 | menu.setTitle(_(label))
24 |
25 | return menu
26 |
27 |
28 |
29 |
30 | class Menu:
31 |
32 | actions = {
33 | # action name => action
34 | }
35 |
36 | connections = {
37 | # action => callback
38 | }
39 |
40 | def __init__(self, app, menu_name, layout, attach_to=None):
41 | self.menu = QMenu(_(menu_name), mw)
42 |
43 | if attach_to:
44 | attach_to.addMenu(self.menu)
45 |
46 | layout = [
47 | entry(app) if hasattr(entry, 'action') else entry
48 | for entry in layout
49 | ]
50 |
51 | self.raw_actions = {
52 | entry.name: entry
53 | for entry in layout
54 | if hasattr(entry, 'action')
55 | }
56 |
57 | for action in self.raw_actions.values():
58 |
59 | self.create_action(
60 | action.name,
61 | _(action.label),
62 | action.action,
63 | checkable=action.checkable,
64 | shortcut=action.shortcut
65 | )
66 |
67 | self.setup_layout(layout)
68 | self.setup_connections()
69 |
70 | def create_action(self, name, text, callback, checkable=False, shortcut=None):
71 | action = QAction(_(text), mw, checkable=checkable)
72 |
73 | if shortcut:
74 | toggle = QKeySequence(shortcut)
75 | action.setShortcut(toggle)
76 |
77 | if name in self.actions:
78 | message = 'Action {0} already exists'.format(name)
79 | raise Exception(message)
80 |
81 | self.actions[name] = action
82 | self.connections[action] = callback
83 |
84 | def set_checked(self, name, value=True):
85 | self.actions[name].setChecked(value)
86 |
87 | def setup_layout(self, layout):
88 | for entry in layout:
89 | if entry == '-':
90 | self.menu.addSeparator()
91 | else:
92 | action = self.actions[entry.name]
93 | self.menu.addAction(action)
94 |
95 | def setup_connections(self):
96 | for menu_entry, connection in self.connections.items():
97 | self.connect(menu_entry, connection)
98 |
99 | def connect(self, action, callback):
100 | action.triggered.connect(callback)
101 |
102 | def update_checkboxes(self, settings):
103 | for name, setting in settings.items():
104 | if name in self.actions and self.raw_actions[name].checkable:
105 | self.set_checked(name, setting.is_checked)
106 |
--------------------------------------------------------------------------------
/redesign/meta.json:
--------------------------------------------------------------------------------
1 | {"name": "Redesign", "mod": 1580262316, "disabled": false}
2 |
--------------------------------------------------------------------------------
/redesign/mode.py:
--------------------------------------------------------------------------------
1 | from PyQt5.QtCore import Qt, pyqtSlot as slot, QTime
2 | from PyQt5.QtWidgets import QWidget, QLabel, QGridLayout, QHBoxLayout, QVBoxLayout, QTimeEdit
3 |
4 | from .gui import create_button, AddonDialog, iterate_widgets
5 |
6 |
7 |
8 |
9 | class TimeEdit(QWidget):
10 |
11 | def __init__(self, parent, initial_time, label, on_update=lambda x: x):
12 | """
13 |
14 | Args:
15 | parent: ColorMapWindow instance
16 | """
17 | QWidget.__init__(self, parent)
18 | self.parent = parent
19 | self.on_update = on_update
20 | self.label = QLabel(label)
21 | self.qt_time = QTime.fromString(initial_time)
22 | self.time_edit = QTimeEdit(self.qt_time)
23 | self.time_edit.timeChanged.connect(self.update)
24 | self.grid = QGridLayout()
25 | self.fill_layout()
26 | self.setLayout(self.grid)
27 |
28 | @property
29 | def time(self):
30 | return self.qt_time.toPyTime().strftime('%H:%M')
31 |
32 | def fill_layout(self):
33 | grid = self.grid
34 | grid.addWidget(self.label, 0, 0)
35 | grid.addWidget(self.time_edit, 1, 0)
36 |
37 | @slot()
38 | def update(self):
39 | self.qt_time = self.time_edit.time()
40 | self.on_update(self.time)
41 |
42 | def update_constraint(self, min_time, max_time):
43 | pass
44 |
45 |
46 |
47 |
48 | class ModeWindow(AddonDialog):
49 |
50 | def __init__(self, parent, settings, title='Manage Redesign', on_update=lambda x: x):
51 | super().__init__(self, parent, Qt.Window)
52 | self.on_update = on_update
53 | self.settings = settings
54 |
55 | self.init_ui(title)
56 |
57 | def init_ui(self, title):
58 | self.setWindowTitle(title)
59 |
60 | btn_close = create_button('Close', self.close)
61 |
62 | buttons = QHBoxLayout()
63 |
64 | buttons.addWidget(btn_close)
65 | buttons.setAlignment(Qt.AlignBottom)
66 |
67 | body = QVBoxLayout()
68 | body.setAlignment(Qt.AlignTop)
69 |
70 | header = QLabel(
71 | 'If you choose an automatic (scheduled) mode '
72 | 'the "ctrl+n" shortcut and menu checkbox for '
73 | 'quick toggle will switch between the manual '
74 | 'and automatic mode (when used for the first '
75 | 'time).'
76 | )
77 | header.setWordWrap(True)
78 |
79 | mode_switches = QHBoxLayout()
80 | mode_switches.addWidget(QLabel('Mode:'))
81 | self.manual = create_button('Manual', self.on_set_manual)
82 | self.auto = create_button('Automatic', self.on_set_automatic)
83 | mode_switches.addWidget(self.manual)
84 | mode_switches.addWidget(self.auto)
85 |
86 | time_controls = QHBoxLayout()
87 | time_controls.setAlignment(Qt.AlignTop)
88 |
89 | start_at = TimeEdit(self, self.settings['start_at'], 'From', self.start_update)
90 | end_at = TimeEdit(self, self.settings['end_at'], 'To', self.end_update)
91 | time_controls.addWidget(start_at)
92 | time_controls.addWidget(end_at)
93 |
94 | self.time_controls = time_controls
95 |
96 | self.set_mode(self.settings['mode'], False)
97 |
98 | body.addWidget(header)
99 | body.addStretch(1)
100 | body.addLayout(mode_switches)
101 | body.addLayout(time_controls)
102 | body.addStretch(1)
103 | body.addLayout(buttons)
104 | self.setLayout(body)
105 |
106 | self.setGeometry(300, 300, 470, 255)
107 | self.show()
108 |
109 | def start_update(self, time):
110 | self.set_time('start_at', time)
111 |
112 | def end_update(self, time):
113 | self.set_time('end_at', time)
114 |
115 | def set_time(self, which, time):
116 | self.settings[which] = time
117 | self.on_update()
118 |
119 | @slot()
120 | def on_set_manual(self):
121 | self.set_mode('manual')
122 |
123 | @slot()
124 | def on_set_automatic(self):
125 | self.set_mode('auto')
126 |
127 | def switch_buttons(self, auto):
128 | self.auto.setEnabled(not auto)
129 | self.manual.setEnabled(auto)
130 | self.auto.setChecked(auto)
131 | self.manual.setChecked(not auto)
132 |
133 | def set_mode(self, mode, run_callback=True):
134 | auto = mode == 'auto'
135 | self.settings['mode'] = mode
136 | # time controls are needed only in the 'auto' mode
137 | for widget in iterate_widgets(self.time_controls):
138 | widget.setEnabled(auto)
139 | self.switch_buttons(auto)
140 | if run_callback:
141 | self.on_update()
142 |
--------------------------------------------------------------------------------
/redesign/redesign.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Copyright: Developer Nick
3 | # License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html
4 |
5 | """
6 |
7 |
8 | Welcome to a brand new way of using Anki! This add-on redesigns Anki from the ground up and is based on the code from the Night Mode add-on (https://ankiweb.net/shared/info/1496166067). It was helpful in creating this redesign and I am very grateful to its author.
9 |
10 | Redesign adds a "View" option in the menu bar with options for switching it on and off and modifying select colors. After enabling the redesign, it changes the colors, animations, buttons, and main screens inside Anki.
11 |
12 |
13 |
14 | Acknowledgements:
15 | Special thanks to the Night Mode author, Krassowski, and the contributors to that code:
16 | - b50 (initial compatibility with 2.1),
17 | - ankitest (compatibility with 1508882486),
18 | - omega3 (useful bug reports and suggestions)
19 | - colchizin
20 | - JulyMorning
21 | - nathanmalloy
22 | - rathsky
23 |
24 | Patreon Contributors:
25 | (If you wish to have your name or username displayed here, please let me know.)
26 | - Basiskarten Jura (https://www.basiskarten.de/)
27 | - Others who wished to remain anonymous!
28 |
29 | Copyright: Developer Nick (nickdeveloper.feedback@gmail.com)
30 | License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html except when stated otherwise.
31 | """
32 |
33 |
34 |
35 |
36 | import traceback
37 |
38 | from anki.hooks import addHook, runHook
39 | from aqt import appVersion
40 | from aqt import mw
41 |
42 | from PyQt5.QtWidgets import QMessageBox
43 |
44 | from .actions_and_settings import *
45 | from .internals import alert
46 | from .config import Config, ConfigValueGetter
47 | from .css_class import inject_css_class
48 | from .icons import Icons
49 | from .menu import get_or_create_menu, Menu
50 | from .stylers import Styler
51 | from .styles import Style, MessageBoxStyle
52 |
53 | __addon_name__ = 'Redesign'
54 | __version__ = '1.2'
55 | __anki_version__ = '2.1'
56 |
57 |
58 |
59 |
60 | if not appVersion.startswith(__anki_version__):
61 | print(
62 | (
63 | 'Unsupported version of Anki. '
64 | 'Redesign requires %s to work properly. '
65 | 'Update to Anki 2.1 or higher to use this add-on.'
66 | ) % __anki_version__
67 | )
68 |
69 |
70 |
71 |
72 | # Add your color replacement mappings here - old: new, comma separated
73 |
74 |
75 |
76 |
77 | class StylingManager:
78 | def __init__(self, app):
79 | self.styles = Style.members
80 | self.stylers = [
81 | styler(app)
82 | for styler in Styler.members
83 | ]
84 | self.config = ConfigValueGetter(app.config)
85 |
86 | @property
87 | def active_stylers(self):
88 | return [
89 | styler
90 | for styler in self.stylers
91 | if styler.name not in self.config.disabled_stylers
92 | ]
93 |
94 | def replace(self):
95 | for styler in self.active_stylers:
96 | styler.replace_attributes()
97 |
98 | def restore(self):
99 | for styler in self.stylers:
100 | styler.restore_attributes()
101 |
102 |
103 |
104 |
105 | class Redesign:
106 |
107 | menu_layout = [
108 | EnableNightMode,
109 | EnableInDialogs,
110 | StyleScrollBars,
111 | '-',
112 | #PrimaryColor,
113 | BackgroundColor,
114 | CardColor,
115 | TextColor,
116 | LightColors,
117 | DarkColors,
118 | '-',
119 | #InvertImage,
120 | #InvertLatex,
121 | #TransparentLatex,
122 | '-',
123 | #ModeSettings,
124 | #UserColorMap,
125 | #DisabledStylers,
126 | '-',
127 | About
128 | ]
129 |
130 | def __init__(self):
131 | self.profile_loaded = False
132 | self.config = Config(self, prefix='nm_')
133 | self.config.init_settings()
134 | self.icons = Icons(mw)
135 | self.styles = StylingManager(self)
136 |
137 | view_menu = get_or_create_menu('addon_view_menu', '&View')
138 | self.menu = Menu(
139 | self,
140 | '&Redesign',
141 | self.menu_layout,
142 | attach_to=view_menu
143 | )
144 |
145 | addHook('unloadProfile', self.save)
146 | # Disabled, uses delay in __init__.py
147 | # addHook('profileLoaded', self.load)
148 | addHook('prepareQA', self.night_class_injection)
149 | addHook('loadNote', self.background_bug_workaround)
150 |
151 | def load(self):
152 | """
153 | Load configuration from profile, set states of checkable menu objects
154 | and turn on redesign if it were enabled on previous session.
155 | """
156 | self.config.load()
157 | self.profile_loaded = True
158 |
159 | self.refresh()
160 | self.update_menu()
161 |
162 | runHook("night_mode_config_loaded", self.config)
163 |
164 |
165 | def update_menu(self):
166 | self.menu.update_checkboxes(self.config.settings)
167 |
168 | def save(self):
169 | self.config.save()
170 |
171 | def on(self):
172 | """Turn on redesign."""
173 | self.styles.replace()
174 | runHook("night_mode_state_changed", True)
175 |
176 | def off(self):
177 | """Turn off redesign."""
178 | self.styles.restore()
179 | runHook("night_mode_state_changed", False)
180 |
181 | def refresh(self, reload=False):
182 | """
183 | Refresh display by re-enabling redesign or normal mode,
184 | regenerate customizable css strings.
185 | """
186 | state = self.config.state_on.value
187 |
188 | if not self.profile_loaded:
189 | alert(ERROR_NO_PROFILE)
190 | return
191 |
192 | try:
193 | if state:
194 | if reload:
195 | self.off()
196 | self.on()
197 | else:
198 | self.off()
199 | except Exception:
200 | alert(ERROR_SWITCH % traceback.format_exc())
201 | return
202 |
203 | # Reload current screen.
204 | if mw.state == 'review':
205 | mw.moveToState('overview')
206 | mw.moveToState('review')
207 | if mw.state == 'deckBrowser':
208 | mw.deckBrowser.refresh()
209 | if mw.state == 'overview':
210 | mw.overview.refresh()
211 |
212 | # Redraw toolbar (should be always visible).
213 | mw.toolbar.draw()
214 | self.update_menu()
215 | return True
216 |
217 | def about(self):
218 | about_box = self.message_box()
219 | about_box.setText(__addon_name__ + ' ' + __version__ + __doc__)
220 | about_box.setGeometry(300, 300, 250, 150)
221 | about_box.setWindowTitle('About \n' + __addon_name__ + ' ' + __version__)
222 |
223 | about_box.exec_()
224 |
225 | def message_box(self):
226 | box = QMessageBox()
227 | if self.config.state_on.value:
228 | box_style = MessageBoxStyle(self)
229 | box.setStyleSheet(box_style.style)
230 | return box
231 |
232 | def night_class_injection(self, html, card, context):
233 | html = inject_css_class(self.config.state_on.value, html)
234 | return html
235 |
236 | def background_bug_workaround(self, editor):
237 |
238 | if self.config.state_on.value:
239 | javascript = """
240 | (function bg_bug_workaround()
241 | {
242 | function getTextNodeAtPosition(root, index){
243 | // Copyright notice:
244 | //
245 | // following function is based on a function created by Pery Mimon:
246 | // https://stackoverflow.com/a/38479462
247 | // and is distributed under CC-BY SA 3.0 license terms:
248 | // https://creativecommons.org/licenses/by-sa/3.0/
249 |
250 | var lastNode = null;
251 | var lastIndex = null
252 |
253 | var treeWalker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT,function next(elem) {
254 | if(index >= elem.textContent.length){
255 | lastIndex = index
256 | index -= elem.textContent.length;
257 | lastNode = elem;
258 | return NodeFilter.FILTER_REJECT
259 | }
260 | return NodeFilter.FILTER_ACCEPT;
261 | });
262 | var c = treeWalker.nextNode();
263 | return {
264 | node: c ? c : lastNode,
265 | position: c ? index : lastIndex
266 | };
267 | }
268 |
269 | var regex = /<(span|strong) style="background-color: rgb\(255, 255, 255\);">(.*?)<\/(span|strong)>/gm
270 |
271 | function background_workaround_callback(raw_field)
272 | {
273 | function get_rid_of_background(){
274 | var field = $(raw_field)
275 | var html = field.html()
276 |
277 | if(html.search(regex) == -1)
278 | return
279 |
280 | var selection = window.getSelection()
281 | var range = selection.getRangeAt(0)
282 | range.setStart(raw_field, 0)
283 | var len = range.toString().length
284 |
285 | field.html(html.replace(regex, '<$1>$2$1>'))
286 |
287 | var range = new Range()
288 | var pos = getTextNodeAtPosition(raw_field, len)
289 |
290 | range.setStart(pos.node, pos.position)
291 |
292 | selection.removeAllRanges()
293 | selection.addRange(range)
294 | }
295 | return get_rid_of_background
296 | }
297 |
298 | var field = $('.field')
299 |
300 | field.on('keydown', function(e){
301 | var raw_field = this
302 | var get_rid_of_background = background_workaround_callback(raw_field)
303 |
304 | if(e.which === 8 || e.which == 46){
305 | window.setTimeout(get_rid_of_background, 0)
306 | }
307 | })
308 |
309 | field.on('paste', function(){
310 | var raw_field = this
311 | var get_rid_of_background = background_workaround_callback(raw_field)
312 |
313 | window.setTimeout(get_rid_of_background, 100)
314 | })
315 |
316 | })()
317 | """
318 | else:
319 | javascript = ''
320 |
321 | editor.web.eval(javascript)
322 |
323 |
324 | ERROR_NO_PROFILE = """Switching to redesign failed: The profile is not loaded yet.
325 | It could be a bug in Anki, or you may have tried to enable the redesign too quickly after starting Anki."""
326 |
327 | ERROR_SWITCH = """Switching to redesign failed: Something went wrong.
328 | This is NOT a problem with Anki, so you can try contacting the add-on author for help.
329 |
330 | Please provide following traceback when reporting the issue:
331 | %s
332 | """
333 |
--------------------------------------------------------------------------------
/redesign/selector.py:
--------------------------------------------------------------------------------
1 | from PyQt5.QtCore import Qt
2 | from PyQt5.QtWidgets import QLabel, QHBoxLayout, QVBoxLayout, QCheckBox
3 |
4 | from .gui import create_button, AddonDialog
5 | from .languages import _
6 |
7 |
8 | class StylerCheckButton(QCheckBox):
9 |
10 | def __init__(self, parent, styler):
11 | QCheckBox.__init__(self, _(styler.friendly_name), parent)
12 | self.styler = styler
13 | if styler.is_active:
14 | self.toggle()
15 | self.stateChanged.connect(self.switch_state)
16 | self.parent = parent
17 |
18 | def switch_state(self, state):
19 | self.parent.update(self.styler, state)
20 |
21 |
22 | class StylersSelectorWindow(AddonDialog):
23 |
24 | def __init__(self, parent, disabled_stylers: set, all_stylers, title=_('Choose what to style'), on_update=None):
25 | super().__init__(self, parent, Qt.Window)
26 | self.on_update = on_update
27 | self.disabled_stylers = disabled_stylers
28 | self.all_stylers = all_stylers
29 |
30 | self.stylers_checkboxes = []
31 | self.stylers_layout = None
32 | self.init_ui(title)
33 |
34 | def init_ui(self, title):
35 | self.setWindowTitle(title)
36 |
37 | btn_close = create_button('Close', self.close)
38 |
39 | buttons = QHBoxLayout()
40 |
41 | buttons.addWidget(btn_close)
42 | buttons.setAlignment(Qt.AlignBottom)
43 |
44 | body = QVBoxLayout()
45 | body.setAlignment(Qt.AlignTop)
46 |
47 | header = QLabel(_(
48 | 'Select which parts of Anki should be displayed '
49 | 'in eye-friendly, dark colors.\n\n'
50 | 'To disable all dialog windows, '
51 | 'use the "Enable in dialogs" switch which is available in menu.'
52 | ))
53 | header.setAlignment(Qt.AlignCenter)
54 |
55 | stylers = QVBoxLayout()
56 | stylers.setAlignment(Qt.AlignTop)
57 |
58 | for styler in sorted(self.all_stylers, key=lambda s: s.name):
59 | styler_checkbox = StylerCheckButton(self, styler)
60 | self.stylers_checkboxes.append(styler_checkbox)
61 | stylers.addWidget(styler_checkbox)
62 |
63 | self.stylers_layout = stylers
64 |
65 | checked_boxes = sum(1 for checkbox in self.stylers_checkboxes if checkbox.isChecked())
66 | check_all = QCheckBox(_('Check/uncheck all'), self)
67 | check_all.setChecked(checked_boxes > len(self.stylers_checkboxes) / 2)
68 | check_all.stateChanged.connect(self.check_uncheck_all)
69 |
70 | body.addWidget(header)
71 | body.addWidget(check_all)
72 | body.addLayout(stylers)
73 | body.addStretch(1)
74 | body.addLayout(buttons)
75 | self.setLayout(body)
76 |
77 | self.setGeometry(300, 300, 350, 300)
78 | self.show()
79 |
80 | def check_uncheck_all(self, state):
81 | for checkbox in self.stylers_checkboxes:
82 | checkbox.setChecked(state)
83 |
84 | def update(self, styler, value):
85 | if value:
86 | self.disabled_stylers.remove(styler.name)
87 | else:
88 | self.disabled_stylers.add(styler.name)
89 |
90 | if self.on_update:
91 | self.on_update()
92 |
--------------------------------------------------------------------------------
/redesign/stylers.py:
--------------------------------------------------------------------------------
1 | from PyQt5.QtCore import Qt
2 | from PyQt5 import QtWidgets
3 |
4 | import aqt
5 | from anki.stats import CollectionStats
6 | from aqt import mw, editor, QPixmap
7 | from aqt.addcards import AddCards
8 | from aqt.browser import Browser
9 | from aqt.clayout import CardLayout
10 | from aqt.editcurrent import EditCurrent
11 | from aqt.editor import Editor
12 | from aqt.progress import ProgressManager
13 | from aqt.stats import DeckStats
14 | from .gui import AddonDialog, iterate_widgets
15 |
16 | from .config import ConfigValueGetter
17 | from .css_class import inject_css_class
18 | from .internals import percent_escaped, move_args_to_kwargs, from_utf8, PropertyDescriptor
19 | from .internals import style_tag, wraps, appends_in_night_mode, replaces_in_night_mode, css
20 | from .styles import SharedStyles, ButtonsStyle, ImageStyle, DeckStyle, LatexStyle, DialogStyle
21 | from .internals import SnakeNameMixin, StylerMetaclass, abstract_property
22 | from .internals import RequiringMixin
23 |
24 |
25 |
26 |
27 | customFont = "default"; # Set your desired font here.
28 | # Format is "Font Name With Spaces" (i.e. customFont="Times New Roman";)
29 | # Note: If you want to change this, you need to do so in two files: styles.py and stylers.py
30 |
31 |
32 |
33 |
34 | class Styler(RequiringMixin, SnakeNameMixin, metaclass=StylerMetaclass):
35 |
36 | def __init__(self, app):
37 | RequiringMixin.__init__(self, app)
38 | self.app = app
39 | self.config = ConfigValueGetter(app.config)
40 | self.original_attributes = {}
41 |
42 | @abstract_property
43 | def target(self):
44 | return None
45 |
46 | @property
47 | def is_active(self):
48 | return self.name not in self.config.disabled_stylers
49 |
50 | @property
51 | def friendly_name(self):
52 | name = self.name.replace('_styler', '')
53 | return name.replace('_', ' ').title()
54 |
55 | def get_or_create_original(self, key):
56 | if key not in self.original_attributes:
57 | original = getattr(self.target, key)
58 | self.original_attributes[key] = original
59 | else:
60 | original = self.original_attributes[key]
61 |
62 | return original
63 |
64 | def replace_attributes(self):
65 | try:
66 | for key, addition in self.additions.items():
67 | original = self.get_or_create_original(key)
68 | setattr(self.target, key, original + addition.value(self))
69 |
70 | for key, replacement in self.replacements.items():
71 | self.get_or_create_original(key)
72 |
73 | if isinstance(replacement, PropertyDescriptor):
74 | replacement = replacement.value(self)
75 |
76 | setattr(self.target, key, replacement)
77 |
78 | except (AttributeError, TypeError):
79 | print('Failed to inject style to:', self.target, key, self.name)
80 | raise
81 |
82 | def restore_attributes(self):
83 | for key, original in self.original_attributes.items():
84 | setattr(self.target, key, original)
85 |
86 |
87 |
88 |
89 | class ToolbarStyler(Styler):
90 |
91 | target = mw.toolbar
92 | require = {
93 | SharedStyles
94 | }
95 |
96 | @appends_in_night_mode
97 | @style_tag
98 | @percent_escaped
99 | def _body(self):
100 | return self.shared.top
101 |
102 |
103 |
104 |
105 | class StyleSetter:
106 |
107 | def __init__(self, target):
108 | self.target = target
109 |
110 | @property
111 | def css(self):
112 | return self.target.styleSheet()
113 |
114 | @css.setter
115 | def css(self, value):
116 | self.target.setStyleSheet(value)
117 |
118 |
119 |
120 |
121 | class MenuStyler(Styler):
122 | target = StyleSetter(mw)
123 |
124 | # Note: this line is commented out for light mode. It affects the menus that pop up like after clicking the gear icon next to each deck on the main screen.
125 | # @appends_in_night_mode
126 | def css(self):
127 | return self.shared.menu
128 |
129 |
130 |
131 |
132 | class ReviewerStyler(Styler):
133 |
134 | target = mw.reviewer
135 | require = {
136 | SharedStyles,
137 | ButtonsStyle
138 | }
139 |
140 | @wraps(position='around')
141 | def _bottomHTML(self, reviewer, _old):
142 | return _old(reviewer) + style_tag(percent_escaped(self.bottom_css))
143 |
144 |
145 |
146 | @property
147 | def bottom_css(self):
148 | return self.buttons.html + self.shared.colors_replacer + """
149 |
150 | /* Note: This is the reviewer screen, bottom bar, background color */
151 | body, #outer{
152 | background-color:""" + self.config.color_b + """;
153 | border-top-color:""" + self.config.color_b + """;
154 | margin: 0 95px 0 95px;
155 | }
156 |
157 | /* Note: This is the reviewer screen, bottom bar, text color of the plus signs */
158 | .stattxt{
159 | color:#bdbdbd;
160 | }
161 |
162 | /* Note: This is the reviewer screen, bottom bar, text color of text (time until next review) located above Again, Hard, Easy, etc. buttons */
163 | .nobold{
164 | color:#888;
165 | font-family:%s;
166 | }
167 | """ % (customFont)
168 |
169 |
170 |
171 |
172 |
173 | class ReviewerCards(Styler):
174 |
175 | target = mw.reviewer
176 | require = {
177 | LatexStyle,
178 | ImageStyle
179 | }
180 |
181 | # TODO: it can be implemented with a nice decorator
182 | @wraps(position='around')
183 | def revHtml(self, reviewer, _old):
184 | return _old(reviewer) + style_tag(percent_escaped(self.body))
185 |
186 | @css
187 | def body(self):
188 | # Invert images and latex if needed
189 |
190 | css_body = """
191 | .card input
192 | {
193 | background-color:black!important;
194 | border-color:#444!important;
195 | color:"""+ self.config.color_c +"""!important
196 | }
197 | .card input::selection{
198 | color: """ + self.config.color_t + """;
199 | background: #0864d4
200 | }
201 | .typeGood{
202 | color:black;
203 | background:#57a957
204 | }
205 | .typeBad{
206 | color:black;
207 | background:#c43c35
208 | }
209 | .typeMissed{
210 | color:black;
211 | background:#ccc
212 | }
213 | #answer{
214 | height:0;
215 | border:0;
216 | border-bottom: 2px solid #333;
217 | border-top: 2px solid black
218 | }
219 | img#star{
220 | -webkit-filter:invert(0%)!important
221 | }
222 |
223 | # This is the answer text color for cloze cards.
224 | # Good red color: #ef5350
225 | .cloze{
226 | color:"""+ self.config.color_p +"""!important
227 | }
228 |
229 | a{
230 | color:#0099CC
231 | }
232 | """
233 |
234 | card_color = """
235 | .card{
236 | color:""" + self.config.color_t + """!important;
237 | }
238 | """
239 |
240 | css = css_body + card_color + self.shared.user_color_map + self.shared.body_colors
241 |
242 | if self.config.invert_image:
243 | css += self.image.invert
244 | if self.config.invert_latex:
245 | css += self.latex.invert
246 |
247 | return css
248 |
249 |
250 |
251 |
252 | class DeckBrowserStyler(Styler):
253 |
254 | target = mw.deckBrowser
255 | require = {
256 | SharedStyles,
257 | DeckStyle
258 | }
259 |
260 | @appends_in_night_mode
261 | def _body(self):
262 | styles_html = style_tag(percent_escaped(self.deck.style + self.shared.body_colors))
263 | return inject_css_class(True, styles_html)
264 |
265 |
266 |
267 |
268 | class DeckBrowserBottomStyler(Styler):
269 |
270 | target = mw.deckBrowser.bottom
271 | require = {
272 | DeckStyle
273 | }
274 |
275 | @appends_in_night_mode
276 | def _centerBody(self):
277 | styles_html = style_tag(percent_escaped(self.deck.bottom))
278 | return inject_css_class(True, styles_html)
279 |
280 |
281 |
282 |
283 | class OverviewStyler(Styler):
284 |
285 | target = mw.overview
286 | require = {
287 | SharedStyles,
288 | ButtonsStyle
289 | }
290 |
291 | @appends_in_night_mode
292 | def _body(self):
293 | styles_html = style_tag(percent_escaped(self.css))
294 | return inject_css_class(True, styles_html)
295 |
296 | @css
297 | def css(self):
298 | return f"""
299 | {self.buttons.html}
300 | {self.shared.colors_replacer}
301 | {self.shared.body_colors}
302 | .descfont
303 | {{
304 | color: {self.config.color_t}
305 | }}
306 | """
307 |
308 |
309 |
310 |
311 | class OverviewBottomStyler(Styler):
312 |
313 | target = mw.overview.bottom
314 | require = {
315 | DeckStyle
316 | }
317 |
318 | @appends_in_night_mode
319 | @style_tag
320 | @percent_escaped
321 | def _centerBody(self):
322 | return self.deck.bottom
323 |
324 |
325 |
326 |
327 | class AnkiWebViewStyler(Styler):
328 |
329 | target = mw.web
330 | require = {
331 | SharedStyles,
332 | ButtonsStyle
333 | }
334 |
335 | @wraps(position='around')
336 | def stdHtml(self, web, *args, **kwargs):
337 | old = kwargs.pop('_old')
338 |
339 | args, kwargs = move_args_to_kwargs(old, [web] + list(args), kwargs)
340 |
341 | kwargs['head'] = kwargs.get('head', '') + style_tag(self.waiting_screen)
342 |
343 | return old(web, *args[1:], **kwargs)
344 |
345 | @css
346 | def waiting_screen(self):
347 | return self.buttons.html + self.shared.body_colors
348 |
349 |
350 |
351 |
352 | class BrowserStyler(Styler):
353 |
354 | target = Browser
355 | require = {
356 | SharedStyles,
357 | ButtonsStyle,
358 | }
359 |
360 | @wraps
361 | def init(self, browser, mw):
362 |
363 | if self.config.enable_in_dialogs:
364 |
365 | basic_css = browser.styleSheet()
366 | global_style = '#' + browser.form.centralwidget.objectName() + '{' + self.shared.colors + '}'
367 | browser.setStyleSheet(self.shared.menu + self.style + basic_css + global_style)
368 |
369 | browser.form.tableView.setStyleSheet(self.table)
370 | browser.form.tableView.horizontalHeader().setStyleSheet(self.table_header)
371 |
372 | browser.form.searchEdit.setStyleSheet(self.search_box)
373 | browser.form.searchEdit.setSizeAdjustPolicy(QtWidgets.QComboBox.SizeAdjustPolicy.AdjustToMinimumContentsLength)
374 |
375 | browser.form.searchButton.setStyleSheet(self.buttons.qt)
376 | browser.form.previewButton.setStyleSheet(self.buttons.qt)
377 |
378 | # TODO: test this
379 | #@wraps
380 | def _renderPreview(self, browser, cardChanged=False):
381 | if browser._previewWindow:
382 | self.app.take_care_of_night_class(web_object=browser._previewWeb)
383 |
384 | @wraps(position='around')
385 | def buildTree(self, browser, _old):
386 | root = _old(browser)
387 | if root: # For Anki 2.1.17++
388 | return root
389 | # ---------------------------
390 | # For Anki 2.1.15--
391 | root = browser.sidebarTree
392 | for item in root.findItems('', Qt.MatchContains | Qt.MatchRecursive):
393 | icon = item.icon(0)
394 | pixmap = icon.pixmap(32, 32)
395 | image = pixmap.toImage()
396 | # image.invertPixels()
397 | new_icon = aqt.QIcon(QPixmap.fromImage(image))
398 | item.setIcon(0, new_icon)
399 |
400 | @wraps
401 | def setupSidebar(self, browser):
402 | browser.sidebarTree.setStyleSheet(self.style)
403 |
404 |
405 | @wraps(position='around')
406 | def _cardInfoData(self, browser, _old):
407 |
408 | rep, cs = _old(browser)
409 |
410 | if self.config.enable_in_dialogs:
411 | rep += style_tag("""
412 | *
413 | {
414 | """ + self.shared.colors + """
415 | }
416 | div
417 | {
418 | border-color:#ff0000!important
419 | }
420 | """ + self.shared.colors_replacer + """
421 | """)
422 | return rep, cs
423 |
424 |
425 | # Sidebar of Browse dialog
426 | @css
427 | def style(self):
428 | return """
429 |
430 | QSplitter::handle
431 | {
432 | /* handled below as QWidget */
433 | }
434 | #""" + from_utf8("widget") + """,
435 |
436 | /* SIDEBAR SPECIFICALLY */
437 | QTreeView
438 | {
439 | margin:20px 0 20px 20px;
440 | border: 0px solid #bdbdbd;
441 | border-right: 1px solid #bdbdbd;
442 | min-width:160px;
443 | font-family:"""+ customFont +""";
444 | """ + self.shared.colors + """
445 | }
446 | QTreeView::item:selected:active, QTreeView::branch:selected:active
447 | {
448 | color:#fff;
449 | background-color:"""+ self.config.color_p +""";
450 | }
451 | /* SELECTED BUT NOT ACTIVE, SO TEXT IS FADED; HAPPENS WHEN YOU SELECT ITEM THEN CLICK AWAY */
452 | QTreeView::item:selected:!active, QTreeView::branch:selected:!active
453 | {
454 | color: rgba(255, 255, 255, 0.8);
455 | background-color:"""+ self.config.color_p +""";
456 | }
457 | """ + (
458 | """
459 | /* make the splitter light-dark (match all widgets as selecting with QSplitter does not work) */
460 | QWidget{
461 | background-color:"""+ self.config.color_b +""";
462 | color: """ + self.config.color_t + """;
463 | }
464 | /* make sure that no other important widgets - like tags box - are light-dark */
465 | QGroupBox{
466 | background-color: """ + self.config.color_b + """;
467 | }
468 | """
469 | if self.config.style_scroll_bars else
470 | ''
471 | )
472 |
473 |
474 |
475 |
476 | # Browse styles:
477 | # 1st (QTableView) is table of cards that can be selected.
478 | # 2nd (QHeaderView) is the header of the table with each column header.
479 | @css
480 | def table(self):
481 | return f"""
482 | QTableView
483 | {{
484 | margin:10px 10px 20px 10px;
485 | border-radius:10px;
486 | border:1px solid #bdbdbd;
487 | selection-color:#fff;
488 | alternate-background-color:#f8f8f8;
489 | gridline-color:{self.config.color_c};
490 | {self.shared.colors};
491 | selection-background-color:{self.config.color_p};
492 | font-family:%s;
493 | }}
494 | """ % (customFont)
495 |
496 | # Background of header behind header text background = QHeaderView; the "color:" is for the currently sorted header's arrow
497 | # Header text and its background = QHeaderView::section
498 | @css
499 | def table_header(self):
500 | return """
501 | QHeaderView
502 | {
503 | background-color:"""+ self.config.color_b +""";
504 | border-radius:15px 15px 0px 0px;
505 | color:"""+ self.config.color_p +""";
506 | }
507 |
508 | QHeaderView::section
509 | {
510 | """ + self.shared.colors + """
511 | height:32px;
512 | background-color:"""+ self.config.color_b +""";
513 | border-radius:15px;
514 | font-family:%s;
515 | font-size:14px;
516 | color:#888;
517 | }
518 | """ % (customFont)
519 |
520 |
521 |
522 |
523 | # Search bar = QComboBox
524 | # Search icon = QComboBox::down-arrow
525 | @css
526 | def search_box(self):
527 | return """
528 |
529 | QComboBox
530 | {
531 | margin:10px 0px 10px 0px;
532 | border:0px solid #bdbdbd;
533 | font-size:14px;
534 | font-family:"""+ customFont +""";
535 | border-radius:20px;
536 | padding:10px 10px 10px 10px;
537 | """ + self.shared.colors + """
538 | }
539 |
540 | QComboBox:!editable
541 | {
542 | background:"""+ self.config.color_c +""";
543 | }
544 |
545 | QComboBox QAbstractItemView
546 | {
547 | border:0px solid #bdbdbd;
548 | border-radius:10px 10px 10px 10px;
549 | """ + self.shared.colors + """
550 | background:"""+ self.config.color_c +""";
551 | }
552 |
553 | QComboBox::drop-down, QComboBox::drop-down:editable
554 | {
555 | """ + self.shared.colors + """;
556 | margin-right:20px;
557 | background:"""+ self.config.color_b +""";
558 | border:0px solid """+ self.config.color_b +""";
559 | padding:10px 10px 10px 10px;
560 | }
561 |
562 | QComboBox::down-arrow
563 | {
564 | width:15px;
565 | height:15px;
566 | image: url('""" + self.app.icons.arrow + """')
567 | }
568 | """
569 |
570 |
571 |
572 |
573 | # Allows styling of sidebar in Browse dialog (necessary for Anki 2.1.17 and beyond)
574 | try:
575 | from aqt.browser import SidebarModel
576 |
577 | class SidebarModelStyler(Styler):
578 |
579 | target = SidebarModel
580 |
581 | @wraps(position='around')
582 | def iconFromRef(self, sidebar_model, iconRef, _old):
583 | icon = _old(sidebar_model, iconRef)
584 | if icon:
585 | pixmap = icon.pixmap(32, 32)
586 | image = pixmap.toImage()
587 | image.invertPixels()
588 | new_icon = aqt.QIcon(QPixmap.fromImage(image))
589 | return new_icon
590 | return icon
591 | except ImportError:
592 | pass
593 |
594 |
595 |
596 |
597 | class AddCardsStyler(Styler):
598 |
599 | target = AddCards
600 | require = {
601 | SharedStyles,
602 | ButtonsStyle,
603 | }
604 |
605 | @wraps
606 | def init(self, add_cards, mw):
607 | if self.config.enable_in_dialogs:
608 |
609 | # style add/history button
610 | add_cards.form.buttonBox.setStyleSheet(self.buttons.qt)
611 |
612 | self.set_style_to_objects_inside(add_cards.form.horizontalLayout, self.buttons.qt)
613 |
614 | # style the single line which has some bright color
615 | add_cards.form.line.setStyleSheet('#' + from_utf8('line') + '{border: 0px solid #333}')
616 |
617 | add_cards.form.fieldsArea.setAutoFillBackground(False)
618 |
619 | @staticmethod
620 | def set_style_to_objects_inside(layout, style):
621 | for widget in iterate_widgets(layout):
622 | widget.setStyleSheet(style)
623 |
624 |
625 | class EditCurrentStyler(Styler):
626 |
627 | target = EditCurrent
628 | require = {
629 | ButtonsStyle,
630 | }
631 |
632 | @wraps
633 | def init(self, edit_current, mw):
634 | if self.config.enable_in_dialogs:
635 | # style close button
636 | edit_current.form.buttonBox.setStyleSheet(self.buttons.qt)
637 |
638 |
639 | class ProgressStyler(Styler):
640 |
641 | target = None
642 | require = {
643 | SharedStyles,
644 | DialogStyle,
645 | ButtonsStyle
646 | }
647 |
648 | def init(self, progress, *args, **kwargs):
649 | if self.config.enable_in_dialogs:
650 | progress.setStyleSheet(self.buttons.qt + self.dialog.style)
651 |
652 |
653 | if hasattr(ProgressManager, 'ProgressNoCancel'):
654 | # before beta 31
655 | class LegacyProgressStyler(Styler):
656 |
657 | target = None
658 | require = {
659 | SharedStyles,
660 | DialogStyle,
661 | ButtonsStyle
662 | }
663 |
664 | def init(self, progress, label='', *args, **kwargs):
665 | if self.config.enable_in_dialogs:
666 | # Set label and its styles explicitly (otherwise styling does not work)
667 | label = aqt.QLabel(label)
668 | progress.setLabel(label)
669 | label.setAlignment(Qt.AlignCenter)
670 | label.setStyleSheet(self.dialog.style)
671 |
672 | progress.setStyleSheet(self.buttons.qt + self.dialog.style)
673 |
674 | class ProgressNoCancel(Styler):
675 |
676 | target = ProgressManager.ProgressNoCancel
677 | require = {LegacyProgressStyler}
678 |
679 | # so this bit is required to enable init wrapping of Qt objects
680 | def init(cls, label='', *args, **kwargs):
681 | aqt.QProgressDialog.__init__(cls, label, *args, **kwargs)
682 |
683 | target.__init__ = init
684 |
685 | @wraps
686 | def init(self, progress, *args, **kwargs):
687 | self.legacy_progress_styler.init(progress, *args, **kwargs)
688 |
689 |
690 | class ProgressCancelable(Styler):
691 |
692 | target = ProgressManager.ProgressCancellable
693 | require = {LegacyProgressStyler}
694 |
695 | @wraps
696 | def init(self, progress, *args, **kwargs):
697 | self.legacy_progress_styler.init(progress, *args, **kwargs)
698 |
699 | else:
700 | # beta 31 or newer
701 |
702 | class ProgressDialog(Styler):
703 |
704 | target = ProgressManager.ProgressDialog
705 | require = {ProgressStyler}
706 |
707 | @wraps
708 | def init(self, progress, *args, **kwargs):
709 | self.progress_styler.init(progress, *args, **kwargs)
710 |
711 |
712 | class StatsWindowStyler(Styler):
713 |
714 | target = DeckStats
715 |
716 | require = {
717 | DialogStyle,
718 | ButtonsStyle
719 | }
720 |
721 | @wraps
722 | def init(self, stats, *args, **kwargs):
723 | if self.config.enable_in_dialogs:
724 | stats.setStyleSheet(self.buttons.qt + self.dialog.style)
725 |
726 |
727 | class StatsReportStyler(Styler):
728 |
729 | target = CollectionStats
730 |
731 | require = {
732 | SharedStyles,
733 | DialogStyle
734 | }
735 |
736 | @appends_in_night_mode
737 | @style_tag
738 | @percent_escaped
739 | def css(self):
740 | return (
741 | self.shared.user_color_map + self.shared.body_colors + """
742 | body{background-image: none}
743 | """
744 | )
745 |
746 |
747 | class EditorStyler(Styler):
748 |
749 | target = Editor
750 |
751 | require = {
752 | SharedStyles,
753 | DialogStyle,
754 | ButtonsStyle
755 | }
756 |
757 | # TODO: this would make more sense if we add some styling to .editor-btn
758 | def _addButton(self, editor, icon, command, *args, **kwargs):
759 | original_function = kwargs.pop('_old')
760 | button = original_function(editor, icon, command, *args, **kwargs)
761 | return button.replace('