Copyright (c) Hyperion Entertainment and contributors.
BOOPSI - Object Oriented Intuition
Contents
BOOPSI - Object Oriented Intuition
BOOPSI is an acronym for Basic Object Oriented Programming System for Intuition. Using the Object Oriented Programming (OOP) model, BOOPSI represents certain Intuition entities, like Gadgets and Images, as objects.
There are many advantages to using BOOPSI:
- BOOPSI makes Intuition customizable and extensible. BOOPSI programmers can create new types of BOOPSI objects to suit the needs of their applications. These new types of objects are part of Intuition and can be made public so other applications can use them. Because applications can share the new types, application writers don't have to waste their time duplicating each other's efforts writing the same objects.
- New types of BOOPSI objects can build on old types of BOOPSI objects, inheriting the old object's behavior. The result is that BOOPSI programmers don't have to waste their time building new objects from scratch, they simply add to the existing object.
- OOP and BOOPSI apply the concept of interchangeable parts to Intuition programming. A BOOPSI programmer can combine different BOOPSI objects (like gadgets and images) to create an entire Graphical User Interface (GUI). The BOOPSI programmer doesn't have take the time to understand or implement the inner workings of these objects. The BOOPSI programmer only needs to know how to interact with BOOPSI objects and how to make them interact with each other.
- BOOPSI objects have a consistent, command-driven interface. To the BOOPSI programmer, there is no difference between displaying a text, border, or bitmap-based BOOPSI image, even though they are rendered quite differently. Each image object accepts a single command to tell it to render itself.
Before reading this section, you should already be familiar with several Amiga concepts. BOOPSI is built on top of Intuition and uses many of its structures. These include Intuition gadgets, images, and windows. BOOPSI also uses the tag concept to pass parameters. The Utility Library section discusses tags. Utility Library also discusses callback Hooks, which are important to the later sections.
OOP Overview
Understanding BOOPSI requires an understanding of several of the concepts behind Object Oriented Programming. This section is a general overview of these concepts as they pertain to BOOPSI. Because BOOPSI is in part based on the concepts present in the OOP language Smalltalk, a reference book on Smalltalk may provide a deeper understanding of BOOPSI in general. Timothy Budd's book entitled A Little Smalltalk (Addison-Wesley Publishing ISBN 0-201-10698-1) is a good start.
In the BOOPSI version of the Object Oriented Programming model, everything is an Object. For example, a proportional gadget named myprop is an object. Certain objects have similar characteristics and can be classified into groups called classes. As objects, Rover the dog, Bob the cat, and Sam the bird are all distinct objects but they all have something in common, they can all be classified as animals. As objects, myprop the proportional gadget, mystring the string gadget, and mybutton the button gadget all have something in common, they can all be classified as gadgets. A specific object is an instance of a particular class ("Rover" is an instance of class "animal", "myslidergadget" is an instance of class "gadget").
Notice that, although Rover, Bob, and Sam can all be classified as animals, each belongs to a subgroup of the animal class. "Rover" is an instance of class "dog", "Bob" is an instance of class "cat", and "Sam" is an instance of class "bird". Because each of these animal types share common characteristics, each type makes up its own class. Because dog, cat, and bird are subclassifications of the animal class, they are known as subclasses of the animal class. Conversely, the animal class is the superclass of the dog, cat, and bird classes.
Following the branches upward from class to superclass will bring you to a universal root category from which all objects are derived. The OOP language Smalltalk calls this class "Object".
Like Smalltalk, BOOPSI also has a universal root catagory, rootclass. Currently, Intuition defines three immediate subclasses of rootclass. The first, gadgetclass, is the class of BOOPSI gadgets. The second class, imageclass, makes up the class of BOOPSI images.
Unlike gadgetclass and imageclass, the remaining subclass, icclass, does not correspond to an existing Intuition entity, it is a concept new to Intuition. Icclass, or interconnection class, allows one BOOPSI object to notify another BOOPSI object when a specific event occurs. For example, consider a BOOPSI proportional gadget and a BOOPSI image object that displays an integer value. An application can connect these two objects so that the prop gadget tells the image object the prop gadget's current value, which the image object displays. Every time the user slides the prop gadget, the prop gadget notifies the image of the change and the image updates its display to reflect the prop gadget's current integer value. Because these objects are talking to each other rather than the application, the updates happen automatically. The application doesn't have to talk to the two objects, it only has to connect them.
An object's characteristics and behavior are determined by its class. Each class can define a set of attributes and a set of methods that apply to all objects of that class. An attribute is a variable characteristic of an object. For example, an attribute for the animal class could be the number of legs an animal object has. An example of a BOOPSI attribute is the X coordinate of a BOOPSI image object. The data that makes up the values of an object's attributes is collectively known as the instance data for that object.
The behavior of an object depends upon the set of methods associated to it by its class. A method is basically a function that applies to objects of that class. An example of a BOOPSI method is the imageclass method IM_DRAW. This method tells a BOOPSI image to draw itself. All BOOPSI actions are carried out via methods.
From the Object Diagram, two of the methods of the "animal" class could be "eat" and "sleep". One of the methods of the "dog" class could be "bark". Notice that instances of the "dog" class can do more than just bark, they can also eat and sleep. This is because a subclass inherits methods from its superclasses. If there were a subclass of dog called "attack dog", all instances of that class would be able to bark, eat, and sleep, as well as "attack". Due to inheritance, a subclass has all of the methods and all of the attributes of its superclass. For example, the IA_Height attribute is defined by imageclass. All instances of the subclasses of imageclass have their own IA_Height attribute, even though the subclasses do not explicitly define IA_Height. In turn, all instances of subclasses of the imageclass subclasses also inherit the IA_Height attribute. All classes on levels below a class will inherit its methods and attributes.
When an application or a BOOPSI object wants another BOOPSI object to perform a method, it passes it a command in the form of a BOOPSI message. A BOOPSI message tells an object which method to perform. The message may also contain some parameters that the method requires.
Watch Out! |
---|
The term "message" used in object oriented terminology can be little confusing to the Amiga programmer because the BOOPSI message has nothing to do with an Exec message. |
BOOPSI classes can be either public or private. Public classes have ASCII names associated with them and are accessible to all applications. Private classes have no ASCII name and normally can only be accessed by the application that created the private class.
Using BOOPSI
There are several levels on which a programmer can use BOOPSI. The most elementary level is to use Intuition functions to create and manipulate BOOPSI objects that are instances of existing, public classes.
The BOOPSI classes are shown in class diagrams and tables in the BOOPSI Class Reference.
BOOPSI and Tags
BOOPSI uses tag lists to pass and manipulate its attributes. To BOOPSI, each TagItem (defined in <utility/tagitem.h>) in a tag list is an attribute/value pair. The TagItem.ti_Tag field contains an ID for the attribute and the ti_Data field holds the attribute's value.
For example, the string gadget class defines an attribute called STRINGA_LongVal, which is the current integer value of the gadget. Certain gadgetclass objects have an attribute called GA_Image. Its value is not an integer, it is a pointer to an image. All of the attributes tags used by BOOPSI objects are documented in the BOOPSI Class Reference - their ID's and possible values. In many cases attributes also have default values that are automatically used by an object, if those values are acceptable then you don't have to define those tags.
As is common with functions using tags, many BOOPSI functions typically have two variants reflecting the two methods of passing tagged attributes:
- The TagList method requires a TagList be declared or allocated and filled with attribute/value pairs before calling the BOOPSI function. The name of functions using the TagList method typically end with an "TagList" or "A", like OpenWindowTagList() or NewObjectA().
- The more commonly used second method relies on a variable length "stack" of tags that are defined within the function call. This method skips the code needed to declare / allocate a TagList. The name of functions using the "stack" tags method may end with "Tags" or omit the "A" suffix (mentioned above), like OpenWindowTags() or NewObject().
Note, every tag list must end (or be "terminated") with a TAG_END tag. These tag lists can also contain utility.library Global System control tags (like TAG_SKIP), which BOOPSI uses in processing its tag lists. Any application that ends up processing these lists should do so using the tag manipulation functions from utility.library.
For more information on tags, see Utility Library - Tags.
Creating and Using Simple BOOPSI Objects
The following link will explore how to create and interact with a simple BOOPSI object - a system requester. This section explains and provide code samples for the basic steps to create a new object, send the object messages, to adjust and read object attributes and to dispose of the object. Finally an example program is provided to fully demonstrate using the requester class. Please see BOOPSI 101.
Creating an Object
The Intuition function NewObjectA() creates a BOOPSI object using a tag list and returns a pointer to a new BOOPSI object:
APTR mynewobject = NewObjectA(Class *privclass, UBYTE *pubclass, struct TagItem *myattrs);
In general, BOOPSI objects are "black boxes". This means the inner workings of BOOPSI objects are not visible to the application programmer, so the programmer does not know what goes on inside it. This really means the inner workings of these objects are none of your business. Unless otherwise documented, only use an object pointer as a handle to the object.
To create an object, NewObjectA() needs to know what class the new object is an instance of. To create a public class object, pass a NULL pointer in privclass and an ASCII string in pubclass naming the object's public class. The privclass pointer is used to create a private class object, which is covered in the "Creating a BOOPSI Class" section later in this chapter.
The myattrs tag list is a list of tag/value pairs, each of which contains an initial value for some object attribute. Most objects have attributes which define the characteristics of the object to be created and used which are set by using specifically named tags. Many of these attributes of BOOPSI gagdets and images are derived from the old Intuition Gadget and Image structures (position, size, etc).
If we were to use the above NewObjectA function with a tag list to create a string object, a code excerpt would look like this:
struct TagItem tags[] = { {GA_ID, 1}, {GA_Left, 0}, {GA_Top, 0}, {STRINGA_LongVal, 100}, {TAG_END,0} }; mystringgagdet = IIntuition->NewObjectA(NULL, "string.gadget", (struct TagItem *) tags);
Instead of declaring or allocating and initializing a tag list and caliing NewObjectA() as above, most applications use the stack-based version of the function: NewObject(). As such, the following code sample builds a BOOPSI string gadget on the stack:
mystringgadget = IIntuition->NewObject(NULL, "string.gadget", GA_ID, 1, GA_Left, 0, GA_Top, 0, STRINGA_LongVal, 100, TAG_END);
If NewObject() is successful, it returns a pointer to a new BOOPSI gadget object. Otherwise, it returns NULL. The class "string.gadget" is one of the public classes built into ReAction. It is a class of string gadgets.
If you look at the diagram of the public classes built into Intuition, you'll see that strgclass is a subclass of gadgetclass. In the example above, the attribute tag IDs that start with "GA_" are defined by gadgetclass and not by strgclass. This is because strgclass inherits these attributes from its superclass, gadgetclass. The other attribute, STRINGA_LongVal, is defined by strgclass. It does two things. First, it tells the object that it is a special type of string gadget which only handles an integer value rather than a generic ASCII string. Second, it passes the object its initial integer value.
Disposing of an Object
When an application is done with an object it has to dispose of the object. To dispose of an object, use the Intuition function DisposeObject():
VOID DisposeObject(APTR boopsiobject);
where boopsiobject is a pointer to the BOOPSI object to be disposed. Note that some classes allow applications to connect child objects to a parent object so that when the application deletes the parent object, it automatically disposes of all of its children. Be careful not to dispose of an object that has already been disposed.
Setting an Existing Object's Attributes
An object's attributes are not necessarily static. An application can ask an object to set certain object attributes using the SetAttrs() function:
ULONG SetAttrs(APTR myobject, Tag1, Value1, ...);
Because BOOPSI gadgets require some extra information about their display, they use a special version of this function, SetGadgetAttrs():
ULONG SetGadgetAttrs(struct Gadget *myobject, struct Window *w, struct Requester *r, Tag1, Value1, ...);
Here myobject is a pointer to the BOOPSI object, w points to the gadget's window, r points to the gadget's requester, and the tag/value pairs are the attributes and their new values. The return value of SetAttrs() and SetGadgetAttrs() is class specific. In general, if the attribute change causes a visual change to some object, the SetAttrs() / SetGadgetAttrs() function should return a non-zero value, otherwise, these functions should return zero (see the BOOPSI Class Reference for information on the return values for specific classes). The following is an example of how to set the current integer value and gadget ID of the gadget created in the NewObject() call above:
IIntuition->SetGadgetAttrs(mystringgadget, mywindow, NULL, STRINGA_LongVal, 75, GA_ID, 2, TAG_END);
This changes two of mystringgadget 's attributes. It changes the gadget's current integer value to 75 and it changes the gadget's ID number to 2.
Note that it is not OK to call SetGadgetAttrs() on a BOOPSI object that isn't a gadget, nor is it OK to call SetAttrs() on a BOOPSI gadget.
Not all object attributes can be set with SetGadgetAttrs() / SetAttrs(). Some classes are set up so that applications cannot change certain attributes. For example, the imagery for the knob of a proportional gadget cannot be altered after the object has been created. Whether or not a specific attribute is "settable" is class dependent. For more information about the attributes of specific classes, see the BOOPSI Class Reference.
Getting an Object's Attributes
The Intuition function GetAttr() asks an object what the value of a specific attribute is:
ULONG GetAttr(ULONG attrID, APTR myobject, ULONG *mydata);
where attrID is the attribute's ID number, myobject is the object to get the attribute from, and mydata points to a data area that will hold the attribute value. This function returns a 0 if the object doesn't recognize the attribute, otherwise it returns some non-zero value, the meaning of which depends on the class. In most cases, GetAttr() returns a 1 when it is successful.
Not all object attributes are obtainable using the GetAttr() function. Some classes are set up so that applications cannot query the state of certain attributes. For example, using the GA_Image attribute, an application can give a BOOPSI prop gadget (propgclass) an Image structure which the gadget uses as the imagery for its knob. This attribute is not "gettable" as there is no need for an application to have to ask the gadget for the structure that the application passed it in the first place. Whether or not a specific attribute is "gettable" is class dependent. For more information about the attributes of specific classes, see the BOOPSI Class Reference.
Passing messages to an Object
To pass a message to an object use the function IDoMethodA(), or its stack-based equivalent, IDoMethod():
ULONG IDoMethodA(Object *myobject, Msg boopsimessage); ULONG IDoMethod(Object *myobject, ULONG methodID, ...);
The return value is class-dependent. The first argument to both of these functions points to the object that will receive the BOOPSI message.
For IDoMethodA(), boopsimessage is the actual BOOPSI message. The layout of it depends on the method. Every method's message starts off with an Msg (from <intuition/classusr.h>):
typedef struct { ULONG MethodID; /* Method-specific data may follow this field */ } *Msg;
IDoMethod() uses the stack to build a message. To use IDoMethod(), just pass the elements of the method's message structure as arguments to IDoMethod() in the order that they appear in the structure. For example, to ask the BOOPSI object myobject to add the object addobject to its personal list:
IIntuition->IDoMethod(myobject, OM_ADDMEMBER, addobject);
Many times, additional context information will be needed for an object to visually render itself while processing a message.
LONG DoGadgetMethodA(struct Gadget *object, struct Window *win, struct Requester *req, Msg msg); LONG DoGadgetMethod(struct Gadget *object, struct Window *win, struct Requester *req, ULONG methodID, ...);
This function is similar to IDoMethod(), except DoGadgetMethod() passes along the object's graphical environment in the form of a GadgetInfo structure. DoGadgetMethodA() gets this structure from the gadget's window (or requester). The GadgetInfo structure is important because a Boopsi gadget needs the information in that structure to render itself. Note that if you give DoGadgetMethodA() a NULL Window and NULL Requester pointer, DoGadgetMethodA() will pass a NULL GadgetInfo pointer.
The name and first parameter of DoGadgetMethodA() may lead you to believe that this function applies only to gadgets. Although you can certainly use this function on gadget objects, the function is not restricted to gadget objects. You can use this function on any BOOPSI object. This is important for an object such as a model object, which may react to any BOOPSI message by invoking some method on gadget objects connected to it. Because the model object receives environment information in the form of a GadgetInfo structure, the model can pass that information on to any objects connected to it.
Before this function, passing the environment information was not that easy. A good example of this is the rkmmodelclass example. In that example, the rkmmodelclass is a subclass of modelclass. The rkmmodelclass object inherits the behavior of modelclass, so its sends updates to its member objects. One feature rkmmodelclass adds to modelclass is that these objects maintain an internal integer value. If that value changes, the rkmmodelclass object propagates that change to its member objects.
If one of the member objects happens to be a gadget object, changing the rkmmodelclass object's internal value may change the visual state of the gadgets. For the gadget's to update their visual state, they need the environment information from the GadgetInfo structure as well as the new internal value of the rkmmodelclass object. If an application used DoMethod() or SetAttrs() to set the rkmmodelclass object's internal value, the rkmmodelclass object would not get a GadgetInfo structure. When the rkmmodelclass object propagates the internal value change to its member objects, it has no environment information to pass on to its member objects. As a result, the member gadgets can not update their visual state directly. This is particularly annoying for a propgclass object, because the visual state of the propgclass object can depend on the rkmmodelclass object's integer value.
DoGadgetMethodA() corrects this problem. It passes a pointer to a GadgetInfo structure for the window (or requester) you pass to it. If you plan on implementing new BOOPSI methods that need a GadgetInfo structure in their BOOPSI message, make sure the second long word of the BOOPSI message is a pointer to the GadgetInfo structure. DoGadgetMethodA() assumes that every method (except for OM_NEW, OM_SET, OM_NOTIFY, and OM_UPDATE; see the next paragraph) expects a GadgetInfo pointer in that place.
If you use DoGadgetMethodA() to send an OM_SET message to a BOOPSI gadget, DoGadgetMethodA() passes a pointer to a GadgetInfo structure as the third long word in the BOOPSI message. DoGadgetMethodA() is smart enough to know that the OM_SET method uses an opSet structure as its message, which has a pointer to a GadgetInfo structure as its third long word. DoGadgetMethodA() passes a GadgetInfo structure as the third parameter for the OM_NEW, OM_SET, OM_NOTIFY, and OM_UPDATE methods. For all other methods, like the gadgetclass methods, DoGadgetMethodA() passes a GadgetInfo structure as the second long word. For more information, see the Autodoc for DoGadgetMethodA() and its varargs equivalent, DoGadgetMethod().
What About the BOOPSI Messages and Methods?
According to the "OOP Overview" section, for an object to perform a method, something has to pass it a BOOPSI message. The previous section discussed using Intuition functions to ask an object to do things like set and get attributes. The functions in the previous section seem to completely ignore all that material about methods and messages. What happened to the methods and messages?
Nothing; these functions don't ignore the OOP constructs, they just shield the programmer from them. Each of these functions corresponds to a BOOPSI method:
NewObject() | OM_NEW |
DisposeObject() | OM_DISPOSE |
SetAttrs()/SetGadgetAttrs() | OM_SET |
GetAttr() | OM_GET |
RefreshSetGadgetAttrs() | OM_SET, GM_RENDER |
These methods are defined on the rootclass level, so all BOOPSI classes inherit them. The Intuition functions that correspond to these methods take care of constructing and sending a BOOPSI message with the appropriate method ID and parameters.
The Public Classes
Intuition Library (intuition.library) contains many public classes, all of which are descendants of the rootclass. There are three primary classes that descend directly from rootclass: imageclass, gadgetclass, and icclass.
The Imageclass Subclasses
Normally, an application does not create an imageclass object. Instead, it will use a subclass of imageclass.
For more information on the classes available see the BOOPSI Class Reference. It describes all of the existing public classes, their methods, and their attributes.
The Gadgetclass Subclasses
Like imageclass, applications do not normally create objects of gadgetclass, but instead create objects of its subclasses.
For specific information on these classes, see the BOOPSI Class Reference.
Making Gadget Objects Talk to Each Other
One use for a proportional gadget is to let the user change some integer value, like the red, green, and blue components of a color. This type of prop gadget is commonly accompanied by an integer string gadget, enabling the user to adjust one integer value by either typing the value into the string gadget or by scrolling the prop gadget. Because these two gadgets reflect the value of the same integer, when the user adjusts the state of one of the gadgets (and thus changing the integer value), the other gadget should automatically update to reflect the new integer value.
When the user manipulates a conventional gadget, the gadget sends messages to an IDCMP port to indicate the state change (for information on IDCMP, see Intuition Input and Output Methods). To connect the string and prop gadgets from the previous paragraph, an application would have to listen for the IDCMP messages from two different gadgets, interpret the IDCMP message's meaning, and manually update the gadgets accordingly. Essentially, the application is responsible for "gluing" the gadgets together. This unnecessarily complicates an application, especially when that application already has to listen for and interpret many other events.
BOOPSI gadgets simplify this. By setting the appropriate attributes, an application can ask a BOOPSI gadget to tell some other object when its state changes. One of the attributes defined by gadgetclass is ICA_TARGET (defined in <intuition/icclass.h>). The ICA_TARGET attribute points to another BOOPSI object. When certain attributes in a BOOPSI gadget change (like the integer value of a prop gadget), that gadget looks to see if it has an ICA_TARGET. If it does, it sends the target a message telling it to perform an OM_UPDATE method.
The OM_UPDATE method is defined by rootclass. This is basically a special type of OM_SET method that is used specifically to tell a BOOPSI object that another BOOPSI object's state changed. Only BOOPSI objects send OM_UPDATE messages. Note that standard classes of BOOPSI gadgets only send out OM_UPDATE messages as a result of the user changing the state of the gadget (scrolling the prop gadget, typing a new number into an integer gadget, etc.). These gadgets do not send out OM_UPDATE messages when they receive OM_SET or OM_UPDATE messages.
A BOOPSI propgclass object has only one attribute that triggers it to send an OM_UPDATE request: PGA_Top. This attribute contains the integer value of the prop gadget. Every time the user moves a prop gadget, the PGA_Top attribute changes. If the prop gadget has an ICA_TARGET, the prop gadget will tell the target object that the PGA_Top value has changed.
A BOOPSI integer string gadget (a strgclass object) also has only one attribute that triggers it to send an OM_UPDATE request: STRINGA_LongVal. This value contains the integer value of the integer string gadget. Like the prop gadget, if the integer string gadget has an ICA_TARGET, when the user changes the gadget's integer value (STRINGA_LongVal), the string gadget will tell the target object that the STRINGA_LongVal value has changed.
When a BOOPSI gadget sends an OM_UPDATE message, it passes the ID of the attribute that changed plus that attribute's new value. For example, if the user typed a 25 into a BOOPSI integer string gadget, that gadget would send an OM_UPDATE message to its ICA_TARGET saying in essence, "Hey, STRINGA_LongVal is 25".
If this string gadget's ICA_TARGET is a propgclass object, the propgclass object will become confused because it has no idea what a STRINGA_LongVal attribute is. The string gadget needs to map its STRINGA_LongVal ID to the PGA_Top ID. This is what the ICA_MAP attribute is for.
The ICA_MAP attribute is defined by gadgetclass (it is also defined for icclass-more on that later). It accepts a tag list of attribute mappings. When a gadget sends out an OM_UPDATE message, it uses this map to translate a specific attribute ID to another attribute ID, without changing the value of the attribute. Each TagItem in the ICA_MAP makes up a single attribute mapping. The TagItem.ti_Tag of the mapping is the ID of an attribute to translate. The gadget translates that attribute ID to the attribute ID in TagItem.ti_Data. For example, an ICA_MAP that maps a string gadget's STRINGA_LongVal attribute to a prop gadget's PGA_Top attribute looks like this:
struct TagItem slidertostring[] = { {PGA_Top, STRINGA_LongVal}, {TAG_END, 0} };
Note that it is OK to have an ICA_TARGET without having an ICA_MAP. In cases where a gadget and its ICA_TARGET have a set of attributes in common, it would be unnecessary to use an ICA_MAP to match a gadget's attributes, as they already match.
Talk2boopsi.c
The following example, Talk2boopsi.c, creates a prop gadget and an integer string gadget which update each other without the example program having to process any messages from them.
// Talk2boopsi.c /* This example creates a BOOPSI prop gadget and integer string gadget, connecting them so they */ /* update each other when the user changes their value. The example program only initializes */ /* the gadgets and puts them on the window; it doesn't have to interact with them to make them */ /* talk to each other. */ #include <exec/types.h> #include <utility/tagitem.h> #include <intuition/intuition.h> #include <intuition/gadgetclass.h> /* contains IDs for gadget attributes */ #include <intuition/icclass.h> /* contains ICA_MAP, ICA_TARGET */ #include <proto/exec.h> #include <proto/intuition.h> struct Library *IntuitionBase; struct IntuitionIFace *IIntuition; struct Window *w; struct IntuiMessage *msg; struct Gadget *prop, *integer; /* The attribute mapping lists */ struct TagItem prop2intmap[] = /* This tells the prop gadget to */ { /* map its PGA_Top attribute to */ {PGA_Top, STRINGA_LongVal}, /* STRINGA_LongVal when it */ {TAG_END,} /* issues an update about the */ }; /* change to its PGA_Top value. */ struct TagItem int2propmap[] = /* This tells the string gadget */ { /* to map its STRINGA_LongVal */ {STRINGA_LongVal, PGA_Top}, /* attribute to PGA_Top when it */ {TAG_END,} /* issues an update. */ }; #define PROPGADGET_ID 1L #define INTGADGET_ID 2L #define PROPGADGETWIDTH 10L #define PROPGADGETHEIGHT 80L #define INTGADGETHEIGHT 18L #define VISIBLE 10L #define TOTAL 100L #define INITIALVAL 25L #define MINWINDOWWIDTH 80 #define MINWINDOWHEIGHT (PROPGADGETHEIGHT + 70) #define MAXCHARS 3L int main() { BOOL done = FALSE; IntuitionBase = IExec->OpenLibrary("intuition.library", 50); IIntuition = (struct IntuitionIFace*)IExec->GetInterface(IntuitionBase, "main", 1, NULL); if (IIntuition != NULL) { /* Open the window--notice that the window's IDCMP port */ /* does not listen for GADGETUP messages. */ if (w = IIntuition->OpenWindowTags(NULL, WA_Flags, WFLG_DEPTHGADGET | WFLG_DRAGBAR | WFLG_CLOSEGADGET | WFLG_SIZEGADGET, WA_IDCMP, IDCMP_CLOSEWINDOW, WA_MinWidth, MINWINDOWWIDTH, WA_MinHeight, MINWINDOWHEIGHT, TAG_END)) { /* Create a new propgclass object */ if (prop = (struct Gadget *)IIntuition->NewObject(NULL, "propgclass", GA_ID, PROPGADGET_ID, /* These are defined by gadgetclass and */ GA_Top, (w->BorderTop) + 5L, /* correspond to similarly named fields in */ GA_Left, (w->BorderLeft) + 5L, /* the Gadget structure. */ GA_Width, PROPGADGETWIDTH, GA_Height, PROPGADGETHEIGHT, ICA_MAP, prop2intmap, /* The prop gadget's attribute map */ /* The rest of this gadget's attributes are defined by propgclass. */ PGA_Total, TOTAL, /* This is the integer range of the prop gadget. */ PGA_Top, INITIALVAL, /* The initial integer value of the prop gadget. */ PGA_Visible, VISIBLE, /* This determines how much of the prop gadget area is */ /* covered by the prop gadget's knob, or how much of */ /* the gadget's TOTAL range is taken up by the prop */ /* gadget's knob. */ PGA_NewLook, TRUE, /* Use new-look prop gadget imagery */ TAG_END)) { /* create the integer string gadget. */ if (integer = (struct Gadget *)IIntuition->NewObject(NULL, "strgclass", GA_ID, INTGADGET_ID, /* Parameters for the Gadget structure */ GA_Top, (w->BorderTop) + 5L, GA_Left, (w->BorderLeft) + PROPGADGETWIDTH + 10L, GA_Width, MINWINDOWWIDTH - (w->BorderLeft + w->BorderRight + PROPGADGETWIDTH + 15L), GA_Height, INTGADGETHEIGHT, ICA_MAP, int2propmap, /* The attribute map */ ICA_TARGET, prop, /* plus the target. */ /* Th GA_Previous attribute is defined by gadgetclass and is used to */ /* wedge a new gadget into a list of gadget's linked by their */ /* Gadget.NextGadget field. When NewObject() creates this gadget, */ /* it inserts the new gadget into this list behind the GA_Previous */ /* gadget. This attribute is a pointer to the previous gadget */ /* (struct Gadget *). This attribute cannot be used to link new */ /* gadgetsinto the gadget list of an open window or requester, */ GA_Previous, prop, /* use AddGList() instead. */ STRINGA_LongVal, INITIALVAL, /* These attributes are defined by strgclass. */ STRINGA_MaxChars, MAXCHARS, /* The first contains the value of the */ TAG_END)) /* integer string gadget. The second is the */ /* maximum number of characters the user is */ /* allowed to type into the gadget. */ { IIntuition->SetGadgetAttrs(prop, w, NULL, /* Because the integer string gadget did not */ ICA_TARGET, integer, /* exist when this example created the prop */ TAG_END); /* gadget, it had to wait to set the */ /* ICA_Target of the prop gadget. */ IIntuition->AddGList(w, prop, -1, -1, NULL); /* Add the gadgets to the */ IIntuition->RefreshGList(prop, w, NULL, -1); /* window and display them. */ while (done == FALSE) /* Wait for the user to click */ { /* the window close gadget. */ IExec->WaitPort((struct MsgPort *)w->UserPort); while (msg = (struct IntuiMessage *) IExec->GetMsg((struct MsgPort *)w->UserPort)) { if (msg->Class == IDCMP_CLOSEWINDOW) done = TRUE; IExec->ReplyMsg(msg); } } IIntuition->RemoveGList(w, prop, -1); IIntuition->DisposeObject(integer); } IIntuition->DisposeObject(prop); } IIntuition->CloseWindow(w); } } IExec->DropInterface((struct Interface*)IIntuition); IExec->CloseLibrary(IntuitionBase); return 0; }
Making Gadgets Talk to an Application
There are two questions that the example above brings to mind. The first is, "What happens if the user types a value into the string gadget that is beyond the bounds of the prop gadget?" The answer is simple: very little. The prop gadget is smart enough to make sure its integer value does not go beyond the bounds of its display. In the example, the prop gadget can only have values from 0 to 90. If the user tries to type a value greater than 90, the prop gadget will set itself to its maximum of 90. Because the integer string gadget doesn't have any bounds checking built into it, the example needs to find an alternative way to check the bounds.
The other question is, "How does talk2boopsi.c know the current value of the gadgets?" That answer is simple too: it doesn't. The example doesn't ask the gadgets what their current values are (which it would do using GetAttr()) and the example doesn't pay attention to gadget events at the window's IDCMP port, so it isn't going to hear about them.
One easy way to hear about changes to the gadget events is to listen for a "release verify". Conventional Intuition gadgets can trigger a release verify IDCMP event when the user finishes manipulating the gadget. BOOPSI gadgets can do this, too, while continuing to update each other.
To make Talk2boopsi.c do this would require only a few changes. First, the window's IDCMP port has to be set up to listen for IDCMP_GADGETUP events. Next, the example needs to set the gadget's GLFG_RELVERIFY flags. It can do this by setting the gadgetclass GA_RelVerify attribute to TRUE for both gadgets. That's enough to trigger the release verify message, so all Talk2boopsi.c needs to do is account for the new type of IDCMP message, IDCMP_GADGETUP. When Talk2boopsi.c gets a release verify message, it can use GetAttr() to ask the integer gadget its value. If this value is out of range, it should explicitly set the value of the integer gadget to a more suitable value using SetGadgetAttrs().
Using the GLFG_RELVERIFY scheme above, an application will only hear about changes to the gadgets after the user is finished changing them. The application does not hear all of the interim updates that, for example, a prop gadget generates. This is useful if an application only needs to hear the final value and not the interim update.
It is also possible to make the IDCMP port of a BOOPSI gadget's window the ICA_TARGET of the gadget. There is a special value for ICA_TARGET called ICTARGET_IDCMP (defined in <intuition/icclass.h>). This tells the gadget to send an IDCMP_IDCMPUPDATE class IntuiMessage to its window's IDCMP port. Of course, the window has to be set up to listen for IDCMP_IDCMPUPDATE IntuiMessages. The BOOPSI gadget passes an address in the IntuiMessage.IAddress field. It points to an attribute tag list containing the attribute (and its new value) that triggered the IDCMP_IDCMPUPDATE message. An application can use the utility.library tag functions to access the gadget's attributes in this list. Using this scheme, an application will hear all of the interim gadget updates. If the application is using a gadget that generates a lot of interim OM_UPDATE messages (like a prop gadget), the application should be prepared to handle a lot of messages.
Using this IDCMP_IDCMPUPDATE scheme, if the gadget uses an ICA_MAP to map the attribute to a special dummy attribute ICSPECIAL_CODE (defined in <intuition/icclass.h>), the IntuiMessage.Code field will contain the value of the attribute. Because the attribute's value is a 32-bit quantity and the IntuiMessage.Code field is only 16 bits wide, only the least significant 16 bits of the attribute will appear in the IntuiMessage.Code field, so it can't hold a 32-bit quantity, like a pointer. Applications should only use the lower 16 bits of the attribute value.
The Interconnection Classes
The IDCMP_IDCMPUPDATE scheme presents a problem to an application that wants to make gadgets talk to each other and talk to the application. BOOPSI gadgets only have one ICA_TARGET. One BOOPSI gadget can talk to either another BOOPSI object or its window's IDCMP port, but not both. Using this scheme alone would force the application to update the integer value of the gadgets, which is what we are trying to avoid in the first place.
One of the standard BOOPSI classes, icclass, is a class of information forwarders. An icclass object receives OM_UPDATE messages from one object and passes those messages on to its own ICA_TARGET. If it needs to map any incoming attributes, it can use its own ICA_MAP to do so.
Icclass has a subclass called modelclass. Using a modelclass object, an application can chain a series of these objects together to set up a "broadcast list" of icclass objects. The modelclass object is similar to the icclass object in that it has its own ICA_TARGET and ICA_MAP. It differs in that an application can use the modelclass OM_ADDMEMBER method to add icclass objects to the modelclass object's broadcast list.
The OM_ADDMEMBER method is defined by rootclass. It adds one BOOPSI object to the personal list of another BOOPSI object. It is up to the BOOPSI object's class to determine the purpose of the objects in the list. Unlike the other methods mentioned so far in this chapter, OM_ADDMEMBER does not have an Intuition function equivalent. To pass an OM_ADDMEMBER message to an object use the function IDoMethodA(), or its stack-based equivalent, IDoMethod().
The message that the OM_ADDMEMBER method uses looks like this (from <intuition/classusr.h>):
struct opMember { ULONG MethodID; Object *opam_Object; };
where MethodID is OM_ADDMEMBER and opam_Object points to the object to add to myobject's list.
To rearrange Talk2boopsi.c so that it uses a modelclass object (also known as a model):
- Create the integer and prop gadget.
- Create the model.
- Create two icclass objects, one called int2prop and the other called prop2int.
- Make the model the ICA_TARGET of both the integer gadget and the prop gadget. The gadgets do not need an ICA_MAP.
- Using IDoMethod() to call OM_ADDMEMBER, add the icclass objects to the model's personal list.
- Make the prop gadget the ICA_TARGET of int2prop. Make the integer gadget the ICA_TARGET of prop2int.
- Create an ICA_MAP map list for int2prop that maps STRINGA_LongVal to PGA_Top. Create an ICA_MAP map list for prop2int that maps PGA_Top to STRINGA_LongVal. Make the ICA_TARGET of the model ICTARGET_IDCMP.
Diagrammatically, the new Talk2boopsi.c should look something like this:
When either of these gadgets has some interim state change (caused by the user manipulating the gadgets), it sends an OM_UPDATE message to its ICA_TARGET, which in this case is the modelclass object. When this model gets the message, it does two things. It sends an IDCMP_IDCMPUPDATE to the IDCMP port of the gadget's window and it also sends OM_UPDATE messages to all of the objects in its personal list. When int2prop gets an OM_UPDATE message, it forwards that message to its ICA_TARGET, the prop gadget. Similarly, when prop2int gets an OM_UPDATE message, it forwards that message to its ICA_TARGET, the integer gadget.
Although in this case it isn't a problem, icclass and modelclass objects contain loop inhibition capabilities. If an icclass object (or modelclass object) receives an OM_UPDATE message, it forwards the message to its target. If somehow that forwarded message gets forwarded (or broadcast) back to the icclass object, the icclass object ignores the message. This prevents the possibility of an infinite OM_UPDATE loop.
Creating a BOOPSI Class
So far this chapter has only hinted at what is possible with BOOPSI. Its power lies in its extensibility. BOOPSI grants the application programmer the power to add custom features to existing classes. If an existing class comes close to your needs, you can build on that class so it does exactly what you want. If you want a class that is unlike an existing class, you can create it.
The heart of a BOOPSI class is its method Dispatcher function. According to the OOP metaphor, when an application wants a BOOPSI object to perform a method, it sends the object a message. In reality, that object is only a data structure, so it does not have the power to do anything. When an object receives a BOOPSI message, a BOOPSI message structure is passed to the dispatcher of that object's class. The dispatcher examines the message and figures out what to do about it.
For example, when an application calls SetGadgetAttrs() on an integer gadget:
IIntuition->SetGadgetAttrs(myintegergadget, mywindow, NULL, STRINGA_LongVal, 75, GA_ID, 2, TAG_END);
the SetGadgetAttrs() function calls the strgclass dispatcher. A BOOPSI dispatcher receives three arguments: a pointer to the dispatcher's Class (defined in <intuition/classes.h>), a pointer to the object that is going to perform the method, and a pointer to the BOOPSI message. In this case, the SetGadgetAttrs() function builds an OM_SET message, finds the strgclass dispatcher, and "sends" the dispatcher the OM_SET message. SetGadgetAttrs() can find the dispatcher because an object contains a reference to its dispatcher.
When the dispatcher function "gets" the message, it examines the message to find out its corresponding method. In this case, the dispatcher recognizes the message as an OM_SET message and proceeds to set myintegergadget's attributes.
An OM_SET message looks like this (defined in <intuition/classusr.h>):
struct opSet { ULONG MethodID; /* This will be set to OM_SET */ struct TagItem *ops_AttrList; /* A tag list containing the */ /* attribute/value pairs of */ /* the attributes to set. */ struct GadgetInfo *ops_GInfo; /* Special information for gadgets */ }
The OM_SET message contains a pointer to a tag list in ops_AttrList that looks like this:
{STRINGA_LongVal, 75}, {GA_ID, 2}, {TAG_END, 0}
The strgclass dispatcher scans through this tag list and recognizes the STRINGA_LongVal attribute. The dispatcher sets myintegergadget's internal STRINGA_LongVal value to the corresponding value (75) from the attribute/value pair.
The strgclass dispatcher continues to scan through the tag list. When it finds GA_ID, it does not process it like STRINGA_LongVal. The strgclass dispatcher's OM_SET method does not recognize the GA_ID attribute because strgclass inherited the GA_ID attribute from gadgetclass. To handle setting the GA_ID attribute, the strgclass dispatcher passes on the OM_SET message to its superclass's dispatcher. The strgclass dispatcher passes control to the gadgetclass dispatcher, which knows about the GA_ID attribute.
Building on Existing Public Classes
A program can create its own subclasses which build on the features of existing classes. For example, a program could create a subclass of modelclass named rkmmodelclass. Rkmmodelclass builds on modelclass by adding a new attribute called RKMMOD_CurrVal. This purpose of this attribute is simply to hold an integer value.
Because this new attribute is built into an rkmmodel object, the object could be implemented so that it exercises a certain amount of control over that value. For example, rkmmodelclass could be implemented so an rkmmodel performs bounds checking on its internal value. When an application asks an rkmmodel to set its internal RKMMOD_CurrVal, the rkmmodel makes sure the new value is not beyond a maximum value. If the new value is beyond the maximum, it sets its current value to the maximum. After the rkmmodelclass object has set its internal RKMMOD_CurrVal, it can broadcast the change on to objects in its broadcast list.
The dispatcher for rkmmodelclass does not have to do a lot of work because it inherits most of its behavior from its superclasses. The rkmmodelclass has to take care of setting aside memory for the RKMMOD_CurrVal attribute and processing any OM_SET requests to set the RKMMOD_CurrVal attribute. For any other attributes or methods, the rkmmodelclass dispatcher passes on processing to its superclass, modelclass.
Building Rkmmodelclass
So far, the theoretical class rkmmodelclass has just one attribute, RKMMOD_CurrVal. A couple of extra attributes can make it more useful. Because the rkmmodel object maintains an upper limit on its RKMMOD_CurrVal integer value, it would be useful if that upper limit was variable. Using a new attribute, RKMMOD_Limit, an application can tell a rkmmodel what its upper limit is. The rkmmodel will enforce the limit internally, so the application doesn’t have to worry about it.
Another useful addition is a pulse increment and decrement for RKMMOD_CurrVal. Whenever the model receives an increment or decrement command, it increments or decrements its internal value. To make the example class simple, rkmmodelclass implements incrementing and decrementing by creating "dummy" attributes called RKMMOD_Up and RKMMOD_Down. When an rkmmodel receives an OM_SET message for one of these attributes, it increments or decrements RKMMOD_CurrVal. An rkmmodelclass object does not care what the value of the RKMMOD_Up and RKMMOD_Down attributes are, it only cares that it received an OM_UPDATE about it.
There are two pieces of data that make up this new class's instance data: the rkmmodel's current value (RKMMOD_CurrVal) and the upper limit of the rkmmodel (RKMMOD_Limit). The example class consolidates them into one structure:
struct RKMModData { ULONG currval; ULONG vallimit; };
Writing the Dispatcher
The C prototype for a BOOPSI dispatcher looks like this:
uint32 dispatchRKMModel(Class *cl, Object *recvobject, Msg msg);
where cl points to the Class (defined in <intuition/classes.h>) of the dispatcher, recvobject points to the object that received the message, and msg is that BOOPSI message. The format of the message varies according to the method. The default BOOPSI message is an Msg (from <intuition/classusr.h>):
typedef struct { uint32 MethodID; } *Msg;
BOOPSI methods that require parameters use custom message structures. The first field of any message structure is always the method's methodID. This makes custom messages look like an Msg. The dispatcher looks at an incoming message's first field to tell what its method is. Rkmmodelclass objects respond to several rootclass methods:
- OM_NEW
- This method creates a new rkmmodelclass object. It uses an opSet structure as its BOOPSI message.
- OM_DISPOSE
- This method tells an object to dispose of itself. It uses an Msg as its BOOPSI message.
- OM_SET
- This method tells an object to set one or more of its attribute values. It uses an opSet structure as its BOOPSI message.
- OM_UPDATE
- This method tells an object to update one or more of its attribute values. It uses an opUpdate structure as its BOOPSI message.
- OM_GET
- This method tells an object to report an attribute value. It uses an opGet structure as its BOOPSI message.
- OM_ADDTAIL
- This method tells an object to add itself to the end of an Exec list. It uses an opAddTail structure as its BOOPSI message.
- OM_REMOVE
- This method tells an object to remove itself from an Exec list. It uses an Msg as its BOOPSI message.
- OM_ADDMEMBER
- This method tells an object to add an object to its broadcast list. It uses an opMember structure as its BOOPSI message.
- OM_REMMEMBER
- This method tells an object to remove an object from its broadcast list. It uses an opMember structure as its BOOPSI message.
- OM_NOTIFY
- This method tells an object to broadcast an attribute change to its broadcast list. It uses an opSet structure as its BOOPSI message.
Of these, rkmmodelclass has to process OM_NEW, OM_SET, OM_UPDATE, and OM_GET.
OM_NEW
The OM_NEW method returns a pointer to a newly created BOOPSI object, or NULL if it failed to create the object. This method receives the following message structure (defined in <intuition/classusr.h>):
struct opSet { /* The OM_NEW method uses the same structure as OM_GET */ ULONG MethodID; struct TagItem *ops_AttrList; struct GadgetInfo *ops_GInfo; };
The ops_AttrList field contains a pointer to a TagItem array of attribute/value pairs. These contain the initial values of the new object's attributes. The ops_GInfo field is always NULL for the OM_NEW method.
Unlike other methods, when a dispatcher gets an OM_NEW message, the object pointer (recvobject from the dispatchRKMModel() prototype above) does not point to an object. It doesn't make sense for recvobject to point to an object because the idea is to create a new object, not act on an existing one.
The pointer normally used to pass a BOOPSI object is instead used to pass the address of the object's "true class". An object's true class is the class of which the object is an instance.
The first thing the dispatcher does when it processes an OM_NEW message is pass the OM_NEW message on to its superclass's dispatcher. It does this using function IDoSuperMethodA():
uint32 IDoSuperMethodA(Class *cl, Object *trueclass, Msg msg);
Each dispatcher passes control to its superclass. Eventually the message will arrive at the rootclass dispatcher. The OM_NEW method in the rootclass dispatcher looks at the object's true class (trueclass from the prototype) to find out which class dispatcher is trying to create a new object. Note that trueclass is not necessarily the same as the current dispatcher's class (cl from the dispatchRKMModel() prototype above), although this would be the case if the object's true class is a subclass of the current dispatcher's class.
The rootclass dispatcher uses the true class to find out how much memory to allocate for the object's instance data. Each class keeps a record of how much memory its local instance data requires. The rootclass dispatcher also looks at each class between the true class and rootclass to find out much memory the local instance data for those classes require. The rootclass dispatcher totals the amount of local instance data memory needed by the true class and each of its superclasses and allocates that much memory.
If all goes well, the rootclass dispatcher increments a private field in the true class that keeps track of how many instances of the true class there currently are. It then returns a pointer to the newly created object and passes control back to the subclass dispatcher that called it, which is icclass in the case of rkmmodelclass. If there was a problem, the rootclass dispatcher does not increment the object count and passes back a NULL.
When the rootclass dispatcher returns, the icclass dispatcher regains control from IDoSuperMethodA(). IDoSuperMethodA() will return either a pointer to the new object or else it returns NULL if there was an error. Although the rootclass dispatcher allocated all the memory the object needs, it only initialized the instance data local to rootclass. Now it's the icclass dispatcher's turn to do some work. It has to initialize the instance data that is local to icclass.
A dispatcher finds its local instance data by using the INST_DATA() macro (defined in <intuition/classes.h>):
VOID * INST_DATA(Class *localclass, Object *object);
INST_DATA() takes two arguments, a pointer to a class and a pointer to the object. The INST_DATA() macro returns a pointer to the instance data local to localclass. When the icclass dispatcher was called, it received three arguments, one of which was a pointer to the local class (icclass). The icclass dispatcher passes this pointer and the new object pointer it got from IDoSuperMethodA() to INST_DATA() to get a pointer to the instance data local to icclass.
After initializing its local instance data, the icclass dispatcher passes control back to the modelclass dispatcher, which in turn, initializes the instance data local to modelclass. Finally, the rkmmodelclass dispatcher regains control and now has to take care of its local instance data.
To find its local instance data, the rkmmodelclass dispatcher needs a pointer to its Class and a pointer to the new object. The dispatcher function gets its Class pointer as its first argument (cl from the dispatchRKMModel() prototype above). It gets the new object pointer as the return value from IDoSuperMethodA(). In this case, INST_DATA() returns a pointer to an RKMModData structure.
Now the dispatcher has to initialize its local instance data. It has to scan through the tag list passed in the OM_NEW message looking for initial values for the RKMMOD_CurrVal and RKMMOD_Limit attributes. As an alternative, the dispatcher's OM_NEW method can use its OM_SET method to handle initializing these "settable" attributes.
Finally, the dispatcher can return. When the dispatcher returns from an OM_NEW method, it returns a pointer to the new object.
If the OM_NEW method fails, it should tell the partially initialized object it got from its superclass' dispatcher to dispose of itself (using OM_DISPOSE) and return NULL.
OM_SET/OM_UPDATE
For the OM_SET message, the rkmmodelclass dispatcher steps through the attribute/value pairs passed to it in the OM_SET message looking for the local attributes (see OM_NEW for the OM_SET message structure). The RKMMOD_Limit attribute is easy to process. Just find it and record the value in the local RKMModData.vallimit field.
Because the function of the rkmmodelclass's OM_SET and OM_UPDATE methods are almost identical, the rkmmodelclass dispatcher handles them as the same case. The only difference is that, because the OM_UPDATE message comes from another BOOPSI object, the OM_UPDATE method can report on transitory state changes of an attribute. For example, when the user slides a BOOPSI prop gadget, that prop gadget sends out an interim OM_UPDATE message for every interim value of PGA_Top. When the user lets go of the prop gadget, the gadget sends out a final OM_UPDATE message. The OM_UPDATE message is almost identical to the OM_SET message:
#define OPUF_INTERIM (1 << 0) struct opUpdate { /* the OM_NOTIFY method uses the same structure */ ULONG MethodID; struct TagItem *opu_AttrList; struct GadgetInfo *opu_GInfo; ULONG opu_Flags; /* The extra field */ };
A dispatcher can tell the difference between an interim and final OM_UPDATE message because the OM_UPDATE message has an extra field on it for flags. If the low order bit (the OPUF_INTERIM bit) is set, this is an interim OM_UPDATE message. The interim flag is useful to a class that wants to ignore any transitory messages, processing only final attribute values. Because rkmmodelclass wants to process all changes to its attributes, it processes all OM_UPDATE messages.
The RKMMOD_CurrVal attribute is a little more complicated to process. The dispatcher has to make sure the new current value is within the limits set by RKMMOD_Limit, then record that new value in the local RKMModData.currval field. Because other objects need to hear about changes to RKMMOD_CurrVal, the dispatcher has to send a notification request. It does this by sending itself an OM_NOTIFY message. The OM_NOTIFY message tells an object to notify its targets (its ICA_TARGET and the objects in its broadcast list) about an attribute change. The OM_NOTIFY method does this by sending OM_UPDATE messages to all of an object's targets.
The rkmmodelclass dispatcher does not handle the OM_NOTIFY message itself. It inherits this method from modelclass, so the rkmmodelclass dispatcher passes OM_NOTIFY messages on to its superclass.
To notify its targets, the rkmmodelclass dispatcher has to construct an OM_NOTIFY message. The OM_NOTIFY method uses the same message structure as OM_UPDATE. Using the stack-based version of IDoSuperMethodA(), IDoSuperMethod(), the dispatcher can build an OM_NOTIFY message on the stack:
. . . struct TagItem tt[2]; struct opUpdate *msg; . . . tt[0].ti_Tag = RKMMOD_CurrVal; /* make a tag list. */ tt[0].ti_Data = mmd->currval; tt[1].ti_Tag = TAG_END; IIntuition->IDoSuperMethod(cl, o, OM_NOTIFY, tt, msg->opu__GInfo, ((msg->MethodID == OM_UPDATE) ? (msg->opu_Flags) : 0L)); . . .
Because the OM_NOTIFY needs a tag list of attributes about which to issue updates, the dispatcher builds a tag list containing just the RKMMOD_CurrVal tag and its new value. The dispatcher doesn't use the tag list passed to it in the OM_UPDATE/OM_NOTIFY message because that list can contain many other attributes besides RKMMOD_CurrVal.
The msg variable in the IDoSuperMethod() call above is the OM_SET or OM_UPDATE message that was passed to the dispatcher. The dispatcher uses that structure to find a pointer to the GadgetInfo structure that the OM_NOTIFY message requires. The GadgetInfo structure comes from Intuition and contains information that BOOPSI gadgets need to render themselves. For the moment, don't worry about what the GadgetInfo structure actually does, just pass it on. The targets of an rkmmodel will probably need it.
Notice that the dispatcher has to test to see if the message is an OM_SET or OM_UPDATE so it can account for the opu_Flags field at the end of the OM_UPDATE message.
Processing the RKMMOD_Up and RKMMOD_Down attributes is similar to the RKMMOD_CurrVal attribute. When the dispatcher sees one of these, it has to increment or decrement the local RKMModData.currval, making sure RKMModData.currval is within limits. The dispatcher then sends an OM_NOTIFY message to the superclass about the change to RKMModData.currval.
The return value from the dispatcher's OM_SET method depends on the what effect the attribute change has to the visual state of the objects in the rkmmodel's broadcast list. If an attribute change will not affect the visual state of the rkmmodel's objects, the OM_SET method returns zero. If the attribute change could trigger a change to the rkmmodel's objects, it returns something besides zero. For example, the rkmmodelclass OM_SET method returns 1L if an rkmmodel's RKMMOD_CurrVal, RKMMOD_Up, or RKMMOD_Down attribute is changed.
At some point the rkmmodelclass dispatcher has to allow its superclasses to process these attributes it inherits. Normally a dispatcher lets the superclass process its attributes before attempting to process any local attributes. The rkmmodelclass dispatcher does this by passing on the OM_SET or OM_UPDATE message using IDoSuperMethodA() (inheritance at work!). As an alternative, the dispatcher can use the function ISetSuperAttrs().
OM_GET
The rkmmodel only has one "gettable" attribute: RKMMOD_CurrVal, which makes processing it easy. The OM_GET message looks like this (defined in <intuition/classusr.h>):
struct opGet { ULONG MethodID; /* OM_GET */ ULONG opg_AttrID; /* The attribute to retrieve */ ULONG *opg_Storage; /* a place to put the attribute's value */ };
When the rkmmodelclass dispatcher receives an OM_GET message with an opg_AttrID equal to RKMMOD_CurrVal, it copies the current value (RKMModData.currval) to the memory location opg_Storage points to and returns a value of TRUE. The TRUE indicates that there was no error. If opg_AttrID is not RKMMOD_CurrVal, the dispatcher should let its superclass handle this message.
The rkmmodelclass dispatcher can take advantage of the fact that the only "gettable" attribute available to an rkmmodel is RKMMOD_CurrVal (the attributes defined by modelclass and icclass are not gettable-see the BOOPSI Class Reference for more details on which attributes are "settable", "gettable", etc.). If opg_AttrID is not RKMMOD_CurrVal, the rkmmodelclass dispatcher can return FALSE, indicating that the attribute was not "gettable".
If the rkmmodelclass dispatcher comes across any other messages besides OM_NEW, OM_SET, OM_UPDATE, and OM_GET message, it blindly passes them on to its superclass for processing.
Making the New Class
The Intuition function MakeClass() creates a new BOOPSI class:
Class *MakeClass(STRPTR newclassID, STRPTR pubsuperclassID, Class *privsuperclass, UWORD instancesize, ULONG flags);
If the new class is going to be public, newclassID is a string naming the new class. If the new class is private, this field is NULL. The next two fields tell MakeClass() where to find the new class's superclass. If the superclass is public, pubsuperclassID points to a string naming that public superclass and the privsuperclass pointer is NULL. If the superclass is private, privsuperclass points to that superclass's Class structure and pubsuperclassID is NULL. The size of the new class's local instance data is instancesize. The last parameter, flags, is for future enhancement. For now, make this zero.
If it is successful, MakeClass() returns a pointer to the new class, otherwise it returns NULL. When MakeClass() is successful, it also takes measures to make sure no one can "close" the new class's superclass (using FreeClass()). It does this by incrementing a private field of the superclass that keeps track of how many subclasses the superclass currently has.
After successfully creating a class, an application has to tell the class where its dispatcher is. The Class pointer (defined in <intuition/classes.h>) returned by MakeClass() contains a Hook structure called cl_Dispatcher, which is used to call the dispatcher. The application has to initialize this hook:
myclass->cl_Dispatcher.h_Entry = dispatchRKMModel; myclass->cl_Dispatcher.h_SubEntry = NULL;
The h_Entry field points to the dispatch function. See the Callback Hooks section of the Utility Library for more details.
To make a class public instead of private, an application has to call AddClass() in addition to giving the class a name in MakeClass(). AddClass() takes one argument, a pointer to a valid Class structure that has been initialized as a public class by MakeClass(). To remove a public class added to the system with AddClass(), pass the public class pointer to RemoveClass(). See the Intuition Autodocs for more details on AddClass() and RemoveClass().
RKMModel.c
The following code, RKMModel.c, makes up an initialization function and the dispatcher function for a private class informally called rkmmodelclass.
/* RKMModel.c - A simple custom modelclass subclass. */ #include <exec/types.h> #include <intuition/intuition.h> #include <intuition/classes.h> #include <intuition/classusr.h> #include <intuition/imageclass.h> #include <intuition/gadgetclass.h> #include <intuition/cghooks.h> #include <intuition/icclass.h> #include <utility/tagitem.h> #include <utility/hooks.h> #include <proto/intuition_protos.h> #include <proto/utility_protos.h> extern struct IntuitionIFace *IIntuition; extern struct UtilityIFace *IUtility; /*************************************************************************************************/ /**************** The attributes defined by this class *****************************************/ /*************************************************************************************************/ #define RKMMOD_CurrVal (TAG_USER + 1) /* This attribute is the current value of the model.*******/ /**********************************************************/ #define RKMMOD_Up (TAG_USER + 2) /* These two are fake attributes that rkmmodelclass *******/ #define RKMMOD_Down (TAG_USER + 3) /* uses as pulse values to increment/decrement the *******/ /* rkmmodel's RKMMOD_CurrVal attribute. *******/ /**********************************************************/ #define RKMMOD_Limit (TAG_USER + 4) /* This attribute contains the upper bound of the *******/ /* rkmmodel's RKMMOD_CurrVal. The rkmmodel has a *******/ /* static lower bound of zero. *******/ /*************************************************************************************************/ #define DEFAULTVALLIMIT 100L /* If the programmer doesn't set */ /* RKMMOD_Limit, it defaults to this. */ struct RKMModData { uint32 currval; /* The instance data for this class. */ uint32 vallimit; }; /*************************************************************************************************/ /************************** The functions in this module ********************************/ /*************************************************************************************************/ Class *initRKMModClass(void); /***************/ BOOL freeRKMModClass(Class *); /***************/ uint32 dispatchRKMModel(Class *, Object *, Msg); /***************/ void NotifyCurrVal(Class *, Object *, struct opUpdate *, struct RKMModData *); /***************/ /*************************************************************************************************/ /*************************************************************************************************/ /******************************** Initialize the class **************************************/ /*************************************************************************************************/ Class *initRKMModClass(void) /* Make the class and set */ { /* up the dispatcher's hook. */ Class *cl; if ( cl = IIntuition->MakeClass( NULL, "modelclass", NULL, sizeof ( struct RKMModData ), 0 )) { cl->cl_Dispatcher.h_Entry = dispatchRKMModel; /* initialize the */ /* cl_Dispatcher Hook. */ } return ( cl ); } /*************************************************************************************************/ /********************************* Free the class ***************************************/ /*************************************************************************************************/ BOOL freeRKMModClass( Class *cl ) { return (IIntuition->FreeClass(cl)); } /*************************************************************************************************/ /******************************** The class Dispatcher ***********************************/ /*************************************************************************************************/ uint32 dispatchRKMModel(Class *cl, Object *o, Msg msg) { struct RKMModData *mmd; APTR retval = NULL; /* A generic return value used by this class's methods. The */ /* meaning of this field depends on the method. For example, */ /* OM_GET uses this a a boolean return value, while OM_NEW */ /* uses it as a pointer to the new object. */ switch (msg->MethodID) { case OM_NEW: /* Pass message onto superclass first so it can set aside the memory */ /* for the object and take care of superclass instance data. */ if (retval = (APTR)IIntuition->IDoSuperMethodA(cl, o, msg)) { /************************************************************************/ /* For the OM_NEW method, the object pointer passed to the dispatcher */ /* does not point to an object (how could it? The object doesn't exist */ /* yet.) IDoSuperMethodA() returns a pointer to a newly created */ /* object. INST_DATA() is a macro defined in <intuition/classes.h> */ /* that returns a pointer to the object's instance data that is local */ /* to this class. For example, the instance data local to this class */ /* is the RKMModData structure defined above. */ /************************************************************************/ mmd = INST_DATA(cl, retval); mmd->currval = IUtility->GetTagData(RKMMOD_CurrVal, 0L, /* initialize object's attributes. */ ((struct opSet *)msg)->ops_AttrList); mmd->vallimit = IUtility->GetTagData(RKMMOD_Limit, DEFAULTVALLIMIT, ((struct opSet *)msg)->ops_AttrList); } break; case OM_SET: case OM_UPDATE: mmd = INST_DATA(cl, o); IIntuition->IDoSuperMethodA(cl, o, msg); /* Let the superclasses set their attributes first. */ { struct TagItem *tstate, *ti; /* grab some temp variables off of the stack. */ ti = ((struct opSet *)msg)->ops_AttrList; tstate = ti; /* Step through all of the attribute/value pairs in the list. Use the */ /* utility.library tag functions to do this so they can properly process */ /* special tag IDs like TAG_SKIP, TAG_IGNORE, etc. */ while (ti = IUtility->NextTagItem(&tstate)) /* Step through all of the attribute/value */ { /* pairs in the list. Use the utility.library tag functions */ /* to do this so they can properly process special tag IDs */ /* like TAG_SKIP, TAG_IGNORE, etc. */ switch (ti->ti_Tag) { case RKMMOD_CurrVal: if ((ti->ti_Data) > mmd->vallimit) ti->ti_Data = mmd->vallimit; mmd->currval = ti->ti_Data; NotifyCurrVal(cl, o, msg, mmd); retval = (APTR)1L; /* Changing RKMMOD_CurrVal can cause a visual */ break; /* change to gadgets in the rkmmodel's broadcast */ /* list. The rkmmodel has to tell the applica- */ /* tion by returning a value besides zero. */ case RKMMOD_Up: mmd->currval++; /* Make sure the current value is not greater than value limit. */ if ((mmd->currval) > mmd->vallimit) mmd->currval = mmd->vallimit; NotifyCurrVal(cl, o, msg, mmd); retval = (APTR)1L; /* Changing RKMMOD_Up can cause a visual */ break; /* change to gadgets in the rkmmodel's broadcast */ /* list. The rkmmodel has to tell the applica- */ /* tion by returning a value besides zero. */ case RKMMOD_Down: mmd->currval--; /* Make sure currval didn't go negative. */ if ((LONG)(mmd->currval) == -1L) mmd->currval = 0L; NotifyCurrVal(cl, o, msg, mmd); retval = (APTR)1L; /* Changing RKMMOD_Down can cause a visual */ break; /* change to gadgets in the rkmmodel's broadcast */ /* list. The rkmmodel has to tell the applica- */ /* tion by returning a value besides zero. */ case RKMMOD_Limit: mmd->vallimit = ti->ti_Data; /* Set the limit. Note that this does */ break; /* not do bounds checking on the */ /* current RKMModData.currval value. */ } } } break; case OM_GET: /* The only attribute that is "gettable" in this */ mmd = INST_DATA(cl, o); /* class or its superclasses is RKMMOD_CurrVal. */ if ((((struct opGet *)msg)->opg_AttrID) == RKMMOD_CurrVal) { *(((struct opGet *)msg)->opg_Storage) = mmd->currval; retval = (Object *)TRUE; } else retval = (APTR)IIntuition->IDoSuperMethodA(cl, o, msg); break; default: /* rkmmodelclass does not recognize the methodID, so let the superclass's */ /* dispatcher take a look at it. */ retval = (APTR)IIntuition->IDoSuperMethodA(cl, o, msg); break; } return((uint32)retval); } void NotifyCurrVal(Class *cl, Object *o, struct opUpdate *msg, struct RKMModData *mmd) { struct TagItem tt[2]; tt[0].ti_Tag = RKMMOD_CurrVal; /* make a tag list. */ tt[0].ti_Data = mmd->currval; tt[1].ti_Tag = TAG_END; /* If the RKMMOD_CurrVal changes, we want everyone to know about */ IIntuition->IDoSuperMethod(cl, o, /* it. Theoretically, the class is supposed to send itself a */ OM_NOTIFY, /* OM_NOTIFY message. Because this class lets its superclass */ tt, /* handle the OM_NOTIFY message, it skips the middleman and */ msg->opu_GInfo, /* sends the OM_NOTIFY directly to its superclass. */ ((msg->MethodID == OM_UPDATE) ? (msg->opu_Flags) : 0L)); /* If this is an OM_UPDATE */ /* method, make sure the part the OM_UPDATE message adds to the */ /* OM_SET message gets added. That lets the dispatcher handle */ } /* OM_UPDATE and OM_SET in the same case. */
Below is a diagram showing how an application could use an rkmmodelclass object:
In this diagram, the application uses buttongclass BOOPSI gadgets to send the rkmmodelclass the RKMMOD_Up and RKMMOD_Down attribute pulses.
The example takes advantage of an odd feature of buttongclass. When the user clicks on a buttongclass gadget, it sends an OM_UPDATE to its ICA_TARGET, even though no BOOPSI attribute of buttongclass has changed. It does this because it's a convenient way to report button clicks.
Whenever a gadget sends a notification, the list of attribute/value pairs in the OM_NOTIFY message always contains the gadget's GA_ID. This is an easy way for the button to inform its target of its ID so the target knows which gadget sent the OM_UPDATE message. When a buttongclass sends a notification because of a button click, it only sends out an OM_UPDATE about its GA_ID because none of its attributes changed.
When the user clicks one of the buttons in the rkmmodelclass diagram, the button uses an ICA_MAP to map its GA_ID to one of the "dummy" pulse attributes, RKMMOD_Up and RKMMOD_Down. When the rkmmodel receives the OM_UPDATE message about RKMMOD_Up or RKMMOD_Down, it increments or decrements its internal value.
There is one more important thing to note about rkmmodelclass. Looking at the rkmmodelclass Object diagram above, an rkmmodel's RKMMOD_CurrVal changes because it received an OM_UPDATE message from one of its gadgets. RKMMOD_CurrVal can also change if the application explicitly set RKMMOD_CurrVal using SetAttrs() or SetGadgetAttrs().
The primary difference between the OM_SET message that SetAttrs() sends and the OM_SET message that SetGadgetAttrs() sends is that SetAttrs() passes a NULL in opSet.ops_GInfo instead of a GadgetInfo pointer. This doesn't present a problem for the rkmmodel object, because it doesn't use the GadgetInfo structure. The problem is that when the rkmmodel notifies its targets, some of which are gadgets, they can't update their visual state because they need a GadgetInfo to render themselves. For this reason, the rkmmodelclass dispatcher returns a positive non-zero value when an attribute change occurs that could cause a change in the visual state of any objects in its broadcast list. An application that uses rkmmodelclass must test the return value when calling SetAttrs() on an rkmmodelclass object to tell if the attribute change requires a visual refresh of the gadgets (see the Intuition Autodocs for RefreshGadgets()).
BOOPSI Dispatchers Can Execute on Intuition's Context |
---|
Notice that the gadgets in the figure above send OM_UPDATE messages to the rkmmodel when the user manipulates them. Because Intuition handles the user input that triggers the OM_UPDATE messages, Intuition itself is sending the OM_UPDATE messages. This means the rkmmodelclass dispatcher must be able to run on Intuition's context, which puts some limitations on what the dispatcher is permitted to do: it can't use dos.library, it can't wait on application signals or message ports and it can't call any Intuition functions which might wait on Intuition. |
Although rkmmodelclass serves as an example of a class, it leaves a little to be desired in a real-world implementation. To create the "prop-integer-up/down" super gadget from the diagram above, the application has to create, initialize, and link nine BOOPSI objects, which is tedious, especially if the application needs several of these super gadgets. Ideally, all these functions would be rolled into some subclass of gadgetclass. If there were such a class, an application would only have to create one instance of this subclass to get such a gadget. When the subclass received an OM_NEW message, it would take care of creating, initializing, and linking all of the BOOPSI objects that make up the whole super gadget.
White Boxes: The Transparent Base Classes
BOOPSI gadgets and images were designed to be backwards compatible with the old Intuition Gadgets and Images, so as part of their instance data, both types of objects have the old Intuition structures built into them. When NewObject() creates a new gadget or image object, the pointer it returns points to the object's embedded Gadget or Image corresponding structure. Because Intuition can tell the difference between BOOPSI images and gadgets and the original images and gadgets, applications can use BOOPSI images and gadgets interchangeably with the older Intuition entities.
Although normally considered a "programming sin", in some cases it is legal for class dispatchers to directly manipulate some internal fields of certain BOOPSI objects. For compatibility reasons, a BOOPSI image or gadget object contains an actual Image or Gadget structure. These objects are instances of the Transparent Base Classes, imageclass and gadgetclass.
To change an attribute of a BOOPSI object, you normally invoke the set method, OM_SET. The Intuition functions SetAttrs() and SetGadgetAttrs() invoke this method. A BOOPSI class is informed of any attribute change at that time, allowing it to react to this change. The reaction can include validating the changed attribute, changing other attributes to match, or informing other objects of the change. That is the inherent advantage of using function calls to change attributes.
When using conventional images and gadgets, you generally modify the structure's fields directly. This operation is very fast. For conventional images and gadgets, there is no class that needs to know about the changes, so there is no problem. However, this is untrue of BOOPSI images and gadgets. Although directly modifying the BOOPSI object's internal structure would provide a performance increase over using the BOOPSI OM_SET mechanism, altering a BOOPSI object's internal structure directly will not give the class the opportunity to react to any structure changes. This violates the BOOPSI concept, and therefore cannot be done in general.
In order to provide a balance between the flexibility of function-access and the performance of direct-access, the transparent base classes imageclass and gadgetclass do not depend on being informed of changes to certain fields in the internal Image and Gadget structures. This means that it is OK for the dispatchers of direct subclasses of imageclass and gadgetclass to modify specific fields of BOOPSI images or gadgets. Applications and indirect subclass dispatchers of imageclass or gadgetclass may not modify those fields, since their parent classes may depend on hearing about changes to these fields, which the SetAttrs() call (or a similar function) provides.
For dispatchers of direct subclasses of imageclass, the following are the only fields of the Image structure that are alterable by application programs:
LeftEdge | Width | ImageData |
TopEdge | Height | PlanePick |
PlaneOnOff |
For dispatchers of direct subclasses of gadgetclass, the following are the only fields of the Gadget structure that are alterable by application programs:
LeftEdge | Flags | GadgetText |
TopEdge | GadgetType | SpecialInfo |
Width | GadgetRender | Activation |
Height | SelectRender |
Under no circumstances may an application or an indirect subclass modify one of these fields, even if the subclass knows the superclasses do not depend on notification for this field. This is the only way to preserve the possibility for future enhancements to that superclass. Note that these fields are not alterable while the gadget or image object is in use (for example, when it is attached to a window).
BOOPSI Images
See BOOPSI Images for more information about BOOPSI Images.
BOOPSI Gadgets
See BOOPSI Gadgets for more information about BOOPSI Gadgets.
Function Reference
The following are brief descriptions of functions discussed in this section. See the SDK for details on each function call.
Function | Description |
---|---|
NewObject() | Create a new BOOPSI object. |
DisposeObject() | Dispose of a BOOPSI object. |
SetAttrs() | Set one or more of a BOOPSI object's attributes. |
SetGadgetAttrs() | Set one or more of a BOOPSI object's attributes. |
RefreshSetGadgetAttrs() | Set one or more of a BOOPSI object's attributes and refresh imagery if required. |
GetAttr() | Obtain an attribute from a BOOPSI object. |
MakeClass() | Create a new private or public BOOPSI class. |
FreeClass() | Free a BOOPSI class created by MakeClass(). |
AddClass() | Add a public BOOPSI class to Intuition's internal list of public classes. |
RemoveClass() | Remove a public BOOPSI class that was added to Intuition's internal list with AddClass(). |
ObtainGIRPort() | Set up a RastPort for use by a BOOPSI gadget dispatcher. |
ReleaseGIRPort() | Free a RastPort set up by ReleaseGIRPort(). |
LockScreenGI() | Do an Intuition friendly LockLayers(). |
UnlockScreenGI() | Unlock a screen locked by LockScreenGI(). |
IDoMethod() | Send a BOOPSI message to a BOOPSI object. |
IDoGadgetMethod() | Send a BOOPSI message to a BOOPSI object with GadgetInfo context. |
IDoSuperMethod() | Send a BOOPSI message to a BOOPSI object as if the object was an instance of its class's superclass. |
ICoerceMethod() | Send a BOOPSI message to a BOOPSI object as if the object was an instance of the specified class. |
ISetSuperAttrs() | Send a BOOPSI OM_SET message to the BOOPSI object's superclass. |
DoRender() | Send a BOOPSI GM_RENDER message to the BOOPSI object. For class implementers only. |