The NSTrackingArea Report

  • I've been playing around with tracking areas with a half-finished test
    app, and I thought I'd report what I found out. Sorry about the long
    post.

    (A) Mouse-moved events.

    Seems to work fine. The only issue is a conceptual one with the
    documentation. There's a difference between the mouse being "over" a
    tracking area or view (i.e. the mouse location within the bounds) and
    "inside" a tracking area or view (i.e. within the bounds of the
    topmost object under the mouse location).

    Mouse-moved events, and tracking areas in general, follow the "over"
    concept. That is, mouse-moved events are generated for every tracking
    area the mouse is over. Nesting views or overlaying tracking areas
    won't mask what's underneath from getting mouse-moved events. If you
    want just the topmost view or tracking area to get mouse-moved events,
    you have to filter out any others yourself. I think it would be
    worthwhile for the documentation to say this explicitly.

    (B) Mouse-entered and mouse-exited events.

    Seems to work fine, mostly. The only strange behavior I saw was that
    after scrolling the view by dragging (autoscroll or dragger hand
    tool), there was sometimes a second mouse-exited event following a
    previous mouse-exited event with no intervening mouse-entered.

    As I posted earlier, there's a Cocoa documentation error for the
    "assume inside" option. The correct meaning is what's in the Leopard
    developer release notes (although it's talking about a bug fix to
    NSTrackingRect, but the NSTrackingArea behavior is the same).

    As I posted earlier, entered/exited tracking is defective (or at least
    disappointing) in that it doesn't tell you whether the mouse is inside
    or outside the tracking area when you first add the tracking area to a
    view. If you don't specify the "assume inside" option, you won't get a
    tracking event until the next time the mouse enters the tracking area
    (so it has to go out and come back in, if it's already inside). If you
    do specify "assume inside", you'll get a mouse-exited event the next
    time the mouse moves, if it's actually outside.

    There's also a kind of defect in the documentation for NSView's
    'updateTrackingAreas' method. The description implies that the correct
    thing to do in 'updateTrackingAreas' is to re-create all of the view's
    tracking areas. In fact, there's no need to re-create tracking areas
    with the "sync-to-visibleRect" option set, and doing so will cause the
    same state of temporary indeterminacy that I described in my previous
    paragraph.

    Based on my testing, I'd have to suggest that entered/exited tracking
    is NOT a suitable mechanism for an application to *persistently* keep
    track of whether the mouse is inside (or over) or outside its views
    (or parts of views, if you use tracking areas smaller than the whole
    view).

    What it's good for is causing events to occur when the mouse crosses
    area boundaries. That is, the entered/exit state implied by the event
    is only reliable during the processing of the event, and can't
    reliably be assumed to persist after that.

    (C) Cursor-update events.

    Seems to work fine, but there's one defect and a documentation error.

    The documentation says that the cursorUpdate message is sent to the
    tracking area's owner. In fact (as correctly described in the Leopard
    developer release notes), the message is sent to the view that the
    mouse is inside (i.e. the topmost view under the mouse location). In
    general, this might be neither the tracking area's owner, nor the view
    the tracking area is attached to.

    The defect with cursor-update events is, again, the lack of persistent
    state. If you need to set the cursor for a custom view and the cursor
    is always the same inside the view, then cursor-update events work
    perfectly, even when views overlap or are nested (for example, if the
    custom view contains text fields).

    If, however, your view wants to set the cursor differently over
    different objects in the view (think of Safari changing the cursor to
    a pointing finger over clickable links and an I-beam over selectable
    text), you're not so well served. You're going to have to set the
    cursor on mouse-moved events, but you don't directly know at event
    time whether your view "owns" the cursor or not.

    The problem is that mouse-moved uses "over" semantics and cursor-
    update uses "inside" semantics. To make it work, you're going to have
    to do an inside/outside check (at least) or a view hit test (at worst)
    to find out whether you're responsible for the cursor on any given
    event.

    (D) What's an application to do?

    To deal with all of these issues, there's a kind of hierarchy of state
    information that an applications needs to maintain persistently and
    consistently across all the custom views it's responsible for:

        inside-view status -> over-view status -> mouse location

    The inside-view status can actually be found directly, using
    NSWindow's hit testing, but assuming that might be expensive to do
    very often, it's worth making it conditional on the over-view status,
    assuming that can be made fairly cheap. If you can make assumptions
    about how your views overlap with other views, you might be able to
    avoid NSWindow hit testing completely, and do a few quick bounds
    checks instead.

    The over-view status can be reduced to a simple NSMouseInRect check,
    per custom view. As I said earlier, it can't really be made any
    cheaper by monitoring entered/exited events without sacrificing
    reliability.

    Both of these depend on reliable information about the mouse location,
    which is where I started this investigation a week ago. (I'm assuming
    that the mouse location is desired to be synchronized to the event
    queue. If not, there's already a reliable answer in [NSEvent
    mouseLocation].)

    As far as I can see, the best strategy is to globally remember the
    mouse location last seen in a mouse-related event, converted to screen
    coordinates. The easiest way to do this is to subclass NSApplication
    and to wrap [NSApplication sendEvent:] in an override that monitors
    mouse events. Then all you need to do is cover the two holes this
    leaves:

    1. At application startup, initialize the global location to [NSEvent
    mouseLocation], then use [NSApplicaton nextEventMatchingMask:
    untilDate: inMode: dequeue:] with no wait and dequeue:NO to peek at
    the first mouse event in the queue if there is one. (I haven't
    actually tried this, but it seems like it would work.)

    2. To cover the case where something in code you didn't write removes
    events from the queue in a local event loop that you can't monitor
    (e.g. a text control -- if that's what it does), turn on entered/
    exited tracking for your views, so at least you'll get a location
    update when the mouse returns to one of your custom views. (I have
    actually tried this, and it works fine, except that I messed things up
    by trying to keep persistent state based on entered/exited events.)

    (E) What could Apple do?

    Apple could help out a lot if it would maintain a little bit of state
    for us. (Actually, Cocoa already knows the state, it just won't tell
    us.)

    First, it would be great if there was a -[NSApplication
    mouseLocation], the mouse location in screen coordinates from the last
    mouse-related event dequeued for any reason, whether in the main event
    loop or a local event loop, properly initialized at application
    startup even if there are no events yet.

    Second, it would be great if there was a -[NSTrackingArea
    isMouseEntered] to report what the tracking area knows but can't
    currently tell us, properly initialized when the tracking area's view
    becomes associated with a window.

    Third, it would be great if there was a -[NSApplication
    cursorUpdateView] or a -[NSWindow cursorUpdateView]) or a -[NSView
    isCursorUpdateInside] to tell us which view most recently received a
    cursor-update event and is therefore responsible for the cursor until
    the next cursor-update event.

    --

    Again, apologies for the long post. Apologies for getting sidetracked
    on the mouse location issue earlier in the discussion. Apologies for
    any errors of fact or deficiencies of opinion in this post. Apologies
    for obsessing over a tiny issue that clearly does *not* burn bright in
    the minds of most who read this list.

    And now, back to my regularly scheduled life ...
  • On Feb 1, 2008, at 11:45 AM, Quincey Morris wrote:

    > Again, apologies for the long post. Apologies for getting
    > sidetracked on the mouse location issue earlier in the discussion.
    > Apologies for any errors of fact or deficiencies of opinion in this
    > post. Apologies for obsessing over a tiny issue that clearly does
    > *not* burn bright in the minds of most who read this list.

    Quincey,

    I trust that you have filed a bunch of bug reports & enhancement
    requests through the proper channels?

    <http://developer.apple.com/bugreporter/>

    j o a r
previous month february 2008 next month
MTWTFSS
        1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29    
Go to today