ui_controls_mac.mm revision 72a454cd3513ac24fbdd0e0cb9ad70b86a99b801
1// Copyright (c) 2010 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 "chrome/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 = [[NSScreen mainScreen] frame].size.height;
275  g_mouse_location = NSMakePoint(x, screenHeight - y);  // flip!
276  NSPoint pointInWindow = g_mouse_location;
277  if (window)
278    pointInWindow = [window convertScreenToBase:pointInWindow];
279  NSTimeInterval timestamp = TimeIntervalSinceSystemStartup();
280
281  NSEvent* event =
282      [NSEvent mouseEventWithType:NSMouseMoved
283                         location:pointInWindow
284                    modifierFlags:0
285                        timestamp:timestamp
286                     windowNumber:[window windowNumber]
287                          context:nil
288                      eventNumber:0
289                       clickCount:0
290                         pressure:0.0];
291  [[NSApplication sharedApplication] postEvent:event atStart:NO];
292
293  if (task)
294    MessageLoop::current()->PostTask(FROM_HERE, new EventQueueWatcher(task));
295
296  return true;
297}
298
299bool SendMouseEvents(MouseButton type, int state) {
300  return SendMouseEventsNotifyWhenDone(type, state, NULL);
301}
302
303bool SendMouseEventsNotifyWhenDone(MouseButton type, int state, Task* task) {
304  // On windows it appears state can be (UP|DOWN).  It is unclear if
305  // that'll happen here but prepare for it just in case.
306  if (state == (UP|DOWN)) {
307    return (SendMouseEventsNotifyWhenDone(type, DOWN, NULL) &&
308            SendMouseEventsNotifyWhenDone(type, UP, task));
309  }
310  NSEventType etype = 0;
311  if (type == LEFT) {
312    if (state == UP) {
313      etype = NSLeftMouseUp;
314    } else {
315      etype = NSLeftMouseDown;
316    }
317  } else if (type == MIDDLE) {
318    if (state == UP) {
319      etype = NSOtherMouseUp;
320    } else {
321      etype = NSOtherMouseDown;
322    }
323  } else if (type == RIGHT) {
324    if (state == UP) {
325      etype = NSRightMouseUp;
326    } else {
327      etype = NSRightMouseDown;
328    }
329  } else {
330    return false;
331  }
332  NSWindow* window = [[NSApplication sharedApplication] keyWindow];
333  NSPoint pointInWindow = g_mouse_location;
334  if (window)
335    pointInWindow = [window convertScreenToBase:pointInWindow];
336
337  NSEvent* event =
338      [NSEvent mouseEventWithType:etype
339                         location:pointInWindow
340                    modifierFlags:0
341                        timestamp:TimeIntervalSinceSystemStartup()
342                     windowNumber:[window windowNumber]
343                          context:nil
344                      eventNumber:0
345                       clickCount:1
346                         pressure:(state == DOWN ? 1.0 : 0.0 )];
347  [[NSApplication sharedApplication] postEvent:event atStart:NO];
348
349  if (task)
350    MessageLoop::current()->PostTask(FROM_HERE, new EventQueueWatcher(task));
351
352  return true;
353}
354
355bool SendMouseClick(MouseButton type) {
356 return SendMouseEventsNotifyWhenDone(type, UP|DOWN, NULL);
357}
358
359void MoveMouseToCenterAndPress(
360    NSView* view,
361    MouseButton button,
362    int state,
363    Task* task) {
364  DCHECK(view);
365  NSWindow* window = [view window];
366  DCHECK(window);
367  NSScreen* screen = [window screen];
368  DCHECK(screen);
369
370  // Converts the center position of the view into the coordinates accepted
371  // by SendMouseMoveNotifyWhenDone() method.
372  NSRect bounds = [view bounds];
373  NSPoint center = NSMakePoint(NSMidX(bounds), NSMidY(bounds));
374  center = [view convertPoint:center toView:nil];
375  center = [window convertBaseToScreen:center];
376  center = NSMakePoint(center.x, [screen frame].size.height - center.y);
377
378  SendMouseMoveNotifyWhenDone(center.x, center.y,
379                              new ClickTask(button, state, task));
380}
381
382}  // ui_controls
383