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/memory/scoped_nsobject.h" 12 #include "base/sys_string_conversions.h" 13 #include "testing/gtest/include/gtest/gtest.h" 14 15 NSEvent* 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 31 NSMenuItem* 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 40 std::ostream& operator<<(std::ostream& out, NSObject* obj) { 41 return out << base::SysNSStringToUTF8([obj description]); 42 } 43 44 std::ostream& operator<<(std::ostream& out, NSMenuItem* item) { 45 return out << "NSMenuItem " << base::SysNSStringToUTF8([item keyEquivalent]); 46 } 47 48 void 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 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 67 void ExpectKeyFiresItem( 68 NSEvent* key, NSMenuItem* item, bool compareCocoa = true) { 69 ExpectKeyFiresItemEq(true, key, item, compareCocoa); 70 } 71 72 void ExpectKeyDoesntFireItem( 73 NSEvent* key, NSMenuItem* item, bool compareCocoa = true) { 74 ExpectKeyFiresItemEq(false, key, item, compareCocoa); 75 } 76 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. 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 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); 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 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); 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.French"] || 333 [layoutId isEqualToString:@"com.apple.keylayout.French-numerical"] || 334 [layoutId isEqualToString:@"com.apple.keylayout.Italian"]) { 335 keyCode = 0x29; 336 } else if ([layoutId isEqualToString:@"com.apple.keylayout.Turkish"]) { 337 keyCode = 0x28; 338 } else if ([layoutId isEqualToString:@"com.apple.keylayout.Dvorak-Left"]) { 339 keyCode = 0x16; 340 } else if ([layoutId isEqualToString:@"com.apple.keylayout.Dvorak-Right"]) { 341 keyCode = 0x1a; 342 } 343 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); 349 } 350 CFRelease(list); 351 } 352