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// Stores the current mouse location on the screen. So that we can use it
49// when firing keyboard and mouse click events.
50NSPoint g_mouse_location = { 0, 0 };
51
52bool 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.
57uint64_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
79NSTimeInterval TimeIntervalSinceSystemStartup() {
80  return UpTimeInNanoseconds() / 1000000000.0;
81}
82
83// Creates and returns an autoreleased key event.
84NSEvent* 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.
130void 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.
199void 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.
219NSWindow* 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
228namespace ui_controls {
229
230void EnableUIControls() {
231  g_ui_controls_enabled = true;
232}
233
234bool 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).
248bool 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
279bool 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.
288bool 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
321bool SendMouseEvents(MouseButton type, int state) {
322  CHECK(g_ui_controls_enabled);
323  return SendMouseEventsNotifyWhenDone(type, state, base::Closure());
324}
325
326bool 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
382bool SendMouseClick(MouseButton type) {
383  CHECK(g_ui_controls_enabled);
384  return SendMouseEventsNotifyWhenDone(type, UP|DOWN, base::Closure());
385}
386
387void RunClosureAfterAllPendingUIEvents(const base::Closure& closure) {
388  base::MessageLoop::current()->PostTask(
389      FROM_HERE, base::Bind(&EventQueueWatcher, closure));
390}
391
392}  // namespace ui_controls
393