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.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.
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.
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.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.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_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.
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.
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).
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.
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:
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
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:
my_rect.Add_Part(select_it);
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.
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.
5.3.5 Specific Interactors
All of the interactors and command objects are summarized in Chapter 11, 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:
typedef enum (Am_CHOICE_SET, Am_CHOICE_CLEAR, Am_CHOICE_TOGGLE,
Am_CHOICE_LIST_TOGGLE } Am_Choice_How_Set;
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).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.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
Am_Define_Method(Am_Custom_Gridding_Method, void, keep_inside_window,
(Am_Object inter, int x, int y,
int& out_x, int & out_y)) { ... }
//see the file samples/space/space.cc for the complete code of this function
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.
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
Am_CREATE_NEW_OBJECT_METHOD: Set with a method to create the object; see next section.
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.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.5.3.5.5.1 Special Slots of Text Edit Interactors
Am_Text_Edit_Method (in inter.h) which is defined as:
Am_Define_Method_Type(Am_Text_Edit_Method, void,
(Am_Object text, Am_Input_Char ic, Am_Object inter));
Am_STOP_WHEN character, but pass the mouse down event on so it can do other actions as well.
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.
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.
Am_Polygon.Create()
Note: the feedback object must be added to a window or group, or it will never be drawn.
.Set (Am_FILL_STYLE, 0);
Am_Polygon with no fill style which is part of a group or window.
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.
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
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.
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. 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:
Am_Define_Formula (bool, grow_or_move) {
Am_Object start_object;
start_object = self.GV(Am_START_OBJECT);
if ((bool)start_object.GV(IS_A_MOVING_HANDLE)) return false;
else return true;
}
Am_Define_Formula (bool, as_line_if_shift) {
Am_Input_Char start_char =
Am_Input_Char::Narrow(self.GV(Am_START_CHAR));
if (start_char.shift) return true;
else return false;
}
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:
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.
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:
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.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.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. 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.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:
Am_Define_Method(Am_Mouse_Event_Method, void, my_mouse_method,
(Am_Object inter_or_cmd, int mouse_x, int mouse_y,
Am_Object ref_obj, Am_Input_Char ic));
Am_Define_Method(Am_Current_Location_Method, void, my_do_method,
(Am_Object inter, Am_Object object_modified,
Am_Inter_Location data)) { ... }
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:
5.5.1.1 Available slots of Am_Choice_Interactor and Am_One_Shot_Interactor
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 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_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.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.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:
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.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).
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:
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))
;
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:
Am_Set_Inter_Trace is called with no parameters, it prints out the current tracing status.