From 9d3da3b84424dd34582f22eeabc87407a84b5b1f Mon Sep 17 00:00:00 2001 From: RumovZ Date: Thu, 25 Feb 2021 11:02:17 +0100 Subject: [PATCH 01/57] Add icons for sidebar tools --- qt/aqt/forms/icons.qrc | 2 + qt/aqt/forms/icons/magnifying_glass.svg | 82 ++++++++++++ qt/aqt/forms/icons/select.svg | 159 ++++++++++++++++++++++++ 3 files changed, 243 insertions(+) create mode 100644 qt/aqt/forms/icons/magnifying_glass.svg create mode 100644 qt/aqt/forms/icons/select.svg diff --git a/qt/aqt/forms/icons.qrc b/qt/aqt/forms/icons.qrc index 277e01c9a..23dd5f5c5 100644 --- a/qt/aqt/forms/icons.qrc +++ b/qt/aqt/forms/icons.qrc @@ -10,5 +10,7 @@ icons/clock.svg icons/card-state.svg icons/flag.svg + icons/select.svg + icons/magnifying_glass.svg diff --git a/qt/aqt/forms/icons/magnifying_glass.svg b/qt/aqt/forms/icons/magnifying_glass.svg new file mode 100644 index 000000000..e9b4840a0 --- /dev/null +++ b/qt/aqt/forms/icons/magnifying_glass.svg @@ -0,0 +1,82 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/qt/aqt/forms/icons/select.svg b/qt/aqt/forms/icons/select.svg new file mode 100644 index 000000000..cf6925454 --- /dev/null +++ b/qt/aqt/forms/icons/select.svg @@ -0,0 +1,159 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + From 17afcb094f9d19ec453590dfe8c585eb43c8e174 Mon Sep 17 00:00:00 2001 From: RumovZ Date: Thu, 25 Feb 2021 11:03:57 +0100 Subject: [PATCH 02/57] Add toolbar to sidebar --- qt/aqt/browser.py | 4 +++- qt/aqt/sidebar.py | 24 ++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/qt/aqt/browser.py b/qt/aqt/browser.py index 23703342b..13c293412 100644 --- a/qt/aqt/browser.py +++ b/qt/aqt/browser.py @@ -28,7 +28,7 @@ from aqt.previewer import BrowserPreviewer as PreviewDialog from aqt.previewer import Previewer from aqt.qt import * from aqt.scheduling import forget_cards, set_due_date_dialog -from aqt.sidebar import SidebarSearchBar, SidebarTreeView +from aqt.sidebar import SidebarSearchBar, SidebarToolbar, SidebarTreeView from aqt.theme import theme_manager from aqt.utils import ( TR, @@ -940,12 +940,14 @@ QTableView {{ gridline-color: {grid} }} self.sidebar = SidebarTreeView(self) self.sidebarTree = self.sidebar # legacy alias dw.setWidget(self.sidebar) + self.sidebar.toolbar = toolbar = SidebarToolbar(self.sidebar) self.sidebar.searchBar = searchBar = SidebarSearchBar(self.sidebar) qconnect( self.form.actionSidebarFilter.triggered, self.focusSidebarSearchBar, ) l = QVBoxLayout() + l.addWidget(toolbar) l.addWidget(searchBar) l.addWidget(self.sidebar) l.setContentsMargins(0, 0, 0, 0) diff --git a/qt/aqt/sidebar.py b/qt/aqt/sidebar.py index 41856e3c7..4e3cef208 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -29,6 +29,11 @@ from aqt.utils import ( ) +class SidebarTool(Enum): + SELECT = auto() + SEARCH = auto() + + class SidebarItemType(Enum): ROOT = auto() SAVED_SEARCH_ROOT = auto() @@ -238,6 +243,25 @@ class SidebarModel(QAbstractItemModel): return cast(Qt.ItemFlags, flags) +class SidebarToolbar(QToolBar): + _tools: Tuple[SidebarTool, str, str] = ( + (SidebarTool.SELECT, ":/icons/select.svg", "select"), + (SidebarTool.SEARCH, ":/icons/magnifying_glass.svg", "search"), + ) + + 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._add_tools() + + def _add_tools(self) -> None: + for row in self._tools: + action = self.addAction(theme_manager.icon_from_resources(row[1]), row[2]) + action.setCheckable(True) + self._action_group.addAction(action) + class SidebarSearchBar(QLineEdit): def __init__(self, sidebar: SidebarTreeView) -> None: QLineEdit.__init__(self, sidebar) From fd784adc31f365ca1e0a0eae2f4ddc5776d3ba93 Mon Sep 17 00:00:00 2001 From: RumovZ Date: Thu, 25 Feb 2021 11:06:59 +0100 Subject: [PATCH 03/57] Add select and search modes to sidebar --- qt/aqt/sidebar.py | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/qt/aqt/sidebar.py b/qt/aqt/sidebar.py index 4e3cef208..c434585c9 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -262,6 +262,11 @@ class SidebarToolbar(QToolBar): action.setCheckable(True) self._action_group.addAction(action) + def _on_action_group_triggered(self, action) -> None: + tool = self._tools[self._action_group.actions().index(action)][0] + self.sidebar.tool = tool + + class SidebarSearchBar(QLineEdit): def __init__(self, sidebar: SidebarTreeView) -> None: QLineEdit.__init__(self, sidebar) @@ -340,9 +345,6 @@ class SidebarTreeView(QTreeView): self.setHeaderHidden(True) self.setIndentation(15) self.setAutoExpandDelay(600) - # pylint: disable=no-member - # mode = QAbstractItemView.SelectionMode.ExtendedSelection # type: ignore - # self.setSelectionMode(mode) self.setDragDropMode(QAbstractItemView.InternalMove) self.setDragDropOverwriteMode(False) @@ -363,6 +365,23 @@ class SidebarTreeView(QTreeView): self.setStyleSheet("QTreeView { %s }" % ";".join(styles)) + @property + def tool(self) -> SidebarTool: + return self._tool + + @tool.setter + def tool(self, tool: SidebarTool) -> None: + if self._tool == tool: + return + self._tool = tool + if tool == SidebarTool.SELECT: + # pylint: disable=no-member + mode = QAbstractItemView.SelectionMode.ExtendedSelection # type: ignore + elif tool == SidebarTool.SEARCH: + # pylint: disable=no-member + mode = QAbstractItemView.SelectionMode.SingleSelection # type: ignore + self.setSelectionMode(mode) + def model(self) -> SidebarModel: return super().model() @@ -465,7 +484,7 @@ class SidebarTreeView(QTreeView): def mouseReleaseEvent(self, event: QMouseEvent) -> None: super().mouseReleaseEvent(event) - if event.button() == Qt.LeftButton: + if self.tool == SidebarTool.SEARCH and event.button() == Qt.LeftButton: idx = self.indexAt(event.pos()) self._on_click_index(idx) From 0889972bb0094a76ac06aa2bf9acf7e84fe90bc5 Mon Sep 17 00:00:00 2001 From: RumovZ Date: Thu, 25 Feb 2021 11:35:31 +0100 Subject: [PATCH 04/57] Save last sidebar tool --- qt/aqt/sidebar.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/qt/aqt/sidebar.py b/qt/aqt/sidebar.py index c434585c9..e16a8cf90 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -254,17 +254,22 @@ class SidebarToolbar(QToolBar): self.sidebar = sidebar self._action_group = QActionGroup(self) qconnect(self._action_group.triggered, self._on_action_group_triggered) - self._add_tools() + self._setup_tools() - def _add_tools(self) -> None: + def _setup_tools(self) -> None: for row in self._tools: action = self.addAction(theme_manager.icon_from_resources(row[1]), row[2]) action.setCheckable(True) 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) -> None: - tool = self._tools[self._action_group.actions().index(action)][0] - self.sidebar.tool = tool + index = self._action_group.actions().index(action) + self.sidebar.col.set_config("sidebarTool", index) + self.sidebar.tool = self._tools[index][0] class SidebarSearchBar(QLineEdit): @@ -371,8 +376,6 @@ class SidebarTreeView(QTreeView): @tool.setter def tool(self, tool: SidebarTool) -> None: - if self._tool == tool: - return self._tool = tool if tool == SidebarTool.SELECT: # pylint: disable=no-member From 4a1e9959341905bc3411fccd21dc896d041a2c8f Mon Sep 17 00:00:00 2001 From: RumovZ Date: Thu, 25 Feb 2021 13:12:51 +0100 Subject: [PATCH 05/57] Add edit mode in sidebar --- qt/aqt/forms/icons.qrc | 1 + qt/aqt/forms/icons/edit.svg | 74 +++++++++++++++++++++++++++++++++++++ qt/aqt/sidebar.py | 15 ++++++-- 3 files changed, 86 insertions(+), 4 deletions(-) create mode 100644 qt/aqt/forms/icons/edit.svg diff --git a/qt/aqt/forms/icons.qrc b/qt/aqt/forms/icons.qrc index 23dd5f5c5..e11359d8a 100644 --- a/qt/aqt/forms/icons.qrc +++ b/qt/aqt/forms/icons.qrc @@ -12,5 +12,6 @@ icons/flag.svg icons/select.svg icons/magnifying_glass.svg + icons/edit.svg diff --git a/qt/aqt/forms/icons/edit.svg b/qt/aqt/forms/icons/edit.svg new file mode 100644 index 000000000..8f8f98fd5 --- /dev/null +++ b/qt/aqt/forms/icons/edit.svg @@ -0,0 +1,74 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/qt/aqt/sidebar.py b/qt/aqt/sidebar.py index e16a8cf90..163d8aded 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -32,6 +32,7 @@ from aqt.utils import ( class SidebarTool(Enum): SELECT = auto() SEARCH = auto() + EDIT = auto() class SidebarItemType(Enum): @@ -247,6 +248,7 @@ class SidebarToolbar(QToolBar): _tools: Tuple[SidebarTool, str, str] = ( (SidebarTool.SELECT, ":/icons/select.svg", "select"), (SidebarTool.SEARCH, ":/icons/magnifying_glass.svg", "search"), + (SidebarTool.EDIT, ":/icons/edit.svg", "edit"), ) def __init__(self, sidebar: SidebarTreeView) -> None: @@ -350,7 +352,6 @@ class SidebarTreeView(QTreeView): self.setHeaderHidden(True) self.setIndentation(15) self.setAutoExpandDelay(600) - self.setDragDropMode(QAbstractItemView.InternalMove) self.setDragDropOverwriteMode(False) qconnect(self.expanded, self._on_expansion) @@ -379,11 +380,17 @@ class SidebarTreeView(QTreeView): self._tool = tool if tool == SidebarTool.SELECT: # pylint: disable=no-member - mode = QAbstractItemView.SelectionMode.ExtendedSelection # type: ignore + selection_mode = QAbstractItemView.ExtendedSelection # type: ignore + drag_drop_mode = QAbstractItemView.NoDragDrop elif tool == SidebarTool.SEARCH: # pylint: disable=no-member - mode = QAbstractItemView.SelectionMode.SingleSelection # type: ignore - self.setSelectionMode(mode) + selection_mode = QAbstractItemView.SingleSelection # type: ignore + drag_drop_mode = QAbstractItemView.NoDragDrop + elif tool == SidebarTool.EDIT: + selection_mode = QAbstractItemView.SingleSelection # type: ignore + drag_drop_mode = QAbstractItemView.InternalMove + self.setSelectionMode(selection_mode) + self.setDragDropMode(drag_drop_mode) def model(self) -> SidebarModel: return super().model() From 47e1e6296756211e057869c1b26eda7ad0167edd Mon Sep 17 00:00:00 2001 From: RumovZ Date: Thu, 25 Feb 2021 19:28:29 +0100 Subject: [PATCH 06/57] Make search first (default) mode --- qt/aqt/sidebar.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/qt/aqt/sidebar.py b/qt/aqt/sidebar.py index 163d8aded..56f55ee0f 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -245,9 +245,9 @@ class SidebarModel(QAbstractItemModel): class SidebarToolbar(QToolBar): - _tools: Tuple[SidebarTool, str, str] = ( - (SidebarTool.SELECT, ":/icons/select.svg", "select"), + _tools: Tuple[Tuple[SidebarTool, str, str], ...] = ( (SidebarTool.SEARCH, ":/icons/magnifying_glass.svg", "search"), + (SidebarTool.SELECT, ":/icons/select.svg", "select"), (SidebarTool.EDIT, ":/icons/edit.svg", "edit"), ) @@ -268,7 +268,7 @@ class SidebarToolbar(QToolBar): self._action_group.actions()[active].setChecked(True) self.sidebar.tool = self._tools[active][0] - def _on_action_group_triggered(self, action) -> None: + 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] From 172133299bc8ddb61cca8ba5ba7c3d68f73175f2 Mon Sep 17 00:00:00 2001 From: RumovZ Date: Thu, 25 Feb 2021 19:57:12 +0100 Subject: [PATCH 07/57] Handle search on event level Instead of assigning each sidebar item a lambda, add a field for search representation and handle searching in event handler. --- qt/aqt/sidebar.py | 86 ++++++++++++++++++++++------------------------- 1 file changed, 40 insertions(+), 46 deletions(-) diff --git a/qt/aqt/sidebar.py b/qt/aqt/sidebar.py index 56f55ee0f..7ae322892 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -81,6 +81,7 @@ class SidebarItem: name: str, icon: Union[str, ColoredIcon], on_click: Callable[[], None] = None, + search_node: Optional[SearchNode] = None, on_expanded: Callable[[bool], None] = None, expanded: bool = False, item_type: SidebarItemType = SidebarItemType.CUSTOM, @@ -95,6 +96,7 @@ class SidebarItem: self.item_type = item_type self.id = id self.on_click = on_click + self.search_node = search_node self.on_expanded = on_expanded self.children: List["SidebarItem"] = [] self.tooltip: Optional[str] = None @@ -113,7 +115,7 @@ class SidebarItem: name: Union[str, TR.V], icon: Union[str, ColoredIcon], type: SidebarItemType, - on_click: Callable[[], None], + search_node: Optional[SearchNode], ) -> SidebarItem: "Add child sidebar item, and return it." if not isinstance(name, str): @@ -121,7 +123,7 @@ class SidebarItem: item = SidebarItem( name=name, icon=icon, - on_click=on_click, + search_node=search_node, item_type=type, ) self.add_child(item) @@ -575,6 +577,8 @@ class SidebarTreeView(QTreeView): if item := self.model().item_for_index(idx): if item.on_click: item.on_click() + elif self.tool == SidebarTool.SEARCH and (search := item.search_node): + self.update_search(search) def _on_expansion(self, idx: QModelIndex) -> None: if self.current_search: @@ -651,9 +655,6 @@ class SidebarTreeView(QTreeView): return top - def _filter_func(self, *terms: Union[str, SearchNode]) -> Callable: - return lambda: self.update_search(*terms) - # Tree: Saved Searches ########################### @@ -678,7 +679,7 @@ class SidebarTreeView(QTreeView): item = SidebarItem( name, icon, - self._filter_func(filt), + search_node=SearchNode(parsable_text=filt), item_type=SidebarItemType.SAVED_SEARCH, ) root.add_child(item) @@ -696,47 +697,42 @@ class SidebarTreeView(QTreeView): type=SidebarItemType.TODAY_ROOT, ) type = SidebarItemType.TODAY - search = self._filter_func root.add_simple( name=TR.BROWSING_SIDEBAR_DUE_TODAY, icon=icon, type=type, - on_click=search(SearchNode(due_on_day=0)), + search_node=SearchNode(due_on_day=0), ) root.add_simple( name=TR.BROWSING_ADDED_TODAY, icon=icon, type=type, - on_click=search(SearchNode(added_in_days=1)), + search_node=SearchNode(added_in_days=1), ) root.add_simple( name=TR.BROWSING_EDITED_TODAY, icon=icon, type=type, - on_click=search(SearchNode(edited_in_days=1)), + search_node=SearchNode(edited_in_days=1), ) root.add_simple( name=TR.BROWSING_STUDIED_TODAY, icon=icon, type=type, - on_click=search(SearchNode(rated=SearchNode.Rated(days=1))), + search_node=SearchNode(rated=SearchNode.Rated(days=1)), ) root.add_simple( name=TR.BROWSING_AGAIN_TODAY, icon=icon, type=type, - on_click=search( - SearchNode( - rated=SearchNode.Rated(days=1, rating=SearchNode.RATING_AGAIN) - ) - ), + search_node=SearchNode(rated=SearchNode.Rated(days=1, rating=SearchNode.RATING_AGAIN)), ) root.add_simple( name=TR.BROWSING_SIDEBAR_OVERDUE, icon=icon, type=type, - on_click=search( + search_node=self.col.group_searches( SearchNode(card_state=SearchNode.CARD_STATE_DUE), SearchNode(negated=SearchNode(due_on_day=0)), ), @@ -755,38 +751,37 @@ class SidebarTreeView(QTreeView): 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(SearchNode(card_state=SearchNode.CARD_STATE_NEW)), + search_node=SearchNode(card_state=SearchNode.CARD_STATE_NEW), ) root.add_simple( name=TR.SCHEDULING_LEARNING, icon=icon.with_color(colors.LEARN_COUNT), type=type, - on_click=search(SearchNode(card_state=SearchNode.CARD_STATE_LEARN)), + search_node=SearchNode(card_state=SearchNode.CARD_STATE_LEARN), ) root.add_simple( name=TR.SCHEDULING_REVIEW, icon=icon.with_color(colors.REVIEW_COUNT), type=type, - on_click=search(SearchNode(card_state=SearchNode.CARD_STATE_REVIEW)), + search_node=SearchNode(card_state=SearchNode.CARD_STATE_REVIEW), ) root.add_simple( name=TR.BROWSING_SUSPENDED, icon=icon.with_color(colors.SUSPENDED_FG), type=type, - on_click=search(SearchNode(card_state=SearchNode.CARD_STATE_SUSPENDED)), + search_node=SearchNode(card_state=SearchNode.CARD_STATE_SUSPENDED), ) root.add_simple( name=TR.BROWSING_BURIED, icon=icon.with_color(colors.BURIED_FG), type=type, - on_click=search(SearchNode(card_state=SearchNode.CARD_STATE_BURIED)), + search_node=SearchNode(card_state=SearchNode.CARD_STATE_BURIED), ) # Tree: Flags @@ -794,7 +789,6 @@ class SidebarTreeView(QTreeView): def _flags_tree(self, root: SidebarItem) -> None: icon = ColoredIcon(path=":/icons/flag.svg", color=colors.DISABLED) - search = self._filter_func root = self._section_root( root=root, name=TR.BROWSING_SIDEBAR_FLAGS, @@ -802,38 +796,38 @@ class SidebarTreeView(QTreeView): collapse_key=Config.Bool.COLLAPSE_FLAGS, type=SidebarItemType.FLAG_ROOT, ) - root.on_click = search(SearchNode(flag=SearchNode.FLAG_ANY)) + root.search_node = SearchNode(flag=SearchNode.FLAG_ANY) type = SidebarItemType.FLAG root.add_simple( TR.ACTIONS_RED_FLAG, icon=icon.with_color(colors.FLAG1_FG), type=type, - on_click=search(SearchNode(flag=SearchNode.FLAG_RED)), + search_node=SearchNode(flag=SearchNode.FLAG_RED), ) root.add_simple( TR.ACTIONS_ORANGE_FLAG, icon=icon.with_color(colors.FLAG2_FG), type=type, - on_click=search(SearchNode(flag=SearchNode.FLAG_ORANGE)), + search_node=SearchNode(flag=SearchNode.FLAG_ORANGE), ) root.add_simple( TR.ACTIONS_GREEN_FLAG, icon=icon.with_color(colors.FLAG3_FG), type=type, - on_click=search(SearchNode(flag=SearchNode.FLAG_GREEN)), + search_node=SearchNode(flag=SearchNode.FLAG_GREEN), ) root.add_simple( TR.ACTIONS_BLUE_FLAG, icon=icon.with_color(colors.FLAG4_FG), type=type, - on_click=search(SearchNode(flag=SearchNode.FLAG_BLUE)), + search_node=SearchNode(flag=SearchNode.FLAG_BLUE), ) root.add_simple( TR.BROWSING_NO_FLAG, icon=icon.with_color(colors.DISABLED), type=type, - on_click=search(SearchNode(flag=SearchNode.FLAG_NONE)), + search_node=SearchNode(flag=SearchNode.FLAG_NONE), ) # Tree: Tags @@ -854,11 +848,11 @@ class SidebarTreeView(QTreeView): ) item = SidebarItem( - node.name, - icon, - self._filter_func(SearchNode(tag=head + node.name)), - toggle_expand(), - node.expanded, + name=node.name, + icon=icon, + search_node=SearchNode(tag=head + node.name), + on_expanded=toggle_expand(), + expanded=node.expanded, item_type=SidebarItemType.TAG, full_name=head + node.name, ) @@ -874,12 +868,12 @@ class SidebarTreeView(QTreeView): collapse_key=Config.Bool.COLLAPSE_TAGS, type=SidebarItemType.TAG_ROOT, ) - root.on_click = self._filter_func(SearchNode(negated=SearchNode(tag="none"))) + root.search_node = SearchNode(negated=SearchNode(tag="none")) root.add_simple( name=tr(TR.BROWSING_SIDEBAR_UNTAGGED), icon=icon, type=SidebarItemType.TAG_NONE, - on_click=self._filter_func(SearchNode(tag="none")), + search_node=SearchNode(tag="none"), ) render(root, tree.children) @@ -900,11 +894,11 @@ class SidebarTreeView(QTreeView): return lambda _: self.mw.col.decks.collapseBrowser(did) item = SidebarItem( - node.name, - icon, - self._filter_func(SearchNode(deck=head + node.name)), - toggle_expand(), - not node.collapsed, + name=node.name, + icon=icon, + search_node=SearchNode(deck=head + node.name), + on_expanded=toggle_expand(), + expanded=not node.collapsed, item_type=SidebarItemType.DECK, id=node.deck_id, full_name=head + node.name, @@ -921,12 +915,12 @@ class SidebarTreeView(QTreeView): collapse_key=Config.Bool.COLLAPSE_DECKS, type=SidebarItemType.DECK_ROOT, ) - root.on_click = self._filter_func(SearchNode(deck="*")) + root.search_node = SearchNode(deck="*") current = root.add_simple( name=tr(TR.BROWSING_CURRENT_DECK), icon=icon, type=SidebarItemType.DECK, - on_click=self._filter_func(SearchNode(deck="current")), + search_node=SearchNode(deck="current"), ) current.id = self.mw.col.decks.selected() @@ -949,7 +943,7 @@ class SidebarTreeView(QTreeView): item = SidebarItem( nt["name"], icon, - self._filter_func(SearchNode(note=nt["name"])), + search_node=SearchNode(note=nt["name"]), item_type=SidebarItemType.NOTETYPE, id=nt["id"], ) @@ -958,7 +952,7 @@ class SidebarTreeView(QTreeView): child = SidebarItem( tmpl["name"], icon, - self._filter_func( + search_node=self.col.group_searches( SearchNode(note=nt["name"]), SearchNode(template=c) ), item_type=SidebarItemType.NOTETYPE_TEMPLATE, From f5981e94bf68bcb6df609f6a942c74644b990010 Mon Sep 17 00:00:00 2001 From: RumovZ Date: Thu, 25 Feb 2021 21:24:11 +0100 Subject: [PATCH 08/57] Add group search context action --- ftl/core/actions.ftl | 2 ++ qt/aqt/sidebar.py | 57 +++++++++++++++++++++++++++++--------------- 2 files changed, 40 insertions(+), 19 deletions(-) diff --git a/ftl/core/actions.ftl b/ftl/core/actions.ftl index 392ee383a..bbeec3fc5 100644 --- a/ftl/core/actions.ftl +++ b/ftl/core/actions.ftl @@ -1,4 +1,6 @@ actions-add = Add +actions-all-selected = All selected +actions-any-selected = Any selected actions-blue-flag = Blue Flag actions-cancel = Cancel actions-choose = Choose diff --git a/qt/aqt/sidebar.py b/qt/aqt/sidebar.py index 7ae322892..94985204e 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -8,7 +8,7 @@ from enum import Enum, auto from typing import Any, Dict, Iterable, List, Optional, Sequence, Tuple, cast import aqt -from anki.collection import Config, SearchNode +from anki.collection import Config, SearchJoiner, SearchNode from anki.decks import DeckTreeNode from anki.errors import DeckRenameError, InvalidInput from anki.tags import TagTreeNode @@ -446,11 +446,15 @@ class SidebarTreeView(QTreeView): if item.is_expanded(searching): self.setExpanded(idx, True) - def update_search(self, *terms: Union[str, SearchNode]) -> None: + def update_search( + self, + *terms: Union[str, SearchNode], + joiner: SearchJoiner = "AND", + ) -> None: """Modify the current search string based on modifier keys, then refresh.""" mods = self.mw.app.keyboardModifiers() previous = SearchNode(parsable_text=self.browser.current_search()) - current = self.mw.col.group_searches(*terms) + current = self.mw.col.group_searches(*terms, joiner=joiner) # if Alt pressed, invert if mods & Qt.AltModifier: @@ -489,9 +493,8 @@ class SidebarTreeView(QTreeView): def dropEvent(self, event: QDropEvent) -> None: model = self.model() - source_items = [model.item_for_index(idx) for idx in self.selectedIndexes()] target_item = model.item_for_index(self.indexAt(event.pos())) - if self.handle_drag_drop(source_items, target_item): + if self.handle_drag_drop(self._selected_items(), target_item): event.acceptProposedAction() def mouseReleaseEvent(self, event: QMouseEvent) -> None: @@ -726,7 +729,9 @@ class SidebarTreeView(QTreeView): name=TR.BROWSING_AGAIN_TODAY, icon=icon, type=type, - search_node=SearchNode(rated=SearchNode.Rated(days=1, rating=SearchNode.RATING_AGAIN)), + search_node=SearchNode( + rated=SearchNode.Rated(days=1, rating=SearchNode.RATING_AGAIN) + ), ) root.add_simple( name=TR.BROWSING_SIDEBAR_OVERDUE, @@ -984,27 +989,35 @@ class SidebarTreeView(QTreeView): a = m.addAction(act_name) qconnect(a.triggered, lambda _, func=act_func: func(item)) + self._maybe_add_search_actions(m) + if idx: self.maybe_add_tree_actions(m, item, idx) if not m.children(): return - # until we support multiple selection, show user that only the current - # item is being operated on by clearing the selection - if idx: - sm = self.selectionModel() - sm.clear() - sm.select( - idx, - cast( - QItemSelectionModel.SelectionFlag, - QItemSelectionModel.SelectCurrent | QItemSelectionModel.Rows, - ), - ) - m.exec_(QCursor.pos()) + def _maybe_add_search_actions(self, menu: QMenu) -> None: + nodes = [ + item.search_node for item in self._selected_items() if item.search_node + ] + if not nodes: + return + menu.addSeparator() + if len(nodes) == 1: + menu.addAction(tr(TR.ACTIONS_SEARCH), lambda: self.update_search(*nodes)) + return + sub_menu = menu.addMenu(tr(TR.ACTIONS_SEARCH)) + sub_menu.addAction( + tr(TR.ACTIONS_ALL_SELECTED), lambda: self.update_search(*nodes) + ) + sub_menu.addAction( + tr(TR.ACTIONS_ANY_SELECTED), + lambda: self.update_search(*nodes, joiner="OR"), + ) + def maybe_add_tree_actions( self, menu: QMenu, item: SidebarItem, parent: QModelIndex ) -> None: @@ -1170,3 +1183,9 @@ class SidebarTreeView(QTreeView): Models( self.mw, parent=self.browser, fromMain=True, selected_notetype_id=item.id ) + + # Helpers + ################## + + def _selected_items(self) -> List[SidebarItem]: + return [self.model().item_for_index(idx) for idx in self.selectedIndexes()] From 2c256459757dd9695e0b2b318f3d82d2b27e02d5 Mon Sep 17 00:00:00 2001 From: RumovZ Date: Fri, 26 Feb 2021 13:04:30 +0100 Subject: [PATCH 09/57] Place sidebar tools right of search bar --- qt/aqt/browser.py | 14 +++++++------- qt/aqt/sidebar.py | 1 + 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/qt/aqt/browser.py b/qt/aqt/browser.py index 13c293412..43641d488 100644 --- a/qt/aqt/browser.py +++ b/qt/aqt/browser.py @@ -946,14 +946,14 @@ QTableView {{ gridline-color: {grid} }} self.form.actionSidebarFilter.triggered, self.focusSidebarSearchBar, ) - l = QVBoxLayout() - l.addWidget(toolbar) - l.addWidget(searchBar) - l.addWidget(self.sidebar) - l.setContentsMargins(0, 0, 0, 0) - l.setSpacing(0) + grid = QGridLayout() + grid.addWidget(searchBar, 0, 0) + grid.addWidget(toolbar, 0, 1) + grid.addWidget(self.sidebar, 1, 0, 1, 2) + grid.setContentsMargins(0, 0, 0, 0) + grid.setSpacing(0) w = QWidget() - w.setLayout(l) + w.setLayout(grid) dw.setWidget(w) self.sidebarDockWidget.setFloating(False) diff --git a/qt/aqt/sidebar.py b/qt/aqt/sidebar.py index 94985204e..52ced70b3 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -259,6 +259,7 @@ class SidebarToolbar(QToolBar): self._action_group = QActionGroup(self) qconnect(self._action_group.triggered, self._on_action_group_triggered) self._setup_tools() + self.setIconSize(QSize(18, 18)) def _setup_tools(self) -> None: for row in self._tools: From f7c20e40b5c389bf6df137d5ccf1997468c3f7a5 Mon Sep 17 00:00:00 2001 From: RumovZ Date: Fri, 26 Feb 2021 19:52:02 +0100 Subject: [PATCH 10/57] Make backend deck deletion take vec of ids --- rslib/backend.proto | 6 +++++- rslib/src/backend/generic.rs | 6 ++++++ rslib/src/backend/mod.rs | 4 ++-- rslib/src/decks/mod.rs | 21 +++++++++++---------- rslib/src/sync/mod.rs | 2 +- 5 files changed, 25 insertions(+), 14 deletions(-) diff --git a/rslib/backend.proto b/rslib/backend.proto index 4fe09b5fd..2300ceb7f 100644 --- a/rslib/backend.proto +++ b/rslib/backend.proto @@ -68,6 +68,10 @@ message DeckID { int64 did = 1; } +message DeckIDs { + repeated int64 dids = 1; +} + message DeckConfigID { int64 dcid = 1; } @@ -146,7 +150,7 @@ service BackendService { rpc GetDeckLegacy(DeckID) returns (Json); rpc GetDeckNames(GetDeckNamesIn) returns (DeckNames); rpc NewDeckLegacy(Bool) returns (Json); - rpc RemoveDeck(DeckID) returns (Empty); + rpc RemoveDecks(DeckIDs) returns (Empty); rpc DragDropDecks(DragDropDecksIn) returns (Empty); // deck config diff --git a/rslib/src/backend/generic.rs b/rslib/src/backend/generic.rs index 0b3665f62..19edbe04a 100644 --- a/rslib/src/backend/generic.rs +++ b/rslib/src/backend/generic.rs @@ -69,6 +69,12 @@ impl From for DeckID { } } +impl From for Vec { + fn from(dids: pb::DeckIDs) -> Self { + dids.dids.into_iter().map(DeckID).collect() + } +} + impl From for DeckConfID { fn from(dcid: pb::DeckConfigId) -> Self { DeckConfID(dcid.dcid) diff --git a/rslib/src/backend/mod.rs b/rslib/src/backend/mod.rs index 9dfece2c5..ba09fc9a4 100644 --- a/rslib/src/backend/mod.rs +++ b/rslib/src/backend/mod.rs @@ -898,8 +898,8 @@ impl BackendService for Backend { .map(Into::into) } - fn remove_deck(&self, input: pb::DeckId) -> BackendResult { - self.with_col(|col| col.remove_deck_and_child_decks(input.into())) + fn remove_decks(&self, input: pb::DeckIDs) -> BackendResult { + self.with_col(|col| col.remove_decks_and_child_decks(input.into())) .map(Into::into) } diff --git a/rslib/src/decks/mod.rs b/rslib/src/decks/mod.rs index 9c69af06d..ffd0b6a6f 100644 --- a/rslib/src/decks/mod.rs +++ b/rslib/src/decks/mod.rs @@ -442,22 +442,23 @@ impl Collection { self.storage.get_deck_id(&machine_name) } - pub fn remove_deck_and_child_decks(&mut self, did: DeckID) -> Result<()> { + pub fn remove_decks_and_child_decks(&mut self, dids: Vec) -> Result<()> { // fixme: vet cache clearing self.state.deck_cache.clear(); self.transact(None, |col| { let usn = col.usn()?; + for did in dids { + if let Some(deck) = col.storage.get_deck(did)? { + let child_decks = col.storage.child_decks(&deck)?; - if let Some(deck) = col.storage.get_deck(did)? { - let child_decks = col.storage.child_decks(&deck)?; - - // top level - col.remove_single_deck(&deck, usn)?; - - // remove children - for deck in child_decks { + // top level col.remove_single_deck(&deck, usn)?; + + // remove children + for deck in child_decks { + col.remove_single_deck(&deck, usn)?; + } } } Ok(()) @@ -775,7 +776,7 @@ mod test { // delete top level let top = col.get_or_create_normal_deck("one")?; - col.remove_deck_and_child_decks(top.id)?; + col.remove_decks_and_child_decks(vec![top.id])?; // should have come back as "Default+" due to conflict assert_eq!(sorted_names(&col), vec!["default", "Default+"]); diff --git a/rslib/src/sync/mod.rs b/rslib/src/sync/mod.rs index 48baa82f0..627a7facb 100644 --- a/rslib/src/sync/mod.rs +++ b/rslib/src/sync/mod.rs @@ -1522,7 +1522,7 @@ mod test { col1.remove_cards_and_orphaned_notes(&[cardid])?; let usn = col1.usn()?; col1.remove_note_only(noteid, usn)?; - col1.remove_deck_and_child_decks(deckid)?; + col1.remove_decks_and_child_decks(vec![deckid])?; let out = ctx.normal_sync(&mut col1).await; assert_eq!(out.required, SyncActionRequired::NoChanges); From 88c69665f3e2450698658d3ab819c1135ac8d5f8 Mon Sep 17 00:00:00 2001 From: RumovZ Date: Fri, 26 Feb 2021 19:52:34 +0100 Subject: [PATCH 11/57] Add support for multi deck deletion in python --- ftl/core/decks.ftl | 1 + pylib/anki/decks.py | 16 ++++++++++++---- qt/aqt/deckbrowser.py | 26 ++++++++++++++++---------- qt/aqt/sidebar.py | 19 +++++++++++++------ 4 files changed, 42 insertions(+), 20 deletions(-) diff --git a/ftl/core/decks.ftl b/ftl/core/decks.ftl index 222a993c2..b4b942b10 100644 --- a/ftl/core/decks.ftl +++ b/ftl/core/decks.ftl @@ -2,6 +2,7 @@ decks-add-new-deck-ctrlandn = Add New Deck (Ctrl+N) decks-are-you-sure-you-wish-to = Are you sure you wish to delete { $val }? decks-build = Build decks-cards-selected-by = cards selected by +decks-confirm-deletion = Are you sure you wish to delete { $deck_count } decks including { $card_count } cards? decks-create-deck = Create Deck decks-custom-steps-in-minutes = Custom steps (in minutes) decks-deck = Deck diff --git a/pylib/anki/decks.py b/pylib/anki/decks.py index 17efb366e..0ca2b6a7c 100644 --- a/pylib/anki/decks.py +++ b/pylib/anki/decks.py @@ -135,7 +135,10 @@ class DeckManager: if isinstance(did, str): did = int(did) assert cardsToo and childrenToo - self.col._backend.remove_deck(did) + self.remove([did]) + + def remove(self, dids: List[int]) -> None: + self.col._backend.remove_decks(dids) def all_names_and_ids( self, skip_empty_default: bool = False, include_filtered: bool = True @@ -212,10 +215,15 @@ class DeckManager: def count(self) -> int: return len(self.all_names_and_ids()) - def card_count(self, did: int, include_subdecks: bool) -> Any: - dids: List[int] = [did] + def card_count( + self, dids: Union[int, Iterable[int]], include_subdecks: bool + ) -> Any: + if isinstance(dids, int): + dids = {dids} + else: + dids = set(dids) if include_subdecks: - dids += [r[1] for r in self.children(did)] + dids.update([child[1] for did in dids for child in self.children(did)]) count = self.col.db.scalar( "select count() from cards where did in {0} or " "odid in {0}".format(ids2str(dids)) diff --git a/qt/aqt/deckbrowser.py b/qt/aqt/deckbrowser.py index cd446fec9..1b9463c07 100644 --- a/qt/aqt/deckbrowser.py +++ b/qt/aqt/deckbrowser.py @@ -5,7 +5,7 @@ from __future__ import annotations from concurrent.futures import Future from copy import deepcopy from dataclasses import dataclass -from typing import Any +from typing import Any, List import aqt from anki.decks import DeckTreeNode @@ -296,20 +296,26 @@ class DeckBrowser: self.show() def ask_delete_deck(self, did: int) -> bool: - deck = self.mw.col.decks.get(did) - if deck["dyn"]: + return self.ask_delete_decks([did]) + + def ask_delete_decks(self, dids: List[int]) -> bool: + decks = [self.mw.col.decks.get(did) for did in dids] + if all([deck["dyn"] for deck in decks]): return True - count = self.mw.col.decks.card_count(did, include_subdecks=True) + count = self.mw.col.decks.card_count(dids, include_subdecks=True) if not count: return True - extra = tr(TR.DECKS_IT_HAS_CARD, count=count) - if askUser( - f"{tr(TR.DECKS_ARE_YOU_SURE_YOU_WISH_TO, val=deck['name'])} {extra}" - ): - return True - return False + if len(dids) == 1: + extra = tr(TR.DECKS_IT_HAS_CARD, count=count) + return askUser( + f"{tr(TR.DECKS_ARE_YOU_SURE_YOU_WISH_TO, val=decks[0]['name'])} {extra}" + ) + + return askUser( + tr(TR.DECKS_CONFIRM_DELETION, deck_count=len(dids), card_count=count) + ) def _delete(self, did: int) -> None: if self.ask_delete_deck(did): diff --git a/qt/aqt/sidebar.py b/qt/aqt/sidebar.py index 52ced70b3..41577534d 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -1108,15 +1108,15 @@ class SidebarTreeView(QTreeView): self.browser.model.beginReset() self.mw.taskman.run_in_background(do_rename, on_done) - def delete_deck(self, item: SidebarItem) -> None: - self.browser.editor.saveNow(lambda: self._delete_deck(item)) + def delete_deck(self, _item: SidebarItem) -> None: + self.browser.editor.saveNow(self._delete_decks) - def _delete_deck(self, item: SidebarItem) -> None: - did = item.id - if self.mw.deckBrowser.ask_delete_deck(did): + def _delete_decks(self) -> None: + dids = self._selected_decks() + if self.mw.deckBrowser.ask_delete_decks(dids): def do_delete() -> None: - return self.mw.col.decks.rem(did, True) + return self.mw.col.decks.remove(dids) def on_done(fut: Future) -> None: self.mw.requireReset(reason=ResetReason.BrowserDeleteDeck, context=self) @@ -1190,3 +1190,10 @@ class SidebarTreeView(QTreeView): def _selected_items(self) -> List[SidebarItem]: return [self.model().item_for_index(idx) for idx in self.selectedIndexes()] + + def _selected_decks(self) -> List[int]: + return [ + item.id + for item in self._selected_items() + if item.item_type == SidebarItemType.DECK + ] From 0b83828508d7374b8881a488e27fff3334e1e755 Mon Sep 17 00:00:00 2001 From: RumovZ Date: Sun, 28 Feb 2021 21:03:19 +0100 Subject: [PATCH 12/57] Enable in-place editing of sidebar deck items --- qt/aqt/sidebar.py | 47 ++++++++++++++++++++++++++++++++++++----------- 1 file changed, 36 insertions(+), 11 deletions(-) diff --git a/qt/aqt/sidebar.py b/qt/aqt/sidebar.py index 41577534d..d9c5f46a9 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -3,6 +3,7 @@ from __future__ import annotations +import re from concurrent.futures import Future from enum import Enum, auto from typing import Any, Dict, Iterable, List, Optional, Sequence, Tuple, cast @@ -87,6 +88,7 @@ class SidebarItem: item_type: SidebarItemType = SidebarItemType.CUSTOM, id: int = 0, full_name: str = None, + editable: bool = False, ) -> None: self.name = name if not full_name: @@ -97,6 +99,7 @@ class SidebarItem: self.id = id self.on_click = on_click self.search_node = search_node + self.editable = editable self.on_expanded = on_expanded self.children: List["SidebarItem"] = [] self.tooltip: Optional[str] = None @@ -151,8 +154,9 @@ class SidebarItem: class SidebarModel(QAbstractItemModel): - def __init__(self, root: SidebarItem) -> None: + def __init__(self, sidebar: SidebarTreeView, root: SidebarItem) -> None: super().__init__() + self.sidebar = sidebar self.root = root self._cache_rows(root) @@ -214,17 +218,19 @@ class SidebarModel(QAbstractItemModel): if not index.isValid(): return QVariant() - if role not in (Qt.DisplayRole, Qt.DecorationRole, Qt.ToolTipRole): + if role not in (Qt.DisplayRole, Qt.DecorationRole, Qt.ToolTipRole, Qt.EditRole): return QVariant() item: SidebarItem = index.internalPointer() - if role == Qt.DisplayRole: + if role in (Qt.DisplayRole, Qt.EditRole): return QVariant(item.name) - elif role == Qt.ToolTipRole: + if role == Qt.ToolTipRole: return QVariant(item.tooltip) - else: - return QVariant(theme_manager.icon_from_resources(item.icon)) + return QVariant(theme_manager.icon_from_resources(item.icon)) + + def setData(self, index: QModelIndex, text: str, _role: int) -> bool: + return self.sidebar.rename_node(index.internalPointer(), text) def supportedDropActions(self) -> Qt.DropActions: return cast(Qt.DropActions, Qt.MoveAction) @@ -242,6 +248,8 @@ class SidebarModel(QAbstractItemModel): SidebarItemType.TAG_ROOT, ): flags |= Qt.ItemIsDragEnabled | Qt.ItemIsDropEnabled + if item.editable: + flags |= Qt.ItemIsEditable return cast(Qt.ItemFlags, flags) @@ -385,15 +393,19 @@ class SidebarTreeView(QTreeView): # pylint: disable=no-member selection_mode = QAbstractItemView.ExtendedSelection # type: ignore drag_drop_mode = QAbstractItemView.NoDragDrop + edit_triggers = QAbstractItemView.EditKeyPressed elif tool == SidebarTool.SEARCH: # pylint: disable=no-member selection_mode = QAbstractItemView.SingleSelection # type: ignore drag_drop_mode = QAbstractItemView.NoDragDrop + edit_triggers = QAbstractItemView.EditKeyPressed elif tool == SidebarTool.EDIT: selection_mode = QAbstractItemView.SingleSelection # type: ignore drag_drop_mode = QAbstractItemView.InternalMove + edit_triggers = QAbstractItemView.DoubleClicked | QAbstractItemView.EditKeyPressed self.setSelectionMode(selection_mode) self.setDragDropMode(drag_drop_mode) + self.setEditTriggers(edit_triggers) def model(self) -> SidebarModel: return super().model() @@ -405,7 +417,7 @@ class SidebarTreeView(QTreeView): def on_done(fut: Future) -> None: root = fut.result() - model = SidebarModel(root) + model = SidebarModel(self, root) # from PyQt5.QtTest import QAbstractItemModelTester # tester = QAbstractItemModelTester(model) @@ -415,7 +427,10 @@ class SidebarTreeView(QTreeView): self.search_for(self.current_search) else: self._expand_where_necessary(model) + self.setUpdatesEnabled(True) + # block repainting during refreshing to avoid flickering + self.setUpdatesEnabled(False) self.mw.taskman.run_in_background(self._root_tree, on_done) def search_for(self, text: str) -> None: @@ -908,6 +923,7 @@ class SidebarTreeView(QTreeView): item_type=SidebarItemType.DECK, id=node.deck_id, full_name=head + node.name, + editable=True, ) root.add_child(item) newhead = f"{head + node.name}::" @@ -1044,21 +1060,22 @@ class SidebarTreeView(QTreeView): lambda: set_children_collapsed(True), ) - def rename_deck(self, item: SidebarItem) -> None: + def rename_deck(self, item: SidebarItem, new_name: Optional[str] = None) -> bool: deck = self.mw.col.decks.get(item.id) old_name = deck["name"] - new_name = getOnlyText(tr(TR.DECKS_NEW_DECK_NAME), default=old_name) + new_name = new_name or getOnlyText(tr(TR.DECKS_NEW_DECK_NAME), default=old_name) new_name = new_name.replace('"', "") if not new_name or new_name == old_name: - return + return False self.mw.checkpoint(tr(TR.ACTIONS_RENAME_DECK)) try: self.mw.col.decks.rename(deck, new_name) except DeckRenameError as e: showWarning(e.description) - return + return False self.refresh() self.mw.deckBrowser.refresh() + return True def remove_tag(self, item: SidebarItem) -> None: self.browser.editor.saveNow(lambda: self._remove_tag(item)) @@ -1129,6 +1146,14 @@ class SidebarTreeView(QTreeView): self.browser.model.beginReset() self.mw.taskman.run_in_background(do_delete, on_done) + def rename_node(self, item: SidebarItem, text: str) -> bool: + if text.replace('"', ""): + new_name = re.sub(re.escape(item.name) + '$', text, item.full_name) + if item.item_type == SidebarItemType.DECK: + return self.rename_deck(item, new_name) + return False + return False + # Saved searches ################## From d0b916a2fff0892b864af6f1328ac576ebc72ecb Mon Sep 17 00:00:00 2001 From: RumovZ Date: Sun, 28 Feb 2021 21:13:26 +0100 Subject: [PATCH 13/57] Enable in-place editing of saved searches --- qt/aqt/sidebar.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/qt/aqt/sidebar.py b/qt/aqt/sidebar.py index d9c5f46a9..886d89c0e 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -700,6 +700,7 @@ class SidebarTreeView(QTreeView): icon, search_node=SearchNode(parsable_text=filt), item_type=SidebarItemType.SAVED_SEARCH, + editable=True ) root.add_child(item) @@ -1151,7 +1152,8 @@ class SidebarTreeView(QTreeView): new_name = re.sub(re.escape(item.name) + '$', text, item.full_name) if item.item_type == SidebarItemType.DECK: return self.rename_deck(item, new_name) - return False + if item.item_type == SidebarItemType.SAVED_SEARCH: + return self.rename_saved_search(item, new_name) return False # Saved searches @@ -1174,20 +1176,21 @@ class SidebarTreeView(QTreeView): self._set_saved_searches(conf) self.refresh() - def rename_saved_search(self, item: SidebarItem) -> None: + def rename_saved_search(self, item: SidebarItem, new_name: str = None) -> bool: old = item.name conf = self._get_saved_searches() try: filt = conf[old] except KeyError: - return - new = getOnlyText(tr(TR.ACTIONS_NEW_NAME), default=old) + return False + new = new_name or getOnlyText(tr(TR.ACTIONS_NEW_NAME), default=old) if new == old or not new: - return + return False conf[new] = filt del conf[old] self._set_saved_searches(conf) self.refresh() + return True def save_current_search(self, _item: Any = None) -> None: try: From 1b8cebb8c5ca958562f0c6415efa4b9b35e1d735 Mon Sep 17 00:00:00 2001 From: RumovZ Date: Sun, 28 Feb 2021 21:50:21 +0100 Subject: [PATCH 14/57] Enable in-place editing of sidebar tags --- qt/aqt/sidebar.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/qt/aqt/sidebar.py b/qt/aqt/sidebar.py index 886d89c0e..06d9f9bfe 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -877,6 +877,7 @@ class SidebarTreeView(QTreeView): expanded=node.expanded, item_type=SidebarItemType.TAG, full_name=head + node.name, + editable=True, ) root.add_child(item) newhead = f"{head + node.name}::" @@ -1098,13 +1099,16 @@ class SidebarTreeView(QTreeView): self.browser.model.beginReset() self.mw.taskman.run_in_background(do_remove, on_done) - def rename_tag(self, item: SidebarItem) -> None: - self.browser.editor.saveNow(lambda: self._rename_tag(item)) + def rename_tag(self, item: SidebarItem, new_name: str = None) -> None: + # block repainting until callback + self.setUpdatesEnabled(False) + self.browser.editor.saveNow(lambda: self._rename_tag(item, new_name)) - def _rename_tag(self, item: SidebarItem) -> None: + def _rename_tag(self, item: SidebarItem, new_name: str = None) -> None: old_name = item.full_name - new_name = getOnlyText(tr(TR.ACTIONS_NEW_NAME), default=old_name) + new_name = new_name or getOnlyText(tr(TR.ACTIONS_NEW_NAME), default=old_name) if new_name == old_name or not new_name: + self.setUpdatesEnabled(True) return def do_rename() -> int: @@ -1154,6 +1158,8 @@ class SidebarTreeView(QTreeView): return self.rename_deck(item, new_name) if item.item_type == SidebarItemType.SAVED_SEARCH: return self.rename_saved_search(item, new_name) + if item.item_type == SidebarItemType.TAG: + self.rename_tag(item, new_name) return False # Saved searches From dc1711b630cb1057a50464c665604725f2351959 Mon Sep 17 00:00:00 2001 From: RumovZ Date: Sun, 28 Feb 2021 22:36:21 +0100 Subject: [PATCH 15/57] Always return False from rename_node setData expects a result but due to the asynchrony of the editor it might not be known, yet. --- qt/aqt/sidebar.py | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/qt/aqt/sidebar.py b/qt/aqt/sidebar.py index 06d9f9bfe..e19b3595c 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -700,7 +700,7 @@ class SidebarTreeView(QTreeView): icon, search_node=SearchNode(parsable_text=filt), item_type=SidebarItemType.SAVED_SEARCH, - editable=True + editable=True, ) root.add_child(item) @@ -1062,22 +1062,21 @@ class SidebarTreeView(QTreeView): lambda: set_children_collapsed(True), ) - def rename_deck(self, item: SidebarItem, new_name: Optional[str] = None) -> bool: + def rename_deck(self, item: SidebarItem, new_name: Optional[str] = None) -> None: deck = self.mw.col.decks.get(item.id) old_name = deck["name"] new_name = new_name or getOnlyText(tr(TR.DECKS_NEW_DECK_NAME), default=old_name) new_name = new_name.replace('"', "") if not new_name or new_name == old_name: - return False + return self.mw.checkpoint(tr(TR.ACTIONS_RENAME_DECK)) try: self.mw.col.decks.rename(deck, new_name) except DeckRenameError as e: showWarning(e.description) - return False + return self.refresh() self.mw.deckBrowser.refresh() - return True def remove_tag(self, item: SidebarItem) -> None: self.browser.editor.saveNow(lambda: self._remove_tag(item)) @@ -1153,13 +1152,14 @@ class SidebarTreeView(QTreeView): def rename_node(self, item: SidebarItem, text: str) -> bool: if text.replace('"', ""): - new_name = re.sub(re.escape(item.name) + '$', text, item.full_name) + new_name = re.sub(re.escape(item.name) + "$", text, item.full_name) if item.item_type == SidebarItemType.DECK: - return self.rename_deck(item, new_name) + self.rename_deck(item, new_name) if item.item_type == SidebarItemType.SAVED_SEARCH: - return self.rename_saved_search(item, new_name) + self.rename_saved_search(item, new_name) if item.item_type == SidebarItemType.TAG: self.rename_tag(item, new_name) + # renaming may be asynchronous so always return False return False # Saved searches @@ -1182,21 +1182,20 @@ class SidebarTreeView(QTreeView): self._set_saved_searches(conf) self.refresh() - def rename_saved_search(self, item: SidebarItem, new_name: str = None) -> bool: + def rename_saved_search(self, item: SidebarItem, new_name: str = None) -> None: old = item.name conf = self._get_saved_searches() try: filt = conf[old] except KeyError: - return False + return new = new_name or getOnlyText(tr(TR.ACTIONS_NEW_NAME), default=old) if new == old or not new: - return False + return conf[new] = filt del conf[old] self._set_saved_searches(conf) self.refresh() - return True def save_current_search(self, _item: Any = None) -> None: try: From e83f0fef0fc7a8b642fdd46c018b93f250bd6538 Mon Sep 17 00:00:00 2001 From: RumovZ Date: Sun, 28 Feb 2021 22:36:31 +0100 Subject: [PATCH 16/57] Fix Qt types --- qt/aqt/sidebar.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/qt/aqt/sidebar.py b/qt/aqt/sidebar.py index e19b3595c..d39cb057d 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -229,7 +229,9 @@ class SidebarModel(QAbstractItemModel): return QVariant(item.tooltip) return QVariant(theme_manager.icon_from_resources(item.icon)) - def setData(self, index: QModelIndex, text: str, _role: int) -> bool: + def setData( + self, index: QModelIndex, text: QVariant, _role: int = Qt.EditRole + ) -> bool: return self.sidebar.rename_node(index.internalPointer(), text) def supportedDropActions(self) -> Qt.DropActions: @@ -390,19 +392,20 @@ class SidebarTreeView(QTreeView): def tool(self, tool: SidebarTool) -> None: self._tool = tool if tool == SidebarTool.SELECT: - # pylint: disable=no-member - selection_mode = QAbstractItemView.ExtendedSelection # type: ignore + selection_mode = QAbstractItemView.ExtendedSelection drag_drop_mode = QAbstractItemView.NoDragDrop edit_triggers = QAbstractItemView.EditKeyPressed elif tool == SidebarTool.SEARCH: - # pylint: disable=no-member - selection_mode = QAbstractItemView.SingleSelection # type: ignore + selection_mode = QAbstractItemView.SingleSelection drag_drop_mode = QAbstractItemView.NoDragDrop edit_triggers = QAbstractItemView.EditKeyPressed elif tool == SidebarTool.EDIT: - selection_mode = QAbstractItemView.SingleSelection # type: ignore + selection_mode = QAbstractItemView.SingleSelection drag_drop_mode = QAbstractItemView.InternalMove - edit_triggers = QAbstractItemView.DoubleClicked | QAbstractItemView.EditKeyPressed + edit_triggers = cast( + QAbstractItemView.EditTriggers, + QAbstractItemView.DoubleClicked | QAbstractItemView.EditKeyPressed, + ) self.setSelectionMode(selection_mode) self.setDragDropMode(drag_drop_mode) self.setEditTriggers(edit_triggers) From 30e7d705b64a332de86c6838544cadb5c16fe1f2 Mon Sep 17 00:00:00 2001 From: RumovZ Date: Mon, 1 Mar 2021 08:45:03 +0100 Subject: [PATCH 17/57] Enable extended selection in edit mode --- qt/aqt/sidebar.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qt/aqt/sidebar.py b/qt/aqt/sidebar.py index d39cb057d..247ce2730 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -400,7 +400,7 @@ class SidebarTreeView(QTreeView): drag_drop_mode = QAbstractItemView.NoDragDrop edit_triggers = QAbstractItemView.EditKeyPressed elif tool == SidebarTool.EDIT: - selection_mode = QAbstractItemView.SingleSelection + selection_mode = QAbstractItemView.ExtendedSelection drag_drop_mode = QAbstractItemView.InternalMove edit_triggers = cast( QAbstractItemView.EditTriggers, From e199bf0b47b6c88c9e32d56b1f79001553faa8d7 Mon Sep 17 00:00:00 2001 From: RumovZ Date: Mon, 1 Mar 2021 08:45:33 +0100 Subject: [PATCH 18/57] Fix repainting when renaming tag via dialogue --- qt/aqt/sidebar.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/qt/aqt/sidebar.py b/qt/aqt/sidebar.py index 247ce2730..6f8e8897c 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -1102,8 +1102,9 @@ class SidebarTreeView(QTreeView): self.mw.taskman.run_in_background(do_remove, on_done) def rename_tag(self, item: SidebarItem, new_name: str = None) -> None: - # block repainting until callback - self.setUpdatesEnabled(False) + if new_name: + # call came from model; block repainting until collection is updated + self.setUpdatesEnabled(False) self.browser.editor.saveNow(lambda: self._rename_tag(item, new_name)) def _rename_tag(self, item: SidebarItem, new_name: str = None) -> None: From f4aeb0c0979bf6e0105aa10ae265d6ce505e52f4 Mon Sep 17 00:00:00 2001 From: RumovZ Date: Mon, 1 Mar 2021 09:41:41 +0100 Subject: [PATCH 19/57] Enable deleting multiple saved searches --- ftl/core/browsing.ftl | 5 +++++ qt/aqt/sidebar.py | 22 +++++++++++++++++----- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/ftl/core/browsing.ftl b/ftl/core/browsing.ftl index f7e803bff..e563cd823 100644 --- a/ftl/core/browsing.ftl +++ b/ftl/core/browsing.ftl @@ -21,6 +21,11 @@ browsing-change-note-type2 = Change Note Type... browsing-change-to = Change { $val } to: browsing-clear-unused = Clear Unused browsing-clear-unused-tags = Clear Unused Tags +browsing-confirm-saved-searches-deletion = + { $count -> + [one] Are you sure you want to delete the selected saved search? + *[other] Are you sure you want to delete the { $count } selected saved searches? + } browsing-created = Created browsing-ctrlandshiftande = Ctrl+Shift+E browsing-current-deck = Current Deck diff --git a/qt/aqt/sidebar.py b/qt/aqt/sidebar.py index 6f8e8897c..f56723953 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -353,7 +353,7 @@ class SidebarTreeView(QTreeView): ), SidebarItemType.SAVED_SEARCH: ( (tr(TR.ACTIONS_RENAME), self.rename_saved_search), - (tr(TR.ACTIONS_DELETE), self.remove_saved_search), + (tr(TR.ACTIONS_DELETE), self.remove_saved_searches), ), SidebarItemType.NOTETYPE: ((tr(TR.ACTIONS_MANAGE), self.manage_notetype),), SidebarItemType.SAVED_SEARCH_ROOT: ( @@ -1177,12 +1177,17 @@ class SidebarTreeView(QTreeView): def _set_saved_searches(self, searches: Dict[str, str]) -> None: self.col.set_config(self._saved_searches_key, searches) - def remove_saved_search(self, item: SidebarItem) -> None: - name = item.name - if not askUser(tr(TR.BROWSING_REMOVE_FROM_YOUR_SAVED_SEARCHES, val=name)): + def remove_saved_searches(self, _item: SidebarItem) -> None: + selected = self._selected_saved_searches() + if len(selected) == 1: + query = tr(TR.BROWSING_REMOVE_FROM_YOUR_SAVED_SEARCHES, val=selected[0]) + else: + query = tr(TR.BROWSING_CONFIRM_SAVED_SEARCHES_DELETION, count=len(selected)) + if not askUser(query): return conf = self._get_saved_searches() - del conf[name] + for name in selected: + del conf[name] self._set_saved_searches(conf) self.refresh() @@ -1234,3 +1239,10 @@ class SidebarTreeView(QTreeView): for item in self._selected_items() if item.item_type == SidebarItemType.DECK ] + + def _selected_saved_searches(self) -> List[str]: + return [ + item.name + for item in self._selected_items() + if item.item_type == SidebarItemType.SAVED_SEARCH + ] From 25d57574c9b27edddeb6cf74048fc7ae68e0f302 Mon Sep 17 00:00:00 2001 From: RumovZ Date: Tue, 2 Mar 2021 11:05:16 +0100 Subject: [PATCH 20/57] Enable removal of multiple tags from the sidebar --- qt/aqt/sidebar.py | 20 +++++++++++++------- rslib/backend.proto | 1 + rslib/src/backend/mod.rs | 7 +++++++ rslib/src/err.rs | 8 ++++++++ rslib/src/notes.rs | 6 ++++++ rslib/src/storage/tag/mod.rs | 9 +++++++++ rslib/src/tags.rs | 31 +++++++++++++++++++++++++++++++ 7 files changed, 75 insertions(+), 7 deletions(-) diff --git a/qt/aqt/sidebar.py b/qt/aqt/sidebar.py index f56723953..3a8b26ad3 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -349,7 +349,7 @@ class SidebarTreeView(QTreeView): ), SidebarItemType.TAG: ( (tr(TR.ACTIONS_RENAME), self.rename_tag), - (tr(TR.ACTIONS_DELETE), self.remove_tag), + (tr(TR.ACTIONS_DELETE), self.remove_tags), ), SidebarItemType.SAVED_SEARCH: ( (tr(TR.ACTIONS_RENAME), self.rename_saved_search), @@ -1081,15 +1081,14 @@ class SidebarTreeView(QTreeView): self.refresh() self.mw.deckBrowser.refresh() - def remove_tag(self, item: SidebarItem) -> None: - self.browser.editor.saveNow(lambda: self._remove_tag(item)) + def remove_tags(self, item: SidebarItem) -> None: + self.browser.editor.saveNow(lambda: self._remove_tags(item)) - def _remove_tag(self, item: SidebarItem) -> None: - old_name = item.full_name + def _remove_tags(self, _item: SidebarItem) -> None: + tags = self._selected_tags() def do_remove() -> None: - self.mw.col.tags.remove(old_name) - self.col.tags.rename(old_name, "") + self.col._backend.expunge_tags(" ".join(tags)) def on_done(fut: Future) -> None: self.mw.requireReset(reason=ResetReason.BrowserRemoveTags, context=self) @@ -1246,3 +1245,10 @@ class SidebarTreeView(QTreeView): for item in self._selected_items() if item.item_type == SidebarItemType.SAVED_SEARCH ] + + def _selected_tags(self) -> List[str]: + return [ + item.full_name + for item in self._selected_items() + if item.item_type == SidebarItemType.TAG + ] diff --git a/rslib/backend.proto b/rslib/backend.proto index 2300ceb7f..8840cdfde 100644 --- a/rslib/backend.proto +++ b/rslib/backend.proto @@ -225,6 +225,7 @@ service BackendService { rpc ClearUnusedTags(Empty) returns (Empty); rpc AllTags(Empty) returns (StringList); + rpc ExpungeTags(String) returns (Empty); rpc SetTagExpanded(SetTagExpandedIn) returns (Empty); rpc ClearTag(String) returns (Empty); rpc TagTree(Empty) returns (TagTreeNode); diff --git a/rslib/src/backend/mod.rs b/rslib/src/backend/mod.rs index ba09fc9a4..72ca2b7ff 100644 --- a/rslib/src/backend/mod.rs +++ b/rslib/src/backend/mod.rs @@ -1418,6 +1418,13 @@ impl BackendService for Backend { }) } + fn expunge_tags(&self, tags: pb::String) -> BackendResult { + self.with_col(|col| { + col.expunge_tags(tags.val.as_str())?; + Ok(().into()) + }) + } + fn set_tag_expanded(&self, input: pb::SetTagExpandedIn) -> BackendResult { self.with_col(|col| { col.transact(None, |col| { diff --git a/rslib/src/err.rs b/rslib/src/err.rs index 6a93df75b..015750197 100644 --- a/rslib/src/err.rs +++ b/rslib/src/err.rs @@ -487,3 +487,11 @@ impl From for AnkiError { AnkiError::ParseNumError } } + +impl From for AnkiError { + fn from(_err: regex::Error) -> Self { + AnkiError::InvalidInput { + info: "invalid regex".into(), + } + } +} diff --git a/rslib/src/notes.rs b/rslib/src/notes.rs index 6be535d98..f60e61d55 100644 --- a/rslib/src/notes.rs +++ b/rslib/src/notes.rs @@ -152,6 +152,12 @@ impl Note { .collect() } + pub(crate) fn remove_tags(&mut self, re: &Regex) -> bool { + let old_len = self.tags.len(); + self.tags.retain(|tag| !re.is_match(tag)); + old_len > self.tags.len() + } + pub(crate) fn replace_tags(&mut self, re: &Regex, mut repl: T) -> bool { let mut changed = false; for tag in &mut self.tags { diff --git a/rslib/src/storage/tag/mod.rs b/rslib/src/storage/tag/mod.rs index d3c56c2a2..b70fd99f7 100644 --- a/rslib/src/storage/tag/mod.rs +++ b/rslib/src/storage/tag/mod.rs @@ -73,6 +73,15 @@ impl SqliteStorage { Ok(()) } + /// Clear all matching tags where tag_group is a regexp group that should not match whitespace. + pub(crate) fn clear_tag_group(&self, tag_group: &str) -> Result<()> { + self.db + .prepare_cached("delete from tags where tag regexp ?")? + .execute(&[format!("(?i)^{}($|::)", tag_group)])?; + + Ok(()) + } + pub(crate) fn set_tag_collapsed(&self, tag: &str, collapsed: bool) -> Result<()> { self.db .prepare_cached("update tags set collapsed = ? where tag = ?")? diff --git a/rslib/src/tags.rs b/rslib/src/tags.rs index 7513fecac..15e6e7310 100644 --- a/rslib/src/tags.rs +++ b/rslib/src/tags.rs @@ -285,6 +285,37 @@ impl Collection { Ok(()) } + /// Take tags as a whitespace-separated string and remove them from all notes and the storage. + pub fn expunge_tags(&mut self, tags: &str) -> Result { + let tag_group = format!("({})", regex::escape(tags.trim()).replace(' ', "|")); + let nids = self.nids_for_tags(&tag_group)?; + let re = Regex::new(&format!("(?i)^{}(::.*)?$", tag_group))?; + self.transact(None, |col| { + col.storage.clear_tag_group(&tag_group)?; + col.transform_notes(&nids, |note, _nt| { + Ok(TransformNoteOutput { + changed: note.remove_tags(&re), + generate_cards: false, + mark_modified: true, + }) + }) + }) + } + + /// Take tags as a regexp group, i.e. separated with pipes and wrapped in brackets, and return + /// the ids of all notes with one of them. + fn nids_for_tags(&mut self, tag_group: &str) -> Result> { + let mut stmt = self + .storage + .db + .prepare("select id from notes where tags regexp ?")?; + let args = format!("(?i).* {}(::| ).*", tag_group); + let nids = stmt + .query_map(&[args], |row| row.get(0))? + .collect::>()?; + Ok(nids) + } + pub(crate) fn set_tag_expanded(&self, name: &str, expanded: bool) -> Result<()> { let mut name = name; let tag; From adaea7227e4c0c1282360397cbd50fb17d553644 Mon Sep 17 00:00:00 2001 From: RumovZ Date: Tue, 2 Mar 2021 23:13:34 +0100 Subject: [PATCH 21/57] Select and scroll to renamed/added sidebar item --- qt/aqt/sidebar.py | 50 ++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 45 insertions(+), 5 deletions(-) diff --git a/qt/aqt/sidebar.py b/qt/aqt/sidebar.py index 3a8b26ad3..64c4854b9 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -169,6 +169,9 @@ class SidebarModel(QAbstractItemModel): 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()) @@ -413,7 +416,9 @@ class SidebarTreeView(QTreeView): def model(self) -> SidebarModel: return super().model() - def refresh(self) -> None: + def refresh( + self, is_current: Optional[Callable[[SidebarItem], bool]] = None + ) -> None: "Refresh list. No-op if sidebar is not visible." if not self.isVisible(): return @@ -431,11 +436,34 @@ class SidebarTreeView(QTreeView): else: self._expand_where_necessary(model) self.setUpdatesEnabled(True) + if is_current: + self.restore_current(is_current) # block repainting during refreshing to avoid flickering self.setUpdatesEnabled(False) self.mw.taskman.run_in_background(self._root_tree, on_done) + def restore_current(self, is_current: Callable[[SidebarItem], bool]) -> None: + if current := self.find_item(is_current): + index = self.model().index_for_item(current) + self.selectionModel().select(index, QItemSelectionModel.SelectCurrent) + self.scrollTo(index) + + def find_item( + self, + is_target: Callable[[SidebarItem], bool], + parent: Optional[SidebarItem] = None, + ) -> Optional[SidebarItem]: + def find_item_rec(parent: SidebarItem) -> Optional[SidebarItem]: + if is_target(parent): + return parent + for child in parent.children: + if item := find_item_rec(child): + return item + return None + + return find_item_rec(parent or self.model().root) + def search_for(self, text: str) -> None: self.showColumn(0) if not text.strip(): @@ -1078,7 +1106,10 @@ class SidebarTreeView(QTreeView): except DeckRenameError as e: showWarning(e.description) return - self.refresh() + self.refresh( + lambda item_: item_.item_type == SidebarItemType.DECK + and item_.id == item.id + ) self.mw.deckBrowser.refresh() def remove_tags(self, item: SidebarItem) -> None: @@ -1126,7 +1157,10 @@ class SidebarTreeView(QTreeView): showInfo(tr(TR.BROWSING_TAG_RENAME_WARNING_EMPTY)) return - self.refresh() + self.refresh( + lambda item: item.item_type == SidebarItemType.TAG + and item.full_name == new_name + ) self.mw.checkpoint(tr(TR.ACTIONS_RENAME_TAG)) self.browser.model.beginReset() @@ -1203,7 +1237,10 @@ class SidebarTreeView(QTreeView): conf[new] = filt del conf[old] self._set_saved_searches(conf) - self.refresh() + self.refresh( + lambda item: item.item_type == SidebarItemType.SAVED_SEARCH + and item.name == new_name + ) def save_current_search(self, _item: Any = None) -> None: try: @@ -1219,7 +1256,10 @@ class SidebarTreeView(QTreeView): conf = self._get_saved_searches() conf[name] = filt self._set_saved_searches(conf) - self.refresh() + self.refresh( + lambda item: item.item_type == SidebarItemType.SAVED_SEARCH + and item.name == name + ) def manage_notetype(self, item: SidebarItem) -> None: Models( From c0d77896dad3fef85e99487e8c2a6e015a43da2f Mon Sep 17 00:00:00 2001 From: RumovZ Date: Wed, 3 Mar 2021 09:15:36 +0100 Subject: [PATCH 22/57] Add DECK_CURRENT as a SidebarItemType Thus, disable renaming, deleting etc. for the current deck item. As a consequence, editable is no longer needed as a field of SidebarItem as it can be derived from its type. --- qt/aqt/sidebar.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/qt/aqt/sidebar.py b/qt/aqt/sidebar.py index 64c4854b9..674df43aa 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -47,6 +47,7 @@ class SidebarItemType(Enum): CARD_STATE_ROOT = auto() CARD_STATE = auto() DECK_ROOT = auto() + DECK_CURRENT = auto() DECK = auto() NOTETYPE_ROOT = auto() NOTETYPE = auto() @@ -64,6 +65,8 @@ class SidebarItemType(Enum): 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) class SidebarStage(Enum): ROOT = auto() @@ -88,7 +91,6 @@ class SidebarItem: item_type: SidebarItemType = SidebarItemType.CUSTOM, id: int = 0, full_name: str = None, - editable: bool = False, ) -> None: self.name = name if not full_name: @@ -99,7 +101,6 @@ class SidebarItem: self.id = id self.on_click = on_click self.search_node = search_node - self.editable = editable self.on_expanded = on_expanded self.children: List["SidebarItem"] = [] self.tooltip: Optional[str] = None @@ -253,7 +254,7 @@ class SidebarModel(QAbstractItemModel): SidebarItemType.TAG_ROOT, ): flags |= Qt.ItemIsDragEnabled | Qt.ItemIsDropEnabled - if item.editable: + if item.item_type.is_editable(): flags |= Qt.ItemIsEditable return cast(Qt.ItemFlags, flags) @@ -731,7 +732,6 @@ class SidebarTreeView(QTreeView): icon, search_node=SearchNode(parsable_text=filt), item_type=SidebarItemType.SAVED_SEARCH, - editable=True, ) root.add_child(item) @@ -908,7 +908,6 @@ class SidebarTreeView(QTreeView): expanded=node.expanded, item_type=SidebarItemType.TAG, full_name=head + node.name, - editable=True, ) root.add_child(item) newhead = f"{head + node.name}::" @@ -956,7 +955,6 @@ class SidebarTreeView(QTreeView): item_type=SidebarItemType.DECK, id=node.deck_id, full_name=head + node.name, - editable=True, ) root.add_child(item) newhead = f"{head + node.name}::" @@ -974,7 +972,7 @@ class SidebarTreeView(QTreeView): current = root.add_simple( name=tr(TR.BROWSING_CURRENT_DECK), icon=icon, - type=SidebarItemType.DECK, + type=SidebarItemType.DECK_CURRENT, search_node=SearchNode(deck="current"), ) current.id = self.mw.col.decks.selected() From e2940de4a48dab6576cf15ff1bb972405a47113c Mon Sep 17 00:00:00 2001 From: RumovZ Date: Wed, 3 Mar 2021 09:20:02 +0100 Subject: [PATCH 23/57] Escape backslashes in re.sub()'s repl --- qt/aqt/sidebar.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/qt/aqt/sidebar.py b/qt/aqt/sidebar.py index 674df43aa..67ffda1c5 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -66,7 +66,12 @@ class SidebarItemType(Enum): return self in self.section_roots() def is_editable(self) -> bool: - return self in (SidebarItemType.SAVED_SEARCH, SidebarItemType.DECK, SidebarItemType.TAG) + return self in ( + SidebarItemType.SAVED_SEARCH, + SidebarItemType.DECK, + SidebarItemType.TAG, + ) + class SidebarStage(Enum): ROOT = auto() @@ -1187,7 +1192,9 @@ class SidebarTreeView(QTreeView): def rename_node(self, item: SidebarItem, text: str) -> bool: if text.replace('"', ""): - new_name = re.sub(re.escape(item.name) + "$", text, item.full_name) + new_name = re.sub( + re.escape(item.name) + "$", text.replace("\\", r"\\"), item.full_name + ) if item.item_type == SidebarItemType.DECK: self.rename_deck(item, new_name) if item.item_type == SidebarItemType.SAVED_SEARCH: From 61e61376a24049f82ee9aa688b4041815b37e2cc Mon Sep 17 00:00:00 2001 From: RumovZ Date: Wed, 3 Mar 2021 11:43:31 +0100 Subject: [PATCH 24/57] Make SidebarItem._is_extended a property --- qt/aqt/sidebar.py | 41 +++++++++++++++++++++++------------------ 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/qt/aqt/sidebar.py b/qt/aqt/sidebar.py index 67ffda1c5..9112295da 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -110,7 +110,7 @@ class SidebarItem: self.children: List["SidebarItem"] = [] self.tooltip: Optional[str] = None self._parent_item: Optional["SidebarItem"] = None - self._is_expanded = expanded + self._expanded = expanded self._row_in_parent: Optional[int] = None self._search_matches_self = False self._search_matches_child = False @@ -138,14 +138,24 @@ class SidebarItem: self.add_child(item) return item - def is_expanded(self, searching: bool) -> bool: + @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._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.is_section_root() + 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 @@ -496,7 +506,7 @@ class SidebarTreeView(QTreeView): continue self._expand_where_necessary(model, idx, searching) if item := model.item_for_index(idx): - if item.is_expanded(searching): + if item.show_expanded(searching): self.setExpanded(idx, True) def update_search( @@ -639,19 +649,14 @@ class SidebarTreeView(QTreeView): def _on_expansion(self, idx: QModelIndex) -> None: if self.current_search: return - self._on_expand_or_collapse(idx, True) + if item := self.model().item_for_index(idx): + item.expanded = True def _on_collapse(self, idx: QModelIndex) -> None: if self.current_search: return - self._on_expand_or_collapse(idx, False) - - def _on_expand_or_collapse(self, idx: QModelIndex, expanded: bool) -> None: - item = self.model().item_for_index(idx) - if item and item._is_expanded != expanded: - item._is_expanded = expanded - if item.on_expanded: - item.on_expanded(expanded) + if item := self.model().item_for_index(idx): + item.expanded = False # Tree building ########################### From 65a2796a0e6f9ccc278b5a818326c9e4eb14841d Mon Sep 17 00:00:00 2001 From: RumovZ Date: Wed, 3 Mar 2021 11:44:42 +0100 Subject: [PATCH 25/57] Enable group expanding/collapsing Also, only show expand/collapse actions if they will have an effect. --- ftl/core/browsing.ftl | 2 ++ qt/aqt/sidebar.py | 55 ++++++++++++++++++++++++++----------------- 2 files changed, 35 insertions(+), 22 deletions(-) diff --git a/ftl/core/browsing.ftl b/ftl/core/browsing.ftl index e563cd823..7302321f5 100644 --- a/ftl/core/browsing.ftl +++ b/ftl/core/browsing.ftl @@ -118,6 +118,8 @@ browsing-note-deleted = *[other] { $count } notes deleted. } browsing-window-title = Browse ({ $selected } of { $total } cards selected) +browsing-sidebar-expand = Expand +browsing-sidebar-collapse = Collapse browsing-sidebar-expand-children = Expand Children browsing-sidebar-collapse-children = Collapse Children browsing-sidebar-decks = Decks diff --git a/qt/aqt/sidebar.py b/qt/aqt/sidebar.py index 9112295da..a9d9f09a4 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -1048,9 +1048,7 @@ class SidebarTreeView(QTreeView): qconnect(a.triggered, lambda _, func=act_func: func(item)) self._maybe_add_search_actions(m) - - if idx: - self.maybe_add_tree_actions(m, item, idx) + self._maybe_add_tree_actions(m) if not m.children(): return @@ -1076,30 +1074,43 @@ class SidebarTreeView(QTreeView): lambda: self.update_search(*nodes, joiner="OR"), ) - def maybe_add_tree_actions( - self, menu: QMenu, item: SidebarItem, parent: QModelIndex - ) -> None: + def _maybe_add_tree_actions(self, menu: QMenu) -> None: + def set_expanded(expanded: bool) -> None: + for index in self.selectedIndexes(): + self.setExpanded(index, expanded) + + def set_children_expanded(expanded: bool) -> None: + for index in self.selectedIndexes(): + self.setExpanded(index, True) + for row in range(self.model().rowCount(index)): + self.setExpanded(self.model().index(row, 0, index), expanded) + if self.current_search: return - if not any(bool(c.children) for c in item.children): + + selected_items = self._selected_items() + if not any(item.children for item in selected_items): return - def set_children_collapsed(collapsed: bool) -> None: - m = self.model() - self.setExpanded(parent, True) - for row in range(m.rowCount(parent)): - idx = m.index(row, 0, parent) - self.setExpanded(idx, not collapsed) - menu.addSeparator() - menu.addAction( - tr(TR.BROWSING_SIDEBAR_EXPAND_CHILDREN), - lambda: set_children_collapsed(False), - ) - menu.addAction( - tr(TR.BROWSING_SIDEBAR_COLLAPSE_CHILDREN), - lambda: set_children_collapsed(True), - ) + if any(not item.expanded for item in selected_items): + menu.addAction(tr(TR.BROWSING_SIDEBAR_EXPAND), lambda: set_expanded(True)) + if any(item.expanded for item in selected_items): + menu.addAction( + tr(TR.BROWSING_SIDEBAR_COLLAPSE), lambda: set_expanded(False) + ) + if any( + not c.expanded for i in selected_items for c in i.children if c.children + ): + menu.addAction( + tr(TR.BROWSING_SIDEBAR_EXPAND_CHILDREN), + lambda: set_children_expanded(True), + ) + if any(c.expanded for i in selected_items for c in i.children if c.children): + menu.addAction( + tr(TR.BROWSING_SIDEBAR_COLLAPSE_CHILDREN), + lambda: set_children_expanded(False), + ) def rename_deck(self, item: SidebarItem, new_name: Optional[str] = None) -> None: deck = self.mw.col.decks.get(item.id) From aa4576dd42f3c5aaef927344223703344af96e0d Mon Sep 17 00:00:00 2001 From: RumovZ Date: Wed, 3 Mar 2021 15:18:50 +0100 Subject: [PATCH 26/57] Enable renaming notetypes --- qt/aqt/sidebar.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/qt/aqt/sidebar.py b/qt/aqt/sidebar.py index a9d9f09a4..df58c45bc 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -70,6 +70,7 @@ class SidebarItemType(Enum): SidebarItemType.SAVED_SEARCH, SidebarItemType.DECK, SidebarItemType.TAG, + SidebarItemType.NOTETYPE, ) @@ -1206,6 +1207,19 @@ class SidebarTreeView(QTreeView): self.browser.model.beginReset() self.mw.taskman.run_in_background(do_delete, on_done) + def rename_notetype(self, item: SidebarItem, new_name: str) -> None: + notetype = self.col.models.get(item.id) + new_name = new_name.replace('"', "") + if not notetype or not new_name or new_name == notetype["name"]: + return + self.mw.checkpoint(tr(TR.ACTIONS_RENAME)) + notetype["name"] = new_name + self.col.models.save(notetype) + self.refresh( + lambda item_: item_.item_type == SidebarItemType.NOTETYPE + and item_.id == item.id + ) + def rename_node(self, item: SidebarItem, text: str) -> bool: if text.replace('"', ""): new_name = re.sub( @@ -1217,6 +1231,8 @@ class SidebarTreeView(QTreeView): self.rename_saved_search(item, new_name) if item.item_type == SidebarItemType.TAG: self.rename_tag(item, new_name) + if item.item_type == SidebarItemType.NOTETYPE: + self.rename_notetype(item, new_name) # renaming may be asynchronous so always return False return False From bcc8a5ac3a02c881178cfc0af50981553031584a Mon Sep 17 00:00:00 2001 From: RumovZ Date: Wed, 3 Mar 2021 18:09:53 +0100 Subject: [PATCH 27/57] Enable renaming templates from the sidebar --- qt/aqt/sidebar.py | 49 ++++++++++++++++++++++++++++++++++------------- 1 file changed, 36 insertions(+), 13 deletions(-) diff --git a/qt/aqt/sidebar.py b/qt/aqt/sidebar.py index df58c45bc..da0754067 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -71,6 +71,7 @@ class SidebarItemType(Enum): SidebarItemType.DECK, SidebarItemType.TAG, SidebarItemType.NOTETYPE, + SidebarItemType.NOTETYPE_TEMPLATE, ) @@ -1021,6 +1022,7 @@ class SidebarTreeView(QTreeView): ), item_type=SidebarItemType.NOTETYPE_TEMPLATE, full_name=f"{nt['name']}::{tmpl['name']}", + id=tmpl["ord"], ) item.add_child(child) @@ -1127,8 +1129,8 @@ class SidebarTreeView(QTreeView): showWarning(e.description) return self.refresh( - lambda item_: item_.item_type == SidebarItemType.DECK - and item_.id == item.id + lambda other: other.item_type == SidebarItemType.DECK + and other.id == item.id ) self.mw.deckBrowser.refresh() @@ -1216,23 +1218,44 @@ class SidebarTreeView(QTreeView): notetype["name"] = new_name self.col.models.save(notetype) self.refresh( - lambda item_: item_.item_type == SidebarItemType.NOTETYPE - and item_.id == item.id + lambda other: other.item_type == SidebarItemType.NOTETYPE + and other.id == item.id ) + self.browser.model.reset() + + def rename_template(self, item: SidebarItem, new_name: str) -> None: + notetype = self.col.models.get(item._parent_item.id) + template = notetype["tmpls"][item.id] + new_name = new_name.replace('"', "") + if not new_name or new_name == template["name"]: + return + self.mw.checkpoint(tr(TR.ACTIONS_RENAME)) + template["name"] = new_name + self.col.models.save(notetype) + self.refresh( + lambda other: other.item_type == SidebarItemType.NOTETYPE_TEMPLATE + and other._parent_item.id == item._parent_item.id + and other.id == item.id + ) + self.browser.model.reset() def rename_node(self, item: SidebarItem, text: str) -> bool: - if text.replace('"', ""): - new_name = re.sub( + def full_new_name() -> str: + return re.sub( re.escape(item.name) + "$", text.replace("\\", r"\\"), item.full_name ) + + if text.replace('"', ""): if item.item_type == SidebarItemType.DECK: - self.rename_deck(item, new_name) - if item.item_type == SidebarItemType.SAVED_SEARCH: - self.rename_saved_search(item, new_name) - if item.item_type == SidebarItemType.TAG: - self.rename_tag(item, new_name) - if item.item_type == SidebarItemType.NOTETYPE: - self.rename_notetype(item, new_name) + self.rename_deck(item, full_new_name()) + elif item.item_type == SidebarItemType.SAVED_SEARCH: + self.rename_saved_search(item, text) + elif item.item_type == SidebarItemType.TAG: + self.rename_tag(item, full_new_name()) + elif item.item_type == SidebarItemType.NOTETYPE: + self.rename_notetype(item, text) + elif item.item_type == SidebarItemType.NOTETYPE_TEMPLATE: + self.rename_template(item, text) # renaming may be asynchronous so always return False return False From 7d3d6edb26a8c3eb911253cdb56fd84380b1f828 Mon Sep 17 00:00:00 2001 From: RumovZ Date: Wed, 3 Mar 2021 21:57:39 +0100 Subject: [PATCH 28/57] Remove renaming dialogues from sidebar ... ... in favour of in-line editing. This is simpler and more ergonomic for the user (and the programmer) but doesn't allow for editing parents through text input (in the case of tags and decks). --- qt/aqt/sidebar.py | 85 +++++++++++++++++++---------------------------- 1 file changed, 34 insertions(+), 51 deletions(-) diff --git a/qt/aqt/sidebar.py b/qt/aqt/sidebar.py index da0754067..1c4c46675 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -364,16 +364,9 @@ class SidebarTreeView(QTreeView): self.setContextMenuPolicy(Qt.CustomContextMenu) self.customContextMenuRequested.connect(self.onContextMenu) # type: ignore self.context_menus: Dict[SidebarItemType, Sequence[Tuple[str, Callable]]] = { - SidebarItemType.DECK: ( - (tr(TR.ACTIONS_RENAME), self.rename_deck), - (tr(TR.ACTIONS_DELETE), self.delete_deck), - ), - SidebarItemType.TAG: ( - (tr(TR.ACTIONS_RENAME), self.rename_tag), - (tr(TR.ACTIONS_DELETE), self.remove_tags), - ), + SidebarItemType.DECK: ((tr(TR.ACTIONS_DELETE), self.delete_deck),), + SidebarItemType.TAG: ((tr(TR.ACTIONS_DELETE), self.remove_tags),), SidebarItemType.SAVED_SEARCH: ( - (tr(TR.ACTIONS_RENAME), self.rename_saved_search), (tr(TR.ACTIONS_DELETE), self.remove_saved_searches), ), SidebarItemType.NOTETYPE: ((tr(TR.ACTIONS_MANAGE), self.manage_notetype),), @@ -1040,9 +1033,14 @@ class SidebarTreeView(QTreeView): # idx is only None when triggering the context menu from a left click on # saved searches - perhaps there is a better way to handle that? - def show_context_menu(self, item: SidebarItem, idx: Optional[QModelIndex]) -> None: + def show_context_menu( + self, item: SidebarItem, index: Optional[QModelIndex] + ) -> None: m = QMenu() + if item.item_type.is_editable(): + m.addAction(tr(TR.ACTIONS_RENAME), lambda: self.edit(index)) + if item.item_type in self.context_menus: for action in self.context_menus[item.item_type]: act_name = action[0] @@ -1115,13 +1113,12 @@ class SidebarTreeView(QTreeView): lambda: set_children_expanded(False), ) - def rename_deck(self, item: SidebarItem, new_name: Optional[str] = None) -> None: + def rename_deck(self, item: SidebarItem, new_name: str) -> None: deck = self.mw.col.decks.get(item.id) old_name = deck["name"] - new_name = new_name or getOnlyText(tr(TR.DECKS_NEW_DECK_NAME), default=old_name) - new_name = new_name.replace('"', "") - if not new_name or new_name == old_name: - return + new_name = re.sub( + re.escape(item.name) + "$", new_name.replace("\\", r"\\"), old_name + ) self.mw.checkpoint(tr(TR.ACTIONS_RENAME_DECK)) try: self.mw.col.decks.rename(deck, new_name) @@ -1153,18 +1150,18 @@ class SidebarTreeView(QTreeView): self.browser.model.beginReset() self.mw.taskman.run_in_background(do_remove, on_done) - def rename_tag(self, item: SidebarItem, new_name: str = None) -> None: - if new_name: - # call came from model; block repainting until collection is updated + def rename_tag(self, item: SidebarItem, new_name: str) -> None: + new_name = new_name.replace(" ", "") + if new_name and new_name != item.name: + # block repainting until collection is updated self.setUpdatesEnabled(False) - self.browser.editor.saveNow(lambda: self._rename_tag(item, new_name)) + self.browser.editor.saveNow(lambda: self._rename_tag(item, new_name)) - def _rename_tag(self, item: SidebarItem, new_name: str = None) -> None: + def _rename_tag(self, item: SidebarItem, new_name: str) -> None: old_name = item.full_name - new_name = new_name or getOnlyText(tr(TR.ACTIONS_NEW_NAME), default=old_name) - if new_name == old_name or not new_name: - self.setUpdatesEnabled(True) - return + new_name = re.sub( + re.escape(item.name) + "$", new_name.replace("\\", r"\\"), old_name + ) def do_rename() -> int: self.mw.col.tags.remove(old_name) @@ -1211,9 +1208,6 @@ class SidebarTreeView(QTreeView): def rename_notetype(self, item: SidebarItem, new_name: str) -> None: notetype = self.col.models.get(item.id) - new_name = new_name.replace('"', "") - if not notetype or not new_name or new_name == notetype["name"]: - return self.mw.checkpoint(tr(TR.ACTIONS_RENAME)) notetype["name"] = new_name self.col.models.save(notetype) @@ -1225,12 +1219,8 @@ class SidebarTreeView(QTreeView): def rename_template(self, item: SidebarItem, new_name: str) -> None: notetype = self.col.models.get(item._parent_item.id) - template = notetype["tmpls"][item.id] - new_name = new_name.replace('"', "") - if not new_name or new_name == template["name"]: - return self.mw.checkpoint(tr(TR.ACTIONS_RENAME)) - template["name"] = new_name + notetype["tmpls"][item.id]["name"] = new_name self.col.models.save(notetype) self.refresh( lambda other: other.item_type == SidebarItemType.NOTETYPE_TEMPLATE @@ -1240,22 +1230,18 @@ class SidebarTreeView(QTreeView): self.browser.model.reset() def rename_node(self, item: SidebarItem, text: str) -> bool: - def full_new_name() -> str: - return re.sub( - re.escape(item.name) + "$", text.replace("\\", r"\\"), item.full_name - ) - - if text.replace('"', ""): + new_name = text.replace('"', "") + if new_name and new_name != item.name: if item.item_type == SidebarItemType.DECK: - self.rename_deck(item, full_new_name()) + self.rename_deck(item, new_name) elif item.item_type == SidebarItemType.SAVED_SEARCH: - self.rename_saved_search(item, text) + self.rename_saved_search(item, new_name) elif item.item_type == SidebarItemType.TAG: - self.rename_tag(item, full_new_name()) + self.rename_tag(item, new_name) elif item.item_type == SidebarItemType.NOTETYPE: - self.rename_notetype(item, text) + self.rename_notetype(item, new_name) elif item.item_type == SidebarItemType.NOTETYPE_TEMPLATE: - self.rename_template(item, text) + self.rename_template(item, new_name) # renaming may be asynchronous so always return False return False @@ -1284,18 +1270,15 @@ class SidebarTreeView(QTreeView): self._set_saved_searches(conf) self.refresh() - def rename_saved_search(self, item: SidebarItem, new_name: str = None) -> None: - old = item.name + def rename_saved_search(self, item: SidebarItem, new_name: str) -> None: + old_name = item.name conf = self._get_saved_searches() try: - filt = conf[old] + filt = conf[old_name] except KeyError: return - new = new_name or getOnlyText(tr(TR.ACTIONS_NEW_NAME), default=old) - if new == old or not new: - return - conf[new] = filt - del conf[old] + conf[new_name] = filt + del conf[old_name] self._set_saved_searches(conf) self.refresh( lambda item: item.item_type == SidebarItemType.SAVED_SEARCH From 5c6eea0d809ec07180fa3a50ec91d50d754597b0 Mon Sep 17 00:00:00 2001 From: RumovZ Date: Wed, 3 Mar 2021 23:00:37 +0100 Subject: [PATCH 29/57] Make renamed item current (don't just select) --- qt/aqt/sidebar.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/qt/aqt/sidebar.py b/qt/aqt/sidebar.py index 1c4c46675..69a213312 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -457,7 +457,9 @@ class SidebarTreeView(QTreeView): def restore_current(self, is_current: Callable[[SidebarItem], bool]) -> None: if current := self.find_item(is_current): index = self.model().index_for_item(current) - self.selectionModel().select(index, QItemSelectionModel.SelectCurrent) + self.selectionModel().setCurrentIndex( + index, QItemSelectionModel.SelectCurrent + ) self.scrollTo(index) def find_item( From 6930ea24a9bf0877313bceb0a5b03286dfac5b92 Mon Sep 17 00:00:00 2001 From: RumovZ Date: Thu, 4 Mar 2021 17:20:10 +0100 Subject: [PATCH 30/57] Adjust sidebar tool icons to smaller size --- qt/aqt/forms/icons/edit.svg | 25 +++++--- qt/aqt/forms/icons/magnifying_glass.svg | 40 ++++++------ qt/aqt/forms/icons/select.svg | 83 ++++++++++++++----------- 3 files changed, 84 insertions(+), 64 deletions(-) diff --git a/qt/aqt/forms/icons/edit.svg b/qt/aqt/forms/icons/edit.svg index 8f8f98fd5..f8e4e5521 100644 --- a/qt/aqt/forms/icons/edit.svg +++ b/qt/aqt/forms/icons/edit.svg @@ -23,9 +23,9 @@ borderopacity="1.0" inkscape:pageopacity="0.0" inkscape:pageshadow="2" - inkscape:zoom="11.313708" - inkscape:cx="32.130617" - inkscape:cy="14.656091" + inkscape:zoom="8.3298966" + inkscape:cx="30.544751" + inkscape:cy="35.370349" inkscape:document-units="mm" inkscape:current-layer="layer1" inkscape:document-rotation="0" @@ -52,7 +52,7 @@ image/svg+xml - + @@ -62,13 +62,22 @@ id="layer1"> + diff --git a/qt/aqt/forms/icons/magnifying_glass.svg b/qt/aqt/forms/icons/magnifying_glass.svg index e9b4840a0..5cf295b2b 100644 --- a/qt/aqt/forms/icons/magnifying_glass.svg +++ b/qt/aqt/forms/icons/magnifying_glass.svg @@ -23,9 +23,9 @@ borderopacity="1.0" inkscape:pageopacity="0.0" inkscape:pageshadow="2" - inkscape:zoom="22.627417" - inkscape:cx="43.949995" - inkscape:cy="46.325282" + inkscape:zoom="8" + inkscape:cx="10.039334" + inkscape:cy="35.645602" inkscape:document-units="mm" inkscape:current-layer="layer1" inkscape:document-rotation="0" @@ -61,22 +61,24 @@ inkscape:groupmode="layer" id="layer1"> - - - + cx="5.5429349" + cy="5.5176048" + r="4.7567849" /> + + + + diff --git a/qt/aqt/forms/icons/select.svg b/qt/aqt/forms/icons/select.svg index cf6925454..fe4ee8c67 100644 --- a/qt/aqt/forms/icons/select.svg +++ b/qt/aqt/forms/icons/select.svg @@ -17,6 +17,36 @@ sodipodi:docname="select.svg"> + + + - - - - @@ -99,11 +112,11 @@ borderopacity="1.0" inkscape:pageopacity="0.0" inkscape:pageshadow="2" - inkscape:zoom="12.921875" - inkscape:cx="30.176126" - inkscape:cy="29.137257" + inkscape:zoom="9.1371454" + inkscape:cx="33.803843" + inkscape:cy="32.605832" inkscape:document-units="mm" - inkscape:current-layer="layer1" + inkscape:current-layer="layer2" inkscape:document-rotation="0" showgrid="true" units="px" @@ -132,18 +145,14 @@ inkscape:groupmode="layer" id="layer2" inkscape:label="Back" - style="opacity:0.997"> + style="display:inline;opacity:0.997"> + transform="translate(0.26458378,0.26458346)" + mask="none" + d="m 7.4083329,10.847917 -6.87916626,0 V 0.52916664 H 14.022917 l 0,5.29166656" + style="opacity:1;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:1.165;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:2.33,2.33;stroke-dashoffset:8.621;stroke-opacity:1;paint-order:normal" + sodipodi:nodetypes="ccccc" /> From 4ab9e6caefeb033ef128ca32a5634251331c94d4 Mon Sep 17 00:00:00 2001 From: RumovZ Date: Thu, 4 Mar 2021 17:22:03 +0100 Subject: [PATCH 31/57] Ask for confirmation when overwriting saved search --- ftl/core/browsing.ftl | 1 + qt/aqt/sidebar.py | 30 +++++++++++++++++++----------- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/ftl/core/browsing.ftl b/ftl/core/browsing.ftl index 7302321f5..c5a13ad0e 100644 --- a/ftl/core/browsing.ftl +++ b/ftl/core/browsing.ftl @@ -21,6 +21,7 @@ browsing-change-note-type2 = Change Note Type... browsing-change-to = Change { $val } to: browsing-clear-unused = Clear Unused browsing-clear-unused-tags = Clear Unused Tags +browsing-confirm-saved-search-overwrite = A saved search with the name { $name } already exists. Do you want to overwrite it? browsing-confirm-saved-searches-deletion = { $count -> [one] Are you sure you want to delete the selected saved search? diff --git a/qt/aqt/sidebar.py b/qt/aqt/sidebar.py index 69a213312..471ff2664 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -1279,6 +1279,10 @@ class SidebarTreeView(QTreeView): filt = conf[old_name] except KeyError: return + if new_name in conf and not askUser( + tr(TR.BROWSING_CONFIRM_SAVED_SEARCH_OVERWRITE, name=new_name) + ): + return conf[new_name] = filt del conf[old_name] self._set_saved_searches(conf) @@ -1294,17 +1298,21 @@ class SidebarTreeView(QTreeView): ) except InvalidInput as e: show_invalid_search_error(e) - else: - name = getOnlyText(tr(TR.BROWSING_PLEASE_GIVE_YOUR_FILTER_A_NAME)) - if not name: - return - conf = self._get_saved_searches() - conf[name] = filt - self._set_saved_searches(conf) - self.refresh( - lambda item: item.item_type == SidebarItemType.SAVED_SEARCH - and item.name == name - ) + return + name = getOnlyText(tr(TR.BROWSING_PLEASE_GIVE_YOUR_FILTER_A_NAME)) + if not name: + return + conf = self._get_saved_searches() + if name in conf and not askUser( + tr(TR.BROWSING_CONFIRM_SAVED_SEARCH_OVERWRITE, name=name) + ): + return + conf[name] = filt + self._set_saved_searches(conf) + self.refresh( + lambda item: item.item_type == SidebarItemType.SAVED_SEARCH + and item.name == name + ) def manage_notetype(self, item: SidebarItem) -> None: Models( From 1f500c1fb84bdfbd25665f234c9880dbcc42fa68 Mon Sep 17 00:00:00 2001 From: RumovZ Date: Thu, 4 Mar 2021 17:40:12 +0100 Subject: [PATCH 32/57] Enable Enter/Return search in all modes ... ... but don't trigger search if the key closes the editor. Also get rid of the on_click of the saved searches root which has already been removed on main. --- qt/aqt/sidebar.py | 22 +++++++--------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/qt/aqt/sidebar.py b/qt/aqt/sidebar.py index 471ff2664..e517ee6ca 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -91,7 +91,6 @@ class SidebarItem: self, name: str, icon: Union[str, ColoredIcon], - on_click: Callable[[], None] = None, search_node: Optional[SearchNode] = None, on_expanded: Callable[[bool], None] = None, expanded: bool = False, @@ -106,7 +105,6 @@ class SidebarItem: self.icon = icon self.item_type = item_type self.id = id - self.on_click = on_click self.search_node = search_node self.on_expanded = on_expanded self.children: List["SidebarItem"] = [] @@ -561,12 +559,13 @@ class SidebarTreeView(QTreeView): super().mouseReleaseEvent(event) if self.tool == SidebarTool.SEARCH and event.button() == Qt.LeftButton: idx = self.indexAt(event.pos()) - self._on_click_index(idx) + self._search_for_indicated(idx) def keyPressEvent(self, event: QKeyEvent) -> None: if event.key() in (Qt.Key_Return, Qt.Key_Enter): idx = self.currentIndex() - self._on_click_index(idx) + if not self.isPersistentEditorOpen(idx): + self._search_for_indicated(idx) else: super().keyPressEvent(event) @@ -636,12 +635,10 @@ class SidebarTreeView(QTreeView): self.browser.editor.saveNow(on_save) return True - def _on_click_index(self, idx: QModelIndex) -> None: - if item := self.model().item_for_index(idx): - if item.on_click: - item.on_click() - elif self.tool == SidebarTool.SEARCH and (search := item.search_node): - self.update_search(search) + def _search_for_indicated(self, index: QModelIndex) -> None: + if item := self.model().item_for_index(index): + if search_node := item.search_node: + self.update_search(search_node) def _on_expansion(self, idx: QModelIndex) -> None: if self.current_search: @@ -728,11 +725,6 @@ class SidebarTreeView(QTreeView): type=SidebarItemType.SAVED_SEARCH_ROOT, ) - def on_click() -> None: - self.show_context_menu(root, None) - - root.on_click = on_click - for name, filt in sorted(saved.items()): item = SidebarItem( name, From 513e7bdfb4a8c0ab9a8be0635b280ed6d8af13d9 Mon Sep 17 00:00:00 2001 From: RumovZ Date: Thu, 4 Mar 2021 18:31:35 +0100 Subject: [PATCH 33/57] Enable deleting via delete key --- qt/aqt/sidebar.py | 37 ++++++++++++++++++++++++++----------- 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/qt/aqt/sidebar.py b/qt/aqt/sidebar.py index e517ee6ca..54ba6e959 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -74,6 +74,13 @@ class SidebarItemType(Enum): SidebarItemType.NOTETYPE_TEMPLATE, ) + def is_deletable(self) -> bool: + return self in ( + SidebarItemType.SAVED_SEARCH, + SidebarItemType.DECK, + SidebarItemType.TAG, + ) + class SidebarStage(Enum): ROOT = auto() @@ -362,11 +369,6 @@ class SidebarTreeView(QTreeView): self.setContextMenuPolicy(Qt.CustomContextMenu) self.customContextMenuRequested.connect(self.onContextMenu) # type: ignore self.context_menus: Dict[SidebarItemType, Sequence[Tuple[str, Callable]]] = { - SidebarItemType.DECK: ((tr(TR.ACTIONS_DELETE), self.delete_deck),), - SidebarItemType.TAG: ((tr(TR.ACTIONS_DELETE), self.remove_tags),), - SidebarItemType.SAVED_SEARCH: ( - (tr(TR.ACTIONS_DELETE), self.remove_saved_searches), - ), SidebarItemType.NOTETYPE: ((tr(TR.ACTIONS_MANAGE), self.manage_notetype),), SidebarItemType.SAVED_SEARCH_ROOT: ( (tr(TR.BROWSING_SIDEBAR_SAVE_CURRENT_SEARCH), self.save_current_search), @@ -559,13 +561,15 @@ class SidebarTreeView(QTreeView): super().mouseReleaseEvent(event) if self.tool == SidebarTool.SEARCH and event.button() == Qt.LeftButton: idx = self.indexAt(event.pos()) - self._search_for_indicated(idx) + self._on_search(idx) def keyPressEvent(self, event: QKeyEvent) -> None: + index = self.currentIndex() if event.key() in (Qt.Key_Return, Qt.Key_Enter): - idx = self.currentIndex() - if not self.isPersistentEditorOpen(idx): - self._search_for_indicated(idx) + if not self.isPersistentEditorOpen(index): + self._on_search(index) + elif event.key() == Qt.Key_Delete: + self._on_delete(index) else: super().keyPressEvent(event) @@ -635,11 +639,20 @@ class SidebarTreeView(QTreeView): self.browser.editor.saveNow(on_save) return True - def _search_for_indicated(self, index: QModelIndex) -> None: + def _on_search(self, index: QModelIndex) -> None: if item := self.model().item_for_index(index): if search_node := item.search_node: self.update_search(search_node) + def _on_delete(self, index: QModelIndex) -> None: + if item := self.model().item_for_index(index): + if item.item_type == SidebarItemType.SAVED_SEARCH: + self.remove_saved_searches(item) + elif item.item_type == SidebarItemType.DECK: + self.delete_decks(item) + elif item.item_type == SidebarItemType.TAG: + self.remove_tags(item) + def _on_expansion(self, idx: QModelIndex) -> None: if self.current_search: return @@ -1032,6 +1045,8 @@ class SidebarTreeView(QTreeView): ) -> None: m = QMenu() + if item.item_type.is_deletable(): + m.addAction(tr(TR.ACTIONS_DELETE), lambda: self._on_delete(index)) if item.item_type.is_editable(): m.addAction(tr(TR.ACTIONS_RENAME), lambda: self.edit(index)) @@ -1179,7 +1194,7 @@ class SidebarTreeView(QTreeView): self.browser.model.beginReset() self.mw.taskman.run_in_background(do_rename, on_done) - def delete_deck(self, _item: SidebarItem) -> None: + def delete_decks(self, _item: SidebarItem) -> None: self.browser.editor.saveNow(self._delete_decks) def _delete_decks(self) -> None: From 39dad049cdd0d6ca648fd75e520ee0da2f8fb34e Mon Sep 17 00:00:00 2001 From: RumovZ Date: Fri, 5 Mar 2021 10:27:44 +0100 Subject: [PATCH 34/57] Fix children check in context tree actions --- qt/aqt/sidebar.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qt/aqt/sidebar.py b/qt/aqt/sidebar.py index 54ba6e959..f7e84223a 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -1103,9 +1103,9 @@ class SidebarTreeView(QTreeView): return menu.addSeparator() - if any(not item.expanded for item in selected_items): + if any(not item.expanded for item in selected_items if item.children): menu.addAction(tr(TR.BROWSING_SIDEBAR_EXPAND), lambda: set_expanded(True)) - if any(item.expanded for item in selected_items): + if any(item.expanded for item in selected_items if item.children): menu.addAction( tr(TR.BROWSING_SIDEBAR_COLLAPSE), lambda: set_expanded(False) ) From cce1b1f7021b1a2a7a7c58f628321149df9411b5 Mon Sep 17 00:00:00 2001 From: RumovZ Date: Fri, 5 Mar 2021 12:22:49 +0100 Subject: [PATCH 35/57] Remove context action dict Now that almost all actions can be triggered from outside the context menu and are available for more than one item type, it's easier to check for available actions dynamically. --- qt/aqt/sidebar.py | 29 +++++++++++------------------ 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/qt/aqt/sidebar.py b/qt/aqt/sidebar.py index f7e84223a..f9bd9f6c2 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -6,7 +6,7 @@ from __future__ import annotations import re from concurrent.futures import Future from enum import Enum, auto -from typing import Any, Dict, Iterable, List, Optional, Sequence, Tuple, cast +from typing import Dict, Iterable, List, Optional, Tuple, cast import aqt from anki.collection import Config, SearchJoiner, SearchNode @@ -368,13 +368,6 @@ class SidebarTreeView(QTreeView): self.setContextMenuPolicy(Qt.CustomContextMenu) self.customContextMenuRequested.connect(self.onContextMenu) # type: ignore - self.context_menus: Dict[SidebarItemType, Sequence[Tuple[str, Callable]]] = { - SidebarItemType.NOTETYPE: ((tr(TR.ACTIONS_MANAGE), self.manage_notetype),), - SidebarItemType.SAVED_SEARCH_ROOT: ( - (tr(TR.BROWSING_SIDEBAR_SAVE_CURRENT_SEARCH), self.save_current_search), - ), - } - self.setUniformRowHeights(True) self.setHeaderHidden(True) self.setIndentation(15) @@ -1044,19 +1037,11 @@ class SidebarTreeView(QTreeView): self, item: SidebarItem, index: Optional[QModelIndex] ) -> None: m = QMenu() - + self._maybe_add_type_specific_actions(m, item) if item.item_type.is_deletable(): m.addAction(tr(TR.ACTIONS_DELETE), lambda: self._on_delete(index)) if item.item_type.is_editable(): m.addAction(tr(TR.ACTIONS_RENAME), lambda: self.edit(index)) - - if item.item_type in self.context_menus: - for action in self.context_menus[item.item_type]: - act_name = action[0] - act_func = action[1] - a = m.addAction(act_name) - qconnect(a.triggered, lambda _, func=act_func: func(item)) - self._maybe_add_search_actions(m) self._maybe_add_tree_actions(m) @@ -1065,6 +1050,14 @@ class SidebarTreeView(QTreeView): m.exec_(QCursor.pos()) + def _maybe_add_type_specific_actions(self, menu: QMenu, item: SidebarItem) -> None: + if item.item_type in (SidebarItemType.NOTETYPE, SidebarItemType.NOTETYPE_ROOT): + menu.addAction(tr(TR.ACTIONS_MANAGE), lambda: self.manage_notetype(item)) + elif item.item_type == SidebarItemType.SAVED_SEARCH_ROOT: + menu.addAction( + tr(TR.BROWSING_SIDEBAR_SAVE_CURRENT_SEARCH), self.save_current_search + ) + def _maybe_add_search_actions(self, menu: QMenu) -> None: nodes = [ item.search_node for item in self._selected_items() if item.search_node @@ -1298,7 +1291,7 @@ class SidebarTreeView(QTreeView): and item.name == new_name ) - def save_current_search(self, _item: Any = None) -> None: + def save_current_search(self) -> None: try: filt = self.col.build_search_string( self.browser.form.searchEdit.lineEdit().text() From 23777eed673202458ec9fea905bde649957a4a83 Mon Sep 17 00:00:00 2001 From: RumovZ Date: Sun, 7 Mar 2021 09:47:17 +0100 Subject: [PATCH 36/57] Fix repainting in case of tree building exception --- qt/aqt/sidebar.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qt/aqt/sidebar.py b/qt/aqt/sidebar.py index f9bd9f6c2..9d528267b 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -428,6 +428,7 @@ class SidebarTreeView(QTreeView): return def on_done(fut: Future) -> None: + self.setUpdatesEnabled(True) root = fut.result() model = SidebarModel(self, root) @@ -439,7 +440,6 @@ class SidebarTreeView(QTreeView): self.search_for(self.current_search) else: self._expand_where_necessary(model) - self.setUpdatesEnabled(True) if is_current: self.restore_current(is_current) From f72daacae499bf2fd2f5c848b8528ee9324d4de3 Mon Sep 17 00:00:00 2001 From: RumovZ Date: Sun, 7 Mar 2021 10:30:20 +0100 Subject: [PATCH 37/57] Only show edit actions with conform selection --- qt/aqt/sidebar.py | 50 +++++++++++++++++++++++++---------------------- 1 file changed, 27 insertions(+), 23 deletions(-) diff --git a/qt/aqt/sidebar.py b/qt/aqt/sidebar.py index 9d528267b..08b9b2cb8 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -1025,30 +1025,20 @@ class SidebarTreeView(QTreeView): ########################### def onContextMenu(self, point: QPoint) -> None: - idx: QModelIndex = self.indexAt(point) - item = self.model().item_for_index(idx) - if not item: - return - self.show_context_menu(item, idx) + index: QModelIndex = self.indexAt(point) + item = self.model().item_for_index(index) + if item and self.selectionModel().isSelected(index): + self.show_context_menu(item, index) - # idx is only None when triggering the context menu from a left click on - # saved searches - perhaps there is a better way to handle that? - def show_context_menu( - self, item: SidebarItem, index: Optional[QModelIndex] - ) -> None: - m = QMenu() - self._maybe_add_type_specific_actions(m, item) - if item.item_type.is_deletable(): - m.addAction(tr(TR.ACTIONS_DELETE), lambda: self._on_delete(index)) - if item.item_type.is_editable(): - m.addAction(tr(TR.ACTIONS_RENAME), lambda: self.edit(index)) - self._maybe_add_search_actions(m) - self._maybe_add_tree_actions(m) - - if not m.children(): - return - - m.exec_(QCursor.pos()) + def show_context_menu(self, item: SidebarItem, index: QModelIndex) -> None: + menu = QMenu() + self._maybe_add_type_specific_actions(menu, item) + self._maybe_add_delete_action(menu, item, index) + self._maybe_add_rename_action(menu, item, index) + self._maybe_add_search_actions(menu) + self._maybe_add_tree_actions(menu) + if menu.children(): + menu.exec_(QCursor.pos()) def _maybe_add_type_specific_actions(self, menu: QMenu, item: SidebarItem) -> None: if item.item_type in (SidebarItemType.NOTETYPE, SidebarItemType.NOTETYPE_ROOT): @@ -1058,6 +1048,20 @@ class SidebarTreeView(QTreeView): tr(TR.BROWSING_SIDEBAR_SAVE_CURRENT_SEARCH), self.save_current_search ) + def _maybe_add_delete_action( + self, menu: QMenu, item: SidebarItem, index: QModelIndex + ) -> None: + if item.item_type.is_deletable() and all( + s.item_type == item.item_type for s in self._selected_items() + ): + menu.addAction(tr(TR.ACTIONS_DELETE), lambda: self._on_delete(index)) + + def _maybe_add_rename_action( + self, menu: QMenu, item: SidebarItem, index: QModelIndex + ) -> None: + if item.item_type.is_editable() and len(self._selected_items()) == 1: + menu.addAction(tr(TR.ACTIONS_RENAME), lambda: self.edit(index)) + def _maybe_add_search_actions(self, menu: QMenu) -> None: nodes = [ item.search_node for item in self._selected_items() if item.search_node From 6c4d6457fb8f37bd65322ee028591942b5119d1b Mon Sep 17 00:00:00 2001 From: RumovZ Date: Sun, 7 Mar 2021 11:05:43 +0100 Subject: [PATCH 38/57] Simplify multi deletion confirmation strings --- ftl/core/browsing.ftl | 6 +----- ftl/core/decks.ftl | 2 +- qt/aqt/deckbrowser.py | 4 +--- qt/aqt/sidebar.py | 2 +- 4 files changed, 4 insertions(+), 10 deletions(-) diff --git a/ftl/core/browsing.ftl b/ftl/core/browsing.ftl index c5a13ad0e..06160d468 100644 --- a/ftl/core/browsing.ftl +++ b/ftl/core/browsing.ftl @@ -22,11 +22,7 @@ browsing-change-to = Change { $val } to: browsing-clear-unused = Clear Unused browsing-clear-unused-tags = Clear Unused Tags browsing-confirm-saved-search-overwrite = A saved search with the name { $name } already exists. Do you want to overwrite it? -browsing-confirm-saved-searches-deletion = - { $count -> - [one] Are you sure you want to delete the selected saved search? - *[other] Are you sure you want to delete the { $count } selected saved searches? - } +browsing-confirm-saved-searches-deletion = Are you sure you want to delete all selected saved searches? browsing-created = Created browsing-ctrlandshiftande = Ctrl+Shift+E browsing-current-deck = Current Deck diff --git a/ftl/core/decks.ftl b/ftl/core/decks.ftl index b4b942b10..29e359c5c 100644 --- a/ftl/core/decks.ftl +++ b/ftl/core/decks.ftl @@ -2,7 +2,7 @@ decks-add-new-deck-ctrlandn = Add New Deck (Ctrl+N) decks-are-you-sure-you-wish-to = Are you sure you wish to delete { $val }? decks-build = Build decks-cards-selected-by = cards selected by -decks-confirm-deletion = Are you sure you wish to delete { $deck_count } decks including { $card_count } cards? +decks-confirm-deletion = Are you sure you want to delete all selected decks including { $count } cards? decks-create-deck = Create Deck decks-custom-steps-in-minutes = Custom steps (in minutes) decks-deck = Deck diff --git a/qt/aqt/deckbrowser.py b/qt/aqt/deckbrowser.py index 1b9463c07..1afc1c298 100644 --- a/qt/aqt/deckbrowser.py +++ b/qt/aqt/deckbrowser.py @@ -313,9 +313,7 @@ class DeckBrowser: f"{tr(TR.DECKS_ARE_YOU_SURE_YOU_WISH_TO, val=decks[0]['name'])} {extra}" ) - return askUser( - tr(TR.DECKS_CONFIRM_DELETION, deck_count=len(dids), card_count=count) - ) + return askUser(tr(TR.DECKS_CONFIRM_DELETION, count=count)) def _delete(self, did: int) -> None: if self.ask_delete_deck(did): diff --git a/qt/aqt/sidebar.py b/qt/aqt/sidebar.py index 08b9b2cb8..20ada84cd 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -1267,7 +1267,7 @@ class SidebarTreeView(QTreeView): if len(selected) == 1: query = tr(TR.BROWSING_REMOVE_FROM_YOUR_SAVED_SEARCHES, val=selected[0]) else: - query = tr(TR.BROWSING_CONFIRM_SAVED_SEARCHES_DELETION, count=len(selected)) + query = tr(TR.BROWSING_CONFIRM_SAVED_SEARCHES_DELETION) if not askUser(query): return conf = self._get_saved_searches() From f07890c178053265bfc76edfbc9a670ad60b4007 Mon Sep 17 00:00:00 2001 From: RumovZ Date: Sun, 7 Mar 2021 11:40:11 +0100 Subject: [PATCH 39/57] Ask before removing tags from collection --- ftl/core/browsing.ftl | 2 ++ qt/aqt/sidebar.py | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/ftl/core/browsing.ftl b/ftl/core/browsing.ftl index 06160d468..222843473 100644 --- a/ftl/core/browsing.ftl +++ b/ftl/core/browsing.ftl @@ -134,3 +134,5 @@ browsing-edited-today = Edited browsing-sidebar-due-today = Due browsing-sidebar-untagged = Untagged browsing-sidebar-overdue = Overdue +browsing-sidebar-remove-tag = Are you sure you want to delete the tag “{ $name }” from { $count } notes? +browsing-sidebar-remove-tags = Are you sure you want to delete all selected tags from { $count } notes? diff --git a/qt/aqt/sidebar.py b/qt/aqt/sidebar.py index 20ada84cd..2709335ed 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -1142,6 +1142,8 @@ class SidebarTreeView(QTreeView): def _remove_tags(self, _item: SidebarItem) -> None: tags = self._selected_tags() + if not self.ask_remove_tags(tags): + return def do_remove() -> None: self.col._backend.expunge_tags(" ".join(tags)) @@ -1349,3 +1351,19 @@ class SidebarTreeView(QTreeView): for item in self._selected_items() if item.item_type == SidebarItemType.TAG ] + + def ask_remove_tags(self, tags: List[str]) -> bool: + count = len( + self.col.find_notes( + self.col.build_search_string( + *(SearchNode(tag=tag) for tag in tags), joiner="OR" + ) + ) + ) + if not count: + return True + if len(tags) == 1: + return askUser( + tr(TR.BROWSING_SIDEBAR_REMOVE_TAG, name=tags[0], count=count) + ) + return askUser(tr(TR.BROWSING_SIDEBAR_REMOVE_TAGS, count=count)) From 8d9072193ce82adb05d7736052268dfebc9b7067 Mon Sep 17 00:00:00 2001 From: RumovZ Date: Mon, 8 Mar 2021 11:35:39 +0100 Subject: [PATCH 40/57] Enable drag for all sidebar items ... ... and set valid drop targets dynamically based on the current selection. --- qt/aqt/sidebar.py | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/qt/aqt/sidebar.py b/qt/aqt/sidebar.py index 2709335ed..45a6c99cb 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -266,16 +266,10 @@ class SidebarModel(QAbstractItemModel): def flags(self, index: QModelIndex) -> Qt.ItemFlags: if not index.isValid(): return cast(Qt.ItemFlags, Qt.ItemIsEnabled) - flags = Qt.ItemIsEnabled | Qt.ItemIsSelectable - + flags = Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsDragEnabled item: SidebarItem = index.internalPointer() - if item.item_type in ( - SidebarItemType.DECK, - SidebarItemType.DECK_ROOT, - SidebarItemType.TAG, - SidebarItemType.TAG_ROOT, - ): - flags |= Qt.ItemIsDragEnabled | Qt.ItemIsDropEnabled + if item.item_type in self.sidebar.valid_drop_types: + flags |= Qt.ItemIsDropEnabled if item.item_type.is_editable(): flags |= Qt.ItemIsEditable @@ -365,6 +359,7 @@ class SidebarTreeView(QTreeView): self.mw = browser.mw self.col = self.mw.col self.current_search: Optional[str] = None + self.valid_drop_types: Tuple[SidebarItemType, ...] = () self.setContextMenuPolicy(Qt.CustomContextMenu) self.customContextMenuRequested.connect(self.onContextMenu) # type: ignore @@ -436,6 +431,7 @@ class SidebarTreeView(QTreeView): # tester = QAbstractItemModelTester(model) self.setModel(model) + qconnect(self.selectionModel().selectionChanged, self._on_selection_changed) if self.current_search: self.search_for(self.current_search) else: @@ -568,6 +564,15 @@ class SidebarTreeView(QTreeView): ########### + def _on_selection_changed(self, _new: QItemSelection, _old: QItemSelection) -> None: + selected_types = [item.item_type for item in self._selected_items()] + if all(item_type == SidebarItemType.DECK for item_type in selected_types): + self.valid_drop_types = (SidebarItemType.DECK, SidebarItemType.DECK_ROOT) + elif all(item_type == SidebarItemType.TAG for item_type in selected_types): + self.valid_drop_types = (SidebarItemType.TAG, SidebarItemType.TAG_ROOT) + else: + self.valid_drop_types = () + 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) From 08c09bcb0f1ebf87b65e9eced9f29891b97c25b7 Mon Sep 17 00:00:00 2001 From: RumovZ Date: Mon, 8 Mar 2021 11:55:15 +0100 Subject: [PATCH 41/57] Remove edit mode --- ftl/core/actions.ftl | 1 + qt/aqt/forms/icons.qrc | 1 - qt/aqt/forms/icons/edit.svg | 83 ------------------------------------- qt/aqt/sidebar.py | 27 ++++-------- 4 files changed, 10 insertions(+), 102 deletions(-) delete mode 100644 qt/aqt/forms/icons/edit.svg diff --git a/ftl/core/actions.ftl b/ftl/core/actions.ftl index bbeec3fc5..dd15e114c 100644 --- a/ftl/core/actions.ftl +++ b/ftl/core/actions.ftl @@ -32,6 +32,7 @@ actions-replay-audio = Replay Audio actions-reposition = Reposition actions-save = Save actions-search = Search +actions-select = Select actions-shortcut-key = Shortcut key: { $val } actions-suspend-card = Suspend Card actions-set-due-date = Set Due Date diff --git a/qt/aqt/forms/icons.qrc b/qt/aqt/forms/icons.qrc index e11359d8a..23dd5f5c5 100644 --- a/qt/aqt/forms/icons.qrc +++ b/qt/aqt/forms/icons.qrc @@ -12,6 +12,5 @@ icons/flag.svg icons/select.svg icons/magnifying_glass.svg - icons/edit.svg diff --git a/qt/aqt/forms/icons/edit.svg b/qt/aqt/forms/icons/edit.svg deleted file mode 100644 index f8e4e5521..000000000 --- a/qt/aqt/forms/icons/edit.svg +++ /dev/null @@ -1,83 +0,0 @@ - - - - - - - - - - image/svg+xml - - - - - - - - - - - diff --git a/qt/aqt/sidebar.py b/qt/aqt/sidebar.py index 45a6c99cb..9d4f512a7 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -33,7 +33,6 @@ from aqt.utils import ( class SidebarTool(Enum): SELECT = auto() SEARCH = auto() - EDIT = auto() class SidebarItemType(Enum): @@ -277,10 +276,9 @@ class SidebarModel(QAbstractItemModel): class SidebarToolbar(QToolBar): - _tools: Tuple[Tuple[SidebarTool, str, str], ...] = ( - (SidebarTool.SEARCH, ":/icons/magnifying_glass.svg", "search"), - (SidebarTool.SELECT, ":/icons/select.svg", "select"), - (SidebarTool.EDIT, ":/icons/edit.svg", "edit"), + _tools: Tuple[Tuple[SidebarTool, str, TR.V], ...] = ( + (SidebarTool.SEARCH, ":/icons/magnifying_glass.svg", TR.ACTIONS_SEARCH), + (SidebarTool.SELECT, ":/icons/select.svg", TR.ACTIONS_SELECT), ) def __init__(self, sidebar: SidebarTreeView) -> None: @@ -293,7 +291,9 @@ class SidebarToolbar(QToolBar): def _setup_tools(self) -> None: for row in self._tools: - action = self.addAction(theme_manager.icon_from_resources(row[1]), row[2]) + action = self.addAction( + theme_manager.icon_from_resources(row[1]), tr(row[2]) + ) action.setCheckable(True) self._action_group.addAction(action) saved = self.sidebar.col.get_config("sidebarTool", 0) @@ -368,6 +368,7 @@ class SidebarTreeView(QTreeView): self.setIndentation(15) self.setAutoExpandDelay(600) self.setDragDropOverwriteMode(False) + self.setEditTriggers(QAbstractItemView.EditKeyPressed) qconnect(self.expanded, self._on_expansion) qconnect(self.collapsed, self._on_collapse) @@ -393,24 +394,14 @@ class SidebarTreeView(QTreeView): @tool.setter def tool(self, tool: SidebarTool) -> None: self._tool = tool - if tool == SidebarTool.SELECT: - selection_mode = QAbstractItemView.ExtendedSelection - drag_drop_mode = QAbstractItemView.NoDragDrop - edit_triggers = QAbstractItemView.EditKeyPressed - elif tool == SidebarTool.SEARCH: + if tool == SidebarTool.SEARCH: selection_mode = QAbstractItemView.SingleSelection drag_drop_mode = QAbstractItemView.NoDragDrop - edit_triggers = QAbstractItemView.EditKeyPressed - elif tool == SidebarTool.EDIT: + else: selection_mode = QAbstractItemView.ExtendedSelection drag_drop_mode = QAbstractItemView.InternalMove - edit_triggers = cast( - QAbstractItemView.EditTriggers, - QAbstractItemView.DoubleClicked | QAbstractItemView.EditKeyPressed, - ) self.setSelectionMode(selection_mode) self.setDragDropMode(drag_drop_mode) - self.setEditTriggers(edit_triggers) def model(self) -> SidebarModel: return super().model() From 28402c701578731d9ea19216fd20f434ada86ee1 Mon Sep 17 00:00:00 2001 From: RumovZ Date: Tue, 9 Mar 2021 08:50:01 +0100 Subject: [PATCH 42/57] Improve toolbar styling for macOS --- qt/aqt/sidebar.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/qt/aqt/sidebar.py b/qt/aqt/sidebar.py index 9d4f512a7..ede513fc0 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -287,7 +287,8 @@ class SidebarToolbar(QToolBar): self._action_group = QActionGroup(self) qconnect(self._action_group.triggered, self._on_action_group_triggered) self._setup_tools() - self.setIconSize(QSize(18, 18)) + self.setIconSize(QSize(16, 16)) + self.setStyle(QStyleFactory.create("fusion")) def _setup_tools(self) -> None: for row in self._tools: From 3f772ce0fe044163beaa15723e1493252dee932a Mon Sep 17 00:00:00 2001 From: RumovZ Date: Tue, 9 Mar 2021 11:19:44 +0100 Subject: [PATCH 43/57] Add shortcuts for sidebar tools --- qt/aqt/sidebar.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/qt/aqt/sidebar.py b/qt/aqt/sidebar.py index ede513fc0..faf1cd05d 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -291,11 +291,12 @@ class SidebarToolbar(QToolBar): self.setStyle(QStyleFactory.create("fusion")) def _setup_tools(self) -> None: - for row in self._tools: + for row, tool in enumerate(self._tools): action = self.addAction( - theme_manager.icon_from_resources(row[1]), tr(row[2]) + theme_manager.icon_from_resources(tool[1]), tr(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 From a9ea7e39aea702c50ef1fc43025ffbf914f18044 Mon Sep 17 00:00:00 2001 From: RumovZ Date: Tue, 9 Mar 2021 20:18:12 +0100 Subject: [PATCH 44/57] Disable expand on double click in search mode --- qt/aqt/sidebar.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/qt/aqt/sidebar.py b/qt/aqt/sidebar.py index faf1cd05d..624b8e3d4 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -399,11 +399,14 @@ class SidebarTreeView(QTreeView): if tool == SidebarTool.SEARCH: selection_mode = QAbstractItemView.SingleSelection drag_drop_mode = QAbstractItemView.NoDragDrop + double_click_expands = False else: selection_mode = QAbstractItemView.ExtendedSelection drag_drop_mode = QAbstractItemView.InternalMove + double_click_expands = True self.setSelectionMode(selection_mode) self.setDragDropMode(drag_drop_mode) + self.setExpandsOnDoubleClick(double_click_expands) def model(self) -> SidebarModel: return super().model() From 9a844591feffbd103281209b851b01b4d1cc4fbc Mon Sep 17 00:00:00 2001 From: RumovZ Date: Tue, 9 Mar 2021 20:36:15 +0100 Subject: [PATCH 45/57] Ensure mouse is at current index before searching Thus, no search will be triggered when clicking an expansion indicator as this doesn't update the current element. However, if the indicator belongs to the current item, a search will be triggered anyway. --- qt/aqt/sidebar.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qt/aqt/sidebar.py b/qt/aqt/sidebar.py index 624b8e3d4..729b815e7 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -545,8 +545,8 @@ class SidebarTreeView(QTreeView): def mouseReleaseEvent(self, event: QMouseEvent) -> None: super().mouseReleaseEvent(event) if self.tool == SidebarTool.SEARCH and event.button() == Qt.LeftButton: - idx = self.indexAt(event.pos()) - self._on_search(idx) + if (index := self.currentIndex()) == self.indexAt(event.pos()): + self._on_search(index) def keyPressEvent(self, event: QKeyEvent) -> None: index = self.currentIndex() From ffbb0b7c077dcd21de959c5dd05fd004c11d4f3c Mon Sep 17 00:00:00 2001 From: RumovZ Date: Wed, 10 Mar 2021 10:14:06 +0100 Subject: [PATCH 46/57] Disable renaming models and templates ... ... but add context action CLayout for templates. --- qt/aqt/sidebar.py | 41 +++++++++++------------------------------ 1 file changed, 11 insertions(+), 30 deletions(-) diff --git a/qt/aqt/sidebar.py b/qt/aqt/sidebar.py index 729b815e7..6dc14f9dd 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -12,9 +12,11 @@ import aqt from anki.collection import Config, SearchJoiner, SearchNode from anki.decks import DeckTreeNode from anki.errors import DeckRenameError, InvalidInput +from anki.notes import Note from anki.tags import TagTreeNode from anki.types import assert_exhaustive from aqt import colors, gui_hooks +from aqt.clayout import CardLayout from aqt.main import ResetReason from aqt.models import Models from aqt.qt import * @@ -69,8 +71,6 @@ class SidebarItemType(Enum): SidebarItemType.SAVED_SEARCH, SidebarItemType.DECK, SidebarItemType.TAG, - SidebarItemType.NOTETYPE, - SidebarItemType.NOTETYPE_TEMPLATE, ) def is_deletable(self) -> bool: @@ -1043,7 +1043,11 @@ class SidebarTreeView(QTreeView): def _maybe_add_type_specific_actions(self, menu: QMenu, item: SidebarItem) -> None: if item.item_type in (SidebarItemType.NOTETYPE, SidebarItemType.NOTETYPE_ROOT): - menu.addAction(tr(TR.ACTIONS_MANAGE), lambda: self.manage_notetype(item)) + menu.addAction( + tr(TR.BROWSING_MANAGE_NOTE_TYPES), lambda: self.manage_notetype(item) + ) + elif item.item_type == SidebarItemType.NOTETYPE_TEMPLATE: + menu.addAction(tr(TR.NOTETYPES_CARDS), lambda: self.manage_template(item)) elif item.item_type == SidebarItemType.SAVED_SEARCH_ROOT: menu.addAction( tr(TR.BROWSING_SIDEBAR_SAVE_CURRENT_SEARCH), self.save_current_search @@ -1215,29 +1219,6 @@ class SidebarTreeView(QTreeView): self.browser.model.beginReset() self.mw.taskman.run_in_background(do_delete, on_done) - def rename_notetype(self, item: SidebarItem, new_name: str) -> None: - notetype = self.col.models.get(item.id) - self.mw.checkpoint(tr(TR.ACTIONS_RENAME)) - notetype["name"] = new_name - self.col.models.save(notetype) - self.refresh( - lambda other: other.item_type == SidebarItemType.NOTETYPE - and other.id == item.id - ) - self.browser.model.reset() - - def rename_template(self, item: SidebarItem, new_name: str) -> None: - notetype = self.col.models.get(item._parent_item.id) - self.mw.checkpoint(tr(TR.ACTIONS_RENAME)) - notetype["tmpls"][item.id]["name"] = new_name - self.col.models.save(notetype) - self.refresh( - lambda other: other.item_type == SidebarItemType.NOTETYPE_TEMPLATE - and other._parent_item.id == item._parent_item.id - and other.id == item.id - ) - self.browser.model.reset() - def rename_node(self, item: SidebarItem, text: str) -> bool: new_name = text.replace('"', "") if new_name and new_name != item.name: @@ -1247,10 +1228,6 @@ class SidebarTreeView(QTreeView): self.rename_saved_search(item, new_name) elif item.item_type == SidebarItemType.TAG: self.rename_tag(item, new_name) - elif item.item_type == SidebarItemType.NOTETYPE: - self.rename_notetype(item, new_name) - elif item.item_type == SidebarItemType.NOTETYPE_TEMPLATE: - self.rename_template(item, new_name) # renaming may be asynchronous so always return False return False @@ -1326,6 +1303,10 @@ class SidebarTreeView(QTreeView): self.mw, parent=self.browser, fromMain=True, selected_notetype_id=item.id ) + def manage_template(self, item: SidebarItem) -> None: + note = Note(self.col, self.col.models.get(item._parent_item.id)) + CardLayout(self.mw, note, ord=item.id, parent=self, fill_empty=True) + # Helpers ################## From 602ffa67a7a6c60507083fa3240546780c79f7b4 Mon Sep 17 00:00:00 2001 From: RumovZ Date: Wed, 10 Mar 2021 11:34:28 +0100 Subject: [PATCH 47/57] Update about screen --- qt/aqt/about.py | 1 + 1 file changed, 1 insertion(+) diff --git a/qt/aqt/about.py b/qt/aqt/about.py index 04d577c2d..da8efe13b 100644 --- a/qt/aqt/about.py +++ b/qt/aqt/about.py @@ -205,6 +205,7 @@ def show(mw: aqt.AnkiQt) -> QDialog: "Gustavo Costa", "余时行", "叶峻峣", + "RumovZ", ) ) From e1db8e1da17ddf155c4baf1c928296d6b61e7c1a Mon Sep 17 00:00:00 2001 From: RumovZ Date: Wed, 10 Mar 2021 15:56:54 +0100 Subject: [PATCH 48/57] Borrow dids in remove_decks_and_child_decks --- rslib/src/backend/mod.rs | 2 +- rslib/src/decks/mod.rs | 6 +++--- rslib/src/sync/mod.rs | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/rslib/src/backend/mod.rs b/rslib/src/backend/mod.rs index 511a9830b..fa399529a 100644 --- a/rslib/src/backend/mod.rs +++ b/rslib/src/backend/mod.rs @@ -663,7 +663,7 @@ impl BackendService for Backend { } fn remove_decks(&self, input: pb::DeckIDs) -> BackendResult { - self.with_col(|col| col.remove_decks_and_child_decks(input.into())) + self.with_col(|col| col.remove_decks_and_child_decks(&Into::>::into(input))) .map(Into::into) } diff --git a/rslib/src/decks/mod.rs b/rslib/src/decks/mod.rs index 0aeccf8a0..763aaf7db 100644 --- a/rslib/src/decks/mod.rs +++ b/rslib/src/decks/mod.rs @@ -440,14 +440,14 @@ impl Collection { self.storage.get_deck_id(&machine_name) } - pub fn remove_decks_and_child_decks(&mut self, dids: Vec) -> Result<()> { + pub fn remove_decks_and_child_decks(&mut self, dids: &[DeckID]) -> Result<()> { // fixme: vet cache clearing self.state.deck_cache.clear(); self.transact(None, |col| { let usn = col.usn()?; for did in dids { - if let Some(deck) = col.storage.get_deck(did)? { + if let Some(deck) = col.storage.get_deck(*did)? { let child_decks = col.storage.child_decks(&deck)?; // top level @@ -779,7 +779,7 @@ mod test { // delete top level let top = col.get_or_create_normal_deck("one")?; - col.remove_decks_and_child_decks(vec![top.id])?; + col.remove_decks_and_child_decks(&[top.id])?; // should have come back as "Default+" due to conflict assert_eq!(sorted_names(&col), vec!["default", "Default+"]); diff --git a/rslib/src/sync/mod.rs b/rslib/src/sync/mod.rs index 8c899c4e6..dbfa87d0c 100644 --- a/rslib/src/sync/mod.rs +++ b/rslib/src/sync/mod.rs @@ -1532,7 +1532,7 @@ mod test { col1.remove_cards_and_orphaned_notes(&[cardid])?; let usn = col1.usn()?; col1.remove_note_only_undoable(noteid, usn)?; - col1.remove_decks_and_child_decks(vec![deckid])?; + col1.remove_decks_and_child_decks(&[deckid])?; let out = ctx.normal_sync(&mut col1).await; assert_eq!(out.required, SyncActionRequired::NoChanges); From bc7043c38409205071c71ab07b019ac800bdd46b Mon Sep 17 00:00:00 2001 From: RumovZ Date: Wed, 10 Mar 2021 16:38:29 +0100 Subject: [PATCH 49/57] Store name prefix of sidebar items --- qt/aqt/sidebar.py | 23 ++++++++--------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/qt/aqt/sidebar.py b/qt/aqt/sidebar.py index 4b42a7b55..f484dd248 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -3,7 +3,6 @@ from __future__ import annotations -import re from concurrent.futures import Future from enum import Enum, auto from typing import Dict, Iterable, List, Optional, Tuple, cast @@ -102,12 +101,11 @@ class SidebarItem: expanded: bool = False, item_type: SidebarItemType = SidebarItemType.CUSTOM, id: int = 0, - full_name: str = None, + name_prefix: str = "", ) -> None: self.name = name - if not full_name: - full_name = name - self.full_name = full_name + self.name_prefix = name_prefix + self.full_name = name_prefix + name self.icon = icon self.item_type = item_type self.id = id @@ -917,7 +915,7 @@ class SidebarTreeView(QTreeView): on_expanded=toggle_expand(), expanded=node.expanded, item_type=SidebarItemType.TAG, - full_name=head + node.name, + name_prefix=head, ) root.add_child(item) newhead = f"{head + node.name}::" @@ -964,7 +962,7 @@ class SidebarTreeView(QTreeView): expanded=not node.collapsed, item_type=SidebarItemType.DECK, id=node.deck_id, - full_name=head + node.name, + name_prefix=head, ) root.add_child(item) newhead = f"{head + node.name}::" @@ -1019,7 +1017,7 @@ class SidebarTreeView(QTreeView): SearchNode(note=nt["name"]), SearchNode(template=c) ), item_type=SidebarItemType.NOTETYPE_TEMPLATE, - full_name=f"{nt['name']}::{tmpl['name']}", + name_prefix=f"{nt['name']}::", id=tmpl["ord"], ) item.add_child(child) @@ -1130,10 +1128,7 @@ class SidebarTreeView(QTreeView): def rename_deck(self, item: SidebarItem, new_name: str) -> None: deck = self.mw.col.decks.get(item.id) - old_name = deck["name"] - new_name = re.sub( - re.escape(item.name) + "$", new_name.replace("\\", r"\\"), old_name - ) + new_name = item.name_prefix + new_name self.mw.checkpoint(tr(TR.ACTIONS_RENAME_DECK)) try: self.mw.col.decks.rename(deck, new_name) @@ -1176,9 +1171,7 @@ class SidebarTreeView(QTreeView): def _rename_tag(self, item: SidebarItem, new_name: str) -> None: old_name = item.full_name - new_name = re.sub( - re.escape(item.name) + "$", new_name.replace("\\", r"\\"), old_name - ) + new_name = item.name_prefix + new_name def do_rename() -> int: self.mw.col.tags.remove(old_name) From 8e9331e42486ea57b1d56ef22b2f55e0ed5b8c1f Mon Sep 17 00:00:00 2001 From: RumovZ Date: Wed, 10 Mar 2021 21:50:46 +0100 Subject: [PATCH 50/57] Fix repainting in case of tag renaming exception --- qt/aqt/sidebar.py | 1 + 1 file changed, 1 insertion(+) diff --git a/qt/aqt/sidebar.py b/qt/aqt/sidebar.py index f484dd248..46f561798 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -1178,6 +1178,7 @@ class SidebarTreeView(QTreeView): return self.col.tags.rename(old_name, new_name) def on_done(fut: Future) -> None: + self.setUpdatesEnabled(True) self.mw.requireReset(reason=ResetReason.BrowserAddTags, context=self) self.browser.model.endReset() From 186a0202ea68332c022b96fdc01b7ff014190870 Mon Sep 17 00:00:00 2001 From: RumovZ Date: Thu, 11 Mar 2021 09:14:50 +0100 Subject: [PATCH 51/57] Show tooltip instead of prompt for removing tags --- ftl/core/browsing.ftl | 7 +++++-- qt/aqt/sidebar.py | 23 +++-------------------- rslib/backend.proto | 2 +- rslib/src/backend/generic.rs | 6 ++++++ rslib/src/backend/mod.rs | 5 ++--- 5 files changed, 17 insertions(+), 26 deletions(-) diff --git a/ftl/core/browsing.ftl b/ftl/core/browsing.ftl index 222843473..f20030b11 100644 --- a/ftl/core/browsing.ftl +++ b/ftl/core/browsing.ftl @@ -114,6 +114,11 @@ browsing-note-deleted = [one] { $count } note deleted. *[other] { $count } notes deleted. } +browsing-notes-updated = + { $count -> + [one] { $count } note updated. + *[other] { $count } notes updated. + } browsing-window-title = Browse ({ $selected } of { $total } cards selected) browsing-sidebar-expand = Expand browsing-sidebar-collapse = Collapse @@ -134,5 +139,3 @@ browsing-edited-today = Edited browsing-sidebar-due-today = Due browsing-sidebar-untagged = Untagged browsing-sidebar-overdue = Overdue -browsing-sidebar-remove-tag = Are you sure you want to delete the tag “{ $name }” from { $count } notes? -browsing-sidebar-remove-tags = Are you sure you want to delete all selected tags from { $count } notes? diff --git a/qt/aqt/sidebar.py b/qt/aqt/sidebar.py index 46f561798..e590a1eea 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -27,6 +27,7 @@ from aqt.utils import ( show_invalid_search_error, showInfo, showWarning, + tooltip, tr, ) @@ -1146,16 +1147,14 @@ class SidebarTreeView(QTreeView): def _remove_tags(self, _item: SidebarItem) -> None: tags = self._selected_tags() - if not self.ask_remove_tags(tags): - return def do_remove() -> None: - self.col._backend.expunge_tags(" ".join(tags)) + return self.col._backend.expunge_tags(" ".join(tags)) def on_done(fut: Future) -> None: self.mw.requireReset(reason=ResetReason.BrowserRemoveTags, context=self) self.browser.model.endReset() - fut.result() + tooltip(tr(TR.BROWSING_NOTES_UPDATED, count=fut.result()), parent=self) self.refresh() self.mw.checkpoint(tr(TR.ACTIONS_REMOVE_TAG)) @@ -1331,19 +1330,3 @@ class SidebarTreeView(QTreeView): for item in self._selected_items() if item.item_type == SidebarItemType.TAG ] - - def ask_remove_tags(self, tags: List[str]) -> bool: - count = len( - self.col.find_notes( - self.col.build_search_string( - *(SearchNode(tag=tag) for tag in tags), joiner="OR" - ) - ) - ) - if not count: - return True - if len(tags) == 1: - return askUser( - tr(TR.BROWSING_SIDEBAR_REMOVE_TAG, name=tags[0], count=count) - ) - return askUser(tr(TR.BROWSING_SIDEBAR_REMOVE_TAGS, count=count)) diff --git a/rslib/backend.proto b/rslib/backend.proto index 751c14116..db6ab7d8c 100644 --- a/rslib/backend.proto +++ b/rslib/backend.proto @@ -230,7 +230,7 @@ service BackendService { rpc ClearUnusedTags(Empty) returns (Empty); rpc AllTags(Empty) returns (StringList); - rpc ExpungeTags(String) returns (Empty); + rpc ExpungeTags(String) returns (UInt32); rpc SetTagExpanded(SetTagExpandedIn) returns (Empty); rpc ClearTag(String) returns (Empty); rpc TagTree(Empty) returns (TagTreeNode); diff --git a/rslib/src/backend/generic.rs b/rslib/src/backend/generic.rs index e554761f4..68664954f 100644 --- a/rslib/src/backend/generic.rs +++ b/rslib/src/backend/generic.rs @@ -33,6 +33,12 @@ impl From for pb::UInt32 { } } +impl From for pb::UInt32 { + fn from(val: usize) -> Self { + pb::UInt32 { val: val as u32 } + } +} + impl From<()> for pb::Empty { fn from(_val: ()) -> Self { pb::Empty {} diff --git a/rslib/src/backend/mod.rs b/rslib/src/backend/mod.rs index fa399529a..b42f9b179 100644 --- a/rslib/src/backend/mod.rs +++ b/rslib/src/backend/mod.rs @@ -1217,10 +1217,9 @@ impl BackendService for Backend { }) } - fn expunge_tags(&self, tags: pb::String) -> BackendResult { + fn expunge_tags(&self, tags: pb::String) -> BackendResult { self.with_col(|col| { - col.expunge_tags(tags.val.as_str())?; - Ok(().into()) + col.expunge_tags(tags.val.as_str()).map(Into::into) }) } From 337ef0ae212eb70ed739ac9cb890298a688dadd4 Mon Sep 17 00:00:00 2001 From: RumovZ Date: Thu, 11 Mar 2021 09:17:22 +0100 Subject: [PATCH 52/57] Show count of affected notes after tag renaming --- qt/aqt/sidebar.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/qt/aqt/sidebar.py b/qt/aqt/sidebar.py index e590a1eea..cd8d0e40a 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -1184,12 +1184,12 @@ class SidebarTreeView(QTreeView): count = fut.result() if not count: showInfo(tr(TR.BROWSING_TAG_RENAME_WARNING_EMPTY)) - return - - self.refresh( - lambda item: item.item_type == SidebarItemType.TAG - and item.full_name == new_name - ) + else: + tooltip(tr(TR.BROWSING_NOTES_UPDATED, count=count), parent=self) + self.refresh( + lambda item: item.item_type == SidebarItemType.TAG + and item.full_name == new_name + ) self.mw.checkpoint(tr(TR.ACTIONS_RENAME_TAG)) self.browser.model.beginReset() From 3219bb2539ae8613fc0d8af810f2bc3e7085a7c2 Mon Sep 17 00:00:00 2001 From: RumovZ Date: Thu, 11 Mar 2021 09:20:41 +0100 Subject: [PATCH 53/57] Remove prompt when deleting saved searches --- ftl/core/browsing.ftl | 4 ---- qt/aqt/sidebar.py | 6 ------ 2 files changed, 10 deletions(-) diff --git a/ftl/core/browsing.ftl b/ftl/core/browsing.ftl index f20030b11..47bbcf293 100644 --- a/ftl/core/browsing.ftl +++ b/ftl/core/browsing.ftl @@ -22,7 +22,6 @@ browsing-change-to = Change { $val } to: browsing-clear-unused = Clear Unused browsing-clear-unused-tags = Clear Unused Tags browsing-confirm-saved-search-overwrite = A saved search with the name { $name } already exists. Do you want to overwrite it? -browsing-confirm-saved-searches-deletion = Are you sure you want to delete all selected saved searches? browsing-created = Created browsing-ctrlandshiftande = Ctrl+Shift+E browsing-current-deck = Current Deck @@ -72,14 +71,11 @@ browsing-question = Question browsing-queue-bottom = Queue bottom: { $val } browsing-queue-top = Queue top: { $val } browsing-randomize-order = Randomize order -browsing-remove-current-filter = Remove Current Filter... -browsing-remove-from-your-saved-searches = Remove { $val } from your saved searches? browsing-remove-tags = Remove Tags... browsing-replace-with = Replace With: browsing-reposition = Reposition... browsing-reposition-new-cards = Reposition New Cards browsing-reschedule = Reschedule -browsing-save-current-filter = Save Current Filter... browsing-search-bar-hint = Search cards/notes (type text, then press Enter) browsing-search-in = Search in: browsing-search-within-formatting-slow = Search within formatting (slow) diff --git a/qt/aqt/sidebar.py b/qt/aqt/sidebar.py index cd8d0e40a..f135b475b 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -1241,12 +1241,6 @@ class SidebarTreeView(QTreeView): def remove_saved_searches(self, _item: SidebarItem) -> None: selected = self._selected_saved_searches() - if len(selected) == 1: - query = tr(TR.BROWSING_REMOVE_FROM_YOUR_SAVED_SEARCHES, val=selected[0]) - else: - query = tr(TR.BROWSING_CONFIRM_SAVED_SEARCHES_DELETION) - if not askUser(query): - return conf = self._get_saved_searches() for name in selected: del conf[name] From f1dd01048957e9beec21dc54ccfda272bb512d5e Mon Sep 17 00:00:00 2001 From: RumovZ Date: Thu, 11 Mar 2021 09:52:11 +0100 Subject: [PATCH 54/57] Remove deck remove prompt but show card count --- ftl/core/browsing.ftl | 5 +++++ ftl/core/decks.ftl | 1 - pylib/anki/decks.py | 4 ++-- qt/aqt/sidebar.py | 28 +++++++++++++--------------- rslib/backend.proto | 2 +- rslib/src/backend/mod.rs | 6 ++---- rslib/src/decks/mod.rs | 28 +++++++++++++++++----------- 7 files changed, 40 insertions(+), 34 deletions(-) diff --git a/ftl/core/browsing.ftl b/ftl/core/browsing.ftl index 47bbcf293..d473c8180 100644 --- a/ftl/core/browsing.ftl +++ b/ftl/core/browsing.ftl @@ -14,6 +14,11 @@ browsing-card = Card browsing-card-list = Card List browsing-card-state = Card State browsing-cards-cant-be-manually-moved-into = Cards can't be manually moved into a filtered deck. +browsing-cards-deleted = + { $count -> + [one] { $count } card deleted. + *[other] { $count } cards deleted. + } browsing-change-deck = Change Deck browsing-change-deck2 = Change Deck... browsing-change-note-type = Change Note Type diff --git a/ftl/core/decks.ftl b/ftl/core/decks.ftl index 3c74c863d..3ffe2a470 100644 --- a/ftl/core/decks.ftl +++ b/ftl/core/decks.ftl @@ -2,7 +2,6 @@ decks-add-new-deck-ctrlandn = Add New Deck (Ctrl+N) decks-are-you-sure-you-wish-to = Are you sure you wish to delete { $val }? decks-build = Build decks-cards-selected-by = cards selected by -decks-confirm-deletion = Are you sure you want to delete all selected decks including { $count } cards? decks-create-deck = Create Deck decks-custom-steps-in-minutes = Custom steps (in minutes) decks-deck = Deck diff --git a/pylib/anki/decks.py b/pylib/anki/decks.py index 9347f128d..7284af4fe 100644 --- a/pylib/anki/decks.py +++ b/pylib/anki/decks.py @@ -137,8 +137,8 @@ class DeckManager: assert cardsToo and childrenToo self.remove([did]) - def remove(self, dids: List[int]) -> None: - self.col._backend.remove_decks(dids) + def remove(self, dids: List[int]) -> int: + return self.col._backend.remove_decks(dids) def all_names_and_ids( self, skip_empty_default: bool = False, include_filtered: bool = True diff --git a/qt/aqt/sidebar.py b/qt/aqt/sidebar.py index f135b475b..5e72fe4de 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -1199,22 +1199,20 @@ class SidebarTreeView(QTreeView): self.browser.editor.saveNow(self._delete_decks) def _delete_decks(self) -> None: + def do_delete() -> None: + return self.mw.col.decks.remove(dids) + + def on_done(fut: Future) -> None: + self.mw.requireReset(reason=ResetReason.BrowserDeleteDeck, context=self) + self.browser.search() + self.browser.model.endReset() + tooltip(tr(TR.BROWSING_CARDS_DELETED, count=fut.result()), parent=self) + self.refresh() + dids = self._selected_decks() - if self.mw.deckBrowser.ask_delete_decks(dids): - - def do_delete() -> None: - return self.mw.col.decks.remove(dids) - - def on_done(fut: Future) -> None: - self.mw.requireReset(reason=ResetReason.BrowserDeleteDeck, context=self) - self.browser.search() - self.browser.model.endReset() - self.refresh() - res = fut.result() # Required to check for errors - - self.mw.checkpoint(tr(TR.DECKS_DELETE_DECK)) - self.browser.model.beginReset() - self.mw.taskman.run_in_background(do_delete, on_done) + self.mw.checkpoint(tr(TR.DECKS_DELETE_DECK)) + self.browser.model.beginReset() + self.mw.taskman.run_in_background(do_delete, on_done) def rename_node(self, item: SidebarItem, text: str) -> bool: new_name = text.replace('"', "") diff --git a/rslib/backend.proto b/rslib/backend.proto index db6ab7d8c..45ae4b6ed 100644 --- a/rslib/backend.proto +++ b/rslib/backend.proto @@ -151,7 +151,7 @@ service BackendService { rpc GetDeckLegacy(DeckID) returns (Json); rpc GetDeckNames(GetDeckNamesIn) returns (DeckNames); rpc NewDeckLegacy(Bool) returns (Json); - rpc RemoveDecks(DeckIDs) returns (Empty); + rpc RemoveDecks(DeckIDs) returns (UInt32); rpc DragDropDecks(DragDropDecksIn) returns (Empty); // deck config diff --git a/rslib/src/backend/mod.rs b/rslib/src/backend/mod.rs index b42f9b179..fc9b37ca4 100644 --- a/rslib/src/backend/mod.rs +++ b/rslib/src/backend/mod.rs @@ -662,7 +662,7 @@ impl BackendService for Backend { .map(Into::into) } - fn remove_decks(&self, input: pb::DeckIDs) -> BackendResult { + fn remove_decks(&self, input: pb::DeckIDs) -> BackendResult { self.with_col(|col| col.remove_decks_and_child_decks(&Into::>::into(input))) .map(Into::into) } @@ -1218,9 +1218,7 @@ impl BackendService for Backend { } fn expunge_tags(&self, tags: pb::String) -> BackendResult { - self.with_col(|col| { - col.expunge_tags(tags.val.as_str()).map(Into::into) - }) + self.with_col(|col| col.expunge_tags(tags.val.as_str()).map(Into::into)) } fn set_tag_expanded(&self, input: pb::SetTagExpandedIn) -> BackendResult { diff --git a/rslib/src/decks/mod.rs b/rslib/src/decks/mod.rs index 763aaf7db..668de14a3 100644 --- a/rslib/src/decks/mod.rs +++ b/rslib/src/decks/mod.rs @@ -440,9 +440,10 @@ impl Collection { self.storage.get_deck_id(&machine_name) } - pub fn remove_decks_and_child_decks(&mut self, dids: &[DeckID]) -> Result<()> { + pub fn remove_decks_and_child_decks(&mut self, dids: &[DeckID]) -> Result { // fixme: vet cache clearing self.state.deck_cache.clear(); + let mut card_count = 0; self.transact(None, |col| { let usn = col.usn()?; @@ -451,24 +452,28 @@ impl Collection { let child_decks = col.storage.child_decks(&deck)?; // top level - col.remove_single_deck(&deck, usn)?; + card_count += col.remove_single_deck(&deck, usn)?; // remove children for deck in child_decks { - col.remove_single_deck(&deck, usn)?; + card_count += col.remove_single_deck(&deck, usn)?; } } } Ok(()) - }) + })?; + Ok(card_count) } - pub(crate) fn remove_single_deck(&mut self, deck: &Deck, usn: Usn) -> Result<()> { + pub(crate) fn remove_single_deck(&mut self, deck: &Deck, usn: Usn) -> Result { // fixme: undo - match deck.kind { + let card_count = match deck.kind { DeckKind::Normal(_) => self.delete_all_cards_in_normal_deck(deck.id)?, - DeckKind::Filtered(_) => self.return_all_cards_in_filtered_deck(deck.id)?, - } + DeckKind::Filtered(_) => { + self.return_all_cards_in_filtered_deck(deck.id)?; + 0 + } + }; self.clear_aux_config_for_deck(deck.id)?; if deck.id.0 == 1 { let mut deck = deck.to_owned(); @@ -480,12 +485,13 @@ impl Collection { self.storage.remove_deck(deck.id)?; self.storage.add_deck_grave(deck.id, usn)?; } - Ok(()) + Ok(card_count) } - fn delete_all_cards_in_normal_deck(&mut self, did: DeckID) -> Result<()> { + fn delete_all_cards_in_normal_deck(&mut self, did: DeckID) -> Result { let cids = self.storage.all_cards_in_single_deck(did)?; - self.remove_cards_and_orphaned_notes(&cids) + self.remove_cards_and_orphaned_notes(&cids)?; + Ok(cids.len()) } pub fn get_all_deck_names(&self, skip_empty_default: bool) -> Result> { From 5d93832713eeeefd2d34df41d39d7db8944b290c Mon Sep 17 00:00:00 2001 From: RumovZ Date: Thu, 11 Mar 2021 10:04:58 +0100 Subject: [PATCH 55/57] Run background tasks with progress --- qt/aqt/sidebar.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/qt/aqt/sidebar.py b/qt/aqt/sidebar.py index 5e72fe4de..766424782 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -1159,7 +1159,7 @@ class SidebarTreeView(QTreeView): self.mw.checkpoint(tr(TR.ACTIONS_REMOVE_TAG)) self.browser.model.beginReset() - self.mw.taskman.run_in_background(do_remove, on_done) + self.mw.taskman.with_progress(do_remove, on_done) def rename_tag(self, item: SidebarItem, new_name: str) -> None: new_name = new_name.replace(" ", "") @@ -1193,7 +1193,7 @@ class SidebarTreeView(QTreeView): self.mw.checkpoint(tr(TR.ACTIONS_RENAME_TAG)) self.browser.model.beginReset() - self.mw.taskman.run_in_background(do_rename, on_done) + self.mw.taskman.with_progress(do_rename, on_done) def delete_decks(self, _item: SidebarItem) -> None: self.browser.editor.saveNow(self._delete_decks) @@ -1212,7 +1212,7 @@ class SidebarTreeView(QTreeView): dids = self._selected_decks() self.mw.checkpoint(tr(TR.DECKS_DELETE_DECK)) self.browser.model.beginReset() - self.mw.taskman.run_in_background(do_delete, on_done) + self.mw.taskman.with_progress(do_delete, on_done) def rename_node(self, item: SidebarItem, text: str) -> bool: new_name = text.replace('"', "") From c11a394753dded3745fd501e963c6a742cd4c9e1 Mon Sep 17 00:00:00 2001 From: RumovZ Date: Thu, 11 Mar 2021 10:28:23 +0100 Subject: [PATCH 56/57] Remove prompt when deleting from deckbrowser --- ftl/core/browsing.ftl | 2 +- ftl/core/decks.ftl | 6 ------ qt/aqt/deckbrowser.py | 39 +++++++++------------------------------ qt/aqt/sidebar.py | 4 ++-- 4 files changed, 12 insertions(+), 39 deletions(-) diff --git a/ftl/core/browsing.ftl b/ftl/core/browsing.ftl index d473c8180..cb676d53c 100644 --- a/ftl/core/browsing.ftl +++ b/ftl/core/browsing.ftl @@ -115,7 +115,7 @@ browsing-note-deleted = [one] { $count } note deleted. *[other] { $count } notes deleted. } -browsing-notes-updated = +browsing-notes-updated = { $count -> [one] { $count } note updated. *[other] { $count } notes updated. diff --git a/ftl/core/decks.ftl b/ftl/core/decks.ftl index 3ffe2a470..fb7451edf 100644 --- a/ftl/core/decks.ftl +++ b/ftl/core/decks.ftl @@ -1,5 +1,4 @@ decks-add-new-deck-ctrlandn = Add New Deck (Ctrl+N) -decks-are-you-sure-you-wish-to = Are you sure you wish to delete { $val }? decks-build = Build decks-cards-selected-by = cards selected by decks-create-deck = Create Deck @@ -32,8 +31,3 @@ decks-study = Study decks-study-deck = Study Deck decks-the-provided-search-did-not-match = The provided search did not match any cards. Would you like to revise it? decks-unmovable-cards = Show any excluded cards -decks-it-has-card = - { $count -> - [one] It has { $count } card. - *[other] It has { $count } cards. - } diff --git a/qt/aqt/deckbrowser.py b/qt/aqt/deckbrowser.py index bbaeae8b0..a48c8de9c 100644 --- a/qt/aqt/deckbrowser.py +++ b/qt/aqt/deckbrowser.py @@ -5,7 +5,7 @@ from __future__ import annotations from concurrent.futures import Future from copy import deepcopy from dataclasses import dataclass -from typing import Any, List +from typing import Any import aqt from anki.decks import DeckTreeNode @@ -23,6 +23,7 @@ from aqt.utils import ( shortcut, showInfo, showWarning, + tooltip, tr, ) @@ -295,38 +296,16 @@ class DeckBrowser: gui_hooks.sidebar_should_refresh_decks() self.show() - def ask_delete_deck(self, did: int) -> bool: - return self.ask_delete_decks([did]) - - def ask_delete_decks(self, dids: List[int]) -> bool: - decks = [self.mw.col.decks.get(did) for did in dids] - if all([deck["dyn"] for deck in decks]): - return True - - count = self.mw.col.decks.card_count(dids, include_subdecks=True) - if not count: - return True - - if len(dids) == 1: - extra = tr(TR.DECKS_IT_HAS_CARD, count=count) - return askUser( - f"{tr(TR.DECKS_ARE_YOU_SURE_YOU_WISH_TO, val=decks[0]['name'])} {extra}" - ) - - return askUser(tr(TR.DECKS_CONFIRM_DELETION, count=count)) - def _delete(self, did: int) -> None: - if self.ask_delete_deck(did): + def do_delete() -> int: + return self.mw.col.decks.remove([did]) - def do_delete() -> None: - return self.mw.col.decks.rem(did, True) + def on_done(fut: Future) -> None: + self.show() + tooltip(tr(TR.BROWSING_CARDS_DELETED, count=fut.result())) - def on_done(fut: Future) -> None: - self.show() - res = fut.result() # Required to check for errors - - self.mw.checkpoint(tr(TR.DECKS_DELETE_DECK)) - self.mw.taskman.with_progress(do_delete, on_done) + self.mw.checkpoint(tr(TR.DECKS_DELETE_DECK)) + self.mw.taskman.with_progress(do_delete, on_done) # Top buttons ###################################################################### diff --git a/qt/aqt/sidebar.py b/qt/aqt/sidebar.py index 766424782..6ce7d3427 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -1148,7 +1148,7 @@ class SidebarTreeView(QTreeView): def _remove_tags(self, _item: SidebarItem) -> None: tags = self._selected_tags() - def do_remove() -> None: + def do_remove() -> int: return self.col._backend.expunge_tags(" ".join(tags)) def on_done(fut: Future) -> None: @@ -1199,7 +1199,7 @@ class SidebarTreeView(QTreeView): self.browser.editor.saveNow(self._delete_decks) def _delete_decks(self) -> None: - def do_delete() -> None: + def do_delete() -> int: return self.mw.col.decks.remove(dids) def on_done(fut: Future) -> None: From dad92e1e2258e37a4ce4657b854a23a40337c41b Mon Sep 17 00:00:00 2001 From: RumovZ Date: Thu, 11 Mar 2021 11:26:35 +0100 Subject: [PATCH 57/57] Annotate decks.rem as deprecated --- pylib/anki/decks.py | 3 ++- pylib/anki/utils.py | 25 ++++++++++++++++++++++++- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/pylib/anki/decks.py b/pylib/anki/decks.py index 7284af4fe..830e89e83 100644 --- a/pylib/anki/decks.py +++ b/pylib/anki/decks.py @@ -13,7 +13,7 @@ import anki # pylint: disable=unused-import import anki._backend.backend_pb2 as _pb from anki.consts import * from anki.errors import NotFoundError -from anki.utils import from_json_bytes, ids2str, intTime, to_json_bytes +from anki.utils import from_json_bytes, ids2str, intTime, legacy_func, to_json_bytes # public exports DeckTreeNode = _pb.DeckTreeNode @@ -130,6 +130,7 @@ class DeckManager: return deck["id"] + @legacy_func(sub="remove") def rem(self, did: int, cardsToo: bool = True, childrenToo: bool = True) -> None: "Remove the deck. If cardsToo, delete any cards inside." if isinstance(did, str): diff --git a/pylib/anki/utils.py b/pylib/anki/utils.py index 330797c0f..7322f8002 100644 --- a/pylib/anki/utils.py +++ b/pylib/anki/utils.py @@ -18,7 +18,7 @@ import traceback from contextlib import contextmanager from hashlib import sha1 from html.entities import name2codepoint -from typing import Any, Iterable, Iterator, List, Match, Optional, Union +from typing import Any, Callable, Iterable, Iterator, List, Match, Optional, Union from anki.dbproxy import DBProxy @@ -372,3 +372,26 @@ def pointVersion() -> int: from anki.buildinfo import version return int(version.split(".")[-1]) + + +# Legacy support +############################################################################## + + +def legacy_func(sub: Optional[str] = None) -> Callable: + """Print a deprecation warning for the decorated callable recommending the use of + 'sub' instead, if provided. + """ + if sub: + hint = f", use '{sub}' instead" + else: + hint = "" + + def decorater(func: Callable) -> Callable: + def decorated_func(*args: Any, **kwargs: Any) -> Any: + print(f"'{func.__name__}' is deprecated{hint}.") + return func(*args, **kwargs) + + return decorated_func + + return decorater