drawLayer not called when subclassing CALayer - cocoa

I have a simple CALayer subclass (BoxLayer) with this drawLayer method:
- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx {
NSLog(#"drawLayer");
NSGraphicsContext *nsGraphicsContext;
nsGraphicsContext = [NSGraphicsContext graphicsContextWithGraphicsPort:ctx
flipped:NO];
[NSGraphicsContext saveGraphicsState];
[NSGraphicsContext setCurrentContext:nsGraphicsContext];
// ...Draw content using NS APIs...
NSPoint origin = { 21,21 };
NSRect rect;
rect.origin = origin;
rect.size.width = 128;
rect.size.height = 128;
NSBezierPath * path;
path = [NSBezierPath bezierPathWithRect:rect];
[path setLineWidth:4];
[[NSColor whiteColor] set];
[path fill];
[[NSColor grayColor] set];
[path stroke];
[NSGraphicsContext restoreGraphicsState];
}
I then have this awakeFromNib in my NSView subclass:
- (void)awakeFromNib {
CALayer* rootLayer = [CALayer layer];
[self setLayer:rootLayer];
[self setWantsLayer:YES];
box1 = [CALayer layer];
box1.bounds = CGRectMake(0, 0, 70, 30);
box1.position = CGPointMake(80, 80);
box1.cornerRadius = 10;
box1.borderColor = CGColorCreateGenericRGB(255, 0, 0, 1);
box1.borderWidth = 1.5;
[rootLayer addSublayer:box1];
box2 = [BoxLayer layer];
[box2 setDelegate:box2];
[box2 setNeedsDisplay];
[rootLayer addSublayer:box2];
}
My drawLayer is never called though, but why not?
Thank you

Possibly because the frame of your layer seems to be equal to CGRectZero, so the OS might think it's invisible so it doesn't need to draw it.
On a side note, why are going the complicated way of setting the layer's delegate to itself and implementing drawLayer:inContext: instead of using drawInContext: directly?

Related

How could the existence of `-saveGraphicsContext` and `-restoreGraphicsContext` be causing these issues in 10.10?

We have a custom NSPopUpButtonCell, and overriding this method:
- (void)drawBorderAndBackgroundWithFrame:(NSRect)cellFrame inView:(NSView*)controlView
{
[[NSGraphicsContext currentContext] saveGraphicsState];
CGFloat strokeWidth = 1;
cellFrame = NSInsetRect(cellFrame, strokeWidth/2.0, strokeWidth/2.0);
NSBezierPath* rectanglePath = [NSBezierPath bezierPathWithRoundedRect:cellFrame xRadius:2 yRadius:2];
rectanglePath.lineWidth = strokeWidth;
if (self.horizontalImageOffset != 0)
{
CGRect stateImageRect = [self stateImageRectForBounds:cellFrame];
CGFloat verticalSeparatorX = NSMaxX(stateImageRect);
NSBezierPath* verticalSeparator = [NSBezierPath bezierPath];
[verticalSeparator moveToPoint:NSMakePoint(verticalSeparatorX, NSHeight(cellFrame) - strokeWidth)];
[verticalSeparator lineToPoint:NSMakePoint(verticalSeparatorX, strokeWidth + 1)];
verticalSeparator.lineWidth = strokeWidth;
[verticalSeparator stroke];
}
[NSGraphicsContext restoreGraphicsState];
}
The issue is that when I bring this class into my project, it makes another one of my (unrelated) views disappear-- as in, won't draw properly
If I comment out the -saveGraphicsState and -restoreGraphicsState it works and draws my other view properly
What could be going wrong here?
EDIT: Kicker is that teammates on 10.11 and 10.12 don't see this issue
I think you should call
[NSGraphicsContext saveGraphicsState];
not
[[NSGraphicsContext currentContext] saveGraphicsState];

UIImage from MASKED CALayer

I'm in need of an UIImage from a Masked CALayer. This is the function I use:
- (UIImage *)imageFromLayer:(CALayer *)layer
{
UIGraphicsBeginImageContext([layer frame].size);
[layer renderInContext:UIGraphicsGetCurrentContext()];
UIImage *outputImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return outputImage;
}
The problem is that the mask isn't maintained.
This is the completed code:
CAShapeLayer * layerRight= [CAShapeLayer layer];
layerRight.path = elasticoRight;
im2.layer.mask = layerRight;
CAShapeLayer * layerLeft= [CAShapeLayer layer];
layerLeft.path = elasticoLeft;
im3.layer.mask = layerLeft;
[viewImage.layer addSublayer:im2.layer];
[viewImage.layer addSublayer:im3.layer];
UIImage *image_result = [self imageFromLayer:viewImage.layer];
If I visualize the viewImage, the result is correct, but if I try to obtain the image relative to the layer, the masks are lost.
I've solved.
Now i obtaining the image mask and use CGContextClipToMask.
CGRect rect = CGRectMake(0, 0, 1024, 768);
UIGraphicsBeginImageContextWithOptions(rect.size, YES, 0.0);
{
[[UIColor blackColor] setFill];
UIRectFill(rect);
[[UIColor whiteColor] setFill];
UIBezierPath *leftPath = [UIBezierPath bezierPath];
// Set the starting point of the shape.
CGPoint p1 = [(NSValue *)[leftPoints objectAtIndex:0] CGPointValue];
[leftPath moveToPoint:CGPointMake(p1.x, p1.y)];
for (uint i=1; i<leftPoints.count; i++)
{
CGPoint p = [(NSValue *)[leftPoints objectAtIndex:i] CGPointValue];
[leftPath addLineToPoint:CGPointMake(p.x, p.y)];
}
[leftPath closePath];
[leftPath fill];
}
UIImage *mask = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
UIGraphicsBeginImageContextWithOptions(rect.size, NO, 0.0);
{
CGContextClipToMask(UIGraphicsGetCurrentContext(), rect, mask.CGImage);
[im_senza drawAtPoint:CGPointZero];
}
UIImage *maskedImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();

NSSegmentedCell subclass drawing

I have created subclass of NSSegmentedCell and implemented drawWithFrame as following:
#import "CustomSegmentedCell.h"
#implementation CustomSegmentedCell
- (void)drawWithFrame:(NSRect)cellFrame inView:(NSView *)controlView {
int i=0, count=[self segmentCount];
NSRect segmentFrame=cellFrame;
for(i=0; i<count; i++) {
segmentFrame.size.width=[self widthForSegment:i];
[NSGraphicsContext saveGraphicsState];
// Make sure that segment drawing is not allowed to spill out into other segments
NSBezierPath* clipPath = [NSBezierPath bezierPathWithRect: segmentFrame];
[clipPath addClip];
[self drawSegment:i inFrame:segmentFrame withView:controlView];
[NSGraphicsContext restoreGraphicsState];
segmentFrame.origin.x+=segmentFrame.size.width;
}
_lastDrawRect=cellFrame;
}
#end
The problem is segments didn't get drawn on first launch of app, it gets visible only when i clicked with mouse on blank area where segmented Control suppose to drawn.Please let me know, what i am missing here.
Thanks,
Subclass and implement drawSegment:inFrame:withView:
- (void)drawSegment:(NSInteger)segment inFrame:(NSRect)frame withView:(NSView *)controlView
{
NSBezierPath *path = [NSBezierPath bezierPathWithRoundedRect:frame xRadius:8 yRadius:8];
[path setClip];
[path setLineWidth:2.5];
[[NSColor grayColor] setStroke];
NSColor* bgColr;
if (segment%2) {
bgColr = [NSColor blackColor];
}
else {
bgColr = [NSColor redColor];
}
[bgColr setFill];
[NSBezierPath fillRect:frame];
}

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];

Draw an Inset NSShadow and Inset Stroke

I have an NSBezierPath and I want to draw in inset shadow (similar to Photoshop) inside the path.
Is there anyway to do this? Also, I know you can -stroke paths, but can you stroke inside a path (similar to Stroke Inside in Photoshop)?
Update 3
static NSImage * graydient = nil;
if (!graydient) {
graydient = [[NSImage alloc] initWithSize: NSMakeSize(22, 22)];
[graydient lockFocus];
NSGradient * gradient = [[NSGradient alloc] initWithColorsAndLocations: clr(#"#262729"), 0.0f, clr(#"#37383a"), 0.43f, clr(#"#37383a"), 1.0f, nil];
[gradient drawInRect: NSMakeRect(0, 4.179, 22, 13.578) angle: 90.0f];
[gradient release];
[graydient unlockFocus];
}
NSColor * gcolor = [NSColor colorWithPatternImage: graydient];
[gcolor set];
NSShadow * shadow = [NSShadow new];
[shadow setShadowColor: [NSColor colorWithDeviceWhite: 1.0f alpha: 1.0f]];
[shadow setShadowBlurRadius: 0.0f];
[shadow setShadowOffset: NSMakeSize(0, 1)];
[shadow set];
[path fill];
[NSGraphicsContext saveGraphicsState];
[[path pathFromIntersectionWithPath: [NSBezierPath bezierPathWithRect: NSInsetRect([path bounds], 0.6, 0)]] setClip];
[gcolor set];
[shadow setShadowOffset: NSMakeSize(0, 1)];
[shadow setShadowColor: [NSColor blackColor]];
[shadow set];
[outer stroke];
[NSGraphicsContext restoreGraphicsState];
[NSGraphicsContext saveGraphicsState];
[[NSGraphicsContext currentContext] setCompositingOperation: NSCompositeSourceOut];
[shadow set];
[[NSColor whiteColor] set];
[inner fill];
[shadow set];
[inner fill];
[NSGraphicsContext restoreGraphicsState];
This is my final result. It looks pretty good. I had to change the shadow to White # 1.0 Alpha to make it work. Even though the shadow alpha norm for menu bar items is 0.5, it doesn't look half bad.
Many thanks go out to Joshua Nozzi.
I think you can do this by setting clip on the bezier path to use it as a mask and stroking the shadow, then adding a normal stroke if desired.
Update based on updated code:
Here you go. I'm feeling procrastinate-y tonight. :-)
// Describe an inset rect (adjust for pixel border)
NSRect whiteRect = NSInsetRect([self bounds], 20, 20);
whiteRect.origin.x += 0.5;
whiteRect.origin.y += 0.5;
// Create and fill the shown path
NSBezierPath * path = [NSBezierPath bezierPathWithRect:whiteRect];
[[NSColor whiteColor] set];
[path fill];
// Save the graphics state for shadow
[NSGraphicsContext saveGraphicsState];
// Set the shown path as the clip
[path setClip];
// Create and stroke the shadow
NSShadow * shadow = [[[NSShadow alloc] init] autorelease];
[shadow setShadowColor:[NSColor redColor]];
[shadow setShadowBlurRadius:10.0];
[shadow set];
[path stroke];
// Restore the graphics state
[NSGraphicsContext restoreGraphicsState];
// Add a nice stroke for a border
[path setLineWidth:1.0];
[[NSColor grayColor] set];
[path stroke];

Resources