faster sidebar implementation

- qtreewidget is too slow on Qt5
- expanding/collapsing still todo
This commit is contained in:
Damien Elmes 2019-12-19 21:11:12 +10:00
parent 18e60f7678
commit 867136eace

View file

@ -8,6 +8,8 @@ import time
import re import re
import unicodedata import unicodedata
from operator import itemgetter from operator import itemgetter
from typing import Callable, List, Dict, Optional
from anki.lang import ngettext from anki.lang import ngettext
import json import json
@ -377,6 +379,106 @@ class StatusDelegate(QItemDelegate):
return QItemDelegate.paint(self, painter, option, index) return QItemDelegate.paint(self, painter, option, index)
# Sidebar
######################################################################
class SidebarItem:
def __init__(self,
name: str,
icon: str,
onclick: Callable[[], None] = None,
oncollapse: Callable[[], None] = None,
expanded: bool = False) -> None:
self.name = name
self.icon = icon
self.onclick = onclick
self.oncollapse = oncollapse
self.expanded = expanded
self.children: List["SidebarItem"] = []
self.parentItem: Optional[SidebarItem] = 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
self.iconCache: Dict[str, QIcon] = {}
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 hasChildren(self, parent: QModelIndex = QModelIndex()) -> bool:
if not parent.isValid():
return True
item: SidebarItem = parent.internalPointer()
return len(item.children) > 0
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):
return QVariant()
item: SidebarItem = index.internalPointer()
if role == Qt.DisplayRole:
return QVariant(item.name)
else:
return QVariant(self.iconFromRef(item.icon))
def iconFromRef(self, iconRef: str) -> QIcon:
icon = self.iconCache.get(iconRef)
if icon is None:
icon = QIcon(iconRef)
self.iconCache[iconRef] = icon
return icon
# Browser window # Browser window
###################################################################### ######################################################################
@ -798,114 +900,112 @@ by clicking on one on the left."""))
# Sidebar # Sidebar
###################################################################### ######################################################################
class CallbackItem(QTreeWidgetItem): class SidebarTreeView(QTreeView):
def __init__(self, root, name, onclick, oncollapse=None, expanded=False): def onClickCurrent(self) -> None:
QTreeWidgetItem.__init__(self, root, [name]) idx = self.currentIndex()
self.setExpanded(expanded) if idx.isValid():
self.onclick = onclick item: SidebarItem = idx.internalPointer()
self.oncollapse = oncollapse if item.onclick:
class SidebarTreeWidget(QTreeWidget):
def __init__(self):
QTreeWidget.__init__(self)
self.itemClicked.connect(self.onTreeClick)
self.itemExpanded.connect(self.onTreeCollapse)
self.itemCollapsed.connect(self.onTreeCollapse)
def keyPressEvent(self, evt):
if evt.key() in (Qt.Key_Return, Qt.Key_Enter):
item = self.currentItem()
self.onTreeClick(item, 0)
else:
super().keyPressEvent(evt)
def onTreeClick(self, item, col):
if getattr(item, 'onclick', None):
item.onclick() item.onclick()
def onTreeCollapse(self, item): def mouseReleaseEvent(self, event: QMouseEvent) -> None:
if getattr(item, 'oncollapse', None): self.onClickCurrent()
item.oncollapse()
def setupSidebar(self): def keyPressEvent(self, event: QKeyEvent) -> None:
if event.key() in (Qt.Key_Return, Qt.Key_Enter):
self.onClickCurrent()
else:
super().keyPressEvent(event)
def setupSidebar(self) -> None:
dw = self.sidebarDockWidget = QDockWidget(_("Sidebar"), self) dw = self.sidebarDockWidget = QDockWidget(_("Sidebar"), self)
dw.setFeatures(QDockWidget.DockWidgetClosable) dw.setFeatures(QDockWidget.DockWidgetClosable)
dw.setObjectName("Sidebar") dw.setObjectName("Sidebar")
dw.setAllowedAreas(Qt.LeftDockWidgetArea) dw.setAllowedAreas(Qt.LeftDockWidgetArea)
self.sidebarTree = self.SidebarTreeWidget() self.sidebarTree = self.SidebarTreeView()
self.sidebarTree.mw = self.mw self.sidebarTree.mw = self.mw
self.sidebarTree.header().setVisible(False) self.sidebarTree.setUniformRowHeights(True)
self.sidebarTree.setHeaderHidden(True)
self.sidebarTree.setIndentation(15)
dw.setWidget(self.sidebarTree) dw.setWidget(self.sidebarTree)
p = QPalette() p = QPalette()
p.setColor(QPalette.Base, p.window().color()) p.setColor(QPalette.Base, p.window().color())
self.sidebarTree.setPalette(p) self.sidebarTree.setPalette(p)
self.sidebarDockWidget.setFloating(False) self.sidebarDockWidget.setFloating(False)
self.sidebarDockWidget.visibilityChanged.connect(self.onSidebarVisChanged) self.sidebarDockWidget.visibilityChanged.connect(self.onSidebarVisChanged) # type: ignore
self.sidebarDockWidget.setTitleBarWidget(QWidget()) self.sidebarDockWidget.setTitleBarWidget(QWidget())
self.addDockWidget(Qt.LeftDockWidgetArea, dw) self.addDockWidget(Qt.LeftDockWidgetArea, dw)
def onSidebarVisChanged(self, visible): def onSidebarVisChanged(self, _visible: bool) -> None:
if visible: self.maybeRefreshSidebar()
self.buildTree()
else:
pass
def focusSidebar(self): def focusSidebar(self) -> None:
self.sidebarDockWidget.setVisible(True) self.sidebarDockWidget.setVisible(True)
self.sidebarTree.setFocus() self.sidebarTree.setFocus()
def maybeRefreshSidebar(self): def maybeRefreshSidebar(self) -> None:
if self.sidebarDockWidget.isVisible(): if self.sidebarDockWidget.isVisible():
self.buildTree() # add slight delay to allow browser window to appear first
def deferredDisplay():
root = self.buildTree()
model = SidebarModel(root)
self.sidebarTree.setModel(model)
self.mw.progress.timer(10, deferredDisplay, False)
def buildTree(self): def buildTree(self) -> SidebarItem:
self.sidebarTree.clear() root = SidebarItem("", "")
root = self.sidebarTree
self._stdTree(root) self._stdTree(root)
self._favTree(root) self._favTree(root)
self._decksTree(root) self._decksTree(root)
self._modelTree(root) self._modelTree(root)
self._userTagTree(root) self._userTagTree(root)
self.sidebarTree.setIndentation(15) return root
def _stdTree(self, root): def _stdTree(self, root) -> None:
for name, filt, icon in [[_("Whole Collection"), "", "collection"], for name, filt, icon in [[_("Whole Collection"), "", "collection"],
[_("Current Deck"), "deck:current", "deck"]]: [_("Current Deck"), "deck:current", "deck"]]:
item = self.CallbackItem( item = SidebarItem(
root, name, self._filterFunc(filt)) name, ":/icons/{}.svg".format(icon), self._filterFunc(filt))
item.setIcon(0, QIcon(":/icons/{}.svg".format(icon))) root.addChild(item)
def _favTree(self, root): def _favTree(self, root) -> None:
assert self.col
saved = self.col.conf.get('savedFilters', {}) saved = self.col.conf.get('savedFilters', {})
for name, filt in sorted(saved.items()): for name, filt in sorted(saved.items()):
item = self.CallbackItem(root, name, lambda s=filt: self.setFilter(s)) item = SidebarItem(name, ":/icons/heart.svg",
item.setIcon(0, QIcon(":/icons/heart.svg")) lambda s=filt: self.setFilter(s) # type: ignore
)
root.addChild(item)
def _userTagTree(self, root): def _userTagTree(self, root) -> None:
assert self.col
for t in sorted(self.col.tags.all(), key=lambda t: t.lower()): for t in sorted(self.col.tags.all(), key=lambda t: t.lower()):
item = self.CallbackItem( item = SidebarItem(
root, t, lambda t=t: self.setFilter("tag", t)) t, ":/icons/tag.svg", lambda t=t: self.setFilter("tag", t)) # type: ignore
item.setIcon(0, QIcon(":/icons/tag.svg")) root.addChild(item)
def _decksTree(self, root): def _decksTree(self, root) -> None:
assert self.col
grps = self.col.sched.deckDueTree() grps = self.col.sched.deckDueTree()
def fillGroups(root, grps, head=""): def fillGroups(root, grps, head=""):
for g in grps: for g in grps:
item = self.CallbackItem( item = SidebarItem(
root, g[0], g[0],
":/icons/deck.svg",
lambda g=g: self.setFilter("deck", head+g[0]), lambda g=g: self.setFilter("deck", head+g[0]),
lambda g=g: self.mw.col.decks.collapseBrowser(g[1]), lambda g=g: self.mw.col.decks.collapseBrowser(g[1]),
not self.mw.col.decks.get(g[1]).get('browserCollapsed', False)) not self.mw.col.decks.get(g[1]).get('browserCollapsed', False))
item.setIcon(0, QIcon(":/icons/deck.svg")) root.addChild(item)
newhead = head + g[0]+"::" newhead = head + g[0]+"::"
fillGroups(item, g[5], newhead) fillGroups(item, g[5], newhead)
fillGroups(root, grps) fillGroups(root, grps)
def _modelTree(self, root): def _modelTree(self, root) -> None:
assert self.col
for m in sorted(self.col.models.all(), key=itemgetter("name")): for m in sorted(self.col.models.all(), key=itemgetter("name")):
mitem = self.CallbackItem( item = SidebarItem(
root, m['name'], lambda m=m: self.setFilter("note", m['name'])) m['name'], ":/icons/notetype.svg", lambda m=m: self.setFilter("note", m['name'])) # type: ignore
mitem.setIcon(0, QIcon(":/icons/notetype.svg")) root.addChild(item)
# Filter tree # Filter tree
###################################################################### ######################################################################