mirror of
https://github.com/ankitects/anki.git
synced 2025-09-24 16:56:36 -04:00
move sidebar code from browser.py to sidebar.py and tidy up
https://github.com/ankitects/help-wanted/issues/6 Some notes: - use our own routine to toggle the sidebar, which avoids a useless refresh on browser close, and allows us to limit the delayed loading to initial browser load. - add-ons that limited themselves to the browser_will_build_tree hook should theoretically continue working; ones that were monkey patching will likely break. A few appear to be broken at the moment anyway, so it's probably a good time to be making this change.
This commit is contained in:
parent
9db3f0dd19
commit
8d5dca4bda
2 changed files with 394 additions and 388 deletions
|
@ -7,13 +7,11 @@ import html
|
|||
import time
|
||||
from concurrent.futures import Future
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from operator import itemgetter
|
||||
from typing import Callable, List, Optional, Sequence, Tuple, Union, cast
|
||||
from typing import List, Sequence, Tuple, cast
|
||||
|
||||
from markdown import markdown
|
||||
|
||||
import anki
|
||||
import aqt
|
||||
import aqt.forms
|
||||
from anki.cards import Card
|
||||
|
@ -25,12 +23,10 @@ from anki.notes import Note
|
|||
from anki.rsbackend import (
|
||||
BackendNoteTypeID,
|
||||
ConcatSeparator,
|
||||
DeckTreeNode,
|
||||
DupeIn,
|
||||
FilterToSearchIn,
|
||||
InvalidInput,
|
||||
NamedFilter,
|
||||
TagTreeNode,
|
||||
)
|
||||
from anki.stats import CardStats
|
||||
from anki.utils import htmlToTextLine, ids2str, isMac, isWin
|
||||
|
@ -41,7 +37,7 @@ from aqt.main import ResetReason
|
|||
from aqt.previewer import BrowserPreviewer as PreviewDialog
|
||||
from aqt.previewer import Previewer
|
||||
from aqt.qt import *
|
||||
from aqt.sidebar import NewSidebarTreeView, SidebarItemType, SidebarTreeViewBase
|
||||
from aqt.sidebar import SidebarTreeView
|
||||
from aqt.theme import theme_manager
|
||||
from aqt.utils import (
|
||||
TR,
|
||||
|
@ -75,6 +71,10 @@ from aqt.utils import (
|
|||
)
|
||||
from aqt.webview import AnkiWebView
|
||||
|
||||
# legacy add-on support
|
||||
# pylint: disable=unused-import
|
||||
from aqt.sidebar import SidebarItem, SidebarStage # isort: skip
|
||||
|
||||
|
||||
@dataclass
|
||||
class FindDupesDialog:
|
||||
|
@ -446,156 +446,6 @@ class StatusDelegate(QItemDelegate):
|
|||
return QItemDelegate.paint(self, painter, option, index)
|
||||
|
||||
|
||||
# Sidebar
|
||||
######################################################################
|
||||
|
||||
|
||||
class SidebarStage(Enum):
|
||||
ROOT = 0
|
||||
STANDARD = 1
|
||||
FAVORITES = 2
|
||||
DECKS = 3
|
||||
MODELS = 4
|
||||
TAGS = 5
|
||||
|
||||
|
||||
class SidebarItem:
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
icon: str,
|
||||
onClick: Callable[[], None] = None,
|
||||
onExpanded: Callable[[bool], None] = None,
|
||||
expanded: bool = False,
|
||||
item_type: SidebarItemType = SidebarItemType.CUSTOM,
|
||||
id: int = 0,
|
||||
full_name: str = None,
|
||||
) -> None:
|
||||
self.name = name
|
||||
if not full_name:
|
||||
full_name = name
|
||||
self.full_name = full_name
|
||||
self.icon = icon
|
||||
self.item_type = item_type
|
||||
self.id = id
|
||||
self.onClick = onClick
|
||||
self.onExpanded = onExpanded
|
||||
self.expanded = expanded
|
||||
self.children: List["SidebarItem"] = []
|
||||
self.parentItem: Optional["SidebarItem"] = None
|
||||
self.tooltip: Optional[str] = None
|
||||
|
||||
def addChild(self, cb: "SidebarItem") -> None:
|
||||
self.children.append(cb)
|
||||
cb.parentItem = self
|
||||
|
||||
def rowForChild(self, child: "SidebarItem") -> Optional[int]:
|
||||
try:
|
||||
return self.children.index(child)
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
|
||||
class SidebarModel(QAbstractItemModel):
|
||||
def __init__(self, root: SidebarItem) -> None:
|
||||
super().__init__()
|
||||
self.root = root
|
||||
|
||||
# Qt API
|
||||
######################################################################
|
||||
|
||||
def rowCount(self, parent: QModelIndex = QModelIndex()) -> int:
|
||||
if not parent.isValid():
|
||||
return len(self.root.children)
|
||||
else:
|
||||
item: SidebarItem = parent.internalPointer()
|
||||
return len(item.children)
|
||||
|
||||
def columnCount(self, parent: QModelIndex = QModelIndex()) -> int:
|
||||
return 1
|
||||
|
||||
def index(
|
||||
self, row: int, column: int, parent: QModelIndex = QModelIndex()
|
||||
) -> QModelIndex:
|
||||
if not self.hasIndex(row, column, parent):
|
||||
return QModelIndex()
|
||||
|
||||
parentItem: SidebarItem
|
||||
if not parent.isValid():
|
||||
parentItem = self.root
|
||||
else:
|
||||
parentItem = parent.internalPointer()
|
||||
|
||||
item = parentItem.children[row]
|
||||
return self.createIndex(row, column, item)
|
||||
|
||||
def parent(self, child: QModelIndex) -> QModelIndex: # type: ignore
|
||||
if not child.isValid():
|
||||
return QModelIndex()
|
||||
|
||||
childItem: SidebarItem = child.internalPointer()
|
||||
parentItem = childItem.parentItem
|
||||
|
||||
if parentItem is None or parentItem == self.root:
|
||||
return QModelIndex()
|
||||
|
||||
row = parentItem.rowForChild(childItem)
|
||||
if row is None:
|
||||
return QModelIndex()
|
||||
|
||||
return self.createIndex(row, 0, parentItem)
|
||||
|
||||
def data(self, index: QModelIndex, role: int = Qt.DisplayRole) -> QVariant:
|
||||
if not index.isValid():
|
||||
return QVariant()
|
||||
|
||||
if role not in (Qt.DisplayRole, Qt.DecorationRole, Qt.ToolTipRole):
|
||||
return QVariant()
|
||||
|
||||
item: SidebarItem = index.internalPointer()
|
||||
|
||||
if role == Qt.DisplayRole:
|
||||
return QVariant(item.name)
|
||||
elif role == Qt.ToolTipRole:
|
||||
return QVariant(item.tooltip)
|
||||
else:
|
||||
return QVariant(theme_manager.icon_from_resources(item.icon))
|
||||
|
||||
# Helpers
|
||||
######################################################################
|
||||
|
||||
def iconFromRef(self, iconRef: str) -> QIcon:
|
||||
print("iconFromRef() deprecated")
|
||||
return theme_manager.icon_from_resources(iconRef)
|
||||
|
||||
def expandWhereNeccessary(self, tree: QTreeView) -> None:
|
||||
for row, child in enumerate(self.root.children):
|
||||
if child.expanded:
|
||||
idx = self.index(row, 0, QModelIndex())
|
||||
self._expandWhereNeccessary(idx, tree)
|
||||
|
||||
def _expandWhereNeccessary(self, parent: QModelIndex, tree: QTreeView) -> None:
|
||||
parentItem: SidebarItem
|
||||
if not parent.isValid():
|
||||
parentItem = self.root
|
||||
else:
|
||||
parentItem = parent.internalPointer()
|
||||
|
||||
# nothing to do?
|
||||
if not parentItem.expanded:
|
||||
return
|
||||
|
||||
# expand children
|
||||
for row, child in enumerate(parentItem.children):
|
||||
if not child.expanded:
|
||||
continue
|
||||
childIdx = self.index(row, 0, parent)
|
||||
self._expandWhereNeccessary(childIdx, tree)
|
||||
|
||||
# then ourselves
|
||||
tree.setExpanded(parent, True)
|
||||
|
||||
|
||||
# Browser window
|
||||
######################################################################
|
||||
|
||||
|
@ -611,7 +461,6 @@ class Browser(QMainWindow):
|
|||
def __init__(self, mw: AnkiQt) -> None:
|
||||
QMainWindow.__init__(self, None, Qt.Window)
|
||||
self.mw = mw
|
||||
self.want_old_sidebar = mw.app.queryKeyboardModifiers() & Qt.ShiftModifier
|
||||
self.col = self.mw.col
|
||||
self.lastFilter = ""
|
||||
self.focusTo: Optional[int] = None
|
||||
|
@ -853,7 +702,7 @@ class Browser(QMainWindow):
|
|||
return selected
|
||||
|
||||
def onReset(self):
|
||||
self.maybeRefreshSidebar()
|
||||
self.sidebar.refresh()
|
||||
self.editor.setNote(None)
|
||||
self.search()
|
||||
|
||||
|
@ -1062,190 +911,41 @@ QTableView {{ gridline-color: {grid} }}
|
|||
def onColumnMoved(self, a, b, c):
|
||||
self.setColumnSizes()
|
||||
|
||||
# implementation moved to sidebar.py. this is kept for compatibility
|
||||
class SidebarTreeView(SidebarTreeViewBase):
|
||||
pass
|
||||
|
||||
def setupSidebar(self) -> None:
|
||||
dw = self.sidebarDockWidget = QDockWidget(tr(TR.BROWSING_SIDEBAR), self)
|
||||
dw.setFeatures(QDockWidget.DockWidgetClosable)
|
||||
dw.setObjectName("Sidebar")
|
||||
dw.setAllowedAreas(Qt.LeftDockWidgetArea)
|
||||
|
||||
self.sidebarTree: SidebarTreeViewBase
|
||||
if self.want_old_sidebar:
|
||||
self.sidebarTree = self.SidebarTreeView()
|
||||
else:
|
||||
self.sidebarTree = NewSidebarTreeView(self)
|
||||
|
||||
self.sidebarTree.mw = self.mw
|
||||
self.sidebarTree.setUniformRowHeights(True)
|
||||
self.sidebarTree.setHeaderHidden(True)
|
||||
self.sidebarTree.setIndentation(15)
|
||||
qconnect(self.sidebarTree.expanded, self.onSidebarItemExpanded)
|
||||
dw.setWidget(self.sidebarTree)
|
||||
# match window background color
|
||||
bgcolor = QPalette().window().color().name()
|
||||
self.sidebarTree.setStyleSheet("QTreeView { background: '%s'; }" % bgcolor)
|
||||
self.sidebar = SidebarTreeView(self)
|
||||
self.sidebarTree = self.sidebar # legacy alias
|
||||
dw.setWidget(self.sidebar)
|
||||
self.sidebarDockWidget.setFloating(False)
|
||||
qconnect(self.sidebarDockWidget.visibilityChanged, self.onSidebarVisChanged)
|
||||
|
||||
self.sidebarDockWidget.setTitleBarWidget(QWidget())
|
||||
self.addDockWidget(Qt.LeftDockWidgetArea, dw)
|
||||
|
||||
def onSidebarItemExpanded(self, idx: QModelIndex) -> None:
|
||||
item: SidebarItem = idx.internalPointer()
|
||||
# item.on
|
||||
|
||||
def onSidebarVisChanged(self, _visible: bool) -> None:
|
||||
self.maybeRefreshSidebar()
|
||||
# schedule sidebar to refresh after browser window has loaded, so the
|
||||
# UI is more responsive
|
||||
self.mw.progress.timer(10, self.sidebar.refresh, False)
|
||||
|
||||
def focusSidebar(self) -> None:
|
||||
# workaround for PyQt focus bug
|
||||
self.editor.hideCompleters()
|
||||
|
||||
self.sidebarDockWidget.setVisible(True)
|
||||
self.sidebarTree.setFocus()
|
||||
self.sidebar.setFocus()
|
||||
|
||||
# legacy
|
||||
def maybeRefreshSidebar(self) -> None:
|
||||
if self.sidebarDockWidget.isVisible():
|
||||
# add slight delay to allow browser window to appear first
|
||||
def deferredDisplay():
|
||||
root = self.buildTree()
|
||||
model = SidebarModel(root)
|
||||
self.sidebarTree.setModel(model)
|
||||
model.expandWhereNeccessary(self.sidebarTree)
|
||||
self.sidebar.refresh()
|
||||
|
||||
self.mw.progress.timer(10, deferredDisplay, False)
|
||||
def toggle_sidebar(self):
|
||||
want_visible = not self.sidebarDockWidget.isVisible()
|
||||
self.sidebarDockWidget.setVisible(want_visible)
|
||||
if want_visible:
|
||||
self.sidebar.refresh()
|
||||
|
||||
def buildTree(self) -> SidebarItem:
|
||||
root = SidebarItem("", "", item_type=SidebarItemType.ROOT)
|
||||
|
||||
handled = gui_hooks.browser_will_build_tree(
|
||||
False, root, SidebarStage.ROOT, self
|
||||
)
|
||||
if handled:
|
||||
return root
|
||||
|
||||
for stage, builder in zip(
|
||||
list(SidebarStage)[1:],
|
||||
(
|
||||
self._stdTree,
|
||||
self._favTree,
|
||||
self._decksTree,
|
||||
self._modelTree,
|
||||
self._userTagTree,
|
||||
),
|
||||
):
|
||||
handled = gui_hooks.browser_will_build_tree(False, root, stage, self)
|
||||
if not handled and builder:
|
||||
builder(root)
|
||||
|
||||
return root
|
||||
|
||||
def _stdTree(self, root) -> None:
|
||||
item = SidebarItem(
|
||||
tr(TR.BROWSING_WHOLE_COLLECTION),
|
||||
":/icons/collection.svg",
|
||||
self._named_filter(NamedFilter.WHOLE_COLLECTION),
|
||||
item_type=SidebarItemType.COLLECTION,
|
||||
)
|
||||
root.addChild(item)
|
||||
item = SidebarItem(
|
||||
tr(TR.BROWSING_CURRENT_DECK),
|
||||
":/icons/deck.svg",
|
||||
self._named_filter(NamedFilter.CURRENT_DECK),
|
||||
item_type=SidebarItemType.CURRENT_DECK,
|
||||
)
|
||||
root.addChild(item)
|
||||
|
||||
def _favTree(self, root) -> None:
|
||||
assert self.col
|
||||
saved = self.col.get_config("savedFilters", {})
|
||||
for name, filt in sorted(saved.items()):
|
||||
item = SidebarItem(
|
||||
name,
|
||||
":/icons/heart.svg",
|
||||
self._saved_filter(filt),
|
||||
item_type=SidebarItemType.FILTER,
|
||||
)
|
||||
root.addChild(item)
|
||||
|
||||
def _userTagTree(self, root) -> None:
|
||||
tree = self.col.backend.tag_tree()
|
||||
|
||||
def fillGroups(root, nodes: Sequence[TagTreeNode], head=""):
|
||||
for node in nodes:
|
||||
|
||||
def toggle_expand():
|
||||
full_name = head + node.name # pylint: disable=cell-var-from-loop
|
||||
return lambda expanded: self.mw.col.tags.set_collapsed(
|
||||
full_name, not expanded
|
||||
)
|
||||
|
||||
item = SidebarItem(
|
||||
node.name,
|
||||
":/icons/tag.svg",
|
||||
self._tag_filter(head + node.name),
|
||||
toggle_expand(),
|
||||
not node.collapsed,
|
||||
item_type=SidebarItemType.TAG,
|
||||
full_name=head + node.name,
|
||||
)
|
||||
root.addChild(item)
|
||||
newhead = head + node.name + "::"
|
||||
fillGroups(item, node.children, newhead)
|
||||
|
||||
fillGroups(root, tree.children)
|
||||
|
||||
def _decksTree(self, root) -> None:
|
||||
tree = self.col.decks.deck_tree()
|
||||
|
||||
def fillGroups(root, nodes: Sequence[DeckTreeNode], head=""):
|
||||
for node in nodes:
|
||||
|
||||
def toggle_expand():
|
||||
did = node.deck_id # pylint: disable=cell-var-from-loop
|
||||
return lambda _: self.mw.col.decks.collapseBrowser(did)
|
||||
|
||||
item = SidebarItem(
|
||||
node.name,
|
||||
":/icons/deck.svg",
|
||||
self._deck_filter(head + node.name),
|
||||
toggle_expand(),
|
||||
not node.collapsed,
|
||||
item_type=SidebarItemType.DECK,
|
||||
id=node.deck_id,
|
||||
)
|
||||
root.addChild(item)
|
||||
newhead = head + node.name + "::"
|
||||
fillGroups(item, node.children, newhead)
|
||||
|
||||
fillGroups(root, tree.children)
|
||||
|
||||
def _modelTree(self, root) -> None:
|
||||
assert self.col
|
||||
|
||||
for nt in sorted(self.col.models.all(), key=lambda nt: nt["name"].lower()):
|
||||
item = SidebarItem(
|
||||
nt["name"],
|
||||
":/icons/notetype.svg",
|
||||
self._note_filter(nt["name"]),
|
||||
item_type=SidebarItemType.NOTETYPE,
|
||||
id=nt["id"],
|
||||
)
|
||||
|
||||
for c, tmpl in enumerate(nt["tmpls"]):
|
||||
child = SidebarItem(
|
||||
tmpl["name"],
|
||||
":/icons/notetype.svg",
|
||||
self._template_filter(nt["name"], c),
|
||||
item_type=SidebarItemType.TEMPLATE,
|
||||
)
|
||||
item.addChild(child)
|
||||
|
||||
root.addChild(item)
|
||||
|
||||
# Filter tree
|
||||
# Filter button and sidebar helpers
|
||||
######################################################################
|
||||
|
||||
def onFilterButton(self):
|
||||
|
@ -1255,17 +955,22 @@ QTableView {{ gridline-color: {grid} }}
|
|||
ml.addChild(self._cardStateFilters())
|
||||
ml.addSeparator()
|
||||
|
||||
ml.addChild(self.sidebarDockWidget.toggleViewAction())
|
||||
toggle_sidebar = QAction(tr(TR.BROWSING_SIDEBAR))
|
||||
qconnect(toggle_sidebar.triggered, self.toggle_sidebar)
|
||||
toggle_sidebar.setCheckable(True)
|
||||
toggle_sidebar.setChecked(self.sidebarDockWidget.isVisible())
|
||||
ml.addChild(toggle_sidebar)
|
||||
ml.addSeparator()
|
||||
|
||||
ml.addChild(self._savedSearches())
|
||||
|
||||
ml.popupOver(self.form.filter)
|
||||
|
||||
def setFilter(self, *searches):
|
||||
def update_search(self, *terms: str):
|
||||
"Modify the current search string based on modified keys, then refresh."
|
||||
try:
|
||||
search = self.col.backend.concatenate_searches(
|
||||
sep=ConcatSeparator.AND, searches=searches
|
||||
sep=ConcatSeparator.AND, searches=terms
|
||||
)
|
||||
mods = self.mw.app.keyboardModifiers()
|
||||
if mods & Qt.AltModifier:
|
||||
|
@ -1294,6 +999,10 @@ QTableView {{ gridline-color: {grid} }}
|
|||
self.form.searchEdit.lineEdit().setText(search)
|
||||
self.onSearchActivated()
|
||||
|
||||
# legacy
|
||||
def setFilter(self, *terms: str):
|
||||
self.set_filter_then_search(*terms)
|
||||
|
||||
def _simpleFilters(self, items):
|
||||
ml = MenuList()
|
||||
for row in items:
|
||||
|
@ -1301,38 +1010,9 @@ QTableView {{ gridline-color: {grid} }}
|
|||
ml.addSeparator()
|
||||
else:
|
||||
label, filter_name = row
|
||||
ml.addItem(label, self._named_filter(filter_name))
|
||||
ml.addItem(label, self.sidebar._named_filter(filter_name))
|
||||
return ml
|
||||
|
||||
def _named_filter(self, name: "FilterToSearchIn.NamedFilterValue") -> Callable:
|
||||
return lambda: self.setFilter(
|
||||
self.col.backend.filter_to_search(FilterToSearchIn(name=name))
|
||||
)
|
||||
|
||||
def _tag_filter(self, tag: str) -> Callable:
|
||||
return lambda: self.setFilter(
|
||||
self.col.backend.filter_to_search(FilterToSearchIn(tag=tag))
|
||||
)
|
||||
|
||||
def _deck_filter(self, deck: str) -> Callable:
|
||||
return lambda: self.setFilter(
|
||||
self.col.backend.filter_to_search(FilterToSearchIn(deck=deck))
|
||||
)
|
||||
|
||||
def _note_filter(self, note: str) -> Callable:
|
||||
return lambda: self.setFilter(
|
||||
self.col.backend.filter_to_search(FilterToSearchIn(note=note))
|
||||
)
|
||||
|
||||
def _template_filter(self, note: str, template: int) -> Callable:
|
||||
return lambda: self.setFilter(
|
||||
self.col.backend.filter_to_search(FilterToSearchIn(note=note)),
|
||||
self.col.backend.filter_to_search(FilterToSearchIn(template=template)),
|
||||
)
|
||||
|
||||
def _saved_filter(self, saved: str) -> Callable:
|
||||
return lambda: self.setFilter(saved)
|
||||
|
||||
def _todayFilters(self):
|
||||
subm = SubMenu(tr(TR.BROWSING_TODAY))
|
||||
subm.addChild(
|
||||
|
@ -1370,9 +1050,6 @@ QTableView {{ gridline-color: {grid} }}
|
|||
)
|
||||
return subm
|
||||
|
||||
def _escapeMenuItem(self, label):
|
||||
return label.replace("&", "&&")
|
||||
|
||||
# Favourites
|
||||
######################################################################
|
||||
|
||||
|
@ -1405,7 +1082,7 @@ QTableView {{ gridline-color: {grid} }}
|
|||
conf = self.col.get_config("savedFilters")
|
||||
conf[name] = filt
|
||||
self.col.set_config("savedFilters", conf)
|
||||
self.maybeRefreshSidebar()
|
||||
self.sidebar.refresh()
|
||||
|
||||
def _onRemoveFilter(self) -> None:
|
||||
self.removeFilter(self._currentFilterIsSaved())
|
||||
|
@ -1416,7 +1093,7 @@ QTableView {{ gridline-color: {grid} }}
|
|||
conf = self.col.get_config("savedFilters")
|
||||
del conf[name]
|
||||
self.col.set_config("savedFilters", conf)
|
||||
self.maybeRefreshSidebar()
|
||||
self.sidebar.refresh()
|
||||
|
||||
def renameFilter(self, old: str) -> None:
|
||||
conf = self.col.get_config("savedFilters")
|
||||
|
@ -1430,7 +1107,7 @@ QTableView {{ gridline-color: {grid} }}
|
|||
conf[new] = filt
|
||||
del conf[old]
|
||||
self.col.set_config("savedFilters", conf)
|
||||
self.maybeRefreshSidebar()
|
||||
self.sidebar.refresh()
|
||||
|
||||
# returns name if found
|
||||
def _currentFilterIsSaved(self) -> Optional[str]:
|
||||
|
@ -1879,10 +1556,10 @@ where id in %s"""
|
|||
|
||||
# covers the tag, note and deck case
|
||||
def on_item_added(self, item: Any = None) -> None:
|
||||
self.maybeRefreshSidebar()
|
||||
self.sidebar.refresh()
|
||||
|
||||
def on_tag_list_update(self):
|
||||
self.maybeRefreshSidebar()
|
||||
self.sidebar.refresh()
|
||||
|
||||
def onUndoState(self, on):
|
||||
self.form.actionUndo.setEnabled(on)
|
||||
|
|
|
@ -1,16 +1,21 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright: Ankitects Pty Ltd and contributors
|
||||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from concurrent.futures import Future
|
||||
from enum import Enum
|
||||
from typing import Iterable, List, Optional
|
||||
|
||||
import aqt
|
||||
from anki.errors import DeckRenameError
|
||||
from anki.rsbackend import DeckTreeNode, FilterToSearchIn, NamedFilter, TagTreeNode
|
||||
from aqt import gui_hooks
|
||||
from aqt.main import ResetReason
|
||||
from aqt.models import Models
|
||||
from aqt.qt import *
|
||||
from aqt.theme import theme_manager
|
||||
from aqt.utils import TR, getOnlyText, showInfo, showWarning, tr
|
||||
|
||||
|
||||
|
@ -26,12 +31,199 @@ class SidebarItemType(Enum):
|
|||
TEMPLATE = 8
|
||||
|
||||
|
||||
class SidebarTreeViewBase(QTreeView):
|
||||
def __init__(self):
|
||||
# used by an add-on hook
|
||||
class SidebarStage(Enum):
|
||||
ROOT = 0
|
||||
STANDARD = 1
|
||||
FAVORITES = 2
|
||||
DECKS = 3
|
||||
MODELS = 4
|
||||
TAGS = 5
|
||||
|
||||
|
||||
class SidebarItem:
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
icon: str,
|
||||
onClick: Callable[[], None] = None,
|
||||
onExpanded: Callable[[bool], None] = None,
|
||||
expanded: bool = False,
|
||||
item_type: SidebarItemType = SidebarItemType.CUSTOM,
|
||||
id: int = 0,
|
||||
full_name: str = None,
|
||||
) -> None:
|
||||
self.name = name
|
||||
if not full_name:
|
||||
full_name = name
|
||||
self.full_name = full_name
|
||||
self.icon = icon
|
||||
self.item_type = item_type
|
||||
self.id = id
|
||||
self.onClick = onClick
|
||||
self.onExpanded = onExpanded
|
||||
self.expanded = expanded
|
||||
self.children: List["SidebarItem"] = []
|
||||
self.parentItem: Optional["SidebarItem"] = None
|
||||
self.tooltip: Optional[str] = None
|
||||
|
||||
def addChild(self, cb: "SidebarItem") -> None:
|
||||
self.children.append(cb)
|
||||
cb.parentItem = self
|
||||
|
||||
def rowForChild(self, child: "SidebarItem") -> Optional[int]:
|
||||
try:
|
||||
return self.children.index(child)
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
|
||||
class SidebarModel(QAbstractItemModel):
|
||||
def __init__(self, root: SidebarItem) -> None:
|
||||
super().__init__()
|
||||
self.root = root
|
||||
|
||||
# Qt API
|
||||
######################################################################
|
||||
|
||||
def rowCount(self, parent: QModelIndex = QModelIndex()) -> int:
|
||||
if not parent.isValid():
|
||||
return len(self.root.children)
|
||||
else:
|
||||
item: SidebarItem = parent.internalPointer()
|
||||
return len(item.children)
|
||||
|
||||
def columnCount(self, parent: QModelIndex = QModelIndex()) -> int:
|
||||
return 1
|
||||
|
||||
def index(
|
||||
self, row: int, column: int, parent: QModelIndex = QModelIndex()
|
||||
) -> QModelIndex:
|
||||
if not self.hasIndex(row, column, parent):
|
||||
return QModelIndex()
|
||||
|
||||
parentItem: SidebarItem
|
||||
if not parent.isValid():
|
||||
parentItem = self.root
|
||||
else:
|
||||
parentItem = parent.internalPointer()
|
||||
|
||||
item = parentItem.children[row]
|
||||
return self.createIndex(row, column, item)
|
||||
|
||||
def parent(self, child: QModelIndex) -> QModelIndex: # type: ignore
|
||||
if not child.isValid():
|
||||
return QModelIndex()
|
||||
|
||||
childItem: SidebarItem = child.internalPointer()
|
||||
parentItem = childItem.parentItem
|
||||
|
||||
if parentItem is None or parentItem == self.root:
|
||||
return QModelIndex()
|
||||
|
||||
row = parentItem.rowForChild(childItem)
|
||||
if row is None:
|
||||
return QModelIndex()
|
||||
|
||||
return self.createIndex(row, 0, parentItem)
|
||||
|
||||
def data(self, index: QModelIndex, role: int = Qt.DisplayRole) -> QVariant:
|
||||
if not index.isValid():
|
||||
return QVariant()
|
||||
|
||||
if role not in (Qt.DisplayRole, Qt.DecorationRole, Qt.ToolTipRole):
|
||||
return QVariant()
|
||||
|
||||
item: SidebarItem = index.internalPointer()
|
||||
|
||||
if role == Qt.DisplayRole:
|
||||
return QVariant(item.name)
|
||||
elif role == Qt.ToolTipRole:
|
||||
return QVariant(item.tooltip)
|
||||
else:
|
||||
return QVariant(theme_manager.icon_from_resources(item.icon))
|
||||
|
||||
# Helpers
|
||||
######################################################################
|
||||
|
||||
def iconFromRef(self, iconRef: str) -> QIcon:
|
||||
print("iconFromRef() deprecated")
|
||||
return theme_manager.icon_from_resources(iconRef)
|
||||
|
||||
def expandWhereNeccessary(self, tree: QTreeView) -> None:
|
||||
for row, child in enumerate(self.root.children):
|
||||
if child.expanded:
|
||||
idx = self.index(row, 0, QModelIndex())
|
||||
self._expandWhereNeccessary(idx, tree)
|
||||
|
||||
def _expandWhereNeccessary(self, parent: QModelIndex, tree: QTreeView) -> None:
|
||||
parentItem: SidebarItem
|
||||
if not parent.isValid():
|
||||
parentItem = self.root
|
||||
else:
|
||||
parentItem = parent.internalPointer()
|
||||
|
||||
# nothing to do?
|
||||
if not parentItem.expanded:
|
||||
return
|
||||
|
||||
# expand children
|
||||
for row, child in enumerate(parentItem.children):
|
||||
if not child.expanded:
|
||||
continue
|
||||
childIdx = self.index(row, 0, parent)
|
||||
self._expandWhereNeccessary(childIdx, tree)
|
||||
|
||||
# then ourselves
|
||||
tree.setExpanded(parent, True)
|
||||
|
||||
|
||||
class SidebarTreeView(QTreeView):
|
||||
def __init__(self, browser: aqt.browser.Browser) -> None:
|
||||
super().__init__()
|
||||
self.browser = browser
|
||||
self.mw = browser.mw
|
||||
self.col = self.mw.col
|
||||
|
||||
self.setContextMenuPolicy(Qt.CustomContextMenu)
|
||||
self.customContextMenuRequested.connect(self.onContextMenu) # type: ignore
|
||||
self.context_menus = {
|
||||
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_tag),
|
||||
),
|
||||
SidebarItemType.FILTER: (
|
||||
(tr(TR.ACTIONS_RENAME), self.rename_filter),
|
||||
(tr(TR.ACTIONS_DELETE), self.remove_filter),
|
||||
),
|
||||
SidebarItemType.NOTETYPE: ((tr(TR.ACTIONS_MANAGE), self.manage_notetype),),
|
||||
}
|
||||
|
||||
self.setUniformRowHeights(True)
|
||||
self.setHeaderHidden(True)
|
||||
self.setIndentation(15)
|
||||
|
||||
qconnect(self.expanded, self.onExpansion)
|
||||
qconnect(self.collapsed, self.onCollapse)
|
||||
|
||||
# match window background color
|
||||
bgcolor = QPalette().window().color().name()
|
||||
self.setStyleSheet("QTreeView { background: '%s'; }" % bgcolor)
|
||||
|
||||
def refresh(self) -> None:
|
||||
"Refresh list. No-op if sidebar is not visible."
|
||||
if not self.isVisible():
|
||||
return
|
||||
|
||||
root = self._root_tree()
|
||||
model = SidebarModel(root)
|
||||
self.setModel(model)
|
||||
model.expandWhereNeccessary(self)
|
||||
|
||||
def onClickCurrent(self) -> None:
|
||||
idx = self.currentIndex()
|
||||
if idx.isValid():
|
||||
|
@ -63,31 +255,168 @@ class SidebarTreeViewBase(QTreeView):
|
|||
if item.onExpanded:
|
||||
item.onExpanded(expanded)
|
||||
|
||||
# Tree building
|
||||
###########################
|
||||
|
||||
class NewSidebarTreeView(SidebarTreeViewBase):
|
||||
def __init__(self, browser: aqt.browser.Browser) -> None:
|
||||
super().__init__()
|
||||
self.browser = browser
|
||||
self.mw = browser.mw
|
||||
self.col = self.mw.col
|
||||
def _root_tree(self) -> SidebarItem:
|
||||
root = SidebarItem("", "", item_type=SidebarItemType.ROOT)
|
||||
|
||||
self.setContextMenuPolicy(Qt.CustomContextMenu)
|
||||
self.customContextMenuRequested.connect(self.onContextMenu) # type: ignore
|
||||
self.context_menus = {
|
||||
SidebarItemType.DECK: (
|
||||
(tr(TR.ACTIONS_RENAME), self.rename_deck),
|
||||
(tr(TR.ACTIONS_DELETE), self.delete_deck),
|
||||
handled = gui_hooks.browser_will_build_tree(
|
||||
False, root, SidebarStage.ROOT, self
|
||||
)
|
||||
if handled:
|
||||
return root
|
||||
|
||||
for stage, builder in zip(
|
||||
list(SidebarStage)[1:],
|
||||
(
|
||||
self._commonly_used_tree,
|
||||
self._favorites_tree,
|
||||
self._deck_tree,
|
||||
self._notetype_tree,
|
||||
self._tag_tree,
|
||||
),
|
||||
SidebarItemType.TAG: (
|
||||
(tr(TR.ACTIONS_RENAME), self.rename_tag),
|
||||
(tr(TR.ACTIONS_DELETE), self.remove_tag),
|
||||
),
|
||||
SidebarItemType.FILTER: (
|
||||
(tr(TR.ACTIONS_RENAME), self.rename_filter),
|
||||
(tr(TR.ACTIONS_DELETE), self.remove_filter),
|
||||
),
|
||||
SidebarItemType.NOTETYPE: ((tr(TR.ACTIONS_MANAGE), self.manage_notetype),),
|
||||
}
|
||||
):
|
||||
handled = gui_hooks.browser_will_build_tree(False, root, stage, self)
|
||||
if not handled and builder:
|
||||
builder(root)
|
||||
|
||||
return root
|
||||
|
||||
def _commonly_used_tree(self, root: SidebarItem) -> None:
|
||||
item = SidebarItem(
|
||||
tr(TR.BROWSING_WHOLE_COLLECTION),
|
||||
":/icons/collection.svg",
|
||||
self._named_filter(NamedFilter.WHOLE_COLLECTION),
|
||||
item_type=SidebarItemType.COLLECTION,
|
||||
)
|
||||
root.addChild(item)
|
||||
item = SidebarItem(
|
||||
tr(TR.BROWSING_CURRENT_DECK),
|
||||
":/icons/deck.svg",
|
||||
self._named_filter(NamedFilter.CURRENT_DECK),
|
||||
item_type=SidebarItemType.CURRENT_DECK,
|
||||
)
|
||||
root.addChild(item)
|
||||
|
||||
def _favorites_tree(self, root: SidebarItem) -> None:
|
||||
assert self.col
|
||||
saved = self.col.get_config("savedFilters", {})
|
||||
for name, filt in sorted(saved.items()):
|
||||
item = SidebarItem(
|
||||
name,
|
||||
":/icons/heart.svg",
|
||||
self._saved_filter(filt),
|
||||
item_type=SidebarItemType.FILTER,
|
||||
)
|
||||
root.addChild(item)
|
||||
|
||||
def _tag_tree(self, root: SidebarItem) -> None:
|
||||
tree = self.col.backend.tag_tree()
|
||||
|
||||
def render(root: SidebarItem, nodes: Iterable[TagTreeNode], head="") -> None:
|
||||
for node in nodes:
|
||||
|
||||
def toggle_expand():
|
||||
full_name = head + node.name # pylint: disable=cell-var-from-loop
|
||||
return lambda expanded: self.mw.col.tags.set_collapsed(
|
||||
full_name, not expanded
|
||||
)
|
||||
|
||||
item = SidebarItem(
|
||||
node.name,
|
||||
":/icons/tag.svg",
|
||||
self._tag_filter(head + node.name),
|
||||
toggle_expand(),
|
||||
not node.collapsed,
|
||||
item_type=SidebarItemType.TAG,
|
||||
full_name=head + node.name,
|
||||
)
|
||||
root.addChild(item)
|
||||
newhead = head + node.name + "::"
|
||||
render(item, node.children, newhead)
|
||||
|
||||
render(root, tree.children)
|
||||
|
||||
def _deck_tree(self, root: SidebarItem) -> None:
|
||||
tree = self.col.decks.deck_tree()
|
||||
|
||||
def render(root, nodes: Iterable[DeckTreeNode], head="") -> None:
|
||||
for node in nodes:
|
||||
|
||||
def toggle_expand():
|
||||
did = node.deck_id # pylint: disable=cell-var-from-loop
|
||||
return lambda _: self.mw.col.decks.collapseBrowser(did)
|
||||
|
||||
item = SidebarItem(
|
||||
node.name,
|
||||
":/icons/deck.svg",
|
||||
self._deck_filter(head + node.name),
|
||||
toggle_expand(),
|
||||
not node.collapsed,
|
||||
item_type=SidebarItemType.DECK,
|
||||
id=node.deck_id,
|
||||
)
|
||||
root.addChild(item)
|
||||
newhead = head + node.name + "::"
|
||||
render(item, node.children, newhead)
|
||||
|
||||
render(root, tree.children)
|
||||
|
||||
def _notetype_tree(self, root: SidebarItem) -> None:
|
||||
assert self.col
|
||||
|
||||
for nt in sorted(self.col.models.all(), key=lambda nt: nt["name"].lower()):
|
||||
item = SidebarItem(
|
||||
nt["name"],
|
||||
":/icons/notetype.svg",
|
||||
self._note_filter(nt["name"]),
|
||||
item_type=SidebarItemType.NOTETYPE,
|
||||
id=nt["id"],
|
||||
)
|
||||
|
||||
for c, tmpl in enumerate(nt["tmpls"]):
|
||||
child = SidebarItem(
|
||||
tmpl["name"],
|
||||
":/icons/notetype.svg",
|
||||
self._template_filter(nt["name"], c),
|
||||
item_type=SidebarItemType.TEMPLATE,
|
||||
)
|
||||
item.addChild(child)
|
||||
|
||||
root.addChild(item)
|
||||
|
||||
def _named_filter(self, name: "FilterToSearchIn.NamedFilterValue") -> Callable:
|
||||
return lambda: self.browser.update_search(
|
||||
self.col.backend.filter_to_search(FilterToSearchIn(name=name))
|
||||
)
|
||||
|
||||
def _tag_filter(self, tag: str) -> Callable:
|
||||
return lambda: self.browser.update_search(
|
||||
self.col.backend.filter_to_search(FilterToSearchIn(tag=tag))
|
||||
)
|
||||
|
||||
def _deck_filter(self, deck: str) -> Callable:
|
||||
return lambda: self.browser.update_search(
|
||||
self.col.backend.filter_to_search(FilterToSearchIn(deck=deck))
|
||||
)
|
||||
|
||||
def _note_filter(self, note: str) -> Callable:
|
||||
return lambda: self.browser.update_search(
|
||||
self.col.backend.filter_to_search(FilterToSearchIn(note=note))
|
||||
)
|
||||
|
||||
def _template_filter(self, note: str, template: int) -> Callable:
|
||||
return lambda: self.browser.update_search(
|
||||
self.col.backend.filter_to_search(FilterToSearchIn(note=note)),
|
||||
self.col.backend.filter_to_search(FilterToSearchIn(template=template)),
|
||||
)
|
||||
|
||||
def _saved_filter(self, saved: str) -> Callable:
|
||||
return lambda: self.browser.update_search(saved)
|
||||
|
||||
# Context menu actions
|
||||
###########################
|
||||
|
||||
def onContextMenu(self, point: QPoint) -> None:
|
||||
idx: QModelIndex = self.indexAt(point)
|
||||
|
|
Loading…
Reference in a new issue