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.
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())