diff options
-rw-r--r-- | chrome/browser/cocoa/nsmenuitem_additions.h | 18 | ||||
-rw-r--r-- | chrome/browser/cocoa/nsmenuitem_additions.mm | 100 | ||||
-rw-r--r-- | chrome/browser/cocoa/nsmenuitem_additions_unittest.mm | 341 | ||||
-rwxr-xr-x | chrome/chrome.gyp | 3 |
4 files changed, 462 insertions, 0 deletions
diff --git a/chrome/browser/cocoa/nsmenuitem_additions.h b/chrome/browser/cocoa/nsmenuitem_additions.h new file mode 100644 index 0000000..2e17674 --- /dev/null +++ b/chrome/browser/cocoa/nsmenuitem_additions.h @@ -0,0 +1,18 @@ +// 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. + +#ifndef CHROME_BROWSER_COCOA_NSMENUITEM_ADDITIONS_H_ +#define CHROME_BROWSER_COCOA_NSMENUITEM_ADDITIONS_H_ + +#import <Cocoa/Cocoa.h> + +@interface NSMenuItem(ChromeAdditions) + +// Returns true exactly if the menu item would fire if it would be put into +// a menu and then |menu performKeyEquivalent:event| was called. +- (BOOL)cr_firesForKeyEvent:(NSEvent*)event; + +@end + +#endif // CHROME_BROWSER_COCOA_NSMENUITEM_ADDITIONS_H_ diff --git a/chrome/browser/cocoa/nsmenuitem_additions.mm b/chrome/browser/cocoa/nsmenuitem_additions.mm new file mode 100644 index 0000000..2a4ccde --- /dev/null +++ b/chrome/browser/cocoa/nsmenuitem_additions.mm @@ -0,0 +1,100 @@ +#import "chrome/browser/cocoa/nsmenuitem_additions.h" + +#include <Carbon/Carbon.h> + +#include "base/logging.h" + +@implementation NSMenuItem(ChromeAdditions) + +- (BOOL)cr_firesForKeyEvent:(NSEvent*)event { + DCHECK([event type] == NSKeyDown); + if (![self isEnabled]) + return NO; + + // 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; + + 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; + } + + // 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 characterAtIndex:0] > 0x7f && + [[event characters] length] > 0 && + [[event characters] characterAtIndex:0] <= 0x7f) + eventString = [event characters]; + + // When both |characters| and |charactersIgnoringModifiers| are ascii, we + // want to use |characters| if it's a character and + // |charactersIgnoringModifiers| else (on dvorak, cmd-shift-z should fire + // "cmd-:" instead of "cmd-;", but on dvorak-qwerty, cmd-shift-z should fire + // cmd-shift-z instead of cmd-:). + if ([eventString characterAtIndex:0] <= 0x7f && + [[event characters] length] > 0 && + [[event characters] characterAtIndex:0] <= 0x7f && + isalpha([[event characters] characterAtIndex:0])) + eventString = [event characters]; + + // Clear shift key for printable characters. + if ((eventModifiers & (NSNumericPadKeyMask | NSFunctionKeyMask)) == 0 && + [[self keyEquivalent] characterAtIndex:0] != '\r') + eventModifiers &= ~NSShiftKeyMask; + + // Clear all non-interesting modifiers + eventModifiers &= NSCommandKeyMask | + NSControlKeyMask | + NSAlternateKeyMask | + NSShiftKeyMask; + + return [eventString isEqualToString:[self keyEquivalent]] + && eventModifiers == [self keyEquivalentModifierMask]; +} + +@end + diff --git a/chrome/browser/cocoa/nsmenuitem_additions_unittest.mm b/chrome/browser/cocoa/nsmenuitem_additions_unittest.mm new file mode 100644 index 0000000..1279e0c --- /dev/null +++ b/chrome/browser/cocoa/nsmenuitem_additions_unittest.mm @@ -0,0 +1,341 @@ +#import "chrome/browser/cocoa/nsmenuitem_additions.h" + +#include <iostream> +#include <Carbon/Carbon.h> + +#include "base/scoped_nsobject.h" +#include "base/sys_string_conversions.h" +#include "testing/gtest/include/gtest/gtest.h" + +NSEvent* KeyEvent(const NSUInteger modifierFlags, + NSString* chars, + NSString* charsNoMods, + const NSUInteger keyCode) { + return [NSEvent keyEventWithType:NSKeyDown + location:NSZeroPoint + modifierFlags:modifierFlags + timestamp:0.0 + windowNumber:0 + context:nil + characters:chars + charactersIgnoringModifiers:charsNoMods + isARepeat:NO + keyCode:keyCode]; +} + +NSMenuItem* MenuItem(NSString* equiv, NSUInteger mask) { + NSMenuItem* item = [[[NSMenuItem alloc] initWithTitle:@"" + action:NULL + keyEquivalent:@""] autorelease]; + [item setKeyEquivalent:equiv]; + [item setKeyEquivalentModifierMask:mask]; + return item; +} + +std::ostream& operator<<(std::ostream& out, NSObject* obj) { + return out << base::SysNSStringToUTF8([obj description]); +} + +std::ostream& operator<<(std::ostream& out, NSMenuItem* item) { + return out << "NSMenuItem " << base::SysNSStringToUTF8([item keyEquivalent]); +} + +void ExpectKeyFiresItemEq(bool result, NSEvent* key, NSMenuItem* item, + bool compareCocoa ) { + EXPECT_EQ(result, [item cr_firesForKeyEvent:key]) << key << '\n' << item; + + // Make sure that Cocoa does in fact agree with our expectations. However, + // in some cases cocoa behaves weirdly (if you create e.g. a new event that + // contains all fields of the event that you get when hitting cmd-a with a + // russion keyboard layout, the copy won't fire a menu item that has cmd-a as + // key equivalent, even though the original event would) and isn't a good + // oracle function. + if (compareCocoa) { + scoped_nsobject<NSMenu> menu([[NSMenu alloc] initWithTitle:@"Menu!"]); + [menu setAutoenablesItems:NO]; + EXPECT_FALSE([menu performKeyEquivalent:key]); + [menu addItem:item]; + EXPECT_EQ(result, [menu performKeyEquivalent:key]) << key << '\n' << item; + } +} + +void ExpectKeyFiresItem( + NSEvent* key, NSMenuItem* item, bool compareCocoa = true) { + ExpectKeyFiresItemEq(true, key, item, compareCocoa); +} + +void ExpectKeyDoesntFireItem( + NSEvent* key, NSMenuItem* item, bool compareCocoa = true) { + ExpectKeyFiresItemEq(false, key, item, compareCocoa); +} + +TEST(NSMenuItemAdditionsTest, TestFiresForKeyEvent) { + // These test cases were built by writing a small test app that has a + // MainMenu.xib with a given key equivalent set in Interface Builder and a + // some code that prints both the key equivalent that fires a menu item and + // the menu item's key equivalent and modifier masks. I then pasted those + // below. This was done with a US layout, unless otherwise noted. In the + // comments, "z" always means the physical "z" key on a US layout no matter + // what character that key produces. + + NSMenuItem* item; + NSEvent* key; + unichar ch; + NSString* s; + + // Sanity + item = MenuItem(@"", 0); + EXPECT_TRUE([item isEnabled]); + + // a + key = KeyEvent(0x100, @"a", @"a", 0); + item = MenuItem(@"a", 0); + ExpectKeyFiresItem(key, item); + ExpectKeyDoesntFireItem(KeyEvent(0x20102, @"A", @"A", 0), item); + + // Disabled menu item + key = KeyEvent(0x100, @"a", @"a", 0); + item = MenuItem(@"a", 0); + [item setEnabled:NO]; + ExpectKeyDoesntFireItem(key, item, false); + + // shift-a + key = KeyEvent(0x20102, @"A", @"A", 0); + item = MenuItem(@"A", 0); + ExpectKeyFiresItem(key, item); + ExpectKeyDoesntFireItem(KeyEvent(0x100, @"a", @"a", 0), item); + + // cmd-opt-shift-a + key = KeyEvent(0x1a012a, @"\u00c5", @"A", 0); + item = MenuItem(@"A", 0x180000); + ExpectKeyFiresItem(key, item); + + // cmd-opt-a + key = KeyEvent(0x18012a, @"\u00e5", @"a", 0); + item = MenuItem(@"a", 0x180000); + ExpectKeyFiresItem(key, item); + + // cmd-= + key = KeyEvent(0x100110, @"=", @"=", 0x18); + item = MenuItem(@"=", 0x100000); + ExpectKeyFiresItem(key, item); + + // cmd-shift-= + key = KeyEvent(0x12010a, @"=", @"+", 0x18); + item = MenuItem(@"+", 0x100000); + ExpectKeyFiresItem(key, item); + + // Turns out Cocoa fires "+ 100108 + 18" if you hit cmd-= and the menu only + // has a cmd-+ shortcut. But that's transparent for |cr_firesForKeyEvent:|. + + // ctrl-3 + key = KeyEvent(0x40101, @"3", @"3", 0x14); + item = MenuItem(@"3", 0x40000); + ExpectKeyFiresItem(key, item); + + // return + key = KeyEvent(0, @"\r", @"\r", 0x24); + item = MenuItem(@"\r", 0); + ExpectKeyFiresItem(key, item); + + // shift-return + key = KeyEvent(0x20102, @"\r", @"\r", 0x24); + item = MenuItem(@"\r", 0x20000); + ExpectKeyFiresItem(key, item); + + // shift-left + ch = NSLeftArrowFunctionKey; + s = [NSString stringWithCharacters:&ch length:1]; + key = KeyEvent(0xa20102, s, s, 0x7b); + item = MenuItem(s, 0x20000); + ExpectKeyFiresItem(key, item); + + // shift-f1 (with a layout that needs the fn key down for f1) + ch = NSF1FunctionKey; + s = [NSString stringWithCharacters:&ch length:1]; + key = KeyEvent(0x820102, s, s, 0x7a); + item = MenuItem(s, 0x20000); + ExpectKeyFiresItem(key, item); + + // esc + // Turns out this doesn't fire. + key = KeyEvent(0x100, @"\e", @"\e", 0x35); + item = MenuItem(@"\e", 0); + ExpectKeyDoesntFireItem(key,item, false); + + // shift-esc + // Turns out this doesn't fire. + key = KeyEvent(0x20102, @"\e", @"\e", 0x35); + item = MenuItem(@"\e", 0x20000); + ExpectKeyDoesntFireItem(key,item, false); + + // cmd-esc + key = KeyEvent(0x100108, @"\e", @"\e", 0x35); + item = MenuItem(@"\e", 0x100000); + ExpectKeyFiresItem(key, item); + + // ctrl-esc + key = KeyEvent(0x40101, @"\e", @"\e", 0x35); + item = MenuItem(@"\e", 0x40000); + ExpectKeyFiresItem(key, item); + + // delete ("backspace") + key = KeyEvent(0x100, @"\x7f", @"\x7f", 0x33); + item = MenuItem(@"\x08", 0); + ExpectKeyFiresItem(key, item, false); + + // shift-delete + key = KeyEvent(0x20102, @"\x7f", @"\x7f", 0x33); + item = MenuItem(@"\x08", 0x20000); + ExpectKeyFiresItem(key, item, false); + + // forwarddelete (fn-delete / fn-backspace) + ch = NSDeleteFunctionKey; + s = [NSString stringWithCharacters:&ch length:1]; + key = KeyEvent(0x800100, s, s, 0x75); + item = MenuItem(@"\x7f", 0); + ExpectKeyFiresItem(key, item, false); + + // shift-forwarddelete (shift-fn-delete / shift-fn-backspace) + ch = NSDeleteFunctionKey; + s = [NSString stringWithCharacters:&ch length:1]; + key = KeyEvent(0x820102, s, s, 0x75); + item = MenuItem(@"\x7f", 0x20000); + ExpectKeyFiresItem(key, item, false); + + // fn-left + ch = NSHomeFunctionKey; + s = [NSString stringWithCharacters:&ch length:1]; + key = KeyEvent(0x800100, s, s, 0x73); + item = MenuItem(s, 0); + ExpectKeyFiresItem(key, item); + + // cmd-left + ch = NSLeftArrowFunctionKey; + s = [NSString stringWithCharacters:&ch length:1]; + key = KeyEvent(0xb00108, s, s, 0x7b); + item = MenuItem(s, 0x100000); + ExpectKeyFiresItem(key, item); + + // Hitting the "a" key with a russian keyboard layout -- does not fire + // a menu item that has "a" as key equiv. + key = KeyEvent(0x100, @"\u0444", @"\u0444", 0); + item = MenuItem(@"a", 0); + ExpectKeyDoesntFireItem(key,item); + + // cmd-a on a russion layout -- fires for a menu item with cmd-a as key equiv. + key = KeyEvent(0x100108, @"a", @"\u0444", 0); + item = MenuItem(@"a", 0x100000); + ExpectKeyFiresItem(key, item, false); + + // cmd-z on US layout + key = KeyEvent(0x100108, @"z", @"z", 6); + item = MenuItem(@"z", 0x100000); + ExpectKeyFiresItem(key, item); + + // cmd-y on german layout (has same keycode as cmd-z on us layout, shouldn't + // fire). + key = KeyEvent(0x100108, @"y", @"y", 6); + item = MenuItem(@"z", 0x100000); + ExpectKeyDoesntFireItem(key,item); + + // cmd-z on german layout + key = KeyEvent(0x100108, @"z", @"z", 0x10); + item = MenuItem(@"z", 0x100000); + ExpectKeyFiresItem(key, item); + + // fn-return (== enter) + key = KeyEvent(0x800100, @"\x3", @"\x3", 0x4c); + item = MenuItem(@"\r", 0); + ExpectKeyDoesntFireItem(key,item); + + // cmd-z on dvorak layout (so that the key produces ';') + key = KeyEvent(0x100108, @";", @";", 6); + ExpectKeyDoesntFireItem(key, MenuItem(@"z", 0x100000)); + ExpectKeyFiresItem(key, MenuItem(@";", 0x100000)); + + // cmd-z on dvorak qwerty layout (so that the key produces ';', but 'z' if + // cmd is down) + key = KeyEvent(0x100108, @"z", @";", 6); + ExpectKeyFiresItem(key, MenuItem(@"z", 0x100000), false); + ExpectKeyDoesntFireItem(key, MenuItem(@";", 0x100000), false); + + // cmd-shift-z on dvorak layout (so that we get a ':') + key = KeyEvent(0x12010a, @";", @":", 6); + ExpectKeyFiresItem(key, MenuItem(@":", 0x100000)); + ExpectKeyDoesntFireItem(key, MenuItem(@";", 0x100000)); + + // cmd-s with a serbian layout (just "s" produces something that looks a lot + // like "c" in some fonts, but is actually \u0441. cmd-s activates a menu item + // with key equivalent "s", not "c") + key = KeyEvent(0x100108, @"s", @"\u0441", 1); + ExpectKeyFiresItem(key, MenuItem(@"s", 0x100000), false); + ExpectKeyDoesntFireItem(key, MenuItem(@"c", 0x100000)); +} + +NSString* keyCodeToCharacter(NSUInteger keyCode, + EventModifiers modifiers, + TISInputSourceRef layout) { + CFDataRef uchr = (CFDataRef)TISGetInputSourceProperty( + layout, kTISPropertyUnicodeKeyLayoutData); + UCKeyboardLayout* keyLayout = (UCKeyboardLayout*)CFDataGetBytePtr(uchr); + + UInt32 deadKeyState = 0; + OSStatus err = noErr; + UniCharCount maxStringLength = 4, actualStringLength; + UniChar unicodeString[4]; + err = UCKeyTranslate(keyLayout, + (UInt16)keyCode, + kUCKeyActionDown, + modifiers, + LMGetKbdType(), + kUCKeyTranslateNoDeadKeysBit, + &deadKeyState, + maxStringLength, + &actualStringLength, + unicodeString); + assert(err == noErr); + + CFStringRef temp = CFStringCreateWithCharacters( + kCFAllocatorDefault, unicodeString, 1); + return [(NSString*)temp autorelease]; +} + +TEST(NSMenuItemAdditionsTest, TestMOnDifferentLayouts) { + // There's one key -- "m" -- that has the same keycode on most keyboard + //layouts. This function tests a menu item with cmd-m as key equivalent + // can be fired on all layouts. + NSMenuItem* item = MenuItem(@"m", 0x100000); + + NSDictionary* filter = [NSDictionary + dictionaryWithObject:(NSString*)kTISTypeKeyboardLayout + forKey:(NSString*)kTISPropertyInputSourceType]; + + // Docs say that including all layouts instead of just the active ones is + // slow, but there's no way around that. + NSArray* list = (NSArray*)TISCreateInputSourceList( + (CFDictionaryRef)filter, true); + for (id layout in list) { + TISInputSourceRef ref = (TISInputSourceRef)layout; + + NSUInteger keyCode = 0x2e; // "m" on a us layout and most other layouts. + + // On a few layouts, "m" has a different key code. + NSString* layoutId = (NSString*)TISGetInputSourceProperty( + ref, kTISPropertyInputSourceID); + if ([layoutId isEqualToString:@"com.apple.keylayout.Belgian"] || + [layoutId isEqualToString:@"com.apple.keylayout.French"] || + [layoutId isEqualToString:@"com.apple.keylayout.French-numerical"] || + [layoutId isEqualToString:@"com.apple.keylayout.Italian"]) + keyCode = 0x29; + else if ([layoutId isEqualToString:@"com.apple.keylayout.Turkish"]) + keyCode = 0x28; + + EventModifiers modifiers = cmdKey >> 8; + NSString* chars = keyCodeToCharacter(keyCode, modifiers, ref); + NSString* charsIgnoringMods = keyCodeToCharacter(keyCode, 0, ref); + NSEvent* key = KeyEvent(0x100000, chars, charsIgnoringMods, keyCode); + ExpectKeyFiresItem(key, item, false); + } + CFRelease(list); +} diff --git a/chrome/chrome.gyp b/chrome/chrome.gyp index 75a3490..994e0d1 100755 --- a/chrome/chrome.gyp +++ b/chrome/chrome.gyp @@ -1159,6 +1159,8 @@ 'browser/cocoa/menu_button.mm', 'browser/cocoa/multi_key_equivalent_button.h', 'browser/cocoa/multi_key_equivalent_button.mm', + 'browser/cocoa/nsmenuitem_additions.h', + 'browser/cocoa/nsmenuitem_additions.mm', 'browser/cocoa/nswindow_local_state.h', 'browser/cocoa/nswindow_local_state.mm', 'browser/cocoa/objc_method_swizzle.h', @@ -4462,6 +4464,7 @@ 'browser/cocoa/hyperlink_button_cell_unittest.mm', 'browser/cocoa/menu_button_unittest.mm', 'browser/cocoa/nsimage_cache_unittest.mm', + 'browser/cocoa/nsmenuitem_additions_unittest.mm', 'browser/cocoa/nswindow_local_state_unittest.mm', 'browser/cocoa/objc_method_swizzle_unittest.mm', 'browser/cocoa/page_info_window_controller_unittest.mm', |