Chapter 6. ViewKit Undo Management and Command Classes

Many applications offer users the ability to reverse or “undo” various actions. This chapter describes how ViewKit provides undo support. It also describes how ViewKit supports command classes, commands implemented as classes.

Figure 6-1 shows the inheritance graph for ViewKit classes that support undo management and command classes.

Figure 6-1. Inheritance Graph for the ViewKit Classes Supporting Undo Management and Command Classes

Figure 6-1 Inheritance Graph for the ViewKit Classes Supporting Undo Management and Command Classes

Undo Management

This section describes the ViewKit undo manager, which supports reversing or “undoing” actions.

Overview of ViewKit Undo Management

The VkMenuUndoManager class is the basis of ViewKit's undo manager. The ViewKit undo manager provides an easy-to-use method for users to undo commands that they issue to your application.

The user interface to the ViewKit undo manager is a single menu item that you add to one of your application's menus. By default, the label of that menu item is “Undo: last_command”, where last_command is the name of the last command the user issued. Whenever the user issues a command, the undo manager automatically updates the menu item to reflect the latest command. To undo the command, the user simply chooses the undo manager's menu item.

By default, ViewKit's undo manager provides multi-level undo support. The undo manager keeps commands on a stack. When the user undoes a command, the undo manager pops it from the stack, revealing the previously executed command. Once a user has undone at least one command, executing any new command clears the undo stack. Also, executing any non-undoable command clears the undo stack. If you choose, you can also force the undo manager to provide only single-level undo support, where it remembers only the last command the user issued.

You can use the undo manager to support undoing any command, regardless of whether the user issues the command through a menu or through other interface methods (for example, pushbuttons). The undo manager also supports undoing command classes as implemented by the VkAction(3x) and VkMenuActionObject(3x) classes described in “Command Classes”. In most cases, all you need to provide for each command is a callback function that reverses the effects of that command.

Using ViewKit's Undo Manager

The programmatic interface to the undo manager is simple to use. Because the VkMenuUndoManager class is a subclass of VkMenuItem, you can add it to a menu and manipulate it as you would any other menu item.

To add undo support for an undoable menu item (VkMenuAction(3x) and VkMenuToggle(3x) items), simply provide an undo callback function (a function that reverses the effects of the item's action) when you either statically or dynamically define the menu item. Similarly, to add undo support for a command class (VkAction and VkMenuActionObject objects), you provide a member function to undo the effects of the command. For those action that are not implemented in your application as menu items or action classes, you can add undo callbacks directly to the undo stack.

Instantiating the ViewKit Undo Manager

Do not directly instantiate a VkMenuUndoManager object in your program. If you provide an undo callback to any menu item or if you use a subclass of VkAction or VkMenuActionObject in your program, ViewKit automatically creates an instance of VkMenuUndoManager named “Undo”. (“Command Classes” describes the VkAction and VkMenuActionObject classes.) The <Vk/VkMenuItem.h> header file provides theUndoManager, a global pointer to this instance. To access the ViewKit undo manager, simply use this global pointer.[6]

Adding the Undo Manager to a Menu

You add the undo manager to a menu just as you would any other menu item: using the VkMenu::add() function of the menu object to which you want to add the undo manager. For example, the following line adds the undo manager to a menu pane specified by the variable edit:

edit->add(theUndoManager);

You cannot include the undo manager in a static menu description; however, you can add the undo manager to a statically-defined menu after creating the menu. To specify the position of the undo manager within the menu, include a position parameter when you add the undo manager. For example, the following line adds the undo manager to the top of a menu pane specified by the variable edit:

edit->add(theUndoManager, 0);

Providing Undo Support for Actions That Are Menu Items

To add undo support for an undoable menu item (VkMenuAction and VkMenuToggle items), simply provide an undo callback function when you define the menu item. The undo callback function should reverse the effects of the item's action.

For example, the following static description describes a Cut menu item that executes the callback function cutCallback() when the user chooses the item and undoCutCallback() when the user undoes the command:

class EditWindow: public VkWindow {
  private:
    static VkMenuDesc editPane[];
    static void cutCallback(Widget, XtPointer, XtPointer);
    static void undoCutCallback(Widget, XtPointer, XtPointer);
    // ...
};
 
VkMenuDesc EditWindow::editPane[] = {
  { ACTION,   "Cut",   &EditWindow::cutCallback,
                               NULL,   NULL,   &EditWindow::undoCutCallback },
  { END }
};

You could do the same thing by adding the menu item dynamically:

class EditWindow: public VkWindow {
  private:
    static VkSubMenu *editMenu;
    static void cutCallback(Widget, XtPointer, XtPointer);
    static void undoCutCallback(Widget, XtPointer, XtPointer);
    // ...
};
 
EditWindow::EditWindow(char *name) : VkWindow(name)
{
  // ...
  editMenu->addAction("Cut", &EditWindow::cutCallback,
                      &EditWindow::undoCutCallback, this);
}

Providing Undo Support for Actions That Are Not Menu Items

Sometimes you might want to provide undo support for an action not implemented as a menu item (for example, an action invoked by a pushbutton). ViewKit allows you to do this by adding the action directly to the undo stack using VkMenuUndoManager::add():

void add(const char *name,
         XtCallbackProc undoCallback,
         XtPointer clientData)

The name argument provides a name for the action to appear in the undo manager's menu item. The undoCallback argument must be an Xt-style callback function that the undo manager can call to undo the action. The undo manager passes the clientData argument to the undo callback function as client data when it invokes the callback. Following ViewKit conventions as described in “Using Xt Callbacks With Components”, you should pass the this pointer as client data so that the callback function can retrieve the pointer, cast it to the expected component type, and call a corresponding member function.


Note: add() simply adds an action to the undo stack; it does not “register” a permanent undo callback for an action. Once the undo stack is cleared, the undo information for that action is deleted. If you later perform the action again and you want to provide undo support for that action, you must use add() again to add the action to the undo stack.

Example 6-1 shows a simple example of adding an action to the undo stack. The MyComponent constructor creates a pushbutton as part of its widget hierarchy and registers actionCallback() as the button's activation callback function. actionCallback(), in addition to performing an action, adds undoActionCallback() to the undo stack.

Example 6-1. Adding a Non-Menu Item Directly to the Undo Stack

MyComponent: public VkComponent {
 
  public:
    MyComponent(const char *, Widget);
    void actionCallback(Widget, XtPointer, XtPointer);
    void undoActionCallback(Widget, XtPointer, XtPointer);
    // ...
};
 
MyComponent::MyComponent(const char *, Widget parent)
{
  // ...
  Widget button = XmCreatePushButton(viewWidget, "button", NULL, 0);
  XtAddCallback(button, XmNactivateCallback,
                &MyWindow::actionCallback, (XtPointer) this);
  // ...
}
void MyComponent::actionCallback(Widget w, XtPointer clientData,
                                     XtPointer callData)
{
  // Perform action...
 
  theUndoManager->add("Action", &MyComponent::undoActionCallback, this);
}


Providing Undo Support for Command Class Objects

The ViewKit classes that support command classes, VkAction and VkMenuActionObject, both require you to override the pure virtual function undoit(), which the undo manager calls to undo an action implemented as a command class. “Command Classes” describes how to use VkAction and VkMenuActionObject to implement command classes.

Enabling and Disabling Multi-Level Undo Support

By default, VkMenuUndoManager provides multi-level undo support. The undo manager keeps commands on a stack. When the user undoes a command, the undo manager pops it from the stack, revealing the previously executed command. Once a user has undone at least one command, executing any new command clears the undo stack. Also, executing any undoable command clears the undo stack.

Supporting multi-level undo in your application can be difficult. If you prefer to support undoing only the last command executed, you can change the behavior of the undo manager with the VkMenuUndoManager::multiLevel() function:

void multiLevel(Boolean flag)

If flag is FALSE, the undo manager remembers only the last command executed.

Clearing the Undo Stack

You can force the undo manager to clear its command stack with the VkMenuUndoManager::reset() function:

void reset()

Examining the Undo Stack

You can examine the contents of the undo manager's command stack using VkMenuUndoManager::historyList():

VkComponentList *historyList()

historyList() returns a list of objects representing commands that have been executed and are available to be undone. Commands are listed in order of execution; the last command executed is the last item in the list. All of the objects in the list are subclasses of VkMenuItem. Commands added directly to the undo stack (as described in “Providing Undo Support for Actions That Are Not Menu Items”) or commands implemented as VkAction objects (as described in “Command Classes”) appear as VkMenuActionStub objects. VkMenuActionStub is an empty subclass of VkMenuAction.

Setting the Label of the Undo Manager Menu Item

The label that the undo manager menu item displays is of the form Undo_label:Command_label. Undo_label is the value of the labelXmNlabelString resource of the undo manager. By default, this value is “Undo”. You can change this string (for example, for a German-language app-defaults file) by providing a different value for the XmNlabelString resource. For example, you could set the resource as follows:

*Undo.labelString:    Annul

Command_label is the label for the last executed command registered with the undo manager, determined as follows:

  • For commands executed by menu items—VkMenuAction, VkMenuToggle, or VkMenuActionObject (described in “Command Classes”) objects—the label is the item's XmNlabelString resource.

  • For VkAction objects (described in “Command Classes”), the undo manager uses the object's “labelString” resource if one is defined, otherwise it uses the VkAction object's name as the label.

  • For actions that you add directly to the undo stack (described in “Providing Undo Support for Actions That Are Not Menu Items”), the undo manager uses the action name that you provided when you added the action.

Using ViewKit's Undo Manager

Example 6-2 demonstrates the use of the undo manager.

Example 6-2. Using the Undo Manager

////////////////////////////////////////////////////////////////
// Simple example to exercise Vk undo facilities
///////////////////////////////////////////////////////////////
#include <Vk/VkApp.h>
#include <Vk/VkWindow.h>
#include <Vk/VkMenu.h>
#include <Vk/VkMenuItem.h>
#include <Vk/VkSubMenu.h>
#include <stream.h>
#include <Xm/Label.h>
#include <Xm/RowColumn.h>
#include <Xm/PushB.h>
 
class MyWindow: public VkWindow {
  
  private:
  
    static void pushCallback( Widget,  XtPointer, XtPointer);
    static void undoPushCallback( Widget,  XtPointer, XtPointer);
  
    static void oneCallback( Widget,  XtPointer , XtPointer);
    static void twoCallback( Widget,  XtPointer , XtPointer);
    static void threeCallback( Widget,  XtPointer , XtPointer);
  
    static void undoOneCallback( Widget,  XtPointer , XtPointer);
    static void undoTwoCallback( Widget,  XtPointer , XtPointer);
    static void undoThreeCallback( Widget,  XtPointer , XtPointer);
  
    static void quitCallback( Widget,  XtPointer , XtPointer);
  
    void quit();
    void one();
    void two();
    void three();
    void undoOne();
  
    void undoTwo();
    void undoThree();
  
    static VkMenuDesc appMenuPane[];
    static VkMenuDesc mainMenuPane[];
  
  public:
 
    MyWindow( const char *name);
    ~MyWindow( );
    virtual const char* className();
};
MyWindow::MyWindow( const char *name) : VkWindow( name) 
{
    Widget rc =  XmCreateRowColumn(mainWindowWidget(), "rc", NULL, 0);
    Widget label =  XmCreateLabel(rc, "an undo test", NULL, 0);
    Widget pb =  XmCreatePushButton(rc, "push", NULL, 0);
 
    XtAddCallback(pb, XmNactivateCallback, &MyWindow::pushCallback,
                  (XtPointer) this);
    XtManageChild(label);
    XtManageChild(pb);
    
    setMenuBar(mainMenuPane);
    
    VkSubMenu *	editMenuPane = addMenuPane("Edit");
    
    editMenuPane->add(theUndoManager);
    
    addView(rc);
}
 
MyWindow::~MyWindow()
{
 
}
 
const char* MyWindow::className() 
{
 return "MyWindow";
}
 
// The menu bar is essentially a set of cascading menu panes, so the
// top level of the menu tree is always defined as a list of submenus
 
VkMenuDesc  MyWindow::mainMenuPane[] = {
  { SUBMENU, "Application",  NULL, MyWindow::appMenuPane},
  { END}
};
 
VkMenuDesc MyWindow::appMenuPane[] = {
  { ACTION,   "Command One",   &MyWindow::oneCallback, NULL, NULL,
                                                 &MyWindow::undoOneCallback },
  { ACTION,   "Command Two",   &MyWindow::twoCallback, NULL, NULL,
                                                 &MyWindow::undoTwoCallback },
  { ACTION,   "Command Three", &MyWindow::threeCallback, NULL, NULL,
                                                 &MyWindow::undoThreeCallback },
  { SEPARATOR },
  { CONFIRMFIRSTACTION,  "Quit",    &MyWindow::quitCallback},
  { END},
};
 
void MyWindow::one()
{
    cout << "Command One executed" <<  "\n" << flush;
}
 
void MyWindow::two()
{
    cout << "Command Two executed" <<  "\n" << flush;
}
 
void MyWindow::three()
{
    cout << "Command Three executed" <<  "\n" << flush;
}
 
void MyWindow::undoOne()
{
    cout << "Undoing Command One" <<  "\n" << flush;
}
 
void MyWindow::undoTwo()
{
    cout << "UNdoing Command Two" <<  "\n" << flush;
}
 
void MyWindow::undoThree()
{
    cout << "Undoing Command Three" <<  "\n" << flush;
}
 
void MyWindow::oneCallback( Widget,  XtPointer clientData, XtPointer)
{
    MyWindow *obj = (MyWindow *) clientData;
    obj->one();
}
void MyWindow::twoCallback( Widget,  XtPointer clientData, XtPointer)
{
    MyWindow *obj = (MyWindow *) clientData;
    obj->two();
}
void MyWindow::threeCallback( Widget,  XtPointer clientData, XtPointer)
{
    MyWindow *obj = (MyWindow *) clientData;
    obj->three();
}
 
void MyWindow::undoOneCallback( Widget,  XtPointer clientData, XtPointer)
{
    MyWindow *obj = (MyWindow *) clientData;
    obj->undoOne();
}
void MyWindow::undoTwoCallback( Widget,  XtPointer clientData, XtPointer)
{
    MyWindow *obj = (MyWindow *) clientData;
    obj->undoTwo();
}
void MyWindow::undoThreeCallback( Widget,  XtPointer clientData, XtPointer)
{
    MyWindow *obj = (MyWindow *) clientData;
    obj->undoThree();
}
 
void MyWindow::quitCallback ( Widget, XtPointer clientData, XtPointer )
{
    MyWindow *obj = (MyWindow*) clientData;
    delete obj;
}
 
void MyWindow::pushCallback( Widget,  XtPointer clientData, XtPointer)
{
    cout << "doing a push command\n" << flush;
 
    theUndoManager->add("Push", &MyWindow::undoPushCallback, (XtPointer) clientData);
}
  
  
void MyWindow::undoPushCallback( Widget,  XtPointer clientData, XtPointer)
{
    cout << "undoing the push command\n" << flush;
}
 
 
main(int argc, char **argv)
{
  VkApp     *app    = new VkApp("Menudemo",  &argc,  argv);
  MyWindow  *win  = new MyWindow("MenuWindow");
 
  win->show();
  app->run();
}


Command Classes

This section describes the VkAction class, which supports ViewKit command classes. Command classes allow you to implement actions as objects.

Overview of Command Classes

Nearly every user action in an interactive application can be thought of as a “command.” Programmers typically implement commands as functions (callback functions, for example) that are invoked as a result of some user action. This section explores an approach in which each command in a system is modelled as an object.

Representing commands as objects has many advantages. Many commands have some state or data associated with the command, while others may involve a set of related functions. In both cases, a class allows the data and functions associated with a single logical operation to be encapsulated in one place. Because command objects are complete and self-contained, you can queue them for later execution, store them in “history” lists, re-execute them, and so on. Representing commands as objects can also facilitate undoing the command. For example, to prepare to undo a command, you might need to save some state data before executing the command. When you model commands as objects, you can store this information in data members.

The VkMenuAction class (described in “Menu Actions”) implements the command class model to a certain extent in that it allows you to specify callback functions both for performing an action and undoing that action. But the VkMenuAction class does not provide a true command class in that it does not allow you to encapsulate any data or support functions the action might need within a discrete object. Furthermore, you must use the VkMenuAction class within a menu; it does not allow you to implement command classes activated by pushbuttons, text fields, or other input mechanisms.

ViewKit provides two abstract classes to implement command classes in an application: VkAction and VkMenuActionObject. VkAction supports commands that do not appear in menus and VkMenuActionObject supports commands that appear in menus. VkAction does not inherit from any other classes, whereas VkMenuActionObject is a subclass of VkMenuAction, which allows you to add instances of it to a menu and manipulate them as you would any other menu item.

You can encapsulate with a subclass of VkAction or VkMenuActionObject any data or support functions required to perform an action. Additionally, commands implemented as subclasses of VkAction and VkMenuActionObject automatically register themselves with the ViewKit undo manager whenever you execute them.

Using Command Classes in ViewKit

To use command classes in ViewKit, you must create a separate subclass for each command in your application.

Command Class Constructors

The syntax of the VkAction constructor is as follows:

VkAction(const char *name)

Each class derived from VkAction should provide a constructor that takes at least one argument: the object's name. All derived class constructors should pass the name to the VkAction constructor to initialize the basic class data members, and then initialize any subclass-specific data members.

The syntax of the VkMenuActionObject constructor is as follows:

VkMenuActionObject(const char *name, XtPointer clientData = NULL)

Each class derived from VkMenuActionObject should provide a constructor that takes two arguments: the object's name and optional client data. All derived class constructors should pass the name and the client data to the VkMenuActionObject constructor to initialize the basic class data members, and then initialize any subclass-specific data members.

The VkMenuActionObject constructor stores the client data in the protected data member _clientData:

void *_clientData

VkMenuActionObject objects do not use the _clientData data member for callback functions. Instead it is simply an untyped pointer that you can use to pass any information your command object might need. For example, you could pass a pointer to another object, a value, a string, or any other value. You can access and manipulate _clientData from member functions of your command subclass.

Overriding Virtual Functions

Both VkAction and VkMenuActionObject have two protected pure virtual functions that you must override—doit() and undoit():

virtual void doit()
virtual void undoit()

doit() performs the command class's action; undoit() undoes the action.

Using Command Classes as Menu Items

You can use command classes derived only from VkMenuActionObject in a ViewKit menu. Because VkAction is not derived from VkMenuItem, it does not provide the services required of a menu item.

You cannot specify VkMenuActionObject objects in a static menu description; you must add them dynamically using VkMenu::add(), which is described in “Functions for Dynamically Creating Menus”.

Activating Command Classes

When a user chooses a VkMenuActionObject command object from a menu, ViewKit executes the command by calling the object's doit() function. ViewKit also automatically registers the command with the undo manager.

To activate a command object that is a subclass of VkAction, call that action's execute() member function:

void execute()

execute() calls the object's doit() function. execute() also registers the command with the undo manager.


Note: Do not call a command object's doit() function directly. If you do, ViewKit cannot register the command with the undo manager.


Setting the Label Used by Command Classes

You can set the label of a VkMenuActionObject command object as you would any other VkMenuItem item: by setting the object's XmNlabelString resource or by calling the object's setLabel() function. “Setting Menu Item Labels” describes how to set the label for a menu item.

Because VkAction objects are command classes and not interface classes, they technically do not have labels; however, the undo manager requires a label that it can display after you have executed a VkAction command. Therefore, ViewKit allows you to set the value of a labelString resource for VkAction objects, qualified by the object's name. For example, if you have an instance of a VkAction named “formatPara,” you can set the label for this object by providing a value for the formatPara.labelString resource:

*formatPara:    Format Paragraph

If you do not provide a value for a VkAction object's labelString resource, the undo manager uses the object's name as the label.


Note: The VkAction labelString resource is a “synthetic” resource, not a widget resource. The only way that you can set the value of this resource is through a resource file. You can't use XtSetValues() because the object contains no widgets, and you can't use setDefaultResources() because VkAction is not a subclass of VkComponent.



[6] theUndoManager is actually implemented as a compiler macro that invokes a VkUndoManager access function to return a pointer to the unique instantiation of the VkUndoManager class. Although you should never need to use this access function directly, you might encounter it while debugging a ViewKit application that uses the undo manager.