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.

12 thoughts on “Capturing Middle-Mouse Click in Safari

  1. I would be interested in finding out your solution if you manage to find one.
    Thanks btw for the links about F-Script and SIMBL I hadn’t heard of those tools. Currently working my way, slowly, through Hillegass#s book.
    I remember back in december you were having problems with the pasteboard example, did you manage to get it sorted?
    Thanks
    Rob

  2. No, I never figured out what the problem was. Using Aaron’s code example worked, so I didn’t put too much thought into it.
    I just finished working through Aaron’s book again, so I think I’m pretty well ready to write a cool Mac app. Now I just need to think of something that needs writing… 😦

  3. Cool … did you run into the problem again on your second time through the book? Have you got Aaron’s other book, the advanced one?
    Also did you know that there is a third edition coming out later in the year, likely timed against the release of Leopard. Slightly annoying having bought it last week, but still bettter to start learning now than later. Though I am looking forward to the Garbage Collection!
    Coming up with cool apps is always the difficult bit, personally I want to write a music program a bit like Media Monkey on Windows. iTunes just doesn’t do anything for me, other than generally annoy me.
    Have you come across the 3 main Cocoa pod casts?
    Latenight Cocoa
    Cocoa Cast
    Cocoa Radio?

  4. The 2nd time through I skipped that challenge!
    As for a 3rd edition, I’m glad to hear it, though with the addition of GC, it will probably mean a shorter book. 🙂
    I don’t have the advanced book, because I read some negative comments at Amazon that made it sound more like a general Unix programming book than a Mac-specific book. I may pick it up later.

  5. Oh, and to trap the middle-click event you simply need to check [theEvent type] == NSOtherMouseUp inside mouseDown:. There isn’t a separate method per mouse button.

  6. Clearly that should be “inside mouseUp:”. Checking for a NS*MouseUp event type inside mouseDown: wouldn’t get you very far either 🙂

  7. Hi,
    Have you had any luck in solving this problem? I’d love to be able to middle-click tabs to close them like I do in Firefox, and I’m finding it quite frustrating that you can’t!
    Thanks.

  8. Yes, actually. I got my plugin working and I use it every day. I keep meaning to package it up and release it, but I haven’t gotten around to it. Maybe I’ll try to do it tomorrow.

  9. Please Joey, give us the power to middle-click-close tabs.
    I’d absolutely LOVE to have this.

Comments are closed.