diff --git a/qt/aqt/sidebar.py b/qt/aqt/sidebar.py index 85526e7d6..32060b1fe 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -93,6 +93,8 @@ class SidebarItem: self.parentItem: Optional["SidebarItem"] = None self.tooltip: Optional[str] = None self.row_in_parent: Optional[int] = None + self._search_matches_self = False + self._search_matches_child = False def addChild(self, cb: "SidebarItem") -> None: self.children.append(cb) @@ -104,6 +106,30 @@ class SidebarItem: except ValueError: 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): def __init__(self, root: SidebarItem) -> None: @@ -120,6 +146,9 @@ class SidebarModel(QAbstractItemModel): def item_for_index(self, idx: QModelIndex) -> SidebarItem: return idx.internalPointer() + def search(self, text: str) -> None: + self.root.search(text.lower()) + # Qt API ###################################################################### @@ -204,39 +233,20 @@ class SidebarModel(QAbstractItemModel): def expand_where_necessary( - model: SidebarModel, tree: QTreeView, parent: Optional[QModelIndex] = None + model: SidebarModel, + tree: QTreeView, + parent: Optional[QModelIndex] = None, + searching: bool = False, ) -> None: parent = parent or QModelIndex() for row in range(model.rowCount(parent)): idx = model.index(row, 0, parent) if not idx.isValid(): continue - expand_where_necessary(model, tree, idx) - item = model.item_for_index(idx) - if item and item.expanded: - 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) + expand_where_necessary(model, tree, idx, searching) + if item := model.item_for_index(idx): + if item.is_expanded(searching): + tree.setExpanded(idx, True) class SidebarSearchBar(QLineEdit): @@ -312,7 +322,7 @@ class SidebarTreeView(QTreeView): bgcolor = QPalette().window().color().name() self.setStyleSheet("QTreeView { background: '%s'; }" % bgcolor) - def model(self) -> Union[FilterModel, SidebarModel]: + def model(self) -> SidebarModel: return super().model() def refresh(self) -> None: @@ -340,47 +350,22 @@ class SidebarTreeView(QTreeView): self.current_search = None self.refresh() 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 - # Without collapsing first, can be very slow. Surely there's - # a better way than this? + # start from a collapsed state, as it's faster self.collapseAll() - filter_model.setFilterFixedString(text) - self.expandMatches(self.rootIndex()) - - 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 + self.model().search(text) + expand_where_necessary(self.model(), self, searching=True) def drawRow( self, painter: QPainter, options: QStyleOptionViewItem, idx: QModelIndex ) -> None: - if self.current_search is None: - return super().drawRow(painter, options, idx) - if not (item := self.model().item_for_index(idx)): - return super().drawRow(painter, options, idx) - if self.current_search.lower() in item.name.lower(): - brush = QBrush(theme_manager.qcolor("suspended-bg")) - painter.save() - painter.fillRect(options.rect, brush) - painter.restore() + if self.current_search and (item := self.model().item_for_index(idx)): + if item.is_highlighted(): + brush = QBrush(theme_manager.qcolor("suspended-bg")) + painter.save() + painter.fillRect(options.rect, brush) + painter.restore() return super().drawRow(painter, options, idx) def dropEvent(self, event: QDropEvent) -> None: