Introduction
We sometimes develop Mac OS X applications that contain forms where the user needs to enter different types of pieces of data (into text fields, pop-up buttons, radio buttons, etc.). If the form contains a lot of fields to fill in, or if the user needs to fill it in often, we would like to make filling out the form quick and easy. This would (hopefully) be achieved if the user could tab through all the fields and enter all information without having to grab the mouse.
There is a setting in the Keyboard section of Mac OS X that is ideal for this. It is called "Full Keyboard Access".
The problem is that by default, Full Keyboard Access is switched off. This means that when you tab, focus jumps between text boxes and lists and skips over all other GUI components such as popup-buttons, buttons, sliders, etc.
Edit Note: A reader of this article brought to my attention that there is a far simpler way to achieve the Full Keyboard Access behaviour than the method described in the rest of this article. Please read the comment below as well as its response at the bottom of this article.
Background
Before thinking of a solution, let's stop first and have a rough review of how keyboard events in Mac OS X applications work. In Mac OS X, there is always one application that is active, and one can quickly change which application is active by pressing Cmd
+Tab
. The active application is the application that receives keyboard inputs. More specifically, if an application has multiple windows, exactly one window is the current key window of the application. It is called the key window, because this is the window which receives keyboard events from the application. Furthermore, a window has a first responder, which is the component of that window that currently receives all keyboard events. If a text field on a window is currently focused, it receives all keyboard events so that you can type into that text field and we say that the text field is the current
"first responder" (Why first responder? Basically, because if the text field does not handle the keyboard event, the event moves up the so-called responder chain, which consists of the first-responder view, followed by its superviews, followed by the key window). In case you would like to read more about keyboard events, the chapter "Keyboard Events" in the book "Cocoa Programming for Mac OS X" by Hillegass & Preble provides a good overview of the subject.
Returning to the problem at hand, let's suppose that the user of our application has Full Keyboard Access switched to the default setting, which is "off" (text boxes and lists only). We could try to tell the user to switch this setting on, but that's not ideal. It is easy to query if this setting is on by calling the isFullKeyboardAccessEnabled
method in the NSApplication
class, but I could not find a way to programmatically alter this setting. Besides, it's typically not a good idea to automatically make changes to the user's system settings.
Maybe we can somehow programmatically override the default tabbing-behaviour in our application. On that point, how does a view on a key window become first responder when the user tabs from one view to the next? When the user presses Tab, a keyboard event is emitted to the first responder of the key window. If the first responder - or any of its superviews - does not handle the event (and most views do not handle a Tab-press), the key window will receive the event and handle it as follows: it queries the nextKeyView
property of the current first responder to find the next view (the nextKeyView
property of each view can be set using Xcode's interface builder). The key window asks the next view if it accepts first responder status by calling its acceptsFirstResponder
method. If the view declines, the key view queries the nextKeyView
property of that view and continues.
Based on this fact, could we make a pop-up button (which does not become first responder if Full Keyboard Access is off) become first responder when tabbed into by sub-classing it and making its acceptsFirstResponder
method always return YES
? Apparently not. It turns out that NSPopupButton
actually does always return YES
, irrespective of the current Full Keyboard Access setting. By investigation, we can determine that before the pop-up button would have become first responder, the window actually calls its acceptsFirstResponder
method (which returns YES
), but it never calls its becomeFirstResponder
method, and never makes the pop-up button first responder, but moves on to its nextKeyView
instead. This behaviour is quite interesting (why does it even bother calling acceptsFirstResponder
? O.o ), but it does show that the window somehow identifies the pop-up button as one of those views that are not made first responder when Full Keyboard Access is off.
A pop-up button that has first-responder status
Can we somehow manipulate which type of NSView
objects are identified by the key window as "views that are made first responder"? Well, I really don't know how these views are identified by the key window, so to me, this is a dead end. Edit: In his comment, uchuugaka provides the answer to this question: the key window calls the NSView
's canBecomeKeyView
method. Somehow this method has completely evaded me! O.o
So how can this work?! How about we somehow override the window's logic which finds the next view to make first responder? Evidently, this is possible! This is the approach taken in this article and is explained in the following section.
Implementation
When the user presses Tab, it causes a keyboard event to be fired within our application. The key window's first responder is asked to handle the event first. If it chooses not to handle it, the keyboard event moves up the responder chain until someone handles it. We can register an event monitor to listen for specific types of events and handle them if we choose to. This is achieved by the following code statement:
id keyDownMonitor = [NSEvent addLocalMonitorForEventsMatchingMask:TypeOfEventMask handler:^ NSEvent* (NSEvent* event) {
if ( )
{
return nil;
}
return event;
}];
If we wish, we could then later deregister the monitor like this:
[NSEvent removeMonitor:keyDownMonitor];
We are interested in events matching the mask NSKeyDownMask
, and where the keyCode
of the NSEvent
is 48
(for the tab key), so here we go!
First, we need to take care of an awkward technicality. We will often find that the first responder of the key window is some obscure type of sub-view of a familiar view that we are actually interested in. For instance, if a text field is first responder, you might find that [NSApplication sharedApplication].keyWindow.firstResponder
yields an object of type NSTextView
. The superview
of this object would be an _NSKeyboardFocusClipView
, and the superview
of that would be the NSTextField
. Here is the code that finds the view that we would like to work with:
NSView* currentKeyView = (NSView*) keyWindow.firstResponder;
while ((currentKeyView) &&
(! [currentKeyView.class isSubclassOfClass:[NSTextField class]]) &&
(! [currentKeyView.class isSubclassOfClass:[NSDatePicker class]]) &&
(! [currentKeyView.class isSubclassOfClass:[NSPopUpButton class]]) &&
(! [currentKeyView.class isSubclassOfClass:[NSButton class]]) &&
(! [currentKeyView.class isSubclassOfClass:[NSStepper class]]) &&
(! [currentKeyView.class isSubclassOfClass:[NSSegmentedControl class]]) &&
(! [currentKeyView.class isSubclassOfClass:[NSMatrix class]]) &&
(! [currentKeyView.class isSubclassOfClass:[NSSlider class]]))
currentKeyView = currentKeyView.superview;
Of course we now only support a limited number of types of views. I didn't say that my solution was perfect, so what are you gonna do?
The next step is to call the nextKeyView
method on the view (or previousKeyView
, if the user is pressing the Shift-key along with Tab). On the view that is returned, we ask it if it is prepared to become first responder, by calling its acceptsFirstResponder
method. If it returns YES
, we have found the new first responder, otherwise we call that view's nextKeyView
to find the next candidate. Finally, to actually bestow first-responder status upon the view, we call the makeFirstResponder:
method of the key window.
This leads to the following code statement:
id keyDownMonitor = [NSEvent addLocalMonitorForEventsMatchingMask:NSKeyDownMask handler:^ NSEvent* (NSEvent* event) {
NSWindow* keyWindow = [NSApplication sharedApplication].keyWindow;
if ((event.keyCode==48 ) && ([keyWindow.firstResponder.class isSubclassOfClass:[NSView class]]))
{
NSView* currentKeyView = (NSView*) keyWindow.firstResponder;
while ((currentKeyView) &&
(! [currentKeyView.class isSubclassOfClass:[NSTextField class]]) &&
(! [currentKeyView.class isSubclassOfClass:[NSDatePicker class]]) &&
(! [currentKeyView.class isSubclassOfClass:[NSPopUpButton class]]) &&
(! [currentKeyView.class isSubclassOfClass:[NSButton class]]) &&
(! [currentKeyView.class isSubclassOfClass:[NSStepper class]]) &&
(! [currentKeyView.class isSubclassOfClass:[NSSegmentedControl class]]) &&
(! [currentKeyView.class isSubclassOfClass:[NSMatrix class]]) &&
(! [currentKeyView.class isSubclassOfClass:[NSSlider class]]))
currentKeyView = currentKeyView.superview;
if (currentKeyView)
{
NSView* nextKeyView = currentKeyView;
NSMutableArray* chain = [[NSMutableArray alloc] init];
do
{
[chain addObject:nextKeyView];
nextKeyView = (event.modifierFlags & NSShiftKeyMask)
? nextKeyView.previousKeyView : nextKeyView.nextKeyView;
}
while ((nextKeyView) && (! [chain containsObject:nextKeyView]) &&
(! nextKeyView.acceptsFirstResponder));
[chain release];
if (nextKeyView)
[keyWindow makeFirstResponder:nextKeyView];
return nil;
}
}
return event;
}];
There you have it - we have solved the problem with just one (huge) assignment statement!
A final touch
Tabbing through UI elements works nicely now. But if the user clicks on a pop-up button it still doesn't become first responder. To change the value selected by the pop-up button using the keyboard, a user would have to click on something like a text field, and then tab to the pop-up button (by the way, it's the same with Full Keyboard Access switched on).
To make the pop-up button take on first responder status when it is clicked, we only have to catch the mouse-down event and call makeFirstResponder
with the pop-up button. Sadly, I could not find an elegant way to catch the mouse-down events. We could create a sub-class of each type of view and override the mouseDown:
method to broadcast a mouse-down notification via NSNotificationCentre
. But that is impractical, since we would have to replace all the views in our xib's with our special custom mouse-down-broadcaster views. Nevertheless, overriding the mouseDown:
method actually is the approach that I have taken - but without sub-classing. Admittedly, it's a bit ugly, but it works.
Thanks to the dynamic nature of Objective-C method calls, we can replace the implementation of a class's method with our own... during run-time! This is achieved using a technique called "method swizzling". This technique is explained by Mike Ash here, so I won't go into details (the resource also explains why writing the method in a category won't cut it).
Basically, I have created a category (and it could just as well have been a regular class), where the mouseDown:
methods of the most common views that don't normally receive first responder status via a mouse click are swapped with methods that first broadcast a notification and then call the original method. Elsewhere, I can then register to receive these notifications and make the view that was clicked the first responder. To see how it is done, you can have a look at the NSView+EventNotifications
category in this article's source code.
Using the code
If you had read through the previous sections, you'd probably have known what to do and what code to copy. However, to make things a bit easier, I have created a demo-application (see the source code of this article) that demonstrates the concepts in action and contains two classes that you can copy and use in your own project. This is what the demo application looks like:
When the "Override Default Behaviour" check box is ticked, the "Full Keyboard Access" check box controls if you can tab through all of the views (except if the Full Keyboard Access setting is switched on in your Mac settings - then you will tab through all the views anyway :\ ).
When "Override Default Behaviour" is not ticked, the "Full
Keyboard Access" check box is disabled and its tick indicates if the Full
Keyboard Access setting is switched on in the Mac OS X settings.
To use the code in your own project copy the four source files in the folder FullKeyboardAccess/FullKeyboardAccess/FullKeyboardAccess into your project. To enable full-keyboard-access, simply call the enableFullKeyboardAccess
method of the FullKeyboardAccess
class. If you do not want to enable the click-to-focus function, as described in the A final touch section above, simply comment out the following line in FullKeyboardAccess.h:
#define ENABLE_FOCUS_ON_CLICK
This will remove FullKeyboardAccess
's dependency upon the NSView+EventNotifications
category and you won't have to copy the NSView+EventNotifications.h and -.m files into your project.
History
-
2013/02/03
- 2013/04/02
- Added a link to and a note in the article about uchuugaka's comment.