Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

QCompleter Custom Completion Rules

Tags:

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

like image 578
jcuenod Avatar asked Feb 26 '11 19:02

jcuenod


2 Answers

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) 
like image 69
Bruno Avatar answered Oct 12 '22 10:10

Bruno


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.

like image 43
P.R. Avatar answered Oct 12 '22 12:10

P.R.