06/14/2009 Update: You can download a plugin to do this from here.
I have been a consistent user of the nightly builds of the WebKit project for some time now. For those of you who don’t know, WebKit is essentially the work-in-progress that will be the next production version of Safari. I like the nightlies because they are extremely fast, and while they occasionally have problems, that’s OK.
The point of that discussion was to say that I have stopped using Firefox on my Mac, because the nightly builds of WebKit are so much faster. But there’s one problem. In Firefox I could click a tab with my middle-mouse button, and it would close. Safari doesn’t have that feature, and that’s the one feature from Firefox that I really miss.
So, I’ve been trying to solve this problem using SIMBL. What SIMBL does is let you write a standard Cocoa bundle and have it load into another program, like Safari. Once loaded, you can replace methods in the application with your own versions, in a way known as “method swizzling.” I have successfully written a Cocoa bundle, made the approrpriate changes to make it loadable by SIMBL, and have loaded it into Safari/WebKit. I have logging statements at various points in the bundle, and I can see these on the system console, thus I know it’s loading.
Once the bundle was loading, I needed to pick the objects and their methods that I thought would be most likely to let me do what I needed, and then swizzle in my changes. Using F-Script Anywhere I was able to identify a single tab as an instance of TabButton. Using class-dump I was able to generate header files for Safari that would let me see the methods on TabButton and it’s parents. My first thought was to override mouseUp:. This works, and I can now trap mouse events when you click on any tab. According to the Apple docs, once I get a mouse event, I should be able to call [theEvent buttonNumber] to figure out which button was pressed. Well, maybe. No matter if I clicked with the left- or middle-mouse button, [theEvent buttonNumber] always returned 0. Further digging in the docs turned up an event called NSOtherMouseUp that is sent when a button “other” than left- or right-mouse is clicked. Supposedly, I should be able to override otherMouseUp: to get those events. I have successfully swizzled this method, but it never gets called. I know that TabButton had a version of this method, because when I swizzle, I can tell if there was already a method there, and there was. I’m just not sure why it isn’t being called.
So what did I get working? Well, after working a long time trying to get the middle-mouse detection working, I decided to punt for the moment. I added some code to the mouseUp method to check the event for modifiers and if the user held down Command while clicking, then I will close the tab. This is close to what I want, but nearly as sexy as just middle-mouse clicking. In case you’re interested, my TabButton.m looks like this:
1 #import "TabButton.h" 2 #import "WebKit/WebKit.h" 3 4 @implementation TabButton (MCCSwizzle) 5 - (void)_mcc_mouseUp:(NSEvent *) theEvent 6 { 7 NSLog(@"_mcc_mouseDown"); 8 int buttonNumber = [theEvent buttonNumber]; 9 10 NSLog(@"buttonNumber: %d", buttonNumber); 11 NSLog(@"type: %d", [theEvent type]); 12 NSLog(@"modifierFlags: %d", [theEvent modifierFlags]); 13 14 if ([theEvent modifierFlags] & NSCommandKeyMask) { 15 NSLog(@"Cmd-Click!"); 16 [self closeTab: theEvent]; 17 } else { 18 [self _safari_mouseUp: theEvent]; 19 } 20 } 21 @end
and then the swizzling looks like this:
1 #import 2 #import "AppController.h" 3 #import "MiddleClickClose.h" 4 5 typedef struct objc_method *Method; 6 7 struct objc_method { 8 SEL method_name; 9 char *method_types; 10 IMP method_imp; 11 }; 12 13 BOOL MCCRenameSelector(Class _class, SEL _oldSelector, SEL _newSelector) 14 { 15 NSLog(@"OLD: %s", _oldSelector); 16 NSLog(@"NEW: %s", _newSelector); 17 18 Method method = nil; 19 20 // Look for the methods 21 method = (Method)class_getInstanceMethod(_class, _oldSelector); 22 if (method == nil) 23 return NO; 24 25 // Point the method to a new function 26 method->method_name = _newSelector; 27 return YES; 28 } 29 30 @implementation MiddleClickClose 31 + (void) load 32 { 33 int rc; 34 35 rc = MCCRenameSelector([TabButton class], @selector(mouseUp:), 36 @selector (_safari_mouseUp:)); 37 NSLog(@"RC: %d", rc); 38 39 rc = MCCRenameSelector([TabButton class], @selector(_mcc_mouseUp:), 40 @selector(mouseUp:)); 41 NSLog(@"RC: %d", rc); 42 43 rc = MCCRenameSelector([TabButton class], @selector(rightMouseUp:), 44 @selector (_safari_rightMouseUp:)); 45 NSLog(@"RC: %d", rc); 46 47 rc = MCCRenameSelector([TabButton class], @selector(_mcc_rightMouseUp:), 48 @selector(rightMouseUp:)); 49 NSLog(@"RC: %d", rc); 50 51 NSLog(@"MiddleClickClose loaded"); 52 } 53 54 + (MiddleClickClose*) sharedInstance 55 { 56 static MiddleClickClose* plugin = nil; 57 58 if (plugin == nil) 59 { 60 plugin = [[MiddleClickClose alloc] init]; 61 } 62 63 return plugin; 64 } 65 @end
Line 35 renames Safari’s original mouseUp: method to _safari_mouseUp: and then line 39 renames my _mcc_mouseUp: method to mouseUp:. The otherMouseUp: method is handled on lines 43 and 47, but as I said otherMouseUp never gets called.
What’s especially frustrating about this is that I know Safari knows how to bag a middle-click, because I frequently will middle-click on a link in a web page, and Safari will open that link in a new tab. So, why can’t I get a middle-click in the TabButton instance? Does anyone have any ideas on this? I’d appreciate any pointers. I feel like I’m thiiiiiiiiiiis close to getting this working, but there’s some small piece of info that is eluding me.
03/03/2008 Update: I got this working and have released it under the GPL. Read about it and get it over here.