1 // Copyright (c) 2011 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #import "chrome/browser/ui/cocoa/nsmenuitem_additions.h"
7 #include <Carbon/Carbon.h>
11 #include "base/mac/scoped_nsobject.h"
12 #include "base/strings/sys_string_conversions.h"
13 #include "testing/gtest/include/gtest/gtest.h"
15 NSEvent* KeyEvent(const NSUInteger modifierFlags,
17 NSString* charsNoMods,
18 const NSUInteger keyCode) {
19 return [NSEvent keyEventWithType:NSKeyDown
21 modifierFlags:modifierFlags
26 charactersIgnoringModifiers:charsNoMods
31 NSMenuItem* MenuItem(NSString* equiv, NSUInteger mask) {
32 NSMenuItem* item = [[[NSMenuItem alloc] initWithTitle:@""
34 keyEquivalent:@""] autorelease];
35 [item setKeyEquivalent:equiv];
36 [item setKeyEquivalentModifierMask:mask];
40 std::ostream& operator<<(std::ostream& out, NSObject* obj) {
41 return out << base::SysNSStringToUTF8([obj description]);
44 std::ostream& operator<<(std::ostream& out, NSMenuItem* item) {
45 return out << "NSMenuItem " << base::SysNSStringToUTF8([item keyEquivalent]);
48 void ExpectKeyFiresItemEq(bool result, NSEvent* key, NSMenuItem* item,
50 EXPECT_EQ(result, [item cr_firesForKeyEvent:key]) << key << '\n' << item;
52 // Make sure that Cocoa does in fact agree with our expectations. However,
53 // in some cases cocoa behaves weirdly (if you create e.g. a new event that
54 // contains all fields of the event that you get when hitting cmd-a with a
55 // russion keyboard layout, the copy won't fire a menu item that has cmd-a as
56 // key equivalent, even though the original event would) and isn't a good
59 base::scoped_nsobject<NSMenu> menu([[NSMenu alloc] initWithTitle:@"Menu!"]);
60 [menu setAutoenablesItems:NO];
61 EXPECT_FALSE([menu performKeyEquivalent:key]);
63 EXPECT_EQ(result, [menu performKeyEquivalent:key]) << key << '\n' << item;
67 void ExpectKeyFiresItem(
68 NSEvent* key, NSMenuItem* item, bool compareCocoa = true) {
69 ExpectKeyFiresItemEq(true, key, item, compareCocoa);
72 void ExpectKeyDoesntFireItem(
73 NSEvent* key, NSMenuItem* item, bool compareCocoa = true) {
74 ExpectKeyFiresItemEq(false, key, item, compareCocoa);
77 TEST(NSMenuItemAdditionsTest, TestFiresForKeyEvent) {
78 // These test cases were built by writing a small test app that has a
79 // MainMenu.xib with a given key equivalent set in Interface Builder and a
80 // some code that prints both the key equivalent that fires a menu item and
81 // the menu item's key equivalent and modifier masks. I then pasted those
82 // below. This was done with a US layout, unless otherwise noted. In the
83 // comments, "z" always means the physical "z" key on a US layout no matter
84 // what character that key produces.
92 item = MenuItem(@"", 0);
93 EXPECT_TRUE([item isEnabled]);
96 key = KeyEvent(0x100, @"a", @"a", 0);
97 item = MenuItem(@"a", 0);
98 ExpectKeyFiresItem(key, item);
99 ExpectKeyDoesntFireItem(KeyEvent(0x20102, @"A", @"A", 0), item);
101 // Disabled menu item
102 key = KeyEvent(0x100, @"a", @"a", 0);
103 item = MenuItem(@"a", 0);
104 [item setEnabled:NO];
105 ExpectKeyDoesntFireItem(key, item, false);
108 key = KeyEvent(0x20102, @"A", @"A", 0);
109 item = MenuItem(@"A", 0);
110 ExpectKeyFiresItem(key, item);
111 ExpectKeyDoesntFireItem(KeyEvent(0x100, @"a", @"a", 0), item);
114 key = KeyEvent(0x1a012a, @"\u00c5", @"A", 0);
115 item = MenuItem(@"A", 0x180000);
116 ExpectKeyFiresItem(key, item);
119 key = KeyEvent(0x18012a, @"\u00e5", @"a", 0);
120 item = MenuItem(@"a", 0x180000);
121 ExpectKeyFiresItem(key, item);
124 key = KeyEvent(0x100110, @"=", @"=", 0x18);
125 item = MenuItem(@"=", 0x100000);
126 ExpectKeyFiresItem(key, item);
129 key = KeyEvent(0x12010a, @"=", @"+", 0x18);
130 item = MenuItem(@"+", 0x100000);
131 ExpectKeyFiresItem(key, item);
133 // Turns out Cocoa fires "+ 100108 + 18" if you hit cmd-= and the menu only
134 // has a cmd-+ shortcut. But that's transparent for |cr_firesForKeyEvent:|.
137 key = KeyEvent(0x40101, @"3", @"3", 0x14);
138 item = MenuItem(@"3", 0x40000);
139 ExpectKeyFiresItem(key, item);
142 key = KeyEvent(0, @"\r", @"\r", 0x24);
143 item = MenuItem(@"\r", 0);
144 ExpectKeyFiresItem(key, item);
147 key = KeyEvent(0x20102, @"\r", @"\r", 0x24);
148 item = MenuItem(@"\r", 0x20000);
149 ExpectKeyFiresItem(key, item);
152 ch = NSLeftArrowFunctionKey;
153 s = [NSString stringWithCharacters:&ch length:1];
154 key = KeyEvent(0xa20102, s, s, 0x7b);
155 item = MenuItem(s, 0x20000);
156 ExpectKeyFiresItem(key, item);
158 // shift-f1 (with a layout that needs the fn key down for f1)
159 ch = NSF1FunctionKey;
160 s = [NSString stringWithCharacters:&ch length:1];
161 key = KeyEvent(0x820102, s, s, 0x7a);
162 item = MenuItem(s, 0x20000);
163 ExpectKeyFiresItem(key, item);
166 // Turns out this doesn't fire.
167 key = KeyEvent(0x100, @"\e", @"\e", 0x35);
168 item = MenuItem(@"\e", 0);
169 ExpectKeyDoesntFireItem(key,item, false);
172 // Turns out this doesn't fire.
173 key = KeyEvent(0x20102, @"\e", @"\e", 0x35);
174 item = MenuItem(@"\e", 0x20000);
175 ExpectKeyDoesntFireItem(key,item, false);
178 key = KeyEvent(0x100108, @"\e", @"\e", 0x35);
179 item = MenuItem(@"\e", 0x100000);
180 ExpectKeyFiresItem(key, item);
183 key = KeyEvent(0x40101, @"\e", @"\e", 0x35);
184 item = MenuItem(@"\e", 0x40000);
185 ExpectKeyFiresItem(key, item);
187 // delete ("backspace")
188 key = KeyEvent(0x100, @"\x7f", @"\x7f", 0x33);
189 item = MenuItem(@"\x08", 0);
190 ExpectKeyFiresItem(key, item, false);
193 key = KeyEvent(0x20102, @"\x7f", @"\x7f", 0x33);
194 item = MenuItem(@"\x08", 0x20000);
195 ExpectKeyFiresItem(key, item, false);
197 // forwarddelete (fn-delete / fn-backspace)
198 ch = NSDeleteFunctionKey;
199 s = [NSString stringWithCharacters:&ch length:1];
200 key = KeyEvent(0x800100, s, s, 0x75);
201 item = MenuItem(@"\x7f", 0);
202 ExpectKeyFiresItem(key, item, false);
204 // shift-forwarddelete (shift-fn-delete / shift-fn-backspace)
205 ch = NSDeleteFunctionKey;
206 s = [NSString stringWithCharacters:&ch length:1];
207 key = KeyEvent(0x820102, s, s, 0x75);
208 item = MenuItem(@"\x7f", 0x20000);
209 ExpectKeyFiresItem(key, item, false);
212 ch = NSHomeFunctionKey;
213 s = [NSString stringWithCharacters:&ch length:1];
214 key = KeyEvent(0x800100, s, s, 0x73);
215 item = MenuItem(s, 0);
216 ExpectKeyFiresItem(key, item);
219 ch = NSLeftArrowFunctionKey;
220 s = [NSString stringWithCharacters:&ch length:1];
221 key = KeyEvent(0xb00108, s, s, 0x7b);
222 item = MenuItem(s, 0x100000);
223 ExpectKeyFiresItem(key, item);
225 // Hitting the "a" key with a russian keyboard layout -- does not fire
226 // a menu item that has "a" as key equiv.
227 key = KeyEvent(0x100, @"\u0444", @"\u0444", 0);
228 item = MenuItem(@"a", 0);
229 ExpectKeyDoesntFireItem(key,item);
231 // cmd-a on a russion layout -- fires for a menu item with cmd-a as key equiv.
232 key = KeyEvent(0x100108, @"a", @"\u0444", 0);
233 item = MenuItem(@"a", 0x100000);
234 ExpectKeyFiresItem(key, item, false);
236 // cmd-z on US layout
237 key = KeyEvent(0x100108, @"z", @"z", 6);
238 item = MenuItem(@"z", 0x100000);
239 ExpectKeyFiresItem(key, item);
241 // cmd-y on german layout (has same keycode as cmd-z on us layout, shouldn't
243 key = KeyEvent(0x100108, @"y", @"y", 6);
244 item = MenuItem(@"z", 0x100000);
245 ExpectKeyDoesntFireItem(key,item);
247 // cmd-z on german layout
248 key = KeyEvent(0x100108, @"z", @"z", 0x10);
249 item = MenuItem(@"z", 0x100000);
250 ExpectKeyFiresItem(key, item);
252 // fn-return (== enter)
253 key = KeyEvent(0x800100, @"\x3", @"\x3", 0x4c);
254 item = MenuItem(@"\r", 0);
255 ExpectKeyDoesntFireItem(key,item);
257 // cmd-z on dvorak layout (so that the key produces ';')
258 key = KeyEvent(0x100108, @";", @";", 6);
259 ExpectKeyDoesntFireItem(key, MenuItem(@"z", 0x100000));
260 ExpectKeyFiresItem(key, MenuItem(@";", 0x100000));
262 // cmd-z on dvorak qwerty layout (so that the key produces ';', but 'z' if
264 key = KeyEvent(0x100108, @"z", @";", 6);
265 ExpectKeyFiresItem(key, MenuItem(@"z", 0x100000), false);
266 ExpectKeyDoesntFireItem(key, MenuItem(@";", 0x100000), false);
268 // cmd-shift-z on dvorak layout (so that we get a ':')
269 key = KeyEvent(0x12010a, @";", @":", 6);
270 ExpectKeyFiresItem(key, MenuItem(@":", 0x100000));
271 ExpectKeyDoesntFireItem(key, MenuItem(@";", 0x100000));
273 // cmd-s with a serbian layout (just "s" produces something that looks a lot
274 // like "c" in some fonts, but is actually \u0441. cmd-s activates a menu item
275 // with key equivalent "s", not "c")
276 key = KeyEvent(0x100108, @"s", @"\u0441", 1);
277 ExpectKeyFiresItem(key, MenuItem(@"s", 0x100000), false);
278 ExpectKeyDoesntFireItem(key, MenuItem(@"c", 0x100000));
281 NSString* keyCodeToCharacter(NSUInteger keyCode,
282 EventModifiers modifiers,
283 TISInputSourceRef layout) {
284 CFDataRef uchr = (CFDataRef)TISGetInputSourceProperty(
285 layout, kTISPropertyUnicodeKeyLayoutData);
286 UCKeyboardLayout* keyLayout = (UCKeyboardLayout*)CFDataGetBytePtr(uchr);
288 UInt32 deadKeyState = 0;
289 OSStatus err = noErr;
290 UniCharCount maxStringLength = 4, actualStringLength;
291 UniChar unicodeString[4];
292 err = UCKeyTranslate(keyLayout,
297 kUCKeyTranslateNoDeadKeysBit,
302 assert(err == noErr);
304 CFStringRef temp = CFStringCreateWithCharacters(
305 kCFAllocatorDefault, unicodeString, 1);
306 return [(NSString*)temp autorelease];
309 TEST(NSMenuItemAdditionsTest, TestMOnDifferentLayouts) {
310 // There's one key -- "m" -- that has the same keycode on most keyboard
311 // layouts. This function tests a menu item with cmd-m as key equivalent
312 // can be fired on all layouts.
313 NSMenuItem* item = MenuItem(@"m", 0x100000);
315 NSDictionary* filter = [NSDictionary
316 dictionaryWithObject:(NSString*)kTISTypeKeyboardLayout
317 forKey:(NSString*)kTISPropertyInputSourceType];
319 // Docs say that including all layouts instead of just the active ones is
320 // slow, but there's no way around that.
321 NSArray* list = (NSArray*)TISCreateInputSourceList(
322 (CFDictionaryRef)filter, true);
323 for (id layout in list) {
324 TISInputSourceRef ref = (TISInputSourceRef)layout;
326 NSUInteger keyCode = 0x2e; // "m" on a US layout and most other layouts.
328 // On a few layouts, "m" has a different key code.
329 NSString* layoutId = (NSString*)TISGetInputSourceProperty(
330 ref, kTISPropertyInputSourceID);
331 if ([layoutId isEqualToString:@"com.apple.keylayout.Belgian"] ||
332 [layoutId isEqualToString:@"com.apple.keylayout.Italian"] ||
333 [layoutId isEqualToString:@"com.apple.keylayout.ABC-AZERTY"] ||
334 [layoutId hasPrefix:@"com.apple.keylayout.French"]) {
336 } else if ([layoutId isEqualToString:@"com.apple.keylayout.Turkish"]) {
338 } else if ([layoutId isEqualToString:@"com.apple.keylayout.Dvorak-Left"]) {
340 } else if ([layoutId isEqualToString:@"com.apple.keylayout.Dvorak-Right"]) {
344 EventModifiers modifiers = cmdKey >> 8;
345 NSString* chars = keyCodeToCharacter(keyCode, modifiers, ref);
346 NSString* charsIgnoringMods = keyCodeToCharacter(keyCode, 0, ref);
347 NSEvent* key = KeyEvent(0x100000, chars, charsIgnoringMods, keyCode);
348 ExpectKeyFiresItem(key, item, false);