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 } // namespace ui_controls 393