• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
45namespace {
46
47// From
48// http://stackoverflow.com/questions/1597383/cgeventtimestamp-to-nsdate
49// Which credits Apple sample code for this routine.
50uint64_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
72NSTimeInterval TimeIntervalSinceSystemStartup() {
73  return UpTimeInNanoseconds() / 1000000000.0;
74}
75
76// Creates and returns an autoreleased key event.
77NSEvent* 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.
123void 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.
192class 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.
216NSPoint g_mouse_location = { 0, 0 };
217
218}  // anonymous namespace
219
220
221namespace ui_controls {
222
223bool 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).
236bool 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
264bool 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.
272bool 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
300bool SendMouseEvents(MouseButton type, int state) {
301  return SendMouseEventsNotifyWhenDone(type, state, NULL);
302}
303
304bool 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
356bool SendMouseClick(MouseButton type) {
357 return SendMouseEventsNotifyWhenDone(type, UP|DOWN, NULL);
358}
359
360void 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