blob: 040e05d0e3bcfc1dd3d336889cef4c3011d8326f [file] [log] [blame]
// Copyright (c) 2009 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "chrome/browser/ui/cocoa/nsmenuitem_additions.h"
#include <Carbon/Carbon.h>
#include "base/logging.h"
#include "base/mac/scoped_cftyperef.h"
#include "ui/events/keycodes/keyboard_code_conversion_mac.h"
namespace {
bool g_is_input_source_dvorak_qwerty = false;
} // namespace
void SetIsInputSourceDvorakQwertyForTesting(bool is_dvorak_qwerty) {
g_is_input_source_dvorak_qwerty = is_dvorak_qwerty;
}
@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];
[super dealloc];
}
- (void)updateInputSource {
base::ScopedCFTypeRef<TISInputSourceRef> inputSource(
TISCopyCurrentKeyboardInputSource());
NSString* inputSourceID = (NSString*)TISGetInputSourceProperty(
inputSource.get(), kTISPropertyInputSourceID);
g_is_input_source_dvorak_qwerty =
[inputSourceID isEqualToString:@"com.apple.keylayout.DVORAK-QWERTYCMD"];
}
- (void)inputSourceDidChange:(NSNotification*)notification {
[self updateInputSource];
}
@end
@implementation NSMenuItem(ChromeAdditions)
- (BOOL)cr_firesForKeyEvent:(NSEvent*)event {
if (![self isEnabled])
return NO;
DCHECK([event type] == NSKeyDown);
// 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] & NSDeviceIndependentModifierFlagsMask;
// 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 try a pragmatic hack.
if ([eventString length] == 0 ||
([eventString characterAtIndex:0] > 0x7f &&
[[event characters] length] > 0 &&
[[event characters] characterAtIndex:0] <= 0x7f)) {
eventString = [event characters];
// Process the shift if necessary.
if (eventModifiers & NSShiftKeyMask)
eventString = [eventString uppercaseString];
}
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 & (NSControlKeyMask | NSCommandKeyMask)) == 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 |= NSFunctionKeyMask;
}
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 |= NSFunctionKeyMask;
}
// We intentionally leak this object.
static __attribute__((unused)) KeyboardInputSourceListener* listener =
[[KeyboardInputSourceListener alloc] init];
// We typically want to compare [NSMenuItem keyEquivalent] against [NSEvent
// charactersIgnoringModifiers]. There is a special keyboard layout "Dvorak -
// QWERTY" which uses 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 (g_is_input_source_dvorak_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, 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 (eventModifiers == NSCommandKeyMask) {
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 & NSShiftKeyMask) &&
[eventString isEqualToString:@"\x19"]) {
eventString = @"\x9";
} else {
// Clear shift key for printable characters, excluding tab.
if ((eventModifiers & (NSNumericPadKeyMask | NSFunctionKeyMask)) == 0 &&
[[self keyEquivalent] characterAtIndex:0] != '\r' &&
[[self keyEquivalent] characterAtIndex:0] != '\x9') {
eventModifiers &= ~NSShiftKeyMask;
}
}
// Clear all non-interesting modifiers
eventModifiers &= NSCommandKeyMask |
NSControlKeyMask |
NSAlternateKeyMask |
NSShiftKeyMask;
return [eventString isEqualToString:[self keyEquivalent]]
&& eventModifiers == [self keyEquivalentModifierMask];
}
@end