Last active
February 19, 2026 17:50
-
-
Save Wowfunhappy/b09900c5420767651338a57692348e31 to your computer and use it in GitHub Desktop.
Firefox Key Equivalent Fixer
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #import <Foundation/Foundation.h> | |
| #import <AppKit/AppKit.h> | |
| #import "ZKSwizzle.h" | |
| @interface myNSApplication : NSApplication | |
| @end | |
| @implementation myNSApplication | |
| - (void)sendEvent:(NSEvent *)event { | |
| // Firefox does not handle user-defined key equivalents properly | |
| // https://bugzilla.mozilla.org/show_bug.cgi?id=1333781 | |
| // Check if the event is a key down event with a modifier key. (Otherwise, it can't be a keyboard shortcut.) | |
| if ( | |
| [event type] == NSKeyDown && | |
| [event modifierFlags] & (NSCommandKeyMask | NSAlternateKeyMask | NSControlKeyMask) | |
| //We don't check NSShiftKeyMask because shortcuts aren't allowed to use Shift as the only modifier key. | |
| ) { | |
| // Query user-defined key equivalents | |
| NSDictionary *userKeyEquivalents = [[NSUserDefaults standardUserDefaults] objectForKey:@"NSUserKeyEquivalents"]; | |
| if (userKeyEquivalents) { | |
| // Check if the event matches any user-defined key equivalents | |
| for (NSString *menuItemTitle in userKeyEquivalents) { | |
| NSString *shortcut = userKeyEquivalents[menuItemTitle]; | |
| if ([self event:event matchesShortcut:shortcut]) { | |
| [self performKeyEquivalent:event]; | |
| [self performActionForItemWithTitle:menuItemTitle inMenu:[NSApp mainMenu]]; | |
| return; | |
| } | |
| } | |
| } | |
| } | |
| // Pass event to Firefox to handle normally. | |
| ZKOrig(void, event); | |
| } | |
| - (BOOL)event:(NSEvent *)event matchesShortcut:(NSString *)shortcut { | |
| // Convert the shortcut string to a key equivalent and modifier mask | |
| NSString *characterKey = [shortcut substringFromIndex:[shortcut length] - 1]; | |
| NSString *modifierString = [shortcut substringToIndex:[shortcut length] - 1]; | |
| NSUInteger modifierMask = 0; | |
| for (int i = 0; i < [modifierString length]; i++) { | |
| switch ([modifierString characterAtIndex:i]) { | |
| case '@': | |
| modifierMask |= NSCommandKeyMask; | |
| break; | |
| case '~': | |
| modifierMask |= NSAlternateKeyMask; | |
| break; | |
| case '^': | |
| modifierMask |= NSControlKeyMask; | |
| break; | |
| case '$': | |
| modifierMask |= NSShiftKeyMask; | |
| break; | |
| } | |
| } | |
| // Compare with the event | |
| return ( | |
| [[[event charactersIgnoringModifiers] lowercaseString] isEqualToString:characterKey] && | |
| ([event modifierFlags] & NSDeviceIndependentModifierFlagsMask) == modifierMask | |
| ); | |
| } | |
| - (void)performActionForItemWithTitle:(NSString *)title inMenu:(NSMenu *)menu { | |
| for (NSMenuItem *menuItem in [menu itemArray]) { | |
| if ([menuItem hasSubmenu]) { | |
| [self performActionForItemWithTitle:title inMenu:[menuItem submenu]]; | |
| } else { | |
| //Ensure three periods ("...") matches the elipses character ("…"). | |
| title = [title stringByReplacingOccurrencesOfString:@"..." withString:@"…"]; | |
| NSString *menuItemTitle = [[menuItem title] stringByReplacingOccurrencesOfString:@"..." withString:@"…"]; | |
| if ([menuItemTitle isEqualToString:title]) { | |
| [[menuItem menu] update]; //highlight menu | |
| [[menuItem menu] performActionForItemAtIndex:[[menuItem menu] indexOfItem:menuItem]]; | |
| return; | |
| } | |
| } | |
| } | |
| } | |
| @end | |
| @interface myNSWindow : NSWindow | |
| @end | |
| @implementation myNSWindow | |
| - (BOOL)makeFirstResponder:(NSResponder *)responder { | |
| //Initialize menus to ensure: | |
| //1. Every item can be triggerred by its key equivalents | |
| //2. Every item can appear in the search results of the `Help` search box. | |
| [self initializeSubmenusOf: [NSApp mainMenu]]; | |
| //We should also listen for menu changes. Unfortunately, the below code will crash Firefox. | |
| //Todo: figure out how to do this properly! | |
| /*[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(initializeSubmenusOf:) name:@"NSMenuDidAddItemNotification" object:[NSApp mainMenu]]; | |
| [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(initializeSubmenusOf:) name:@"NSMenuDidRemoveItemNotification" object:[NSApp mainMenu]];*/ | |
| return ZKOrig(BOOL, responder); | |
| } | |
| - (void)initializeSubmenusOf:(NSMenu *)menu { | |
| [[menu delegate] performSelector:@selector(menuWillOpen:) withObject:menu]; | |
| [[menu delegate] performSelector:@selector(menuDidClose:) withObject:menu]; | |
| for (NSMenuItem *menuItem in [menu itemArray]) { | |
| if ([menuItem hasSubmenu]) { | |
| [self initializeSubmenusOf:[menuItem submenu]]; | |
| } | |
| [self fixUpMenuItem: menuItem]; | |
| } | |
| } | |
| - (void)fixUpMenuItem:(NSMenuItem *)menuItem { | |
| // In Firefox, `Select All` menu item is initially disabled for some reason | |
| if (![menuItem isEnabled] && [[menuItem title] isEqualToString:NSLocalizedString(@"Select All", nil)]) { | |
| [menuItem setEnabled:YES]; | |
| } | |
| } | |
| @end | |
| @implementation NSObject (main) | |
| + (void)load { | |
| ZKSwizzle(myNSApplication, NSApplication); | |
| ZKSwizzle(myNSWindow, NSWindow); | |
| } | |
| @end | |
| int main() {} |
Author
It is terrible though. I keep hitting Cmd-M by mistake for some reason in daily usage of the Mac, so I reassigned Minimize to Cmd-Opt-Ctrl-Shift-M or something like that, but in Firefox, the app I probably use the most, Cmd-M still minimizes the window.
That’s an annoying use case I didn’t think of! Open to ideas on how to resolve this one.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
It is terrible though. I keep hitting Cmd-M by mistake for some reason in daily usage of the Mac, so I reassigned Minimize to Cmd-Opt-Ctrl-Shift-M or something like that, but in Firefox, the app I probably use the most, Cmd-M still minimizes the window.