NSTrackingArea and Scrolling: Defective By Design?

Cocoa is a pretty cool framework for writing applications. However, like all frameworks, it has some rough edges. Take, for example, mouse handling…

Most UI frameworks allow you to receive messages when the mouse moves above a particular control or view. Cocoa supplies this ability as well, but you must first create an NSTrackingArea that specifies which areas interest you, whether you wish to receive all mouse movement events (as opposed to simply enter/exit), and whether you wish to receive events when the view is in an inactive window.

For most common scenarios, this system works pretty well. There is however, one case in which it fails miserably: embedding a view with tracking areas in a scroll view.

In Logos 4, we have a portion of our app that displays a report as a series of section, each of which can be expanded, collapsed, rearranged, and even removed. As an affordance, we display the section heading under the mouse a little bit differently, in order to encourage the user to click it. Here’s what it looks like on my machine:

Hover.png

Everything seems great, until you discover that scrolling in the report causes mouse enter events, but no mouse exit events:

BadScroll.png

If not for the mouse enter events, I suppose one could argue that this behavior is by design (after all, the mouse itself hasn’t moved), although the rationale for such a design eludes me. In any case, we have a feature to write, and we need to find a way around it. Now, we know that when the user scrolls, our content must be redrawn, so why don’t we try adding some logic to drawRect: that checks to see if the mouse is still present within our view? Here’s what it looks like:

BadScroll2.png

We’re closer, but still not there. If we study the behavior of these views carefully, we’ll see that drawRect: is only being called when new content is exposed on the view. In all other cases, the scroll view caches the content that has previously been drawn and reuses it, with no need to call drawRect:. Could we perhaps disable this behavior? As it turns out, we can, and all it takes is a single click in Interface Builder:

InterfaceBuilder.png

We’ve now sacrificed a bit of efficiency, but we get the effect that we want, and it hasn’t provided a noticeable degradation in performance. On the other hand, putting mouse handling code in a drawing method does feel very dirty, so if you have a better workaround, please post it in the comments!

Posted by David Mitchell on December 23, 2009