| // Copyright 2009 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #import "ui/base/cocoa/nsmenuitem_additions.h" |
| #include "base/apple/foundation_util.h" |
| |
| #include <Carbon/Carbon.h> |
| |
| #include "base/apple/bridging.h" |
| #include "base/apple/scoped_cftyperef.h" |
| #include "base/check.h" |
| #include "ui/events/keycodes/keyboard_code_conversion_mac.h" |
| |
| namespace ui::cocoa { |
| |
| namespace { |
| bool g_is_input_source_command_qwerty = false; |
| bool g_is_input_source_dvorak_right_or_left = false; |
| bool g_is_input_source_command_hebrew = false; |
| } // namespace |
| |
| void SetIsInputSourceCommandQwertyForTesting(bool is_command_qwerty) { |
| g_is_input_source_command_qwerty = is_command_qwerty; |
| } |
| |
| void SetIsInputSourceDvorakRightOrLeftForTesting(bool is_dvorak_right_or_left) { |
| g_is_input_source_dvorak_right_or_left = is_dvorak_right_or_left; |
| } |
| |
| void SetIsInputSourceCommandHebrewForTesting(bool is_command_hebrew) { |
| g_is_input_source_command_hebrew = is_command_hebrew; |
| } |
| |
| bool IsKeyboardLayoutCommandQwerty(NSString* layout_id) { |
| return [layout_id isEqualToString:@"com.apple.keylayout.DVORAK-QWERTYCMD"] || |
| [layout_id isEqualToString:@"com.apple.keylayout.Dhivehi-QWERTY"] || |
| [layout_id isEqualToString:@"com.apple.keylayout.Inuktitut-QWERTY"] || |
| [layout_id isEqualToString:@"com.apple.keylayout.Cherokee-QWERTY"]; |
| } |
| |
| bool IsKeyboardLayoutDvorakRightOrLeft(NSString* layout_id) { |
| return [layout_id isEqualToString:@"com.apple.keylayout.Dvorak-Right"] || |
| [layout_id isEqualToString:@"com.apple.keylayout.Dvorak-Left"]; |
| } |
| |
| bool IsKeyboardLayoutCommandHebrew(NSString* layout_id) { |
| // com.apple.keylayout.Hebrew, com.apple.keylayout.Hebrew-PC, |
| // com.apple.keylayout.Hebrew-QWERTY. |
| return [layout_id hasPrefix:@"com.apple.keylayout.Hebrew"]; |
| } |
| |
| NSUInteger ModifierMaskForKeyEvent(NSEvent* event) { |
| NSUInteger eventModifierMask = |
| NSEventModifierFlagCommand | NSEventModifierFlagControl | |
| NSEventModifierFlagOption | NSEventModifierFlagShift; |
| |
| // If `event` isn't a function key press or it's not a character key press |
| // (e.g. it's a flags change), we can simply return the mask. |
| if ((event.modifierFlags & NSEventModifierFlagFunction) == 0 || |
| event.type != NSEventTypeKeyDown) { |
| return eventModifierMask; |
| } |
| |
| NSString* eventString = event.charactersIgnoringModifiers; |
| if (eventString.length == 0) { |
| return eventModifierMask; |
| } |
| |
| // "Up arrow", home, and other "function" key events include |
| // NSEventModifierFlagFunction in their flags even though the user isn't |
| // holding down the keyboard's function / world key. Add |
| // NSEventModifierFlagFunction to the returned modifier mask only if the |
| // event isn't for a function key. |
| unichar firstCharacter = [eventString characterAtIndex:0]; |
| if (firstCharacter < NSUpArrowFunctionKey || |
| firstCharacter > NSModeSwitchFunctionKey) |
| eventModifierMask |= NSEventModifierFlagFunction; |
| |
| return eventModifierMask; |
| } |
| |
| } // namespace ui::cocoa |
| |
| @interface KeyboardInputSourceListener : NSObject |
| @end |
| |
| @implementation KeyboardInputSourceListener |
| |
| - (instancetype)init { |
| if (self = [super init]) { |
| [NSNotificationCenter.defaultCenter |
| addObserver:self |
| selector:@selector(inputSourceDidChange:) |
| name:NSTextInputContextKeyboardSelectionDidChangeNotification |
| object:nil]; |
| [self updateInputSource]; |
| } |
| return self; |
| } |
| |
| - (void)dealloc { |
| [NSNotificationCenter.defaultCenter removeObserver:self]; |
| } |
| |
| - (void)updateInputSource { |
| base::ScopedCFTypeRef<TISInputSourceRef> inputSource( |
| TISCopyCurrentKeyboardInputSource()); |
| NSString* layoutId = base::apple::CFToNSPtrCast( |
| base::apple::CFCast<CFStringRef>(TISGetInputSourceProperty( |
| inputSource.get(), kTISPropertyInputSourceID))); |
| ui::cocoa::g_is_input_source_command_qwerty = |
| ui::cocoa::IsKeyboardLayoutCommandQwerty(layoutId); |
| ui::cocoa::g_is_input_source_dvorak_right_or_left = |
| ui::cocoa::IsKeyboardLayoutDvorakRightOrLeft(layoutId); |
| ui::cocoa::g_is_input_source_command_hebrew = |
| ui::cocoa::IsKeyboardLayoutCommandHebrew(layoutId); |
| } |
| |
| - (void)inputSourceDidChange:(NSNotification*)notification { |
| [self updateInputSource]; |
| } |
| |
| @end |
| |
| @implementation NSMenuItem (ChromeAdditions) |
| |
| - (BOOL)cr_firesForKeyEquivalentEvent:(NSEvent*)event { |
| if (![self isEnabled]) |
| return NO; |
| |
| DCHECK(event.type == NSEventTypeKeyDown); |
| // In System Preferences->Keyboard->Keyboard Shortcuts, it is possible to add |
| // arbitrary keyboard shortcuts to applications. It is not documented how this |
| // works in detail, but |NSMenuItem| has a method |userKeyEquivalent| that |
| // sounds related. |
| // However, it looks like |userKeyEquivalent| is equal to |keyEquivalent| when |
| // a user shortcut is set in system preferences, i.e. Cocoa automatically |
| // sets/overwrites |keyEquivalent| as well. Hence, this method can ignore |
| // |userKeyEquivalent| and check |keyEquivalent| only. |
| |
| // Menu item key equivalents are nearly all stored without modifiers. The |
| // exception is shift, which is included in the key and not in the modifiers |
| // for printable characters (but not for stuff like arrow keys etc). |
| NSString* eventString = event.charactersIgnoringModifiers; |
| NSUInteger eventModifiers = |
| event.modifierFlags & NSEventModifierFlagDeviceIndependentFlagsMask; |
| |
| // cmd-opt-a gives some weird char as characters and "a" as |
| // charactersWithoutModifiers with an US layout, but an "a" as characters and |
| // a weird char as "charactersWithoutModifiers" with a cyrillic layout. Oh, |
| // Cocoa! Instead of getting the current layout from Text Input Services, |
| // and then requesting the kTISPropertyUnicodeKeyLayoutData and looking in |
| // there, let's go with a pragmatic hack. |
| bool useEventCharacters = eventString.length == 0; |
| NSString* eventCharacters = event.characters; |
| if (eventString.length > 0 && eventCharacters.length > 0) { |
| if ([eventString characterAtIndex:0] > 0x7f && |
| [eventCharacters characterAtIndex:0] <= 0x7f) { |
| useEventCharacters = true; |
| } else if (ui::cocoa::g_is_input_source_command_hebrew && |
| [eventString isEqualToString:@"/"] && |
| [eventCharacters isEqualToString:@"q"]) { |
| // Our pragmatic hack works very well except for the "q" key in Hebrew |
| // layouts. In this case, the first char of eventString ("/") is |
| // not < 0x7f, so the hack doesn't choose eventCharacters (which is |
| // "q"). This causes Cmd-q to not take the normal processing path which |
| // includes a warning to hold "Cmd q" to quit (if that option is set). |
| // Instead, the Cmd-q likely travels to the renderer and upon its return |
| // triggers -[NSApplication terminate:], the selector associated with |
| // Chrome -> Quit. We handle this special case here. |
| useEventCharacters = true; |
| } |
| } |
| if (useEventCharacters) { |
| eventString = eventCharacters; |
| |
| // If the user is pressing the Shift key, force the shortcut string to |
| // uppercase. Otherwise, if only Caps Lock is down, ensure the shortcut |
| // string is lowercase. |
| if (eventModifiers & NSEventModifierFlagShift) { |
| eventString = eventString.uppercaseString; |
| } else if (eventModifiers & NSEventModifierFlagCapsLock) { |
| eventString = eventString.lowercaseString; |
| } |
| } |
| |
| if (eventString.length == 0 || self.keyEquivalent.length == 0) { |
| return NO; |
| } |
| |
| // Turns out esc never fires unless cmd or ctrl is down. |
| if (event.keyCode == kVK_Escape && |
| (eventModifiers & |
| (NSEventModifierFlagControl | NSEventModifierFlagCommand)) == 0) { |
| return NO; |
| } |
| |
| // From the |NSMenuItem setKeyEquivalent:| documentation: |
| // |
| // If you want to specify the Backspace key as the key equivalent for a menu |
| // item, use a single character string with NSBackspaceCharacter (defined in |
| // NSText.h as 0x08) and for the Forward Delete key, use NSDeleteCharacter |
| // (defined in NSText.h as 0x7F). Note that these are not the same characters |
| // you get from an NSEvent key-down event when pressing those keys. |
| if ([self.keyEquivalent characterAtIndex:0] == NSBackspaceCharacter && |
| [eventString characterAtIndex:0] == NSDeleteCharacter) { |
| unichar chr = NSBackspaceCharacter; |
| eventString = [NSString stringWithCharacters:&chr length:1]; |
| |
| // Make sure "shift" is not removed from modifiers below. |
| eventModifiers |= NSEventModifierFlagFunction; |
| } |
| if ([self.keyEquivalent characterAtIndex:0] == NSDeleteCharacter && |
| [eventString characterAtIndex:0] == NSDeleteFunctionKey) { |
| unichar chr = NSDeleteCharacter; |
| eventString = [NSString stringWithCharacters:&chr length:1]; |
| |
| // Make sure "shift" is not removed from modifiers below. |
| eventModifiers |= NSEventModifierFlagFunction; |
| } |
| |
| // We intentionally leak this object. |
| [[maybe_unused]] static KeyboardInputSourceListener* listener = |
| [[KeyboardInputSourceListener alloc] init]; |
| |
| // We typically want to compare [NSMenuItem keyEquivalent] against [NSEvent |
| // charactersIgnoringModifiers]. There are special command-qwerty layouts |
| // (such as DVORAK-QWERTY) which use QWERTY-style shortcuts when the Command |
| // key is held down. In this case, we want to use the keycode of the event |
| // rather than looking at the characters. |
| if (ui::cocoa::g_is_input_source_command_qwerty) { |
| ui::KeyboardCode windows_keycode = |
| ui::KeyboardCodeFromKeyCode(event.keyCode); |
| unichar shifted_character, character; |
| ui::MacKeyCodeForWindowsKeyCode(windows_keycode, event.modifierFlags, |
| &shifted_character, &character); |
| eventString = [NSString stringWithFormat:@"%C", shifted_character]; |
| } |
| |
| // On all keyboards except Dvorak-Right/Left, treat cmd + <number key> as the |
| // equivalent numerical key. This is technically incorrect, since the actual |
| // character produced may not be a number key, but this causes Chrome to match |
| // platform behavior. For example, on the Czech keyboard, we want to interpret |
| // cmd + '+' as cmd + '1', even though the '1' character normally requires |
| // cmd + shift + '+'. |
| if (!ui::cocoa::g_is_input_source_dvorak_right_or_left && |
| eventModifiers == NSEventModifierFlagCommand) { |
| ui::KeyboardCode windows_keycode = |
| ui::KeyboardCodeFromKeyCode(event.keyCode); |
| if (windows_keycode >= ui::VKEY_0 && windows_keycode <= ui::VKEY_9) { |
| eventString = |
| [NSString stringWithFormat:@"%d", windows_keycode - ui::VKEY_0]; |
| } |
| } |
| |
| // [ctr + shift + tab] generates the "End of Medium" keyEquivalent rather than |
| // "Horizontal Tab". We still use "Horizontal Tab" in the main menu to match |
| // the behavior of Safari and Terminal. Thus, we need to explicitly check for |
| // this case. |
| if ((eventModifiers & NSEventModifierFlagShift) && |
| [eventString isEqualToString:@"\x19"]) { |
| eventString = @"\x9"; |
| } else { |
| // Clear shift key for printable characters, excluding tab. |
| if ((eventModifiers & |
| (NSEventModifierFlagNumericPad | NSEventModifierFlagFunction)) == 0 && |
| [self.keyEquivalent characterAtIndex:0] != '\r' && |
| [self.keyEquivalent characterAtIndex:0] != '\x9') { |
| eventModifiers &= ~NSEventModifierFlagShift; |
| } |
| } |
| |
| // Clear all non-interesting modifiers |
| eventModifiers &= ui::cocoa::ModifierMaskForKeyEvent(event); |
| |
| return [eventString isEqualToString:self.keyEquivalent] && |
| eventModifiers == self.keyEquivalentModifierMask; |
| } |
| |
| - (void)cr_setKeyEquivalent:(NSString*)aString |
| modifierMask:(NSEventModifierFlags)mask { |
| DCHECK(aString); |
| self.keyEquivalent = aString; |
| self.keyEquivalentModifierMask = mask; |
| } |
| |
| - (void)cr_clearKeyEquivalent { |
| self.keyEquivalent = @""; |
| self.keyEquivalentModifierMask = 0; |
| } |
| |
| @end |