Aros/Developer/Docs/Libraries/Intuition/BOOPSI
History
editBOOPSI (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
editBasic 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.
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
editClasses 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
editThe 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
editAn 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
editObjects 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
editObjects, 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.
/* example of communication between a prop gadget, string gadget, and an app */
struct TagItem prop2intmap[] =
{
{PGA_Top, STRINGA_LongVal},
{TAG_DONE,}
};
struct TagItem int2propmap[] =
{
{STRINGA_LongVal, PGA_Top},
{TAG_DONE,}
};
#define PROPID 1
#define STRID 2
void main()
{
/* Open libs and window */
prop = NewObject( NULL, "propgclass",
GA_Left, 0,
...
GA_ID, PROPID,
ICA_MAP, prop2intmap,
PGA_Top, 0,
...
TAG_DONE );
if( prop && (integer = NewObject( NULL, "strgclass",
GA_ID, STRID,
...
ICA_TARGET, prop,
ICA_MAP, int2propmap,
GA_Previous, prop,
STRINGA_LongVal, 0,
...
TAG_DONE )) )
{
SetAttrs( prop, ICA_TARGET, integer, TAG_DONE );
AddGList( window, prop, -1, -1, 0 );
RefreshGList( prop, window, 0, -1 );
...
RemoveGList( window, prop, -1 );
DisposeObject( integer );
}
DisposeObject( prop );
}
Here is a the same example with communication with the IDCMP port.
struct TagItem prop2intmap[] =
{
{PGA_Top, STRINGA_LongVal},
{TAG_DONE,}
};
struct TagItem int2propmap[] =
{
{STRINGA_LongVal, PGA_Top},
{TAG_DONE,}
};
#define PROPID 1
#define STRID 2
void main()
{
/* Open libs and window */
model = NewObject( NULL, "modelclass",
ICA_TARGET, ICTARGET_IDCMP,
ICA_MAP, int2propmap
TAG_DONE );
prop = NewObject( NULL, "propgclass",
GA_Left, 0,
...
GA_ID, PROPID,
ICA_TARGET, model
PGA_Top, 0,
...
TAG_DONE );
if( int2prop = NewObject( NULL, "icclass",
ICA_TARGET, prop,
ICA_MAP, int2propmap,
TAG_DONE ) )
{
DoMethod( model, OM_ADDMEMBER, int2prop );
if( model && prop && int2prop && (integer = NewObject( NULL, "strgclass",
GA_ID, STRID,
...
ICA_TARGET, model,
GA_Previous, prop,
STRINGA_LongVal, 0,
...
TAG_DONE )) )
{
if( prop2int = NewObject( NULL, "icclass",
ICA_TARGET, integer,
ICA_MAP, prop2intmap,
TAG_DONE ) )
{
DoMethod( model, OM_ADDMEMBER, prop2int );
AddGList( window, prop, -1, -1, 0 );
RefreshGList( prop, window, 0, -1 );
...
RemoveGList( window, prop, -1 );
}
DisposeObject( integer );
}
}
DisposeObject( prop );
/* we only have to delete the model and not the icclasses since it
will dispose of its internal list when we dispose of it */
DisposeObject( model );
}
Dispose
editFinally, 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.
/* use */
gad = NewObject(...
gad2 = NewObject(...
if( gad && gad2 )
{
}
DisposeObject( gad );
DisposeObject( gad2 );
/* instead of */
if( gad = NewObject(...) )
{
if( gad2 = NewObject(...) )
{
DisposeObject( gad2 );
}
DisposeObject( gad );
}
/* if you can :) */
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
editWhen 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
editFigure 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 #ifndef BBBUTTONCLASS_H #define BBBUTTONCLASS_H #define BGA_Dummy (TAG_USER + 0x60000) #define BGA_Push (BGA_Dummy + 1) #define BGA_Image (BGA_Dummy + 2) //Convenient macro for creating buttons #define ButtonObject NewObject( NULL, "bbbuttongadget" #endif Casting Macros I like to use casting macros a lot so here are some: #define GA(o) ((struct Gadget *)o) #define IA(o) ((struct Image *)o) #define IM(o) ((struct Image *)o) #define SET(o) ((struct opSet *)o) #define GET(o) ((struct opGet *)o) #define GPR(o) ((struct gpRender *)o) #define GPI(o) ((struct gpInput *)o) #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
editJust about every class needs some flags associated with it to encode options or states.
#define BB_TEXT 0 //is the label text? #define BB_PUSH 1 //should the button hold its state when (de)selected #define BF_TEXT (1L << BB_TEXT) #define BF_PUSH (1L << BB_PUSH)
Protos
editInstead 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
editWe 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.
#define MYCLASSID "bbbuttongadget"
#define SUPERCLASSID "gadgetclass"
/* OpenLibrary/CloseLibrary
I do all of my classes so that they are in libraries, you don't have to though, you can make private classes and public classes in your own code. I just like being able to have them shareable and easily replaced. */
int ASM SAVEDS __UserLibInit( REG(a6) struct MyLibrary *libbase )
{
//Open libraries and classes
if( OpenLibraries( olt ) )
{
if( OpenClasses( oct ) )
{
//setup our default drawinfo
defpens[BACKGROUNDPEN] = 0;
defpens[SHADOWPEN] = 1;
defpens[SHINEPEN] = 2;
defpens[TEXTPEN] = 1;
defdri.dri_Pens = defpens;
defdri.dri_NumPens = NUMDRIPENS;
defdri.dri_Font = GfxBase->DefaultFont;
//setup our class
if( cl = MakeClass( MYCLASSID,
SUPERCLASSID, NULL,
sizeof(struct localObjData), 0))
{
/* Fill in the callback hook */
cl->cl_Dispatcher.h_Entry = (ULONG (*) ())dispatchClass;
/* Keep track of the libbase here since we'll need it later */
cl->cl_UserData = (ULONG)libbase;
/* Make the class public */
AddClass( cl );
return( FALSE );
}
/* something is hosed, close the classes and libs */
}
CloseClasses( oct );
}
CloseLibraries( olt );
return( TRUE );
}
void ASM SAVEDS __UserLibCleanup( REG(a6) struct MyLibrary *libbase )
{
if( cl )
{
/* Remove and free our class structure */
RemoveClass( cl );
FreeClass(cl);
}
/* Close libs and classes, see how easy it is :) */
CloseClasses( oct );
CloseLibraries( olt );
}
Dispatcher... Ugh Here comes the hard part... Notice no __saveds, it can't be used here...
ULONG ASM dispatchClass( REG(a0) Class *cl, REG(a2) Object *o, REG(a1) Msg msg)
{
ULONG retval = FALSE;
Object *newobj;
struct localObjData *lod;
/* C can be a pain when coding shared libraries. Just because this function is in a shared library doesn't mean that a6 will be set to our library base so we must first put our lib base, which we saved in cl_UserData, into a6 before we can get a4 and access to all our global data (library bases, etc...). */
putreg( REG_A6, cl->cl_UserData );
geta4();
switch (msg->MethodID)
{
case OM_NEW: /* First, pass up to superclass */
#ifdef DEBUG
kprintf( "class.class/OM_NEW:\n" );
#endif
if(newobj = (Object *)DSM(cl, o, msg))
{
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.
/* Initial local instance data */
lod = INST_DATA( cl, newobj );
lod->dri = &defdri;
//Use set function to interpret the tags passed in
setClassAttrs( cl, newobj, (struct opSet *)msg );
retval = (ULONG)newobj;
}
break;
case OM_SET:
#ifdef DEBUG
kprintf( "class.class/OM_SET:\n" );
#endif
retval = DSM( cl, o, msg );
retval += setClassAttrs( cl, o, SET(msg) );
break;
case OM_GET:
#ifdef DEBUG
kprintf( "class.class/OM_GET:\n" );
#endif
retval = getClassAttr( cl, o, GET(msg) );
break;
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.
case GM_RENDER:
{
struct RastPort *rp = GPR(msg)->gpr_RPort;
struct DrawInfo *dri;
//Support for the GREL_ flags, these should be made into macros someday.
WORD left = (GA(o)->Flags & GFLG_RELRIGHT) ? GPR(msg)->gpr_GInfo->gi_Domain.Width + GA(o)->LeftEdge - 1 : GA(o)->LeftEdge;
WORD top = (GA(o)->Flags & GFLG_RELBOTTOM) ? GPR(msg)->gpr_GInfo->gi_Domain.Height + GA(o)->TopEdge - 1 : GA(o)->TopEdge;
WORD width = (GA(o)->Flags & GFLG_RELWIDTH) ? GPR(msg)->gpr_GInfo->gi_Domain.Width + GA(o)->Width : GA(o)->Width;
WORD height = (GA(o)->Flags & GFLG_RELHEIGHT) ? GPR(msg)->gpr_GInfo->gi_Domain.Height + GA(o)->Height : GA(o)->Height;
lod = INST_DATA( cl, o );
dri = lod->dri; //cache a pointer to the drawinfo
//here we check if there is a border image (most likely a frameiclass object) which we should draw else we just setup a background color
if( GA(o)->GadgetRender )
{
DrawImageState( rp, GA(o)->GadgetRender, left, top, (GA(o)->Flags & GFLG_SELECTED) ? IDS_SELECTED : IDS_NORMAL, GPR(msg)->gpr_GInfo->gi_DrInfo );
SetDrMd( rp, JAM1 );
}
else
{
SetAPen( rp, (GA(o)->Flags & GFLG_SELECTED) ? dri->dri_Pens[FILLPEN] : dri->dri_Pens[BACKGROUNDPEN] );
RectFill( rp, left, top, left + width - 1, top + height - 1 );
}
if( GA(o)->GadgetText )
{
WORD lwidth, lheight;
WORD xoffset, yoffset;
//check if we're drawing some text or an image, and figure out the width and height of which one
if( (lod->flags & BF_TEXT) )
{
lwidth = myTextLength( dri->dri_Font, (char *)GA(o)->GadgetText, strlen( (char *)GA(o)->GadgetText ) );
lheight = dri->dri_Font->tf_YSize;
}
else
{
lwidth = IA(GA(o)->GadgetText)->Width;
lheight = IA(GA(o)->GadgetText)->Height;
}
//center the text or image inside the gadget
xoffset = left + ((width - lwidth) / 2);
yoffset = top + ((height - lheight) / 2);
//draw whatever it is
if( (lod->flags & BF_TEXT) )
{
SetFont( rp, dri->dri_Font );
SetAPen( rp, (GA(o)->Flags & GFLG_SELECTED) ? dri->dri_Pens[HIGHLIGHTTEXTPEN] : dri->dri_Pens[TEXTPEN] );
Move( rp, xoffset, yoffset + dri->dri_Font->tf_Baseline );
Text( rp, (STRPTR)GA(o)->GadgetText, strlen( (char *)GA(o)->GadgetText ) );
}
else
{
DrawImageState( rp, GA(o)->GadgetRender, xoffset, yoffset, (GA(o)->Flags & GFLG_SELECTED) ? IDS_SELECTED : IDS_NORMAL, GPR(msg)->gpr_GInfo->gi_DrInfo );
}
}
//check if we're disabled and draw a ghosting pattern if we are
if( GA(o)->Flags & GFLG_DISABLED )
{
SetAPen( rp, GPI(msg)->gpi_GInfo->gi_DrInfo->dri_Pens[SHADOWPEN] );
SetAfPt( rp, ghostdata, 1 );
RectFill( rp, left, top, left + width - 1, top + height - 1 );
}
}
break;
/* GM_LAYOUT is an OS 3.x only function but if it is here it won't hurt. Here we use it to automatically resize the framing image, although you can use it for a number of other things. Remember though that you shouldn't do any drawing here since Intuition will call GM_RENDER later, this is just setup so you can do recomputing. */
case GM_LAYOUT:
{
WORD width = (GA(o)->Flags & GFLG_RELWIDTH) ? GPR(msg)->gpr_GInfo->gi_Domain.Width + GA(o)->Width : GA(o)->Width;
WORD height = (GA(o)->Flags & GFLG_RELHEIGHT) ? GPR(msg)->gpr_GInfo->gi_Domain.Height + GA(o)->Height : GA(o)->Height;
lod = INST_DATA( cl, o );
if( GA(o)->GadgetRender )
{
SetAttrs( GA(o)->GadgetRender,
IA_Width, width,
IA_Height, height,
TAG_DONE );
}
}
break;
/* GM_HITTEST is here so that you can tell Intuition whether the gadget was hit or not and then respond accordingly. Here we just respond that we were in fact hit and we should be activated since it is a square gadget. Although this method can be used so that it check something else to see if it was hit or not (ie. call IM_HITTEST on a BOOPSI image object) */
case GM_HITTEST:
retval = GMR_GADGETHIT;
break;
/* GM_GOACTIVE is here so that you can set things up and do any long time operations before the gadget starts to receive all of the input.device messages. This function has a special return value which tells intuition what it should do with the input event which caused us to go active. They are GMR_NOREUSE which tells Intuition to just kill the event, GMR_REUSE which means it should use the event again (ie. if there was a right mouse button event we would want the menu to show up instead of just trashing it, this can only happen in GM_HANDLEINPUT though), GMR_NEXTACTIVE which is just like pressing tab when a string gadget is selected, GMR_PREVACTIVE which is just like pressing shift tab when a string gadget is selected, and GMR_MEACTIVE which tells Intuition that we are active and want to stay that way. Note that below should've checked to see if an input event activated us or if it was ActivateGadget(), this should be fixed. */
case GM_GOACTIVE:
{
struct RastPort *rp;
lod = INST_DATA( cl, o );
//check if we're a push in button and act appropriately
if( lod->flags & BF_PUSH )
GA(o)->Flags ^= GFLG_SELECTED;
else
GA(o)->Flags |= GFLG_SELECTED;
//Always, always, always use ObtainGIRPort to get the RastPort,
//unless of course its a GM_RENDER msg
if( rp = ObtainGIRPort( GPI(msg)->gpi_GInfo ) )
{
DoMethod( o, GM_RENDER, GPI(msg)->gpi_GInfo, rp, GREDRAW_REDRAW );
ReleaseGIRPort( rp );
}
//If its a push in button there is no need to go into GM_HANDLEINPUT
//so we just return GMR_NOREUSE.
if( lod->flags & BF_PUSH )
retval = GMR_NOREUSE;
else
retval = GMR_MEACTIVE;
}
break;
/* GM_HANDLEINPUT is where the gadget does all the processing of the input events. Here its pretty simple since just need to catch timer events and send messages on those and check to make sure the mouse is actually over the gadget */
case GM_HANDLEINPUT:
{
struct RastPort *rp;
WORD x = GPI(msg)->gpi_Mouse.X; //these are relative to the upper left corner of the gadget
WORD y = GPI(msg)->gpi_Mouse.Y;
struct InputEvent *ie = GPI(msg)->gpi_IEvent;
BOOL sel = FALSE;
WORD left = (GA(o)->Flags & GFLG_RELRIGHT) ? GPR(msg)->gpr_GInfo->gi_Domain.Width + GA(o)->LeftEdge - 1 : GA(o)->LeftEdge;
WORD top = (GA(o)->Flags & GFLG_RELBOTTOM) ? GPR(msg)->gpr_GInfo->gi_Domain.Height + GA(o)->TopEdge - 1 : GA(o)->TopEdge;
WORD width = (GA(o)->Flags & GFLG_RELWIDTH) ? GPR(msg)->gpr_GInfo->gi_Domain.Width + GA(o)->Width : GA(o)->Width;
WORD height = (GA(o)->Flags & GFLG_RELHEIGHT) ? GPR(msg)->gpr_GInfo->gi_Domain.Height + GA(o)->Height : GA(o)->Height;
lod = INST_DATA( cl, o );
//check to see if the mouse is over the gadget
if( (x >= 0) && (x < (width)) &&
(y >= 0) && (y < (height)) )
{
sel = TRUE;
}
//on timer events we should send messages to our ICA_TARGET
//Notice they are OPUF_INTERIM since the user isn't done with us yet
if( ie->ie_Class == IECLASS_TIMER )
{
Notify( cl, o, msg, OPUF_INTERIM, sel, GPI(msg)->gpi_GInfo );
}
//this takes care of mouse events
if( ie->ie_Class == IECLASS_RAWMOUSE )
{
switch( ie->ie_Code )
{
case SELECTUP:
//user let up the select button we need to deactivate
retval = GMR_NOREUSE;
//if we are selected we need to tell intuition to send a IDCMP_RELVERIFY
if( GA(o)->Flags & GFLG_SELECTED )
retval |= GMR_VERIFY;
//send a final notify, (ie. no OPUF_INTERIM flag)
Notify( cl, o, msg, 0, sel, GPI(msg)->gpi_GInfo );
//Set the code field of the IntuiMessage to our GadgetID
(*GPI(msg)->gpi_Termination) = GA(o)->GadgetID;
break;
case MENUDOWN:
//this is where GMR_REUSE come into play mainly
retval = GMR_REUSE;
break;
default:
//check if we need to change the graphics of the gadget if
//the mouse is/isn't over the gadget
if( (!sel && (GA(o)->Flags & GFLG_SELECTED)) ||
(sel && !(GA(o)->Flags & GFLG_SELECTED)) )
{
GA(o)->Flags ^= GFLG_SELECTED;
if( rp = ObtainGIRPort( GPI(msg)->gpi_GInfo ) )
{
DoMethod( o, GM_RENDER, GPI(msg)->gpi_GInfo, rp, GREDRAW_REDRAW );
ReleaseGIRPort( rp );
}
}
break;
}
}
}
break;
/* GM_GOINACTIVE is where we cleanup after GM_GOACTIVE and GM_HANDLEINPUT. Do the final render here since the gadget could've been inactivated by a select up or a mouse down, so we would've had to write this code for both GM_HANDLEINPUT. */
case GM_GOINACTIVE:
{
struct RastPort *rp;
lod = INST_DATA( cl, o );
if( !(lod->flags & BF_PUSH) )
{
GA(o)->Flags &= ~GFLG_SELECTED;
if( rp = ObtainGIRPort( GPI(msg)->gpi_GInfo ) )
{
DoMethod( o, GM_RENDER, GPI(msg)->gpi_GInfo, rp, GREDRAW_REDRAW );
ReleaseGIRPort( rp );
}
}
}
break;
/* Finally, if we don't know what the method is we just pass it up to the parent classes */
default:
retval = DSM(cl, o, msg);
break;
}
return(retval);
}
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.
ULONG
ASM setClassAttrs( REG(a0) Class * cl, REG(a2) Object * o, REG(a1) struct opSet * msg )
{
struct localObjData *lod = INST_DATA(cl, o);
struct TagItem *tags = msg->ops_AttrList;
struct TagItem *tstate;
struct TagItem *tag;
ULONG tidata;
BOOL change = FALSE;
BOOL sizechange = FALSE;
putreg( REG_A6, cl->cl_UserData );
geta4();
/* process rest */
tstate = tags;
while (tag = NextTagItem(&tstate))
{
tidata = tag->ti_Data;
switch (tag->ti_Tag)
{
//This is a special attribute to tell make the gadget just
//show the image instead of trying to try the frame
case BGA_Image:
GA(o)->GadgetRender = tidata;
GA(o)->Width = IM(tidata)->Width;
GA(o)->Height = IM(tidata)->Height;
break;
//A text label is wanted, set flags and set change flag
case GA_Text:
lod->flags |= BF_TEXT;
change = TRUE;
break;
//An image for the label is wanted, clear flag and set change flag
case GA_LabelImage:
lod->flags &= ~BF_TEXT;
change = TRUE;
break;
case GA_DrawInfo:
lod->dri = (struct DrawInfo *)tidata;
break;
//programmatic size change, do a redraw
case GA_Width:
case GA_Height:
sizechange = TRUE;
break;
//flag thingies, should prolly use one of the utility.library functions.
case BGA_Push:
if( tidata )
lod->flags |= BF_PUSH;
else
lod->flags &= ~BF_PUSH;
break;
//dunno what it is
default:
break;
}
}
//Check for size change, change the frame's rectangle, should prolly do a GM_RENDER here, oops
if( sizechange )
{
if( GA(o)->GadgetRender )
{
SetAttrs( GA(o)->GadgetRender,
IA_Width, GA(o)->Width,
IA_Height, GA(o)->Height,
TAG_DONE );
}
}
//Theres some new text gotta adjust the size. There should be some flags here
//so that the programmer can stop the resize from occurring
if( change && GA(o)->GadgetText )
{
struct IBox cont, frame;
WORD width, height;
//check for text/image label and find the size
if( (lod->flags & BF_TEXT) )
{
struct DrawInfo *dri = lod->dri;
width = myTextLength( dri->dri_Font, (char *)GA(o)->GadgetText, strlen( (char *)GA(o)->GadgetText ) );
height = dri->dri_Font->tf_YSize;
}
else
{
width = IA(GA(o)->GadgetText)->Width;
height = IA(GA(o)->GadgetText)->Height;
}
//try and do an IM_FRAMEBOX to figure out how big the frame wants to be for this size graphic
cont.Width = width;
cont.Height = height;
if( GA(o)->GadgetRender )
{
frame.Left = 0;
frame.Top = 0;
frame.Width = width;
frame.Height = height;
DoMethod( GA(o)->GadgetRender, IM_FRAMEBOX, &frame, &cont, lod->dri, 0 );
}
//GREL_ flags should be check above
if( !(GA(o)->Flags & GFLG_RELWIDTH) )
GA(o)->Width = cont.Width;
if( !(GA(o)->Flags & GFLG_RELHEIGHT) )
GA(o)->Height = cont.Height;
//Adjust the frame size, dunno if we wanna redraw here or not since the
//programmer might want to manually do it so a redraw would be ugly
if( GA(o)->GadgetRender )
{
SetAttrs( GA(o)->GadgetRender,
IA_Width, cont.Width,
IA_Height, cont.Height,
TAG_DONE );
}
}
return (1L);
}
GetClassAtr... Just pure fat. This is currently of no use to us here, no reason to take it out though.
ULONG
ASM getClassAttr( REG(a0) Class *cl, REG(a2) Object *o, REG(a1) struct opGet * msg )
{
struct localObjData *lod = INST_DATA(cl, o);
putreg( REG_A6, cl->cl_UserData );
geta4();
switch (msg->opg_AttrID)
{
default:
return ((ULONG) DSM(cl, o, (Msg)msg));
}
return (1L);
}
myTextLength... Ooooh Baaaad... This is prolly bad but I hate having to make a RastPort just to get a text length.
LONG myTextLength( struct TextFont *font, char *str, int len )
{
int lpc;
LONG width = 0;
int currch;
if( font->tf_Flags & FPF_PROPORTIONAL )
{
for( lpc = 0; lpc < len; lpc++ )
{
currch = str[lpc] - font->tf_LoChar;
width += ((WORD *)font->tf_CharSpace)[currch] + ((WORD *)font->tf_CharKern[currch]);
}
}
else
{
width = font->tf_XSize * len;
}
return( width );
}
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.
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 )
{
struct TagItem tt[2];
struct localObjData *lod = INST_DATA(cl, o);
putreg( REG_A6, cl->cl_UserData );
geta4();
tt[0].ti_Tag = GA_ID;
tt[0].ti_Data = sel ? GA(o)->GadgetID : -GA(o)->GadgetID;
tt[1].ti_Tag = TAG_DONE;
DoSuperMethod( cl, o, OM_NOTIFY, tt, ginfo, flags );
}
//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) * Creates a sysiclass object. */ { 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. */
Creating Objects
editCreating 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.
struct Gadget *gad;
Class *privclass;
/* for public classes */
gad = NewObject( NULL, "strgclass", ... );
/* for private classes */
gad = NewObject( privclass, 0, ... );
/* defines are a handy shortcut instead of writing them out all the time */
#define ButtonObject NewObject( NULL, "buttongclass"
There are a few gotchas though. Some attributes can't be specified or can only be given during creation so you need to be careful about what you pass in. For example, the frbuttonclass won't use the GA_Width/GA_Height attributes during creation so you need to use a separate SetAttrs() call in order to adjust it properly.
struct Image *frame;
struct Gadget *gad;
frame = NewObject( NULL, "frameiclass", TAG_DONE );
if( frame )
{
gad = NewObject( NULL, "frbuttonclass",
GA_Left, 10,
GA_Top, 10,
GA_Width, 200, /* these won't do anything */
GA_Height, 30,
GA_Image, frame,
GA_Text, "Hello World",
TAG_DONE );
if( gad )
{
/* you have to use a SetAttrs() in order for it to work */
SetAttrs( gad, GA_Width, 200, GA_Height, 30, TAG_DONE );
AddGadget( win, gad, -1 );
RefreshGadgets( gad, win, 0 );
}
}
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'.
/* use */
gad = NewObject( ... );
gad2 = NewObject( ... );
if( gad && gad2 )
{
}
/* DisposeObject() is smart enough to not dispose of a NULL so we can do this safely */
DisposeObject( gad );
DisposeObject( gad2 );
/* instead of */
if( gad = NewObject( ... ) )
{
if( gad2 = NewObject( ... ) )
{
/* ... */
DisposeObject( gad2 );
}
DisposeObject( gad );
}
Communication
editBOOPSI 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.
struct TagItem prop2idcmp[] = {
{PGA_Top, ICSPECIAL_CODE}, /* we are mapping the PGA_Top to the
IntuiMessages Code field */
{TAG_DONE}
};
void main()
{
struct Window *win;
if( win = OpenWindowTags( NULL,
...,
/* use IDCMP_IDCMPUPDATE to get taglists from gadgets */
WA_IDCMP, IDCMP_IDCMPUPDATE|(otherflags),
...,
TAG_DONE ) )
{
struct Gadget *prop;
prop = NewObject( NULL, "propgclass",
...,
GA_ID, 1, /* set the GadgetID so we can distinguish it from the others */
ICA_MAP, prop2idcmp, /* pass in the map taglist */
ICA_TARGET, ICTARGET_IDCMP, /* tell the gadget to send
an IDCMP_IDCMPUPDATE message
when a notify attribute changes */
TAG_DONE );
if( prop )
{
ULONG waitsigs, portsig;
BOOL done = FALSE;
struct IntuiMessage *imsg;
ULONG gadgetid;
AddGadget( win, prop, -1 );
RefreshGadgets( prop, win, 0 );
portsig = 1L << win->UserPort->mp_SigBit;
while( !done )
{
waitsigs = Wait( portsig | SIGBREAKF_CTRL_C );
if( waitsigs & portsig )
{
while( imsg = GetMsg( win->UserPort ) )
{
switch( imsg->Class )
{
case IDCMP_IDCMPUPDATE:
/* the taglist will have a GA_ID with it's gadget ID
We use GetTagData() and set the default to 0 so
we know if something went wrong and we got a message
without a GA_ID */
gadgetid = GetTagData( GA_ID, 0, imsg->IAddress );
switch( gadgetid )
{
case 1:
/* its the prop gad, do something */
printf( "prop top %ld\n", imsg->Code );
break;
default:
break;
}
break;
}
ReplyMsg( imsg );
}
}
if( waitsigs & SIGBREAKF_CTRL_C )
done = TRUE;
}
RemoveGadget( win, prop );
}
DisposeObject( prop );
CloseWindow( win );
}
}
Maintained by Tim Stack(stack@cs.utah.edu) Last Changed on 27-Dec-1997, 09:51
ObjC and ObjFW
editBOOPSI 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.
References
editNewObject NewObjectA DisposeObject
SetAttrs SetGadgetAttrs GetAttr
MakeClass AddClass RemoveClass FreeClass
AddGadget RefreshGadgets FreeGadget
ObtainGIRPort() ReleaseGIRPort()
DoMathod DoMethodA DoSuperMethod DoSuperMethodA
CoerceMethodA CoerceMethod
SetSuperAttrs
ULONG DoGadgetMethodA(struct Gadget *object, struct Window *win, struct Requester *req, Msg msg);