diff --git a/qt/aqt/browser/browser.py b/qt/aqt/browser/browser.py index 95eddfd2a..cbc600762 100644 --- a/qt/aqt/browser/browser.py +++ b/qt/aqt/browser/browser.py @@ -42,6 +42,7 @@ from aqt.utils import ( current_top_level_widget, ensure_editor_saved, getTag, + load_flags, no_arg_trigger, openHelp, qtMenuShortcutWorkaround, @@ -136,7 +137,6 @@ class Browser(QMainWindow): self.sidebar.refresh_if_needed() def setupMenus(self) -> None: - # pylint: disable=unnecessary-lambda # actions f = self.form # edit @@ -166,16 +166,15 @@ class Browser(QMainWindow): qconnect(f.action_set_due_date.triggered, self.set_due_date) qconnect(f.action_forget.triggered, self.forget_cards) qconnect(f.actionToggle_Suspend.triggered, self.suspend_selected_cards) - qconnect(f.actionRed_Flag.triggered, lambda: self.set_flag_of_selected_cards(1)) - qconnect( - f.actionOrange_Flag.triggered, lambda: self.set_flag_of_selected_cards(2) - ) - qconnect( - f.actionGreen_Flag.triggered, lambda: self.set_flag_of_selected_cards(3) - ) - qconnect( - f.actionBlue_Flag.triggered, lambda: self.set_flag_of_selected_cards(4) - ) + + def set_flag_func(desired_flag: int) -> Callable: + return lambda: self.set_flag_of_selected_cards(desired_flag) + + for flag in load_flags(self.col): + qconnect( + getattr(self.form, flag.action).triggered, set_flag_func(flag.index) + ) + self._update_flag_labels() qconnect(f.actionExport.triggered, self._on_export_notes) # jumps qconnect(f.actionPreviousCard.triggered, self.onPreviousCard) @@ -711,18 +710,15 @@ where id in %s""" flag = self.card and self.card.user_flag() flag = flag or 0 - flagActions = [ - self.form.actionRed_Flag, - self.form.actionOrange_Flag, - self.form.actionGreen_Flag, - self.form.actionBlue_Flag, - ] - - for c, act in enumerate(flagActions): - act.setChecked(flag == c + 1) + for f in load_flags(self.col): + getattr(self.form, f.action).setChecked(flag == f.index) qtMenuShortcutWorkaround(self.form.menuFlag) + def _update_flag_labels(self) -> None: + for flag in load_flags(self.col): + getattr(self.form, flag.action).setText(flag.label) + def toggle_mark_of_selected_notes(self, checked: bool) -> None: if checked: self.add_tags_to_selected_notes(tags=MARKED_TAG) diff --git a/qt/aqt/browser/sidebar/item.py b/qt/aqt/browser/sidebar/item.py index 4c38eece2..d38e8a7c4 100644 --- a/qt/aqt/browser/sidebar/item.py +++ b/qt/aqt/browser/sidebar/item.py @@ -40,6 +40,7 @@ class SidebarItemType(Enum): def is_editable(self) -> bool: return self in ( + SidebarItemType.FLAG, SidebarItemType.SAVED_SEARCH, SidebarItemType.DECK, SidebarItemType.TAG, diff --git a/qt/aqt/browser/sidebar/tree.py b/qt/aqt/browser/sidebar/tree.py index 407faadef..a20403d2e 100644 --- a/qt/aqt/browser/sidebar/tree.py +++ b/qt/aqt/browser/sidebar/tree.py @@ -35,7 +35,14 @@ from aqt.operations.tag import ( ) from aqt.qt import * from aqt.theme import ColoredIcon, theme_manager -from aqt.utils import KeyboardModifiersPressed, askUser, getOnlyText, showWarning, tr +from aqt.utils import ( + KeyboardModifiersPressed, + askUser, + getOnlyText, + load_flags, + showWarning, + tr, +) class SidebarStage(Enum): @@ -361,6 +368,8 @@ class SidebarTreeView(QTreeView): self.rename_saved_search(item, new_name) elif item.item_type == SidebarItemType.TAG: self.rename_tag(item, new_name) + elif item.item_type == SidebarItemType.FLAG: + self.rename_flag(item, new_name) # renaming may be asynchronous so always return False return False @@ -600,35 +609,21 @@ class SidebarTreeView(QTreeView): ) root.search_node = SearchNode(flag=SearchNode.FLAG_ANY) - type = SidebarItemType.FLAG - root.add_simple( - tr.actions_red_flag(), - icon=icon.with_color(colors.FLAG1_FG), - type=type, - search_node=SearchNode(flag=SearchNode.FLAG_RED), - ) - root.add_simple( - tr.actions_orange_flag(), - icon=icon.with_color(colors.FLAG2_FG), - type=type, - search_node=SearchNode(flag=SearchNode.FLAG_ORANGE), - ) - root.add_simple( - tr.actions_green_flag(), - icon=icon.with_color(colors.FLAG3_FG), - type=type, - search_node=SearchNode(flag=SearchNode.FLAG_GREEN), - ) - root.add_simple( - tr.actions_blue_flag(), - icon=icon.with_color(colors.FLAG4_FG), - type=type, - search_node=SearchNode(flag=SearchNode.FLAG_BLUE), - ) + for flag in load_flags(self.col): + root.add_child( + SidebarItem( + name=flag.label, + icon=flag.icon, + search_node=flag.search_node, + item_type=SidebarItemType.FLAG, + id=flag.index, + ) + ) + root.add_simple( tr.browsing_no_flag(), - icon=icon.with_color(colors.DISABLED), - type=type, + icon=icon, + type=SidebarItemType.FLAG, search_node=SearchNode(flag=SearchNode.FLAG_NONE), ) @@ -872,6 +867,17 @@ class SidebarTreeView(QTreeView): lambda: set_children_expanded(False), ) + # Flags + ########################### + + def rename_flag(self, item: SidebarItem, new_name: str) -> None: + labels = self.col.get_config("flagLabels", {}) + labels[str(item.id)] = new_name + self.col.set_config("flagLabels", labels) + item.name = new_name + self.browser._update_flag_labels() + self.refresh() + # Decks ########################### diff --git a/qt/aqt/reviewer.py b/qt/aqt/reviewer.py index 4c99ff8a9..4fddcd38e 100644 --- a/qt/aqt/reviewer.py +++ b/qt/aqt/reviewer.py @@ -51,7 +51,14 @@ from aqt.qt import * from aqt.sound import av_player, play_clicked_audio, record_audio from aqt.theme import theme_manager from aqt.toolbar import BottomBar -from aqt.utils import askUserDialog, downArrow, qtMenuShortcutWorkaround, tooltip, tr +from aqt.utils import ( + askUserDialog, + downArrow, + load_flags, + qtMenuShortcutWorkaround, + tooltip, + tr, +) from aqt.webview import AnkiWebView @@ -443,7 +450,7 @@ class Reviewer: def _shortcutKeys( self, - ) -> List[Union[Tuple[str, Callable], Tuple[Qt.Key, Callable]]]: + ) -> Sequence[Union[Tuple[str, Callable], Tuple[Qt.Key, Callable]]]: return [ ("e", self.mw.onEditCurrent), (" ", self.onEnterKey), @@ -452,10 +459,10 @@ class Reviewer: ("m", self.showContextMenu), ("r", self.replayAudio), (Qt.Key_F5, self.replayAudio), - ("Ctrl+1", lambda: self.set_flag_on_current_card(1)), - ("Ctrl+2", lambda: self.set_flag_on_current_card(2)), - ("Ctrl+3", lambda: self.set_flag_on_current_card(3)), - ("Ctrl+4", lambda: self.set_flag_on_current_card(4)), + *( + (f"Ctrl+{flag.index}", self.set_flag_func(flag.index)) + for flag in load_flags(self.mw.col) + ), ("*", self.toggle_mark_on_current_note), ("=", self.bury_current_note), ("-", self.bury_current_card), @@ -905,29 +912,12 @@ time = %(time)d; tr.studying_flag_card(), [ [ - tr.actions_red_flag(), - "Ctrl+1", - lambda: self.set_flag_on_current_card(1), - dict(checked=currentFlag == 1), - ], - [ - tr.actions_orange_flag(), - "Ctrl+2", - lambda: self.set_flag_on_current_card(2), - dict(checked=currentFlag == 2), - ], - [ - tr.actions_green_flag(), - "Ctrl+3", - lambda: self.set_flag_on_current_card(3), - dict(checked=currentFlag == 3), - ], - [ - tr.actions_blue_flag(), - "Ctrl+4", - lambda: self.set_flag_on_current_card(4), - dict(checked=currentFlag == 4), - ], + flag.label, + f"Ctrl+{flag.index}", + self.set_flag_func(flag.index), + dict(checked=currentFlag == flag.index), + ] + for flag in load_flags(self.mw.col) ], ], [tr.studying_mark_note(), "*", self.toggle_mark_on_current_note], @@ -998,6 +988,9 @@ time = %(time)d; redraw_flag ).run_in_background(initiator=self) + def set_flag_func(self, desired_flag: int) -> Callable: + return lambda: self.set_flag_on_current_card(desired_flag) + def toggle_mark_on_current_note(self) -> None: def redraw_mark(out: OpChangesWithCount) -> None: self.card.load() diff --git a/qt/aqt/utils.py b/qt/aqt/utils.py index 7533e4566..3407b2733 100644 --- a/qt/aqt/utils.py +++ b/qt/aqt/utils.py @@ -6,12 +6,14 @@ import os import re import subprocess import sys +from dataclasses import dataclass from enum import Enum from functools import wraps from typing import ( TYPE_CHECKING, Any, Callable, + Dict, List, Literal, Optional, @@ -35,10 +37,12 @@ from PyQt5.QtWidgets import ( import aqt from anki import Collection +from anki.collection import SearchNode from anki.lang import TR, tr_legacyglobal # pylint: disable=unused-import from anki.utils import invalidFilename, isMac, isWin, noBundledLibs, versionWithBuild +from aqt import colors from aqt.qt import * -from aqt.theme import theme_manager +from aqt.theme import ColoredIcon, theme_manager if TYPE_CHECKING: TextFormat = Union[Literal["plain", "rich"]] @@ -1023,6 +1027,51 @@ def no_arg_trigger(func: Callable) -> Callable: return pyqtSlot()(func) # type: ignore +@dataclass +class Flag: + index: int + label: str + icon: ColoredIcon + search_node: SearchNode + action: str + + +def load_flags(col: Collection) -> List[Flag]: + labels = cast(Dict[str, str], col.get_config("flagLabels", {})) + icon = ColoredIcon(path=":/icons/flag.svg", color=colors.DISABLED) + + return [ + Flag( + 1, + labels["1"] if "1" in labels else tr.actions_red_flag(), + icon.with_color(colors.FLAG1_FG), + SearchNode(flag=SearchNode.FLAG_RED), + "actionRed_Flag", + ), + Flag( + 2, + labels["2"] if "2" in labels else tr.actions_orange_flag(), + icon.with_color(colors.FLAG2_FG), + SearchNode(flag=SearchNode.FLAG_ORANGE), + "actionOrange_Flag", + ), + Flag( + 3, + labels["3"] if "3" in labels else tr.actions_green_flag(), + icon.with_color(colors.FLAG3_FG), + SearchNode(flag=SearchNode.FLAG_GREEN), + "actionGreen_Flag", + ), + Flag( + 4, + labels["4"] if "4" in labels else tr.actions_blue_flag(), + icon.with_color(colors.FLAG4_FG), + SearchNode(flag=SearchNode.FLAG_BLUE), + "actionBlue_Flag", + ), + ] + + class KeyboardModifiersPressed: "Util for type-safe checks of currently-pressed modifier keys."