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 // From 49 // http://stackoverflow.com/questions/1597383/cgeventtimestamp-to-nsdate 50 // Which credits Apple sample code for this routine. 51 uint64_t UpTimeInNanoseconds(void) { 52 uint64_t time; 53 uint64_t timeNano; 54 static mach_timebase_info_data_t sTimebaseInfo; 55 56 time = mach_absolute_time(); 57 58 // Convert to nanoseconds. 59 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); 66 } 67 68 // This could overflow; for testing needs we probably don't care. 69 timeNano = time * sTimebaseInfo.numer / sTimebaseInfo.denom; 70 return timeNano; 71 } 72 73 NSTimeInterval TimeIntervalSinceSystemStartup() { 74 return UpTimeInNanoseconds() / 1000000000.0; 75 } 76 77 // Creates and returns an autoreleased key event. 78 NSEvent* SynthesizeKeyEvent(NSWindow* window, 79 bool keyDown, 80 ui::KeyboardCode keycode, 81 NSUInteger flags) { 82 unichar character; 83 unichar characterIgnoringModifiers; 84 int macKeycode = ui::MacKeyCodeForWindowsKeyCode( 85 keycode, flags, &character, &characterIgnoringModifiers); 86 87 if (macKeycode < 0) 88 return nil; 89 90 NSString* charactersIgnoringModifiers = 91 [[[NSString alloc] initWithCharacters:&characterIgnoringModifiers 92 length:1] 93 autorelease]; 94 NSString* characters = 95 [[[NSString alloc] initWithCharacters:&character length:1] autorelease]; 96 97 NSEventType type = (keyDown ? NSKeyDown : NSKeyUp); 98 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; 104 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. 108 NSEvent* event = 109 [NSEvent keyEventWithType:type 110 location:NSZeroPoint 111 modifierFlags:flags 112 timestamp:TimeIntervalSinceSystemStartup() 113 windowNumber:[window windowNumber] 114 context:nil 115 characters:characters 116 charactersIgnoringModifiers:charactersIgnoringModifiers 117 isARepeat:NO 118 keyCode:(unsigned short)macKeycode]; 119 120 return event; 121 } 122 123 // Creates the proper sequence of autoreleased key events for a key down + up. 124 void SynthesizeKeyEventsSequence(NSWindow* window, 125 ui::KeyboardCode keycode, 126 bool control, 127 bool shift, 128 bool alt, 129 bool command, 130 std::vector<NSEvent*>* events) { 131 NSEvent* event = nil; 132 NSUInteger flags = 0; 133 if (control) { 134 flags |= NSControlKeyMask; 135 event = SynthesizeKeyEvent(window, true, ui::VKEY_CONTROL, flags); 136 DCHECK(event); 137 events->push_back(event); 138 } 139 if (shift) { 140 flags |= NSShiftKeyMask; 141 event = SynthesizeKeyEvent(window, true, ui::VKEY_SHIFT, flags); 142 DCHECK(event); 143 events->push_back(event); 144 } 145 if (alt) { 146 flags |= NSAlternateKeyMask; 147 event = SynthesizeKeyEvent(window, true, ui::VKEY_MENU, flags); 148 DCHECK(event); 149 events->push_back(event); 150 } 151 if (command) { 152 flags |= NSCommandKeyMask; 153 event = SynthesizeKeyEvent(window, true, ui::VKEY_COMMAND, flags); 154 DCHECK(event); 155 events->push_back(event); 156 } 157 158 event = SynthesizeKeyEvent(window, true, keycode, flags); 159 DCHECK(event); 160 events->push_back(event); 161 event = SynthesizeKeyEvent(window, false, keycode, flags); 162 DCHECK(event); 163 events->push_back(event); 164 165 if (command) { 166 flags &= ~NSCommandKeyMask; 167 event = SynthesizeKeyEvent(window, false, ui::VKEY_COMMAND, flags); 168 DCHECK(event); 169 events->push_back(event); 170 } 171 if (alt) { 172 flags &= ~NSAlternateKeyMask; 173 event = SynthesizeKeyEvent(window, false, ui::VKEY_MENU, flags); 174 DCHECK(event); 175 events->push_back(event); 176 } 177 if (shift) { 178 flags &= ~NSShiftKeyMask; 179 event = SynthesizeKeyEvent(window, false, ui::VKEY_SHIFT, flags); 180 DCHECK(event); 181 events->push_back(event); 182 } 183 if (control) { 184 flags &= ~NSControlKeyMask; 185 event = SynthesizeKeyEvent(window, false, ui::VKEY_CONTROL, flags); 186 DCHECK(event); 187 events->push_back(event); 188 } 189 } 190 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 195 untilDate:nil 196 inMode:NSDefaultRunLoopMode 197 dequeue:NO]; 198 // If there is still event in the queue, then we need to check again. 199 if (event) { 200 base::MessageLoop::current()->PostTask( 201 FROM_HERE, 202 base::Bind(&EventQueueWatcher, task)); 203 } else { 204 base::MessageLoop::current()->PostTask(FROM_HERE, task); 205 } 206 } 207 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 }; 211 212 bool g_ui_controls_enabled = false; 213 214 } // namespace 215 216 namespace ui_controls { 217 218 void EnableUIControls() { 219 g_ui_controls_enabled = true; 220 } 221 222 bool SendKeyPress(gfx::NativeWindow window, 223 ui::KeyboardCode key, 224 bool control, 225 bool shift, 226 bool alt, 227 bool command) { 228 CHECK(g_ui_controls_enabled); 229 return SendKeyPressNotifyWhenDone(window, key, 230 control, shift, alt, command, 231 base::Closure()); 232 } 233 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, 238 bool control, 239 bool shift, 240 bool alt, 241 bool command, 242 const base::Closure& task) { 243 CHECK(g_ui_controls_enabled); 244 DCHECK_EQ(base::MessageLoop::TYPE_UI, base::MessageLoop::current()->type()); 245 246 std::vector<NSEvent*> events; 247 SynthesizeKeyEventsSequence( 248 window, key, control, shift, alt, command, &events); 249 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 254 // a keyboard event. 255 for (std::vector<NSEvent*>::iterator iter = events.begin(); 256 iter != events.end(); ++iter) 257 [[NSApplication sharedApplication] sendEvent:*iter]; 258 259 if (!task.is_null()) { 260 base::MessageLoop::current()->PostTask( 261 FROM_HERE, base::Bind(&EventQueueWatcher, task)); 262 } 263 264 return true; 265 } 266 267 bool SendMouseMove(long x, long y) { 268 CHECK(g_ui_controls_enabled); 269 return SendMouseMoveNotifyWhenDone(x, y, base::Closure()); 270 } 271 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; 283 if (window) 284 pointInWindow = [window convertScreenToBase:pointInWindow]; 285 NSTimeInterval timestamp = TimeIntervalSinceSystemStartup(); 286 287 NSEvent* event = 288 [NSEvent mouseEventWithType:NSMouseMoved 289 location:pointInWindow 290 modifierFlags:0 291 timestamp:timestamp 292 windowNumber:[window windowNumber] 293 context:nil 294 eventNumber:0 295 clickCount:0 296 pressure:0.0]; 297 [[NSApplication sharedApplication] postEvent:event atStart:NO]; 298 299 if (!task.is_null()) { 300 base::MessageLoop::current()->PostTask( 301 FROM_HERE, base::Bind(&EventQueueWatcher, task)); 302 } 303 304 return true; 305 } 306 307 bool SendMouseEvents(MouseButton type, int state) { 308 CHECK(g_ui_controls_enabled); 309 return SendMouseEventsNotifyWhenDone(type, state, base::Closure()); 310 } 311 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)); 320 } 321 NSEventType etype = 0; 322 if (type == LEFT) { 323 if (state == UP) { 324 etype = NSLeftMouseUp; 325 } else { 326 etype = NSLeftMouseDown; 327 } 328 } else if (type == MIDDLE) { 329 if (state == UP) { 330 etype = NSOtherMouseUp; 331 } else { 332 etype = NSOtherMouseDown; 333 } 334 } else if (type == RIGHT) { 335 if (state == UP) { 336 etype = NSRightMouseUp; 337 } else { 338 etype = NSRightMouseDown; 339 } 340 } else { 341 return false; 342 } 343 NSWindow* window = [[NSApplication sharedApplication] keyWindow]; 344 NSPoint pointInWindow = g_mouse_location; 345 if (window) 346 pointInWindow = [window convertScreenToBase:pointInWindow]; 347 348 NSEvent* event = 349 [NSEvent mouseEventWithType:etype 350 location:pointInWindow 351 modifierFlags:0 352 timestamp:TimeIntervalSinceSystemStartup() 353 windowNumber:[window windowNumber] 354 context:nil 355 eventNumber:0 356 clickCount:1 357 pressure:(state == DOWN ? 1.0 : 0.0 )]; 358 [[NSApplication sharedApplication] postEvent:event atStart:NO]; 359 360 if (!task.is_null()) { 361 base::MessageLoop::current()->PostTask( 362 FROM_HERE, base::Bind(&EventQueueWatcher, task)); 363 } 364 365 return true; 366 } 367 368 bool SendMouseClick(MouseButton type) { 369 CHECK(g_ui_controls_enabled); 370 return SendMouseEventsNotifyWhenDone(type, UP|DOWN, base::Closure()); 371 } 372 373 } // namespace ui_controls 374