using System;
using System.Diagnostics;
namespace Netron.Diagramming.Core {
///
/// UndoManager is a concrete class that maintains the undo list
/// and redo stack data structures. It also provides methods that
/// tell you whether there is something to undo or redo. The class
/// is designed to be used directly in undo/redo menu item handlers,
/// and undo/redo menu item state update functions.
///
public class UndoManager : IUndoSupport {
///
/// Occurs when the undo/redo history has changed.
///
public event EventHandler OnHistoryChange;
#region Fields
///
/// the max level to keep history
///
private int undoLevel;
///
/// the undo list
///
private UndoCollection undoList;
///
/// the internal stack of redoable operations
///
private StackBase redoStack;
#endregion
#region Constructor
///
/// Constructor which initializes the manager with up to 8 levels
/// of undo/redo.
///
/// the undo level
public UndoManager(int level) {
undoLevel = level;
undoList = new UndoCollection();
redoStack = new StackBase();
// The following events are linked in the sense that when an item
// is popped from the stack it'll be put in the undo collection
// again. So, strictly speaking, you need only two of the four
// events to make the surface aware of the history changes, but
// we'll keep it as is. It depends on your own perception of the
// undo/reo process.
redoStack.OnItemPopped +=
new EventHandler>(
HistoryChanged);
redoStack.OnItemPushed +=
new EventHandler>(
HistoryChanged);
undoList.OnItemAdded +=
new EventHandler>(
HistoryChanged);
undoList.OnItemRemoved +=
new EventHandler>(
HistoryChanged);
}
private void HistoryChanged(object sender, CollectionEventArgs e) {
RaiseHistoryChange();
}
#endregion
#region Properties
///
/// Property for the maximum undo level.
///
public int MaxUndoLevel {
get {
return undoLevel;
}
set {
Debug.Assert(value >= 0);
// To keep things simple, if you change the undo level,
// we clear all outstanding undo/redo commands.
if (value != undoLevel) {
ClearUndoRedo();
undoLevel = value;
}
}
}
///
/// Gets the undo list.
///
/// The undo list.
internal UndoCollection UndoList {
get { return undoList; }
}
#endregion
#region Methods
///
/// Raises the OnHistoryChange event
///
private void RaiseHistoryChange() {
if (OnHistoryChange != null)
OnHistoryChange(this, EventArgs.Empty);
}
///
/// Register a new undo command. Use this method after your
/// application has performed an operation/command that is
/// undoable.
///
/// New command to add to the manager.
public void AddUndoCommand(ICommand cmd) {
Debug.Assert(cmd != null);
Debug.Assert(undoList.Count <= undoLevel);
if (undoLevel == 0)
return;
CommandInfo info = null;
if (undoList.Count == undoLevel) {
// Remove the oldest entry from the undo list to make room.
info = (CommandInfo)undoList[0];
undoList.RemoveAt(0);
}
// Insert the new undoable command into the undo list.
if (info == null)
info = new CommandInfo();
info.Command = cmd;
info.Handler = null;
undoList.Add(info);
// Clear the redo stack.
ClearRedo();
}
///
/// Register a new undo command along with an undo handler. The
/// undo handler is used to perform the actual undo or redo
/// operation later when requested.
///
/// New command to add to the manager.
/// Undo handler to perform the actual undo/redo operation.
public void AddUndoCommand(ICommand cmd, IUndoSupport undoHandler) {
AddUndoCommand(cmd);
if (undoList.Count > 0) {
CommandInfo info = (CommandInfo)undoList[undoList.Count - 1];
Debug.Assert(info != null);
info.Handler = undoHandler;
}
}
///
/// Clear the internal undo/redo data structures. Use this method
/// when your application performs an operation that cannot be undone.
/// For example, when the user "saves" or "commits" all the changes in
/// the application.
///
public void ClearUndoRedo() {
ClearUndo();
ClearRedo();
}
///
/// Check if there is something to undo. Use this method to decide
/// whether your application's "Undo" menu item should be enabled
/// or disabled.
///
/// Returns true if there is something to undo, false otherwise.
public bool CanUndo() {
return undoList.Count > 0;
}
///
/// Check if there is something to redo. Use this method to decide
/// whether your application's "Redo" menu item should be enabled
/// or disabled.
///
/// Returns true if there is something to redo, false otherwise.
public bool CanRedo() {
return redoStack.Count > 0;
}
///
/// Perform the undo operation. If an undo handler is specified, it
/// will be used to perform the actual operation. Otherwise, the command
/// instance is asked to perform the undo.
///
public void Undo() {
if (!CanUndo())
return;
// Remove newest entry from the undo list.
CommandInfo info = (CommandInfo)undoList[undoList.Count - 1];
undoList.RemoveAt(undoList.Count - 1);
// Perform the undo.
Debug.Assert(info.Command != null);
info.Command.Undo();
// Now the command is available for redo. Push it onto
// the redo stack.
redoStack.Push(info);
}
///
/// Perform the redo operation. If an undo handler is specified, it
/// will be used to perform the actual operation. Otherwise, the command
/// instance is asked to perform the redo.
///
public void Redo() {
if (!CanRedo())
return;
// Remove newest entry from the redo stack.
CommandInfo info = (CommandInfo)redoStack.Pop();
// Perform the redo.
Debug.Assert(info.Command != null);
info.Command.Redo();
// Now the command is available for undo again. Put it back
// into the undo list.
undoList.Add(info);
}
///
/// Get the text value of the next undo command. Use this method
/// to update the Text property of your "Undo" menu item if
/// desired. For example, the text value for a command might be
/// "Draw Circle". This allows you to change your menu item Text
/// property to "Undo Draw Circle".
///
/// Text value of the next undo command.
public string UndoText {
get {
ICommand cmd = NextUndoCommand;
if (cmd == null)
return "";
return cmd.Text;
}
}
///
///
/// Get the text value of the next redo command. Use this method
/// to update the Text property of your "Redo" menu item if desired.
/// For example, the text value for a command might be "Draw Line".
/// This allows you to change your menu item text to "Redo Draw Line".
///
///
/// The redo text.
/// Text value of the next redo command.
public string RedoText {
get {
ICommand cmd = NextRedoCommand;
if (cmd == null)
return "";
return cmd.Text;
}
}
///
/// Get the next (or newest) undo command. This is like a "Peek"
/// method. It does not remove the command from the undo list.
///
/// The next undo command.
public ICommand NextUndoCommand {
get {
if (undoList.Count == 0)
return null;
CommandInfo info = (CommandInfo)undoList[undoList.Count - 1];
return info.Command;
}
}
///
/// Get the next redo command. This is like a "Peek"
/// method. It does not remove the command from the redo stack.
///
/// The next redo command.
public ICommand NextRedoCommand {
get {
if (redoStack.Count == 0)
return null;
CommandInfo info = (CommandInfo)redoStack.Peek();
return info.Command;
}
}
///
/// Retrieve all of the undo commands. Useful for debugging,
/// to analyze the contents of the undo list.
///
/// Array of commands for undo.
public ICommand[] GetUndoCommands() {
if (undoList.Count == 0)
return null;
ICommand[] cmdList = new ICommand[undoList.Count];
object[] objList = undoList.ToArray();
for (int i = 0; i < objList.Length; i++) {
CommandInfo info = (CommandInfo)objList[i];
cmdList[i] = info.Command;
}
return cmdList;
}
///
/// Retrieve all of the redo commands. Useful for debugging,
/// to analyze the contents of the redo stack.
///
/// Array of commands for redo.
public ICommand[] GetRedoCommands() {
if (redoStack.Count == 0)
return null;
ICommand[] cmdList = new ICommand[redoStack.Count];
object[] objList = redoStack.ToArray();
for (int i = 0; i < objList.Length; i++) {
CommandInfo info = (CommandInfo)objList[i];
cmdList[i] = info.Command;
}
return cmdList;
}
///
/// Clear the contents of the undo list.
///
private void ClearUndo() {
while (undoList.Count > 0) {
CommandInfo info = (CommandInfo)undoList[undoList.Count - 1];
undoList.RemoveAt(undoList.Count - 1);
info.Command = null;
info.Handler = null;
}
}
///
/// Clear the contents of the redo stack.
///
private void ClearRedo() {
while (redoStack.Count > 0) {
CommandInfo info = (CommandInfo)redoStack.Pop();
info.Command = null;
info.Handler = null;
}
}
#endregion
}
class UndoCollection : CollectionBase {
}
}