drawLayer:inContext: draws background over content when using Layer-Hosting NSView - cocoa

This is causing me some pain...
I wish to use layer-hosting views in my app and I'm having this weird problem.
Here is a simple example. Simply implemented by creating a new project in Xcode and entering the following in the AddDelegate: (after adding QuartzCore to the project):
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
NSView *thisView = [[NSView alloc] initWithFrame:CGRectInset([self.window.contentView bounds], 50, 50)];
[thisView setLayer:[CALayer layer]];
[thisView setWantsLayer:YES];
thisView.layer.delegate = self;
thisView.layer.backgroundColor = CGColorCreateGenericRGB(1,1,0,1);
thisView.layer.anchorPoint = NSMakePoint(0.5, 0.5);
[self.window.contentView addSubview:thisView];
//Create custom content
[thisView.layer display];
}
I also implement the following CALayer Delegate method:
- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx {
[[NSColor blueColor] setFill];
NSBezierPath *theBez = [NSBezierPath bezierPathWithOvalInRect:layer.bounds];
[theBez fill];
}
If I run this code, I can see the subview being added to the windows contentView (big yellow rectangle), and I'm supposing it is a layer-hosting view... and I can see the oval being drawn in blue, but it is underneath the yellow rectangle, and it's origin is at (0,0) in the main Window... it is like it is not actually being drawn inside the yellow layer.
I'm guessing either my view is not really layer-hosting, or that the context being passed to the layer is wrong... but why would it be underneath?
I must be doing something wrong...
To continue with the weirdness, if I add a CABasicAnimation to the layer, like so:
CABasicAnimation *myAnimation = [CABasicAnimation animation];
myAnimation = [CABasicAnimation animationWithKeyPath:#"transform.rotation"];
myAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
myAnimation.fromValue = [NSNumber numberWithFloat:0.0];
myAnimation.toValue = [NSNumber numberWithFloat:((360*M_PI)/180)];
myAnimation.duration = 1.0;
myAnimation.repeatCount = HUGE_VALF;
[thisView.layer addAnimation:myAnimation forKey:#"testAnimation"];
thisView.layer.anchorPoint = NSMakePoint(0.5, 0.5);
The yellow background gets animated, rotating about its center, but the blue ellipse gets drawn correctly inside the layer's frame (but also outside, at the origin of the Window, so it is there twice) but does not animate. I would expect the ellipse to rotate with the rest of the layer of course.
I have made this project available here for those willing to give a hand.
Renaud

Got it. I was confused by the fact that the context being called in this situation is a CGContextRef, not an NSGraphicsContext!
I seem to be able to get the result I need by setting the NSGraphicsContext from the CGContextRef:
NSGraphicsContext *gc = [NSGraphicsContext graphicsContextWithGraphicsPort:ctx flipped:NO];
[NSGraphicsContext saveGraphicsState];
[NSGraphicsContext setCurrentContext:gc];
//Insert drawing code here
[NSGraphicsContext restoreGraphicsState];

Related

Drawing artefact with a NSBackgroundStyleRaised text field when the parent view draws a gradient

Here is a NSView subclass that simply draws a subtle blue gradient (it's hard to see in this image but it is there) in it's bounds. Inside the view I place an NSTextField and set the cell to have NSBackgroundStyleRaised. The artefact is the background of the text field is draws with what looks like the same colour as it's superview. In this case the superview draws a gradient things look strange.
The -drawRect: for the blue view is,
- (void)drawRect:(NSRect)dirtyRect
{
[super drawRect:dirtyRect];
// Base colours
baseColor = [NSColor colorWithCalibratedRed: 0.271 green: 0.578 blue: 0.874 alpha: 1];
baseColorDarkened = [NSColor colorWithCalibratedRed: 0.159 green: 0.491 blue: 0.911 alpha: 1];
// Draw gradient
NSGradient* gradient = [[NSGradient alloc] initWithStartingColor:baseColor endingColor:baseColorDarkened];
NSBezierPath* rectanglePath = [NSBezierPath bezierPathWithRect:dirtyRect];
[gradient drawInBezierPath: rectanglePath angle: 90];
// Draw thin border
NSBezierPath *border = [NSBezierPath bezierPath];
[[baseColorDarkened blendedColorWithFraction:0.2 ofColor:[NSColor blackColor]] set];
[border setLineWidth:1];
[border stroke];
}
I'm using a document based application so in -windowControllerDidLoadNib: I set the properties of the text field,
- (void)windowControllerDidLoadNib:(NSWindowController *)aController
{
[super windowControllerDidLoadNib:aController];
[[_label1 cell] setBackgroundStyle:NSBackgroundStyleLowered];
[_label1 setBackgroundColor:[NSColor clearColor]]; // <-- Doesn't help :(
}
Some background styles seem to require special compositing (probably due to the shadows?).
OS X uses a cached & scaled representation of the superviews background when drawing the NSTextField.
I used your drawing code but changed the top color to make the cached drawing more apparent:
I found 2 workarounds:
Simply enable layer backing on the superview of the label.
When you can't enable layer backing, you could enforce a re-display of the label's superview. e.g. in the NSWindowDelegate method windowDidUpdate:
The second approach feels a bit hacky though:
- (void)windowDidUpdate:(NSNotification*)notification
{
[self.label.superview setNeedsDisplay:YES];
}
Result:

How to shadow documentView in NSScrollView?

How to shadow documentView in NSScrollView?
The effect look likes iBook Author:
You need to inset the content in your document view to allow space for the shadow to be displayed, then layer back the view and set a shadow on it. Example:
view.wantsLayer = YES;
NSShadow *shadow = [NSShadow new];
shadow.shadowColor = [NSColor blackColor]
shadow.shadowBlurRadius = 4.f;
shadow.shadowOffset = NSMakeSize(0.f, -5.f);
view.shadow = shadow;
The NSScrollView contentView is an NSView subclass, which has a shadow field, if you create a shadow object and assign it to this field, the view will automatically show a drop shadow when drawn
NSShadow* shadow = [[NSShadow alloc] init];
shadow.shadowBlurRadius = 2; //set how many pixels the shadow has
shadow.shadowOffset = NSMakeSize(2, -2); //the distance from the view the shadow is dropped
shadow.shadowColor = [NSColor blackColor];
self.scrollView.contentView.shadow = shadow;
This works because all views when are drawn on drawRect use this shadow property by using [shadow set].
doing [shadow set] during a draw operation makes whatever is drawn after that to be replicated underneath
I'm new to entering posts on stack overflow but I had the same issue and have solved it so I thought after searching the net for hours to find a solution it would be nice to answer it.
My solution is to create a subclass for NSClipView with the following code for drawRect...
- (void)drawRect:(NSRect)dirtyRect
{
[super drawRect:dirtyRect];
NSRect childRect = [[self documentView] frame];
[NSGraphicsContext saveGraphicsState];
// Create the shadow below and to the right of the shape.
NSShadow* theShadow = [[NSShadow alloc] init];
[theShadow setShadowOffset:NSMakeSize(4.0, -4.0)];
[theShadow setShadowBlurRadius:3.0];
// Use a partially transparent color for shapes that overlap.
[theShadow setShadowColor:[[NSColor grayColor]
colorWithAlphaComponent:0.95f]];
[theShadow set];
[[self backgroundColor] setFill];
NSRectFill(childRect);
// Draw your custom content here. Anything you draw
// automatically has the shadow effect applied to it.
[NSGraphicsContext restoreGraphicsState];
}
You then need to create an instance of the subclass and set it with the setContentView selector.
You also need to repaint the clip view every time the content view size changes. If you have your content view set up to change in terms of canvas size when the user wants then unless you repaint the clip view some nasty shadow marks will left behind.
You don't need to mess about with clips as others have suggested.
Hope it helps!

Prevent NSButtonCell image from drawing outside of its containing NSScrollView

I have asked (and answered) a very similar question before. That question was able to be solved because I knew the dirtyRect, and thus, knew where I should draw the image. Now, I am seeing the same behavior with an subclassed NSButtonCell:
In my subclass, I am simply overriding the drawImage:withFrame:inView method to add a shadow.
- (void)drawImage:(NSImage *)image withFrame:(NSRect)frame inView:(NSView *)controlView {
NSGraphicsContext *ctx = [NSGraphicsContext currentContext];
[ctx saveGraphicsState];
// Rounded Rect
NSBezierPath *path = [NSBezierPath bezierPathWithRoundedRect:frame cornerRadius:kDefaultCornerRadius];
NSBezierPath *shadowPath = [NSBezierPath bezierPathWithRoundedRect:NSInsetRect(frame, 0.5, 0.5) cornerRadius:kDefaultCornerRadius]; // prevents the baground showing through the image corner
// Shadow
NSColor *shadowColor = [UAColor colorWithCalibratedWhite:0 alpha:0.8];
[NSShadow setShadowWithColor:shadowColor
blurRadius:3.0
offset:NSMakeSize(0, -1)];
[[NSColor colorWithCalibratedWhite:0.635 alpha:1.0] set];
[shadowPath fill];
[NSShadow clearShadow];
// Background Gradient in case image is nil
NSGradient *gradient = [[NSGradient alloc] initWithStartingColor:[UAColor grayColor] endingColor:[UAColor lightGrayColor]];
[gradient drawInBezierPath:shadowPath angle:90.0];
[gradient release];
// Image
if (image) {
[path setClip];
[image drawInRect:frame fromRect:CGRectZero operation:NSCompositeSourceAtop fraction:1.0];
}
[ctx restoreGraphicsState];
}
It seems all pretty standard, but the image is still drawing outside of the containing scrollview bounds. How can I avoid this?
You shouldn't call -setClip on an NSBezierPath unless you really mean it. Generally you want ‑addClip, which adds your clipping path to the set of current clipping regions.
NSScrollView works by using its own clipping path and you're blowing that path away before drawing your image.
This tripped me up for a long time too.

Rounded rect on NSView that clips all containing subviews

I am creating a NSView subclass that has rounded corners. This view is meant to be a container and other subviews will be added to it. I am trying to get the rounded corners of the NSView to clip all of the subview's corners as well, but am not able to get it.
- (void)drawRect:(NSRect)dirtyRect {
NSRect rect = [self bounds];
NSBezierPath *path = [NSBezierPath bezierPathWithRoundedRect:rect xRadius:self.radius yRadius:self.radius];
[path addClip];
[[NSColor redColor] set];
NSRectFill(dirtyRect);
[super drawRect:dirtyRect];
}
The red is just for example. If I add a subview to the rect, The corners are not clipped:
How can I achieve this?
Using Core Animation layers will clip sublayers correctly.
In your container NSView subclass:
- (id)initWithFrame:(NSRect)frame
{
self = [super initWithFrame:frame];
if (self) {
self.layer = _layer; // strangely necessary
self.wantsLayer = YES;
self.layer.masksToBounds = YES;
self.layer.cornerRadius = 10.0;
}
return self;
}
You can do it in the interface builder without subclassing adding User Defined Runtime Attributes"
Have you tried clipping with layers?
self.layer.cornerRadius = self.radius;
self.layer.masksToBounds = YES;
Ah, sorry, somehow I've missed that you were talking about NSView, not UIView. It would be hard to clip NSView subviews in all cases because it seems that most of Cocoa standard views set their own clipping path. It might be easier to layout subviews with some paddings and avoid need for clipping.

NSView Not updating?

Im working on a drag n' drop view and found some handlers for drag and drop actions on the web. I want to make it so it turns blue when the user drags a file over the drag and drop area and gray again when they exit the drag and drop area. The issues is its not updating when you drag your mouse over it or exit it. Heres some of the code:
- (void)drawRect:(NSRect)rect
{
NSRect bounds = [self bounds];
[[NSColor grayColor] set];
[NSBezierPath fillRect:bounds];
}
- (NSDragOperation)draggingEntered:(id <NSDraggingInfo>)sender {
NSRect bounds = [self bounds];
[[NSColor blueColor] set];
[NSBezierPath fillRect:bounds];
return NSDragOperationCopy;
}
- (void)draggingExited:(id <NSDraggingInfo>)sender {
NSRect bounds = [self bounds];
[[NSColor grayColor] set];
[NSBezierPath fillRect:bounds];
}
Thanks for any help.
Are you calling [yourView: setNeedsDisplay] anywhere?
This is how you let the drawing framework know it needs to message your UIView subclass with drawRect:, so you should do it whenever things have changed. In your case, this probably means when the mouse enters or exits the drop area.
Drawing only works when a context (like a canvas for painting) is set up for you to draw into. When the framework calls -drawRect: it has set up a drawing context for you, so drawing commands like -[NSColor set] and -[NSBezierPath fillRect:] work as you expect.
Outside of -drawRect: there is usually no drawing context set up. Using drawing commands outside of -drawRect: is like waving a paintbrush in the air; there's no canvas, so no painting happens.
In 99.99% of cases, all view drawing should be kept within -drawRect: because NSView does a lot of work that you don't want to do to get the drawing context set up correctly and efficiently.
So, how do you change your view's drawing within your -draggingEntered: and -draggingExited: methods? By side effects.
You're doing the same thing in all three cases: 1) Setting a color and 2) Drawing a rectangle. The only difference is the color changes in each method. So, why not control which color you use in -drawRect: with an ivar, like so:
- (void)draggingEntered:(id <NSDraggingInfo>)sender {
drawBlueColorIvar = YES;
// ...
}
Then in -drawRect: you do this:
- (void)drawRect:(NSRect)rect {
NSColor *color = drawBlueColorIvar ? [NSColor blueColor] : [NSColor grayColor];
[color set];
[NSBezierPath fillRect:rect];
}
(Notice I didn't use [self bounds]. It is more efficient to just draw into the "dirty" rect, when possible.)
Finally, you need some way to tell the framework that your view needs to redraw when drawBlueColorIvar changes. The framework won't draw anything unless it's told it needs to. As Chris Cooper said, you do this with [self setNeedsDisplay:YES]. This should go after any place you change drawBlueColorIvar.

Resources