1// Copyright (c) 2011 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 "chrome/browser/automation/ui_controls.h" 6 7#import <Cocoa/Cocoa.h> 8#include <mach/mach_time.h> 9#include <vector> 10 11#include "base/message_loop.h" 12#include "chrome/browser/automation/ui_controls_internal.h" 13#include "content/browser/browser_thread.h" 14#include "ui/base/keycodes/keyboard_code_conversion_mac.h" 15 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). 27 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. 37// 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. 44 45namespace { 46 47// From 48// http://stackoverflow.com/questions/1597383/cgeventtimestamp-to-nsdate 49// Which credits Apple sample code for this routine. 50uint64_t UpTimeInNanoseconds(void) { 51 uint64_t time; 52 uint64_t timeNano; 53 static mach_timebase_info_data_t sTimebaseInfo; 54 55 time = mach_absolute_time(); 56 57 // Convert to nanoseconds. 58 59 // If this is the first time we've run, get the timebase. 60 // We can use denom == 0 to indicate that sTimebaseInfo is 61 // uninitialised because it makes no sense to have a zero 62 // denominator is a fraction. 63 if (sTimebaseInfo.denom == 0) { 64 (void) mach_timebase_info(&sTimebaseInfo); 65 } 66 67 // This could overflow; for testing needs we probably don't care. 68 timeNano = time * sTimebaseInfo.numer / sTimebaseInfo.denom; 69 return timeNano; 70} 71 72NSTimeInterval TimeIntervalSinceSystemStartup() { 73 return UpTimeInNanoseconds() / 1000000000.0; 74} 75 76// Creates and returns an autoreleased key event. 77NSEvent* SynthesizeKeyEvent(NSWindow* window, 78 bool keyDown, 79 ui::KeyboardCode keycode, 80 NSUInteger flags) { 81 unichar character; 82 unichar characterIgnoringModifiers; 83 int macKeycode = ui::MacKeyCodeForWindowsKeyCode( 84 keycode, flags, &character, &characterIgnoringModifiers); 85 86 if (macKeycode < 0) 87 return nil; 88 89 NSString* charactersIgnoringModifiers = 90 [[[NSString alloc] initWithCharacters:&characterIgnoringModifiers 91 length:1] 92 autorelease]; 93 NSString* characters = 94 [[[NSString alloc] initWithCharacters:&character length:1] autorelease]; 95 96 NSEventType type = (keyDown ? NSKeyDown : NSKeyUp); 97 98 // Modifier keys generate NSFlagsChanged event rather than 99 // NSKeyDown/NSKeyUp events. 100 if (keycode == ui::VKEY_CONTROL || keycode == ui::VKEY_SHIFT || 101 keycode == ui::VKEY_MENU || keycode == ui::VKEY_COMMAND) 102 type = NSFlagsChanged; 103 104 // For events other than mouse moved, [event locationInWindow] is 105 // UNDEFINED if the event is not NSMouseMoved. Thus, the (0,0) 106 // location should be fine. 107 NSEvent* event = 108 [NSEvent keyEventWithType:type 109 location:NSMakePoint(0, 0) 110 modifierFlags:flags 111 timestamp:TimeIntervalSinceSystemStartup() 112 windowNumber:[window windowNumber] 113 context:nil 114 characters:characters 115 charactersIgnoringModifiers:charactersIgnoringModifiers 116 isARepeat:NO 117 keyCode:(unsigned short)macKeycode]; 118 119 return event; 120} 121 122// Creates the proper sequence of autoreleased key events for a key down + up. 123void SynthesizeKeyEventsSequence(NSWindow* window, 124 ui::KeyboardCode keycode, 125 bool control, 126 bool shift, 127 bool alt, 128 bool command, 129 std::vector<NSEvent*>* events) { 130 NSEvent* event = nil; 131 NSUInteger flags = 0; 132 if (control) { 133 flags |= NSControlKeyMask; 134 event = SynthesizeKeyEvent(window, true, ui::VKEY_CONTROL, flags); 135 DCHECK(event); 136 events->push_back(event); 137 } 138 if (shift) { 139 flags |= NSShiftKeyMask; 140 event = SynthesizeKeyEvent(window, true, ui::VKEY_SHIFT, flags); 141 DCHECK(event); 142 events->push_back(event); 143 } 144 if (alt) { 145 flags |= NSAlternateKeyMask; 146 event = SynthesizeKeyEvent(window, true, ui::VKEY_MENU, flags); 147 DCHECK(event); 148 events->push_back(event); 149 } 150 if (command) { 151 flags |= NSCommandKeyMask; 152 event = SynthesizeKeyEvent(window, true, ui::VKEY_COMMAND, flags); 153 DCHECK(event); 154 events->push_back(event); 155 } 156 157 event = SynthesizeKeyEvent(window, true, keycode, flags); 158 DCHECK(event); 159 events->push_back(event); 160 event = SynthesizeKeyEvent(window, false, keycode, flags); 161 DCHECK(event); 162 events->push_back(event); 163 164 if (command) { 165 flags &= ~NSCommandKeyMask; 166 event = SynthesizeKeyEvent(window, false, ui::VKEY_COMMAND, flags); 167 DCHECK(event); 168 events->push_back(event); 169 } 170 if (alt) { 171 flags &= ~NSAlternateKeyMask; 172 event = SynthesizeKeyEvent(window, false, ui::VKEY_MENU, flags); 173 DCHECK(event); 174 events->push_back(event); 175 } 176 if (shift) { 177 flags &= ~NSShiftKeyMask; 178 event = SynthesizeKeyEvent(window, false, ui::VKEY_SHIFT, flags); 179 DCHECK(event); 180 events->push_back(event); 181 } 182 if (control) { 183 flags &= ~NSControlKeyMask; 184 event = SynthesizeKeyEvent(window, false, ui::VKEY_CONTROL, flags); 185 DCHECK(event); 186 events->push_back(event); 187 } 188} 189 190// A task class to watch for the event queue. The specific task will be fired 191// when there is no more event in the queue. 192class EventQueueWatcher : public Task { 193 public: 194 EventQueueWatcher(Task* task) : task_(task) {} 195 196 virtual ~EventQueueWatcher() {} 197 198 virtual void Run() { 199 NSEvent* event = [NSApp nextEventMatchingMask:NSAnyEventMask 200 untilDate:nil 201 inMode:NSDefaultRunLoopMode 202 dequeue:NO]; 203 // If there is still event in the queue, then we need to check again. 204 if (event) 205 MessageLoop::current()->PostTask(FROM_HERE, new EventQueueWatcher(task_)); 206 else 207 MessageLoop::current()->PostTask(FROM_HERE, task_); 208 } 209 210 private: 211 Task* task_; 212}; 213 214// Stores the current mouse location on the screen. So that we can use it 215// when firing keyboard and mouse click events. 216NSPoint g_mouse_location = { 0, 0 }; 217 218} // anonymous namespace 219 220 221namespace ui_controls { 222 223bool SendKeyPress(gfx::NativeWindow window, 224 ui::KeyboardCode key, 225 bool control, 226 bool shift, 227 bool alt, 228 bool command) { 229 return SendKeyPressNotifyWhenDone(window, key, 230 control, shift, alt, command, 231 NULL); 232} 233 234// Win and Linux implement a SendKeyPress() this as a 235// SendKeyPressAndRelease(), so we should as well (despite the name). 236bool SendKeyPressNotifyWhenDone(gfx::NativeWindow window, 237 ui::KeyboardCode key, 238 bool control, 239 bool shift, 240 bool alt, 241 bool command, 242 Task* task) { 243 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 244 245 std::vector<NSEvent*> events; 246 SynthesizeKeyEventsSequence( 247 window, key, control, shift, alt, command, &events); 248 249 // TODO(suzhe): Using [NSApplication postEvent:atStart:] here causes 250 // BrowserKeyEventsTest.CommandKeyEvents to fail. See http://crbug.com/49270 251 // But using [NSApplication sendEvent:] should be safe for keyboard events, 252 // because until now, no code wants to retrieve the next event when handling 253 // a keyboard event. 254 for (std::vector<NSEvent*>::iterator iter = events.begin(); 255 iter != events.end(); ++iter) 256 [[NSApplication sharedApplication] sendEvent:*iter]; 257 258 if (task) 259 MessageLoop::current()->PostTask(FROM_HERE, new EventQueueWatcher(task)); 260 261 return true; 262} 263 264bool SendMouseMove(long x, long y) { 265 return SendMouseMoveNotifyWhenDone(x, y, NULL); 266} 267 268// Input position is in screen coordinates. However, NSMouseMoved 269// events require them window-relative, so we adjust. We *DO* flip 270// the coordinate space, so input events can be the same for all 271// platforms. E.g. (0,0) is upper-left. 272bool SendMouseMoveNotifyWhenDone(long x, long y, Task* task) { 273 NSWindow* window = [[NSApplication sharedApplication] keyWindow]; 274 CGFloat screenHeight = 275 [[[NSScreen screens] objectAtIndex:0] frame].size.height; 276 g_mouse_location = NSMakePoint(x, screenHeight - y); // flip! 277 NSPoint pointInWindow = g_mouse_location; 278 if (window) 279 pointInWindow = [window convertScreenToBase:pointInWindow]; 280 NSTimeInterval timestamp = TimeIntervalSinceSystemStartup(); 281 282 NSEvent* event = 283 [NSEvent mouseEventWithType:NSMouseMoved 284 location:pointInWindow 285 modifierFlags:0 286 timestamp:timestamp 287 windowNumber:[window windowNumber] 288 context:nil 289 eventNumber:0 290 clickCount:0 291 pressure:0.0]; 292 [[NSApplication sharedApplication] postEvent:event atStart:NO]; 293 294 if (task) 295 MessageLoop::current()->PostTask(FROM_HERE, new EventQueueWatcher(task)); 296 297 return true; 298} 299 300bool SendMouseEvents(MouseButton type, int state) { 301 return SendMouseEventsNotifyWhenDone(type, state, NULL); 302} 303 304bool SendMouseEventsNotifyWhenDone(MouseButton type, int state, Task* task) { 305 // On windows it appears state can be (UP|DOWN). It is unclear if 306 // that'll happen here but prepare for it just in case. 307 if (state == (UP|DOWN)) { 308 return (SendMouseEventsNotifyWhenDone(type, DOWN, NULL) && 309 SendMouseEventsNotifyWhenDone(type, UP, task)); 310 } 311 NSEventType etype = 0; 312 if (type == LEFT) { 313 if (state == UP) { 314 etype = NSLeftMouseUp; 315 } else { 316 etype = NSLeftMouseDown; 317 } 318 } else if (type == MIDDLE) { 319 if (state == UP) { 320 etype = NSOtherMouseUp; 321 } else { 322 etype = NSOtherMouseDown; 323 } 324 } else if (type == RIGHT) { 325 if (state == UP) { 326 etype = NSRightMouseUp; 327 } else { 328 etype = NSRightMouseDown; 329 } 330 } else { 331 return false; 332 } 333 NSWindow* window = [[NSApplication sharedApplication] keyWindow]; 334 NSPoint pointInWindow = g_mouse_location; 335 if (window) 336 pointInWindow = [window convertScreenToBase:pointInWindow]; 337 338 NSEvent* event = 339 [NSEvent mouseEventWithType:etype 340 location:pointInWindow 341 modifierFlags:0 342 timestamp:TimeIntervalSinceSystemStartup() 343 windowNumber:[window windowNumber] 344 context:nil 345 eventNumber:0 346 clickCount:1 347 pressure:(state == DOWN ? 1.0 : 0.0 )]; 348 [[NSApplication sharedApplication] postEvent:event atStart:NO]; 349 350 if (task) 351 MessageLoop::current()->PostTask(FROM_HERE, new EventQueueWatcher(task)); 352 353 return true; 354} 355 356bool SendMouseClick(MouseButton type) { 357 return SendMouseEventsNotifyWhenDone(type, UP|DOWN, NULL); 358} 359 360void MoveMouseToCenterAndPress( 361 NSView* view, 362 MouseButton button, 363 int state, 364 Task* task) { 365 DCHECK(view); 366 NSWindow* window = [view window]; 367 DCHECK(window); 368 NSScreen* screen = [window screen]; 369 DCHECK(screen); 370 371 // Converts the center position of the view into the coordinates accepted 372 // by SendMouseMoveNotifyWhenDone() method. 373 NSRect bounds = [view bounds]; 374 NSPoint center = NSMakePoint(NSMidX(bounds), NSMidY(bounds)); 375 center = [view convertPoint:center toView:nil]; 376 center = [window convertBaseToScreen:center]; 377 center = NSMakePoint(center.x, [screen frame].size.height - center.y); 378 379 SendMouseMoveNotifyWhenDone(center.x, center.y, 380 new ClickTask(button, state, task)); 381} 382 383} // ui_controls 384