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>();
}
}