Skip to content
  • Categories
  • Recent
  • Tags
  • Popular
  • Users
  • Groups
  • Search
  • Get Qt Extensions
  • Unsolved
Collapse
Brand Logo
  1. Home
  2. Qt Development
  3. Qt for Python
  4. Custom QComboBox with QCompleter & QSortFilterProxyModel has misbehaving popup
Forum Updated to NodeBB v4.3 + New Features

Custom QComboBox with QCompleter & QSortFilterProxyModel has misbehaving popup

Scheduled Pinned Locked Moved Unsolved Qt for Python
pysidepythonqt for python
3 Posts 3 Posters 740 Views 1 Watching
  • Oldest to Newest
  • Newest to Oldest
  • Most Votes
Reply
  • Reply as topic
Log in to reply
This topic has been deleted. Only users with topic management privileges can see it.
  • A Offline
    A Offline
    adur
    wrote on last edited by
    #1

    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())```
    1 Reply Last reply
    0
    • R Offline
      R Offline
      redhead
      wrote on last edited by
      #2

      What's the resolution of this problem? It's occurring for me as well.

      SGaistS 1 Reply Last reply
      0
      • R redhead

        What's the resolution of this problem? It's occurring for me as well.

        SGaistS Offline
        SGaistS Offline
        SGaist
        Lifetime Qt Champion
        wrote on last edited by
        #3

        @redhead Hi,

        It's working correctly on macOS with PySide6 6.7.2.

        Interested in AI ? www.idiap.ch
        Please read the Qt Code of Conduct - https://forum.qt.io/topic/113070/qt-code-of-conduct

        1 Reply Last reply
        0

        • Login

        • Login or register to search.
        • First post
          Last post
        0
        • Categories
        • Recent
        • Tags
        • Popular
        • Users
        • Groups
        • Search
        • Get Qt Extensions
        • Unsolved