There are many questions regarding the basics of QUndoCommand
on StackOverflow, but I cannot find a minimal working example of undo/redo for a QComboBox
in PyQt5, so I am trying to create one. However, I am stuck with segmentation fault errors with the code below.
Questions:
Goal:
Ctrl
+ Z
Ctrl
+ Shift
+ Z
To reproduce an error (using code below)
Ctrl
+ Z
to undo (this correctly reverts to "a")Ctrl
+ Shift
+ Z
to redo (this produces a segmentation fault)import sys
from PyQt5 import QtWidgets, QtCore
class MyUndoCommand(QtWidgets.QUndoCommand):
def __init__(self, combobox, ind0, ind1):
super().__init__()
self.combobox = combobox
self.ind0 = ind0
self.ind1 = ind1
def redo(self):
self.combobox.setCurrentIndex( self.ind1 )
def undo(self):
self.combobox.setCurrentIndex( self.ind0 )
class MyComboBox(QtWidgets.QComboBox):
def __init__(self, *args):
super().__init__(*args)
self.addItems( ['a', 'b', 'c'] )
self.ind0 = 0
self.undostack = QtWidgets.QUndoStack()
self.currentIndexChanged.connect( self.on_index_changed )
def keyPressEvent(self, e):
z = e.key() == QtCore.Qt.Key_Z
ctrl = e.modifiers() == QtCore.Qt.ControlModifier
ctrlshift = e.modifiers() == QtCore.Qt.ControlModifier | QtCore.Qt.ShiftModifier
if ctrl and z:
if self.undostack.canUndo():
self.undostack.undo()
if ctrlshift and z:
if self.undostack.canRedo():
self.undostack.redo()
def on_index_changed(self, ind):
cmd = MyUndoCommand(self, self.ind0, ind)
self.undostack.push( cmd )
self.ind0 = ind
if __name__ == '__main__':
app = QtWidgets.QApplication( sys.argv )
widget = MyComboBox()
widget.show()
sys.exit(app.exec_())
The problem is that you connected the currentIndexChanged
signal to a function that creates an undo command no matter what, and since MyUndoCommand
does change the current index, the result is that you get a recursive call for it.
A possible solution is to create a flag that is checked whenever the index is changed and does not create a further undo command whenever that index change is triggered by another undo/redo.
class MyComboBox(QtWidgets.QComboBox):
undoActive = False
def __init__(self, *args):
super().__init__(*args)
self.addItems(['a', 'b', 'c'])
self.ind0 = 0
self.undostack = QtWidgets.QUndoStack()
self.currentIndexChanged.connect(self.on_index_changed)
def keyPressEvent(self, e):
if e.key() == QtCore.Qt.Key_Z:
ctrl = e.modifiers() == QtCore.Qt.ControlModifier
ctrlshift = e.modifiers() == QtCore.Qt.ControlModifier | QtCore.Qt.ShiftModifier
if ctrl and self.undostack.canUndo():
self.undoActive = True
self.undostack.undo()
self.undoActive = False
return
elif ctrlshift and self.undostack.canRedo():
self.undoActive = True
self.undostack.redo()
self.undoActive = False
return
super().keyPressEvent(e)
def on_index_changed(self, ind):
if not self.undoActive:
cmd = MyUndoCommand(self, self.ind0, ind)
self.undostack.push( cmd )
self.undoActive = False
self.ind0 = ind
Note that I've changed the keyPressEvent
handler in order to ensure that unhandled key events get processed, which is important for keyboard navigation and item selection.