ditch QSortFilterProxyModel in favour of our own code

Simpler and approximately twice as fast in a large collection:

old approach
search for a: 371ms
search for an: 260ms

new approach:
search for a: 171ms
search for an: 149ms

Still todo: add enum defs for the other root categories, update
the _section_root() calls, and update is_expanded() to use the new
extra types
This commit is contained in:
Damien Elmes 2021-02-02 10:40:50 +10:00
parent c96248d67f
commit 3a8fce69dd

View File

@ -93,6 +93,8 @@ class SidebarItem:
self.parentItem: Optional["SidebarItem"] = None self.parentItem: Optional["SidebarItem"] = None
self.tooltip: Optional[str] = None self.tooltip: Optional[str] = None
self.row_in_parent: Optional[int] = None self.row_in_parent: Optional[int] = None
self._search_matches_self = False
self._search_matches_child = False
def addChild(self, cb: "SidebarItem") -> None: def addChild(self, cb: "SidebarItem") -> None:
self.children.append(cb) self.children.append(cb)
@ -104,6 +106,30 @@ class SidebarItem:
except ValueError: except ValueError:
return None return None
def is_expanded(self, searching: bool) -> bool:
if not searching:
return self.expanded
else:
if self._search_matches_child:
return True
# if search matches top level, expand children one level
# FIXME: add types for other roots
return self._search_matches_self and self.item_type in (
SidebarItemType.SAVED_SEARCH_ROOT,
SidebarItemType.DECK_ROOT,
)
def is_highlighted(self) -> bool:
return self._search_matches_self
def search(self, lowered_text: str) -> bool:
"True if we or child matched."
self._search_matches_self = lowered_text in self.name.lower()
self._search_matches_child = any(
[child.search(lowered_text) for child in self.children]
)
return self._search_matches_self or self._search_matches_child
class SidebarModel(QAbstractItemModel): class SidebarModel(QAbstractItemModel):
def __init__(self, root: SidebarItem) -> None: def __init__(self, root: SidebarItem) -> None:
@ -120,6 +146,9 @@ class SidebarModel(QAbstractItemModel):
def item_for_index(self, idx: QModelIndex) -> SidebarItem: def item_for_index(self, idx: QModelIndex) -> SidebarItem:
return idx.internalPointer() return idx.internalPointer()
def search(self, text: str) -> None:
self.root.search(text.lower())
# Qt API # Qt API
###################################################################### ######################################################################
@ -204,39 +233,20 @@ class SidebarModel(QAbstractItemModel):
def expand_where_necessary( def expand_where_necessary(
model: SidebarModel, tree: QTreeView, parent: Optional[QModelIndex] = None model: SidebarModel,
tree: QTreeView,
parent: Optional[QModelIndex] = None,
searching: bool = False,
) -> None: ) -> None:
parent = parent or QModelIndex() parent = parent or QModelIndex()
for row in range(model.rowCount(parent)): for row in range(model.rowCount(parent)):
idx = model.index(row, 0, parent) idx = model.index(row, 0, parent)
if not idx.isValid(): if not idx.isValid():
continue continue
expand_where_necessary(model, tree, idx) expand_where_necessary(model, tree, idx, searching)
item = model.item_for_index(idx) if item := model.item_for_index(idx):
if item and item.expanded: if item.is_expanded(searching):
tree.setExpanded(idx, True) tree.setExpanded(idx, True)
class FilterModel(QSortFilterProxyModel):
def item_for_index(self, idx: QModelIndex) -> Optional[SidebarItem]:
if not idx.isValid():
return None
return self.mapToSource(idx).internalPointer()
def _anyParentMatches(self, item: SidebarItem) -> bool:
if not item.parentItem:
return False
if self.parent().current_search.lower() in item.parentItem.name.lower():
return True
return self._anyParentMatches(item.parentItem)
def filterAcceptsRow(self, row: int, parent: QModelIndex) -> bool:
current_search = self.parent().current_search
if not current_search:
return False
current_search = current_search.lower()
item = self.sourceModel().index(row, 0, parent).internalPointer()
return current_search in item.name.lower() or self._anyParentMatches(item)
class SidebarSearchBar(QLineEdit): class SidebarSearchBar(QLineEdit):
@ -312,7 +322,7 @@ class SidebarTreeView(QTreeView):
bgcolor = QPalette().window().color().name() bgcolor = QPalette().window().color().name()
self.setStyleSheet("QTreeView { background: '%s'; }" % bgcolor) self.setStyleSheet("QTreeView { background: '%s'; }" % bgcolor)
def model(self) -> Union[FilterModel, SidebarModel]: def model(self) -> SidebarModel:
return super().model() return super().model()
def refresh(self) -> None: def refresh(self) -> None:
@ -340,47 +350,22 @@ class SidebarTreeView(QTreeView):
self.current_search = None self.current_search = None
self.refresh() self.refresh()
return return
if not isinstance(self.model(), FilterModel):
filter_model = FilterModel(self)
filter_model.setSourceModel(self.model())
filter_model.setFilterCaseSensitivity(False) # type: ignore
filter_model.setRecursiveFilteringEnabled(True)
self.setModel(filter_model)
else:
filter_model = self.model()
self.current_search = text self.current_search = text
# Without collapsing first, can be very slow. Surely there's # start from a collapsed state, as it's faster
# a better way than this?
self.collapseAll() self.collapseAll()
filter_model.setFilterFixedString(text) self.model().search(text)
self.expandMatches(self.rootIndex()) expand_where_necessary(self.model(), self, searching=True)
def expandMatches(self, parent: QModelIndex) -> bool:
"Expand match trees one level."
expand = False
for i in range(self.model().rowCount(parent)):
idx = self.model().index(i, 0, parent)
item = self.model().item_for_index(idx)
expandChild = self.expandMatches(idx) or (
bool(item) and self.current_search.lower() in item.name.lower()
)
expand |= expandChild
self.setExpanded(idx, expandChild)
return expand
def drawRow( def drawRow(
self, painter: QPainter, options: QStyleOptionViewItem, idx: QModelIndex self, painter: QPainter, options: QStyleOptionViewItem, idx: QModelIndex
) -> None: ) -> None:
if self.current_search is None: if self.current_search and (item := self.model().item_for_index(idx)):
return super().drawRow(painter, options, idx) if item.is_highlighted():
if not (item := self.model().item_for_index(idx)): brush = QBrush(theme_manager.qcolor("suspended-bg"))
return super().drawRow(painter, options, idx) painter.save()
if self.current_search.lower() in item.name.lower(): painter.fillRect(options.rect, brush)
brush = QBrush(theme_manager.qcolor("suspended-bg")) painter.restore()
painter.save()
painter.fillRect(options.rect, brush)
painter.restore()
return super().drawRow(painter, options, idx) return super().drawRow(painter, options, idx)
def dropEvent(self, event: QDropEvent) -> None: def dropEvent(self, event: QDropEvent) -> None: