5. Interactors and Command Objects for Handling Input

Graphical objects in Amulet do not respond to input events; they are purely output. When the programmer wants to make an object respond to a user action, an Interactor object is attached to the graphical object. The built-in types of Interactors usually enable the programmer to simply choose the correct type and fill in a few parameters. The intention is to significantly reduce the amount of coding necessary to define behaviors.

When an Interactor or a widget (see the Widgets chapter) finishes its operation, it allocates a Command object and then invokes the `do' method of that Command object. Thus, the Command objects take the place of call-back procedures in other systems. The reason for having Command objects is that in addition to the `do' method, a Command object also has methods to support undo, help, and selective enabling of operations. As with Interactors, Amulet supplies a library of Command objects so that often programmers can use a Command object from the library without writing any code.

5.1 Include Files

The primary include files that control the Interactors and Command objects are inter.h for the main, top-level objects and procedures, idefs.h for the definitions specific to input events, and inter_advanced.h for what you might need if you are going to create your own custom Interactors. All the slots of the Interactor and Command objects are defined in standard_slots.h. Some of the functions and types needed to customize the text editing Interactor are defined in text_fns.h. For more information on the Amulet header files and how to use them, see Section 1.6 in the Overview chapter.

5.2 Overview of Interactors and Commands

The graphical objects created with Opal do not respond to input devices: they are just static graphics. In order to handle input from the user, you create an ``Interactor'' object and attach it to the graphics. The Interactor objects have built-in behaviors that correspond to the normal operations performed in direct manipulation user interfaces, so usually coding interactive interfaces is quick and easy using interactors. However, like programming with constraints, programming with Interactors requires a different ``mind set'' and the programming style is probably different than what most programmers are used to.

All of the Interactors are highly parameterized so that you can control many aspects of the behavior simply by setting slots of the Interactor object. For example, you can easily specify which mouse button or keyboard key starts the interactor. In order to affect the graphics and connect to application programs, each Interactor has multiple protocols. For example, the ``Move-Grow'' interactor, for moving graphical objects with the mouse, explicitly sets the Am_LEFT and Am_TOP slots of the object, and also calls the Am_DO_METHOD method stored in the Command object attached to the Interactor. Therefore, there are multiple ways to use an Interactor, to give programmers flexibility in what they need to achieve.

When an Interactor or a widget (see the Widgets chapter) finishes its operation, it allocates a Command object and then invokes the `do' method of that Command object. Thus, the Command objects take the place of call-back procedures in other systems. The reason for having Command objects is that in addition to the`do' method, a Command object also has methods to support undo, redo, selective undo and redo, help, and enabling of operations. Each Interactor and Widget has a Command object as the part named Am_COMMAND, and Interactors set the Am_VALUE and other slots in its command object, and then call the Am_DO_METHOD method. This and other methods in the Command objects implement the functionality of the Interactors.

Amulet currently supports two undo models, fully described in Section 5.5.2. The first is a simple single undo, like on the Macintosh. The second is a sophisticated new undo model which provides undo, redo (undo the undo), and selective undo and repeat of any previous command. The sections about the various Interactors discuss their default operation for undo, redo and repeat.

5.3 Standard Operation

We hope that most normal behaviors and operations will be supported by the Interactors and Command objects in the library. This section discusses how to use these. If you find that the standard operations are not sufficient, then you can override the standard methods, as described in Section 5.5.2. If you want an additional operation in addition to the regular operation, then you can just add a command object to the Interactor, as explained in Section 5.5. If neither of these is sufficient, you may need to create your own Interactor as discussed in Section 5.7.

5.3.1 Designing Behaviors

The first task when designing the interaction for your interface is to choose the desired behavior. The first choice is whether one of the built-in widgets provides the right interface. If so, then you can choose the widget from the Widgets chapter and then attach the appropriate Command object to the widget. The widgets, such as buttons, scroll bars and text-input fields, combine a standard graphical presentation with an interactive behavior. If you want custom graphics, or you want an application-specific graphical object to be moved, selected or edited with the mouse, then you will want to create your own graphics and Interactors.

The first step in programming an Interactor is to pick one of the fundamental built-in styles of behavior that is closest to the interaction you want. The current choices are (these are exported in inter.h):

5.3.2 General Interactor Operation

Once an Interactor is created and its parameters are set (see Section 5.3.3), the programmer will then attach the Interactor to some object in a window (see Section 5.3.3.2.1). Amulet then waits for the user to perform the Interactor's start event (for example by pressing the left mouse button, see Section 5.3.3.1.5) over the graphical object to which the Interactor is attached. Am_One_Shot_Interactors then immediately execute their Command's DO method and go back to waiting. Other types of Interactors, however, usually show interim feedback while waiting for a specific stop event (for example, left mouse button up). While Interactors are operating, the user might move the mouse outside the Interactor's operating area, in which case the Interactor stops working (for example, a choice Interactor used in a menu will turn off the highlighting if the mouse goes outside the menu). If the mouse goes back inside, then the Interactor resumes operation. If the abort_event is executed while an Interactor is running, then it is aborted (and the Command's DO method is not executed). Similarly, if the stop event is executed while the mouse is outside, the Interactor also aborts. The operation is summarized by the following diagram.

Multiple Interactors can be running at the same time. Each Interactor keeps track of its own status, and for each input event, Amulet checks which Interactor or Interactors are interested in the event. The appropriate Interactor(s) will then process that event and return.

5.3.3 Parameters

Once the programmer has chosen the basic behavior that is desired, then the various parameters of the specific Interactor must be filled in. The next sections give the details of these parameters. Some, such as the start and abort events, are shared by all Interactors, and other parameters, such as gridding, are specific to only a few types of Interactors.

The parameters are set as normal slots of the objects. The names of the slots are defined in standard_slots.h and are described below. As an example, the following creates a choice Interactor called `Select It' assigned to the variable select_it and sets its start event to the middle mouse button. See the ORE chapter for how to create and name objects.

select_it = Am_Choice_Interactor.Create("Select It")
              .Set(Am_START_WHEN, "MIDDLE_DOWN");

5.3.3.1 Events

One of the most important parameters for all Interactors are the input events that cause them to start, stop and abort. These are encoded as an Am_Input_Char which are defined in idefs.h. Normally, you do not have to worry about these since they are automatically created out of normal C strings, but you can convert a string into an Am_Input_Char for efficiency, or if you want to set or access specific fields.

Note: Do not use a C++ char to represent the events. It must be a C string or an Am_Input_Char object.

5.3.3.1.1 Event Slots
There are three slots of Interactors that can hold events: Am_START_WHEN, Am_ABORT_WHEN, and Am_STOP_WHEN.

Am_START_WHEN determines when the Interactor begins operating. The default value is Am_Default_Start_Char which is "LEFT_DOWN" with no modifier keys (see Section 5.3.3.1.3) but with any number of clicks (see Section 5.3.3.1.4). So by default, all interactors will operate on both single and double clicks.

Am_ABORT_WHEN allows the Interactor to be aborted by the user while it is operating. The default value is "CONTROL_g". Aborting is different from undoing since you abort an operation while it is running, but you undo an operation after it is completed. All interactors can be aborted while they are running.

Am_STOP_WHEN determines when the Interactor should stop. The default value is Am_Default_Stop_Char which is "ANY_MOUSE_UP" so even if you change the start_when, you can often leave the stop_when as the default value.

5.3.3.1.2 Event Values
In any of these slots, you can provide an Am_Input_Char, a string in the format described below, or the special values true or false. The value true matches any event, and false will never match any event. You might use false in the Am_ABORT_WHEN slot of an Interactor to make sure it is never aborted.

The general form for the events is a string with the modifiers first and the specific keyboard key or mouse button last. The specific keys include the regular keyboard keys, like "A", "z", "[", and "\"" (use the standard C++ mechanism to get special characters into the string). The various function and special keys are generally named the same thing as their label, such as "F1", "R5", "HELP", and "DELETE". Sometimes, keys have multiple markings, in which case we usually use the more specific or textual marking, or sometimes both markings will work. Also, the arrow keys are always called "LEFT_ARROW", "UP_ARROW", "DOWN_ARROW", and "RIGHT_ARROW". Note that keys with names made out of multiple words are separated by underscores. For keyboard keys, we currently only support operations on the button being pressed, and no events are generated when the button is released. You can specify any keyboard key with the special event "ANY_KEYBOARD" (see Section 5.4.1 for how to find out which key was hit). You can find out what the mapping for a keyboard key is by running the test program testinput, which is in the src/gem directory. We have tried to provide appropriate mappings for all of the keyboards we have come across, but if there are keyboard keys on your keyboard that are not mapped appropriately, then please send mail to amulet@cs.cmu.edu and we will add them to the next release.

For the mouse buttons, we support both pressing and releasing. The names of the mouse buttons are "LEFT", "MIDDLE" and "RIGHT" (on a 2-button mouse, they are "LEFT" and "RIGHT" and on a 1-button mouse, just "LEFT"), and you must append either "UP" or "DOWN". Thus, the event for the left button down is "LEFT_DOWN". You can specify any mouse button down or up using "ANY_MOUSE_DOWN" and "ANY_MOUSE_UP". On the Macintosh, you can generate the right mouse down event using OPTION-mouse down, and the middle mouse event using OPTION-SHIFT-mouse down. On the PC with a two-button mouse, there is no way to generate the middle button event.

5.3.3.1.3 Event Modifiers
The modifiers can be specified in any order and the case of the modifiers does not matter. There are long and short prefix forms for each modifier. You can use either one in strings to be converted into Am_Input_Chars. For example "CONTROL_f" and "^f" represent the same key. Note that the short form uses a hypen (it looks better in menus) and the long form uses an underscore (to be consistent with other Amulet symbols).

The currently supported modifiers are:

5.3.3.1.4 Multiple Clicks
Amulet supports the detection of multiple click events from the mouse. To double-click, the user must press down on the same mouse button quickly two times in succession. The clicks must be faster than Am_Double_Click_Time (which is defined in gem.h), which defaults to 250, and is measured in milliseconds. (On the Macintosh, Am_Double_Click_Time is ignored, and the system constant for double click time is used instead, which is set with the Mouse control panel.)

On the PC, Amulet detects single and double clicks, and on Unix and the Mac, Amulet will detect up to five clicks. The multiple clicks are named by preceding the event name with the words "DOUBLE_", "TRIPLE_", "QUAD_", and "FIVE_". For example, "double_left_down", or "shift_meta_triple_right_down". When the user double clicks, a single click event will still be generated first. For example, for the left button, the sequence of received events will be "LEFT_DOWN", "LEFT_UP", "DOUBLE_LEFT_DOWN", "DOUBLE_LEFT_UP". The "ANY_" prefix can be used to accept any number of clicks, so "ANY_LEFT_DOWN" will accept single or multiple clicks with any modifier held down.

5.3.3.1.5 Am_Input_Char type
The Am_Input_Char is defined in idefs.h. It is a regular C++ object (not an Amulet object). It has constructors from a string or from the various pieces:


Am_Input_Char (const char *s); //from a string like "META_LEFT_DOWN"
Am_Input_Char (short c = 0, bool shf = false,
			bool ctrl = false,
			bool meta = false, Am_Button_Down down = Am_NEITHER,
			Am_Click_Count click = Am_NOT_MOUSE,
			bool any_mod = false);
It can be converted to a string, to a short string, to a long (which is only useful for storing the Am_Input_Char into a slot of an object) or to a character (which returns 0 if it is not an normal ascii character). An Am_Input_Char will also print to a stream as a string. If ic is an Am_Input_Char:

The member variables of an Am_Input_Char are:

typedef enum { Am_NOT_MOUSE = 0,  //When not a mouse button.
               Am_SINGLE_CLICK = 1, //Also for mouse moved, with Am_NEITHER.
               Am_DOUBLE_CLICK = 2, Am_TRIPLE_CLICK = 3,
               Am_QUAD_CLICK = 4, Am_FIVE_CLICK = 5, Am_MANY_CLICK = 6,
               Am_ANY_CLICK = 7  // when don't care about how many clicks
              } Am_Click_Count;

typedef enum { Am_NEITHER = 0, Am_BUTTON_DOWN = 1,
               Am_BUTTON_UP = 2, Am_ANY_DOWN_UP = 3} Am_Button_Down;

short code; // the base code.
bool shift;  // whether these modifier keys were down
bool control;
bool meta;
bool any_modifier; //true if don't care about modifiers
Am_Button_Down button_down;  // whether a down or up transition.
                             // For keyboard, only support down.
Am_Click_Count click_count;  // 0==not mouse, otherwise # clicks

5.3.3.2 Graphical Objects

5.3.3.2.1 Start_Where
For an Interactor to become active, it must be added as a part to a graphical object which is part of a window. To do this, you use the regular Add_Part method of objects. For example, to make the select_it Interactor defined above in Section 5.3.3 select the object my_rect, the following code could be used:


my_rect.Add_Part(select_it);
Interactors can be added as parts to any kind of graphical object, including primitives (like rectangles and strings), groups, and windows. You can add multiple Interactors to any object, and they can be interspersed with graphical parts for groups and windows. Interactors can be removed or queried with the standard object routines for parts. If you make instances of the object to which the Interactor is attached, then an instance will be made of the Interactor as well (see the ORE chapter). For example:

Am_Slot_Key INTER_SLOT = Am_Register_Slot_Name ("INTER_SLOT");
my_rect.Add_Part(INTER_SLOT, select_it); //named part
rect2 = my_rect.Create(); //rect2 will have its own which is an instance of select_it
It is very common for a behavior to operate over the parts of a group, rather than just on the object itself. For example, a choice Interactor might choose any of the items (parts) in a menu (group), or a move_grow Interactor might move any of the objects in the graphics window. Therefore, the slot Am_START_WHERE_TEST can hold a function to determine where the mouse should be when the start-when event happens for the Interactor to start. The built-in functions for the slot (from inter.h) are as follows. Each of these returns the object over which the Interactor should start, or NULL if the mouse is in the wrong place so the Interactor should not start.

For example, the following interactor will move whichever part of the group my_group that the user clicks on. Since the interactor is also a part of the group, an instance of the interactor will be created whenever an instance is made of the group, as explained in Section 3.6.2.

Am_Slot_Key INTER_SLOT = Am_Register_Slot_Name ("INTER_SLOT");
my_group.Add_Part(INTER_SLOT, Am_Move_Grow_Interactor.Create()
				.Set(Am_START_WHERE_TEST, Am_Inter_In_Part));
my_group.Add(rect);
my_group.Add(rect2);
//now the interactor will move either rect or rect2
group2 = my_group.Create();
 //group2 will have its own interactor as well as instances of rect and rect2
If none of these functions returns the object you are interested in, then you are free to define your own function. It should be a method of type Am_Where_Method, and should return the object the Interactor should manipulate, or Am_No_Object if none. For example:

Am_Define_Method(Am_Where_Method, Am_Object, in_special_obj_part,
		 (Am_Object /* inter */,
		  Am_Object object, Am_Object event_window,
		  Am_Input_Char /*ic*/, int x, int y)) {
  Am_Object val = Am_Point_In_Part(object, x, y, event_window);
  if (val.Valid() && (bool)val.Get(MY_SPECIAL_SLOT)) 		return val;
  else return Am_No_Object;
}
Note that this means that the Interactor may actually operate on an object different from the one to which it is attached. For example, Interactors will often be attached to a group but actually modify a part of that group. With a custom Am_START_WHERE_TEST function, the programmer can have the Interactor operate on a completely independent object.

5.3.3.3 Active

It is often convenient to be able to create a number of Interactors, and then have them turn on and off based on the global mode or application state. The Am_ACTIVE slot of an Interactor can be set to false to disable the Interactor, and it can be set to true to re-enable the Interactor. By default, all Interactors are active. Setting the Am_ACTIVE slot is more efficient than creating and destroying the Interactor. The Am_ACTIVE slot can also be set with a constraint that returns true or false.

5.3.3.4 Am_Inter_Location

Some interactors require a parameter to describe a location and/or size of an object. Since the coordinate system of each object is defined by its group, just giving a number for X and Y would be meaningless without also supplying a reference object. Therefore, we have introduced a wrapper type, called Am_Inter_Location, which encapsulates the coordinates with a reference object. As described in the Opal manual, some objects are defined by their left, top, width and height, and others by two end points, and this information is also included in the Am_Inter_Location. The methods on an Am_Inter_Location (from inter.h) are:

class Am_Inter_Location {
public:
  Am_Inter_Location (); // empty
  //create a new one. If as_line, then a,b is x1, y1  and c,d is x2, y2.
  //  If not as_line, then a,b is left, top and c,d is width, height
Am_Inter_Location (bool as_line, Am_Object ref_obj,
		     int a, int b, int c, int d);
  //change the values of an existing one
  void Set_Location (bool as_line, Am_Object ref_obj,
		     int a, int b, int c, int d, bool make_unique = true);

  //change just the first coordinate of an existing one
  void Set_Location (bool as_line, Am_Object ref_obj,
		     int a, int b, bool make_unique = true);

  //return all the values
  void Get_Location (bool &as_line, Am_Object &ref_obj,
		     int &a, int &b, int &c, int &d) const;

  //return just the coordinates, the reference object, whether it is a line or not
  void Get_Points (int &a, int &b, int &c, int &d) const;
  Am_Object Get_Ref_Obj () const;
  void Get_As_Line (bool &as_line) const;

  //copy from or swap with another Am_Inter_Location
  void Copy_From (Am_Inter_Location& other_obj, bool make_unique = true);
  void Swap_With (Am_Inter_Location& other_obj, bool make_unique = true);

  //translate the coordinates so they now are with respect to dest_obj
  bool Translate_To(Am_Object dest_obj);

  Am_Inter_Location Copy() const; //make a new one like me
  virtual void Print_Name (ostream& os); //print my contents on the stream
};

5.3.4 Top Level Interactor

Am_Interactor is used to build new, custom interactors. This object won't do anything if you simply instantiate it and add it to a window.

These are the default values of Am_Interactor's slots. Most of these are advanced features and are discussed in Section 5.4.

5.3.5 Specific Interactors

All of the interactors and command objects are summarized in Chapter 10, Summary of Exported Objects and Slots. The next sections discuss each one in detail.

5.3.5.1 Am_Choice_Interactor

The Am_Choice_Interactor is used whenever the programmer wants to choose one or more out of a set of objects, such as in a menu or to select objects in a graphics window. The standard behavior allows the programmer to choose whether one or more objects can be selected, and special slots called Am_INTERIM_SELECTED and Am_SELECTED of these objects are set by default. Typically, the programmer would define constraints on the look of the object (e.g. the color) based on the values of these slots. Note that Am_INTERIM_SELECTED and Am_SELECTED are set in the graphical object the Interactor operates on, not in the Interactor itself.

5.3.5.1.1 Special Slots of Choice Interactors
Two slots of choice Interactors can be set to customize its behavior:

These mean:

The default value for the Am_HOW_SET slot is Am_CHOICE_TOGGLE.

5.3.5.1.2 Standard operation of the Am_Choice_Interactor
As the Choice_Interactor is operating, it calls the various internal methods. The default operation of these methods is as follows. If this is not sufficient for your needs, then you may need to override the methods, as explained in Section 5.5.2.

As the Interactor moves over various graphical objects, the Am_INTERIM_SELECTED slot of the object is set to true for the object which is under the mouse, and false for all other objects. Typically, the graphical objects that the Interactor affects will have a constraint to the Am_INTERIM_SELECTED slot from the Am_FILL_STYLE or other slot. At any time, the Interactor can be aborted by typing the key in Am_ABORT_WHEN (the default is "control_g"). When the Am_STOP_WHEN event occurs, the Am_INTERIM_SELECTED slot is set to false, and the Am_HOW_SET slot of the Interactor is used to decide how many objects are allowed to be selected (as explained above). The objects that should end up being selected have their Am_SELECTED slot set to true, and the rest of the objects have their Am_SELECTED slot set to false. Also the Am_VALUE slot of the Interactor and the Am_VALUE slot of the command object in the Am_COMMAND slot of the Interactor will contain the current value. If Am_HOW_SET is not Am_CHOICE_LIST_ TOGGLE, then the Am_VALUE slot will either contain the selected object or Am_No_Object (NULL). If Am_HOW_SET is Am_CHOICE_LIST_TOGGLE, then the Am_VALUE slot of the Command object will contain a Am_Value_List containing the list of the selected objects (or it will be the empty list).

The default undo of the Am_Choice_Interactor simply resets the Am_SELECTED slots of the selected object(s) and the Am_Value of the Interactor and the command object to be as they were before the Am_Choice_Interactor was run. Redo restores the values.

5.3.5.1.3 Simple Example
See the file testinter.cc for lots of additional examples of uses of Interactors and Command objects. The following Interactor works on any object which is directly a part of the window. Due to the constraints, if you press the mouse down over any rectangle created from rect_proto that is in the window, it will change to having a thick line style when they mouse is over it (when it is ``interim-selected''), and they will turn white when the mouse button is release (and it becomes selected).


Am_Define_Style_Formula (rect_line) {
  if ((bool)self.GV (Am_INTERIM_SELECTED)) return thick_line;
  else return thin_line;
}
Am_Define_Style_Formula (rect_fill)  {
  if ((bool)self.GV (Am_SELECTED)) return Am_White;
  else return self.GV (Am_VALUE);  //the real color
}
rect_proto = Am_Rectangle.Create ("rect_proto")
	.Set (Am_WIDTH, 30)
	.Set (Am_HEIGHT, 30)
	.Set (Am_SELECTED, false)
	.Set (Am_INTERIM_SELECTED, false)
	.Set (Am_VALUE, Am_Purple)  //put the real color here
	.Set (Am_FILL_STYLE, rect_fill)
	.Set (Am_LINE_STYLE, rect_line)
	;
select_inter = Am_Choice_Interactor.Create("choose_rect")
			.Set (Am_START_WHERE_TEST, Am_Inter_In_Part);
window.Add_Part (select_inter);

5.3.5.2 Am_One_Shot_Interactor

The Am_One_Shot_Interactor is used when you want something to happen immediately on an event. For example, you might want a command to be executed when a keyboard key is hit, or when the mouse button is first pressed. The parameters and default behavior for the Am_One_Shot_Interactor are the same as for a Am_Choice_Interactor, in case you want to have an object be selected when the start_when event happens. The programmer can choose whether one or more objects can be selected, and the slots Am_INTERIM_SELECTED and Am_SELECTED of these objects are set by the Am_One_Shot_Interactor the same was as the Am_Choice_Interactor.

The slots for the Am_One_Shot_Interactor are identical to those for the Am_Choice_Interactor (see above).

5.3.5.2.1 Simple Example
In this example, we create a Am_One_Shot_Interactor which calls the Am_DO_METHOD of the change_setting_command (which is do_change_setting) when any keyboard key is hit in the window. The change_setting_command's Am_UNDO_METHOD (which is undo_change_setting) will be used to undo this action. The programmer would write the methods for do and undo.


Am_Object change_setting_command = Am_Command.Create()
		.Set(Am_DO_METHOD, do_change_setting)
		.Set(Am_UNDO_METHOD, undo_change_setting);

Am_Object how_set_inter =
	Am_One_Shot_Interactor.Create("change_settings")
		.Set(Am_START_WHEN, "ANY_KEYBOARD")
		.Add_Part(Am_COMMAND, change_setting_command)
		;
window.Add_Part (how_set_inter);

5.3.5.3 Am_Move_Grow_Interactor

The Am_Move_Grow_Interactor is used to move or change the size of graphical objects with the mouse. The default methods in the Am_Move_Grow_Interactor directly set the appropriate slots of the object to cause it to move or change size. For rectangles, circles, groups and most other objects, the default methods set the Am_LEFT, Am_TOP, Am_WIDTH and Am_HEIGHT. For lines (more specifically, any object whose Am_AS_LINE slot is true), the methods may instead set the Am_X1, Am_Y1, Am_X2 and Am_Y2 slots.

5.3.5.3.1 Special Slots of Move_Grow Interactors
5.3.5.3.2 Gridding
There are two ways to do gridding for Am_Move_Grow_Interactors and Am_New_Point_Interactors. The first is to provide a method, and the second is the provide the gridding origin and multiples:

5.3.5.3.3 Standard operation of the Am_Move_Grow_Interactor
As the Am_Move_Grow_Interactor is operating, it calls the various internal methods. The default operation of these methods is as follows. If this is not sufficient for your needs, then you may need to override the methods, as explained in Section 5.5.2.

If the Interactor's Am_GROWING slot is set to true, the interactor grows the object, otherwise the interactor moves the object. If the Interactor's Am_AS_LINE slot is false, the object is moved or grown by setting its Am_LEFT, Am_TOP, Am_WIDTH and Am_HEIGHT slots. If the Interactor's Am_AS_LINE slot is true, the object is moved or grown by setting its Am_X1, Am_Y1, Am_X2 and Am_Y2 slots. If there is a feedback object in the Am_FEEDBACK_OBJECT slot then its size is set to the size of the object being manipulated, and its Am_VISIBLE slot is set to true. Then it is moved or its size is changed with the mouse. Otherwise, the object itself is manipulated. At any time while the Interactor is running, the abort event can be hit (default is "control-g") to restore the object to its original position and size. When the stop event happens, then the feedback object is made invisible, and the object is moved or changed size to the final position.

The default Undo method of the Am_Move_Grow_Interactor simply resets the object to its original size and position. Redo undoes the undo. When selectively repeating the operation, there are two possible interpretations: change the object by the same absolute values, or change by the same relative amounts. We chose to use the absolute values, so repeating the move or grow will put the object in the same place the original object was moved or grown to. Similarly, selective undo will always return the object to where the original object was before the move or grow.

5.3.5.3.4 Simple Example
See the file testinter.cc for additional examples that use Interactors and Command objects. The following Interactor will move any object in the window when the middle button is held down.


Am_Object move_inter = Am_Move_Grow_Interactor.Create("move_object")
   .Set (Am_START_WHERE_TEST, Am_Inter_In_Part)
   .Set (Am_START_WHEN, "MIDDLE_DOWN");
window.Add_Part (move_inter);

5.3.5.4 Am_New_Points_Interactor

The Am_New_Points_Interactor is used for creating new objects. The programmer can specify how many points are used to define the object (currently, only 1 or 2 points are supported), and the Interactor lets the user rubber-band out the new points. It is generally required for the programmer to provide a feedback object for a Am_New_Points_Interactor so the user can see where the new object will be. If one point is desired, the feedback will still follow the mouse until the stop event, but the final point will be returned, rather than the initial point. Gridding can be used as with a Am_Move_Grow_Interactor. To create the actual new objects, the programmer provides a call-back function in the Am_CREATE_NEW_OBJECT_METHOD slot of the Interactor.

5.3.5.4.1 Special Slots of Am_New_Points_Interactors
5.3.5.4.2 Standard operation of the Am_New_Point_Interactor
As the Am_New_Point_Interactor is operating, it calls the various internal methods. The default operation of these methods is as follows. If this is not sufficient for your needs, then you may need to override the methods, as explained in Section 5.5.2.

While the Interactor is operating, the appropriate slots of the feedback object are set, as controlled by the parameters described above. If the user hits the abort key while the Interactor is running ("control_g" by default), the feedback object is made invisible and the Interactor aborts. If the user performs the Am_STOP_WHEN event (usually by releasing the mouse button), then the Am_CREATE_NEW_OBJECT_METHOD is called. (If there is no procedure, then nothing happens.) The method must be of type Am_Create_New_Object_Method which is defined as:

// type of method in the Am_CREATE_NEW_OBJECT_METHOD slot of Am_New_Points_Interactor.
// Should return the new object created.
// ** old_object is Valid if this is being called as a result of a Repeat undo call, and means that a new
// object should be created like that old_object.  
Am_Define_Method_Type(Am_Create_New_Object_Method, Am_Object,
				(Am_Object inter, Am_Inter_Location location,
				 Am_Object old_object));
The Am_Inter_Location type is explained in Section 5.3.3.4. (Note: this interface may change when we support more than 2 points). After creating the new object and adding it as a part to some group or window, the procedure should return the new object.

The default undo and selective undo methods remove the object from its owner, and the default redo method adds it back to the owner again. The default repeat method calls the Am_CREATE_NEW_OBJECT_METHOD again passing a copy of the original object, and the method is expected to make a new object like the old one. If the create has been undone, then when redo or repeat is no longer possible (determined by the type of undo handler in use--Section 5.5), and the saved objects are automatically destroyed.

Note: if you use the Am_Create_New_Object_Method for something other than creating objects, then do not have a Am_Create_New_Object_Method return the affected object, because the buily-in undo methods may automatically delete the object. For example, in space.cc, a Am_New_Points_Interactor is used to draw the phaser which deletes objects, and this is handled in the command's DO method. It would be an error to do this from the Am_Create_New_Object_Method since the Undo method might delete the object.

5.3.5.5 Am_Text_Edit_Interactor

The Am_Text_Edit_Interactor is used for single-line, single-font editing of the text in Am_Text objects. (Support for multi-line, multi-font text editing will be in a future release.) The default behavior is to directly set the Am_TEXT and Am_CURSOR_INDEX slots of the Am_Text object to reflect the user's changes. Most of the special operations and types used by the Am_Text_Edit_Interactor are defined in text_fns.h.

When the Am_START_WHEN event occurs, the Interactor puts the text object's cursor where the start event occurred. Subsequent events are sent to the editing method in the Am_TEXT_EDIT_METHOD slot which modifies the Am_Text object. When the stop_when event happens, the cursor is turned off and the command object's DO_METHOD method is called. The stop_when event is not entered into the string.

The default Am_RUNNING_WHERE_OBJECT is true, meaning the Interactor will run no matter where the user moves the cursor. If this slot is set to be a particular object, leaving that object causes the Interactor to hide the text object's cursor until the user moves back into the object.

Notice that Am_Text_Edit_Interactor's Am_START_WHERE_TEST slot is set to the value Am_Inter_In_Text_Object_Or_Part. Am_Text_Edit_Interactors only work properly on Am_Text objects, so one of the text tests should be used for the Am_START_WHERE_TEST slot.

5.3.5.5.1 Special Slots of Text Edit Interactors
The text edit function should edit the text object's Am_TEXT field given the input character ic. It can also modify the Am_CURSOR_INDEX slot of the object, but shouldn't change other slots. The default function: Am_Default_Text_Edit_Method, uses the Am_Edit_Translation_Table specified in the Interactor's Am_EDIT_TRANSLATION_TABLE slot to provide the basic editing operations (see the description of the Am_EDIT_TRANSLATION_TABLE slot below). If the input character doesn't match any operation in the translation table, the default edit function does the following:
5.3.5.5.2 Standard operation of the Am_Edit_Text_Interactor
The text interactor's start action saves a copy of the text object's original Am_TEXT slot in the Am_OLD_VALUE slot of the Interactor, and it moves the cursor to the location specified by where the Interactor start event occurred. All subsequent events are passed to the Am_Text_Edit_Method specified in the Interactor's Am_TEXT_EDIT_METHOD slot. An abort event causes the original text object's text to be restored, and the object's Am_CURSOR_INDEX is set to Am_NO_CURSOR. When the stop event occurs, the Command object's Am_VALUE slot is set to the new value of the text object's Am_TEXT slot, and the text object's Am_CURSOR_INDEX is set to Am_NO_CURSOR. The stop_when event is not entered into the string.

Undo and selective undo restore the text object to its previous value, and redo undoes the undo. Repeat sets the text object to have the string that the user edited it to.

5.3.5.6 Am_Gesture_Interactor

The Am_Gesture_Interactor records a gesture, which is the path traced out by the mouse (or other pointing device) while the interactor is running. Beginning with its start event, the gesture interactor records the location of every input event until the interactor stops or aborts. When the interactor stops, it saves the gesture as a list of points (a Am_Point_List) in its Am_POINT_LIST slot. This behavior (which is the default) is also useful for a free-hand drawing tool, which would retrieve the point list and install it in a Am_Polygon object to create a curve duplicating the mouse path.

The real power of the gesture interactor, however, lies in its ability to recognize and classify gestures into categories defined by the programmer. The categories are defined by a Am_Gesture_Classifier object installed in the gesture interactor's Am_CLASSIFIER slot. (The procedure for creating a classifier is explained in Section 5.3.5.6.1) When a classifier is installed, the gesture interactor attempts to classify each gesture into one of the categories. If a gesture is successfully recognized, the name of its category (a Am_String) is stored in the Am_VALUE slots of the interactor and its command object. If a gesture is unrecognized -- that is, if it is too different from the prototypical gestures in each category -- then Am_VALUE is set to 0.

In addition, to simplify applications where gestures represent commands (such as cut, copy, or paste), the gesture interactor can take a list of command objects in its Am_ITEMS slot. Before the interactor invokes its command object, it first searches the Am_ITEMS list for a command object whose Am_LABEL is identical to Am_VALUE (which is the gesture's name if it was recognized and NULL if not). The first matching command object in the list is invoked instead of the interactor's command object. If no command object in Am_ITEMS matches the gesture, then the interactor's command object is invoked instead. Thus, a gesture interactor can be configured much like a menu widget or button panel widget, by supplying a list of commands.

By default, the gesture interactor provides no visible feedback while the user is tracing out a gesture. To provide feedback, set the Am_FEEDBACK_OBJECT slot to a graphical object which has a Am_POINT_LIST slot. As the interactor records a gesture, it updates the feedback object's point list. The usual feedback object is a Am_Polygon with no fill style, which draws an unfilled, black curve as the user gestures. Here is some code that creates a feedback object:

Am_Polygon.Create()
.Set (Am_FILL_STYLE, 0);
Note: the feedback object must be added to a window or group, or it will never be drawn.

The list below summarizes the three most common ways to use a gesture interactor, showing how the interactor's slots should be initialized in each case.

5.3.5.6.1 Creating and Using a Gesture Classifier
A gesture classifier maps gestures into named categories. Amulet uses a statistical classifier developed by Rubine. In Rubine's algorithm, the classifier is trained by giving it a number of examples for each gesture category. Each example is converted into a feature vector (containing global features like path length and initial direction), and the examples in each category are combined to give a mean feature vector for the category and a covariance matrix for the entire classifier. After training, the classifier can be used to recognize a gesture. To recognize a gesture, the classifier converts it to a feature vector and determines the most likely gesture category to which it belongs (using a maximum likelihood estimator). For more information, see Rubine's thesis1.

In Amulet, gesture classifiers are trained in a standalone application, called Agate. Agate may be found in samples/agate under the Amulet root directory. To create a classifier using Agate, add a class for each different type of gesture, giving a unique name to each class. For instance, a drawing program might have a class called ``line'' which contains straight-line gestures, and a class called ``circle'' which contains looping gestures. To demonstrate examples for a gesture class, select the class, then draw its gestures in the large empty area at the bottom of the Agate window. To produce the most forgiving classifier, try to include examples with varying size, orientation, and direction (except where your gesture classes rely on such information for uniqueness), and provide 10 to 20 examples for each class. At any point while training a classifier, you can switch from Train mode to Recognize mode in order to test the classifier. In Recognize mode, the classifier attempts to recognize the gesture you draw, highlighting the class to which it most likely belongs.

After training a classifier, save it to a file. Classifiers can be saved either with or without the examples that constructed them, depending on a checkbox in Agate's Save-File dialog. If you want to change the classifier later, then you should save it with examples, otherwise editing will be impossible. If you want to make the saved classifier as small as possible (perhaps to distribute with your application), then you can save it without examples. Either kind of file can be read into an Amulet program and used in a gesture interactor.

To use a saved classifier in an Amulet program, create an instance of the wrapper Am_Gesture_Classifier and load the classifier file into it. A classifier can be loaded in either of two ways: by specifying the filename in the constructor:

Am_Gesture_Classifier my_classifier (``my-classifier-file.cl'');
or by opening the file as a stream and using >>:

Am_Gesture_Classifier my_classifier;
ifstream in(``my-classifier-file.cl'');
in >> the_classifier;
After the Am_Gesture_Classifier object is initialized with a classifier, it can be installed into the Am_CLASSIFIER slot of a gesture interactor:

gesture_interactor.Set (Am_CLASSIFIER, my_classifier);
5.3.5.6.2 Special Slots of Gesture Interactors
5.3.5.6.3 Standard operation of the Am_Gesture_Interactor
As the gesture interactor is operating, it calls various internal methods. The default operation of these methods is as follows. If this is not sufficient for your needs, then you may need to override the methods, as explained in Section 5.5.2.

While the Interactor is running, it appends the points visited by the user's mouse to the Am_Point_List in its Am_POINT_LIST slot. If a feedback object has been provided, it also appends the points to the feedback object's Am_POINT_LIST slot. If the user hits the abort key while the Interactor is running ("control_g" by default), the feedback object is made invisible and the Interactor aborts.

Otherwise, when the user performs the Am_STOP_WHEN event (usually by releasing the mouse button), then the interactor attempts to recognize the gesture using the classifier in its Am_CLASSIFER slot. If the gesture is successfully recognized, then its name is stored in Am_VALUE. Otherwise, if Am_CLASSIFIER is 0, or if the gesture cannot be recognized because it is too ambiguous (by Am_MIN_NONAMBIGUITY_PROB) or too different from the known gestures (by Am_MAX_DIST_TO_MEAN), then 0 is stored in Am_VALUE.

After classifying the gesture, the interactor looks for a command object to invoke. First, it searches the list in its Am_ITEMS slot for a command object whose Am_LABEL matches Am_VALUE (which, recall, is either the gesture name or 0 if the gesture was unrecognized). If a matching command is found, the interactor invokes its DO method; otherwise, it invokes the DO method of the command in the interactor's Am_COMMAND slot.

5.4 Advanced Features

5.4.1 Output Slots of Interactors

As they are operating, Interactors set a number of slots in themselves which you can access from the Command's DO procedure, or from constraints that determine slots of the object. The slots set by all Interactors are:

The specific Interactors also set special slots in themselves as they are running, and then in their Command objects when they finish. These are described below in Section 5.5.3 about each specific type of Interactor.

5.4.2 Priority Levels

When an input event occurs in a window, Amulet tests the Interactors attached to objects in that window in a particular order. Normally, the correct Interactor is executed. However, there are cases where the programmer needs more control over which Interactors are run, and this section discusses the two slots which control this:

Am_PRIORITY: The priority of this Interactor. The default is 1.0.
Am_RUN_ALSO: If true, then let other Interactors run after this one is completed. The default is false.
All the Interactors that can operate on a window are kept in a sorted list. The list is sorted first by the Interactor's priority number, and then by the display order of the graphical object the Interactor is attached to. The result is that for Interactors of the same priority, the one attached to the least covered (front-most) graphical object is handled first.

The priority of the Interactor is stored in the Am_PRIORITY slot and can be any positive or negative number. When an Interactor starts running, the priority level is increased by a fixed amount (defined by Am_INTER_PRIORITY_DIFF which is 100.0 and is defined in inter_advanced.h). This makes sure that Interactors that are running take priority over those that are just waiting. If you want to make sure that your Interactor runs before other default Interactors which may be running, then use a priority higher than 101.0. For example, the debugging Interactor which pops up the inspector (see Section 5.6) uses a priority of 300.0.

For Interactors with the same priority, the Interactor attached to the front and leaf most graphical object will take precedence. This is implemented using the slots Am_OWNER_DEPTH and Am_RANK of the graphical objects which are maintained by Opal. What this means is that an Interactor attached to a part has priority over an Interactor attached to the group that the part is in, if they both have the same value in the Am_PRIORITY slot. Note that this determinations does not take into account which objects the Interactor actually affects, just what object the Interactor is a part of. Thus, if Interactor A is attached to group G and has a Am_START_WHERE_TEST of Am_Inter_In_Part, and Interactor B is attached to part P which is in G and has a Am_START_WHERE_TEST of Am_Inter_In, then the Interactor on B will take precedence by default, even though both A and B can affect P.

If the Interactor that accepts the event has its Am_RUN_ALSO slot set to true (the default is false), then other Interactors will also get to process the current event. Thus, the run-also Interactor operates without grabbing the input event. This might be useful for Interactors that just want to monitor the activity in a window, say to provide a ``tele-pointer'' in a multi-user application. In this case, you would want the Interactor with Am_RUN_ALSO set to true to have a high priority.

Furthermore, if an Interactor that has Am_RUN_ALSO slot set to false accepts the current event, the system will continue to search to find if there are any other Interactors with Am_RUN_ALSO slot set to true that have the same priority as the Interactor that is running. These are also allowed to process the current event.

5.4.3 Multiple Windows

A single Interactor can handle objects which are in multiple windows. Since an Interactor must be attached to a single window, graphical object or group, a special mechanism is needed to have an Interactor operate across multiple windows. This is achieved by using the Am_MULTI_OWNERS slot. The value of this slot can be:

Of course, the function in the Am_START_WHERE_TEST slot must search for objects in all of the appropriate windows. It might use the value of the Am_WINDOW slot of the Interactor, which will contain the window of the current event. This is normally sufficient for Am_Choice_Interactors, Am_One_Shot_Interactors, and Am_Text_Edit_Interactors that want to operate on objects in multiple windows. For these interactors, the Am_MULTI_OWNERS slot can be either a list of windows or a list of objects in those windows. Be sure to include the window to which the interactor is attached.

Special features are built-in to support interactors that might want to move an object from one window to another, such as Am_Move_Grow_Interactors, Am_New_Points_Interactors, and Am_Gesture_Interactors. In this case, the Am_MULTI_OWNERS slot should contain a list of objects which should serve as the owners of the graphical objects as they are moved from one window to another. Then, the interactor will automatically search the Am_MULTI_OWNERS list for an object in the window that the cursor is currently in, and if found, then the object being moved (returned by the Am_START_WHERE_TEST) will change to have that object as its owner.

Often the feedback objects should be in a different owner than the ``real'' objects being moved. In this case, you can set the Am_MULTI_FEEDBACK_OWNERS slot with a list of owner objects for the feedback object. In this case, the feedback object of the interactor (specified in the Am_FEEDBACK_OBJECT slot of the interactor) will automatically be changed to be in the object of the appropriate window. If the Am_MULTI_FEEDBACK_OWNERS slot is NULL, then the owners in the Am_MULTI_OWNERS slot are used for the feedback object as well.

The Am_FEEDBACK_OBJECT can also be a top-level window object, in which case the various types of interactor objects will move the object around on the screen. This is particularly useful when you want to be sure to see the feedback even when the cursor is not over an Amulet window. For move-grow interactors when using a window as the feedback, you should use something like Am_ATTACH_NW for the Am_WHERE_ATTACH slot.

Examples of using multi-window interactors are in the test file inter/testinter.cc.

5.4.4 Running_Where

Section 5.3.2 mentioned that Interactors can be defined so that they stop operating when the mouse goes outside of their active area. The active area is defined by the value of the Am_RUNNING_WHERE_OBJECT slot. This slot should contain either a graphical object or true, which means anywhere (so the Interactor never goes outside). The default for most Interactors for this slot is true, but for Choice Interactors, this slot contains a constraint that makes it have the same object as where the Interactor starts. To refine where the Interactor should be considered outside, the programmer can also supply a value for the Am_RUNNING_WHERE_TEST slot, which defaults to Am_Inter_In except for choice Interactors, where it contains a constraint that uses the same function as the Am_START_WHERE_TEST. We have found that programmers rarely need to specify the running where of Interactors.

5.4.5 Starting, Stopping and Aborting Interactors

Interactors normally start, stop and abort due to actions by the user, but it is sometimes useful for the programmer to be able to explicitly control the Interactors. The following functions are useful for controlling this. If you have a widget, you would use the corresponding widget functions instead (Am_Start_Widget, Am_Stop_Widget, and Am_Abort_Widget -- Section 5.5).

extern void Am_Abort_Interactor(Am_Object inter);
Am_Abort_Interactor causes the Interactor to abort (stop running). The command associated with the Interactor is not queued for Undo.

extern void Am_Stop_Interactor(Am_Object inter,
			       Am_Object stop_obj = Am_No_Object,
			       Am_Input_Char stop_char = Am_Default_Stop_Char,
			       Am_Object stop_window = Am_No_Object, int stop_x = 0,
			       int stop_y = 0);
Am_Stop_Interactor explicitly stops an interactor as if it had completed normally (as if the stop event had happened). The command associated with the interactor is queued for Undo, if appropriate. If the interactor was not running, Am_Stop_Interactor raises an error. If the stop_obj parameter is not supplied, then Am_Stop_Interactor uses the last object the interactor was operating on (if any). stop_char is the character sent to the Interactor's routines to stand in for the final event of the Interactor. If stop_window is not supplied, then will use stop_obj's window, and stop_x and stop_y will be stop_obj's origin. If stop_window is supplied, then it should be the window that the final event is with respect to, and you must also supply stop_x and stop_y as the coordinates in the window at which the interactor should stop.

extern void Am_Start_Interactor(Am_Object inter,
			Am_Object start_obj = Am_No_Object,
		     Am_Input_Char start_char = Am_Default_Start_Char,
			Am_Object start_window = Am_No_Object, int start_x = 0,
			int start_y = 0);
Am_Start_Interactor is used to explicitly start an Interactor. If the interactor is already running, this does nothing. If start_obj is not supplied, then will use the inter's owner. If start_window is not supplied, then uses start_obj's and sets start_x and start_y to start_obj's origin. start_char is the initial character to start the interactor with. If start_window is supplied, then you must also supply start_x and start_y as the initial coordinate (with respect to the window) for the Interactor to start at.

If you want an interactor to never start by itself, you can have the Am_START_WHEN slot be NULL, and then start it explicitly using the Am_Start_Interactor procedure. Similarly, the Am_STOP_WHEN slot can be NULL for an Interactor that should never stop by itself.

5.4.6 Support for Popping-up Windows and Modal Windows

It is often useful to be able to pop up a window, and wait for the user to respond. This can eliminate having to chain a number of DO methods together when you just want to ask the user a question. Amulet provides low-level functions to support this. If the window to ask the question is one of a few standard types, you might alternatively use the built-in dialog boxes described in Section 6.3 of the widgets chapter.

The way the low level routines are used is that the programmer calls Am_Pop_Up_Window_And_Wait passing in the window to wait on, and then that window contains a number of widgets, at least one of which (usually the ``OK'' button) calls Am_Finish_Pop_Up_Waiting as part of its DO method. The value passed to Am_Finish_Pop_Up_Waiting is then returned by the Am_Pop_Up_Window_And_Wait call.

extern void Am_Pop_Up_Window_And_Wait(Am_Object window,
				      Am_Value &return_value,
				      bool modal = true);
Am_Pop_Up_Window_And_Wait sets the visible of the window to true, and then waits for a routine in that window to call Am_Finish_Pop_Up_Waiting on that window. Returns the value passed to Am_Finish_Pop_Up_Waiting by setting the return_value parameter. If modal is true, the default, then the user will only be able to work on this one window, and all other windows of this application will be frozen (input attempts to other windows will beep). Note that input can still be directed to other applications, unlike modal dialog boxes on the Macintosh.

It is legal to call Am_Pop_Up_Window_And_Wait from inside a DO method, so a modal window might pop up another modal window.

extern void Am_Finish_Pop_Up_Waiting(Am_Object window,
						     Am_Value return_value);
Am_Finish_Pop_Up_Waiting sets window's Am_VISIBLE to FALSE, and makes the Am_Pop_Up_Window_And_Wait called on the same window return with the value passed as the return_value. Although there is no default, it is acceptable to pass Am_No_Value as the return value.

This allows code like:

Am_Value val;
Am_Pop_Up_Window_And_Wait(my_query_window, val);
if (val.Valid()) ...
else ...;

5.5 Customizing Interactor Objects

Amulet allows the programmer to customize the behavior of Interactors at multiple levels. As described in the previous sections, many aspects of the behavior of Interactors can be controlled through the parameters of the Interactors. We believe that in almost all cases, programmers will be able to create their applications by using these built-in parameters of the pre-defined types of Interactors. If you are happy with the standard behavior, but want some additional actions to happen when the interactor is finished, then you can attach a custom Command object to the interactor, as described in Section 5.5.1. If you want behavior similar to a standard Interactor,. but slightly different, then you might want to override some of the standard methods that implement the Interactor's behavior, as described in Section 5.5.2. However, there might be rare cases when an entirely new type of Interactor is required, as described in Section 5.5.3. For example, in Garnet which had a similar Interactor model, none of the applications created using Garnet needed to create their own Interactor types. However, when the Garnet group wanted to add support for Gesture recognition, this required writing a new Interactor. Since Amulet is designed to support investigation into new interactive styles and techniques, new kinds of Interactors may be needed to explore new types of interaction beyond the conventional direct manipulation styles supported by the built-in Interactors. In summary, we feel you should only need to create a new kind of Interactor when you are supporting a radically different interaction style.

5.5.1 Adding Behaviors to Interactors

If you want the standard behavior of an interactor plus some additional behavior, you can override the methods of the Command object in the Interactor. (See Section 5.6 for a complete discussion of Command objects.) The command object in the Interactors all have empty methods, so you can override the methods without concern. The methods that you can override include:

There are three different kinds of methods that you can put into any of these slots of the command objects:

Note that the Am_Mouse_Event_Method and the Am_Current_Location_Method cannot be used for command objects in widgets, only for the command objects in Interactors. The command objects in widgets only support Am_Object_Method.

In addition to the information passed as parameters to the command object, slots of the command object and of the Interactor are set by the Interactor and may be of use to the methods. For all command objects, the following slots are available:

In the command objects:

Each of the types of Interactor sets specific slots in the Interactor that the command object might want to access, as described in the following sections. Remember that these slots are set in the Interactor, not in the Command object. The slots described below are in addition to the slots described with the specific interactors in Section 5.3.5 and the slots set into all interactors, described in Section 5.4.1.

5.5.1.1 Available slots of Am_Choice_Interactor and Am_One_Shot_Interactor

5.5.1.2 Available slots of Am_Move_Grow_Interactors

5.5.1.3 Available slots of Am_New_Point_Interactors

5.5.1.4 Available slots of Am_Text_Interactors

5.5.1.5 Available slots of Am_Gesture_Interactors

5.5.2 Modifying the Behavior of the Built-in Interactors

If you need to override the standard behavior of an Interactor, and the supplied parameterization is not sufficient, then you may need to override one of the standard methods of the Interactor that control its behavior. All Interactors internally use the same state machine, as shown in the Figure in Section 5.3.2. At each input event, Amulet will call an internal method of the Interactor, which typically sets some internal slots of the Interactor, and first calls a customizable method described below, and then calls the corresponding method of the command object, described in Section 5.5.1. The methods of the Interactor you can override are the same as the methods in the command object described above:

As with the command objects in Interactors, the methods directly in the Interactors can take one of the three types: Am_Object_Method, Am_Mouse_Event_Method, or Am_Current_Location_Method, as defined in Section 5.5.1.

Remember that if you override the various DO methods, this will remove the standard behavior of the Interactors, so there may be some of the behavior you may want to re-implement in your code. For example, the Am_ABORT_DO_METHOD and the Am_DO_METHOD take care of making the feedback object (if any) invisible. The Am_ABORT_DO_METHOD is also responsible for restoring the object to its original state.

5.5.3 Entirely New Interactors

This section gives an overview of the how to build an entirely new type of Interactor. As said above, we believe this almost never be necessary. You may need to look at the source code for one of the built-in Interactors to see how they operate in detail.

The main event loop in Amulet takes each input event and looks at the sorted list of Interactors with each window, and then asks each Interactor in turn if they want to handle the input event. This is done by sending the Interactor one of the messages listed below, based on the current state of the Interactor, held in the slot Am_CURRENT_STATE (see the state machine figure in Section 5.3.2). The different Interactors are distinguished by having different functions for these messages. All of the messages are of type Am_Inter_Internal_Method (defined in inter_advanced.h). Most of these methods set up some slots of the Interactor, and then call the appropriate Interactor DO method. The specific internal methods you need to write for a new type of Interactor are stored in the following slots:

5.6 Command Objects

Unlike other toolkits where the widgets call ``call-back'' procedures, the widgets and Interactors in Amulet allocate Command Objects and call their ``Do'' methods. Whereas so far this is pretty much equivalent, Command Objects also have slots that handle undoing, enabling and disabling, and help. Command objects must be added as parts of the objects they are attached to, so every Interactor and widget has a part named Am_COMMAND which contains a Am_Command object. For all widgets and Interactor commands, the default methods are empty, so you can freely supply any method you want. There are also a set of command objects supplied in the library which you might be able to use in applications, as described in Section 6.4 of the Widgets chapter. If you create your own custom command objects, be sure to create UNDO methods, as described in Section 5.6.2.

The top-level definition of a Command object is:

  Am_Command = Am_Root_Object.Create ("Am_Command")
    .Set (Am_DO_METHOD, NULL)
    .Set (Am_UNDO_METHOD, NULL)
    .Set (Am_REDO_METHOD, NULL)
    .Set (Am_SELECTIVE_UNDO_METHOD, NULL)
    .Set (Am_SELECTIVE_REPEAT_SAME_METHOD, NULL)
    .Set (Am_SELECTIVE_REPEAT_ON_NEW_METHOD, NULL)
    .Set (Am_SELECTIVE_UNDO_ALLOWED, Am_Standard_Selective_Allowed)
    .Set (Am_SELECTIVE_REPEAT_SAME_ALLOWED, Am_Standard_Selective_Allowed)
    .Set (Am_SELECTIVE_REPEAT_NEW_ALLOWED,
	  Am_Standard_Selective_New_Allowed)

    .Set (Am_ACTIVE, true)
    .Set (Am_LABEL, "A command")
    .Set (Am_SHORT_LABEL, 0) //if 0 then uses Am_LABEL
.Set (Am_ACCELERATOR, 0) // event to also execute this

.Set (Am_ID, 0) // if non-zero, identifies the cmd instead of label

    .Set (Am_VALUE, 0)
    .Set (Am_OLD_VALUE, 0) //usually for undo
    .Set (Am_OBJECT_MODIFIED, 0)
    .Set (Am_SAVED_OLD_OWNER, NULL)
    .Set (Am_IMPLEMENTATION_PARENT, 0)
    ;
Most Command objects supply a Am_DO_METHOD procedure which is used to actually execute the Command. It will typically also store some information in the Command object itself (often in the Am_VALUE and Am_OLD_VALUE slots) to be used in case the Command is undone. The Am_UNDO_METHOD procedure is called if the user wants to undo this Command, and usually swaps the object's current values with the stored old values. The Am_REDO_METHOD procedure is used when the user wants to undo the undo. Often, it is the same procedure as the Am_UNDO_METHOD. The various SELECTIVE_ methods support selective undo and repeat the command, as explained in Section 5.6.2.3. The Am_ACTIVE slot controls whether the Interactor or widget that owns this Command object should be active or not. This works because widgets and Interactors have a constraint in their active field that looks at the value of the Am_ACTIVE slot of their Command object. Often, the Am_ACTIVE will contain a constraint that depends on some state of the application, such as whether there is an object selected or not. The Am_LABEL slot is used for Command objects which are placed into buttons and menus to show what label should be shown for this Command. If supplied, the Am_SHORT_LABEL is used in the Undo dialog box to label the command. For commands in button and menu widgets, the Am_ACCELERATOR slot can contain an Am_Input_Char which will be used as the accelerator to perform the command (see Section 6.2.3.2 of the Widgets chapter). For commands in button widgets, the widgets use the command to determine the value to return when the button is hit. If the Am_ID slot is set, then this value, which can be of any type, is used. If Am_ID is 0, then the value of the Am_LABEL slot is returned.

Various slots are typically set by the DO method for use by the UNDO methods. The Am_VALUE slot is set with the value for use. You have to look at the documentation for each Interactor or Widget to see what form the data in the Am_VALUE slot is. The DO method typically also sets the Am_OBJECT_MODIFIED slot with the object (or a Am_Value_List of objects) that the Command affects. The Am_SAVED_OLD_OWNER slot is set by the Interactors and Widgets to contain the Interactor and widget itself. Finally, the Am_IMPLEMENTATION_PARENT supports the hierarchical decomposition of commands, as described in the next section (Section 5.6.1).

As mentioned above in Section 5.5.1, command objects which are used in Interactors also support the Am_START_DO_METHOD, Am_INTERIM_DO_METHOD, and the Am_ABORT_DO_METHOD.

5.6.1 Implementation_Parent hierarchy

Normal objects are part of two hierarchies: the prototype-instance hierarchy and the part-owner hierarchy. The Command objects has an additional hierarchy defined by the Am_IMPLEMENTATION_PARENT slot. Based on the Ph.D. research of David Kosbie, we allow lower-level Command objects to invoke higher-level Command objects. For example, the Command object attached to a move-grow Interactor which is allowing the user to move a scroll bar indicator calls the Command object attached to the scrollbar itself.

This novel model for Command objects is more completely described in a conference paper which is available from the Amulet WWW site (http://www.cs.cmu.edu/~amulet/papers/commandsCHI.html).

Simply, the system calls the Am_DO_METHOD of the lowest level command associated with the Interactor or widget, and then looks in the Am_IMPLEMENTATION_PARENT slot of that command. If that slot contains a command object, then the Am_DO_METHOD of that command is also called, and so on. Internally, all the widgets use this mechanism to chain commands together, and you can use it also to execute multiple commands on an event.

When a command is queued for undo, all the commands along the Am_IMPLEMENTATION_PARENT chain are queued. When the user requests an undo or selective undo or redo, then all the commands in the Am_IMPLEMENTATION_PARENT chain are undone in the same order as they were originally executed (from child to parent). This is in case lower-level commands set state which is used by higher-level commands.

The advantage of this design is that the low-level Command objects do not need to know how they are being used, and can just operate normally, and the higher-level Command objects will update whatever is necessary. Note that the Am_IMPLEMENTATION_PARENT hierarchy is not usually the same as the part-owner hierarchy. Unfortunately, it seems to be difficult or impossible for Amulet to deduce the parent hierarchy from the part-owner hierarchy, which is why the programmer must explicitly set the Am_IMPLEMENTATION_PARENT slot when appropriate. Of course, the built-in widgets (like the scroll bar) have the internal Command objects set up appropriately.

You might use the Am_IMPLEMENTATION_PARENT slot for the Command object in the `OK' button widget inside a dialog box, so the OK widgets's action will automatically call the dialog box's Command object. Another example is that for the button panel widget (see the Widgets chapter), you can have a Command object for each individual item and/or a Command object for the entire panel. If you want the individual item's Command to be called and the top-level Command to be called, then you would make the top-level Command be the Am_IMPLEMENTATION_PARENT of each of the individual item Commands.

5.6.2 Undo

All of the Command objects built into the Interactors and widgets automatically support full undo, redo and selective undo and repeat. This means that the default Am_DO_METHOD procedures store the appropriate information. Built-in ``undo-handlers'' know how to copy the command objects when they are executed, save them in a list of undoable actions, and execute the undo, redo and selective methods of the commands. Thus, to have an application support undo is generally a simple process. You need to create an undo-handler object and attach it to a window, and then have some button or menu item in your application call the undo-handler's method for undo, redo, etc.

5.6.2.1 Enabling and Disabling Undoing of Individual Commands

If there are operations in the application that are not undoable, for example like File Save, then you should have the Am_UNDO_METHOD and Am_REDO_METHOD slots as null. As explained below, there are special methods that determine whether the command is selective undoable and repeatable.

If there are operations that should not go on the undo list at all, for example like scrolling, there is an easy way to specify this. Simply set the Am_IMPLEMENTATION_PARENT slot of the top-level command to be the constant value Am_NOT_USUALLY_UNDONE, which is defined in inter.h. (All commands whose Am_IMPLEMENTATION_PARENT slot is null are assumed to be top-level commands and are queued for undo.)

5.6.2.2 Using the standard Undo Mechanisms

There are three styles of undo supplied by Amulet. These are described in this and the next sections. Section 5.6.2.3 discusses how programmers can implement other undo mechanisms. The undo mechanisms are implemented using Am_Undo_Handler objects.

The three kinds of undo supplied by Amulet are:

To make an application support undo, it is only necessary to put an instance of an undo handler object into the Am_UNDO_HANDLER slot of a window. For example:

my_win = Am_Window.Create("my_win")
  .Set (Am_LEFT, ...)
  ...
  .Set (Am_UNDO_HANDLER, Am_Multiple_Undo_Object.Create("undo"))
You can put the same undo-handler object into the Am_UNDO_HANDLER slot of multiple windows, if you want a single list of undo actions for multiple windows (for example, for applications which use multiple windows). Now, all the Commands executed by any widgets or Interactors that are part of this window will be automatically registered for undoing.

Next, you need to have a widget that will allow the user to execute the undo and redo. The Am_Undo_Command object provided by widgets.h encapsulates all you typically need to support undo, and the Am_Redo_Command supports redo (see Section 5.6 of the widgets chapter). There is also a dialog box provided to support selective undo (next section).

If the built-in undo and redo commands are not sufficient, then you can easily create your own. The command's Am_ACTIVE slot should depend on whether the undo and redo are currently allowed. The undo_handler objects provide the Am_UNDO_ALLOWED slot to tell whether undo is allowed. This slot contains the command object that will be undone (in case you want to have the label of the Undo Command show what will be undone). The Am_REDO_ALLOWED slot of the undo object tells whether redo is allowed and it also will contain a command object or NULL. To actually perform the undo or redo, you call the method in the Am_PERFORM_UNDO or Am_PERFORM_REDO slots of the undo object. These methods are of type Am_Object_Method.

5.6.2.3 The Selective Undo Mechanism

In addition to the regular multiple-undo mechanism, Amulet now supports a novel selective undo mechanism. This allows the user to select previously executed commands from the history and undo or repeat them. See the conference paper mentioned above for a complete description.

We use the terminology from Windows and the Macintosh, where ``undo'' means to undo the command, ``redo'' means to undo-the-undo (make the command no longer be undone), and ``repeat'' to do the command again. The mechanism supports the conventional undo and redo, as described above. In addition, the user can select a command from a visible list. At this point, the user can ask that the command be undone, it can be repeated on the original objects, or repeated on the currently selected objects. For example, for a command that changes an object that was originally blue to be red, undo would clearly restore the object to be blue, and redo would make it red again. Imagine the object was later turned green. Selective undo of the original change-color command would restore the object to be blue, and selective repeat would make it be red again. Since the selective commands add a new command to the history list, the user might undo the selective operation itself, which would make the object be green again. If a new object was selected, selectively repeating the original command on this new object would make it be red.

All of the built-in commands support the complete selective undo mechanism. The next section describes what you need to implement to create your own commands that support selective undo. Next is described the commands you would add to your application to allow access to the selective undo features.

5.6.2.3.1 Supporting Selective Undo in your own Command Objects
Regular undo and redo operate on the original command object, and remove or add it to the undo list. However, selective undo and repeat create a copy of the original command object, and then add this copy to the top of the undo list. Thus, using the regular undo method or selective undo of the most recent command should both have the same effect to the application, they have very different affects on the undo list. (Thus, you can implement an undo like in the Emacs editor, where all undo's are added to the command history, by simply always using selective undo instead of the ``regular'' undo.) This copying is automatically handled by the default undo handlers (including renaming the command to have ``Undo'' or ``Repeat'' in front of the name).

When a selective undo or repeat operation is requested by the end user, the standard undo handler first makes a copy of the command object, and then executes the appropriate methods. This means that the methods are free to set local data into the commands to support possible subsequent undo of the command.

The messages in each command that support selective undo are:

In addition to the methods described above, additional methods that might be useful that are provided include:

5.6.2.3.2 Interface to Selective Undo in Applications: Undo Dialog Box
Amulet comes with an experimental interface to selective undo, in the form of the Am_Undo_Dialog_Box object which implements one form of undo dialog box. This is exported from undo_dialog.h (or undo_dia.h on the PC). You can see an example of this dialog box in the test file testselectionwidgets.cc (or testselw.cpp on the PC).

This dialog box is not part of the standard system, so you have to initialize it explicitly using Am_Initialize_Undo_Dialog_Box(). You can then create an instance of the Am_Undo_Dialog_Box, and set its required slots, which are:

To display the undo dialog box, you might use the Am_Show_Undo_Dialog_Box_Command from undo_dialog.h. This command requires that the dialog box object be put into the Am_UNDO_DIALOG_BOX slot of the command object.

For example, the set up of the undo dialog box in the test program testselectionwidgets.cc is:

Am_Initialize_Undo_Dialog_Box();
my_undo_dialog = Am_Undo_Dialog_Box.Create("My_Undo_Dialog")
    .Set(Am_LEFT, 550)
    .Set(Am_TOP, 200)
    .Set(Am_UNDO_HANDLER_TO_DISPLAY, undo_handler)
    .Set(Am_SELECTION_WIDGET, my_selection)
    .Set(Am_SCROLLING_GROUP, scroller)
    .Set(Am_VISIBLE, false)
    ;
Am_Screen.Add_Part(my_undo_dialog); //don't forget to add the db to the screen
menu_bar = Am_Menu_Bar.Create("menu_bar")
      .Set(Am_ITEMS, Am_Value_List ()
		   ...
			.Add(Am_Show_Undo_Dialog_Box_Command.Create()
					.Set(Am_UNDO_DIALOG_BOX, my_undo_dialog))
		;

5.6.2.4 Building your own Undo Mechanisms

Usually, one of the two the supplied Undo objects will do what you want, but Amulet is designed to be easily extensible with new kinds of undo mechanisms. For example, you might want to support arbitrary numbers of redo's or put a limit on the number of Commands that can be undone. To implement these, you would make your own undo handler object as an instance of Am_Undo_Handler and supply values in the following slots:

If your undo handler will support selective undo, then the following slots should also be supported:

5.7 Debugging

The Inspector and Interactors provide a number of mechanisms to help programmers debug programs. The primary one is a tracing mechanism that supports printing to standard output (cout) whenever an ``interesting'' Interactor or Command event happens. Amulet supplies many options for controlling when printout occurs, as described below. You typically would set these in the Inspector, but you can also set these parameters in your code.

typedef enum { Am_INTER_TRACE_NONE, Am_INTER_TRACE_ALL,
Am_INTER_TRACE_EVENTS, Am_INTER_TRACE_SETTING,
		 Am_INTER_TRACE_PRIORITIES, Am_INTER_TRACE_NEXT,
		 Am_INTER_TRACE_SHORT } Am_Inter_Trace_Options; 

void Am_Set_Inter_Trace(); //prints current status
void Am_Set_Inter_Trace(Am_Inter_Trace_Options trace_code);
void Am_Set_Inter_Trace(Am_Object inter_to_trace);
void Am_Clear_Inter_Trace();
By default, tracing is off. Each call to Am_Set_Inter_Trace adds tracing of the parameter to the set of things being traced (except for Am_INTER_TRACE_NONE which clears the entire trace set). The options for Am_Set_Inter_Trace are:

We have found that tracing an interactor will usually tell you why it didn't run when you expected it to, what it is changing, in case the right objects do not seem to be set, and what interactors are running and why, if wrong ones seem to be running. It is also useful to set breakpoints or traces on object slots using the Inspector to figure out why a slot is being set with incorrect values.


Last Modified: 03:23pm EDT, May 24, 1996