Aros/Developer/Docs/Libraries/Intuition/BOOPSI

History
BOOPSI (Basic Object Oriented Programming System for Intuition) is an object-oriented programming system for AmigaOS. It extends the AmigaOS windowing environment (Intuition) with an object-oriented subsystem allowing a hierarchy of object classes in which every class defines a single GUI widget or interface event.

BOOPSI made it easier for developers to create their own system of widgets and create standardized graphical user interfaces. Magic User Interface (MUI) and ReAction are examples of complete widget toolkits built on BOOPSI. Both toolkits have become popular with Amiga software programmers to generate and maintain graphical user interfaces.

The object-oriented design brings advantages such as straightforward coupling of objects with other objects. For example, a programmer may link a numerical input field and a sliding control, where if the user adjusts the sliding control the numerical value in the input field changes automatically.

BOOPSI was officially introduced with AmigaOS (TM) 2.0 and was further extended in later releases. Datatypes become easier with a little understanding of BOOPSI as well.

oop.library is the other AROS object oriented software base used to build the HIDDs. (graphics HIDD, PCI HIDD, mouse HIDD etc.) In the past Staf (overseeing the ABI changes) said there were some aspects he'd probably change there (he doesn't want to freeze it for the current ABIv1 work), so its probably best to get inputs from him regarding what those concerns were, if there's any sense in unifying things with any compiler/framework support.

Introduction
Basic Object Oriented Programming System for Intuition (BOOPSI) is a simple OS supported model for producing system wide and language independent classes for use by your programs. BOOPSI itself is made available through the intuition.library although there is no reason that it cannot be used for things other than GUIs. The system works by providing a way to make object oriented style classes available and then providing a set of functions to create objects and perform operations on these objects.

See Open Boopsi Project

Create the gadget object, and call AddGadget. Set up the position and size data in order to get it to be placed where it is needed. Calculate the new positions and size when the window is resized, etc. Intuition does have a new gadget method called GM_LAYOUT to tell a Boopsi gadget that its window's dimensions have changed. The GM_LAYOUT method uses the gpLayout structure. So you can add, remove and modify BOOPSI objects already added to a gadget object. By used the LAYOUT_AddChild, LAYOUT_AddImage, LAYOUT_RemoveChild and LAYOUT_ModifyChild tags.

Why Zune is recommended as the default gui option for AROS.

Classes
Classes in BOOPSI are extremely easy to create, maintain, and use. They are simply a function which acts as a dispatcher for each of a class's "methods." Essentially, a "message" pertaining to the method requested is sent to an object using the DoMethod function. The DoMethod function will then look at the object and determine the class that it belongs to and then passes the message onto that class's method dispatcher. The message itself is a simple structure of the form

typedef struct { ULONG MethodID; /* Method specific information follows */ } *Msg;

For example, the OM_SET message looks like this: struct opSet { ULONG MethodID; struct TagItem *ops_AttrList; struct GadgetInfo *ops_GInfo; };

Dispatcher
The dispatcher is really just a single function which is called with pointers to its Class structure, the object on which the method was called, and the message that was sent to the object. The dispatcher then looks inside the message to see what method was called on the object and then acts appropriately. Here's an example of a dispatcher:

ULONG dispatcher( Class *cl, Object *o, Msg msg ) {	ULONG retval = NULL; /* figure out what method was called */ switch( msg->MethodID ) {		/* these are just some of the standard methods defined by rootclass */ case OM_NEW: break; case OM_SET: break; case OM_GET: break; /* other methods that the class can handle are placed here... */		/* the method called isn't understood by this class, call our parent class with the message */ default: DoSuperMethodA( cl, o, msg ); break; }	return( retval ); }

Object
An object in BOOPSI is simply a chunk of allocated memory which is divided into parts owned by the class the object is an instance of and all of its ancestors. Each class knows where its data is through a field in its Class structure which is setup by the system, so in order to get access to its data the class looks in this field and then adds its value to the object pointer. Fortunately a macro is provided to perform this function for you.

struct localObjData { ULONG value1; ULONG value2; };

void dispatcher( Class *cl, Object *o, Msg msg ) {	struct localObjData *lod; ULONG retval;

switch( msg->MethodID ) {		case OM_NEW: break; ...		case SM_SOMEMETHOD: lod = INST_DATA(cl, o); /* get the pointer to our instance data */ lod->value2 = lod->value1; /* do something with it */ lod->value1 = SOME_NUMBER; break; ...		default: DoSuperMethodA(cl, o, msg); break; }	return( retval ); }

!!!!!!!!!!!!!!!ALERT!!!!!!!!!!!!!!!!! If you plan on making classes ALWAYS make sure your instance data pointer, lod here, is set otherwise you will encounter a ton of trouble. I can't tell you how many times I forgot to set it and it took me many minutes to finally figure out that that was the problem.

Classes are kept track of by the system so in order to add a new class to the system you must first use the MakeClass function to make a class structure that the system functions can use and understand. The MakeClass function simply takes some information such as the name of the class, some identifier for the parentclass, and the instance size. The dispatcher isn't passed into the function because it is added to the Class structure later. Then if you want to make the class publicly available the AddClass function must be used.

if( cl = MakeClass( "someclassnamehere", "parentclassnamehere", 0, /* this is used to point to a private Class structure */ sizeof( struct localObjData ), /* the size of the class */ 0 ) ) {	cl->cl_Dispatcher.h_Entry = dispatcher; AddClass( cl ); }

And when the class is not needed anymore it is removed by the RemoveClass and FreeClass functions. All of this work has been done in a simple Skeleton class which is implemented as a shared library that will automatically add itself to the system when it is loaded.

Objects in BOOPSI are simply allocated chunks of memory that are controlled by the class that the object is an instance of. To create a new object you use the NewObject call in intuition.library and pass the identifier for the class that the object is to be an instance of and any initial attribute settings.

struct Gadget *gad;

gad = NewObject( NULL, "buttongclass",	GA_Left, 0,	GA_Top, 0,	GA_Width, 10,	GA_Height, 10,	...	TAG_DONE );

Once the object has been created with NewObject you can perform a number of functions on it by calling one of its methods through DoMethod. For some system supported methods, such as OM_SET and OM_GET, functions are provided for you so that you don't need to use DoMethod.

ULONG data;

SetAttrs( someobject, GA_Left, 10, GA_Top, 10, TAG_DONE );

SetGadgetAttrs( somegadget, window, requester, GA_Left, 10, ..., TAG_DONE );

/* get some attribute from some object and put it in the data ULONG */ GetAttr( &data, someobject, SA_SomeAttribute );

DoMethod( someobject, SM_SOMEMETHOD, ... );

SetGadgetAttrs is provided for any subclasses of the gadgetclass so that some special information needed by the classes can be created. Although it is not always needed when setting the attributes for a gadget, any attributes that might change the graphics of the gadgets should be set with SetGadgetAttrs.

The DoMethod function is actually just a part of the ordinary link library and is usually simple to operate although it can cause problems. The stack based DoMethod is made so that each parameter you pass to it will be interpreted as a ULONG and converted to one if it isn't, so if you need to pass data which is smaller be sure to package it appropriately or just use the tag array DoMethod. For example, the IM_DRAW method used by the imageclass classes take an X and Y parameter which are words:

struct impDraw {	ULONG MethodID; struct RastPort *imp_RPort; struct {		WORD X;		WORD Y;	} imp_Offset; ULONG imp_State; struct DrawInfo *imp_DrInfo; struct {		WORD Width; WORD Height; } imp_Dimensions; };

/* BAD BAD BAD BAD BAD BAD */

DoMethod( imageobject, IM_DRAW, rp, x, y, state, dri );

/* end BAD */

/* GOOD GOOD GOOD */

struct impDraw msg;

msg.MethodID = IM_DRAW; msg.imp_RPort = rp; ... DoMethodA( imageobject, &msg );

/* also GOOD */

struct Offset { WORD X;	WORD Y; };

/* ... */

struct Offset off;

off.X = x; off.Y = y;

DoMethod( imageobject, IM_DRAW, rp, off, state, dri ); ^^^                 /* this is the key since it will put the two words on the stack as if it were just a ULONG */

/* end GOOD */

Notice that although the imp_Draw structure is bigger than the structure we were passing on the stack it didn't matter since only the IM_DRAWFRAME method pays attention to the extra fields so there was no need to do the extra work.

MinNode
Objects in BOOPSI have the special property that there is a structure at a negative offset in each object created. This structure is put in place by the rootclass and contains a pointer to the object's class and a MinNode structure which can be used to hold objects in lists.

To use the MinNode structure in each object the rootclass provides the OM_ADDTAIL, OM_REMOVE, OM_ADDMEMBER, and OM_REMMEMBER methods. The OM_ADDTAIL and OM_REMOVE methods are implemented by the root class to do their expected function so you can use them in programs or in classes. However, the OM_*MEMBER methods aren't implemented by the rootclass but are there to provide a model for subclasses to implement. An example of the member functions might be to add items to some list view object that you've allocated.

DoMethod( listview, OM_ADDMEMBER, sometextitem );

Since the objects have the property that the rootclass data is at a negative offset it makes it easy to create base classes and make their structures publicly available so there is no need to use the SetAttrs and GetAttr functions. This is used effectively in the gadgetclass base class so that BOOPSI objects which are gadgets can be easily used as older style gadgets in Intuition.

struct Gadget *gad;

gad = NewObject( NULL, "strgclass",		GA_Left, 0,		...		TAG_DONE );

AddGadget( win, gad, -1 ); RefreshGadgets( gad, win, 0 ); ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ /* Dont forget this!!! Its the number one problem ppl have */

Gadgets
Objects, that are instances of icclass or gadgetclass, also have the ability to talk to each other through their attributes. A target object is setup by the ICA_TARGET attribute and then whenever some attribute of class has that attribute changed it will send an OM_UPDATE message to the target object which can then act on it somehow. The target doesn't have to be an object though since you are able to set the ICA_TARGET value to be ICTARGET_IDCMP so that any OM_UPDATE messages sent from the object will be directed to the application through the IDCMP port. Unfortunately, when the target is an object there can be confusion about what an attribute identifier is supposed to mean. To fix this a special attribute called ICA_MAP is used to map an attribute ID to another one which the target object will be able to understand. For example, the propgclass uses the PGA_Top attribute for its current value while a strgclass uses the STRINGA_LongVal attribute for its current value. Both of these attributes have different identifiers so if they had each other as targets they wouldn't be able to understand what the attribute is supposed to mean.

Here is a the same example with communication with the IDCMP port.

Dispose
Finally, when your completely done with an object you need to dispose of it through the DisposeObject function. Its simple to use but you need to be careful of not disposing of something twice. The only real time when this might occur is if you've passed the object to some other object and it has disposed of it for you. A second possibility is if you've added your own system gadgets to a window (close, depth, etc...) then once you've closed the window the system will automatically dispose of any system gadgets in the window for you. You should also remember that the DisposeObject function is smart enough to know not to try and free a null pointer. This property allows you to make many objects that don't depend on each other without having to encase them in if's.

Note that if you do this you must take care not to use the object pointers in any of the other objects. For example, the GA_Previous attribute takes a pointer to a gadget and then modifies its NextGadget field to point to the object that is being created. If the gadget passed into this attribute doesn't exist than it will trash memory.

Make you own Class
When making your own classes I advise you to use the Skeleton class and then proceed from there since it already has a lot of the dirty work finished for you so you can get right to writing the guts of your class.

A Button Class
Figure the best way to go about this is to just take you through the code of a button class... Any bugs found while writing this are left as an exercise for the reader :)

Header File


 * 1) ifndef BBBUTTONCLASS_H
 * 2) define BBBUTTONCLASS_H


 * 1) define BGA_Dummy (TAG_USER + 0x60000)
 * 2) define BGA_Push (BGA_Dummy + 1)
 * 3) define BGA_Image (BGA_Dummy + 2)

//Convenient macro for creating buttons
 * 1) define ButtonObject NewObject( NULL, "bbbuttongadget"

Casting Macros I like to use casting macros a lot so here are some:
 * 1) endif
 * 1) define GA(o) ((struct Gadget *)o)
 * 2) define IA(o) ((struct Image *)o)
 * 3) define IM(o) ((struct Image *)o)
 * 4) define SET(o) ((struct opSet *)o)
 * 5) define GET(o) ((struct opGet *)o)
 * 6) define GPR(o) ((struct gpRender *)o)
 * 7) define GPI(o) ((struct gpInput *)o)
 * 8) define GPL(o) ((struct gpLayout *)o)

They just provide a simple and quick way to cast BOOPSI style messages from Msg to the appropriate structure for that message

Flags
Just about every class needs some flags associated with it to encode options or states.


 * 1) define BB_TEXT 0 //is the label text?
 * 2) define BB_PUSH 1 //should the button hold its state when (de)selected


 * 1) define BF_TEXT (1L << BB_TEXT)
 * 2) define BF_PUSH (1L << BB_PUSH)

Protos
Instead of just packing every bit of code into dispatcher its a good idea to break it up into separate functions.

//always good for debugging, since you can't use printf from a library or even a class i think... extern int kprintf( const char *str, ... );

//the dispatcher itself ULONG ASM dispatchClass( REG(a0) Class * cl, REG(a2) Object * o, REG(a1) Msg msg );

//Used to set our attributes ULONG ASM setClassAttrs( REG(a0) Class * cl, REG(a2) Object * o, REG(a1) struct opSet * msg );

//Used to get some attribute ULONG ASM getClassAttr( REG(a0) Class *cl, REG(a2) Object *o, REG(a1) struct opGet * msg);

//a custom TextLength that doesn't need a RastPort LONG myTextLength( struct TextFont *font, char *str, int len );

//The ubiquitous Notify function found in all my gadget classes //It takes care of sending the OM_UPDATE messages to the target object void ASM Notify( REG(a0) Class *cl, REG(a2) Object *o, REG(a1) struct opSet *msg, REG(d1) ULONG flags, REG(d2) sel, REG(a3) struct GadgetInfo *ginfo ); Globals Class *cl = 0; // our class structure pointer struct Library *ClasservBase; //pointer to the classerv.library base, see the my classes page

struct DrawInfo defdri; //a default drawinfo in case one isn't passed in //we use it to find a height and width of buttons with text labels so we really need it

WORD defpens[NUMDRIPENS]; //default pen array for the drawinfo

//in case we're disabled we should ghost USHORT ghostdata[] = {	0x2222, 0x8888 };

//our instance data, try to make it small struct localObjData { struct DrawInfo *dri; BYTE flags; };

//ahhh, easy open and closing of libs struct OpenLibTemplate olt[] = {	{"intuition.library",36,&IntuitionBase}, {"graphics.library",0,&GfxBase}, {"utility.library",0,&UtilityBase}, {"classerv.library",0,&ClasservBase}, {0} };

//same for classes although we don't need any here struct OpenClassTemplate oct[] = {	{0} };

Names
We need to pick a name for our gadget and for our superclass. As naming conventions go there really isn't a standard yet. Once the classerv.library is brought to its full potential there should be one.

Dispatcher... Ugh Here comes the hard part... Notice no __saveds, it can't be used here...

First you pass the message up using DoSuperMethodA, DSM for short, and this will eventually hit rootclass creating our object and then it will come back down to us after our parents have initialized their instance data to the defaults. Then below we get our instance data pointer and set any defaults. ALWAYS make sure that your instance pointer is set and DO NOT ever copy and paste the instance macro from the OM_NEW method since it uses lod = INST_DATA( cl, newobj ) where every other method should use lod = INST_DATA(cl, o), notice it should be o not newobj. NB: You should always pass the message up to your parent class, in the future some people might want to implement error handling so that any objects passed in the taglists will be disposed of if the OM_NEW fails.

Here comes GM_RENDER, it does all of the drawing for the gadget whenever intuition asks it too. Since this is the first function pertaining to gadget we'll discuss the GadgetInfo structure now.

struct GadgetInfo {	struct Screen *gi_Screen; struct Window *gi_Window; struct Requester *gi_Requester; struct RastPort *gi_RastPort; struct Layer *gi_Layer; struct IBox gi_Domain; struct { UBYTE DetailPen; UBYTE BlockPen; } gi_Pens; struct DrawInfo *gi_DrInfo; ULONG gi_Reserved[4]; };

This structure gives you a bunch of information about where your gadget is in the system as well as any information needed for drawing. It is available in all gadget method messages and the OM_SET message defined by the rootclass. Be warned though, all of the gadget methods have the pointer just after the MethodID, but the OM_SET message has it in a different place, this has caused me problems before.

SetAttrs... Doesn't get any better here The SetClassAttr function provided in the skeleton class is provided to make it easy to process all of the silly tags. Again notice, no __saveds in the function, although I think it should work with it since its always being called by the dispatcher, but who knows.

GetClassAtr... Just pure fat. This is currently of no use to us here, no reason to take it out though.

myTextLength... Ooooh Baaaad... This is prolly bad but I hate having to make a RastPort just to get a text length.

Notify... Easy to do but hard in concept The OM_NOTIFY/OM_UPDATE connection is something I will never understand. Anyways we need to send an OM_UPDATE to our target with our GA_ID as the attribute. The ti_Data of this attribute reflects whether the mouse is over the button or not.

//Set up proportional gadget and arrows using standard Intuition. /* Obtain Images for the up and down arrows (use BOOPSI sysiclass)). */

upImage = NewObject(NULL, SYSICLASS, SYSIA_Which, UPIMAGE, SYSIA_DrawInfo, di, SYSIA_Size, SYSISIZE_MEDRES, TAG_END);

downImage = NewObject(NULL, SYSICLASS, SYSIA_Which, DOWNIMAGE, SYSIA_DrawInfo, di, SYSIA_Size, SYSISIZE_MEDRES, TAG_END);

...

/* Up and down gadget definitions */ upArrow.NextGadget = &vPropGad; upArrow.LeftEdge = (-sim.win[WIN_SRC]->BorderRight + 1); upArrow.TopEdge = (-upImage->Height - downImage->Height - sizeGHeight); upArrow.Width = upImage->Width; upArrow.Height = upImage->Height; upArrow.Flags = GFLG_RELBOTTOM | GFLG_RELRIGHT | GFLG_EXTENDED | GFLG_GADGIMAGE; upArrow.Activation = GACT_IMMEDIATE | GACT_RIGHTBORDER | GACT_RELVERIFY | GACT_BOOLEXTEND; upArrow.GadgetType = GTYP_BOOLGADGET; upArrow.GadgetRender = upImage; upArrow.SelectRender = NULL; upArrow.GadgetText = NULL; upArrow.MutualExclude = 0; //Obsolete anyway. upArrow.SpecialInfo = NULL; upArrow.GadgetID = GAD_SRC_UP; upArrow.UserData = NULL; upArrow.MoreFlags = 0;

/* Bounds variables not initialised because GMORE_BOUNDS is not set */ downArrow.NextGadget = &upArrow; downArrow.LeftEdge = (-sim.win[WIN_SRC]->BorderRight + 1); downArrow.TopEdge = (-downImage->Height - sizeGHeight); downArrow.Width = upImage->Width; downArrow.Height = upImage->Height; downArrow.Flags = GFLG_RELBOTTOM | GFLG_RELRIGHT | GFLG_EXTENDED | GFLG_GADGIMAGE; downArrow.Activation = GACT_IMMEDIATE | GACT_RIGHTBORDER |GACT_RELVERIFY | GACT_BOOLEXTEND; downArrow.GadgetType = GTYP_BOOLGADGET; downArrow.GadgetRender = downImage; downArrow.SelectRender = NULL; downArrow.GadgetText = NULL; downArrow.MutualExclude = 0; //Obsolete anyway. downArrow.SpecialInfo = NULL; downArrow.GadgetID = GAD_SRC_DOWN; downArrow.UserData = NULL; downArrow.MoreFlags = 0;

/* Bounds variables not initialised because GMORE_BOUNDS is not set */ (void)AddGList(sim.win[WIN_SRC], (struct Gadget *)&downArrow, 0, -1, NULL); RefreshGadgets((struct Gadget *)&downArrow, sim.win[WIN_SRC], NULL);

struct Image *NewImageObject (ULONG which) { return ((struct Image *)NewObject (NULL, SYSICLASS, SYSIA_DrawInfo,	DrawInfo, SYSIA_Which,	which, SYSIA_Size, Scr->Flags & SCREENHIRES ? SYSISIZE_MEDRES : SYSISIZE_LOWRES, TAG_DONE)); /* NB: SYSISIZE_HIRES not yet supported. */
 * Creates a sysiclass object. */

Creating Objects
Creating objects is relatively easy, its just a matter of finding out the class name you want to make an object out of or pass in the class pointer of the private class.

Creating many objects can also be a pain since you should check each creation to make sure it was successful before you can use it. Doing this for every creation is quite tedious so its best to create several at a time and then check them in one 'if'.

Communication
BOOPSI gadgets are just like regular gadgets so they communicate through the IDCMP port with the same messages. They also have an extra message class called IDCMP_IDMCPUPDATE which allows gadgets to send attribute lists to the task. To set this up you need to set the ICA_TARGET attribute of the gadget to be ICTARGET_IDMCP and optionally you can map an attribute to ICSPECIAL_CODE so that the attribute's value is inserted into the Code field of the IntuiMessage. The gadget will also put the GA_ID attribute with the gadget's ID into the taglist so you can tell which gadget sent the message.

Maintained by Tim Stack(stack@cs.utah.edu) Last Changed on 27-Dec-1997, 09:51

ObjC and ObjFW
BOOPSI and ObjC both seem to use a similar Object model. ObjC allows direct access to the instant variables, while BOOPSI seems to require a macro for that. However, even though ObjC allows direct access to instant variables, this is almost never used, but accessors are used instead (direct access of instance variables is only done inside the class, @public instance variables are almost never used due to the fact that this kills ducktyping).

The dispatch seems to be similar as well, both have a dispatch table that is used for each call. However, there is one significant difference: Selectors (this is what an entry in the dispatch table is called in ObjC and specifies the message to send to the object) seem to be sequential integers in BOOPSI that are per-class, while ObjC has integers (they can be sequential, but they don't have to - usually they are not) that are globally unique. A selector in ObjC is global for every class, if two classes have a method by the same name, they share the same selector. Selectors (like you get them with @selector for example) are a struct that contains the name and (if known to the compiler) the type encoding of the method, the runtime then registers the method and replaces the contents of the struct to point to the entry in the dispatch tables.

So, this means dispatch tables in BOOPSI are small because they only contain the methods implemented by that class, while in ObjC, they are big, because they need to have a slot for every selector that exists. The solution in ObjC is to use a sparse array (3 levels in my runtime, GNU uses 4, which makes it significantly slower (factor 2!) with no real gain, since 2^24 should be enough).

So, in order to unite them, there are two problems:


 * How do we unite selectors? Do we change BOOPSI to use a sparse array? Otherwise BOOPSI would get into trouble if a BOOPSI object should be able to accept ObjC messages.
 * Can we change BOOPSI objects to have a pointer to the ObjC class at index 0? That would allow the runtime to call them.

If you are really interested in briding the two, I'm sure it can be done. But it might need changes to BOOPSI. See also the CoreFoundation / Foundation bridge Apple did.