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:
parent
5ff7944a26
commit
6ba5ff5a01
@ -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
|
||||||
|
@ -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()
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user