Using the identical question asked about QListWidgets as a guide, I am trying to make a QStandardItemModel
in which I can undo the edit of an item.
As can be seen in the SSCCE below, I'm pretty much copying the example exactly, but with some minor tweaking because currentItemChanged
isn't available for QStandardItemModel
. To get around that, I'm using the clicked
signal instead to fix the previous text for an item.
Strangely, the correct description shows up in the undostack, but when I click the undo
button it doesn't actually undo anything.
Note the present question is superficially the same as this question. The answer that was accepted at that other version is less an answer, more a hint. It's a hint I am trying to implement here, but it is not working yet. Since this question is more specific and detailed, it shouldn't count as a duplicate, IMO.
SSCCE
from PySide import QtGui, QtCore
import sys
class CommandItemEdit(QtGui.QUndoCommand):
def __init__(self, model, item, textBeforeEdit, description = "Item edited"):
QtGui.QUndoCommand.__init__(self, description)
self.model = model
self.item = item
self.textBeforeEdit = textBeforeEdit
self.textAfterEdit = item.text()
def redo(self):
self.model.blockSignals(True)
self.item.setText(self.textAfterEdit)
self.model.blockSignals(False)
def undo(self):
self.model.blockSignals(True)
self.item.setText(self.textBeforeEdit)
self.model.blockSignals(False)
class UndoableTree(QtGui.QWidget):
def __init__(self, parent = None):
QtGui.QWidget.__init__(self, parent = None)
self.setAttribute(QtCore.Qt.WA_DeleteOnClose)
self.view = QtGui.QTreeView()
self.model = self.createModel()
self.view.setModel(self.model)
self.view.expandAll()
self.undoStack = QtGui.QUndoStack(self)
undoView = QtGui.QUndoView(self.undoStack)
buttonLayout = self.buttonSetup()
mainLayout = QtGui.QHBoxLayout(self)
mainLayout.addWidget(undoView)
mainLayout.addWidget(self.view)
mainLayout.addLayout(buttonLayout)
self.setLayout(mainLayout)
self.makeConnections()
#For undo/redo editing
self.textBeforeEdit = ""
def makeConnections(self):
self.view.clicked.connect(self.itemClicked)
self.model.itemChanged.connect(self.itemChanged)
self.quitButton.clicked.connect(self.close)
self.undoButton.clicked.connect(self.undoStack.undo)
self.redoButton.clicked.connect(self.undoStack.redo)
def itemClicked(self, index):
item = self.model.itemFromIndex(index)
self.textBeforeEdit = item.text()
def itemChanged(self, item):
command = CommandItemEdit(self.model, item, self.textBeforeEdit,
"Renamed '{0}' to '{1}'".format(self.textBeforeEdit, item.text()))
self.undoStack.push(command)
def buttonSetup(self):
self.undoButton = QtGui.QPushButton("Undo")
self.redoButton = QtGui.QPushButton("Redo")
self.quitButton = QtGui.QPushButton("Quit")
buttonLayout = QtGui.QVBoxLayout()
buttonLayout.addStretch()
buttonLayout.addWidget(self.undoButton)
buttonLayout.addWidget(self.redoButton)
buttonLayout.addStretch()
buttonLayout.addWidget(self.quitButton)
return buttonLayout
def createModel(self):
model = QtGui.QStandardItemModel()
model.setHorizontalHeaderLabels(['Titles', 'Summaries'])
rootItem = model.invisibleRootItem()
#First top-level row and children
item0 = [QtGui.QStandardItem('Title0'), QtGui.QStandardItem('Summary0')]
item00 = [QtGui.QStandardItem('Title00'), QtGui.QStandardItem('Summary00')]
item01 = [QtGui.QStandardItem('Title01'), QtGui.QStandardItem('Summary01')]
rootItem.appendRow(item0)
item0[0].appendRow(item00)
item0[0].appendRow(item01)
#Second top-level item and its children
item1 = [QtGui.QStandardItem('Title1'), QtGui.QStandardItem('Summary1')]
item10 = [QtGui.QStandardItem('Title10'), QtGui.QStandardItem('Summary10')]
item11 = [QtGui.QStandardItem('Title11'), QtGui.QStandardItem('Summary11')]
rootItem.appendRow(item1)
item1[0].appendRow(item10)
item1[0].appendRow(item11)
return model
def main():
app = QtGui.QApplication(sys.argv)
newTree = UndoableTree()
newTree.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()
The issue appears to arise because
blockSignals()
is preventing the treeview from being told to repaint. I think this is because the model emits a signal to the treeview when data in the model is modified, which is obviously being blocked when you callmodel.blockSignals(True)
. If you manually resize the window after clicking undo/redo (obviously only works if there is something to undo/redo), you see that the undo/redo has actually been applied, it just didn't initially show it.To work around this, I've modified the code so that instead of blocking signals, we disconnect the relevant signal and reconnect it. This allows the model and treeview to continue to communicate correctly while the undo/redo is in progress.
See the below code
Additional Information
I've discovered that you could use you original implementation of
CommandItemEdit
if you explicitly callself.model.layoutChanged.emit()
after unblocking the signals. This forces the treeview to update without resulting in a call to theUndoableTree.itemChanged()
slot.Note, the treeview is connected to the model signals, while the treeview is in turn connected to the
UndoableTree.itemChanged()
slot.I also tried emitting the
dataChanged()
signal, but this ends up ultimately calling the still connectedUndoableTree.itemChanged()
slot, which results in infinite recursion. I think this is signal is the target of the call tomodel.blockSignals()
, so it makes sense not to call it explicitly!So in the end, while one of these additional methods does work, I would still go with my first answer of explicitly disconnecting the signal. This is simply because I think it is better to leave communication between the model and treeview completely intact, rather than restrict some communication while manually triggering the signals you still want. The latter route is likely to have unintended side-effects and be a pain to debug.
To quote another answer at a closely related question :
The same post went on to suggest creating a custom signal that emits old/new data when the data are actually changed. Ultimately I used three ideas that I stole shamelessly from SO. First, three_pineapples' insight that
disconnect
is needed to avoid infinite recursion. Second, subclassQStandardItemModel
to define a newitemDataChanged
signal that sends both previous and new data to a slot. Third, subclassQStandardItem
and have it emit this signal when the data is changed: this is handled in a reimplementation ofsetData()
).Here is the full code:
Overall, this seems better than using
clicked
.