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 15cdb7963b019d11df0e749c13239fb6a40faafe..c9a47faad8d40141743f2a05148b322e341c5b19 100644 |
--- a/ui/base/test/ui_controls_mac.mm |
+++ b/ui/base/test/ui_controls_mac.mm |
@@ -9,9 +9,18 @@ |
#include "base/bind.h" |
#include "base/callback.h" |
+#include "base/debug/stack_trace.h" |
+#include "base/mac/foundation_util.h" |
+#include "base/mac/scoped_nsautorelease_pool.h" |
+#include "base/mac/scoped_nsobject.h" |
tapted
2016/03/04 06:33:08
the objc headers should be #imported
themblsha
2016/03/09 17:40:22
Done.
|
+#include "base/mac/scoped_objc_class_swizzler.h" |
#include "base/message_loop/message_loop.h" |
+#include "base/thread_task_runner_handle.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" |
+#include "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 |
@@ -47,9 +56,14 @@ 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 }; |
+bool g_mouse_button_down[3] = { false, false, false }; |
bool g_ui_controls_enabled = false; |
@@ -121,6 +135,12 @@ 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. |
void EventQueueWatcher(const base::Closure& task) { |
@@ -172,6 +192,177 @@ 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. |
+ return [event_ type] == [event type] /*&& |
+ CGPointEqualToPoint([event_ locationInWindow], |
+ [event locationInWindow]) */; |
+ } |
+ |
+ 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_; |
+}; |
+ |
+// 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() { |
@@ -202,6 +393,9 @@ bool SendKeyPressNotifyWhenDone(gfx::NativeWindow window, |
CHECK(g_ui_controls_enabled); |
DCHECK(base::MessageLoopForUI::IsCurrent()); |
+ // we want to destroy the autoreleased event ASAP |
+ base::mac::ScopedNSAutoreleasePool pool; |
+ |
std::vector<NSEvent*> events; |
SynthesizeKeyEventsSequence( |
window, key, control, shift, alt, command, &events); |
@@ -234,9 +428,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(); |
@@ -245,21 +438,40 @@ bool SendMouseMoveNotifyWhenDone(long x, long y, const base::Closure& task) { |
pointInWindow = [window convertScreenToBase:pointInWindow]; |
NSTimeInterval timestamp = TimeIntervalSinceSystemStartup(); |
+ NSEventType etype = NSMouseMoved; |
+ if (g_mouse_button_down[LEFT]) { |
+ etype = NSLeftMouseDragged; |
+ } else if (g_mouse_button_down[RIGHT]) { |
+ etype = NSRightMouseDragged; |
+ } else if (g_mouse_button_down[MIDDLE]) { |
+ etype = NSOtherMouseDragged; |
+ } |
+ |
+ // we want to destroy the autoreleased event ASAP |
+ base::mac::ScopedNSAutoreleasePool pool; |
+ |
NSEvent* event = |
- [NSEvent mouseEventWithType:NSMouseMoved |
+ [NSEvent mouseEventWithType:etype |
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:(etype == NSMouseMoved ? 0 : 1) |
+ pressure:(etype == 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; |
@@ -283,20 +495,26 @@ bool SendMouseEventsNotifyWhenDone(MouseButton type, int state, |
if (type == LEFT) { |
if (state == UP) { |
etype = NSLeftMouseUp; |
+ g_mouse_button_down[LEFT] = false; |
} else { |
etype = NSLeftMouseDown; |
+ g_mouse_button_down[LEFT] = true; |
} |
} else if (type == MIDDLE) { |
if (state == UP) { |
etype = NSOtherMouseUp; |
+ g_mouse_button_down[MIDDLE] = false; |
} else { |
etype = NSOtherMouseDown; |
+ g_mouse_button_down[MIDDLE] = true; |
} |
} else if (type == RIGHT) { |
if (state == UP) { |
etype = NSRightMouseUp; |
+ g_mouse_button_down[RIGHT] = false; |
} else { |
etype = NSRightMouseDown; |
+ g_mouse_button_down[RIGHT] = true; |
} |
} else { |
return false; |
@@ -306,6 +524,11 @@ bool SendMouseEventsNotifyWhenDone(MouseButton type, int state, |
if (window) |
pointInWindow = [window convertScreenToBase:pointInWindow]; |
+ NSEventSwizzler::Install(); |
+ |
+ // we want to destroy the autoreleased event ASAP |
+ base::mac::ScopedNSAutoreleasePool pool; |
+ |
NSEvent* event = |
[NSEvent mouseEventWithType:etype |
location:pointInWindow |
@@ -316,11 +539,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; |
@@ -340,4 +567,12 @@ bool IsFullKeyboardAccessEnabled() { |
return [NSApp isFullKeyboardAccessEnabled]; |
} |
+void SetSendMouseEventsAsCGEvents(bool enable_cgevents) { |
+ g_use_cgevents = enable_cgevents; |
+} |
+ |
+bool SendMouseEventsAsCGEvents() { |
+ return g_use_cgevents; |
+} |
+ |
} // namespace ui_controls |