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.
4
5#import "chrome/browser/ui/cocoa/nsmenuitem_additions.h"
6
7#include <Carbon/Carbon.h>
8
9#include <ostream>
10
11#include "base/mac/scoped_nsobject.h"
12#include "base/strings/sys_string_conversions.h"
13#include "testing/gtest/include/gtest/gtest.h"
14
15NSEvent* KeyEvent(const NSUInteger modifierFlags,
16                  NSString* chars,
17                  NSString* charsNoMods,
18                  const NSUInteger keyCode) {
19  return [NSEvent keyEventWithType:NSKeyDown
20                          location:NSZeroPoint
21                     modifierFlags:modifierFlags
22                         timestamp:0.0
23                      windowNumber:0
24                           context:nil
25                        characters:chars
26       charactersIgnoringModifiers:charsNoMods
27                         isARepeat:NO
28                           keyCode:keyCode];
29}
30
31NSMenuItem* MenuItem(NSString* equiv, NSUInteger mask) {
32  NSMenuItem* item = [[[NSMenuItem alloc] initWithTitle:@""
33                                                 action:NULL
34                                          keyEquivalent:@""] autorelease];
35  [item setKeyEquivalent:equiv];
36  [item setKeyEquivalentModifierMask:mask];
37  return item;
38}
39
40std::ostream& operator<<(std::ostream& out, NSObject* obj) {
41  return out << base::SysNSStringToUTF8([obj description]);
42}
43
44std::ostream& operator<<(std::ostream& out, NSMenuItem* item) {
45  return out << "NSMenuItem " << base::SysNSStringToUTF8([item keyEquivalent]);
46}
47
48void ExpectKeyFiresItemEq(bool result, NSEvent* key, NSMenuItem* item,
49    bool compareCocoa) {
50  EXPECT_EQ(result, [item cr_firesForKeyEvent:key]) << key << '\n' << item;
51
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
57  // oracle function.
58  if (compareCocoa) {
59    base::scoped_nsobject<NSMenu> menu([[NSMenu alloc] initWithTitle:@"Menu!"]);
60    [menu setAutoenablesItems:NO];
61    EXPECT_FALSE([menu performKeyEquivalent:key]);
62    [menu addItem:item];
63    EXPECT_EQ(result, [menu performKeyEquivalent:key]) << key << '\n' << item;
64  }
65}
66
67void ExpectKeyFiresItem(
68    NSEvent* key, NSMenuItem* item, bool compareCocoa = true) {
69  ExpectKeyFiresItemEq(true, key, item, compareCocoa);
70}
71
72void ExpectKeyDoesntFireItem(
73    NSEvent* key, NSMenuItem* item, bool compareCocoa = true) {
74  ExpectKeyFiresItemEq(false, key, item, compareCocoa);
75}
76
77TEST(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.
85
86  NSMenuItem* item;
87  NSEvent* key;
88  unichar ch;
89  NSString* s;
90
91  // Sanity
92  item = MenuItem(@"", 0);
93  EXPECT_TRUE([item isEnabled]);
94
95  // a
96  key = KeyEvent(0x100, @"a", @"a", 0);
97  item = MenuItem(@"a", 0);
98  ExpectKeyFiresItem(key, item);
99  ExpectKeyDoesntFireItem(KeyEvent(0x20102, @"A", @"A", 0), item);
100
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);
106
107  // shift-a
108  key = KeyEvent(0x20102, @"A", @"A", 0);
109  item = MenuItem(@"A", 0);
110  ExpectKeyFiresItem(key, item);
111  ExpectKeyDoesntFireItem(KeyEvent(0x100, @"a", @"a", 0), item);
112
113  // cmd-opt-shift-a
114  key = KeyEvent(0x1a012a, @"\u00c5", @"A", 0);
115  item = MenuItem(@"A", 0x180000);
116  ExpectKeyFiresItem(key, item);
117
118  // cmd-opt-a
119  key = KeyEvent(0x18012a, @"\u00e5", @"a", 0);
120  item = MenuItem(@"a", 0x180000);
121  ExpectKeyFiresItem(key, item);
122
123  // cmd-=
124  key = KeyEvent(0x100110, @"=", @"=", 0x18);
125  item = MenuItem(@"=", 0x100000);
126  ExpectKeyFiresItem(key, item);
127
128  // cmd-shift-=
129  key = KeyEvent(0x12010a, @"=", @"+", 0x18);
130  item = MenuItem(@"+", 0x100000);
131  ExpectKeyFiresItem(key, item);
132
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:|.
135
136  // ctrl-3
137  key = KeyEvent(0x40101, @"3", @"3", 0x14);
138  item = MenuItem(@"3", 0x40000);
139  ExpectKeyFiresItem(key, item);
140
141  // return
142  key = KeyEvent(0, @"\r", @"\r", 0x24);
143  item = MenuItem(@"\r", 0);
144  ExpectKeyFiresItem(key, item);
145
146  // shift-return
147  key = KeyEvent(0x20102, @"\r", @"\r", 0x24);
148  item = MenuItem(@"\r", 0x20000);
149  ExpectKeyFiresItem(key, item);
150
151  // shift-left
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);
157
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);
164
165  // esc
166  // Turns out this doesn't fire.
167  key = KeyEvent(0x100, @"\e", @"\e", 0x35);
168  item = MenuItem(@"\e", 0);
169  ExpectKeyDoesntFireItem(key,item, false);
170
171  // shift-esc
172  // Turns out this doesn't fire.
173  key = KeyEvent(0x20102, @"\e", @"\e", 0x35);
174  item = MenuItem(@"\e", 0x20000);
175  ExpectKeyDoesntFireItem(key,item, false);
176
177  // cmd-esc
178  key = KeyEvent(0x100108, @"\e", @"\e", 0x35);
179  item = MenuItem(@"\e", 0x100000);
180  ExpectKeyFiresItem(key, item);
181
182  // ctrl-esc
183  key = KeyEvent(0x40101, @"\e", @"\e", 0x35);
184  item = MenuItem(@"\e", 0x40000);
185  ExpectKeyFiresItem(key, item);
186
187  // delete ("backspace")
188  key = KeyEvent(0x100, @"\x7f", @"\x7f", 0x33);
189  item = MenuItem(@"\x08", 0);
190  ExpectKeyFiresItem(key, item, false);
191
192  // shift-delete
193  key = KeyEvent(0x20102, @"\x7f", @"\x7f", 0x33);
194  item = MenuItem(@"\x08", 0x20000);
195  ExpectKeyFiresItem(key, item, false);
196
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);
203
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);
210
211  // fn-left
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);
217
218  // cmd-left
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);
224
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);
230
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);
235
236  // cmd-z on US layout
237  key = KeyEvent(0x100108, @"z", @"z", 6);
238  item = MenuItem(@"z", 0x100000);
239  ExpectKeyFiresItem(key, item);
240
241  // cmd-y on german layout (has same keycode as cmd-z on us layout, shouldn't
242  // fire).
243  key = KeyEvent(0x100108, @"y", @"y", 6);
244  item = MenuItem(@"z", 0x100000);
245  ExpectKeyDoesntFireItem(key,item);
246
247  // cmd-z on german layout
248  key = KeyEvent(0x100108, @"z", @"z", 0x10);
249  item = MenuItem(@"z", 0x100000);
250  ExpectKeyFiresItem(key, item);
251
252  // fn-return (== enter)
253  key = KeyEvent(0x800100, @"\x3", @"\x3", 0x4c);
254  item = MenuItem(@"\r", 0);
255  ExpectKeyDoesntFireItem(key,item);
256
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));
261
262  // cmd-z on dvorak qwerty layout (so that the key produces ';', but 'z' if
263  // cmd is down)
264  key = KeyEvent(0x100108, @"z", @";", 6);
265  ExpectKeyFiresItem(key, MenuItem(@"z", 0x100000), false);
266  ExpectKeyDoesntFireItem(key, MenuItem(@";", 0x100000), false);
267
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));
272
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));
279}
280
281NSString* keyCodeToCharacter(NSUInteger keyCode,
282                             EventModifiers modifiers,
283                             TISInputSourceRef layout) {
284  CFDataRef uchr = (CFDataRef)TISGetInputSourceProperty(
285      layout, kTISPropertyUnicodeKeyLayoutData);
286  UCKeyboardLayout* keyLayout = (UCKeyboardLayout*)CFDataGetBytePtr(uchr);
287
288  UInt32 deadKeyState = 0;
289  OSStatus err = noErr;
290  UniCharCount maxStringLength = 4, actualStringLength;
291  UniChar unicodeString[4];
292  err = UCKeyTranslate(keyLayout,
293      (UInt16)keyCode,
294      kUCKeyActionDown,
295      modifiers,
296      LMGetKbdType(),
297      kUCKeyTranslateNoDeadKeysBit,
298      &deadKeyState,
299      maxStringLength,
300      &actualStringLength,
301      unicodeString);
302  assert(err == noErr);
303
304  CFStringRef temp = CFStringCreateWithCharacters(
305      kCFAllocatorDefault, unicodeString, 1);
306  return [(NSString*)temp autorelease];
307}
308
309TEST(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);
314
315  NSDictionary* filter = [NSDictionary
316    dictionaryWithObject:(NSString*)kTISTypeKeyboardLayout
317                  forKey:(NSString*)kTISPropertyInputSourceType];
318
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;
325
326    NSUInteger keyCode = 0x2e;  // "m" on a US layout and most other layouts.
327
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 hasPrefix:@"com.apple.keylayout.French"]) {
334      keyCode = 0x29;
335    } else if ([layoutId isEqualToString:@"com.apple.keylayout.Turkish"]) {
336      keyCode = 0x28;
337    } else if ([layoutId isEqualToString:@"com.apple.keylayout.Dvorak-Left"]) {
338      keyCode = 0x16;
339    } else if ([layoutId isEqualToString:@"com.apple.keylayout.Dvorak-Right"]) {
340      keyCode = 0x1a;
341    }
342
343    EventModifiers modifiers = cmdKey >> 8;
344    NSString* chars = keyCodeToCharacter(keyCode, modifiers, ref);
345    NSString* charsIgnoringMods = keyCodeToCharacter(keyCode, 0, ref);
346    NSEvent* key = KeyEvent(0x100000, chars, charsIgnoringMods, keyCode);
347    ExpectKeyFiresItem(key, item, false);
348  }
349  CFRelease(list);
350}
351