Chromium Code Reviews| Index: ui/base/test/ui_controls_mac.mm |
| diff --git a/ui/base/test/ui_controls_mac.mm b/ui/base/test/ui_controls_mac.mm |
| index cd4103d6db1a6827219d1297e875ece574c27608..269061b565650c083f5ad9388a7d6f10e6df2699 100644 |
| --- a/ui/base/test/ui_controls_mac.mm |
| +++ b/ui/base/test/ui_controls_mac.mm |
| @@ -9,10 +9,18 @@ |
| #include "base/bind.h" |
| #include "base/callback.h" |
| +#import "base/mac/foundation_util.h" |
| +#import "base/mac/scoped_nsautorelease_pool.h" |
| +#import "base/mac/scoped_nsobject.h" |
| +#import "base/mac/scoped_objc_class_swizzler.h" |
| #include "base/message_loop/message_loop.h" |
| +#include "base/thread_task_runner_handle.h" |
| #include "ui/base/cocoa/cocoa_base_utils.h" |
| +#include "content/public/browser/browser_thread.h" |
| #include "ui/events/keycodes/keyboard_code_conversion_mac.h" |
| #import "ui/events/test/cocoa_test_event_utils.h" |
| +#import "ui/gfx/mac/coordinate_conversion.h" |
| +#include "ui/gfx/screen.h" |
| // Implementation details: We use [NSApplication sendEvent:] instead |
| // of [NSApplication postEvent:atStart:] so that the event gets sent |
| @@ -48,10 +56,18 @@ using cocoa_test_event_utils::TimeIntervalSinceSystemStartup; |
| namespace { |
| +// When enabled, all simulated mouse events will be posted to |
| +// the WindowServer, and the actual mouse will move on the screen. |
| +bool g_use_cgevents = false; |
| + |
| // Stores the current mouse location on the screen. So that we can use it |
| // when firing keyboard and mouse click events. |
| NSPoint g_mouse_location = { 0, 0 }; |
| +// Stores the current pressed mouse buttons. Indexed by |
| +// ui_controls::MouseButton. |
| +bool g_mouse_button_down[3] = { false, false, false }; |
| + |
| bool g_ui_controls_enabled = false; |
| // Creates the proper sequence of autoreleased key events for a key down + up. |
| @@ -122,9 +138,18 @@ void SynthesizeKeyEventsSequence(NSWindow* window, |
| } |
| } |
| +void RunTaskInTaskRunner( |
| + scoped_refptr<base::SingleThreadTaskRunner> task_runner, |
| + const base::Closure& task) { |
| + task_runner->PostTask(FROM_HERE, task); |
| +} |
| + |
| // A helper function to watch for the event queue. The specific task will be |
| // fired when there is no more event in the queue. |
| +// NOTE: It should be run on the UI thread, as otherwise it will always report |
| +// there are no pending events. |
| void EventQueueWatcher(const base::Closure& task) { |
| + DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)); |
| NSEvent* event = [NSApp nextEventMatchingMask:NSAnyEventMask |
| untilDate:nil |
| inMode:NSDefaultRunLoopMode |
| @@ -173,6 +198,179 @@ NSWindow* WindowAtCurrentMouseLocation() { |
| } // namespace |
| +// Since CGEvents take some time to reach -[NSApplication sendEvent:] we use |
| +// this class to wait for the events to start being processed before sending |
| +// finish notifications. |
| +class EventMonitor { |
| + public: |
| + void NotifyWhenEventIsProcessed(NSEvent* event, const base::Closure& task) { |
| + dispatch_sync(queue_, ^{ |
| + tasks_.emplace_back(Task(event, task)); |
| + }); |
| + } |
| + |
| + void ProcessingEvent(NSEvent* event) { |
| + dispatch_sync(queue_, ^{ |
| + auto it = std::find_if( |
| + tasks_.begin(), tasks_.end(), |
| + [&event](const Task& task) { return task.MatchesEvent(event); }); |
| + if (it != tasks_.end()) { |
| + it->Run(); |
| + tasks_.erase(it); |
| + } |
| + }); |
| + } |
| + |
| + static EventMonitor* Instance() { |
| + static EventMonitor* monitor = nullptr; |
| + if (!monitor) { |
| + monitor = new EventMonitor(); |
| + } |
| + return monitor; |
| + } |
| + |
| + private: |
| + class Task { |
| + public: |
| + Task(NSEvent* event, const base::Closure& task) |
| + : task_runner_(base::ThreadTaskRunnerHandle::Get()), |
| + event_([event retain]), |
| + finish_closure_(task) {} |
| + |
| + bool MatchesEvent(NSEvent* event) const { |
| + // When moving the window using BridgedNativeWidget::RunMoveLoop the |
| + // locationInWindow can be a little inconsistent with what we expect. |
| + // Seems that only comparing event type is fine. |
| + return |
| + [event_ type] == [event type] && [event_ subtype] == [event subtype]; |
| + } |
| + |
| + void Run() { |
| + // We get here before the event is actually processed. Run the |
| + // EventQueueWatcher on the main thread in order to wait for all events to |
| + // finish processing. |
| + content::BrowserThread::PostTask( |
| + content::BrowserThread::UI, FROM_HERE, |
| + base::Bind( |
| + &EventQueueWatcher, |
| + base::Bind(&RunTaskInTaskRunner, task_runner_, finish_closure_))); |
| + } |
| + |
| + private: |
| + // Events could be spawned on background threads. Be sure to invoke the |
| + // |finish_closure_| on an appropriate thread. |
| + scoped_refptr<base::SingleThreadTaskRunner> task_runner_; |
| + base::scoped_nsobject<NSEvent> event_; |
| + base::Closure finish_closure_; |
| + }; |
| + |
| + EventMonitor() |
| + : queue_(dispatch_queue_create("ui_controls_mac.EventMonitor", |
| + DISPATCH_QUEUE_SERIAL)), |
| + send_event_swizzler_( |
| + new base::mac::ScopedObjCClassSwizzler([NSApplication class], |
| + @selector(sendEvent:), |
| + @selector(cr_sendEvent:))) { |
| + } |
| + |
| + std::vector<Task> tasks_; |
| + |
| + // synchronizes access to the |tasks_| in case we spawn the events on a |
| + // different thread |
| + dispatch_queue_t queue_ = 0; |
| + |
| + scoped_ptr<base::mac::ScopedObjCClassSwizzler> |
| + send_event_swizzler_; |
| + |
| + DISALLOW_COPY_AND_ASSIGN(EventMonitor); |
| +}; |
| + |
| +// Donates testing implementations of NSApplication methods. We can't simply |
| +// use -[NSEvent addLocalMonitorForEventsMatchingMask:handler:], as other event |
| +// monitors could have precedence, and they could filter the events before we |
| +// can see them. But since nobody swizzles -[NSApplication sendEvent:] we should |
| +// be safe. |
| +@interface NSApplication(TestingDonor) |
| +@end |
| + |
| +@implementation NSApplication(TestingDonor) |
| +- (void)cr_sendEvent:(NSEvent*)event { |
| + // Invoke the finish handler before the event is processed, since we can get |
| + // stuck in BridgedNativeWidget::RunMoveLoop and would never see the event |
| + // otherwise. |
| + EventMonitor::Instance()->ProcessingEvent(event); |
| + |
| + [self cr_sendEvent:event]; |
| +} |
| +@end |
| + |
| +// Donates testing implementations of NSEvent methods. |
| +@interface FakeNSEventTestingDonor : NSObject |
| +@end |
| + |
| +@implementation FakeNSEventTestingDonor |
| ++ (NSPoint)mouseLocation { |
| + return g_mouse_location; |
| +} |
| + |
| ++ (NSUInteger)pressedMouseButtons { |
| + NSUInteger result = 0; |
| + const int buttons[3] = { |
| + ui_controls::LEFT, ui_controls::RIGHT, ui_controls::MIDDLE}; |
| + for (unsigned int i = 0; i < arraysize(buttons); ++i) { |
| + if (g_mouse_button_down[buttons[i]]) |
| + result |= (1 << i); |
| + } |
| + return result; |
| +} |
| +@end |
| + |
| +// Donates testing implementations of NSWindow methods. |
| +@interface FakeNSWindowTestingDonor : NSObject |
| +@end |
| + |
| +@implementation FakeNSWindowTestingDonor |
| +- (NSPoint)mouseLocationOutsideOfEventStream { |
| + NSWindow* window = base::mac::ObjCCastStrict<NSWindow>(self); |
| + return [window convertScreenToBase:g_mouse_location]; |
| +} |
| +@end |
| + |
| +namespace { |
| +class NSEventSwizzler { |
| + public: |
| + static void Install() { |
| + static NSEventSwizzler* swizzler = nullptr; |
| + if (!swizzler) { |
| + swizzler = new NSEventSwizzler(); |
| + } |
| + } |
| + |
| + protected: |
| + NSEventSwizzler() |
| + : mouse_location_swizzler_(new base::mac::ScopedObjCClassSwizzler( |
| + [NSEvent class], |
| + [FakeNSEventTestingDonor class], |
| + @selector(mouseLocation))), |
| + pressed_mouse_buttons_swizzler_(new base::mac::ScopedObjCClassSwizzler( |
| + [NSEvent class], |
| + [FakeNSEventTestingDonor class], |
| + @selector(pressedMouseButtons))), |
| + mouse_location_outside_of_event_stream_swizzler_( |
| + new base::mac::ScopedObjCClassSwizzler( |
| + [NSWindow class], |
| + [FakeNSWindowTestingDonor class], |
| + @selector(mouseLocationOutsideOfEventStream))) {} |
| + |
| + private: |
| + scoped_ptr<base::mac::ScopedObjCClassSwizzler> mouse_location_swizzler_; |
| + scoped_ptr<base::mac::ScopedObjCClassSwizzler> |
| + pressed_mouse_buttons_swizzler_; |
| + scoped_ptr<base::mac::ScopedObjCClassSwizzler> |
| + mouse_location_outside_of_event_stream_swizzler_; |
| +}; |
| +} // namespace |
| + |
| namespace ui_controls { |
| void EnableUIControls() { |
| @@ -200,9 +398,13 @@ bool SendKeyPressNotifyWhenDone(gfx::NativeWindow window, |
| bool alt, |
| bool command, |
| const base::Closure& task) { |
| + DCHECK(!g_use_cgevents) << "Not implemented"; |
| CHECK(g_ui_controls_enabled); |
| DCHECK(base::MessageLoopForUI::IsCurrent()); |
| + // We want to destroy the autoreleased event ASAP. |
|
tapted
2016/03/10 11:51:19
what happens if we don't? I think since we rely on
themblsha
2016/03/10 17:18:58
It was necessary when I've added an associated obj
|
| + base::mac::ScopedNSAutoreleasePool pool; |
| + |
| std::vector<NSEvent*> events; |
| SynthesizeKeyEventsSequence( |
| window, key, control, shift, alt, command, &events); |
| @@ -235,9 +437,8 @@ bool SendMouseMove(long x, long y) { |
| // platforms. E.g. (0,0) is upper-left. |
| bool SendMouseMoveNotifyWhenDone(long x, long y, const base::Closure& task) { |
| CHECK(g_ui_controls_enabled); |
| - CGFloat screenHeight = |
| - [[[NSScreen screens] firstObject] frame].size.height; |
| - g_mouse_location = NSMakePoint(x, screenHeight - y); // flip! |
| + g_mouse_location = gfx::ScreenPointToNSPoint(gfx::Point(x, y)); // flip! |
| + NSEventSwizzler::Install(); |
| NSWindow* window = WindowAtCurrentMouseLocation(); |
| @@ -246,21 +447,40 @@ bool SendMouseMoveNotifyWhenDone(long x, long y, const base::Closure& task) { |
| pointInWindow = ui::ConvertPointFromScreenToWindow(window, pointInWindow); |
| NSTimeInterval timestamp = TimeIntervalSinceSystemStartup(); |
| + NSEventType event_type = NSMouseMoved; |
| + if (g_mouse_button_down[LEFT]) { |
| + event_type = NSLeftMouseDragged; |
| + } else if (g_mouse_button_down[RIGHT]) { |
| + event_type = NSRightMouseDragged; |
| + } else if (g_mouse_button_down[MIDDLE]) { |
| + event_type = NSOtherMouseDragged; |
| + } |
| + |
| + // We want to destroy the autoreleased event ASAP. |
| + base::mac::ScopedNSAutoreleasePool pool; |
| + |
| NSEvent* event = |
| - [NSEvent mouseEventWithType:NSMouseMoved |
| + [NSEvent mouseEventWithType:event_type |
| location:pointInWindow |
| modifierFlags:0 |
| timestamp:timestamp |
| windowNumber:[window windowNumber] |
| context:nil |
| eventNumber:0 |
| - clickCount:0 |
| - pressure:0.0]; |
| - [[NSApplication sharedApplication] postEvent:event atStart:NO]; |
| + clickCount:(event_type == NSMouseMoved ? 0 : 1) |
| + pressure:(event_type == NSMouseMoved ? 0.0 : 1.0)]; |
| if (!task.is_null()) { |
| - base::MessageLoop::current()->PostTask( |
| - FROM_HERE, base::Bind(&EventQueueWatcher, task)); |
| + EventMonitor::Instance()->NotifyWhenEventIsProcessed(event, task); |
| + } |
| + |
| + gfx::Point gp = gfx::ScreenPointFromNSPoint(g_mouse_location); |
| + CGWarpMouseCursorPosition(CGPointMake(gp.x(), gp.y())); |
| + |
| + if (g_use_cgevents) { |
| + CGEventPost(kCGSessionEventTap, [event CGEvent]); |
| + } else { |
| + [[NSApplication sharedApplication] postEvent:event atStart:NO]; |
| } |
| return true; |
| @@ -280,35 +500,43 @@ bool SendMouseEventsNotifyWhenDone(MouseButton type, int state, |
| return (SendMouseEventsNotifyWhenDone(type, DOWN, base::Closure()) && |
| SendMouseEventsNotifyWhenDone(type, UP, task)); |
| } |
| - NSEventType etype = NSLeftMouseDown; |
| + NSEventType event_type = NSLeftMouseDown; |
| if (type == LEFT) { |
| if (state == UP) { |
| - etype = NSLeftMouseUp; |
| + event_type = NSLeftMouseUp; |
| } else { |
| - etype = NSLeftMouseDown; |
| + event_type = NSLeftMouseDown; |
| } |
| } else if (type == MIDDLE) { |
| if (state == UP) { |
| - etype = NSOtherMouseUp; |
| + event_type = NSOtherMouseUp; |
| } else { |
| - etype = NSOtherMouseDown; |
| + event_type = NSOtherMouseDown; |
| } |
| } else if (type == RIGHT) { |
| if (state == UP) { |
| - etype = NSRightMouseUp; |
| + event_type = NSRightMouseUp; |
| } else { |
| - etype = NSRightMouseDown; |
| + event_type = NSRightMouseDown; |
| } |
| } else { |
| + NOTREACHED(); |
| return false; |
| } |
| + g_mouse_button_down[type] = state == DOWN; |
| + |
| NSWindow* window = WindowAtCurrentMouseLocation(); |
| NSPoint pointInWindow = g_mouse_location; |
| if (window) |
| pointInWindow = ui::ConvertPointFromScreenToWindow(window, pointInWindow); |
| + NSEventSwizzler::Install(); |
| + |
| + // We want to destroy the autoreleased event ASAP. |
| + base::mac::ScopedNSAutoreleasePool pool; |
| + |
| NSEvent* event = |
| - [NSEvent mouseEventWithType:etype |
| + [NSEvent mouseEventWithType:event_type |
| location:pointInWindow |
| modifierFlags:0 |
| timestamp:TimeIntervalSinceSystemStartup() |
| @@ -317,11 +545,15 @@ bool SendMouseEventsNotifyWhenDone(MouseButton type, int state, |
| eventNumber:0 |
| clickCount:1 |
| pressure:(state == DOWN ? 1.0 : 0.0 )]; |
| - [[NSApplication sharedApplication] postEvent:event atStart:NO]; |
| if (!task.is_null()) { |
| - base::MessageLoop::current()->PostTask( |
| - FROM_HERE, base::Bind(&EventQueueWatcher, task)); |
| + EventMonitor::Instance()->NotifyWhenEventIsProcessed(event, task); |
| + } |
| + |
| + if (g_use_cgevents) { |
| + CGEventPost(kCGSessionEventTap, [event CGEvent]); |
| + } else { |
| + [[NSApplication sharedApplication] postEvent:event atStart:NO]; |
| } |
| return true; |
| @@ -341,4 +573,16 @@ bool IsFullKeyboardAccessEnabled() { |
| return [NSApp isFullKeyboardAccessEnabled]; |
| } |
| +void SetSendMouseEventsAsCGEvents(bool enable_cgevents) { |
| + g_use_cgevents = enable_cgevents; |
| +} |
| + |
| +bool SendMouseEventsAsCGEvents() { |
| + return g_use_cgevents; |
| +} |
| + |
| +void NotifyWhenEventIsProcessed(NSEvent* event, const base::Closure& task) { |
| + EventMonitor::Instance()->NotifyWhenEventIsProcessed(event, task); |
| +} |
| + |
| } // namespace ui_controls |