Matt Gemmell's NSBezierPath+StrokeExtensions draws outside of clip rect - cocoa

I'm using Matt Gemmell's NSBezierPath+StrokeExtensions category to draw an inside stroke on an NSRect. Here is the code for the entire category:
- (void)strokeInside
{
/* Stroke within path using no additional clipping rectangle. */
[self strokeInsideWithinRect:NSZeroRect];
}
- (void)strokeInsideWithinRect:(NSRect)clipRect
{
NSGraphicsContext *thisContext = [NSGraphicsContext currentContext];
float lineWidth = [self lineWidth];
/* Save the current graphics context. */
[thisContext saveGraphicsState];
/* Double the stroke width, since -stroke centers strokes on paths. */
[self setLineWidth:(lineWidth * 2.0)];
/* Clip drawing to this path; draw nothing outwith the path. */
[self setClip];
/* Further clip drawing to clipRect, usually the view's frame. */
if (clipRect.size.width > 0.0 && clipRect.size.height > 0.0) {
[NSBezierPath clipRect:clipRect];
}
/* Stroke the path. */
[self stroke];
/* Restore the previous graphics context. */
[thisContext restoreGraphicsState];
[self setLineWidth:lineWidth];
}
Here is my drawRect: method:
- (void)drawRect:(NSRect)dirtyRect {
NSRect myRect = NSMakeRect(500, 0, 400, 100);
[[NSColor colorWithCalibratedRed:0 green:0 blue:1.0 alpha:0.5] set];
NSRectFillUsingOperation(myRect, NSCompositeSourceOver);
[[NSColor blueColor] set];
[[NSBezierPath bezierPathWithRect:myRect] strokeInside];
}
However, this happens when I scroll up:
As you can see, the inside stroke draws onto the toolbar. Why does this happen? How can I fix the strokeInside method? Note, this does not happen with the regular stroke method.

OK, I figured it out. Instead of this line:
[self setClip];
Use this:
[self addClip];
Works fine and makes sense.

Related

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.

Cocoa NSView: Making circles but they are getting cropped

the outer edges of my circle at each point of the compass are getting cropped (presumably by the rect frame). How do I get the circle to display within the frame? (This is getting created from a button click):
In my AppController.m
#import "AppController.h"
#import "MakeCircle.h"
#implementation AppController
- (IBAction)makeCircle:(id)sender {
MakeCircle* newCircle = [[MakeCircle alloc] initWithFrame:NSMakeRect(100.0, 100.0, 30.0, 30.0)];
[[[[NSApplication sharedApplication] mainWindow] contentView] addSubview:newCircle];
[newCircle release];
}
#end
In my MakeCircle.m
- (void)drawRect:(NSRect)rect {
[self setNeedsDisplay:YES];
[[NSColor blackColor] setStroke];
// Create our circle path
NSBezierPath* circlePath = [NSBezierPath bezierPath];
[circlePath appendBezierPathWithOvalInRect: rect];
//give the line some thickness
[circlePath setLineWidth:4];
// Outline and fill the path
[circlePath stroke];
}
Thanks.
I think you see only half of the edge, right? You can calculate the half of the thickness of the edge and subtract that from the rectangle:
#define STROKE_COLOR ([NSColor blackColor])
#define STROKE_WIDTH (4.0)
- (void)drawRect:(NSRect)dirtyRect {
NSBezierPath *path;
NSRect rectangle;
/* Calculate rectangle */
rectangle = [self bounds];
rectangle.origin.x += STROKE_WIDTH / 2.0;
rectangle.origin.y += STROKE_WIDTH / 2.0;
rectangle.size.width -= STROKE_WIDTH / 2.0;
rectangle.size.height -= STROKE_WIDTH / 2.0;
path = [NSBezierPath path];
[path appendBezierPathWithOvalInRect:rectangle];
[path setLineWidth:STROKE_WIDTH];
[STROKE_COLOR setStroke];
[path stroke];
}
I have no Mac at the moment, so I can't test it, but I think it should solve your problem.
Als don't call [self setNeedsDisplay:YES]. The method is used when you want to redraw your whole NSView, and calling it from the drawing method is a little bit recursive. That's why I'm surprised your code actually draws something.
And I have another tip: [[NSApplication sharedApplication] mainWindow] is actually the same as [NSApp mainWindow]. NSApp is a global variable containing the main application.
Hope it helps,
ief2

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

Switching line properties while using NSBezierPath

I'm having some very basic problems changing line color/width when drawing different lines (intended for a chart) with NSBezierPath. The following code should make it clear what I'm trying to do:
#import "DrawTest.h"
#implementation DrawTest
- (id)initWithFrame:(NSRect)frameRect
{
NSLog(#"in 'initWithFrame'...");
if ((self = [super initWithFrame:frameRect]) != nil)
{
[self drawBaseline];
[self display]; // ...NO!
[self drawPlotline];
}
return self;
}
- (void)drawBaseline
{
NSRect windowRect;
int width, height;
windowRect = [self bounds];
width = round(windowRect.size.width);
height = round(windowRect.size.height);
theLineWidth=1.0;
[[NSColor grayColor] set];
// allocate an instance of 'NSBezierPath'...
path = [[NSBezierPath alloc]init];
// draw a HORIZONTAL line...
[path moveToPoint:NSMakePoint(0,height/2)];
[path lineToPoint:NSMakePoint(width,height/2)];
}
- (void)drawPlotline
{
theLineWidth=10.0;
[[NSColor redColor] set];
// draw a VERTICAL line...
[path moveToPoint:NSMakePoint(100,125)]; // x,y
[path lineToPoint:NSMakePoint(100,500)];
}
- (void)drawRect:(NSRect)rect
{
NSLog(#"in 'drawRect'...");
// draw the path line(s)
// [[NSColor redColor] set];
[path setLineWidth:theLineWidth];
[path stroke];
}
- (void)dealloc
{
[path release];
[super dealloc];
}
#end
The problem obviously lies in the fact that 'drawRect' only gets called ONCE (after both methods have run), with the result that all lines appear in the last color and line width set. I've tried calling [self display] etc. in the hope of forcing 'drawRect' to redraw the NSView contents between the two method calls, but to no avail.
Can anyone suggest a basic strategy to achieve what I'm trying to do here, please? Any help would be much appreciated.
Quick answer is: move [self drawBaseline] and [self drawPlotline] inside drawRect.
Also, you need to call [path stroke] once per color, before changing the color. So, the pseudo-code is something like
-(void)drawRect:(NSRect)rect
{
NSBezierPath*path1=...
construct the path for color red...
[[NSColor redColor] set];
[path1 stroke];
NSBezierPath*path2=...
construct the path for color blue...
[[NSColor blueColor] set];
[path2 stroke];
}
Remember:
[path moveToPoint:] etc. does not draw the path. It just constructs the path inside the object. Once it's constructed, you stroke the path using [path stroke].
Also, the color is not the property of the path constructed inside the NSBezierPath instance. So, [color set] is not recorded inside your NSBezierPath instance! It's the property of the graphic context. Conceptually, 1. you construct the path. 2. you set the color to the context. 3. you stroke the path.
In Cocoa, it's not that you tell the system when to draw, when to refresh, etc. Cocoa tells you when to draw, by calling your drawRect:. The graphic context on which you draw is not available outside of drawRect:, unless you create off-screen context yourself. So, don't bother drawing beforehand. Just draw everything inside drawRect .

Resources