diff --git a/ftl/core/browsing.ftl b/ftl/core/browsing.ftl index d2af15637..4050fd634 100644 --- a/ftl/core/browsing.ftl +++ b/ftl/core/browsing.ftl @@ -4,6 +4,7 @@ browsing-add-tags2 = Add Tags... browsing-added-today = Added Today browsing-addon = Add-on browsing-again-today = Again Today +browsing-edited-today = Edited Today browsing-all-card-types = All Card Types browsing-all-fields = All Fields browsing-answer = Answer @@ -34,7 +35,6 @@ browsing-ease = Ease browsing-end = End browsing-enter-tags-to-add = Enter tags to add: browsing-enter-tags-to-delete = Enter tags to delete: -browsing-filter = Filter... browsing-filtered = (filtered) browsing-find = Find: browsing-find-and-replace = Find and Replace @@ -127,3 +127,8 @@ browsing-sidebar-tags = Tags browsing-sidebar-notetypes = Note Types browsing-sidebar-saved-searches = Saved Searches browsing-sidebar-save-current-search = Save Current Search +browsing-sidebar-card-state = Card State +browsing-sidebar-flags = Flags +browsing-sidebar-recent = Recent +browsing-sidebar-due-today = Due Today +browsing-sidebar-due-tomorrow = Due Tomorrow diff --git a/ftl/core/filtering.ftl b/ftl/core/filtering.ftl deleted file mode 100644 index 1564e1570..000000000 --- a/ftl/core/filtering.ftl +++ /dev/null @@ -1,2 +0,0 @@ -# True if a card is due/ready for review -filtering-is-due = Due diff --git a/pylib/anki/types.py b/pylib/anki/types.py index 1783afc67..210a62c74 100644 --- a/pylib/anki/types.py +++ b/pylib/anki/types.py @@ -2,4 +2,5 @@ from typing import NoReturn def assert_exhaustive(arg: NoReturn) -> NoReturn: + """The type definition will cause mypy to tell us if we've missed an enum case.""" raise Exception(f"unexpected arg received: {type(arg)} {arg}") diff --git a/qt/aqt/__init__.py b/qt/aqt/__init__.py index fdadd0ec9..040c643fd 100644 --- a/qt/aqt/__init__.py +++ b/qt/aqt/__init__.py @@ -488,6 +488,7 @@ def _run(argv: Optional[List[str]] = None, exec: bool = True) -> Optional[AnkiAp # opt in to full hidpi support? if not os.environ.get("ANKI_NOHIGHDPI"): QCoreApplication.setAttribute(Qt.AA_EnableHighDpiScaling) + QCoreApplication.setAttribute(Qt.AA_UseHighDpiPixmaps) os.environ["QT_ENABLE_HIGHDPI_SCALING"] = "1" os.environ["QT_SCALE_FACTOR_ROUNDING_POLICY"] = "PassThrough" diff --git a/qt/aqt/browser.py b/qt/aqt/browser.py index 1b9460476..a55094e62 100644 --- a/qt/aqt/browser.py +++ b/qt/aqt/browser.py @@ -33,8 +33,6 @@ from aqt.theme import theme_manager from aqt.utils import ( TR, HelpPage, - MenuList, - SubMenu, askUser, disable_help_button, getTag, @@ -63,10 +61,6 @@ from aqt.utils import ( ) from aqt.webview import AnkiWebView -# legacy add-on support -# pylint: disable=unused-import -from aqt.sidebar import SidebarItem, SidebarStage # isort: skip - @dataclass class FindDupesDialog: @@ -486,7 +480,6 @@ class Browser(QMainWindow): # pylint: disable=unnecessary-lambda # actions f = self.form - qconnect(f.filter.clicked, self.onFilterButton) # edit qconnect(f.actionUndo.triggered, self.mw.onUndo) qconnect(f.actionInvertSelection.triggered, self.invertSelection) @@ -978,34 +971,15 @@ QTableView {{ gridline-color: {grid} }} self.showSidebar() self.sidebar.searchBar.setFocus() - # legacy - def maybeRefreshSidebar(self) -> None: - self.sidebar.refresh() - def toggle_sidebar(self) -> None: want_visible = not self.sidebarDockWidget.isVisible() self.sidebarDockWidget.setVisible(want_visible) if want_visible: self.sidebar.refresh() - # Filter button and sidebar helpers + # Sidebar helpers ###################################################################### - def onFilterButton(self) -> None: - ml = MenuList() - - ml.addChild(self._todayFilters()) - ml.addChild(self._cardStateFilters()) - ml.addSeparator() - - toggle_sidebar = QAction(tr(TR.BROWSING_SIDEBAR)) - qconnect(toggle_sidebar.triggered, self.toggle_sidebar) - toggle_sidebar.setCheckable(True) - toggle_sidebar.setChecked(self.sidebarDockWidget.isVisible()) - ml.addChild(toggle_sidebar) - - ml.popupOver(self.form.filter) - def update_search(self, *terms: Union[str, SearchTerm]) -> None: """Modify the current search string based on modified keys, then refresh.""" try: @@ -1030,84 +1004,6 @@ QTableView {{ gridline-color: {grid} }} def setFilter(self, *terms: str) -> None: self.set_filter_then_search(*terms) - def _simpleFilters(self, items: Sequence[Tuple[str, SearchTerm]]) -> MenuList: - ml = MenuList() - for row in items: - if row is None: - ml.addSeparator() - else: - label, filter_name = row - ml.addItem(label, self.sidebar._filter_func(filter_name)) - return ml - - def _todayFilters(self) -> SubMenu: - subm = SubMenu(tr(TR.BROWSING_TODAY)) - subm.addChild( - self._simpleFilters( - ( - (tr(TR.BROWSING_ADDED_TODAY), SearchTerm(added_in_days=1)), - ( - tr(TR.BROWSING_STUDIED_TODAY), - SearchTerm(rated=SearchTerm.Rated(days=1)), - ), - ( - tr(TR.BROWSING_AGAIN_TODAY), - SearchTerm( - rated=SearchTerm.Rated( - days=1, rating=SearchTerm.RATING_AGAIN - ) - ), - ), - ) - ) - ) - return subm - - def _cardStateFilters(self) -> SubMenu: - subm = SubMenu(tr(TR.BROWSING_CARD_STATE)) - subm.addChild( - self._simpleFilters( - ( - ( - tr(TR.ACTIONS_NEW), - SearchTerm(card_state=SearchTerm.CARD_STATE_NEW), - ), - ( - tr(TR.SCHEDULING_LEARNING), - SearchTerm(card_state=SearchTerm.CARD_STATE_LEARN), - ), - ( - tr(TR.SCHEDULING_REVIEW), - SearchTerm(card_state=SearchTerm.CARD_STATE_REVIEW), - ), - ( - tr(TR.FILTERING_IS_DUE), - SearchTerm(card_state=SearchTerm.CARD_STATE_DUE), - ), - None, - ( - tr(TR.BROWSING_SUSPENDED), - SearchTerm(card_state=SearchTerm.CARD_STATE_SUSPENDED), - ), - ( - tr(TR.BROWSING_BURIED), - SearchTerm(card_state=SearchTerm.CARD_STATE_BURIED), - ), - None, - (tr(TR.ACTIONS_RED_FLAG), SearchTerm(flag=SearchTerm.FLAG_RED)), - ( - tr(TR.ACTIONS_ORANGE_FLAG), - SearchTerm(flag=SearchTerm.FLAG_ORANGE), - ), - (tr(TR.ACTIONS_GREEN_FLAG), SearchTerm(flag=SearchTerm.FLAG_GREEN)), - (tr(TR.ACTIONS_BLUE_FLAG), SearchTerm(flag=SearchTerm.FLAG_BLUE)), - (tr(TR.BROWSING_NO_FLAG), SearchTerm(flag=SearchTerm.FLAG_NONE)), - (tr(TR.BROWSING_ANY_FLAG), SearchTerm(flag=SearchTerm.FLAG_ANY)), - ) - ) - ) - return subm - # Info ###################################################################### diff --git a/qt/aqt/colors.py b/qt/aqt/colors.py new file mode 120000 index 000000000..37fcf5a36 --- /dev/null +++ b/qt/aqt/colors.py @@ -0,0 +1 @@ +../../bazel-bin/qt/aqt/colors.py \ No newline at end of file diff --git a/qt/aqt/forms/browser.ui b/qt/aqt/forms/browser.ui index d2cf92f50..ffed2649f 100644 --- a/qt/aqt/forms/browser.ui +++ b/qt/aqt/forms/browser.ui @@ -91,7 +91,7 @@ 0 - + @@ -107,13 +107,6 @@ - - - - BROWSING_FILTER - - - @@ -151,12 +144,12 @@ false - - false - 20 + + false + true @@ -216,7 +209,7 @@ 0 0 750 - 21 + 24 diff --git a/qt/aqt/forms/icons.qrc b/qt/aqt/forms/icons.qrc index 63b9a3077..277e01c9a 100644 --- a/qt/aqt/forms/icons.qrc +++ b/qt/aqt/forms/icons.qrc @@ -1,11 +1,14 @@ - - icons/anki.png - icons/tag.svg - icons/deck.svg - icons/notetype.svg - icons/heart.svg - icons/collection.svg - icons/media-record.png - + + icons/anki.png + icons/tag.svg + icons/deck.svg + icons/notetype.svg + icons/heart.svg + icons/collection.svg + icons/media-record.png + icons/clock.svg + icons/card-state.svg + icons/flag.svg + diff --git a/qt/aqt/forms/icons/card-state.svg b/qt/aqt/forms/icons/card-state.svg new file mode 100644 index 000000000..54ed5eb3c --- /dev/null +++ b/qt/aqt/forms/icons/card-state.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/qt/aqt/forms/icons/clock.svg b/qt/aqt/forms/icons/clock.svg new file mode 100644 index 000000000..747eb1a47 --- /dev/null +++ b/qt/aqt/forms/icons/clock.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/qt/aqt/forms/icons/flag.svg b/qt/aqt/forms/icons/flag.svg new file mode 100644 index 000000000..5936653de --- /dev/null +++ b/qt/aqt/forms/icons/flag.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/qt/aqt/sidebar.py b/qt/aqt/sidebar.py index 8c36e5411..7f4041b1b 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -5,7 +5,7 @@ from __future__ import annotations from concurrent.futures import Future -from enum import Enum +from enum import Enum, auto from typing import Any, Dict, Iterable, List, Optional, Sequence, Tuple, cast import aqt @@ -13,11 +13,12 @@ from anki.collection import ConfigBoolKey, SearchTerm from anki.decks import DeckTreeNode from anki.errors import DeckRenameError, InvalidInput from anki.tags import TagTreeNode -from aqt import gui_hooks +from anki.types import assert_exhaustive +from aqt import colors, gui_hooks from aqt.main import ResetReason from aqt.models import Models from aqt.qt import * -from aqt.theme import theme_manager +from aqt.theme import ColoredIcon, theme_manager from aqt.utils import ( TR, askUser, @@ -30,39 +31,51 @@ from aqt.utils import ( class SidebarItemType(Enum): - ROOT = 0 - COLLECTION = 1 - CURRENT_DECK = 2 - SAVED_SEARCH = 3 - FILTER = 3 # legacy alias for SAVED_SEARCH - DECK = 4 - NOTETYPE = 5 - TAG = 6 - CUSTOM = 7 - TEMPLATE = 8 - SAVED_SEARCH_ROOT = 9 - DECK_ROOT = 10 - NOTETYPE_ROOT = 11 - TAG_ROOT = 12 + ROOT = auto() + SAVED_SEARCH_ROOT = auto() + SAVED_SEARCH = auto() + RECENT_ROOT = auto() + RECENT = auto() + CARD_STATE_ROOT = auto() + CARD_STATE = auto() + FLAG_ROOT = auto() + FLAG = auto() + DECK_ROOT = auto() + DECK = auto() + NOTETYPE_ROOT = auto() + NOTETYPE = auto() + NOTETYPE_TEMPLATE = auto() + TAG_ROOT = auto() + TAG = auto() + + CUSTOM = auto() + + @staticmethod + def section_roots() -> Iterable[SidebarItemType]: + return (type for type in SidebarItemType if type.name.endswith("_ROOT")) + + def is_section_root(self) -> bool: + return self in self.section_roots() -# used by an add-on hook class SidebarStage(Enum): - ROOT = 0 - STANDARD = 1 - FAVORITES = 2 - DECKS = 3 - MODELS = 4 - TAGS = 5 + ROOT = auto() + SAVED_SEARCHES = auto() + RECENT = auto() + SCHEDULING = auto() + FLAGS = auto() + DECKS = auto() + NOTETYPES = auto() + TAGS = auto() class SidebarItem: def __init__( self, name: str, - icon: str, - onClick: Callable[[], None] = None, - onExpanded: Callable[[bool], None] = None, + icon: Union[str, ColoredIcon], + on_click: Callable[[], None] = None, + on_expanded: Callable[[bool], None] = None, expanded: bool = False, item_type: SidebarItemType = SidebarItemType.CUSTOM, id: int = 0, @@ -75,39 +88,47 @@ class SidebarItem: self.icon = icon self.item_type = item_type self.id = id - self.onClick = onClick - self.onExpanded = onExpanded - self.expanded = expanded + self.on_click = on_click + self.on_expanded = on_expanded self.children: List["SidebarItem"] = [] - self.parentItem: Optional["SidebarItem"] = None self.tooltip: Optional[str] = None - self.row_in_parent: Optional[int] = None + self._parent_item: Optional["SidebarItem"] = None + self._is_expanded = expanded + self._row_in_parent: Optional[int] = None self._search_matches_self = False self._search_matches_child = False - def addChild(self, cb: "SidebarItem") -> None: + def add_child(self, cb: "SidebarItem") -> None: self.children.append(cb) - cb.parentItem = self + cb._parent_item = self - def rowForChild(self, child: "SidebarItem") -> Optional[int]: - try: - return self.children.index(child) - except ValueError: - return None + def add_simple( + self, + name: Union[str, TR.V], + icon: Union[str, ColoredIcon], + type: SidebarItemType, + on_click: Callable[[], None], + ) -> SidebarItem: + "Add child sidebar item, and return it." + if not isinstance(name, str): + name = tr(name) + item = SidebarItem( + name=name, + icon=icon, + on_click=on_click, + item_type=type, + ) + self.add_child(item) + return item def is_expanded(self, searching: bool) -> bool: if not searching: - return self.expanded + return self._is_expanded else: if self._search_matches_child: return True # if search matches top level, expand children one level - return self._search_matches_self and self.item_type in ( - SidebarItemType.SAVED_SEARCH_ROOT, - SidebarItemType.DECK_ROOT, - SidebarItemType.NOTETYPE_ROOT, - SidebarItemType.TAG_ROOT, - ) + return self._search_matches_self and self.item_type.is_section_root() def is_highlighted(self) -> bool: return self._search_matches_self @@ -130,7 +151,7 @@ class SidebarModel(QAbstractItemModel): def _cache_rows(self, node: SidebarItem) -> None: "Cache index of children in parent." for row, item in enumerate(node.children): - item.row_in_parent = row + item._row_in_parent = row self._cache_rows(item) def item_for_index(self, idx: QModelIndex) -> SidebarItem: @@ -172,12 +193,12 @@ class SidebarModel(QAbstractItemModel): return QModelIndex() childItem: SidebarItem = child.internalPointer() - parentItem = childItem.parentItem + parentItem = childItem._parent_item if parentItem is None or parentItem == self.root: return QModelIndex() - row = parentItem.row_in_parent + row = parentItem._row_in_parent return self.createIndex(row, 0, parentItem) @@ -216,30 +237,6 @@ class SidebarModel(QAbstractItemModel): return cast(Qt.ItemFlags, flags) - # Helpers - ###################################################################### - - def iconFromRef(self, iconRef: str) -> QIcon: - print("iconFromRef() deprecated") - return theme_manager.icon_from_resources(iconRef) - - -def expand_where_necessary( - model: SidebarModel, - tree: QTreeView, - parent: Optional[QModelIndex] = None, - searching: bool = False, -) -> None: - parent = parent or QModelIndex() - for row in range(model.rowCount(parent)): - idx = model.index(row, 0, parent) - if not idx.isValid(): - continue - expand_where_necessary(model, tree, idx, searching) - if item := model.item_for_index(idx): - if item.is_expanded(searching): - tree.setExpanded(idx, True) - class SidebarSearchBar(QLineEdit): def __init__(self, sidebar: SidebarTreeView) -> None: @@ -308,8 +305,8 @@ class SidebarTreeView(QTreeView): self.setDragDropMode(QAbstractItemView.InternalMove) self.setDragDropOverwriteMode(False) - qconnect(self.expanded, self.onExpansion) - qconnect(self.collapsed, self.onCollapse) + qconnect(self.expanded, self._on_expansion) + qconnect(self.collapsed, self._on_collapse) # match window background color bgcolor = QPalette().window().color().name() @@ -334,7 +331,7 @@ class SidebarTreeView(QTreeView): if self.current_search: self.search_for(self.current_search) else: - expand_where_necessary(model, self) + self._expand_where_necessary(model) self.mw.taskman.run_in_background(self._root_tree, on_done) @@ -349,7 +346,26 @@ class SidebarTreeView(QTreeView): # start from a collapsed state, as it's faster self.collapseAll() self.setColumnHidden(0, not self.model().search(text)) - expand_where_necessary(self.model(), self, searching=True) + self._expand_where_necessary(self.model(), searching=True) + + def _expand_where_necessary( + self, + model: SidebarModel, + parent: Optional[QModelIndex] = None, + searching: bool = False, + ) -> None: + parent = parent or QModelIndex() + for row in range(model.rowCount(parent)): + idx = model.index(row, 0, parent) + if not idx.isValid(): + continue + self._expand_where_necessary(model, idx, searching) + if item := model.item_for_index(idx): + if item.is_expanded(searching): + self.setExpanded(idx, True) + + # Qt API + ########### def drawRow( self, painter: QPainter, options: QStyleOptionViewItem, idx: QModelIndex @@ -369,6 +385,19 @@ class SidebarTreeView(QTreeView): if self.handle_drag_drop(source_items, target_item): event.acceptProposedAction() + def mouseReleaseEvent(self, event: QMouseEvent) -> None: + super().mouseReleaseEvent(event) + if event.button() == Qt.LeftButton: + self._on_click_current() + + def keyPressEvent(self, event: QKeyEvent) -> None: + if event.key() in (Qt.Key_Return, Qt.Key_Enter): + self._on_click_current() + else: + super().keyPressEvent(event) + + ########### + def handle_drag_drop(self, sources: List[SidebarItem], target: SidebarItem) -> bool: if target.item_type in (SidebarItemType.DECK, SidebarItemType.DECK_ROOT): return self._handle_drag_drop_decks(sources, target) @@ -433,74 +462,70 @@ class SidebarTreeView(QTreeView): self.browser.editor.saveNow(on_save) return True - def onClickCurrent(self) -> None: + def _on_click_current(self) -> None: idx = self.currentIndex() if item := self.model().item_for_index(idx): - if item.onClick: - item.onClick() + if item.on_click: + item.on_click() - def mouseReleaseEvent(self, event: QMouseEvent) -> None: - super().mouseReleaseEvent(event) - if event.button() == Qt.LeftButton: - self.onClickCurrent() - - def keyPressEvent(self, event: QKeyEvent) -> None: - if event.key() in (Qt.Key_Return, Qt.Key_Enter): - self.onClickCurrent() - else: - super().keyPressEvent(event) - - def onExpansion(self, idx: QModelIndex) -> None: + def _on_expansion(self, idx: QModelIndex) -> None: if self.current_search: return - self._onExpansionChange(idx, True) + self._on_expand_or_collapse(idx, True) - def onCollapse(self, idx: QModelIndex) -> None: + def _on_collapse(self, idx: QModelIndex) -> None: if self.current_search: return - self._onExpansionChange(idx, False) + self._on_expand_or_collapse(idx, False) - def _onExpansionChange(self, idx: QModelIndex, expanded: bool) -> None: + def _on_expand_or_collapse(self, idx: QModelIndex, expanded: bool) -> None: item = self.model().item_for_index(idx) - if item and item.expanded != expanded: - item.expanded = expanded - if item.onExpanded: - item.onExpanded(expanded) + if item and item._is_expanded != expanded: + item._is_expanded = expanded + if item.on_expanded: + item.on_expanded(expanded) # Tree building ########################### def _root_tree(self) -> SidebarItem: - root = SidebarItem("", "", item_type=SidebarItemType.ROOT) + root: Optional[SidebarItem] = None - handled = gui_hooks.browser_will_build_tree( - False, root, SidebarStage.ROOT, self - ) - if handled: - return root - - for stage, builder in zip( - list(SidebarStage)[1:], - ( - self._commonly_used_tree, - self._saved_searches_tree, - self._deck_tree, - self._notetype_tree, - self._tag_tree, - ), - ): + for stage in SidebarStage: + if stage == SidebarStage.ROOT: + root = SidebarItem("", "", item_type=SidebarItemType.ROOT) handled = gui_hooks.browser_will_build_tree(False, root, stage, self) - if not handled and builder: - builder(root) + if not handled: + self._build_stage(root, stage) return root + def _build_stage(self, root: SidebarItem, stage: SidebarStage) -> None: + if stage is SidebarStage.SAVED_SEARCHES: + self._saved_searches_tree(root) + elif stage is SidebarStage.SCHEDULING: + self._card_state_tree(root) + elif stage is SidebarStage.RECENT: + self._recent_tree(root) + elif stage is SidebarStage.FLAGS: + self._flags_tree(root) + elif stage is SidebarStage.DECKS: + self._deck_tree(root) + elif stage is SidebarStage.NOTETYPES: + self._notetype_tree(root) + elif stage is SidebarStage.TAGS: + self._tag_tree(root) + elif stage is SidebarStage.ROOT: + pass + else: + assert_exhaustive(stage) + def _section_root( self, *, root: SidebarItem, name: TR.V, - icon: str, + icon: Union[str, ColoredIcon], collapse_key: ConfigBoolKey.V, type: Optional[SidebarItemType] = None, ) -> SidebarItem: @@ -510,29 +535,19 @@ class SidebarTreeView(QTreeView): top = SidebarItem( tr(name), icon, - onExpanded=update, + on_expanded=update, expanded=not self.col.get_config_bool(collapse_key), item_type=type, ) - root.addChild(top) + root.add_child(top) return top - def _commonly_used_tree(self, root: SidebarItem) -> None: - item = SidebarItem( - tr(TR.BROWSING_WHOLE_COLLECTION), - ":/icons/collection.svg", - self._filter_func(), - item_type=SidebarItemType.COLLECTION, - ) - root.addChild(item) - item = SidebarItem( - tr(TR.BROWSING_CURRENT_DECK), - ":/icons/deck.svg", - self._filter_func(SearchTerm(deck="current")), - item_type=SidebarItemType.CURRENT_DECK, - ) - root.addChild(item) + def _filter_func(self, *terms: Union[str, SearchTerm]) -> Callable: + return lambda: self.browser.update_search(self.col.build_search_string(*terms)) + + # Tree: Saved Searches + ########################### def _saved_searches_tree(self, root: SidebarItem) -> None: icon = ":/icons/heart.svg" @@ -542,23 +557,187 @@ class SidebarTreeView(QTreeView): root=root, name=TR.BROWSING_SIDEBAR_SAVED_SEARCHES, icon=icon, - collapse_key=ConfigBoolKey.COLLAPSE_FAVORITES, + collapse_key=ConfigBoolKey.COLLAPSE_SAVED_SEARCHES, type=SidebarItemType.SAVED_SEARCH_ROOT, ) def on_click() -> None: self.show_context_menu(root, None) - root.onClick = on_click + root.on_click = on_click for name, filt in sorted(saved.items()): item = SidebarItem( name, icon, self._filter_func(filt), - item_type=SidebarItemType.FILTER, + item_type=SidebarItemType.SAVED_SEARCH, ) - root.addChild(item) + root.add_child(item) + + # Tree: Recent + ########################### + + def _recent_tree(self, root: SidebarItem) -> None: + icon = ":/icons/clock.svg" + root = self._section_root( + root=root, + name=TR.BROWSING_SIDEBAR_RECENT, + icon=icon, + collapse_key=ConfigBoolKey.COLLAPSE_RECENT, + type=SidebarItemType.FLAG_ROOT, + ) + type = SidebarItemType.FLAG + search = self._filter_func + + root.add_simple( + TR.BROWSING_CURRENT_DECK, + icon=icon, + type=type, + on_click=search(SearchTerm(deck="current")), + ) + root.add_simple( + name=TR.BROWSING_SIDEBAR_DUE_TODAY, + icon=icon, + type=type, + on_click=search(SearchTerm(card_state=SearchTerm.CARD_STATE_DUE)), + ) + root.add_simple( + name=TR.BROWSING_ADDED_TODAY, + icon=icon, + type=type, + on_click=search(SearchTerm(added_in_days=1)), + ) + root.add_simple( + name=TR.BROWSING_EDITED_TODAY, + icon=icon, + type=type, + on_click=search(SearchTerm(edited_in_days=1)), + ) + root.add_simple( + name=TR.BROWSING_STUDIED_TODAY, + icon=icon, + type=type, + on_click=search(SearchTerm(rated=SearchTerm.Rated(days=1))), + ) + root.add_simple( + name=TR.BROWSING_AGAIN_TODAY, + icon=icon, + type=type, + on_click=search( + SearchTerm( + rated=SearchTerm.Rated(days=1, rating=SearchTerm.RATING_AGAIN) + ) + ), + ) + root.add_simple( + name=TR.BROWSING_SIDEBAR_DUE_TOMORROW, + icon=icon, + type=type, + on_click=search(SearchTerm(due_in_days=1)), + ) + + # Tree: Card State + ########################### + + def _card_state_tree(self, root: SidebarItem) -> None: + icon = ColoredIcon(path=":/icons/card-state.svg", color=colors.DISABLED) + root = self._section_root( + root=root, + name=TR.BROWSING_SIDEBAR_CARD_STATE, + icon=icon, + collapse_key=ConfigBoolKey.COLLAPSE_CARD_STATE, + type=SidebarItemType.CARD_STATE_ROOT, + ) + type = SidebarItemType.CARD_STATE + search = self._filter_func + + root.add_simple( + TR.ACTIONS_NEW, + icon=icon.with_color(colors.NEW_COUNT), + type=type, + on_click=search(SearchTerm(card_state=SearchTerm.CARD_STATE_NEW)), + ) + + root.add_simple( + name=TR.SCHEDULING_LEARNING, + icon=icon.with_color(colors.LEARN_COUNT), + type=type, + on_click=search(SearchTerm(card_state=SearchTerm.CARD_STATE_LEARN)), + ) + root.add_simple( + name=TR.SCHEDULING_REVIEW, + icon=icon.with_color(colors.REVIEW_COUNT), + type=type, + on_click=search(SearchTerm(card_state=SearchTerm.CARD_STATE_REVIEW)), + ) + root.add_simple( + name=TR.BROWSING_SUSPENDED, + icon=icon.with_color(colors.SUSPENDED_FG), + type=type, + on_click=search(SearchTerm(card_state=SearchTerm.CARD_STATE_SUSPENDED)), + ) + root.add_simple( + name=TR.BROWSING_BURIED, + icon=icon.with_color(colors.BURIED_FG), + type=type, + on_click=search(SearchTerm(card_state=SearchTerm.CARD_STATE_BURIED)), + ) + + # Tree: Flags + ########################### + + def _flags_tree(self, root: SidebarItem) -> None: + icon = ColoredIcon(path=":/icons/flag.svg", color=colors.DISABLED) + root = self._section_root( + root=root, + name=TR.BROWSING_SIDEBAR_FLAGS, + icon=icon, + collapse_key=ConfigBoolKey.COLLAPSE_FLAGS, + type=SidebarItemType.FLAG_ROOT, + ) + type = SidebarItemType.FLAG + search = self._filter_func + + root.add_simple( + TR.ACTIONS_RED_FLAG, + icon=icon.with_color(colors.FLAG1_FG), + type=type, + on_click=search(SearchTerm(flag=SearchTerm.FLAG_RED)), + ) + root.add_simple( + TR.ACTIONS_ORANGE_FLAG, + icon=icon.with_color(colors.FLAG2_FG), + type=type, + on_click=search(SearchTerm(flag=SearchTerm.FLAG_ORANGE)), + ) + root.add_simple( + TR.ACTIONS_GREEN_FLAG, + icon=icon.with_color(colors.FLAG3_FG), + type=type, + on_click=search(SearchTerm(flag=SearchTerm.FLAG_GREEN)), + ) + root.add_simple( + TR.ACTIONS_BLUE_FLAG, + icon=icon.with_color(colors.FLAG4_FG), + type=type, + on_click=search(SearchTerm(flag=SearchTerm.FLAG_BLUE)), + ) + root.add_simple( + TR.BROWSING_ANY_FLAG, + icon=icon.with_color(colors.TEXT_FG), + type=type, + on_click=search(SearchTerm(flag=SearchTerm.FLAG_ANY)), + ) + root.add_simple( + TR.BROWSING_NO_FLAG, + icon=icon.with_color(colors.DISABLED), + type=type, + on_click=search(SearchTerm(flag=SearchTerm.FLAG_NONE)), + ) + + # Tree: Tags + ########################### def _tag_tree(self, root: SidebarItem) -> None: icon = ":/icons/tag.svg" @@ -583,7 +762,7 @@ class SidebarTreeView(QTreeView): item_type=SidebarItemType.TAG, full_name=head + node.name, ) - root.addChild(item) + root.add_child(item) newhead = head + node.name + "::" render(item, node.children, newhead) @@ -597,6 +776,9 @@ class SidebarTreeView(QTreeView): ) render(root, tree.children) + # Tree: Decks + ########################### + def _deck_tree(self, root: SidebarItem) -> None: icon = ":/icons/deck.svg" @@ -619,7 +801,7 @@ class SidebarTreeView(QTreeView): id=node.deck_id, full_name=head + node.name, ) - root.addChild(item) + root.add_child(item) newhead = head + node.name + "::" render(item, node.children, newhead) @@ -633,6 +815,9 @@ class SidebarTreeView(QTreeView): ) render(root, tree.children) + # Tree: Notetypes + ########################### + def _notetype_tree(self, root: SidebarItem) -> None: icon = ":/icons/notetype.svg" root = self._section_root( @@ -659,15 +844,12 @@ class SidebarTreeView(QTreeView): self._filter_func( SearchTerm(note=nt["name"]), SearchTerm(template=c) ), - item_type=SidebarItemType.TEMPLATE, + item_type=SidebarItemType.NOTETYPE_TEMPLATE, full_name=nt["name"] + "::" + tmpl["name"], ) - item.addChild(child) + item.add_child(child) - root.addChild(item) - - def _filter_func(self, *terms: Union[str, SearchTerm]) -> Callable: - return lambda: self.browser.update_search(self.col.build_search_string(*terms)) + root.add_child(item) # Context menu actions ########################### @@ -735,7 +917,7 @@ class SidebarTreeView(QTreeView): lambda: set_children_collapsed(True), ) - def rename_deck(self, item: "aqt.browser.SidebarItem") -> None: + def rename_deck(self, item: SidebarItem) -> None: deck = self.mw.col.decks.get(item.id) old_name = deck["name"] new_name = getOnlyText(tr(TR.DECKS_NEW_DECK_NAME), default=old_name) @@ -751,10 +933,10 @@ class SidebarTreeView(QTreeView): self.refresh() self.mw.deckBrowser.refresh() - def remove_tag(self, item: "aqt.browser.SidebarItem") -> None: + def remove_tag(self, item: SidebarItem) -> None: self.browser.editor.saveNow(lambda: self._remove_tag(item)) - def _remove_tag(self, item: "aqt.browser.SidebarItem") -> None: + def _remove_tag(self, item: SidebarItem) -> None: old_name = item.full_name def do_remove() -> None: @@ -771,10 +953,10 @@ class SidebarTreeView(QTreeView): self.browser.model.beginReset() self.mw.taskman.run_in_background(do_remove, on_done) - def rename_tag(self, item: "aqt.browser.SidebarItem") -> None: + def rename_tag(self, item: SidebarItem) -> None: self.browser.editor.saveNow(lambda: self._rename_tag(item)) - def _rename_tag(self, item: "aqt.browser.SidebarItem") -> None: + def _rename_tag(self, item: SidebarItem) -> None: old_name = item.full_name new_name = getOnlyText(tr(TR.ACTIONS_NEW_NAME), default=old_name) if new_name == old_name or not new_name: @@ -799,10 +981,10 @@ class SidebarTreeView(QTreeView): self.browser.model.beginReset() self.mw.taskman.run_in_background(do_rename, on_done) - def delete_deck(self, item: "aqt.browser.SidebarItem") -> None: + def delete_deck(self, item: SidebarItem) -> None: self.browser.editor.saveNow(lambda: self._delete_deck(item)) - def _delete_deck(self, item: "aqt.browser.SidebarItem") -> None: + def _delete_deck(self, item: SidebarItem) -> None: did = item.id if self.mw.deckBrowser.ask_delete_deck(did): @@ -820,7 +1002,7 @@ class SidebarTreeView(QTreeView): self.browser.model.beginReset() self.mw.taskman.run_in_background(do_delete, on_done) - def remove_saved_search(self, item: "aqt.browser.SidebarItem") -> None: + def remove_saved_search(self, item: SidebarItem) -> None: name = item.name if not askUser(tr(TR.BROWSING_REMOVE_FROM_YOUR_SAVED_SEARCHES, val=name)): return @@ -829,7 +1011,7 @@ class SidebarTreeView(QTreeView): self.col.set_config("savedFilters", conf) self.refresh() - def rename_saved_search(self, item: "aqt.browser.SidebarItem") -> None: + def rename_saved_search(self, item: SidebarItem) -> None: old = item.name conf = self.col.get_config("savedFilters") try: @@ -860,7 +1042,7 @@ class SidebarTreeView(QTreeView): self.col.set_config("savedFilters", conf) self.refresh() - def manage_notetype(self, item: "aqt.browser.SidebarItem") -> None: + def manage_notetype(self, item: SidebarItem) -> None: Models( self.mw, parent=self.browser, fromMain=True, selected_notetype_id=item.id ) diff --git a/qt/aqt/theme.py b/qt/aqt/theme.py index 2741bccb0..f9d1573c3 100644 --- a/qt/aqt/theme.py +++ b/qt/aqt/theme.py @@ -2,14 +2,32 @@ # Copyright: Ankitects Pty Ltd and contributors # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +from __future__ import annotations + import platform -from typing import Dict, Optional +from dataclasses import dataclass +from typing import Dict, Optional, Tuple, Union from anki.utils import isMac -from aqt import QApplication, gui_hooks, isWin -from aqt.colors import colors +from aqt import QApplication, colors, gui_hooks, isWin from aqt.platform import set_dark_mode -from aqt.qt import QColor, QIcon, QPalette, QPixmap, QStyleFactory, Qt +from aqt.qt import QColor, QIcon, QPainter, QPalette, QPixmap, QStyleFactory, Qt + + +@dataclass +class ColoredIcon: + path: str + # (day, night) + color: Tuple[str, str] + + def current_color(self, night_mode: bool) -> str: + if night_mode: + return self.color[1] + else: + return self.color[0] + + def with_color(self, color: Tuple[str, str]) -> ColoredIcon: + return ColoredIcon(path=self.path, color=color) class ThemeManager: @@ -43,22 +61,39 @@ class ThemeManager: night_mode = property(get_night_mode, set_night_mode) - def icon_from_resources(self, path: str) -> QIcon: + def icon_from_resources(self, path: Union[str, ColoredIcon]) -> QIcon: "Fetch icon from Qt resources, and invert if in night mode." if self.night_mode: cache = self._icon_cache_light else: cache = self._icon_cache_dark - icon = cache.get(path) + + if isinstance(path, str): + key = path + else: + key = f"{path.path}-{path.color}" + + icon = cache.get(key) if icon: return icon - icon = QIcon(path) - - if self.night_mode: - img = icon.pixmap(self._icon_size, self._icon_size).toImage() - img.invertPixels() - icon = QIcon(QPixmap(img)) + if isinstance(path, str): + # default black/white + icon = QIcon(path) + if self.night_mode: + img = icon.pixmap(self._icon_size, self._icon_size).toImage() + img.invertPixels() + icon = QIcon(QPixmap(img)) + else: + # specified colours + icon = QIcon(path.path) + img = icon.pixmap(16) + painter = QPainter(img) + painter.setCompositionMode(QPainter.CompositionMode_SourceIn) + painter.fillRect(img.rect(), QColor(path.current_color(self.night_mode))) + painter.end() + icon = QIcon(img) + return icon return cache.setdefault(path, icon) @@ -94,10 +129,10 @@ class ThemeManager: Returns the color as a string hex code or color name.""" idx = 1 if self.night_mode else 0 - c = colors.get(key) - if c is None: - raise Exception("no such color:", key) - return c[idx] + + key = key.replace("-", "_").upper() + + return getattr(colors, key)[idx] def qcolor(self, key: str) -> QColor: """Get a color defined in _vars.scss as a QColor.""" diff --git a/qt/aqt/utils.py b/qt/aqt/utils.py index 46a6c5e64..6eb822559 100644 --- a/qt/aqt/utils.py +++ b/qt/aqt/utils.py @@ -805,12 +805,17 @@ def checkInvalidFilename(str: str, dirsep: bool = True) -> bool: # Menus ###################################################################### +# This code will be removed in the future, please don't rely on it. MenuListChild = Union["SubMenu", QAction, "MenuItem", "MenuList"] class MenuList: def __init__(self) -> None: + traceback.print_stack(file=sys.stdout) + print( + "MenuList will be removed; please copy it into your add-on's code if you need it." + ) self.children: List[MenuListChild] = [] def addItem(self, title: str, func: Callable) -> MenuItem: diff --git a/qt/icons/README.md b/qt/icons/README.md new file mode 100644 index 000000000..de8132c7c --- /dev/null +++ b/qt/icons/README.md @@ -0,0 +1 @@ +Source files used to produce some of the svg/png files. diff --git a/qt/icons/sidebar.afdesign b/qt/icons/sidebar.afdesign new file mode 100644 index 000000000..0e37a4ef4 Binary files /dev/null and b/qt/icons/sidebar.afdesign differ diff --git a/qt/tools/extract_sass_colors.py b/qt/tools/extract_sass_colors.py index e0d1db8ea..6a0c40667 100644 --- a/qt/tools/extract_sass_colors.py +++ b/qt/tools/extract_sass_colors.py @@ -32,4 +32,6 @@ for line in open(input_scss): with open(output_py, "w") as buf: buf.write("# this file is auto-generated from _vars.scss\n") - buf.write("colors = " + json.dumps(colors)) + for color, (day, night) in colors.items(): + color = color.replace("-", "_").upper() + buf.write(f"{color} = (\"{day}\", \"{night}\")\n") diff --git a/qt/tools/genhooks_gui.py b/qt/tools/genhooks_gui.py index b983a5ae3..4512dbbc9 100644 --- a/qt/tools/genhooks_gui.py +++ b/qt/tools/genhooks_gui.py @@ -304,8 +304,8 @@ hooks = [ name="browser_will_build_tree", args=[ "handled: bool", - "tree: aqt.browser.SidebarItem", - "stage: aqt.browser.SidebarStage", + "tree: aqt.sidebar.SidebarItem", + "stage: aqt.sidebar.SidebarStage", "browser: aqt.browser.Browser", ], return_type="bool", @@ -316,7 +316,7 @@ hooks = [ 'stage' is an enum describing the different construction stages of the sidebar tree at which you can interject your changes. The different values can be inspected by looking at - aqt.browser.SidebarStage. + aqt.sidebar.SidebarStage. If you want Anki to proceed with the construction of the tree stage in question after your have performed your changes or additions, diff --git a/rslib/backend.proto b/rslib/backend.proto index 5a450712f..6426dd4de 100644 --- a/rslib/backend.proto +++ b/rslib/backend.proto @@ -821,6 +821,7 @@ message SearchTerm { Flag flag = 11; CardState card_state = 12; IdList nids = 13; + uint32 edited_in_days = 14; } } @@ -1222,8 +1223,10 @@ message ConfigBool { COLLAPSE_TAGS = 2; COLLAPSE_NOTETYPES = 3; COLLAPSE_DECKS = 4; - COLLAPSE_FAVORITES = 5; - COLLAPSE_COMMON = 6; + COLLAPSE_SAVED_SEARCHES = 5; + COLLAPSE_RECENT = 6; + COLLAPSE_CARD_STATE = 7; + COLLAPSE_FLAGS = 8; } Key key = 1; } diff --git a/rslib/src/backend/mod.rs b/rslib/src/backend/mod.rs index 0a4040d5a..e638f7d27 100644 --- a/rslib/src/backend/mod.rs +++ b/rslib/src/backend/mod.rs @@ -330,6 +330,7 @@ impl From for Node<'_> { operator: "<=".to_string(), kind: PropertyKind::Due(i), }), + Filter::EditedInDays(u) => Node::Search(SearchNode::EditedInDays(u)), Filter::CardState(state) => Node::Search(SearchNode::State( pb::search_term::CardState::from_i32(state) .unwrap_or_default() diff --git a/rslib/src/config.rs b/rslib/src/config.rs index ada846602..dac2d56d4 100644 --- a/rslib/src/config.rs +++ b/rslib/src/config.rs @@ -42,10 +42,12 @@ pub(crate) enum ConfigKey { BrowserSortKind, BrowserSortReverse, CardCountsSeparateInactive, - CollapseCommon, + CollapseCardState, CollapseDecks, - CollapseFavorites, + CollapseFlags, CollapseNotetypes, + CollapseRecent, + CollapseSavedSearches, CollapseTags, CreationOffset, CurrentDeckID, @@ -79,9 +81,11 @@ impl From for &'static str { ConfigKey::BrowserSortKind => "sortType", ConfigKey::BrowserSortReverse => "sortBackwards", ConfigKey::CardCountsSeparateInactive => "cardCountsSeparateInactive", - ConfigKey::CollapseCommon => "collapseCommon", + ConfigKey::CollapseRecent => "collapseRecent", + ConfigKey::CollapseCardState => "collapseCardState", + ConfigKey::CollapseFlags => "collapseFlags", ConfigKey::CollapseDecks => "collapseDecks", - ConfigKey::CollapseFavorites => "collapseFavorites", + ConfigKey::CollapseSavedSearches => "collapseSavedSearches", ConfigKey::CollapseNotetypes => "collapseNotetypes", ConfigKey::CollapseTags => "collapseTags", ConfigKey::CreationOffset => "creationOffset", @@ -109,12 +113,14 @@ impl From for ConfigKey { fn from(key: BoolKey) -> Self { match key { BoolKey::BrowserSortBackwards => ConfigKey::BrowserSortReverse, - BoolKey::PreviewBothSides => ConfigKey::PreviewBothSides, - BoolKey::CollapseTags => ConfigKey::CollapseTags, - BoolKey::CollapseNotetypes => ConfigKey::CollapseNotetypes, + BoolKey::CollapseCardState => ConfigKey::CollapseCardState, BoolKey::CollapseDecks => ConfigKey::CollapseDecks, - BoolKey::CollapseFavorites => ConfigKey::CollapseFavorites, - BoolKey::CollapseCommon => ConfigKey::CollapseCommon, + BoolKey::CollapseFlags => ConfigKey::CollapseFlags, + BoolKey::CollapseNotetypes => ConfigKey::CollapseNotetypes, + BoolKey::CollapseRecent => ConfigKey::CollapseRecent, + BoolKey::CollapseSavedSearches => ConfigKey::CollapseSavedSearches, + BoolKey::CollapseTags => ConfigKey::CollapseTags, + BoolKey::PreviewBothSides => ConfigKey::PreviewBothSides, } } } diff --git a/ts/sass/_vars.scss b/ts/sass/_vars.scss index 7ce330c11..f12e88fcd 100644 --- a/ts/sass/_vars.scss +++ b/ts/sass/_vars.scss @@ -16,10 +16,16 @@ --highlight-bg: #77ccff; --highlight-fg: black; --disabled: #777; + --flag1-fg: #c35617; + --flag2-fg: #ffb347; + --flag3-fg: #0a0; + --flag4-fg: #77ccff; --flag1-bg: #ffaaaa; --flag2-bg: #ffb347; --flag3-bg: #82e0aa; --flag4-bg: #85c1e9; + --buried-fg: #aaaa33; + --suspended-fg: #dd0; --suspended-bg: #ffffb2; --marked-bg: #cce; --tooltip-bg: #fcfcfc; @@ -40,10 +46,16 @@ --highlight-bg: #77ccff; --highlight-fg: white; --disabled: #777; + --flag1-fg: #ffaaaa; + --flag2-fg: #ffb347; + --flag3-fg: #82e0aa; + --flag4-fg: #85c1e9; --flag1-bg: #aa5555; --flag2-bg: #aa6337; --flag3-bg: #33a055; --flag4-bg: #3581a9; + --buried-fg: #777733; + --suspended-fg: #ffffb2; --suspended-bg: #aaaa33; --marked-bg: #77c; --tooltip-bg: #272727;