Event Handling Basics

Some tasks found in event-handling code are common to events of more than one type. The following sections describe these basic event-handling tasks. Some of the information presented in this chapter is discussed at length, but using different examples, in the Creating a Custom View chapter of View Programming Guide.

Preparing a Custom View for Receiving Events

Although any type of responder object can handle events, NSView objects are by far the most common recipient of events. They are typically the first objects to respond to both mouse events and key events, often changing their appearance in reaction to those events. Many Application Kit classes are implemented to handle events, but by itself NSView does not. When you create your own custom view and you want it to respond to events, you have to add event-handling capabilities to it.

The bare-minimum steps required for this are few:

A view that is first responder accepts key events and action messages before other objects in a window. It also typically takes part in the key loop (see Keyboard Interface Control). By default, view objects refuse first-responder status by returning NO in acceptsFirstResponder. If you want your custom view to respond to key events and actions, your class must override this method to return YES:

- (BOOL)acceptsFirstResponder {
    return YES;
}

The second item in the list above—overriding an NSResponder method to handle an event—is of course a large subject and what most remaining chapters in this document are about. But there are a few basic guidelines to consider for event messages:

Implementing Action Methods

Action messages are typically sent by NSMenuItem objects or by NSControl objects. The latter objects usually work together with one or more NSCell objects. The cell object stores a method selector identifying the action message to be sent and a reference to the target object. (A menu item encapsulates its own action and target data.) When a menu item or control object is clicked or otherwise manipulated, it gets the action selector and target object—in the control’s case, from one of its cells—and sends the message to the target.

You can set the action selector and target programmatically using, respectively, the methods setAction: and setTarget: (declared by NSActionCell, NSMenuItem, and other classes). However, you typically specify these in Interface Builder. In this application, you connect a control object to another object (the target) in the nib file by Control-dragging from the control object to the target and then selecting the action method of the target to invoke. If you want the action message to be untargeted, you can either set the target to nil programmatically or, in Interface Builder, make a connection between the menu item or control and the First Responder icon in the nib file window, as shown in Figure 3-1.

Figure 3-1  Connecting an untargeted action in Interface Builder
Connecting an untargeted action in Interface Builder

From Interface Builder you can generate a header file and implementation file for your Xcode project that include a declaration and skeletal implementation, respectively, for each action method defined for a class. These Interface Builder–defined methods have a return “value” of IBAction, which acts as a tag to indicated that the target-action connection is archived in a nib file. You can also add the declarations and skeletal implementations of action methods yourself; in this case, the return type is void.) The remaining required part of the signature is a single parameter typed as id and named (by convention) sender.

Listing 3-1 illustrates a straightforward implementation of an action method that toggles a clock’s AM-PM indicator when a user clicks a button.

Listing 3-1  Simple implementation of an action method

- (IBAction)toggleAmPm:(id)sender {
    [self incrementHour:12 andMinute: 0];
}

Action methods, unlike NSResponder event methods, don’t have default implementations, so responder subclasses shouldn’t blindly forward action messages to super. The passing of action messages up the responder chain in the Application Kit is predicated merely on whether an object responds to the method, unlike with the passing of event messages. Of course, if you know that a superclass does in fact implement the method, you can pass it on up from your subclass, but otherwise don’t.

An important feature of action messages is that you can send messages back to sender to get further information or associated data. For example, the menu items in a given menu might have represented objects assigned to them; for example, a menu item with a title of “Red” might have a represented object that is an NSColor object. You can access this object by sending representedObject to sender.

You can take the messaging-back feature of action methods one step further by using it to dynamically change sender’s target, action, title, and similar attributes. Here is a simple test case: You want to control a progress indicator (an NSProgressIndicator object) with a single button; one click of the button starts the indicator and changes the button’s title to “Stop”, and then the next click stops the indicator and changes the title to back to “Start”. Listing 3-2 shows one way to do this.

Listing 3-2  Resetting target and action of sender—good implementation

- (IBAction)controlIndicator:(id)sender
{
    [[sender cell] setTarget:indicator]; // indicator is NSProgressIndicator
    if ( [[sender title] isEqualToString:@"Start"] ) {
        [[sender cell] setAction:@selector(startAnimation:)];
        [sender setTitle:@"Stop"];
    } else {
        [[sender cell] setAction:@selector(stopAnimation:)];
        [sender setTitle:@"Start"];
    }
    [[sender cell] performClick:self];
    // set target and action back to what they were
    [[sender cell] setAction:@selector(controlIndicator:)];
    [[sender cell] setTarget:self];
}

However, this implementation requires that the target and action information be set back to what they were after the redirected action message is sent via performClick:. You could simplify this implementation by invoking directly sendAction:to:from:, the method used by the application object (NSApp) to dispatch action messages (see Listing 3-3).

Listing 3-3  Resetting target and action of sender—better implementation

- (IBAction)controlIndicator:(id)sender
{
    SEL theSelector;
    if ( [[sender title] isEqualToString:@"Start"] ) {
        theSelector = @selector(startAnimation:);
        [sender setTitle:@"Stop"];
    } else {
        theSelector = @selector(stopAnimation:);
        [sender setTitle:@"Start"];
    }
    [NSApp sendAction:theSelector to:indicator from:sender];
}

In keyboard action messages, an action method is invoked as a result of a particular key-press being interpreted through the key bindings mechanism. Because such messages are so closely connected to specific key events, the implementation of the action method can get the event by sending currentEvent to NSApp and then query the NSEvent object for details. Listing 3-7 gives an example of this technique. See The Path of Key Events for a summary of keyboard action messages; also see Key Bindings for a description of that mechanism.

Getting the Location of an Event

You can get the location of a mouse or tablet-pointer event by sending locationInWindow to an NSEvent object. But, as the name of the method denotes, this location (an NSPoint structure) is in the base coordinate system of a window, not in the coordinate system of the view that typically handles the event. Therefore a view must convert the point to its own coordinate system using the method convertPoint:fromView:, as shown in Listing 3-4.

Listing 3-4  Converting a mouse-dragged location to be in a view’s coordinate system

- (void)mouseDragged:(NSEvent *)event {
    NSPoint eventLocation = [event locationInWindow];
    center = [self convertPoint:eventLocation fromView:nil];
    [self setNeedsDisplay:YES];
}

The second parameter of convertPoint:fromView: is nil to indicate that the conversion is from the window’s base coordinate system.

Keep in mind that the locationInWindow method is not appropriate for key events, periodic events, or any other type of event other than mouse and tablet-pointer events.

Testing for Event Type and Modifier Flags

On occasion you might need to discover the type of an event. However, you do not need to do this within an event-handling method of NSResponder because the type of the event is apparent from the method name: rightMouseDragged:, keyDown:, tabletProximity:, and so on. But from elsewhere in an application you can always obtain the currently handled event by sending currentEvent to NSApp. To find out what type of event this is, send type to the NSEvent object and then compare the returned value with one of the NSEventType constants. Listing 3-5 gives an example of this.

Listing 3-5  Testing for event type

NSEvent *currentEvent = [NSApp currentEvent];
NSPoint mousePoint = [controlView convertPoint: [currentEvent locationInWindow] fromView:nil];
switch ([currentEvent type]) {
    case NSLeftMouseDown:
    case NSLeftMouseDragged:
        [self doSomethingWithEvent:currentEvent];
        break;
    default:
        // If we find anything other than a mouse down or dragged we are done.
         return YES;
}

A common test performed in event-handling methods is finding out whether specific modifier keys were pressed at the same moment as a key-press, mouse click, or similar user action. The modifier key usually imparts special significance to the event. The code example in Listing 3-6 shows an implementation of mouseDown: that determines whether the Command key was pressed while the mouse was clicked. If it was, it rotates the receiver (a view) 90 degrees. The identification of modifier keys requires the event handler to send modifierFlags to the passed-in event object and then perform a bitwise-AND operation on the returned value using one or more of the modifier mask constants declared in NSEvent.h.

Listing 3-6  Testing for modifier keys pressed—event method

- (void)mouseDown:(NSEvent *)theEvent {
 
    //  if Command-click rotate 90 degrees
    if ([theEvent modifierFlags] & NSCommandKeyMask) {
        [self setFrameRotation:[self frameRotation]+90.0];
        [self setNeedsDisplay:YES];
    }
}

You can test an event object to find out if any from a set of modifier keys was pressed by performing a bitwise-OR with the modifier-mask constants, as in Listing 3-7. (Note also that this example shows the use of the currentEvent method in a keyboard action method.)

Listing 3-7  Testing for modifier keys pressed—action method

- (void)moveLeft:(id)sender {
    // Use left arrow to decrement the time.  If a shift key is down, use a big step size.
    BOOL shiftKeyDown = ([[NSApp currentEvent] modifierFlags] &
        (NSShiftKeyMask | NSAlphaShiftKeyMask)) !=0;
    [self incrementHour:0 andMinute:-(shiftKeyDown ? 15 : 1)];
}

Further, you can look for certain modifier-key combinations by linking the individual modifier-key tests together with the logical AND operator (&&). For example, if the example method in Listing 3-6 were to look for Command-Shift-click rather than Command-click, the complete test would be the following:

    if ( ( [theEvent modifierFlags] & NSCommandKeyMask ) &&
        ( [theEvent modifierFlags] & NSShiftKeyMask ) )

In addition to testing an NSEvent object for individual event types, you can also restrict the events fetched from the event queue to specified types. You can perform this filtering of event types in the nextEventMatchingMask:untilDate:inMode:dequeue: method (NSApplication and NSWindow) or the nextEventMatchingMask: method of NSWindow. The second parameter of all these methods takes one or more of the event-type mask constants declared in NSEvent.h—for example NSLeftMouseDraggedMask, NSFlagsChangedMask, and NSTabletProximityMask. You can either specify these constants individually or perform a bitwise-OR on them.

Because the nextEventMatchingMask:untilDate:inMode:dequeue: method is used almost exclusively in closed loops for processing a related series of mouse events, the use of it is described in Handling Mouse Events.

Responder-Related Tasks

The following sections describe tasks related to the first-responder status of objects.

Determining First-Responder Status

Usually an NSResponder object can always determine if it's currently the first responder by asking its window (or itself, if it's an NSWindow object) for the first responder and then comparing itself to that object. You ask an NSWindow object for the first responder by sending it a firstResponder message. For an NSView object, this comparison would look like the following bit of code:

if ([[self window] firstResponder] == self) {
      // do something based upon first-responder status
}

A complication of this simple scenario occurs with text fields. When a text field has input focus, it is not the first responder. Instead, the field editor for the window is the first responder; if you send firstResponder to the NSWindow object, an NSTextView object (the field editor) is what is returned. To determine if a given NSTextField is currently active, retrieve the first responder from the window and find out it is an NSTextView object and if its delegate is equal to the NSTextField object. Listing 3-8 shows how you might do this.

Listing 3-8  Determining if a text field is first responder

if ( [[[self window] firstResponder] isKindOfClass:[NSTextView class]] &&
   [window fieldEditor:NO forObject:nil] != nil ) {
        NSTextField *field = [[[self window] firstResponder] delegate];
        if (field == self) {
            // do something based upon first-responder status
        }
}

The control that a field editor is editing is always the current delegate of the field editor, so (as the example shows) you can obtain the text field by asking for the field editor's delegate. For more on the field editor, see Text Fields, Text Views, and the Field Editor.

Setting the First Responder

You can programmatically change the first responder by sending makeFirstResponder: to an NSWindow object; the argument of this message must be a responder object (that is, an object that inherits from NSResponder). This message initiates a kind of protocol in which one object loses its first responder status and another gains it.

  1. makeFirstResponder: always asks the current first responder if it is ready to resign its status by sending it resignFirstResponder.

  2. If the current first responder returns NO when sent this message, makeFirstResponder: fails and likewise returns NO.

    A view object or other responder may decline to resign first responder status for many reasons, such as when an action is incomplete.

  3. If the current first responder returns YES to resignFirstResponder, then the new first responder is sent a becomeFirstResponder message to inform it that it can be the first responder.

  4. This object can return NO to reject the assignment, in which case the NSWindow itself becomes the first responder.

Figure 3-2 and Figure 3-3 illustrate two possible outcomes of this protocol.

Figure 3-2  Making a view a first responder—current view refuses to resign status
Making a view a first responder—current view refuses to resign status
Figure 3-3  Making a view a first responder—new view becomes first responder
Making a view a first responder—new view becomes first responder

Listing 3-9 shows a custom NSCell class (in this case a subclass of NSActionCell) implementing resignFirstResponder and becomeFirstResponder to manipulate the keyboard focus ring of its superclass.

Listing 3-9  Resigning and becoming first responder

- (BOOL)becomeFirstResponder {
    BOOL okToChange = [super becomeFirstResponder];
    if (okToChange) [self setKeyboardFocusRingNeedsDisplayInRect: [self bounds]];
    return okToChange;
}
 
- (BOOL)resignFirstResponder {
    BOOL okToChange = [super resignFirstResponder];
    if (okToChange) [self setKeyboardFocusRingNeedsDisplayInRect: [self bounds]];
    return okToChange;
}

You can also set the initial first responder for a window—that is, the first responder set when the window is first placed onscreen. You can set the initial first responder programmatically by sending setInitialFirstResponder: to an NSWindow object. You can also set it when you create a user interface in Interface Builder. To do this, complete the following steps.

  1. Control-drag from the window icon in the nib file window to an NSView object in the user interface.

  2. In the Connections pane of the inspector, select the initialFirstResponder outlet and click Connect.

After awakeFromNib is called for all objects in a nib file, NSWindow makes the first responder whatever was set as initial first responder in the nib file. Note that you should not send setInitialFirstResponder: to an NSWindow object in awakeFromNib and expect the message to be effective.