Custom Window Widgets:
This is tricky and didn't really think it would work because all the info found was more than 3 years old. But it did!
I could get a window with custom widgets (Close/Miminize/Maximize and theorically fullscreen buttons), :) . I hope someday I know photoshop as much as objc and make my own cool buttons, for now I have to play with iCal's buttons.
Normal Cocoa NSWindowButtons | Customized NSWindowButtons |
The left picture shows the normal cocoa NSWindowButtons, right picture on the right shows darker and yellowish buttons, not very nice on the gray titlebar but ... Is because I don't know photoshop! (涙) But with great potential huh?! (笑)
Introduction:
In cocoa you get the buttons of a window :
NSButton *button = [window standardWindowButton:NSWindowZoomButton];
But
button
is not really of kind NSButton
is in fact an instance of _NSThemeWidget
. Furthermore, it does not use a normal NSButtonCell
but it has its own _NSThemeWidgetCell
. Furthermore, the close button is a subclass of them: _NSThemeCloseWidget
and _NSThemeCloseWidget
because it has a dirty state of the documentIn this post I show how to customize the images of these private classes because
setImage:
does nothing :(How this works
I tried to make a new class and pose it as an
_NSThemeWidgetCell
but posing is being deprecated and is not possible anymore, at least not that I know.Crayson told me that I should better try the method swizzling approach. This was a huge hint!.
So using a couple of objective-c runtime functions I added a method dynamically to _NSThemeWidgetCell class.
Then it was very easy to exchange methods : the one newly added
alt_drawWithFrame:inView:
with the other that does the drawing as usual drawWithFrame:inView:
.This part the code and the whole source + the images I borrowed from Lion's iCal can be downloaded from github :)
void drawWithFrameInView(id self, SEL _cmd, NSRect frame, id view) { NSLog(@"hacking drawWithFrameInView ..."); NSString *imageName = @"titlebarcontrols_regularwin"; //Get button ID int buttonID = (int)[self buttonID]; NSLog(@"%d", buttonID); switch (buttonID) { case 127: // Close button imageName = [imageName stringByAppendingFormat:@"_close"]; break; case 128: // Minimize button imageName = [imageName stringByAppendingFormat:@"_minimize"]; break; case 129: // Zoom button imageName = [imageName stringByAppendingFormat:@"_zoom" ]; break; case 130: // Toolbar button imageName = [imageName stringByAppendingFormat:@"_toolbar_button" ]; break; } //Get System preferences: Window style: (Aqua or graphite) NSString * const kAppleAquaColorVariant = @"AppleAquaColorVariant"; NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults]; [userDefaults addSuiteNamed:NSGlobalDomain]; NSNumber *color = [userDefaults objectForKey:kAppleAquaColorVariant]; if ([color intValue] == 6) {//graphite is 6 imageName = [imageName stringByAppendingFormat:@"_graphite"]; }else{//defaults to aqua, (aqua is 1) imageName = [imageName stringByAppendingFormat:@"_colorsryg"]; } //Get button state if ([self respondsToSelector:@selector(getState:)]) { int state = (int)[self getState:view]; //NSLog(@"state %d", state); switch (state) { //Known states //active = 0 //activenokey = not used? //disabled = not used? //inactive = 3 //pressed = 2 //rollover = 1 case 0: imageName = [imageName stringByAppendingFormat:@"_active"]; break; case 1: imageName = [imageName stringByAppendingFormat:@"_rollover"]; break; case 2: imageName = [imageName stringByAppendingFormat:@"_pressed"]; break; case 3: imageName = [imageName stringByAppendingFormat:@"_inactive"]; break; case 4: break;//disabled? activenokey? case 5: break;//disabled? activenokey? default: break; } NSImage *img = [NSImage imageNamed:imageName]; if (img){ [img dissolveToPoint:NSMakePoint(frame.origin.x, frame.origin.y + frame.size.height) fraction:1.0]; }else{ [(NSButtonCell*)self alt_drawWithFrame:frame inView:view];//original implementation } } }
And the part of code that does the objective-c runtime magic:
@implementation TestAppDelegate @synthesize window; - (void)applicationWillFinishLaunching:(NSNotification *)notification { Class class = NSClassFromString(@"_NSThemeWidgetCell"); SEL new_selector = @selector(alt_drawWithFrame:inView:); SEL orig_selector = @selector(drawWithFrame:inView:); //Add a new method dinamically because _NSThemeWidgetCell is a private class BOOL success = class_addMethod(class, new_selector, (IMP)drawWithFrameInView, "v@:{CGRect={CGPoint=dd}{CGSize=dd}}@"); if (success) { //Get the methods to exchange Method originalMethod = class_getInstanceMethod(class, orig_selector); Method newMethod = class_getInstanceMethod(class, new_selector); // If both are found, swizzle them if ((originalMethod != nil) && (newMethod != nil)){ method_exchangeImplementations(originalMethod, newMethod); } } //TEST:a new method should appear "alt_drawWithFrame:inView:" in the console //uint methodCount = 0; //class = NSClassFromString(@"UIWebDocumentView"); //Method *mlist = class_copyMethodList(class, &methodCount); //for (int i = 0; i < methodCount; ++i){ // NSLog(@"%@", NSStringFromSelector(method_getName(mlist[i]))); //} //TEST: "hacking drawWithFrameInView ..." should appear in the console //NSButton *but = [window standardWindowButton:NSWindowZoomButton]; //[[but cell] drawWithFrame:NSZeroRect inView:nil]; }References and useful Links
3 comments :
This uses a private API right?
Yes it uses _NSThemeWidget and _NSThemeWidgetCell class which are respectively subclasses of NSButton and NSButtonCell
Post a Comment