Alastair’s Place

Software development, Cocoa, Objective-C, life. Stuff like that.

Commands and Mouse Event Handling in Cocoa

In the Cocoa Event-Handling Guide, it talks about two methods of handling mouse drags, namely with a tracking-loop and what it refers to as the “three-method approach”.

The two have some obvious pros and cons, so sometimes Cocoa programmers use one method and sometimes the other, even in the same program.

Usually you can write your code either way and it will pretty much work. However there are a couple of nasties that I ran into today that I couldn’t find any information on anywhere. Maybe there’re obvious to some of the people who’ve been writing Cocoa programs since before it was even called Cocoa, but they weren’t to me and it wasn’t especially obvious that there was a good way to work around them.

What are these problems? Well, consider this… your application is tracking the mouse because the user is manipulating some object in your document. Now, your user is feeling mean, and during tracking hits ⌘X, Cut (if that one works in your app, try hitting ⌘Z, Undo, instead).

I’m guessing that a substantial number of apps just fell over. Or at least behaved in a less than ideal way. And even those that didn’t might well find that their behaviour is inconsistent because some parts of their app use a tracking-loop and others use the three-method approach.

So, let’s explain what happens:

Case 1: The Modal Tracking Loop

This is often the easiest way to implement mouse tracking code. It also happens to have slightly better performance in many cases, particularly on slower machines. As a result, it’s quite widespread, even within the Cocoa frameworks themselves.

Most probably the code you have is using some variation on what’s written in the documentation, i.e. something like this:

- (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...
  }
}

If you do this, what you’ll find is that your application runs all the commands corresponding to the mnemonics the user pressed after he or she lets go of the mouse button. Quite what your user intended to do isn’t clear, but there’s a good chance it wasn’t that, particularly if they’ve hit e.g. ⌘Z more than once during the tracking loop.

Fortunately, the solution here is rather simple; all you need to do is discard any keypresses you don’t want to handle. The necessary changes are highlighted in italics:

- (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...
  }
}

Depending on your application, you may also want to discard NSKeyUp events, though you probably don’t want to beep for those.

Case 2: The Three-Method Approach

Let’s start by explaining what happens. If you use the three-method tracking approach, keyboard events are processed as normal during tracking. And that means that NSApplication will look up mnemonics and dispatch the appropriate messages. So if, for instance, you’re working on a drawing application and your user is dragging that blue circle that they just created when, in a fit of pique, they hit ⌘X with the mouse button still down, your Cut code is going to run, removing the object that your mouse tracking code is trying to move. In a well-written application, things might not break, but it isn’t pretty, and it probably wasn’t what the user wanted to do.

Even if your application gracefully handles this problem, the chances are that somewhere else in your code you’re using the other mouse tracking approach, so the behaviour of your application is very probably inconsistent.

This problem had me worried for a while. All I could find searching the web were solutions like this, which is really ugly and would only really work for tiny applications because you’d end up with all potentially disruptive methods implemented on every view that might be tracking the mouse.

But no, as is usually the case with Cocoa, the solution is quite simple when you know it. All you need to do is something like this (again, the additional code is in italics):

@interface MyView : NSView {
  ...
  BOOL isBeingManipulated;
  ...
}

...
- (BOOL)performKeyEquivalent:(NSEvent *)anEvent;
...

@end

@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;
  ...
}

- (void)mouseUp:(NSEvent *)anEvent
{
  isBeingManipulated = NO;
  ...
}

@end

Obviously this is all heavily simplified for the purposes of this post. In a real application, you might want to check the event type, you may not always want to set the isBeingManipulated variable, and you may even find that you want to handle some key equivalents but not others.