Mercurial > projects > doodle
diff doodle/core/undo.d @ 47:14f1c051c35b
Moved undo into core.
author | daveb |
---|---|
date | Tue, 03 Aug 2010 16:57:06 +0930 |
parents | doodle/undo/undo.d@ad3ba55ae57b |
children | 576b9fba4677 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/doodle/core/undo.d Tue Aug 03 16:57:06 2010 +0930 @@ -0,0 +1,148 @@ +module doodle.core.undo; + +import std.array; +import std.date; + +// 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 + +abstract class Edit { + private { + string _description; + d_time _timeStamp; + } + + this(in string description, d_time timeStamp) { + assert(description); + _description = description; + _timeStamp = timeStamp; + } + + string description() const { return _description; } + d_time timeStamp() const { return _timeStamp; } + + 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(); + 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); +} + +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() { + assert(canUndo); + auto edit = _undoEdits.back; + edit.undo; + _undoEdits.popBack; + _redoEdits ~= edit; + + notifyObservers(); + } + + void redo() { + assert(canRedo); + auto edit = _redoEdits.back; + edit.redo; + _redoEdits.popBack; + _undoEdits ~= edit; + + notifyObservers(); + } + + bool canUndo() const { return !_undoEdits.empty; } + bool canRedo() const { return !_redoEdits.empty; } + + void reset() { + _undoEdits.length = _redoEdits.length = 0; + notifyObservers(); + } + + void addObserver(IUndoManagerObserver observer) { + _observers ~= observer; + } + + void removeObserver(IUndoManagerObserver observer) { + // NYI + } + + // IUndoManager overrides: + + private { + 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); + } + } + } +}