Mega Code Archive

 
Categories / Delphi / Examples
 

Undo Redo using Commands

Title: Undo Redo using Commands Question: There are 2 ways to do undo - redo, one is with state, the other is using commands. This artical explains using commands and provides full source code implementation of a TUndoRedoManager Answer: (See Also Using State) ($2.00 EMail assistance for usage and implementation + example application, go to http://www.eggcentric.com/UndoRedoCommands.htm) This article will cover 1.Command 2.Requirements of a command 3.Command Stack 4.Undo redo manager 5.Command grouping 6.Full source code implementation A command is simply an object that implements an action in the system, for example in a paint program a command may be a line command, or a circle command, or a rectangle command, and so on. In order to implement command based undo redo you must design your editing to use command objects. Because we want to undo and redo the effects of commands, the commands themselves must be able to undo and redo their own action as well as execute the initial action. The primary methods of a command is Execute Undo Redo You may wonder why there is a seprate Redo instead of simply reusing the Execute method. This is because the redo implementation may be different than the Execute. For example, if this were a paint command. The Execute may choose the brush and follow some algorithm to draw some sort of gradual transparent circle. The redo could simply copy a image of the results of the paint rather than painting again. In any case, if this functionality is not needed then simply call the Execute method from within your Redo method. Ok, so now we have one command. We need to remember the sequence of commands so we can have multilevel undo and redo. This is the command stack. When you undo, you take the last command and call its undo method. The next time you undo, you call the undo method of the 2nd command from the top and so on. When you redo, you call the redo method of the last command that you called undo on. To simplify this we create 2 lists, an undo list and a redo list and encapsulate these with an undo manager. For the undoredo manager, we give it 3 methods. ExecuteCommand(Command) Undo Redo Internally the UndoRedoManager will maintain 2 lists of commands, Undo and Redo Here is the full sequence: 1.Execute a command by passing it to the ExecuteCommand method, internally the UndoRedoManager will call the Execute method of the command and then add the command to the top of the Undo list. 2.Calling undo, the manager will take the last command in the undo list, call its undo method and then remove the command from the undo list and add it to the redo list. 3.Calling redo will do the reverse of undo, it will take the last command from the redo list, call its redo method, then remove it from the redo list and add it to the top of the undo list 4.Now, the next time ExecuteCommand is called, we must prune the redo list... delete all commands in it. Sometimes, or most of the time, you will execute a bunch of commands as a single group. Calling undo and redo should undo and redo this entire group and not the individual commands within it one at a time. An example might be some wizard that did a lot of things, you would want to undo and redo this as one group. I'll add 2 methods to the UndoRedoManager BeginTransaction EndTransaction All commands executed between calls to BeginTransaction and EndTransaction will be stored as one group. You should be allowed to make nested calls to BeginTransaction and EndTransaction. Using inheritence, this can be easy to implement. We make a command group class that inherits from the Command, that way the manager acts as if it is working with single commands. Below is the Full source code of a working UndoRedoManager along with interfaces for IUndoRedoCommand and IUndoRedoCommandGroup. Note: I think a lot of people associate delphi interfaces with ActiveX or COM and then think that interfaces ARE ActiveX or COM. This is not true, you can create classes that implement interfaces and those classes do not have any implementation of ActiveX or COM. They do not require registering and all the things that go with COM or ActiveX. You should keep in mind that interfaces are reference counted, they are freed when there are not more references. Source CODE: unit UndoRedoCommand; interface uses Classes, SysUtils; type IUndoRedoCommand = interface (IUnknown) ['{D84BFD00-8396-11D6-B4FA-000021D960D4}'] procedure Execute; procedure Redo; procedure Undo; end; IUndoRedoCommandGroup = interface (IUndoRedoCommand) ['{9169AE00-839B-11D6-B4FA-000021D960D4}'] function GetUndoRedoCommands: TInterfaceList; property UndoRedoCommands: TInterfaceList read GetUndoRedoCommands; end; TUndoRedoCommandGroup = class (TInterfacedObject, IUndoRedoCommandGroup, IUndoRedoCommand) private FList: TInterfaceList; FCanRedo: Boolean; public constructor Create; destructor Destroy; override; procedure Execute; function GetUndoRedoCommands: TInterfaceList; procedure Redo; procedure Undo; property UndoRedoCommands: TInterfaceList read GetUndoRedoCommands; end; TUndoRedoManager = class (TObject) private FRedoList: TInterfaceList; FUndoList: TInterfaceList; FTransactLevel: Integer; FTransaction: IUndoRedoCommandGroup; function GetCanRedo: Integer; function GetCanUndo: Integer; public constructor Create; destructor Destroy; override; procedure BeginTransaction; procedure EndTransaction; procedure ExecCommand(const AUndoRedoCommand: IUndoRedoCommand); procedure Redo(RedoCount: Integer = 1); procedure Undo(UndoCount: Integer = 1); property CanRedo: Integer read GetCanRedo; property CanUndo: Integer read GetCanUndo; end; implementation { **************************** TUndoRedoCommandGroup ***************************** } constructor TUndoRedoCommandGroup.Create; begin inherited Create; FList:= TInterfaceList.Create; end; destructor TUndoRedoCommandGroup.Destroy; begin FList.Free; inherited Destroy; end; procedure TUndoRedoCommandGroup.Execute; var I: Integer; begin for I:= 0 to FList.Count-1 do (FList[I] as IUndoRedoCommand).Execute; end; function TUndoRedoCommandGroup.GetUndoRedoCommands: TInterfaceList; begin Result:= FList; end; procedure TUndoRedoCommandGroup.Redo; var I: Integer; begin if FCanRedo then begin for I:= 0 to FList.Count-1 do (FList[I] as IUndoRedoCommand).Redo; FCanRedo:= False; end else raise Exception.Create('Must call TUndoRedoCommandGroup.Undo before calling Redo.'); end; procedure TUndoRedoCommandGroup.Undo; var I: Integer; begin if FCanRedo then raise Exception.Create('TUndoRedoCommandGroup.Undo already called'); for I:= FList.Count-1 downto 0 do (FList[I] as IUndoRedoCommand).Undo; FCanRedo:= True; end; { ******************************* TUndoRedoManager ******************************* } constructor TUndoRedoManager.Create; begin inherited Create; FRedoList:= TInterfaceList.Create; FUndoList:= TInterfaceList.Create; end; destructor TUndoRedoManager.Destroy; begin FRedoList.Free; FUndoList.Free; inherited Destroy; end; procedure TUndoRedoManager.BeginTransaction; begin Inc(FTransactLevel); if FTransactLevel = 1 then FTransaction:= TUndoRedoCommandGroup.Create; end; procedure TUndoRedoManager.EndTransaction; begin Dec(FTransactLevel); if (FTransactLevel = 0) then begin if FTransaction.UndoRedoCommands.Count 0 then begin FRedoList.Clear; FUndoList.Add(FTransaction); end; FTransaction:= nil; end else if FTransactLevel raise Exception.Create('Unmatched TUndoRedoManager.BeginTransaction and EndTransaction'); end; procedure TUndoRedoManager.ExecCommand(const AUndoRedoCommand: IUndoRedoCommand); begin BeginTransaction; try FTransaction.UndoRedoCommands.Add(AUndoRedoCommand); AUndoRedoCommand.Execute; finally EndTransaction; end; end; function TUndoRedoManager.GetCanRedo: Integer; begin Result:= FRedoList.Count; end; function TUndoRedoManager.GetCanUndo: Integer; begin Result:= FUndoList.Count; end; procedure TUndoRedoManager.Redo(RedoCount: Integer = 1); var I: Integer; Item: IUndoRedoCommand; RedoLast: Integer; begin if FTransactLevel 0 then raise Exception.Create('Cannot Redo while in Transaction'); // Index of last redo item RedoLast:= FRedoList.Count - RedoCount; if RedoLast RedoLast:= 0; for I:= FRedoList.Count-1 downto RedoLast do begin Item:= FRedoList[I] as IUndoRedoCommand; FRedoList.Delete(I); FUndoList.Add(Item); Item.Redo; end; end; procedure TUndoRedoManager.Undo(UndoCount: Integer = 1); var I: Integer; Item: IUndoRedoCommand; UndoLast: Integer; begin if FTransactLevel 0 then raise Exception.Create('Cannot undo while in Transaction'); // Index of last undo item UndoLast:= FUndoList.Count - UndoCount; if UndoLast UndoLast:= 0; for I:= FUndoList.Count-1 downto UndoLast do begin Item:= FUndoList[I] as IUndoRedoCommand; FUndoList.Delete(I); FRedoList.Add(Item); Item.Undo; end; end; end.