Changing the fill to a already drawn NSBezierPath - objective-c

I'd like to change the fill of a button that I've drawn (I subclassed NSButton)
Here's the code I've got already:
- (void)drawRect:(NSRect)dirtyRect {
// Drawing code here.
// Create the Gradient
NSGradient *fillGradient = [[NSGradient alloc] initWithStartingColor:[NSColor lightGrayColor] endingColor:[NSColor darkGrayColor]];
// Create the path
aPath = [NSBezierPath bezierPath];
[aPath moveToPoint:NSMakePoint(10.0, 0.0)];
[aPath lineToPoint:NSMakePoint(85.0, 0.0)];
[aPath lineToPoint:NSMakePoint(85.0, 20.0)];
[aPath lineToPoint:NSMakePoint(10.0, 20.0)];
[aPath lineToPoint:NSMakePoint(0.0, 10.0)];
[aPath lineToPoint:NSMakePoint(10.0, 0.0)];
[fillGradient drawInBezierPath:aPath angle:90.0];
[fillGradient release];
}
- (void)mouseDown:(NSEvent *)theEvent {
NSGradient *fillGradient = [[NSGradient alloc] initWithStartingColor:[NSColor lightGrayColor] endingColor:[NSColor darkGrayColor]];
[fillGradient drawInBezierPath:aPath angle:-90.0];
}
and I get a EXC_BAD_ACCESS signal. How would I do this ?

The reason for the EXC_BAD_ACCESS is that the following line:
aPath = [NSBezierPath bezierPath];
creates an autoreleased bezierPath which will be released at the end of the current iteration of the run loop. To avoid the error, you'd need to change it to:
aPath = [[NSBezierPath bezierPath] retain];
However, you're approaching the problem from the wrong direction. Drawing should only be done in the -drawRect: method (or methods which are only called from -drawRect:). Instead of trying to draw in your mouseDown: method, you should create a new BOOL instance variable for your class (called, for instance, mouseIsDown) and set that in mouseDown:. Then use that boolean to determine how to fill the button:
- (void)drawRect:(NSRect)aRect {
NSGradient *fillGradient = nil;
if (mouseIsDown)
fillGradient = [[NSGradient alloc] initWithStartingColor:[NSColor lightGrayColor] endingColor:[NSColor darkGrayColor]];
else
fillGradient = [[NSGradient alloc] initWithStartingColor:[NSColor lightGrayColor] endingColor:[NSColor darkGrayColor]];
// Do the rest of your drawRect method
}
- (void)mouseDown:(NSEvent *)theEvent {
mouseIsDown = YES;
[self setNeedsDisplay:YES];
}
- (void)mouseUp:(NSEvent *)theEvent {
mouseIsDown = NO;
[self setNeedsDisplay:YES];
}

This isn't directly related to your question, but your question's title belies a false assumption that I want to address.
Changing the fill to a already drawn NSBezierPath
That's not possible.
In Cocoa, “fill” is a verb, not a property of an object like it is in Illustrator or Lineform. You do not set the fill of a path, nor change it later; you fill the path, and what you change by this is pixels in some unseen backing store. The real-world analogy would be setting up splines on a flat pane of glass, then filling the area bounded by the splines with paint and letting it dry. The “fill” is not a property of the splines; it's the act of pouring paint into the shape they define.
As with the analogy, you cannot remove or alter a fill you have previously done—the paint is stuck to the glass; there's no getting it off*. The only way to accomplish that effect is to re-do the fill with some other color. You can move the splines (create a new path) or add splines (add an intersecting subpath to the existing path) before filling, if you want to only change part of the existing drawing.
All of that applies to all drawing operations, including stroking paths and drawing raster images. Text drawing, too, as that's just another case of filling and/or stroking a path.
*OK, you probably could remove real paint from real glass, either chemically or by scraping. No analogy is perfect. ☺

Retain aPath after you create it, since it will have been autoreleased by the time the mouseDown: message is sent. Just make sure to release it in your class's dealloc or wherever else that it would be appropriate.
Edit:
Refer to Matt Ball's answer instead.

Related

NSView with masked CIFilter for OS X app

I am developing an app that contains lots of custom NSView objects being moved around. I have implemented a gaussian blur background filter for one of the custom NSView subclasses like so:
- (id)init {
self = [super init];
if (self) {
...
CIFilter *saturationFilter = [CIFilter filterWithName:#"CIColorControls"];
[saturationFilter setDefaults];
[saturationFilter setValue:#.5 forKey:#"inputSaturation"];
CIFilter *blurFilter = [CIFilter filterWithName:#"CIGaussianBlur"];
[blurFilter setDefaults];
[blurFilter setValue:#2.0 forKey:#"inputRadius"];
self.wantsLayer = YES;
self.layer.backgroundColor = [NSColor clearColor].CGColor;
self.layer.masksToBounds = YES;
self.layer.needsDisplayOnBoundsChange = YES;
self.layerUsesCoreImageFilters = YES;
[self updateFrame]; //this is where the frame size is set
self.layer.backgroundFilters = #[saturationFilter, blurFilter];
...
return self;
}
else return nil;
}
This works great and creates a gaussian blur effect within the entire contents of the view. The problem is that I do not want the gaussian blur to cover the entire view. There is about an (intentional) 12px padding between the actual size of the NSView and the drawing of its content box:
- (void)drawRect:(NSRect)dirtyRect {
[super drawRect:dirtyRect];
NSColor* strokeColor = [NSColor colorWithRed:.5 green:.8 blue:1 alpha:1];
NSColor* fillColor = [NSColor colorWithRed:.5 green:.8 blue:1 alpha:.2];
...
[strokeColor setStroke];
[fillColor setFill];
NSBezierPath *box = [NSBezierPath bezierPathWithRoundedRect:NSMakeRect(self.bounds.origin.x + 12, self.bounds.origin.y + 12, self.bounds.size.width - 24, self.bounds.size.height - 24) xRadius:6 yRadius:6];
box.lineWidth = 6;
[box stroke];
[box fill];
...
}
The reason for this padding is that there are some pieces of the GUI that inhabit this region and are drawn seamlessly into the containing box. I would like to mask the Blur effect to only have effect on the interior of the drawn box rather than the entire view. Here is what I have tried.
ATTEMPT 1: Create a sublayer
I created a sublayer in the NSView with the appropriately sized frame, and added the blur effect to this sublayer. PROBLEM: The blur effect seems to only apply to the immediate parent layer, so rather than blur the contents behind the NSView, it blurs the contents of the NSView's self.layer (which is basically empty).
ATTEMPT 2: Create a masking layer
I tried to create a masking layer and set it to self.layer.mask. However, since the positions of the GUI content do change (via the DrawRect function), I would need to get a copy of the current layer to use as the masking layer. I tried the following code, but it had no effect.
self.layer.mask = nil;
NSArray *bgFilters = self.layer.backgroundFilters;
self.layer.backgroundFilters = nil;
CALayer *maskingLayer = self.layer.presentationLayer;
self.layer.mask = maskingLayer;
self.layer.backgroundFilters = bgFilters;
ATTEMPT 3: Draw a masking layer directly
I could not find any examples of how to draw directly on a layer. I can not use a static UIImage to mast with, because, as I said above, the mask has to change with user interaction. I was looking for something equivalent to the DrawRect function. Any help would be appreciated.
SO...
It seems to me that the sublayer way would be the best and simplest way to go, if I could just figure out how to change the priority of the blur effect to be the background behind the NSView not the NSView's background layer behind the sublayer.
Well, I would still like to know if there is a more elegant way, but I have found a solution that works. Basically, I have created a masking layer from an NSImage drawn from a modified version of the drawRect function:
- (id)init {
self = [super init];
if (self) {
// SETUP VIEW SAME AS ABOVE
CALayer *maskLayer = [CALayer layer];
maskLayer.contents = [NSImage imageWithSize:self.frame.size flipped:YES drawingHandler:^BOOL(NSRect dstRect) {
[self drawMask:self.bounds];
return YES;
}];
maskLayer.frame = self.bounds;
self.layer.mask = maskLayer;
return self;
}
else return nil;
}
- (void)drawMask:(NSRect)dirtyRect {
[[NSColor clearColor] set];
NSRectFill(self.bounds);
[[NSColor blackColor] set];
// SAME DRAWING CODE AS drawRect
// EXCEPT EVERYTHING IS SOLID BLACK (NO ALPHA TRANSPARENCY)
// AND ONLY NEED TO DRAW PARTS THAT EFFECT THE EXTERNAL BOUNDARIES
}

I get wrong custom background color and black color instead of background image in NSView

I am trying to make trivial app, in which I have a sidebar, and I am trying either to get background color based on RGB values from the PSD file, either to use background image as pattern....
I have make attempts in both ways, and nothing work's so far. Any help will be deeply appreciated.
-(void) drawRect:(NSRect)dirtyRect {
CALayer *viewLayer = [CALayer layer];
[viewLayer setBackgroundColor:CGColorCreateGenericRGB(85.0, 179.0, 217.0, 1.0)]; //RGB plus Alpha Channel
[self setWantsLayer:YES]; // view's backing store is using a Core Animation Layer
[self setLayer:viewLayer];
}
This code should show bluish color, result is almost white color...not even close to what I want.
Second code, show black background, even that my png file is in the folder supporting files.
- (void)drawRect:(NSRect)dirtyRect {
NSGraphicsContext* theContext = [NSGraphicsContext currentContext];
[theContext saveGraphicsState];
[[NSGraphicsContext currentContext] setPatternPhase:NSMakePoint(0,[self frame].size.height)];
[self.customBackgroundColour set];
NSRectFill([self bounds]);
[theContext restoreGraphicsState];
}
- (id)initWithFrame:(NSRect)frame {
self = [super initWithFrame:frame];
if (self) {
self.customBackgroundColour = [NSColor colorWithPatternImage:
[NSImage imageNamed:#"buttonBg.png"]];
}
return self;
}
Again, any help will be deeply appreciated.
If I recall correctly CGColorCreateGenericRGB expects a range from 0.0-1.0 and would explain why it is white. This should fix the white issue.
[viewLayer setBackgroundColor:CGColorCreateGenericRGB(85.0/255.0, 179.0/255.0, 217.0/255.0, 1.0)]; //RGB plus Alpha Channel
Hopefully that helps.

NSComboBox - problems with background when selected

I'm a iOS developer, and recently I'm programming a desktop APP for MAC OSX. I still don't have much experience with the View's components of OSX, so maybe it's a silly or easy question, but I have made a little research about this problem and haven't found any solution yet.
Here's the problem:
I have a custom specialization of a NSView, that is used as the view of a Content ViewController used in my NSPopover.
Inside this view, that I'm calling "PopoverBackgroundView", I painted inside the drawRect this red background, and calculated another minor rect and painted with this gray-like color. Here's the code:
- (void)drawRect:(NSRect)dirtyRect
{
[[NSColor colorWithDeviceRed:174/255.0 green:72/255.0 blue:72/255.0 alpha:1.0] setFill];
NSRectFill(dirtyRect);
[[NSColor colorWithDeviceRed:51/255.0 green:51/255.0 blue:51/255.0 alpha:1.0] setFill];
NSRectFill(NSMakeRect(BORDER_WIDTH, BORDER_WIDTH, dirtyRect.size.width - 2*BORDER_WIDTH, dirtyRect.size.height - 2*BORDER_WIDTH));
}
So, inside the PopoverBackgroundView.m I'm programatically creating a NSComboBox. This comboBox will have the numbers 1 to 10. When I allocate it, everything seems just fine:
The problem is, after I select any options inside the combobox, it's background somehow "goes away" became transparent, I don't know, and become like this:
Please notice the red-like frame (background color of the view) around the NSComboBox, that appeared just AFTER I select something.
Here's the code where I'm allocation the comboBox and initializing it:
- (id)initWithFrame:(NSRect)frame
{
self = [super initWithFrame:frame];
if (self) {
(...)
self.comboBox = [[NSComboBox alloc] initWithFrame:CGRectMake(15, frame.size.height - 55, 90, 25)];
self.comboBox.delegate = self;
[self.comboBox setDrawsBackground:NO];
[self.comboBox setSelectable:YES];
[self.comboBox setEditable:NO];
for (int i = 1; i<=10; i++)
{
NSString *mystr = [NSString stringWithFormat:#"%d", i];
[self.comboBox addItemWithObjectValue:mystr];
}
[self addSubview:self.comboBox];
}
return self;
}
Any idea how can I 'fix' this "selected background"? All that I want it's the selected state to be equals to the normal state, i. e. ,the comboBox should be always like the first image, even after the selection.
Is there something wrong with the allocation code? Something mission? I'm really thinking that just some property that I'm not using or initializing, but I couldn't find yet.
Thanks in advance,
Just to give a feedback, I finally was able to resolve my problem.
I don't know exactly why, but the problem was all caused because the way I was drawing my border in the drawRect. Somehow, these code
[[NSColor colorWithDeviceRed:174/255.0 green:72/255.0 blue:72/255.0 alpha:1.0] setFill];
NSRectFill(dirtyRect);
were been propagated by the subviews, don't know if was the setFill or the NSRectFill. So, the "background" of the NSCombobox was been painted with this color.
After seeing this post :Adding border and Rounded Rect in the NSView I changed my draw rect to this:
- (void)drawRect:(NSRect)dirtyRect
{
NSBezierPath *background = [NSBezierPath bezierPathWithRoundedRect:self.bounds xRadius:10 yRadius:10];
[[NSColor colorWithDeviceRed:174/255.0 green:72/255.0 blue:72/255.0 alpha:1.0] set];
[background setLineWidth:10];
[background stroke];
}
and everything is working fine now, as I wanted. Now, after I select my combobox, no strange background is been drawing.
If someone knows whey this was happening with the previous code, please, let me know.

being called but not drawing

I have some code in the drawRect: method that draws a series of circles from an array. I can verify that the points stay in memory long enough and get to the point where they should be drawn. However, it seems as if the fill method of NSBezierPath is not drawing the circles.
Here is my drawRect:
- (void)drawRect:(NSRect)dirtyRect
{
NSPoint aPoint;
NSRect aRect = NSMakeRect(0, 0, 6, 6);
// Erase the rectangle
[[NSColor clearColor] set];
NSRectFill([self bounds]);
// Draw the dots
[[NSColor colorWithCalibratedRed:(77.0/255.0) green:(11.0/255.0) blue:(11.0/255.0) alpha:1.0] set];
for(NSValue *aValuePoint in arrayOfPoints) {
aPoint = [aValuePoint pointValue];
aRect.origin.y = aPoint.y - aRect.size.height/2.0;
aRect.origin.x = aPoint.x - aRect.size.width/2.0;
// The code does get to this point, it does not draw however...
[[NSBezierPath bezierPathWithOvalInRect:aRect] fill];
}
}
I have another method drawDotAtPoint: where I add the points to an array
- (void)drawDotAtPoint:(NSPoint)aPoint
{
NSLog(#"drawDotAtPoint:(%f, %f)", aPoint.x, aPoint.y);
[arrayOfPoints addObject:[NSValue valueWithPoint:aPoint]];
// I've also tried using [self superview] instead of just self
[self setNeedsDisplay: YES];
}
And while I am able to verify that these methods are called at the correct times (or so it seems) no drawing after the initial drawRect: will take place
Sidenote:
My goal is to actually draw a point and have a ring appear around it, enlarge and fade out. Much similar to the GPS current location indicator on iOS, in the Maps app. Any insight on that would also be appreciated.
Check your points and make sure that you don't have an inverted rectangle and that your X/Y coords are what you expect. Make sure that your anchor point and coordinates are correctly relative.
You may be helped along a bit by using a tool called Quartz Debug that comes with apple's developer tools. It is in the /Developer/Applications/Graphics Tools/ folder. After starting this tool, go to the "tools" menu and turn on "Flash Screen Updates". This will give you an idea of when things are being redrawn or not.
Hope that helps some.
I've copy-pasted your view code. I stuck this in an app delegate for testing. BadView is the custom view here (no reflection on you -- "Bad" because it's misbehaving :) ) The windows are both created in IB, and the WebView is likewise placed in the parent window in IB.
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
// badWindow is an IBOutlet to the overlay window, parentWindow an IBOutlet
// to the window with the webview, myWebView an IBOutlet to the webview
BadView * myBadView = [[BadView alloc] initWithFrame:[[badWindow contentView] frame]];
// Create some random points for testing
[myBadView createArrayOfPoints];
[badWindow setStyleMask:NSBorderlessWindowMask];
[badWindow setOpaque:NO];
[badWindow setAlphaValue:0.9]; // Will still act opaque without this
[badWindow setHasShadow:NO];
[badWindow setContentView:myBadView];
[parentWindow addChildWindow:badWindow ordered:NSWindowAbove];
[[myWebView mainFrame] loadRequest:[NSURLRequest requestWithURL:
[NSURL URLWithString:#"http://www.apple.com/"]]];
[myBadView release];
}

CALayer - clear internal rendering context in favor of custom drawing code

When subclassing an CALayer and implementing the drawInContext method, I would assume that any drawing I do within there is all that will show up, but instead if I set (for example) borderWidth/borderColor then CALayer will draw a border on it's own above all my custom drawing code.
This is a CALayer subclass:
#implementation MyCustomCALayer
- (id)init
{
self = [super init];
if(self)
{
[self setNeedsDisplayOnBoundsChange:YES];
}
return self;
}
- (void)drawInContext:(CGContextRef)context
{
CGRect rect = CGContextGetClipBoundingBox(context);
CGContextSetFillColorWithColor(context, [UIColor redColor].CGColor);
CGContextFillRect(context, rect);
}
#end
Created in a UIView something like:
- (void)ensureLayer
{
if(myLayer)
return;
myLayer = [[[MyCustomCALayer alloc] init] autorelease];
myLayer.borderColor = [UIColor greenColor].CGColor;
myLayer.borderWidth = 1;
myLayer.frame = CGRectMake(0, 0, self.frame.size.width, self.frame.size.height);
}
- (void)layoutSublayersOfLayer:(CALayer *)layer
{
[super layoutSublayersOfLayer:layer];
[self ensureLayer];
if(![[layer.sublayers objectAtIndex:0] isEqual:myLayer])
[layer insertSublayer:myLayer atIndex:0];
}
What happens, is the MyCustomCALayer fills a rectangle with red, this is what I would expect to see and nothing else, since i've implemented the drawInContext method, but instead I see a red rectangle with a green border on top, always on top, i've tried just about every combination I can think of to get rid of the green border being drawn and cannot figure it out.
My reasoning is I would like to use the borderWidth and borderColor and other properties of the CALayer instead of creating my own properties, because the code that I need to draw contains a border, a fill, etc... but the rendering I need to do is not a simple shape. So the only way i've found around this is to set the borderWidth to 0 and add my own property to my subclass, like myBorderWidth, which is ridiculous.
This is done with the latest iOS SDK, but i'd imagine it's the same for Mac.
Hope this makes sense, any thoughts?
Thanks!
You’re out of luck; CoreAnimation doesn’t support overriding its implementation of rendering for the basic layer properties. Please do file a bug.