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>
10 #include "base/bind.h"
11 #include "base/callback.h"
12 #include "base/message_loop/message_loop.h"
13 #include "ui/events/keycodes/keyboard_code_conversion_mac.h"
14 #import "ui/events/test/cocoa_test_event_utils.h"
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).
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.
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.
45 using cocoa_test_event_utils::SynthesizeKeyEvent;
46 using cocoa_test_event_utils::TimeIntervalSinceSystemStartup;
50 // Stores the current mouse location on the screen. So that we can use it
51 // when firing keyboard and mouse click events.
52 NSPoint g_mouse_location = { 0, 0 };
54 bool g_ui_controls_enabled = false;
56 // Creates the proper sequence of autoreleased key events for a key down + up.
57 void SynthesizeKeyEventsSequence(NSWindow* window,
58 ui::KeyboardCode keycode,
63 std::vector<NSEvent*>* events) {
67 flags |= NSControlKeyMask;
68 event = SynthesizeKeyEvent(window, true, ui::VKEY_CONTROL, flags);
70 events->push_back(event);
73 flags |= NSShiftKeyMask;
74 event = SynthesizeKeyEvent(window, true, ui::VKEY_SHIFT, flags);
76 events->push_back(event);
79 flags |= NSAlternateKeyMask;
80 event = SynthesizeKeyEvent(window, true, ui::VKEY_MENU, flags);
82 events->push_back(event);
85 flags |= NSCommandKeyMask;
86 event = SynthesizeKeyEvent(window, true, ui::VKEY_COMMAND, flags);
88 events->push_back(event);
91 event = SynthesizeKeyEvent(window, true, keycode, flags);
93 events->push_back(event);
94 event = SynthesizeKeyEvent(window, false, keycode, flags);
96 events->push_back(event);
99 flags &= ~NSCommandKeyMask;
100 event = SynthesizeKeyEvent(window, false, ui::VKEY_COMMAND, flags);
102 events->push_back(event);
105 flags &= ~NSAlternateKeyMask;
106 event = SynthesizeKeyEvent(window, false, ui::VKEY_MENU, flags);
108 events->push_back(event);
111 flags &= ~NSShiftKeyMask;
112 event = SynthesizeKeyEvent(window, false, ui::VKEY_SHIFT, flags);
114 events->push_back(event);
117 flags &= ~NSControlKeyMask;
118 event = SynthesizeKeyEvent(window, false, ui::VKEY_CONTROL, flags);
120 events->push_back(event);
124 // A helper function to watch for the event queue. The specific task will be
125 // fired when there is no more event in the queue.
126 void EventQueueWatcher(const base::Closure& task) {
127 NSEvent* event = [NSApp nextEventMatchingMask:NSAnyEventMask
129 inMode:NSDefaultRunLoopMode
131 // If there is still event in the queue, then we need to check again.
133 base::MessageLoop::current()->PostTask(
135 base::Bind(&EventQueueWatcher, task));
137 base::MessageLoop::current()->PostTask(FROM_HERE, task);
141 // Returns the NSWindow located at |g_mouse_location|. NULL if there is no
142 // window there, or if the window located there is not owned by the application.
143 // On Mac, unless dragging, mouse events are sent to the window under the
144 // cursor. Note that the OS will ignore transparent windows and windows that
145 // explicitly ignore mouse events.
146 NSWindow* WindowAtCurrentMouseLocation() {
147 NSInteger window_number = [NSWindow windowNumberAtPoint:g_mouse_location
148 belowWindowWithWindowNumber:0];
150 [[NSApplication sharedApplication] windowWithWindowNumber:window_number];
154 // It's possible for a window owned by another application to be at that
155 // location. Cocoa won't provide an NSWindow* for those. Tests should not care
156 // about other applications, and raising windows in a headless application is
157 // flaky due to OS restrictions. For tests, hunt through all of this
158 // application's windows, top to bottom, looking for a good candidate.
159 NSArray* window_list = [[NSApplication sharedApplication] orderedWindows];
160 for (window in window_list) {
161 // Note this skips the extra checks (e.g. fully-transparent windows), that
162 // +[NSWindow windowNumberAtPoint:] performs. Tests that care about that
163 // should check separately (the goal here is to minimize flakiness).
164 if (NSPointInRect(g_mouse_location, [window frame]))
168 // Note that -[NSApplication orderedWindows] won't include NSPanels. If a test
169 // uses those, it will need to handle that itself.
175 namespace ui_controls {
177 void EnableUIControls() {
178 g_ui_controls_enabled = true;
181 bool SendKeyPress(gfx::NativeWindow window,
182 ui::KeyboardCode key,
187 CHECK(g_ui_controls_enabled);
188 return SendKeyPressNotifyWhenDone(window, key,
189 control, shift, alt, command,
193 // Win and Linux implement a SendKeyPress() this as a
194 // SendKeyPressAndRelease(), so we should as well (despite the name).
195 bool SendKeyPressNotifyWhenDone(gfx::NativeWindow window,
196 ui::KeyboardCode key,
201 const base::Closure& task) {
202 CHECK(g_ui_controls_enabled);
203 DCHECK(base::MessageLoopForUI::IsCurrent());
205 std::vector<NSEvent*> events;
206 SynthesizeKeyEventsSequence(
207 window, key, control, shift, alt, command, &events);
209 // TODO(suzhe): Using [NSApplication postEvent:atStart:] here causes
210 // BrowserKeyEventsTest.CommandKeyEvents to fail. See http://crbug.com/49270
211 // But using [NSApplication sendEvent:] should be safe for keyboard events,
212 // because until now, no code wants to retrieve the next event when handling
214 for (std::vector<NSEvent*>::iterator iter = events.begin();
215 iter != events.end(); ++iter)
216 [[NSApplication sharedApplication] sendEvent:*iter];
218 if (!task.is_null()) {
219 base::MessageLoop::current()->PostTask(
220 FROM_HERE, base::Bind(&EventQueueWatcher, task));
226 bool SendMouseMove(long x, long y) {
227 CHECK(g_ui_controls_enabled);
228 return SendMouseMoveNotifyWhenDone(x, y, base::Closure());
231 // Input position is in screen coordinates. However, NSMouseMoved
232 // events require them window-relative, so we adjust. We *DO* flip
233 // the coordinate space, so input events can be the same for all
234 // platforms. E.g. (0,0) is upper-left.
235 bool SendMouseMoveNotifyWhenDone(long x, long y, const base::Closure& task) {
236 CHECK(g_ui_controls_enabled);
237 CGFloat screenHeight =
238 [[[NSScreen screens] objectAtIndex:0] frame].size.height;
239 g_mouse_location = NSMakePoint(x, screenHeight - y); // flip!
241 NSWindow* window = WindowAtCurrentMouseLocation();
243 NSPoint pointInWindow = g_mouse_location;
245 pointInWindow = [window convertScreenToBase:pointInWindow];
246 NSTimeInterval timestamp = TimeIntervalSinceSystemStartup();
249 [NSEvent mouseEventWithType:NSMouseMoved
250 location:pointInWindow
253 windowNumber:[window windowNumber]
258 [[NSApplication sharedApplication] postEvent:event atStart:NO];
260 if (!task.is_null()) {
261 base::MessageLoop::current()->PostTask(
262 FROM_HERE, base::Bind(&EventQueueWatcher, task));
268 bool SendMouseEvents(MouseButton type, int state) {
269 CHECK(g_ui_controls_enabled);
270 return SendMouseEventsNotifyWhenDone(type, state, base::Closure());
273 bool SendMouseEventsNotifyWhenDone(MouseButton type, int state,
274 const base::Closure& task) {
275 CHECK(g_ui_controls_enabled);
276 // On windows it appears state can be (UP|DOWN). It is unclear if
277 // that'll happen here but prepare for it just in case.
278 if (state == (UP|DOWN)) {
279 return (SendMouseEventsNotifyWhenDone(type, DOWN, base::Closure()) &&
280 SendMouseEventsNotifyWhenDone(type, UP, task));
282 NSEventType etype = 0;
285 etype = NSLeftMouseUp;
287 etype = NSLeftMouseDown;
289 } else if (type == MIDDLE) {
291 etype = NSOtherMouseUp;
293 etype = NSOtherMouseDown;
295 } else if (type == RIGHT) {
297 etype = NSRightMouseUp;
299 etype = NSRightMouseDown;
304 NSWindow* window = WindowAtCurrentMouseLocation();
305 NSPoint pointInWindow = g_mouse_location;
307 pointInWindow = [window convertScreenToBase:pointInWindow];
310 [NSEvent mouseEventWithType:etype
311 location:pointInWindow
313 timestamp:TimeIntervalSinceSystemStartup()
314 windowNumber:[window windowNumber]
318 pressure:(state == DOWN ? 1.0 : 0.0 )];
319 [[NSApplication sharedApplication] postEvent:event atStart:NO];
321 if (!task.is_null()) {
322 base::MessageLoop::current()->PostTask(
323 FROM_HERE, base::Bind(&EventQueueWatcher, task));
329 bool SendMouseClick(MouseButton type) {
330 CHECK(g_ui_controls_enabled);
331 return SendMouseEventsNotifyWhenDone(type, UP|DOWN, base::Closure());
334 void RunClosureAfterAllPendingUIEvents(const base::Closure& closure) {
335 base::MessageLoop::current()->PostTask(
336 FROM_HERE, base::Bind(&EventQueueWatcher, closure));
339 bool IsFullKeyboardAccessEnabled() {
340 return [NSApp isFullKeyboardAccessEnabled];
343 } // namespace ui_controls