chromium/ui/events/test/cocoa_test_event_utils.mm

// Copyright 2014 The Chromium Authors
// 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/apple/scoped_cftyperef.h"
#include "base/notreached.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 == NSEventTypeOtherMouseUp) {
    // 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 };
    base::apple::ScopedCFTypeRef<CGEventRef> cg_event(CGEventCreateMouseEvent(
        nullptr, 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.get(), static_cast<CGEventFlags>(modifiers));
    NSEvent* event = [NSEvent eventWithCGEvent:cg_event.get()];
    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, NSEventTypeRightMouseDown, window, 1);
}

NSEvent* RightMouseDownAtPoint(NSPoint point) {
  return RightMouseDownAtPointInWindow(point, nil);
}

NSEvent* LeftMouseDownAtPointInWindow(NSPoint point, NSWindow* window) {
  return MouseEventAtPointInWindow(point, NSEventTypeLeftMouseDown, window, 1);
}

NSEvent* LeftMouseDownAtPoint(NSPoint point) {
  return LeftMouseDownAtPointInWindow(point, nil);
}

NSArray<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, NSEventTypeLeftMouseDown,
                                            view.window, clickCount);
  NSEvent* up = MouseEventAtPointInWindow(mid_point, NSEventTypeLeftMouseUp,
                                          view.window, clickCount);
  return @[ down, up ];
}

NSArray<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, NSEventTypeRightMouseDown, view.window, clickCount);
  NSEvent* up = MouseEventAtPointInWindow(mid_point, NSEventTypeRightMouseUp,
                                          view.window, clickCount);
  return @[ 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::apple::ScopedCFTypeRef<CGEventRef> scroll(CGEventCreateScrollWheelEvent(
      nullptr, units, wheel_count, wheel1, wheel2));
  CGEventSetLocation(scroll.get(), ScreenPointFromWindow(window_point, window));

  // Always set event flags, otherwise +[NSEvent eventWithCGEvent:] populates
  // flags from current keyboard state which can make tests flaky.
  CGEventSetFlags(scroll.get(), 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.get(),
                                             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_IN_MIGRATION();
    }
    CGEventSetIntegerValueField(scroll.get(), kCGScrollWheelEventScrollPhase,
                                cg_event_phase);
    CGEventSetIntegerValueField(scroll.get(), kCGScrollWheelEventMomentumPhase,
                                cg_momentum_phase);
  }
  NSEvent* event = AttachWindowToCGEvent(scroll.get(), 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* KeyDownEventWithRepeat() {
  return [NSEvent keyEventWithType:NSEventTypeKeyDown
                          location:NSZeroPoint
                     modifierFlags:0
                         timestamp:TimeIntervalSinceSystemStartup()
                      windowNumber:0
                           context:nil
                        characters:@""
       charactersIgnoringModifiers:@""
                         isARepeat:YES
                           keyCode:0x78];
}

NSEvent* KeyEventWithCharacter(unichar c) {
  return KeyEventWithKeyCode(0, c, NSEventTypeKeyDown, 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:NSEventTypeFlagsChanged
                          location:NSZeroPoint
                     modifierFlags:modifiers
                         timestamp:TimeIntervalSinceSystemStartup()
                      windowNumber:0
                           context:nil
                        characters:@""
       charactersIgnoringModifiers:@""
                         isARepeat:NO
                           keyCode:key_code];
}

static NSEvent* EnterExitEventWithType(NSPoint point,
                                       NSEventType event_type,
                                       NSWindow* window) {
  return [NSEvent enterExitEventWithType:event_type
                                location:point
                           modifierFlags:0
                               timestamp:TimeIntervalSinceSystemStartup()
                            windowNumber:window.windowNumber
                                 context:nil
                             eventNumber:0
                          trackingNumber:0
                                userData:nullptr];
}

NSEvent* EnterEvent(NSPoint point, NSWindow* window) {
  return EnterExitEventWithType(point, NSEventTypeMouseEntered, window);
}

NSEvent* ExitEvent(NSPoint point, NSWindow* window) {
  return EnterExitEventWithType(point, NSEventTypeMouseExited, window);
}

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 & NSEventModifierFlagCapsLock))
    flags |= NSEventModifierFlagShift;

  // Clear caps regardless -- MacKeyCodeForWindowsKeyCode doesn't implement
  // logic to support it.
  flags &= ~NSEventModifierFlagCapsLock;

  // 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 charactersIgnoringModifiers]" 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];

  // Control + [Shift] Tab is special.
  if (keycode == ui::VKEY_TAB && (flags & NSEventModifierFlagControl)) {
    if (flags & NSEventModifierFlagShift) {
      charactersIgnoringModifiers = @"\x19";
    } else {
      charactersIgnoringModifiers = @"\x9";
    }
  }

  NSString* characters;
  // The following were determined empirically on OS X 10.9.
  if (flags & NSEventModifierFlagControl) {
    // If Ctrl is pressed, Cocoa always puts an empty string into |characters|.
    characters = @"";
  } else if (flags & NSEventModifierFlagCommand) {
    // 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];
  } 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 ? NSEventTypeKeyDown : NSEventTypeKeyUp);

  // Modifier keys generate NSEventTypeFlagsChanged event rather than
  // NSEventTypeKeyDown/NSEventTypeKeyUp events.
  if (keycode == ui::VKEY_CONTROL || keycode == ui::VKEY_SHIFT ||
      keycode == ui::VKEY_MENU || keycode == ui::VKEY_COMMAND)
    type = NSEventTypeFlagsChanged;

  // For events other than mouse moved, [event locationInWindow] is
  // UNDEFINED if the event is not NSEventTypeMouseMoved.  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