OLD | NEW |
(Empty) | |
| 1 #import "chrome/browser/cocoa/nsmenuitem_additions.h" |
| 2 |
| 3 #include <iostream> |
| 4 #include <Carbon/Carbon.h> |
| 5 |
| 6 #include "base/scoped_nsobject.h" |
| 7 #include "base/sys_string_conversions.h" |
| 8 #include "testing/gtest/include/gtest/gtest.h" |
| 9 |
| 10 NSEvent* KeyEvent(const NSUInteger modifierFlags, |
| 11 NSString* chars, |
| 12 NSString* charsNoMods, |
| 13 const NSUInteger keyCode) { |
| 14 return [NSEvent keyEventWithType:NSKeyDown |
| 15 location:NSZeroPoint |
| 16 modifierFlags:modifierFlags |
| 17 timestamp:0.0 |
| 18 windowNumber:0 |
| 19 context:nil |
| 20 characters:chars |
| 21 charactersIgnoringModifiers:charsNoMods |
| 22 isARepeat:NO |
| 23 keyCode:keyCode]; |
| 24 } |
| 25 |
| 26 NSMenuItem* MenuItem(NSString* equiv, NSUInteger mask) { |
| 27 NSMenuItem* item = [[[NSMenuItem alloc] initWithTitle:@"" |
| 28 action:NULL |
| 29 keyEquivalent:@""] autorelease]; |
| 30 [item setKeyEquivalent:equiv]; |
| 31 [item setKeyEquivalentModifierMask:mask]; |
| 32 return item; |
| 33 } |
| 34 |
| 35 std::ostream& operator<<(std::ostream& out, NSObject* obj) { |
| 36 return out << base::SysNSStringToUTF8([obj description]); |
| 37 } |
| 38 |
| 39 std::ostream& operator<<(std::ostream& out, NSMenuItem* item) { |
| 40 return out << "NSMenuItem " << base::SysNSStringToUTF8([item keyEquivalent]); |
| 41 } |
| 42 |
| 43 void ExpectKeyFiresItemEq(bool result, NSEvent* key, NSMenuItem* item, |
| 44 bool compareCocoa ) { |
| 45 EXPECT_EQ(result, [item cr_firesForKeyEvent:key]) << key << '\n' << item; |
| 46 |
| 47 // Make sure that Cocoa does in fact agree with our expectations. However, |
| 48 // in some cases cocoa behaves weirdly (if you create e.g. a new event that |
| 49 // contains all fields of the event that you get when hitting cmd-a with a |
| 50 // russion keyboard layout, the copy won't fire a menu item that has cmd-a as |
| 51 // key equivalent, even though the original event would) and isn't a good |
| 52 // oracle function. |
| 53 if (compareCocoa) { |
| 54 scoped_nsobject<NSMenu> menu([[NSMenu alloc] initWithTitle:@"Menu!"]); |
| 55 [menu setAutoenablesItems:NO]; |
| 56 EXPECT_FALSE([menu performKeyEquivalent:key]); |
| 57 [menu addItem:item]; |
| 58 EXPECT_EQ(result, [menu performKeyEquivalent:key]) << key << '\n' << item; |
| 59 } |
| 60 } |
| 61 |
| 62 void ExpectKeyFiresItem( |
| 63 NSEvent* key, NSMenuItem* item, bool compareCocoa = true) { |
| 64 ExpectKeyFiresItemEq(true, key, item, compareCocoa); |
| 65 } |
| 66 |
| 67 void ExpectKeyDoesntFireItem( |
| 68 NSEvent* key, NSMenuItem* item, bool compareCocoa = true) { |
| 69 ExpectKeyFiresItemEq(false, key, item, compareCocoa); |
| 70 } |
| 71 |
| 72 TEST(NSMenuItemAdditionsTest, TestFiresForKeyEvent) { |
| 73 // These test cases were built by writing a small test app that has a |
| 74 // MainMenu.xib with a given key equivalent set in Interface Builder and a |
| 75 // some code that prints both the key equivalent that fires a menu item and |
| 76 // the menu item's key equivalent and modifier masks. I then pasted those |
| 77 // below. This was done with a US layout, unless otherwise noted. In the |
| 78 // comments, "z" always means the physical "z" key on a US layout no matter |
| 79 // what character that key produces. |
| 80 |
| 81 NSMenuItem* item; |
| 82 NSEvent* key; |
| 83 unichar ch; |
| 84 NSString* s; |
| 85 |
| 86 // Sanity |
| 87 item = MenuItem(@"", 0); |
| 88 EXPECT_TRUE([item isEnabled]); |
| 89 |
| 90 // a |
| 91 key = KeyEvent(0x100, @"a", @"a", 0); |
| 92 item = MenuItem(@"a", 0); |
| 93 ExpectKeyFiresItem(key, item); |
| 94 ExpectKeyDoesntFireItem(KeyEvent(0x20102, @"A", @"A", 0), item); |
| 95 |
| 96 // Disabled menu item |
| 97 key = KeyEvent(0x100, @"a", @"a", 0); |
| 98 item = MenuItem(@"a", 0); |
| 99 [item setEnabled:NO]; |
| 100 ExpectKeyDoesntFireItem(key, item, false); |
| 101 |
| 102 // shift-a |
| 103 key = KeyEvent(0x20102, @"A", @"A", 0); |
| 104 item = MenuItem(@"A", 0); |
| 105 ExpectKeyFiresItem(key, item); |
| 106 ExpectKeyDoesntFireItem(KeyEvent(0x100, @"a", @"a", 0), item); |
| 107 |
| 108 // cmd-opt-shift-a |
| 109 key = KeyEvent(0x1a012a, @"\u00c5", @"A", 0); |
| 110 item = MenuItem(@"A", 0x180000); |
| 111 ExpectKeyFiresItem(key, item); |
| 112 |
| 113 // cmd-opt-a |
| 114 key = KeyEvent(0x18012a, @"\u00e5", @"a", 0); |
| 115 item = MenuItem(@"a", 0x180000); |
| 116 ExpectKeyFiresItem(key, item); |
| 117 |
| 118 // cmd-= |
| 119 key = KeyEvent(0x100110, @"=", @"=", 0x18); |
| 120 item = MenuItem(@"=", 0x100000); |
| 121 ExpectKeyFiresItem(key, item); |
| 122 |
| 123 // cmd-shift-= |
| 124 key = KeyEvent(0x12010a, @"=", @"+", 0x18); |
| 125 item = MenuItem(@"+", 0x100000); |
| 126 ExpectKeyFiresItem(key, item); |
| 127 |
| 128 // Turns out Cocoa fires "+ 100108 + 18" if you hit cmd-= and the menu only |
| 129 // has a cmd-+ shortcut. But that's transparent for |cr_firesForKeyEvent:|. |
| 130 |
| 131 // ctrl-3 |
| 132 key = KeyEvent(0x40101, @"3", @"3", 0x14); |
| 133 item = MenuItem(@"3", 0x40000); |
| 134 ExpectKeyFiresItem(key, item); |
| 135 |
| 136 // return |
| 137 key = KeyEvent(0, @"\r", @"\r", 0x24); |
| 138 item = MenuItem(@"\r", 0); |
| 139 ExpectKeyFiresItem(key, item); |
| 140 |
| 141 // shift-return |
| 142 key = KeyEvent(0x20102, @"\r", @"\r", 0x24); |
| 143 item = MenuItem(@"\r", 0x20000); |
| 144 ExpectKeyFiresItem(key, item); |
| 145 |
| 146 // shift-left |
| 147 ch = NSLeftArrowFunctionKey; |
| 148 s = [NSString stringWithCharacters:&ch length:1]; |
| 149 key = KeyEvent(0xa20102, s, s, 0x7b); |
| 150 item = MenuItem(s, 0x20000); |
| 151 ExpectKeyFiresItem(key, item); |
| 152 |
| 153 // shift-f1 (with a layout that needs the fn key down for f1) |
| 154 ch = NSF1FunctionKey; |
| 155 s = [NSString stringWithCharacters:&ch length:1]; |
| 156 key = KeyEvent(0x820102, s, s, 0x7a); |
| 157 item = MenuItem(s, 0x20000); |
| 158 ExpectKeyFiresItem(key, item); |
| 159 |
| 160 // esc |
| 161 // Turns out this doesn't fire. |
| 162 key = KeyEvent(0x100, @"\e", @"\e", 0x35); |
| 163 item = MenuItem(@"\e", 0); |
| 164 ExpectKeyDoesntFireItem(key,item, false); |
| 165 |
| 166 // shift-esc |
| 167 // Turns out this doesn't fire. |
| 168 key = KeyEvent(0x20102, @"\e", @"\e", 0x35); |
| 169 item = MenuItem(@"\e", 0x20000); |
| 170 ExpectKeyDoesntFireItem(key,item, false); |
| 171 |
| 172 // cmd-esc |
| 173 key = KeyEvent(0x100108, @"\e", @"\e", 0x35); |
| 174 item = MenuItem(@"\e", 0x100000); |
| 175 ExpectKeyFiresItem(key, item); |
| 176 |
| 177 // ctrl-esc |
| 178 key = KeyEvent(0x40101, @"\e", @"\e", 0x35); |
| 179 item = MenuItem(@"\e", 0x40000); |
| 180 ExpectKeyFiresItem(key, item); |
| 181 |
| 182 // delete ("backspace") |
| 183 key = KeyEvent(0x100, @"\x7f", @"\x7f", 0x33); |
| 184 item = MenuItem(@"\x08", 0); |
| 185 ExpectKeyFiresItem(key, item, false); |
| 186 |
| 187 // shift-delete |
| 188 key = KeyEvent(0x20102, @"\x7f", @"\x7f", 0x33); |
| 189 item = MenuItem(@"\x08", 0x20000); |
| 190 ExpectKeyFiresItem(key, item, false); |
| 191 |
| 192 // forwarddelete (fn-delete / fn-backspace) |
| 193 ch = NSDeleteFunctionKey; |
| 194 s = [NSString stringWithCharacters:&ch length:1]; |
| 195 key = KeyEvent(0x800100, s, s, 0x75); |
| 196 item = MenuItem(@"\x7f", 0); |
| 197 ExpectKeyFiresItem(key, item, false); |
| 198 |
| 199 // shift-forwarddelete (shift-fn-delete / shift-fn-backspace) |
| 200 ch = NSDeleteFunctionKey; |
| 201 s = [NSString stringWithCharacters:&ch length:1]; |
| 202 key = KeyEvent(0x820102, s, s, 0x75); |
| 203 item = MenuItem(@"\x7f", 0x20000); |
| 204 ExpectKeyFiresItem(key, item, false); |
| 205 |
| 206 // fn-left |
| 207 ch = NSHomeFunctionKey; |
| 208 s = [NSString stringWithCharacters:&ch length:1]; |
| 209 key = KeyEvent(0x800100, s, s, 0x73); |
| 210 item = MenuItem(s, 0); |
| 211 ExpectKeyFiresItem(key, item); |
| 212 |
| 213 // cmd-left |
| 214 ch = NSLeftArrowFunctionKey; |
| 215 s = [NSString stringWithCharacters:&ch length:1]; |
| 216 key = KeyEvent(0xb00108, s, s, 0x7b); |
| 217 item = MenuItem(s, 0x100000); |
| 218 ExpectKeyFiresItem(key, item); |
| 219 |
| 220 // Hitting the "a" key with a russian keyboard layout -- does not fire |
| 221 // a menu item that has "a" as key equiv. |
| 222 key = KeyEvent(0x100, @"\u0444", @"\u0444", 0); |
| 223 item = MenuItem(@"a", 0); |
| 224 ExpectKeyDoesntFireItem(key,item); |
| 225 |
| 226 // cmd-a on a russion layout -- fires for a menu item with cmd-a as key equiv. |
| 227 key = KeyEvent(0x100108, @"a", @"\u0444", 0); |
| 228 item = MenuItem(@"a", 0x100000); |
| 229 ExpectKeyFiresItem(key, item, false); |
| 230 |
| 231 // cmd-z on US layout |
| 232 key = KeyEvent(0x100108, @"z", @"z", 6); |
| 233 item = MenuItem(@"z", 0x100000); |
| 234 ExpectKeyFiresItem(key, item); |
| 235 |
| 236 // cmd-y on german layout (has same keycode as cmd-z on us layout, shouldn't |
| 237 // fire). |
| 238 key = KeyEvent(0x100108, @"y", @"y", 6); |
| 239 item = MenuItem(@"z", 0x100000); |
| 240 ExpectKeyDoesntFireItem(key,item); |
| 241 |
| 242 // cmd-z on german layout |
| 243 key = KeyEvent(0x100108, @"z", @"z", 0x10); |
| 244 item = MenuItem(@"z", 0x100000); |
| 245 ExpectKeyFiresItem(key, item); |
| 246 |
| 247 // fn-return (== enter) |
| 248 key = KeyEvent(0x800100, @"\x3", @"\x3", 0x4c); |
| 249 item = MenuItem(@"\r", 0); |
| 250 ExpectKeyDoesntFireItem(key,item); |
| 251 |
| 252 // cmd-z on dvorak layout (so that the key produces ';') |
| 253 key = KeyEvent(0x100108, @";", @";", 6); |
| 254 ExpectKeyDoesntFireItem(key, MenuItem(@"z", 0x100000)); |
| 255 ExpectKeyFiresItem(key, MenuItem(@";", 0x100000)); |
| 256 |
| 257 // cmd-z on dvorak qwerty layout (so that the key produces ';', but 'z' if |
| 258 // cmd is down) |
| 259 key = KeyEvent(0x100108, @"z", @";", 6); |
| 260 ExpectKeyFiresItem(key, MenuItem(@"z", 0x100000), false); |
| 261 ExpectKeyDoesntFireItem(key, MenuItem(@";", 0x100000), false); |
| 262 |
| 263 // cmd-shift-z on dvorak layout (so that we get a ':') |
| 264 key = KeyEvent(0x12010a, @";", @":", 6); |
| 265 ExpectKeyFiresItem(key, MenuItem(@":", 0x100000)); |
| 266 ExpectKeyDoesntFireItem(key, MenuItem(@";", 0x100000)); |
| 267 |
| 268 // cmd-s with a serbian layout (just "s" produces something that looks a lot |
| 269 // like "c" in some fonts, but is actually \u0441. cmd-s activates a menu item |
| 270 // with key equivalent "s", not "c") |
| 271 key = KeyEvent(0x100108, @"s", @"\u0441", 1); |
| 272 ExpectKeyFiresItem(key, MenuItem(@"s", 0x100000), false); |
| 273 ExpectKeyDoesntFireItem(key, MenuItem(@"c", 0x100000)); |
| 274 } |
| 275 |
| 276 NSString* keyCodeToCharacter(NSUInteger keyCode, |
| 277 EventModifiers modifiers, |
| 278 TISInputSourceRef layout) { |
| 279 CFDataRef uchr = (CFDataRef)TISGetInputSourceProperty( |
| 280 layout, kTISPropertyUnicodeKeyLayoutData); |
| 281 UCKeyboardLayout* keyLayout = (UCKeyboardLayout*)CFDataGetBytePtr(uchr); |
| 282 |
| 283 UInt32 deadKeyState = 0; |
| 284 OSStatus err = noErr; |
| 285 UniCharCount maxStringLength = 4, actualStringLength; |
| 286 UniChar unicodeString[4]; |
| 287 err = UCKeyTranslate(keyLayout, |
| 288 (UInt16)keyCode, |
| 289 kUCKeyActionDown, |
| 290 modifiers, |
| 291 LMGetKbdType(), |
| 292 kUCKeyTranslateNoDeadKeysBit, |
| 293 &deadKeyState, |
| 294 maxStringLength, |
| 295 &actualStringLength, |
| 296 unicodeString); |
| 297 assert(err == noErr); |
| 298 |
| 299 CFStringRef temp = CFStringCreateWithCharacters( |
| 300 kCFAllocatorDefault, unicodeString, 1); |
| 301 return [(NSString*)temp autorelease]; |
| 302 } |
| 303 |
| 304 TEST(NSMenuItemAdditionsTest, TestMOnDifferentLayouts) { |
| 305 // There's one key -- "m" -- that has the same keycode on most keyboard |
| 306 //layouts. This function tests a menu item with cmd-m as key equivalent |
| 307 // can be fired on all layouts. |
| 308 NSMenuItem* item = MenuItem(@"m", 0x100000); |
| 309 |
| 310 NSDictionary* filter = [NSDictionary |
| 311 dictionaryWithObject:(NSString*)kTISTypeKeyboardLayout |
| 312 forKey:(NSString*)kTISPropertyInputSourceType]; |
| 313 |
| 314 // Docs say that including all layouts instead of just the active ones is |
| 315 // slow, but there's no way around that. |
| 316 NSArray* list = (NSArray*)TISCreateInputSourceList( |
| 317 (CFDictionaryRef)filter, true); |
| 318 for (id layout in list) { |
| 319 TISInputSourceRef ref = (TISInputSourceRef)layout; |
| 320 |
| 321 NSUInteger keyCode = 0x2e; // "m" on a us layout and most other layouts. |
| 322 |
| 323 // On a few layouts, "m" has a different key code. |
| 324 NSString* layoutId = (NSString*)TISGetInputSourceProperty( |
| 325 ref, kTISPropertyInputSourceID); |
| 326 if ([layoutId isEqualToString:@"com.apple.keylayout.Belgian"] || |
| 327 [layoutId isEqualToString:@"com.apple.keylayout.French"] || |
| 328 [layoutId isEqualToString:@"com.apple.keylayout.French-numerical"] || |
| 329 [layoutId isEqualToString:@"com.apple.keylayout.Italian"]) |
| 330 keyCode = 0x29; |
| 331 else if ([layoutId isEqualToString:@"com.apple.keylayout.Turkish"]) |
| 332 keyCode = 0x28; |
| 333 |
| 334 EventModifiers modifiers = cmdKey >> 8; |
| 335 NSString* chars = keyCodeToCharacter(keyCode, modifiers, ref); |
| 336 NSString* charsIgnoringMods = keyCodeToCharacter(keyCode, 0, ref); |
| 337 NSEvent* key = KeyEvent(0x100000, chars, charsIgnoringMods, keyCode); |
| 338 ExpectKeyFiresItem(key, item, false); |
| 339 } |
| 340 CFRelease(list); |
| 341 } |
OLD | NEW |