chromium/chrome/browser/resources/chromeos/accessibility/common/event_generator.ts

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

import {KeyCode} from './key_code.js';
import {TestImportManager} from './testing/test_import_manager.js';

export interface MouseClickParams {
  // The amount of time to wait between press and release.
  delayMs?: number;
  mouseButton?: chrome.accessibilityPrivate.SyntheticMouseEventButton;
}

interface MouseClick {
  x: number;
  y: number;
  params: MouseClickParams;
}

/** Functions to send synthetic key and mouse events. */
export class EventGenerator {
  static midMouseClickButton:
      chrome.accessibilityPrivate.SyntheticMouseEventButton|undefined;
  static mouseClickQueue: MouseClick[] = [];

  /**
   * Sends a single key stroke (down and up) with the given key code and
   *     keyboard modifiers (whether or not CTRL, ALT, SEARCH, and SHIFT are
   *     being held).
   * @param useRewriters If true, uses rewriters for the key event;
   *     only allowed if used from Dictation. Otherwise indicates that rewriters
   *     should be skipped.
   */
  static sendKeyPress(
      keyCode: KeyCode,
      modifiers: chrome.accessibilityPrivate.SyntheticKeyboardModifiers = {},
      useRewriters = false): void {
    EventGenerator.sendKeyDown(keyCode, modifiers, useRewriters);
    EventGenerator.sendKeyUp(keyCode, modifiers, useRewriters);
  }

  /**
   * Sends a single key down event with the given key code and
   *     keyboard modifiers (whether or not CTRL, ALT, SEARCH, and SHIFT are
   *     being held).
   * @param useRewriters If true, uses rewriters for the key event;
   *     only allowed if used from Dictation. Otherwise indicates that rewriters
   *     should be skipped.
   */
  static sendKeyDown(
      keyCode: KeyCode,
      modifiers: chrome.accessibilityPrivate.SyntheticKeyboardModifiers = {},
      useRewriters = false): void {
    const type = chrome.accessibilityPrivate.SyntheticKeyboardEventType.KEYDOWN;
    chrome.accessibilityPrivate.sendSyntheticKeyEvent(
        {type, keyCode, modifiers}, useRewriters);
  }

  /**
   * Sends a single key up event with the given key code and
   *     keyboard modifiers (whether or not CTRL, ALT, SEARCH, and SHIFT are
   *     being held).
   * @param useRewriters If true, uses rewriters for the key event;
   *     only allowed if used from Dictation. Otherwise indicates that rewriters
   *     should be skipped.
   */
  static sendKeyUp(
      keyCode: KeyCode,
      modifiers: chrome.accessibilityPrivate.SyntheticKeyboardModifiers = {},
      useRewriters = false): void {
    const type = chrome.accessibilityPrivate.SyntheticKeyboardEventType.KEYUP;
    chrome.accessibilityPrivate.sendSyntheticKeyEvent(
        {type, keyCode, modifiers}, useRewriters);
  }

  /**
   * Sends a synthetic mouse press (a mouse press and and a mouse release)
   *     to simulate a mouse click.
   */
  static sendMouseClick(x: number, y: number, params: MouseClickParams = {
    delayMs: 0,
    mouseButton: chrome.accessibilityPrivate.SyntheticMouseEventButton.LEFT,
  }): void {
    const delayMs = params.delayMs ? params.delayMs : 0;
    const mouseButton = params.mouseButton ?
        params.mouseButton :
        chrome.accessibilityPrivate.SyntheticMouseEventButton.LEFT;

    if (EventGenerator.midMouseClickButton !== undefined) {
      // Add it to the queue for later.
      EventGenerator.mouseClickQueue.push({x, y, params});
      return;
    }

    EventGenerator.sendMousePress(x, y, mouseButton);

    if (delayMs > 0) {
      setTimeout(() => EventGenerator.sendMouseRelease(x, y), params.delayMs);
    } else {
      EventGenerator.sendMouseRelease(x, y);
    }
  }

  /**
   * Sends a synthetic mouse press event, if we are not in the middle of a
   * mouse click event. If we are in the middle of a mouse click, returns
   * false as no press event was sent.
   */
  static sendMousePress(
      x: number, y: number,
      mouseButton: chrome.accessibilityPrivate.SyntheticMouseEventButton,
      isDoubleClick = false): boolean {
    if (EventGenerator.midMouseClickButton !== undefined) {
      return false;
    }

    EventGenerator.midMouseClickButton = mouseButton;

    // chrome.accessibilityPrivate.sendSyntheticMouseEvent only accepts
    // integers.
    x = Math.round(x);
    y = Math.round(y);

    const type = chrome.accessibilityPrivate.SyntheticMouseEventType.PRESS;
    chrome.accessibilityPrivate.sendSyntheticMouseEvent(
        {type, x, y, mouseButton, isDoubleClick});
    return true;
  }

  /**
   * Sends a synthetic mouse release event, if we are mid mouse click.
   * Will start the next click from the click queue if there is one.
   * If we are not mid mouse click, returns false as no release event
   * was sent.
   */
  static sendMouseRelease(x: number, y: number, isDoubleClick = false):
      boolean {
    if (EventGenerator.midMouseClickButton === undefined) {
      return false;
    }

    // chrome.accessibilityPrivate.sendSyntheticMouseEvent only accepts
    // integers.
    x = Math.round(x);
    y = Math.round(y);

    const type = chrome.accessibilityPrivate.SyntheticMouseEventType.RELEASE;
    chrome.accessibilityPrivate.sendSyntheticMouseEvent({
      type,
      x,
      y,
      mouseButton: EventGenerator.midMouseClickButton,
      isDoubleClick,
    });

    EventGenerator.midMouseClickButton = undefined;

    if (EventGenerator.mouseClickQueue.length > 0) {
      const {x, y, params} = EventGenerator.mouseClickQueue.shift()!;
      EventGenerator.sendMouseClick(x, y, params);
    }

    return true;
  }

  /** Sends a synthetic mouse event to simulate a move or drag event. */
  static sendMouseMove(x: number, y: number, touchAccessibility = false): void {
    const type = EventGenerator.midMouseClickButton !== undefined ?
        chrome.accessibilityPrivate.SyntheticMouseEventType.DRAG :
        chrome.accessibilityPrivate.SyntheticMouseEventType.MOVE;
    chrome.accessibilityPrivate.sendSyntheticMouseEvent({
      type,
      x,
      y,
      touchAccessibility,
      mouseButton: EventGenerator.midMouseClickButton,
    });
  }
}

TestImportManager.exportForTesting(EventGenerator);