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.

Third Time’s The Charm on Mac RAM — I Hope

Being at the end of a UPS route is hard. Tuesday morning I saw from the UPS website that my third set of RAM from Crucial was “out for delivery” from the local hub. What this means is that it’s on a truck, heading for my house. Unfortunately, we’re at the tail-end of said route, and I have yet to receive a UPS delivery before 4:00 PM. We were going to be leaving around 5:00 and since UPS is a “drop and run” courier, if it got there after we left, it would have been sitting on the porch for several hours until we got home. Fortunately, it arrived about 4:45. So it got inside the house, but wouldn’t get inside the Mac until later.

When I got home Tuesday night, I installed the RAM. So far, it’s working perfectly. Of course, the first set worked perfectly for a week or so, so I’ll have to just keep an eye on it. Thus, once again, my Activity Monitor looks like this

Exploding Soda

A few days ago I went by the grocery store. One of the things I picked up were some 12-packs of soda. I had three 12-packs in my cart when I went to check-out, and the clerk said that they had a special running: 4 12-packs for $10, and you got three 2-liters for free. It made sense for me to go get some more soda, so I did.

When I got home, I was able to get everything except two of the 12-packs in one trip, with the intention of coming back out for the other two. But something happened, and I never made it back out. The 2 12-packs in question were in the back floorboard, out of direct sunlight, so I didn’t see a problem.

The next day, Thomas and I were going somewhere. I opened his door for him, and noticed that the 2 12-packs were obstructing his leg-room. I moved one of the 12-packs to the back seat, behind the passenger seat, and off we went.

On Friday morning, I needed to run an errand. I went out to my car, got in and started it up. I noticed a strange smell, but couldn’t quite place it. I then turned around and saw a 12-pack of Pepsi One on the back seat. Actually, I should say I noticed the remnants of a 12-pack of Pepsi One on the back seat. Remnants, because the whole bloody thing had exploded all over the inside of my car. It was then that I noticed the brown spots all over the car: the ceiling, the sunroof, the back window, the side windows, the windshield, the back seat, the backs of the front seats. Yes, pretty much everywhere that could have gotten splattered did.

I got out, went back in the house and got Thomas to come out and see the carnage. He was amused. I have to say, I was, too. I just couldn’t help but laugh at this absurd occurrence. I got the remnants out, and as I gently dropped it down onto the driveway, another can exploded. It missed me, but barely. There were three others that looked like they were near the bursting point, so I carefully knocked the side of each one against the edge of the driveway to puncture it in a “controlled” fashion.

So, of a 12-pack, 8 exploded in the car, one on its own, and three via “controlled detonation.”

The lesson to be learned: if you live in the South, don’t leave sodas in your car on hot Summer days.