Mouse tracking in Cocoa

I’ve just implemented mouse tracking in my (tentatively named) GFF Viewer app and thought I’d share my experiences doing so. Hopefully this will make life a bit easier for others trying to implement similar functionality, since Apple’s documentation on the matter is spread across a few different documents.

Tracking rectangles
By default, the Application Kit doesn’t provide pixel-by-pixel notifications of the cursor’s onscreen location. This is because the frequency of “mouse-moved” events is so high that constantly tracking them would clog up the event queue (as noted here). Instead, AppKit puts NSWindow in charge of a lighter-weight system that utilises tracking rectangles.

A tracking rectangle is an area onscreen owned by an object (most commonly the NSView subclass in which the rectangle resides) that causes NSMouseEntered and NSMouseExited events to be sent to the owning object when the cursor moves into and out of the area.

Adding a tracking rectangle
Tracking rectangles are added using the NSView method addTrackingRect:owner:userData:assumeInside:. For example, from within an NSView object, you might call [self addTrackingRect:[self visibleRect] owner:self userData:nil assumeInside:NO]
to add a tracking rectangle that covers the whole visible area of the view.

The addTrackingRect: owner: userData: assumeInside: method should be called from NSView’s viewDidMoveToWindow: method rather than initWithFrame:. Although the tracking rectangles are added and removed by NSView, NSWindow actually manages the current list of rectangles and, when initWithFrame: is called, NSView doesn’t yet have a parent window.

addTrackingRect: owner: userData: assumeInside: returns an NSTrackingRectTag (actually just an integer) to uniquely identify the tracking rectangle, allowing for its removal by calling NSView’s – (void)removeTrackingRect:(NSTrackingRectTag)tag method. As recommended here, this should be called as such (from the NSView object):
- (void)viewWillMoveToWindow:(NSWindow *)newWindow {
   if ( [self window] && trackingRect ) {
      [self removeTrackingRect:trackingRect];

This ensures that the tracking rectangle is removed when the view is deallocated (or, as the method name implies, is moved to another window).

Keeping tracking rectangles up to date
A side-effect of tracking rectangles being overseen by NSWindow is that they are “static” and will not update as your NSView subclass resizes, scrolls, transforms itself etc. As such, Apple recommends that tracking rectangles are removed and then re-established in NSView’s setBounds: and setFrame: methods. However, I’ve found that using resetCursorRects: to achieve the same thing works very well. My NSView subclass’s resetCursorRects: looks like this:
   [super resetCursorRects];
   [self clearTrackingRect];
   [self setTrackingRect];

where clearTrackingRect: calls [self removeTrackingRect: trackingTag] and setTrackingRect: calls trackingTag = [self addTrackingRect:[self visibleRect] owner:self ­userData:nil assumeInside:NO] respectively.

Implementing mouseMoved:
The way I’ve set mouse tracking up in GFF Viewer is to (as alluded to above) have one large tracking rectangle covering my entire view and activate “mouse-moved” events when the mouse moves into that tracking rectangle. To receive NSMouseMoved events, two conditions must first be met:

  1. the view responding to the events must be the first responder
  2. the parent window must told to accept NSMouseMoved events by sending it a setAcceptsMouseMovedEvents:YES

Both of these conditions can be put in place when the mouseEntered: event is received for the tracking rectangle covering the view.

I’m currently using the following mouseEntered: method in my NSView subclass:
- (void)mouseEntered:(NSEvent *)theEvent
   mouseInView = YES;
   windowWasAcceptingMouseEvents = [[self window] acceptsMouseMovedEvents];
   [[self window] setAcceptsMouseMovedEvents:YES];
   [[self window] makeFirstResponder:self];
   [self displayIfNeeded];

where windowWasAcceptingMouseEvents is just a BOOL to note whether or not the window was accepting mouse events before this method was called. mouseInView is also a BOOL for my own use in my drawRect method. My mouseExited: method effectively does the reverse (although it leaves the view as the first responder until another object claims that status):
- (void)mouseExited:(NSEvent *)theEvent
   mouseInView = NO;
   [[self window] setAcceptsMouseMovedEvents:windowWasAcceptingMouseEvents];
   [self setNeedsDisplay:YES];

Actually following the mouse
With the above methods in place, the time between mouseEntered: and mouseExited: is spent actually tracking mouse movements in the mouseMoved:. My implementation of this method reads simply:
- (void)mouseMoved:(NSEvent *)theEvent
   cursorLocation = [self convertPoint:[theEvent locationInWindow] fromView:nil];
   [self setNeedsDisplay:YES];

The convertPoint:fromView: method is included because the point returned is in window coordinates, rather than view coordinates, which is usually undesirable. cursorLocation is an NSPoint instance variable that I use in drawRect: to draw a translucent blue bar that follows the mouse left and right across my NSView, like this:
GFF Viewer with mouse tracking

Leave a Reply

Your email address will not be published. Required fields are marked *