using System; using System.Collections.Generic; using Microsoft.Xna.Framework.Graphics; using Kensei.Dev; namespace Kensei { /// /// An interface for objects used in the Command Pattern, which represents functions /// as objects; allowing them to separate the caller from the callee, allowing them /// to potentially be queued, and allowing a chain of infinite Undo/Redo (provided /// by the CommandStack class). /// abstract public class ICommand { /// /// Execute the command (do, or redo). The parameters of the command may need /// to be saved, so that an Unexecute (undo) can be carried out later. /// true if the command executed successfully and should be added /// to the command stack, or false if it did not, and should not be. /// abstract public bool Execute(); /// /// Unexecute the command (undo). Will probably rely on state that was earlier /// saved as part of the Execute. /// abstract public void Unexecute(); /// /// Indicates whether this command can be undone (usually, this will be a class- /// wide value). If not the command stack will get cleared when this is added /// to the stack, as there's no point saving Undo information that can never be /// called. It is the calling application's responsibility to inform the user /// that an action is not undoable, and confirm they wish to go ahead with it. /// NOTE this should not be set to true in the case where undoing has no effect /// (though such a Command probably isn't really a Command at all), only in the /// case where an Undo is impossible - either due to not being implemented yet, /// or because it simply doesn't make any sense in the problem domain. /// /// Whether the command can be undone. abstract public bool CanBeUndone(); /// /// Attempts to merge this command with one that immediately follows it on the /// command stack. For example: Command A adds +10 to a value. Command B adds /// +20 to the same value. A->Merge( B ) modifies A to add +30 to the value, /// and returns true, indicating that B has been merged and can be released. /// /// The command to merge into this one. /// Whether the commands were successfully merged. abstract public bool Merge( ICommand command ); /// /// ToString. Derived commands will likely want to override this further, with /// type- and instance-specific details. /// /// A string representation of the object. override public string ToString() { return "Command Pattern function object."; } } /// /// A Command containing a list of child Commands, allowing many Commands to be /// issued while appearing to be a single, atomic Command. This serves partly /// as an example of how the ICommand interface is intended to be used. /// sealed public class MacroCommand : ICommand { // Contains all the sub-commands that this macro command comprises. private List m_commands = new List(); /// /// Executes each child Command in turn. /// public sealed override bool Execute() { for ( int i = 0; i < m_commands.Count; ++i ) { m_commands[i].Execute(); } return m_commands.Count > 0; } /// /// Unexecutes each child Command, in reverse order. /// public sealed override void Unexecute() { if ( CanBeUndone() ) { for ( int i = m_commands.Count - 1; i >= 0; --i ) { m_commands[i].Unexecute(); } } } /// /// MacroCommands can be undone iff each child command can be undone. /// /// Whether the operation can be undone. public sealed override bool CanBeUndone() { for ( int i = 0; i < m_commands.Count; ++i ) { if ( !m_commands[i].CanBeUndone() ) { return false; } } return true; } /// /// Attempts to merge with another command. /// /// The command to merge into this one. /// Always false - MacroCommands are meant to be atomic so /// it wouldn't make much sense to merge them, and would be tricky. public sealed override bool Merge( ICommand command ) { return false; } /// /// Provides a representation of the object as a string. /// /// The string. public sealed override string ToString() { string asString = "Macro Command:\n"; for ( int i = 0; i < m_commands.Count; ++i ) { asString += "+ " + m_commands[i].ToString() + "\n"; } return asString; } /// /// Parameterised constructor for the macro command. /// /// The commands to execute in the macro. public MacroCommand( IEnumerable commands ) { m_commands.AddRange( commands ); } /// /// Default constructor. You'll need to use AddCommand to add commands to the macro. /// public MacroCommand() { } /// /// Adds a command to the macro. /// /// The command to add to the macro. public void AddCommand( ICommand command ) { m_commands.Add( command ); } } /// /// An implementation of a stack used in the Command Pattern, allowing commands /// to be issued, and supporting infinite levels of Undo and Redo. In XNA terms, /// this is most likely to be used to massively increase usability of game editor /// tools; most games probably won't need anything like this within the game itself. /// public class CommandStack { /// /// Update function, only really used for debug stuff /// public void Update() { if ( Kensei.Dev.Options.GetOption( "CommandStack.ShowUndoRedo" ) ) { Kensei.Dev.DevText.Print( "Undo Stack Count: " + m_undoCommands.Count.ToString() + ", Redo Stack Count: " + m_redoCommands.Count.ToString(), Color.Cyan ); Kensei.Dev.DevText.Print( "Last Save At: " + m_undoCommandCountAtLastSave.ToString() + " " + ( IsDirty ? "(dirty)" : "(not dirty)" ), Color.Cyan ); if ( m_undoCommands.Count > 0 ) { Kensei.Dev.DevText.Print( "Next Undo: " + m_undoCommands.Peek().ToString(), Color.Cyan ); } if ( m_redoCommands.Count > 0 ) { Kensei.Dev.DevText.Print( "Next Redo: " + m_redoCommands.Peek().ToString(), Color.Cyan ); } } } /// /// Indicates whether calling Undo is valid (though if it's called even if /// it's not valid, it will simply do nothing). /// public bool CanUndo { get { return m_undoCommands.Count > 0; } } /// /// Undoes the previously executed command on the stack. /// public void Undo() { if ( CanUndo ) { ICommand command = m_undoCommands.Pop(); command.Unexecute(); m_redoCommands.Push( command ); } } /// /// Indicates whether calling Redo is valid (though if it's called even if /// it's not valid, it will simply do nothing). /// public bool CanRedo { get { return m_redoCommands.Count > 0; } } /// /// Redoes the previously undone command on the stack. /// public void Redo() { if ( CanRedo ) { ICommand command = m_redoCommands.Pop(); command.Execute(); m_undoCommands.Push( command ); } } /// /// Indicates whether, if the program were closed now, changes since the last /// save would be lost. Programs may wish to display a * after the document /// or window title to indicate this, just like Visual Studio does, and it /// can also be used for a "do you wish to save changes?" check on exit. /// public bool IsDirty { get { return m_undoCommandCountAtLastSave != m_undoCommands.Count; } } /// /// Inform the system that progress has been saved, so the command stack is /// no longer dirty. /// public void ProgressSaved() { m_undoCommandCountAtLastSave = m_undoCommands.Count; } /// /// Adds the command to the top of the command stack (which may involve losing /// Redo or even potentially Undo information) and issues the command. /// /// The command to add to the stack and execute public void AddCommand( ICommand command ) { if ( command.Execute() ) { if ( !command.CanBeUndone() ) { // This command cannot be undone, so clear the stack - we won't need it any more m_undoCommands.Clear(); m_redoCommands.Clear(); m_undoCommandCountAtLastSave = -1; // We can't undo to get to the last saved state // No point pushing the command on the stack - we can't undo it! } else { // We can't redo anything having just done something m_redoCommands.Clear(); if ( m_undoCommandCountAtLastSave > m_undoCommands.Count ) { m_undoCommandCountAtLastSave = -1; // We can't undo to get to the last saved state } // Attempt to merge the command with the command on top of the stack. (For example, in // Visual Studio, you can type 'a' 'b' 'c' each of which is a separate command, but when // you press Undo it will clear the whole of "abc"). if ( m_undoCommands.Count == 0 || !m_undoCommands.Peek().Merge( command ) ) { m_undoCommands.Push( command ); } } } } #region Member Data private Stack m_undoCommands = new Stack(); private Stack m_redoCommands = new Stack(); private int m_undoCommandCountAtLastSave; #endregion // IDEA: support saving the command stack. This can be useful for investigating // bugs in your editor: in the event of a crash, save off the command stack, then // on a development machine load it in from the last saved data. If it crashes in // the same place you know it's a reproducible problem (and can fix it easily). } }