Implementing an Undo System


In this section I give you pointers and tips on how to implement your own undo system. Because every software program is unique, I can’t simply give you code here to drop into your system while you step back and watch your new undo system run. Instead, as usual, I’ll give you some starter code that you can work with.

The best way to implement an undo system is by extending the command-based system I’ve discussed a great deal in this book. Since you’re already implementing your software using a command-based system (you are, right?), this shouldn’t be a problem. (Remember, a command-based system is nothing new; I didn’t invent it. Most people attribute its invention to the Design Patterns book (Gamma et al.)

With the command pattern, you implement a new command for each feature in your application. To add an undo capability, you add an undo to each command. For example, if you’re creating a graphics program and you have a command that applies a particular filter to an image, then you include an undo with that command that undoes the filter.

But how do you undo a filter? Well, you could try to actually run the filter in reverse, but that wouldn’t work if you lost data in the filtering process. Instead, you store the previous version of the image. Yes, that can get costly in terms of memory, but a multilevel undo is extremely important in a graphics program.

Back in Chapter 1, “The UUI: The Useable User Interface,” I provided you with a very simple way to execute commands. In that sample, each command is a function. A better and more useful way to make this work is to make each command an object instead. One way is to use templates; however, to keep this sample easy, I’ll start with a Command class that contains a run method and an undo method. From there I will derive some commands. I’ll also have a CommandList class that contains a list of the commands, an undo stack, and a redo stack. As I execute a command, I’ll push the command onto the undo stack, provided the command can be undone. (Some commands, such as Print, usually aren’t undoable.) Then when somebody issues an undo, I’ll pop the command off the undo stack, call the command’s undo method, and push the command onto the redo stack.

Quiz time: How many levels deep should I allow for the undo stack?

Answer: If you’ve read this entire chapter, you know the answer is that I shouldn’t specify a fixed level.

However, suppose you’re writing a graphics program and each level saves a copy of the image. That could eat up memory in a hurry. In that case, you’ll want to monitor how much memory is available and make a rough estimate of how much space you can allow for the undo stack. Then, make this number clear to the users in one of two ways: either by choosing the level of undo for the users and noting it, perhaps right on the undo menu item itself, or by allowing the users to choose how many undo levels they want while telling them how many levels are available. But never just hard-code a magic number, saying, for example, “Twenty undo levels is good.”

Now here’s the sample code:

 #include <iostream>  #include <stdlib.h>  #include <stack>  #include <map>  using namespace std;  // Base class for all commands  class Command {  protected:      bool undoable;  public:      bool isUndoable() { return undoable; }      virtual void run() {}      virtual void undo() {}  };  // Sample command  class ApplyFilter : public Command {  public:      ApplyFilter() { undoable = true; }      virtual void run();      virtual void undo();  };  // Sample Command  class PrintPage : public Command {  public:      PrintPage() { undoable = false; }      virtual void run();  };  void ApplyFilter::run() {      // ApplyFilter code goes here!      cout << "Running filter..." << endl;  }  void ApplyFilter::undo() {      cout << "Undoing filter..." << endl;  }  void PrintPage::run() {      // PrintPage code goes here!      cout << "Printing..." << endl;  }  typedef stack<Command *> CommandStack;  typedef map<string, Command *> NamedCommands;  class CommandList {  protected:      NamedCommands Commands;      CommandStack UndoList;      CommandStack RedoList;  public:      void AddCommand(string name, Command *cmd);      void CommandList::DoCommand(string name);      void Undo();      void Redo();      bool HaveUndo() { return UndoList.size() != 0; }      bool HaveRedo() { return RedoList.size() != 0; }  };  void CommandList::AddCommand(string name, Command *cmd) {      Commands[name] = cmd;  }  void CommandList::DoCommand(string name) {      // Do the command      Command *cmd = Commands[name];      cmd->run();      // Add the command to the undo list      if (cmd->isUndoable()) {          UndoList.push(cmd);      }  }  void CommandList::Undo() {      if (HaveUndo()) {          Command * cmd = UndoList.top();          UndoList.pop();          RedoList.push(cmd);          cout << "UNDO::";          cmd->undo();      }  }  void CommandList::Redo() {      if (HaveRedo()) {          Command * cmd = RedoList.top();          RedoList.pop();          UndoList.push(cmd);          cout << "REDO::";          cmd->run();      }  }  int main(int argc, char *argv[])  {      // Create the commands      CommandList Commands;      Commands.AddCommand("ApplyFilter", new ApplyFilter());      Commands.AddCommand("PrintPage", new PrintPage());      // Run a couple      Commands.DoCommand("ApplyFilter");      Commands.DoCommand("ApplyFilter");      Commands.DoCommand("PrintPage");      // Undo      Commands.Undo();      // Redo      Commands.Redo();      return 0;  } 

You can easily adapt this code to your own needs. Remember some pointers, however:

  • Your commands usually won’t have parameters. Instead, those commands might open a dialog box that will allow them to gather information on the parameters, or they may gather the parameters from other means.

  • A dialog box should be undone as a single unit. If the user configures a bunch of settings in a dialog box, clicks OK, and then chooses Undo, the state of your software should go back to just before the dialog box.

This second item shouldn’t be an issue when you recognize that a dialog box is opened by a command, meaning that an undo will undo the entire command, including the results of the dialog box.

For example, if a command displays a Font dialog box, and the user sets the height to 10, then to 15, then to 20, then to 25, and finally back to 15, and then clicks OK, she would intend the Font size to be set to 15. She would be a bit disconcerted, however, if choosing Undo took the height back to 25, then another Undo action took it back to 20, then 15, and finally back to 10.




Designing Highly Useable Software
Designing Highly Useable Software
ISBN: 0782143016
EAN: 2147483647
Year: 2003
Pages: 114

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net