blob: 181dc3c649b9e39e913fc2a4033cacb2e9d5f677 [file] [log] [blame]
// Copyright 2014 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.
#import "ui/events/test/cocoa_test_event_utils.h"
#include <stdint.h>
#include "base/mac/scoped_cftyperef.h"
#include "base/time/time.h"
#include "ui/events/base_event_utils.h"
#import "ui/events/keycodes/keyboard_code_conversion_mac.h"
namespace cocoa_test_event_utils {
CGPoint ScreenPointFromWindow(NSPoint window_point, NSWindow* window) {
NSRect window_rect = NSMakeRect(window_point.x, window_point.y, 0, 0);
NSPoint screen_point = window
? [window convertRectToScreen:window_rect].origin
: window_rect.origin;
CGFloat primary_screen_height =
NSHeight([[[NSScreen screens] firstObject] frame]);
screen_point.y = primary_screen_height - screen_point.y;
return NSPointToCGPoint(screen_point);
}
NSEvent* AttachWindowToCGEvent(CGEventRef event, NSWindow* window) {
// -[NSEvent locationInWindow] changes from screen coordinates to window
// coordinates when a window is attached to the mouse event. -[NSEvent
// eventWithCGEvent:] handles the Quartz -> AppKit coordinate flipping, but
// not the offset. Unfortunately -eventWithCGEvent: uses the *screen* height
// to flip, not the window height (it doesn't know about the window yet). So
// to get the correct -[NSEvent locationInWindow], anticipate the bogus screen
// flip that eventWithCGEvent: will do. This is yuck, but NSEvent does not
// provide a way to generate test scrolling events any other way. Fortunately,
// once you do all the algebra, all we need to do here is offset by the window
// origin, but in different directions for x/y.
CGPoint location = CGEventGetLocation(event);
location.y += NSMinY([window frame]);
location.x -= NSMinX([window frame]);
CGEventSetLocation(event, location);
// These CGEventFields were made public in the 10.7 SDK, but don't help to
// populate the -[NSEvent window] pointer when creating an event with
// +[NSEvent eventWithCGEvent:]. Set that separately, using reflection.
CGEventSetIntegerValueField(event, kCGMouseEventWindowUnderMousePointer,
[window windowNumber]);
CGEventSetIntegerValueField(
event, kCGMouseEventWindowUnderMousePointerThatCanHandleThisEvent,
[window windowNumber]);
// CGEventTimestamp is nanoseconds since system startup as a 64-bit integer.
// Use EventTimeForNow() so that it can be mocked for tests.
CGEventTimestamp timestamp =
(ui::EventTimeForNow() - base::TimeTicks()).InMicroseconds() *
base::Time::kNanosecondsPerMicrosecond;
CGEventSetTimestamp(event, timestamp);
NSEvent* ns_event = [NSEvent eventWithCGEvent:event];
DCHECK_EQ(nil, [ns_event window]); // Verify assumptions.
[ns_event setValue:window forKey:@"_window"];
DCHECK_EQ(window, [ns_event window]);
return ns_event;
}
NSEvent* MouseEventAtPoint(NSPoint point, NSEventType type,
NSUInteger modifiers) {
if (type == NSOtherMouseUp) {
// To synthesize middle clicks we need to create a CGEvent with the
// "center" button flags so that our resulting NSEvent will have the
// appropriate buttonNumber field. NSEvent provides no way to create a
// mouse event with a buttonNumber directly.
CGPoint location = { point.x, point.y };
CGEventRef cg_event = CGEventCreateMouseEvent(NULL, kCGEventOtherMouseUp,
location,
kCGMouseButtonCenter);
// Also specify the modifiers for the middle click case. This makes this
// test resilient to external modifiers being pressed.
CGEventSetFlags(cg_event, static_cast<CGEventFlags>(modifiers));
NSEvent* event = [NSEvent eventWithCGEvent:cg_event];
CFRelease(cg_event);
return event;
}
return [NSEvent mouseEventWithType:type
location:point
modifierFlags:modifiers
timestamp:TimeIntervalSinceSystemStartup()
windowNumber:0
context:nil
eventNumber:0
clickCount:1
pressure:1.0];
}
NSEvent* MouseEventWithType(NSEventType type, NSUInteger modifiers) {
return MouseEventAtPoint(NSZeroPoint, type, modifiers);
}
NSEvent* MouseEventAtPointInWindow(NSPoint point,
NSEventType type,
NSWindow* window,
NSUInteger clickCount) {
return [NSEvent mouseEventWithType:type
location:point
modifierFlags:0
timestamp:TimeIntervalSinceSystemStartup()
windowNumber:[window windowNumber]
context:nil
eventNumber:0
clickCount:clickCount
pressure:1.0];
}
NSEvent* RightMouseDownAtPointInWindow(NSPoint point, NSWindow* window) {
return MouseEventAtPointInWindow(point, NSRightMouseDown, window, 1);
}
NSEvent* RightMouseDownAtPoint(NSPoint point) {
return RightMouseDownAtPointInWindow(point, nil);
}
NSEvent* LeftMouseDownAtPointInWindow(NSPoint point, NSWindow* window) {
return MouseEventAtPointInWindow(point, NSLeftMouseDown, window, 1);
}
NSEvent* LeftMouseDownAtPoint(NSPoint point) {
return LeftMouseDownAtPointInWindow(point, nil);
}
std::pair<NSEvent*,NSEvent*> MouseClickInView(NSView* view,
NSUInteger clickCount) {
const NSRect bounds = [view convertRect:[view bounds] toView:nil];
const NSPoint mid_point = NSMakePoint(NSMidX(bounds), NSMidY(bounds));
NSEvent* down = MouseEventAtPointInWindow(mid_point, NSLeftMouseDown,
[view window], clickCount);
NSEvent* up = MouseEventAtPointInWindow(mid_point, NSLeftMouseUp,
[view window], clickCount);
return std::make_pair(down, up);
}
std::pair<NSEvent*, NSEvent*> RightMouseClickInView(NSView* view,
NSUInteger clickCount) {
const NSRect bounds = [view convertRect:[view bounds] toView:nil];
const NSPoint mid_point = NSMakePoint(NSMidX(bounds), NSMidY(bounds));
NSEvent* down = MouseEventAtPointInWindow(mid_point, NSRightMouseDown,
[view window], clickCount);
NSEvent* up = MouseEventAtPointInWindow(mid_point, NSRightMouseUp,
[view window], clickCount);
return std::make_pair(down, up);
}
NSEvent* TestScrollEvent(NSPoint window_point,
NSWindow* window,
CGFloat delta_x,
CGFloat delta_y,
bool has_precise_deltas,
NSEventPhase event_phase,
NSEventPhase momentum_phase) {
const uint32_t wheel_count = 2;
int32_t wheel1 = static_cast<int>(delta_y);
int32_t wheel2 = static_cast<int>(delta_x);
CGScrollEventUnit units =
has_precise_deltas ? kCGScrollEventUnitPixel : kCGScrollEventUnitLine;
base::ScopedCFTypeRef<CGEventRef> scroll(CGEventCreateScrollWheelEvent(
nullptr, units, wheel_count, wheel1, wheel2));
CGEventSetLocation(scroll, ScreenPointFromWindow(window_point, window));
// Always set event flags, otherwise +[NSEvent eventWithCGEvent:] populates
// flags from current keyboard state which can make tests flaky.
CGEventSetFlags(scroll, static_cast<CGEventFlags>(0));
if (has_precise_deltas) {
// kCGScrollWheelEventIsContinuous is -[NSEvent hasPreciseScrollingDeltas].
// CGEventTypes.h says it should be non-zero for pixel-based scrolling.
// Verify that CGEventCreateScrollWheelEvent() set it.
DCHECK_EQ(1, CGEventGetIntegerValueField(scroll,
kCGScrollWheelEventIsContinuous));
}
// Don't set phase information when neither.
if (event_phase != NSEventPhaseNone || momentum_phase != NSEventPhaseNone) {
// AppKit conflates CGScrollPhase (bitmask flags) and CGMomentumScrollPhase
// (an enum) into NSEventPhase, where it is used for both -[NSEvent phase]
// and -[NSEvent momentumPhase]. Do a reverse mapping here.
int cg_event_phase = 0;
if (event_phase & NSEventPhaseBegan)
cg_event_phase |= kCGScrollPhaseBegan;
if (event_phase & NSEventPhaseChanged)
cg_event_phase |= kCGScrollPhaseChanged;
if (event_phase & NSEventPhaseEnded)
cg_event_phase |= kCGScrollPhaseEnded;
if (event_phase & NSEventPhaseCancelled)
cg_event_phase |= kCGScrollPhaseCancelled;
if (event_phase & NSEventPhaseMayBegin)
cg_event_phase |= kCGScrollPhaseMayBegin;
CGMomentumScrollPhase cg_momentum_phase = kCGMomentumScrollPhaseNone;
switch (momentum_phase) {
case NSEventPhaseNone:
break;
case NSEventPhaseBegan:
cg_momentum_phase = kCGMomentumScrollPhaseBegin;
break;
case NSEventPhaseChanged:
cg_momentum_phase = kCGMomentumScrollPhaseContinue;
break;
case NSEventPhaseEnded:
cg_momentum_phase = kCGMomentumScrollPhaseEnd;
break;
default:
// Those are the only 4 options for CGMomentumScrollPhase. If something
// else was provided it should probably never appear on an NSEvent.
NOTREACHED();
}
CGEventSetIntegerValueField(scroll, kCGScrollWheelEventScrollPhase,
cg_event_phase);
CGEventSetIntegerValueField(scroll, kCGScrollWheelEventMomentumPhase,
cg_momentum_phase);
}
NSEvent* event = AttachWindowToCGEvent(scroll, window);
DCHECK_EQ(has_precise_deltas, [event hasPreciseScrollingDeltas]);
DCHECK_EQ(event_phase, [event phase]);
DCHECK_EQ(momentum_phase, [event momentumPhase]);
DCHECK_EQ(window_point.x, [event locationInWindow].x);
DCHECK_EQ(window_point.y, [event locationInWindow].y);
return event;
}
NSEvent* KeyEventWithCharacter(unichar c) {
return KeyEventWithKeyCode(0, c, NSKeyDown, 0);
}
NSEvent* KeyEventWithType(NSEventType event_type, NSUInteger modifiers) {
return KeyEventWithKeyCode(0x78, 'x', event_type, modifiers);
}
NSEvent* KeyEventWithKeyCode(unsigned short key_code,
unichar c,
NSEventType event_type,
NSUInteger modifiers) {
NSString* chars = [NSString stringWithCharacters:&c length:1];
return [NSEvent keyEventWithType:event_type
location:NSZeroPoint
modifierFlags:modifiers
timestamp:TimeIntervalSinceSystemStartup()
windowNumber:0
context:nil
characters:chars
charactersIgnoringModifiers:chars
isARepeat:NO
keyCode:key_code];
}
NSEvent* KeyEventWithModifierOnly(unsigned short key_code,
NSUInteger modifiers) {
return [NSEvent keyEventWithType:NSFlagsChanged
location:NSZeroPoint
modifierFlags:modifiers
timestamp:TimeIntervalSinceSystemStartup()
windowNumber:0
context:nil
characters:@""
charactersIgnoringModifiers:@""
isARepeat:NO
keyCode:key_code];
}
static NSEvent* EnterExitEventWithType(NSEventType event_type) {
return [NSEvent enterExitEventWithType:event_type
location:NSZeroPoint
modifierFlags:0
timestamp:TimeIntervalSinceSystemStartup()
windowNumber:0
context:nil
eventNumber:0
trackingNumber:0
userData:NULL];
}
NSEvent* EnterEvent() {
return EnterExitEventWithType(NSMouseEntered);
}
NSEvent* ExitEvent() {
return EnterExitEventWithType(NSMouseExited);
}
NSEvent* OtherEventWithType(NSEventType event_type) {
return [NSEvent otherEventWithType:event_type
location:NSZeroPoint
modifierFlags:0
timestamp:TimeIntervalSinceSystemStartup()
windowNumber:0
context:nil
subtype:0
data1:0
data2:0];
}
NSTimeInterval TimeIntervalSinceSystemStartup() {
base::TimeDelta time_elapsed = ui::EventTimeForNow() - base::TimeTicks();
return time_elapsed.InSecondsF();
}
NSEvent* SynthesizeKeyEvent(NSWindow* window,
bool keyDown,
ui::KeyboardCode keycode,
NSUInteger flags,
ui::DomKey dom_key) {
// If caps lock is set for an alpha keycode, treat it as if shift was pressed.
// Note on Mac (unlike other platforms) shift while caps is down does not go
// back to lowercase.
if (keycode >= ui::VKEY_A && keycode <= ui::VKEY_Z &&
(flags & NSAlphaShiftKeyMask))
flags |= NSShiftKeyMask;
// Clear caps regardless -- MacKeyCodeForWindowsKeyCode doesn't implement
// logic to support it.
flags &= ~NSAlphaShiftKeyMask;
// Call sites may generate unicode character events with an undefined
// keycode. Since it's not feasible to determine the correct keycode for
// each unicode character, we use a dummy keycode corresponding to key 'A'.
if (dom_key.IsCharacter() && keycode == ui::VKEY_UNKNOWN)
keycode = ui::VKEY_A;
unichar character;
unichar shifted_character;
int macKeycode = ui::MacKeyCodeForWindowsKeyCode(
keycode, flags, &shifted_character, &character);
if (macKeycode < 0)
return nil;
// If an explicit unicode character is provided, use that instead of the one
// derived from the keycode.
if (dom_key.IsCharacter())
shifted_character = dom_key.ToCharacter();
// Note that, in line with AppKit's documentation (and tracing "real" events),
// -[NSEvent charactersIngoringModifiers]" are "the characters generated by
// the receiving key event as if no modifier key (except for Shift)".
// So |charactersIgnoringModifiers| uses |shifted_character|.
NSString* charactersIgnoringModifiers =
[[[NSString alloc] initWithCharacters:&shifted_character
length:1] autorelease];
// Control + [Shift] Tab is special.
if (keycode == ui::VKEY_TAB && (flags & NSControlKeyMask)) {
if (flags & NSShiftKeyMask) {
charactersIgnoringModifiers = @"\x19";
} else {
charactersIgnoringModifiers = @"\x9";
}
}
NSString* characters;
// The following were determined empirically on OSX 10.9.
if (flags & NSControlKeyMask) {
// If Ctrl is pressed, Cocoa always puts an empty string into |characters|.
characters = [NSString string];
} else if (flags & NSCommandKeyMask) {
// If Cmd is pressed, Cocoa puts a lowercase character into |characters|,
// regardless of Shift. If, however, Alt is also pressed then shift *is*
// preserved, but re-mappings for Alt are not implemented. Although we still
// need to support Alt for things like Alt+Left/Right which don't care.
characters =
[[[NSString alloc] initWithCharacters:&character length:1] autorelease];
} else {
// If just Shift or nothing is pressed, |characters| will match
// |charactersIgnoringModifiers|. Alt puts a special character into
// |characters| (not |charactersIgnoringModifiers|), but they're not mapped
// here.
characters = charactersIgnoringModifiers;
}
NSEventType type = (keyDown ? NSKeyDown : NSKeyUp);
// Modifier keys generate NSFlagsChanged event rather than
// NSKeyDown/NSKeyUp events.
if (keycode == ui::VKEY_CONTROL || keycode == ui::VKEY_SHIFT ||
keycode == ui::VKEY_MENU || keycode == ui::VKEY_COMMAND)
type = NSFlagsChanged;
// For events other than mouse moved, [event locationInWindow] is
// UNDEFINED if the event is not NSMouseMoved. Thus, the (0,0)
// location should be fine.
NSEvent* event = [NSEvent keyEventWithType:type
location:NSZeroPoint
modifierFlags:flags
timestamp:TimeIntervalSinceSystemStartup()
windowNumber:[window windowNumber]
context:nil
characters:characters
charactersIgnoringModifiers:charactersIgnoringModifiers
isARepeat:NO
keyCode:(unsigned short)macKeycode];
return event;
}
} // namespace cocoa_test_event_utils