(Note: a certain familiarity with OOP is assumed throughout these texts, also, I'm a terrible writer so bear with me :) )

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.

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;
};
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 );
}
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

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 );
                                               ^^^^^^^^ /* don't forget this it can
                                                           cause some trouble */

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 amiga.lib 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.

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.

boopsi struct

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 to 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 */
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.
/* 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.
connection diagram
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 );
}
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.

/* 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.

Well I hoped that helped a little bit...


Maintained by Tim Stack(stack@cs.utah.edu)
Last Changed on 26-Dec-1997, 19:31