Using Tracking-Area Objects

An instance of the NSTrackingArea class defines an area of a view that is responsive to the movements of the mouse. When the mouse cursor enters this area, moves around in it, and leaves it, the Application Kit sends (depending on the options specified) mouse-tracking, mouse-moved, and cursor-update messages to a designated object.

The Application Kit sends the mouse-tracking messages mouseEntered: and mouseExited: to an object when the mouse cursor enters and exits a region of a window. Mouse tracking enables the view owning the region to respond, for example, by drawing a highlight color or displaying a tool tip. The Application Kit also sends mouseMoved: messages to the object if NSMouseMoved types of events are requested. Cursor-update events are a special kind of mouse-tracking event that the Application Kit handles automatically. When the mouse pointer enters a cursor rectangle, the Application Kit displays a cursor image appropriate to the type of view under the rectangle; for example, when a mouse pointer enters a text view, an I-beam cursor is displayed.

The sections in this chapter describe how to create NSTrackingArea objects, attach them to views, respond to related events, and manage the objects when changes in view geometry occur.

Creating an NSTrackingArea Object

An NSTrackingArea object defines a region of a view that is sensitive to the movements of the mouse. When the mouse enters, moves about, and exits that region, the Application Kit sends mouse-tracking, mouse-moved, and cursor-update messages. The region is a rectangle specified in the local coordinate system of its associated view. The recipient of the messages (the owner) is specified when the tracking-area object is created; although the owner can be any object, it is often the view associated with the tracking-area object.

When you create an NSTrackingArea object you must specify one or more options. These options, which are enumerated constants declared by the class, configure various aspects of tracking-area behavior. They fall into three general categories:

You initialize an allocated instance of NSTrackingArea with the initWithRect:options:owner:userInfo: method. After creating the object, you must associate it with a view by invoking the NSView method addTrackingArea:. You can create an NSTrackingArea instance and add it to a view at any point because (unlike the mouse-tracking API in earlier releases), successful creation does not depend on the view being added to a window. Listing 6-1 shows the creation and addition of an NSTrackingArea instance in a custom view’s initWithFrame: method; in this case, the owning view is requesting that the Application Kit send mouseEntered:, mouseExited: and mouseMoved: messages whenever its window is the key window.

Listing 6-1  Initializing an NSTrackingArea instance

- (id)initWithFrame:(NSRect)frame {
    self = [super initWithFrame:frame];
    if (self) {
        trackingArea = [[NSTrackingArea alloc] initWithRect:eyeBox
            options: (NSTrackingMouseEnteredAndExited | NSTrackingMouseMoved | NSTrackingActiveInKeyWindow )
            owner:self userInfo:nil];
        [self addTrackingArea:trackingArea];
    }
    return self;
}

The last parameter of initWithRect:options:owner:userInfo: is a dictionary that you can use to pass arbitrary data to the recipient of mouse-tracking and cursor-update messages. The receiving object can access the dictionary by sending userData to the NSEvent object passed into the mouseEntered:, mouseExited:, or cursorUpdate: method. Sending userData messages in mouseMoved: methods causes an assertion failure. (In the example in Listing 6-1, the userInfo parameter is set to nil.)

Tracking rectangle bounds are inclusive for the top and left edges, but not for the bottom and right edges. Thus, if you have a unflipped view with a tracking rectangle covering its bounds, and the view’s frame has the geometry frame.origin = (100, 100), frame.size = (200, 200), then the area for which the tracking rectangle is active is frame.origin = (100, 101), frame.size = (199, 199), in frame coordinates.

Managing a Tracking-Area Object

Because NSTrackingArea objects are owned by their views, the Application Kit can automatically recompute the tracking-area regions when the view is added or removed from a window or when the view changes position within its window. But in situations where the Application Kit cannot recompute an affected tracking area (or areas), it sends updateTrackingAreas to the associated view, asking it to recompute and reset the areas. One such situation is when a change in view location affects the view’s visible rectangle (visibleRect)—unless the NSTrackingArea object for that view was created with the NSTrackingInVisibleRect option, in which case the Application Kit handles the re-computation. Note that the Application Kit sends updateTrackingAreas to every view whether it has a tracking area or not.

You can override the updateTrackingAreas method as shown in Listing 6-2 to remove the current tracking areas from their views, release them, and then add new NSTrackingArea objects to the same views with recomputed regions.

Listing 6-2  Resetting a tracking-area object

- (void)updateTrackingAreas {
    NSRect eyeBox;
    [self removeTrackingArea:trackingArea];
    [trackingArea release];
    eyeBox = [self resetEye];
    trackingArea = [[NSTrackingArea alloc] initWithRect:eyeBox
        options: (NSTrackingMouseEnteredAndExited | NSTrackingMouseMoved | NSTrackingActiveInKeyWindow)
        owner:self userInfo:nil];
    [self addTrackingArea:trackingArea];
}

If your class is not a custom view class, you can register your class instance as an observer for the notification NSViewFrameDidChangeNotification and have it reestablish the tracking rectangles on receiving the notification.

Responding to Mouse-Tracking Events

The owner of an NSTrackingArea object created with the NSTrackingMouseEnteredAndExited option receives a mouseEntered: whenever the mouse cursor enters the region of a view defined by the tracking-area object; subsequently, it receives a mouseExited: messages when the mouse leaves that region. If the NSTrackingMouseMoved option is also specified for the tracking-area object, the owner also receives one or more mouseMoved: messages between each mouseEntered: and mouseExited: message. You override the corresponding NSResponder methods to handle these messages to perform such tasks as highlighting the view, displaying a custom tool tip, or displaying related information in another view.

The tracking code in Listing 6-2 is used in making an “eyeball” follow the movement of the mouse pointer when it enters a tracking rectangle.

Listing 6-3  Handling mouse-entered, mouse-moved, and mouse-exited events

- (void)mouseEntered:(NSEvent *)theEvent {
    NSPoint eyeCenter = [self convertPoint:[theEvent locationInWindow] fromView:nil];
    eyeBox = NSMakeRect((eyeCenter.x-10.0), (eyeCenter.y-10.0), 20.0, 20.0);
    [self setNeedsDisplayInRect:eyeBox];
    [self displayIfNeeded];
}
 
- (void)mouseMoved:(NSEvent *)theEvent {
    NSPoint eyeCenter = [self convertPoint:[theEvent locationInWindow] fromView:nil];
    eyeBox = NSMakeRect((eyeCenter.x-10.0), (eyeCenter.y-10.0), 20.0, 20.0);
    [self setNeedsDisplayInRect:eyeBox];
    [self displayIfNeeded];
}
 
- (void)mouseExited:(NSEvent *)theEvent {
    [self resetEye];
    [self setNeedsDisplayInRect:eyeBox];
    [self displayIfNeeded];
}

Just as you can with mouse-down and mouse-up messages, you can query the passed-in NSEvent objects to get information related to the event.

Managing Cursor-Update Events

One common use of NSTrackingArea objects is to change the cursor image over different types of views. Text, for example, typically requires an I-beam cursor. Many Application Kit classes provide cursor images appropriate to their view instances; you get this behavior “for free.“ However, you may want to specify a specific (or different) cursor image for instances of your custom view subclasses.

You change the cursor image for your view in an override of the NSResponder method cursorUpdate:. To receive this message, you must create an NSTrackingArea object by invoking the initWithRect:options:owner:userInfo: initializer with an option of NSTrackingCursorUpdate (along with any other desired options). Then add the created object to a view with addTrackingArea:. Thereafter, the entry of the mouse into the tracking area generates an NSCursorUpdate event; the NSWindow object handles this event by sending a cursorUpdate: message to the owner of the tracking area (which is typically the view itself). The implementation of cursorUpdate: should use the appropriate NSCursor methods to set a standard or custom cursor image.

Cursor rectangles may overlap or be completely nested, one within the other. Arbitration of cursor updates follows the normal responder chain mechanism. The cursor rectangle of the view under the mouse cursor first receives the cursorUpdate: message. It may either display a cursor or pass the message up the responder chain, where a view with an overlapped cursor rectangle can then respond to the message.

Listing 6-4 shows an implementation of cursorUpdate: that sets the cursor of the view to a cross-hair image. Note that it is not necessary to reset the cursor image back to what it was when the mouse exits the tracking area. The Application Kit handles this automatically for you by sending a cursorUpdate: message to the view over which the mouse cursor moves as it exits the cursor rectangle.

Listing 6-4  Handling a cursor-update event

-(void)cursorUpdate:(NSEvent *)theEvent
{
    [[NSCursor crosshairCursor] set];
}

If the responder owning the tracking-area object does not implement the cursorUpdate: method, the default implementation forwards the message up the responder chain. If the responder implements cursorUpdate: but decides not to handle the current event, it should invoke the superclass implementation of cursorUpdate:.

As with any other kind of NSTrackingArea object, you might occasionally need to recompute and recreate a tracking-area object used for cursor updates when the associated view has changes in its location or size. See “Managing a Tracking-Area Object” for more information.

Compatibility Issues

Beginning with OS X v10.5, the NSTrackingArea class and the related NSView methods addTrackingArea:, removeTrackingArea:, updateTrackingAreas, and trackingAreas replace the following methods of NSView, which are considered legacy API but remain supported for compatibility:

The NSTrackingRectTag type, returned by the addTrackingRect:owner:userData:assumeInside: and passed into the removeTrackingRect: methods, is also a legacy type. Internally this type is treated the same as NSTrackingArea *.

The underlying implementation for the legacy methods is based on NSTrackingArea, resulting in the following implications:

In addition, the following NSWindow cursor-related methods are legacy API, but maintained for compatibility: