How to draw a native looking statusitem background on a CALayer - cocoa

Actually I want to draw the background of a selected NSStatusItem on the CALayer of my custom statusItemView. But since
- (void)drawStatusBarBackgroundInRect:(NSRect)rect withHighlight:(BOOL)highlight
does not work (?) on layers I've tried it to draw the color with the backgroundColor property. But converting the selectedMenuItemColor into RGB doesn't help very much. It looks really plain without the gradient. :-/
I converted [NSColor selectedMenuItemColor] into a CGColorRef with this code:
- (CGColorRef)highlightColor {
static CGColorRef highlight = NULL;
if(highlight == NULL) {
CGFloat red, green, blue, alpha;
NSColor *hlclr = [[NSColor selectedMenuItemColor] colorUsingColorSpace:
[NSColorSpace genericRGBColorSpace]];
[hlclr getRed:&red green:&green blue:&blue alpha:&alpha];
CGFloat values[4] = {red, green, blue, alpha};
highlight = CGColorCreate([self genericRGBSpace], values);
}
return highlight;
}
Any idea how to draw a native looking statusitem background on a CALayer?

NSImage *backgroundImage = [[NSImage alloc] initWithSize:self.frame.size]];
[backgroundImage lockFocus];
[self.statusItem drawStatusBarBackgroundInRect:self.bounds withHighlight:YES];
[backgroundImage unlockFocus];
[self.layer setContents:backgroundImage];
[backgroundImage release];

Try subclassing CALayer and implementing the drawInContext: method to create an NSGraphicsContext for the CGContext, set the NSGraphicsContext as the current context, and then tell the status item to draw its background.

I use this code in my layer delegate:
- (void)drawLayer:(CALayer *)layer
inContext:(CGContextRef)context {
NSGraphicsContext* gc = [NSGraphicsContext graphicsContextWithGraphicsPort:context flipped:NO];
[NSGraphicsContext saveGraphicsState];
[NSGraphicsContext setCurrentContext:gc];
[self.statusItem drawStatusBarBackgroundInRect:self.frame
withHighlight:self.isHighlighted];
[NSGraphicsContext restoreGraphicsState];
}

Related

NSScrollView background image in Lion

I'm working on a Mac application with an NSScrollView, and I want the NSScrollView to have a custom background image. I used this code in the custom documentView NSView subclass:
- (void)drawRect:(NSRect)rect {
[[NSColor colorWithPatternImage:[NSImage imageNamed:#"wood.jpg"]] set];
NSRectFill(rect);
}
That displays a pattern image as a background for the documentView.
But now in Mac OS X Lion, the NSScrollView bounces when scrolling further than possible, showing ugly white space. How can I make the white space also being covered by the background image?
Instead of overriding drawRect:, use the scroll view's setBackgroundColor: method, passing the NSColor you created with the pattern image.
You should subclass use NSScrollView setBackgroundColor, but then you should subclass NSClipView like this to pin the texture origin to the top:
#implementation MYClipView
- (id)initWithFrame:(NSRect)frame
{
self = [super initWithFrame:frame];
if (self) {
// Initialization code here.
}
return self;
}
- (void)drawRect:(NSRect)dirtyRect
{
if (self.drawsBackground)
{
NSGraphicsContext* theContext = [NSGraphicsContext currentContext];
[theContext saveGraphicsState];
float xOffset = NSMinX([self convertRect:[self frame] toView:nil]);
float yOffset = NSMaxY([self convertRect:[self frame] toView:nil]);
[theContext setPatternPhase:NSMakePoint(xOffset, yOffset)];
NSColor* color = self.backgroundColor;
[color set];
NSRectFill([self bounds]);
[theContext restoreGraphicsState];
}
// Note: We don't call [super drawRect:dirtyRect] because we don't need it to draw over our background.
}
+ (void)replaceClipViewInScrollView:(NSScrollView*)scrollView
{
NSView* docView = [scrollView documentView]; //[[scrollView documentView] retain];
MYClipView* newClipView = nil;
newClipView = [[[self class] alloc] initWithFrame:[[scrollView contentView] frame]];
[newClipView setBackgroundColor:[[scrollView contentView] backgroundColor]];
[scrollView setContentView:(NSClipView*)newClipView]; [scrollView setDocumentView:docView];
// [newClipView release];
// [docView release];
}
#end
And call + (void)replaceClipViewInScrollView:(NSScrollView*)scrollView with the NSScrollView instance.
Put your drawRect code in an NSScrollView subclass. In IB, change the NSScrollView to use your custom subclass instead of NSScrollView. Also make sure to uncheck Draw Background in the scroll view's attributes inspector.

How to draw a blurred shape?

How do I draw a blurred shape in Cocoa? Think of a shadow with a blurRadius accompanying a filled path, but without sharp-edged foreground path shape.
What I tried is using a filled path with a shadow, and setting the fill color to transparent (alpha 0.0). But that makes the shadow invisible as well, as it is apparently taking the shadow casting "object's" alpha into account.
This is actually reasonably tricky. I struggled with this for a while until I came up with this category on NSShadow:
#implementation NSShadow (Extras)
//draw a shadow using a bezier path but do not draw the bezier path
- (void)drawUsingBezierPath:(NSBezierPath*) path alpha:(CGFloat) alpha
{
[NSGraphicsContext saveGraphicsState];
//get the bounds of the path
NSRect bounds = [path bounds];
//create a rectangle that outsets the size of the path bounds by the blur radius amount
CGFloat blurRadius = [self shadowBlurRadius];
NSRect shadowBounds = NSInsetRect(bounds, -blurRadius, -blurRadius);
//create an image to hold the shadow
NSImage* shadowImage = [[NSImage alloc] initWithSize:shadowBounds.size];
//make a copy of the shadow and set its offset so that when the path is drawn, the shadow is drawn in the middle of the image
NSShadow* aShadow = [self copy];
[aShadow setShadowOffset:NSMakeSize(0, -NSHeight(shadowBounds))];
//lock focus on the image
[shadowImage lockFocus];
//we want to draw the path directly above the shadow image and offset the shadow so it is drawn in the image rect
//to do this we must translate the drawing into the correct location
NSAffineTransform* transform=[NSAffineTransform transform];
//first get it to the zero point
[transform translateXBy:-shadowBounds.origin.x yBy:-shadowBounds.origin.y];
//now translate it by the height of the image so that it draws outside the image bounds
[transform translateXBy:0.0 yBy:NSHeight(shadowBounds)];
NSBezierPath* translatedPath = [transform transformBezierPath:path];
//apply the shadow
[aShadow set];
//fill the path with an arbitrary black color
[[NSColor blackColor] set];
[translatedPath fill];
[aShadow release];
[shadowImage unlockFocus];
//draw the image at the correct location relative to the original path
NSPoint imageOrigin = bounds.origin;
imageOrigin.x = (imageOrigin.x - blurRadius) + [self shadowOffset].width;
imageOrigin.y = (imageOrigin.y - blurRadius) - [self shadowOffset].height;
[shadowImage drawAtPoint:imageOrigin fromRect:NSZeroRect operation:NSCompositeSourceOver fraction:alpha];
[shadowImage release];
[NSGraphicsContext restoreGraphicsState];
}
#end
I found one implementation which does exactly what I meant online. Written by Sean Patrick O'Brien. It is a category on NSBezierPath, like so:
#interface NSBezierPath (MCAdditions)
- (void)drawBlurWithColor:(NSColor *)color radius:(CGFloat)radius;
#end
#implementation NSBezierPath (MCAdditions)
- (void)drawBlurWithColor:(NSColor *)color radius:(CGFloat)radius
{
NSRect bounds = NSInsetRect(self.bounds, -radius, -radius);
NSShadow *shadow = [[NSShadow alloc] init];
shadow.shadowOffset = NSMakeSize(0, bounds.size.height);
shadow.shadowBlurRadius = radius;
shadow.shadowColor = color;
NSBezierPath *path = [self copy];
NSAffineTransform *transform = [NSAffineTransform transform];
if ([[NSGraphicsContext currentContext] isFlipped])
[transform translateXBy:0 yBy:bounds.size.height];
else
[transform translateXBy:0 yBy:-bounds.size.height];
[path transformUsingAffineTransform:transform];
[NSGraphicsContext saveGraphicsState];
[shadow set];
[[NSColor blackColor] set];
NSRectClip(bounds);
[path fill];
[NSGraphicsContext restoreGraphicsState];
[path release];
[shadow release];
}
#end
This code is downloadable from this blog post. I didn't find it separately as code anywhere online.

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.

Image inside NSScrollView drawing on top of other views

I have a custom NSView that lives inside of a NSScrollView that is in a NSSplitView. The custom view uses the following drawing code:
- (void)drawRect:(NSRect)dirtyRect {
NSGraphicsContext *ctx = [NSGraphicsContext currentContext];
[ctx saveGraphicsState];
// Rounded Rect
NSRect rect = [self bounds];
NSRect pathRect = NSMakeRect(rect.origin.x + 3, rect.origin.y + 6, rect.size.width - 6, rect.size.height - 6);
NSBezierPath *path = [NSBezierPath bezierPathWithRoundedRect:pathRect cornerRadius:kDefaultCornerRadius];
// Shadow
[NSShadow setShadowWithColor:[NSColor colorWithCalibratedWhite:0 alpha:0.66]
blurRadius:4.0
offset:NSMakeSize(0, -3)];
[[NSColor colorWithCalibratedWhite:0.196 alpha:1.0] set];
[path fill];
[NSShadow clearShadow];
// Background Gradient
NSGradient *gradient = [[NSGradient alloc] initWithStartingColor:[UAColor darkBlackColor] endingColor:[UAColor lightBlackColor]];
[gradient drawInBezierPath:path angle:90.0];
[gradient release];
// Image
[path setClip];
NSRect imageRect = NSMakeRect(pathRect.origin.x, pathRect.origin.y, pathRect.size.height * kLargeImageRatio, pathRect.size.height);
[self.image drawInRect:imageRect
fromRect:NSZeroRect
operation:NSCompositeSourceAtop
fraction:1.0];
[ctx restoreGraphicsState];
[super drawRect:dirtyRect];
}
I have tried every different type of operation but the image still draws on top of the other half of the NSSplitView like so:
…instead of drawing under the NSScrollView. I think this has to do with drawing everything instead of the dirtyRect only, but I don't know how I could edit the image drawing code to only draw the part of it that lies in the dirtyRect. How can I either prevent it from drawing on top, or only draw the dirty rect for this NSImage?
I finally got it. I don't know if it is optimal yet, but I will find out when doing performance testing. I just had to figure out the intersection of the image rect and the dirty rect using NSIntersectionRect, then figuring out which subpart of the NSImage to draw for the drawInRect:fromRect:operation:fraction: call.
Here is the important part:
NSRect imageRect = NSMakeRect(pathRect.origin.x, pathRect.origin.y, pathRect.size.height * kLargeImageRatio, pathRect.size.height);
[self.image setSize:imageRect.size];
NSRect intersectionRect = NSIntersectionRect(dirtyRect, imageRect);
NSRect fromRect = NSMakeRect(intersectionRect.origin.x - imageRect.origin.x,
intersectionRect.origin.y - imageRect.origin.y,
intersectionRect.size.width,
intersectionRect.size.height);
[self.image drawInRect:intersectionRect
fromRect:fromRect
operation:NSCompositeSourceOver
fraction:1.0];

Drawing text with gradient fill in Cocoa

I have a project that needs to draw text in a view with a gradient fill in a custom subclass of NSView, like this example below.
I'm wondering how I can achieve this, as I'm pretty new to Cocoa drawing.
Try creating an alpha mask from the text then using NSGradient to draw into it. Here's a simple example based on the linked code:
- (void)drawRect:(NSRect)rect
{
// Create a grayscale context for the mask
CGColorSpaceRef colorspace = CGColorSpaceCreateDeviceGray();
CGContextRef maskContext =
CGBitmapContextCreate(
NULL,
self.bounds.size.width,
self.bounds.size.height,
8,
self.bounds.size.width,
colorspace,
0);
CGColorSpaceRelease(colorspace);
// Switch to the context for drawing
NSGraphicsContext *maskGraphicsContext =
[NSGraphicsContext
graphicsContextWithGraphicsPort:maskContext
flipped:NO];
[NSGraphicsContext saveGraphicsState];
[NSGraphicsContext setCurrentContext:maskGraphicsContext];
// Draw the text right-way-up (non-flipped context)
[text
drawInRect:rect
withAttributes:
[NSDictionary dictionaryWithObjectsAndKeys:
[NSFont fontWithName:#"HelveticaNeue-Bold" size:124], NSFontAttributeName,
[NSColor whiteColor], NSForegroundColorAttributeName,
nil]];
// Switch back to the window's context
[NSGraphicsContext restoreGraphicsState];
// Create an image mask from what we've drawn so far
CGImageRef alphaMask = CGBitmapContextCreateImage(maskContext);
// Draw a white background in the window
CGContextRef windowContext = [[NSGraphicsContext currentContext] graphicsPort];
[[NSColor whiteColor] setFill];
CGContextFillRect(windowContext, rect);
// Draw the gradient, clipped by the mask
CGContextSaveGState(windowContext);
CGContextClipToMask(windowContext, NSRectToCGRect(self.bounds), alphaMask);
NSGradient *gradient = [[NSGradient alloc] initWithStartingColor:[NSColor blackColor] endingColor:[NSColor grayColor]];
[gradient drawInRect:rect angle:-90];
[gradient release];
CGContextRestoreGState(windowContext);
CGImageRelease(alphaMask);
}
This uses the view bounds as the gradient bounds; if you wanted to be more accurate, you'd need to get the text height (information about that here).

Resources