chromium/ui/base/test/ui_controls_mac.mm

// Copyright 2013 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#ifdef UNSAFE_BUFFERS_BUILD
// TODO(crbug.com/40285824): Remove this and convert code to safer constructs.
#pragma allow_unsafe_buffers
#endif

#include "ui/base/test/ui_controls.h"

#import <Cocoa/Cocoa.h>

#include <vector>

#import "base/apple/foundation_util.h"
#import "base/apple/scoped_objc_class_swizzler.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/task/current_thread.h"
#import "base/task/single_thread_task_runner.h"
#include "base/task/single_thread_task_runner.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,
                                 int key_event_types,
                                 int accelerator_state,
                                 std::vector<NSEvent*>* events) {
  NSEvent* event = nil;
  NSUInteger flags = 0;
  if (key_event_types & ui_controls::kKeyPress) {
    if (accelerator_state & ui_controls::kControl) {
      flags |= NSEventModifierFlagControl;
      event = SynthesizeKeyEvent(window, true, ui::VKEY_CONTROL, flags);
      DCHECK(event);
      events->push_back(event);
    }
    if (accelerator_state & ui_controls::kShift) {
      flags |= NSEventModifierFlagShift;
      event = SynthesizeKeyEvent(window, true, ui::VKEY_SHIFT, flags);
      DCHECK(event);
      events->push_back(event);
    }
    if (accelerator_state & ui_controls::kAlt) {
      flags |= NSEventModifierFlagOption;
      event = SynthesizeKeyEvent(window, true, ui::VKEY_MENU, flags);
      DCHECK(event);
      events->push_back(event);
    }
    if (accelerator_state & ui_controls::kCommand) {
      flags |= NSEventModifierFlagCommand;
      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);
  }

  if (key_event_types & ui_controls::kKeyRelease) {
    event = SynthesizeKeyEvent(window, false, keycode, flags);
    DCHECK(event);
    events->push_back(event);

    if (accelerator_state & ui_controls::kCommand) {
      flags &= ~NSEventModifierFlagCommand;
      event = SynthesizeKeyEvent(window, false, ui::VKEY_COMMAND, flags);
      DCHECK(event);
      events->push_back(event);
    }
    if (accelerator_state & ui_controls::kAlt) {
      flags &= ~NSEventModifierFlagOption;
      event = SynthesizeKeyEvent(window, false, ui::VKEY_MENU, flags);
      DCHECK(event);
      events->push_back(event);
    }
    if (accelerator_state & ui_controls::kShift) {
      flags &= ~NSEventModifierFlagShift;
      event = SynthesizeKeyEvent(window, false, ui::VKEY_SHIFT, flags);
      DCHECK(event);
      events->push_back(event);
    }
    if (accelerator_state & ui_controls::kControl) {
      flags &= ~NSEventModifierFlagControl;
      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:NSEventMaskAny
                                      untilDate:nil
                                         inMode:NSDefaultRunLoopMode
                                        dequeue:NO];
  // If there is still event in the queue, then we need to check again.
  if (event) {
    base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
        FROM_HERE, base::BindOnce(&EventQueueWatcher, std::move(task)));
  } else {
    base::SingleThreadTaskRunner::GetCurrentDefault()->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 < std::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();
    }
  }

  MockNSEventClassMethods(const MockNSEventClassMethods&) = delete;
  MockNSEventClassMethods& operator=(const MockNSEventClassMethods&) = delete;

 private:
  MockNSEventClassMethods()
      : mouse_location_swizzler_([NSEvent class],
                                 [FakeNSEventTestingDonor class],
                                 @selector(mouseLocation)),
        pressed_mouse_buttons_swizzler_([NSEvent class],
                                        [FakeNSEventTestingDonor class],
                                        @selector(pressedMouseButtons)) {}

  base::apple::ScopedObjCClassSwizzler mouse_location_swizzler_;
  base::apple::ScopedObjCClassSwizzler pressed_mouse_buttons_swizzler_;
};

}  // namespace

namespace ui_controls {

void EnableUIControls() {
  g_ui_controls_enabled = true;
  MockNSEventClassMethods::Init();
}

bool IsUIControlsEnabled() {
  return g_ui_controls_enabled;
}

void ResetUIControlsIfEnabled() {}

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());
}

// The implementation in ui_controls_aura.cc sends key press *and* release, so
// this implementation does the same.
bool SendKeyPressNotifyWhenDone(gfx::NativeWindow window,
                                ui::KeyboardCode key,
                                bool control,
                                bool shift,
                                bool alt,
                                bool command,
                                base::OnceClosure task,
                                KeyEventType wait_for) {
  // This doesn't time out if `window` is deleted before the key release events
  // are dispatched, so it's fine to ignore `wait_for` and always wait for key
  // release events.
  CHECK(g_ui_controls_enabled);
  return SendKeyEventsNotifyWhenDone(
      window, key, kKeyPress | kKeyRelease, std::move(task),
      GenerateAcceleratorState(control, shift, alt, command));
}

bool SendKeyEvents(gfx::NativeWindow window,
                   ui::KeyboardCode key,
                   int key_event_types,
                   int accelerator_state) {
  CHECK(g_ui_controls_enabled);
  return SendKeyEventsNotifyWhenDone(window, key, key_event_types,
                                     base::OnceClosure(), accelerator_state);
}

bool SendKeyEventsNotifyWhenDone(gfx::NativeWindow window,
                                 ui::KeyboardCode key,
                                 int key_event_types,
                                 base::OnceClosure task,
                                 int accelerator_state) {
  CHECK(g_ui_controls_enabled);
  DCHECK(base::CurrentUIThread::IsSet());

  std::vector<NSEvent*> events;
  SynthesizeKeyEventsSequence(window.GetNativeNSWindow(), key, key_event_types,
                              accelerator_state, &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::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
        FROM_HERE, base::BindOnce(&EventQueueWatcher, std::move(task)));
  }

  return true;
}

bool SendMouseMove(int x, int y, gfx::NativeWindow window_hint) {
  CHECK(g_ui_controls_enabled);
  return SendMouseMoveNotifyWhenDone(x, y, base::OnceClosure(), window_hint);
}

// Input position is in screen coordinates.  However, NSEventTypeMouseMoved
// 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,
                                 gfx::NativeWindow window_hint) {
  CHECK(g_ui_controls_enabled);
  g_mouse_location = gfx::ScreenPointToNSPoint(gfx::Point(x, y));  // flip!

  NSWindow* window = window_hint ? window_hint.GetNativeNSWindow()
                                 : WindowAtCurrentMouseLocation();

  NSPoint pointInWindow = g_mouse_location;
  if (window) {
    pointInWindow = [window convertPointFromScreen:pointInWindow];
  }
  NSTimeInterval timestamp = TimeIntervalSinceSystemStartup();

  NSEventType event_type = NSEventTypeMouseMoved;
  if (g_mouse_button_down[LEFT]) {
    event_type = NSEventTypeLeftMouseDragged;
  } else if (g_mouse_button_down[RIGHT]) {
    event_type = NSEventTypeRightMouseDragged;
  } else if (g_mouse_button_down[MIDDLE]) {
    event_type = NSEventTypeOtherMouseDragged;
  }

  NSEvent* event = [NSEvent
      mouseEventWithType:event_type
                location:pointInWindow
           modifierFlags:0
               timestamp:timestamp
            windowNumber:[window windowNumber]
                 context:nil
             eventNumber:0
              clickCount:event_type == NSEventTypeMouseMoved ? 0 : 1
                pressure:event_type == NSEventTypeMouseMoved ? 0.0 : 1.0];
  [[NSApplication sharedApplication] postEvent:event atStart:NO];

  if (!task.is_null()) {
    base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
        FROM_HERE, base::BindOnce(&EventQueueWatcher, std::move(task)));
  }

  return true;
}

bool SendMouseEvents(MouseButton type,
                     int button_state,
                     int accelerator_state,
                     gfx::NativeWindow window_hint) {
  CHECK(g_ui_controls_enabled);
  return SendMouseEventsNotifyWhenDone(type, button_state, base::OnceClosure(),
                                       accelerator_state, window_hint);
}

bool SendMouseEventsNotifyWhenDone(MouseButton type,
                                   int button_state,
                                   base::OnceClosure task,
                                   int accelerator_state,
                                   gfx::NativeWindow window_hint) {
  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, window_hint) &&
            SendMouseEventsNotifyWhenDone(type, UP, std::move(task),
                                          accelerator_state, window_hint));
  }
  NSEventType event_type = NSEventTypeLeftMouseDown;
  if (type == LEFT) {
    if (button_state == UP) {
      event_type = NSEventTypeLeftMouseUp;
    } else {
      event_type = NSEventTypeLeftMouseDown;
    }
  } else if (type == MIDDLE) {
    if (button_state == UP) {
      event_type = NSEventTypeOtherMouseUp;
    } else {
      event_type = NSEventTypeOtherMouseDown;
    }
  } else {
    CHECK_EQ(type, RIGHT);
    if (button_state == UP) {
      event_type = NSEventTypeRightMouseUp;
    } else {
      event_type = NSEventTypeRightMouseDown;
    }
  }
  g_mouse_button_down[type] = button_state == DOWN;

  NSWindow* window = window_hint ? window_hint.GetNativeNSWindow()
                                 : WindowAtCurrentMouseLocation();

  NSPoint pointInWindow = g_mouse_location;
  if (window) {
    pointInWindow = [window convertPointFromScreen: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::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
        FROM_HERE, base::BindOnce(&EventQueueWatcher, std::move(task)));
  }

  return true;
}

bool SendMouseClick(MouseButton type, gfx::NativeWindow window_hint) {
  CHECK(g_ui_controls_enabled);
  return SendMouseEventsNotifyWhenDone(type, UP | DOWN, base::OnceClosure(),
                                       kNoAccelerator, window_hint);
}

bool IsFullKeyboardAccessEnabled() {
  return [NSApp isFullKeyboardAccessEnabled];
}

}  // namespace ui_controls