From b8d67cdad54b933aafb310759f840cf82204e5f2 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Fri, 5 Feb 2021 15:26:12 +1000 Subject: [PATCH] move remaining Filter button items into sidebar - Closes #976 - Added helper to apply arbitrary colour to an icon. - Fix #979 - low res icons in night mode. - The icons and colours are not perfect - please feel free to send through a PR if you can improve them. - Convert colors dictionary into module consts, so we can use code completion. - Added "Edited Today" and "Due Tomorrow" - Rename camelCase attribute to snake_case and tweak the wording of some enum constants. We've already broken compatibility with the major sidebar add-ons, so we may as well make these changes while we can. - Removed Filter button. Currently there is no exposed way to toggle the Sidebar off - wonder if we still need it? --- ftl/core/browsing.ftl | 7 +- ftl/core/filtering.ftl | 2 - pylib/anki/types.py | 1 + qt/aqt/__init__.py | 1 + qt/aqt/browser.py | 106 +------ qt/aqt/colors.py | 1 + qt/aqt/forms/browser.ui | 17 +- qt/aqt/forms/icons.qrc | 21 +- qt/aqt/forms/icons/card-state.svg | 12 + qt/aqt/forms/icons/clock.svg | 25 ++ qt/aqt/forms/icons/flag.svg | 13 + qt/aqt/sidebar.py | 500 ++++++++++++++++++++---------- qt/aqt/theme.py | 67 +++- qt/aqt/utils.py | 5 + qt/icons/README.md | 1 + qt/icons/sidebar.afdesign | Bin 0 -> 26793 bytes qt/tools/extract_sass_colors.py | 4 +- qt/tools/genhooks_gui.py | 6 +- rslib/backend.proto | 7 +- rslib/src/backend/mod.rs | 1 + rslib/src/config.rs | 24 +- ts/sass/_vars.scss | 12 + 22 files changed, 514 insertions(+), 319 deletions(-) delete mode 100644 ftl/core/filtering.ftl create mode 120000 qt/aqt/colors.py create mode 100644 qt/aqt/forms/icons/card-state.svg create mode 100644 qt/aqt/forms/icons/clock.svg create mode 100644 qt/aqt/forms/icons/flag.svg create mode 100644 qt/icons/README.md create mode 100644 qt/icons/sidebar.afdesign 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 0000000000000000000000000000000000000000..0e37a4ef4aea19aee75fae980a44cb262b98fb3a GIT binary patch literal 26793 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 literal 0 HcmV?d00001 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;