Universal Back/Forward mouse buttons in OSX instead of M4/M5?
I added a tap to all my NSWindow
events. Turns out... the Master is simulating swipe events!
NSEvent: type=Swipe loc=(252,60) time=5443.9 flags=0x100 win=0x100b091f0 winNum=2014 ctxt=0x0 phase: 1 axis:0 amount=0.000 velocity={0, 0}
NSEvent: type=Swipe loc=(252,60) time=5443.9 flags=0x100 win=0x100b091f0 winNum=2014 ctxt=0x0 phase: 8 axis:0 amount=0.000 velocity={0, 0}
NSEvent: type=Swipe loc=(252,60) time=5445.7 flags=0x100 win=0x100b091f0 winNum=2014 ctxt=0x0 phase: 1 axis:0 amount=0.000 velocity={0, 0}
NSEvent: type=Swipe loc=(252,60) time=5445.7 flags=0x100 win=0x100b091f0 winNum=2014 ctxt=0x0 phase: 8 axis:0 amount=0.000 velocity={0, 0}
OK, that's pretty clever, since it basically means that it'll work in any view that supports the swipeWithEvent:
selector. I have no idea why this isn't the default behavior for the side buttons! Now I have to figure out how to add this functionality to my other mice. I don't think USB Overdrive can do something like this... unless AppleScript has a way to simulate gestures.
UPDATE: I have managed to replicate these events using natevw's reverse-engineered gesture simulation functions, https://github.com/calftrail/Touch. Might still need to be fixed up a bit, but it works! Final step will be to create an always-running app that eats M4 and M5 events and spits out these gestures.
TLInfoSwipeDirection dir = kTLInfoSwipeLeft;
NSDictionary* swipeInfo1 = [NSDictionary dictionaryWithObjectsAndKeys:
@(kTLInfoSubtypeSwipe), kTLInfoKeyGestureSubtype,
@(1), kTLInfoKeyGesturePhase,
nil];
NSDictionary* swipeInfo2 = [NSDictionary dictionaryWithObjectsAndKeys:
@(kTLInfoSubtypeSwipe), kTLInfoKeyGestureSubtype,
@(dir), kTLInfoKeySwipeDirection,
@(4), kTLInfoKeyGesturePhase,
nil];
CGEventRef event1 = tl_CGEventCreateFromGesture((__bridge CFDictionaryRef)(swipeInfo1), (__bridge CFArrayRef)@[]);
CGEventRef event2 = tl_CGEventCreateFromGesture((__bridge CFDictionaryRef)(swipeInfo2), (__bridge CFArrayRef)@[]);
CGEventPost(kCGHIDEventTap, event1);
CGEventPost(kCGHIDEventTap, event2);
// not sure if necessary under ARC
CFRelease(event1);
CFRelease(event2);
UPDATE 2: Here's a rough working sketch of a View Controller that globally captures M4 and M5 and emits swipes.
static void SBFFakeSwipe(TLInfoSwipeDirection dir) {
NSDictionary* swipeInfo1 = [NSDictionary dictionaryWithObjectsAndKeys:
@(kTLInfoSubtypeSwipe), kTLInfoKeyGestureSubtype,
@(1), kTLInfoKeyGesturePhase,
nil];
NSDictionary* swipeInfo2 = [NSDictionary dictionaryWithObjectsAndKeys:
@(kTLInfoSubtypeSwipe), kTLInfoKeyGestureSubtype,
@(dir), kTLInfoKeySwipeDirection,
@(4), kTLInfoKeyGesturePhase,
nil];
CGEventRef event1 = tl_CGEventCreateFromGesture((__bridge CFDictionaryRef)(swipeInfo1), (__bridge CFArrayRef)@[]);
CGEventRef event2 = tl_CGEventCreateFromGesture((__bridge CFDictionaryRef)(swipeInfo2), (__bridge CFArrayRef)@[]);
CGEventPost(kCGHIDEventTap, event1);
CGEventPost(kCGHIDEventTap, event2);
CFRelease(event1);
CFRelease(event2);
}
static CGEventRef KeyDownCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon) {
int64_t number = CGEventGetIntegerValueField(event, kCGMouseEventButtonNumber);
BOOL down = (CGEventGetType(event) == kCGEventOtherMouseDown);
if (number == 3) {
if (down) {
SBFFakeSwipe(kTLInfoSwipeLeft);
}
return NULL;
}
else if (number == 4) {
if (down) {
SBFFakeSwipe(kTLInfoSwipeRight);
}
return NULL;
}
else {
return event;
}
}
@implementation ViewController
-(void) viewDidLoad {
[super viewDidLoad];
NSDictionary* options = @{ (__bridge id)kAXTrustedCheckOptionPrompt: @YES };
BOOL accessibilityEnabled = AXIsProcessTrustedWithOptions((CFDictionaryRef)options);
assert(accessibilityEnabled);
CFMachPortRef eventTap = CGEventTapCreate(kCGHIDEventTap,
kCGHeadInsertEventTap,
kCGEventTapOptionDefault,
CGEventMaskBit(kCGEventOtherMouseUp)|CGEventMaskBit(kCGEventOtherMouseDown),
&KeyDownCallback,
NULL);
assert(eventTap != NULL);
CFRunLoopSourceRef runLoopSource = CFMachPortCreateRunLoopSource(NULL, eventTap, 0);
CFRunLoopAddSource(CFRunLoopGetCurrent(), runLoopSource, kCFRunLoopCommonModes);
CFRelease(runLoopSource);
CGEventTapEnable(eventTap, true);
//CFRelease(eventTap); -- needs to be done on dealloc, I think
}
@end
UPDATE 3: I've released an open-source menu bar app that replicates the Master's behavior for all third-party mice. It's called SensibleSideButtons. The technical details are described on the website.