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 // Stores the current mouse location on the screen. So that we can use it
     49 // when firing keyboard and mouse click events.
     50 NSPoint g_mouse_location = { 0, 0 };
     51 
     52 bool g_ui_controls_enabled = false;
     53 
     54 // From
     55 // http://stackoverflow.com/questions/1597383/cgeventtimestamp-to-nsdate
     56 // Which credits Apple sample code for this routine.
     57 uint64_t UpTimeInNanoseconds(void) {
     58   uint64_t time;
     59   uint64_t timeNano;
     60   static mach_timebase_info_data_t sTimebaseInfo;
     61 
     62   time = mach_absolute_time();
     63 
     64   // Convert to nanoseconds.
     65 
     66   // If this is the first time we've run, get the timebase.
     67   // We can use denom == 0 to indicate that sTimebaseInfo is
     68   // uninitialised because it makes no sense to have a zero
     69   // denominator is a fraction.
     70   if (sTimebaseInfo.denom == 0) {
     71     (void) mach_timebase_info(&sTimebaseInfo);
     72   }
     73 
     74   // This could overflow; for testing needs we probably don't care.
     75   timeNano = time * sTimebaseInfo.numer / sTimebaseInfo.denom;
     76   return timeNano;
     77 }
     78 
     79 NSTimeInterval TimeIntervalSinceSystemStartup() {
     80   return UpTimeInNanoseconds() / 1000000000.0;
     81 }
     82 
     83 // Creates and returns an autoreleased key event.
     84 NSEvent* SynthesizeKeyEvent(NSWindow* window,
     85                             bool keyDown,
     86                             ui::KeyboardCode keycode,
     87                             NSUInteger flags) {
     88   unichar character;
     89   unichar characterIgnoringModifiers;
     90   int macKeycode = ui::MacKeyCodeForWindowsKeyCode(
     91       keycode, flags, &character, &characterIgnoringModifiers);
     92 
     93   if (macKeycode < 0)
     94     return nil;
     95 
     96   NSString* charactersIgnoringModifiers =
     97       [[[NSString alloc] initWithCharacters:&characterIgnoringModifiers
     98                                      length:1]
     99         autorelease];
    100   NSString* characters =
    101       [[[NSString alloc] initWithCharacters:&character length:1] autorelease];
    102 
    103   NSEventType type = (keyDown ? NSKeyDown : NSKeyUp);
    104 
    105   // Modifier keys generate NSFlagsChanged event rather than
    106   // NSKeyDown/NSKeyUp events.
    107   if (keycode == ui::VKEY_CONTROL || keycode == ui::VKEY_SHIFT ||
    108       keycode == ui::VKEY_MENU || keycode == ui::VKEY_COMMAND)
    109     type = NSFlagsChanged;
    110 
    111   // For events other than mouse moved, [event locationInWindow] is
    112   // UNDEFINED if the event is not NSMouseMoved.  Thus, the (0,0)
    113   // location should be fine.
    114   NSEvent* event =
    115       [NSEvent keyEventWithType:type
    116                        location:NSZeroPoint
    117                   modifierFlags:flags
    118                       timestamp:TimeIntervalSinceSystemStartup()
    119                    windowNumber:[window windowNumber]
    120                         context:nil
    121                      characters:characters
    122     charactersIgnoringModifiers:charactersIgnoringModifiers
    123                       isARepeat:NO
    124                         keyCode:(unsigned short)macKeycode];
    125 
    126   return event;
    127 }
    128 
    129 // Creates the proper sequence of autoreleased key events for a key down + up.
    130 void SynthesizeKeyEventsSequence(NSWindow* window,
    131                                  ui::KeyboardCode keycode,
    132                                  bool control,
    133                                  bool shift,
    134                                  bool alt,
    135                                  bool command,
    136                                  std::vector<NSEvent*>* events) {
    137   NSEvent* event = nil;
    138   NSUInteger flags = 0;
    139   if (control) {
    140     flags |= NSControlKeyMask;
    141     event = SynthesizeKeyEvent(window, true, ui::VKEY_CONTROL, flags);
    142     DCHECK(event);
    143     events->push_back(event);
    144   }
    145   if (shift) {
    146     flags |= NSShiftKeyMask;
    147     event = SynthesizeKeyEvent(window, true, ui::VKEY_SHIFT, flags);
    148     DCHECK(event);
    149     events->push_back(event);
    150   }
    151   if (alt) {
    152     flags |= NSAlternateKeyMask;
    153     event = SynthesizeKeyEvent(window, true, ui::VKEY_MENU, flags);
    154     DCHECK(event);
    155     events->push_back(event);
    156   }
    157   if (command) {
    158     flags |= NSCommandKeyMask;
    159     event = SynthesizeKeyEvent(window, true, ui::VKEY_COMMAND, flags);
    160     DCHECK(event);
    161     events->push_back(event);
    162   }
    163 
    164   event = SynthesizeKeyEvent(window, true, keycode, flags);
    165   DCHECK(event);
    166   events->push_back(event);
    167   event = SynthesizeKeyEvent(window, false, keycode, flags);
    168   DCHECK(event);
    169   events->push_back(event);
    170 
    171   if (command) {
    172     flags &= ~NSCommandKeyMask;
    173     event = SynthesizeKeyEvent(window, false, ui::VKEY_COMMAND, flags);
    174     DCHECK(event);
    175     events->push_back(event);
    176   }
    177   if (alt) {
    178     flags &= ~NSAlternateKeyMask;
    179     event = SynthesizeKeyEvent(window, false, ui::VKEY_MENU, flags);
    180     DCHECK(event);
    181     events->push_back(event);
    182   }
    183   if (shift) {
    184     flags &= ~NSShiftKeyMask;
    185     event = SynthesizeKeyEvent(window, false, ui::VKEY_SHIFT, flags);
    186     DCHECK(event);
    187     events->push_back(event);
    188   }
    189   if (control) {
    190     flags &= ~NSControlKeyMask;
    191     event = SynthesizeKeyEvent(window, false, ui::VKEY_CONTROL, flags);
    192     DCHECK(event);
    193     events->push_back(event);
    194   }
    195 }
    196 
    197 // A helper function to watch for the event queue. The specific task will be
    198 // fired when there is no more event in the queue.
    199 void EventQueueWatcher(const base::Closure& task) {
    200   NSEvent* event = [NSApp nextEventMatchingMask:NSAnyEventMask
    201                                       untilDate:nil
    202                                          inMode:NSDefaultRunLoopMode
    203                                         dequeue:NO];
    204   // If there is still event in the queue, then we need to check again.
    205   if (event) {
    206     base::MessageLoop::current()->PostTask(
    207         FROM_HERE,
    208         base::Bind(&EventQueueWatcher, task));
    209   } else {
    210     base::MessageLoop::current()->PostTask(FROM_HERE, task);
    211   }
    212 }
    213 
    214 // Returns the NSWindow located at |g_mouse_location|. NULL if there is no
    215 // window there, or if the window located there is not owned by the application.
    216 // On Mac, unless dragging, mouse events are sent to the window under the
    217 // cursor. Note that the OS will ignore transparent windows and windows that
    218 // explicitly ignore mouse events.
    219 NSWindow* WindowAtCurrentMouseLocation() {
    220   NSInteger window_number = [NSWindow windowNumberAtPoint:g_mouse_location
    221                               belowWindowWithWindowNumber:0];
    222   return
    223       [[NSApplication sharedApplication] windowWithWindowNumber:window_number];
    224 }
    225 
    226 }  // namespace
    227 
    228 namespace ui_controls {
    229 
    230 void EnableUIControls() {
    231   g_ui_controls_enabled = true;
    232 }
    233 
    234 bool SendKeyPress(gfx::NativeWindow window,
    235                   ui::KeyboardCode key,
    236                   bool control,
    237                   bool shift,
    238                   bool alt,
    239                   bool command) {
    240   CHECK(g_ui_controls_enabled);
    241   return SendKeyPressNotifyWhenDone(window, key,
    242                                     control, shift, alt, command,
    243                                     base::Closure());
    244 }
    245 
    246 // Win and Linux implement a SendKeyPress() this as a
    247 // SendKeyPressAndRelease(), so we should as well (despite the name).
    248 bool SendKeyPressNotifyWhenDone(gfx::NativeWindow window,
    249                                 ui::KeyboardCode key,
    250                                 bool control,
    251                                 bool shift,
    252                                 bool alt,
    253                                 bool command,
    254                                 const base::Closure& task) {
    255   CHECK(g_ui_controls_enabled);
    256   DCHECK(base::MessageLoopForUI::IsCurrent());
    257 
    258   std::vector<NSEvent*> events;
    259   SynthesizeKeyEventsSequence(
    260       window, key, control, shift, alt, command, &events);
    261 
    262   // TODO(suzhe): Using [NSApplication postEvent:atStart:] here causes
    263   // BrowserKeyEventsTest.CommandKeyEvents to fail. See http://crbug.com/49270
    264   // But using [NSApplication sendEvent:] should be safe for keyboard events,
    265   // because until now, no code wants to retrieve the next event when handling
    266   // a keyboard event.
    267   for (std::vector<NSEvent*>::iterator iter = events.begin();
    268        iter != events.end(); ++iter)
    269     [[NSApplication sharedApplication] sendEvent:*iter];
    270 
    271   if (!task.is_null()) {
    272     base::MessageLoop::current()->PostTask(
    273         FROM_HERE, base::Bind(&EventQueueWatcher, task));
    274   }
    275 
    276   return true;
    277 }
    278 
    279 bool SendMouseMove(long x, long y) {
    280   CHECK(g_ui_controls_enabled);
    281   return SendMouseMoveNotifyWhenDone(x, y, base::Closure());
    282 }
    283 
    284 // Input position is in screen coordinates.  However, NSMouseMoved
    285 // events require them window-relative, so we adjust.  We *DO* flip
    286 // the coordinate space, so input events can be the same for all
    287 // platforms.  E.g. (0,0) is upper-left.
    288 bool SendMouseMoveNotifyWhenDone(long x, long y, const base::Closure& task) {
    289   CHECK(g_ui_controls_enabled);
    290   CGFloat screenHeight =
    291     [[[NSScreen screens] objectAtIndex:0] frame].size.height;
    292   g_mouse_location = NSMakePoint(x, screenHeight - y);  // flip!
    293 
    294   NSWindow* window = WindowAtCurrentMouseLocation();
    295 
    296   NSPoint pointInWindow = g_mouse_location;
    297   if (window)
    298     pointInWindow = [window convertScreenToBase:pointInWindow];
    299   NSTimeInterval timestamp = TimeIntervalSinceSystemStartup();
    300 
    301   NSEvent* event =
    302       [NSEvent mouseEventWithType:NSMouseMoved
    303                          location:pointInWindow
    304                     modifierFlags:0
    305                         timestamp:timestamp
    306                      windowNumber:[window windowNumber]
    307                           context:nil
    308                       eventNumber:0
    309                        clickCount:0
    310                          pressure:0.0];
    311   [[NSApplication sharedApplication] postEvent:event atStart:NO];
    312 
    313   if (!task.is_null()) {
    314     base::MessageLoop::current()->PostTask(
    315         FROM_HERE, base::Bind(&EventQueueWatcher, task));
    316   }
    317 
    318   return true;
    319 }
    320 
    321 bool SendMouseEvents(MouseButton type, int state) {
    322   CHECK(g_ui_controls_enabled);
    323   return SendMouseEventsNotifyWhenDone(type, state, base::Closure());
    324 }
    325 
    326 bool SendMouseEventsNotifyWhenDone(MouseButton type, int state,
    327                                    const base::Closure& task) {
    328   CHECK(g_ui_controls_enabled);
    329   // On windows it appears state can be (UP|DOWN).  It is unclear if
    330   // that'll happen here but prepare for it just in case.
    331   if (state == (UP|DOWN)) {
    332     return (SendMouseEventsNotifyWhenDone(type, DOWN, base::Closure()) &&
    333             SendMouseEventsNotifyWhenDone(type, UP, task));
    334   }
    335   NSEventType etype = 0;
    336   if (type == LEFT) {
    337     if (state == UP) {
    338       etype = NSLeftMouseUp;
    339     } else {
    340       etype = NSLeftMouseDown;
    341     }
    342   } else if (type == MIDDLE) {
    343     if (state == UP) {
    344       etype = NSOtherMouseUp;
    345     } else {
    346       etype = NSOtherMouseDown;
    347     }
    348   } else if (type == RIGHT) {
    349     if (state == UP) {
    350       etype = NSRightMouseUp;
    351     } else {
    352       etype = NSRightMouseDown;
    353     }
    354   } else {
    355     return false;
    356   }
    357   NSWindow* window = WindowAtCurrentMouseLocation();
    358   NSPoint pointInWindow = g_mouse_location;
    359   if (window)
    360     pointInWindow = [window convertScreenToBase:pointInWindow];
    361 
    362   NSEvent* event =
    363       [NSEvent mouseEventWithType:etype
    364                          location:pointInWindow
    365                     modifierFlags:0
    366                         timestamp:TimeIntervalSinceSystemStartup()
    367                      windowNumber:[window windowNumber]
    368                           context:nil
    369                       eventNumber:0
    370                        clickCount:1
    371                          pressure:(state == DOWN ? 1.0 : 0.0 )];
    372   [[NSApplication sharedApplication] postEvent:event atStart:NO];
    373 
    374   if (!task.is_null()) {
    375     base::MessageLoop::current()->PostTask(
    376         FROM_HERE, base::Bind(&EventQueueWatcher, task));
    377   }
    378 
    379   return true;
    380 }
    381 
    382 bool SendMouseClick(MouseButton type) {
    383   CHECK(g_ui_controls_enabled);
    384   return SendMouseEventsNotifyWhenDone(type, UP|DOWN, base::Closure());
    385 }
    386 
    387 void RunClosureAfterAllPendingUIEvents(const base::Closure& closure) {
    388   base::MessageLoop::current()->PostTask(
    389       FROM_HERE, base::Bind(&EventQueueWatcher, closure));
    390 }
    391 
    392 }  // namespace ui_controls
    393