Among the problems we’ll face is that we want asynchronous APIs that are undoable and that we want to switch to read only, undoable editing, non-undoable editing and that QML doesn’t really work well with QFuture. At least not yet. We want an interface that is easy to talk with from QML. Yet we want to switch between complicated behaviors.
We will also want synchronous mode and asynchronous mode. Because I just invented that requirement out of thin air.
Ok, first the “design”. We see a lot of behaviors, for something that can do something. The behaviors will perform for that something, the actions it can do. That is the strategy design pattern, then. It’s the one about ducks and wing fly behavior and rocket propelled fly behavior and the ostrich that has a can’t fly behavior. For undo and redo, we have the command pattern. We have this neat thing in Qt for that. We’ll use it. We don’t reinvent the wheel. Reinventing the wheel is stupid.
Let’s create the duck. I mean, the thing-editor as I will use “Thing” for the thing that is being edited. We want copy (sync is sufficient), paste (must be aysnc), and edit (must be async). We could also have insert and delete, but those APIs would be just like edit. Paste is usually similar to insert, of course. Except that it can be a combined delete and insert when overwriting content. The command pattern allows you to make such combinations. Not the purpose of this example, though.
Enough explanation. Let’s start! The ThingEditor, is like the flying Duck in strategy. This is going to be more or less the API that we will present to the QML world. It could be your ViewModel, for example (ie. you could let your ThingViewModel subclass ThingEditor).
class ThingEditor : public QObject { Q_OBJECT Q_PROPERTY ( ThingEditingBehavior* editingBehavior READ editingBehavior WRITE setEditingBehavior NOTIFY editingBehaviorChanged ) Q_PROPERTY ( Thing* thing READ thing WRITE setThing NOTIFY thingChanged ) public: explicit ThingEditor( QSharedPointer<Thing> &a_thing, ThingEditingBehavior *a_editBehavior, QObject *a_parent = nullptr ); explicit ThingEditor( QObject *a_parent = nullptr ); Thing* thing() const { return m_thing.data(); } virtual void setThing( QSharedPointer<Thing> &a_thing ); virtual void setThing( Thing *a_thing ); ThingEditingBehavior* editingBehavior() const { return m_editingBehavior.data(); } virtual void setEditingBehavior ( ThingEditingBehavior *a_editingBehavior ); Q_INVOKABLE virtual void copyCurrentToClipboard ( ); Q_INVOKABLE virtual void editCurrentAsync( const QString &a_value ); Q_INVOKABLE virtual void pasteCurrentFromClipboardAsync( ); signals: void editingBehaviorChanged (); void thingChanged(); void editCurrentFinished( EditCurrentCommand *a_command ); void pasteCurrentFromClipboardFinished( EditCurrentCommand *a_command ); private slots: void onEditCurrentFinished(); void onPasteCurrentFromClipboardFinished(); private: QScopedPointer<ThingEditingBehavior> m_editingBehavior; QSharedPointer<Thing> m_thing; QList<QFutureWatcher<EditCurrentCommand*> *> m_editCurrentFutureWatchers; QList<QFutureWatcher<EditCurrentCommand*> *> m_pasteCurrentFromClipboardFutureWatchers; };
For the implementation of this class, I’ll only provide the non-obvious pieces. I’m sure you can do that setThing, setEditingBehavior and the constructor yourself. I’m also providing it only once, and also only for the EditCurrentCommand. The one about paste is going to be exactly the same.
void ThingEditor::copyCurrentToClipboard ( ) { m_editingBehavior->copyCurrentToClipboard( ); } void ThingEditor::onEditCurrentFinished( ) { QFutureWatcher<EditCurrentCommand*> *resultWatcher = static_cast<QFutureWatcher<EditCurrentCommand*>*> ( sender() ); emit editCurrentFinished ( resultWatcher->result() ); if (m_editCurrentFutureWatchers.contains( resultWatcher )) { m_editCurrentFutureWatchers.removeAll( resultWatcher ); } delete resultWatcher; } void ThingEditor::editCurrentAsync( const QString &a_value ) { QFutureWatcher<EditCurrentCommand*> *resultWatcher = new QFutureWatcher<EditCurrentCommand*>(); connect ( resultWatcher, &QFutureWatcher<EditCurrentCommand*>::finished, this, &ThingEditor::onEditCurrentFinished, Qt::QueuedConnection ); resultWatcher->setFuture ( m_editingBehavior->editCurrentAsync( a_value ) ); m_editCurrentFutureWatchers.append ( resultWatcher ); }
For QUndo we’ll need a QUndoCommand. For each undoable action we indeed need to make such a command. You could add more state and pass it to the constructor. It’s common, for example, to pass Thing, or the ThingEditor or the behavior (this is why I used QSharedPointer for those: as long as your command lives in the stack, you’ll need it to hold a reference to that state).
class EditCurrentCommand: public QUndoCommand { public: explicit EditCurrentCommand( const QString &a_value, QUndoCommand *a_parent = nullptr ) : QUndoCommand ( a_parent ) , m_value ( a_value ) { } void redo() Q_DECL_OVERRIDE { // Perform action goes here } void undo() Q_DECL_OVERRIDE { // Undo what got performed goes here } private: const QString &m_value; };
You can (and probably should) also make this one abstract (and/or a so called pure interface), as you’ll usually want many implementations of this one (one for every kind of editing behavior). Note that it leaks the QUndoCommand instances unless you handle them (ie. storing them in a QUndoStack). That in itself is a good reason to keep it abstract.
class ThingEditingBehavior : public QObject { Q_OBJECT Q_PROPERTY ( ThingEditor* editor READ editor WRITE setEditor NOTIFY editorChanged ) Q_PROPERTY ( Thing* thing READ thing NOTIFY thingChanged ) public: explicit ThingEditingBehavior( ThingEditor *a_editor, QObject *a_parent = nullptr ) : QObject ( a_parent ) , m_editor ( a_editor ) { } explicit ThingEditingBehavior( QObject *a_parent = nullptr ) : QObject ( a_parent ) { } ThingEditor* editor() const { return m_editor.data(); } virtual void setEditor( ThingEditor *a_editor ); Thing* thing() const; virtual void copyCurrentToClipboard ( ); virtual QFuture<EditCurrentCommand*> editCurrentAsync( const QString &a_value, bool a_exec = true ); virtual QFuture<EditCurrentCommand*> pasteCurrentFromClipboardAsync( bool a_exec = true ); protected: virtual EditCurrentCommand* editCurrentSync( const QString &a_value, bool a_exec = true ); virtual EditCurrentCommand* pasteCurrentFromClipboardSync( bool a_exec = true ); signals: void editorChanged(); void thingChanged(); private: QPointer<ThingEditor> m_editor; bool m_synchronous = true; };
That setEditor, the constructor, etc: these are too obvious to write here. Here are the non-obvious ones:
void ThingEditingBehavior::copyToClipboard ( ) { } EditCurrentCommand* ThingEditingBehavior::editCurrentSync( const QString &a_value, bool a_exec ) { EditCurrentCommand *ret = new EditCurrentCommand ( a_value ); if ( a_exec ) ret->redo(); return ret; } QFuture<EditCurrentCommand*> ThingEditingBehavior::editCurrentAsync( const QString &a_value, bool a_exec ) { QFuture<EditCurrentCommand*> resultFuture = QtConcurrent::run( QThreadPool::globalInstance(), this, &ThingEditingBehavior::editCurrentSync, a_value, a_exec ); if (m_synchronous) resultFuture.waitForFinished(); return resultFuture; }
And now we can make the whole thing undoable by making a undoable editing behavior. I’ll leave a non-undoable editing behavior as an exercise to the reader (ie. just perform redo() on the QUndoCommand, don’t store it in the QUndoStack and immediately delete or cmd->deleteLater() the instance).
Note that if m_synchronous is false, that (all access to) m_undoStack, and the undo and redo methods of your QUndoCommands, must be (made) thread-safe. The thread-safety is not the purpose of this example, though.
class UndoableThingEditingBehavior : public ThingEditingBehavior { Q_OBJECT public: explicit UndoableThingEditingBehavior( ThingEditor *a_editor, QObject *a_parent = nullptr ); protected: EditCellCommand* editCurrentSync( const QString &a_value, bool a_exec = true ) Q_DECL_OVERRIDE; EditCurrentCommand* pasteCurrentFromClipboardSync( bool a_exec = true ) Q_DECL_OVERRIDE; private: QScopedPointer<QUndoStack> m_undoStack; }; EditCellCommand* UndoableThingEditingBehavior::editCurrentSync( const QString &a_value, bool a_exec ) { Q_UNUSED(a_exec) EditCellCommand *undoable = ThingEditingBehavior::editCurrentSync( a_value, false ); m_undoStack->push( undoable ); return undoable; } EditCellCommand* UndoableThingEditingBehavior::pasteCurrentFromClipboardSync( bool a_exec ) { Q_UNUSED(a_exec) EditCellCommand *undoable = ThingEditingBehavior::pasteCurrentFromClipboardSync( false ); m_undoStack->push( undoable ); return undoable; }