2. Amulet Tutorial

Amulet is a user interface development environment that makes it easier to create highly interactive, direct manipulation user interfaces in C++ for Microsoft Windows, Unix X/11, or the Macintosh. This tutorial introduces the reader to the basic concepts of Amulet. After reading this tutorial and trying the examples with a C++ compiler, the reader will have a basic understanding of: the prototype-instance system of objects in Amulet; how to create windows and display graphical objects inside them; how to constrain the positions of objects to each other using formulas; how to use interactors to define behaviors on objects (such as selecting objects with the mouse); how to collect objects together into groups; how to use the Amulet widgets; and how to use some of the debugging tools in Amulet.

2.1 Setting Up

2.1.1 Install Amulet in your Environment

Before beginning this tutorial, you should have already installed Amulet in your computing environment, and you should have a compiled version of the Amulet library. Instructions on how to do this can be found in the Amulet Overview, Section 1.4. This tutorial assumes that you are familiar with C++ and with the C++ development environment on your system.

In this tutorial, you will be introduced to the most commonly used parts of Amulet: the ORE Object system, Opal graphical object, Interactors, Command Objects, and Widgets. It includes code examples that can you can type in and compile yourself, along with discussions of Amulet programming techniques.

There is another programming interface to Amulet at the Gem layer (the Graphics and Events Module). By accessing the Gem layer, you can explicitly call the functions that Amulet uses to draw objects on the screen. Most Amulet users will not need to call Gem functions directly, because Amulet graphical objects redraw themselves automatically once they are added to a window. Gem is only needed by programmers who cannot get sufficient performance using the higher level interface.

2.1.2 Copy the Tutorial Starter Program

Throughout this tutorial, you will be typing and compiling code to observe its behavior. A starter program is installed with Amulet in the directory amulet/samples/tutorial/ in Unix, in amulet:samples\tutorial\ in Windows, and amulet:samples:tutorial in Macintosh. By following the instructions in this tutorial, you will iteratively edit and recompile this program in your local area while learning about Amulet.

Under Unix and Windows, copy the tutorial directory and its contents into your local filespace; on the Macintosh, duplicate the tutorial folder within the amulet:samples folder. You will edit this copy of the tutorial files while going through the tutorial, and not the original copy. Your copy of the directory should contain the files Makefile and tutorial.cc, if you're on a Unix platform, the files tutorial.cpp, tutorial.mak and tutorial.dsp if you're on the PC, or the files tutorial.cc, tutorial68K.proj, and tutorialpPC.proj if you're on a Macintosh.

You should now build the initial tutorial program to make sure everything is installed correctly. In Unix, simply type make on the command line. On the PC, open workspace tutorial.mak (MSVC++ 4.x) or tutorial.dsp (MSVC++ 5.0) with Microsoft Developer Studio and configure the project as described in Section On the Macintosh, open tutorial***.proj with CodeWarrior (where *** is either 68K or PPC). Next, build the project.

You should now be able to execute the tutorial program, which creates an empty window in the upper-left corner of the screen. Exit the program by placing the mouse in the Amulet window (and clicking in the window to make it active, if necessary) and typing the Amulet escape sequence, . The meta key is the diamond key on Sun keyboards, the EXTEND-CHAR key on HPs, the Command (Apple) key on Macintosh keyboards, and the ALT key on PC keyboards. On other Unix keyboards, it is generally whatever is used for ``meta'' by Emacs and the window manager.

If you have trouble copying the starter program or generating the tutorial executable, there may be a problem with the way that Amulet was installed at your site. Consult Section 1.4 for detailed instructions about installing Amulet.

2.2 The Prototype-Instance System

Amulet provides a prototype-instance object system built on top of the C++ class-object hierarchy. C++ classes are defined at compile time, and the amount and type of data stored in a C++ object cannot change at run time. The C++ class is an abstract description of how to make an object, but contains no data by itself. In Amulet, every object is ``real,'' and there is no underlying abstract class that describes an Amulet object at compile time. The prototype for an object in Amulet is another object, not an abstract class as in C++. All Amulet objects have the C++ type Am_Object.

The Amulet library provides many prototype objects which you can instantiate and customize in your programs. Examples include lines, circles, groups, windows, polygons, and so on. These prototypes have default values for all of the important properties of the object, such as size, position, and color. You can change these values in your instances of the objects, to customize their appearance and behavior. Any properties you do not customize will be inherited from the object's prototype.

While most of the objects we'll be working with in this Tutorial are graphical objects, Amulet objects are not necessarily tied down to graphical representations. Command objects and interactors are examples of nongraphical Amulet objects.

For a complete list of all of Amulet's default prototype objects, see Chapter 11, Summary of Exported Objects and Slots.

2.2.1 Objects and Slots

The properties of an Amulet object are stored in its slots, which are similar to a class's member variables in C++. A rectangle's slots contain values for its position (left, top), size (width, height), line-style, filling-style, and so on. In the following code, a rectangle is created and some of its slots are set with new values (it is not necessary to type in this code, it is just for discussion):

Am_Object my_rect = Am_Rectangle.Create (``my_rect'')
	.Set (Am_LEFT, 20)
	.Set (Am_TOP, 20)
	.Set (Am_LINE_STYLE, Am_Black)
	.Set (Am_FILL_STYLE, Am_Red);

int my_left = my_rect.Get (Am_LEFT);    // my_left has value 20
The Set operation sets the values of the objects' slots, and the corresponding Get operation retrieves them. Set takes a slot key, such as Am_LEFT, and a new value to store in the slot. Get takes a slot key and returns the value stored in the slot. A slot key is an index into the set of slots in an object.

There are many predefined slot keys used by Amulet objects, all starting with the ``Am_'' prefix, declared in the header file standard_slots.h. Slot keys that you create for your own use need to be explicitly declared with special Amulet functions, as in:

Am_Slot_Key MY_SLOT = Am_Register_Slot_Name ("MY_SLOT");
The Add operation is used to add new slots to an object that do not appear in the object's prototype. Like Set, Add takes a slot key and a value to store in the slot.

my_rect.Add (MY_SLOT, 43);
There are many examples of setting and retrieving slot values throughout this tutorial.

An important difference between C++ classes and Amulet objects is that Amulet allows the dynamic creation of slots in objects. An Amulet program can add and remove slots from an object as needed. In C++ classes, the value of the class's data can be modified at runtime, but changing the amount of data in an object requires recompiling your code.

2.2.2 Dynamic Typing

Another difference between Amulet objects and C++ classes involves the type restrictions of the values being stored. In C++ classes, you are restricted to declaring member variables of a specific type, and you can only store data of that type in the variables. In contrast, Amulet uses dynamic typing, where the type of a slot is determined by the value currently stored in it. Any slot can hold any type of data, and a slot's type can change whenever a new value is set into the slot.

Amulet achieves dynamic typing by overloading the Set and Get operators. There are versions of Set and Get that handle most simple C++ types including int, float, double, char, and bool. They also handle more general types like strings, Amulet objects, functions, and void*. Other types are encapsulated in a type called Am_Wrapper, which allows C++ data structures to be stored in slots.

For example:

int i = obj.Get(Am_LEFT);
Amulet looks in the object obj and tries to find its slot Am_LEFT. If the slot exists, and its value is an integer, it is assigned to i. If the value there is not an integer, however, this causes an error. If you do not know what type a slot contains, you can get the slot into a generic Am_Value type, which is the C++ class that Amulet uses to store slot values, or ask the slot what type it contains using obj.Get_Slot_Type (see Section 3.4).

2.2.3 Inheritance

When instances of an Amulet object are created, an inheritance link is established between the prototype and the instance. Inheritance allows instances to use slots in their prototypes without setting those slots in the instances themselves. For example, if we set the fill style of a rectangle to be gray, and then we create an instance of that rectangle, then the instance will also have a gray fill style.

This inheritance creates a hierarchy among the objects in the Amulet system. There is one object, Am_Graphical_Object, that all graphical objects are instances of. Figure 1 shows some of the objects in Amulet and how they fit into the inheritance hierarchy. Objects shown in bold are used by all Amulet programmers, while the others are internal, and intended to be accessed only by advanced users. The Am_Map and Am_Group objects are both special types of aggregates, and they inherit most of their properties from the Am_Aggregate prototype object. Some of their slots are inherited from Am_Graphical_Object through Am_Aggregate. The Widgets (the Amulet gadgets) are not pictured in this hierarchy, but most of them are instances of the Am_Aggregate object

To demonstrate inheritance, let's create an instance of a window and look at some of its inherited values. If you have followed the instructions in Section 2.1.2, you should have the file tutorial.cc (Unix and Mac) or tutorial.cpp (PC) in your local area. Open the file. You should see:.

#include <amulet.h>

main (void)
Am_Initialize ();

Am_Object my_win = Am_Window.Create (``my_win'')
.Set (Am_LEFT, 20)
.Set (Am_TOP, 50);

Am_Screen.Add_Part (my_win);
/* ************************************************************ */
/* During the Tutorial, do not add or edit text below this line */
/* ************************************************************ */

Am_Main_Event_Loop ();
Am_Cleanup ();
If you have not already compiled this file, do so now. In UNIX, invoke make in your tutorial/ directory to generate the tutorial binary. On the PC and Macintosh, select ``Build'' from the ``Project'' menu. Execute tutorial to create a window in the upper-left corner of the screen.

The tutorial program creates an object called my_win, which is an instance of Am_Window. A value of 20 was installed in its Am_LEFT slot and 50 in its Am_TOP slot. These values are reflected in the position of the window on the screen.

To check that the slot values are correct, bring up the Amulet Inspector to examine the slots and values of the window. Move the mouse over the window and press the F1 key. The Amulet Inspector will pop up a window that displays the slots and values of my_win, as shown in Figure 2. You will see many slots displayed, some of which are internal and not intended for external use. The slots with ``~'' in their names are internal slots. You can hide these internal slots by selecting the menu option View: Hide Internal Slots. Some of the other slots are ``advanced'' and should not be needed by most programmers. Chapter 11, Summary of Exported Objects and Slots, lists the primary exported slots of the main Amulet objects.

By default, object slots are sorted alphabetically by name in the Inspector. To turn this option off, choose View: Stop Sorting By Name from the Inspector's menu.


The Am_LEFT and Am_TOP slots of my_win shown in the Inspector contain the expected values. The Am_WIDTH and Am_HEIGHT slots contain values that were not set by the tutorial program. These values were inherited from the prototype. They were defined in the Am_Window object when it was created, and now my_win inherits those values from Am_Window as if you had set those slots directly into my_win. The Inspector shows they are inherited by displaying the slots in blue. Slots with local values are displayed in black. You can use the menu command View: Hide Inherited Slots to hide all of my_win's inherited slots.

To exit the tutorial program and destroy the Amulet window, position the mouse over the window (and click to select the window, if necessary) and type . You could also choose the Inspector's Objects: Quit Application menu item.

Let's change the width and height of my_win using Set, the function that sets the values of slots. Edit the source code, and add the following lines immediately after the definition of my_win:

my_win.Set (Am_WIDTH, 200)
.Set (Am_HEIGHT, 400);
Notice that we can cascade the calls to Set without placing semi-colons at the end of each line. Set makes this possible by returning the object that is being changed, so that the return value of Set can be used without intermediate binding. After compiling and executing the file, and hitting F1 to invoke the Inspector, you can see that we have successfully overridden the Am_WIDTH and Am_HEIGHT slots in my_win with our local values. If you move and resize the window from the window manager, the values in the inspector should change to reflect these changes as well.

The counterpart to Set is Get, which retrieves values from slots. The Inspector uses Get on my_win to obtain the values to print in the Inspector window. We can use Get directly by typing the following code into the source code, after the definition of my_win:

int left  = my_win.Get (Am_LEFT);
int width = my_win.Get (Am_WIDTH);
cout << "left == "  << left  << endl;
cout << "width == " << width << endl;

Delete the code we used to set the left, top, width, and height of the window, and see what values are printed by the cout statement when the program is run. You can see that Am_LEFT defaults to 0, and Am_WIDTH defaults to 100 if you don't set the slots explicitly.

The inheritance hierarchy shown in Figure 1 is traced from the leaves toward the root (from right to left) during a search for a value. Whenever we use Get to retrieve the value of a slot, the object first checks to see if it has a local value for that slot. If there is no value for the slot in the object, then the object looks to its prototype to see if it has a value for the slot. This search continues until either a value for the slot is found or the root object is reached. When no inherited or local value for the slot is found, an error is raised. This might occur if you are asking for a slot from the wrong object, or if you forget to add the slot to the object.

2.2.4 Instances

All of the objects displayed in a window are instances of other objects. In tutorial, my_win is an instance of Am_Window. Let's create several instances of graphical objects and add them to my_win. First, make sure that your window is large enough, at least 200x200. Change your definition of my_win to look something like this:

Am_Object my_win = Am_Window.Create ("my_win")
.Set (Am_LEFT, 20)
.Set (Am_TOP, 50)
.Set (Am_WIDTH, 200)
.Set (Am_HEIGHT, 200);

Am_Screen.Add_Part (my_win); // Puts my_win on the screen
Now we can create several graphical objects and add them to the window. Type the following code into the tutorial program after the definition of my_win, then recompile and execute tutorial.

	Am_Object my_arc = Am_Arc.Create ("my_arc")
.Set (Am_LEFT, 10)
.Set (Am_TOP, 10);

Am_Object my_text = Am_Text.Create ("my_text")
.Set (Am_LEFT, 80)
.Set (Am_TOP, 30)
.Set (Am_TEXT, "This is my_text");

Am_Object my_rect = Am_Rectangle.Create ("my_rect")
.Set (Am_LEFT, 10)
.Set (Am_TOP, 100)
.Set (Am_WIDTH, 180)
.Set (Am_HEIGHT, 80)
.Set (Am_FILL_STYLE, Am_Red);

my_win.Add_Part (my_arc)
.Add_Part (my_text)
.Add_Part (my_rect);
The circle, text, and rectangle will be displayed in the window. You can position the mouse over any of the objects and hit F1 to display the slots of the object in the Inspector. If you hit F1 while the mouse is over the background of the window, you will raise the Inspector for the window itself. While inspecting my_win, you can see at the bottom of the Inspector display that the new objects have been added as parts of the window.

Amulet supplies a large collection of objects that you can instantiate, including the basic graphical primitives like rectangles and circles, and the standard widgets like menus, buttons and scroll bars. Chapter 11, Summary of Exported Objects and Slots, lists the exported Amulet objects you can make instances of.

2.2.5 Prototypes

When programming in Amulet, inheritance among objects can eliminate a lot of duplicated code. If we want to create several objects that look similar, we could create each of them from scratch and copy all the values that we need into each object. However, inheritance allows us to define these objects more efficiently, by creating several similar objects as instances of a single prototype.

To start, look at the picture in Figure 3. We are going to define three rectangles with different fill styles and put them in the window. Using your current version of tutorial, make sure it will create a window of size at least 200x200.

Let's consider the design for the rectangles. The first thing to notice is that all of the rectangles have the same width and height. We will create a prototype rectangle which has a width of 40 and a height of 20, and then we will create three instances of that rectangle. To create the prototype rectangle, type the following.

	Am_Object proto_rect = Am_Rectangle.Create ("proto_rect")
.Set (Am_WIDTH, 40)
.Set (Am_HEIGHT, 20);
This rectangle will not appear anywhere, because it will not be added to the window. We will create three instances of this prototype rectangle, which will be displayed. Since the prototype has the correct values for the width and height, we only need to specify the left, top, and fill styles of our instances.

	Am_Object r1 = proto_rect.Create ("r1")
.Set (Am_LEFT, 20)
.Set (Am_TOP, 20)
.Set (Am_FILL_STYLE, Am_White);

Am_Object r2 = proto_rect.Create ("r2")
.Set (Am_LEFT, 40)
.Set (Am_TOP, 30)
.Set (Am_FILL_STYLE, Am_Opaque_Gray_Stipple);

Am_Object r3 = proto_rect.Create ("r3")
.Set (Am_LEFT, 60)
.Set (Am_TOP, 40)
.Set (Am_FILL_STYLE, Am_Black);

When you recompile and execute, you can see that the instances r1, r2, and r3 have inherited their width and height from proto_rect. You may wish to use the Inspector to verify this. With these three rectangles still in the window, we are ready to look at another important use of inheritance by changing values in the prototype.

Inspect proto_rect.You can do this by inspecting one of the three rectangles in the window, and using the right mouse button to click on <proto_rect> on the ``Instance of <proto_rect>'' line. Other ways to inspect this object include double left clicking on the object name, and choosing the Objects: Inspect Object menu item, or choosing Objects: Inspect Object Named... and typing ``proto_rect'' into the dialog box.

When you inspect proto_rect, the contents of the Inspector window will be replaced by the slots and values of proto_rect. You can bring up the new object in its own inspector window by holding down the shift key while clicking the right mouse button over proto_rect, or by double clicking on the object name and choosing Objects: Inspect In New Window.

The values of certain types of slots in the inspector can be changed by clicking on the slot's value, editing the value, and then hitting return. When you click the left mouse button in an integer or wrapper (object, style, font) value, a cursor appears and you can use standard Amulet text editing commands to change the value. Here is a brief summary of text editing commands (note: ``^f'' means control-f).

^f or rightarrow forward one character
^b or leftarrow backward one character
^a go to beginning of line
^e go to end of line
^h, DELETE, BACKSPACE delete previous character
^w, ^DELETE, ^BACKSPACE delete previous word
^d delete next character
^u delete entire string
^k kill (or delete) rest of line
^y, INSERT insert the contents of the cut buffer into the string at the
current point
^c copy the current string into the cut buffer
^g aborts editing and returns the string to the way it was
before editing started
leftdown (inside the string) move the cursor to the specified point
All other characters go into the string (except other control characters which beep).
By editing the values in the Inspector window, change the width of proto_rect to 30 and change its height to 40. The result should look like the rectangles in Figure 4. Just by changing the values in the prototype rectangle, we were able to change the appearance of all its instances. This is because the three instances inherit their width and height from the prototype, even when the prototype changes.

For our last look at inheritance in this section, let's override the inherited slots in one of the instances. Suppose we now want the rectangles to look like Figure 5. In this case, we only want to change the dimensions of one of the instances. Bring r3 (the black rectangle) up in the Inspector, and change the value of its width slot to 100.

The rectangle r3 now has its own value for its Am_WIDTH slot, and no longer inherits it from proto_rect. If you change the width of the prototype again, the width of r3 will not be affected. However, the width of r1 and r2 will change with the prototype, because they still inherit the values for their Am_WIDTH slots. This shows how inheritance can be used flexibly to make specific exceptions to the prototype object.

2.2.6 Default Values

Because of inheritance, all instances of Amulet prototype objects have reasonable default values when they are created. As we saw in Section 2.2.4, the Am_Window object has its own Am_WIDTH value. If an instance of it is created without an explicitly defined width, the width of the instance will be inherited from the prototype. This inherited value can be considered a default value for slots in an instance. Section 11 contains a complete list of Amulet objects and the default values of their slots.

2.2.7 Destroying Objects

After objects have fulfilled their purpose, it is appropriate to destroy them. All objects occupy space in memory, and continue to do so until explicitly destroyed (or the program terminates). A Destroy() method is defined on all objects, so at any point in a program you can do obj.Destroy() to destroy obj.

When you destroy a graphical object (like a line or a circle), it is automatically removed from any window or group that it might be in and erased from the screen. Destroying a window or a group will destroy all of its parts. Destroying a prototype also destroys all of its instances.

2.2.8 Unnamed Objects

Sometimes you will want to create objects that do not have a particular name. Or, you might not care what the name of an object is, so you'd rather not bother thinking of a name. For example, you may want to write a function that returns a rectangle, but it will be called repeatedly and should not return multiple objects with the same name. In this case, you should allow Amulet to generate a unique name for you.

As an example, the following code creates unnamed objects and displays them in a window. Instead of supplying a quoted name to Create, we invoke it with no parameters.

	Am_Object obj;
for (int i=0; i<10; i++) {
obj = Am_Rectangle.Create()
.Set (Am_LEFT, i*10)
.Set (Am_TOP, i*10);
my_win.Add_Part (obj);
When no name string is supplied to Create, Amulet generates a unique name for the object being created. In this case, something like <Am_Rectangle_5>. This name has a unique number as a suffix that prevents it from being confused with other rectangles in Amulet.

2.3 Graphical Objects

2.3.1 Lines, Rectangles, and Circles

The Opal module provides different graphical shapes including circles, rectangles, roundtangles, lines, text, bitmaps, and polygons. Each graphical object has special slots that determine its appearance, which are fully documented in chapter 4, Opal Graphics System and summarized in chapter 11, Summary of Exported Objects and Slots. Examples of creating instances of graphical objects appear throughout this tutorial.

2.3.2 Groups

In order to put a large number of objects into a window, we might create all of the objects and then add them, one at a time, to the window. However, this is usually not how we organize the objects conceptually. If we were to create a sophisticated interface with tool palettes, icons with labels, and feedback objects, we would not want to add each line and rectangle directly to the window. Instead, we would think of creating each palette from its composite rectangles, then creating the labeled icons, and then adding each assembled group to the window.

Grouping objects together like this is the function of the Am_Group object. Any graphical object can be part of a group - lines, circles, rectangles, widgets, and even other groups (note: Am_Window is not considered a graphical object, even though it does appear on the screen). Usually all the parts of a group are related in some way, like all the selectable icons in a tool palette.

Groups define their own coordinate system, meaning that the left and top of their parts is offset from the origin of the group. Changing the position of the group translates the position of all its parts. Groups also clip their parts to the bounding box of the group, meaning that objects outside the left, top, width, or height of the group are not drawn.

In Amulet terminology, a group is the owner of all of its parts. The Add_Part() and Remove_Part() methods are used to add and remove parts. You can optionally provide a slot key (a slot name, such as MY_PART) in an Add_Part() call. If a slot key is provided, then in addition to becoming a part of the group, the new part will be stored in that slot of the group. The Set_Part() method is used to change the part that is stored in a named slot. Parts with slot keys are always instantiated when instances of an existing group are created, and parts without a key are instantiated unless you specify otherwise. It is often convenient to provide slot keys for parts so that functions and formulas can easily access these objects in their groups.

Named slots can either be part slots, used to store graphical parts that are instantiated when new instances of an existing group are created, or non-part slots, used to store values that are inherited when new instances of an existing object are created. The same slot in the same object cannot be used to successively store both a non-part value and a graphical part, unless the slot is first removed using Remove_Slot() and then added back using Add or Add_Part, whichever is appropriate.

Objects may be added directly to a window or to a group which, in turn, has been added to the window. When groups have other groups as parts, a group hierarchy is formed.

In the scroll bar hierarchy, Figure 6, all of the leaves correspond to shapes that appear in the scroll bar. The leaves are always Amulet graphic primitives, like rectangles and text. The nodes Top_Trill_Box and Bottom_Trill_Box are both groups, each with two parts. And, of course, the top-level Scroll_Bar node is a group.

This group hierarchy should not be confused with the inheritance hierarchy discussed earlier. Parts of a group do not inherit values from their owners. Relationships among groups and their parts must be explicitly defined using constraints, a concept which will be discussed shortly in this tutorial.

2.3.3 Am_Group

Am_Group and Am_Map are used to form groups of other objects. They both define their own coordinate system, so that their parts are offset from the origin of the group.

You may create a group and add components to it in distinct steps, or you can use the cascading style of method invocation to perform all the Set and Add_Part operations in one expression. Here is an example of code implementing a group that contains an arc and a rectangle.

// Declared at the top-level, outside of main()
// You may install new slots in any object, but if they are not pre-defined Amulet slots,
// starting with the ``Am_'' prefix, then you must define them separately at the top-level.
// See
Section 2.2.1
Am_Slot_Key ARC_PART = Am_Register_Slot_Name ("ARC_PART");
Am_Slot_Key RECT_PART = Am_Register_Slot_Name ("RECT_PART");

// Defined inside of main()
Am_Object my_group = Am_Group.Create ("my_group")
.Set (Am_LEFT, 20)
.Set (Am_TOP, 20)
.Set (Am_WIDTH, 100)
.Set (Am_HEIGHT, 100)
.Add_Part(ARC_PART, Am_Arc.Create ("my_circle")
.Set (Am_WIDTH, 100)
.Set (Am_HEIGHT, 100))
.Add_Part(RECT_PART, Am_Rectangle.Create ("my_rect")
.Set (Am_WIDTH, 100)
.Set (Am_HEIGHT, 100)
.Set (Am_FILL_STYLE, Am_No_Style));

// Instances of my_group
Am_Object my_group2 = my_group.Create ("my_group2")
.Set (Am_LEFT, 150);

Am_Object my_group3 = my_group.Create ("my_group3")
.Set (Am_TOP, 150);

// Don't forget to add the graphical objects to the window!
my_win.Add_Part (my_group)
.Add_Part (my_group2)
.Add_Part (my_group3);
The Add_Part() method works like Add. It takes an optional slot key, and an object to install in the group. In addition to making the object an official part of the group, it is installed in the given slot in the group, if a slot key is supplied. The objects my_circle and my_rect are stored in slots ARC_PART and RECT_PART of my_group. The slots ARC_PART and RECT_PART are pointer slots because they point to other objects. These slots provide immediate access to these objects through my_group, which is useful when defining constraints among the objects. Once installed, the parts can be retrieved by name from the group with the methods Get() and Get_Object().

When an instance of my_group is created, its parts are duplicated in the new group. Groups my_group2 and my_group3 have the same structure as my_group, but at different positions. You can explicitly specify that a part should not be duplicated in instances of its owner by providing a second boolean parameter to Add_Part without a slot key. Object.Add_Part(my_part, false) will add my_part as a part to Object, but my_part will not be instantiated as a part of instances of Object.

2.3.4 Am_Map

A map is a kind of group that has many similar parts, all generated from a single prototype. In an Am_Map, a single object is defined to be an item-prototype, and instances of this object are generated according to a set of items. See chapter 4, Opal Graphics System, for details and examples of maps.

2.3.5 Windows

Any object must be added to a window in order for it to be shown on the screen. Or, the object must be added to a group that, in turn, has been added to a window. All objects in a window are continually redrawn as necessary while the Am_Main_Event_Loop() is running (see Section 2.5.6).

As shown in previous examples, objects are added to windows using the Add_Part() method. Subwindows can also be attached to windows using Add_Part(), using exactly the same syntax for adding groups or other graphical objects.

2.4 Constraints

In the course of putting objects in a window, it is often desirable to define relationships among the objects. You might want the tops of several objects to be aligned, or you might want a set of circles to have the same center, or you may want an object to change color if it is selected. Constraints are used in Amulet to define these relationships among objects.

Constraints can be arbitrary C++ code, and can contain local variables and calls to functions. They may also have side effects on unrelated data structures with no ill effect, including setting slots and creating and destroying other Amulet objects.

Although all the examples in this section use constraints on the positions of objects, it should be clear that constraints can be defined for fill styles, strings, or any other property of an Amulet object. Many examples of constraints can be found in the following sections of this tutorial.

2.4.1 Formulas

A formula is an explicit definition of how to calculate the value for a slot. If we want to constrain the top of one object to be the same as another, then we define a formula, and put it in the Am_TOP slot of the dependent object. With constraints, the value of one slot always depends on the value of one or more other slots, and we say the formula in that slot has dependencies on the other slots.

An important point about constraints is that they are always automatically maintained by the system. They are evaluated once when they are first created, and then they are re-evaluated when any of their dependencies change. If several objects depend on the top of a certain rectangle, then all the objects will change position whenever the rectangle is moved.

This section mostly discusses how to create your own, custom constraints. Many times, however, you can just use one of the built-in constraints from the library, as described in Section 2.4.6 and completely in the Opal Chapter (Section 4.13).

2.4.2 Declaring and Defining Formulas

There are several macros that are used to define formulas. These macros expand to conventional function definitions, but with special context information that Amulet uses to keep track of the constraint's dependencies. The particular macro you should use to define your formula depends on the type of the value to be returned from the formula.

Am_Define_Formula (return_type, formula_name) -- General purpose: returns specified return_type
Am_Define_No_Self_Formula (return_type, function_name) -- General purpose: returns specified return_type. Used when the formula does not reference the special self variable, so compiler warnings are avoided.
Am_Define_Value_List_Formula (formula_name) -- Return type is Am_Value_List
Am_Define_Object_Formula (formula_name) -- Return type is Am_Object
Am_Define_Style_Formula (formula_name) -- Return type is Am_Style
Am_Define_Font_Formula (formula_name) -- Return type is Am_Font
Am_Define_Point_List_Formula (formula_name) -- Return type is Am_Point_List
Am_Define_Image_Formula (formula_name) -- Return type is Am_Image_Array
Am_Define_Cursor_Formula (formula_name) -- Return type is Am_Cursor
To declare a formula in a header file to be exported, you should declare it of type Am_Formula. For example:

// inside my_file.h:
extern Am_Formula my_formula; // my_formula is defined in my_file.cc using Am_Define_Formula()

2.4.3 An Example of Constraints

As our first example of defining constraints among objects, we will make the window in Figure 7. Let's begin by creating the white rectangle at an absolute position, and then create the other objects relative to it.

The constraints in the following examples will reference global values, and it is essential that the object variables and formulas be defined at the top-level of the program, outside of main(). Create the window and the first box with the following code.

// Defined at the top-level, outside of main()
Am_Object my_win, white_rect, gray_rect, black_arc;

// Defined inside main()

// Create the window and display it on the screen
my_win = Am_Window.Create ("my_win")
.Set (Am_LEFT, 20)
.Set (Am_TOP, 50)
.Set (Am_WIDTH, 260)
.Set (Am_HEIGHT, 100);
Am_Screen.Add_Part (my_win);

// Create the white rectangle
white_rect = Am_Rectangle.Create ("white_rect")
.Set (Am_LEFT, 20)
.Set (Am_TOP, 30)
.Set (Am_WIDTH, 60)
.Set (Am_HEIGHT, 40)
.Set (Am_FILL_STYLE, Am_White);

// Add the rectangle to the window
my_win.Add_Part (white_rect);
We are now ready to create the other objects that are aligned with white_rect. We could simply create another rectangle and a circle that each have their top at 30, but this would lead to extra work if we ever wanted to change the top of all the objects, since each object's Am_TOP slot would have to be changed individually. If we instead define a constraint that depends on the top of white_rect, then whenever the top of white_rect changes, the top of the other objects will automatically change, too.

Define and use a constraint that depends on the top of white_rect as follows:

// Define this at the top-level, outside of main()
Am_Define_No_Self_Formula (int, top_of_white_rect) {
// The formula is named top_of_white_rect, and returns an int
return white_rect.Get (Am_TOP);

// Define this inside main(), after white_rect
gray_rect = Am_Rectangle.Create ("gray_rect")
.Set (Am_LEFT, 110)
.Set (Am_TOP, top_of_white_rect)
.Set (Am_WIDTH, 60)
.Set (Am_HEIGHT, 40)
.Set (Am_FILL_STYLE, Am_Gray_Stipple);

my_win.Add_Part (gray_rect);
Without specifying an absolute position for the top of the gray rectangle, we have constrained it to always have the same top as the white rectangle. The formula in the Am_TOP slot of the gray rectangle was defined using the macro Am_Define_No_Self_Formula. Like the other Am_Define_Formula macros, the Am_Define_No_Self_Formula macro helps to define a function to be used as a constraint. The formula is named top_of_white_rect, and returns an int.

When a constraint is used to set the value of a slot, it establishes dependencies between that slot and any slots referenced by the constraint, so that the formula will be reevaluated when the value in the referenced slot changes.

To see if our constraint is working, bring up the Inspector on white_rect by hitting F1 while the mouse is positioned over the white rectangle. Change the top of white_rect and notice how the gray rectangle stays aligned with its top. This shows that the formula in gray_rect is being re-evaluated whenever the value it depends on changes.

Alternatively, instead of creating our own top_of_white_rect formula, we could have used the built-in formula Am_From_Object, which gets the specified slot from the specified object:

gray_rect.Set(Am_TOP, Am_From_Object(white_rect, Am_TOP);
Warning: Note that the constraint references white_rect as a global variable. It is important that the global variable white_rect be set before the constraint is first evaluated, because otherwise a NULL object will get dereferenced. Since you have no control over when the constraint is evaluated, it is a good idea to make sure that you assign the object into the global variable before the constraint is set into the slot of the other object, as is the case in these examples.

Now we are ready to add the black circle to the window. We have a choice of whether to constrain the top of the circle to the white rectangle or the gray rectangle. Since we are going to be examining these objects closely in the next few paragraphs, let's constrain the circle to the gray rectangle, resulting in an indirect relationship with the white one.

Define another constraint and the black circle with the following code.

// Define this at the top-level, outside of main()
Am_Define_Formula (int, top_of_gray_rect) {
return gray_rect.Get (Am_TOP);

// Define this inside main(), after gray_rect
black_arc = Am_Arc.Create ("black_arc")
.Set (Am_LEFT, 200)
.Set (Am_TOP, top_of_gray_rect)
.Set (Am_WIDTH, 40)
.Set (Am_HEIGHT, 40)
.Set (Am_FILL_STYLE, Am_Black);

my_win.Add_Part (black_arc);
At this point, you may want to inspect the white rectangle again and change its top just to make sure the black circle follows the gray rectangle.

2.4.4 Values and constraints in slots

What happens if you set the Am_TOP of the gray rectangle now? The default for most slots, including the Am_TOP slot of Am_Rectangle, is that the new value replaces any formula in the slot. Bring up the gray rectangle in the inspector. Notice that the inspector tells you there is a constraint in the rectangle's Am_TOP slot. Change the Am_TOP of the gray rectangle by editing the value in the inspector window. You should see the grey rectangle move in the application window. Also, the inspector should no longer show a constraint in the rectangle's Am_TOP slot. The rectangle's position will not be recalculated by the constraint if white_rect moves, because the formula that was in the slot has been destroyed and replaced with a constant value.

In some slots of certain objects, such as the button widgets, there are formulas in the slots by default which are required to maintain proper behavior of the objects. If the formulas were to be destroyed, the object would no longer work as expected. These slots have a special flag set which tells Amulet to keep the formula around even if you set the slot with a new value, and to reevaluate the formula if any of its dependencies change. Setting these slots with a new value does not replace the formula in the slot, it simply overrides the current cached value of the formula.

Any slot can be set so that formulas will not be destroyed when the slot is set. This feature is described in chapter 3, ORE Object and Constraint System.

2.4.5 Constraints in Groups

As mentioned in Section 2.3.3, parts can be stored in pointer slots of their group, making it easier for the parts to reference each other. Additionally, the owner is set in each part as they are added to a group. In this section, we will examine how pointer slots and the owner slot can be used to communicate among parts of a group.

These examples also go over the difference between directly referencing objects, as was done above by using a global variable containing the object name, and referencing objects indirectly, by getting the objects out of slots of other objects. It is generally better to reference objects indirectly, as will be described next, because using global variables means that the constraints can only be used to refer to one object, whereas indirection allows you to re-use the constraint in multiple places referring to different objects. Another problem with global variables is that you must insure that the variable is set before the first time the constraint is evaluated.

The group we will use in this example will make the picture of concentric shapes in Figure 8. Suppose we want to be able to change the size and position of the shapes easily, and by setting as few slots as possible.

From the picture, we see that the dimensions of the rectangle are the same as the diameter of the circle. It will be helpful to put slots for the size and position at the top-level of the group, and have the parts reference these top-level values through formulas.

// Declared at the top-level, outside of main()
Am_Slot_Key ARC_PART = Am_Register_Slot_Name ("ARC_PART");
Am_Slot_Key RECT_PART = Am_Register_Slot_Name ("RECT_PART");

//self is an Am_Object parameter to all formulas that holds the object the constraint is in.
// The Am_Define_Formula macro expands to define self and some other necessary variables.
Am_Define_Formula (int, owner_width) {
return self.Get_Owner().Get(Am_WIDTH);

Am_Define_Formula (int, owner_height) {
return self.Get_Owner().Get(Am_HEIGHT);

// Defined inside of main()
Am_Object my_group = Am_Group.Create ("my_group")
.Set (Am_LEFT, 20)
.Set (Am_TOP, 20)
.Set (Am_WIDTH, 100)
.Set (Am_HEIGHT, 100)
.Add_Part(ARC_PART, Am_Arc.Create ("my_circle")
.Set (Am_WIDTH, owner_width)
.Set (Am_HEIGHT,owner_height))
.Add_Part(RECT_PART, Am_Rectangle.Create ("my_rect")
.Set (Am_WIDTH, owner_width)
.Set (Am_HEIGHT, owner_height)
.Set (Am_FILL_STYLE, Am_No_Style));
Both parts of my_group get their position and dimensions from the top-level slots in my_group. The reference to my_group from the arc is through the Get_Owner() function, which links the part to its group. The special variable self is used in the formulas to reference slots within the object that the formula is installed on. The arc's left and top are relative to the origin of my_group, so as it inherits a position of (0,0) from the Am_Arc prototype, it will appear at (20,20) in the window.

Notice that the parts do not ``inherit'' any values from their owner. Adding parts to a group sets up a group hierarchy, where values travel back-and-forth over constraints, not inheritance links. If you want a part to depend on values in its owner, you have to define constraints.

The slot names for the parts could have been used to define the constraints, also. Instead of asking its owner for its dimensions, the rectangle part could have asked the arc for its dimensions. In this example the result would be the same, but here are alternate definitions for the rectangle's width and height formulas to illustrate the use of aggregate pointer slots:

Am_Define_Formula (int, arc_width) {
return self.Get_Owner().Get(ARC_PART).Get(Am_WIDTH);

Am_Define_Formula (int, arc_height) {
return self.Get_Owner().Get(ARC_PART).Get(Am_HEIGHT);

2.4.6 Common Formula Shortcuts

There are many constraints which are used very commonly, such as getting a slot value from the object's owner, or getting the value directly from another slot in the same object. There are some built in functions in Amulet to make these common constraints easier to use.

The following code can be used in main() to define my_group, instead of the code given above. This code does not require the two Am_Define_Formula() calls:

	// Defined inside of main()
Am_Object my_group = Am_Group.Create (``my_group'')
.Set (Am_LEFT, 20)
.Set (Am_TOP, 20)
.Set (Am_WIDTH, 100)
.Set (Am_HEIGHT, 100)
.Add_Part(ARC_PART, Am_Arc.Create (``my_circle'')
.Set (Am_WIDTH, Am_From_Owner (Am_WIDTH))
.Set (Am_HEIGHT, Am_From_Owner (Am_HEIGHT)))
.Add_Part(RECT_PART, Am_Rectangle.Create (``my_rect'')
.Set (Am_WIDTH, Am_From_Sibling (ARC_PART, Am_WIDTH))
.Set (Am_HEIGHT, Am_From_Sibling (ARC_PART, Am_HEIGHT))
.Set (Am_FILL_STYLE, Am_No_Style));

2.5 Interactors

Amulet's graphical objects do not directly respond to input events. Instead, you create invisible interactor objects and attach them to graphical objects to respond to input. Sometimes you may just want a function to be executed when the mouse is clicked, but often you will want changes to occur in the graphics depending on the actions of the mouse. Examples include moving objects around with the mouse, editing text with the mouse and keyboard, and selecting an object from a given set.

Interactors are described in detail in chapter 5, Interactors and Command Objects for Handling Input, and a summary of interactors can be found in the object summary, Section 11.7. It is important to note that all of the widgets (Section 2.6 and chapter 7, Widgets) come with their interactors already attached. You do not need to create interactors for the widgets.

Interactors communicate with graphical objects by setting slots in the objects in response to mouse movements and keyboard keystrokes. Interactors generate side effects in the objects that they operate on. For example, the Am_Move_Grow_Interactor sets the left, top, width, and height slots of objects. The Am_Choice_Interactor sets the Am_SELECTED and Am_INTERIM_SELECTED slots to indicate when an object is currently being operated on. You might define formulas that depend on these special slots, causing the appearance of the objects (i.e., the graphics of the interface) to change in response to the mouse. The examples in the following sections show how you can use interactors this way.

Another way to use interactors (and widgets) is through their command objects (Section 2.5.5). Command objects contain methods that support undo, help, and selective enabling of operations associated with interactors and widgets. They can also contain a custom function that will be executed whenever the user operates the interactor or widget.

Figure 9 shows the general data flow when input events occur: the user hits a keyboard key or a mouse event, which is passed to the window manager. The Gem layer of Amulet converts it into a machine-independent form and passes it to the Interactors which finds the right interactor object to handle the event. Each interactor has an embedded command object that causes the appropriate action to take place. If this interactor is part of a widget, then the command object in the interactor calls the widget's command object. Eventually, some graphics will be modified in the Opal layer, which is automatically transformed into drawing calls at the Gem level, and then to the window manager.

In this section we will see some examples of how to change graphics in conjunction with interactors. Section 2.7.2 describes how to use an important debugging function for interactors called Am_Set_Inter_Trace(). Although this tutorial only gives examples of using the Am_One_Shot_Interactor and Am_Move_Grow_Interactor, there are examples of interactors in the sample applications and test programs included with the Amulet files. See for example, samples/space.cc in your Amulet source files. Instructions for compiling and running the samples are in the Overview chapter.

2.5.1 Kinds of Interactors

The design of the interactors is based on the observation that there are only a few kinds of behaviors that are typically used in graphical user interfaces. Below is a list of the available interactors.

2.5.2 The Am_One_Shot_Interactor

In this example, we will perform an elementary operation with an interactor. We will create a window with a white rectangle inside, and then create an interactor that will make it change colors when the mouse is clicked inside of it. First, make sure you have working code that creates a window (maybe from Section 2.2.4), then add the following definitions to your program. Remember to add the rectangle to your window using Add_Part().

// Defined at the top-level, outside of main()
Am_Define_Style_Formula (compute_fill) {
// Sometimes you need to cast the value returned from Get,
	// since a slot can contain any type of object.
if ((bool) self.Get (Am_SELECTED))
return Am_Black;
return Am_White;

// Defined inside main()
Am_Object changing_rect = Am_Rectangle.Create ("changing_rect")
.Set (Am_LEFT, 20)
.Set (Am_TOP, 30)
.Set (Am_WIDTH, 60)
.Set (Am_HEIGHT, 40)
.Add (Am_SELECTED, false) // Slot not present in prototype -- use Add not Set
.Set (Am_FILL_STYLE, compute_fill);

my_win.Add_Part (changing_rect);
The slot Am_SELECTED is not present in the Am_Rectangle prototype, so it must be initialized using Add() rather than Set() or a run-time error will occur.

From the definition of the compute_fill formula, you can see that if the Am_SELECTED slot in changing_rect were set to true, then its color would turn to black. You can test this by bringing up the Inspector on changing_rect, and changing the value of the slot to 1. Setting the Am_SELECTED slot is one of the side effects of the Am_One_Shot_Interactor. The following code defines an interactor which will set the Am_SELECTED slot of an object, and attaches it to changing_rect.

	Am_Object color_inter = Am_One_Shot_Interactor.Create ("color_inter");

changing_rect.Add_Part (color_inter);
Now you can click on the rectangle repeatedly and it will change from white to black, and back again. From this observation, and knowing how we defined the compute_fill formula of changing_rect, you can conclude that the Am_One_Shot_Interactor is setting (and clearing) the Am_SELECTED slot of the object. This is one of the functions of this type of interactor.

2.5.3 The Am_Move_Grow_Interactor

From the previous example, you can see that it is easy to change the graphics in the window using the mouse. We are now going to define several more objects in the window and create an interactor to move and grow them. The following code creates a prototype circle and several instances of it.

	Am_Object moving_circle = Am_Arc.Create ("moving_circle")
.Set (Am_WIDTH, 40)
.Set (Am_HEIGHT, 40)
.Set (Am_FILL_STYLE, Am_No_Style);

Am_Object objs_group = Am_Group.Create ("objs_group")
.Set (Am_WIDTH, Am_Width_Of_Parts)
.Set (Am_HEIGHT, Am_Height_Of_Parts)
.Add_Part (moving_circle.Create())
.Add_Part (moving_circle.Create().Set (Am_LEFT, 50))
.Add_Part (moving_circle.Create().Set (Am_LEFT, 100));
The predefined constraints, Am_Width_Of_Parts and Am_Height_Of_Parts, compute the size of a group based on the size of its parts.

Now let's create an instance of the Am_Move_Grow_Interactor which will cause the moving circles to change position. The following interactor, when added to objs_group, works on all the parts of that group.

Am_Object objs_mover = Am_Move_Grow_Interactor.Create ("objs_mover");

By default, interactors try to figure out which graphical object they're supposed to manipulate. If the interactor is attached to a group-like object (Am_Window, Am_Screen, Am_Group or Am_Scrolling_Group), it looks for a part of that object to act on. Otherwise, it tests whether the mouse is directly in the object the interactor is attached to. You can change this default when you want to specify exactly what objects the interactor should operate on, by setting the interactor's Am_START_WHERE_TEST slot. Other methods for the Am_START_WHERE_TEST are described in Section, or you can write your own start-where-test procedure to return the appropriate object.

Compile and run tutorial again. Now you can drag the circles around using the left mouse button. The interactor activates when you push the left button down inside any of the parts of objs_group. As long as you hold the button down, it moves the objects around by setting their left and top slots.

While the tutorial is running, inspect the objs_mover interactor. To do this, first bring up the inspector window on any of the objects on the screen. Then choose the Objects: Inspect Object Named... option from the menu, type in objs_mover, and hit return (or click Okay). Click in the value field of the objs_mover's Am_GROWING slot and change the value to 1. Now dragging the circles will cause them to change size rather than move.

2.5.4 A Feedback Object with the Am_Move_Grow_Interactor

Now let's add a feedback object to the window that will work with the moving circles. In this case, the feedback object will appear whenever we click on and try to drag a circle. The mouse will drag the feedback object, and then the real circle will move to the final position when the mouse is released.

Our feedback object will be a circle with a thick line. The feedback_circle object defined below will have its left, top, and visible slots set by the interactor. Given our moving_circle prototype, the feedback object is easy to define:

Am_Object feedback_circle = moving_circle.Create ("feedback_circle")
.Set (Am_LINE_STYLE, Am_Line_8)
.Set (Am_VISIBLE, false);

my_win.Add_Part (feedback_circle);

// The definition of the interactor, with feedback object
Am_Object objs_mover = Am_Move_Grow_Interactor.Create ("objs_mover")
.Set (Am_START_WHERE_TEST, Am_Inter_In_Part)
.Set (Am_GROWING, true) // Makes the circles grow instead of move
.Set (Am_FEEDBACK_OBJECT, feedback_circle);

objs_group.Add_Part (objs_mover);

// Don't forget to add feedback_circle and objs_mover to the right owners!
The Am_VISIBLE slot of feedback_circle is set to false, because we do not want it visible unless it is being used by objs_mover. The interactor will set the Am_VISIBLE slot to true and false when appropriate. Now when you move or grow the circles with the mouse, the feedback object will follow the mouse, instead of the real circle following it directly.

2.5.5 Command Objects

All interactors and widgets have command objects associated with them stored as their Am_COMMAND part. Command objects contain functions that determine what the interactor will do as it operates. For example, you can store a function in a command object that will be executed as the interactor runs in order to cause side-effects in your program. See Section 5.6 for more information on command objects inside interactors.

You can also store methods in a command object to support undo, help, and selective enabling of operations. There is a library of pre-defined command objects, so you can often use a command object from the library without writing any code. Section 7.4, Supplied Command Objects, describes the predefined command objects. See example1 and space for sample code that uses command objects.

Most interactors do three different things. As they run, they directly modify the associated graphical objects or feedback objects (like setting the Am_SELECTED slot). When they're finished running, they set their Am_VALUE slot and the Am_VALUE slot of their attached command object, and finally they call the Am_DO_METHOD of the attached command object.

To use the Am_VALUE slot of the interactor or its command object, you can establish a constraint from your object to either of these slots. If you want to make the interactor do a certain action only after it's finished running, it's best to build a custom command object. This is similar to providing a callback for the interactor to call when it's finished running. Both of these methods of using the results of an interaction are described in Section 2.6.

2.5.6 The Am_Main_Event_Loop

In order for interactors to perceive input from the mouse and keyboard, the main event loop must be running. This loop constantly checks to see if there is an event, and processes it if there is one. The automatic redrawing of graphics also relies on the main-event-loop. Exposure events, which occur when one window is uncovered or exposed, cause Amulet to refresh the window by redrawing the objects in the exposed area.

All Amulet programs should call two routines at the end of main(). Am_Main_Event_Loop() should be called, followed by Am_Cleanup(), which destroys the resources Amulet allocated. Your program will continue to run until Amulet perceives the escape sequence, which by default is META_SHIFT_F1. Typically, your program will have some sort of Quit button. Its do method should call Am_Exit_Main_Event_Loop(), which will cause the main event loop to terminate.

2.6 Widgets

The Amulet Widgets are a set of ready made gadgets that can be added directly to a window or a group just like other graphical objects. You do not have to define separate interactors to operate the gadgets, they already have their own interactors. They have slots that can be set to customize their appearance and behavior. The Amulet Widgets are common interface building objects such as scroll bars, menus, buttons and editable text fields. Section 11.8 summarizes the widget objects, and chapter 7, Widgets, discusses them all in detail.

The Widgets are available in several versions, simulating the look-and-feel of the standard widgets available in the Motif, Windows, and Macintosh toolkits. These widgets work on all platforms. Examples that use widgets can be found in several Amulet demos, including samples/example/example1 and samples/space. See Section 1.5 for information about the sample applications and test programs.

In this section we will use a radio button panel and a scroll bar to change the appearance of a rectangle.

There are two ways to interact with widgets. You can define a formula that depends on the value of the widget, or you can define a method to be executed by the widget's command object whenever the user activates the widget.

The code below defines the radio button panel pictured in Figure 11. Here, we define a formula for the fill style of the rectangle that depends on the value of the button panel. This formula is reevaluated every time the buttons are operated, so the rectangle changes color.

// Declared at the top-level, outside of main()
Am_Object color_buttons, color_rect;

// Declared at the top-level, outside of
Am_Define_Style_Formula (color_from_panel) {
Am_String s = color_buttons.Get (Am_VALUE);
if ((const char*)s) {
if (strcmp(s, "Red") == 0) return Am_Red;
else if (strcmp(s, "Blue") == 0) return Am_Blue;
else if (strcmp(s, "Green") == 0) return Am_Green;
else if (strcmp(s, "Yellow") == 0) return Am_Yellow;
else if (strcmp(s, "Orange") == 0) return Am_Orange;
else return Am_White;
else return Am_White;

// Defined inside
color_buttons = Am_Radio_Button_Panel.Create("color_buttons")
.Set (Am_LEFT, 10)
.Set (Am_TOP, 10)
	  .Set (Am_ITEMS, Am_Value_List () // An Am_Value_List supports an arbitrary list
	        .Add("Red")               // of dynamically typed values
.Set (Am_FILL_STYLE, Am_Motif_Gray);

// Defined inside
color_rect = Am_Rectangle.Create("color_rect")
.Set(Am_LEFT, 100)
.Set(Am_TOP, 50)
.Set(Am_WIDTH, 50)
.Set(Am_HEIGHT, 50)
.Set(Am_FILL_STYLE, color_from_panel);

my_win.Add_Part (color_buttons)
.Add_Part (color_rect);
Now let's create the scroll bar to change the position of the rectangle. We could define a formula that depends on the value of the scroll bar. Instead, let's use the Am_DO_METHOD of the scroll bar's command object to call a function each time the widget is operated.

// Defined at the top-level, outside of main()
Am_Object my_scrollbar;

// Defined at the top-level, outside of
Am_Define_Method(Am_Object_Method, void, my_scrollbar_do, (Am_Object cmd))
int value = cmd.Get(Am_VALUE);
color_rect.Set (Am_TOP, 20 + value);

// Defined inside
my_scrollbar = Am_Vertical_Scroll_Bar.Create ("my_scrollbar")
.Set (Am_LEFT, 250)
.Set (Am_TOP, 10)
.Set (Am_VALUE_1, 0)
.Set (Am_VALUE_2, 100);

my_scrollbar.Get_Object(Am_COMMAND).Set(Am_DO_METHOD, my_scrollbar_do);

my_win.Add_Part (my_scrollbar);
Am_Define_Method is a macro that defines a method of an Amulet object. The first parameter is the type of the method being defined. Do methods are of type Am_Object_Method, meaning they take one parameter, an Amulet object, and have return type void. The second parameter to Am_Define_Method is the return type of the method, and then comes the method's name. Last is the method's parameter list, with an extra set of parentheses. For more information about Amulet method type declaration and method definition, see Section 3.4.9.

Compile and run the tutorial. The radio buttons will control the color of the rectangle, and the scrollbar will control its position on the screen.

2.7 Debugging

2.7.1 The Inspector

The Inspector is an important tool for examining properties of objects. As long as you compile with the DEBUG switch set on in your makefile or project, you will get all of the inspector code. The inspector will be automatically initialized when you call Am_Initialize(). While running your program, press the F1 key over an object to inspect it.

If your keyboard does not have an F1 key, or hitting it does not seem to do anything, you can start the Inspector from your program by calling the function Am_Inspect(obj) with the object you want to inspect as its argument. See Chapter 10, Debugging and the Inspector for details. The method obj.Text_Inspect() prints the object's slots and values to stdout instead of popping up an interactive window, and is sometimes useful in a debugging environment such as gdb.

By default, the Inspector shows all of an object's inherited and local slots, sorted by name, with the inherited slots shown in blue, and the local slots shown in black. You can hide inherited slots by choosing the menu item View: Hide Inherited Slots. You can hide an object's internal slots (those you shouldn't modify) with the menu item View: Hide Internal Slots.

By defaults, the parts of an object are displayed. This can be turned off with the View: Hide Parts command. You can show instances of the object being displayed by choosing the View: Show Instances menu item.

You can edit the value of many slots in the inspector. Editing an inherited value causes the value to become local, and changes its color in the inspector from blue to black. You can edit integers, strings, Amulet Objects, Styles, Fonts, Images, and Methods from the inspector. You cannot edit Value Lists, Constraints, or parts and instances of an object.

In the Inspector window, clicking the right mouse button over a value that is an object will inspect that object in the same inspector window. To display an object in a new window, hold down the SHIFT key while pressing the right mouse button over its name in the Inspector window. You can specify the name of an object to inspect by using the Objects: Inspect Object Named...option. When you are finished with the Inspector, you can choose the Objects: Done menu item to make the current Inspector window disappear, or choose Objects: Done All if you want all of the inspector windows to be destroyed.

By default, the inspector automatically updates its contents if slots in the objects you're inspecting change. This can bog down performance when you're inspecting certain active objects. To turn off automatic refresh, choose View:Manual Refresh. To refresh the display in manual refresh mode, choose Objects: Refresh Display.

You can select slots, constraints, or objects by double clicking on their name in the Inspector. This will enable various menu items such as showing the object's prototype and instances, showing a slot's properties, or displaying a constraint's dependencies.

Sometimes you might not know exactly which object you're inspecting. Or, you might want to find out why you can't see a particular object which you think should be on the screen. With the mysterious object on the screen, choose Objects: Flash Object. This will cause the bounding box of the object to blink several times so you can see where it is. If you wouldn't be able to see the object flash (it's not attached to a window, it's invisible, etc.), Amulet tries to figure out why not, and prints a message to cerr describing why it thinks the object could not flash.

2.7.2 Tracing Interactors

The interactors and widgets provide a number of mechanisms to help programmers debug their interactions. The primary one is a tracing mechanism that supports printing to standard output (cout) whenever an ``interesting'' interactor event happens. Amulet supplies many options for controlling when printout occurs, as described below (full details are in the Interactors chapter). You can either set these parameters in your code and recompile, or they can be dynamically changed as your application is running, using the Interactors menu of the Inspector window.

The tracing choices in the Inspector's Interactors menu are:

2.8 To Learn More

The Amulet sample applications include two example programs that demonstrate common Amulet idioms. Example1, in samples/examples/example1.cc, is a simple editor for creating objects. It demonstrates:

1) How to organize objects in groups.
2) How to attach interactors and widgets to groups so they only operate on the correct objects.
3) How to enable and disable interactors based on a global palette.
4) Use of the menubar, standard, built-in command objects, and the standard selection handles widget.
5) How to use the standard load/save mechanism.
Example2, in samples/examples/example2.cc, uses dialog boxes and the Am_Text_Input_Widget. It demonstrates:

1) Using text input fields.
2) Constraining a value to be the same as a text field's value.
3) Popping up a dialog box and waiting for the answer.
4) Using the error built-in dialog box.
5) Using a dialog box created using the Gilt Interface Builder.
6) Using the Am_Tab_To_Next_Widget_Interactor to allow the user to tab among the fields.
7) Using a filter on text input field values.
8) Using a number entry field.
We also encourage you to experiment with the Gilt tool, described in Chapter 8, The Gilt Interface Builder.

Last Modified: 01:35pm EDT, August 13, 1997