Pin Chrome's shortcut to the Win10 Start menu on install and OS upgrade.
[chromium-blink-merge.git] / ui / base / test / ui_controls_mac.mm
blob002cb03e646f0dbce4ebdbccacab5e039151effb
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 <vector>
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.
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.
45 using cocoa_test_event_utils::SynthesizeKeyEvent;
46 using cocoa_test_event_utils::TimeIntervalSinceSystemStartup;
48 namespace {
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,
59                                  bool control,
60                                  bool shift,
61                                  bool alt,
62                                  bool command,
63                                  std::vector<NSEvent*>* events) {
64   NSEvent* event = nil;
65   NSUInteger flags = 0;
66   if (control) {
67     flags |= NSControlKeyMask;
68     event = SynthesizeKeyEvent(window, true, ui::VKEY_CONTROL, flags);
69     DCHECK(event);
70     events->push_back(event);
71   }
72   if (shift) {
73     flags |= NSShiftKeyMask;
74     event = SynthesizeKeyEvent(window, true, ui::VKEY_SHIFT, flags);
75     DCHECK(event);
76     events->push_back(event);
77   }
78   if (alt) {
79     flags |= NSAlternateKeyMask;
80     event = SynthesizeKeyEvent(window, true, ui::VKEY_MENU, flags);
81     DCHECK(event);
82     events->push_back(event);
83   }
84   if (command) {
85     flags |= NSCommandKeyMask;
86     event = SynthesizeKeyEvent(window, true, ui::VKEY_COMMAND, flags);
87     DCHECK(event);
88     events->push_back(event);
89   }
91   event = SynthesizeKeyEvent(window, true, keycode, flags);
92   DCHECK(event);
93   events->push_back(event);
94   event = SynthesizeKeyEvent(window, false, keycode, flags);
95   DCHECK(event);
96   events->push_back(event);
98   if (command) {
99     flags &= ~NSCommandKeyMask;
100     event = SynthesizeKeyEvent(window, false, ui::VKEY_COMMAND, flags);
101     DCHECK(event);
102     events->push_back(event);
103   }
104   if (alt) {
105     flags &= ~NSAlternateKeyMask;
106     event = SynthesizeKeyEvent(window, false, ui::VKEY_MENU, flags);
107     DCHECK(event);
108     events->push_back(event);
109   }
110   if (shift) {
111     flags &= ~NSShiftKeyMask;
112     event = SynthesizeKeyEvent(window, false, ui::VKEY_SHIFT, flags);
113     DCHECK(event);
114     events->push_back(event);
115   }
116   if (control) {
117     flags &= ~NSControlKeyMask;
118     event = SynthesizeKeyEvent(window, false, ui::VKEY_CONTROL, flags);
119     DCHECK(event);
120     events->push_back(event);
121   }
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
128                                       untilDate:nil
129                                          inMode:NSDefaultRunLoopMode
130                                         dequeue:NO];
131   // If there is still event in the queue, then we need to check again.
132   if (event) {
133     base::MessageLoop::current()->PostTask(
134         FROM_HERE,
135         base::Bind(&EventQueueWatcher, task));
136   } else {
137     base::MessageLoop::current()->PostTask(FROM_HERE, task);
138   }
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];
149   NSWindow* window =
150       [[NSApplication sharedApplication] windowWithWindowNumber:window_number];
151   if (window)
152     return window;
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]))
165       return window;
166   }
168   // Note that -[NSApplication orderedWindows] won't include NSPanels. If a test
169   // uses those, it will need to handle that itself.
170   return nil;
173 }  // namespace
175 namespace ui_controls {
177 void EnableUIControls() {
178   g_ui_controls_enabled = true;
181 bool SendKeyPress(gfx::NativeWindow window,
182                   ui::KeyboardCode key,
183                   bool control,
184                   bool shift,
185                   bool alt,
186                   bool command) {
187   CHECK(g_ui_controls_enabled);
188   return SendKeyPressNotifyWhenDone(window, key,
189                                     control, shift, alt, command,
190                                     base::Closure());
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,
197                                 bool control,
198                                 bool shift,
199                                 bool alt,
200                                 bool command,
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
213   // a keyboard event.
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));
221   }
223   return true;
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;
244   if (window)
245     pointInWindow = [window convertScreenToBase:pointInWindow];
246   NSTimeInterval timestamp = TimeIntervalSinceSystemStartup();
248   NSEvent* event =
249       [NSEvent mouseEventWithType:NSMouseMoved
250                          location:pointInWindow
251                     modifierFlags:0
252                         timestamp:timestamp
253                      windowNumber:[window windowNumber]
254                           context:nil
255                       eventNumber:0
256                        clickCount:0
257                          pressure:0.0];
258   [[NSApplication sharedApplication] postEvent:event atStart:NO];
260   if (!task.is_null()) {
261     base::MessageLoop::current()->PostTask(
262         FROM_HERE, base::Bind(&EventQueueWatcher, task));
263   }
265   return true;
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));
281   }
282   NSEventType etype = NSLeftMouseDown;
283   if (type == LEFT) {
284     if (state == UP) {
285       etype = NSLeftMouseUp;
286     } else {
287       etype = NSLeftMouseDown;
288     }
289   } else if (type == MIDDLE) {
290     if (state == UP) {
291       etype = NSOtherMouseUp;
292     } else {
293       etype = NSOtherMouseDown;
294     }
295   } else if (type == RIGHT) {
296     if (state == UP) {
297       etype = NSRightMouseUp;
298     } else {
299       etype = NSRightMouseDown;
300     }
301   } else {
302     return false;
303   }
304   NSWindow* window = WindowAtCurrentMouseLocation();
305   NSPoint pointInWindow = g_mouse_location;
306   if (window)
307     pointInWindow = [window convertScreenToBase:pointInWindow];
309   NSEvent* event =
310       [NSEvent mouseEventWithType:etype
311                          location:pointInWindow
312                     modifierFlags:0
313                         timestamp:TimeIntervalSinceSystemStartup()
314                      windowNumber:[window windowNumber]
315                           context:nil
316                       eventNumber:0
317                        clickCount:1
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));
324   }
326   return true;
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