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).
}
}