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:
types.h:
Definitions for all the basic types including Am_Value_Type
, Am_Value
, Am_Wrapper
, and Am_Method_Wrapper
.
standard_slots.h
: The functions for defining slot names, and the list of Amulet-defined slots.
objects.h:
All of the basic object methods including slot and part iterators.
objects_advanced.h:
Needed for using any of the advanced features discussed in Section 3.11.
value_list.h:
Defines the type Am_Value_List
and all its related methods.
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. Am_VALUE
slot can hold an integer at one time, and then a string later. ORE keeps track of the current type stored in the slot, and supports full integration with the C++ type system, including dynamic type checking.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 Get
s 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.standard_slots.h
. There are four essential functions to do this: Am_Register_Slot_Name, Am_Register_Slot_Key, Am_Get_Slot_Name
, and Am_Slot_Name_Exists
.Am_Register_Slot_Name
is the major function for defining new slot keys. The function returns a key which is guaranteed not to conflict with any other key chosen by this function (it is actually just a simple counter). The return value is normally stored in a global variable which is used throughout the application code. If the string name passed already has a key associated with it, Am_Register_Slot_Name
will return the old key rather than allocating a new one. Thus, Am_Register_Slot_Name
can also be used to look up a name to find out its current key assignment.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.Am_Register_Slot_Key
is for directly pairing a number with a name. This is useful for times when one does not want to use a global variable to store the number returned by Am_Register_Slot_Name
. The number and name chosen must be known beforehand not to conflict with any other slot key chosen in the system. The range of numbers that programmers are allowed to use for their own slot keys is 10000 to 29999. Numbers outside that range are allocated for use by Amulet. The number of new slot keys needed by an application is likely to be small so running out of numbers is not likely to be a problem. The main concern will be conflicting with numbers chosen by other applications written in Amulet. #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.void*
can be used to store any type of object, ORE supports a type called Am_Wrapper
which is used to encapsulate C++ classes and structures so that general C++ data can be stored in slots while still maintaining a degree of type checking.my_object.Set (Am_LEFT, 50);
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);
A // type == Am_Style
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.Set
to use for each of the following: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.Am_Ptr
type (defined in types.h
) should be used wherever one would normally use a void*
pointer, because Visual C++ cannot differentiate void*
from some more specific pointers used by Amulet. Am_Ptr
is defined as void*
in Unix and unsigned char*
in Windows.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 bool
s 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 ()
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);
A wrapper's slot type is available for testing purposes. Common wrapper types have a constant slot type such as // Using a wrapper with Get.
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.NULL
object. The name of the NULL
object is Am_No_
Typename
where typename
is replaced by the actual name for the type. Examples are Am_No_Font
, Am_No_Object
, Am_No_Value
, and Am_No_Style
. All of the NULL
wrapper types are essentially equivalent to a NULL
pointer. To test whether a wrapper is NULL
or not one uses the method Valid()
. If a wrapper is not valid, then it should not be used to perform operations.//
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
macro. In the following example, an Am_Object_Method
is defined. An object method has a void
type and takes a single Am_Object
as a parameter. Other method types can have different signatures.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 (<<
), ==
, !=
, Exists
, and Valid
. Exists
and Valid
are used to check the contents of the Am_Value
. Exists
returns true
only if the type is not Am_NONE
or Am_UNINIT
which would happen if the slot has no value or is uninitialized. Valid
returns true if the value exists
(as in Exists
) and if the value is not zero as well:Am_Value value;
my_object.Get (SOME_SLOT, value); // Get the value regardless of type
if (value.Exists()) {
// then it is safe to use value, but value could still be zero
...
}
my_object.Get (SOME_SLOT, value);
// This time we know slot must be an Am_Object
// or uninitialized.
if (value.Valid ()) {
// Checks both existing and value != 0
// 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_Rectangle
which is defined as one of the Amulet graphical objects.
Create
method on the prototype. The optional parameter to Create
is a string which is used if one prints out the name of the object and can be used for other sorts of debugging. The alternative to Create
is the Copy
method which is described below.
Set
the initial values of slots. This includes making new slots if desired.
Create
and Set
call on its own line.
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.Am_Root_Object
. Programmers will typically create instances of the pre-defined objects exported by the various Amulet files (as shown in the examples in this manual), but Am_Root_Object
is useful if one defines application-specific objects that do not concern the Amulet toolkit per se.Copy
method can also be used to make new objects. Instead of being an instance of the prototype object, a copied object will become a sibling of the object. Every slot in the prototype is copied in the same manner as in the original. If a slot is local in the original, it will be local in the copy likewise if the slot is inherited, the copy will also be inherited.
Is_Instance_Of (
object)
- Returns true if the object is an instance of the parameter. Objects are defined to be instances of themselves so obj.Is_Instance_Of (obj)
will return true
.
Get_Prototype ()
- Returns the prototype for the object.
Is_Slot_Inherited (
slot_key)
- Returns true
if the slot is not local, and the value is inherited from a prototype.
obj.Destroy ()
- Destroys the object and all its parts.
Get_Name ()
- Returns the string name of the object defined when the object was created. ORE also defines operator<<
so name can be printed using C++ cout
statements.
Print_Name (
ostream&)
- Prints the string name of the object to the given stream. Acts like the <<
operator.
Text_Inspect ()
- This prints out all of the object's data including slots and parts to cout
. It is commonly used for debugging and has been designed to be unlikely to crash when invoked inside a debugger.
==
and !=
with other objects.
Remove_Slot (
slot_key)
- Removes the slot from the object.
Am_Command
objects (see the Interactor chapter, Section 5.6) are like this. A command object never stores a reference to itself so when all variables that refer to the command object are reset or destroyed, the command object will go away. Unfortunately, this kind of automatic deallocation is not guaranteed. Objects are containers that can hold arbitrary wrapper objects including references to itself. Any circularity will defeat the reference counting scheme; hence, most objects have to be explicitly destroyed.
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.Am_Window
and Am_Group
objects which are designed to hold graphical parts (rectangles, circles, text, etc.). Thus, if you want to make a composite of graphical objects, they should be added as part of a window or group. Non-graphical objects can be made parts of any kind of object. Thus, an Interactor object can be a part of a rectangle, group, or any other object. Similarly, any kind of object that an application defines can be added as a part of any other object. For a graphical object, its ``owner'' will be a window or a group, but the owner of an interactor or application object can be any kind of object. Opal does not support adding graphical objects as parts of any other graphical objects (so that one cannot add a rectangle directly as a part of a circle; instead, one creates a group object and adds the rectangle and circle as parts of the group).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
When an object is copied using the // to the same value, other_object.
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.
Get_Part (
part_key) -
Returns the part of an object.
Get_Owner ()
- Returns the owner of an object.
Get_Sibling (
part_key)
- Equivalent to obj.Get_Owner().Get_Part(
part_name);
Remove_From_Owner ()
- Removes an object from its owner.
Remove_Part (
part_key)
- Removes the part named part_name
.
Remove_Part (
object)
- Removes object part
from owner obj
Is_Part_Of (
object)
- Returns true if the object is a part of the given parameter object. Objects are considered to be parts of themselves i.e. obj.Is_Part_Of (obj)
returns true.
Get_Key ()
- If the object is a named part, then this will return the slot key name for the part. If it is unnamed it will return Am_NONE
if it can be inherited and Am_NO_INHERIT
if it cannot.
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
.Am_Define_Style_Formula
returns the type Am_Style
. Actually, all wrapper formulas return the same type, Am_Wrapper*.
Since it is confusing to look at a formula function that is supposed to return Am_Style
or Am_Object
and see that it returns Am_Wrapper*,
these macros are available to make the code better reflect what is really intended. The various wrapper types are described in the Opal chapter:
Am_Define_Formula (type, formula_name)
- General purpose: returns specified type
.
Am_Define_No_Self_Formula (type, function_name)
- General purpose: returns specified type
. Used when the formula does not reference the special self
variable, so compiler warnings are avoided.
Am_Define_Value_Formula (formula_name)
- Return type is void. This formula has a Am_Value&
parameter named value
which the programmer uses to return a value. Used when the formula might return different types, described in Section 3.7.1.2.
Am_Define_Object_Formula (formula_name)
- Return type is Am_Object
.
Am_Define_String_Formula (formula_name)
- Return type is Am_String
.
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_Value_List_Formula (formula_name)
- Return type is Am_Value_List
.
Am_Define_Cursor_Formula (formula_name)
- Return type is Am_Cursor
.
Am_Formula
variable is the name that needs to be externally declared. For example:
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.
Am_Value
as return value or a parameter instead of using a normal return value.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;
}
Get
method is used with a constraint context (or equivalently, when the GV
macro is used), the constraint solver decides how the actual Get
is performed. The formula constraint solver sets up a dependency to the slot being fetched. Whenever the fetched slot changes value, the formula will be notified by the system and will automatically update its value by calling the formula procedure. The formula:
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:
GV (
slot) -
Get the specified slot, setting up a constraint.
GVM (
s1,
value) -
Get the specified slot into a Am_Value
parameter, setting up a constraint. Unfortunately, macros cannot be overloaded like procedures; hence the different name.
GV_Owner ()
- Get the owner of an object, setting up a constraint.
GV_Part (
part_name)
- Get the specified part of an object, setting up a constraint.
GV_Sibling (
part_name)
- Get the specified sibling of this object, setting up a constraint.
GV_Object (
slot)
- Get the value of a slot, setting up a constraint. The slot must contain an Am_Object
.
Am_Formula
variable. Normally, the Am_Formula
variable will be defined using the standard macros in which case the programmer calls Set
with the formula name to install it. If the programmer has defined the formula procedure without using the macros, then one then needs to create a formula object using that procedure. The formula object is then Set
into a slot.
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.Am_Value_List
is a form of wrapper, it supports all the standard wrapper operations, including:
Am_Value_List l2 = l1;
Am_Value_List
: Am_Value_List::Test (value)
.
void Start () -
Make first element be current. This is always legal, even if the list is empty.
void End ()
- Make last element be current. This is always legal, even if the list is empty.
void Prev ()
- Make previous element be current. This will wrap around to the end of the list when current is at the head of the list.
void Next ()
- Make next element be current. This will wrap around to the beginning of the list when current is at the last element in the list.
bool First ()
- Returns true when current element passes the first element.
bool Last ()
- Returns true when current element passes the last element.
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
.
Am_Value_Lists
are circular lists. Prev()
and Next()
wrap around, but be aware that there is a NULL
list item, which you cannot do a Get()
on, between the Last
and First
elements in the list. To wrap around, you should do an extra Next()
or Prev()
at the end of the list:
my_list.Start();
while (true) { // endless circular loop
do_something_with(my_list.Get());
my_list.Next();
if (my_list.Last()) my_list.Next(); // an extra Next at the end of the list
}
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)
Get
method retrieves the value at the current position. Like Am_Object
's Get
, it returns an Am_Value
which can be cast into the appropriate type. Another Get
is available that returns Am_Value
as a parameter. For example:
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_List
s 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
.
Am_Value_List
for some types of list-like information, and instead supplies an ``iterator.'' An iterator object contains a current pointer and allows the programmer to examine the elements one at a time. There are three kinds of iterators available in ORE: one for slots, one for instances, and another for parts. Each of the iterators has the same basic form, and the interface is essentially the same as for Am_Value_Lists
.
Start
, Next
, and Get
. Start
places the iterator at the first element. Next
moves the iterator to the next element. And Get
returns the current element. To initialize the list, assign the iterator with the object that contains the information that the programmer want to iterate upon. For example:
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.
Am_Part_Iterator
iterates over the parts of an object. Its Get
method returns an Am_Object
which is the part. To list the parts stored in Am_Groups
and Am_Windows
, however, it is better to use the Am_Value_List
stored in the Am_GRAPHICAL_PARTS
slot instead of the part iterator. The part iterator would list all parts in the object including many that are not graphical objects.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.
Am_GRAPHICAL_PARTS
slot which contains an Am_Value_List
of the graphical parts, sorted correctly).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.
Am_Error
routine which prints out the error and then aborts the program. If you have a debugger running, it should cause the program to enter the debugger.
Am_Style
's, are immutable, which means that once created, the programmer cannot change their values. Other wrapper objects, like Am_Value_Lists
are mutable. The default Amulet interface copies the wrapper every time it is used and automatically destroys the copies when the programmer is finished with them (explained in Section 3.11.2.2). This design prevents the programmer from accidentally changing a wrapper value that is stored in multiple places, and it helps prevent memory leaks. However, for programmers that understand how Amulet manages memory, it is unnecessarily wasteful since making copies of wrappers is not always required. This section discusses how you can modify a wrapper value without making a copy. See also the discussion of the wrapper implementation in Section 3.11.2.
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);
The purpose of the // tell Amulet that I modified the slot
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.Make_Unique
be called before one actually Get
's the value. By storing the wrapper in a local variable, the wrapper will not be unique and Make_Unique
will make a unique copy for the slot. If the programmer would like to examine the contents of a slot before deciding whether to perform a destructive change, then one can use the Is_Unique
method. Is_Unique
returns a bool that tells whether the slot's value is already unique. By storing the value from Is_Unique
before calling Get
, the programmer can decide whether to perform destructive modification or not.// 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.Am_Style
is the object layer for the Am_Style
wrapper. Inside the object layer is the data layer. For Am_Style
, the type is called Am_Style_Data
. Normally, programmers are not permitted access to the data layer. The object layer of the wrapper is used to manipulate the data layer which is where the actual data for the wrapper is stored.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.Note_Reference
tells the wrapper object that the value is being referenced by another variable. The reference could be a slot or a local variable or anything else. The implementation of Note_Reference
is normally to simply add one to the reference count. Release
is the opposite of Note_Reference
. It says that the variable that used to hold the value does not any longer. Typical implementation is to reduce the reference count by one. If the reference count reaches zero, then the memory should be deallocated. Ref_Count
returns the value of the reference count so that external code can count how many of the total references it holds.Make_Unique
is the trickiest of these methods to understand. The basic idea is that a programmer should not be allowed to modify any wrapper value that is not unique. For example, if the programmer retrieves a Am_Value_List
from a slot and adds an item to the list, this destructive modification should normally not affect the list that is still in the slot. The way to maintain this paradigm is for the method used to modify the wrapper's data to first call Make_Unique
. If the reference count is one, the wrapper value is already unique and Make_Unique
simply returns this
. If the reference count is greater than one, then Make_Unique
generates a new allocation for the value that is unique from the original and returns it. Either way only a unique wrapper value will be modified. Some wrapper types have boolean parameters on their destructive operations that turn off the behavior of Make_Unique
to allow the programmer to do destructive modifications (see Section 3.11.1).
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.Am_ID_Tag
which is an unsigned integer. Integers are dispensed using the function Am_Get_Unique_ID_Tag
. Normal procedure is to define a static member to the wrapper data class called id
which gets initialized by calling Am_Get_Unique_ID_Tag
. This function takes a name and a class ID number to generate an ID for the wrapper. ID tags and value types are one and the same concept. The wrapper ID is the same value that is stored as the wrapper's type.Am_WRAPPER_DATA_DECL
and Am_WRAPPER_DATA_IMPL
macros. The macros require that the user define at least two methods in the wrapper data class. The first required method is a constructor to be used by Make_Unique
. The method is used to create a copy of the original value which can be modified without affecting the original. This can be done by making a constructor that takes a pointer to its own class as its parameter. Make sure that in all the data class constructors to initialize the refs
member (defined by Am_WRAPPER_DECL
) to 1. The second required method is an operator==
to test equality. The ==
method does not need to check that the parameter is of the correct type because that is handled by the default implementation of the operator==
that takes a Am_Wrapper&
and which calls the specific == routine if the types are the same.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.Am_Foo
type but currently has a Am_Foo_Data*
stored in his data.// 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 ();
The way to test if a given wrapper is the same type as a known wrapper class is to compare IDs. The static method // Release data when through
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.Make_Unique
is called before the existing object is returned. If the object can be destructively modified, then the wrapper must be made safe before the modifications occur. However, if the programmer wants the wrapper object to behave as if it were the original class then some reimplementation may be required.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.Am_Web_Create_Proc
, Am_Web_Initialize_Proc
, and Am_Web_Validate_Proc
. These procedures always use the same signature since there is no return type to worry about. The validation procedure is the most similar to the formula's procedure. It is executed everytime the web is reevaluated and it typically consists of the most code. The initialization procedure is run once when the web is first created. Its purpose is to set up the web's dependencies, especially the output dependencies. The create procedure concerns what happens when a slot containing a web constraint is copied or instantiated. Since a web can have multiple output dependencies, it does not really belong to any one slot. So, a slot is chosen by the programmer to be the ``primary slot.'' That slot is considered to be the web's owner and the location where a new web will be instantiated. The create procedure lets the programmer identify the primary slot.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
.SV
in a web is similar to using GV
except that it is used for output instead of input. Every slot set with SV
will be assigned a constraint pointer to that web making the slot dependent on the web. It is legal to both GV
and SV
a single slot. In that case, the web would both be a constraint and a dependency on that slot. Both the constraint and dependency are kept whenever either are used in the validation procedure. Thus, in the above example, when GV
is called on the SUM
slot, the web's constraint on that slot will be kept. If neither GV
or SV
are called on a slot during validation, the dependency and/or constraint will be removed.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.Get_Slot
retrieves a slot and returns it as the advanced class Am_Slot
. Get_Slot
will always return a slot local to the object. If the slot was previously not local because it is still inherited or for other reasons, Get_Slot
will make a placeholder slot locally in the object and return that. If the slot does not exist at all, Get_Slot
will return Am_NULL_SLOT
. There are two other methods used for fetching slots: Get_Owner_Slot
, and Get_Part_Slot
. These methods are similar to Get_Slot
except they are to be used only for fetching part or owner slots. It is entirely possible to use Get_Slot
instead of these specialized methods, but the specialized methods are more efficient. Other Am_Object_Advanced
method are:
Get_Slot_Locale (
slot_key)
- Returns the object where the given slot is locally defined. Note that being locally defined does not mean that the slot is not inherited for that object. Slots can become local when they are depended on by constraints for other similar reasons.
Disinherit_Slot (
slot_key)
- Breaks the inheritance of a slot from the prototype object. This will delete formulas and values that were not set locally.
Print_Name_And_Data (
ostream&)
- Used for debugging, this prints the contents of the object out in ASCII text.
Am_Window
object has a slot that points to the actual X/11 or MS Windows window object associated with that window. This slot should not be shared by multiple objects. The choices are defined by the enum Am_Inherit_Rule
and are:
Am_INHERIT
: The default. Slots are inherited from prototypes to instances, and subsequent changes to the prototype will affect all instances that do not override the value.
Am_LOCAL
: The slot is never inherited by instances. Thus, the slot will not exist in an instance unless explicitly set there.
Am_COPY
: When the instance is created, a copy is made of the prototype's value and this copy is installed in the instance. Any further changes to the prototype will not affect the instance.
Am_STATIC
: All instances will share the same value. Changing the value in one object will affect all objects that share the slot. We have found this to be rarely useful.
Am_DRAWONABLE
slot of new_win
to be local:
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 independanc
e
#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 ();
Am_VALUE
slot of widgets contain formulas that compute the value based on the user's actions. However, programmers are also allowed to set the Am_VALUE
slot if they want the widget to reflect an application-computed value. In this case, the default formula in the Am_VALUE
slot should not be removed if the programmer sets the slot. To achieve this, the programmer must set the slot's Single_Constraint_Mode
to false
(the default is true
).
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.Get
. By invoking the demons on Get
, Amulet can simulate the effects of eager evaluation because any demon that affects the value of different slots will be invoked whenever a slot is fetched.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.Am_Object_Demon
.// 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.Am_TOP
and Am_LEFT
slots both affect the position of the object. A demon that handles object motion only needs to be triggered once if either of these slots changes. For this case, we use the per-object style. Whenever multiple slots change in a single object, the per-object demon will only be enqueued once. Only after the demon has been invoked will it reset and be allowed to trigger again. The other style of demon activation is per-slot. In this case, the demons act independently on each slot they are assigned. The demon triggers once for each slot and after it is invoked, it will be reset. The per-slot demon does not check to see if other demons have already been enqueued for the same object.// 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 ();
To cause newly created objects to have certain demon bits set, one changes the default demon bits.slot.Set_Demon_Bits (0x0020 | prev_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.Get,
is called on an object. This makes sure that any demons that affect the slot being retrieved are brought up to date. Another time is when the Destroy
method is called on the object. The other time the queue is invoked is when updating occurs in the main loop and other window updating procedures. When windows are updated, the demon queue is invoked to update changed object values.Invoke
method. The queue does have a means for deleting entries. One deletes all the demons that have a given slot or object as a parameter by using the Delete
method.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.