chromium/ash/accessibility/mouse_keys/mouse_keys_controller.cc

// Copyright 2024 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 "ash/accessibility/mouse_keys/mouse_keys_controller.h"

#include "ash/display/window_tree_host_manager.h"
#include "ash/public/cpp/window_tree_host_lookup.h"
#include "ash/shell.h"
#include "ash/wm/window_util.h"
#include "base/containers/flat_map.h"
#include "base/logging.h"
#include "ui/accessibility/accessibility_features.h"
#include "ui/aura/client/cursor_client.h"
#include "ui/aura/window.h"
#include "ui/aura/window_tree_host.h"
#include "ui/events/event_sink.h"
#include "ui/events/event_utils.h"
#include "ui/wm/core/coordinate_conversion.h"

namespace ash {

namespace {
const base::flat_map<ui::DomCode, MouseKeysController::MouseKey>
    kLeftHandedKeys({
        {ui::DomCode::US_W, MouseKeysController::kKeyClick},
        {ui::DomCode::US_V, MouseKeysController::kKeyDoubleClick},
        {ui::DomCode::US_Z, MouseKeysController::kKeyDragStart},
        {ui::DomCode::US_C, MouseKeysController::kKeyDragStop},
        {ui::DomCode::DIGIT1, MouseKeysController::kKeyUpLeft},
        {ui::DomCode::DIGIT2, MouseKeysController::kKeyUp},
        {ui::DomCode::DIGIT3, MouseKeysController::kKeyUpRight},
        {ui::DomCode::US_Q, MouseKeysController::kKeyLeft},
        {ui::DomCode::US_E, MouseKeysController::kKeyRight},
        {ui::DomCode::US_A, MouseKeysController::kKeyDownLeft},
        {ui::DomCode::US_S, MouseKeysController::kKeyDown},
        {ui::DomCode::US_D, MouseKeysController::kKeyDownRight},
        {ui::DomCode::US_X, MouseKeysController::kKeySelectNextButton},
    });

const base::flat_map<ui::DomCode, MouseKeysController::MouseKey>
    kRightHandedKeys({
        {ui::DomCode::US_I, MouseKeysController::kKeyClick},
        {ui::DomCode::SLASH, MouseKeysController::kKeyDoubleClick},
        {ui::DomCode::US_M, MouseKeysController::kKeyDragStart},
        {ui::DomCode::PERIOD, MouseKeysController::kKeyDragStop},
        {ui::DomCode::DIGIT7, MouseKeysController::kKeyUpLeft},
        {ui::DomCode::DIGIT8, MouseKeysController::kKeyUp},
        {ui::DomCode::DIGIT9, MouseKeysController::kKeyUpRight},
        {ui::DomCode::US_U, MouseKeysController::kKeyLeft},
        {ui::DomCode::US_O, MouseKeysController::kKeyRight},
        {ui::DomCode::US_J, MouseKeysController::kKeyDownLeft},
        {ui::DomCode::US_K, MouseKeysController::kKeyDown},
        {ui::DomCode::US_L, MouseKeysController::kKeyDownRight},
        {ui::DomCode::COMMA, MouseKeysController::kKeySelectNextButton},
    });

const base::flat_map<ui::DomCode, MouseKeysController::MouseKey> kNumPadKeys({
    {ui::DomCode::NUMPAD5, MouseKeysController::kKeyClick},
    {ui::DomCode::NUMPAD_ADD, MouseKeysController::kKeyDoubleClick},
    {ui::DomCode::NUMPAD0, MouseKeysController::kKeyDragStart},
    {ui::DomCode::NUMPAD_DECIMAL, MouseKeysController::kKeyDragStop},
    {ui::DomCode::NUMPAD7, MouseKeysController::kKeyUpLeft},
    {ui::DomCode::NUMPAD8, MouseKeysController::kKeyUp},
    {ui::DomCode::NUMPAD9, MouseKeysController::kKeyUpRight},
    {ui::DomCode::NUMPAD4, MouseKeysController::kKeyLeft},
    {ui::DomCode::NUMPAD6, MouseKeysController::kKeyRight},
    {ui::DomCode::NUMPAD1, MouseKeysController::kKeyDownLeft},
    {ui::DomCode::NUMPAD2, MouseKeysController::kKeyDown},
    {ui::DomCode::NUMPAD3, MouseKeysController::kKeyDownRight},
    {ui::DomCode::NUMPAD_DIVIDE, MouseKeysController::kKeySelectLeftButton},
    {ui::DomCode::NUMPAD_SUBTRACT, MouseKeysController::kKeySelectRightButton},
    {ui::DomCode::NUMPAD_MULTIPLY, MouseKeysController::kKeySelectBothButtons},
});

}  // namespace

MouseKeysController::MouseKeysController() {
  SetMaxSpeed(kDefaultMaxSpeed);
  for (int c = 0; c < kKeyCount; ++c) {
    pressed_keys_[c] = false;
  }
  Shell::Get()->AddAccessibilityEventHandler(
      this, AccessibilityEventHandlerManager::HandlerType::kMouseKeys);
}

MouseKeysController::~MouseKeysController() {
  Shell* shell = Shell::Get();
  shell->RemoveAccessibilityEventHandler(this);
}

void MouseKeysController::Toggle() {
  paused_ = !paused_;
  if (paused_) {
    // Reset everything when pausing.
    ResetMovement();
    dragging_ = false;
  }
}

bool MouseKeysController::RewriteEvent(const ui::Event& event) {
  if (!enabled_ || !event.IsKeyEvent()) {
    return false;
  }

  int modifier_mask = ui::EF_SHIFT_DOWN | ui::EF_CONTROL_DOWN |
                      ui::EF_ALT_DOWN | ui::EF_COMMAND_DOWN |
                      ui::EF_IS_EXTENDED_KEY;
  event_flags_ = event.flags() & modifier_mask;

  if (paused_) {
    return false;
  }

  CenterMouseIfUninitialized();

  // Check primary keyboard keys.
  const ui::KeyEvent* key_event = event.AsKeyEvent();
  if (use_primary_keys_) {
    auto mappings = left_handed_ ? kLeftHandedKeys : kRightHandedKeys;
    for (auto mapping : mappings) {
      if (CheckFlagsAndMaybeSendEvent(*key_event, mapping.first,
                                      mapping.second)) {
        return true;
      }
    }
  }

  // Check num pad.
  for (auto mapping : kNumPadKeys) {
    if (CheckFlagsAndMaybeSendEvent(*key_event, mapping.first,
                                    mapping.second)) {
      return true;
    }
  }

  return false;
}

void MouseKeysController::OnMouseEvent(ui::MouseEvent* event) {
  bool is_synthesized = event->IsSynthesized() ||
                        event->source_device_id() == ui::ED_UNKNOWN_DEVICE;
  if (is_synthesized || event->type() != ui::EventType::kMouseMoved) {
    return;
  }
  if (event->target()) {
    last_mouse_position_dips_ = event->target()->GetScreenLocation(*event);
  }
}

void MouseKeysController::SendMouseEventToLocation(ui::EventType type,
                                                   const gfx::Point& location,
                                                   int flags) {
  int event_flags = event_flags_ | flags;
  int button = 0;
  switch (current_mouse_button_) {
    case kLeft:
      button = ui::EF_LEFT_MOUSE_BUTTON;
      break;
    case kRight:
      button = ui::EF_RIGHT_MOUSE_BUTTON;
      break;
    case kBoth:
      button = ui::EF_LEFT_MOUSE_BUTTON | ui::EF_RIGHT_MOUSE_BUTTON;
      break;
  }
  aura::Window* root_window = window_util::GetRootWindowAt(location);
  DCHECK(root_window)
      << "Root window not found while attempting mouse keys click.";
  gfx::Point location_in_pixels(location);
  ::wm::ConvertPointFromScreen(root_window, &location_in_pixels);
  aura::WindowTreeHost* host = root_window->GetHost();
  host->ConvertDIPToPixels(&location_in_pixels);
  ui::MouseEvent event(type, location_in_pixels, location_in_pixels,
                       ui::EventTimeForNow(), event_flags | button, button);

  (void)host->GetEventSink()->OnEventFromSource(&event);
}

void MouseKeysController::MoveMouse(const gfx::Vector2d& move_delta_dip) {
  gfx::Point location = last_mouse_position_dips_ + move_delta_dip;

  // Update the cursor position; this will generate a synthetic mouse event that
  // will pass through the standard event flow.
  const display::Display& display =
      display::Screen::GetScreen()->GetDisplayNearestPoint(location);
  auto* host = ash::GetWindowTreeHostForDisplay(display.id());
  if (!host) {
    return;
  }

  // Show the cursor if needed.
  auto* cursor_client = aura::client::GetCursorClient(host->window());
  if (cursor_client && !cursor_client->IsCursorVisible()) {
    cursor_client->ShowCursor();
  }

  host->MoveCursorToLocationInDIP(location);
  if (dragging_) {
    SendMouseEventToLocation(ui::EventType::kMouseDragged, location);
  }
  last_mouse_position_dips_ = location;
}

void MouseKeysController::CenterMouseIfUninitialized() {
  if (last_mouse_position_dips_ == gfx::Point(-1, -1)) {
    aura::Window* root_window = Shell::GetPrimaryRootWindow();
    DCHECK(root_window)
        << "Root window not found while attempting to center mouse.";
    last_mouse_position_dips_ = root_window->bounds().CenterPoint();
  }
}

bool MouseKeysController::CheckFlagsAndMaybeSendEvent(
    const ui::KeyEvent& key_event,
    ui::DomCode input,
    MouseKey output) {
  if (key_event.code() != input) {
    return false;
  }

  // Ignore key repeats but still consume them.
  if (key_event.flags() & ui::EF_IS_REPEAT) {
    return true;
  }

  // All KeyEvents are either EventType::kKeyPressed or EventType::kKeyReleased.
  if (key_event.type() == ui::EventType::kKeyPressed) {
    PressKey(output);
  } else {
    DCHECK_EQ(key_event.type(), ui::EventType::kKeyReleased);
    ReleaseKey(output);
  }
  return true;
}

void MouseKeysController::PressKey(MouseKey key) {
  pressed_keys_[key] = true;
  switch (key) {
    case kKeyUpLeft:
    case kKeyUp:
    case kKeyUpRight:
    case kKeyLeft:
    case kKeyRight:
    case kKeyDownLeft:
    case kKeyDown:
    case kKeyDownRight:
      RefreshVelocity();
      break;
    case kKeyClick:
    case kKeyDragStart:
      if (!dragging_) {
        SendMouseEventToLocation(ui::EventType::kMousePressed,
                                 last_mouse_position_dips_);
        dragging_ = true;
      }
      break;
    case kKeyDragStop:
      if (dragging_) {
        SendMouseEventToLocation(ui::EventType::kMouseReleased,
                                 last_mouse_position_dips_);
        dragging_ = false;
      }
      break;
    case kKeyDoubleClick:
      if (current_mouse_button_ == kLeft) {
        SendMouseEventToLocation(ui::EventType::kMousePressed,
                                 last_mouse_position_dips_);
        SendMouseEventToLocation(ui::EventType::kMouseReleased,
                                 last_mouse_position_dips_);
        SendMouseEventToLocation(ui::EventType::kMousePressed,
                                 last_mouse_position_dips_,
                                 ui::EF_IS_DOUBLE_CLICK);
        SendMouseEventToLocation(ui::EventType::kMouseReleased,
                                 last_mouse_position_dips_,
                                 ui::EF_IS_DOUBLE_CLICK);
      }
      break;
    case kKeySelectLeftButton:
      current_mouse_button_ = kLeft;
      break;
    case kKeySelectRightButton:
      current_mouse_button_ = kRight;
      break;
    case kKeySelectBothButtons:
      current_mouse_button_ = kBoth;
      break;
    case kKeySelectNextButton:
      SelectNextButton();
      break;
    case kKeyCount:
      NOTREACHED();
  }
}

void MouseKeysController::ReleaseKey(MouseKey key) {
  pressed_keys_[key] = false;
  switch (key) {
    case kKeyUpLeft:
    case kKeyUp:
    case kKeyUpRight:
    case kKeyLeft:
    case kKeyRight:
    case kKeyDownLeft:
    case kKeyDown:
    case kKeyDownRight:
      RefreshVelocity();
      break;
    case kKeyClick:
      if (dragging_) {
        SendMouseEventToLocation(ui::EventType::kMouseReleased,
                                 last_mouse_position_dips_);
        dragging_ = false;
      }
      break;
    case kKeyDragStart:
    case kKeyDragStop:
    case kKeyDoubleClick:
    case kKeySelectLeftButton:
    case kKeySelectRightButton:
    case kKeySelectBothButtons:
    case kKeySelectNextButton:
      break;
    case kKeyCount:
      NOTREACHED();
  }
}

void MouseKeysController::SelectNextButton() {
  switch (current_mouse_button_) {
    case kLeft:
      current_mouse_button_ = kRight;
      break;
    case kRight:
      current_mouse_button_ = kBoth;
      break;
    case kBoth:
      current_mouse_button_ = kLeft;
      break;
  }
}

void MouseKeysController::RefreshVelocity() {
  int x_direction = 0;
  int y_direction = 0;

  if (pressed_keys_[kKeyUpLeft] || pressed_keys_[kKeyLeft] ||
      pressed_keys_[kKeyDownLeft]) {
    // Left takes precedence.
    x_direction = -1;
  } else if (pressed_keys_[kKeyUpRight] || pressed_keys_[kKeyRight] ||
             pressed_keys_[kKeyDownRight]) {
    x_direction = 1;
  }

  if (pressed_keys_[kKeyUpLeft] || pressed_keys_[kKeyUp] ||
      pressed_keys_[kKeyUpRight]) {
    // Up takes precedence.
    y_direction = -1;
  } else if (pressed_keys_[kKeyDownLeft] || pressed_keys_[kKeyDown] ||
             pressed_keys_[kKeyDownRight]) {
    y_direction = 1;
  }

  // Set the base movement.
  move_direction_ = gfx::Vector2d(x_direction, y_direction);

  if (x_direction == 0 && y_direction == 0) {
    // Reset everything if there is no movement.
    ResetMovement();
    return;
  }

  if (speed_ == 0) {
    // If movement is just starting, initialize everything.
    if (acceleration_ == 0) {
      // If there is no acceleration, start at the max speed.
      speed_ = max_speed_;
    } else {
      speed_ = kBaseSpeedDIPPerSecond * kUpdateFrequencyInSeconds;
    }
    update_timer_.Start(FROM_HERE, base::Seconds(kUpdateFrequencyInSeconds),
                        this, &MouseKeysController::UpdateState);
  }

  UpdateState();
}

void MouseKeysController::UpdateState() {
  MoveMouse(gfx::Vector2d(move_direction_.x() * speed_,
                          move_direction_.y() * speed_));
  double acceleration = acceleration_ * kBaseAccelerationDIPPerSecondSquared *
                        kUpdateFrequencyInSeconds;
  speed_ = std::clamp(speed_ + acceleration, 0.0, max_speed_);
}

void MouseKeysController::ResetMovement() {
  speed_ = 0;
  if (update_timer_.IsRunning()) {
    update_timer_.Stop();
  }
}

}  // namespace ash