A modern program user interface should provide, among other things, a quick and easy way of handling data files. Imagine a user who frequently has to click his/her way through a system of various menus, toolbars and requesters only to be able to open a file: will they feel comfortable using the program? Wouldn't it be faster and more straightforward if the user could just "drop" the file into the window, then let the program do whatever it is supposed to do with the file?
Amiga’s desktop environment – Workbench – has offered this functionality for decades. In this tutorial, we will have a look at how you can provide and use this handy feature in your ReAction-based programs. The article also gives some insight into the use of hooks under OS4.
The subsystem responsible for informing a program that a file (or more precisely: its icon) has been dropped into the program window is the Workbench Library. However, programs do not receive this information by default: two steps are required. First, the program window must be set up to become aware of messages from Workbench. Second, the program must implement code for its input handling routine that identifies Workbench messages and reacts accordingly.
A window that can receive messages from Workbench is called an AppWindow; the messages themselves are referred to as AppMessages (after struct AppMessage, a system data structure that contains the information sent by Workbench).
Beware of a possible misunderstanding. AmigaOS4's Application Library can also send messages to running programs. While these "application messages" are fundamentally different from Workbench's AppMessages, the name similarity may lead to confusion. (Perhaps it's time to give the Application Library messages an unambiguous name?)
3. Do I need AppWindows at all?
That depends. As a general rule, if your program has a GUI and implements an "Open", "Load" or "Import" function to use data files, you should make it an AppWindow and allow the user to drop files as an alternative to your menu or toolbar functions. Text editors, image editors and viewers, music and video players... all these should behave like an AppWindow.
The difference in the comfort of use can be enormous! As an example I'll mention the well-known Commodore 64 emulator, VICE. In the current AmigaOS4 version, the process of loading a game is unbelievably complicated. Yet all the hassle of attaching tape- or disk images and typing BASIC commands would be gone if the emulator window was implemented as an AppWindow. (The Windows version of VICE does this, and is therefore much more fun to use.)
Remember: the keyword here is workflow, not workslow!
4. ReAction and AppMessages
The ReAction Window Class not only supports receiving AppMessages but also makes the creation of AppWindows very easy. Turning a ReAction window into an AppWindow is indeed quicker than doing the same with a regular Intuition window. Your GUI code doesn’t need to call any Workbench Library functions, the class calls them internally. All you have to do for your window to become an AppWindow is to:
- provide a window application port (AppPort);
- specify that your window will be an AppWindow, by setting the WINDOW_AppWindow tag to TRUE.
The existence of an AppPort is also a pre-requisite for program window iconification. Even if you specify WINDOW_IconifyGadget, TRUE in your window object definition (and the resulting window displays a nice Iconify gadget), iconification will not work unless you specify the AppPort as well. This is quite logical: when a program is iconified, Workbench needs to tell it when there's time to "wake up" and reopen the window - without an AppPort, Workbench would have nowhere to send the message.
A message port is a type of system resource so the AppPort for your window must be allocated and initialized through the AllocSysObject() function (and freed accordingly; see section 7):
- struct MsgPort *winAppPort;
- winAppPort = (struct MsgPort *)IExec->AllocSysObject(ASOT_PORT, NULL);
To make it all work, your window object definition should contain the following tags:
- WINDOW_AppPort, winAppPort, /* the application port */
- WINDOW_AppWindow, TRUE, /* we want an AppWindow */
- WINDOW_IconifyGadget, TRUE, /* allow iconification */
You do not need to supply the WINDOW_AppWindow tag if you only require iconification – providing the AppPort is enough.
5. AppMessage handling
The key to ReAction’s message handling mechanism is the WM_HANDLEINPUT method, which processes all input events: gadget selections, menu picks, key presses, and the like. The result of the method call informs you of most events that the window has received. I say most - but not all. The pre-defined result values (see classes/window.h) only cover the most common types of input events: should you need others, you must use an extension called a hook. A typical example would be an ”unsupported” IDCMP event, such as GADGETDOWN. Window Class does support these, it just considers them less common and less likely to happen than other types of event (such as GADGETUP or MENUPICK). So it leaves it to the programmer to install an IDCMP hook and process all other events there.
AppMessages are handled in a similar fashion. The only AppMessage that the WM_HANDLEINPUT method can identify by the result value is WMHI_UNICONIFY, that is, Workbench telling the application to reopen its window. For other AppMessages you must provide an AppMessage hook and pass the hook pointer to the window object via the WINDOW_AppMsgHook tag.
You will notice that in older documentation, namely the ROM Kernel Manual: Libraries, AppMessages are handled differently in the input loop. The technique required setting up a signal for Exec’s Wait() and then processing the message via GetMsg() / ReplyMsg(). This is not the way to do it in ReAction. Using the old technique in combination with the WM_HANDLEINPUT method would mean mixing things up. Use the AppMessage hook instead.
6. The AppMessage hook
In case you have never used a hook before and are not sure what we are talking about: hooking is a practice of intercepting function calls, messages or events with custom code; this code is called a hook function. Whenever specific conditions are met, the currently running section of the program code gives control to the hook function and the custom code is executed. To bridge the intercepted code section and the hook function, you need a data structure called a hook. Once in place, the hook is responsible for calling the hook function and providing it with data from the intercepted code.
The AmigaOS API employs hooks quite extensively: many system components allow hooking in order to extend their functionality in a transparent and standardized fashion. ReAction’s WM_HANDLEINPUT method works this way, too - instead of checking for every possible type of event, it handles a limited set and lets you install a hook if you need more.
In order to use a hook, you have to:
- open Utility Library (which provides the hooking functionality);
- write the code for the hook function (see 6.1);
- allocate and initialize memory for the hook data structure (see 6.2);
- install the hook by passing the data structure pointer to the appropriate component that is to be intercepted (see 6.3).
6.1 The hook function
The hook function is a custom function you write in order to be able to handle a specific situation that arises from intercepting the main code (such as, processing a message). The function must be designed to accept three arguments, whose order and type is given. On 68K systems, the arguments had to be put in specific CPU registers (A0, A2 and A1) – this is no longer the case under AmigaOS4. Using hooks is easier and more transparent now.
The first argument is a pointer to struct Hook, the hook data structure (see 6.2 below). The second is a generic pointer to an object and is dependent on the context the hook function was called from. For example, if the hook call was triggered by a ReAction gadget or window, the argument will point to the respective object instance. (AppMessage hooks are called by the Window Class so the object will point to the window object.) The third argument is a pointer to a message, the type of which is also context-dependent. For instance, when providing an IDCMP hook, the third argument will point to a struct IntuiMessage; if we are providing an AppMessage hook, the argument will point to a struct AppMessage.
All it takes to process AppMessages inside the hook function is to read from the message structure, identify the type of message, and act accordingly. In the following code snippet we’ll process an AppWindow message, supposedly a notification that a file icon has been dropped into our window. We’ll obtain the name of the file and call our hypothetical loading routine:
- #include <workbench/workbench.h>
- #include <workbench/startup.h>
- #define FNAME_MAX 2048 /* maximum filename length */
- void my_hook_function(struct Hook *hook, Object *object, struct AppMessage *msg)
- struct WBArg *argPtr = NULL;
- char fileName[FNAME_MAX];
- if ( msg->am_Type == AMTYPE_APPWINDOW )
- /* An icon was dropped into the AppWindow.
- Check the AppMessage class and the number of arguments
- to make sure that we really have a file to open. */
- if ( (msg->am_Class == AMCLASSICON_Open) && (msg->am_NumArgs > 0) )
- /* Obtain the filename and load the file. */
- argPtr = msg->am_ArgList;
- IDOS->NameFromLock(argPtr->wa_Lock, fileName, FNAME_MAX);
- IDOS->AddPart(fileName, argPtr->wa_Name, FNAME_MAX);
6.2 The hook structure
Now that our hook function is ready, we must prepare the hook data structure. Under OS4, this structure is considered a system resource object so you are supposed to allocate it using AllocSysObjectTags(). Do not initialize its members directly, as older documentation shows. Instead, declare a pointer to struct Hook and initialize it by passing the tags:
- struct Hook *appMessageHook;
- appMessageHook = IExec->AllocSysObjectTags(ASOT_HOOK,
- ASOHOOK_Entry, my_hook_function,
- ASOHOOK_Subentry, NULL,
- ASOHOOK_Data, NULL,
ASOHOOK_Entry points directly to the hook function. The function can receive arbitrary data through the ASOHOOK_Data tag (we do not need any for our AppWindow hook so the pointer value is NULL here). This data can then be accessed inside the hook function by using the hook pointer, which the function receives as its first argument. For example, if we pass the string “Hello world!” as hook data (ASOHOOK_Data, “Hello world!”, ... ), you can print out the string inside the hook function like this:
- void my_hook_function(struct Hook *hook, Object *object, struct AppMessage *msg)
- puts( (STRPTR) hook->h_Data);
6.3 Installing the hook
All that is left to do is install the AppWindow hook in the ReAction window object. You can do this at object creation time, by putting “WINDOW_AppMsgHook, appMessageHook, ...” in the window definition tag list, but you can also do it later, using SetAttrs():
- IIntuition->SetAttrs(windowObject, WINDOW_AppMsgHook, appMessageHook, TAG_END);
7. Resource disposal
Apart from the ReAction window we have defined and used as our AppWindow, we have two other resources to dispose of when the program has run its course: the application port and the hook. These are not BOOPSI objects so the Window Class does not free them automatically at DisposeObject() time. After you dispose of the window object, you must call FreeSysObject() to release the memory allocated for the AppPort and the hook, respectively:
- IExec->FreeSysObject(ASOT_PORT, winAppPort);
- IExec->FreeSysObject(ASOT_HOOK, appMessageHook);
8. Example code
To give you a better picture of how AppMessages are handled by Window Class and processed in the code, I’ve made a full working example in C (see attached). The program opens a ReAction window with a display area that is, in fact, a big read-only Button Gadget. The window is set up as an AppWindow of course. If you drop a file icon into the window, the gadget will change its text and display the file name.