Home | History | Annotate | Download | only in cocoa
      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