summaryrefslogtreecommitdiffstats
path: root/chrome/browser/ui/cocoa/nsmenuitem_additions_unittest.mm
diff options
context:
space:
mode:
Diffstat (limited to 'chrome/browser/ui/cocoa/nsmenuitem_additions_unittest.mm')
-rw-r--r--chrome/browser/ui/cocoa/nsmenuitem_additions_unittest.mm351
1 files changed, 351 insertions, 0 deletions
diff --git a/chrome/browser/ui/cocoa/nsmenuitem_additions_unittest.mm b/chrome/browser/ui/cocoa/nsmenuitem_additions_unittest.mm
new file mode 100644
index 0000000..ecbf07d
--- /dev/null
+++ b/chrome/browser/ui/cocoa/nsmenuitem_additions_unittest.mm
@@ -0,0 +1,351 @@
+// Copyright (c) 2010 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 <ostream>
+
+#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;
+ } else if ([layoutId isEqualToString:@"com.apple.keylayout.Dvorak-Left"]) {
+ keyCode = 0x16;
+ } else if ([layoutId isEqualToString:@"com.apple.keylayout.Dvorak-Right"]) {
+ keyCode = 0x1a;
+ }
+
+ 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);
+}