Skip to content

Instantly share code, notes, and snippets.

@Wowfunhappy
Last active October 31, 2024 01:34
Show Gist options
  • Save Wowfunhappy/b09900c5420767651338a57692348e31 to your computer and use it in GitHub Desktop.
Save Wowfunhappy/b09900c5420767651338a57692348e31 to your computer and use it in GitHub Desktop.
Firefox Key Equivalent Fixer
#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() {}
@Wowfunhappy
Copy link
Author

Wowfunhappy commented Oct 20, 2024

New version is much better! As a bonus, searching the menu bar via Help will work properly now! (Previously, some menu items such as bookmarks would not appear in the results.)

I am aware of four remaining issues. All are relatively minor:

  1. Menu items are initialized when the Firefox window is created. If the menu changes while the window is open—by adding or removing a bookmark which has a keyboard shortcut assigned in System Preferences, for example—you will need to close and re-open the Firefox window before the keyboard shortcut works.

    • I tried to fix this by listening for the relevant notifications, see lines 106–109. Unfortunately this broke Firefox.
  2. Default shortcuts still work if nothing else is assigned to them. For example, Firefox sets ⌘D to "Bookmark Current Tab..." by default. You can assign "Bookmark Current Tab..." to ⌥⇧D and use those keys to add a bookmark, but pressing ⌘D will also add a bookmark unless you additionally assign ⌘D to something else.

    • Although this doesn't match the behavior of proper cocoa apps, it's not terrible and I don't see an easy way to fix it.
  3. Custom keyboard shortcuts don't work while the menu is open. (I.e., if you click File, custom keyboard shortcuts won't work until you close the menu.)

    • My swizzled sendEvent method doesn't seem to get called at all while the menu is open.
    • This kind of bothers me but I don't imagine it will ever happen during real-world use.
  4. After opening a new window, you need to wait a few seconds before custom keyboard shortcuts begin working.

    • I have no idea what causes this. My initializeSubmenusOf method finishes running within milliseconds.

@krackers This issue doesn't appear to bother you that much, but if you're interested... 🙂

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment