File: KoTextEditor_undo.cpp

package info (click to toggle)
calligra 1%3A25.04.2%2Bdfsg-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 309,164 kB
  • sloc: cpp: 667,890; xml: 126,105; perl: 2,724; python: 2,497; yacc: 1,817; ansic: 1,326; sh: 1,223; lex: 1,107; javascript: 495; makefile: 24
file content (340 lines) | stat: -rw-r--r-- 21,206 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
/* This file is part of the KDE project
 * SPDX-FileCopyrightText: 2009-2012 Pierre Stirnweiss <pstirnweiss@googlemail.com>
 * SPDX-FileCopyrightText: 2006-2010 Thomas Zander <zander@kde.org>
 * SPDX-FileCopyrightText: 2011 Boudewijn Rempt <boud@kogmbh.com>
 * SPDX-FileCopyrightText: 2011-2012 C. Boemann <cbo@boemann.dk>
 *
 * SPDX-License-Identifier: LGPL-2.0-or-later
 */

#include "KoTextEditor.h"
#include "KoTextEditor_p.h"

#include "KoTextDocument.h"

#include <kundo2command.h>

#include <KLocalizedString>

#include <QPointer>
#include <QTextDocument>

#include "TextDebug.h"

/** Calligra's undo/redo framework.
    The @c KoTextEditor undo/redo framework sits between the @c QTextDocument and the application's undo/redo stack.

    When the @c QTextDocument is changed by an editing action, it internally creates an undo/redo command. When doing so a signal (undoCommandAdded()) is
   emitted by the @c QTextDocument in order for applications to update their undo/redo stack accordingly. Each @c QTextDocument used in Calligra is handled by a
   specific @c KoTextEditor. It is responsible for on the one hand edit the @c QTextDocument, and on the other hand to listen for the QTextDocument's signal.

    Calligra uses a @c KUndo2Stack as its application undo/redo stack. This stack is populated by @c KUndo2Command or sub-classes of it.

    In order to limit the number of command sub-classes, KoTextEditor provides a framework which uses a generic command.

    The framework is based on a sort of state machine. The KoTextEditor can be in several different states (see KoTextEditor::Private::State).
    These are:
    NoOp: this states indicates that the KoTextEditor is not editing the QTextDocument.
    KeyPress: this state indicates that the user is typing text. All text typed in succession should correspond to one undo command. To be used when entering
   text outside of an insertTextCommand. Delete: this state indicates that the user is deleting characters. All deletions done in succession should correspond
   to one undo command. To be used for deleting outside a deleteCommand. Currently not in use, our deletion is done through a command because of inline objects.
    Format: this state indicates that we are formatting text. To be used when formatting outside of a command.
    Custom: this state indicates that the QTextDocument is changed through a KUndo2Command.

    KoTextEditor reacts differently when receiving the QTextDocument's signal, depending on its state.

    In addition the framework allows to encapsulate modifications in a on-the-fly created custom command (\sa beginEditBlock() endEditBlock()).
    Furthermore the framework allows to push complete KUndo2Commands.

    See the documentation file for how to use this framework.
*/

/*
  Important members:

  commandStack: This stack holds the headCommands. These parent the generated UndoTextCommands. When undo or redo is called, they will in turn call
  UndoTextCommand::undo/redo.

  editorState: Holds the state of the KoTextEditor. see above

  commandTitle: Holds the title which is to be used when creating a headCommand.

  addNewCommand: bool used to tell the framework to create a new headCommand and push it on the commandStack, when receiving an undoCommandAdded signal from
  QTextDocument.

  customCommandCount: counter used to keep track of nested KUndo2Commands that are pushed on the KoTextEditor.
  */

// This slot is called when the KoTextEditor receives the signal undoCommandAdded() from QTextDocument. A generic UndoTextCommand is used to match the
// QTextDocument's internal undo command. This command calls QTextDocument::undo() or QTextDocument::redo() respectively, which triggers the undo redo actions
// on the document.
// In order to allow nesting commands, we maintain a stack of commands. The top command will be the parent of our auto generated UndoTextCommands.
// Depending on the case, we might create a command which will serve as head command. This is pushed to our commandStack and eventually to the application's
// stack.
void KoTextEditor::Private::documentCommandAdded()
{
    class UndoTextCommand : public KUndo2Command
    {
    public:
        UndoTextCommand(QTextDocument *document, KoTextEditor::Private *p, KUndo2Command *parent = nullptr)
            : KUndo2Command(kundo2_i18n("Text"), parent)
            , m_document(document)
            , m_p(p)
        {
        }

        void undo() override
        {
            QTextDocument *doc = m_document.data();
            if (doc == nullptr)
                return;
            doc->undo(KoTextDocument(doc).textEditor()->cursor());
            m_p->emitTextFormatChanged();
        }

        void redo() override
        {
            QTextDocument *doc = m_document.data();
            if (doc == nullptr)
                return;
            doc->redo(KoTextDocument(doc).textEditor()->cursor());
            m_p->emitTextFormatChanged();
        }

        QPointer<QTextDocument> m_document;
        KoTextEditor::Private *m_p;
    };

    debugText << "received a QTextDocument undoCommand signal";
    debugText << "commandStack count: " << commandStack.count();
    debugText << "addCommand: " << addNewCommand;
    debugText << "editorState: " << editorState;
    if (commandStack.isEmpty()) {
        // We have an empty stack. We need a head command which is to be pushed onto our commandStack and on the application stack if there is one.
        // This command will serve as a parent for the auto-generated UndoTextCommands.
        debugText << "empty stack, will push a new headCommand on both commandStack and application's stack. title: " << commandTitle;
        commandStack.push(new KUndo2Command(commandTitle));
        if (KoTextDocument(document).undoStack()) {
            KoTextDocument(document).undoStack()->push(commandStack.top());
        }
        addNewCommand = false;
        debugText << "commandStack is now: " << commandStack.count();
    } else if (addNewCommand) {
        // We have already a headCommand on the commandStack. However we want a new child headCommand (nested commands) on the commandStack for parenting
        // further UndoTextCommands. This shouldn't be pushed on the application's stack because it is a child of the current commandStack's top.
        debugText << "we have a headCommand on the commandStack but need a new child command. we will push it only on the commandStack: " << commandTitle;
        commandStack.push(new KUndo2Command(commandTitle, commandStack.top()));
        addNewCommand = false;
        debugText << "commandStack count is now: " << commandStack.count();
    } else if ((editorState == KeyPress || editorState == Delete) && !commandStack.isEmpty() && commandStack.top()->childCount()) {
        // QTextDocument emits a signal on the first key press (or delete) and "merges" the subsequent ones, until an incompatible one is done. In which case it
        // re-emit a signal. Here we are in KeyPress (or Delete) state. The fact that the commandStack isn't empty and its top command has children means that
        // we just received such a signal. We therefore need to pop the previous headCommand (which were either key press or delete) and create a new one to
        // parent the UndoTextCommands. This command also need to be pushed on the application's stack.
        debugText << "we are in subsequent keyPress/delete state and still received a signal. we need to create a new headCommand: " << commandTitle;
        debugText << "so we pop the current one and push the new one on both the commandStack and the application's stack";
        commandStack.pop();
        commandStack.push(new KUndo2Command(commandTitle, !commandStack.isEmpty() ? commandStack.top() : 0));
        if (KoTextDocument(document).undoStack()) {
            KoTextDocument(document).undoStack()->push(commandStack.top());
        }
        debugText << "commandStack count: " << commandStack.count();
    }

    // Now we can create our UndoTextCommand which is parented to the commandStack't top headCommand.
    new UndoTextCommand(document, this, commandStack.top());
    debugText << "done creating the dummy UndoTextCommand";
}

// This method is used to update the KoTextEditor state, which will condition how the QTextDocument::undoCommandAdded signal will get handled.
void KoTextEditor::Private::updateState(KoTextEditor::Private::State newState, const KUndo2MagicString &title)
{
    debugText << "updateState from: " << editorState << " to: " << newState << " with: " << title;
    debugText << "commandStack count: " << commandStack.count();
    if (editorState == Custom && newState != NoOp) {
        // We already are in a custom state (meaning that either a KUndo2Command was pushed on us, an on-the-fly macro command was started or we are executing a
        // complex editing from within the KoTextEditor. In that state any update of the state different from NoOp is part of that "macro". However, updating
        // the state means that we are now wanting to have a new command for parenting the UndoTextCommand generated after the signal from QTextDocument. This
        // is to ensure that undo/redo actions are done in the proper order. Setting addNewCommand will ensure that we create such a child headCommand on the
        // commandStack. This command will not be pushed on the application's stack.
        debugText << "we are already in a custom state. a new state, which is not NoOp is part of the macro we are doing. we need however to create a new "
                     "command on the commandStack to parent a signal induced UndoTextCommand";
        addNewCommand = true;
        if (!title.isEmpty())
            commandTitle = title;
        else
            commandTitle = kundo2_i18n("Text");
        debugText << "returning now. commandStack is not modified at this stage";
        return;
    }
    if (newState == NoOp && !commandStack.isEmpty()) {
        // Calling updateState to NoOp when the commandStack isn't empty means that the current headCommand on the commandStack is finished. Further
        // UndoTextCommands do not belong to it. So we pop it. If after popping the headCommand we still have some commands on the commandStack means we have
        // not finished with the highest "macro". In that case we need to stay in the "Custom" state. On the contrary, an empty commandStack means we have
        // finished with the "macro". In that case, we set the editor to NoOp state. A signal from the QTextDocument should also generate a new headCommand.
        debugText << "we are in a macro and update the state to NoOp. this means that the command on top of the commandStack is finished. we should pop it";
        debugText << "commandStack count before: " << commandStack.count();
        commandStack.pop();
        debugText << "commandStack count after: " << commandStack.count();
        if (commandStack.isEmpty()) {
            debugText << "we have no more commands on the commandStack. the macro is complete. next signal induced command will need to be parented to a new "
                         "headCommand. Also the editor should go to NoOp";
            addNewCommand = true;
            editorState = NoOp;
        }
        debugText << "returning now. commandStack count: " << commandStack.count();
        return;
    }
    if (editorState != newState || commandTitle != title) {
        // We are not in "Custom" state but either are moving to a new state (from editing to format,...) or the command type is the same, but not the command
        // itself (like format:bold, format:italic). The later case is caracterised by a different command title. When we change command, we need to pop the
        // current commandStack's top and ask for a new headCommand to be created.
        debugText << "we are not in a custom state but change the command";
        debugText << "commandStack count: " << commandStack.count();
        if (!commandStack.isEmpty()) {
            debugText << "the commandStack is not empty. however the command on it is not a macro. so we pop it and ask to recreate a new one: " << title;
            commandStack.pop();
            addNewCommand = true;
        }
    }
    editorState = newState;
    if (!title.isEmpty())
        commandTitle = title;
    else
        commandTitle = kundo2_i18n("Text");
    debugText << "returning now. commandStack count: " << commandStack.count();
}

/// This method is used to push a complete KUndo2Command on the KoTextEditor. This command will be pushed on the application's stack if needed. The logic allows
/// to push several commands which are then going to be nested, provided these children are pushed from within the redo method of their parent.
void KoTextEditor::addCommand(KUndo2Command *command)
{
    debugText << "we receive a command to add on the stack.";
    debugText << "commandStack count: " << d->commandStack.count();
    debugText << "customCommandCount counter: " << d->customCommandCount << " will increase";

    // We increase the customCommandCount counter to inform the framework that we are having a further KUndo2Command and update the KoTextEditor's state to
    // Custom. However, this update will request a new headCommand to be pushed on the commandStack. This is what we want for internal complex editions but not
    // in this case. Indeed, it must be the KUndo2Command which will parent the UndoTextCommands. Therefore we set the addNewCommand back to false. If the
    // commandStack is empty, we are the highest "macro" command and we should therefore push the KUndo2Command on the application's stack. On the contrary, if
    // the commandStack is not empty, or the pushed command has a parent, it means that we are adding a nested KUndo2Command. In which case we just want to put
    // it on the commandStack to parent UndoTextCommands. We need to call the redo method manually though.
    ++d->customCommandCount;
    debugText << "we will now go to custom state";
    d->updateState(KoTextEditor::Private::Custom, (!command->text().isEmpty()) ? command->text() : kundo2_i18n("Text"));
    debugText << "but will set the addCommand to false. we don't want a new headCommand";
    d->addNewCommand = false;
    debugText << "commandStack count is: " << d->commandStack.count();
    if (d->commandStack.isEmpty()) {
        debugText << "the commandStack is empty. this means we are the top most command";
        d->commandStack.push(command);
        debugText << "command pushed on the commandStack. count: " << d->commandStack.count();
        KUndo2QStack *stack = KoTextDocument(d->document).undoStack();
        if (stack && !command->hasParent()) {
            debugText << "we have an application stack and the command is not a sub command of a non text command (which have been pushed outside kotext";
            stack->push(command);
            debugText << "so we pushed it on the application's' stack";
        } else {
            debugText << "we either have no application's stack, or our command is actually the child of a non kotext command";
            command->redo();
            debugText << "still called redo on it";
        }
    } else {
        debugText << "the commandStack is not empty, our command is actually nested in another kotext command. we don't push on the application stack but only "
                     "on the commandStack";
        d->commandStack.push(command);
        debugText << "commandStack count after push: " << d->commandStack.count();
        command->redo();
        debugText << "called redo still";
    }

    // When we reach that point, the command has been executed. We first need to clean up all the automatically generated headCommand on our commandStack, which
    // could potentially have been created during the editing. When we reach our pushed command, the commandStack is clean. We can then call a state update to
    // NoOp and decrease the customCommandCount counter.
    debugText << "the command has been executed. we need to clean up the commandStack of the auto generated headCommands";
    debugText << "before cleaning. commandStack count: " << d->commandStack.count();
    while (d->commandStack.top() != command) {
        d->commandStack.pop();
    }
    debugText << "after cleaning. commandStack count: " << d->commandStack.count() << " will set NoOp";
    d->updateState(KoTextEditor::Private::NoOp);
    debugText << "after NoOp set. inCustomCounter: " << d->customCommandCount << " will decrease and return";
    --d->customCommandCount;
}

/// DO NOT USE THIS. It stays here for compiling reasons. But it will severely break everything. Again: DO NOT USE THIS.
void KoTextEditor::instantlyExecuteCommand(KUndo2Command *command)
{
    d->updateState(KoTextEditor::Private::Custom, (!command->text().isEmpty()) ? command->text() : kundo2_i18n("Text"));
    command->redo();
    // instant replay done let's not keep it dangling
    if (!command->hasParent()) {
        d->updateState(KoTextEditor::Private::NoOp);
    }
}

/// This method is used to start an on-the-fly macro command. Use KoTextEditor::endEditBlock to stop it.
/// ***
/// Important note:
/// ***
/// The framework does not allow to push a complete KUndo2Command (through KoTextEditor::addCommand) from within an EditBlock. Doing so will lead in the best
/// case to several undo/redo commands on the application's stack instead of one, in the worst case to an out of sync application's stack.
/// ***
KUndo2Command *KoTextEditor::beginEditBlock(const KUndo2MagicString &title)
{
    debugText << "beginEditBlock";
    debugText << "commandStack count: " << d->commandStack.count();
    debugText << "customCommandCount counter: " << d->customCommandCount;
    if (!d->customCommandCount) {
        // We are not in a custom macro command. So we first need to update the KoTextEditor's state to Custom. Additionally, if the commandStack is empty, we
        // need to create a master headCommand for our macro and push it on the stack.
        debugText << "we are not in a custom command. will update state to custom";
        d->updateState(KoTextEditor::Private::Custom, title);
        debugText << "commandStack count: " << d->commandStack.count();
        if (d->commandStack.isEmpty()) {
            debugText << "the commandStack is empty. we need a dummy headCommand both on the commandStack and on the application's stack";
            KUndo2Command *command = new KUndo2Command(title);
            d->commandStack.push(command);
            ++d->customCommandCount;
            d->dummyMacroAdded = true; // This bool is used to tell endEditBlock that we have created a master headCommand.
            KUndo2QStack *stack = KoTextDocument(d->document).undoStack();
            if (stack) {
                stack->push(command);
            } else {
                command->redo();
            }
            debugText << "done adding the headCommand. commandStack count: " << d->commandStack.count() << " inCommand counter: " << d->customCommandCount;
        }
    }
    // QTextDocument sends the undoCommandAdded signal at the end of the QTextCursor edit block. Since we want our master headCommand to parent the signal
    // induced UndoTextCommands, we should not call QTextCursor::beginEditBlock for the headCommand.
    if (!(d->dummyMacroAdded && d->customCommandCount == 1)) {
        debugText << "we did not add a dummy command, or we are further down nesting. call beginEditBlock on the caret to nest the QTextDoc changes";
        // we don't call beginEditBlock for the first headCommand because we want the signals to be sent before we finished our command.
        d->caret.beginEditBlock();
    }
    debugText << "will return top od commandStack";
    return (d->commandStack.isEmpty()) ? 0 : d->commandStack.top();
}

void KoTextEditor::endEditBlock()
{
    debugText << "endEditBlock";
    // Only the self created master headCommand (see beginEditBlock) is left on the commandStack, we need to decrease the customCommandCount counter that we
    // increased on creation. If we are not yet at this master headCommand, we can call QTextCursor::endEditBlock
    if (d->dummyMacroAdded && d->customCommandCount == 1) {
        debugText << "only the created dummy headCommand from beginEditBlock is left. we need to decrease further the nesting counter";
        // we don't call caret.endEditBlock because we did not begin a block for the first headCommand
        --d->customCommandCount;
        d->dummyMacroAdded = false;
    } else {
        debugText << "we are not at our top dummy headCommand. call caret.endEditBlock";
        d->caret.endEditBlock();
    }
    if (!d->customCommandCount) {
        // We have now finished completely the macro, set the editor state to NoOp then.
        debugText << "we have finished completely the macro, set the state to NoOp now. commandStack count: " << d->commandStack.count();
        d->updateState(KoTextEditor::Private::NoOp);
        debugText << "done setting the state. editorState: " << d->editorState << " commandStack count: " << d->commandStack.count();
    }
}