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()
To implement the features, there are a few things I modified and added into your established code
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 onekeyPressEvent()
method to delegate the controls (move_up()
, move_down()
, confirm()
, cancel()
) to different keyboard presseskeyPressEvent()
successfully filtered, I had to disable focus completely to the combobox/list widget and cancel button. I hope this is okay with youactivated
/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()