July 01 Cover 2
Volume Number: 17 (2001)
Issue Number: 07
Column Tag: Carbon Development
Introduction to Carbon Events
by Ed Voas, Apple Computer, Inc.
The basic concepts, terms, and philosophy of the Carbon Event Model
Introduction
Without question, the single most important advent in the history of the Mac OS Toolbox is the Carbon Event Model (CEM). "What the heck is that?" you ask. It is your new best friend. It will make it possible for you to write an application in a lot less time, and make your code cleaner and more consistent. It also allows you to tap into the code flow of the Toolbox without patching, and allows Apple to implement new functionality without many undesired side effects. This article gives a high level overview of the terminology and structure of the CEM, and hopefully will demonstrate the power and ease of use it can bring to your applications.
Overview
The CEM is the underlying event model for Carbon. This means that classic Toolbox Event Manager routines like WaitNextEvent are built on top of this new model. When calling WaitNextEvent, native Carbon Events are converted into EventRecord structures so your application can deal with them as it always has.
But of course, this new model does much more than serve as a mere foundation for WaitNextEvent — it also exposes a new way of receiving events in your application. Instead of looping around to pick up an event, deciding where it goes and dispatching it yourself, you can instead rely on the Toolbox to do this for you. Also, Carbon Events are much more expressive and expandable than the classic EventRecord structure. This means Apple can add more information to events as the Toolbox evolves.
An important point to note is that this event model isn't just picking up events and doing something with them, but rather it's a whole new inter-object messaging infrastructure. Every component in the Human Interface Toolbox (HIToolbox) ties into Carbon Events somehow. It's quite pervasive.
Another key thing is that the CEM is not something that needs to be adopted all at once. Existing applications can adopt it in stages. We recommend new applications use Carbon Events from the start, as it saves a lot of coding time. I think that once you see what it can do, you'll want to start using it right away (waves hand ala Jedi).
Model Building
There were several compelling reasons to invent a new model.
First, we wanted it to be incredibly simple to write a Carbon application. Most of a basic application involves receiving and delivering events to the appropriate place and handling them with old standbys. For example, what do you do when you receive a click? Typically, you call FindWindow to find out what object was hit by the mouse (window or menu bar). If the click is in the menu bar, you call MenuSelect and act on the result. If the click is in a title bar of a window, you might call DragWindow to move the window around. With CEM, all this default logic is now in the Toolbox, so you don't have to write it anymore. Good. It was a pain anyway.
Second, we wanted to promote an API that encouraged good performance, especially on Mac OS X. Many applications written for past Mac OS releases have used programming idioms and APIs that worked fine on those releases. On Mac OS X, however, they actually work against the system. For example, simple mouse tracking loops will take up close to 100% of the CPU on Mac OS X. With new APIs to replace those tracking loops, the CPU usage in these cases is next to zero.
Third, we have a multitude of messaging models in the Toolbox. We have callbacks, control definitions, window definitions, list definitions, menu definitions, events, and Apple Events. The goal of the CEM is to unify almost all of these into one consistent model. Once you learn it, you know all you need to know in order to listen for new events in the future. Carbon Events subsume everything except Apple Events, which remain the de facto interprocess communication medium.
Fourth, the old EventRecord structure was too limiting. Because of that structure, plus the fact that an event mask was only 16 bits wide, we knew we needed a new transport for events. We needed something that would be able to carry hundreds or thousands of event kinds with plenty of room to grow.
Lastly, this new model supports plug-ins better. Because we can directly dispatch events to windows, controls, and menus, a plug-in can actually get its events directly without necessarily relying on the host application to hand the events to it.
EventRefs: Opaque and Loving It!
The basic building block of the CEM is the EventRef. EventRefs are opaque (virtually all Toolbox entities that end in –Ref are opaque). To get at information contained within an event, you must call accessor functions. For example, to find out when an event occurred, you would call GetEventTime. (We pride ourselves on our original API names.) An event is specified by its class and kind. For example, a mouse down event has a class of kEventClassMouse, and a kind of kEventMouseDown (Man, we're clever!). There are a ton of new event types defined in the CEM, many more than you've ever seen before on Mac OS.
There are two types of information in an event — information common to all events (such as class, kind, and time), and information specific to a particular event or family of events (such as mouse button, or keyboard modifiers). This type of information is what we term parameters of an event. They are accessed via the SetEventParameter and GetEventParameter APIs (how do we think these up?). Parameters are merely generic blobs of data that can be attached to an event. You access them by symbolic names such as kEventParamMouseLocation. The type of data stored in the parameter is represented by a constant such as typeQDPoint. For those familiar with Apple Events, this will seem very similar. Well... OK, identical!
The advantage of this parameterized method is that we can add any number of parameters to any event. We can also do this at any time, meaning that in later versions of Carbon, we can add new parameters to the mouse down event without affecting existing applications. We can even start to store information as different types and automatically coerce them to the original type for existing clients. That's a powerful advantage of opaque types and generic parameter APIs. We dare say they rock.
One other note about events in the New World of Carbon: events are not necessarily one-way. They are also used for two-way communication between objects in the Toolbox. You can ask questions of objects and get responses which get stored as parameters. You do this by sending events directly to objects instead of posting them to the event queue.
Times in an EventRef are expressed differently than in the past as well. In an EventRecord, the when field yielded the time since boot in ticks (via a call to TickCount). EventRef times are also time since boot, but given in seconds as a floating point number. So fifteen and a half seconds after boot is, incredulously, 15.5. The replacement for TickCount when dealing with EventRefs is GetCurrentEventTime. There are macros in the headers that allow you to convert between ticks and the new EventTime type.
The Event Queue
When events come into an application, they are turned into EventRefs and posted onto the application's event queue. There they wait until the application calls an Event Manager API like WaitNextEvent to fetch them.
The event queue is a little different under Carbon than under traditional Mac OS. First, like EventRefs, it is an opaque entity. You cannot directly walk it like you could under traditional Mac OS. Because of this, there is a complete set of routines to allow you to post, search for, and flush events from the event queue.
The event queue is also per-process under Carbon, not global as it is in traditional Mac OS. This means you cannot peek at events destined for other processes, nor can you post events for other processes to receive (except, of course, Apple Events).
There can actually be more than one event queue in your application. The main thread and all cooperative threads share one event queue, which we call the main event queue. Any MPTasks that you might create in your application get their own event queue. This is quite handy if you wish to use Carbon Events as an inter-thread communication medium.
The Event Loop
I mentioned above that events come into an application and get turned into EventRefs. Well, the Event Loop is what takes care of that. When the Event Loop is ‘run', it waits for events and converts any that show up into EventRefs and posts them to the event queue. Typically, the Event Loop is run for you inside calls like EventAvail or WaitNextEvent.
The Event Loop is also where your application will block while waiting for an event. It essentially puts your application to sleep until an event (mouse click, Apple Event, etc.) comes into your application. You can think of the Event Loop as a condition variable (for those familiar with such constructs) which you wait on until some event arrives. There's some other stuff that goes on inside here, but we'll cover that later.
It is important to know about the Event Loop and how it works, since it means that there are only certain times the Event Loop is run and events get into your application. If you never run the Event Loop, your event queue will always be empty!
For every Carbon event queue, there is a corresponding Event Loop that is tied to it. With that in mind, like the event queue, the main thread and all cooperative threads share the ‘main' Event Loop. MPTasks have their own event loop.
BTW, I'm going to tell you now that the Event Loop term is going to confuse you. It was not the best choice of names, but I couldn't think of anything better at the time.
Event Handlers
For anyone who has used Apple Events, Carbon Event handlers will seem pretty familiar in form and function, with some improvements. These handlers are the means in which your application receives events. You install them by calling InstallEventHandler. You can install handlers onto windows, controls, menus, and the application. The application is a root-level place where events go if not handled by any other handlers.
I won't get into where it's best to handle certain events here. That's beyond the scope of this article and best suited for another, more in-depth one. We call that ‘job security.'
You install an event handler by calling InstallEventHandler:
InstallEventHandler(
EventTargetRef inTarget,
EventHandlerUPP inHandlerProc,
UInt32 inNumEvents,
const EventTypeSpec* inEventList,
void * inUserData,
EventHandlerRef* outHandlerRef );
As you can see from the InstallEventHandler prototype, you don't actually install handlers onto windows and the like directly. Instead, handlers can only be installed onto Event Targets. An event target is quite simply something that can receive events. To install a handler onto a window then, you actually need to get its event target. For example:
InstallEventHandler( GetWindowEventTarget(window), ... );
One other thing you'll notice about InstallEventHandler is that it takes an event count and event list in the inNumEvents and inEventList parameters. This is how you tell the Toolbox what events your handler is interested in. Keep in mind that a) you only will receive events you have registered for and b) you can't receive events that don't make sense to be delivered to a specific target. What that means is that if you have a control in a window, and you install an event handler on it that registers for the kEventWindowUpdate event, it will never receive it since those events are always destined for windows.
The EventTypeSpec structure used to specify events in the inEventList parameter is a simple structure defined thusly:
struct EventTypeSpec
{
UInt32 eventClass;
UInt32 eventKind;
};
The canonical way to define your event list and call InstallEventHandler is:
const EventTypeSpec kEvents[] =
{
{ kEventClassCommand, kEventCommandUpdateStatus },
{ kEventClassCommand, kEventCommandProcess }
};
InstallEventHandler( GetApplicationEventTarget(),
MyEventHandlerUPP, GetEventTypeCount( kEvents ),
kEvents, myUserData, &handler );
This registers MyEventHandlerUPP to receive two events. The events in this example are command events, which correspond to menu selections. We won't discuss these in this article (remember - job security), but in time you'll see that they're a very powerful way of dealing with menu items and menu selections under the CEM.
Well, that's all fine and dandy, but what does an event handler actually look like? The prototype for an event handler looks like this:
OSStatus MyEventHandler( EventHandlerCallRef inCallRef,
EventRef inEvent,
void * inUserData );
We'll discuss the inCallRef parameter later, but essentially your handler receives an event and the user data you passed into InstallEventHandler. You should return an appropriate OSStatus as the function result. This controls how events propagate, as we'll see shortly.
Events get to your handlers by means of the Toolbox dispatcher. This entity is called for you when you are fetching events via WaitNextEvent or RunApplicationEventLoop (described later). The dispatcher decides where events should go and routes the event to the appropriate entity.
Stack ‘Em Up
When handlers are installed, they are actually pushed onto a stack for an Event Target you specify. When an event is sent to that target, the event always goes to the top event handler on the stack. If that handler does not handle the event, it then falls to the next one and so on through all the handlers installed on the target in the reverse order in which they were installed.
This stacked behavior allows your application to override standard Toolbox behaviors. One of the things you can do with the CEM is create a window with a standard Toolbox event handler attached to it. Such a window directly receives events for clicks within its surface, and will automatically handle dragging, clicking the widgets, as well as tracking any controls that are in it. Some of its behaviors are pretty basic, though. For example, when we handle the close box being hit, we dispose the window. In practice, your application might want to do a bit more than that. Installing an event handler pushes a handler on top of the standard handler, allowing your application code to see the event first.
When your handler sees the event, it can choose to handle an event or not. You can also call through to have any handlers below you do their thing first and then post-process the event. You control this by two mechanisms — implicit and explicit propagation.
Implicit propagation is the natural order of things. In this situation, event propagation is controlled via the result code of your handler. If your handler returns any other error but eventNotHandledErr, it is assumed your handler has either handled the event, or tried to handle it but encountered an error. A completely successful handler would return noErr. For any result other than eventNotHandledErr, event processing for the current event will stop right after your handler returns.
On the other hand, if your handler returns eventNotHandledErr, the event will be propagated onto the next handler in the stack for the current target. It is certainly possible for an event to not be handled at all by any handler. This is perfectly OK, and sometimes actually desirable. Here is an example of a typical event handler:
Listing 1: Typical event handler
OSStatus MyEventHandler( EventHandlerCallRef inCallRef,
EventRef inEvent,
void * inUserData )
{
OSStatus result = eventNotHandledErr;
UInt32 theClass, theKind;
WindowRef window = (WindowRef)inUserData;
theClass = GetEventClass( inEvent );
theKind = GetEventKind( inEvent );
If ( theClass == kEventClassWindow &&
theKind == kEventWindowDrawContent )
{
DrawMyContent( window );
Result = noErr;
}
return result;
}
As you can see from the first line in the function, we always assume we didn't handle the event. That is the best assumption to make. You should only indicate you actually handled an event if you really, honestly, truly handled the event. If you say you handled an event, but you really didn't, you'll feel really guilty later, and you'll be subject to the rules of Karma. In the example above, if we received our draw content event, we set result to noErr, indicating we handled the event.
There are times, though, that you want to have the system handlers deal with the event first, and then you can follow up with some neat postlude to the standard behavior. This is where explicit propagation comes into play. In this situation, you want to force handlers below your handler to be called immediately, and then do your follow-up work. You do this via the CallNextEventHandler API. When your handler receives an event, you would call through with this API and then do your thing. Typically, you should make sure that CallNextEventHandler returns noErr (indicating it handled the event) before doing any post-processing. Your handler should also return whatever CallNextEventHandler returned for correctness. Here's an example:
Listing 2: Calling through
OSStatus MyEventHandler( EventHandlerCallRef inCallRef,
EventRef inEvent,
void * inUserData )
{
OSStatus result = eventNotHandledErr;
UInt32 theClass, theKind;
theClass = GetEventClass( inEvent );
theKind = GetEventKind( inEvent );
If ( theClass == kEventClassControl &&
theKind == kEventControlDraw )
{
result = CallNextEventHandler(
inCallRef, inEvent );
if ( result == noErr )
{
// draw ‘handles' on the control, which
// the user can manipulate to resize, drag, etc.
MyDrawControlHandles( inEvent );
}
}
return result;
}
Now you're saying "Aha! That's what the call ref is for!" Yes, it's strictly used when calling the next event handler, it is basically a blob of data that helps the dispatcher keep track of where it is so it can properly call through. Notice that we only call our function if the event was handled (in this case indicating that the frame was actually drawn). We also return the result of CallNextEventHandler, and don't set the result ourselves. The event was handled, so why should we lie about it? Like I said, you'd feel really bad about yourself if you did.
Target Flow
If a target does not handle an event, where does the event go? Well, that depends. In the general case, an event will flow from target to target until handled, based on certain rules imposed by the targets themselves. In general, an event might flow to a control, then it's parent control, then eventually the window and then the application. There are some exceptions, but we'll leave those for some other time.
This target flow can be useful, in that you can handle certain events at any level. For example, a mouse click on a control could be handled at the control level, or possibly the window level. The Toolbox actually uses this to its advantage, using one handler for basic control tracking at the window level, rather than manage many different handlers on every control in a window.
Making It All Go
When writing a Classic Mac OS application, you typically would call WaitNextEvent to, well, wait for events. This is the top of the application's event loop (not to be confused with the Event Loop construct in the CEM). WNE also yields time to other applications. If there are no events waiting for your application, it is put to sleep until one arrives or the timeout you pass to WNE expires.
In the CEM, there is a similar routine — ReceiveNextEvent. This routine acts almost identically to WNE. The only real difference is that you can control whether you want to actually pull the event from the queue or not. It can also return any event in the event queue while WaitNextEvent can only return the events it always has, not any of the new ones. While ReceiveNextEvent is a fine, glorious API, you'll probably never call it, except in special circumstances.
Instead, to drive an app written purely to the CEM, your application would call RunApplicationEventLoop. This API drives your entire application, and it pulls events off the queue for you and dispatches the events to your handlers automatically. Once called, it does not exit until QuitApplicationEventLoop is called. Essentially all of your application code is run via your event handlers.
From Theory To Practice
Now that we've covered most of the basics, let's see what a fully Carbon Event-based application looks like. The simplest case is to use Interface Builder nib files, so that's what we'll base this on.
Listing 3: Basic nib-based Carbon application
#include <Carbon/Carbon.h>
int main(int argc, char* argv[])
{
IBNibRef nibRef;
WindowRef window;
OSStatus err;
// Create a Nib reference passing the name
// of the nib file (without the .nib extension)
// CreateNibReference only searches into the
// application bundle.
err = CreateNibReference(CFSTR("main"), &nibRef);
require_noerr( err, CantGetNibRef );
// Once the nib reference is created, set the
// menu bar. "MainMenu" is the name of the menu bar
// object. This name is set in InterfaceBuilder
// when the nib is created.
err = SetMenuBarFromNib(nibRef, CFSTR("MainMenu"));
require_noerr( err, CantSetMenuBar );
// Then create a window. "MainWindow" is the name
// of the window object. This name is set in
// InterfaceBuilder when the nib is created.
err = CreateWindowFromNib(nibRef,
CFSTR("MainWindow"), &window);
require_noerr( err, CantCreateWindow );
// We don't need the nib reference anymore.
DisposeNibReference(nibRef);
// The window was created hidden so show it.
ShowWindow( window );
// Call the event loop
RunApplicationEventLoop();
CantCreateWindow:
CantSetMenuBar:
CantGetNibRef:
return err;
}
Well, that wasn't so bad, was it? Keep in mind that this application actually does stuff. It creates a menu bar and a simple window using nibs, shows the window and runs the application event loop. There it remains until the app is quit. While running, the app is fully functional — the menus track and the window can be moved or resized, etc. If the user selects quit from the file menu, the app will automatically terminate. We'll see how that works in a short while.
If you have not used Interface Builder before, you should start now. It does an absolutely awesome job of letting you lay out windows and menus. It supports all the latest Toolbox features, and replaces old-style resources with new-style nib files, which are basically XML descriptions of Toolbox UI objects. One of the coolest features it has helps you deal with Aqua HIG specs — when you drag items around, little guide lines appear to help you know what the standard HIG spacing is between items. It's really cool.
One other comment about the code above — please notice our use of the standard require macros, which make code much more readable while handling exceptions. This style of error handling was written up a long time ago in Apple's former Develop magazine, and we now have these macros in our Universal Interfaces in Debugging.h for everyone to use. I highly recommend them. Consult the header for more details. I mention this because you're going to see it more and more in our sample code and project templates, so it's helpful to be familiar with it.
Periodic Time
RunApplicationEventLoop will block forever while it waits for events. But if it does that, how will you get time to do periodic tasks in your application? Traditionally, applications have relied on null events returned by WaitNextEvent to get such work done. Null events do not exist in the CEM — Event Loop timers are the replacement. If you adopt only one aspect of the CEM, it should be timers. They work with RunApplicationEventLoop-based applications as well as WaitNextEvent-based applications. It's all good.
Timers run at task level — they are not like low-level timer mechanisms which fire asynchronously with respect to your main application flow. You can safely allocate memory and call the toolbox willy-nilly from an Event Loop Timer. They are synchronous in nature, i.e. a timer cannot be interrupted by another timer on the same thread of execution. If you want true asynchronous behavior, you should probably use an MPTask instead, but then you're limited in what you can do (for example, you can't call most of the Toolbox from an MPTask at present).
Timers are run when the Event Loop is run. I mentioned earlier that the Event Loop does some ‘other stuff.' This is it. If you don't run your Event Loop, your timers will never fire. This means that timers aren't necessarily a guaranteed heartbeat, but they are more than adequate for most periodic tasks.
Timers are merely callbacks that you install via the InstallEventLoopTimer API:
OSStatus InstallEventLoopTimer(
EventLoopRef inEventLoop,
EventTimerInterval inFireDelay,
EventTimerInterval inInterval,
EventLoopTimerUPP inTimerProc,
void* inTimerData,
EventLoopTimerRef* outTimer );
When installing a timer, you can specify whether you want a periodic timer or a one-shot timer (meaning it fires only once). Passing a non-zero value in the inInterval parameter implies that the timer is periodic, and passing zero means it is one-shot.
Typically, a one-shot timer would be set to fire some number of seconds into the future. (Otherwise, if you want to fire a timer one time immediately, why not just make a function call?) That is what the inFireDelay parameter is for. This parameter can also be used with periodic timers. Basically, inFireDelay is the time interval to wait until we first fire the timer. Once we start to fire, if we have an interval, we will continue to fire at the rate specified in the interval parameter.
The routine we call when the timer fires looks like so:
void MyTimer(EventLoopTimerRef inTimer, void *inUserData);
The timer that fired is passed into inTimer, and the data you passed in the inTimerData parameter in InstallEventLoopTimer is passed to your timer in the inUserData parameter.
Inside your timer proc, you can do whatever it is you need to, including removing the timer, or resetting it:
OSStatus RemoveEventLoopTimer(
EventLoopTimerRef inTimer );
OSStatus SetEventLoopTimerNextFireTime(
EventLoopTimerRef inTimer,
EventTimerInterval inNextFire );
RemoveEventLoopTimer does exactly what you'd expect, it removes the timer and stops it from firing. After calling that API, the timer reference is now invalid and should no longer be used. You need to remove one-shot timers as well as periodic ones — even though they may only fire once, they do not remove themselves automatically. This is mostly because you might want to reset the timer after it fires.
SetEventLoopTimerNextFireTime lets you reset a timer. This is typically used for a one-shot timer. For example, let's say if the user doesn't move the mouse in the next 10 seconds, you're going to quit your application (that's just the way you are). You would install a one-shot timer with an inFireDelay of 10 seconds, and an interval of 0. Each time the user moves the mouse, you would reset the timer with SetEventLoopTimerNextFireTime, passing 10 into inNextFireTime. If the timer fires, you set your quit flag and remove the timer.
You can also use SetEventLoopTimerNextFireTime with periodic timers if you wish, possibly postponing some sort of periodic action for a number of seconds. This API does exactly what it says, it sets the next time the timer will fire. This applies to one-shot as well as periodic timers. If you have a timer which fires every second, and you set the next fire time to 10 seconds from now, the timer will be dormant for 10 seconds, and then start firing every second after that again.
Performance On Mac OS X
All this talk about timers reminds me of performance issues, particularly on Mac OS X. The goal which every application should strive for on Mac OS X is to use 0% of the CPU when idle. That's right, zero. If your app is not foreground, there should be no activity. This has a substantial impact on overall system responsiveness and performance. This means that 100% of the CPU is available for meaningful tasks, such as downloading files, or rendering images. It also means that the VM working set is substantially less, since we don't have to keep pages hot for applications in the background. If you are polling in the background, you are stealing resources from the foreground application, making things slower for quite possibly no good reason.
Some people respond to this by saying "Wait a second you crackpot, I thought Mac OS X was preemptively scheduled. It shouldn't matter if I spin the processor." While this is true, remember that we are all sharing the CPU on X, and if two apps are spinning as fast as they can, they will only get about 50% of the CPU. Three app and you get 33%. 10 apps and you can now only hope for 10% of the CPU. If nine of those apps are just checking to see if a file has appeared in some folder, and one is trying to do real work and download a file, your download will be 10 times slower than it could be. That's pretty significant. Yes, even in this brave new world, we need to be good citizens. We can't stop another process from running like we can in traditional Mac OS, but we can sure slow it down.
Now, don't get me wrong — there certainly are classes of application that must get idle time constantly: browsers displaying animated GIFs, clock applications, music players which display frequency meters, etc. The goal though is to not take up any more idle time than is necessary. If you are displaying an animated GIF that displays each frame every second, then you only need a one second idle heartbeat in your application. If you are displaying no animated GIFs, you might not need any time. The trick is to only get the time you absolutely need.
Reaching this goal of zero CPU usage may seem impossible, but the CEM goes a long way to help you reach that goal. We offer several means of accomplishing that — Timers, better tracking loops, and new events to notify you when certain things change. You'll find that getting down to zero CPU usage is not impossible at all.
User Focus
One concept we are introducing with the CEM is that of user focus. Put simply, the user focus is the Event Target where keyboard events are sent. You can think of it as the extension of the current keyboard focus concept in the Control Manager. Menu commands are also sent to the user focus, but we'll look at those in the next section.
The user focus is the combination of the current user focus window and any currently focused control in that window. The user focus window is normally defined as the currently active document window. If no controls are focused in that window, the window is the current User Focus target, and keystrokes are sent there. If there is a focused control in the window, the control is the current User Focus target, and it receives keystrokes.
When the user clicks on a document window, the Window Manager changes the user focus window to the newly selected window automatically. If the user clicks in a floating window, the user focus window does not normally change. However, if the user clicks in an editable text field in the floating window (or any control that wants focus on mouse clicks), the Toolbox will switch the user focus to the floating window for you. Yes, the Toolbox makes the rash assumption that you'd like to let the user actually type into that swell text field you put in your floating window. If the user clicks in the body of another floater (but not in a focus-on-click control), the user focus is reset to the currently active document window. Likewise, if the user clicks in the currently active window (or some other document window) while a floater has focus, the user focus is reset to the window that was clicked.
It is possible to alter where keystrokes go by either changing the keyboard focus via the existing Control Manager APIs such as AdvanceKeyboardFocus, or by using the new SetUserFocusWindow API. This API can divert focus away from the default of the active window. This is how the floating window behavior above is implemented. You can also reset the focus to the Toolbox's standard choice with SetUserFocusWindow by passing (WindowRef)-1L for the window ref you pass in.
Commands
Menu commands have been supported since Mac OS 8.0. Essentially, they represent a position-independent way of identifying menu items or menu selections. No matter where your menu item happens to be, the command ID is always constant. Carbon Events extends the concept of menu commands to include controls as well. Both menus and controls use Carbon events to indicate that the user has manipulated the object. A menu selection or a click on a control will generate a Carbon event of class kEventClassCommand and kind kEventCommandProcess containing the command ID associated with the command.
The Carbon event manager packages the command ID generated by a menu or control into a new object called an HICommand. This structure contains information about the command, such as where it was generated from (menu and item number), as well as the actual command ID. Currently, control information is not contained in this structure, but that will likely change in the future. The Toolbox uses this to determine how to propagate commands through the containment hierarchy. An HICommand that was generated from a menu will get sent first to the menu it came from, and then to the user focus, and so on down the chain. An HICommand generated from a control merely starts at the control it came from and down the containment hierarchy as any other control event would.
Commands are essential to a Carbon Event-based application. It is, in fact, a menu command that allows a standard Carbon Event-based app to quit. As long as the Quit menu item has the standard menu command of kHICommandQuit, your application will terminate normally. This is how the nib-based example we looked at earlier quits. When the kHICommandQuit event is sent from the menu selection, it eventually reaches the standard application-level Toolbox handler, where the Toolbox responds by sending a kEventQuitApplication event to the application target. If the kEventQuitApplication event is not handled, the Toolbox receives it in the same application-level Toolbox handler and call QuitApplicationEventLoop. This terminates the call to RunApplicationEventLoop, and eventually exits the program. If you happen to be a WaitNextEvent application, you won't quit automatically, but you could intercept the command event with an application-level event handler and trigger your normal quit process.
Commands are also important for menu item enabling. Whenever a menu is about to be displayed, the Menu Manager attempts to ensure each item is in the correct state, i.e. it is enabled or disabled, has the right text, and so forth. It does this by sending kEventCommandUpdateStatus events to the current user focus for each item in the menu. This is the time for the user focus to examine its current state and adjust its menu items as appropriate. For example, if the Edit menu was about to be displayed, you might want to enable the Cut and Copy menu items in your Edit menu if you have an active selection. If your application needs to change the name of the item (for example, to make the Undo item say "Undo Typing"), you can do that at the same time. Essentially, you should make sure the menu item which corresponds to the command sent as part of the kEventCommandUpdateStatus event is set up correctly for display and selection by the user.
Let's look at a small example of a handler that deals with kEventCommandUpdateStatus events.
Listing 4: A command status handler
OSStatus
MyCommandStatusHandler( EventHandlerCallRef inCallRef,
EventRef inEvent,
void * inUserData )
{
OSStatus result = eventNotHandledErr;
HICommand cmd;
MyObject* object = (MyObject*)inUserData;
// The direct object for a command event is the HICommand.
// Extract it here and switch off the command ID.
GetEventParameter( inEvent, kEventParamDirectObject,
typeHICommand, NULL, sizeof( cmd ), NULL, &cmd );
// Only deal with menu commands. The Toolbox will eventually send update status
// events for control commands as well, so we should limit our scope to menus if
// that's all we care about.
if ( (cmd.attributes & kHICommandFromMenu) != 0 )
{
// You'll note below that we enable and disable
// by menu ref and index rather than the command
// ID. This is because it is more efficient to do
// so, since access by command ID requires the
// Toolbox to search all menus. Either way is fine,
// but I thought I'd point that out.
switch ( cmd.commandID )
{
case kHICommandCopy:
case kHICommandCut:
if ( object->HasSelection() )
{
EnableMenuItem( cmd.menu.menuRef,
cmd.menu.menuItemIndex );
}
else
{
DisableMenuItem( cmd.menu.menuRef,
cmd.menu.menuItemIndex );
}
result = noErr;
break;
case kHICommandUndo:
if ( object->CanUndo )
{
EnableMenuItem( cmd.menu.menuRef,
cmd.menu.menuItemIndex );
SetMenuItemText( cmd.menu.menuRef,
cmd.menu.menuItemIndex,
object->GetUndoString );
}
else
{
DisableMenuItem( cmd.menu.menuRef,
cmd.menu.menuItemIndex );
SetMenuItemText( cmd.menu.menuRef,
cmd.menu.menuItemIndex,
CFSTR( "Can't Undo" ) );
}
result = noErr;
break;
}
}
return result;
}
As you can see from the listing, we look for the cut, copy, and undo commands in our handler. For cut and copy, we merely enable or disable the menu item depending on whether our object has a selection. For the undo command, not only do we enable or disable the item, but we also change the text of the menu item as appropriate. In each case, we return noErr to let the event system know this handler handled the event.
This very mechanism allows the editable text controls in the Toolbox to update the Edit menu appropriately depending on the current selection. For this reason, it is important that the Edit menu in your application uses the standard cut, copy, paste, etc. commands that are currently defined in CarbonEvents.h. If the Edit menu does not work right for a standard Toolbox text field, this is probably what's wrong. We used to have a really slimy mechanism in place to do this behind the app's back. On Mac OS X, that mechanism is gone, so we need a little cooperation from your application so we can all play in the same menu sandbox.
One thing to keep in mind — do not assume a command came from a menu! Always check the attributes of the command to make sure the kHICommandFromMenu bit is set. If not, don't try to access the menu.menuRef or menu.menuItemIndex fields. Remember that commands can come from controls, and Apple will be adjusting this structure in the future to accommodate that.
Modality
There are times in your application where you need to put up a modal dialog, i.e. it disallows clicks in other windows. Usually, when in this state, the menu bar is disabled except for a few choice menu items. In the past, you would have had to either use ModalDialog, or roll your own modal event loop.
The CEM offers a new way to go about this: RunAppModalLoopForWindow. Like RunApplicationEventLoop, once called, your application stays inside this API until a corresponding call to QuitAppModalLoopForWindow is made. Once you enter this state, the menu bar automatically disables. Depending on what command handlers you have installed for the window, you can enable certain menu items within your modal context. For example, even though the menu bar is disabled, if you tab into a text field, the Edit menu will react as it should, handling kEventCommandUpdateStatus events that are sent to it.
Usually when you implement something like this, you install an event handler for the window to capture command events. Inside this handler is where you would end up calling QuitAppModalLoopForWindow. Listing 5 shows an example of creating a dialog from a nib file and running it modally. We are essentially asking the user a yes or no question, and the result of the interaction with the user determines whether we return true or false.
Listing 5: Running a dialog modally
Boolean
ConfirmActivateSelfDestruct()
{
IBNibRef nibRef;
EventTypeSpec cmdEvent =
{ kEventClassCommand, kEventCommandProcess };
WindowRef window;
OSStatus err;
EventHandlerUPP handler;
ConfirmData data;
Boolean result = false;
// Get the nib reference, create the window from it, and release the nib reference.
err = CreateNibReference( CFSTR( "Main" ), &nibRef );
require_noerr( err, CantOpenNib );
err = CreateWindowFromNib( nibRef,
CFSTR( "ActivateSelfDestruct" ), &window );
require_noerr( err, CantCreateWindow );
DisposeNibReference( nibRef );
// Install an event handler to listen to command events for the window.
// This is the only type of event we care about!
// We pass data needed by our handler in to our call to InstallWindowEventHandler.
// Here we pass the window ref, as well as the magic boolean which tells us
// whether the self-destruct mechanism should be activated.
data.confirmed = false;
data.window = window;
handler = NewEventHandlerUPP( SelfDestructHandler );
InstallWindowEventHandler( window, handler, 1,
&cmdEvent, &data, NULL );
// Move the window into position and show it
RepositionWindow( window, NULL,
kWindowAlertPositionOnMainScreen );
ShowWindow( window );
// Now run the window modally. We will remain inside
// RunAppModalLoopForWindow until the Quit... call is made
// in our EraseDiskHandler routine.
RunAppModalLoopForWindow( window );
// We're done! Get our result, dispose the window, and exit
result = data.confirmed;
DisposeWindow( window );
DisposeEventHandlerUPP( handler );
return result;
CantCreateWindow:
DisposeNibReference( nibRef );
CantOpenNib:
return result;
}
If you look closely, you will see it's extremely similar to our main function in our basic application we looked at earlier. The only real difference is we have installed a window event handler on the window we create to handle command events. The next listing shows a simple example of such a handler:
Listing 6: Our self-destruct modal dialog handler
OSStatus
SelfDestructHandler(EventHandlerCallRef inCallRef,
EventRef inEvent,
void* inUserData)
{
HICommand cmd;
OSStatus result = eventNotHandledErr;
ConfirmData* data = (ConfirmData *)inUserData;
// The direct object for a ‘process command' event is the HICommand.
// Extract it here and switch off the command ID.
GetEventParameter( inEvent, kEventParamDirectObject,
typeHICommand, NULL, sizeof( cmd ), NULL, &cmd );
switch ( cmd.commandID )
{
case kHICommandOK:
// The OK button was clicked. Store true into our magic boolean.
data->confirmed = true;
// Stick a fork in us, we're done. Terminate the call to
// RunAppModalLoopForWindow and return noErr to indicate
// we handled this command.
QuitAppModalLoopForWindow( data->window );
result = noErr;
break;
case kHICommandCancel:
// The user clicked cancel. Our data was passed in to us with
// false set, but since we don't want to risk the ship based on
// my assumptions, we set it to false explicitly! We then quit
// the modal loop and return noErr to indicate handled this event.
data->confirmed = false;
QuitAppModalLoopForWindow( data->window );
result = noErr;
break;
}
return result;
}
If you're wondering where these commands are coming from, they are being read in as part of the nib file. For any control in a nib, you can set a command ID for it. When the nib is read in and the controls are created, any command ID you specified will automatically be set for the control. In our example, if the kHICommandCancel or kHICommandOK commands are received, we call QuitAppModalLoopForWindow, which terminates our modal loop.
The Dialog What?
The interesting thing with the examples in the last section is that you'll notice that with the combination of nibs and commands, the Dialog Manager is obsolete. The only thing we didn't cover is being able to access controls by control ID. This is something new in Carbon that allows you to get at your controls with a unique identifier. For dialog items using the Dialog Manager you would have typically used GetDialogItem or GetDialogItemAsControl, passing the dialog item number of the item you were interested in. That has a major failing in that if you reorder your items, you need to change all your constants, as the dialog item number is just an index into the dialog item list. Control IDs allow position independence — it doesn't matter where the control is in the control hierarchy for a window, you can easily find it with the control ID. If you move it, you'll still find it with the same code. Interface Builder lets you assign IDs to controls so that when you instantiate an object from a nib, the control IDs are all set to go.
The other thing the Dialog Manager has historically kept track of is the default and cancel items of a dialog. The default item is the item that would be ‘hit' by pressing enter or return, and the cancel item is the item which would be ‘hit' by pressing command-period or escape on the keyboard. To replace that, we have introduced SetWindowDefaultButton and SetWindowCancelButton. The standard Toolbox event handlers for windows check for default and cancel button ‘hits' like the Dialog Manager would. Again, Interface Builder lets you specify that a button is the default or cancel button.
All of this means you can now lay out dialogs with Interface Builder (which is really easy!), and run them with the Carbon Event Model, and you don't have to worry about the Dialog Manager's idiosyncrasies from 1984.
Ow! My brain hurts!
Wow. That was a lot of information! But I think we covered all of the basic aspects of the Carbon Event Model and how they work. Now you have a good foundation of knowledge upon which you can start building. In future articles, we can start moving into concrete examples and more esoteric aspects of all this.
As you can see, it's a very exciting addition to the Toolbox. We see it as the piece that has been missing for too long which will allow the Toolbox to grow into something really amazing as we move forward. Any new functionality we add will generally involve some aspect of this new event system, so it's important to understand it.
As was evident from the examples, you can write a lot of application in very little code. In the future, we will be able to reduce that code even further, possibly to as little as 3 lines of code to start up an application! We will also be able to add more functionality for free while still allowing your applications to alter behaviors. I encourage you all to start playing with this stuff and see what it can do.
It's the beginning of a whole new era for Carbon application programming!
Special thanks to Eric Schlegel, Guy Fullerton, and Matt Ackeret for reviewing this article.
Ed Voas is the principal architect and developer of Carbon Events. He is also the Manager/Tech Lead of the Carbon High Level Toolbox. When not coding, he is usually practicing Shotokan Karate or attempting to climb a solid 5.5. He can be reached at voas@apple.com. Maybe.