Adam Thompson
Adam Thompson

Reputation: 53

How to debug QComboBox emitting multiple "activated" signals

I found a great resource here for building a QComboBox that gives a filtered list of suggested. It works well except for the fact that the "activated" and "currentIndexChanged" signals get emitted three times every time I select a suggested option in the combobox. The behavior is different depending on if the option is selected by mouse or using the arrow keys and the enter button.

My question is, how do I debug this? There is no point in the code to catch and prevent the first two signals from getting emitted. Is there a way to override the QComboBox "activated" signal to try and catch it in the act? Or do I have to define my own signal and use that instead?

Here's the code:

#!/usr/bin/env python
# -*- coding: utf-8 -*-
from PySide2 import QtCore, QtGui, QtWidgets
from PySide2.QtCore import Qt, QSortFilterProxyModel
from PySide2.QtWidgets import QCompleter, QComboBox

class ExtendedComboBox(QComboBox):
    def __init__(self, parent=None):
        super(ExtendedComboBox, self).__init__(parent)

        self.setFocusPolicy(Qt.StrongFocus)
        self.setEditable(True)

        # add a filter model to filter matching items
        self.pFilterModel = QSortFilterProxyModel(self)
        self.pFilterModel.setFilterCaseSensitivity(Qt.CaseInsensitive)
        self.pFilterModel.setSourceModel(self.model())

        # add a completer, which uses the filter model
        self.completer = QtWidgets.QCompleter(self)
        self.completer.setModel(self.pFilterModel)

        # always show all (filtered) completions
        self.completer.setCompletionMode(QCompleter.UnfilteredPopupCompletion)
        self.setCompleter(self.completer)

        # connect signals
        self.lineEdit().textEdited.connect(self.pFilterModel.setFilterFixedString)
        self.completer.activated.connect(self.on_completer_activated)

    # on selection of an item from the completer, select the corresponding item from combobox 
    def on_completer_activated(self, text):
        if text:
            index = self.findText(text)
            self.setCurrentIndex(index)
            # self.activated.emit(self.itemText(index))


    # on model change, update the models of the filter and completer as well 
    def setModel(self, model):
        super(ExtendedComboBox, self).setModel(model)
        self.pFilterModel.setSourceModel(model)
        self.completer.setModel(self.pFilterModel)


    # on model column change, update the model column of the filter and completer as well
    def setModelColumn(self, column):
        self.completer.setCompletionColumn(column)
        self.pFilterModel.setFilterKeyColumn(column)
        super(ExtendedComboBox, self).setModelColumn(column)    

def change_option(text):
    print(text)

if __name__ == "__main__":
    import sys
    from PySide2.QtWidgets import QApplication
    from PySide2.QtCore import QStringListModel

    app = QApplication(sys.argv)

    string_list = ['hola muchachos', 'adios amigos', 'hello world', 'good bye']

    combo = ExtendedComboBox()

    # either fill the standard model of the combobox
    combo.addItems(string_list)
    combo.currentIndexChanged[str].connect(change_option)
    # or use another model
    #combo.setModel(QStringListModel(string_list))

    combo.resize(300, 40)
    combo.show()

    sys.exit(app.exec_())

You'll notice if you run the code and start typing "hello" into the text box, then click on the suggested "hello world", the activated signal returns the correct "hello world". While if you start typing "hello" but this time use the arrow keys to scroll down to "hello world", it will emit three times.

I've tried multiple implementations of this same idea all with the same result. I've even noticed similar behavior with unmodified QComboBox after swapping out the model with a new one.

PySide2 5.6.0a1 Windows 10.0.18362 Build 18362

Thanks for taking a look!

Upvotes: 1

Views: 762

Answers (2)

Adam Thompson
Adam Thompson

Reputation: 53

I was using PySide2 5.6.0a1 because that's the one Anaconda installs in Python 2.7 environments. @eyllanesc pointed out this is an early and outdated version and probably buggy.

When I tried the same code in a Python 3.7 environment with PySide2-5.13.1 everything worked as expected.

Upvotes: 2

Dennis Jensen
Dennis Jensen

Reputation: 224

I do not have PySide2 but for the most part I think all you have to do is replace my PyQt5 reference with PySide2 -- as that is about all I did to get you program switched from PySide2 to PyQt5 --- that along with a bit of restructuring and fine-tuning which gave me the following functioning bit of code:

from sys import exit as sysExit

from PyQt5.QtCore import Qt, QSortFilterProxyModel, QStringListModel, pyqtSlot
from PyQt5.QtWidgets import QApplication, QWidget, QCompleter, QComboBox, QCompleter, QHBoxLayout

class ExtendedComboBox(QComboBox):
    def __init__(self):
        QComboBox.__init__(self)

        self.setFocusPolicy(Qt.StrongFocus)
        self.setEditable(True)

        # add a filter model to filter matching items
        self.pFilterModel = QSortFilterProxyModel(self)
        self.pFilterModel.setFilterCaseSensitivity(Qt.CaseInsensitive)
        self.pFilterModel.setSourceModel(self.model())

        # add a completer, which uses the filter model
        self.completer = QCompleter(self)
        self.completer.setModel(self.pFilterModel)

        # always show all (filtered) completions
        self.completer.setCompletionMode(QCompleter.UnfilteredPopupCompletion)
        self.setCompleter(self.completer)

        # connect signals
        self.lineEdit().textEdited.connect(self.pFilterModel.setFilterFixedString)
        self.completer.activated.connect(self.on_completer_activated)

    # on selection of an item from the completer, select the corresponding item from combobox 
    def on_completer_activated(self, text):
        if text:
            index = self.findText(text)
            self.setCurrentIndex(index)
            # self.activated.emit(self.itemText(index))


    # on model change, update the models of the filter and completer as well 
    def setModel(self, model):
        self.setModel(model)
        self.pFilterModel.setSourceModel(model)
        self.completer.setModel(self.pFilterModel)


    # on model column change, update the model column of the filter and completer as well
    def setModelColumn(self, column):
        self.completer.setCompletionColumn(column)
        self.pFilterModel.setFilterKeyColumn(column)
        self.setModelColumn(column)    

class MainApp(QWidget):
    def __init__(self):
        QWidget.__init__(self)

        string_list = ['hola muchachos', 'adios amigos', 'hello world', 'good bye']

        self.combo = ExtendedComboBox()

        # either fill the standard model of the combobox
        self.combo.addItems(string_list)
        self.combo.currentIndexChanged[str].connect(self.change_option)
        # or use another model
        #combo.setModel(QStringListModel(string_list))

        self.resize(300, 100)
        self.combo.resize(300, 50)

        HBox = QHBoxLayout()
        HBox.addWidget(self.combo)

        self.setLayout(HBox)

    @pyqtSlot(str)
    def change_option(self, text):
        print(text)

if __name__ == "__main__":
    MainThred = QApplication([])

    MainGui = MainApp()
    MainGui.show()

    sysExit(MainThred.exec_())

I think the issue was you were trying to use Signals/Slots with a non-QObject function (aka) your change_option function is not associated directly with anything that inherits from a QObject so I am not sure what it was or was not doing but that is only a guess as all I did was put into a normal Qt structure and it worked just fine

Upvotes: 1

Related Questions