WWDC2010 Session 145

Transcript

>> Peter Ammon: Welcome to Key
Equivalent Handling in Cocoa Applications.
My name is Peter Ammon.
I'm an engineer in the Cocoa Frameworks team.
Before we begin, I must confess that the
title of the session is a little incomplete.
The first half will indeed be about key equivalents
in Cocoa applications, but in the second half we will bring
out my colleague Raleigh who will show
you how to create custom views and menus.
For example, the finder label view or also entirely
simulated menus like the suggestions popup in Safari.
So you don't want to miss that.
So, we're going to cover the basics of key equivalent
handling, what are key equivalents, how do you set one.
We'll go over the keyboard event flow.
So, when the user actually types the
key event, what methods get invoked
and how can your application involve itself with that.
We'll talk about handling key equivalents
from other applications so you can respond
to events when you aren't even the front most app.
We'll go over to conflict handling.
What happens when two menu items
claim the same key equivalent?
Which one wins?
We'll go over how to debug these conflicts
when they occur and then I'll bring up Raleigh
for the custom menu items and simulated menus.
So, key equivalent basics.
Well, key equivalents are there to accelerate access to
commonly used menu items and buttons via the keyboard.
So here we see the Save button which has a key
equivalent of return which is why it's showing blue
and we have the New menu item which has
a key equivalent of command-N of course.
And it's the same API on NSButton and NSMenuItem
but most key equivalents are on menu items.
Now, programmatically, we refer to the N
that you see there as the key equivalent
and you can set that with the setKeyEquivalent method.
Now, notice that the N we pass is lower case even
though it draws as uppercase in the menu item.
This is important.
It will draw uppercase even though
you pass a lowercase string.
If you pass an uppercase string
it would in fact interpret it
as if you were requesting shift
that's part of the modifier mask.
So you want to stick to lowercase
characters unless you want the Shift key.
Now the command key you see next
to it is part of the modifier mask.
Of course, there are four different
modifier flags that are available.
There is command, option, control and shift.
You can also pass nothing at all.
It's perfectly valid to have the key
equivalent with no modifier mask.
You may be wondering about the Function key.
A lot of keyboards have that but
in fact that is not supported.
The reason for that is that not all keyboards have that.
So those key equivalents will be unavailable to those users.
And again, to set the key equivalent modifier mask,
of course you call setKeyEquivalentModifierMask
and you pass a bitmask of the modifier
flags that you would also get from NSEvent
and this is the same on NSButton as it is on menu item.
Now, you can set this programmatically but most of
the time of course you set them in Interface Builder.
Here the new menu item is selected and down
at the lower left you can see the portion
of the inspector where you can set the key equivalent.
So which item should get key equivalents?
Well, you want to assign them to frequently use command
and in fact the HIG lists a lot of key equivalents
that have standard established meanings and I mean a lot.
There's a lot there.
It's very comprehensive.
You want to try to avoid repurposing the standard or
reserved key equivalents because users expect them
to behave consistently across applications.
Now, a very powerful technique for enabling
power users is to use an alternate menu item.
This is for example Close All.
The Close All appears when you press the
Option key, that's why it's called alternate
and this doesn't clutter up the user interface.
But because these items aren't very
discoverable you have to know what to press.
You always want to provide another
way to accomplish the task.
For the example of Close All, you can of
course just close each window individually.
Now, it's worth noting that the key equivalent
you set may not be the one that appears.
The one reason for that is what's
called the user key equivalents.
These are things that user can
set in the system preferences.
So here the user setting command-Option-C, I'm
sorry command-Shift-C as close for the text data.
It's called the user key equivalent and you can access
it with the user key equivalent method on the menu item.
So we actually expose when these occurs and you can
get what the currently selected user key equivalent is.
So those are just the basics.
What happens when the user actually types an event?
Well, event handling always starts
in NSApplication sendEvent.
That is the earliest point at which you can catch
an event and it's worth nothing in this diagram
that I'll show you every orange box represents
a point in which your application can override.
It's an override point that you can
involve yourself in event handling.
And the first thing sendEvent will do is
invoke what are called local event monitors.
So here's an event and local event monitors,
they can just pass the event on through.
They can handle it or they can return
in an entirely different event.
So in this case the event goes in and it comes out and
you can see it's totally different, different color.
So let's say that the local event
monitor does not ignore the event.
It returns it so it can continue being processed.
Now, the next step whether the event contains command or
control is an important fact in how the event is dispatched.
If it contains command or control, it's going
to treat it like a key equivalent first.
So it's going to call performKeyEquivalent on the keyWindow.
So before it even reaches the main
menu, it will start on the keyWindow.
And the default behavior performKeyEquivalent is to call
performKeyEquivalent on all the views and this is recursive.
So if you wanted to do a custom key equivalent on
a view you could override performKeyEquivalent.
Now assuming none of the key-- the event
is not handled by performKeyEquivalent.
It will also try other active windows.
What do I mean by an active window?
I mean a window such as a drawer attached to
the keyWindow, or a sheet, or the main window.
It's different than the keyWindow.
But in this case, it only sends-- calls
performKeyEquivalent on views that manage menus.
That's specifically our popup buttons
and segmented controls that have menus.
So for example, a popup button in the main window can handle
key equivalents and this is recursive in the same way.
Now, performKeyEquivalent returns YES, that's a way
of indicating that the view handled the key equivalent
and if it returns NO, it indicates it
doesn't and the recursion should continue.
So let's say that keyWindow didn't handle the key
event and neither did any other active windows.
Finally, we reach the main menu performKeyEquivalent
is called and normally through dynamic key equivalents
in the main menu, you want to implement delegate methods.
A very tempting looking delegate method is menu
has key equivalent for event target action.
This allows you to return to YES that does have a key
equivalent and NO that doesn't and this looks tempting
but in fact this is a problematic
method for a couple of reasons.
One of them is that if you just dynamically
say YES, it had it or NO, it didn't have it.
We don't know what to draw in the menu item
and it doesn't get called if the menu was open.
So if the user opens the menu and then types a
key equivalent, this method will not get called.
And lastly, it doesn't work by
default with user key equivalent.
You have to go to heroics to make user key
equivalents work with this type of delegate method.
But it has one good use and that's just to press
expensive population for menus without key equivalents.
So for example, the open reasons menu
has a heap of disk to be populated.
You don't want to do that every time the user presses
a key so you might implement this delegate method
to just return NO which is a way of
saying don't even bother populating me
for key equivalent searching because
I promise I don't have any.
Now, a better method to implement is menu needs update.
This is called not just for key equivalent handling
but also when the menu is opened when it searched
for accessibility, when it searched spotlight for help.
So it's called in a number of places and your delegate
method should populate the menu with the menu items that--
and their key equivalents and then the frameworks
will actually take responsibility for determining
which items match the key equivalent if any.
So the owners lift it off your application
for determining which key equivalents match.
So, so far we've been assuming the event had
command or control in this modifier mask.
What if it didn't have those?
Or what if performKeyEquivalent returned NO?
So it not recognizes a key equivalent.
So we're going to take off the command key.
Well this is where-- the next step is
that app command hot keys are handled.
So these are keys like command-tilde
for windows cycling or there are keys
for moving a focus in the menu bar or for the toolbar.
Now, other hot keys that you might register for with
register hot key don't actually get invoked via this path.
In that case the event is never ever
sent to the front most application.
It's sent only to the app that registered the hot key.
But for this sort of app command hot keys,
these do get sent to the app at this point.
And finally, we reach sendEvent on the keyWindow.
And the keyWindow is going to call keyDown on
the first responder, for example a text view.
And the default behavior is to call
keyDown up that responder chain
until we reach the window itself NSWindow keyDown.
And at this point the default behavior
is to call performKeyEquivalent.
Now, if this is an event without command or control this
is the first time performKeyEquivalent has been called.
If it did have command or control,
this is in fact the second time.
But most of the time it wouldn't get that far.
And if the performKeyEquivalent on
the keyWindow did not return NO,
then it will call performKeyEquivalent on the main menu.
So again, this might be the first or second time that
performKeyEquivalent is called depending on the event.
And the very last thing it does is it
will beep and then the event is destroyed.
So this is the very fallback behavior.
So, that's a lot to take in I know but
these are the things to keep in mind.
Windows get first crack a key
equivalent before the main menu.
Now if the event has command or control
it's treated like a key equivalent first,
performKeyEquivalent is called before sendEvent.
Other events performKeyEquivalent is called after sendEvent.
So, the best practices to do dynamic key equivalent.
If you want to apply them to a menu
you would implement menu needs update
to just install the menu items with the key equivalents.
If it's on view you would override performKeyEquivalent.
Now, if you want to do your special work
before normal key equivalent handling,
you can override NSApplication sendEvent, NSWindow
sendEvent and NSWindow performKeyEquivalent
or you can call and install a local event monitor.
If you want to do your special processing after all
the other work is done, you can override keyDown
on NSWindow and you call through the super.
So that's how you handle key event
within your own application.
How do you handle them from other applications?
Well, a very powerful and polite
way to do this is to use a service.
We see here's a bit of the services
declaration from Stickies.
Notice that Declare is a key equivalent of Y and because
it's uppercase it will actually be command-Shift-Y.
This is very powerful because your
app doesn't even have to be running.
The frameworks will launch your application for you
and users can customize this in System Preferences.
It's polite because if the front most application also uses
the key equivalent that you want, the application will win.
So you won't interfere with the front most app.
And you can also interact with the text selection.
Stickies will create a new sticky
with whatever text is selected.
And to do this of course you specify the NSKeyEquivalent
key in the service declaration like we see here
and I recommend seeing my talk from last
year about services for more information.
Now, another very powerful approach is the use
of hot key and your application has be running
to use a hot key and hot key always take precedence.
So it's very possible to register for a hot key
for the letter N and then anytime the user types N
in any application that will get sent to your app instead of
the front most app because that will be a rude thing to do.
This requires a virtual key code so you can't just pass
the string and you have to pass a virtual key code.
You can find many of these in the Events.h header
in HIToolbox framework or another technique is
to actually ask the user type the key event you want to be
the hot key and then you can pull out the virtual key code
from the NSEvent with the key code method.
Now, to install a hot key you use
the RegisterEventHotKey function.
This is a carbon function but unlike many carbon
function this one is available in 64-bit which is nice.
The third option is to use a global event monitor.
This is sort of the analog to local event monitors.
They asynchronously respond to
event and they don't block events.
So a hot key will actually swallow the event whereas the
global event monitors will just allow you to observe them.
To do this you use addGlobalMonitorForEventsMatchingMask
at handler and handler is a block that you pass
when you want your-- to be invoked
when the right event is dispatched.
So on to conflict handling.
Most key equivalents are always visible, right.
Close just shows command-W all the time and some of them can
be revealed such as Close All when you press the option key.
And some of them are just sort of power user secrets like
you can hit Escape to close the dialog with a Cancel button.
Now, when the key equivalents are visible, it's
really important that what you see is what you get.
What do I mean?
Well, imagine an app location like this with
delete and duplicate both showing command-D,
user types command-D hoping to
duplicate instead it deleted something.
That's bad.
This might seem like a very contrive example but
if the menu items are in very different menus,
maybe ones that are popup, who knows, it could be confusing.
So when two menu items claim the same key equivalent
NSMenu will actually mediate between them to determine
which one wins and the other menu item
will not show that key equivalent.
And you may think, you know, I designed my app.
I pick all the key equivalents.
I know there are no conflicts but as we've seen,
key equivalents can come from other places.
They can come from the service.
They can come from something the user
requested as a user key equivalent.
Your application has plug-ins.
They can come from there.
Input managers and any future menu items
added by added by AppKit such as Close All.
Now, the first come first serve meaning the first menu item
that requests the key equivalent will get it
except that user key equivalent always win.
We respect the users' wishes and services
always lose because it's very polite.
And a surprising fact is that the key equivalent method
actually returns to currently applicable key equivalent.
So if I call a setKeyEquivalent when I pass N
and then I call key equivalent to get it back
and I get back the empty string that's a good hint that
some other menu item has claimed that key equivalent.
Now the NI pass isn't lost.
It just sort of dormant in the menu item and when the winner
goes out of the menu or it changes its key equivalent,
my item will, its N will become prominent and then
it will return it from the key equivalent method.
So I'll give you a demo of the conflict handling.
So here I have a simple application which shows some files.
I can get info on a particular file and I can edit it.
You'll notice that get info contains
command-I as the key equivalent.
Now, command I is also used for italics, right?
So in the Font menu you can see that Bold and Underline
have their normal key equivalent but italic does not.
This is because the get info item got to it first.
Now if I open this file, you'll notice
that get info loses its key equivalent.
Where did it go?
Well, if you remember that keyWindow get
to try key equivalents before the main menu
and in fact this keyWindow has a popup button
which has key equivalents in it and you can
that italics goes to that-- is in that popup button.
If I hit command-I, in fact, it
does do italic instead of get info.
And you also notice that the italics menu item also
has command-I which should be surprising because I said
that key equivalents can't be shared
so we'll talk about how that works.
And if I go back and make this the keyWindow, get info
recovers those key equivalents and italic loses it.
So the winner can be determined very dynamically.
So as we saw with italic, sometimes it's OK
if two menu items share a key equivalent.
It's OK when they sort of do the same thing.
In that case there's no confusion if they both show it.
To indicate to AppKit that it's okay if these
two menu items have the same key equivalent,
you can give them the same action.
They don't need the same target.
They just need the same action and that's true as
we saw even if one is in a popup and the other is
in the main menu, they can be widely separated.
So debugging.
Often as I said you'll set a key
equivalent and then it doesn't show up.
You know, where did it go?
Well, another menu item has it but where?
How do you find out?
Well, this is an important enough question
that will share a bit of NSMenus internals.
Here it is.
You can actually access the uniqueing properties, the main
menu uniquer and print out its content to figure out, OK,
which menu items have which key equivalents registered?
This is of course for debugging only because, you
know, absolutely this is going to change in the future.
Hopefully, it will become easier.
But for now this is a powerful way to solve this problem.
I'll give you a demo of that.
So I've launched this application in GDB over here.
I'm going to pause it and I'll call what I said
NSMenuKUniquer MinMenuUniquer print contents
and it outputs a lot of information and we're
looking for italics so let's search for italic.
There it is and you can see that italic is registered for
command-I but so is get info and get info is ahead of it
which shows you that get info is the winner and that's
why get info has the keyEquivalent and italics does not.
That's a powerful way to debug conflicts.
So that is the end of the key equivalent
portion of this session.
I'm going to hand the podium over to my coworker Raleigh
who will talk about how to create custom menu items
like finders label view and how to simulate
menus when you need the ultimate and flexibility.
[ Applause ]
>> Raleigh Ledet: Thank you Peter.
My name is Raleigh Ledet.
I'm a coworker with Peter on the Coca Frameworks.
So custom menus and simulating menus,
we're kind of switching gears here.
So let me give you a couple of examples of the
things that we do in OS X today and I'm going
to show you how to implement those things yourself.
This is the menu from the finder.
Hopefully, you're familiar with it.
In this particular portion is you could set
the label color that you want for your files.
This isn't just a text.
This isn't just some text in the menu.
You can actually select little code boxes and they've
done this by putting in a custom view inside the menu
and we'll go ahead and show you how to do that yourself.
Sometimes though menus aren't quite what
you need and so for our example here
when you start typing a location it
puts up a custom list of suggestions.
The problem with having this in a menu is that focus is
actually still on the text field and the menu wants to focus
for itself and so that won't work
and you can't put up a menu.
So you actually have to simulate a menu.
So I'm going to show you how to do that as well.
So we have some sample code.
Hopefully, you've already downloaded it.
If not, you can find it there and we'll give
you a quick tour of what the sample code does.
So right over here we have a custom menu items and you see
it has a normal regular menu item here and we can track
around and these are just pulling images from the
desktop images and we can also search for them here.
Just type L and we see a list of suggestions and we can
choose a suggestion or we can change that and here's all
of the L items and maybe we want the ladybug instead.
So this is the sample code and will
show you how to do both of those items.
So first let's talk about custom menu items.
Whenever you can it's preferable that you actually
create a custom view and put that in a menu
when what you really want is true menu behavior.
This is the most appropriate way to do it and
the most future proof way of accomplishing that.
And so this is our analog to what
we've been doing in the finder.
It's the exact same thing.
We've got a custom view with 4 images.
And so the first thing you need to do is you
need to create your custom view and set it
as the view that the menu item should use.
So Interface Builder then I went ahead in and
I created a custom view with four image wells.
I also put in four progress indicators so the
progress indicator will spin while we're waiting
for the thumbnails to be generated.
And then the other thing that you need to do which is really
important, you actually need to go in Interface Builder
and then Properties and set your class for your view
to some custom subclass that you're going to write.
You need to write a custom subclass that you can
handle the mouse events and the keyboard events.
Now that you have a custom view, how do you put
that custom view in to the actual menu item?
It's actually fairly easy in Interface Builder.
You can jot down your menu, pull off its properties
and then connect the view outlet right to the view.
Now when you run your application instead of item
two it will put your custom menu item in there.
You have custom view.
That's not what I actually went in the sample code
because I actually look at that source folder.
You said it was one menu item where you could
select the folder to be a source of images
and there are four images per menu item
so I dynamically add menu items as needed.
And so since I do this dynamically in code,
there's an API call on NSMenuItem called setView.
All you have to do is call menuItem
setView and you add your view
and now when the menu is displayed
your view will be shown there.
So now we got our view inside of menuItem.
It's coming up correctly.
Let's talk about mouse navigation.
Hopefully, you hear for the previous talk
that Troy gave and he talked some great--
some interesting hints about mouse navigation
and a lot of people go into tracking loop
so you override mouseDown, mouse drag, and mouseUp.
We don't want to do that in this case for this application.
Particularly, we don't want to do a tracking loop because
the user can track from your menu item to other menu items
and for that matter, the user might press the mouse button
down and keep it held down while we pop up the menu,
drag to the menu item they want to select and then let go.
So you might not ever get a mouseDown anyway.
Instead, we're going to go ahead
and use updateTrackingAreas.
We're going to create tracking areas
to do our mouse tracking and you do
that by overriding updateTrackingAreas in you custom view.
In this case we just loop through each one of our
image views and we're going to call an internal method
that we'll get to in a little bit called
trackingAreaForIndex and the key important thing
in the slide here is what you call self addTrackingArea.
So addTrackignArea is an API on NSView and that's how
you add your tracking areas once you've created them.
Here is the internal method inside discussed in subclass
called trackingAreaForIndex and we have to do a number
of things before we actually allocate our tracking
area and the first is that I create a dictionary
to hold the index that this tracking area is for.
In this index I've just used so I can correlate which
is the imageView that this index is associate with,
what's the spinner that this index is associated with,
and more importantly what's the URL that's associated
with this index which is actually what other
parts of the application are looking for.
So then we go ahead and we grab our view that's
associated with that index and we convert its bounds
to the coordinate system of our custom subview and then
we have to figure out what our tracking area options are.
As I mentioned earlier, the user can drag
through your menu item to other menu items.
So you want to get your entered and exited tracking
area events for both mouse moves and mouse drags.
So we add the NSTrackingEnabledDuringMouseDrag
to our options.
That way we'll get them in both cases.
We want both the mouseEntered and the mouseExited.
We want to see whenever you come over one of the image views
and whenever you leave it so we
update our selection properly.
And then the last option is NSTrackingActiveInActiveApp.
When the menu is up your app is active.
That works great and anything happens
that's going to require your apps
to lose activation, the menu is going to go away anyway.
So that's a nice little option to add on there as well.
Now that we have all of that figured out we can go
ahead and call NSTrackingArea alloc initWithRect,
the trackingRect that we in our local
coordinate system that we determined earlier.
The options that we figured up above, real important.
Make the owner sell so that your custom view
receives the mouseEntered and mouseExited events.
And then that dictionary that we created at the beginning as
the user info and this will allow us to associate the view,
the spinner and the URL that's all
associated with this tracking area.
So now we have our tracking areas
implemented and they're in place
and they automatically get updated whenever
the system thinks that they need to be updated.
We can go ahead and implement our mouseEntered responder
method and so you could just call a user data right off
of event and this will be that user info
dictionary that we added to the tracking area,
grab our index value out of there and just
set our property selected index to that index.
I'm not going to show you the code for it
but in the implementation I've actually created a custom
set selected index set of method for this property.
And in that set of method I make sure that whenever
the index changes that I call self set needs display.
In that way, at any time for whatever
means that the index changes,
we know that we need to redraw with the proper selection.
Then we implement the mouseExited responder method.
We don't need to look at the user dictionary
in this case because the user is gone
out of our tracking area so at
this point we have kNoSelection.
We'll receive a mouseEntered if the user moves with the
curser into another image view inside this menu item.
So we can rely on that and just return those
selections here, set our selected index to kNoSelection.
Now we're not implementing mouseDown and mouseDrag
but it is important that we implement mouseUp.
When you get a mouseUp event in your custom view,
you'll only get that when the user has
actually released the mouseOver your menu item,
your custom view for the menu item.
If it's any other menu item you won't get the mouseUp.
So now that a mouseUp has occurred in your
menu item, we obviously want to go ahead
and send the action that's associated with that
and here's what we're doing, we send the action.
There is a method on NSView called enclosing menu item.
If you're associated with the menu
item and of course in this case we are,
it will return to you the menu
item that you're associated with.
And in this case I would call NSApp sendAction and actually
get the action and the target that's already associated
with the menu item that was wired up in Interface
Builder so I didn't have to do any additional work here.
I could just wire it up in Interface Builder.
Now, that we've actually sent the action we need to go ahead
and cancelTracking for the menu so that the menu dismisses.
And for menu item we can ask for its
parent menu, the menu it's associated with
and then just call cancelTracking on that menu.
That's all you really need to do for mouse navigation.
Very pretty, it's very simple but now we also need to handle
keyboard navigation because of course the user can navigate
with the keyboard as well and I'll
give you a quick demo of that.
So here we have the custom menu application so I'm going
to like move the mouse way over here so not using it.
Everything is going to be keyboard driven now.
OK. [Whispering] There we go.
Mouse is out the way and so of course I can
navigate with the keyboard, move around,
I can jump to the beginning and to the end.
You hit the Space for return and the item is selected and
of course you can just hit Escape and dismiss the menu.
There we go.
So let's see how we handle that in code.
So if you're here for the last talk, then you already
know that you're not going to receive any keyboard events
until you override acceptsFirstResponder return YES.
By default NSView returns NO.
So, we need to become first responder.
Once you've informed Cocoa that you
do in fact acceptsFirstResponder,
you will at some point be asked to become first responder.
There are 2 cases that you might become first responder.
The users are navigated into the view of
the keyboard or perhaps the user clicked
onto your custom view inside the menu item.
If it's a user click, then we've already set
the selection based on the tracking areas.
So if there is a selection, we don't want to overwrite that.
But if the user arrowed in to the menu item view of
the keyboard then we don't currently have a selection
but we want to set the selection
to something so that the user sees
that that is the item that they're
manipulating at the moment.
So, in this case, we just our selected index to 0.
If you want to get really fancy, you might remember the last
selection that was there on the menu item so when they move
to it, you move to the last remembered one.
Since you've become the first responder,
at some point you're going to be asked
to resign first responder generally moving off with
the keyboard or perhaps the menu is getting dismissed.
If the menu is getting dismissed and your
item was the one that was activated, we've--
at this point we have already set the action so setting
the selected index to kNoSelection doesn't do anything.
But if you-- if we're resigning first responder
because of keyboard movement then it's important
that we set our selected index to kNoSelection
so that the user doesn't get two highlighted
items inside the menu and get confused.
Finally, we can get a keyDown message and we're going to
do the typical Cocoa thing of just call interpretKeyEvents.
This will key bindings to kick in and look at the key
event and figure out what is the user's intention.
And what's really important in this example which is
kind of unique is that after we call interpretKeyEvents,
we still need to call super keyDown so that
eventually it will go up the responder chain
and then this menu can handle the
key events that we don't handle.
So, keyDown is just going to look at the event
and if it's the right arrow or the left arrow,
it's going to call moveRight, moveLeft in our view.
This is great so now you don't have to forget
what the key code is or the appropriate character
for the right arrow key and the left arrow key.
It's taken care of for you.
So, here we just move our selection right or left and
likewise, the user might do command-right or command-left
and that will be interpreted as move to beginning
of line so you can move your selection to the end
of the beginning in the sample code's case.
Then of course if the user hits the Return key or the
Enter key, we'll want to go ahead and send the action
if they've committed it so we can do send action and
we've already taken a look at that implementation.
But we also want to activate our menu whenever the user hits
the space bar and traditionally, key binding just thinks
of space as being part of texts
usually associated with the text field.
So, there's no user action associated with space.
So, when key binding doesn't find some action
associated with that, it will call insertText so,
we overwrite insertText and we look for the space string
that has to be inserted and if that's the case, again,
we want to go ahead and send the action
and commit that item and dismiss the menu.
The way key bindings actually works when you
call interpretKeyEvents is that once it figures
out what the command is, that it wasn't send for an action,
it calls doCommandBySelector on your view and by default,
this will find out-- the default implementation
will determine if your view responds to selector.
If it doesn't, we'll call that.
If not, it will route up the responder chain appropriately.
And if it goes all the way to the end of the responder chain
and nobody has responded to it, he will get the system B
and menus, whenever there's some invalid
keyboard movement, they don't beat.
So, we want to go ahead and just check for
the selectors that we've implemented above.
If it's one of those selectors, we'll go ahead and just
call up the super which will call back the correct method
but if it's not one of those methods,
we specifically don't want to call super
so that we eat the action right
here and we don't have a system B.
Now, you might be wondering well,
if we don't allow this to call
up the super then how can you navigate
up or down off of your menu item.
Or remember previously, when we implemented keyDown after
calling interpretKeyEvents, we called up the super keyDown
and that's why we're-- it's perfectly fine
to eat this here and not worry about it.
Now, we got menu navigation via the
keyboard and we can navigate via mouse.
You might want to throw some animations
in your custom menu item.
Now, you don't want your animations just
running wild while the menu isn't even showing.
That's a waste of resources but you
can override viewDidMoveToWindow.
The way this works is whenever we show a menu, we create
a new window and we add all the menu items in there.
If you have a custom view as your menu
item, that gets added to the window
and you'll-- we'll get a viewDidMoveToWindow call.
You could check your window property from self.window.
If you have a valid value, you can start your animations.
When the menu is dismissed, we deallocate the
window after we pulled out all of the menu items
and including removing your view from the window.
Your view is not going to be deallocated or released because
it's actually still being held onto by the menu item.
However, you'll get a viewDidMoveToWindow
and window will now be nil so you know
that you can stop any of your animations.
So that was creating custom menu items.
Real easy to do.
It's a recommended approach but
sometimes that's not quite good enough.
As our example here with our custom suggestions
example, we need something that acts a lot like a menu,
looks like a menu, but is not in full control
because we want the text to still be type--
go into the text field as the user types.
So, the first thing we need to do when we create-- when
we're going to create our suggestion in windows is we need
to create a borderless window, fairly easy to do.
In this case, we have a custom subclass of NSWindow
called suggestions window in the sample code
and we've created a subclass for two reasons.
First off because Interface Builder,
there's no checkbox or a series
of checkboxes you can click to get a borderless window.
If you have a subclass, you can set the
class in Interface Builder to your subclass
which will force a borderless window property.
But the main reason we did it in this sample code was
that we're doing a unique situation here that we need
to inform accessibility about and having a custom subclass
of NSWindow provides the appropriate places to hook
into that and we'll cover a little bit of that later.
In NSWindow, the designated initializer is
initWithContentRect styleMask backing defer.
So we're going to overwrite the designated initializer.
When we call up the super, we're always going to
send the NSBorderlessWindowMask as the styleMask.
So, regardless of what set in Interface Builder when this
gets called, we're going to create a borderless window
or if you call it programatically,
you'll get a borderless window.
That's great, we get this which isn't quite what we
want so we will add some depth by saying setHasShadow.
That's great but we want the nice kind of rounded corners
like a menu has and we do that with a custom view that's set
up as the content view for drawing so we just need to
tell the window that it has a clear background color.
But then we get these black tips.
We're getting our rounded drawing but
we're going to have these black tips.
And that's because the window server doesn't realize
that this window has some transparency in it.
It still thinks it's completely opaque.
So, we tell the window setOpaque:NO and at this point,
the window server does an appropriate compositing now
and we have a nice rounded cornered
window that looks a lot like a menu.
We're ready to add content.
The next thing we need to do now
that we have our window is we need
to make our suggestion window a
child window of the parent window.
This is really important.
Why it's important is best shown when things go wrong.
So, this is not actually in the sample code but
I've modified it to do some things wrong here.
So, here we have the suggestion window is up
and if I go ahead and do expose nice and slowly,
we see that our suggestion window kind of
disappeared and that's not what we want.
And I was playing around with the window level there.
Maybe we would change the window level to
something else and we get this one we expose.
Well, now this is definitely not what we want.
What is the user really going to be
clicking on here, we don't want it.
We definitely don't want that.
So, we'll go ahead and make it a child
window and we'll make it a child window.
You'll see that a suggestion window
in expose will stay with our window
and it plays nicely and scales with the expose scaling.
And if you would have tried this with the suggestions list
up in Safari, this is the exact same behavior that occurs.
So, that's the best we have.
And now, what happened there?
This is what happens if you aren't careful on how you
deconstruct your child window whenever you go to dismiss it.
I'll show you that again.
Now a child window and it's doing
the right thing, great, but if--
when I click anywhere, this is going to dismiss
this menu, the whole window went away and if we look
at the real example that you have in the code,
as you've seen earlier, and this window is up.
I can click here and it dismisses properly.
So, I'll show you how to avoid that mistake as well.
So as I've said you need to add at your suggestion
window the child window of the parent window.
parentWindow addChildWindow:suggestionWindow
ordered:NSWindowAbove.
This will bring the child window above
your parent window and be displayed.
Put in the appropriate place.
So, now you don't need to call order front on the window
and we obviously don't want to make it key so we don't want
to call it a front made key because the key
focus needs to remain in the text field.
And whenever the suggestion window is dismissed
and we do that in our cancelSuggestions method inside
SuggestionsWindowController.m in the sample code.
The trick is to remove your child window from
the parent before ordering out the child window,
and our child window here is our suggestions window.
If you order out a child window, it will
automatically order out its parent window
and that's what was going wrong in the demo.
So make sure you remove your parent-child
relationship then order out your child window.
When do we want to activate our suggestions window?
And how do we get that to work?
So, in Interface Builder, I went ahead and wired up the
text field to the CustomMenusApplication delegate class
and I set it as the delegate of the text field.
Then I went ahead and implemented
the NSTextFieldDelegate methods.
In this case, controlTextDidBeginEditing.
The user started typing something.
This seems like a wonderful place to go ahead
and start putting up a list of suggestions.
controlTextDidChange, we probably need to modify our
suggestions or maybe we no longer have suggestions.
We need to dismiss our suggestions window.
Or maybe for some reason it was dismissed and now we
do have suggestions so we need to make it reappear.
controlTextDidEndEditing, we'll implement
that one and dismiss our suggestions window
because the user's either committed whatever they had typed
or some other means canceled that
or perhaps key focus changed.
So, we'll need to go ahead and
dismiss our suggestions window.
And then last but not the least,
control:textView:doCommandBySelector.
If you remember earlier in our custom menu section of
the talk, interpretKeyEvents calls doCommandBySelector
and this is what's actually going on in the text field.
In the text field then, we'll go ahead and forward that on
to its delegate via this
control:textView:doCommandBySelector.
And the main reason we want to implement
this is to search for the complete action.
AppKit has an autocompletion mechanism built in
whenever you're typing and we need to suppress
that 'cause we don't want the autocompletion popping up
on top of our suggestions, our custom suggestions window.
And it's also great place if we can catch this,
suppress the autocompletion from AppKit but use this
to toggle the behavior that the user would expect.
So, when the users hit escape to do autocompletion
we can go ahead and jot down our list of suggestions
and when the users hit escape and our list of suggestions
is already up, we can go ahead and dismiss that.
Now, since we're simulating a menu, there are times
when we need to go ahead and watch for the mouse
and various other actions that go through the system
and automatically dismiss our suggestions window.
So, let's take a look at some of those cases.
So, here we have a suggestions window up and perhaps
the user clicks into the desktop or another application.
Obviously, we want our suggestions window to go away.
You've already seen how the user can click
in the parent window and it goes away.
Here's an interesting trick.
If I would have clicked on the menu here, the menu
doesn't pop up because we don't want the menu to pop up.
If I click again, the menu is going to go ahead and display.
Or perhaps the user wasn't mousing at all and
they did command-tab and switch that way of--
so, there we want our suggestions
window to go away there as well.
They hit did command-tilde to switch menus--
to switch windows inside your application.
So, we need to go ahead and catch all those cases
but there's one other interesting little case
which is perhaps the user clicks in this text field.
We still want that to work because we want to
allow the user to be able to modify their selection
so they can then go ahead and perhaps type
an A instead and see the ladybug in lavender.
Just go ahead and select the Menu button.
Peter has already talked earlier in the talk about
localEventMonitors and when they are processed
and this is a great place for us to use it.
It's a new-- event monitors are new in Snow Leopard.
So, whenever we go to the show our suggestions window,
we'll go ahead and add a localEventMonitor and this is great
because now we don't have to subclass
NSApplication or NSWindow.
So, we'll addLocalEventMonitorForEventsMatchingMask
and we're interested in any of the mouseDowns
with just the right mouse button, the primary
mouse button or the any other mouse button.
And riding along we can go ahead and add our
code to how to respond to that type of situation.
First thing we're going to do for our mousedown event is
we're going to find out what window did this event occur in.
Did it occur in our suggestions window?
When it occurred in the suggestion window, we
want to go ahead and allow the click to process
through and allow that to act as an action.
So, we don't want to do anything.
If it's not the suggestion window
then perhaps it's our parent window.
Well, if it's not our parent window,
this is a LocalEventMonitor so it has
to be some click that's associated with our application.
If it's not our parent window, it must be
some other window inside of our application.
So, we'll go ahead and cancel our suggestions window.
If it is in the parent window, then
we have a little bit more work to do.
You got a whole bunch of code here and the whole point of
this code is basically to find out did the user click here.
The user clicked in the text field, we want to go ahead and
allow that click to go through and be processed normally
so that user can move the selection, change
the selection or just move the carat.
All of this work is just basically doing normal hit testing
but the real key here is that whenever the user is typing
in the text field, we actually place the field editor
there and so hit testing will actually hit test
to the field editor and not the text field.
So, we need to check the hit tested view here if it's
the text field or if it's the field editor associated
with that text field that we want to
let the event process do normally.
If it's anywhere else in our parent window,
we'll go ahead and make event equal nil.
This is the event that was passed
into us in the beginning of our block.
We're going to return to that in
a moment and you'll see that.
And this will effectively eat the event.
So, Cocoa will no longer run this event because we
can modify the event here and the event is eaten
but we'll go ahead and cancel our suggestions
window which will dismiss the window
and this is how we accomplish the user clicking
on the popup button dismissing the suggestions
but not having the popup button pop up.
At the end of a block here for
completeness, you can see how we return event.
And in this case, we will either always return the event,
the mouseDown event as it was passed to us or we're going
to return nil and effectively eat the event.
So, we set up our localEventMonitor
whenever we showed our suggestions window.
Likewise, we need to remove our event monitor
whenever we dismiss our suggestions window.
So, event monitors are automatically retained and the
memory management is automatically already done for you.
So, when we add our localEventMonitor, we didn't need to
call retain on it but we did need to hang on to the observer
so that we can call NSEvent removeMonitor
and we could pass in that observer.
Since we didn't retain it, we don't need to release
it but we're going to go ahead and make sure
that we set our local number variable to nil because
we don't want to hold on to an invalid object.
So, that handles a lot of the mouse cases but the user
can navigate with the keyboard or perhaps something else
like an alert pops up and we want to handle that case.
And that can all be done by listening for the
NSWindowDidResignKeyNotification on the parent window.
So, this will handle all the other cases
that we need to worry about and you'll see
that I'm using here the new the 10.6
notification method addObserverForName.
That way, this allows me to put right in line
the behavior that I want associated with that
which is simply to dismiss our suggestions window.
So, we go ahead and we install the notification
for NSWindowDidResignKeyNotification
whenever we show our suggestions window,
and likewise whenever we dismiss our suggestions window,
we need to go ahead and remove that observer as well
and just like the localEventMonitors, you don't-- the
observer that's returned to, you don't need to retain it.
The memory is handled for you but you do need to go ahead
and remove the observer or else it's hanging around forever.
And likewise, once you've removed the
observer, we need to set our local ivar to nil.
Otherwise, we're hanging on to a potentially invalid object.
So, that's great.
So, we got our suggestions window showing up.
It's playing around nicely with the
keyboard and the mouse and autocancelling.
But how do we get to play nicely with the
keyboard and the mouse as far as tracking goes.
This turns out that it's very similar to what
we're doing in our custom menu item view case so,
I'm not going to go over that code here
because it's practically the same thing.
Please look at the sample code
for some of the monitor changes.
But I do want to touch a little bit on accessibility.
We're doing something a little bit unique here.
So, we have to inform accessibility of that.
Cocoa does a lot of things for us but what this
looks like to the user is that there's a list
of suggestions associated with the text field.
In reality, we have a child window
that's associated with the parent window.
And we need to inform accessibility of the
logical relationship that we have here.
Namely, that we have a list of suggestions
that of the child of some text field.
So in this suggestions window class in the sample
code, in the suggestion with text field cell
in the suggestions controller, whenever we show the
suggestions window we go ahead and build this relationship
and whenever we dismiss the suggestions window, we go ahead
and tear down this logical relationship for accessibility.
I'm not going to go over the code but real
quick in Interface Builder for that text field,
I went down and I gave it a custom cell class
which is the suggestible text field cell,
and this cell knows that it can potentially
have a child window associated with it
and that's how we handle this relationship.
And we have a whole bunch of items on here.
We need to report accessibility that this is an AX list
and we actually handle this in the rounded corners view
and set NSView subclass that draws the nice rounded
corners in our content for our suggestions window
but it will also report for us that we have--
that it contains a list of items to accessibility.
There's not a lot of code there.
It's in the sample code.
And then we need to report that the image, the main
label and the blue label are all part of the same group.
And we go ahead and we do that
in the highlight view subclass.
It's already had it there to handle the
highlighting that you see at the top
of the window as the selection is changed.
So, it's a great place to go ahead and report
accessibility that it contains a group that all
of its item should be considered part of a group.
From there, the label views will report the
appropriate things to accessibility and that's great.
But we still need to deal with this image.
For an accessibility user, this image is just an
opaque field unless we do a little bit of work.
And in 10.6 we added an API that
go ahead and allow you to do that.
You can call setAccessibilityDescription.
You can go ahead and put in a localized description there.
In this case, we'll go ahead and put the file
name since that's all that we know about it.
If it's images in your application,
call in setAccessibilityDescription,
we'll add a wealth of information for controls
and displaying the intent to your users.
That's real easy to do.
Particularly since we've also added that you can-- if
you're using named image whenever you create your images,
you can actually just create an accessibility image
descriptions that strings file and localizes file
and the accessibility description will be
localized from this file and just add it
to the image automatically and
then report it to accessibility.
There was a good talk about this in the Cocoa Tips
and Tricks so I recommend you check out that session.
So, we go ahead and look at our key equivalents.
They allow user to accelerate their use of your application.
So, we've looked at how they flow through the system.
We've looked at how the appropriate places for you
to implement overrides our implement delegate methods
to dynamically modify your key equivalents.
We've looked at how key equivalents in their conflicts
are mediated by NSMenu and how you can even debug that.
And then the last two items here, we've
looked at how to create a custom view
and put it in the menu, it's really simple to do.
It's fairly easy.
This is the recommended approach as we change
things in the operating system for the menu,
you'll get the new look and behavior appropriately.
But when that absolutely won't do and you have to
do it yourself, you can look at the sample code
to see how we go ahead and we simulate the menu
where appropriate and do all the appropriate things
in the autocancellation and attaching to the child
window and working correctly with the system.
For more information, you can see
our-- the frameworks evangelist
and there are a couple of links to our documentation.
Now forums are always a great place to get
some more information and ask questions.
The Cocoa Tips and Tricks session I referenced earlier.
There's a lot of great information there and went
to a little bit more detail on some of the topics
and the Crafting Custom Cocoa Views which
was the session right before this one.
Hopefully, you've seen that one and if you're watching
this on the video, you can go back and check that one out.
There's some good information there as
well that's related and a little bit more
in depth in some areas that we've covered here.