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 document

In 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

Instrospection in Objective-C
Sometimes public APIs in cocoa/cocoa-touch frameworks is simply not enough or maybe a private API could save you some work.

So, this is not something new: If you read Objective-c runtime reference you will find really handy and hacky functions there.
These are some snippets:

List up methods of a class
uint methodCount = 0;
Class class = NSClassFromString(@"UIWebDocumentView");
Method *mlist = class_copyMethodList(class, &methodCount);
for (int i = 0; i < methodCount; ++i){
    NSLog(@"%@", NSStringFromSelector(method_getName(mlist[i])));
}
List up protocols conformed by a class
Class class = [UITextView class];
Protocol **p1 = class_copyProtocolList(class, NULL);
for (int i = 0; p1[i]; i++) {
    printf(@"%s\n", protocol_getName(p1[i]));
}
free(p1);
List up ivars of a class
Class class = [UITextView class];
uint ivarsNum = 0;
Ivar *ivars = class_copyIvarList(class, &ivarsNum);
for (int i = 0; i < ivarsNum; i++) {
    printf(@"%s\n", ivar_getName(ivars[i]));
}
Introspect arguments of a method
Class class = [UITextView class];
SEL selector = @selector(keyboardInput:shouldInsertText:isMarkedText:);
Method method = class_getInstanceMethod(class, selector);
char *arg = method_copyArgumentType(method, 0);
printf(@"_%s_\n", arg);
free(arg);

Here you will get: @ for objects, i for integers, f for floats. That is all you get.

Using GDB In Xcode set a symbolic break point to:
-[UITextView keyboardInput:shouldInsertText:isMarkedText:]
or in gdb type:
b -[UITextView keyboardInput:shouldInsertText:isMarkedText:]
so the debugger will stop at that method. When stopped is possible to show the registers values hence is possible to inspect the arguments passed :) This is a more complete list of how to call the registers in different architectures. Since the iOS Simulator is in i386 (after prolog) I can inspect this particular method doing:
(gdb) po *(id*)($ebp + 8)
<MyTextView: 0x5911270; baseClass = UITextView; frame = (80 70; 240 323); text = 'Lorem ipsum dolor sit er ...'; clipsToBounds = YES; autoresize = RM+BM; layer = <CALayer: 0x5c0c7d0>; contentOffset: {0, 0}>

(gdb) p *(SEL*)($ebp + 12)
$1 = (SEL) 0xbd19

(gdb) po *(id*)($ebp + 16)
<UIWebDocumentView: 0xa02f000; frame = (0 0; 240 457); text = 'Lorem ipsum dolor sit er ...'; opaque = NO; userInteractionEnabled = NO; layer = <UIWebLayer: 0x5c35070>>

(gdb) po *(id*)($ebp + 20)
t

(gdb) p *(id*)($ebp + 24)
$2 = (id) 0x0
and finally to get the selector name from a SEL in gdb
(gdb) p (char*)$1
$1 = 0xbd19 "keyboardInput:shouldInsertText:isMarkedText:"

I found this super useful because now I can get the list of method of any class and dig in. Yippeee!

For example I wanted to see if I can customize some shortcuts in the iOS when using a hardware keyboard. But it turns out that the UITextView is not the one who controls that. UITextView seems to be a mere client of UIWebDocumentView which is the one who handles text input and also keyboard events. So overriding private methods of UITextView is just not enough. This is not as simple as I thought to I will save this for another post :)

Links

This work is licensed under BSD Zero Clause License | nacho4d ®