Search code examples
pythonqcombobox

using keys to navigate a QComboBox in a QDialog


I have a QDialog with a QComboBox and a cancel button. The two options the user has is to either select an item from the QComboBox or hit the cancel button. I would like the user to have the option of navigating with keys instead of just the mouse. ESC would press the cancel button, moving the keys up and down move the selection in the QComboBox, and enter/return selects the item. This code supports either a QComboBox or a QListWidget (depending on the as_list parameter). Ultimately I would like this concept to work with both.

Here is the code:

class OptionsDialog(QDialog):
    def __init__(self, title, selected, options, as_list=False):
        super().__init__()
        self.type = title
        self.options = options
        self.ignore_keys = ignore_keys
        self.combo_box = None
        self.list_box = None
        self.selected_option = None
        self.setModal(True)

        top_layout = QVBoxLayout()
        layout = QHBoxLayout()

        if as_list:
            self.list_box = QListWidget()
            layout.addWidget(self.list_box)
            i = 0

            for option in self.options:
                self.list_box.insertItem(i, option)

                if option == selected:
                    self.list_box.setCurrentRow(i)

                i += 1

            self.list_box.setSelectionMode(QListWidget.SingleSelection)
            self.list_box.clicked.connect(self.list_box_changed)
        else:
            self.combo_box = QComboBox()
            layout.addWidget(self.combo_box)

            for option in self.options:
                self.combo_box.addItem(option)

            self.combo_box.currentIndexChanged.connect(self.combo_box_changed)
        top_layout.addLayout(layout)

        layout = QHBoxLayout()
        button = QPushButton('Cancel')
        layout.addWidget(button)
        button.clicked.connect(self.cancel_pressed)

        top_layout.addLayout(layout)
        self.setLayout(top_layout)

    def combo_box_changed(self, index):
        self.selected_option = self.combo_box.currentText()
        self.close()

    def list_box_changed(self):
        self.selected_option = self.list_box.currentItem().text()
        self.close()

    def cancel_pressed(self):
        self.selected_option = None
        self.close()

Solution

  • To implement the features, there are a few things I modified and added into your established code

    • To have the navigation and control scheme similar with both QComboBox and QListWidget, I assigned either objects to self.options_widget instead of two member variables, then narrow down into different ways to implement the controls with isinstance(self.options_widget, QComboBox) or the other one
    • I overrode the keyPressEvent() method to delegate the controls (move_up(), move_down(), confirm(), cancel()) to different keyboard presses
    • To have the keyPressEvent() successfully filtered, I had to disable focus completely to the combobox/list widget and cancel button. I hope this is okay with you
    • Finally, I used activated/itemActivated signal from QComboBox/QListWidget respectively for user mouse input, since they only send on user input as opposed to currentIndexChanged/clicked

    Here is the code I tried and tested, it uses PySide2 and Python 3.7 but it should work for other versions too

    from PySide2.QtCore import Qt
    from PySide2.QtGui import QKeyEvent
    from PySide2.QtWidgets import QDialog, QVBoxLayout, QHBoxLayout, QListWidget, \
        QComboBox, QPushButton, QApplication, QAbstractItemView
    
    
    class OptionsDialog(QDialog):
        def __init__(self, title, selected, options, as_list=False):
            super().__init__()
            self.type = title
            self.options = options
            # self.ignore_keys = ignore_keys  # idk what this does
            self.options_widget = None  # combine the combo_box and list_box into one
            # self.combo_box = None
            # self.list_box = None
            self.selected_option = None
            self.setModal(True)
    
            top_layout = QVBoxLayout()
            layout = QHBoxLayout()
    
            if as_list:
                self.options_widget = QListWidget()
                self.options_widget.setSelectionMode(QAbstractItemView.SingleSelection)
                # Single selection is important
                self.options_widget.itemActivated.connect(self.confirm)
                # itemActivated is emitted on USER double click
                i = 0
    
                for option in self.options:
                    self.options_widget.insertItem(i, option)
    
                    if option == selected:
                        self.options_widget.setCurrentRow(i)
    
                    i += 1
            else:
                self.options_widget = QComboBox()
                self.options_widget.activated.connect(self.confirm)
                # activated is emitted on USER chooses an item
    
                for option in self.options:
                    self.options_widget.addItem(option)
            self.options_widget.setFocusPolicy(Qt.NoFocus)  # IMPORTANT for keypress
            layout.addWidget(self.options_widget)
            top_layout.addLayout(layout)
    
            layout = QHBoxLayout()
            button = QPushButton('Cancel')
            button.setFocusPolicy(Qt.NoFocus)  # IMPORTANT for keypress
            layout.addWidget(button)
            button.clicked.connect(self.cancel)
    
            top_layout.addLayout(layout)
            self.setLayout(top_layout)
    
        def keyPressEvent(self, event: QKeyEvent):
            if event.key() == Qt.Key_Up:
                self.move_up()
            elif event.key() == Qt.Key_Down:
                self.move_down()
            elif event.key() == Qt.Key_Return:
                self.confirm()
            elif event.key() == Qt.Key_Escape:
                self.cancel()
    
        def move_up(self):
            w = self.options_widget
            if isinstance(w, QComboBox):
                # calculate next index
                i = w.currentIndex() - 1
                if i < 0:  # overflow index
                    i += w.count()
                w.setCurrentIndex(i)
            elif isinstance(w, QListWidget):
                i = w.currentRow() - 1
                if i < 0:  # overflow index
                    i += w.count()
                w.setCurrentRow(i)
    
        def move_down(self):
            w = self.options_widget
            if isinstance(w, QComboBox):
                # calculate next index
                i = (w.currentIndex() + 1) % w.count()  # mod resolves overflow
                w.setCurrentIndex(i)
            elif isinstance(w, QListWidget):
                i = (w.currentRow() + 1) % w.count()  # mod resolves overflow
                w.setCurrentRow(i)
    
        def confirm(self):
            w = self.options_widget
            if isinstance(w, QComboBox):
                self.selected_option = w.currentText()
            elif isinstance(w, QListWidget):
                self.selected_option = w.currentItem().text()
            self.close()
    
        def cancel(self):
            self.selected_option = None
            self.close()