Capturing Middle-Mouse Click in Safari

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.