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.14 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 who want to extend Amulet.
3.2 Version
The version of Amulet you are using is stored in the variable Am_VERSION
which is in amulet.h. The value will be a string, something like:''3.0''.3.3 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
. (On the Macintosh, this file is namedstandard_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.14.
value_list.h:
Defines the type Am_Value_List
and all its related methods.
3.4 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.4.1 Get, Peek, Set and Add
The basic operations performed on slots are Get
, Set
and Add
. A slot is essentially a key-value pair. Get
takes a slot key and returns the slot's value. Peek
is the same as Get
, except it doesn't raise an error if the slot is not there. Set
takes a key and value and replaces the slot's previous value with the one given. Add
is the same as Set
, except it is used to create new slots. Creating new slots is done by performing Add
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.Add (FOO, 45.3f); // Create a new slot called FOO and initialize with the float 45.3
Am_Value v = my_object.Peek(ZIP); // Get the value of ZIP if it is there.
Calling Get
on a slot which does not exist raises an error (but see Section 3.4.10 for how to avoid the error). Calling Peek
never raises an error, but may return an Am_Value
which is an error value. Calling Set
on a slot that does not exist, or calling Add
on a slot which already exists, also raises an error (but see Section 3.4.10 for how to avoid these errors). The motivation for this is that we found in previous versions of Amulet that users were often Getting and Setting the wrong slot and wondering why the nothing happened. Therefore, the system now helps with this by notifying you when a slot that is being set or get isn't one of the standard slots. If you intend to add a new slot, you must specify that explicitly using Add instead of Set.
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_Object(PART_SLOT).Get (Am_LEFT);
3.4.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. Finally, there is Am_From_Slot_Name
which takes a slot name and returns its key or 0
if the slot is not defined.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";
Am_Slot_Key key = Am_From_Slot_Name("MY_BAR_SLOT");
if (key == 0) cout << "No slot by that name\n";
3.4.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);
// type == Am_Style
3.4.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.4.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.4.11).
3.4.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.4.8). 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.4.7 No Type, Zero Type, and Error Types
There are various kinds of error and empty values supported by Amulet, and they are used for different situations:
Am_No_Value
: Is of type Am_NONE
. This is the only value of type Am_NONE
, and it used to explicitly set a slot with an illegal value. For example, the Am_VALUE_1
slot of the Am_Number_Input_Widget
can be any integer to specify the minimum value allowed, or it can be Am_No_Value
to mean that there is no minimum specified. Am_No_Value
is not considered an error type.
Am_FORMULA_INVALID, Am_MISSING_SLOT
and Am_GET_ON_NULL_OBJECT
: When a Peek
(or Get
with the Am_OK_IF_NOT_THERE
parameter, see Section 3.4.10) is performed and the value cannot be returned, it is sometimes useful to be able to determine why the value is not available. Therefore, there are three different error types depending on why the slot is not available.
Am_Zero_Value
: Sometimes it is useful to get a value back from Get
that can be used as a zero. If the Am_RETURN_ZERO_ON_ERROR
flag is passed to Get
(see Section 3.4.10), then the Am_Zero_Value
will be returned instead of the above error values.
false
from the xxx.Valid()
and xxx.Exists()
tests (see Section 3.4.11).
3.4.8 Using Wrapper Types
Although one could store C++ objects into ORE slots as a void*
, ORE provides two different ways to ``wrap'' these pointers so they can be handled in a type-safe manner. The most robust is to use the Am_Wrapper
type which provides 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.4 and is somewhat complex. A simpler technique uses the Pointer_Wrapper types, which just provide type safety for the pointers, but no memory management. See *** for how to make new Pointer_Wrapper types.
However, using any of the wrapper types is simple. Notable wrapper types in Amulet are Am_Style
, Am_Font
, Am_String
, Am_Value_List
(see Section 3.9), 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.Am_Value v = object.Get(MY_SLOT);
if (v.type == Am_Style::Type_ID ())
Am_Style color = v;
Also, as described in the next section, most wrappers supply the Test
static method which takes an Am_Value
:Am_Value v = object.Get(Am_VALUE);
if (Am_Value_List::Test(v)) { //then is a value list
3.4.8.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()
or Exists()
(see Section 3.4.11). 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.Peek (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.4.9 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 returns nothing (it has a void
return type) and it 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 Am_Define_Method
macro, the compiler can check to make sure that the actual method signature matches the one defined in the type. To set the method into a slot and retrieve the typename value, one uses Set
or Add
and Get
or Peek
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 *Type_ID
method.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 Am_Define_Method_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.4.10 Flag parameters to Get and Set
The Get
, Peek
, Add
and Set
methods (and various other methods discussed below) take an optional final parameter which is of type Am_Slot_Flags
. These flags control various errors and other properties of the setting and getting operations. The flags are bit fields, so they can be OR'ed together to get multiple affects. The current flags are:
Am_OK_IF_NOT_THERE
- Specifies that a Set
or Get
should not raise an error if the slot is not there.
Am_OK_IF_THERE
- Specifies that an Add
should not raise an error if the slot is already there.
Am_RETURN_ZERO_ON_ERROR
- Specifies that a Get
should return a value which will turn itself into a zero if there is an error.
Am_NO_DEPENDENCY
- Specifies that a Get
should not set up a formula constraint dependency (see Section 3.8.2)
Am_OVERRIDE_READ_ONLY
- Specifies that a Set
should work even if the slot is declared read only. See ****.
Am_KEEP_FORMULAS
- Specifies that the formulas in the slot (if any) should not be removed because of this Set
, even if the formula is not marked Multi_Constraint. See Section 3.14.5.
Am_NO_ANIMATION
- Specifies that no animations should happen as a result of this Set
. See Section 6.6 in the Animations chapter.
Am_WITH_ANIMATION
- Specifies that animations should happen as a result of this Set
even if the animator's Am_ACTIVE
slot is false. See Section 6.6 in the Animations chapter.
Calling
Set
with the parameter Am_OK_IF_NOT_THERE
or calling Add
with the parameter Am_OK_IF_THERE
are entirely equivalent, and will never return an error.
Peek
is entirely equivalent to Get
with the parameter Am_OK_IF_NOT_THERE
.Some examples:
obj.Set(SLOT, Am_No_Value);
int i = obj.Get(SLOT); //will raise an error because not an integer
Am_Value v = obj.Get(SLOT); //v be Am_No_Value
v = obj.Peek(NO_SUCH_SLOT); //assume slot NO_SUCH_SLOT is not in obj
// v has type Am_MISSING_SLOT
if (v.Valid()) {} // v is not Valid
v = obj.Get(NO_SUCH_SLOT); //will raise an error because no such slot
v = obj.Get(NO_SUCH_SLOT, Am_OK_IF_NOT_THERE); //returns Am_MISSING_SLOT
int i = obj.Get(NO_SUCH_SLOT, Am_OK_IF_NOT_THERE); //error because not int
int i = obj.Get(NO_SUCH_SLOT, Am_RETURN_ZERO_ON_ERROR); // i = 0
// next Set doesn't remove formulas and doesn't start an animation
obj.Set(Am_LEFT, 15, Am_KEEP_FORMULAS | Am_NO_ANIMATION);
Get
and Peek
methods are defined as:
const Am_Value& Get (Am_Slot_Key key, Am_Slot_Flags flags = 0) const;
const Am_Value& Peek (Am_Slot_Key key, Am_Slot_Flags flags = 0) const
{ return Get(key, flags | Am_OK_IF_NOT_THERE); }The
Am_Value
type which is used as the return type is a union for all the ORE types. The Am_Value
type can be coerced to all the standard Amulet types automatically. Normally, the programmer simply sets the return value directly into the final destination variable:
//Get returns an Am_Value type, and the compiler uses the Am_Value's
// operator int method to turn it into an int automatically.
int i = obj.Get(Am_LEFT);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 just use the Am_Value
returned directly:
Am_Value v = obj.Get(Am_LEFT);
if (v.type == Am_INT) ...If you are not sure whether the slot is there or not, be sure to use
Peek
or the Am_OK_IF_NOT_THERE
or Am_RETURN_ZERO_ON_ERROR
flags, as described in Section 3.4.10.
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
.bool Am_Value::Exists() const;
bool Am_Value::Valid () const
Exists
returns false
if the type is an error type (Am_FORMULA_INVALID
, Am_MISSING_SLOT
or Am_GET_ON_NULL_OBJECT
) or Am_No_Value
(Am_NONE
type). Note that Exists
returns true
for 0
and NULL
and for Am_ZERO
types.Valid
returns true
if the value exists
(as in Exists
) and if the value is not zero as well. Thus, Valid
returns false
if the value is false
or zero or NULL
, or illegal.Am_Value value;
value = my_object.Get (SOME_SLOT); // Get the value regardless of type
if (value.Exists()) {
// then it is safe to use value, but value could still be zero
...
}
value = my_object.Get (SOME_SLOT); // 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.
...
}
obj.Set(Am_LEFT, 0);
bool b = obj.Valid(); //false because is zero
bool b = obj.Exists(); //true because is not an error type
Am_Values also can be used to print and get the names of objects. This normally only works if Amulet is compiled with the DEBUG
switch on.value.Print(ostream& out) const;
value.Println() const { Print(cout); cout << endl << flush; }
Print
prints the object to the specified stream. The Println
version is designed for use in a debugger, and should print out the name of the object. If the value holds a wrapper type which is not an object, then these use the wrapper's type_support mechanism as described in Section 3.11.4.2.
To get the name of an object, use:
const char * To_String() const;
To_String
returns the string name of the value. For objects, this looks up the name. Some wrapper types will generate a name. Uses strstream
and Print(out)
if the value doesn't have a string name, so this is somewhat inefficient.static Am_Value From_String(const char * string, Am_ID_Tag type = 0);
This can be used to look up an object or other type given its name. If the string is in the global name registry, then the type is not needed. If the name is not there, then looks for a Am_Type_Support
for the ID. Will return Am_No_Value
if can't be found or can't be parsed. As an example, the following will return the rectangle prototype object, if this is compiled with DEBUG
on:Am_Object rect_proto_obj = Am_Value::From_String("Am_Rectangle");
3.4.12 Advanced features of the Am_Value_Type
The Am_Value_Type
. which is what is returned by obj.Get_Slot_Type(xxx), is actually 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 3 bits are the type class. Currently, there are five kinds of classes,
Am_SIMPLE_TYPE
- Used for the C++ types like Am_INT
and Am_FLOAT.
Am_ERROR_VALUE_TYPE
- Used for all of the error types, including Am_FORMULA_INVALID
, Am_MISSING_SLOT
and Am_GET_ON_NULL_OBJECT
.
Am_WRAPPER
- Used to denote wrappers like Am_Object
and Am_Style.
Am_METHOD
- Used for types that are methods like Am_Object_Method
and Am_Where_Method.
Am_ENUM
- Used for types that are represented as longs, such as enumerated types, external pointers, and encoded bit fields.
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 (this is not usually useful).
Am_Value_Type type = object.Get_Slot_Type (Am_FILL_STYLE); // An Am_Style.
Am_Value_Type type_class = Am_Type_Class (type);
// type_class == Am_WRAPPER
Here is an example of creating an ORE object and setting some of its slots:
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 are:
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 with Add
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 an Am_LINE_STYLE
slot with the same value. If a slot is inherited, it will change it 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.14.4 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 original object, a copied object will become a sibling of the original, and an instance of the original's prototype. Every slot 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. Like Create
, Copy
can take an optional string name for the new object.Copy_Value_Only
is like Copy
, except that it throws away all constraints. This is used when you want to ``archive'' an object, and is used by the undo mechanism so command objects on the history list keep the values used when the command was executed, since constraints used during execution should not be re-evaluated at Undo time.
Am_Object Create (const char* new_name = NULL) const;
new_obj = obj.Create()
new_obj.Get_Prototype() == obj
Here new_obj
will be an instance of obj
. Changes to obj
will affect new_obj
.
Am_Object Copy (const char* new_name = NULL) const;
new_obj = obj.Copy()
new_obj.Get_Prototype() == obj.Get_Prototype()
Here new_obj
will be a copy of obj
. Changes to obj
will not affect new_obj
.
Am_Object Copy_Value_Only (const char* new_name = NULL) const;
new_obj = obj.Copy_Value_Only()
Here new_obj
will be a copy of obj
, except that new_obj
will not contain any constraints.
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.
Am_Object& Set_Name (const char* name)
- changes the name of the object.
Am_Value
From_String
(const char * name
) - When the Amulet library is compiled with the preprocessor symbol DEBUG
defined, returns the object that was created with the specified name. When the library is compiled without defining DEBUG
, returns Am_No_Value
. Note that the methods for creating objects do not guarantee that all objects have unique names.
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. This returns the original object (like Set
) so it can be used in a series of Set
calls.
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.7 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.7.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_Object
(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.10) or else by reading it from a list like the Am_GRAPHICAL_PARTS
slot in group objects (described in the Opal chapter, Section 4.8.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
Named parts that already exist should be set with Set_Part
. Thus Add_Part
is for adding parts that do not exist (just as Add
is for adding slots that don't exist) and Set_Part
is for setting parts that already exist (just as Set
is for setting slots that already exist). Like Add
and Set
, Add_Part
and Set_Part
have an optional Am_Slot_Flags
parameter so you can use flags like Am_OK_IF_THERE
and Am_OK_IF_NOT_THERE
:// Adds a part to an object. The part either be named by providing a key or unnamed.
Am_Object& Add_Part (Am_Object new_part, bool inherit = true);
//slot must not already exist (unless Am_OK_IF_THERE flag).
//slot must never be a regular slot
Am_Object& Add_Part (Am_Slot_Key key, Am_Object new_part,
Am_Slot_Flags set_flags = 0);
//Slot must exist (unless Am_OK_IF_THERE), and must already be a part slot
Am_Object& Set_Part (Am_Slot_Key key, Am_Object new_part,
Am_Slot_Flags set_flags = 0);
3.7.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 parameter
// 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.7.3 Other Operations on Parts
Other methods on objects relevant to parts are listed below.
Am_Object Get_Object (Am_Slot_Key key, Am_Slot_Flags flags = 0) const
Get_Object
works for part slots or regular slots. It is equivalent to coercing the return of the Get
to an Am_Object
.
Am_Object Peek_Object (Am_Slot_Key key, Am_Slot_Flags flags = 0) const
{ return Get_Object (key, flags | Am_OK_IF_NOT_THERE); }
Am_Object Get_Owner (Am_Slot_Flags flags = 0) const
{ return Get_Object (Am_OWNER, flags); Am_Object Get_Sibling (Am_Slot_Key key, Am_Slot_Flags flags = 0) const
{ return Get_Owner (flags).Get_Object(key, flags); }
Am_Object Peek_Sibling (Am_Slot_Key key, Am_Slot_Flags flags = 0) const
{ return Get_Owner(flags).Get_Object(key, flags | Am_OK_IF_NOT_THERE); }
void Remove_From_Owner () --
Removes an object from its owner.
Am_Object& Remove_Part (
part_key) --
Removes the part named part_key.
Am_Object& Remove_Part (
object) --
Removes the part object.
bool Is_Part_Of (
object)
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
.
Am_Slot_Key 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_NO_SLOT
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_Object (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.14.2.
3.8.1 Formula Functions
An ORE formula consists of a C++ function that computes the value to set into a slot based on the values of the slot's dependencies. The formula function takes a single parameter, self
, an Am_Object&
that points to the object containing the slot. The return value of the formula function is the type that the slot will be when it takes on the returned value. Dependencies are set up automatically when Get
is used to reference another slot, using a internal mechanism known as a constraint context. While constraint contexts were visible in Amulet v2 as opaque handles, and hidden only by macros, they do not clutter the interface in Amulet v3.Am_Define_Formula
macros. For example:// 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.Get_Owner ();
int owner_width = owner.Get (Am_WIDTH);
int my_width = self.Get (Am_WIDTH);
return (owner_width - my_width) / 2;
}
When used as the value of a slot, this formula returns half the difference between the containing object's owner's Am_WIDTH
and its own Am_WIDTH
. Whenever the value of either of these Am_WIDTH
slots changes, the formula will be recomputed.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_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
.
my_left_formula
defined above using the Am_Define_Formula
macro expands into the following code.
// 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_Object& self) {
Am_Object owner = self.Get_Owner ();
int owner_width = owner.Get (Am_WIDTH);
int my_width = self.Get (Am_WIDTH);
return (owner_width - my_width) / 2;
}
Am_Formula my_left_formula (my_left_formula_proc, "my_left_formula");It defines a global variable of type
Am_Formula
named my_left_formula
, which is the actual constraint that can be used to set a slot, and a static
C++ procedure of type int
(in this case) named my_left_formula_proc
, which is called when the constraint needs to be reevaluated. In order to use a formula constraint outside of the file that defines it, C++ expects an external declaration. The formula procedure does not need to externally declared. In fact formula procedures are normally declared static so that they do not infringe on the C++ external namespace. The
Am_Formula
variable is the name that needs to be externally declared. For example:
extern Am_Formula my_left_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
, a polymorphic formula should return a value of type Am_Value
, and leave it to the slot's user to interpret the value.A formula might return an
Am_Value
because it returns the value of some other slot without checking its type:
Am_Define_Formula (Am_Value, my_copy_formula) {
return other_obj.Get (SOME_SLOT);
}Or a formula might return one type under some conditions and another when those conditions are false. In this 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.
Am_Define_Formula (Am_Value, my_formula) {
if ((bool)self.Get (Am_SELECTED))
return 5;
else
return Am_Blue;
}To read a slot set with an unknown type, the programmer can call
Get_Slot_Type
or simply retrieve its value into an Am_Value
.
Get
method is used with within a formula, a dependency is automatically created on the slot being fetched. Then whenever the fetched slot changes value, the formula is notified by the system and automatically updates its value by calling the formula procedure.The formula:
Am_Define_Formula (int, my_left_formula) {
int owner_width = self.Get_Owner ().Get (Am_WIDTH);
int my_width = self.Get (Am_WIDTH);
return (owner_width - my_width) / 2;
}defines three slots as dependencies: the object's
Am_OWNER
slot (the Get_Owner
macro expands into a Get
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 Get
on different slots. Thus the above formula's dependency on the owner's width will change if the object ever gets moved to a new owner.To avoid creating a dependency on a fetched slot, the programmer should pass the
Am_NO_DEPENDENCY
flag when calling Get
. For example, the Am_Width_Of_Parts
formula contains a test to avoid creating a dependency on a slot that isn't there.
if (self.Peek (Am_H_FRAME, Am_NO_DEPENDENCY).Exists())
max_x += (int)self.Get (Am_H_FRAME);
return max_x;Other formulas make sure that
self
has an owner before checking any slot values. This technique is useful to prevent a constraint from being marked invalid (See the next section.) due to initialization order dependencies.
Am_Object button = self.Get_Owner(Am_NO_DEPENDENCY);
if( button.Valid() )
return (bool)button.Get( Am_ACTIVE ) && (bool)button.Get( Am_ACTIVE_2 );
return false;Note that is perfectly reasonable to use
Peek
within a formula when a slot may be missing or uninitialized. A dependency will be created, however, unless the Am_NO_DEPENDENCY
flag is used.
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.Get_Owner ().Get (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) {
return (int)self.GV_Owner ().Get (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 variables that a formula references are defined before the formula is set into a slot. Formulas that reference missing or undefined slots or
NULL
objects never crash; instead the constraint is marked as invalid and Get
returns the Am_Value
Am_Zero_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.Get_Owner ().Get (Am_LEFT);
}It is an error to fetch the value of an uninitialized slot from outside a formula. One can check if a slot is uninitialized by calling
Get_Slot_Type
or by retrieving the value into an Am_Value
using Peek
and then calling Exists()
or Valid()
. For example:
Am_Value v = obj.Peek(SLOT);
if (v.Exists()) ... //value is not an errorIf the slot is invalid, the
Am_Value
returned by Peek
will have an Am_Error_Value_Type
and Exists()
will return false
.
Multi_Constraint()
method, for example:
my_rect.Set (Am_LEFT, my_left_formula.Multi_Constraint());This is an advanced feature covered in Section 3.14.5.
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.
3.8.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. 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 (self) + 1;
}
3.9 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 stored as a slot value. 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:
=
), which copies the list: Am_Value_List l2 = l1;
==
), which tests whether the two lists contain identical values (it iterates through each element testing for ==
).
list.Valid ()
.
Am_Value_List
: Am_Value_List::Test (value)
.
Am_Value_List
contains a pointer to the ``current'' item. This pointer is manipulated using the following functions:
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 elements 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 does 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 an 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. For example:
Am_Value v;
for (my_list.End (); !my_list.First (); my_list.Prev ()) {
v = my_list.Get();
cout << "List item type is " << v.type << endl << flush;
}To find the type of an item without fetching the value use
Get_Type()
.
Get_Nth
is used to retrieve the nth item in a list (the first item is the 0th item); Move_Nth
makes the nth item the current item. Both methods take an int
argument.
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
. Empty_List
() may also be used to create an Empty
list that is not NULL
.Finally,
Copy
creates a new copy of a list.
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 effect 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.
Amulet provides a number of ways to link to external objects. In all of these, you store the C++ object or a pointer to the C++ object into a slot of an Amulet object. To put any kind of Amulet type or object into your C++ object, you can just declare it normally, like you would as a variable. For example:
class myclass {
public:
Am_Object my_object_in_class;
Am_Style my_style_in_class;
int i_in_class;
void method_in_class();
};You can now create instances or subclasses of myclass and assign the Amulet objects. Amulet will correctly handle the storage allocation for these objects and types.
To store your C++ objects into an Amulet object, there are various options depending on how much you need and how much work you want to do. These include coercing a pointer to a void*, using the Pointer Wrapper, using the Enum Wrapper, or creating a full-fledged wrapper type. In brief:
Am_Ptr
instead of void*
as the type, since this is guaranteed to work on all architectures. For example:
myclass* m = new myclass;
//all of these explicit coercions are required by most compilers
Am_Object x = Am_Rectangle.Create()
.Set(Am_VALUE, (Am_Ptr)m);
myclass* n = (myclass*)(Am_Ptr)x.Get(Am_VALUE);
Am_Type_Support
class is used to describe the C++ objects stored into slots, mostly for debugging. You will create a subclass of the main Am_Type_Support
class to customize the print and read methods, and you can of course add extra instance variables to hold specific information. For example, Am_Enum_Support
is a subclass of Am_Type_Support
which adds extra fields to store the list of the enumerated values. The methods of the type support subclass that might be overridden include:
virtual void Print (ostream& os, const Am_Value& value) const;
This method should print a user understandable representation of the value. This method is used by the Inspector. The default method prints the type name if available and the pointer in hex.
virtual const char * To_String(const Am_Value &v) const;
This returns a string version of the type if that does not require allocating any memory. Otherwise, it should return NULL. The default method looks up the value in the name registry and if there, returns the string, and if not, returns NULL.
virtual Am_Value From_String (const char* string) const;
This is used to convert a string version of the value into the C++ object, which is stored as the Am_Value
return value. If no value can be created, then return Am_No_Value
. The default method looks up the name in the name registry and returns the associated object, or Am_No_Value
if not there. You might supply an entire parser for whatever format is produced by Print. This method is used by the Inspector.
class Am_Slot_Key_Type_Support_Class : public Am_Type_Support {
public:
void Print (ostream& os, const Am_Value& value) const {
os << Am_Get_Slot_Name((Am_Slot_Key)value.value.long_value);
}
const char * To_String(const Am_Value &value) const {
return Am_Get_Slot_Name((Am_Slot_Key)value.value.long_value);
}
Am_Value From_String (const char* string) const {
Am_Slot_Key key = Am_From_Slot_Name (string);
if (key) {
Am_Value v(key, Am_SLOT_KEY_TYPE);
return v;
}
else return Am_No_Value;
}
};
Am_Type_Support *slot_key_type_support =To associate the new type support with a particular type, you will probably use one of the macros described in the next sections. If these macros are not appropriate, then there are two other methods for associating a type support pointer with a type. The first is to register the type when you create a new
new Am_Slot_Key_Type_Support_Class();
Am_ID_Tag
using the form of Am_Get_Unique_ID_Tag
:
extern Am_ID_Tag Am_Get_Unique_ID_Tag (const char* type_name,
Am_Type_Support* support, Am_ID_Tag base);The base is used to separate out the different classes of IDs, as described in Section 3.4.12.
If you already have the
Am_ID_Tag
, then you can use:extern void Am_Register_Support (Am_Value_Type type,
For example, the slot key type support created above is registered using the following code:
Am_Type_Support* support);
Am_Register_Support(Am_SLOT_KEY_TYPE, slot_key_type_support);
Finally, to find the type support object associated with a type, you can use Am_Find_Support. This is used by the Inspector. (Am_Value_Type is the same as Am_ID_Tag).extern Am_Type_Support* Am_Find_Support (Am_Value_Type type);
3.11.2 Pointer Wrappers
A Pointer Wrapper is used to ``wrap'' pointers to C++ objects. This is a relatively simple interface which supports some type checking and debugging support. It does not provide memory allocation, however, so the programmer must be careful about this. To create a pointer wrapper, use the macros Am_Define_Pointer_Wrapper
in the .h
file, and Am_Define_Pointer_Wrapper_Impl
in the .cc
file (or use both in the .cc
file if there is no .h
file). The _Impl
version takes a pointer to an Am_Type_Support
which you should create for your type, as described in the previous section. Note that in these macros, the Type_name
argument is the name of the base type, not of the pointer to the type.Am_Define_Pointer_Wrapper(Type_name)
Am_Define_Pointer_Wrapper_Impl(Type_name, Type_Support_Ptr)
The result of these macros is a new type, called Am_
Type_name (with Type_name replaced with the actual name of your type) which is used when the pointers are assigned into a slot. Note: be sure to use the wrapper when assigning, or else the type information will not be saved. To get the value out of the Am_
Type_name wrapper, use the .value
accessor. For example, Am_
Type_name(obj.Get(SLOT)).value
. If you want to avoid all type-checking on accessing, you can use the void*
(Am_Ptr
) form on the Get
as described above (Section 3.11):
As a complete example:
//In the .h file:
class my_class {
public:
... // whatever is required
};
Am_Define_Pointer_Wrapper(my_class)
// In the .cc file:
class my_class_wrapper_support : public Am_Type_Support {
public:
void Print (ostream& os, const Am_Value& val) const
{ my_class * m = Am_my_class(val).value;
os << print out my_class's data
}
} my_class_wrapper_support_obj;
Am_Define_Pointer_Wrapper_Impl(my_class, &my_class_wrapper_support_obj);
my_class *ii = new my_class;
obj.Set(SLOT, Am_my_class(ii)); //wrap the pointer to my_class
my_class *ii2 = Am_my_class(obj.Get(SLOT)).value;
Remember that pointer wrappers do not deal with memory allocation. Thus, if you create an instance or copy of obj in the code above, both the new object and the old object will share the same pointer to the same my_class value. If you want memory management, you have to use a ``real'' wrapper, as discussed in Section 3.11.4. Thus,
my_class *ii3 = (my_class*)(Am_Ptr)obj.Get(SLOT); //unsafe Get
Am_Object new_obj = obj.Create();
my_class *ii4 = Am_my_class(new_obj.Get(SLOT)).value;
ii4 == ii2; //returns true
ii4 == ii; //also true
3.11.3 Enum Wrappers
The Enum wrapper can be used for any type which can fit into a word (32 bits). It is often used for enumerated types, and special macros are available for associating the string names of the enumeration labels with the various values, so the Inspector will print out and accept typing of the string names. The enum wrapper can also be used for other types which can fit into a single word, such as bit fields.3.11.3.1 Enumerated Types
For an enumerated type, the programmer can use the macros Am_Define_Enum_Type or Am_Define_Enum_Long_Type in the .h
file, and Am_Define_Enum_Support or Am_Define_Enum_Type_Impl in the .cc
file. In either case, the programmer uses these macros to define a new type, and then has a declaration using this type of a global value for each value that the user will use. Then, these values can be set directly into slots. The .value
of each of the items defined, will be the long
or enumerated type associated with it at declaration time.3.11.3.1.1 Am_Define_Enum_Type
Am_Define_Enum_Type is used when you want to convert the value back and forth from the wrapper form to an enumerated type which you can use in a switch statement. In this case, you would name the enumeration values some internal name (we use xxx_val) and then define each exported name with the associated internal name. For example (from inter.h)://This enum is used internally; users should use the Am_Move_Grow_Where_Attach values instead.
enum Am_Move_Grow_Where_Attach_vals
{ Am_ATTACH_WHERE_HIT_val, Am_ATTACH_NW_val,
Am_ATTACH_N_val, Am_ATTACH_NE_val,
Am_ATTACH_E_val, Am_ATTACH_SE_val, Am_ATTACH_S_val,
Am_ATTACH_SW_val, Am_ATTACH_W_val,
Am_ATTACH_END_1_val, Am_ATTACH_END_2_val,
Am_ATTACH_CENTER_val };
// type of the Am_WHERE_ATTACH slot for Move_Grow Interactors
Am_Define_Enum_Type(Am_Move_Grow_Where_Attach
Am_Move_Grow_Where_Attach_vals)
const Am_Move_Grow_Where_Attach
Am_ATTACH_WHERE_HIT(Am_ATTACH_WHERE_HIT_val);
const Am_Move_Grow_Where_Attach Am_ATTACH_NW(Am_ATTACH_NW_val);
const Am_Move_Grow_Where_Attach Am_ATTACH_N(Am_ATTACH_N_val);
const Am_Move_Grow_Where_Attach Am_ATTACH_NE(Am_ATTACH_NE_val);
const Am_Move_Grow_Where_Attach Am_ATTACH_E(Am_ATTACH_E_val);
const Am_Move_Grow_Where_Attach Am_ATTACH_SE(Am_ATTACH_SE_val);
const Am_Move_Grow_Where_Attach Am_ATTACH_S(Am_ATTACH_S_val);
const Am_Move_Grow_Where_Attach Am_ATTACH_SW(Am_ATTACH_SW_val);
const Am_Move_Grow_Where_Attach Am_ATTACH_W(Am_ATTACH_W_val);
const Am_Move_Grow_Where_Attach Am_ATTACH_END_1(Am_ATTACH_END_1_val);
const Am_Move_Grow_Where_Attach Am_ATTACH_END_2(Am_ATTACH_END_2_val);
const Am_Move_Grow_Where_Attach Am_ATTACH_CENTER(Am_ATTACH_CENTER_val);
Now, users set the Am_Move_Grow_Where_Attach values into slots: obj.Set(Am_WHERE_ATTACH, Am_ATTACH_NW);
And the programs can get the values out of a slot the usual way: Am_Move_Grow_Where_Attach attach = inter.Get(Am_WHERE_ATTACH);
Individual values can be tested using ==, as follows: if (attach == Am_ATTACH_WHERE_HIT) ...
but if you want to use a switch statement, you can use the .value
which will be the original enum: Am_Move_Grow_Where_Attach attach = inter.Get(Am_WHERE_ATTACH);
switch (attach.value) {
case Am_ATTACH_NW_val:
case Am_ATTACH_SW_val:
3.11.3.1.2 Am_Define_Enum_Long_Type
Am_Define_Enum_Long_Type is used when you are happy to define the enumeration values as long numbers. This would be the case where you never expected to need to access the values in a switch statement, but were happy just to use the == form. This saves the trouble of defining a separate enum type. For example (again from inter.h): // type of the Am_HOW_SET slot for Choice Interactors
Am_Define_Enum_Long_Type(Am_Choice_How_Set)
const Am_Choice_How_Set Am_CHOICE_SET (0);
const Am_Choice_How_Set Am_CHOICE_CLEAR (1);
const Am_Choice_How_Set Am_CHOICE_TOGGLE (2);
const Am_Choice_How_Set Am_CHOICE_LIST_TOGGLE (3);
As before, these values can be set directly into slots, and can be gotten out again and tested with ==: inter.Set (Am_HOW_SET, Am_CHOICE_TOGGLE);
Am_Choice_How_Set how_set = inter.Get (Am_HOW_SET);
if (how_set == Am_CHOICE_LIST_TOGGLE) ...
3.11.3.1.3 Am_Define_Enum_Support and Am_Define_Enum_Type_Impl
In addition to the declarations above, you must also add some code to the .cc file. The macro Am_Define_Enum_Support
can be used when the enum values start at 0 and increment by one, which is the default if you do not explicitly specify values for the enumeration values. This macro can be used for either Am_Define_Enum_Type
or Am_Define_Enum_Long_Type
. The other macro, Am_Define_Enum_Type_Impl
is more general, since it requires that you provide your own Am_Type_Support
object which can do any necessary mapping for the printing and parsing.Am_Define_Enum_Support (Am_Choice_How_Set, ``Am_CHOICE_SET Am_CHOICE_CLEAR ``
``Am_CHOICE_TOGGLE Am_CHOICE_LIST_TOGGLE'');
Am_Define_Enum_Support (Am_Move_Grow_Where_Attach,
``Am_ATTACH_WHERE_HIT Am_ATTACH_NW Am_ATTACH_N Am_ATTACH_NE ``
``Am_ATTACH_E Am_ATTACH_SE Am_ATTACH_S Am_ATTACH_SW Am_ATTACH_W ``
``Am_ATTACH_END_1 Am_ATTACH_END_2 Am_ATTACH_CENTER'');
3.11.3.2 Bit Fields and Longs
Sometimes you might have a 32-bit value which is actually composed of different pieces crammed together. For example, the Am_Input_Char
is composed of a number of fields packed into a structure that fits into 32 bits. The Enum Wrapper mechanism also can be used to wrap these types so the Inspector can view and edit them. In this case, you need to create your own Am_Type_Support
object that will do the printing and parsing. Then you register this type_support with a new ID allocated for the type. Your class needs to have an operator Am_Value () const
which will return the type converted into a long, and with the type set to the ID. You also need a constructor that will create your type from an const Am_Value& value
.Am_Input_Char
, part of the definition of the class (in idefs.h
) is:class Am_Input_Char {
...
Am_Input_Char (const Am_Value& value) {
long l = value.value.long_value;
if (l) {
if (value.type != Am_Input_Char_ID)
Am_Error (``** Tried to set an Am_Input_Char with a non ``
``Am_Input_Char wrapper.'');
}
//make an Am_Input_Char out of l
}
operator Am_Value () const;
Then, in the .cc file:class Input_Char_Support : public Am_Type_Support {
public:
void Print (ostream& os, const Am_Value& value) const
{
Am_Input_Char ch = value;
os << ch;
}
// Reads a string, potentially provided by a user and converts to its own type.
//
Returns Am_No_Value when there is an error.
Am_Value From_String (const char* string) const {
Am_Input_Char ch(string, false);
if (ch.code != 0) return ch;
else return Am_No_Value;
}
} Input_Char_Object; //Input_Char_Object is an instance of Am_Type_Support
// Now allocate the ID Tag using the type_support object. Be sure to declare it to be Am_ENUM class
Am_ID_Tag Am_Input_Char::Am_Input_Char_ID =
Am_Get_Unique_ID_Tag (``Am_Input_Char'', &Input_Char_Object, Am_ENUM);
// Convert an Am_Input_Char to a Am_Value: build a long from the Am_Input_Char's bits
Am_Input_Char::operator Am_Value () const {
long l;
//make l out of this object
return Am_Value (l, Am_Input_Char_ID);
}
Now, you can set the type into a slot and get it out, and the compiler will insert the correct coersion operators, and the Inspector will be able to view and edit the value of the slot.Am_Input_Char c, c1;
obj.Set(SLOT, c);
c1 = obj.Get(SLOT);
3.11.4 Writing a Wrapper Using Amulet's Wrapper Macros
If you need full memory management for C++ objects that you store into the slots of Amulet objects, consider creating a new type of wrapper. Recall that 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.4.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 many 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.14.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. 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
}
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.4.2 Printing and Editing Values
Usually, you will want to override the methods for Printing and getting the values from a string. The default methods just look the value up in the name registry, and if not there, then prints the pointer in hex. These methods are all defined on the Wrapper type itself (that is a subclass of Am_Wrapper. In the example of the previous section, these would be methods of Foo_Data). The methods to define are:
virtual const char * To_String() const;
Inherited from Am_Registered_Type. This should return the string value of the type if this can be done without allocating memory, and otherwise return NULL. The default just looks up the value in the name registry.
virtual void Print(ostream& out) const;
Inherited from Am_Registered_Type. This should print the value of the type to the stream in some human-understandable way.
virtual Am_Value From_String(const char * string) const;
This takes a string and returns an Am_Value with one of the type in it, parsed from the string. The object that this method is passed to is generally ignored. The default method looks up the string in the name registry. Note: This editing in the Inspector is not yet implemented for Wrappers unless the value is named in the registry (so it doesn't currently help to override this method).
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.Consider the following example: The programmer wants to return a
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 initialized 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 = (Foo_Data*) data->Make_Unique ();
data->Modify_Somehow ();
}
Note that Make_Unique
returns an Am_Wrapper*
that must be cast to the appropriate type when assigning the result to the wrapper data pointer.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 throughThe 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.
_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 = (Foo_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.
Putting a wrapper around an existing C++ class is not difficult. One can make the original class a piece of data for the wrapper data layer. If the programmer does not want to reimplement all the methods that come with the existing class, one provides a single method that returns the original class and calls the methods on that. Be certain that
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.
An important feature of the generated files is that any type of value in a slot should be savable, including references to other objects. If these other objects have already been created, then a reference to that object is used. Otherwise, the referenced object is saved or loaded, and the object is remembered in case it is referenced again.
3.12.1 Simple Interface For Storing and Loading Objects
There is a very flexible save and load architecture that allows you to create your own save and load mechanism and even your own file formats (see Section 3.12.3). However, most of the time, the basic mechanism is sufficient, and much easier to use. In this basic mechanism, you use Am_Default_Load_Save_Context
, which knows how to save the specified slots of the objects.
Each object that you want to save should have a slot called Am_SAVE_OBJECT_METHOD
containing Am_Standard_Save_Object
and a Am_SLOTS_TO_SAVE
which must contain an Am_Value_List
containing the slot names of all the slots you want to save. This list must be sufficient to restore the object. Amulet has built-in methods for storing most types to a file (see Section 3.12.2). For example, in samples/example/example1.cc, all the rectangles are the same color, so the following set of slots is sufficient:
rect_proto = Am_Rectangle.Create(``Rect_Proto'')
.Set (Am_LEFT, 0)
.Set (Am_TOP, 0)
.Set (Am_WIDTH, 12)
.Set (Am_HEIGHT, 8)
.Set (Am_LINE_STYLE, Am_Black)
.Set (Am_FILL_STYLE, Am_Red)
//Allow this object to be saved to disk and read back in using the standard method
.Add (Am_SAVE_OBJECT_METHOD, Am_Standard_Save_Object)
//The standard method needs the list of slots that are different in each object.
//
Here, just the bounding box.
.Add (Am_SLOTS_TO_SAVE, Am_Value_List()
.Add(Am_LEFT).Add(Am_TOP).Add(Am_WIDTH).Add(Am_HEIGHT))
;
In src/widgets/testselectionwidget, each rectangle can be a different color, so the slots are:my_rectangle_proto = Am_Rectangle.Create(``Sel_Rect_Proto'')
.Add (Am_SAVE_OBJECT_METHOD, Am_Standard_Save_Object)
.Add (Am_SLOTS_TO_SAVE, Am_Value_List()
.Add(Am_LEFT).Add(Am_TOP).Add(Am_WIDTH).Add(Am_HEIGHT)
.Add(Am_FILL_STYLE));
If the object you want to save is a group object, with parts that need to be saved in addition to slots, use Am_Standard_Save_Group
to set the Am_SAVE_OBJECT_METHOD
slot.Am_SLOTS_TO_SAVE
will be restored to their old values. You register the prototypes using Am_Default_Load_Save_Context.Register_Prototype
. The first parameter is a string that will be what this object is called in the file. This string must NOT contain spaces, and it must be unique among all the types of objects stored to this file. For example, from src/widgets/testselectionwidget.cc
:Am_Default_Load_Save_Context.Register_Prototype(``ARC'', my_arc_proto);
Am_Default_Load_Save_Context.Register_Prototype(``RECT'',
my_rectangle_proto);
Am_Default_Load_Save_Context.Register_Prototype(``LIN'', my_line_proto);
Am_Default_Load_Save_Context.Register_Prototype(``POLY'', my_polygon_proto);
Am_Default_Load_Save_Context.Register_Prototype(``FREE'',
freehand_line_proto);
The final step is to actually cause a Command object to Save and Open (load, read) the file. The easiest way to do this is to use the built-in commands Am_Open_Command
, Am_Save_As_Command
and Am_Save_Command
described in Section 7.4.2. That section describes how to use those commands.
If you want to create your own commands, then the methods of interest are
Am_Default_Load_Save_Context.Reset()
which initializes the load-save context, and then Am_Default_Load_Save_Context.Load
which takes an ifstream initialized with ios::in, and returns an Am_Value
, which if valid, will be filled with an Am_Value_List
of the objects that were loaded. If saving, then after reset, call Am_Default_Load_Save_Context.Save()
which takes an iofstream initialized with ios::out and an Am_Value
which should contain an Am_Value_List
of the objects to be saved. For example:void my_save (Am_Value_List items_to_save, Am_String & filename) {
ofstream out_file (filename, ios::out);
if (!out_file) Am_Error(``Can't save file'');
Am_Default_Load_Save_Context.Reset ();
Am_Default_Load_Save_Context.Save (out_file, Am_Value (items_to_save));
}
Am_Value_List my_open (Am_String & filename) {
ifstream in_file ((const char*)filename, ios::in);
if (!in_file) Am_Error(``Can't open file'');
Am_Default_Load_Save_Context.Reset ();
Am_Value contents = Am_Default_Load_Save_Context.Load (in_file);
if (!contents.Valid () || !Am_Value_List::Test (contents))
Am_Error(``Bad file contents'');
Am_Value_List loaded = contents;
return loaded;
}
If the string names in the file do not match the names used with Register_Prototype
, then the load will fail. This can happen if you try to load a file stored by one program into another program that uses different names (e.g., trying to load a program generated by circuit into testselectionwidget). Since the prototypes are not available in the other program, it doesn't know how to create the objects specified in the file. Therefore, it is a good idea to name your types with a unique name.3.12.2 Types of Data to Store
Amulet has built-in method for storing the following types to a file. If you want to store a different type to a file:
Am_Red
and Am_Line_2
. Note that arbitrary fonts and styles are NOT available for loading and storing. To get around this, Gilt creates a table of the fonts and styles, and stores the index into the table to the file. Then, when the object is read back in, Gilt restores the real style and font slots.If you want to store a new type to a file, then you need to write your own method for storing and retrieving it. The methods are of form
Am_Save_Method
and Am_Load_Method
. For example:
Am_Define_Method (Am_Load_Method, Am_Value, load_long,
(istream& is, Am_Load_Save_Context& /* context */)) {
long value;
is >> value;
char ch;
is.get (ch); // skip eoln
return Am_Value (value);
}
Am_Define_Method (Am_Save_Method, void, save_long,
(ostream& os, Am_Load_Save_Context& context,
const Am_Value& value)) {
context.Save_Type_Name (os, ``Am_LONG'');
os << (long)value << endl;
}Then, these methods are registered with the context:
Am_Default_Load_Save_Context.Register_Loader (``Am_LONG'', load_long);
Am_Default_Load_Save_Context.Register_Saver (Am_LONG, save_long);There are many other examples in the file src/opal/opal.cc. Whenever an Am_Value with type ID Am_LONG is seen, then save_long will be called, which writes the string ``Am_LONG'' to the file, and then when the reader sees the ``Am_LONG'' string, it will call the load_long method to read the data. Anything can be written to the file, as long as the saver and loader are consistent.
Am_Default_Load_Save_Context
which is described above (and exported by opal.h), which is an instance of the Am_Load_Save_Context
. If you need to create a new kind of load and save, the Am_Load_Save_Context
is defined as:
// This class holds the state of load or save in process. This will keep track of the names of items loaded
//
and will handle references to objects previously loaded or saved.
class Am_Load_Save_Context {
Am_WRAPPER_DECL (Am_Load_Save_Context)
public:
Am_Load_Save_Context ()
{ data = NULL; }
// This method is used to record items that are referred to by the objects being loaded or saved,
//
but the items themselves are permanent parts of the application hence they shouldn't
//
(or possibly can't) be saved as well. By providing a base number one can use the same name over
//
and over.
void Register_Prototype (const char* name, Am_Wrapper* value);
void Register_Prototype (const char* name, unsigned base,
Am_Wrapper* value);
//returns the name if the value is registered as a prototype. If not registered, returns NULL
const char* Is_Registered_Prototype (Am_Wrapper* value);
// Load methods are registered based on a string type name. The type name must be a single
//
alphanumeric word (no spaces). The loader is responsible for returning a value for anything
//
stream that is declared with that type name. If two or more methods are registered on the
//
same name, the last one defined will be used.
void Register_Loader (const char* type_name,
const Am_Load_Method& method);
// Save methods are registered based on a value type. If a single type can be saved in multiple ways,
// it is up to the save method to delagate the save to the proper method.
// If two or more methods are registered on the same type, the last one defined will be used.
void Register_Saver (Am_ID_Tag type, const Am_Save_Method& method);
// Reset internal counters and references. Used to start a new load or save session.
// Should be called once per new stream before the first call to Load or Save.
// Will not remove registered items such as loaders, savers, and prototypes.
void Reset ();
// Load value from stream. Call this once for each value saved.
// Returns Am_No_Value when stream is empty.
Am_Value Load (istream& is);
// Save value to stream. Call once for each value saved.
void Save (ostream& os, const Am_Value& value);
// Structures that are recursive must call this function before Load is called recusively.
// The value is the pointer to the structure being loaded. This value will be used by internal values that
// refer to the parent structure.
void Recursive_Load_Ahead (Am_Wrapper* value);
// This procedure must be called by each save method as the first thing it writes to the stream.
// The procedure will put the name into the stream with the proper format.
// After that, the save method can do whatever it needs.
void Save_Type_Name (ostream& os, const char* type_name);
};
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.4.3). 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.4.
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.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.14.2 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 every time 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.14.2.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_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.Get (ADDEND1);
int addend2 = self.Get (ADDEND2);
self.Set (SUM, addend1 + addend2);
}
break;
case ADDEND2: { // ADDEND2 has changed last.
int prev_value = events.Get_Prev_Value ();
int addend2 = self.Get (ADDEND2);
events.Prev ();
if (events.First () || events.Get ().Get_Key () != SUM) {
int addend1 = self.Get (ADDEND1);
self.Set (SUM, addend1 + addend2);
} else { // SUM was set before ADDEND2. Must propagate previous result to ADDEND1.
int sum = self.Get (SUM, Am_NO_DEPENDENCY);
self.Set (ADDEND1, sum - prev_value);
self.Set (SUM, sum - prev_value + addend2);
}
}
break;
case SUM: { // SUM has changed last.
int sum = self.Get (SUM);
int addend2 = self.Get (ADDEND2);
self.Set (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
.Set
in a web is similar to using Get
except that it is used for output instead of input. Every slot set with Set
will be assigned a constraint pointer to that web making the slot dependent on the web. It is legal to both Get
and Set
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 Get
is called on the SUM
slot, the web's constraint on that slot will be kept. If neither Get
or Set
are called on a slot during validation, the dependency and/or constraint will be removed.3.14.2.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 (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);
}
The Am_Web_Init
class is used to make dependency and constraint connections without performing a Get
or Set
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 they can use Get
or Set
as usual. The slot
parameter provides the primary slot to which the web was attached. Note that it is not necessary to call Note_Output
on the primary slot, since it is already an output slot, by virtue of setting the slot with the web.3.14.2.3 Installing Into a Slot
A web is put into an object by setting it into its primary slot. First, an 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.14.3 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
. Unless the slot has the inheritance rule Am_STATIC
(See Section 3.14.4.) in the object's, prototype, Get_Slot
will always return a slot local to the object. If the slot was previously inherited, Get_Slot
will make a placeholder slot locally in the object and return that. If the slot does not exist locally and is not inherited from the prototype, Get_Slot
will return a slot whose type is Am_MISSING_SLOT
.
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
defined in object.h
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 added 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. Note that if the slot points to a wrapper, then only the pointer to the wrapper is duplicated, not the wrapper object itself. In particular, if the slot contains an object, the object itself is not copied. If you want an object to be copied in the instance, make the object be a part by using Add_Part
instead of set. For example:
obj.Set(Am_SELECTED, false)
.Set_Inherit_Rule(Am_SELECTED, Am_COPY);
Am_SELECTED
slot exist in all instances and copies of obj, but each one will have an independent value, so setting the Am_SELECTED
slot of the prototype will not affect the instances.
obj.Set(Am_FEEDBACK_OBJ, rect3)This is probably not useful. Each instance of obj (e.g., inst) will also point to rect3, but setting the
.Set_Inherit_Rule(Am_SELECTED, Am_COPY);
inst = obj.Create();
Am_FEEDBACK_OBJ
slot of obj will not affect the values of inst:
obj.Set(Am_FEEDBACK_OBJ, rect4);
inst.Get(Am_FEEDBACK_OBJ); // ==> returns rect3Instead, use:
obj.Add_Part(Am_FEEDBACK_OBJ, rect4);
inst = obj.Create();
inst.Get(Am_FEEDBACK_OBJ); // ==> returns rect3_723Now, all instances of obj will have independent objects in the
Am_FEEDBACK_OBJ
slot, which are created as instances of rect4.
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 independence
#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 call the formula's Multi_Constraint
method when setting the slot. For example,
obj.Set (Am_SELECTED, button_sel_from_value.Multi_Constraint());
Now obj
and any of its instances will always retain that constraint, even if another constraint or value is set into the instance. Furthermore, calls like Remove_Constraint
on an instance's slot will still not remove the inherited constraint (though it will remove any additional constraints set directly into the instance). Calling Remove_Constraint
on obj
itself, however, will destroy the constraint normally.Multi_Constraint
method is also used to attach multiple constraints to a single slot. Any formula set into a slot using the Multi_Constraint
suffix will not be removed by subsequent sets.3.14.6 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.14.6.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 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* proto_create = self.Get_Prototype ().
Get_Demons ().Get_Object_Demon (Am_CREATE_OBJ);
if (proto_create)
proto_create (self); // Call prototype create demon.
// Do my own code.
}
3.14.6.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.14.6.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. Thus before modifying 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_Demons ().Copy ());
my_demons.Set_Object_Demon (Am_DESTROY_OBJ, my_destroy_demon);
my_adv_obj.Set_Demons (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.14.6.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.14.6.5 How to Allocate Demon Bits and the Eager Demon
In order to develop a new slot demon, 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.