Skip to content

Leopard hates custom views that draw focus rings

The Cocoa release notes say this:

To help guarantee correct redraw of focus rings, AppKit may automatically draw additional parts of a window beyond those that application code marked as needing display. It may do this for the first responder view in the application’s key window, if that first responder view’s focusRingType property is set to a value other than NSFocusRingTypeNone. Any view that can become first responder, but doesn’t draw a focus ring, should have its focusRingType set to NSFocusRingTypeNone to avoid unnecessary additional redraw.

What this means is that when your view calls NSSetFocusRingStyle(NSFocusRingOnly), this ends up calling -[NSWindow _setLastFocusRingView:bleedRegion:], with this view as the first argument. From that point forward, until another view becomes first responder, the entire view will be redrawn any time part of it becomes dirty. This effectively kills your drawing performance.

Setting the view’s focusRingType to NSFocusRingTypeNone has no effect.

There are a few private methods that AppKit is using here.


@interface NSView (NSPrivateFocusRingSupport)
- (id)_focusRingBleedRegion;
- (id)_focusRingClipAncestor;
@end

@interface NSView : NSResponder <NSAnimatablePropertyContainer>
- (NSRect)_focusRingVisibleRect;
@end

NSView’s implementation of _focusRingBleedRegion roughly does this:

  • Converts the visible rect to window base coordinates, scales it, and then insets it be -3 pixels on each side.
  • Calls -_focusRingClipAncestor, and converts that view’s _focusRingVisibleRect to window base coordinates.
  • Creates an NSRegion out of the intersection of two of these rectangles (not exactly sure which two).
  • Insets a rectangle and subtracts that from the region.
  • Returns this region.

The subclasses _NSBroswerScrollView, NSButton, and NSTableView, each implement this method, presumably because they need to. But if they need to, isn’t it likely that other views might need to as well? Except that it’s private, and uses an entirely private class, NSRegion.

Fortunately my workaround doesn’t need to use this method. A little experimentation showed that the focus view and region can be nil. So I just need to override -[NSWindow setLastFocusView:bleedRegion:] and pass on nil values for my particular view:


@implementation NSView (STExtensions)

// This is used on 10.5 by my NSWindow category to work around a problem with focus ring bleed regions.  The default is YES,
// meaning just do the normal behavior.  Subclasses can return NO to disable AppKit's automatic bleed region dirtying for them.
- (BOOL)canControlFocusRingBleedRegion;
{
    return YES;
}

@end

@implementation NSWindow (STExtensions)

+ (void)load;
{
    if (self == [NSWindow class]) {
        Method originalMethod, replacementMethod;

        originalMethod = class_getInstanceMethod(self, @selector(_setLastFocusRingView:bleedRegion:));
        replacementMethod = class_getInstanceMethod(self, @selector(replacement_setLastFocusRingView:bleedRegion:));
        method_exchangeImplementations(originalMethod, replacementMethod);
    }
}

- (void)replacement_setLastFocusRingView:(id)view bleedRegion:(id)region;
{
    if ([view canControlFocusRingBleedRegion]) {
        [self replacement_setLastFocusRingView:view bleedRegion:region]; // Not really recursive.
    } else {
        // We can't control it, so disable it.
        [self replacement_setLastFocusRingView:nil bleedRegion:nil]; // Not really recursive.
    }
}

Then my view just has to return NO from -canControlFocusRingBleedRegion. This works like a charm, assuming your view draws it’s focus ring correctly in all cases, which mine does.

Here’s what the Cocoa release notes have to say about private methods:

The last point (Increased restricted visibility of private symbols in the runtime) is especially important, since it would prevent applications using these symbols from launching in future releases. As always: Please do not access undeclared APIs in your applications. This includes undeclared classes, methods, and instance variables. If you have a strong need for a private API and have no workaround, please let us know.

This is a perfect example of Apple leaving us no choice but to use private API. I’ve filed a bug report about this case.

Post a Comment

You must be logged in to post a comment.