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