Mercurial > projects > doodle
changeset 46:ad3ba55ae57b
Undo framework mostly complete
author | daveb |
---|---|
date | Tue, 03 Aug 2010 16:55:45 +0930 |
parents | 01bbf3f6f966 |
children | 14f1c051c35b |
files | doodle/undo/undo.d |
diffstat | 1 files changed, 121 insertions(+), 39 deletions(-) [+] |
line wrap: on
line diff
--- a/doodle/undo/undo.d Mon Aug 02 14:44:12 2010 +0930 +++ b/doodle/undo/undo.d Tue Aug 03 16:55:45 2010 +0930 @@ -1,66 +1,148 @@ module doodle.undo.undo; -// http://doc.trolltech.com/4.2/qundo.html -// http://www.codeproject.com/KB/cs/undo_support.aspx +import std.array; +import std.date; -// rename Action to Edit? +// An abstract framework for undo/redo. +// Assume the application works on one document at a time, +// therefore a single undo/redo history. +// Each change to the document is modelled as an Edit. +// Previous edits are represented by an undo stack +// and can be undone in order. +// As edits are undone the are placed on a redo stack +// and can be redone in order. +// As edits are redone the are placed on the undo stack, etc. +// +// When a new edit is made an attempt to merge it with +// the previous Edit is made. For example, typing characters +// in short succession generates many Edits that may be +// merged into one Edit. +// When a new edit is made the redo stack is cleared. +// +// Application code must generate Edits from user interaction. +// Typically application code will receive some input event and: +// * Attempt to perform an action, +// * If the action succeeds then encapsulate the action +// in an Edit and add it to the undo manager. +// Note, not all interaction results in Edits, for example, +// changing the view, zooming/scrolling, etc are not edits +// and do not affect undo/redo -// Related design patterns: Command, Memento +abstract class Edit { + private { + string _description; + d_time _timeStamp; + } + + this(in string description, d_time timeStamp) { + assert(description); + _description = description; + _timeStamp = timeStamp; + } -// Action represents and encapsulates the information needed -// Consider command merging and time-stamping of commands + string description() const { return _description; } + d_time timeStamp() const { return _timeStamp; } -abstract class Action { + final bool merge(Edit subsequent) { + if (mergeImpl(subsequent)) { + // Adopt the new timestamp and description + _timeStamp = subsequent._timeStamp; + _description = subsequent._description; + return true; + } + else { + return false; + } + } + void undo(); void redo(); - //string description() const; - // bool merge(Action other); - // time-stamp + protected bool mergeImpl(Edit other) { return false; } +} + +interface IUndoManagerObserver { + // Each description is null if the associated bool is false + void undoRedoUpdate(in bool canUndo, in string undoDescription, + in bool canRedo, in string redoDescription); } -final class CompoundAction { - this(in Action[] sub_actions) { - mSubActions = sub_actions.dup; +interface IUndoManager { + void addEdit(Edit edit); + void undo(); + void redo(); + void reset(); + + void addObserver(IUndoManagerObserver observer); + void removeObserver(IUndoManagerObserver observer); +} + +class UndoManager : IUndoManager { + this(int maxUndoLevel = -1) { + _maxUndoLevel = maxUndoLevel; + } + + void addEdit(Edit edit) { + _redoEdits.length = 0; + + if (_undoEdits.empty || !_undoEdits.back.merge(edit)) { + _undoEdits ~= edit; + if (_maxUndoLevel >= 0 && _undoEdits.length > _maxUndoLevel) { + _undoEdits.length = _undoEdits.length - 1; + } + } + + notifyObservers(); } void undo() { - foreach_reverse(a; mSubActions) { a.undo(); } + assert(canUndo); + auto edit = _undoEdits.back; + edit.undo; + _undoEdits.popBack; + _redoEdits ~= edit; + + notifyObservers(); } void redo() { - foreach(a; mSubActions) { a.redo(); } + assert(canRedo); + auto edit = _redoEdits.back; + edit.redo; + _redoEdits.popBack; + _undoEdits ~= edit; + + notifyObservers(); } - private { - Action[] mSubActions; - } -} - -interface IUndoManagerObserver { - void canUndo(in bool value, in string description); - void canRedo(in bool value, in string description); -} + bool canUndo() const { return !_undoEdits.empty; } + bool canRedo() const { return !_redoEdits.empty; } -interface IUndoManager { - void addAction(Action action); - void undo(); - void redo(); - // bool can_undo() const; - // bool can_redo() const; -} - -class UndoManager : IUndoManager { - this(int max_undo_level = -1) { + void reset() { + _undoEdits.length = _redoEdits.length = 0; + notifyObservers(); } - void addAction(Action action); - void undo(); - void redo(); + void addObserver(IUndoManagerObserver observer) { + _observers ~= observer; + } + + void removeObserver(IUndoManagerObserver observer) { + // NYI + } // IUndoManager overrides: private { - Action[] mUndoActions; - Action[] mRedoActions; + int _maxUndoLevel; + Edit[] _undoEdits; + Edit[] _redoEdits; + IUndoManagerObserver[] _observers; // FIXME, use a different container + + void notifyObservers() { + foreach (o; _observers) { + o.undoRedoUpdate(canUndo, canUndo ? _undoEdits.back.description : null, + canRedo, canRedo ? _redoEdits.back.description : null); + } + } } }