-
-
Save Wowfunhappy/b09900c5420767651338a57692348e31 to your computer and use it in GitHub Desktop.
#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() {} |
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:
-
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.
-
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.
-
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.
- My swizzled
-
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.
- I have no idea what causes this. My
@krackers This issue doesn't appear to bother you that much, but if you're interested... 🙂
Currently, this mostly works! The only problem is, if you define a custom keyboard shortcut for one menu item, but those keys are already in use by another menu item, Firefox will use whichever menu item comes first in the menu bar (left to right, top to bottom).
For example:
I want to fix this!