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.
5 #include "ui/base/test/ui_controls.h"
7 #import <Cocoa/Cocoa.h>
8 #include <mach/mach_time.h>
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"
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).
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.
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.
49 // http://stackoverflow.com/questions/1597383/cgeventtimestamp-to-nsdate
50 // Which credits Apple sample code for this routine.
51 uint64_t UpTimeInNanoseconds(void) {
54 static mach_timebase_info_data_t sTimebaseInfo;
56 time = mach_absolute_time();
58 // Convert to nanoseconds.
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);
68 // This could overflow; for testing needs we probably don't care.
69 timeNano = time * sTimebaseInfo.numer / sTimebaseInfo.denom;
73 NSTimeInterval TimeIntervalSinceSystemStartup() {
74 return UpTimeInNanoseconds() / 1000000000.0;
77 // Creates and returns an autoreleased key event.
78 NSEvent* SynthesizeKeyEvent(NSWindow* window,
80 ui::KeyboardCode keycode,
83 unichar characterIgnoringModifiers;
84 int macKeycode = ui::MacKeyCodeForWindowsKeyCode(
85 keycode, flags, &character, &characterIgnoringModifiers);
90 NSString* charactersIgnoringModifiers =
91 [[[NSString alloc] initWithCharacters:&characterIgnoringModifiers
94 NSString* characters =
95 [[[NSString alloc] initWithCharacters:&character length:1] autorelease];
97 NSEventType type = (keyDown ? NSKeyDown : NSKeyUp);
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;
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.
109 [NSEvent keyEventWithType:type
112 timestamp:TimeIntervalSinceSystemStartup()
113 windowNumber:[window windowNumber]
115 characters:characters
116 charactersIgnoringModifiers:charactersIgnoringModifiers
118 keyCode:(unsigned short)macKeycode];
123 // Creates the proper sequence of autoreleased key events for a key down + up.
124 void SynthesizeKeyEventsSequence(NSWindow* window,
125 ui::KeyboardCode keycode,
130 std::vector<NSEvent*>* events) {
131 NSEvent* event = nil;
132 NSUInteger flags = 0;
134 flags |= NSControlKeyMask;
135 event = SynthesizeKeyEvent(window, true, ui::VKEY_CONTROL, flags);
137 events->push_back(event);
140 flags |= NSShiftKeyMask;
141 event = SynthesizeKeyEvent(window, true, ui::VKEY_SHIFT, flags);
143 events->push_back(event);
146 flags |= NSAlternateKeyMask;
147 event = SynthesizeKeyEvent(window, true, ui::VKEY_MENU, flags);
149 events->push_back(event);
152 flags |= NSCommandKeyMask;
153 event = SynthesizeKeyEvent(window, true, ui::VKEY_COMMAND, flags);
155 events->push_back(event);
158 event = SynthesizeKeyEvent(window, true, keycode, flags);
160 events->push_back(event);
161 event = SynthesizeKeyEvent(window, false, keycode, flags);
163 events->push_back(event);
166 flags &= ~NSCommandKeyMask;
167 event = SynthesizeKeyEvent(window, false, ui::VKEY_COMMAND, flags);
169 events->push_back(event);
172 flags &= ~NSAlternateKeyMask;
173 event = SynthesizeKeyEvent(window, false, ui::VKEY_MENU, flags);
175 events->push_back(event);
178 flags &= ~NSShiftKeyMask;
179 event = SynthesizeKeyEvent(window, false, ui::VKEY_SHIFT, flags);
181 events->push_back(event);
184 flags &= ~NSControlKeyMask;
185 event = SynthesizeKeyEvent(window, false, ui::VKEY_CONTROL, flags);
187 events->push_back(event);
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.
193 void EventQueueWatcher(const base::Closure& task) {
194 NSEvent* event = [NSApp nextEventMatchingMask:NSAnyEventMask
196 inMode:NSDefaultRunLoopMode
198 // If there is still event in the queue, then we need to check again.
200 base::MessageLoop::current()->PostTask(
202 base::Bind(&EventQueueWatcher, task));
204 base::MessageLoop::current()->PostTask(FROM_HERE, task);
208 // Stores the current mouse location on the screen. So that we can use it
209 // when firing keyboard and mouse click events.
210 NSPoint g_mouse_location = { 0, 0 };
212 bool g_ui_controls_enabled = false;
216 namespace ui_controls {
218 void EnableUIControls() {
219 g_ui_controls_enabled = true;
222 bool SendKeyPress(gfx::NativeWindow window,
223 ui::KeyboardCode key,
228 CHECK(g_ui_controls_enabled);
229 return SendKeyPressNotifyWhenDone(window, key,
230 control, shift, alt, command,
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,
242 const base::Closure& task) {
243 CHECK(g_ui_controls_enabled);
244 DCHECK(base::MessageLoopForUI::IsCurrent());
246 std::vector<NSEvent*> events;
247 SynthesizeKeyEventsSequence(
248 window, key, control, shift, alt, command, &events);
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
255 for (std::vector<NSEvent*>::iterator iter = events.begin();
256 iter != events.end(); ++iter)
257 [[NSApplication sharedApplication] sendEvent:*iter];
259 if (!task.is_null()) {
260 base::MessageLoop::current()->PostTask(
261 FROM_HERE, base::Bind(&EventQueueWatcher, task));
267 bool SendMouseMove(long x, long y) {
268 CHECK(g_ui_controls_enabled);
269 return SendMouseMoveNotifyWhenDone(x, y, base::Closure());
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.
276 bool 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;
284 pointInWindow = [window convertScreenToBase:pointInWindow];
285 NSTimeInterval timestamp = TimeIntervalSinceSystemStartup();
288 [NSEvent mouseEventWithType:NSMouseMoved
289 location:pointInWindow
292 windowNumber:[window windowNumber]
297 [[NSApplication sharedApplication] postEvent:event atStart:NO];
299 if (!task.is_null()) {
300 base::MessageLoop::current()->PostTask(
301 FROM_HERE, base::Bind(&EventQueueWatcher, task));
307 bool SendMouseEvents(MouseButton type, int state) {
308 CHECK(g_ui_controls_enabled);
309 return SendMouseEventsNotifyWhenDone(type, state, base::Closure());
312 bool 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));
321 NSEventType etype = 0;
324 etype = NSLeftMouseUp;
326 etype = NSLeftMouseDown;
328 } else if (type == MIDDLE) {
330 etype = NSOtherMouseUp;
332 etype = NSOtherMouseDown;
334 } else if (type == RIGHT) {
336 etype = NSRightMouseUp;
338 etype = NSRightMouseDown;
343 NSWindow* window = [[NSApplication sharedApplication] keyWindow];
344 NSPoint pointInWindow = g_mouse_location;
346 pointInWindow = [window convertScreenToBase:pointInWindow];
349 [NSEvent mouseEventWithType:etype
350 location:pointInWindow
352 timestamp:TimeIntervalSinceSystemStartup()
353 windowNumber:[window windowNumber]
357 pressure:(state == DOWN ? 1.0 : 0.0 )];
358 [[NSApplication sharedApplication] postEvent:event atStart:NO];
360 if (!task.is_null()) {
361 base::MessageLoop::current()->PostTask(
362 FROM_HERE, base::Bind(&EventQueueWatcher, task));
368 bool SendMouseClick(MouseButton type) {
369 CHECK(g_ui_controls_enabled);
370 return SendMouseEventsNotifyWhenDone(type, UP|DOWN, base::Closure());
373 } // namespace ui_controls