ToddP
ToddP

Reputation: 712

How can I implement undo-redo for a QComboBox in PyQt5?

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:


To reproduce an error (using code below)


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_())

Upvotes: 1

Views: 753

Answers (1)

musicamante
musicamante

Reputation: 48260

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.

Upvotes: 3

Related Questions