The NSTrackingArea Report
The NSTrackingArea Report
- Subject: The NSTrackingArea Report
- From: Quincey Morris <email@hidden>
- Date: Fri, 1 Feb 2008 11:45:15 -0800
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 ...
_______________________________________________
Cocoa-dev mailing list (email@hidden)
Please do not post admin requests or moderator comments to the list.
Contact the moderators at cocoa-dev-admins(at)lists.apple.com
Help/Unsubscribe/Update your Subscription:
This email sent to email@hidden