Custom QComboBox with QCompleter & QSortFilterProxyModel has misbehaving popup
-
I'm encountering an issue while attempting to create a custom searchable QComboBox. Specifically, I find that I need to click on a popup item twice before it activates the selection. This behavior seems to stem from my usage of QCompleter and QFilterProxyModel. While the default completer works as expected, I encounter this issue when applying custom filtering and sorting. Additionally, I've noticed that the hover highlighting on the completer popup is missing. Although I can set the stylesheet to address this, I suspect there might be an underlying issue causing its disappearance.
You can observe the issues in this video demonstration: https://streamable.com/j2vrg6.
The first click sets the text in the QLineEdit, but the popup persists and the clicked and activated signals of the popup are not emitted. If I click the same item again it seems to finally trigger the selection, but the text seems to have to match what is in the QLineEdit for this to happen.
I'd appreciate any insights or suggestions on resolving these issues.
from PySide6.QtCore import Qt, QSortFilterProxyModel from PySide6.QtWidgets import QLineEdit, QComboBox, QApplication, QLineEdit, QWidget, QCompleter class SubstringFilterProxyModel(QSortFilterProxyModel): def filterAcceptsRow(self, source_row, source_parent): model = self.sourceModel() if model is None: return False text_filter = self.filterRegularExpression().pattern().casefold() if text_filter == '': return True model_index = model.index(source_row, 0, source_parent) if not model_index.isValid(): return False model_data = model.data(model_index, Qt.DisplayRole) for substring in text_filter.split(' '): if (substring not in model_data.casefold()): return False return True def lessThan(self, left, right): # get the data from the source model left_data = self.sourceModel().data(left) right_data = self.sourceModel().data(right) if left_data is None: return False if right_data is None: return True # find the index of the search string in each data text_filter = self.filterRegularExpression().pattern().casefold() left_index = left_data.find(text_filter) right_index = right_data.find(text_filter) # compare the indexes return left_index < right_index class CustomQCompleter(QCompleter): def splitPath(self, path): self.model().invalidate() # Invalidates the current sorting and filtering. self.model().sort(0, Qt.AscendingOrder) return '' class SelectAllLineEdit(QLineEdit): def __init__(self, parent=None): super(SelectAllLineEdit, self).__init__(parent) self.ready_to_edit = True def mousePressEvent(self, e): super(SelectAllLineEdit, self).mousePressEvent(e) # deselect on 2nd click if self.ready_to_edit: self.selectAll() self.ready_to_edit = False def focusOutEvent(self, e): super(SelectAllLineEdit, self).focusOutEvent(e) # remove cursor on focusOut self.deselect() self.ready_to_edit = True class SearchableComboBox(QComboBox): def __init__(self, parent: QWidget | None = None ) -> None: super().__init__(parent) self.setInsertPolicy(QComboBox.InsertPolicy.NoInsert) self.setEditable(True) self.setLineEdit(SelectAllLineEdit()) self.lineEdit().setEchoMode(QLineEdit.EchoMode.Normal) proxy_model = SubstringFilterProxyModel() proxy_model.setSourceModel(self.model()) self.lineEdit().textChanged.connect(proxy_model.setFilterRegularExpression) self.lineEdit().editingFinished.connect(self.editing_finished) completer = CustomQCompleter() completer.setModel(proxy_model) completer.setCompletionMode(QCompleter.CompletionMode.PopupCompletion) # completer.popup().setStyleSheet("QListView::item:hover {background-color: rgb(55,134,209);}") completer.popup().clicked.connect(lambda: print("clicked")) # Set the completer for the combo box self.setCompleter(completer) # this works, but no custom filtering # self.completer().setCompletionMode(QCompleter.CompletionMode.PopupCompletion) # self.completer().popup().setStyleSheet("QListView::item:hover {background-color: rgb(55,134,209);}") # self.completer().setModel(proxy_model) # this makes default completer act as described in the post def editing_finished(self): text = self.completer().currentCompletion() index = self.findText(text) self.setCurrentIndex(index) self.clearFocus() self.activated.emit(index) if __name__ == "__main__": import sys from PySide6.QtWidgets import QApplication app = QApplication(sys.argv) app.setStyle('Fusion') comboBox = SearchableComboBox() comboBox.addItems(["Apple", "Still filter apple", "Archer", "alchemy", "Orange apple", "banana", "pineapple", "pine apple", "pine apple but with more", "pine and apple"]) comboBox.show() sys.exit(app.exec())```