Frameworks • OS X • 54:46
Increase the productivity of your users by expediting common operations via keyboard shortcuts. Discover your options for implementing key equivalents for menus and controls, and how hot keys and Services can handle events from within any app. Learn the ways that users can customize key equivalents and how your app can facilitate their use. Also learn about advanced techniques for emulating contextual menus when you need ultimate control.
Speakers: Peter Ammon, Raleigh Ledet
Unlisted on Apple Developer site
Downloads from Apple
Transcript
This transcript has potential transcription errors. We are working on an improved version.
[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.