Handling Mouse Events

Mouse events are one of the two most frequent kinds of events handled by an application (the other kind being, of course, key events). Mouse clicks—which involve a user pressing and then releasing a mouse button—generally indicate selection, but what the selection means is left up to the object responding to the event. For example, a mouse click could tell the responding object to alter its appearance and then send an action message. Mouse drags generally indicate that the receiving view should move itself or a drawn object within its bounds. The following sections describe how you might handle mouse-down, mouse-up, and mouse-drag events.

Overview of Mouse Events

Before going into the “how to” of mouse-event handling, let’s review some central facts about mouse events discussed in Event Architecture, Event Objects and Types, and Event Handling Basics:

Handling Mouse Clicks

One of the earliest things to consider in handling mouse-down events is whether the receiving NSView object should become the first responder, which means that it will be the first candidate for subsequent key events and action messages. Views that handle graphic elements that the user can select—drawing shapes or text, for example—should typically accept first responder status on a mouse-down event by overriding the acceptsFirstResponder method to return YES, as discussed in Preparing a Custom View for Receiving Events.

By default, a mouse-down event in a window that isn’t the key window simply brings the window forward and makes it key; the event isn’t sent to the NSView object over which the mouse click occurs. The NSView can claim an initial mouse-down event, however, by overriding acceptsFirstMouse: to return YES. The argument of this method is the mouse-down event that occurred in the non-key window, which the view object can examine to determine whether it wants to receive the mouse event and potentially become first responder. You want the default behavior of this method in, for example, a control that affects the selected object in a window. However, in certain cases it’s appropriate to override this behavior, such as for controls that should receive mouseDown: messages even when the window is inactive. Examples of controls that support this click-through behavior are the title-bar buttons of a window.

In your implementation of an NSResponder mouse-event method, often the first thing you might do is examine the passed-in NSEvent object to decide if this is an event you want to handle. If it is an event that you handle, then you may need to extract information from the NSEvent object to help you handle it. Specifically, you can get the following information from the NSEvent object:

Many view objects in the Application Kit (such as controls and menu items) change their appearance in response to mouse-down events, sometimes only until the subsequent mouse-up event. Doing this provides visual confirmation to the user that their action is effective or that the clicked object is now selected. Listing 4-1 shows a simple example of this.

Listing 4-1  Simple handling of mouse click—changing view’s appearance

- (void)mouseDown:(NSEvent *)theEvent {
    [self setFrameColor:[NSColor redColor]];
    [self setNeedsDisplay:YES];
}
 
- (void)mouseUp:(NSEvent *)theEvent {
    [self setFrameColor:[NSColor greenColor]];
    [self setNeedsDisplay:YES];
}
 
- (void)drawRect:(NSRect)rect {
    [[self frameColor] set];
    NSRectFill(rect);
}

But many view objects, particularly controls and cells, do more than simply change their appearance in response to mouse clicks. One common paradigm is for the clicked view to send an action message to a target object (where both action and target are settable properties of the view). As shown in Listing 4-2, the view typically sends the message on mouseUp: rather than mouseDown:, thus giving users an opportunity to change their minds mid-click.

Listing 4-2  Simple handling of mouse click—sending an action message

- (void)mouseDown:(NSEvent *)theEvent {
    [self setFrameColor:[NSColor redColor]];
    [self setNeedsDisplay:YES];
}
 
- (void)mouseUp:(NSEvent *)theEvent {
    [self setFrameColor:[NSColor greenColor]];
    [self setNeedsDisplay:YES];
    [NSApp sendAction:[self action] to:[self target] from:self];
}
 
- (SEL)action {return action; }
 
- (void)setAction:(SEL)newAction {
    action = newAction;
}
 
- (id)target { return target; }
 
- (void)setTarget:(id)newTarget {
    target = newTarget;
}

Listing 4-3 gives a more complex, real-world example. (It’s from the example project for the Sketch application.) This implementation of mouseDown: determines if users double-clicked a graphical object and, if they did, enables the editing of that object. Otherwise, if a palette object is selected, it creates an instance of that object at the location of the mouse click.

Listing 4-3  Handling a mouse-down event—Sketch application

- (void)mouseDown:(NSEvent *)theEvent {
    Class theClass = [[SKTToolPaletteController sharedToolPaletteController] currentGraphicClass];
    if ([self editingGraphic]) {
        [self endEditing];
    }
    if ([theEvent clickCount] > 1) {
        NSPoint curPoint = [self convertPoint:[theEvent locationInWindow] fromView:nil];
        SKTGraphic *graphic = [self graphicUnderPoint:curPoint];
        if (graphic && [graphic isEditable]) {
            [self startEditingGraphic:graphic withEvent:theEvent];
            return;
        }
    }
    if (theClass) {
        [self clearSelection];
        [self createGraphicOfClass:theClass withEvent:theEvent];
    } else {
        [self selectAndTrackMouseWithEvent:theEvent];
    }
}

The classes of the Application Kit that implement controls manage this target-action behavior for you.

Handling Mouse Dragging Operations

Mouse-down and mouse-up events, which are the events associated with mouse clicks, are not the only types of mouse events dispatched in an application. Views can also receive mouse-dragged events when a user moves a mouse while pressing down a mouse button. A view typically interprets mouse-dragged events as commands to move itself by altering its frame location or to move a region within its bounds by redrawing it. However, other interpretations of mouse-dragged events are possible; for example, a view could respond to mouse-dragged events by magnifying the region that the mouse pointer is dragged over.

With the Application Kit you can take one of two general approaches when handling mouse-dragged events. The first approach is overriding the three NSResponder methods mouseDown:, mouseDragged:, and mouseUp: (for left mouse-button operations). For each dragging sequence, the Application Kit sends a mouseDown: message to a responder object, then sends one or more mouseDragged: messages, and ends the sequence with a mouseUp: message. The Three-Method Approach describes this approach.

The other approach treats the mouse events in a dragging sequence as a single event, from mouse down, through dragging, to mouse up. To do this, the responder object must usually short-circuit the application’s normal event loop by entering a event-tracking loop to filter and process only mouse events of interest. For example, an NSButton object highlights itself upon a mouse-down event, then follows the mouse location during dragging, highlighting when the mouse is inside and unhighlighting when the mouse is outside. If the mouse is inside on the mouse-up event, the button object sends its action message. This approach is described in The Mouse-Tracking Loop Approach.

Both of these approaches have their advantages and drawbacks. Establishing a mouse-tracking loop gives you greater control over the way other events interact with your application during a dragging operation. However, the application’s main thread is unable to process any other requests during an event-tracking loop and timers might not fire as expected. The mouse-tracking approach is more efficient because it typically requires less code and allows all dragging variables to be local. However, the class implementing it becomes more difficult to extend without the subclass reimplementing all the dragging code.

Implementing the individual mouseDown:, mouseDragged:, and mouseUp: methods is often a better design choice when writing an event-driven application. Each of the methods have a clearly defined scope, which often leads to clearer code. This approach also makes it much easier for subclasses to override behavior for handling mouse-down, mouse-dragged, and mouse-up events. However, this technique can require more code and instance variables.

The Three-Method Approach

To handle a mouse-dragging operation, you can override the three NSResponder methods that mark the discrete stages of a mouse-dragging operation: mouseDown:, mouseDragged:, and mouseUp: (or, for right-mouse dragging, rightMouseDown:, rightMouseDragged:, and rightMouseUp:). A mouse-dragging sequence consists of one mouseDown: message, followed by (typically) multiple mouseDragged: messages, and a concluding mouseUp: message.

The subclass implementing these methods often has to declare instance variables to hold the changing values or states of various things between successive events. These things could be geometric entities such as rectangles or points (corresponding to view frames or regions within views) or they could be simple Boolean values indicating, for example, that an object is selected. In the mouseDown: method, the view generally initializes any dragging-related instance variables; in the mouseDragged: method it might update those instance variables or check them prior to performing an action; and in the mouseUp: method, it often resets those instance variables to their initial values.

Because mouse-dragging operations often redraw an object in the incrementally changing locations where users drag that object, implementations of the three mouse-dragging methods usually need to find the location of each mouse event in the view’s coordinate system. As explained in Getting the Location of an Event, this requires the view to send locationInWindow to the passed-in NSEvent object and then use the convertPoint:fromView: method to convert the resulting location to the local coordinate system. When changes in location or geometry require the view to redraw itself or a portion of itself, the view marks the areas needing display using the setNeedsDisplay: or setNeedsDisplayInRect: method and the view is later asked to redraw itself (in drawRect:) through the auto-display mechanism (assuming that mechanism is not turned off).

Listing 4-4 illustrates the use of dragging-related instance variables, getting the mouse location in local coordinates (and testing whether that location is in a specific region), and marking that region of the receiving view for redisplay.

Listing 4-4  Handling a mouse-dragging operation—three-method approach

- (void)mouseDown:(NSEvent *)theEvent {
    // mouseInCloseBox and trackingCloseBoxHit are instance variables
    if (mouseInCloseBox = NSPointInRect([self convertPoint:[theEvent locationInWindow] fromView:nil], closeBox)) {
        trackingCloseBoxHit = YES;
        [self setNeedsDisplayInRect:closeBox];
    }
    else if ([theEvent clickCount] > 1) {
        [[self window] miniaturize:self];
        return;
    }
}
 
- (void)mouseDragged:(NSEvent *)theEvent {
    NSPoint windowOrigin;
    NSWindow *window = [self window];
 
    if (trackingCloseBoxHit) {
        mouseInCloseBox = NSPointInRect([self convertPoint:[theEvent locationInWindow] fromView:nil], closeBox);
        [self setNeedsDisplayInRect:closeBox];
        return;
    }
 
    windowOrigin = [window frame].origin;
 
    [window setFrameOrigin:NSMakePoint(windowOrigin.x + [theEvent deltaX], windowOrigin.y - [theEvent deltaY])];
}
 
- (void)mouseUp:(NSEvent *)theEvent {
    if (NSPointInRect([self convertPoint:[theEvent locationInWindow] fromView:nil], closeBox)) {
        [self tryToCloseWindow];
        return;
    }
    trackingCloseBoxHit = NO;
    [self setNeedsDisplayInRect:closeBox];
}

The Mouse-Tracking Loop Approach

The mouse-tracking technique for handling mouse-dragging operations is applied in a single method, usually (but not necessarily) in mouseDown:. The implementing responder object first declares and possibly initializes one or more local variables to use within the loop. Often one of these variables holds a value, often Boolean, that is used to control the loop. When some condition is met, typically the receipt of a mouse-up event, the variable value is changed; when this variable is tested next time through the loop, control exits the loop.

The central method of a mouse-tracking loop is the NSApplication method nextEventMatchingMask:untilDate:inMode:dequeue: and the NSWindow method of the same name. These methods fetch events from the event queue that are of the types specified by one or more type-mask constants; for mouse-dragging, these constants are typically NSLeftMouseDraggedMask and NSLeftMouseUpMask. Events of other types are left in the queue. (The run-loop mode parameter of both nextEventMatchingMask:untilDate:inMode:dequeue: methods should be NSEventTrackingRunLoopMode.)

After receiving a mouse-down event, fetch subsequent mouse events within the loop using nextEventMatchingMask:untilDate:inMode:dequeue:. Process NSLeftMouseDragged events as you would process them in the mouseDragged: method (described in The Three-Method Approach); similarly, handle NSLeftMouseUp events as you would handle them in mouseUp:. Usually mouse-up events indicate that execution control should break out of the loop.

The mouseDown: method template in Listing 4-5 shows one possible kind of modal event loop.

Listing 4-5  Handling mouse-dragging in a mouse-tracking loop—simple example

- (void)mouseDown:(NSEvent *)theEvent {
    BOOL keepOn = YES;
    BOOL isInside = YES;
    NSPoint mouseLoc;
 
    while (keepOn) {
        theEvent = [[self window] nextEventMatchingMask: NSLeftMouseUpMask |
                NSLeftMouseDraggedMask];
        mouseLoc = [self convertPoint:[theEvent locationInWindow] fromView:nil];
        isInside = [self mouse:mouseLoc inRect:[self bounds]];
 
        switch ([theEvent type]) {
            case NSLeftMouseDragged:
                    [self highlight:isInside];
                    break;
            case NSLeftMouseUp:
                    if (isInside) [self doSomethingSignificant];
                    [self highlight:NO];
                    keepOn = NO;
                    break;
            default:
                    /* Ignore any other kind of event. */
                    break;
        }
 
    };
 
    return;
}

This loop converts the mouse location and checks whether it’s inside the receiver. It highlights itself using the fictional highlight: method and, on receiving a mouse-up event, it invokes doSomethingSignificant to perform an important action. Instead of merely highlighting, a custom NSView object might move a selected object, draw a graphic image according to the mouse’s location, and so on.

Listing 4-6 is a slightly more complicated example that includes the use of an autorelease pool and testing for a modifier key.

Listing 4-6  Handling mouse-dragging in a mouse-tracking loop—complex example

- (void)mouseDown:(NSEvent *)theEvent
{
    if ([theEvent modifierFlags] & NSAlternateKeyMask)  {
        BOOL                dragActive = YES;
        NSPoint             location = [renderView convertPoint:[theEvent locationInWindow] fromView:nil];
        NSAutoreleasePool   *myPool = nil;
        NSEvent*            event = NULL;
        NSWindow            *targetWindow = [renderView window];
 
        myPool = [[NSAutoreleasePool alloc] init];
        while (dragActive) {
            event = [targetWindow nextEventMatchingMask:(NSLeftMouseDraggedMask | NSLeftMouseUpMask)
                                untilDate:[NSDate distantFuture]
                                inMode:NSEventTrackingRunLoopMode
                                dequeue:YES];
            if(!event)
                continue;
            location = [renderView convertPoint:[event locationInWindow] fromView:nil];
            switch ([event type]) {
                case NSLeftMouseDragged:
                    annotationPeel = (location.x * 2.0 / [renderView bounds].size.width);
                    [imageLayer showLens:(annotationPeel <= 0.0)];
                    [peelOffFilter setValue:[NSNumber numberWithFloat:annotationPeel] forKey:@"inputTime"];
                    [self refresh];
                    break;
 
                case NSLeftMouseUp:
                    dragActive = NO;
                    break;
 
                default:
                    break;
            }
        }
        [myPool release];
    } else {
        // other tasks handled here......
    }
}

A mouse-tracking loop is driven only as long as the user actually moves the mouse. It won’t work, for example, to cause continual scrolling if the user presses the mouse button but never moves the mouse itself. For this, your loop should start a periodic event stream using the NSEvent class method startPeriodicEventsAfterDelay:withPeriod:, and add NSPeriodicMask to the mask bit-field passed to nextEventMatchingMask:. In the switch statement the implementing view object can then check for events of type NSPeriodic and take whatever action it needs to—scrolling a document view or moving a step in an animation, for example. If you need to check the mouse location during a periodic event, you can use the NSWindow method mouseLocationOutsideOfEventStream.

Filtering Out Key Events During Mouse-Tracking Operations

A potential problem with mouse-tracking code is the user pressing a key combination that is a command, such as Command-z (undo), while a tracking operation is underway. Because your mouse-tracking code (either in the three-method approach or a mouse-tracking loop) isn’t looking for that key event, the code might not know how to handle that key command or could handle it wrongly, with unwelcome consequences.

The problem here with a mouse-tracking loop might not be readily apparent because, after all, the nextEventMatchingMask:untilDate:inMode:dequeue: loop ensures only mouse-tracking events are delivered. Why would key events be a problem? Consider the code in Listing 4-7. While this mouse-tracking loop is active, let’s say the user issues some key-equivalent commands.

Listing 4-7  Typical mouse-tracking loop

- (void)mouseDown:(NSEvent *)theEvent {
  NSPoint pos;
 
  while ((theEvent = [[self window] nextEventMatchingMask:
    NSLeftMouseUpMask | NSLeftMouseDraggedMask])) {
 
    NSPoint pos = [self convertPoint:[theEvent locationInWindow]
                            fromView:nil];
 
    if ([theEvent type] == NSLeftMouseUp)
      break;
 
    // Do some other processing...
  }
}

What happens after the mouse-tracking loop concludes? After the user lets go of the mouse button, the application handles all of the pending commands corresponding to the mnemonics the user pressed during the loop. The effects of this delayed handling are probably undesirable. You can guard against this by filtering the event stream for key events and then explicitly ignoring them. Listing 4-8 shows how you can modify the code above to accomplish this (new code in italics).

Listing 4-8  Typical mouse-tracking loop—with key events negated

- (void)mouseDown:(NSEvent *)theEvent {
  NSPoint pos;
 
  while ((theEvent = [[self window] nextEventMatchingMask:
      NSLeftMouseUpMask | NSLeftMouseDraggedMask | NSKeyDownMask])) {
 
    NSPoint pos = [self convertPoint:[theEvent locationInWindow]
                            fromView:nil];
 
    if ([theEvent type] == NSLeftMouseUp)
        break;
         else if ([theEvent type] == NSKeyDown) {
                NSBeep();
                continue;
    }
 
    // Do some other processing...
  }
}

For dragging operations handled with the three-method approach, the situation with simultaneous mouse and key events is a bit different. In this case, the AppKit processes keyboard events as it normally does during tracking. If the user presses a command mnemonic, even while a tracking operation is going on, the application object dispatches the corresponding messages to its targets. So if, for example, in a drawing application the user drags a blue circle they just created and then (perhaps accidentally) presses Command-x (cut) while the mouse button is still down, the code handling the cut operation is going to run, deleting the object the user was dragging.

The solution to this problem involves a few steps:

  • Declare a Boolean instance variable that reflects when a dragging operation is underway.

  • Set this variable to YES in mouseDown: and reset it to NO in mouseUp:.

  • Override performKeyEquivalent: to check the value of the instance variable and, if a dragging operation is occurring, to discard the key event.

Listing 4-9 shows the implementation code for this (isBeingManipulated is the Boolean instance variable).

Listing 4-9  Discarding key events during a dragging operation with the three-method approach

@implementation MyView
 
- (BOOL)performKeyEquivalent:(NSEvent *)anEvent
{
  if (isBeingManipulated) {
    if ([anEvent type] == NSKeyDown) // Can get NSKeyUp here too
      NSBeep ();
    return YES; // Claim we handled it
  }
 
  return NO;
}
 
- (void)mouseDown:(NSEvent *)anEvent
{
  isBeingManipulated = YES;
  // other code goes  here...
}
 
- (void)mouseUp:(NSEvent *)anEvent
{
  isBeingManipulated = NO;
  // other code goes here ...
}
 
@end

This solution to the problem is simplified and is intended for general illustration. In a real application you might want to check the event type, conditionally set the isBeingManipulated variable, and selectively handle key equivalents.