using System; using System.Collections.Generic; using System.Linq; namespace Topten.RichTextKit.Utils { /// /// Implements an manager for undo operations /// /// A context object type (eg: document type) public class UndoManager { /// /// Constructs a new undo manager /// /// The document context object public UndoManager(T context) { _maxUnits = 100; _context = context; } /// /// Execute an undo unit and add it to the manager /// /// The undo unit to execute public void Do(UndoUnit unit) { // Only if not blocked if (IsBlocked) throw new InvalidOperationException("Attempt to execute undo operation while blocked"); // Fire start if (CurrentGroup == null) OnStartOperation(); // Remember if was modified bool wasModified = IsModified; try { // Do it unit.Do(_context); Add(unit); } finally { // End operation if not in a group if (CurrentGroup == null) OnEndOperation(); // Fire modified changed if (wasModified != IsModified) OnModifiedChanged(); } } /// /// Undoes the last performed operation /// public void Undo() { // Check if can if (!CanUndo) return; // Remember if was modified bool wasModified = IsModified; // Fire start op OnStartOperation(); // Seal the currently open item Seal(); // Undo Block(); _units[_currentPos - 1].Undo(_context); Unblock(); // Update position _currentPos--; // End operation OnEndOperation(); // Fire modified event if (wasModified != IsModified) OnModifiedChanged(); } /// /// Redoes previously undone operations /// public void Redo() { // Check if can if (!CanRedo) return; // Remember if modified bool wasModified = IsModified; // Fire start events OnStartOperation(); // Seal the last item Seal(); // Undo Block(); _units[_currentPos].Redo(_context); Unblock(); // Update position _currentPos++; // Fire end events OnEndOperation(); // Fire modified event if (wasModified != IsModified) OnModifiedChanged(); } /// /// Stars a group operation /// /// A user readable description of the operation /// An IDisposable that when disposed will close the group public IDisposable OpenGroup(string description) { return OpenGroup(new UndoGroup(description)); } /// /// Stars a group operation /// /// The UndoGroup to be used /// An IDisposable that when disposed will close the group public IDisposable OpenGroup(UndoGroup group) { if (IsBlocked) throw new InvalidOperationException("Attempt to add undo group while blocked"); // First group? if (_openGroups.Count == 0) OnStartOperation(); // Notified it's open group.OnOpen(_context); // Add to stack _openGroups.Push(group); // Seal the last item Seal(); // Return a disposable if (_groupDisposer == null) _groupDisposer = new GroupDisposer(this); return _groupDisposer; } /// /// Ends the current group operation /// public void CloseGroup() { if (IsBlocked) throw new InvalidOperationException("Attempt to end undo group while blocked"); if (CurrentGroup == null) throw new InvalidOperationException("Attempt to end unopened undo group"); // Remember the group var group = CurrentGroup; // Pop the group and add it to either the outer open // group, or the main undo stack Add(_openGroups.Pop()); // Notify closed group.OnClose(_context); // End operation if no open groups if (_openGroups.Count == 0) OnEndOperation(); } /// /// Clear and reset the undo manager /// public void Clear() { _units.Clear(); _currentPos = 0; _unmodifiedPos = -1; _openGroups.Clear(); _blockDepth = 0; } /// /// Check if can undo /// public bool CanUndo { get { return GetUndoUnit() != null; } } /// /// Check if can redo /// public bool CanRedo { get { return GetRedoUnit() != null; } } /// /// Gets the description of the next undo operation /// public string UndoDescription { get { var unit = GetUndoUnit(); if (unit == null) return null; return unit.Description; } } /// /// Gets the description of the next redo operation /// public string RedoDescription { get { var unit = GetRedoUnit(); if (unit == null) return null; return unit.Description; } } /// /// Event fired when any operation (or group of operations) starts /// public event Action StartOperation; /// /// Event fired when any operation (or group of operations) ends /// public event Action EndOperation; /// /// Fired when the modified state of the document changes /// public event Action ModifiedChanged; /// /// Checks if the document is currently modified /// public bool IsModified { get { return _unmodifiedPos != _currentPos; } } /// /// Mark the document as currently unmodified /// /// /// Typically this method would be called when the document /// is saved. /// public void MarkUnmodified() { // Remember if was modified bool wasModified = IsModified; // Mark as currently unmodified _unmodifiedPos = _currentPos; // Prevent additions to the open item Seal(); // Fire modified changed event if (wasModified) OnModifiedChanged(); } /// /// Seals the last item to prevent changes /// public void Seal() { if (_units.Count > 0) _units[_units.Count - 1].Seal(); } /// /// Get the current unsealed unit /// /// The unsealed unit if available, otherwise null public UndoUnit GetUnsealedUnit() { // Don't allow coalescing while we have open groups. if (_openGroups.Count > 0) return null; var unit = GetUndoUnit(); if (unit == null) return null; if (unit.Sealed) return null; return unit; } /// /// Retrieves the unit that would be executed on Undo /// /// An UndoUnit, or null public UndoUnit GetUndoUnit() { if (_currentPos > 0) return _units[_currentPos - 1]; else return null; } /// /// Retrieves the unit that would be executed on Redo /// /// An UndoUnit, or null public UndoUnit GetRedoUnit() { if (_currentPos < _units.Count) return _units[_currentPos]; else return null; } /// /// Notifies that an operation (or group of operations) is about to start /// protected virtual void OnStartOperation() { StartOperation?.Invoke(); } /// /// Notifies that an operation (or group of operations) has finished /// protected virtual void OnEndOperation() { EndOperation?.Invoke(); } /// /// Notifies when the modified state of the document changes /// protected virtual void OnModifiedChanged() { ModifiedChanged?.Invoke(); } /// /// Adds a unit to the undo manager without executing it /// /// The UndoUnit to add void Add(UndoUnit unit) { if (IsBlocked) throw new InvalidOperationException("Attempt to add undo operation while blocked"); if (CurrentGroup != null) { CurrentGroup.Add(unit); } else { RemoveAllRedoUnits(); _units.Add(unit); // Limit undo stack size if (_units.Count > _maxUnits) { // Update unmodified index if (_unmodifiedPos >= 0) _unmodifiedPos--; // Remove _units.RemoveAt(0); } else { _currentPos++; } } } /// /// Removes all units in the redo queue /// void RemoveAllRedoUnits() { System.Diagnostics.Debug.Assert(_openGroups.Count == 0); // If the unmodified position has been undone // we can never get back to clean position if (_unmodifiedPos > _currentPos) { _unmodifiedPos = -1; } // Remove redo units while (_currentPos < _units.Count) { _units.RemoveAt(_currentPos); } // Seal the last item Seal(); } /// /// Checks if the undo manager is currently blocked /// bool IsBlocked { get { return _blockDepth > 0; } } /// /// Blocks the undo manager /// void Block() { _blockDepth++; Seal(); } /// /// Unblocks the undo manager /// void Unblock() { if (_blockDepth == 0) throw new InvalidOperationException("Attempt to unblock already unblocked undo manager"); _blockDepth--; } /// /// Get the currently undo group /// UndoGroup CurrentGroup { get { if (_openGroups.Count > 0) return _openGroups.Peek(); else return null; } } class GroupDisposer : IDisposable { public GroupDisposer(UndoManager owner) { _owner = owner; } UndoManager _owner; public void Dispose() { _owner.CloseGroup(); } } // Private members T _context; List> _units = new List>(); Stack> _openGroups = new Stack>(); int _currentPos; int _unmodifiedPos; int _maxUnits; int _blockDepth; GroupDisposer _groupDisposer; } /// /// Base class for all undo units /// /// The document context type public abstract class UndoUnit { /// /// Constructs a new UndoUnit /// public UndoUnit() { } /// /// Constructs a new UndoUnit with a description /// /// The description of this unit public UndoUnit(string description) { _description = description; } /// /// Gets the description of this undo unit /// public virtual string Description { get { return _description; } protected set { _description = value; } } /// /// Instructs the unit to execute the "Do" operation /// /// The document context object public abstract void Do(T context); /// /// Instructs the unit to execute the "ReDo" operation /// /// /// The default implementation simply calls "Do" /// /// The document context object public virtual void Redo(T context) { Do(context); } /// /// Instructs the unit to execute the "Undo" operation /// /// The document context object public abstract void Undo(T context); /// /// Informs the unit that no subsequent coalescing operations /// will be appended to this unit /// public virtual void Seal() { _sealed = true; } /// /// Checks is this item is sealed /// public bool Sealed => _sealed; /// /// Gets or sets the group that owns this undo unit /// /// /// Will be null if the undo unit isn't within a group operation /// public UndoGroup Group { get; set; } // Private members string _description; bool _sealed; } /// /// Implements an Undo unit that groups other units /// into a single operation /// /// The document context type public class UndoGroup : UndoUnit { /// /// Constructs a new UndoGroup with a description /// /// The description public UndoGroup(string description) : base(description) { } /// /// Notifies this group that it's been opened /// /// The document context object public virtual void OnOpen(T context) { } /// /// Notifies this group that it's been closed /// /// The document context object public virtual void OnClose(T context) { } /// /// Adds a unit to this group /// /// The UndoUnit to be added public void Add(UndoUnit unit) { unit.Group = this; _units.Add(unit); } /// /// Inserts an unit to this group /// /// The position at which the unit should be inserted /// The UndoUnit to be inserted public void Insert(int position, UndoUnit unit) { unit.Group = this; _units.Insert(position, unit); } /// /// Gets the last UndoUnit in this group /// public UndoUnit LastUnit { get { if (_units.Count > 0) return _units[_units.Count - 1]; else return null; } } /// /// Get the list of units in this group /// public IReadOnlyList> Units => _units; /// /// The method on the UndoGroup class is never called by the /// UndoManager Never. See OnOpen and OnClose instead which /// are called as the group is constructed /// public override void Do(T context) { throw new NotImplementedException(); } /// public override void Redo(T context) { foreach (var u in _units) { u.Redo(context); } } /// public override void Undo(T context) { foreach (var u in _units.Reverse>()) { u.Undo(context); } } // Private members List> _units = new List>(); } }