• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
46namespace {
47
48// From
49// http://stackoverflow.com/questions/1597383/cgeventtimestamp-to-nsdate
50// Which credits Apple sample code for this routine.
51uint64_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
73NSTimeInterval TimeIntervalSinceSystemStartup() {
74  return UpTimeInNanoseconds() / 1000000000.0;
75}
76
77// Creates and returns an autoreleased key event.
78NSEvent* 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.
124void 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.
193void 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.
210NSPoint g_mouse_location = { 0, 0 };
211
212bool g_ui_controls_enabled = false;
213
214}  // namespace
215
216namespace ui_controls {
217
218void EnableUIControls() {
219  g_ui_controls_enabled = true;
220}
221
222bool 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).
236bool 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_EQ(base::MessageLoop::TYPE_UI, base::MessageLoop::current()->type());
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
267bool 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.
276bool 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
307bool SendMouseEvents(MouseButton type, int state) {
308  CHECK(g_ui_controls_enabled);
309  return SendMouseEventsNotifyWhenDone(type, state, base::Closure());
310}
311
312bool 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
368bool SendMouseClick(MouseButton type) {
369  CHECK(g_ui_controls_enabled);
370  return SendMouseEventsNotifyWhenDone(type, UP|DOWN, base::Closure());
371}
372
373}  // namespace ui_controls
374