#import "chrome/browser/cocoa/nsmenuitem_additions.h" #include #include #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 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); }