The Turtle^W Indexes move

Last week a patch I submitted to Qt was merged into master, which will eventually be Qt4.6.

The patch is interesting, I think, as it takes a lot of workload away from model and proxy model implementors. The TL;DR version is: It’s now easier to move rows and columns in your QAbstractItemModel and QAbstractProxyModel implementations. Instead of figuring out how each persistent index should be updated between layout{,AboutToBe}Changed signals, you just call beginMoveRows(…) and endMoveRows(). This causes rowsAboutToBeMoved and rowsMoved signals to be emitted by the model.

Since January or so I’ve been implementing a complex generic model for representing a tree of pim data from Akonadi, and several proxy models which when used together make it possible to implement efficent, portable applications using the Model/View framework. In the Model/View pattern, a single model may have several views observing or changing it. The model has no mechanism for knowing which views are attached to it, so it must broadcast to any observing views any time some state in the model changes. That’s why there are protected methods in in QAbstractItemModel like beginInsertRows and endInsertRows. The model notifies observers that it is about to change, and that it has completed the change. That way, consistency is always maintained, and the views can update their visualization of the model.

There are also existing methods for notifing about rowsAboutToBeRemoved and rowsRemoved etc, but there are no methods for signalling that rows are to be *moved* in the model. One way to solve that problem would be to remove the rows from one position, and add them to another position, however, that would mean that any information about which items are expanded or selected in a view would be invalidated. Also, this is a slow method of moving things. Compare using the mv command to move a directory containing gigabytes of data, versus using the cp command followed by rm. The mv command is far faster because it’s essentially a metadata change on the filesystem. Similarly, when implementing models, if an index is removed, all of its child indexes must also be removed, and then reinserted (possibly re-fetched if the model works asynchronously). Remove + Insert is obviously not an acceptable situation.

To solve this issue, the model emits layoutAboutToBeChanged and layoutChanged signals before and after moving, and in between, the model implementer must assure that all QPersistentModelIndexes are updated to their new positions. This ensures that selection is not broken, and that child indexes of moved indexes are not affected by the move. Implementing model moves can be tricky because you have to make sure that moving an item to one of its own descendants can not happen for example. In addition, the layout{,AboutToBe}Changed signals don’t convey any information about which rows or columns are being moved. That means that any proxy models in the application would have to take persistent model indexes of every single item in its source model when layoutAboutToBeChanged is emitted, and then when layoutChanged is emitted, compare the stored persistent indexes against the indexes currently in the model, and then update its own persistent indexes so observing proxies and views stay up to date. In applications a with a very large model and several proxies it would be a large performance hit for each proxy to take persistent indexes like that each time an item is moved, particularly in applications such as KMail, where it is not unusual to have mail folders containing tens of thousands of mails, and on platforms with less powerful hardware such as mobile devices.

So we have development complexity in implementing moves in the root model, and a performance hit in proxies. To solve this for Akonadi, I initially created an AbstractItemModel class and an AbstractProxyModel class containing new API for emitting move notifications. Then, instead of calling layout{,AboutToBe}Changed, and futzing around with QPersistentModelIndexes, I’d just call beginMoveRows(source, startRow, endRow, destination, destinationRow) and endMoveRows(). I was able to unit test it so I knew it worked, and I didn’t have to worry about writing buggy implementation code in the multiple proxies I was writing.

Eventually, though I decided that having those extra abstract classes was the wrong approach, and could easily be broken by accident in the future (it uses a mysterious and ugly reinterpret_cast hack). So after meeting Trolls Olivier and Marius at GCDS, I instead rewrote a better patch and better unit tests for QAbstractItemModel itself and sent it upstream to Qt for everyone to use.

The system works.

Leave a Reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: