blob: 979aca2bd2a2800d0f4a475dcc5eb8d1368bfd82 [file] [log] [blame]
// Copyright 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ui/base/test/ui_controls.h"
#import <Cocoa/Cocoa.h>
#include <vector>
#include "base/bind.h"
#include "base/callback.h"
#import "base/mac/foundation_util.h"
#import "base/mac/scoped_objc_class_swizzler.h"
#include "base/stl_util.h"
#include "base/task/current_thread.h"
#include "base/threading/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"
#include "ui/gfx/geometry/point.h"
#import "ui/gfx/mac/coordinate_conversion.h"
// Implementation details: We use [NSApplication sendEvent:] instead
// of [NSApplication postEvent:atStart:] so that the event gets sent
// immediately. This lets us run the post-event task right
// immediately as well. Unfortunately I cannot subclass NSEvent (it's
// probably a class cluster) to allow other easy answers. For
// example, if I could subclass NSEvent, I could run the Task in it's
// dealloc routine (which necessarily happens after the event is
// dispatched). Unlike Linux, Mac does not have message loop
// observer/notification. Unlike windows, I cannot post non-events
// into the event queue. (I can post other kinds of tasks but can't
// guarantee their order with regards to events).
// But [NSApplication sendEvent:] causes a problem when sending mouse click
// events. Because in order to handle mouse drag, when processing a mouse
// click event, the application may want to retrieve the next event
// synchronously by calling NSApplication's nextEventMatchingMask method.
// In this case, [NSApplication sendEvent:] causes deadlock.
// So we need to use [NSApplication postEvent:atStart:] for mouse click
// events. In order to notify the caller correctly after all events has been
// processed, we setup a task to watch for the event queue time to time and
// notify the caller as soon as there is no event in the queue.
//
// TODO(suzhe):
// 1. Investigate why using [NSApplication postEvent:atStart:] for keyboard
// events causes BrowserKeyEventsTest.CommandKeyEvents to fail.
// See http://crbug.com/49270
// 2. On OSX 10.6, [NSEvent addLocalMonitorForEventsMatchingMask:handler:] may
// be used, so that we don't need to poll the event queue time to time.
using cocoa_test_event_utils::SynthesizeKeyEvent;
using cocoa_test_event_utils::TimeIntervalSinceSystemStartup;
namespace {
// 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.
void SynthesizeKeyEventsSequence(NSWindow* window,
ui::KeyboardCode keycode,
bool control,
bool shift,
bool alt,
bool command,
std::vector<NSEvent*>* events) {
NSEvent* event = nil;
NSUInteger flags = 0;
if (control) {
flags |= NSControlKeyMask;
event = SynthesizeKeyEvent(window, true, ui::VKEY_CONTROL, flags);
DCHECK(event);
events->push_back(event);
}
if (shift) {
flags |= NSShiftKeyMask;
event = SynthesizeKeyEvent(window, true, ui::VKEY_SHIFT, flags);
DCHECK(event);
events->push_back(event);
}
if (alt) {
flags |= NSAlternateKeyMask;
event = SynthesizeKeyEvent(window, true, ui::VKEY_MENU, flags);
DCHECK(event);
events->push_back(event);
}
if (command) {
flags |= NSCommandKeyMask;
event = SynthesizeKeyEvent(window, true, ui::VKEY_COMMAND, flags);
DCHECK(event);
events->push_back(event);
}
event = SynthesizeKeyEvent(window, true, keycode, flags);
DCHECK(event);
events->push_back(event);
event = SynthesizeKeyEvent(window, false, keycode, flags);
DCHECK(event);
events->push_back(event);
if (command) {
flags &= ~NSCommandKeyMask;
event = SynthesizeKeyEvent(window, false, ui::VKEY_COMMAND, flags);
DCHECK(event);
events->push_back(event);
}
if (alt) {
flags &= ~NSAlternateKeyMask;
event = SynthesizeKeyEvent(window, false, ui::VKEY_MENU, flags);
DCHECK(event);
events->push_back(event);
}
if (shift) {
flags &= ~NSShiftKeyMask;
event = SynthesizeKeyEvent(window, false, ui::VKEY_SHIFT, flags);
DCHECK(event);
events->push_back(event);
}
if (control) {
flags &= ~NSControlKeyMask;
event = SynthesizeKeyEvent(window, false, ui::VKEY_CONTROL, flags);
DCHECK(event);
events->push_back(event);
}
}
// 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(base::OnceClosure task) {
NSEvent* event = [NSApp nextEventMatchingMask:NSAnyEventMask
untilDate:nil
inMode:NSDefaultRunLoopMode
dequeue:NO];
// If there is still event in the queue, then we need to check again.
if (event) {
base::ThreadTaskRunnerHandle::Get()->PostTask(
FROM_HERE, base::BindOnce(&EventQueueWatcher, std::move(task)));
} else {
base::ThreadTaskRunnerHandle::Get()->PostTask(FROM_HERE, std::move(task));
}
}
// Returns the NSWindow located at |g_mouse_location|. NULL if there is no
// window there, or if the window located there is not owned by the application.
// On Mac, unless dragging, mouse events are sent to the window under the
// cursor. Note that the OS will ignore transparent windows and windows that
// explicitly ignore mouse events.
NSWindow* WindowAtCurrentMouseLocation() {
NSInteger window_number = [NSWindow windowNumberAtPoint:g_mouse_location
belowWindowWithWindowNumber:0];
NSWindow* window =
[[NSApplication sharedApplication] windowWithWindowNumber:window_number];
if (window)
return window;
// It's possible for a window owned by another application to be at that
// location. Cocoa won't provide an NSWindow* for those. Tests should not care
// about other applications, and raising windows in a headless application is
// flaky due to OS restrictions. For tests, hunt through all of this
// application's windows, top to bottom, looking for a good candidate.
NSArray* window_list = [[NSApplication sharedApplication] orderedWindows];
for (window in window_list) {
// Note this skips the extra checks (e.g. fully-transparent windows), that
// +[NSWindow windowNumberAtPoint:] performs. Tests that care about that
// should check separately (the goal here is to minimize flakiness).
if (NSPointInRect(g_mouse_location, [window frame]))
return window;
}
// Note that -[NSApplication orderedWindows] won't include NSPanels. If a test
// uses those, it will need to handle that itself.
return nil;
}
} // namespace
// 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 (size_t i = 0; i < base::size(buttons); ++i) {
if (g_mouse_button_down[buttons[i]])
result |= (1 << i);
}
return result;
}
@end
namespace {
// Swizzles several Cocoa functions that are used to directly get mouse state,
// so that they will return the current simulated mouse position and pressed
// mouse buttons.
class MockNSEventClassMethods {
public:
static void Init() {
static MockNSEventClassMethods* swizzler = nullptr;
if (!swizzler) {
swizzler = new MockNSEventClassMethods();
}
}
private:
MockNSEventClassMethods()
: mouse_location_swizzler_([NSEvent class],
[FakeNSEventTestingDonor class],
@selector(mouseLocation)),
pressed_mouse_buttons_swizzler_([NSEvent class],
[FakeNSEventTestingDonor class],
@selector(pressedMouseButtons)) {}
base::mac::ScopedObjCClassSwizzler mouse_location_swizzler_;
base::mac::ScopedObjCClassSwizzler pressed_mouse_buttons_swizzler_;
DISALLOW_COPY_AND_ASSIGN(MockNSEventClassMethods);
};
} // namespace
namespace ui_controls {
void EnableUIControls() {
g_ui_controls_enabled = true;
MockNSEventClassMethods::Init();
}
bool IsUIControlsEnabled() {
return g_ui_controls_enabled;
}
bool SendKeyPress(gfx::NativeWindow window,
ui::KeyboardCode key,
bool control,
bool shift,
bool alt,
bool command) {
CHECK(g_ui_controls_enabled);
return SendKeyPressNotifyWhenDone(window, key, control, shift, alt, command,
base::OnceClosure());
}
// Win and Linux implement a SendKeyPress() this as a
// SendKeyPressAndRelease(), so we should as well (despite the name).
bool SendKeyPressNotifyWhenDone(gfx::NativeWindow window,
ui::KeyboardCode key,
bool control,
bool shift,
bool alt,
bool command,
base::OnceClosure task) {
CHECK(g_ui_controls_enabled);
DCHECK(base::CurrentUIThread::IsSet());
std::vector<NSEvent*> events;
SynthesizeKeyEventsSequence(window.GetNativeNSWindow(), key, control, shift,
alt, command, &events);
// TODO(suzhe): Using [NSApplication postEvent:atStart:] here causes
// BrowserKeyEventsTest.CommandKeyEvents to fail. See http://crbug.com/49270
// But using [NSApplication sendEvent:] should be safe for keyboard events,
// because until now, no code wants to retrieve the next event when handling
// a keyboard event.
for (std::vector<NSEvent*>::iterator iter = events.begin();
iter != events.end(); ++iter)
[[NSApplication sharedApplication] sendEvent:*iter];
if (!task.is_null()) {
base::ThreadTaskRunnerHandle::Get()->PostTask(
FROM_HERE, base::BindOnce(&EventQueueWatcher, std::move(task)));
}
return true;
}
bool SendMouseMove(int x, int y) {
CHECK(g_ui_controls_enabled);
return SendMouseMoveNotifyWhenDone(x, y, base::OnceClosure());
}
// Input position is in screen coordinates. However, NSMouseMoved
// events require them window-relative, so we adjust. We *DO* flip
// the coordinate space, so input events can be the same for all
// platforms. E.g. (0,0) is upper-left.
bool SendMouseMoveNotifyWhenDone(int x, int y, base::OnceClosure task) {
CHECK(g_ui_controls_enabled);
g_mouse_location = gfx::ScreenPointToNSPoint(gfx::Point(x, y)); // flip!
NSWindow* window = WindowAtCurrentMouseLocation();
NSPoint pointInWindow = g_mouse_location;
if (window)
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:event_type
location:pointInWindow
modifierFlags:0
timestamp:timestamp
windowNumber:[window windowNumber]
context:nil
eventNumber:0
clickCount:event_type == NSMouseMoved ? 0 : 1
pressure:event_type == NSMouseMoved ? 0.0 : 1.0];
[[NSApplication sharedApplication] postEvent:event atStart:NO];
if (!task.is_null()) {
base::ThreadTaskRunnerHandle::Get()->PostTask(
FROM_HERE, base::BindOnce(&EventQueueWatcher, std::move(task)));
}
return true;
}
bool SendMouseEvents(MouseButton type,
int button_state,
int accelerator_state) {
CHECK(g_ui_controls_enabled);
return SendMouseEventsNotifyWhenDone(type, button_state, base::OnceClosure(),
accelerator_state);
}
bool SendMouseEventsNotifyWhenDone(MouseButton type,
int button_state,
base::OnceClosure task,
int accelerator_state) {
CHECK(g_ui_controls_enabled);
// Handle the special case of mouse clicking (UP | DOWN) case.
if (button_state == (UP | DOWN)) {
return (SendMouseEventsNotifyWhenDone(type, DOWN, base::OnceClosure(),
accelerator_state) &&
SendMouseEventsNotifyWhenDone(type, UP, std::move(task),
accelerator_state));
}
NSEventType event_type = NSLeftMouseDown;
if (type == LEFT) {
if (button_state == UP) {
event_type = NSLeftMouseUp;
} else {
event_type = NSLeftMouseDown;
}
} else if (type == MIDDLE) {
if (button_state == UP) {
event_type = NSOtherMouseUp;
} else {
event_type = NSOtherMouseDown;
}
} else if (type == RIGHT) {
if (button_state == UP) {
event_type = NSRightMouseUp;
} else {
event_type = NSRightMouseDown;
}
} else {
NOTREACHED();
return false;
}
g_mouse_button_down[type] = button_state == DOWN;
NSWindow* window = WindowAtCurrentMouseLocation();
NSPoint pointInWindow = g_mouse_location;
if (window)
pointInWindow = ui::ConvertPointFromScreenToWindow(window, pointInWindow);
// Process the accelerator key state.
NSEventModifierFlags modifier = 0;
if (accelerator_state & kShift)
modifier |= NSEventModifierFlagShift;
if (accelerator_state & kControl)
modifier |= NSEventModifierFlagControl;
if (accelerator_state & kAlt)
modifier |= NSEventModifierFlagOption;
if (accelerator_state & kCommand)
modifier |= NSEventModifierFlagCommand;
NSEvent* event =
[NSEvent mouseEventWithType:event_type
location:pointInWindow
modifierFlags:modifier
timestamp:TimeIntervalSinceSystemStartup()
windowNumber:[window windowNumber]
context:nil
eventNumber:0
clickCount:1
pressure:button_state == DOWN ? 1.0 : 0.0];
[[NSApplication sharedApplication] postEvent:event atStart:NO];
if (!task.is_null()) {
base::ThreadTaskRunnerHandle::Get()->PostTask(
FROM_HERE, base::BindOnce(&EventQueueWatcher, std::move(task)));
}
return true;
}
bool SendMouseClick(MouseButton type) {
CHECK(g_ui_controls_enabled);
return SendMouseEventsNotifyWhenDone(type, UP | DOWN, base::OnceClosure());
}
bool IsFullKeyboardAccessEnabled() {
return [NSApp isFullKeyboardAccessEnabled];
}
} // namespace ui_controls