| 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 b7615b6c2fd78eab1ddbb87a351cf33f47c4efd1..a4bf8470d4f727983e14add950269fa1896cb655 100644
|
| --- a/ui/base/test/ui_controls_mac.mm
|
| +++ b/ui/base/test/ui_controls_mac.mm
|
| @@ -5,14 +5,21 @@
|
| #include "ui/base/test/ui_controls.h"
|
|
|
| #import <Cocoa/Cocoa.h>
|
| +#include <memory>
|
| #include <vector>
|
|
|
| #include "base/bind.h"
|
| #include "base/callback.h"
|
| +#import "base/mac/foundation_util.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 "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 +55,23 @@ 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 };
|
|
|
| +// If enabled overrides results returned by +[NSEvent mouseLocation] and
|
| +// -[NSWindow mouseLocationOutsideOfEventStream]
|
| +NSPoint g_mouse_location_override = {0, 0};
|
| +bool g_mouse_location_override_enabled = false;
|
| +
|
| +// 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 +142,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.
|
| void EventQueueWatcher(const base::Closure& task) {
|
| + DCHECK_EQ(dispatch_get_current_queue(), dispatch_get_main_queue())
|
| + << "It should be run on the UI thread, as otherwise it will always "
|
| + "report there are no pending events";
|
| NSEvent* event = [NSApp nextEventMatchingMask:NSAnyEventMask
|
| untilDate:nil
|
| inMode:NSDefaultRunLoopMode
|
| @@ -173,6 +202,188 @@ 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) {
|
| + if (g_use_cgevents) {
|
| + CHECK([[NSApplication sharedApplication] isActive])
|
| + << "If we generate CGEvents and the application is not active, we'll "
|
| + "deadlock waiting for NSMouseMoved events. Use "
|
| + "ui_test_utils::BringBrowserWindowToFront() to activate the "
|
| + "browser window prior to generating CGEvents.";
|
| + }
|
| +
|
| + base::AutoLock auto_lock(lock_);
|
| + tasks_.emplace_back(Task(event, task));
|
| + }
|
| +
|
| + void ProcessingEvent(NSEvent* event) {
|
| + base::AutoLock auto_lock(lock_);
|
| + 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.
|
| + base::MessageLoop::current()->PostTask(
|
| + 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()
|
| + : 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
|
| + base::Lock lock_;
|
| +
|
| + std::unique_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 {
|
| + if (g_mouse_location_override_enabled)
|
| + return g_mouse_location_override;
|
| +
|
| + 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);
|
| + if (g_mouse_location_override_enabled) {
|
| + return ui::ConvertPointFromScreenToWindow(window,
|
| + g_mouse_location_override);
|
| + }
|
| +
|
| + return ui::ConvertPointFromScreenToWindow(window, 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:
|
| + std::unique_ptr<base::mac::ScopedObjCClassSwizzler> mouse_location_swizzler_;
|
| + std::unique_ptr<base::mac::ScopedObjCClassSwizzler>
|
| + pressed_mouse_buttons_swizzler_;
|
| + std::unique_ptr<base::mac::ScopedObjCClassSwizzler>
|
| + mouse_location_outside_of_event_stream_swizzler_;
|
| +};
|
| +} // namespace
|
| +
|
| namespace ui_controls {
|
|
|
| void EnableUIControls() {
|
| @@ -204,6 +415,7 @@ 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());
|
|
|
| @@ -239,9 +451,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();
|
|
|
| @@ -250,21 +461,37 @@ 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;
|
| + }
|
| +
|
| 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;
|
| @@ -284,48 +511,56 @@ 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);
|
|
|
| - NSEvent* event =
|
| - [NSEvent mouseEventWithType:etype
|
| - location:pointInWindow
|
| - modifierFlags:0
|
| - timestamp:TimeIntervalSinceSystemStartup()
|
| - windowNumber:[window windowNumber]
|
| - context:nil
|
| - eventNumber:0
|
| - clickCount:1
|
| - pressure:(state == DOWN ? 1.0 : 0.0 )];
|
| - [[NSApplication sharedApplication] postEvent:event atStart:NO];
|
| + NSEventSwizzler::Install();
|
| +
|
| + NSEvent* event = [NSEvent mouseEventWithType:event_type
|
| + location:pointInWindow
|
| + modifierFlags:0
|
| + timestamp:TimeIntervalSinceSystemStartup()
|
| + windowNumber:[window windowNumber]
|
| + context:nil
|
| + eventNumber:0
|
| + clickCount:1
|
| + pressure:(state == DOWN ? 1.0 : 0.0)];
|
|
|
| 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;
|
| @@ -345,4 +580,21 @@ 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);
|
| +}
|
| +
|
| +void SetMousePositionOverride(bool enable_override, const gfx::Point& p) {
|
| + g_mouse_location_override_enabled = enable_override;
|
| + g_mouse_location_override = gfx::ScreenPointToNSPoint(p); // flip!
|
| +}
|
| +
|
| } // namespace ui_controls
|
|
|