How can I access the QUndoStack of a QTextDocument?
Asked Answered
F

3

7

How can I access the QUndoStack of a QTextDocument?

(For example, I want to be able to add custom QUndoCommand objects to the document's undo stack)

Fleuron answered 7/6, 2010 at 21:28 Comment(0)
M
4

I have been reading the documentation and it doesn't seems to be a way to get the QUndoStack directly for the Widget.

Probably the only way is to create your own QUndoStack object and manually add the changes and then re-implement the redo() / undo() slots. I would have a look to the source code, you can probably get most of the code you need from there to store the changes in QTextDocument.

Macnamara answered 10/6, 2010 at 16:46 Comment(0)
L
2

There is no way :(

The way I used is modifying QTextDocument Class for my needs and then recompile Gui module.

The static linking is a good choice for this purpose.

Levey answered 18/7, 2010 at 13:25 Comment(0)
R
1

PySide2 solution: no reimplementation needed!


Background and explanation (skip below for the code&instructions):

As others have said, indeed there seems to be no way to directly access the Undo stack as for May 2020. E.g. This 2017 answer by user mrjj at the Qt forum says that the stack lives inside qtextdocument_p.cpp and that there isn't a way to access it through the interfaces.

Instead, everyone suggests to implement your own undo-able commands and that it is a piece of cake, but I wasn't able to find such meaningful implementation. Also, the built-in functionality is well documented in the QTextDocument docs and at least to me it doesn't look too straightforward to just reimplement:

Undo/redo of operations performed on the document can be controlled using the setUndoRedoEnabled() function. The undo/redo system can be controlled by an editor widget through the undo() and redo() slots; the document also provides contentsChanged() , undoAvailable() , and redoAvailable() signals that inform connected editor widgets about the state of the undo/redo system. The following are the undo/redo operations of a QTextDocument :

  • Insertion or removal of characters. A sequence of insertions or removals within the same text block are regarded as a single undo/redo operation.
  • Insertion or removal of text blocks. Sequences of insertion or removals in a single operation (e.g., by selecting and then deleting text) are regarded as a single undo/redo operation.
  • Text character format changes.
  • Text block format changes.
  • Text block group format changes.

As we can see it integrates many different kinds of complex events and on the top of that it features command compressions. I personally disliked the idea of reimplementing that very much.

Ideally, we would access the stack through the API and we would be done! Hopefully this is supported at some point (please let me know in the comments if that is the case). In this answer, I show a way to integrate the built-in QTextDocument Undo stack with minimal effort and retaining all its functionality. I tried many different ways and I liked this one the best. Hope this helps!


Code & Instructions

This code exemplifies the usage with a QPlainTextEdit, but you can reproduce it with other widgets. See the docstrings for explanations:

from PySide2 import QtWidgets, QtGui, QtCore


class TextDocumentUndoWrapperCommand(QtWidgets.QUndoCommand):
    """
    This command is a wrapper that simply uses the text document stack, but
    allows to register the action on a different stack for integration.
    """

    def __init__(self, txt_editor, parent=None):
        super().__init__("Text Document Action", parent)
        self.txt_editor = txt_editor

    def undo(self):
        self.txt_editor.document().undo()

    def redo(self):
        self.txt_editor.document().redo()


class TextEditor(QtWidgets.QPlainTextEdit):
"""
QTextDocument document has a really nice built-in undo stack, but
unfortunately it cannot be accessed or integrated with other undo stacks.
This class exemplifies such integration, as follows:

1. Important: we do NOT disable undo/redo functionality. We keep it on!
2. Every time that QTextDocument adds a Command to its own stack, we add
   a wrapper command to our own main stack
3. Every time the user sends an undo/redo event, we intercept it and send
   it through our wrapper command. This way we have effectively integrated
   the built-in undo stack into our own main stack.
"""

def __init__(self, parent=None, undo_stack=None):
    """
    """
    super().__init__(parent)
    self.setLineWrapMode(self.WidgetWidth)  # matter of taste
    if undo_stack is not None:
        # if we provide a stack, integrate internal stack with it
        self.installEventFilter(self)
        self.undo_stack = undo_stack
        self.document().undoCommandAdded.connect(self.handle_undo_added)

def handle_undo_added(self, *args, **kwargs):
    """
    The key information is WHEN to create an undo command. Luckily,
    the QTextDocument provides us that information. That way, we can keep
    both undo stacks in perfect sync.
    """
    cmd = TextDocumentUndoWrapperCommand(self)
    self.undo_stack.push(cmd)

def eventFilter(self, obj, evt):
    """
    We didn't deactivate the undo functionality. We simply want to
    re-route it through our stack, which is synched with the built-in
    one.
    """
    if evt.type() == QtCore.QEvent.KeyPress:
        if evt.matches(QtGui.QKeySequence.Undo):
            self.undo_stack.undo()
            return True
        if evt.matches(QtGui.QKeySequence.Redo):
            self.undo_stack.redo()
            return True
    return super().eventFilter(obj, evt)

The TextEditor can be then simply used as a regular widget. If we don't provide a stack to the constructor, the default built-in hidden stack will be used. If we provide one, the wrapper mechanism will integrate the hidden stack into the provided one.


Note: I am not providing a solution for "just the QTextDocument" because I wasn't able to make the eventFilter work for it (I'm happy to hear about others' efforts). In any case, the QTextDocument is always inside any sort of parent widget/window, and then this logic should apply identically. There are plenty of forums asking for this functionality, and I think this was the best place to post this answer (let me know otherwise).

Reflective answered 4/5, 2021 at 15:58 Comment(2)
Note: this will not work properly if the undo/redo is triggered by the context menu. A possible work around would be to override the contextMenuEvent(), get the default menu with createStandardContextMenu(), retrieve the undo/redo actions with findChild() using their object name (edit-undo and edit-redo), disconnect their triggered signals and connect them with the related slots of the custom undo stack.Periwinkle
Dear @musicamante, thanks for the valuable remark and suggested fix! Indeed, this solution is tailored for simple apps with simple undo/redo stack architecture. In any case, feel free to use/share my code in your own improved solution if you wish to.Reflective

© 2022 - 2024 — McMap. All rights reserved.