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