Krita/Transactions Design

From KDE Community Wiki

Reasoning

Why change the way things used to work? Well, we need to achieve two goals with our new transactions API:

  • We need to explicitly mark the boundaries of every transaction. We need to be able to explisitly say to the tile engine: "Yeah, here we start painting and there we stop". It is extremely useful information for the engine as at the moment the user stops doing something he starts (we hope so) thinking over his future action. This thinking time is precious for the engine, as it can use it for doing some useful routine work like preallocation of new tiles (pooler), swapping out some data and be sure that it won't disturb the user in any way.
  • We need to explicitly mark the lifetime of every saved undo command. That is, when the caller does not need undo for a particucar operation, he can simply delete it. That is what QUndoStack performs for limiting number of undo operations.

Old tile engine used to use Memento pattern for achieving this aim. Yeah, it is a good pattern, but every KisMemento had to have two hash tables at least 4KiB each for echieving versioning. And those tables should have been created every time a new transaction was requested.

The new engine doesn't incoprorate memento pattern. It has a centralized storage of the history. Thanks to this, it has three hash tables per data manager only. But this design requires undo command to report when it dies to clear the history it points to.

Implementation Details

That is why in Deventer we decided to perform refactoring for transactions use-patterns. New transactions system is split into two objects: KisTransactionData and KisTransaction.

  • KisTransactionData object presents actual command, performed during transaction. This object inherits QUndoCommand and this very object will be added into QUndoStack. The lifetime of KisTransactionData object explicitly defines the lifetime of undo information for the transaction. Therefore, when you delete this object, it reports to the data manager about its death, and the latter one free's all the history preceeding the time the dead transaction point to.
  • KisTransaction is an object representing transaction abstraction. It owns and wraps around KisTransactionData object and defines the boundaries of the transaction. When KisTransaction object dies, transaction finishes. One more reason of having this wrapper is that KisTransactionData (as you'll see below) does not conforms Qt's design of QUndoCommand, so this class completely hides this inconsistence inside [1]. It has quite an obvious outer interface:
* KisTransaction(const QString& name, KisPaintDeviceSP device, QUndoCommand* parent = 0);
* void commit(KisUndoAdapter* undoAdapter);
* void end();
* void revert();

This interface completely hides KisTransactionData object from the user and forbids access to it.

Result - Two Types of Actions

So what we've got? We have come to a new, quite complete, system for managing undo operations. It is based on two types of activities: commands and transaction. Both can be used throughout Krita freely. So what is the difference?

  • Commands. These objects are exactly what is suggested by Qt's QUndoCommand paradigm. When you want to perform an action, you create a command object, push it into the stack, the stack calls to command->redo(), and redo() function performs operations you desire.
  • Transactions are different. They do not perform any work for you. They just record your actions, while you are performing them yourself. This is the key difference between two types of operations. To help you fully understand this, take a look into KisTransactionData::redo() method. It's first invocation (the one that will be done by QUndoStack) does nothing! It is just skipped using a flag! So committing having just created transaction makes no sense.

This difference is the reason why KisTransactionData should be completely hidden inside a wrapper class.

Code Examples

Regular transaction, that is intended to go to undo stack:

const KoColorSpace * cs = KoColorSpaceRegistry::instance()->rgb8();
KisPaintDeviceSP dev = new KisPaintDevice(cs);

KisTransaction transaction("Foo Transaction", dev, 0);

/**
 * Do some useful work with 'dev'
 * dev->...
 */
transaction.commit(m_image->undoAdapter());

Some transactions should not go to the undo stack. For example, transactions, created for getting oldRawData() functionality for filters or paintops. Such transactions should be finished with end() or just forcing the destruction of transaction object:

transaction.end();
const KoColorSpace * cs = KoColorSpaceRegistry::instance()->rgb8();
KisPaintDeviceSP dev = new KisPaintDevice(cs);

{
    KisTransaction transaction("Foo Transaction", dev, 0);
    
    /**
     * Do some useful work with 'dev'
     * dev->...
     */
}
// Transaction is finished by this line

If you want to undo the transaction you've just started, simply call a revert() method:

const KoColorSpace * cs = KoColorSpaceRegistry::instance()->rgb8();
KisPaintDeviceSP dev = new KisPaintDevice(cs);

KisTransaction transaction("Bar Transaction", dev, 0);

/**
 * Do some unuseful work with 'dev'
 * dev->...
 */
transaction.revert();


Notes

  • Be careful, because transactions can not be nested! Be sure that you have finished previous transaction before requesting a new one.
  • Iterators do not support oldRawData() functionality when no transaction is in progress!

[1] - Well, there is one exception - undoCommand(), but it is done for legacy code and its future use is discouraged.