Calligra Words: undo/redo framework

As C.Boemann already said, we met at my place for two days in order to fix some serious issues we had with the undo/redo framework in Calligra Words (and Stage for that matter).

The undo/redo framework is something I wrote when I first started contributing to KOffice about 3 years ago. I have to say that I was not really looking forward having to jump into this stuff again. I am not such a masochist and the memories I have of writing this are not ones of an easy glide.
It actually turned out to be really fun and gratifying. There were some headaches involved to be sure but overall I really enjoyed it.

To summarise a bit (more detailed description bellow for the hard hearted): we use Qt Scribe framework for our document. When an edition is done on a QTextDocument, it emits a signal telling that an undo/redo action was added on the QTextDocument internal undoStack. The application listens to this signal and can create an undo action on its stack to match QTextDocument's internal one.
The initial framework I created basically followed that behaviour. There was one thing that it was never meant to handle: nested commands. This means that when nesting commands, like for example the delete command now contains a deleteInlineObject command, the framework would create 2 commands on the application's stack.

So we sat with Boemann thinking how to solve that problem. In the end we only needed to add, instead of a single head command member in the framework, a stack of head commands.
Now we have a framework which is way more complete and solid, plus it is now documented in the code.
There are some improvements we could already think of to make the API a bit more flexible, but we can be confident now that we have solid foundations for the upcoming Calligra Words releases.

Overall, I had a really good time coding with C.Boemann, who is not only a very talented coder but also somebody I really appreciate. It is amazing to see what we achieved in those just two days.

A bit more details:

As I said, we use QTextDocuments to hold the data of one text frame (text shapes). This document is edited through a specific handler: a KoTextEditor. This editor is not only responsible for editing the QTextDocument but also to listen to the QTextDocument's undoCommandAdded signal and keep our application's stack in sync.
There are 3 use cases of our undo/redo framework:
- editing done within the KoTextEditor
- complete commands pushed on the KoTextEditor by an external tool
- on-the-fly macro commands

In addition to this, there are two special cases in editing QTextDocument: inserting text and deleting text. These two actions only trigger a signal on the first edit. Any subsequent compatible edit is "merged" into the original edit command and will not trigger a signal. Inserting text and deleting are therefore open ended actions, as far as our framework is concerned.

In order to handle this, a sort of state machine is used. The KoTextEditor can be in a NoOp, KeyPress, Delete, Format or Custom state. Furthermore, for each signal received from the QTextDocument, we create a "dummy" UndoTextCommand, whose sole purpose is to call QTextDocument::undo or QTextDocument::redo. These commands need to be parented to a command which we push on our application's stack. This head command will call undo or redo on all its children when the user press undo or redo in the application.
In order to allow for nested head commands, we maintain a stack. Its top-most command will be the parent of the signal induced UndoTextCommands.
Depending on the KoTextEditor state and commandStack, new head commands are pushed on the commandStack, or the current top command is popped.

I will not enter into more details here, if you are interested in the whole gory logic of this, you can look at the code in calligra/libs/kotext/KoTextEditor_undo.cpp (which for now lies in the text_undo_munichsprint git branch). The code has now been pretty well documented, something I had not done before.

That's it for today. Once again, I ask from as much of you to try our next test release, specifically the undo/redo framework, so that we can ensure that we release a really good stable Calligra Words.


Nice read. Thanks for the insight in Calligra's undo/redo framework :)

By Sebastian Sauer at Fri, 02/24/2012 - 05:32

Thanks for blogging and for the kind words which you know I reciprocate. It was really fun and rewarding work we did.

By boemann at Fri, 02/24/2012 - 08:49

Why not move away from the Scribe/QTextDocument framework completely?

Clearly it was never developed with the use-case of a full-featured high-precision word processor in mind.
The Qt developers clearly intended it only for simple text editor apps, or generic info boxes and rich text fields in normal GUI apps.

Its not just this "undo" mess which you are now forced to fix with complicated workarounds.
Remember how many years it took to fix "ugly fonts" bug, because it relied on upstream patch for Scribe framework being accepted by Nokia/Qt Project?

How many similar problems will open up in future, which you will again be forced to either fix with workarounds involving additional layers of abstraction (and therefore code duplication and performance loss) or wait for several years again for upstream patches to go into Qt?

Wouldn't it, for the long-term benefit, make sense to fork QTextDocument and make it a clean, specialized text handling framework optimized for the advanced usecases needed in Calligra?

Every other office suite on the planet has a custom, specialized text handling framework.
Do you really think Calligra can get away with relying on a simple, generic text framework provided by a general-purpose GUI-toolkit?

By jdavet at Sat, 02/25/2012 - 20:18

This is a very valid question, actually.

Well, to start with our topic here (the undo/redo framework), there is a two step answer:
- the first is the one of the short term (ie. Calligra 2.4) problem we had to solve: there was simply not enough time to go as deep as this. One might wonder, why wasn't it done before?
Well, QTextDocument's undo/redo takes care of quite a lot actually. all formatting, deletions,... At the time I first wrote KOffice's undo/redo framework (3 years back), this very question actually came naturally. But our requirements for the undo/redo framework were not as advanced back then. It just looked like a higher price to actually having to re-implement all these low level undo/redo stuff.

- now, for the future: well, it indeed seems like the framework could be much simpler if designed from the ground up with our use case in mind. However, QTextDocument's internal undo/redo is very tied up into it. I am not sure really how much it would cost us (design and then maintenance) to supersede it inside QTextDocument. That question would however probably raise again if our updated framework prove too limited or not resilient enough in the future.

For your more general remark about forking QTextDocument, and eventual future similar problems. Well, there are actually some already identified similar problems in sight. We have a problem with tabulations together with text direction, and we have a missing feature which affect the change tracking framework.
Both are actually quite big and we are evaluating how best to proceed further.
All in all, this question is actually never really far from the table.

Thanks for your interest,


By pstirnweiss at Mon, 02/27/2012 - 08:17