From 7ee40e3dce3aa832a7c19723aefcbe4804ce9de6 Mon Sep 17 00:00:00 2001 From: RumovZ Date: Tue, 13 Apr 2021 11:05:49 +0200 Subject: [PATCH 1/4] Refactor sidebar.py into browser folder --- qt/aqt/browser/__init__.py | 10 + qt/aqt/browser/browser.py | 2 +- qt/aqt/browser/sidebar/__init__.py | 22 ++ qt/aqt/browser/sidebar/item.py | 145 ++++++++ qt/aqt/browser/sidebar/model.py | 110 ++++++ qt/aqt/browser/sidebar/searchbar.py | 50 +++ qt/aqt/browser/sidebar/toolbar.py | 51 +++ .../{sidebar.py => browser/sidebar/tree.py} | 324 +----------------- qt/tools/genhooks_gui.py | 6 +- 9 files changed, 397 insertions(+), 323 deletions(-) create mode 100644 qt/aqt/browser/sidebar/__init__.py create mode 100644 qt/aqt/browser/sidebar/item.py create mode 100644 qt/aqt/browser/sidebar/model.py create mode 100644 qt/aqt/browser/sidebar/searchbar.py create mode 100644 qt/aqt/browser/sidebar/toolbar.py rename qt/aqt/{sidebar.py => browser/sidebar/tree.py} (77%) diff --git a/qt/aqt/browser/__init__.py b/qt/aqt/browser/__init__.py index 56319c6b9..af3113e13 100644 --- a/qt/aqt/browser/__init__.py +++ b/qt/aqt/browser/__init__.py @@ -5,6 +5,16 @@ from __future__ import annotations from .browser import Browser from .dialogs import CardInfoDialog, ChangeModel, FindDupesDialog +from .sidebar import ( + SidebarItem, + SidebarItemType, + SidebarModel, + SidebarSearchBar, + SidebarStage, + SidebarTool, + SidebarToolbar, + SidebarTreeView, +) from .table import ( CardState, Cell, diff --git a/qt/aqt/browser/browser.py b/qt/aqt/browser/browser.py index 7dc2db756..d489345d0 100644 --- a/qt/aqt/browser/browser.py +++ b/qt/aqt/browser/browser.py @@ -19,6 +19,7 @@ from anki.tags import MARKED_TAG from anki.utils import ids2str, isMac from aqt import AnkiQt, gui_hooks from aqt.browser.dialogs import CardInfoDialog, ChangeModel, FindDupesDialog +from aqt.browser.sidebar import SidebarTreeView from aqt.browser.table import Table from aqt.editor import Editor from aqt.exporting import ExportDialog @@ -42,7 +43,6 @@ from aqt.operations.tag import ( from aqt.previewer import BrowserPreviewer as PreviewDialog from aqt.previewer import Previewer from aqt.qt import * -from aqt.sidebar import SidebarTreeView from aqt.switch import Switch from aqt.utils import ( HelpPage, diff --git a/qt/aqt/browser/sidebar/__init__.py b/qt/aqt/browser/sidebar/__init__.py new file mode 100644 index 000000000..5eaef6809 --- /dev/null +++ b/qt/aqt/browser/sidebar/__init__.py @@ -0,0 +1,22 @@ +# Copyright: Ankitects Pty Ltd and contributors +# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +import sys + +import aqt +from anki.utils import isMac +from aqt.theme import theme_manager + + +def _want_right_border() -> bool: + return not isMac or theme_manager.night_mode + + +from .item import SidebarItem, SidebarItemType +from .model import SidebarModel +from .searchbar import SidebarSearchBar +from .toolbar import SidebarTool, SidebarToolbar +from .tree import SidebarStage, SidebarTreeView + +# alias for the legacy pathname +sys.modules["aqt.sidebar"] = sys.modules["aqt.browser.sidebar"] +aqt.sidebar = sys.modules["aqt.browser.sidebar"] # type: ignore diff --git a/qt/aqt/browser/sidebar/item.py b/qt/aqt/browser/sidebar/item.py new file mode 100644 index 000000000..4c38eece2 --- /dev/null +++ b/qt/aqt/browser/sidebar/item.py @@ -0,0 +1,145 @@ +# Copyright: Ankitects Pty Ltd and contributors +# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +from __future__ import annotations + +from enum import Enum, auto +from typing import Callable, Iterable, List, Optional, Union + +from anki.collection import SearchNode +from aqt.theme import ColoredIcon + + +class SidebarItemType(Enum): + ROOT = auto() + SAVED_SEARCH_ROOT = auto() + SAVED_SEARCH = auto() + TODAY_ROOT = auto() + TODAY = auto() + FLAG_ROOT = auto() + FLAG = auto() + CARD_STATE_ROOT = auto() + CARD_STATE = auto() + DECK_ROOT = auto() + DECK_CURRENT = auto() + DECK = auto() + NOTETYPE_ROOT = auto() + NOTETYPE = auto() + NOTETYPE_TEMPLATE = auto() + TAG_ROOT = auto() + TAG_NONE = 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() + + def is_editable(self) -> bool: + return self in ( + SidebarItemType.SAVED_SEARCH, + SidebarItemType.DECK, + SidebarItemType.TAG, + ) + + def is_deletable(self) -> bool: + return self in ( + SidebarItemType.SAVED_SEARCH, + SidebarItemType.DECK, + SidebarItemType.TAG, + ) + + +class SidebarItem: + def __init__( + self, + name: str, + icon: Union[str, ColoredIcon], + search_node: Optional[SearchNode] = None, + on_expanded: Callable[[bool], None] = None, + expanded: bool = False, + item_type: SidebarItemType = SidebarItemType.CUSTOM, + id: int = 0, + name_prefix: str = "", + ) -> None: + self.name = name + self.name_prefix = name_prefix + self.full_name = name_prefix + name + self.icon = icon + self.item_type = item_type + self.id = id + self.search_node = search_node + self.on_expanded = on_expanded + self.children: List["SidebarItem"] = [] + self.tooltip: Optional[str] = None + self._parent_item: Optional["SidebarItem"] = None + self._expanded = expanded + self._row_in_parent: Optional[int] = None + self._search_matches_self = False + self._search_matches_child = False + + def add_child(self, cb: "SidebarItem") -> None: + self.children.append(cb) + cb._parent_item = self + + def add_simple( + self, + name: str, + icon: Union[str, ColoredIcon], + type: SidebarItemType, + search_node: Optional[SearchNode], + ) -> SidebarItem: + "Add child sidebar item, and return it." + item = SidebarItem( + name=name, + icon=icon, + search_node=search_node, + item_type=type, + ) + self.add_child(item) + return item + + @property + def expanded(self) -> bool: + return self._expanded + + @expanded.setter + def expanded(self, expanded: bool) -> None: + if self.expanded != expanded: + self._expanded = expanded + if self.on_expanded: + self.on_expanded(expanded) + + def show_expanded(self, searching: bool) -> bool: + if not searching: + return self.expanded + 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.is_section_root() + + def is_highlighted(self) -> bool: + return self._search_matches_self + + def search(self, lowered_text: str) -> bool: + "True if we or child matched." + self._search_matches_self = lowered_text in self.name.lower() + self._search_matches_child = any( + [child.search(lowered_text) for child in self.children] + ) + return self._search_matches_self or self._search_matches_child + + def has_same_id(self, other: SidebarItem) -> bool: + "True if `other` is same type, with same id/name." + if other.item_type == self.item_type: + if self.item_type == SidebarItemType.TAG: + return self.full_name == other.full_name + elif self.item_type == SidebarItemType.SAVED_SEARCH: + return self.name == other.name + else: + return other.id == self.id + + return False diff --git a/qt/aqt/browser/sidebar/model.py b/qt/aqt/browser/sidebar/model.py new file mode 100644 index 000000000..6376f176f --- /dev/null +++ b/qt/aqt/browser/sidebar/model.py @@ -0,0 +1,110 @@ +# Copyright: Ankitects Pty Ltd and contributors +# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +from __future__ import annotations + +from typing import cast + +import aqt +from aqt.browser.sidebar.item import SidebarItem +from aqt.qt import * +from aqt.theme import theme_manager + + +class SidebarModel(QAbstractItemModel): + def __init__( + self, sidebar: aqt.browser.sidebar.SidebarTreeView, root: SidebarItem + ) -> None: + super().__init__() + self.sidebar = sidebar + self.root = root + self._cache_rows(root) + + 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 + self._cache_rows(item) + + def item_for_index(self, idx: QModelIndex) -> SidebarItem: + return idx.internalPointer() + + def index_for_item(self, item: SidebarItem) -> QModelIndex: + return self.createIndex(item._row_in_parent, 0, item) + + def search(self, text: str) -> bool: + return self.root.search(text.lower()) + + # Qt API + ###################################################################### + + def rowCount(self, parent: QModelIndex = QModelIndex()) -> int: + if not parent.isValid(): + return len(self.root.children) + else: + item: SidebarItem = parent.internalPointer() + return len(item.children) + + def columnCount(self, parent: QModelIndex = QModelIndex()) -> int: + return 1 + + def index( + self, row: int, column: int, parent: QModelIndex = QModelIndex() + ) -> QModelIndex: + if not self.hasIndex(row, column, parent): + return QModelIndex() + + parentItem: SidebarItem + if not parent.isValid(): + parentItem = self.root + else: + parentItem = parent.internalPointer() + + item = parentItem.children[row] + return self.createIndex(row, column, item) + + def parent(self, child: QModelIndex) -> QModelIndex: # type: ignore + if not child.isValid(): + return QModelIndex() + + childItem: SidebarItem = child.internalPointer() + parentItem = childItem._parent_item + + if parentItem is None or parentItem == self.root: + return QModelIndex() + + row = parentItem._row_in_parent + + return self.createIndex(row, 0, parentItem) + + def data(self, index: QModelIndex, role: int = Qt.DisplayRole) -> QVariant: + if not index.isValid(): + return QVariant() + + if role not in (Qt.DisplayRole, Qt.DecorationRole, Qt.ToolTipRole, Qt.EditRole): + return QVariant() + + item: SidebarItem = index.internalPointer() + + if role in (Qt.DisplayRole, Qt.EditRole): + return QVariant(item.name) + if role == Qt.ToolTipRole: + return QVariant(item.tooltip) + return QVariant(theme_manager.icon_from_resources(item.icon)) + + def setData(self, index: QModelIndex, text: str, _role: int = Qt.EditRole) -> bool: + return self.sidebar._on_rename(index.internalPointer(), text) + + def supportedDropActions(self) -> Qt.DropActions: + return cast(Qt.DropActions, Qt.MoveAction) + + def flags(self, index: QModelIndex) -> Qt.ItemFlags: + if not index.isValid(): + return cast(Qt.ItemFlags, Qt.ItemIsEnabled) + flags = Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsDragEnabled + item: SidebarItem = index.internalPointer() + if item.item_type in self.sidebar.valid_drop_types: + flags |= Qt.ItemIsDropEnabled + if item.item_type.is_editable(): + flags |= Qt.ItemIsEditable + + return cast(Qt.ItemFlags, flags) diff --git a/qt/aqt/browser/sidebar/searchbar.py b/qt/aqt/browser/sidebar/searchbar.py new file mode 100644 index 000000000..56eaebb8d --- /dev/null +++ b/qt/aqt/browser/sidebar/searchbar.py @@ -0,0 +1,50 @@ +# 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 aqt +from aqt import colors +from aqt.browser.sidebar import _want_right_border +from aqt.qt import * +from aqt.theme import theme_manager + + +class SidebarSearchBar(QLineEdit): + def __init__(self, sidebar: aqt.browser.sidebar.SidebarTreeView) -> None: + QLineEdit.__init__(self, sidebar) + self.setPlaceholderText(sidebar.col.tr.browsing_sidebar_filter()) + self.sidebar = sidebar + self.timer = QTimer(self) + self.timer.setInterval(600) + self.timer.setSingleShot(True) + self.setFrame(False) + border = theme_manager.color(colors.MEDIUM_BORDER) + styles = [ + "padding: 1px", + "padding-left: 3px", + f"border-bottom: 1px solid {border}", + ] + if _want_right_border(): + styles.append( + f"border-right: 1px solid {border}", + ) + + self.setStyleSheet("QLineEdit { %s }" % ";".join(styles)) + + qconnect(self.timer.timeout, self.onSearch) + qconnect(self.textChanged, self.onTextChanged) + + def onTextChanged(self, text: str) -> None: + if not self.timer.isActive(): + self.timer.start() + + def onSearch(self) -> None: + self.sidebar.search_for(self.text()) + + def keyPressEvent(self, evt: QKeyEvent) -> None: + if evt.key() in (Qt.Key_Up, Qt.Key_Down): + self.sidebar.setFocus() + elif evt.key() in (Qt.Key_Enter, Qt.Key_Return): + self.onSearch() + else: + QLineEdit.keyPressEvent(self, evt) diff --git a/qt/aqt/browser/sidebar/toolbar.py b/qt/aqt/browser/sidebar/toolbar.py new file mode 100644 index 000000000..ee2d26618 --- /dev/null +++ b/qt/aqt/browser/sidebar/toolbar.py @@ -0,0 +1,51 @@ +# Copyright: Ankitects Pty Ltd and contributors +# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +from __future__ import annotations + +from enum import Enum, auto +from typing import Callable, Tuple + +import aqt +from aqt.qt import * +from aqt.theme import theme_manager +from aqt.utils import tr + + +class SidebarTool(Enum): + SELECT = auto() + SEARCH = auto() + + +class SidebarToolbar(QToolBar): + _tools: Tuple[Tuple[SidebarTool, str, Callable[[], str]], ...] = ( + (SidebarTool.SEARCH, ":/icons/magnifying_glass.svg", tr.actions_search), + (SidebarTool.SELECT, ":/icons/select.svg", tr.actions_select), + ) + + def __init__(self, sidebar: aqt.browser.sidebar.SidebarTreeView) -> None: + super().__init__() + self.sidebar = sidebar + self._action_group = QActionGroup(self) + qconnect(self._action_group.triggered, self._on_action_group_triggered) + self._setup_tools() + self.setIconSize(QSize(16, 16)) + self.setStyle(QStyleFactory.create("fusion")) + + def _setup_tools(self) -> None: + for row, tool in enumerate(self._tools): + action = self.addAction( + theme_manager.icon_from_resources(tool[1]), tool[2]() + ) + action.setCheckable(True) + action.setShortcut(f"Alt+{row + 1}") + self._action_group.addAction(action) + saved = self.sidebar.col.get_config("sidebarTool", 0) + active = saved if saved < len(self._tools) else 0 + self._action_group.actions()[active].setChecked(True) + self.sidebar.tool = self._tools[active][0] + + def _on_action_group_triggered(self, action: QAction) -> None: + index = self._action_group.actions().index(action) + self.sidebar.col.set_config("sidebarTool", index) + self.sidebar.tool = self._tools[index][0] diff --git a/qt/aqt/sidebar.py b/qt/aqt/browser/sidebar/tree.py similarity index 77% rename from qt/aqt/sidebar.py rename to qt/aqt/browser/sidebar/tree.py index 2a9c78bea..15f6b5af7 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/browser/sidebar/tree.py @@ -1,6 +1,5 @@ # Copyright: Ankitects Pty Ltd and contributors # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html - from __future__ import annotations from enum import Enum, auto @@ -14,6 +13,11 @@ from anki.notes import Note from anki.tags import TagTreeNode from anki.types import assert_exhaustive from aqt import colors, gui_hooks +from aqt.browser.sidebar import _want_right_border +from aqt.browser.sidebar.item import SidebarItem, SidebarItemType +from aqt.browser.sidebar.model import SidebarModel +from aqt.browser.sidebar.searchbar import SidebarSearchBar +from aqt.browser.sidebar.toolbar import SidebarTool, SidebarToolbar from aqt.clayout import CardLayout from aqt.models import Models from aqt.operations.deck import ( @@ -33,55 +37,6 @@ from aqt.theme import ColoredIcon, theme_manager from aqt.utils import KeyboardModifiersPressed, askUser, getOnlyText, showWarning, tr -class SidebarTool(Enum): - SELECT = auto() - SEARCH = auto() - - -class SidebarItemType(Enum): - ROOT = auto() - SAVED_SEARCH_ROOT = auto() - SAVED_SEARCH = auto() - TODAY_ROOT = auto() - TODAY = auto() - FLAG_ROOT = auto() - FLAG = auto() - CARD_STATE_ROOT = auto() - CARD_STATE = auto() - DECK_ROOT = auto() - DECK_CURRENT = auto() - DECK = auto() - NOTETYPE_ROOT = auto() - NOTETYPE = auto() - NOTETYPE_TEMPLATE = auto() - TAG_ROOT = auto() - TAG_NONE = 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() - - def is_editable(self) -> bool: - return self in ( - SidebarItemType.SAVED_SEARCH, - SidebarItemType.DECK, - SidebarItemType.TAG, - ) - - def is_deletable(self) -> bool: - return self in ( - SidebarItemType.SAVED_SEARCH, - SidebarItemType.DECK, - SidebarItemType.TAG, - ) - - class SidebarStage(Enum): ROOT = auto() SAVED_SEARCHES = auto() @@ -93,275 +48,6 @@ class SidebarStage(Enum): TAGS = auto() -class SidebarItem: - def __init__( - self, - name: str, - icon: Union[str, ColoredIcon], - search_node: Optional[SearchNode] = None, - on_expanded: Callable[[bool], None] = None, - expanded: bool = False, - item_type: SidebarItemType = SidebarItemType.CUSTOM, - id: int = 0, - name_prefix: str = "", - ) -> None: - self.name = name - self.name_prefix = name_prefix - self.full_name = name_prefix + name - self.icon = icon - self.item_type = item_type - self.id = id - self.search_node = search_node - self.on_expanded = on_expanded - self.children: List["SidebarItem"] = [] - self.tooltip: Optional[str] = None - self._parent_item: Optional["SidebarItem"] = None - self._expanded = expanded - self._row_in_parent: Optional[int] = None - self._search_matches_self = False - self._search_matches_child = False - - def add_child(self, cb: "SidebarItem") -> None: - self.children.append(cb) - cb._parent_item = self - - def add_simple( - self, - name: str, - icon: Union[str, ColoredIcon], - type: SidebarItemType, - search_node: Optional[SearchNode], - ) -> SidebarItem: - "Add child sidebar item, and return it." - item = SidebarItem( - name=name, - icon=icon, - search_node=search_node, - item_type=type, - ) - self.add_child(item) - return item - - @property - def expanded(self) -> bool: - return self._expanded - - @expanded.setter - def expanded(self, expanded: bool) -> None: - if self.expanded != expanded: - self._expanded = expanded - if self.on_expanded: - self.on_expanded(expanded) - - def show_expanded(self, searching: bool) -> bool: - if not searching: - return self.expanded - 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.is_section_root() - - def is_highlighted(self) -> bool: - return self._search_matches_self - - def search(self, lowered_text: str) -> bool: - "True if we or child matched." - self._search_matches_self = lowered_text in self.name.lower() - self._search_matches_child = any( - [child.search(lowered_text) for child in self.children] - ) - return self._search_matches_self or self._search_matches_child - - def has_same_id(self, other: SidebarItem) -> bool: - "True if `other` is same type, with same id/name." - if other.item_type == self.item_type: - if self.item_type == SidebarItemType.TAG: - return self.full_name == other.full_name - elif self.item_type == SidebarItemType.SAVED_SEARCH: - return self.name == other.name - else: - return other.id == self.id - - return False - - -class SidebarModel(QAbstractItemModel): - def __init__(self, sidebar: SidebarTreeView, root: SidebarItem) -> None: - super().__init__() - self.sidebar = sidebar - self.root = root - self._cache_rows(root) - - 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 - self._cache_rows(item) - - def item_for_index(self, idx: QModelIndex) -> SidebarItem: - return idx.internalPointer() - - def index_for_item(self, item: SidebarItem) -> QModelIndex: - return self.createIndex(item._row_in_parent, 0, item) - - def search(self, text: str) -> bool: - return self.root.search(text.lower()) - - # Qt API - ###################################################################### - - def rowCount(self, parent: QModelIndex = QModelIndex()) -> int: - if not parent.isValid(): - return len(self.root.children) - else: - item: SidebarItem = parent.internalPointer() - return len(item.children) - - def columnCount(self, parent: QModelIndex = QModelIndex()) -> int: - return 1 - - def index( - self, row: int, column: int, parent: QModelIndex = QModelIndex() - ) -> QModelIndex: - if not self.hasIndex(row, column, parent): - return QModelIndex() - - parentItem: SidebarItem - if not parent.isValid(): - parentItem = self.root - else: - parentItem = parent.internalPointer() - - item = parentItem.children[row] - return self.createIndex(row, column, item) - - def parent(self, child: QModelIndex) -> QModelIndex: # type: ignore - if not child.isValid(): - return QModelIndex() - - childItem: SidebarItem = child.internalPointer() - parentItem = childItem._parent_item - - if parentItem is None or parentItem == self.root: - return QModelIndex() - - row = parentItem._row_in_parent - - return self.createIndex(row, 0, parentItem) - - def data(self, index: QModelIndex, role: int = Qt.DisplayRole) -> QVariant: - if not index.isValid(): - return QVariant() - - if role not in (Qt.DisplayRole, Qt.DecorationRole, Qt.ToolTipRole, Qt.EditRole): - return QVariant() - - item: SidebarItem = index.internalPointer() - - if role in (Qt.DisplayRole, Qt.EditRole): - return QVariant(item.name) - if role == Qt.ToolTipRole: - return QVariant(item.tooltip) - return QVariant(theme_manager.icon_from_resources(item.icon)) - - def setData(self, index: QModelIndex, text: str, _role: int = Qt.EditRole) -> bool: - return self.sidebar._on_rename(index.internalPointer(), text) - - def supportedDropActions(self) -> Qt.DropActions: - return cast(Qt.DropActions, Qt.MoveAction) - - def flags(self, index: QModelIndex) -> Qt.ItemFlags: - if not index.isValid(): - return cast(Qt.ItemFlags, Qt.ItemIsEnabled) - flags = Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsDragEnabled - item: SidebarItem = index.internalPointer() - if item.item_type in self.sidebar.valid_drop_types: - flags |= Qt.ItemIsDropEnabled - if item.item_type.is_editable(): - flags |= Qt.ItemIsEditable - - return cast(Qt.ItemFlags, flags) - - -class SidebarToolbar(QToolBar): - _tools: Tuple[Tuple[SidebarTool, str, Callable[[], str]], ...] = ( - (SidebarTool.SEARCH, ":/icons/magnifying_glass.svg", tr.actions_search), - (SidebarTool.SELECT, ":/icons/select.svg", tr.actions_select), - ) - - def __init__(self, sidebar: SidebarTreeView) -> None: - super().__init__() - self.sidebar = sidebar - self._action_group = QActionGroup(self) - qconnect(self._action_group.triggered, self._on_action_group_triggered) - self._setup_tools() - self.setIconSize(QSize(16, 16)) - self.setStyle(QStyleFactory.create("fusion")) - - def _setup_tools(self) -> None: - for row, tool in enumerate(self._tools): - action = self.addAction( - theme_manager.icon_from_resources(tool[1]), tool[2]() - ) - action.setCheckable(True) - action.setShortcut(f"Alt+{row + 1}") - self._action_group.addAction(action) - saved = self.sidebar.col.get_config("sidebarTool", 0) - active = saved if saved < len(self._tools) else 0 - self._action_group.actions()[active].setChecked(True) - self.sidebar.tool = self._tools[active][0] - - def _on_action_group_triggered(self, action: QAction) -> None: - index = self._action_group.actions().index(action) - self.sidebar.col.set_config("sidebarTool", index) - self.sidebar.tool = self._tools[index][0] - - -class SidebarSearchBar(QLineEdit): - def __init__(self, sidebar: SidebarTreeView) -> None: - QLineEdit.__init__(self, sidebar) - self.setPlaceholderText(sidebar.col.tr.browsing_sidebar_filter()) - self.sidebar = sidebar - self.timer = QTimer(self) - self.timer.setInterval(600) - self.timer.setSingleShot(True) - self.setFrame(False) - border = theme_manager.color(colors.MEDIUM_BORDER) - styles = [ - "padding: 1px", - "padding-left: 3px", - f"border-bottom: 1px solid {border}", - ] - if _want_right_border(): - styles.append( - f"border-right: 1px solid {border}", - ) - - self.setStyleSheet("QLineEdit { %s }" % ";".join(styles)) - - qconnect(self.timer.timeout, self.onSearch) - qconnect(self.textChanged, self.onTextChanged) - - def onTextChanged(self, text: str) -> None: - if not self.timer.isActive(): - self.timer.start() - - def onSearch(self) -> None: - self.sidebar.search_for(self.text()) - - def keyPressEvent(self, evt: QKeyEvent) -> None: - if evt.key() in (Qt.Key_Up, Qt.Key_Down): - self.sidebar.setFocus() - elif evt.key() in (Qt.Key_Enter, Qt.Key_Return): - self.onSearch() - else: - QLineEdit.keyPressEvent(self, evt) - - -def _want_right_border() -> bool: - return not isMac or theme_manager.night_mode - - # fixme: we should have a top-level Sidebar class inheriting from QWidget that # handles the treeview, search bar and so on. Currently the treeview embeds the # search bar which is wrong, and the layout code is handled in browser.py instead diff --git a/qt/tools/genhooks_gui.py b/qt/tools/genhooks_gui.py index 19d7a62f0..5e3247975 100644 --- a/qt/tools/genhooks_gui.py +++ b/qt/tools/genhooks_gui.py @@ -343,8 +343,8 @@ hooks = [ name="browser_will_build_tree", args=[ "handled: bool", - "tree: aqt.sidebar.SidebarItem", - "stage: aqt.sidebar.SidebarStage", + "tree: aqt.browser.SidebarItem", + "stage: aqt.browser.SidebarStage", "browser: aqt.browser.Browser", ], return_type="bool", @@ -355,7 +355,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.sidebar.SidebarStage. + aqt.browser.SidebarStage. If you want Anki to proceed with the construction of the tree stage in question after your have performed your changes or additions, From 88086596b6ec939948eb57ec5d8fa1f57308cec5 Mon Sep 17 00:00:00 2001 From: RumovZ Date: Tue, 13 Apr 2021 11:21:36 +0200 Subject: [PATCH 2/4] Move find_and_replace.py into browser/dialogs --- qt/aqt/browser/__init__.py | 11 +++- qt/aqt/browser/browser.py | 8 ++- qt/aqt/browser/dialogs.py | 111 +++++++++++++++++++++++++++++++- qt/aqt/find_and_replace.py | 128 ------------------------------------- 4 files changed, 125 insertions(+), 133 deletions(-) delete mode 100644 qt/aqt/find_and_replace.py diff --git a/qt/aqt/browser/__init__.py b/qt/aqt/browser/__init__.py index af3113e13..15e42606f 100644 --- a/qt/aqt/browser/__init__.py +++ b/qt/aqt/browser/__init__.py @@ -1,10 +1,13 @@ # 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 sys + +import aqt + from .browser import Browser -from .dialogs import CardInfoDialog, ChangeModel, FindDupesDialog +from .dialogs import CardInfoDialog, ChangeModel, FindAndReplaceDialog, FindDupesDialog from .sidebar import ( SidebarItem, SidebarItemType, @@ -30,3 +33,7 @@ from .table import ( StatusDelegate, Table, ) + +# aliases for legacy pathnames +sys.modules["aqt.find_and_replace"] = sys.modules["aqt.browser.dialogs"] +aqt.find_and_replace = sys.modules["aqt.browser.dialogs"] # type: ignore diff --git a/qt/aqt/browser/browser.py b/qt/aqt/browser/browser.py index d489345d0..71623f12b 100644 --- a/qt/aqt/browser/browser.py +++ b/qt/aqt/browser/browser.py @@ -18,12 +18,16 @@ from anki.stats import CardStats from anki.tags import MARKED_TAG from anki.utils import ids2str, isMac from aqt import AnkiQt, gui_hooks -from aqt.browser.dialogs import CardInfoDialog, ChangeModel, FindDupesDialog +from aqt.browser.dialogs import ( + CardInfoDialog, + ChangeModel, + FindAndReplaceDialog, + FindDupesDialog, +) from aqt.browser.sidebar import SidebarTreeView from aqt.browser.table import Table from aqt.editor import Editor from aqt.exporting import ExportDialog -from aqt.find_and_replace import FindAndReplaceDialog from aqt.main import ResetReason from aqt.operations.card import set_card_deck, set_card_flag from aqt.operations.collection import undo diff --git a/qt/aqt/browser/dialogs.py b/qt/aqt/browser/dialogs.py index f98df777a..9f3671d1b 100644 --- a/qt/aqt/browser/dialogs.py +++ b/qt/aqt/browser/dialogs.py @@ -10,14 +10,23 @@ import aqt from anki.consts import * from anki.models import NotetypeDict from anki.notes import NoteId -from aqt import gui_hooks +from aqt import AnkiQt, QWidget, gui_hooks +from aqt.operations.note import find_and_replace +from aqt.operations.tag import find_and_replace_tag from aqt.qt import * from aqt.utils import ( HelpPage, askUser, disable_help_button, openHelp, + qconnect, + restore_combo_history, + restore_combo_index_for_session, + restore_is_checked, restoreGeom, + save_combo_history, + save_combo_index_for_session, + save_is_checked, saveGeom, tr, ) @@ -224,3 +233,103 @@ class CardInfoDialog(QDialog): def reject(self) -> None: saveGeom(self, "revlog") return QDialog.reject(self) + + +class FindAndReplaceDialog(QDialog): + COMBO_NAME = "BrowserFindAndReplace" + + def __init__( + self, parent: QWidget, *, mw: AnkiQt, note_ids: Sequence[NoteId] + ) -> None: + super().__init__(parent) + self.mw = mw + self.note_ids = note_ids + self.field_names: List[str] = [] + + # fetch field names and then show + mw.query_op( + lambda: mw.col.field_names_for_note_ids(note_ids), + success=self._show, + ) + + def _show(self, field_names: Sequence[str]) -> None: + # add "all fields" and "tags" to the top of the list + self.field_names = [ + tr.browsing_all_fields(), + tr.editing_tags(), + ] + list(field_names) + + disable_help_button(self) + self.form = aqt.forms.findreplace.Ui_Dialog() + self.form.setupUi(self) + self.setWindowModality(Qt.WindowModal) + + self._find_history = restore_combo_history( + self.form.find, self.COMBO_NAME + "Find" + ) + self.form.find.completer().setCaseSensitivity(Qt.CaseSensitive) + self._replace_history = restore_combo_history( + self.form.replace, self.COMBO_NAME + "Replace" + ) + self.form.replace.completer().setCaseSensitivity(Qt.CaseSensitive) + + restore_is_checked(self.form.re, self.COMBO_NAME + "Regex") + restore_is_checked(self.form.ignoreCase, self.COMBO_NAME + "ignoreCase") + + self.form.field.addItems(self.field_names) + restore_combo_index_for_session( + self.form.field, self.field_names, self.COMBO_NAME + "Field" + ) + + qconnect(self.form.buttonBox.helpRequested, self.show_help) + + restoreGeom(self, "findreplace") + self.show() + self.form.find.setFocus() + + def accept(self) -> None: + saveGeom(self, "findreplace") + save_combo_index_for_session(self.form.field, self.COMBO_NAME + "Field") + + search = save_combo_history( + self.form.find, self._find_history, self.COMBO_NAME + "Find" + ) + replace = save_combo_history( + self.form.replace, self._replace_history, self.COMBO_NAME + "Replace" + ) + regex = self.form.re.isChecked() + match_case = not self.form.ignoreCase.isChecked() + save_is_checked(self.form.re, self.COMBO_NAME + "Regex") + save_is_checked(self.form.ignoreCase, self.COMBO_NAME + "ignoreCase") + + # tags? + if self.form.field.currentIndex() == 1: + find_and_replace_tag( + parent=self.parentWidget(), + note_ids=self.note_ids, + search=search, + replacement=replace, + regex=regex, + match_case=match_case, + ) + else: + # fields + if self.form.field.currentIndex() == 0: + field = None + else: + field = self.field_names[self.form.field.currentIndex() - 2] + + find_and_replace( + parent=self.parentWidget(), + note_ids=self.note_ids, + search=search, + replacement=replace, + regex=regex, + field_name=field, + match_case=match_case, + ) + + super().accept() + + def show_help(self) -> None: + openHelp(HelpPage.BROWSING_FIND_AND_REPLACE) diff --git a/qt/aqt/find_and_replace.py b/qt/aqt/find_and_replace.py deleted file mode 100644 index 4f6b14e3f..000000000 --- a/qt/aqt/find_and_replace.py +++ /dev/null @@ -1,128 +0,0 @@ -# Copyright: Ankitects Pty Ltd and contributors -# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html - -from __future__ import annotations - -from typing import List, Sequence - -import aqt -from anki.notes import NoteId -from aqt import AnkiQt, QWidget -from aqt.operations.note import find_and_replace -from aqt.operations.tag import find_and_replace_tag -from aqt.qt import QDialog, Qt -from aqt.utils import ( - HelpPage, - disable_help_button, - openHelp, - qconnect, - restore_combo_history, - restore_combo_index_for_session, - restore_is_checked, - restoreGeom, - save_combo_history, - save_combo_index_for_session, - save_is_checked, - saveGeom, - tr, -) - - -class FindAndReplaceDialog(QDialog): - COMBO_NAME = "BrowserFindAndReplace" - - def __init__( - self, parent: QWidget, *, mw: AnkiQt, note_ids: Sequence[NoteId] - ) -> None: - super().__init__(parent) - self.mw = mw - self.note_ids = note_ids - self.field_names: List[str] = [] - - # fetch field names and then show - mw.query_op( - lambda: mw.col.field_names_for_note_ids(note_ids), - success=self._show, - ) - - def _show(self, field_names: Sequence[str]) -> None: - # add "all fields" and "tags" to the top of the list - self.field_names = [ - tr.browsing_all_fields(), - tr.editing_tags(), - ] + list(field_names) - - disable_help_button(self) - self.form = aqt.forms.findreplace.Ui_Dialog() - self.form.setupUi(self) - self.setWindowModality(Qt.WindowModal) - - self._find_history = restore_combo_history( - self.form.find, self.COMBO_NAME + "Find" - ) - self.form.find.completer().setCaseSensitivity(Qt.CaseSensitive) - self._replace_history = restore_combo_history( - self.form.replace, self.COMBO_NAME + "Replace" - ) - self.form.replace.completer().setCaseSensitivity(Qt.CaseSensitive) - - restore_is_checked(self.form.re, self.COMBO_NAME + "Regex") - restore_is_checked(self.form.ignoreCase, self.COMBO_NAME + "ignoreCase") - - self.form.field.addItems(self.field_names) - restore_combo_index_for_session( - self.form.field, self.field_names, self.COMBO_NAME + "Field" - ) - - qconnect(self.form.buttonBox.helpRequested, self.show_help) - - restoreGeom(self, "findreplace") - self.show() - self.form.find.setFocus() - - def accept(self) -> None: - saveGeom(self, "findreplace") - save_combo_index_for_session(self.form.field, self.COMBO_NAME + "Field") - - search = save_combo_history( - self.form.find, self._find_history, self.COMBO_NAME + "Find" - ) - replace = save_combo_history( - self.form.replace, self._replace_history, self.COMBO_NAME + "Replace" - ) - regex = self.form.re.isChecked() - match_case = not self.form.ignoreCase.isChecked() - save_is_checked(self.form.re, self.COMBO_NAME + "Regex") - save_is_checked(self.form.ignoreCase, self.COMBO_NAME + "ignoreCase") - - # tags? - if self.form.field.currentIndex() == 1: - find_and_replace_tag( - parent=self.parentWidget(), - note_ids=self.note_ids, - search=search, - replacement=replace, - regex=regex, - match_case=match_case, - ) - else: - # fields - if self.form.field.currentIndex() == 0: - field = None - else: - field = self.field_names[self.form.field.currentIndex() - 2] - - find_and_replace( - parent=self.parentWidget(), - note_ids=self.note_ids, - search=search, - replacement=replace, - regex=regex, - field_name=field, - match_case=match_case, - ) - - super().accept() - - def show_help(self) -> None: - openHelp(HelpPage.BROWSING_FIND_AND_REPLACE) From e43f11339fa2fc85bb69e1cf774391a43de3764a Mon Sep 17 00:00:00 2001 From: RumovZ Date: Tue, 13 Apr 2021 11:30:32 +0200 Subject: [PATCH 3/4] Declare all legacy pathnames in browser init --- qt/aqt/browser/__init__.py | 2 ++ qt/aqt/browser/sidebar/__init__.py | 7 ------- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/qt/aqt/browser/__init__.py b/qt/aqt/browser/__init__.py index 15e42606f..4ce6d4e92 100644 --- a/qt/aqt/browser/__init__.py +++ b/qt/aqt/browser/__init__.py @@ -37,3 +37,5 @@ from .table import ( # aliases for legacy pathnames sys.modules["aqt.find_and_replace"] = sys.modules["aqt.browser.dialogs"] aqt.find_and_replace = sys.modules["aqt.browser.dialogs"] # type: ignore +sys.modules["aqt.sidebar"] = sys.modules["aqt.browser.sidebar"] +aqt.sidebar = sys.modules["aqt.browser.sidebar"] # type: ignore diff --git a/qt/aqt/browser/sidebar/__init__.py b/qt/aqt/browser/sidebar/__init__.py index 5eaef6809..f7c73c32a 100644 --- a/qt/aqt/browser/sidebar/__init__.py +++ b/qt/aqt/browser/sidebar/__init__.py @@ -1,8 +1,5 @@ # Copyright: Ankitects Pty Ltd and contributors # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html -import sys - -import aqt from anki.utils import isMac from aqt.theme import theme_manager @@ -16,7 +13,3 @@ from .model import SidebarModel from .searchbar import SidebarSearchBar from .toolbar import SidebarTool, SidebarToolbar from .tree import SidebarStage, SidebarTreeView - -# alias for the legacy pathname -sys.modules["aqt.sidebar"] = sys.modules["aqt.browser.sidebar"] -aqt.sidebar = sys.modules["aqt.browser.sidebar"] # type: ignore From 594a98eff17c12029e98a249ab7587f89e412d19 Mon Sep 17 00:00:00 2001 From: RumovZ Date: Tue, 13 Apr 2021 11:38:35 +0200 Subject: [PATCH 4/4] Move previewer.py into browser folder --- qt/aqt/browser/__init__.py | 3 +++ qt/aqt/browser/browser.py | 4 ++-- qt/aqt/{ => browser}/previewer.py | 0 3 files changed, 5 insertions(+), 2 deletions(-) rename qt/aqt/{ => browser}/previewer.py (100%) diff --git a/qt/aqt/browser/__init__.py b/qt/aqt/browser/__init__.py index 4ce6d4e92..6d1de886b 100644 --- a/qt/aqt/browser/__init__.py +++ b/qt/aqt/browser/__init__.py @@ -8,6 +8,7 @@ import aqt from .browser import Browser from .dialogs import CardInfoDialog, ChangeModel, FindAndReplaceDialog, FindDupesDialog +from .previewer import BrowserPreviewer, MultiCardPreviewer, Previewer from .sidebar import ( SidebarItem, SidebarItemType, @@ -39,3 +40,5 @@ sys.modules["aqt.find_and_replace"] = sys.modules["aqt.browser.dialogs"] aqt.find_and_replace = sys.modules["aqt.browser.dialogs"] # type: ignore sys.modules["aqt.sidebar"] = sys.modules["aqt.browser.sidebar"] aqt.sidebar = sys.modules["aqt.browser.sidebar"] # type: ignore +sys.modules["aqt.previewer"] = sys.modules["aqt.browser.previewer"] +aqt.previewer = sys.modules["aqt.browser.previewer"] # type: ignore diff --git a/qt/aqt/browser/browser.py b/qt/aqt/browser/browser.py index 71623f12b..e053bf37d 100644 --- a/qt/aqt/browser/browser.py +++ b/qt/aqt/browser/browser.py @@ -24,6 +24,8 @@ from aqt.browser.dialogs import ( FindAndReplaceDialog, FindDupesDialog, ) +from aqt.browser.previewer import BrowserPreviewer as PreviewDialog +from aqt.browser.previewer import Previewer from aqt.browser.sidebar import SidebarTreeView from aqt.browser.table import Table from aqt.editor import Editor @@ -44,8 +46,6 @@ from aqt.operations.tag import ( clear_unused_tags, remove_tags_from_notes, ) -from aqt.previewer import BrowserPreviewer as PreviewDialog -from aqt.previewer import Previewer from aqt.qt import * from aqt.switch import Switch from aqt.utils import ( diff --git a/qt/aqt/previewer.py b/qt/aqt/browser/previewer.py similarity index 100% rename from qt/aqt/previewer.py rename to qt/aqt/browser/previewer.py