place each sidebar section under its own collapsible parent node

- Allows for group operations like "clear unused tags"
- Allows users to hide groups they're not interested in
This commit is contained in:
Damien Elmes 2021-01-29 22:10:31 +10:00
parent 5ff7944a26
commit 6ba5ff5a01
4 changed files with 118 additions and 21 deletions

View File

@ -87,6 +87,7 @@ browsing-search-in = Search in:
browsing-search-within-formatting-slow = Search within formatting (slow) browsing-search-within-formatting-slow = Search within formatting (slow)
browsing-shift-position-of-existing-cards = Shift position of existing cards browsing-shift-position-of-existing-cards = Shift position of existing cards
browsing-sidebar = Sidebar browsing-sidebar = Sidebar
browsing-sidebar-filter = Sidebar filter
browsing-sort-field = Sort Field browsing-sort-field = Sort Field
browsing-sorting-on-this-column-is-not = Sorting on this column is not supported. Please choose another. browsing-sorting-on-this-column-is-not = Sorting on this column is not supported. Please choose another.
browsing-start-position = Start position: browsing-start-position = Start position:
@ -119,3 +120,8 @@ browsing-note-deleted =
*[other] { $count } notes deleted. *[other] { $count } notes deleted.
} }
browsing-window-title = Browse ({ $selected } of { $total } cards selected) browsing-window-title = Browse ({ $selected } of { $total } cards selected)
browsing-sidebar-decks = Decks
browsing-sidebar-tags = Tags
browsing-sidebar-notetypes = Note Types
browsing-sidebar-saved-searches = Saved Searches
browsing-sidebar-save-current-search = Save Current Search

View File

@ -6,9 +6,10 @@ from __future__ import annotations
from concurrent.futures import Future from concurrent.futures import Future
from enum import Enum from enum import Enum
from typing import Iterable, List, Optional from typing import TYPE_CHECKING, Iterable, List, Optional
import aqt import aqt
from anki.collection import ConfigBoolKey
from anki.errors import DeckRenameError from anki.errors import DeckRenameError
from anki.rsbackend import DeckTreeNode, FilterToSearchIn, NamedFilter, TagTreeNode from anki.rsbackend import DeckTreeNode, FilterToSearchIn, NamedFilter, TagTreeNode
from aqt import gui_hooks from aqt import gui_hooks
@ -18,17 +19,21 @@ from aqt.qt import *
from aqt.theme import theme_manager from aqt.theme import theme_manager
from aqt.utils import TR, getOnlyText, showInfo, showWarning, tr from aqt.utils import TR, getOnlyText, showInfo, showWarning, tr
if TYPE_CHECKING:
from anki.collection import ConfigBoolKeyValue, TRValue
class SidebarItemType(Enum): class SidebarItemType(Enum):
ROOT = 0 ROOT = 0
COLLECTION = 1 COLLECTION = 1
CURRENT_DECK = 2 CURRENT_DECK = 2
FILTER = 3 SAVED_SEARCH = 3
DECK = 4 DECK = 4
NOTETYPE = 5 NOTETYPE = 5
TAG = 6 TAG = 6
CUSTOM = 7 CUSTOM = 7
TEMPLATE = 8 TEMPLATE = 8
SAVED_SEARCH_ROOT = 9
# used by an add-on hook # used by an add-on hook
@ -182,6 +187,7 @@ class FilterModel(QSortFilterProxyModel):
class SidebarSearchBar(QLineEdit): class SidebarSearchBar(QLineEdit):
def __init__(self, sidebar: SidebarTreeView): def __init__(self, sidebar: SidebarTreeView):
QLineEdit.__init__(self, sidebar) QLineEdit.__init__(self, sidebar)
self.setPlaceholderText(sidebar.col.tr(TR.BROWSING_SIDEBAR_FILTER))
self.sidebar = sidebar self.sidebar = sidebar
self.timer = QTimer(self) self.timer = QTimer(self)
self.timer.setInterval(600) self.timer.setInterval(600)
@ -224,11 +230,14 @@ class SidebarTreeView(QTreeView):
(tr(TR.ACTIONS_RENAME), self.rename_tag), (tr(TR.ACTIONS_RENAME), self.rename_tag),
(tr(TR.ACTIONS_DELETE), self.remove_tag), (tr(TR.ACTIONS_DELETE), self.remove_tag),
), ),
SidebarItemType.FILTER: ( SidebarItemType.SAVED_SEARCH: (
(tr(TR.ACTIONS_RENAME), self.rename_filter), (tr(TR.ACTIONS_RENAME), self.rename_filter),
(tr(TR.ACTIONS_DELETE), self.remove_filter), (tr(TR.ACTIONS_DELETE), self.remove_filter),
), ),
SidebarItemType.NOTETYPE: ((tr(TR.ACTIONS_MANAGE), self.manage_notetype),), 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.setUniformRowHeights(True)
@ -350,7 +359,7 @@ class SidebarTreeView(QTreeView):
list(SidebarStage)[1:], list(SidebarStage)[1:],
( (
self._commonly_used_tree, self._commonly_used_tree,
self._favorites_tree, self._saved_searches_tree,
self._deck_tree, self._deck_tree,
self._notetype_tree, self._notetype_tree,
self._tag_tree, self._tag_tree,
@ -362,6 +371,29 @@ class SidebarTreeView(QTreeView):
return root return root
def _section_root(
self,
*,
root: SidebarItem,
name: TRValue,
icon: str,
collapse_key: ConfigBoolKeyValue,
type: Optional[SidebarItemType] = None,
) -> SidebarItem:
def update(expanded: bool):
self.col.set_config_bool(collapse_key, not expanded)
top = SidebarItem(
self.col.tr(name),
icon,
onExpanded=update,
expanded=not self.col.get_config_bool(collapse_key),
item_type=type,
)
root.addChild(top)
return top
def _commonly_used_tree(self, root: SidebarItem) -> None: def _commonly_used_tree(self, root: SidebarItem) -> None:
item = SidebarItem( item = SidebarItem(
tr(TR.BROWSING_WHOLE_COLLECTION), tr(TR.BROWSING_WHOLE_COLLECTION),
@ -378,20 +410,34 @@ class SidebarTreeView(QTreeView):
) )
root.addChild(item) root.addChild(item)
def _favorites_tree(self, root: SidebarItem) -> None: def _saved_searches_tree(self, root: SidebarItem) -> None:
assert self.col icon = ":/icons/heart.svg"
saved = self.col.get_config("savedFilters", {}) saved = self.col.get_config("savedFilters", {})
root = self._section_root(
root=root,
name=TR.BROWSING_SIDEBAR_SAVED_SEARCHES,
icon=icon,
collapse_key=ConfigBoolKey.COLLAPSE_FAVORITES,
type=SidebarItemType.SAVED_SEARCH_ROOT,
)
def on_click():
self.show_context_menu(root)
root.onClick = on_click
for name, filt in sorted(saved.items()): for name, filt in sorted(saved.items()):
item = SidebarItem( item = SidebarItem(
name, name,
":/icons/heart.svg", icon,
self._saved_filter(filt), self._saved_filter(filt),
item_type=SidebarItemType.FILTER, item_type=SidebarItemType.SAVED_SEARCH,
) )
root.addChild(item) root.addChild(item)
def _tag_tree(self, root: SidebarItem) -> None: def _tag_tree(self, root: SidebarItem) -> None:
tree = self.col.backend.tag_tree() icon = ":/icons/tag.svg"
def render(root: SidebarItem, nodes: Iterable[TagTreeNode], head="") -> None: def render(root: SidebarItem, nodes: Iterable[TagTreeNode], head="") -> None:
for node in nodes: for node in nodes:
@ -404,7 +450,7 @@ class SidebarTreeView(QTreeView):
item = SidebarItem( item = SidebarItem(
node.name, node.name,
":/icons/tag.svg", icon,
self._tag_filter(head + node.name), self._tag_filter(head + node.name),
toggle_expand(), toggle_expand(),
not node.collapsed, not node.collapsed,
@ -415,10 +461,17 @@ class SidebarTreeView(QTreeView):
newhead = head + node.name + "::" newhead = head + node.name + "::"
render(item, node.children, newhead) render(item, node.children, newhead)
tree = self.col.backend.tag_tree()
root = self._section_root(
root=root,
name=TR.BROWSING_SIDEBAR_TAGS,
icon=icon,
collapse_key=ConfigBoolKey.COLLAPSE_TAGS,
)
render(root, tree.children) render(root, tree.children)
def _deck_tree(self, root: SidebarItem) -> None: def _deck_tree(self, root: SidebarItem) -> None:
tree = self.col.decks.deck_tree() icon = ":/icons/deck.svg"
def render(root, nodes: Iterable[DeckTreeNode], head="") -> None: def render(root, nodes: Iterable[DeckTreeNode], head="") -> None:
for node in nodes: for node in nodes:
@ -429,7 +482,7 @@ class SidebarTreeView(QTreeView):
item = SidebarItem( item = SidebarItem(
node.name, node.name,
":/icons/deck.svg", icon,
self._deck_filter(head + node.name), self._deck_filter(head + node.name),
toggle_expand(), toggle_expand(),
not node.collapsed, not node.collapsed,
@ -441,16 +494,29 @@ class SidebarTreeView(QTreeView):
newhead = head + node.name + "::" newhead = head + node.name + "::"
render(item, node.children, newhead) render(item, node.children, newhead)
tree = self.col.decks.deck_tree()
root = self._section_root(
root=root,
name=TR.BROWSING_SIDEBAR_DECKS,
icon=icon,
collapse_key=ConfigBoolKey.COLLAPSE_DECKS,
)
render(root, tree.children) render(root, tree.children)
def _notetype_tree(self, root: SidebarItem) -> None: def _notetype_tree(self, root: SidebarItem) -> None:
assert self.col icon = ":/icons/notetype.svg"
root = self._section_root(
root=root,
name=TR.BROWSING_SIDEBAR_NOTETYPES,
icon=icon,
collapse_key=ConfigBoolKey.COLLAPSE_NOTETYPES,
)
for nt in sorted(self.col.models.all(), key=lambda nt: nt["name"].lower()): for nt in sorted(self.col.models.all(), key=lambda nt: nt["name"].lower()):
item = SidebarItem( item = SidebarItem(
nt["name"], nt["name"],
":/icons/notetype.svg", icon=icon,
self._note_filter(nt["name"]), onClick=self._note_filter(nt["name"]),
item_type=SidebarItemType.NOTETYPE, item_type=SidebarItemType.NOTETYPE,
id=nt["id"], id=nt["id"],
) )
@ -458,8 +524,8 @@ class SidebarTreeView(QTreeView):
for c, tmpl in enumerate(nt["tmpls"]): for c, tmpl in enumerate(nt["tmpls"]):
child = SidebarItem( child = SidebarItem(
tmpl["name"], tmpl["name"],
":/icons/notetype.svg", icon,
self._template_filter(nt["name"], c), onClick=self._template_filter(nt["name"], c),
item_type=SidebarItemType.TEMPLATE, item_type=SidebarItemType.TEMPLATE,
full_name=nt["name"] + "::" + tmpl["name"], full_name=nt["name"] + "::" + tmpl["name"],
) )
@ -504,16 +570,18 @@ class SidebarTreeView(QTreeView):
item = self.model().item_for_index(idx) item = self.model().item_for_index(idx)
if not item: if not item:
return return
item_type: SidebarItemType = item.item_type self.show_context_menu(item)
if item_type not in self.context_menus:
def show_context_menu(self, item: SidebarItem):
if item.item_type not in self.context_menus:
return return
m = QMenu() m = QMenu()
for action in self.context_menus[item_type]: for action in self.context_menus[item.item_type]:
act_name = action[0] act_name = action[0]
act_func = action[1] act_func = action[1]
a = m.addAction(act_name) a = m.addAction(act_name)
a.triggered.connect(lambda _, func=act_func: func(item)) # type: ignore qconnect(a.triggered, lambda _, func=act_func: func(item))
m.exec_(QCursor.pos()) m.exec_(QCursor.pos())
def rename_deck(self, item: "aqt.browser.SidebarItem") -> None: def rename_deck(self, item: "aqt.browser.SidebarItem") -> None:
@ -610,3 +678,6 @@ class SidebarTreeView(QTreeView):
Models( Models(
self.mw, parent=self.browser, fromMain=True, selected_notetype_id=item.id self.mw, parent=self.browser, fromMain=True, selected_notetype_id=item.id
) )
def save_current_search(self, _item=None) -> None:
self.browser._onSaveFilter()

View File

@ -1193,6 +1193,11 @@ message ConfigBool {
enum Key { enum Key {
BROWSER_SORT_BACKWARDS = 0; BROWSER_SORT_BACKWARDS = 0;
PREVIEW_BOTH_SIDES = 1; PREVIEW_BOTH_SIDES = 1;
COLLAPSE_TAGS = 2;
COLLAPSE_NOTETYPES = 3;
COLLAPSE_DECKS = 4;
COLLAPSE_FAVORITES = 5;
COLLAPSE_COMMON = 6;
} }
Key key = 1; Key key = 1;
} }

View File

@ -42,6 +42,11 @@ pub(crate) enum ConfigKey {
BrowserSortKind, BrowserSortKind,
BrowserSortReverse, BrowserSortReverse,
CardCountsSeparateInactive, CardCountsSeparateInactive,
CollapseCommon,
CollapseDecks,
CollapseFavorites,
CollapseNotetypes,
CollapseTags,
CreationOffset, CreationOffset,
CurrentDeckID, CurrentDeckID,
CurrentNoteTypeID, CurrentNoteTypeID,
@ -74,6 +79,11 @@ impl From<ConfigKey> for &'static str {
ConfigKey::BrowserSortKind => "sortType", ConfigKey::BrowserSortKind => "sortType",
ConfigKey::BrowserSortReverse => "sortBackwards", ConfigKey::BrowserSortReverse => "sortBackwards",
ConfigKey::CardCountsSeparateInactive => "cardCountsSeparateInactive", ConfigKey::CardCountsSeparateInactive => "cardCountsSeparateInactive",
ConfigKey::CollapseCommon => "collapseCommon",
ConfigKey::CollapseDecks => "collapseDecks",
ConfigKey::CollapseFavorites => "collapseFavorites",
ConfigKey::CollapseNotetypes => "collapseNotetypes",
ConfigKey::CollapseTags => "collapseTags",
ConfigKey::CreationOffset => "creationOffset", ConfigKey::CreationOffset => "creationOffset",
ConfigKey::CurrentDeckID => "curDeck", ConfigKey::CurrentDeckID => "curDeck",
ConfigKey::CurrentNoteTypeID => "curModel", ConfigKey::CurrentNoteTypeID => "curModel",
@ -100,6 +110,11 @@ impl From<BoolKey> for ConfigKey {
match key { match key {
BoolKey::BrowserSortBackwards => ConfigKey::BrowserSortReverse, BoolKey::BrowserSortBackwards => ConfigKey::BrowserSortReverse,
BoolKey::PreviewBothSides => ConfigKey::PreviewBothSides, BoolKey::PreviewBothSides => ConfigKey::PreviewBothSides,
BoolKey::CollapseTags => ConfigKey::CollapseTags,
BoolKey::CollapseNotetypes => ConfigKey::CollapseNotetypes,
BoolKey::CollapseDecks => ConfigKey::CollapseDecks,
BoolKey::CollapseFavorites => ConfigKey::CollapseFavorites,
BoolKey::CollapseCommon => ConfigKey::CollapseCommon,
} }
} }
} }