Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Subclassing NSScrollView drawRect: Method

I'm customizing the UI for one of my apps, and the idea is that a text area is initially bordered gray when out of focus, and when it comes into focus, the border becomes bright white. My app uses a dark theme, and for a single-lined NSTextField, this works great.

I'm running into problems with a subclassed NSTextView, however. In order to alter the border properly, I ended up having to actually subclass the parent NSScrollView, but am still seeing strange behavior. (See screenshot below.) I want the red box to fill the entire scroll view, as this would allow me to stroke (instead of filling, which is just for testing) the path, producing a nice border. Instead, the red box seems to be only filling to the internal child view.

The following code snippet, which is for the NSScrollView subclass:

- (void)drawRect:(NSRect)dirtyRect {
    [super drawRect:dirtyRect];

    NSRect borderRect = self.bounds;
    borderRect.origin.y += 1;
    borderRect.size.width -= 1;
    borderRect.size.height -= 4;

    BOOL inFocus = ([[self window] firstResponder] == self);

    if (!inFocus) {
        inFocus = [self anySubviewHasFocus:self];
    }

    if (inFocus) {
        [[NSColor colorWithDeviceRed:.8 green:.2 blue:0 alpha:1] set];
    } else {
        [[NSColor colorWithDeviceRed:.1 green:.8 blue:0 alpha:1] set];
    }

    [NSGraphicsContext saveGraphicsState];
    [[NSGraphicsContext currentContext] setShouldAntialias:NO];
    [NSBezierPath fillRect:borderRect];
    [NSGraphicsContext restoreGraphicsState];

    NSLog(@"My bounds: %@", NSStringFromRect(borderRect));
    NSLog(@"Super (%@) bounds: %@", [self superview], NSStringFromRect(borderRect));
}

Produces the screenshot as seen below. Also, see the output in the log, which suggests that the entire view should be filled. This is the only output that is ever shown, regardless of the size of the text inside. Entering carriage returns increases the height of the red box, but does not produce different output. (And I would like the red box to fill the entire bounds.)

2011-04-08 21:30:29.789 MyApp[6515:903] My bounds: {{0, 1}, {196, 87}}
2011-04-08 21:30:29.789 MyApp[6515:903] Super (<EditTaskView: 0x3a0b150>) bounds: {{0, 1}, {196, 87}}

Drawing Issue

Edit: Thanks to Josh Caswell for his answer. See below for the proper behavior when not focused, and when focused.

No Focus

Focus

like image 387
Craig Otis Avatar asked Feb 24 '23 13:02

Craig Otis


1 Answers

As ughoavgfhw noted, NSScrollView doesn't usually do any drawing, and probably has a weird interaction with its child views in that way. I'd suggest putting something like the following in your text view's drawing code to draw this custom focus ring that you want*:

// We're going to be modifying the state for this, 
// so allow it to be restored later
[NSGraphicsContext saveGraphicsState];

// Choose the correct color; isFirstResponder is a custom     
// ivar set in becomeFirstResponder and resignFirstResponder
if( isFirstResponder && [[self window] isKeyWindow]){
    [myFocusedColor set];
} 
else {
    [myNotFocusedColor set];
}

// Create two rects, one slightly outset from the bounds,
// one slightly inset
NSRect bounds = [self bounds];
NSRect innerRect = NSInsetRect(bounds, 2, 2);
NSRect outerRect = NSMakeRect(bounds.origin.x - 2, 
                              bounds.origin.y - 2,
                              bounds.size.width + 4,
                              bounds.size.height + 4);

// Create a bezier path using those two rects; this will
// become the clipping path of the context
NSBezierPath * clipPath = [NSBezierPath bezierPathWithRect:outerRect];
[clipPath appendBezierPath:[NSBezierPath bezierPathWithRect:innerRect]];

// Change the current clipping path of the context to 
// the enclosed area of clipPath; "enclosed" defined by 
// winding rule. Drawing will be restricted to this area.
// N.B. that the winding rule makes the order that the
// rects were added to the path important.
[clipPath setWindingRule:NSEvenOddWindingRule];
[clipPath setClip];
// Fill the rect; drawing is clipped and the inner rect
// is not drawn in
[[NSBezierPath bezierPathWithRect:outerRect] fill];
[NSGraphicsContext restoreGraphicsState];

This should be a reasonable approximation of what AppKit does when it draws a focus ring. Of course, AppKit is sort of allowed to draw outside a view's bounds -- I can't guarantee that this is completely safe, but you seem to get a margin of 3 px to play with. You could draw the ring entirely inside the bounds if you wanted. A true focus ring extends slightly (2 px) inside the view anyways (as I've done here).

Apple docs on Setting the Clipping Region.

EDIT: After re-reading your comments on the question, I realize I may have long-winded-ly buried the real answer. Try either subclassing NSClipView and switching your scroll view's clip view for that, or using a custom view for the document view.


*: You could also put this in the drawing code of a custom view subclass which is set as the document view of the NSScrollView; then your text view could be a subview of that. Or substitute a custom NSClipView subclass.

like image 127
jscs Avatar answered Mar 03 '23 03:03

jscs