----------------------------------------------------------------------------- Environment Adaptive Graphic User Interface ----------------------------------------------------------------------------- $RCSfile: Tutorial.doc,v $ $Revision: 1.3 $ $Date: 1993/12/15 14:24:26 $ ----------------------------------------------------------------------------- Introduction ŻŻŻŻŻŻŻŻŻŻŻŻ The Environment Adaptive Graphic User Interface (EAGUI) is a system which allows you to build interfaces that, as the name suggests, adapt to the environment they're run in. It uses normal GadTools and BOOPSI gadgets, and does not modify them in any way. This allows programmers to implement EAGUI in existing applications easily. This tutorial is a practical introduction to EAGUI. After you've read this, you should be able to write your own interfaces, using the AutoDocs (EAGUI.doc) as a reference. Apart from reading this document, you might also want to take a look at the examples. The tutorial assumes you are familiar with programming the AmigaOS[2], and some knowledge of C is useful too, because all examples are written in C. This tutorial is based on a simple example. The full source of this example can be found in "example.c". Concepts ŻŻŻŻŻŻŻŻ EAGUI divides the contents of a window in a hierarchical object tree. Objects can be either gadgets, images or groups. Groups can contain any number of objects. There are two types of groups: horizontal and vertical groups. Horizontal groups contain objects that are placed next to each other, vertical groups contain objects that are placed below each other. An object tree contains all information to create the gadgets and images in a window. For example, let's look at a string requester window that contains a string gadget, and an "Ok" and "Cancel" button below it. Starting at the top of the tree, we define a vertical group to divide the window in two. The upper object will contain the string gadget, the lower will contain the buttons. The lower object in turn is a horizontal object, which contains three (!) objects: the "Ok" button, an empty spaceholder and the "Cancel" button. The complete tree now looks like this: Vertical group String gadget Horizontal group "Ok" button gadget Empty spaceholder "Cancel" button gadget This tree already holds some information about how objects should be placed in the window, but not enough. Let's assume we want to create a resizable window, since this is clearly the most difficult type of window to maintain. A window can't be arbitrarily small, or else the objects won't fit inside the window. There is a minimum size. This size is determined by looking at the minimum size for each individual object in the window. We start at the bottom of the tree, where we find three objects. It is obvious that the labels of the buttons have to fit within the frame. The minimum size of these objects therefore is determined by the dimensions of the label. The other object, the empty spaceholder, is merely an object that fills up the empty space between the "Ok" and "Cancel" gadgets. This space can be arbitrarily small, so its minimum sizes are zero. Going up in the tree, we arrive at the Horizontal group object. The minimum sizes of its members are already determined, so the minimum sizes of the group are obtained by adding the horizontal minimum sizes of all children and taking the largest vertical size of the children. The next object, the string gadget, also has a minimum size that is determined by the dimensions of the string that has to fit inside the gadget. Finally, going up again, we arrive at the vertical group. Again, its members are known, so determining the size of the group is easy. All size calculations can be done at runtime. This is necessary because an application can not know in advance what font or locale the user has selected, and there is no valid reason why the user should be forced to use a certain font or locale. To allow flexible size calculations, each object uses a method to determine its minimum size. This method is implemented as a Hook. If you want, you can write this Hook function yourself, but for normal gadgets, general purpose methods are already supplied. More information about these Hooks can be found lateron. Apart from the minimum sizes, objects have borders, whose sizes can also be determined using methods if you want. Borders can be seen as whitespace around objects. It can sometimes be hard to remember the exact relation between the size, border size and offset of an object. Therefore, we've supplied a simple IFF picture, which shows the relations graphically. This picture's called "Tutorial_1.pic". Now that we know the minimum sizes, we also know the minimum inner size of the window. Please note that to determine the minimum size of the window, we have to add the sizes of the window borders. This presents us with a small problem, since we do not know the thickness of the borders in advance. There are several ways of dealing with this problem. These will be discussed lateron. The next step is to actually open the window. After we've done that, we actually know the inner sizes of the window, and therefore we know the size of the root (top) object, which, in our example, is the vertical group. The object is set to this size. From hereon, the sizes of all other object are calculated. We've already mentioned the minimum sizes, but there is another important object attribute we've left out up til now. Suppose that the window is bigger than the minimum size: how do we divide the available space? Some objects will want to use as much space as possible, while others will want to remain the same size. To solve this problem, each object can have a weight. This is a factor, which indicates how an object will grow in relation to other objects in the same group. Let's return to our example. The vertical group contains two objects. Each object has a vertical size, that really doesn't need to grow bigger. In practice, this means that the whole window doesn't need to be resizable in the vertical direction, only in the horizontal direction. The horizontal size of the objects is automatically the same as the window's inner width. Now we take a look at the button gadgets. These are grouped in the lower horizontal group. If this group becomes wider, the "Cancel" button should move to the right too. Looking at the three objects in this group, we should allow the middle one (i.e. the empty spaceholder) to grow bigger. We do this by setting its weight to one. Both buttons can have a fixed size, so their weights are zero. An alternative is to give the buttons a weight of one too. That means that the available horizontal space will be evenly divided between the three objects, because each object has the same weight. As you can see, this is a very simple, yet powerful concept. One thing to keep in mind is that if a group contains _no_ weighed objects, it is not filled completely. You're advised to add a custom image object with a weight of one in these cases. This will give the best results. Designing the interface ŻŻŻŻŻŻŻŻŻŻŻŻŻŻŻŻŻŻŻŻŻŻŻ The first thing you have to do, is make a sketch of the interface. You can either do this on a piece of paper, or by using some structured drawing tool. At this stage, you'll have to figure out how to divide the interface in groups of objects. When you start designing an interface, make sure you've read The Amiga User Interface Style Guide[1]. It contains a lot of guidelines, and following these will make your interface easier to use. Also, it is good practice to look at available software, and see how particular problems are solved. Especially the preference editors on your Workbench are good examples of how interfaces should look like (although they don't font adapt, unless you're a developer using a beta Release 3.1). Using the interface ŻŻŻŻŻŻŻŻŻŻŻŻŻŻŻŻŻŻŻ Basically, there are three main services you must provide to work with a window. First, you must initialize everything. Then you must handle incoming messages, until at some point the window is forced to close again, which is the third and last service. This is pretty straightforward. Let's look at the initialization first. Creating a new object is quite simple, and anybody who has used BOOPSI will be familiar with the type of function call. All you do is call ea_NewObjectA(). This function takes the object type as the first argument, and a pointer to a taglist as the second one. Alternatively, you can use ea_NewObject(), which allows you to pass the individual tags as arguments. If all went well, the function will return a pointer to the object, which can be used as a handle. It is not encouraged nor allowed to make assumptions about the data that the pointer points to. The ea_NewObjectA() function can be used to create a whole tree easily. For more information, look at the AutoDocs. Let's take another look at our example: LONG init(VOID) { if (!(winobj_ptr = ea_NewObject(EA_TYPE_VGROUP, EA_Child, ea_NewObject(EA_TYPE_GTGADGET, EA_GTType, STRING_KIND, [...] ), EA_Child, ea_NewObject(EA_TYPE_HGROUP, EA_Child, ea_NewObject(EA_TYPE_GTGADGET, EA_GTType, BUTTON_KIND, EA_GTText, "Ok", [...] ), EA_Child, ea_NewObject(EA_TYPE_CUSTOMIMAGE, [...] ), EA_Child, ea_NewObject(EA_TYPE_GTGADGET, EA_GTType, BUTTON_KIND, EA_GTText, "Cancel", [...] ), [...] ), [...] ))) { Printf("Couldn't create object tree.\n"); return(20); } return(0); } Some tags have been left out here, as marked by the ellipsis [...], because they are not essential to the example. Some other tags must be specified for GadTools or BOOPSI gadgets, and information on this can be found in the AutoDocs of these libraries. The most important thing is the way you can hierarchically define a tree. If something along the way goes wrong (which is usually because there wasn't enough memory) then nothing is created (all objects in the tree which were already created will be freed automatically), and NULL is returned. Now that we've initialized a tree, we must also have a way to clean it up. This is even simpler. All we need is a simple call to ea_DisposeObject(). This removes an object and all its children. In our example, it would look something like this: VOID cleanup(VOID) { ea_DisposeObject(winobj_ptr); } The next step is to calculate the minimum sizes. If you want the window to be resizable, then you must set the window sizing limits with the WindowLimits() call. Before that, you must first obtain the minimum sizes. This can be done easily by using ea_GetAttrs() on the `winobj_ptr'. The following example explains all this: /* obtain the minimum sizes of every object in the tree */ ea_GetMinSizes(winobj_ptr); /* get some attributes */ ea_GetAttrs(winobj_ptr, EA_MinWidth, &w, EA_MinHeight, &h, EA_BorderLeft, &bl, EA_BorderRight, &br, EA_BorderTop, &bt, EA_BorderBottom, &bb, TAG_DONE); /* open the window */ win_ptr = OpenWindowTags([...]); /* set the window limits */ WindowLimits( win_ptr, w + win_ptr->BorderLeft + win_ptr->BorderRight + bl + br, h + win_ptr->BorderTop + win_ptr->BorderBottom + bt + bb, ~0, h + win_ptr->BorderTop + win_ptr->BorderBottom + bt + bb); /* fill in the inner sizes of the window in the root object */ ea_SetAttrs(winobj_ptr, EA_Width, win_ptr->Width - win_ptr->BorderLeft - win_ptr->BorderRight - bl - br, EA_Height, win_ptr->Height - win_ptr->BorderTop - win_ptr->BorderBottom - bt - bb, EA_Left, win_ptr->BorderLeft, EA_Top, win_ptr->BorderTop, TAG_DONE); /* now determine the object sizes and positions */ ea_LayoutObjects(winobj_ptr); /* create the list of gadgets for this window */ rc = ea_CreateGadgetList(winobj_ptr, &gadlist_ptr, visualinfo_ptr, drawinfo_ptr); if (rc != 0) { /* bail out */ exit(20); } /* now you're ready for business */ You can now handle events in exactly the same way you normally do. The only exception is the IDCMP_NEWSIZE event. This is where you can use EAGUI to adapt to the new window size. If you receive one of these, you must do the following things: a) Store any unsaved changes the user has made to the gadgets. This means storing strings that were entered in string gadgets, items that were selected in listviews, cycle gadgets or radio buttons, and things like that. Depending on your program structure, this may not be necessary at all, because the changes were already stored when the IDCMP message that notified that change was received. b) Remove the gadget list from the window, and clean it up, like this: RemoveGList(win_ptr, gadlist_ptr, -1); ea_FreeGadgetList(winobj_ptr, gadlist_ptr); gadlist_ptr = NULL; c) Examine the new dimensions of the window, and set the root object accordingly, like this: ea_GetAttrs(winobj_ptr, EA_BorderLeft, &bl, EA_BorderRight, &br, EA_BorderTop, &bt, EA_BorderBottom, &bb, TAG_DONE); ea_SetAttrs(winobj_ptr, EA_Width, win_ptr->Width - win_ptr->BorderLeft - win_ptr->BorderRight - bl - br, EA_Height, win_ptr->Height - win_ptr->BorderTop - win_ptr->BorderBottom - bt - bb, EA_Left, win_ptr->BorderLeft, EA_Top, win_ptr->BorderTop, TAG_DONE); ea_LayoutObjects(winobj_ptr); d) Build the gadget list again, and connect it to the window. Currently, the method I use to refresh the window looks somewhat strange. I haven't quite figured out what to do with it. It seems that GadTools refreshes the gadgets directly after resizing the window, but before you're notified of that fact. Therefore, it renders over your window borders. After that, you get a IDCMP_NEWSIZE message, and you have to redraw everything yourself (including the window borders). This is example of working code: rc = ea_CreateGadgetList(winobj_ptr, &gadlist_ptr, visualinfo_ptr, drawinfo_ptr); if (rc != 0) { /* bail out */ exit(20); } EraseRect(win_ptr->RPort, win_ptr->BorderLeft, win_ptr->BorderTop, win_ptr->Width - win_ptr->BorderRight - 1, win_ptr->Height - win_ptr->BorderBottom - 1); RefreshWindowFrame(win_ptr); AddGList(win_ptr, gadlist_ptr, -1, -1, NULL); RefreshGList(gadlist_ptr, win_ptr, NULL, -1); GT_RefreshWindow(win_ptr, NULL); Includes and Macros ŻŻŻŻŻŻŻŻŻŻŻŻŻŻŻŻŻŻŻ When using the library in your own program, there are certain headers that you need to include. First, you must include "EAGUI.h", which contains all necessary information for using the library. This header then automatically includes "EAGUI_protos.h", which contains all needed function prototypes. There is one switch that may be of use. If you #define NOEAGUIMACROS before you include the header file, you won't get the macro's in "EAGUI_macros.h", otherwise you will. For normal circumstances, the macros seem to work well, but you may have your own personalized set of macro's that you're more comfortable with, so we won't force you to use them. The second file you should include (at least when you're using SAS/C) is a pragmas file (called "EAGUI_pragmas.h"). Users of other languages may find the EAGUI.fd file useful. If you've written header files for any other language, and you want us to include them in the EAGUI package, please contact us. Standard Methods ŻŻŻŻŻŻŻŻŻŻŻŻŻŻŻŻ For all GadTools gadgets, minimum size and border methods are supplied in the library. The methods that are supplied will try to adapt to all different tags that have an influence on the size of the object. The minimum size methods basically make the gadgets big enough to just fit. String gadgets for example will be high enough for the selected font, and wide enough for the border to be rendered succesfully. Border size methods will adapt to labels which are placed above, left of, right of or below a gadget. Please note that if, for example, you place a text above a gadget, the top border will only be high enough for the text to fit. If the text is _wider_ than the gadget, the gadget will not be adjusted: the label will simply be rendered, and you'll have to make sure that this doesn't conflict with other objects. To use the standard methods, you must specify the EA_StandardMethod tag, like this: ea_NewObject([...] EA_StandardMethod, value, [...] The `value' parameter can be EASM_MINSIZE, EASM_BORDER or a combination of both (EASM_MINSIZE | EASM_BORDER), depending on what standard methods you want to use for this object. The EAGUI_macros.h file already uses this tag, so if you use these macros, the standard methods will be used automatically. Creating your own Methods ŻŻŻŻŻŻŻŻŻŻŻŻŻŻŻŻŻŻŻŻŻŻŻŻŻ Although the standard methods will be sufficient in many cases, there might be times when you want to create your own methods. A good example would be when you use BOOPSI gadgets. Other reasons for creating your own methods could be that your interface uses gadgets of a much simpler form, or that it only uses certain types of gadgets. Custom methods might improve execution speed slightly, since they don't need to be `universal'. When creating you own methods, many things are possible, but there are a few things you have to keep in mind. Every method is called using a callback hook. For more information about initializing and using hooks, please refer to the RKRM's[2] or the header file. A method prototype should look like this: ULONG method( struct Hook * hook_ptr, struct ea_Object * object_ptr, APTR message_ptr); The hook_ptr contains the pointer to the hook structure of the method. The object_ptr points to the object that the method should be applied to. The message_ptr is not used at the moment. The method can change the attributes of the object. We strongly discourage you to let the method change any other attributes than the ones it should set. So a border method should only set the border attributes of the object, and a minsize method should only set the minsize attributes. You are allowed to read other attributes, if you need them for your calculations. Always use ea_GetAttrs() and ea_SetAttrs() to read and write the attributes respectively. Note that when the object is created, it is possible to pass border, size or minsize attributes. EAGUI protects these values, so your method can't change them, even if it tries to. All this is done automatically, so you don't need to worry about this. Relations ŻŻŻŻŻŻŻŻŻ In some interfaces, you want to create special relations between objects. One example that comes to mind is a simple window with an "Ok" and a "Cancel" button. If these both have a fixed size, then one of them will be bigger than the other, simply because the label strings don't have the same width. This does not look very good. To correct this, you can add a relation between the two objects. All objects in a relation must have the same direct parent. This relation is added to the parent of the objects. For example: ea_NewRelation(obj_hgroup_ptr, &relhook, EA_Object, obj_okgad_ptr, EA_Object, obj_cancelgad_ptr, TAG_DONE); Any number of objects can be connected using one and the same relation. You can simply specify a list of objects by using the tag shown above. Relations also use the Hook mechanism. They're called after the minimum sizes were calculated, so you can read the minimum sizes of the objects in this method. To explain how to create relations, a simple example is included below: /* same size relation */ ULONG rel_samesize(struct Hook *hook_ptr, struct List *list_ptr, APTR msg_ptr) { struct ea_RelationObject *ro_ptr; ULONG minx, miny; ULONG x, y; minx = 0; miny = 0; /* examine the list of objects that are affected by the relation */ ro_ptr = (struct ea_RelationObject *)list_ptr->lh_Head; while (ro_ptr->node.ln_Succ) { ea_GetAttrs(ro_ptr->object_ptr, EA_MinWidth, &x, EA_MinHeight, &y, TAG_DONE); /* find the maximum values of the minimum sizes */ minx = MAX(x, minx); miny = MAX(y, miny); ro_ptr = (struct ea_RelationObject *)ro_ptr->node.ln_Succ; } /* set all objects to the newly found minimum sizes */ ro_ptr = (struct ea_RelationObject *)list_ptr->lh_Head; while (ro_ptr->node.ln_Succ) { ea_SetAttrs(ro_ptr->object_ptr, EA_MinWidth, minx, EA_MinHeight, miny, TAG_DONE); ro_ptr = (struct ea_RelationObject *)ro_ptr->node.ln_Succ; } return(0); } The example is pretty self-explaining. Advanced Topics ŻŻŻŻŻŻŻŻŻŻŻŻŻŻŻ There are no advanced topics discussed in this tutorial yet. This might change in future versions of this tutorial. Bibliography ŻŻŻŻŻŻŻŻŻŻŻŻ [1] The Amiga User Interface Style Guide, Addison Wesley [2] The Amiga ROM Kernel Reference Manuals, Addison Wesley