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 277e01c9a0c..23dd5f5c5c1 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 00000000000..e9b4840a074 --- /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 00000000000..cf6925454db --- /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 23703342b8c..13c2934129f 100644 --- a/qt/aqt/browser.py +++ b/qt/aqt/browser.py @@ -28,7 +28,7 @@ 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 @@ def setupSidebar(self) -> None: 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 41856e3c7df..4e3cef208c1 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -29,6 +29,11 @@ ) +class SidebarTool(Enum): + SELECT = auto() + SEARCH = auto() + + class SidebarItemType(Enum): ROOT = auto() SAVED_SEARCH_ROOT = auto() @@ -238,6 +243,25 @@ def flags(self, index: QModelIndex) -> Qt.ItemFlags: 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 4e3cef208c1..c434585c943 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -262,6 +262,11 @@ def _add_tools(self) -> None: 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 @@ def __init__(self, browser: aqt.browser.Browser) -> None: 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 @@ def __init__(self, browser: aqt.browser.Browser) -> None: 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 @@ def dropEvent(self, event: QDropEvent) -> None: 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 c434585c943..e16a8cf9058 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -254,17 +254,22 @@ def __init__(self, sidebar: SidebarTreeView) -> None: 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 @@ def tool(self) -> SidebarTool: @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 23dd5f5c5c1..e11359d8ad8 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 00000000000..8f8f98fd503 --- /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 e16a8cf9058..163d8adedd2 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -32,6 +32,7 @@ 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 @@ def __init__(self, browser: aqt.browser.Browser) -> None: 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 @@ def tool(self, tool: SidebarTool) -> None: 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 163d8adedd2..56f55ee0fb2 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -245,9 +245,9 @@ def flags(self, index: QModelIndex) -> Qt.ItemFlags: 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 @@ def _setup_tools(self) -> None: 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 56f55ee0fb2..7ae32289239 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -81,6 +81,7 @@ def __init__( 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 @@ def __init__( 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 @@ def add_simple( 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 @@ def add_simple( 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 @@ 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 _on_expansion(self, idx: QModelIndex) -> None: if self.current_search: @@ -651,9 +655,6 @@ def update(expanded: bool) -> None: return top - def _filter_func(self, *terms: Union[str, SearchNode]) -> Callable: - return lambda: self.update_search(*terms) - # Tree: Saved Searches ########################### @@ -678,7 +679,7 @@ def on_click() -> None: 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 @@ def _today_tree(self, root: SidebarItem) -> None: 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 @@ def _card_state_tree(self, root: SidebarItem) -> None: 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 @@ def _card_state_tree(self, root: SidebarItem) -> None: 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 @@ def _flags_tree(self, root: SidebarItem) -> None: 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 @@ def toggle_expand() -> Callable[[bool], None]: ) 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 @@ def toggle_expand() -> Callable[[bool], None]: 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 @@ def toggle_expand() -> Callable[[bool], None]: 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 @@ def toggle_expand() -> Callable[[bool], None]: 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 @@ def _notetype_tree(self, root: SidebarItem) -> None: 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 @@ def _notetype_tree(self, root: SidebarItem) -> None: 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 392ee383ac9..bbeec3fc589 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 7ae32289239..94985204e07 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -8,7 +8,7 @@ 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 @@ def _expand_where_necessary( 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 @@ def drawRow( 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 @@ def _today_tree(self, root: SidebarItem) -> None: 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 @@ def show_context_menu(self, item: SidebarItem, idx: Optional[QModelIndex]) -> No 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 @@ def manage_notetype(self, item: SidebarItem) -> None: 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 13c2934129f..43641d488cc 100644 --- a/qt/aqt/browser.py +++ b/qt/aqt/browser.py @@ -946,14 +946,14 @@ def setupSidebar(self) -> None: 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 94985204e07..52ced70b3ee 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -259,6 +259,7 @@ def __init__(self, sidebar: SidebarTreeView) -> None: 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 4fe09b5fde2..2300ceb7f2b 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 0b3665f621c..19edbe04ab6 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 9dfece2c576..ba09fc9a412 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 9c69af06d7a..ffd0b6a6fcc 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 48baa82f08e..627a7facb5a 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 222a993c2bb..b4b942b103c 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 17efb366ee3..0ca2b6a7cf4 100644 --- a/pylib/anki/decks.py +++ b/pylib/anki/decks.py @@ -135,7 +135,10 @@ def rem(self, did: int, cardsToo: bool = True, childrenToo: bool = True) -> None 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 @@ def collapseBrowser(self, did: int) -> None: 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 cd446fec9bf..1b9463c0738 100644 --- a/qt/aqt/deckbrowser.py +++ b/qt/aqt/deckbrowser.py @@ -5,7 +5,7 @@ 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 @@ def _handle_drag_and_drop(self, source: int, target: int) -> None: 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 52ced70b3ee..41577534d8b 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -1108,15 +1108,15 @@ def on_done(fut: Future) -> None: 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 @@ def manage_notetype(self, item: SidebarItem) -> None: 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 41577534d8b..d9c5f46a9c7 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 @@ def __init__( 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 @@ def __init__( 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 @@ def search(self, lowered_text: str) -> bool: 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 @@ def data(self, index: QModelIndex, role: int = Qt.DisplayRole) -> QVariant: if not index.isValid(): return QVariant() - if role not in (Qt.DisplayRole, Qt.DecorationRole, Qt.ToolTipRole): + 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 @@ def flags(self, index: QModelIndex) -> Qt.ItemFlags: SidebarItemType.TAG_ROOT, ): flags |= Qt.ItemIsDragEnabled | Qt.ItemIsDropEnabled + if item.editable: + flags |= Qt.ItemIsEditable return cast(Qt.ItemFlags, flags) @@ -385,15 +393,19 @@ def tool(self, tool: SidebarTool) -> None: # 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 @@ def refresh(self) -> None: 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 @@ def on_done(fut: Future) -> None: 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 @@ def toggle_expand() -> Callable[[bool], None]: 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 @@ def set_children_collapsed(collapsed: bool) -> None: 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 @@ def on_done(fut: Future) -> None: 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 d9c5f46a9c7..886d89c0e78 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -700,6 +700,7 @@ def on_click() -> None: icon, search_node=SearchNode(parsable_text=filt), item_type=SidebarItemType.SAVED_SEARCH, + editable=True ) root.add_child(item) @@ -1151,7 +1152,8 @@ def rename_node(self, item: SidebarItem, text: str) -> bool: 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 @@ def remove_saved_search(self, item: SidebarItem) -> None: 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 886d89c0e78..06d9f9bfefb 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -877,6 +877,7 @@ def toggle_expand() -> Callable[[bool], None]: 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 @@ def on_done(fut: Future) -> None: 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 @@ def rename_node(self, item: SidebarItem, text: str) -> bool: 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 06d9f9bfefb..e19b3595c59 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -700,7 +700,7 @@ def on_click() -> None: icon, search_node=SearchNode(parsable_text=filt), item_type=SidebarItemType.SAVED_SEARCH, - editable=True + editable=True, ) root.add_child(item) @@ -1062,22 +1062,21 @@ def set_children_collapsed(collapsed: bool) -> None: 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 @@ def on_done(fut: Future) -> None: 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 @@ def remove_saved_search(self, item: SidebarItem) -> None: 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 e19b3595c59..d39cb057d12 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -229,7 +229,9 @@ def data(self, index: QModelIndex, role: int = Qt.DisplayRole) -> QVariant: 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 @@ def tool(self) -> SidebarTool: 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 d39cb057d12..247ce2730f0 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -400,7 +400,7 @@ def tool(self, tool: SidebarTool) -> None: 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 247ce2730f0..6f8e8897c71 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -1102,8 +1102,9 @@ def on_done(fut: Future) -> None: 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 f7e803bff1f..e563cd82346 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 6f8e8897c71..f56723953e8 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -353,7 +353,7 @@ def __init__(self, browser: aqt.browser.Browser) -> None: ), 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 @@ def _get_saved_searches(self) -> Dict[str, str]: 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 @@ def _selected_decks(self) -> List[int]: 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 f56723953e8..3a8b26ad3a2 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -349,7 +349,7 @@ def __init__(self, browser: aqt.browser.Browser) -> None: ), 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 @@ def rename_deck(self, item: SidebarItem, new_name: Optional[str] = None) -> None 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 @@ def _selected_saved_searches(self) -> List[str]: 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 2300ceb7f2b..8840cdfdec1 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 ba09fc9a412..72ca2b7ff55 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 6a93df75bd4..015750197ef 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 6be535d985d..f60e61d55c3 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 d3c56c2a26b..b70fd99f7da 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 7513fecacc2..15e6e7310c8 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 3a8b26ad3a2..64c4854b9ac 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -169,6 +169,9 @@ def _cache_rows(self, node: SidebarItem) -> None: 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 @@ def tool(self, tool: SidebarTool) -> None: 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 @@ def on_done(fut: Future) -> None: 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 @@ def rename_deck(self, item: SidebarItem, new_name: Optional[str] = None) -> None 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 @@ def on_done(fut: Future) -> None: 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 @@ def rename_saved_search(self, item: SidebarItem, new_name: str = None) -> None: 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 @@ def save_current_search(self, _item: Any = None) -> None: 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 64c4854b9ac..674df43aaf2 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 @@ def section_roots() -> Iterable[SidebarItemType]: 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 @@ def __init__( 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 @@ def __init__( 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 @@ def flags(self, index: QModelIndex) -> Qt.ItemFlags: 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 @@ def on_click() -> None: icon, search_node=SearchNode(parsable_text=filt), item_type=SidebarItemType.SAVED_SEARCH, - editable=True, ) root.add_child(item) @@ -908,7 +908,6 @@ def toggle_expand() -> Callable[[bool], None]: 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 @@ def toggle_expand() -> Callable[[bool], None]: 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 @@ def toggle_expand() -> Callable[[bool], None]: 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 674df43aaf2..67ffda1c58b 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -66,7 +66,12 @@ 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) + return self in ( + SidebarItemType.SAVED_SEARCH, + SidebarItemType.DECK, + SidebarItemType.TAG, + ) + class SidebarStage(Enum): ROOT = auto() @@ -1187,7 +1192,9 @@ def on_done(fut: Future) -> None: 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 67ffda1c58b..9112295da6d 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -110,7 +110,7 @@ def __init__( 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 @@ def add_simple( 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 @@ def _expand_where_necessary( 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 @@ def _on_click_index(self, idx: QModelIndex) -> None: 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 e563cd82346..7302321f58a 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 9112295da6d..a9d9f09a416 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -1048,9 +1048,7 @@ def show_context_menu(self, item: SidebarItem, idx: Optional[QModelIndex]) -> No 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 @@ def _maybe_add_search_actions(self, menu: QMenu) -> None: 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): - 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) + selected_items = self._selected_items() + if not any(item.children for item in selected_items): + return 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 a9d9f09a416..df58c45bc43 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -70,6 +70,7 @@ def is_editable(self) -> bool: SidebarItemType.SAVED_SEARCH, SidebarItemType.DECK, SidebarItemType.TAG, + SidebarItemType.NOTETYPE, ) @@ -1206,6 +1207,19 @@ def on_done(fut: Future) -> None: 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 @@ def rename_node(self, item: SidebarItem, text: str) -> bool: 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 df58c45bc43..da075406764 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -71,6 +71,7 @@ def is_editable(self) -> bool: SidebarItemType.DECK, SidebarItemType.TAG, SidebarItemType.NOTETYPE, + SidebarItemType.NOTETYPE_TEMPLATE, ) @@ -1021,6 +1022,7 @@ def _notetype_tree(self, root: SidebarItem) -> None: ), item_type=SidebarItemType.NOTETYPE_TEMPLATE, full_name=f"{nt['name']}::{tmpl['name']}", + id=tmpl["ord"], ) item.add_child(child) @@ -1127,8 +1129,8 @@ def rename_deck(self, item: SidebarItem, new_name: Optional[str] = None) -> None 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 @@ def rename_notetype(self, item: SidebarItem, new_name: str) -> None: 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 da075406764..1c4c4667577 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -364,16 +364,9 @@ def __init__(self, browser: aqt.browser.Browser) -> None: 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 @@ def onContextMenu(self, point: QPoint) -> None: # 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 @@ def set_children_expanded(expanded: bool) -> None: 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 @@ def on_done(fut: Future) -> None: 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 @@ def on_done(fut: Future) -> None: 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 @@ def rename_notetype(self, item: SidebarItem, new_name: str) -> None: 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 @@ def rename_template(self, item: SidebarItem, new_name: str) -> None: 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 @@ def remove_saved_searches(self, _item: SidebarItem) -> None: 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 1c4c4667577..69a213312b8 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -457,7 +457,9 @@ def on_done(fut: Future) -> None: 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 8f8f98fd503..f8e4e552125 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 e9b4840a074..5cf295b2b5b 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 cf6925454db..fe4ee8c6735 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 7302321f58a..c5a13ad0e91 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 69a213312b8..471ff266476 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -1279,6 +1279,10 @@ def rename_saved_search(self, item: SidebarItem, new_name: str) -> None: 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 @@ def save_current_search(self, _item: Any = None) -> None: ) 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 471ff266476..e517ee6ca2c 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -91,7 +91,6 @@ def __init__( 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 @@ def __init__( 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 @@ 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_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 @@ def on_save() -> None: 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 @@ def _saved_searches_tree(self, root: SidebarItem) -> None: 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 e517ee6ca2c..54ba6e95986 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -74,6 +74,13 @@ def is_editable(self) -> bool: 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 @@ def __init__(self, browser: aqt.browser.Browser) -> None: 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 @@ 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._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 @@ def on_save() -> None: 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 @@ def show_context_menu( ) -> 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 @@ def on_done(fut: Future) -> None: 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 54ba6e95986..f7e84223afa 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -1103,9 +1103,9 @@ def set_children_expanded(expanded: bool) -> None: 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 f7e84223afa..f9bd9f6c231 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -6,7 +6,7 @@ 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 @@ def __init__(self, browser: aqt.browser.Browser) -> None: 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 @@ 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)) - - 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 @@ def show_context_menu( 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 @@ def rename_saved_search(self, item: SidebarItem, new_name: str) -> None: 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 f9bd9f6c231..9d528267bcf 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -428,6 +428,7 @@ def refresh( return def on_done(fut: Future) -> None: + self.setUpdatesEnabled(True) root = fut.result() model = SidebarModel(self, root) @@ -439,7 +440,6 @@ def on_done(fut: Future) -> None: 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 | 52 +++++++++++++++++++++++++---------------------- 1 file changed, 28 insertions(+), 24 deletions(-) diff --git a/qt/aqt/sidebar.py b/qt/aqt/sidebar.py index 9d528267bcf..08b9b2cb809 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -1025,30 +1025,20 @@ def _notetype_tree(self, root: SidebarItem) -> None: ########################### 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) - - # 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()) + 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) + + 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 @@ def _maybe_add_type_specific_actions(self, menu: QMenu, item: SidebarItem) -> No 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 c5a13ad0e91..06160d4688c 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 b4b942b103c..29e359c5cda 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 1b9463c0738..1afc1c2985f 100644 --- a/qt/aqt/deckbrowser.py +++ b/qt/aqt/deckbrowser.py @@ -313,9 +313,7 @@ def ask_delete_decks(self, dids: List[int]) -> bool: 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 08b9b2cb809..20ada84cd84 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -1267,7 +1267,7 @@ def remove_saved_searches(self, _item: SidebarItem) -> None: 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 06160d4688c..22284347336 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 20ada84cd84..2709335edce 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -1142,6 +1142,8 @@ def remove_tags(self, item: SidebarItem) -> None: 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 @@ def _selected_tags(self) -> List[str]: 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 2709335edce..45a6c99cb18 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -266,16 +266,10 @@ def supportedDropActions(self) -> Qt.DropActions: 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 @@ def __init__(self, browser: aqt.browser.Browser) -> None: 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 @@ def on_done(fut: Future) -> None: # 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 @@ def keyPressEvent(self, event: QKeyEvent) -> None: ########### + 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 bbeec3fc589..dd15e114cb1 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 e11359d8ad8..23dd5f5c5c1 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 f8e4e552125..00000000000 --- 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 45a6c99cb18..9d4f512a70a 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -33,7 +33,6 @@ class SidebarTool(Enum): SELECT = auto() SEARCH = auto() - EDIT = auto() class SidebarItemType(Enum): @@ -277,10 +276,9 @@ def flags(self, index: QModelIndex) -> Qt.ItemFlags: 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 @@ def __init__(self, sidebar: SidebarTreeView) -> None: 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 @@ def __init__(self, browser: aqt.browser.Browser) -> None: 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 @@ def tool(self) -> SidebarTool: @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 9d4f512a70a..ede513fc050 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -287,7 +287,8 @@ def __init__(self, sidebar: SidebarTreeView) -> None: 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 ede513fc050..faf1cd05da0 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -291,11 +291,12 @@ def __init__(self, sidebar: SidebarTreeView) -> None: 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 faf1cd05da0..624b8e3d49f 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -399,11 +399,14 @@ def tool(self, tool: SidebarTool) -> None: 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 624b8e3d49f..729b815e740 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -545,8 +545,8 @@ def dropEvent(self, event: QDropEvent) -> None: 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 729b815e740..6dc14f9dd37 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -12,9 +12,11 @@ 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 @@ def is_editable(self) -> bool: SidebarItemType.SAVED_SEARCH, SidebarItemType.DECK, SidebarItemType.TAG, - SidebarItemType.NOTETYPE, - SidebarItemType.NOTETYPE_TEMPLATE, ) def is_deletable(self) -> bool: @@ -1043,7 +1043,11 @@ def show_context_menu(self, item: SidebarItem, index: QModelIndex) -> None: 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 @@ def on_done(fut: Future) -> None: 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 @@ def rename_node(self, item: SidebarItem, text: str) -> bool: 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 @@ def manage_notetype(self, item: SidebarItem) -> None: 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 04d577c2d28..da8efe13bc0 100644 --- a/qt/aqt/about.py +++ b/qt/aqt/about.py @@ -205,6 +205,7 @@ def onCopy() -> None: "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 511a9830bde..fa399529a4f 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 0aeccf8a0b4..763aaf7db77 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 8c899c4e699..dbfa87d0cfb 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 4b42a7b550c..f484dd248ca 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 @@ def __init__( 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 @@ def toggle_expand() -> Callable[[bool], None]: 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 @@ def toggle_expand() -> Callable[[bool], None]: 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 @@ def _notetype_tree(self, root: SidebarItem) -> None: 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 @@ def set_children_expanded(expanded: bool) -> 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 = 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 @@ def rename_tag(self, item: SidebarItem, new_name: str) -> None: 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 f484dd248ca..46f56179864 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -1178,6 +1178,7 @@ def do_rename() -> int: 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 22284347336..f20030b11e3 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 46f56179864..e590a1eea3b 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -27,6 +27,7 @@ show_invalid_search_error, showInfo, showWarning, + tooltip, tr, ) @@ -1146,16 +1147,14 @@ def remove_tags(self, item: SidebarItem) -> None: 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 @@ def _selected_tags(self) -> List[str]: 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 751c14116d4..db6ab7d8c22 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 e554761f456..68664954f85 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 fa399529a4f..b42f9b179ed 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 e590a1eea3b..cd8d0e40af4 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -1184,12 +1184,12 @@ def on_done(fut: Future) -> None: 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 f20030b11e3..47bbcf29321 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 cd8d0e40af4..f135b475b06 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -1241,12 +1241,6 @@ def _set_saved_searches(self, searches: Dict[str, str]) -> None: 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 | 26 ++++++++++++-------------- rslib/backend.proto | 2 +- rslib/src/backend/mod.rs | 6 ++---- rslib/src/decks/mod.rs | 28 +++++++++++++++++----------- 7 files changed, 39 insertions(+), 33 deletions(-) diff --git a/ftl/core/browsing.ftl b/ftl/core/browsing.ftl index 47bbcf29321..d473c81807e 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 3c74c863df1..3ffe2a470b8 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 9347f128d2b..7284af4fee6 100644 --- a/pylib/anki/decks.py +++ b/pylib/anki/decks.py @@ -137,8 +137,8 @@ def rem(self, did: int, cardsToo: bool = True, childrenToo: bool = True) -> None 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 f135b475b06..5e72fe4deaa 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -1199,22 +1199,20 @@ def delete_decks(self, _item: SidebarItem) -> None: self.browser.editor.saveNow(self._delete_decks) 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.remove(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 + 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() - self.mw.checkpoint(tr(TR.DECKS_DELETE_DECK)) - self.browser.model.beginReset() - self.mw.taskman.run_in_background(do_delete, on_done) + 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) def rename_node(self, item: SidebarItem, text: str) -> bool: new_name = text.replace('"', "") diff --git a/rslib/backend.proto b/rslib/backend.proto index db6ab7d8c22..45ae4b6eda4 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 b42f9b179ed..fc9b37ca49d 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 763aaf7db77..668de14a3aa 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 5e72fe4deaa..766424782cd 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -1159,7 +1159,7 @@ def on_done(fut: Future) -> None: 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 @@ def on_done(fut: Future) -> None: 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 @@ def on_done(fut: Future) -> None: 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 d473c81807e..cb676d53c21 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 3ffe2a470b8..fb7451edf53 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 bbaeae8b0a8..a48c8de9cac 100644 --- a/qt/aqt/deckbrowser.py +++ b/qt/aqt/deckbrowser.py @@ -5,7 +5,7 @@ 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 @@ shortcut, showInfo, showWarning, + tooltip, tr, ) @@ -295,38 +296,16 @@ def _handle_drag_and_drop(self, source: int, target: int) -> None: 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() -> None: - return self.mw.col.decks.rem(did, True) + def do_delete() -> int: + return self.mw.col.decks.remove([did]) - def on_done(fut: Future) -> None: - self.show() - res = fut.result() # Required to check for errors + def on_done(fut: Future) -> None: + self.show() + tooltip(tr(TR.BROWSING_CARDS_DELETED, count=fut.result())) - 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 766424782cd..6ce7d342777 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -1148,7 +1148,7 @@ def remove_tags(self, item: SidebarItem) -> None: 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 @@ def delete_decks(self, _item: SidebarItem) -> None: 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 7284af4fee6..830e89e83c0 100644 --- a/pylib/anki/decks.py +++ b/pylib/anki/decks.py @@ -13,7 +13,7 @@ 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 @@ def id( 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 330797c0f7f..7322f80024b 100644 --- a/pylib/anki/utils.py +++ b/pylib/anki/utils.py @@ -18,7 +18,7 @@ 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