Cocoa: Subclassing NSProgressIndicator on Lion - objective-c

I have a problem that is driving me crazy. I want to subclass NSProgressIndicator in its "Bar" form to change the color of the progress bar based on a couple of logic states. For that i basically override the -drawRect: as usual. However a very strange thing happens on Lion. Even if my -drawRect just calls the superclass' implementation via [super drawRect:] and does nothing else at all the whole progress bar will be displayed using the style that was used before Lion. If you remember, with Lion the progress bar style has changes to a more flat and sleek look from the prior glassy one.
Again, just overriding -drawRect to do nothing but to call the superclass' implementation changes the style of the progress bar to Leopard's. Does anyone have a slightest idea of what is going on and how i can fix this?

How about making the progress indicator layer-backed and applying a Core Image filter to recolour it?

CustomeProgressIndicator.h:
#import <Cocoa/Cocoa.h>
#interface CustomProgressIndicator : NSView {
double m_minValue;
double m_maxValue;
double m_value;
}
- (void)setMaxValue:(double)newMaxValue;
- (void)setMinValue:(double)newMinValue;
- (void)setDoubleValue:(double)newDoubleValue;
- (void)drawRect:(NSRect)dirtyRect;
#end
CustomProgressIndicator.m:
#import "CustomProgressIndicator.h"
#implementation CustomProgressIndicator
- (void)setMaxValue:(double)newMaxValue
{
m_maxValue = newMaxValue;
}
- (void)setMinValue:(double)newMinValue
{
m_minValue = newMinValue;
}
- (void)setDoubleValue:(double)newDoubleValue
{
m_value = newDoubleValue;
[self setNeedsDisplay:YES];
}
- (void)drawRect:(NSRect)dirtyRect
{
[[NSColor grayColor] set];
[NSBezierPath fillRect:dirtyRect];
double width = ((m_value - m_minValue) / (m_maxValue - m_minValue));
CGRect bar = dirtyRect;
bar.size.width *= width;
[[NSColor darkGrayColor] set];
[NSBezierPath fillRect:bar];
}

Related

COCOA: How to create a round button. It should be clickable only in its boundary

I am trying to create a round button which is only clickable in its boundaries.
What I have done
// imported QuartzCore
#import <QuartzCore/QuartzCore.h>
//created an IBOutlet for the button
IBOutlet NSButton* btn;
//defined the button height width (same)
#define ROUND_BUTTON_WIDTH_HEIGHT 150
//set corner radius
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
// Insert code here to initialize your application
btn.frame = CGRectMake(0, 0, ROUND_BUTTON_WIDTH_HEIGHT, ROUND_BUTTON_WIDTH_HEIGHT);
btn.layer.cornerRadius = ROUND_BUTTON_WIDTH_HEIGHT/2.0f;
}
//set view layer to YES
Problem
The button is clickable outside its boundaries.
When I am setting its position and when I am resizing the window it is getting back to its right position (the actual positon of the button is center of the window)
I have also tried to subclass NSButton and assign the class to the button but results are the same.
#import "roundBtn.h"
#import <QuartzCore/QuartzCore.h>
#implementation roundBtn
#define ROUND_BUTTON_WIDTH_HEIGHT 142
- (void)drawRect:(NSRect)dirtyRect {
[super drawRect:dirtyRect];
// Drawing code here.
self.frame = CGRectMake(0, 0, ROUND_BUTTON_WIDTH_HEIGHT, ROUND_BUTTON_WIDTH_HEIGHT);
self.layer.cornerRadius = ROUND_BUTTON_WIDTH_HEIGHT/2.0f;
}
#end
You can either reimplement the button behavior using a custom NSView and then it will respect the mouseDown events of a masksToBounds layer. NSButton doesn't work this way so the only way to tell your subclassed button that it won't be hit outside of the circle is to override the -hitTest: method to detect hits within the circle:
- (NSView *)hitTest:(NSPoint)aPoint {
NSPoint p = [self convertPoint:aPoint fromView:self.superview];
CGFloat midX = NSMidX(self.bounds);
CGFloat midY = NSMidY(self.bounds);
if (sqrt(pow(p.x-midX, 2) + pow(p.y-midY, 2)) < self.bounds.size.width/2.0) {
return self;
}
return nil;
}
In overriding this, you are telling the button that it will only register a hit if it is within the circle (using a little trigonometry). Returning self indicates the button was hit, returning nil indicates it was not hit.

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.

Overlay NSScroller over content

Is there any way to overlay the NSScroller over the content of the scroll view (like in iOS)? I've already tried several approaches:
a) setting the frame of the scroll view content view (NSClipView) to extend into the bounds of the scroller
b) adding an NSScroller object as a subview of the scroll view (positioned where I want)
c) creating an entirely custom scroller view and placing it as a subview (this worked, but that would mean that I need to rewrite all the functionality of NSScroller)
Sparrow seems to successfully do this, and it seems to do it through a regular NSScroller subclass (seeing as it responds to the scroll settings set in System Preferences >> Appearance). It's not really drawing the scroller that's the issue, its just making it overlay the content.
Any advice is appreciated :-)
Here's where you can set the custom class of your scrollbars.
After that, by overriding the -tile method of NSScrollView, you'll get them placed properly.
Here is my solution :
Create a MyScroller class that extends NSScroller
In the MyScroller.m :
#import "MyScroller.h"
#implementation MyScroller
+(CGFloat) scrollerWidth{
return 10;
}
+(CGFloat) scrollerWidthForControlSize:(NSControlSize)controlSize{
return 10;
}
- (void) drawBackground:(NSRect) rect{
NSBezierPath *path = [NSBezierPath bezierPathWithRoundedRect:rect xRadius:0 yRadius:0];
[[NSColor whiteColor] set];
[path fill];
}
- (void)drawKnob{
[self drawBackground:[self rectForPart:0]];
[self drawBackground:[self rectForPart:1]];
[self drawBackground:[self rectForPart:2]];
[self drawBackground:[self rectForPart:4]];
[self drawBackground:[self rectForPart:5]];
[self drawBackground:[self rectForPart:6]];
NSRect knobRect = [self rectForPart:NSScrollerKnob];
NSRect newRect = NSMakeRect((knobRect.size.width - [MyScroller scrollerWidth]) / 2, knobRect.origin.y, [MyScroller scrollerWidth], knobRect.size.height);
NSBezierPath *path = [NSBezierPath bezierPathWithRoundedRect:newRect xRadius:5 yRadius:5];
[[NSColor grayColor] set];
[path fill];
}
#end
Then just set the custom class for the Scroller in Interface Builder.
I've recently released RFOverlayScrollView which should solve your problem:
Source: https://github.com/rheinfabrik/RFOverlayScrollView
Blog Post: http://blog.rheinfabrik.de/blog/2013/01/01/introducing-rfoverlayscrollview/
You can change the scrollerStyle over the NSScrollerView control to .overlay to get the desired overlay effect.
This may overrule the system wide setting in System Settings for scroll bar visibility. To 'survive' scroll bar setting changes while your app is running, you must observe the NSScroller.preferredScrollerStyleDidChangeNotification and re-apply the overlay effect.

NSOutlineView not doing what it has should do in it's subclass

I have created a Sub-Class of NSOutlineView and used the below code to make the row colors alternate.
Header File.
#import <Cocoa/Cocoa.h>
#interface MyOutlineView : NSOutlineView {
}
- (void) drawStripesInRect:(NSRect)clipRect;
#end
Implementation File.
#import "MyOutlineView.h"
// RGB values for stripe color (light blue)
#define STRIPE_RED (237.0 / 255.0)
#define STRIPE_GREEN (243.0 / 255.0)
#define STRIPE_BLUE (254.0 / 255.0)
static NSColor *sStripeColor = nil;
#implementation MyOutlineView
// This is called after the table background is filled in,
// but before the cell contents are drawn.
// We override it so we can do our own light-blue row stripes a la iTunes.
- (void) highlightSelectionInClipRect:(NSRect)rect {
[self drawStripesInRect:rect];
[super highlightSelectionInClipRect:rect];
}
// This routine does the actual blue stripe drawing,
// filling in every other row of the table with a blue background
// so you can follow the rows easier with your eyes.
- (void) drawStripesInRect:(NSRect)clipRect {
NSRect stripeRect;
float fullRowHeight = [self rowHeight] + [self intercellSpacing].height;
float clipBottom = NSMaxY(clipRect);
int firstStripe = clipRect.origin.y / fullRowHeight;
if (firstStripe % 2 == 0)
firstStripe++; // we're only interested in drawing the stripes
// set up first rect
stripeRect.origin.x = clipRect.origin.x;
stripeRect.origin.y = firstStripe * fullRowHeight;
stripeRect.size.width = clipRect.size.width;
stripeRect.size.height = fullRowHeight;
// set the color
if (sStripeColor == nil)
sStripeColor = [[NSColor colorWithCalibratedRed:STRIPE_RED
green:STRIPE_GREEN
blue:STRIPE_BLUE
alpha:1.0] retain];
[sStripeColor set];
// and draw the stripes
while (stripeRect.origin.y < clipBottom) {
NSRectFill(stripeRect);
stripeRect.origin.y += fullRowHeight * 2.0;
}
}
#end
But the Problem is that what the code is supposed to do doesn't happen to the Outline View, the code is correct but do I need to connect the Outline View to the code is some way?
If you instantiate the outline view in IB, you need to set the class name of your outline view to "MyOutlineView" in the Identity inspector. Remember to double click on the control so that the inner rectangle is selected and the Inspector window title is "Outline View Identity"; a single click of the control will only select the scroll view (an outline view is embedded in a scroll view).
If you create your outline view programmatically, just be sure to instantiate a MyOutlineView instead of an NSOutlineView:
MyOutlineView *outlineView = [[MyOutlineView alloc] initWithFrame:rect];
where rect is the frame of your outline view.