According to Apple’s Human Interface Guidelines for OS X (HIG), a focus ring is “Highlighting around onscreen area that is ready to accept user input.”. It is shown as a blue bezel around that control which has the keyboard focus. As I tried to implement a secondary source list today with a gradient button bar resembling this example (no, there is no focus ring visible in the picture):

Source List

.. I noticed that though it is in principle quite easy to implement such a control without using external frameworks such as BWToolkit which provide for a custom button bar: Just take a NSTableView and some NSButtons, put them in the right place in Interface Builder, and off you go. Well, in principle, as there are some problems with the focus ring (on which this post will focus).

Constructing the Source List

Let’s start with the construction of the source list: Take a NSTableView (contained in a NSScrollView) and set its highlight mode to “Source List” and its background to the default white color. Then take some Gradient Buttons and place them right beneath the NSTableView so that their borders overlap. For the rightmost button check the “Refuses First Responder” checkbox, set the “Focus Ring” setting to “None” and the “Type” to “Switch”. This will render the rightmost button as a static and useless view – which is exactly what we want.

The Crippled Focus Ring

When we now compile and run the application, the source list will show up just fine, with one exemtion. Unfortunately, the borders of the source list elements (table view, buttons) overlap, which will cause their focus rings to be drawn incompletely for those elements which are not topmost inside their common super view. The visible result is like this example taken from not from our application but from Apple’s System Preferences Print & Fax pane (in OS X 10.6.6):

Crippled Focus Ring

Crippled Focus Ring

It is easy to see that the focus rings are somewhat crippled and we can observe the same situation in our application or when using BWToolkit. The reason for this is equally simple: The elements of the source list (being NSView descendants) are drawn above and below each other. It doesn’t matter how you order their relative positions in Interface Builder as at all times that view element which has the focus ring should be the topmost element for its focus ring to show completely – which is just not possible to achieve when relying on the static order determined in the NIB file. Interesting that this is also a problem in Apple’s applications.

One possibility to overcome this problem is to draw the focus ring inside the view element instead of at its usual outside position. Apple has done so in the System Preferences Network pane (in OS X 10.6.6):

Focus Ring

Focus Ring

Focus Ring

This is not really convincing. It is not the usual focus ring the user expects to see and it is drawn in different width. It would be better to have the usual focus ring shown. Read on to learn how achieve this. [Edit: The crippled focus rings will only show if you have activated keyboard access for all controls in the keyboard system preferences pane.]

Observing firstResponder

The solution is possible in Snow Leopard only but needs only a few lines of code. In short, we have to observe the firstResponder property of the NSWindow containing our source list; this property is key-value observing compliant only since Snow Leopard. When the firstResponder changes (and the focus ring will be redrawn around the newly focused view element), we have to re-sort the view elements in a way that the element receiving the focus will be the topmost element. Once we have this, the focus ring will show as it should.

To observe the firstResponder property, add the following code in your window controller or app controller when the window is about to be shown:

[[self window] addObserver:self
    forKeyPath:@"firstResponder"
    options:NSKeyValueObservingOptionOld
    context:nil];

Add the following code in your controller when the window is about to be hidden (for example, in the windowWillClose method):

[[self window removeObserver:self forKeyPath:@"firstResponder"];

This will cause your controller to monitor changes of the focused element. When a change occurs, your observeValueForKeyPath method will be called. Therefore, drop the following method in your controller:

-(void) observeValueForKeyPath:(NSString*)keyPath
    ofObject:(id) object
    change:(NSDictionary*) change
    context:(void*) context
{
    [mainView sortSubviewsUsingFunction:
        (NSComparisonResult (*)(id, id, void*))compareViews context:nil];
}

mainView is that content view of the window containing all our source list subviews; it doesn’t have to be the ‘huge’ mainView, it could be any subview of the window as long as it contains our source list subviews. We’re calling the sortSubviewsUsingFunction method which uses a rather old-fashioned way to get a pointer to a sort function (see here for a discussion of this C style parameters). The sort function (named ‘compareViews’ in our example) is to be provided by us. For convenience, put it in the same implementation file as your controller.

Sorting Views

The sorting code I use is:

int compareViews (id firstView, id secondView, void *context);
int compareViews (id firstView, id secondView, void *context)
{
    NSResponder *responder = [[firstView window] firstResponder];
    if (!responder) return NSOrderedSame;
    if (responder == firstView) return NSOrderedDescending;
    if ([responder respondsToSelector:@selector(isDescendantOf:)]) {
        NSView *testView = (NSView*)responder;
        if ([testView isDescendantOf:firstView]) returnNSOrderedDescending;
    }
    if ([firstView isKindOfClass:[NSScrollViewclass]]) return NSOrderedDescending;
    returnNSOrderedSame;
}

This function causes the layer position (above/below) of all subviews to be adjusted every time the focus ring changes inside the main view. The sorting function is called with two view to be compared as arguments and expects a return value indicating which of the two views should be on top of the other view: If you return NSOrderedDescending, the firstView is top, if you return NSOrderedAscending, the secondView is top. If you return NSOrderedSame, nothing will change.

My sort code now just compares the window’s firstResponder property with the views passed to the function as arguments. That view which is identical to the firstResponder property has the focus ring and should be the topmost view. In principle, that’s all. You see that it is not even necessary to compare firstView and secondView, comparing just firstView and the new firstResponder is enough. It should be easily possible to move this code to a custom NSView object acting as super view for the source list. This would facilitate reusability of the code and better fit with the Model-View-Controller design pattern.

But – two more issues here:

  • There is a strange behaviour in Cocoa when we move backward through the views of a window, e.g. with <Shift+Tab>: If the focus is going to be passed to the NSScrollView, the focus is actually not passed to the NSScrollView (though this is done when moving forward through the views), but directly to the NSTableView. Therefore, we also have to check if the new firstResponder is a descendant of one of the views passed to the function to catch this issue.
  • The border lines of a NSScrollView and a gradient-style NSButton don’t have the same color. As the border line color of the gradient-style NSButton is slightly, but noticeably, darker than the border line color of the NSScrollView, the user would see the difference between the colors when the buttons are placed in front of the table view. To avoid this problem, we just place the NSScrollView topmost at the end of our function. This code is only executed if the function hasn’t already returned a result value.

That’s now really all. We have a fine source list with a gradient button bar and a working focus ring:

Focus Ring

Focus Ring

Of course, only a small detail, but in the end it’s all in the small details.