Universal Back/Forward mouse buttons in OSX instead of M4/M5?

5,551

Solution 1

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.

Solution 2

VS Code now supports Go Back and Go Forward actions, with default keybindings of CTRL+- and CTRL+SHIFT+-, respectively.

I was able to configure Logi Options to use these shortcuts, without breaking other apps, by:

  1. Adding a VS Code profile, through All Applications->Add Application->Other..., then searching for "Visual Studio Code"
  2. Remapping the back and forward buttons to a Keystroke assignment, with key sequences matching the VS Code keybindings

Here's what the back button config looks like, with the profile selected:

Logi Options, with Visual Studio Code profile selected

With these changes in place, forward and back buttons work in VS Code, and navigation in other apps, like Chrome, is unaffected.

Share:
5,551

Related videos on Youtube

Ross Llewallyn
Author by

Ross Llewallyn

Updated on September 18, 2022

Comments

  • Ross Llewallyn
    Ross Llewallyn almost 2 years

    I'm stumped by the behavior of the two side buttons on my Logitech MX Master mouse. On all my other mice, the side buttons are detected as generic "button 4" and "button 5". (I verified this using Xcode.) OS X, in contrast to Windows, seems to treat these commands as middle clicks, so in order to get the back/forward behavior you might expect, you need to use a tool like USB Overdrive to map them to +[ and +]. Unfortunately, this workaround doesn't work in every app and blinks the menu bar when you trigger it.

    Meanwhile, the side buttons on the Master don't get detected by my Xcode mouse tap at all, but somehow they work in practically any app with a nav bar. I've tested them in Finder, Safari, System Preferences, and Xcode. There's no menu blink and the mouse cursor has to be over area of the window controlled by the nav bar, which implies that there's some sort of universal back/forward event being sent (as opposed to the usual M4/M5). However, I can't find documentation of such an event existing in OS X. Most M4/M5 fixes involve mapping those buttons to +[ and +].

    So what's the Master doing with those side buttons, and how can I replicate the same behavior on all my other mice?

  • Allon Guralnek
    Allon Guralnek about 2 years
    This is perfect! Apparently another solution would be to uninstall Logi Options if you don't really need any of its functionality. This will make your Logitech back and forward buttons work without special assignments (see github.com/microsoft/vscode/issues/88029#issuecomment-722294‌​184). I don't want to uninstall it since it's the only way I found of disabling scroll wheel acceleration, so your solution was ideal.