Copyright (c) Hyperion Entertainment and contributors.
BOOPSI - Object Oriented Intuition
This page is currently being updated to AmigaOS 4.x. Some of the information contained here may not yet be applicable in part or totally. |
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 chapter, 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" chapter of this manual discusses tags. Utility Library also discusses callback Hooks, which are important to the later sections of this chapter.
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".
Figure 12-1 Object Diagram
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.
epsffigureFig12-2Simple BOOPSI Diagram
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.
At present there is a hierarchy of 14 public classes built into Intuition:
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.
Note that these tag lists can also contain utility.library Global System control tags (like TAG_SKIP and TAG_DONE), 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 and utility.library, see Utility Library.
Creating an Object
The Intuition function NewObjectA() creates a BOOPSI object:
APTR mynewobject = NewObjectA(Class *privclass, UBYTE *pubclass, struct TagItem *myattrs);
The pointer that NewObjectA() returns is a pointer to a BOOPSI object. 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 a set of attributes associated with them, so each attribute has a tag name. For BOOPSI gadgets and images, the attributes include some of the values from the old Gadget and Image structures (position, size, etc.).
Most applications use the stack-based version of NewObjectA(), NewObject(), to create objects. This allows an application to build the tag list of object attributes on the stack rather than having to allocate and initialize a tag list. A code sample from a program that creates a BOOPSI string gadget might look like this:
mystringgadget = (struct Gadget *)NewObject(NULL, "strgclass", 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 "strgclass" is one of the public classes built into Intuition. 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:
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.
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 |
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 contains 14 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. Currently, there are four subclasses: frameiclass, sysiclass, fillrectclass, and itexticlass.
- frameiclass
- An embossed or recessed rectangular frame image, that renders itself using the proper DrawInfo pens. This class is intelligent enough to bound or center its contents.
- sysiclass
- The class of system images. The class includes the images for the system and GadTools gadgets.
- fillrectclass
- A class of rectangle images that have frame and patternfill support.
- itextclass
- A specialized image class used for rendering text.
For more information on these classes 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. Currently, gadgetclass has four subclasses:
- propgclass
- An easy to implement, horizontal or vertical proportional gadget.
- strgclass
- A string gadget.
- groupclass
- A special gadget class that creates one composite gadget out of several others.
- buttongclass
- A button gadget that keeps sending button presses while the user holds it down.
buttongclass has a subclass of its own:
- frbuttonclass
- A buttongclass gadget that outlines its imagery with a frame.
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, } };
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.
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 - Execute me to compile me with SAS/C 5.10b LC -b1 -cfistq -v -y -j73 Talk2boopsi.c Blink FROM LIB:c.o,Talk2boopsi.o TO Talk2boopsi LIBRARY LIB:LC.lib,LIB:Amiga.lib quit ;*/ /* 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 <clib/exec_protos.h> #include <clib/intuition_protos.h> #ifdef LATTICE /* Disable SAS/C CTRL/C handling */ int CXBRK(void) { return(0); } int chkabort(void) { return(0); } #endif UBYTE *vers = "\0$VER: Talk2boopsi 37.1"; struct Library *IntuitionBase; 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 void main(void) { BOOL done = FALSE; if (IntuitionBase = OpenLibrary("intuition.library", 37L)) { /* Open the window--notice that the window's IDCMP port */ /* does not listen for GADGETUP messages. */ if (w = 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 *)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 *)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. */ { 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. */ AddGList(w, prop, -1, -1, NULL); /* Add the gadgets to the */ RefreshGList(prop, w, NULL, -1); /* window and display them. */ while (done == FALSE) /* Wait for the user to click */ { /* the window close gadget. */ WaitPort((struct MsgPort *)w->UserPort); while (msg = (struct IntuiMessage *) GetMsg((struct MsgPort *)w->UserPort)) { if (msg->Class == IDCMP_CLOSEWINDOW) done = TRUE; ReplyMsg(msg); } } RemoveGList(w, prop, -1); DisposeObject(integer); } DisposeObject(prop); } CloseWindow(w); } CloseLibrary(IntuitionBase); } }
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 DoMethodA(), or its stack-based equivalent, DoMethod():
ULONG DoMethodA(Object *myobject, Msg boopsimessage); ULONG DoMethod(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 DoMethodA(), 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;
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.
DoMethod() uses the stack to build a message. To use DoMethod(), just pass the elements of the method's message structure as arguments to DoMethod() 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:
DoMethod(myobject, OM_ADDMEMBER, addobject);
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 DoMethod() 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:
Figure 12-4 ICC Diagram
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:
SetGadgetAttrs(myintegergadget, mywindow, NULL, STRINGA_LongVal, 75L, GA_ID, 2L, 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, 75L}, {GA_ID, 2L}, {TAG_END,}
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 (75L) 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:
ULONG 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 { ULONG 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 DoSuperMethodA():
ULONG DoSuperMethodA(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 DoSuperMethodA(). DoSuperMethodA() 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 DoSuperMethodA() 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 DoSuperMethodA(). 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's 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 DoSuperMethodA(), DoSuperMethod(), 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; DoSuperMethod(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 DoSuperMethod() 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 DoSuperMethodA() (inheritance at work!). As an alternative, the dispatcher can use the amiga.lib function SetSuperAttrs(). See the amiga.lib Autodocs for more details on this function.
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(UBYTE *newclassID, UBYTE *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 = HookEntry; /* <--- HookEntry() is defined in amiga.lib */ myclass->cl_Dispatcher.h_SubEntry = dispatchRKMModel;
The h_Entry field points to a function that copies the function arguments to where the dispatcher expects them. 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. LC -cfist -b1 -y -v -j73 rkmmodel.c quit ;*/ #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 <clib/intuition_protos.h> #include <clib/utility_protos.h> #include <clib/alib_protos.h> #include <clib/alib_stdio_protos.h> extern struct Library *IntuitionBase, *UtilityBase; /*************************************************************************************************/ /**************** 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 { ULONG currval; /* The instance data for this class. */ ULONG vallimit; }; /*************************************************************************************************/ /************************** The functions in this module ********************************/ /*************************************************************************************************/ void geta4(void); /***************/ Class *initRKMModClass(void); /***************/ BOOL freeRKMModClass(Class *); /***************/ ULONG 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; extern ULONG HookEntry(); /* <------- defined in amiga.lib. */ if ( cl = MakeClass( NULL, "modelclass", NULL, sizeof ( struct RKMModData ), 0 )) { cl->cl_Dispatcher.h_Entry = HookEntry; /* initialize the */ cl->cl_Dispatcher.h_SubEntry = dispatchRKMModel; /* cl_Dispatcher */ /* Hook. */ } return ( cl ); } /*************************************************************************************************/ /********************************* Free the class ***************************************/ /*************************************************************************************************/ BOOL freeRKMModClass( Class *cl ) { return (FreeClass(cl)); } /*************************************************************************************************/ /******************************** The class Dispatcher ***********************************/ /*************************************************************************************************/ ULONG 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. */ geta4(); /* SAS/C and Manx function - makes sure A4 contains global data pointer. */ 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)DoSuperMethodA(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.) DoSuperMethodA() 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 = GetTagData(RKMMOD_CurrVal, 0L, /* initialize object's attributes. */ ((struct opSet *)msg)->ops_AttrList); mmd->vallimit = GetTagData(RKMMOD_Limit, DEFAULTVALLIMIT, ((struct opSet *)msg)->ops_AttrList); } break; case OM_SET: case OM_UPDATE: mmd = INST_DATA(cl, o); DoSuperMethodA(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 = 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)DoSuperMethodA(cl, o, msg); break; default: /* rkmmodelclass does not recognize the methodID, so let the superclass's */ /* dispatcher take a look at it. */ retval = (APTR)DoSuperMethodA(cl, o, msg); break; } return((ULONG)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_DONE; /* If the RKMMOD_CurrVal changes, we want everyone to know about */ DoSuperMethod(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:
Figure 12-5 Rkmmodelclass Object Diagram
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 Gadgets
One of the major enhancements to Intuition is the implementation of customizable BOOPSI gadgets. BOOPSI gadgets are not limited by dependencies upon Intuition Image and Gadget structures. Unlike Release 1.3 gadgets, which were handled exclusively by Intuition, BOOPSI gadgets handle their own rendering and their own user input.
Since BOOPSI gadgets draw themselves, there is almost no restriction on what they can look like. A BOOPSI gadget can use graphics.library RastPort drawing functions to draw vector-based imagery which the gadget can scale to any dimension. Instead of just a two-state Boolean gadget, a BOOPSI gadget can have any number of states, each of which has its own imagery. If a programmer wanted to he could even make a BOOPSI gadget that uses the animation system to render itself.
Because BOOPSI gadgets handle their own input, they see all the user's input, which the gadget is free to interpret. While the user has a BOOPSI gadget selected, the gadget can track mouse moves, process mouse and keyboard key presses, or watch the timer events.
The power of a BOOPSI gadget is not limited to its ability to handle its own rendering and user input. BOOPSI gadgets are also BOOPSI objects so the gain all the benefits BOOPSI provides. This means all BOOPSI gadgets inherit the methods and attributes from their superclasses. BOOPSI gadgets can use BOOPSI images to take care of rendering their imagery. A BOOPSI gadget could be a "composite" gadget that is composed of several BOOPSI gadgets, images, and models.
The BOOPSI Gadget Methods
Intuition drives a BOOPSI gadget by sending it BOOPSI messages. Intuition uses a series of five BOOPSI methods:
- GM_RENDER
- This method tells the gadget to render itself.
- GM_HITTEST
- This method asks a gadget whether it has been "hit" by a mouse click.
- GM_GOACTIVE
- This method asks a gadget if it wants to be the active gadget.
- GM_HANDLEINPUT
- This method passes a gadget an input event.
- GM_GOINACTIVE
- This method tells a gadget that it is no longer active.
The formats of each of these BOOPSI messages differ, but they all have two things in common. Like all BOOPSI messages, each starts with their respective method ID. For each of these methods, the method ID field is followed by a pointer to a GadgetInfo structure (defined in <intuition/cghooks.h>). The GadgetInfo structure contains information about the display on which the gadget needs to render itself:
struct GadgetInfo { struct Screen *gi_Screen; struct Window *gi_Window; /* null for screen gadgets */ struct Requester *gi_Requester; /* null if not GTYP_REQGADGET */ /* rendering information: don't use these without cloning/locking. * Official way is to call ObtainGIRPort() */ struct RastPort *gi_RastPort; struct Layer *gi_Layer; /* copy of dimensions of screen/window/g00/req(/group) * that gadget resides in. Left/Top of this box is * offset from window mouse coordinates to gadget coordinates * screen gadgets: 0,0 (from screen coords) * window gadgets (no g00): 0,0 * GTYP_GZZGADGETs (borderlayer): 0,0 * GZZ innerlayer gadget: borderleft, bordertop * Requester gadgets: reqleft, reqtop */ struct IBox gi_Domain; /* these are the pens for the window or screen */ struct { UBYTE DetailPen; UBYTE BlockPen; } gi_Pens; /* the Detail and Block pens in gi_DrInfo->dri_Pens[] are * for the screen. Use the above for window-sensitive colors. */ struct DrawInfo *gi_DrInfo; /* reserved space: this structure is extensible * anyway, but using these saves some recompilation */ ULONG gi_Reserved[6]; };
All the fields in this structure are read only.
Although this structure contains a pointer to the gadget's RastPort structure, applications should not use it for rendering. Instead, use the intuition.library function ObtainGIRPort() to obtain a copy of the GadgetInfo's RastPort. When the gadget is finished with this RastPort, it should call ReleaseGIRPort() to relinquish the RastPort.
GM_RENDER
Every time Intuition feels it is necessary to redraw a BOOPSI gadget, it sends a gadget a GM_RENDER message. The GM_RENDER message (defined in <intuition/gadgetclass.h>) tells a gadget to render itself:
struct gpRender { ULONG MethodID; /* GM_RENDER */ struct GadgetInfo *gpr_GInfo; struct RastPort *gpr_RPort; /* all ready for use */ LONG gpr_Redraw; /* might be a "highlight pass" */ };
Some events that cause Intuition to send a GM_RENDER are: an application passed the gadget to OpenWindow(), the user moved or resized a gadget's window, or an application explicitly asked Intuition to refresh some gadgets.
The GM_RENDER message contains a pointer to the gadget's RastPort so the GM_RENDER method does not have to extract it from the gpr_GInfo GadgetInfo structure using ObtainGIRPort()). The gadget renders itself according to how much imagery it needs to replace. The gpr_Redraw field contains one of three values:
Redraw the entire gadget.
The user has manipulated the gadget, causing a change to its imagery. Update only that part of the gadget's imagery that is effected by the user manipulating the gadget (for example, the knob and scrolling field of the prop gadget).
If this gadget supports it, toggle to or from the highlighting imagery.
Intuition is not the only entity that calls this method. The gadget's other methods may call this method to render the gadget when it goes through state changes. For example, as a prop gadget is following the mouse from the gadget's GM_HANDLEINPUT method, the gadget could send itself GM_RENDER messages, telling itself to update its imagery according to where the mouse has moved.
GM_HITTEST
When Intuition gets a left mouse button click in a window, one of the things it does is check through the window's list of gadgets to see if that click was inside the bounds of a gadget's Gadget structure (using the LeftEdge, TopEdge, Width, and Height fields). If it was (and that gadget is a BOOPSI gadget), Intuition sends that gadget a GM_HITTEST message (defined in <intuition/gadgetclass.h>):
struct gpHitTest { ULONG MethodID; /* GM_HITTEST */ struct GadgetInfo *gpht_GInfo; struct { WORD X; /* Is this point inside of the gadget? */ WORD Y; } gpht_Mouse; };
This message contains the coordinates of the mouse click. These coordinates are relative to the upper-left of the gadget (LeftEdge, TopEdge).
Because Intuition can only tell if the user clicked inside gadget's "bounding box", Intuition only knows that the click was close to the gadget. Intuition uses the GM_HITTEST to ask the gadget if the click was really inside the gadget. The gadget returns GMR_GADGETHIT (defined in <intuition/gadgetclass.h>) to tell Intuition that the user hit it, otherwise it returns zero. This method allows a gadget to be any shape or pattern, rather than just rectangular.
GM_GOACTIVE/GM_HANDLEINPUT
If a gadget returns GMR_GADGETHIT, Intuition will send it a GM_GOACTIVE message (defined in <intuition/gadgetclass.h>):
struct gpInput /* Used by GM_GOACTIVE and GM_HANDLEINPUT */ { ULONG MethodID; struct GadgetInfo *gpi_GInfo; struct InputEvent *gpi_IEvent; /* The input event that triggered this method * (for GM_GOACTIVE, this can be NULL) */ LONG *gpi_Termination; /* For GADGETUP IntuiMessage.Code */ struct { WORD X; /* Mouse position relative to upper */ WORD Y; /* left corner of gadget (LeftEdge, TopEdge) */ } gpi_Mouse; };
The GM_GOACTIVE message gives a gadget the opportunity to become the active gadget. The active gadget is the gadget that is currently receiving user input. Under normal conditions, only one gadget can be the active gadget (it is possible to have more than one active gadget using a groupgclass object (See the BOOPSI Class Reference for more details).
While a gadget is active, Intuition sends it GM_HANDLEINPUT messages. Each GM_HANDLEINPUT message corresponds to a single InputEvent structure. These InputEvents can be keyboard presses, timer events, mouse moves, or mouse button presses. The message's gpi_IEvent field points to this InputEvent structure. It's up to the GM_HANDLEINPUT method to interpret the meaning of these events and update the visual state of the gadget as the user manipulates the gadget. For example, the GM_HANDLEINPUT method of a prop gadget has to track mouse events to see where the user has moved the prop gadget's knob and update the gadget's imagery to reflect the new position of the knob.
For the GM_GOACTIVE method, the gpi_IEvent field points to the struct InputEvent that triggered the GM_GOACTIVE message. Unlike the GM_HANDLEINPUT message, GM_GOACTIVE's gpi_IEvent can be NULL. If the GM_GOACTIVE message was triggered by a function like intuition.library's ActivateGadget() and not by a real InputEvent (like the user clicking the gadget), the gpi_IEvent field will be NULL.
For gadgets that only want to become active as a direct result of a mouse click, this difference is important. For example, the prop gadget becomes active only when the user clicks on its knob. Because the only way the user can control the prop gadget is via the mouse, it does not make sense for anything but the mouse to activate the gadget. On the other hand, a string gadget doesn't care how it is activated because, as soon as it’s active, it gets user input from the keyboard rather than the mouse. Not all gadgets can become active. Some gadgets cannot become active because they have been temporarily disabled (their Gadget.Flags GFLG_DISABLED bit is set). Other gadgets will not become active because they don't need to process input. For example, a toggle gadget won't become active because it only needs to process one input event, the mouse click that toggles the gadget (which it gets from the GM_GOACTIVE message). If a toggle gadget gets a GM_GOACTIVE message and its gpi_IEvent field is not NULL, it will toggle its state and refuse to "go active".
The GM_GOACTIVE method has to take care of any visual state changes to a gadget that a GM_GOACTIVE message might trigger. For example, the toggle gadget in the previous paragraph has to take care of toggling its visual state from selected imagery to unselected imagery. If the gadget goes through a state change when it becomes the active gadget, (like when a string gadget positions its cursor) GM_GOACTIVE has to take care of this.
The return values of both GM_GOACTIVE and GM_HANDLEINPUT tell Intuition whether or not the gadget wants to be active. A gadget's GM_GOACTIVE method returns GMR_MEACTIVE (defined in <intuition/gadgetclass.h>) if it wants to become the active gadget. A gadget's GM_HANDLEINPUT method returns GMR_MEACTIVE if it wants to remain the active gadget. If a gadget either does not want to become or remain the active gadget, it returns one of the "go inactive" return values:
- GMR_NOREUSE
- Tells Intuition to throw away the gpInput.gpi_IEvent InputEvent.
- GMR_REUSE
- Tells Intuition to process the gpInput.gpi_IEvent InputEvent.
- GMR_NEXTACTIVE
- Tells Intuition to throw away the gpInput.gpi_IEvent InputEvent and activate the next GFLG_TagCycle gadget.
- GMR_PREVACTIVE
- Tells Intuition to throw away the gpInput.gpi_IEvent InputEvent and activate the previous GFLG_TagCycle gadget.
GMR_NOREUSE tells Intuition that the gadget does not want to be active and to throw away the InputEvent that triggered the message. For example, an active prop gadget returns GMR_NOREUSE when the user lets go of the left mouse button (thus letting go of the prop gadget's knob).
For the GM_HANDLEINPUT method, a gadget can also return GMR_REUSE, which tells Intuition to reuse the InputEvent. For example, if the user clicks outside the active string gadget, that string gadget returns GMR_REUSE. Intuition can now process that mouse click, which can be over another gadget. Another case where a string gadget returns GMR_REUSE is when the user pushes the right mouse button (the menu button). The string gadget becomes inactive and the menu button InputEvent gets reused. Intuition sees this event and tries to pop up the menu bar.
For the GM_GOACTIVE method, a gadget must not return GMR_REUSE. If a gadget gets a GM_GOACTIVE message from Intuition and the message has an gpi_IEvent, the message was triggered by the user clicking on the gadget. In this case, Intuition knows that the user is trying to select the gadget. Intuition doesn't know if the gadget can be activated, but if it can be activated, the event that triggered the activation has just taken place. If the gadget cannot become active for any reason, it must not let Intuition reuse that InputEvent as the gadget has already taken care of the the event's purpose (clicking on the gadget). In essence, the user tried to activate the gadget and the gadget refused to become active.
The other two possible return values are GMR_NEXTACTIVE and GMR_PREVACTIVE. These tell Intuition that a gadget does not want to be active and that the InputEvent should be discarded. Intuition then looks for the next (GMR_NEXTACTIVE) or previous (GMR_PREVACTIVE) gadget that has its GFLG_TABCYCLE flag set in its Gadget.Activation field (see the gadgetclass GA_TabCycle attribute in the BOOPSI Class Reference).
For both GM_GOACTIVE and GM_HANDLEINPUT, the gadget can bitwise-OR any of these "go inactive" return values with GMR_VERIFY. The GMR_VERIFY flag tells Intuition to send a GADGETUP IntuiMessage to the gadget's window. If the gadget uses GMR_VERIFY, it has to supply a value for the IntuiMessage.Code field. It does this by passing a value in the gpInput.gpi_Termination field. This field points to a long word, the lower 16-bits of which Intuition copies into the Code field. The upper 16-bits are for future enhancements, so clear these bits.
GM_GOINACTIVE
After an active gadget deactivates, Intuition sends it a GM_GOINACTIVE message (defined in <intuition/gadgetclass.h>):
struct gpGoInactive { ULONG MethodID; /* GM_GOINACTIVE */ struct GadgetInfo *gpgi_GInfo; /* V37 field only! DO NOT attempt to read under V36! */ ULONG gpgi_Abort; /* gpgi_Abort=1 if gadget was aborted by Intuition */ /* and 0 if gadget went inactive at its own request. */ };
The gpgi_Abort field contains either a 0 or 1. If 0, the gadget became inactive on its own power (because the GM_GOACTIVE or GM_HANDLEINPUT method returned something besides GMR_MEACTIVE). If gpgi_Abort is 1, Intuition aborted this active gadget. Some instances where Intuition aborts a gadget include: the user clicked in another window or screen, an application removed the active gadget with RemoveGList(), and an application called ActiveWindow() on a window other than the gadget's window.
The Active Gadget
While a gadget is active, Intuition sends it a GM_HANDLEINPUT message for every timer pulse, mouse move, mouse click, and key press that takes place. A timer event pulse arrives about every tenth of a second. Mouse move events can arrive at a much higher rate than the timer pulses. Without even considering the keyboard, a gadget can get a lot of GM_HANDLEINPUT messages in a short amount of time. Because the active gadget has to handle a large volume of GM_HANDLEINPUT messages, the overhead of this method should be kept to a minimum.
Because the gadget will always receive a GM_GOACTIVE message before it is active and a GM_GOINACTIVE message after it is no longer active, the gadget can use these methods to allocate, initialize, and deallocate temporary resources it needs for the GM_HANDLEINPUT method. This can significantly reduce the overhead of GM_HANDLEINPUT because it eliminates the need to allocate, initialize, and deallocate resources for every GM_HANDLEINPUT message.
Note that the RastPort from ObtainGIRPort() is not cachable using this method. If the GM_HANDLEINPUT method needs to use a RastPort, it has to obtain and release the RastPort for every GM_HANDLEINPUT message using ObtainGIRPort() and ReleaseGIRPort().
RKMButtonclass.c
The following example is a sample BOOPSI gadget, RKMButClass.c. While the user has the RKMButton selected, the gadget sends an OM_UPDATE message to its ICA_TARGET for every timer event the button sees. The gadget sends notification about its RKMBUT_Pulse attribute, which is the horizontal distance in screen pixels the mouse is from the center of the button. The gadget takes care of rendering all of its imagery (as opposed to using a BOOPSI image to do it). The gadget's imagery is scalable to any dimensions and can be set (using SetGadgetAttrs()) while the gadget is in place.
One possible use for such a gadget is as buttons for a prop gadget. If the user has the prop gadget's RKMButton selected, while the mouse is to the left of the button's center, the knob on the prop gadget moves left. While the mouse is to the right of the button's center, the knob on the prop gadget moves right. The speed at which the knob moves is proportional to the horizontal distance from the mouse to the active RKMButton.
;/* RKMButClass.c - Example BOOPSI gadget ; Execute me to compile me with Lattice 5.10b LC -b1 -d0 -cfistq -v -y -j73 RKMButClass.c Blink FROM LIB:c.o,RKMButClass.o TO TestBut LIBRARY LIB:LC.lib,LIB:Amiga.lib quit */ #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 <clib/exec_protos.h> #include <clib/intuition_protos.h> #include <clib/graphics_protos.h> #include <clib/utility_protos.h> #include <clib/alib_protos.h> #include <clib/alib_stdio_protos.h> #include <graphics/gfxmacros.h> #ifdef LATTICE int CXBRK(void) { return(0); } /* Disable Lattice CTRL/C handling */ int chkabort(void) { return(0); } #endif UBYTE *vers = "\0$VER: TestBut 37.1"; /***********************************************************/ /**************** Class specifics ****************/ /***********************************************************/ #define RKMBUT_Pulse (TAG_USER + 1) struct ButINST { LONG midX, midY; /* Coordinates of middle of gadget */ }; /* ButINST has one flag: */ #define ERASE_ONLY 0x00000001 /* Tells rendering routine to */ /* only erase the gadget, not */ /* rerender a new one. This */ /* lets the gadget erase it- */ /* self before it rescales. */ /* The functions in this module */ Class *initRKMButGadClass(void); BOOL freeRKMButGadClass(Class *); ULONG dispatchRKMButGad(Class *, Object *, Msg); void NotifyPulse(Class *, Object *, ULONG, LONG, struct gpInput *); ULONG RenderRKMBut(Class *, struct Gadget *, struct gpRender *); void geta4(void); void MainLoop(ULONG, ULONG); /*************************************************************************************************/ /* The main() function connects an RKMButClass object to a BOOPSI integer gadget, which displays */ /* the RKMButClass gadget's RKMBUT_Pulse value. The code scales and move the gadget while it is */ /* in place. */ /*************************************************************************************************/ struct TagItem pulse2int[] = { {RKMBUT_Pulse, STRINGA_LongVal}, {TAG_END,} }; #define INTWIDTH 40 #define INTHEIGHT 20 struct Library *IntuitionBase, *UtilityBase, *GfxBase; struct Window *w; Class *rkmbutcl; struct Gadget *integer, *but; struct IntuiMessage *msg; void main(void) { if (IntuitionBase = OpenLibrary("intuition.library", 37L)) { if (UtilityBase = OpenLibrary("utility.library", 37L)) { if (GfxBase = OpenLibrary("graphics.library", 37L)) { if (w = OpenWindowTags(NULL, WA_Flags, WFLG_DEPTHGADGET | WFLG_DRAGBAR | WFLG_CLOSEGADGET | WFLG_SIZEGADGET, WA_IDCMP, IDCMP_CLOSEWINDOW, WA_Width, 640, WA_Height, 200, TAG_END)) { WindowLimits(w, 450, 200, 640, 200); if (rkmbutcl = initRKMButGadClass()) { if (integer = (struct Gadget *)NewObject(NULL, "strgclass", GA_ID, 1L, GA_Top, (w->BorderTop) + 5L, GA_Left, (w->BorderLeft) + 5L, GA_Width, INTWIDTH, GA_Height, INTHEIGHT, STRINGA_LongVal, 0L, STRINGA_MaxChars, 5L, TAG_END)) { if (but = (struct Gadget *)NewObject(rkmbutcl, NULL, GA_ID, 2L, GA_Top, (w->BorderTop) + 5L, GA_Left, integer->LeftEdge + integer->Width + 5L, GA_Width, 40L, GA_Height, INTHEIGHT, GA_Previous, integer, ICA_MAP, pulse2int, ICA_TARGET, integer, TAG_END)) { AddGList(w, integer, -1, -1, NULL); RefreshGList(integer, w, NULL, -1); SetWindowTitles(w, "<-- Click to resize gadget Height", NULL); MainLoop(TAG_DONE, 0L); SetWindowTitles(w, "<-- Click to resize gadget Width", NULL); MainLoop(GA_Height, 100L); SetWindowTitles(w, "<-- Click to resize gadget Y position", NULL); MainLoop(GA_Width, 100L); SetWindowTitles(w, "<-- Click to resize gadget X position", NULL); MainLoop(GA_Top, but->TopEdge + 20); SetWindowTitles(w, "<-- Click to quit", NULL); MainLoop(GA_Left, but->LeftEdge + 20); RemoveGList(w, integer, -1); DisposeObject(but); } DisposeObject(integer); } freeRKMButGadClass(rkmbutcl); } CloseWindow(w); } CloseLibrary(GfxBase); } CloseLibrary(UtilityBase); } CloseLibrary(IntuitionBase); } } void MainLoop(ULONG attr, ULONG value) { ULONG done = FALSE; SetGadgetAttrs(but, w, NULL, attr, value, TAG_DONE); while (done == FALSE) { WaitPort((struct MsgPort *)w->UserPort); while (msg = (struct IntuiMessage *) GetMsg((struct MsgPort *)w->UserPort)) { if (msg->Class == IDCMP_CLOSEWINDOW) { done = TRUE; } ReplyMsg(msg); } } } /***********************************************************/ /** Make the class and set up the dispatcher's hook **/ /***********************************************************/ Class *initRKMButGadClass(void) { Class *cl = NULL; extern ULONG HookEntry(); /* defined in amiga.lib */ if ( cl = MakeClass( NULL, "gadgetclass", NULL, sizeof ( struct ButINST ), 0 )) { /* initialize the cl_Dispatcher Hook */ cl->cl_Dispatcher.h_Entry = HookEntry; cl->cl_Dispatcher.h_SubEntry = dispatchRKMButGad; } return ( cl ); } /***********************************************************/ /****************** Free the class ****************/ /***********************************************************/ BOOL freeRKMButGadClass( Class *cl ) { return (FreeClass(cl)); } /***********************************************************/ /********** The RKMBut class dispatcher *********/ /***********************************************************/ ULONG dispatchRKMButGad(Class *cl, Object *o, Msg msg) { struct ButINST *inst; ULONG retval = FALSE; Object *object; /* SAS/C and Manx function to make sure register A4 contains a pointer to global data */ geta4(); switch (msg->MethodID) { case OM_NEW: /* First, pass up to superclass */ if (object = (Object *)DoSuperMethodA(cl, o, msg)) { struct Gadget *g = (struct Gadget *)object; /* Initial local instance data */ inst = INST_DATA(cl, object); inst->midX = g->LeftEdge + ( (g->Width) / 2); inst->midY = g->TopEdge + ( (g->Height) / 2); retval = (ULONG)object; } break; case GM_HITTEST: /* Since this is a rectangular gadget this */ /* method always returns GMR_GADGETHIT. */ retval = GMR_GADGETHIT; break; case GM_GOACTIVE: inst = INST_DATA(cl, o); /* Only become active if the GM_GOACTIVE */ /* was triggered by direct user input. */ if (((struct gpInput *)msg)->gpi_IEvent) { /* This gadget is now active, change */ /* visual state to selected and render. */ ((struct Gadget *)o)->Flags |= GFLG_SELECTED; RenderRKMBut(cl, (struct Gadget *)o, (struct gpRender *)msg); retval = GMR_MEACTIVE; } else /* The GM_GOACTIVE was not */ /* triggered by direct user input. */ retval = GMR_NOREUSE; break; case GM_RENDER: retval = RenderRKMBut(cl, (struct Gadget *)o, (struct gpRender *)msg); break; case GM_HANDLEINPUT: /* While it is active, this gadget sends its superclass an */ /* OM_NOTIFY pulse for every IECLASS_TIMER event that goes by */ /* (about one every 10th of a second). Any object that is */ /* connected to this gadget will get A LOT of OM_UPDATE messages. */ { struct Gadget *g = (struct Gadget *)o; struct gpInput *gpi = (struct gpInput *)msg; struct InputEvent *ie = gpi->gpi_IEvent; inst = INST_DATA(cl, o); retval = GMR_MEACTIVE; if (ie->ie_Class == IECLASS_RAWMOUSE) { switch (ie->ie_Code) { case SELECTUP: /* The user let go of the gadget so return GMR_NOREUSE */ /* to deactivate and to tell Intuition not to reuse */ /* this Input Event as we have already processed it. */ /*If the user let go of the gadget while the mouse was */ /*over it, mask GMR_VERIFY into the return value so */ /*Intuition will send a Release Verify (GADGETUP). */ if ( ((gpi->gpi_Mouse).X < g->LeftEdge) || ((gpi->gpi_Mouse).X > g->LeftEdge + g->Width) || ((gpi->gpi_Mouse).Y < g->TopEdge) || ((gpi->gpi_Mouse).Y > g->TopEdge + g->Height) ) retval = GMR_NOREUSE | GMR_VERIFY; else retval = GMR_NOREUSE; /* Since the gadget is going inactive, send a final */ /* notification to the ICA_TARGET. */ NotifyPulse(cl , o, 0L, inst->midX, (struct gp_Input *)msg); break; case MENUDOWN: /* The user hit the menu button. Go inactive and let */ /* Intuition reuse the menu button event so Intuition can */ /* pop up the menu bar. */ retval = GMR_REUSE; /* Since the gadget is going inactive, send a final */ /* notification to the ICA_TARGET. */ NotifyPulse(cl , o, 0L, inst->midX, (struct gp_Input *)msg); break; default: retval = GMR_MEACTIVE; } } else if (ie->ie_Class == IECLASS_TIMER) /* If the gadget gets a timer event, it sends an interim OM_NOTIFY */ NotifyPulse(cl, o, OPUF_INTERIM, inst->midX, gpi); /* to its superclass. */ } break; case GM_GOINACTIVE: /* Intuition said to go inactive. Clear the GFLG_SELECTED */ /* bit and render using unselected imagery. */ ((struct Gadget *)o)->Flags &= ~GFLG_SELECTED; RenderRKMBut(cl, (struct Gadget *)o, (struct gpRender *)msg); break; case OM_SET:/* Although this class doesn't have settable attributes, this gadget class */ /* does have scaleable imagery, so it needs to find out when its size and/or */ /* position has changed so it can erase itself, THEN scale, and rerender. */ if ( FindTagItem(GA_Width, ((struct opSet *)msg)->ops_AttrList) || FindTagItem(GA_Height, ((struct opSet *)msg)->ops_AttrList) || FindTagItem(GA_Top, ((struct opSet *)msg)->ops_AttrList) || FindTagItem(GA_Left, ((struct opSet *)msg)->ops_AttrList) ) { struct RastPort *rp; struct Gadget *g = (struct Gadget *)o; WORD x,y,w,h; x = g->LeftEdge; y = g->TopEdge; w = g->Width; h = g->Height; inst = INST_DATA(cl, o); retval = DoSuperMethodA(cl, o, msg); /* Get pointer to RastPort for gadget. */ if (rp = ObtainGIRPort( ((struct opSet *)msg)->ops_GInfo) ) { UWORD *pens = ((struct opSet *)msg)->ops_GInfo->gi_DrInfo->dri_Pens; SetAPen(rp, pens[BACKGROUNDPEN]); SetDrMd(rp, JAM1); /* Erase the old gadget. */ RectFill(rp, x, y, x+w, y+h); inst->midX = g->LeftEdge + ( (g->Width) / 2); /* Recalculate where the */ inst->midY = g->TopEdge + ( (g->Height) / 2); /* center of the gadget is. */ /* Rerender the gadget. */ DoMethod(o, GM_RENDER, ((struct opSet *)msg)->ops_GInfo, rp, GREDRAW_REDRAW); ReleaseGIRPort(rp); } } else retval = DoSuperMethodA(cl, o, msg); break; default: /* rkmmodelclass does not recognize the methodID, let the superclass's */ /* dispatcher take a look at it. */ retval = DoSuperMethodA(cl, o, msg); break; } return(retval); } /*************************************************************************************************/ /************** Build an OM_NOTIFY message for RKMBUT_Pulse and send it to the superclass. *******/ /*************************************************************************************************/ void NotifyPulse(Class *cl, Object *o, ULONG flags, LONG mid, struct gpInput *gpi) { struct TagItem tt[3]; tt[0].ti_Tag = RKMBUT_Pulse; tt[0].ti_Data = mid - ((gpi->gpi_Mouse).X + ((struct Gadget *)o)->LeftEdge); tt[1].ti_Tag = GA_ID; tt[1].ti_Data = ((struct Gadget *)o)->GadgetID; tt[2].ti_Tag = TAG_DONE; DoSuperMethod(cl, o, OM_NOTIFY, tt, gpi->gpi_GInfo, flags); } /*************************************************************************************************/ /******************************* Erase and rerender the gadget. ******************************/ /*************************************************************************************************/ ULONG RenderRKMBut(Class *cl, struct Gadget *g, struct gpRender *msg) { struct ButINST *inst = INST_DATA(cl, (Object *)g); struct RastPort *rp; ULONG retval = TRUE; UWORD *pens = msg->gpr_GInfo->gi_DrInfo->dri_Pens; if (msg->MethodID == GM_RENDER) /* If msg is truly a GM_RENDER message (not a gpInput that */ /* looks like a gpRender), use the rastport within it... */ rp = msg->gpr_RPort; else /* ...Otherwise, get a rastport using ObtainGIRPort(). */ rp = ObtainGIRPort(msg->gpr_GInfo); if (rp) { UWORD back, shine, shadow, w, h, x, y; if (g->Flags & GFLG_SELECTED) /* If the gadget is selected, reverse the meanings of the */ { /* pens. */ back = pens[FILLPEN]; shine = pens[SHADOWPEN]; shadow = pens[SHINEPEN]; } else { back = pens[BACKGROUNDPEN]; shine = pens[SHINEPEN]; shadow = pens[SHADOWPEN]; } SetDrMd(rp, JAM1); SetAPen(rp, back); /* Erase the old gadget. */ RectFill(rp, g->LeftEdge, g->TopEdge, g->LeftEdge + g->Width, g->TopEdge + g->Height); SetAPen(rp, shadow); /* Draw shadow edge. */ Move(rp, g->LeftEdge + 1, g->TopEdge + g->Height); Draw(rp, g->LeftEdge + g->Width, g->TopEdge + g->Height); Draw(rp, g->LeftEdge + g->Width, g->TopEdge + 1); w = g->Width / 4; /* Draw Arrows - Sorry, no frills imagery */ h = g->Height / 2; x = g->LeftEdge + (w/2); y = g->TopEdge + (h/2); Move(rp, x, inst->midY); Draw(rp, x + w, y); Draw(rp, x + w, y + (g->Height) - h); Draw(rp, x, inst->midY); x = g->LeftEdge + (w/2) + g->Width / 2; Move(rp, x + w, inst->midY); Draw(rp, x, y); Draw(rp, x, y + (g->Height) - h); Draw(rp, x + w, inst->midY); SetAPen(rp, shine); /* Draw shine edge. */ Move(rp, g->LeftEdge, g->TopEdge + g->Height - 1); Draw(rp, g->LeftEdge, g->TopEdge); Draw(rp, g->LeftEdge + g->Width - 1, g->TopEdge); if (msg->MethodID != GM_RENDER) /* If we allocated a rastport, give it back. */ ReleaseGIRPort(rp); } else retval = FALSE; return(retval); }
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. |
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(). |
IDoMethod() | Send a BOOPSI message to a BOOPSI object. |
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. |