QCompleter Custom Completion Rules
I'm using Qt4.6 and I have a QComboBox with a QCompleter in it.
The usual functionality is to provide completion hints (these can be in a dropdown rather than inline - which is my usage) based on a prefix. For example, given
chicken soup
chilli peppers
grilled chicken
entering ch
would match chicken soup
and chilli peppers
but not grilled chicken
.
What I want is to be able to enter ch
and match all of them or, more specifically, chicken
and match chicken soup
and grilled chicken
.
I also want to be able to assign a tag like chs
to chicken soup
to produce another match which is not just on the text's content. I can handle the algorithm but,
Which of QCompleter's functions do I need to override?
I'm not really sure where I should be looking...
Based on @j3frea suggestion, here is a working example (using PySide
). It appears that the model needs to be set every time splitPath
is called (setting the proxy once in setModel
doesn't work).
combobox.setEditable(True)
combobox.setInsertPolicy(QComboBox.NoInsert)
class CustomQCompleter(QCompleter):
def __init__(self, parent=None):
super(CustomQCompleter, self).__init__(parent)
self.local_completion_prefix = ""
self.source_model = None
def setModel(self, model):
self.source_model = model
super(CustomQCompleter, self).setModel(self.source_model)
def updateModel(self):
local_completion_prefix = self.local_completion_prefix
class InnerProxyModel(QSortFilterProxyModel):
def filterAcceptsRow(self, sourceRow, sourceParent):
index0 = self.sourceModel().index(sourceRow, 0, sourceParent)
return local_completion_prefix.lower() in self.sourceModel().data(index0).lower()
proxy_model = InnerProxyModel()
proxy_model.setSourceModel(self.source_model)
super(CustomQCompleter, self).setModel(proxy_model)
def splitPath(self, path):
self.local_completion_prefix = path
self.updateModel()
return ""
completer = CustomQCompleter(combobox)
completer.setCompletionMode(QCompleter.PopupCompletion)
completer.setModel(combobox.model())
combobox.setCompleter(completer)
Building on the answer of @Bruno, I am using the standard QSortFilterProxyModel
function setFilterRegExp
to change the search string. In this way no sub-classing is necessary.
It also fixes a bug in @Bruno's answer, which made the suggestions vanish for some reasons once the input string got corrected with backspace while typing.
class CustomQCompleter(QtGui.QCompleter):
"""
adapted from: http://stackoverflow.com/a/7767999/2156909
"""
def __init__(self, *args):#parent=None):
super(CustomQCompleter, self).__init__(*args)
self.local_completion_prefix = ""
self.source_model = None
self.filterProxyModel = QtGui.QSortFilterProxyModel(self)
self.usingOriginalModel = False
def setModel(self, model):
self.source_model = model
self.filterProxyModel = QtGui.QSortFilterProxyModel(self)
self.filterProxyModel.setSourceModel(self.source_model)
super(CustomQCompleter, self).setModel(self.filterProxyModel)
self.usingOriginalModel = True
def updateModel(self):
if not self.usingOriginalModel:
self.filterProxyModel.setSourceModel(self.source_model)
pattern = QtCore.QRegExp(self.local_completion_prefix,
QtCore.Qt.CaseInsensitive,
QtCore.QRegExp.FixedString)
self.filterProxyModel.setFilterRegExp(pattern)
def splitPath(self, path):
self.local_completion_prefix = path
self.updateModel()
if self.filterProxyModel.rowCount() == 0:
self.usingOriginalModel = False
self.filterProxyModel.setSourceModel(QtGui.QStringListModel([path]))
return [path]
return []
class AutoCompleteComboBox(QtGui.QComboBox):
def __init__(self, *args, **kwargs):
super(AutoCompleteComboBox, self).__init__(*args, **kwargs)
self.setEditable(True)
self.setInsertPolicy(self.NoInsert)
self.comp = CustomQCompleter(self)
self.comp.setCompletionMode(QtGui.QCompleter.PopupCompletion)
self.setCompleter(self.comp)#
self.setModel(["Lola", "Lila", "Cola", 'Lothian'])
def setModel(self, strList):
self.clear()
self.insertItems(0, strList)
self.comp.setModel(self.model())
def focusInEvent(self, event):
self.clearEditText()
super(AutoCompleteComboBox, self).focusInEvent(event)
def keyPressEvent(self, event):
key = event.key()
if key == 16777220:
# Enter (if event.key() == QtCore.Qt.Key_Enter) does not work
# for some reason
# make sure that the completer does not set the
# currentText of the combobox to "" when pressing enter
text = self.currentText()
self.setCompleter(None)
self.setEditText(text)
self.setCompleter(self.comp)
return super(AutoCompleteComboBox, self).keyPressEvent(event)
Update:
I figured that my previous solution worked until the string in the combobox matched none of the list items. Then the QFilterProxyModel
was empty and this in turn reseted the text
of the combobox. I tried to find an elegant solution to this problem, but I ran into problems (referencing deleted object errors) whenever I tried to change something on self.filterProxyModel
. So now the hack is to set the model of self.filterProxyModel
everytime new when its pattern is updated. And whenever the pattern does not match anything in the model anymore, to give it a new model that just contains the current text (aka path
in splitPath
). This might lead to performance issues if you are dealing with very large models, but for me the hack works pretty well.
Update 2:
I realized that this is still not the perfect way to go, because if a new string is typed in the combobox and the user presses enter, the combobox is cleared again. The only way to enter a new string is to select it from the drop down menu after typing.
Update 3:
Now enter works as well. I worked around the reset of the combobox text by simply taking it off charge when the user presses enter. But I put it back in, so that the completion functionality remains in place. If the user decides to do further edits.