11 - GEM Hooks and Hacks + An Insider's AES Tricks
PROFESSIONAL GEM
by Tim Oren
Column #11 - GEM Hooks and Hacks + An Insider's AES Tricks
Welcome to the eleventh episode of ST PRO GEM, which is devoted to exploring some of the little-documented, but powerful, features of GEM. Like the authors of most complex systems, the GEM programmers left behind a set of "hooks", powerful features which would aid them in enhancing the system later. I am going to lay out a number of these methods which have served me well in making creative use of the AES. You will find that most of them concern the object and form libraries, since I was most involved in those parts of GEM. There are probably many more useful tricks waiting to be found in other parts of GEM, so if you happen onto one, please let me know in the Feedback! Before you begin, be sure to pick up the download for this column: GMCL11.C.
POWERFUL OBJECTS
The first four tricks all involve augmenting standard AES objects. This is a powerful technique for two reasons. First, you can take advantage of the regular AES object and form libraries to draw and handle most of your objects, so that your program need only process the exceptions. Second, you can use the RCS to copy the special objects into multiple dialogs or resources. These four tricks are Extended Object Types, User- defined Objects, TOUCHEXIT, and INDIRECT. Let's look at each of them in turn.
EXTENDED OBJECT TYPES
If you look at the AES Object Library documentation, you will notice that the values for the OB_TYPE field in an object are all 32 or less. This means that a number of bits are unused in this field. In fact, the AES completely ignores the top byte of the OB_TYPE field. In addition, the RCS ignores the top byte, but it also preserves its value when an object is read, written, or copied.
This gives you one byte per object to use as you see fit. Since the processing of an object or dialog is (so far) in the hands of the AES, the best uses of Extended Types are in flagging methods for initializing an object or handling its value when the dialog completes.
For example, you might have several dialogs containing editable numeric fields. The Extended Type of each numeric field could be set to the index of the corresponding value in an array. Then your application's dialog initialization code could scan the object tree for such objects, pick up the appropriate value from the array and convert it to ASCII, storing the result in the resource's string area. When the dialog was finished, another pass could be made to reconvert the ASCII to binary and store away the results in the array. (Note that the map_tree() utility from column #5 will scan an entire resource tree.)
Another application is to assign uniform codes to exit buttons in dialogs. If you give every "OK" button an Extended Type of one, and every "Cancel" button an Extended Type of two, then you needn't worry about naming every exit object. Just examine the Extended Type of the object returned by form_do, and proceed accordingly.
The catch, of course, is that you have to find a way to enter the Extended Type code in the first place. Version 1.0 of the RCS, as shipped with the Atari developer's kit, makes no provision for this. So you have your choice of two methods for creating the first object with each Extended Type code.
First, you can dump out a C source of a resource, insert the new type code by hand, and regenerate the resource with STCREATE. Alternately, you could carefully modify the binary resource using SID. You will probably want to reread the AES object manual, ST PRO GEM #4 and #5, and use the C source as a guide when doing so. In both cases, you should make things easy on yourself by creating a one dialog resource with only a single object other than the root. Version 2.0 of the RCS will let you directly enter an Extended Type, but it has not yet been released for the ST by DRI.
Once you have created a prototype extended object by either method, you can use the RCS to propogate it. The safest way is to use the MERGE option to add the modified tree to the resource you are building. Then copy the prototype object via the clipboard to your dialog(s), deleting the extra tree when you are done. If you are using several different extended objects, you can use MERGE and clipboard copies to get them all into one tree which will then become your own object library.
The second way of using RCS is easier, but more dangerous. If you want to try the following technique, BACK UP YOUR RCS DISK FIRST! Put simply, the RCS does not care what is in its dialog partbox. It will make copies of anything that it finds there! This gives you the option of using the RCS on ITS OWN RESOURCE in order to add your customized objects to the partbox.
To do this, open RCS.RSC from the RCS. Since there is no DFN file, you will get a collection of question mark icons. Use the NAME option to make TREE5 into a DIALOG. Open it, and you will see the dialog partbox.
Now you can use the MERGE technique described above to insert your customized objects. Then SAVE the modified resource, and rerun the RCS. Your new objects should now appear in the partbox. If you added several, you may have to stretch the partbox to see them all. You can now make copies of the new objects just like any other part. (Note: DO NOT modify the alert or menu partboxes, you will probably crash the RCS.)
USER-DEFINED OBJECTS
The one type of object which was not discussed in the earlier articles on AES objects was G_USERDEF, the programmer defined object. This is the hook for creating objects with other appearances beyond those provided by the standard AES. By the way, you should note that the G_PROGDEF and APPLBLK mnemonics used in the AES documents are incorrect; the actual names as used defined OBDEFS.H are G_USERDEF and USERBLK.
The RCS does not support the creation of G_USERDEF objects, since it has no idea how they will be drawn by your program. Instead, you must insert a dummy object into your resource where you want the G_USERDEF to appear, and patch it after your application performs its resource load.
You must replace the object's existing OB_TYPE with G_USERDEF, though you may still use the upper byte as an Extended Type. You must also change the OB_SPEC field to be a 32-bit pointer to a USERBLK structure. An USERBLK is simply two LONG (32-bit) fields. The first is the address of the drawing code associated with the user defined object. The second is an arbitrary 32-bit value assigned to the object by your application.
You can designate objects for conversion to G_USERDEFs in the normal fashion by assigning them names which are referenced one by one in your initialization code. You can also combine two tricks by using the Extended Type field as a designator for objects to be converted to G_USERDEF. Each tree can then be scanned for objects to be converted. There is a short code segment in the download which demonstrates this technique.
My usual convention is to define new drawing objects as variants of existing objects, using the Extended Type field to designate the particular variation. Thus an Extended Type of one might designate a G_BUTTON with rounded corners, while a value of two could flag a G_STRING of boldface text. When using this technique, the RCS can be used to build a rough facsimile of the dialog by inserting the basic object type as placeholders. The existing OB_SPEC information can then be copied to the second position in the USERBLK when the object is initialized.
One final note before moving on: There is no reason that the USERBLK cannot be extended beyond two fields. You might want to add extra words to store more information related to drawing the object, such as its original type.
The AES will call your drawing code whenever the G_USERDEF needs to be drawn. This occurs when you make an objc_draw call for its tree, or when an objc_change occurs for that object. If your user-defined object is in a menu drop-drop, then your drawing code will be called any time the user exposes that menu.
Before getting into the details of the AES to application calling sequence, some warnings are in order. First, remember that your drawing code will execute in the AES' context, using its stack. Therefore, be careful not to overuse the stack with deep recursion, long parameter lists, or large dynamic arrays. Second, the AES is NOT re-entrant, so you may not make ANY calls to it from within a G_USERDEF procedure. You may, of course, call the VDI. Finally, realize that drawing code associated with a menu object may be called by the AES at any time. Exercise great care in sharing data space between such code and the rest of the application!
When your drawing code is called by the AES, the stack is set up as if a normal procedure call had occured. There will be one parameter on the stack: a 32-bit pointer to a PARMBLK structure. This structure lies in the AES' data space, so do not write beyond its end!
The PARMBLK contains 15 words. The first two are the long address of the object tree being drawn, and the third word is the number of the G_USERDEF object. You may need these values if the same drawing code is used for more than one object at a time. Words four and five contain the previous and current OB_STATE values of the object. If these values are different, your drawing code is being called in response to an objc_change request. Otherwise, the active AES call is an objc_draw.
Words six through nine contain the object's rectangle on the screen. Remember that you cannot call objc_offset within the drawing code, so you will need these values! The next four words contain the clipping rectangle specified in the active objc_change or objc_draw call. You should set the VDI clip rectangle to this value before doing any output to the screen.
The last two words in the PARMBLK contain a copy of the extra 32-bit parameter from the object's USERBLK. If you have followed the method of copying an OB_SPEC into this location, these words will be your pointer to a string, or BITBLK, or whatever.
When your drawing routine is done, it should return a zero value to the AES. This is a "magic" value; anything else will stop the drawing operation.
The download contains a sample drawing routine which defines one extended drawing object, a rounded rectangle button. You can use this procedure as a starting point for your own User Defined objects.
PUT ANYTHING YOU WANT ON THE DESKTOP!
In ST PRO GEM #2, I described the use of the WF_NEWDESK wind_set call to substitute your own object tree for the normal green desktop background. If the tree you supply contains User Defined objects, you can draw whatever you want on the desktop! Some of the things you might try are free hand drawings imported in metafile format from EasyDraw, or whole screen bit images generated by Degas. If you do the latter, you will have to store the entire image off screen and blit parts of it to the display as requested.
In any case, remember that your desktop drawing code can be called any time that a window is moved, so exercise the same care as with a menu drawer. Also, be aware that making the WF_NEWDESK call does not force an immediate redraw of the desktop. To do that, do a form_dial(3) call for the entire desktop rectangle.
THE TOUCHEXIT FLAG
The TOUCHEXIT attribute is an alternative to the more usual EXIT. When the TOUCHEXIT bit is set in an object's OB_FLAG word, the form_do routine will exit immediately when the mouse button is pressed with the cursor over the object. Your code can immediately take control of the mouse and display, without waiting for the release of the button. This method is used for generating effects such as slider bars within otherwise normal dialogs.
The easiest way to code a TOUCHEXIT handler is to place a loop around the form_do call. If the object number returned is TOUCHEXIT, then the animation procedure is called, followed by a resumption of the form_do (WITHOUT recalling form_dial or objc_draw!). If the object returned is a normal EXIT, the dialog is complete and control flows to the cleanup code.
There is one idiosyncrasy of TOUCHEXIT which should be noted. When the AES "notices" that the mouse button has been pressed over a TOUCHEXIT, it immediately retests the button state. If it has already been released, it waits to see if a double click is performed. If so, the object number returned by form_do will have its high bit set. If you don't care about double clicks, your code should mask off this flag. However, you may want to use the double click to denote some enhanced action. For example, the GEM file selector uses a double click on one of the file name objects to indicate a selection plus immediate exit.
THE INDIRECT FLAG
If the INDIRECT bit is set in an object's OB_STATE word, the AES interprets the 32-bit OB_SPEC field as a pointer to the memory location in which the ACTUAL OB_SPEC is to be found. Like User Defined objects, this capability is not supported by the RCS, so you have to set up the INDIRECT bit and alter the OB_SPEC at run time.
The value of INDIRECT is that you can use it to associate an AES object with other data or code. The general technique is to set up a table with a spare 32-bit location at its beginning. Then, when initializing the application's resource, you move the true OB_SPEC into this location, set the INDIRECT flag, and replace the OB_SPEC field with a pointer to the table. The object behaves normally during drawing and form handling. However, if you receive its number from form_do or objc_find, you have an immediate pointer to the associated table, without having to go through a lookup procedure.
This technique works well in programs like the GEM Desktop. Each G_ICON object is set up with INDIRECT. Its OB_SPEC goes to the beginning of a data area defining the associated file. The blank location at the beginning of file table is filled up with the former OB_SPEC, which points to a ICONBLK.
You can also combine INDIRECT with TOUCHEXIT when creating objects that must change when they are clicked by a user. For instance, a color selection box might be linked to a table containing the various OB_SPEC values through which the program will cycle. Each time the user clicked on the box, the TOUCHEXIT routine would advance the table pointer, copy the next value into the dummy OB_SPEC location at the front of the table, and redraw the object in its new appearance.
A programmer who wanted to follow a truly object-oriented "Smalltalkish" approach could use the INDIRECT method to bind AES drawing object to tables of associated procedures or "methods". For instance, one procedure could be flagged for use when the user clicked on the object, one when the object was dragged, one for double-click, and so on. If the table structure was capable of indicating that the true method was stored in another table, a rudimentary form of class inheritance could be obtained.
INSTANT CO-ROUTINES
We turn to the AES event and message system for this trick. While some languages like Modula 2 provide a method for implementing co-routines, there is no such capability in C. However, we can effectively fake it by using the AES event library.
As already seen in an earlier column, an application can write a message to its own event queue using the appl_write call. Usually, this is a redraw message, but there is nothing to prevent you from using this facility to send messages from one routine in your program to another. To set up co-routines using this method, they would be coded as separate procedures called from the application's main event loop.
When one of the co-routines wanted to call the other, it would post a message containing the request and any associated parameters into the application's queue and then return. The main loop would find the message and make the appropriate call to the second co-routine. It it was necessary to then re-enter the first co-routine at the calling point, the original message could contain an imbedded reply message to be sent back when the request was complete. A simple switch structure could then be used to resume at the appropriate point.
There are two potential problems in using this method. The first is the limited capacity of the application event queue. The queue contains eight entries. While the AES economizes this space by merging redraws and multiple events, it cannot merge messages. Because of this limit, you must be extremely careful when one message received has the potential to generate two or more messages sent. Unless this situation is carefully managed, you can get a sort of "cancer" which will overflow the queue and probably crash your application.
The second danger involves race conditions. Message sent by the application are posted to the end of the queue. If other events have occurred, such as mouse clicks or keyboard presses, they will be seen and processed ahead of the application generated message. This implies that you cannot use this method if the program must complete its action before a new user generated event can be processed.
THAT'S ALL FOR NOW
Hopefully these hints will keep you profitably occupied for a while. ST PRO GEM number twelve will return to the topic of user interfaces. Reaction to the first article on this subject was mostly positive, but a lot of folks wanted to see real code as well. In response to your feedback, there will also be code for implemented your own "mouse sensitive" objects which highlight when the cursor touches them. This will be presented as part of an alternate form manager.
UPDATE: ATARI ST
I have recently gotten more information on some topics mentioned in earlier articles. These notes will bring you up to date:
A number of developers reported that they were unable to get the self-redraw technique described in ST PRO GEM #2 to work. This is usually due to a bug in the appl_init binding in Alcyon C. The value returned from the function, which would normally be assigned to gl_apid, is coming back as garbage. To work around the problem, declare EXTERN WORD gl_apid; in your program and DO NOT assign the value from appl_init. The binding WILL make the assignment. A tip of the hat to Russ Wetmore for this report.
The last column mentioned that turning off the clip rectangle while drawing graphics text will speed things up. It turns out that the VDI will also run at the non-clipped speed if the ENTIRE string to be written is within the current clip rectangle. To compound the problem, there is a one-off bug in the detection algorithm for the right edge. That is, the clip rectangle has to be one pixel BEYOND the right edge of the text for the fast write to work.
The Feedback in ST PRO GEM #10 mentioned that there are known bugs in the Alcyon C floating point library. In fact, this library has been replaced with a new, debugged version in recent shipments of the Toolkit. If you need to use floating point and have run into this bug, you should be able to get an update from Atari. Also, check the Atari Developer's SIG (PCS-57) for a possible download.
In addition, it turns out there is an undocumented feature in Alcyon C which allows you to imbed assembly code in-line. Try using:
asm(".....");
where the dots are replaced with an assembly instruction. You get one instruction per asm(), one asm() per line. Thanks to Leonard Tramiel for both of the above tidbits.
GMCL11.C
ode for initializing User Objects <<<<<<<<<<<<<<<<
GLOBAL USERBLK extobjs[MAX_OBJS]; /* APPLBLK defined in OBDEFS.H */
GLOBAL WORD n_extobjs; /* Set MAX_OBJS to total user */
/* objects in resource */
VOID
obj_init() /* Scan whole resource for user */
{ /* objects. Uses map_tree() */
LONG tree, obspec; /* from GEMCL5.C */
WORD itree, i, obj;
n_extobjs = 0; /* Replace TREE0 with your first */
/* tree, TREEN with the last */
for (itree = TREE0; itree <= TREEN; itree++)
{
rsrc_gaddr(R_TREE, itree, &tree);
map_tree(tree, ROOT, NIL, fix_obj);
}
}
WORD
fix_obj(tree, obj) /* Code to check and fix up */
LONG tree; /* a user defined object */
WORD obj;
{
WORD hibyte;
hibyte = LWGET(OB_TYPE(obj)) & 0xff00; /* check extended */
if (!hibyte) /* type - if none */
return (TRUE); /* ignore object */
extobjs[n_extobjs].ub_code = dr_code; /* set drawcode */
extobjs[n_extobjs].ub_parm = LLGET(OB_SPEC(obj)); /* copy obspec */
LLSET(OB_SPEC(obj), ADDR(&extobjs[n_extobjs])); /* point obspec */
LWSET(OB_TYPE(obj), G_USERDEF | hibyte); /* to userblk & */
n_extobjs++; /* patch type */
return (TRUE);
}
>>>>>>>>>>>>>>>>>>>>>> Sample User Object Drawing Code <<<<<<<<<<<<<<<<<<<<
>>>>>>>>>>>>>>>>>>>>>> Implements Rounded Box based <<<<<<<<<<<<<<<<<<<<
>>>>>>>>>>>>>>>>>>>>>> on G_BOX type <<<<<<<<<<<<<<<<<<<<
WORD
dr_code(pb) /* Sample user object drawing */
PARMBLK *pb; /* code. Caution: NOT portable */
{ /* to Intel small data models */
LONG tree, obspec;
WORD slct, flip, type, ext_type, flags;
WORD pxy[4];
WORD bgc, interior, style, bdc, width, chc;
tree = pb->pb_tree;
obspec = LLGET(pb->pb_parm); /* original obspec from USERBLK */
ext_type = LHIBT(LWGET(OB_TYPE(pb->pb_obj)));
slct = SELECTED & pb->pb_currstate;
flip = SELECTED & (pb->pb_currstate ^ pb->pb_prevstate);
set_clip(TRUE, &pb->pb_xc); /* These two routines in GEMCL9.C */
grect_to_array(&pb->pb_x, pxy);
switch (ext_type) {
case 1: /* Rounded box */
/* Crack color word */
get_colrwd(obspec, &bgc, &style, &interior,
&bdc, &width, &chc);
/* For select effect, use char color */
if (slct) /* In place of background */
bgc = chc;
/* Fill in background */
rr_fill(MD_REPLACE, (width? 0: 1), bgc, interior,
style, pxy);
/* Do perimeter if needed */
/* rr_perim is in GEMCL9.C */
if (width && !flip)
{
pxy[0] -= width; pxy[2] += width;
rr_perim(MD_REPLACE, bdc, FIS_SOLID, width, pxy);
}
break;
default: /* Add more types here */
break;
}
return (0);
}
VOID /* Cracks the obspec color word */
get_colrwd(obspec, bgc, style, interior, bdc, width, chc)
LONG obspec;
WORD *bgc, *style, *interior, *bdc, *width, *chc, *chmode;
{
WORD colorwd;
colorwd = LLOWD(obspec);
*bgc = colorwd & 0xf;
*style = (colorwd & 0x70) >> 4;
if ( !(*style) )
*interior = 0;
else if (*style == 7)
*interior = 1;
else if (colorwd & 0x80) /* HACK: Uses character writing mode */
*interior = 3; /* bit to select alternate interior */
else /* styles! */
*interior = 2;
*bdc = (colorwd & 0xf000) >> 12;
*width = LHIWD(obspec) & 0xff;
if (*width > 127)
*width = 256 - *width;
if (*width && !(*width & 0x1)) /* VDI only renders odd */
(*width)--; /* widths! */
*chc = (colorwd & 0x0f00) >> 8; /* used for select effect */
}
VOID /* Fill a rounded rectangle */
rr_fill(mode, perim, color, interior, style, pxy)
WORD mode, perim, color, style, interior, *pxy;
{
vswr_mode(vdi_handle, mode);
vsf_color(vdi_handle, color);
vsf_style(vdi_handle, style);
vsf_interior(vdi_handle, interior);
vsf_perimeter(vdi_handle, perim);
v_rfbox(vdi_handle, pxy);
}
Capture buffer closed.