5

I have a window with text field and bezel button. It's borderless and transparent, but the issue reproduces on any window.

The content view of that window is set in IB to a custom class that draws window's background.

Here's the code:

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

    float cornerRadius = 10;
    NSBezierPath *path = [NSBezierPath bezierPathWithRoundedRect:self.bounds xRadius:cornerRadius yRadius:cornerRadius];

    [path setClip];

    NSGradient *gradient = [[NSGradient alloc] initWithColorsAndLocations:
                            [NSColor colorWithCalibratedRed:0.96f green:0.96f blue:0.96f alpha:1.00f], 0.0f,
                            [NSColor colorWithCalibratedRed:0.84f green:0.84f blue:0.84f alpha:1.00f], 1.0f,
                            nil];

    [gradient drawInRect:self.bounds angle:270];

    [NSGraphicsContext restoreGraphicsState];
}

It causes some really weird artifacts, like disappearing objects or the text field's background changing to window's:

enter image description here enter image description here

What's going on? I've tried to isolate it, I've been playing around with this "graphics context state saving" thing (which I'm not sure if I understand correctly), but the problem persists.

I have XCode 4.4, SDK is 10.7 (so is my OS), and the deployment target is 10.6. It probably doesn't matter, but I've been doing something similar in the past and I've never had such strange issues.

radex
  • 6,336
  • 4
  • 30
  • 39

3 Answers3

6

The problem seems to be the [path setClip] method. I commented this out and the text field and button drew properly.

So, I then replaced the line:

[gradient drawInRect:self.bounds angle:270];

with

[gradient drawInBezierPath:path angle:270];

and everything worked perfectly. The button and text field appeared as expected.

The Class Reference for NSBezierPath states:

You should avoid using this method as a way of adjusting the clipping path, as it may expand the clipping path beyond the bounds set by the enclosing view. If you do use this method, be sure to save the graphics state prior to modifying the clipping path and restore the graphics state when you are done.

This method uses the current winding rule to determine the clipping shape of the receiver. This method does not affect the receiver’s path.

Does not really explain the problems you saw, but lead me to see how else I could draw the gradient.

Community
  • 1
  • 1
Greg Walters
  • 181
  • 5
  • Thanks for your answer, I'll look into it tomorrow! – radex Aug 27 '12 at 18:05
  • Hey, it seems to work! :) But what if I need to clip my "rounded corners" bezier path to dirty rect? What do I use? +clipRect: of NSBezierPath? It seems to work, but I don't know about edge cases... – radex Aug 28 '12 at 19:32
  • Looking at the drawRect for the view, it is being passed a dirtyRect for the whole view and then the dirtyRect for the Text Field, essentially every time the input cursor blinked. I think this is why the clipping was affecting the Text Field and Button. So the question is, when would you want to apply the "rounded corners" to the dirtyRect. Since dortyRect will represent different parts of the view at different times, you will affect those parts if you use dirtyRect. – Greg Walters Aug 29 '12 at 20:07
  • It all makes sense to me now. When the input cursor blinked, -drawRect was called for window's content view and then for text field, but since the button hasn't changed, I've drawn over button's image. Is that correct? My experience is in webdev, where every layer is independent (like in Core Animation layers, as far as I understand), so it's pretty esoteric to me. – radex Aug 29 '12 at 20:37
  • But I don't understand one thing: if that's so, why does [gradient drawInBezierPath:path angle:270]; work, but [path setClip];[gradient drawInRect:self.bounds angle:270]; doesn't? Is the path in the former automagically clipped to the dirty rect? This is odd. – radex Aug 29 '12 at 20:38
  • I can't explain it either. I found another Stack Overflow question similar to your's and it talked about never using setClip instead using addClip. When I did this, everything work in your original code. [Here's the link](http://stackoverflow.com/questions/8535613/redrawed-inset-nsshadow-on-a-custom-view-using-setclip-method) – Greg Walters Aug 30 '12 at 14:54
0

One workaround to make it work is to make (at window's -awakeFromNib or -init) NSImage with the same size as self.bounds and then draw this gradient into the image (use image's -lockFocus, -unlockFocus). You can then set the image as background using window.backgroundColor = [NSColor colorWithPatternImage:image];

It works, but I'm not happy with the solution.

I don't see any reason why above wouldn't work. And I've had similar odd problems with drawing on views, so I want to know the real reason why it didn't work.

radex
  • 6,336
  • 4
  • 30
  • 39
0

I agree this is weird / annoying. One would think that it's just a matter of where the layers may be interfering with one another.. but it seems difficult to solve without doing the following..

Here is the view as you describe, messed up. and after - with a simple checkbox changed via interface builder..

enter image description here

enter image description here

I have found that by enclosing the troubled controls in a box or a view or whatnot and then clicking on the "Core animation" "setWantsLayer" button in interface builder will allow those interface elements to shine through without all the kurfuffle.

enter image description here

Kind of a dumb workaround… but in reality it works - and is easy.

PS - I find that it helps to make parameters a little bit more dramatic (colors, etc) when troubleshooting this kind of thing - especially if you're trying to describe to people what's wrong..

Alex Gray
  • 16,007
  • 9
  • 96
  • 118
  • Hey, thanks for the tip. It seems useful, though for a particular project where I had these problems using CA layers wasn't an option (because the app was based around WebKit, which for some reason, it appears, messes up when you use layers) – radex Aug 27 '12 at 18:07
  • Interesting! Seems to work, but I get this notice: "It does not make sense to draw an image when [NSGraphicsContext currentContext] is nil. This is a programming error. Break on void _NSWarnForDrawingImageWithNoCurrentContext() to debug. This will be logged only once. This may break in the future.". What does it mean? – radex Aug 28 '12 at 19:35
  • did you "link" to the `QuartzCore.framework`? It works for me also **without** the `[NSGraphicsContext saveGraphicsState];` and `[NSGraphicsContext restoreGraphicsState];`. Just get'em outta there! You generally only need to worry about your contexts when you are mixing cocoa and quartz drawing, or locking focus on particular "sub-rects" within your drawing routines. – Alex Gray Aug 28 '12 at 19:41
  • I always forget to link against QuartzCore ;) (but it still worked, did something change in ML?) I had [path setClip]; in code, so AFAIK saving/restoring graphics context is necessary – radex Aug 28 '12 at 20:20