ORE is used in Amulet as the means for representing all higher-level graphical concepts. Rectangles, for instance, are created using an exported object called Am_Rectangle. The process for moving a rectangle is also represented as an object. It is called Am_Move_Grow_Interactor. Lower-level graphical features like colors and fonts are not ORE objects in Amulet. Instead they are ``wrapper types'' or ``wrapper values,'' ``wrapper objects,'' or just plain ``wrappers.'' What makes wrappers different from regular C++ objects is that they contain data that derives from the class Am_Wrapper. This makes it easy to fetch and store them in ORE objects.
The coding style for ORE objects is declarative. That means the values and behaviors of objects are specified mostly at the time an object gets created by storing initial values and declaring constraints in the needed slots. All high level objects defined in Amulet are designed to be used in a declarative way. Normal programming practice consists of choosing the kinds of objects your program needs, finding out what slots these objects contain and what the semantics of the slots do, and finally assigning values to the slots and grouping the objects together to make the final application.
Many of the concepts and processes in ORE are derived from the Garnet object system called KR. KR differs from ORE in that it was originally designed to be a artificial intelligence knowledge representation framework. The graphics came later and KR underwent an evolution that made it more compatible with the demands of a graphical system. ORE begins where KR left off. In ORE, some KR features were abandoned like multiple inheritance. Many of the good KR features that only made it into KR in the last couple releases have been put into ORE right from the start. These features include dynamic type checking and an efficient algorithm for formula propagation and evaluation. And, of course, there are many brand new features in ORE that were never part of KR. Things like the owner-part hierarchy and the ability to install multiple constraint solvers which are hoped to become very useful to Amulet programmers.
ORE features like defining new wrapper types and writing a new constraint solver are quite advanced and are covered in Section 3.11 of this chapter. These sorts of features are not necessary for the novice Amulet programmer to make working applications, but are intended to be used by system programmers or researchers that want to extend Amulet.
3.2 Include Files
The various objects, types and procedures described in this chapter are spread out though several .h files. Typically, one will include amulet.h in the code which automatically includes all the files needed. This chapter tells where various things are defined so one can look up their exact definitions. The main include files relevant to ORE are:
3.3 Objects and Slots
The Amulet object system, ORE, supports a ``prototype-instance'' object system. Essentially, an object is a collector for data in the form of ``slots.'' Each slot in an object is similar to a field in a structure. Each slot stores a single piece of data for the object. A Am_Rectangle object, for example, has a separate slot for its left, top, width, and height. 3.3.1 Get and Set
The basic operations performed on slots are Get and Set. A slot is essentially a key-value pair. Get takes a slot key and returns the slot's value. Set takes a key and value and replaces the slot's previous value with the one given. Creating new slots is done by performing Set using a key that has not been used before in that object. Another way to think of an object is as a name space for slot keys. A single key can have only one value in a given object.my_object.Set (Am_LEFT, 5); // Set left slot to value 5
int position = my_object.Get (Am_TOP); // Get the value of slot Am_TOP
my_object.Set (Am_ANGLE1, 45.3f); // Set the angle1 slot to float 45.3
Calling Get on a slot which does not exist raises an error. Make sure slots are initialized with values to avoid this problem. To test whether a slot exists yet, use the Get_Slot_Type method on objects (see Section 3.3.3), or else use the Am_Value form of Get (see Section 3.3.9).
For convenience, a special Get_Object method is available to fetch slots that are known to store a Am_Object value. This is useful for chaining together a series of Gets onto a single line without having to store into a variable or use an explicit cast. It is an error to use this method on a slot which does not store an object.
int i = object.Get_Object (OBJECT_SLOT).Get_Owner ().
Get_Part (PART_SLOT).Get (Am_LEFT);
3.3.2 Slot Keys
A slot key in ORE is an unsigned integer. An example of a slot key is Am_LEFT. Most slot keys are defined by the Am_Standard_Slot_Keys enumeration in the file standard_slots.h. Am_LEFT turns out to be the integer 100, but one uses the name Am_LEFT because it is more descriptive. The Am_LEFT slot is used in all the graphical objects like rectangles, circles, and windows and it represents the leftmost position of that object. Potentially, the slot key 100 could be used in another object with semantics completely different from those used in graphical objects, in essence 100 could be a key besides Am_LEFT. However, ORE provides mechanisms to avoid this kind of inconsistency and makes certain that integers and slot names map one to one. The string names associated with slots are mainly used for debugging. For example, they are printed out by the inspector. The string names for slots are not used during normal program execution.Am_Slot_Key MY_FOO_SLOT = Am_Register_Slot_Name ("My Foo Slot");
We recommend that programmers define their slots this way, as shown in the various example programs. #define MY_BAR_SLOT 10500
Am_Register_Slot_Key (MY_BAR_SLOT, "My Bar Slot");
The functions Am_Get_Slot_Name and Am_Slot_Name_Exists are used for testing the library of all slot keys for matches. This is especially useful when generating keys dynamically from user request.const char* name = Am_Get_Slot_Name (MY_BAR_SLOT);
cout << "Slot " << MY_BAR_SLOT << " is named " << name << endl;
if (Am_Slot_Name_Exists (name)) cout << "Slot already exists\n";
3.3.3 Value Types
The value of Am_LEFT in a graphical object is an integer specifying a pixel location. Hence slot values have types, specifically the Am_LEFT slot has integer type in graphical objects. The type of a slot's value is determined by whatever value is stored in the slot. A slot can potentially have different types of values at different times depending on how the slot is used, but a given value has only one type so that a slot has only one type at a time. Thus, slots are ``dynamically typed'' like variables in Lisp.Am_Value_Type type = my_object.Get_Slot_Type (Am_LEFT);
// slot_type == Am_INT
my_object.Set (Am_FILL_STYLE, Am_Blue);
Am_Value_Type type = my_object.Get_Slot_Type (Am_FILL_STYLE);
// type == Am_Style
A Am_Value_Type is an unsigned short with two bit-fields. The lower 12 bits are the type base. These bits are used to distinguish individual members of a type. The upper 4 bits are the type class. Currently, there are four kinds of classes, the basic types, Am_WRAPPER, Am_METHOD, and Am_CONSTRAINT. The basic types include C++ types like Am_INT and Am_FLOAT. The Am_WRAPPER class is used to denote wrappers like Am_Object and Am_Style. Am_Method denotes types that are methods like Am_Object_Method and Am_Where_Method. Am_CONSTRAINT types are usually not stored in slots. Testing the class of a value type is performed using the macro, Am_Type_Class, which will strip off the type's base bits so that the class can be compared. A similar macro, Am_Type_Base will strip off the class bits so that the base can be compared.Am_Value_Type type = object.Get_Slot_Type (Am_FILL_STYLE); // A Am_Style.
Am_Value_Type type_class = Am_Type_Class (type);
// type_class == Am_WRAPPER
3.3.4 The Basic Types
As shown by the examples above, the Set and Get operators are overloaded so that the normal built-in C++ primitive types can be readily used in Amulet. This section discusses some details of the primitive types, and the next few sections discuss some specialized types.my_object.Set (Am_LEFT, 50); //uses int
my_object.Set (Am_TEXT, "Foo"); //uses Am_STRING
my_object.Set (Am_PERCENT_VISIBLE, 0.75); //uses Am_FLOAT
long lng = 600000
my_object.Set (Am_VALUE_1, lng);
However, in some cases, the compiler cannot tell which version to use. In these cases, the programmer must put in an explicit type cast: //without cast, compiler doesn't know whether to use bool, int, void*, ...
if((bool)my_object.Get (Am_VISIBLE)) ...
//without cast, compiler doesn't know whether to use int, long or float
int i = 5 + (int)my_object.Get(Am_LEFT);
Am_INT is the same as Am_LONG on Unix, Windows 95, and NT (32 bits), but on the Macintosh, an Am_INT is only 16 bits, so one must be careful to use long whenever the value might overflow 16 bits when one wants to have portable code.3.3.5 Bools
Amulet makes extensive use of the bool type supplied by some C++ compilers (like gcc). For compilers that do not support it (Visual C++, ObjectCenter, etc.), Amulet defines bool as int and defines true as 1 and false as 0, so a programmer can still use bool in one's code. When bools are supported by the compiler, Amulet knows how to return a bool from any kind of slot value. For example, if a slot contains a string and it is cast into a bool, it will return true if there is a string and false if the string is null. However, for compilers that do not support bool, conversion to an int is not provided, so counting on this conversion is a bad idea. Instead, it would be better to get the value into a Am_Value type and test that for Valid (see Section 3.3.9).
3.3.6 The Am_String Class
The Am_String type (defined in object.h) allows simple, null terminated C++ strings (char*) to be conveniently stored and retrieved from slots. It is implemented as a form of wrapper (see Section 3.3.7). An Am_String can be created directly from a char* type, likewise it can be compared directly against a char*. Because Am_String is a Am_Wrapper which is a reference counted structure, the programmer need not worry about the string's memory being deallocated in a local variable even if an object slot that holds a pointer to the same string gets destroyed.
The Am_String class will make a copy of the string if the programmer wants to modify its contents. The Am_String class does not allow the programmer to perform destructive modification on the string's contents.
Am_String ()
Am_String (const char* initial)
The constructor that takes no parameters essentially creates a NULL char pointer. It is not equivalent to the string ``''. The second constructor creates the Am_String object with a C string as its value. The C string must be '\0' terminated so as to be usable with the standard string functions like strcpy and strcmp. The Am_String object will allocate memory to store its own copy of the string data.operator const char* ()
operator char* ()
These casting operators make it easy to convert a Am_String to the more manipulable char* format. When a programmer casts to const char*, the string cannot be modified so no new memory needs to be allocated. When the programmer casts to char*, however, the copy of the string stored in object slots are protected by making a local copy that can be modified. The modified string can be set back to an object slot by calling Set.3.3.7 Using Wrapper Types
Although one could store C++ objects into ORE slots as a void*, ORE provides the Am_Wrapper type to ``wrap'' C++ objects. Am_Wrappers provide dynamic type checking and memory management to the objects. These wrapper objects add a degree of safety to slots without sacrificing the dynamic aspects. Making new wrapper types is discussed in Section 3.11.2 and requires some practice. On the other hand, using wrapper types is simple. Notable wrapper types in Amulet are Am_Style, Am_Font, Am_String, Am_Value_List (see Section 3.8), and especially Am_Object, itself. Getting and setting a wrapper is syntactically identical to getting and setting an integer.
Am_Style blue (0.0, 0.0, 1.0); // Am_Style is a wrapper type.
my_object.Set (Am_FILL_STYLE, blue); // Using a wrapper with Set.
Am_Style color = my_object.Get (Am_FILL_STYLE); // Using a wrapper with Get.
A wrapper's slot type is available for testing purposes. Common wrapper types have a constant slot type such as Am_OBJECT and Am_STRING. Other wrapper types can be found using the class' static Type_ID method.if (object.Get_Slot_Type (MY_SLOT) == Am_Style::Type_ID ())
Am_Style color = object.Get (MY_SLOT);
3.3.7.1 Standard Wrapper Methods
Amulet wrappers provide a number of useful methods for querying about their state and for testing whether a given Am_Wrapper* belongs to a given class. These methods are common across all wrapper objects that Amulet provides. The methods are also available when programmers build their own wrapper objects using the standard macros.// Here the code checks to see that my_obj is not a NULL pointer by using
// the Valid method.
Am_Object my_obj = other_obj.Get (MY_OBJ);
if (my_obj.Valid ()) {
my_obj.Set (OTHER_SLOT, 6);
}
Besides using the Type_ID method, a programmer can check the type of a value using the static Test method. Test takes a Am_Value as its parameter and returns a bool.// Here the Test method is used to test which kind of wrapper type the
// value holds.
Am_Value_List my_list;
Am_Object my_object;
Am_Value val = obj.Get (MY_VALUE); // Am_Value is discussed later
if (Am_Value_List::Test (val))
my_list = val;
else if (Am_Object::Test (val))
my_object = val;
3.3.8 Storing Methods in Slots
ORE treats methods (procedures) stored in slots exactly the same as data. Thus, method slots can be dynamically stored, retrieved, queried and inherited like all other slots. Method types are dynamically stored just as wrapper types. Macros are provided to make defining and using methods easier.Am_Define_Method (Am_Object_Method, void, my_method,(Am_Object self))
{
self.Set (A_SLOT, 0);
}
By using the macro, the compiler can check to make sure that the actual method signature matches the one defined in the type. To set the value into a slot and retrieve the typename value, one uses Set and Get in the usual way.object.Set (SOME_SLOT, my_method);
Am_Object_Method hold_method = object.Get (SOME_SLOT);
To call a method, one invokes the Call field of the method's class. For instance, an Am_Object_Method has a Call field that is a procedure pointer that returns void and takes an Am_Object parameter.Am_Object_Method my_method = object.Get (SOME_METHOD);
my_method.Call (some_object);
To check the type of method one can use its type's static *_ID method. The ID methods are named using the method's name so an Am_Object_Method's ID is named Am_Object_Method_ID.if (object.Get_Slot_Type (MY_SLOT) ==
Am_Object_Method::Type_ID ()) {
Am_Object_Method method = object.Get (MY_SLOT);
method.Call (some_object);
}
Like common wrapper types, method wrappers also have a static Test method.Am_Value value = object.Get (MY_SLOT);
if (Am_Object_Method::Test (value))
Am_Object_Method method = value;
To define other method types, use the macros Am_Define_Method_Type and Am_Define_Method_Type_Impl. The first macro declares the type of the method. It is normally put into a .h file so that other parts of the code can use it. The IMPL macro is used to store the ID number of the method type. It must be stored in a .cc file to be compiled together with the program. In this example, a method is defined that takes two integers and returns a boolean.// In the .h file
Am_Define_Method_Type (My_Method, bool, (int, int));
// In the .cc file
Am_Define_Method_Type_Impl (My_Method);
With these defined, a programmer can create methods of type My_Method and they will behave as all other ORE methods.Am_Define_Method (My_Method, bool, equals, (int param1, int param2))
{
return param1 == param2;
}
object.Set (EQUALS_SLOT, equals);
The procedure stored in the global method declaration can be used directly by calling its Call field. For example, using the equals method defined above, one can call:bool result = equals.Call (5, 12);
3.3.9 Using Am_Value To Get A Slot Without Errors
Most of the time a programmer knows precisely what sort of value is stored in a slot. For these situations, the most convenient form of Get is the one that returns the value directly. This form has the declaration:const Am_Value& Get (Am_Slot_Key key) const;
Am_Value is a union for all the ORE types. The Am_Value type can be coerced to all the standard Amulet types including wrappers. Normally, the programmer simply sets the return value directly into the final destination variable. But there are times when the programmer will want to call Get on a slot but does not know what type the slot contains (or whether the slot even exists). To determine the type, one can either query the type of the slot using the Get_Slot_Type method for objects, or the programmer can use the other form of Get that ORE provides. This form takes a Am_Value& as parameter instead of returning one. It has the declaration:void Get (Am_Slot_Key key, Am_Value& value) const;
It is always valid to use the parameter style of Get. The return value form of Get will generate an error if the slot does not exist or is uninitialized. The paramter style generates an error only if it is called on an invalid or destroyed object. If the slot does not exist, the parameter form will set the Am_Value with type Am_NONE. If the slot is not initialized, then the type will be Am_UNINIT. (Uninitialized values concern slots with formulas [see Section 3.7].)
int i_value; float f_value;
Am_Value value;
my_object.Get (SOME_SLOT, value); // Get the value regardless of type
if (value.type == Am_INT) // The type field contains the type of value retrieved
i_value = value; // Am_Value defines many casting operators
else if (value.type == Am_FLOAT) // as assignment and constructors to aid
f_value = value; // setting and retrieving the value from
// the Am_Value
The Am_Value type has a number of methods, including printing (<<), ==, != and Valid. Valid returns true if the slot value existed and is not uninitialized (not Am_NONE or Am_UNINIT) and if the value is not zero:Am_Value value;
my_object.Get (SOME_SLOT, value); // Get the value regardless of type
if (value.Valid()) {
// then it is safe to use value
...
}
3.4 Inheritance: Creating Objects
The inheritance style of ORE objects is prototype-instance (as opposed to C++ which is class-instance). A prototype-instance object model means that objects are used directly as the prototypes of other objects. There is no distinction between instances and classes; in essence, there are only instances. Specialization of sub-objects into new types is performed by adding slots to the sub-object or changing the contents of existing slots defined by the prototype. Am_Object my_rectangle = Am_Rectangle.Create ("my rect")
.Set (Am_LEFT, 10)
.Set (Am_TOP, 20)
.Set (Am_WIDTH, 100)
.Set (Am_HEIGHT, 150)
;
A major style convention in ORE is to write an object all in one expression. This is so that the programmer need not repeat the name of the object variable over and over. This works because Set returns the original object. The main components of the creation action involves:
Am_Object my_rectangle;
my_rectangle = Am_Rectangle.Create ("my rect");
my_rectangle.Set (Am_LEFT, 10);
my_rectangle.Set (Am_TOP, 20);
my_rectangle.Set (Am_WIDTH, 100);
my_rectangle.Set (Am_HEIGHT, 150);Objects inherit all the slots of their prototype that are not set locally. Thus, if the Am_Rectangle object defines a color slot called Am_LINE_STYLE with a value of Am_Black, then my_rectangle will also have a Am_LINE_STYLE slot with the same value. If a slot is inherited, it will change value if the prototype's value changes. Thus, if Am_LINE_STYLE of Am_Rectangle is set to Am_Blue, then my_rectangle's Am_LINE_STYLE will also change. However, the Am_LEFT of my_rectangle will not change if the Am_LEFT of Am_Rectangle is set because my_rectangle sets a local value for that slot. See Section 3.11.5 for a discussion about how a programmer can control the inheritance of slots.
The inheritance of Amulet objects' print names is dealt with slightly differently. Objects created with a name parameter to the Create call will keep that name. Objects created without a string parameter will get the name of their prototype plus a number appended at the end to distinguish it from the prototype.
To destroy an object, call the method Destroy. This method cleans out the contents of the objects making it have no slots, parts, or instances. Some operations, like Get_Name, will still work on a destroyed object but most methods will generate an error. Destroy will also destroy the object's instances and parts (parts are described in the next section). If one wants to preserve any parts of a destroyed object, one must be certain to call Remove_From_Owner or Remove_Part before the call to Destroy to salvage them. Note that objects that are not parts but simply stored in a slot will not be destroyed if the slot is destroyed. The vast majority of objects defined by Amulet require the programmer to call Destroy in order to free their memory. Forgetting to call Destroy is the most likely source of memory leaks.
3.6 Parts
In ORE, it is possible to make objects become part of another object. The subordinate object is called a ``part'' of the containing object which is called the ``owner.'' The part-owner relationship is used heavily in Amulet programs.3.6.1 Parts Can Have Names
A very important distinction among parts is whether or not the part is named. A ``named'' part has a slot key. One can generate a key for a part the same way that they are generated for slots. When a part is named, it becomes possible to refer to it by that name in the method Get_Part (or by regular Get) and it also takes on other properties. If the part is unnamed, then the part cannot be accessed from the owner except through the part iterator (Section 3.9) or else by reading it from a list like the Am_GRAPHICAL_PARTS slot in group objects (described in the Opal chapter, Section 4.7.1).
In some ways, a named part of an object is like a slot that contains an object. A named part has a value and can have dependencies just like a slot. Parts are different from slots in that their type can only be Am_Object and that any particular object can only be assigned as a part to only one owner. Parts cannot be generated by a constraint and the inheritance mechanism for parts is not as sophisticated as that for slots.
Am_Slot_Key MY_FOO_PART = Am_Register_Slot_Name ("MY_FOO_PART");
Am_Object my_obj = Am_Group.Create("My_Obj")
.Add_Part(MY_FOO_PART, Am_Rectangle.Create("foo")); //named part
3.6.2 How Parts Behave With Regard To Create and Copy
When an instance is made of an object which has parts, then instances are made for each of the parts also. When an object is copied, copies are made for each part. This instancing and copying behavior can be overridden by specifying false in the Add_Part call when the part is added. Only unnamed parts can be set to be not inherited. Regular slots which contain objects will share the same object when the slot is instanced or copied.Am_Object my_obj = Am_Group.Create ("My_Obj")
.Add_Part (MY_PART, Am_Rectangle.Create("foo")) // named part
.Add_Part (Am_Roundtangle.Create ("bar") // unnamed part
.Add_Part (Am_Circle.Create ("do not instance"), false)
// unnamed part that isn't inherited
.Set(Am_PARENT, other_object); //slot containing an object
Am_Object my_obj2 = my_obj.Create();
// my_obj2 now has a rectangle part called MY_PART which is an instance of foo.
// It also has a roundtangle part that is not named. It does not have a circle part,
// since that part was unnamed and specified not to be inherited by the false paramter
// given in the Add_Part call. The Am_PARENT slot of both my_obj and my_obj2 point
// to the same value, other_object.
When an object is copied using the Copy method, all parts are copied along with the object. Both named and unnamed parts are copied unless the part is specified not to be inherited. Uninherited parts are never copied. If a part has a string name (the string given in the Create call, not the slot name), the same name will be used in the copy but a number is appended to the end of the name to distinguish it from the original.3.6.3 Other Operations on Parts
Other methods on objects relevant to parts are listed below.
Am_Slot_Key RECT = Am_Register_Slot_Name ("RECT");
Am_Object my_window = Am_Window.Create ("a window")
.Add_Part (RECT, Am_Rectangle.Create ("a rectangle"))
.Add_Part (Am_Line.Create ("a line"));
Am_Object rect = my_window.Get_Part (RECT);
my_window.Remove_Part (RECT);As mentioned above, when one destroys an object, all of its parts are destroyed also. Removing a part does not destroy the part.
This method of computing values from dependencies is often called constraint maintenance. ORE's mechanisms for constraint maintenance are actually more general (and complicated) than the formula constraint mentioned here. The full ORE constraint mechanism will be described in a later revision of this manual. The general mechanism allows more than one constraint system to be included in the system at the same time. The formula constraint is just one of many possible constraints that may be used in ORE. For example, Amulet currently also contains a ``Web'' constraint used to support multi-way interactions described in Section 3.11.3.
3.7.1 Formula Functions
An ORE formula consists of a C++ function that defines the dependencies of a slot and returns the value to set into the slot. The parameter list of a formula function is always the same two parameters. The first parameter, self, is an Am_Object which points to the object containing the slot. The second parameter, cc, is an Am_Constraint_Context&. The cc is an opaque handle (which means that its internal representation is not visible) to the state of the formula. It is used internally by the constraint system, but is not meant to be manipulated directly by the programmer. The cc parameter is used to distinguish the two forms of Get: one which just returns the value of the slot, discussed above, the other which returns the value and also sets up a dependency link. There are also constraint versions for most of the Get_XX functions like Get_Part and Get_Owner. The return value of the formula function is the same type that the slot will be when it takes on the returned value.// Example of a formula function. This formula returns a value for
// Am_LEFT which will center itself within its owner's dimensions.
static int my_left_formula_proc (Am_Constraint_Context& cc, Am_Object self)
{
Am_Object owner = self.Get_Owner (cc);
int owner_width = owner.Get (cc, Am_WIDTH);
int my_width = self.Get (cc, Am_WIDTH);
return (owner_width - my_width) / 2;
}
Am_Formula my_left_formula (my_left_formula_proc, "my_left_formula");
The above example uses no macros so it is clear where variables are defined, and which methods take the special cc parameter. The global variable my_left_formula is the actual constraint which one would use to set a slot. Because formula functions have such a generic format, the macro Am_Define_Formula is usually used to save writing. Likewise, using the cc parameter in Get is common enough that macros like GV are available that automatically add the cc parameter. Using macros, the function above would look like the function below. (This particular formula definition could easily be reduced to one line.)// Example of a formula function. This formula returns a value for
// Am_LEFT which will center itself within its owner's dimensions.
Am_Define_Formula (int, my_left_formula) {
Am_Object owner = self.GV_Owner ();
int owner_width = owner.GV (Am_WIDTH);
int my_width = self.GV (Am_WIDTH);
return (owner_width - my_width) / 2;
}
There also exists a Set version of GV called SV, that is used in other kinds of constraints like, in particular for constraints with multiple outputs, like Am_Web constraints. Though it is possible to use SV in a formula, it is generally easier to return the desired value. None of the formulas defined in the sample code in this section use SV.
extern Am_Formula my_formula;The type of the formula is not important in this case. All formula procedures get put into an Am_Formula variable that remembers the type internally.
One kind of value formula declaration has return value of void and it has one extra parameter of type Am_Value& whose name is ``value.'' The self and cc parameters are the same as in normal formulas and are used in the same way. The standard macro for declaring a multiple-type formula of this kind is Am_Define_Value_Formula. Note that this type of formula does not have return value. Instead, the value is returned by setting the Am_Value& value parameter.
Am_Define_Value_Formula (my_formula)
{
if ((bool)self.GV (Am_SELECTED))
value = 5;
else
value = Am_Blue;
}In the above example, if the slot Am_SELECTED is true, the formula will return an integer value of 5. If it is false, it returns the color blue.
Here is an example that passes a value from a different slot and without checking what type it is:
Am_Define_Value_Formula (my_copy_formula) {
other_obj.GVM (SOME_SLOT, value); // value is what is returned from
// this formula
}To read a slot set with a value formula the programmer can use either the Am_Value form of Get or can call Get_Slot_Type.
One can also create a formula that returns a const Am_Value as a return type:
Am_Define_Formula (const Am_Value, my_formula)
{
if ((bool)self.GV (Am_SELECTED)
return 5;
else
return Am_Blue;
}
Am_Define_Formula (int, my_left) {
int owner_width = self.GV_Owner ().GV (Am_WIDTH);
int my_width = self.GV (Am_WIDTH);
return (owner_width - my_width) / 2;
}defines three slots as dependencies: the object's Am_OWNER slot (the GV_Owner macro expands into a GV on the Am_OWNER slot), the owner's Am_WIDTH slot, and the object's Am_WIDTH slot. A formula changes dependencies when it calls GV on different slots. For instance, the above formula's dependency on the owner's width will change if the object ever gets moved to a new owner.
A programmer can use a regular Get without the constraint context in a formula function, but the slot fetched will not become a dependency. Forgetting to use GV instead of Get is a common mistake for Amulet programmers. If it ever seems that a formula is not updating when it is supposed to, check to make sure that GV is being used in the right places.
There exists a form of GV for all forms of Get but one, Get_Prototype. Since the prototype of an object is fixed and can never change, there is never a need to install a dependency to it. The full list of GV forms is:
Am_Define_Formula (int, rect_left)
{
return (int)self.GV_Owner ().GV (Am_WIDTH) / 2;
}
// Here the formula is used.
Am_Object my_rectangle = Am_Rectangle.Create ("my rect")
.Set (Am_LEFT, rect_left))
;In this example, the programmer defines a Am_Formula variable explicitly:
static int rect_left_proc (Am_Object& self, Am_Constraint_Context& cc)
{
return (int)self.GV_Owner ().GV (Am_WIDTH) / 2;
}
// Here the formula procedure is used.
Am_Object my_rectangle = Am_Rectangle.Create ("my rect")
.Set (Am_LEFT, Am_Formula (rect_left_proc, "name of formula"))
;Formulas are evaluated eagerly, which means that the formula expression may be evaluated even before all the slots it depends on are defined. Since a formula cannot detect when external references are changed, it is wise to make sure that any global variable that a formula references are defined before the formula is set into a slot. Formulas that reference undefined slots or NULL objects will return the value Am_UNINIT. It is an error to fetch the value of an uninitialized slot from outside a formula so it is important that anything that a formula will depend on becomes valid as soon as possible. One can check if a slot is uninitialized by using Get_Slot_Type or fetching the slot as a Am_Value.
// This formula will become uninitialized if
// a) it doesn't have an owner.
// b) the owner's left slot doesn't exist.
// c) the owner's left slot is uninitialized.
Am_Define_Formula (int, my_form)
{
return self.GV_Owner ().Get (Am_LEFT);
}
Like values, formulas are inherited from prototypes to their instances. However, the formula in the instance might compute a value different from the formula in the prototype if the formula contains indirect links. For example, the formula that computes the width of a text object depends on the text string and font, and even though the same formula is used in every text object, most will compute different values.
Sometimes, constraints from a prototype should be retained in instances even if the local value is set. This requires declaring that the slot is not Single_Constraint_Mode, which is an advanced feature covered in Section 3.11.5.
3.7.5 Calling a Formula Procedure From Within Another Formula
Given a Am_Formula variable it is possible to call the formula procedure embedded within it. This process is typically done within another formula since one needs to have both a self reference and a cc parameter. Typical use for this ability is to reuse the code of another formula instead of rewriting it. An Am_Formula's procedure call method returns type const Am_Value which can be cast to the desired return type.Am_Define_Formula (int, complicated_formula)
{
// Perform some hairy computation.
}
Am_Define_Fomula (int, complicated_formula_plus_one)
{
return (int)complicated_formula (cc, self) + 1;
}
3.8 Lists
Lists are widely used in Amulet. For example, many widgets require a list of labels to be displayed. The standard ORE list implementation is the Am_Value_List type. ORE defines this list class so that it can be a wrapper and more easily be stored as slot values. The operations on Am_Value_Lists are provided in the file value_list.h. Like slots, Lists can hold any type of value. A single list can also contain many different types of values at the same time.
for (my_list.Start (); !my_list.Last (); my_list.Next ()) {
something = my_list.Get ();
// Use something here
}Similarly, to go in reverse order, one would use:
for (my_list.End (); !my_list.First (); my_list.Prev ()) {
something = my_list.Get ();
// Use something here
}Note that the pointer is not initialized automatically in a list, so the programmer must always call Start or End on the list before accessing its value with Get.
To add items at the beginning or end of the list, use the Add method. Since this does not use the current pointer, one do not need to call Start. The first parameter to Add is the value to be added, which can be any primitive type, an object, a wrapper, or a Am_Value. The second parameter to Add is either Am_TAIL or Am_HEAD which defaults to Am_TAIL. This controls which end the value goes. Add returns the original Am_Value_List so multiple Adds can be chained together:
Am_Value_List l;
l.Add(3)
.Add(4.0)
.Add(Am_Rectangle.Create())
.Add(Am_Blue)
;To add items at the current position, the programmer must first set the current pointer. First, Start, End, Next, and Prev are used to position the current pointer as the desired location then Insert is called. The first parameter of Insert is the value to be stored, and the second parameter specifies whether the new item should go Am_BEFORE or Am_AFTER the current item. There is no default for this parameter. The current pointer does not change in this operation.
Lists can be appended with the Append method. Append is called on the lists whose elements will be at the beginning of the final result. The parameter is a list whose elements will be appended. The result is stored directly in the list on which the method is called. This method returns this just as the Add method so that it can be cascaded.
Am_Value_List start = Am_Value_List ().Add (1).Add (2); // list (1, 2)
Am_Value_List end = Am_Value_List ().Add (3).Add (4); // list (3, 4)
start.Append (end); // start == (1, 2, 3, 4)
Am_Value v;
for (my_list.End (); !my_list.First (); my_list.Prev ()) {
my_list.Get(v);
cout << "List item type is " << v.type << endl << flush;
}To find the type of an item without fetching the value use Get_Type().
Set is used to change the current item in the list. This is differs from Insert in that it deletes the old current item and replaces it with the new value.
The Delete method destroys the item at the current position. It is an error to call Delete if there is no current element. The current pointer is shifted to the element previous to the deleted one. Make_Empty deletes all the items of the list.
Am_Value_Lists support a membership test, using the Member method. This starts from the current position, so be sure to set the pointer before calling Member. For example, to find the first instance of 3 in a list:
l.Start();
if (l.Member(3)) cout << "Found a 3";Member leaves the current pointer at the position of the found item. Calling Set or Delete after a search will affect the found item. Calling Member again finds the next occurrence of the value.
Length returns the current length of the list.
Empty returns true if the list is empty. Note that there is a difference between being Valid and being Empty. Not being valid means that the list is NULL that is, it does not exist. An invalid list is also empty by definition, but an empty list is not always invalid. The list may exist and be made empty by deleting its last element, for instance. In this case, Empty would return true and Valid will also return true.
cout << "The instances of " << my_object << " are:" << endl;
Am_Instance_Iterator iter = my_object;
for (iter.Start (); !iter.Last (); iter.Next ()) {
Am_Object instance = iter.Get ();
cout << instance << endl;
}The first line of the example is used to initialize the iterator. The example prints out the instances of my_object so my_object is the object to assign to the iterator. The Last method is used to detect when the list is complete. These iterators can only be traversed in one direction.
To iterate over the instances of an object, use an Am_Instance_Iterator. Its Get method returns an Am_Object which is the part. To list an object's slots (both inherited and local), use the Am_Slot_Iterator. Its Get method returns the Am_Slot_Key of the current slot. You can use the object method Is_Slot_Inherited to see if the slot is inherited or not.
When items are added to an iterator's list while the list is being searched, the items added are not guaranteed to be seen by the iterator. The iterator may have already skipped them. The value returned by the Length method will be correct, but the only way to make certain all values have been seen is to restart the iterator.
Likewise, the order in which the elements are stored in each iterator is not guaranteed to be maintained when an item is deleted from the list. The iterators themselves cannot be used to destroy an item, but other methods like obj.Destroy, obj.Remove_Part, and obj.Remove_Slot will affect the contents of iterators that hold those values.
When an iterator has a slot or object as its current position, and that item gets removed, the affect on the iterator is not determined. An iterator can be restarted by calling the Start method in which case it will operate as expected. Though an iterator will not likely cause a crash if its current item is deleted, continued use of it could cause odd results.
For iterators that iterate over objects (specifically Am_Part_Iterator and Am_Instance_Iterator), it is possible to continue using the iterator even when items are deleted. If the programmer makes certain that the iterator does not have the deleted object as the current position when the object is removed, then the iterator will remain valid. For example:
// Example: Remove all parts of my_object that are instances of Am_Line.
Am_Part_Iterator iter = my_object;
iter.Start ();
while (!iter.Last ()) {
Am_Object test_obj = iter.Get ();
iter.Next ();
if (test_obj.Is_Instance_Of (Am_Line))
test_obj.Remove_From_Owner ();
}In the above example, the call to Next occurs before the call to Remove_From_Owner. If these method calls were reversed, then iterator would go into an odd state and one would get undetermined results.
The Am_Slot_Iterator type does not have the same deletion properties as the object iterators. If a slot gets removed from an object used by a slot iterator (or the prototype of the object assuming the slot is defined there), then the affect on the iterator is undetermined. The slot iterator must be restarted whenever a slot gets added or removed from the list in order to guarantee that all slots are seen.
When the programmer retrieves a wrapper value out of a slot, it points to the same value that is in the slot. If the value of the wrapper is changed destructively, then both the local pointer and the pointer in the slot will point to the changed value. To control this, most mutable wrappers provide a make_unique optional parameter in their data changing operations. The default for this parameter is true and makes the method create a copy of the value before it modifies it. If you call the procedure with false, then it will not make a copy and the value you modify will be the same as the value pointed to by all the other references. If the wrapper is pointed to in a slot, the object system will not know that the value has changed. To tell the object system that a slot has changed destructively, the programmer calls Note_Changed on the object after the modifications are complete. ORE responds to Note_Changed the same way it would respond if the slot were set with a new value. Thus, Amulet will redraw the object if necessary and notify any slots with a constraint dependent on this slot. For example:
obj.Make_Unique (MY_LIST_SLOT); // make sure that only one value is modified
Am_Value_List list = obj.Get (MY_LIST_SLOT);
list.Start ();
list.Delete (false); // destructively delete the first item
obj.Note_Changed (MY_LIST_SLOT); // tell Amulet that I modified the slot
The purpose of the Make_Unique call at the beginning of the previous segment concerns whether the wrapper value is unique to the slot MY_LIST_SLOT or not. Amulet shares wrappers between multiple slots that are not explicitly made unique. A major source of sharing occurs when an object is instantiated. For example, if one makes an instance of obj in the example above called obj_instance, then both obj_instance and obj will point to the same list in the MY_LIST_SLOT. In that case, the above code would modify the list for both obj and obj_instance, but Amulet would not know about the change to obj_instance. Thus, to make sure the MY_LIST_SLOT is unique, the programmer explicitly calls Make_Unique before the value of the slot is fetched. If it is beyond doubt that a wrapper value is not shared or that it is only shared in places which the programmer knows will be affected, then the Make_Unique call can be eliminated. Make_Unique will do nothing if the slot is already unique.// Example which uses Is_Unique to guarantee the uniqueness of MY_SLOT's value.
bool unique = obj.Is_Unique (MY_SLOT);
Am_Value_List list = obj.Get (MY_SLOT);
if (... list ...) {
list.Add (5, Am_TAIL, !unique); // perform destructive change if unique
if (unique)
obj.Note_Changed (MY_SLOT);
else
obj.Set (MY_SLOT, list);
}
3.11.2 Writing a Wrapper Using Amulet's Wrapper Macros
You should consider creating a new type of wrapper whenever you need to store a C++ object into a slot of an Amulet object. A wrapper manages two things. First, it has a simple mechanism that supports dynamic type checking, so it is possible to check the type of a slot's value at run time. Second, wrappers use a reference counting scheme to prevent the value's memory from being deleted while the value is still in use.3.11.2.1 Creating the Wrapper Data Layer
Both the typing and reference counting is embodied by the definition of the class Am_Wrapper from which the data layer of all wrappers must be derived. The class Am_Wrapper is pure virtual with seven methods, six of which all wrappers must define. Most of the time, the programmer can use the pre-defined macros Am_WRAPPER_DATA_DECL and Am_WRAPPER_DATA_IMPL to define these six methods.void Note_Reference ()
unsigned Ref_Count ()
Am_Wrapper* Make_Unique ()
void Release ()
operator== (Am_Wrapper& test_value)
Am_ID_Tag ID ()
Of the six methods, three are used for maintaining the reference count and making sure that only a unique wrapper value is ever modified. These are Note_Reference, Make_Unique, and Release. The seventh method is void Print_Name (ostream&) which is used to print out the value of the wrapper in a human-readable format. Unlike the other methods, this method has a default implementation though it may be defined by programmers. This method is used mostly for debugging as in the inspector or for printing out values in a property sheet. It is good to implement this method for wrappers that one intends to release for use by the public.
The operator== method allows the object system to compare two wrapper values against one another. The system will automatically compare the pointers so the == method must only compare the actual data. Simply returning false is sufficient for most wrappers.
class Foo_Data : public Am_Wrapper {
Am_WRAPPER_DATA_DECL (Foo)
public:
Foo_Data (Foo_Data* prev)
{
... // initialize member values
refs = 1; // Do not forget this line!
}
operator== (Foo_Data& test_value)
{
... // compare test_value to this
}
protected:
... // define own members
};
// typically this part goes in a .cc file
Am_WRAPPER_DATA_IMPL (Foo, (this))
All the standard wrapper macros take the name of the type as their first parameter. The string ``_Data'' is always automatically appended to the name meaning your wrapper data classes must always end in _Data. If one wants the name of the wrapper type to be Foo, the data layer type must be named Foo_Data. The Am_WRAPPER_DATA_IMPL macro takes a second parameter which is the parameter signature to use when the Make_Unique method calls the data object's constructor. In the above case, ``(this)'' is used because the parameter to the Foo_Data constructor is equivalent to the this pointer in the Make_Unique method. That is, the Make_Unique method will sometimes have to create a new copy of the wrapper object. The new copy will be created using one of the object's constructors. In the above case, the programmer wants to use the constructor Foo_Data(Foo_Data* prev). This constructor requires Make_Unique to pass in its this pointer as the parameter. Therefore, the parameter signature declared in the macro Am_WRAPPER_DATA_IMPL is ``(this).'' If the programmer wanted a different constructor to be used, the parameter set put into the macro would be different.3.11.2.2 Using The Wrapper Data Layer
The wrapper data layer is normally manipulated only by the methods in the wrapper outer layer. One can take the Am_Foo_Data* and manipulate it as a normal C++ object with the following caveat. One must be sure that the reference count is always correct. When one uses the data pointer directly, the methods Note_Reference or Release are not being called automatically so it must be done locally in the code.// Here we move a Foo_Data pointer from one variable to another. The
// object that lives in the first variable must be released and the
// reference of the object moved to the new variable must be incremented
Foo_Data* foo_data1;
Foo_Data* foo_data2;
//... assume that foo_data1 and foo_data2 are somehow initiallized with
// real values...
if (foo_data1)
foo_data1->Release ();
foo_data1 = foo_data2;
foo_data1->Note_Reference ();
To keep changes in a wrapper type local, one must call the Make_Unique method on the data before making a change to it. If the wrapper designer wants to permit the wrapper user to make destructive modifications, a boolean parameter should be added to let the user decide which to do.void Foo::Change_Data (bool destructive = false)
{
if (!destructive)
data = data->Make_Unique ();
data->Modify_Somehow ();
}
Sometimes a programmer will want to use the wrapper data pointer outside the outer wrapper layer of the object. To convert from the wrapper layer to the data layer, one uses Narrow.Foo my_foo;
Foo_Data* data = Foo_Data::Narrow (my_foo);
data->Use_Data ();
data->Release (); // Release data when through
The way to test if a given wrapper is the same type as a known wrapper class is to compare IDs. The static method TypeName_ID is provided in the standard macros.Am_Wrapper* some_wrapper_ptr = something;
Am_ID_Tag id = some_wrapper_ptr->ID ();
if (id == Foo_Data::Foo_Data_ID ())
cout << "This wrapper is foo!" << endl;
Other functions provided for wrapper data classes by the standard macro are Is_Unique, and Is_Zero with are boolean functions that query the state of the reference count.3.11.2.3 Creating The Wrapper Outer Layer
The standard macros for building the wrapper outer layer assume that the class for the wrapper data is called TypeName_Data where TypeName is the name for the wrapper outer layer. Like the data layer macros, there are two outer layer macros, one for the class declaration part and one for the implementation.// Building the outer layer of Foo. This definition normally goes in
// a .h file.
class Foo { // Note there is no subclassing.
Am_WRAPPER_DECL (Foo)
public:
Foo (params);
Use ();
Modify ();
};
// This normally goes in the .cc file.
Am_WRAPPER_IMPL (Foo)
The wrapper outer layer is given a single member named data which is a pointer to the data layer. In all the wrapper methods, one performs operations on the data member.
// A Foo constructor - initializes the data member.
Foo::Foo (params)
{
data = new Foo_Data (params);
}
For methods that modify the contents of the wrapper data, one must be sure that the data is unique. One uses the Make_Unique method to manage uniqueness.
Foo::Modify ()
{
if (!data)
Am_Error ("not initialized!");
data = data->Make_Unique ();
data->Modify ();
}
Methods that do not modify data do not need to force uniqueness so they can use the wrapper data directly.Foo::Use ()
{
if (!data)
Am_Error ("not initialized!");
data->Use ();
}
Somewhere in the code, the wrapper will actually do something: calculate an expression, draw a box, whatever. Whether the programmer puts the implementation in the data layer or the outer layer is not important. Most wrapper implementations will put define their function wherever it is most convenient.3.11.3 The Am_Web Constraint
The Am_Web constraint is a multi-way constraint solver. That means it can store values to more than one slot at a time. On the other hand, the one-way Am_Formula constraint can only store values to one slot, the slot in which the formula is stored. Multi-way constraints tend to be more difficult to use and understand than one-way constraints, which is Am_Web is described here in the advanced section. One would use a web instead of a formula to build a constraint that involves tying several slots together so that their values act as a unit. Also, sometimes it is more efficient to make a single web where many formulas would be required to accomplish the same task. For example, Opal uses a web to tie together the left, top, width, height slots with the x1, y1, x2, y2 slots in the Am_Line object. Finally, the web provides information about which dependencies have been changed during re-validation which may be required to implement certain kinds of constraints.3.11.3.1 The Validation Procedure
For the example, we will construct a multi-direction web constraint that ties together three slots: ADDEND1, ADDEND2, and SUM. SUM is constrained to be ADDEND1 + ADDEND2, and ADDEND1 is constrained to be SUM - ADDEND2. ADDEND2 is not constrained. First, we will write the validation procedure:void sum_two_slots_val (Am_Constraint_Context& cc, Am_Web_Events& events)
{
events.End ();
if (events.First ()) // No events
return;
Am_Slot slot = events.Get ();
Am_Object self = slot.Get_Owner ();
switch (slot.Get_Key ()) {
case ADDEND1: { // ADDEND1 has changed last.
int addend1 = self.GV (ADDEND1);
int addend2 = self.GV (ADDEND2);
self.SV (SUM, addend1 + addend2);
}
break;
case ADDEND2: { // ADDEND2 has changed last.
int prev_value = events.Get_Prev_Value ();
int addend2 = self.GV (ADDEND2);
events.Prev ();
if (events.First () || events.Get ().Get_Key () != SUM) {
int addend1 = self.GV (ADDEND1);
self.SV (SUM, addend1 + addend2);
}
else { // SUM was set before ADDEND2. Must propagate previous result to ADDEND1.
int sum = self.Get (SUM);
self.SV (ADDEND1, sum - prev_value);
self.SV (SUM, sum - prev_value + addend2);
}
}
break;
case SUM: { // SUM has changed last.
int sum = self.GV (SUM);
int addend2 = self.GV (ADDEND2);
self.SV (ADDEND1, sum - addend2);
}
break;
}
}
If the code looks complicated, it is because it is complicated. What makes writing web constraints difficult is that more than one slot can change between validations. The code above has to first check to see what slots have changed. Then it sees if something else has changed before that and then finally carries out the computation. To read which slots have changes, one uses the Am_Web_Events class. This contains a list of the slots that have changed value since the last time the web was validated in the order in which they were changed. This class is structured like the other iterator classes. It has Start, End, Next, Prev, First, and Last methods for traversing its items. Its Get method returns a Am_Slot. There is no self parameter because webs are not attached to a specific slot or object. A web could hypothetically float around between objects as it gets reevaluated; though, this is generally not the case. To get a self pointer, one reads the owner of one of the slots in the event list. It should always be possible to determine a web's location using any one of its slots as a reference. The other method available in the events list is Get_Prev_Value which returns the value that the slot contained before it was changed. To get the slot's current value, one uses Get or GV.3.11.3.2 The Create and Initialization Procedures
The next procedure examined will be the create procedure. The semantics of the create procedure is that it returns true when it is passed the primary slot. For our example web, we have a choice of primary slots. It could be any of ADDEND1, ADDEND2, or SUM. We will choose SUM.bool sum_two_slots_create (const Am_Slot& slot)
{
return slot.Get_Key () == SUM;
}
Although our sum_two_slots web will normally be connected to three slots, when it is inherited, it is only connected to the primary slot. The purpose of the initialize procedure is to reconnect the web to all its other dependencies. Essentially, it is used to fix up the lost connections caused by inheritance.void sum_two_slot_init (Am_Constraint_Context& /*cc*/, const Am_Slot& slot,
Am_Web_Init& init)
{
Am_Object_Advanced self = slot.Get_Owner ();
init.Note_Input (self, ADDEND1);
init.Note_Input (self, ADDEND2);
init.Note_Input (self, SUM);
init.Note_Output (self, ADDEND1);
init.Note_Output (self, ADDEND2);
init.Note_Output (self, SUM);
}
The Am_Web_Init class is used to make dependency and constraint connections without performing a GV or SV on any slot. The method Note_Input creates a dependency and the method Note_Output creates a constraint. Some programmers may want to perform computation during initialization, in which case the usual cc parameter is available for calling GV or SV. The slot parameter provides the primary slot to which the web was attached.3.11.3.3 Installing Into a Slot
A web is put into an object by setting it into its primary slot. First, a Am_Web variable is created using the three procedures defined above. This variable is stored into the primary slot just as a Am_Formula is stored, by calling Set.Am_Web my_web (sum_two_slots_create, sum_two_slots_init,
sum_two_slots_val);
object.Set (SUM, my_web);
Once a web is stored in the primary slot, its initialization procedure will take over and attach the web to any other slots it needs.3.11.4 Using Am_Object_Advanced
There are several extra methods that can be used on any Amulet object that are not available in the regular Am_Object class. A programmer can manipulate these methods by typecasting a regular Am_Object into a Am_Object_Advanced class. For instance, in order to retrieve the Am_Slot form for a slot one uses Get_Slot:#include OBJECT_ADVANCED__H // Note need for special header file
Am_Object_Advanced obj_adv = (Am_Object_Advanced&)my_object;
Am_Slot slot = obj_adv.Get_Slot (Am_LEFT);
A programmer must be careful using the advanced object and slot classes. Many operations can break the object system if used improperly. A general principle should be to use the advanced features for a short time right after an object is first created. After the object is manipulated to add or change whatever is needed, the object should never need to be cast to advanced again.
new_win.Set_Inherit_Rule (Am_DRAWONABLE, Am_LOCAL);The inherit rule may also be set directly on a Am_Slot:
#include <am_inc.h> // defines OBJECT_ADVANCED__H for machine independance
#include OBJECT_ADVANCED__H // for slot_advanced
Am_Object_Advanced obj_adv = (Am_Object_Advanced&)new_win;
Am_Slot slot = obj_adv.Get_Slot(Am_DRAWONABLE);
slot.Set_Inherit_Rule(Am_LOCAL);Using the Am_Slot method is useful when one wants to perform several manipulations on a single slot. By fetching the Am_Slot once and reusing the value, one can save the time needed to search for the slot.
The default rule with which an object will create all new slots added to an object can be changed using the Set_Default_Inherit_Rule method. Likewise, the current inherit rule can be examined using Get_Default_Inherit_Rule.
((Am_Object_Advanced&)my_object).Set_Default_Inherit_Rule (Am_COPY);
Am_Inherit_Rule rule = my_adv_object.Get_Default_Inherit_Rule ();
obj.Set_Single_Constraint_Mode (Am_VALUE, false);
The same parameter can be set directly on the slot as well:Am_Object_Advanced obj_adv = (Am_Object_Advanced&)obj;
obj_adv.Get_Slot (Am_VALUE).Set_Single_Constraint_Mode (false);
Now, if obj contains a constraint, any instances of obj will always retain that constraint, even if another constraint or value is set into the instance. Furthermore, calls like Remove_Constraint on the instance's slot will still not remove the inherited constraint (though it will remove any additional constraints set directly into the instance). 3.11.7 Writing and Incorporating Demon Procedures
Amulet demons are special methods that are attached directly to an object or slot. Demons are used to cause frequently occurring, autonomous object behavior. The demons are written as procedures that are stored in an object's ``demon set.'' The demon set is shared among objects that are inherited or copied from another.3.11.7.1 Object Level Demons
The three demons that handle object creation and destruction are the create, copy, and destroy demons. Each demon is enqueued on its respective event. The create and copy demons get enqueued when the object is first created depending on whether the method Create or Copy was used to make the object. The destroy demon is never enqueued. Since the Destroy operation will cause the object to no longer exist, all demons that are already enqueued will be invoked and then the destroy demon will be called directly. This allows the programmer to still read the object while the destroy demon is running.// Here is an example create demon that initializes the slot MY_SLOT to
// be zero.
void my_create_demon (Am_Object self)
{
self.Set (MY_SLOT, 0);
}
Two object-level demons are used to handle part-changing events. These are the add-part and the change-owner demons. The add-part and change-owner demons are always paired: the add-part demon for the part and the change-owner demon for the part's owner. Both demon procedures have the same parameter signature, three objects, which has the type Am_Part_Demon, but the semantics of each demon is different. The first parameter for both procedures is the self object -- the object being affected by the demon. The next two objects represent the change that has occurred. In the add-part demon, the second object parameter is an object that is being removed or replaced. The third parameter is an object that is being added or is replacing the object in the second parameter. For the change-owner demon, the semantics are reversed -- the second and third parameters represent the change that a part sees in its owner. The second parameter is the owner the part used to have, the third parameter is the new owner that has replaced the old owner.// This owner demon checks to make sure that it's owner is a window. Any
// other owner will cause an error.
void my_owner_demon (Am_Object self, Am_Object prev_owner,
Am_Object new_owner)
{
if (!new_owner.Is_Instance_Of (Am_Window))
Am_Error ("You can only add me to a window!");
}
The events that generate add-part and change-owner demon events are methods such as Add_Part, Remove_Part, Destroy, and other methods that change the object hierarchy. Note that a given add-part demon always has a corresponding change-owner demon. The correspondence is not necessarily one to one because one can conceive of situations where one part is replacing another and thus two add-part calls can be associated with a single change-owner and vice versa.void my_create_demon (Am_Object self)
{
Am_Object_Demon_Type* proto_create = self.Get_Prototype ().
Get_Demon_Set ().Get_Object_Demon (Am_CREATE_OBJ);
if (proto_create)
proto_create (self); // Call prototype create demon.
// Do my own code.
}
3.11.7.2 Slot Level Demons
Slot demons are not given permanent names like the object level demons. The slot demons are assigned a bit in a bit field to serve as their name. The slot demon procedure pointer is stored in the object. By turning on the bit in the slot, the slot will activate the demon procedure from the demon set whenever a triggering event occurs.// Here is an example slot demon. This demon does not do anything
// interesting, but it shows how the parameter can be used.
void my_slot_demon (Am_Slot slot)
{
Am_Object_Advanced self = slot.Get_Owner ();
self.Set (MY_SLOT, 0);
}
3.11.7.3 Modifying the Demon Set and Activating Slot Demons
To activate any demon, the object must know the demon procedure. Objects keep the list of available procedures in a structure called the demon set which is defined by the class Am_Demon_Set. Objects inherit their demon set from their prototype. The demon set is shared between objects in order to conserve memory. To modify the demon set of an object, one must first make the set a local copy. The demon set's Copy method is used to make new sets.// Here we will modify the demon set of my_adv_obj by first making
// a copy of the old set and modifying it. The new demon set is then
// placed back into the object.
Am_Demon_Set my_demons (my_adv_obj.Get_Demon_Set ().Copy ());
my_demons.Set_Object_Demon (Am_DESTROY_OBJ, my_destroy_demon);
my_adv_obj.Set_Demon_Set (my_demons);
When demon procedures are installed for object level demons, the demons will trigger on the next occurrence of their corresponding event. Note that the create and copy demon's events have already occurred for the prototype object where the demon procedures are installed. However, instances of the prototype as well as new copies will cause the new procedures to run. To make the demon procedure run for the current prototype object, one calls the demon procedure directly.
// Here we install a slot demon that uses bit 5. The slot demon's semantics
// are to activate when the slot changes value and only once per object.
// Make sure that the demon set is local to the object (see above section).
my_demons.Set_Slot_Demon (0x0020, my_slot_demon,
Am_DEMON_ON_CHANGE | Am_DEMON_PER_OBJECT);
After the demon procedure is stored, one sets the bits on each slot that is able to activate the demon.// Here we set a new bit to a slot. To make sure we do not turn off
// previously set bits, we first get the old bits and bitwise-or the new one.
Am_Slot slot = my_adv_obj.Get_Slot (MY_SLOT);
unsigned short prev_bits = slot.Get_Demon_Bits ();
slot.Set_Demon_Bits (0x0020 | prev_bits);
To cause newly created objects to have certain demon bits set, one changes the default demon bits.// Make the new slot demon default.
unsigned short default_bits = my_adv_obj.Get_Default_Demon_Bits ();
default_bits |= 0x0020;
my_adv_obj.Set_Default_Demon_Bits (default_bits);
Another factor in slot demon maintenance is the demon mask. The demon mask is used to control whether the presence of a demon bit in a slot will force the slot to make a temporary slot in every instance. A temporary slot is used by ORE to provide a local slot in an object even when the value of the slot is inherited. If a temporary slot is not available, then there will be no demon run for that object. This is necessary when one wants inherited objects to follow the behavior of a prototype object. For instance, in a rectangle object, if one changes the Am_LEFT slot in the prototype, one would like a demon to be fired for the Am_LEFT slot in every instance. That requires there to be a temporary slot for every instance. Set the demon mask bit for all demons that require a temporary slot. For all other demons put zero. A temporary slot will be created for slots whose demon bits contain at least one bit stored in the demon mask.// Setting the demon mask
unsigned short mask = my_adv_obj.Get_Demon_Mask ();
mask |= 0x0020; // add the new demon bit.
my_adv_obj.Set_Demon_Mask (mask);
3.11.7.4 The Demon Queue
The demon queue is where demon procedures are stored when their events occur. Objects hold the demon queue in the same way that they keep their demon set: the same queue is shared when objects are instanced or copied. However, Amulet never uses more than one demon queue. There is only one global queue for all objects. It is possible to make a new queue and store it in an object, but it never happens.// Here is how to make a new queue.
// It is unlikely that anyone will need to do this.
Am_Demon_Queue my_queue;
my_adv_obj.Set_Queue (my_queue);
To find and manipulate the global demon queue, one can take any object and read its queue.Am_Demon_Queue global_queue =
((Am_Object_Advanced&)Am_Root_Object).Get_Queue ();
The demon queue has two basic operations: enqueuing a new demon into the list and causing the queue to invoke. Invoking the queue causes all stored demon procedures to be read out of the queue, in order, and executed. While the queue is invoking, it cannot be invoked recursively. This prevents the queue from being read out of order while a demon is still running.3.11.7.5 How to Allocate Demon Bits and the Eager Demon
In order to develop new slot demons, one must provide a bit name for it. Presently, Amulet does not provide a means for dispensing bit names for demons. To see if a demon bit is being used by an object, read the slot demons from the demon set and see which bits are not being used. This procedure is presently sufficient since one never modifies an object's demon set more than once. Generally, only prototype objects need to be manipulated and one can often know which demons are set in a given prototype object.