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 { } }