Home | History | Annotate | Download | only in test
      1 // Copyright 2013 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 #include "ui/base/test/ui_controls.h"
      6 
      7 #import <Cocoa/Cocoa.h>
      8 #include <mach/mach_time.h>
      9 #include <vector>
     10 
     11 #include "base/bind.h"
     12 #include "base/callback.h"
     13 #include "base/message_loop/message_loop.h"
     14 #include "ui/events/keycodes/keyboard_code_conversion_mac.h"
     15 
     16 
     17 // Implementation details: We use [NSApplication sendEvent:] instead
     18 // of [NSApplication postEvent:atStart:] so that the event gets sent
     19 // immediately.  This lets us run the post-event task right
     20 // immediately as well.  Unfortunately I cannot subclass NSEvent (it's
     21 // probably a class cluster) to allow other easy answers.  For
     22 // example, if I could subclass NSEvent, I could run the Task in it's
     23 // dealloc routine (which necessarily happens after the event is
     24 // dispatched).  Unlike Linux, Mac does not have message loop
     25 // observer/notification.  Unlike windows, I cannot post non-events
     26 // into the event queue.  (I can post other kinds of tasks but can't
     27 // guarantee their order with regards to events).
     28 
     29 // But [NSApplication sendEvent:] causes a problem when sending mouse click
     30 // events. Because in order to handle mouse drag, when processing a mouse
     31 // click event, the application may want to retrieve the next event
     32 // synchronously by calling NSApplication's nextEventMatchingMask method.
     33 // In this case, [NSApplication sendEvent:] causes deadlock.
     34 // So we need to use [NSApplication postEvent:atStart:] for mouse click
     35 // events. In order to notify the caller correctly after all events has been
     36 // processed, we setup a task to watch for the event queue time to time and
     37 // notify the caller as soon as there is no event in the queue.
     38 //
     39 // TODO(suzhe):
     40 // 1. Investigate why using [NSApplication postEvent:atStart:] for keyboard
     41 //    events causes BrowserKeyEventsTest.CommandKeyEvents to fail.
     42 //    See http://crbug.com/49270
     43 // 2. On OSX 10.6, [NSEvent addLocalMonitorForEventsMatchingMask:handler:] may
     44 //    be used, so that we don't need to poll the event queue time to time.
     45 
     46 namespace {
     47 
     48 // From
     49 // http://stackoverflow.com/questions/1597383/cgeventtimestamp-to-nsdate
     50 // Which credits Apple sample code for this routine.
     51 uint64_t UpTimeInNanoseconds(void) {
     52   uint64_t time;
     53   uint64_t timeNano;
     54   static mach_timebase_info_data_t sTimebaseInfo;
     55 
     56   time = mach_absolute_time();
     57 
     58   // Convert to nanoseconds.
     59 
     60   // If this is the first time we've run, get the timebase.
     61   // We can use denom == 0 to indicate that sTimebaseInfo is
     62   // uninitialised because it makes no sense to have a zero
     63   // denominator is a fraction.
     64   if (sTimebaseInfo.denom == 0) {
     65     (void) mach_timebase_info(&sTimebaseInfo);
     66   }
     67 
     68   // This could overflow; for testing needs we probably don't care.
     69   timeNano = time * sTimebaseInfo.numer / sTimebaseInfo.denom;
     70   return timeNano;
     71 }
     72 
     73 NSTimeInterval TimeIntervalSinceSystemStartup() {
     74   return UpTimeInNanoseconds() / 1000000000.0;
     75 }
     76 
     77 // Creates and returns an autoreleased key event.
     78 NSEvent* SynthesizeKeyEvent(NSWindow* window,
     79                             bool keyDown,
     80                             ui::KeyboardCode keycode,
     81                             NSUInteger flags) {
     82   unichar character;
     83   unichar characterIgnoringModifiers;
     84   int macKeycode = ui::MacKeyCodeForWindowsKeyCode(
     85       keycode, flags, &character, &characterIgnoringModifiers);
     86 
     87   if (macKeycode < 0)
     88     return nil;
     89 
     90   NSString* charactersIgnoringModifiers =
     91       [[[NSString alloc] initWithCharacters:&characterIgnoringModifiers
     92                                      length:1]
     93         autorelease];
     94   NSString* characters =
     95       [[[NSString alloc] initWithCharacters:&character length:1] autorelease];
     96 
     97   NSEventType type = (keyDown ? NSKeyDown : NSKeyUp);
     98 
     99   // Modifier keys generate NSFlagsChanged event rather than
    100   // NSKeyDown/NSKeyUp events.
    101   if (keycode == ui::VKEY_CONTROL || keycode == ui::VKEY_SHIFT ||
    102       keycode == ui::VKEY_MENU || keycode == ui::VKEY_COMMAND)
    103     type = NSFlagsChanged;
    104 
    105   // For events other than mouse moved, [event locationInWindow] is
    106   // UNDEFINED if the event is not NSMouseMoved.  Thus, the (0,0)
    107   // location should be fine.
    108   NSEvent* event =
    109       [NSEvent keyEventWithType:type
    110                        location:NSZeroPoint
    111                   modifierFlags:flags
    112                       timestamp:TimeIntervalSinceSystemStartup()
    113                    windowNumber:[window windowNumber]
    114                         context:nil
    115                      characters:characters
    116     charactersIgnoringModifiers:charactersIgnoringModifiers
    117                       isARepeat:NO
    118                         keyCode:(unsigned short)macKeycode];
    119 
    120   return event;
    121 }
    122 
    123 // Creates the proper sequence of autoreleased key events for a key down + up.
    124 void SynthesizeKeyEventsSequence(NSWindow* window,
    125                                  ui::KeyboardCode keycode,
    126                                  bool control,
    127                                  bool shift,
    128                                  bool alt,
    129                                  bool command,
    130                                  std::vector<NSEvent*>* events) {
    131   NSEvent* event = nil;
    132   NSUInteger flags = 0;
    133   if (control) {
    134     flags |= NSControlKeyMask;
    135     event = SynthesizeKeyEvent(window, true, ui::VKEY_CONTROL, flags);
    136     DCHECK(event);
    137     events->push_back(event);
    138   }
    139   if (shift) {
    140     flags |= NSShiftKeyMask;
    141     event = SynthesizeKeyEvent(window, true, ui::VKEY_SHIFT, flags);
    142     DCHECK(event);
    143     events->push_back(event);
    144   }
    145   if (alt) {
    146     flags |= NSAlternateKeyMask;
    147     event = SynthesizeKeyEvent(window, true, ui::VKEY_MENU, flags);
    148     DCHECK(event);
    149     events->push_back(event);
    150   }
    151   if (command) {
    152     flags |= NSCommandKeyMask;
    153     event = SynthesizeKeyEvent(window, true, ui::VKEY_COMMAND, flags);
    154     DCHECK(event);
    155     events->push_back(event);
    156   }
    157 
    158   event = SynthesizeKeyEvent(window, true, keycode, flags);
    159   DCHECK(event);
    160   events->push_back(event);
    161   event = SynthesizeKeyEvent(window, false, keycode, flags);
    162   DCHECK(event);
    163   events->push_back(event);
    164 
    165   if (command) {
    166     flags &= ~NSCommandKeyMask;
    167     event = SynthesizeKeyEvent(window, false, ui::VKEY_COMMAND, flags);
    168     DCHECK(event);
    169     events->push_back(event);
    170   }
    171   if (alt) {
    172     flags &= ~NSAlternateKeyMask;
    173     event = SynthesizeKeyEvent(window, false, ui::VKEY_MENU, flags);
    174     DCHECK(event);
    175     events->push_back(event);
    176   }
    177   if (shift) {
    178     flags &= ~NSShiftKeyMask;
    179     event = SynthesizeKeyEvent(window, false, ui::VKEY_SHIFT, flags);
    180     DCHECK(event);
    181     events->push_back(event);
    182   }
    183   if (control) {
    184     flags &= ~NSControlKeyMask;
    185     event = SynthesizeKeyEvent(window, false, ui::VKEY_CONTROL, flags);
    186     DCHECK(event);
    187     events->push_back(event);
    188   }
    189 }
    190 
    191 // A helper function to watch for the event queue. The specific task will be
    192 // fired when there is no more event in the queue.
    193 void EventQueueWatcher(const base::Closure& task) {
    194   NSEvent* event = [NSApp nextEventMatchingMask:NSAnyEventMask
    195                                       untilDate:nil
    196                                          inMode:NSDefaultRunLoopMode
    197                                         dequeue:NO];
    198   // If there is still event in the queue, then we need to check again.
    199   if (event) {
    200     base::MessageLoop::current()->PostTask(
    201         FROM_HERE,
    202         base::Bind(&EventQueueWatcher, task));
    203   } else {
    204     base::MessageLoop::current()->PostTask(FROM_HERE, task);
    205   }
    206 }
    207 
    208 // Stores the current mouse location on the screen. So that we can use it
    209 // when firing keyboard and mouse click events.
    210 NSPoint g_mouse_location = { 0, 0 };
    211 
    212 bool g_ui_controls_enabled = false;
    213 
    214 }  // namespace
    215 
    216 namespace ui_controls {
    217 
    218 void EnableUIControls() {
    219   g_ui_controls_enabled = true;
    220 }
    221 
    222 bool SendKeyPress(gfx::NativeWindow window,
    223                   ui::KeyboardCode key,
    224                   bool control,
    225                   bool shift,
    226                   bool alt,
    227                   bool command) {
    228   CHECK(g_ui_controls_enabled);
    229   return SendKeyPressNotifyWhenDone(window, key,
    230                                     control, shift, alt, command,
    231                                     base::Closure());
    232 }
    233 
    234 // Win and Linux implement a SendKeyPress() this as a
    235 // SendKeyPressAndRelease(), so we should as well (despite the name).
    236 bool SendKeyPressNotifyWhenDone(gfx::NativeWindow window,
    237                                 ui::KeyboardCode key,
    238                                 bool control,
    239                                 bool shift,
    240                                 bool alt,
    241                                 bool command,
    242                                 const base::Closure& task) {
    243   CHECK(g_ui_controls_enabled);
    244   DCHECK(base::MessageLoopForUI::IsCurrent());
    245 
    246   std::vector<NSEvent*> events;
    247   SynthesizeKeyEventsSequence(
    248       window, key, control, shift, alt, command, &events);
    249 
    250   // TODO(suzhe): Using [NSApplication postEvent:atStart:] here causes
    251   // BrowserKeyEventsTest.CommandKeyEvents to fail. See http://crbug.com/49270
    252   // But using [NSApplication sendEvent:] should be safe for keyboard events,
    253   // because until now, no code wants to retrieve the next event when handling
    254   // a keyboard event.
    255   for (std::vector<NSEvent*>::iterator iter = events.begin();
    256        iter != events.end(); ++iter)
    257     [[NSApplication sharedApplication] sendEvent:*iter];
    258 
    259   if (!task.is_null()) {
    260     base::MessageLoop::current()->PostTask(
    261         FROM_HERE, base::Bind(&EventQueueWatcher, task));
    262   }
    263 
    264   return true;
    265 }
    266 
    267 bool SendMouseMove(long x, long y) {
    268   CHECK(g_ui_controls_enabled);
    269   return SendMouseMoveNotifyWhenDone(x, y, base::Closure());
    270 }
    271 
    272 // Input position is in screen coordinates.  However, NSMouseMoved
    273 // events require them window-relative, so we adjust.  We *DO* flip
    274 // the coordinate space, so input events can be the same for all
    275 // platforms.  E.g. (0,0) is upper-left.
    276 bool SendMouseMoveNotifyWhenDone(long x, long y, const base::Closure& task) {
    277   CHECK(g_ui_controls_enabled);
    278   NSWindow* window = [[NSApplication sharedApplication] keyWindow];
    279   CGFloat screenHeight =
    280     [[[NSScreen screens] objectAtIndex:0] frame].size.height;
    281   g_mouse_location = NSMakePoint(x, screenHeight - y);  // flip!
    282   NSPoint pointInWindow = g_mouse_location;
    283   if (window)
    284     pointInWindow = [window convertScreenToBase:pointInWindow];
    285   NSTimeInterval timestamp = TimeIntervalSinceSystemStartup();
    286 
    287   NSEvent* event =
    288       [NSEvent mouseEventWithType:NSMouseMoved
    289                          location:pointInWindow
    290                     modifierFlags:0
    291                         timestamp:timestamp
    292                      windowNumber:[window windowNumber]
    293                           context:nil
    294                       eventNumber:0
    295                        clickCount:0
    296                          pressure:0.0];
    297   [[NSApplication sharedApplication] postEvent:event atStart:NO];
    298 
    299   if (!task.is_null()) {
    300     base::MessageLoop::current()->PostTask(
    301         FROM_HERE, base::Bind(&EventQueueWatcher, task));
    302   }
    303 
    304   return true;
    305 }
    306 
    307 bool SendMouseEvents(MouseButton type, int state) {
    308   CHECK(g_ui_controls_enabled);
    309   return SendMouseEventsNotifyWhenDone(type, state, base::Closure());
    310 }
    311 
    312 bool SendMouseEventsNotifyWhenDone(MouseButton type, int state,
    313                                    const base::Closure& task) {
    314   CHECK(g_ui_controls_enabled);
    315   // On windows it appears state can be (UP|DOWN).  It is unclear if
    316   // that'll happen here but prepare for it just in case.
    317   if (state == (UP|DOWN)) {
    318     return (SendMouseEventsNotifyWhenDone(type, DOWN, base::Closure()) &&
    319             SendMouseEventsNotifyWhenDone(type, UP, task));
    320   }
    321   NSEventType etype = 0;
    322   if (type == LEFT) {
    323     if (state == UP) {
    324       etype = NSLeftMouseUp;
    325     } else {
    326       etype = NSLeftMouseDown;
    327     }
    328   } else if (type == MIDDLE) {
    329     if (state == UP) {
    330       etype = NSOtherMouseUp;
    331     } else {
    332       etype = NSOtherMouseDown;
    333     }
    334   } else if (type == RIGHT) {
    335     if (state == UP) {
    336       etype = NSRightMouseUp;
    337     } else {
    338       etype = NSRightMouseDown;
    339     }
    340   } else {
    341     return false;
    342   }
    343   NSWindow* window = [[NSApplication sharedApplication] keyWindow];
    344   NSPoint pointInWindow = g_mouse_location;
    345   if (window)
    346     pointInWindow = [window convertScreenToBase:pointInWindow];
    347 
    348   NSEvent* event =
    349       [NSEvent mouseEventWithType:etype
    350                          location:pointInWindow
    351                     modifierFlags:0
    352                         timestamp:TimeIntervalSinceSystemStartup()
    353                      windowNumber:[window windowNumber]
    354                           context:nil
    355                       eventNumber:0
    356                        clickCount:1
    357                          pressure:(state == DOWN ? 1.0 : 0.0 )];
    358   [[NSApplication sharedApplication] postEvent:event atStart:NO];
    359 
    360   if (!task.is_null()) {
    361     base::MessageLoop::current()->PostTask(
    362         FROM_HERE, base::Bind(&EventQueueWatcher, task));
    363   }
    364 
    365   return true;
    366 }
    367 
    368 bool SendMouseClick(MouseButton type) {
    369   CHECK(g_ui_controls_enabled);
    370   return SendMouseEventsNotifyWhenDone(type, UP|DOWN, base::Closure());
    371 }
    372 
    373 }  // namespace ui_controls
    374