2009 04 15Debugging with Activity Monitor
To draw a custom header in a table view, derive from NSTableHeaderCell and write your drawing code in highlight:withFrame:inView:. This works fine until you disable column selection : highlight:withFrame:inView: is never called, although the header cell obviously looks highlighted. To understand why, we can use … Activity Monitor ! As strange as it sounds, double click your process listing to get a sample button. Instead of launching Shark and getting a trace from everything, we can sample for one moment while clicking around to highlight those table header cells. Behold :
// Column selection enabled 1 -[NSTableHeaderView drawRect:] 1 -[NSTableHeaderView _drawColumnHeaderWithIndexes:] 1 -[NSTableHeaderView _drawHeaderOfColumn:] 1 -[NSTableHeaderCell highlight:withFrame:inView:] 1 -[NSTableHeaderCell _drawThemeContents:highlighted:inView:] 1 -[NSTableHeaderCell drawInteriorWithFrame:inView:] // Column selection disabled 3 -[NSTableHeaderView drawRect:] 3 -[NSTableHeaderView _drawColumnHeaderWithIndexes:] 3 -[NSTableHeaderView _drawHeaderOfColumn:] 3 -[NSTableHeaderView _drawHeaderCell:withFrame:withStateFromColumn:] 3 -[NSTableHeaderCell drawWithFrame:inView:] 3 -[NSTableHeaderCell _drawThemeContents:highlighted:inView:] 3 -[NSTableHeaderCell _drawBezelWithFrame:highlighted:inView:]
Et voilà ! When column selection is disabled, highlight:withFrame:inView: is never called. _drawBezelWithFrame:highlighted:inView: is called instead. Hey, there's even some code using that very method ! Using it to draw the highlighted cell does work.
2009 03 25How Core Image Color Tracking works
To overlay an image on top of blob matching a given color, we need a location : the center of the blob.
If we have a set of 3D points, eg all vertices from a model, we add positions and divide by point count :
- add all coordinates (x, y) into one point
- divide by point count
- there's the center !
To work from an image, we could go about the same way : go over all pixels, find the matching ones, average them down. We can't do that in Core Image as it is limited to kernel functions : no explicit loops, no temporary variables — only a kernel taking inputs (images, colors, numbers) and outputting an image.
The trick that CIColorTracking uses to average matching points is to go over ALL pixels of the image, repeatedly halving its size until it's 1x1. (This is done with a custom filter written in ObjC). It adds pixel coordinates and divides by area :
- compute a mask from target color : white indicates target color, black is empty
- convert to a coordinate mask : Red and Green store pixels' position (range is 0..1), Blue stores coverage (copied from the original mask)
- average that mask down to a 1x1 image
- divide the average position by the average coverage, this gives the location
SAMPLE QTZ Color Tracking.qtz You'll need to compile CIColorTracking first.
I guess this method will be obsolete in Snow Leopard, where OpenCL will hopefully simplify this down to one function. 2009 03 15Custom NSThemeFrame4
After seeing Safari 4 drawing its tab bar in a custom way, I wondered how to do that. Each window has a frame view (the superview of contentView) that draws the window, replacing that view's drawRect: with our own will let us draw it ourselves !
First, replace the window's frame drawRect: with our own :
// Get window's frame view class id class = [[[window contentView] superview] class]; // Add our drawRect: to the frame class Method m0 = class_getInstanceMethod([self class], @selector(drawRect:)); class_addMethod(class, @selector(drawRectOriginal:), method_getImplementation(m0), method_getTypeEncoding(m0)); // Exchange methods Method m1 = class_getInstanceMethod(class, @selector(drawRect:)); Method m2 = class_getInstanceMethod(class, @selector(drawRectOriginal:)); method_exchangeImplementations(m1, m2);
Cocoa will then call our method each time the window frame needs to be drawn. We can draw an entirely custom frame or draw over the existing one. (The attached sample does the latter)
A standard Cocoa window has rounded corners that need to be accounted for by building a clipping path to clip our custom drawing to the window shape :
- Get the rounded corner radius with
roundedCornerRadius - Using this radius, build a rounded corner path that describes the window
- Intersect that path with the current
rect - Draw our stuff
- (void)drawRect:(NSRect)rect
{
// Call original drawing method
[self drawRectOriginal:rect];
//
// Build clipping path : intersection of frame clip (bezier path with rounded corners) and rect argument
//
NSRect windowRect = [[self window] frame];
windowRect.origin = NSMakePoint(0, 0);
float cornerRadius = [self roundedCornerRadius];
[[NSBezierPath bezierPathWithRoundedRect:windowRect xRadius:cornerRadius yRadius:cornerRadius] addClip];
[[NSBezierPath bezierPathWithRect:rect] addClip];
// Any custom drawing goes here
...
}
Sample Code NSImageNameActionTemplate image and a custom color (in kCGBlendModeColorDodge) over the existing frame 2009 03 10Which framework is running ?
/System.
Sometimes I forget JSCocoa is running from a nightly JavascriptCore and wonder about this new strange crash :) — so it's time for a console warning :
Dl_info info; // Grab a JavascriptCore symbol with dlsym // Get its framework path with dladdr dladdr(dlsym(RTLD_DEFAULT, "JSClassCreate"), &info); BOOL runningFromSystemLibrary = [[NSString stringWithUTF8String:info.dli_fname] hasPrefix:@"/System"]; // Warn if not running from /System if (!runningFromSystemLibrary) NSLog(@"***Running a nightly JavascriptCore***");
2009 03 07CoreUI can paint pretty big4
The controls are drawn by CoreUI, a private framework new to Leopard. To draw controls at any size, CoreUI uses xml "recipes" to mix BIG pictures and PDFs. We can't access CoreUI directly, but as WebKit is open source, we can peek to see how Safari does it !
Here's the Safari recipe :
- change the scale of the graphics context
- scale back the drawing rect by the same amount
- ask an
NSButtonCellto paint itself
… Et voilà !
// We're in a custom view, derived right from NSView
- (void)drawRect:(NSRect)rect
{
// Allocate a button cell
id cell = [NSButtonCell new];
[cell setButtonType:NSMomentaryPushInButton];
[cell setBezelStyle:NSRoundedBezelStyle];
// Compute scale - scale the button to fit width
float scale = (rect.size.width / [cell cellSize].width);
// Apply scale to the graphics context
CGContextRef context = [[NSGraphicsContext currentContext] graphicsPort];;
CGContextScaleCTM(context, scale, scale);
// Scale down drawing rect
rect.size.width /= scale;
rect.size.height /= scale;
// Draw rect
[cell drawWithFrame:rect inView:self];
// We're done !
[cell release];
}
Sample Code 2009 02 18Localization with functions
// To get this result ...
- 1 fail skopirovalsya.
- 2 faila skopirovalis'.
- 5 failov skopirovalos'.
// ... we need this code.
if (x == 1) str = NSLocalizedString("KEY_1",...);
else if (x == 2) str = NSLocalizedString("KEY_2",...);
else str = NSLocalizedString("KEY_3",...);
Going this way injects grammar specific code for each language into ObjC code that just needs to display information. If we need to support a new language, we may need to change our ObjC code ! That's BAD.
Let's try it in JSCocoa. We'll define strings and functions on the Javascript side …
// Javascript side : register localizations.
// Register raw strings
localizedStrings['Hello World'] = 'Hallo Welt'
// Register javascript functions that will be called back with arguments
localizedStrings['BookCount'] = function (count)
{
if (count == 0) return 'Keine Bücher gefunden !'
if (count == 1) return 'Ein Buch'
return count + ' Bücher'
}
… and use them on the ObjC side :
// Call JSLocalizedString with an identifier and optional parameters id string = JSLocalizedString(@"BookCount", [NSNumber numberWithInt:bookCount], nil); [label setStringValue:string];There ! However many languages we support, we'll always have the same ObjC code : the language-specific logic will be where it belongs, in a localized file. Try out JSLocalizedString in the latest JSCocoa. (Get it from GitHub)
2009 01 30Did you forget to nest alloc and init?
No I didn't, but thanks for the warning ! Calling
NSLog(@"%@", [NSString alloc]); will trigger that message. It does indeed come from the description message called on a non fully initialized class.