chromium/chrome/browser/resources/chromeos/accessibility/common/action_fulfillment/macros/repeatable_key_press_macro.ts

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

import {EventGenerator} from '../../event_generator.js';
import {KeyCode} from '../../key_code.js';
import {TestImportManager} from '../../testing/test_import_manager.js';
import {Context, ContextChecker} from '../context_checker.js';
import {InputController} from '../input_controller.js';

import {CheckContextResult, Macro, MacroError, RunMacroResult} from './macro.js';
import {MacroName} from './macro_names.js';

/**
 * Abstract class that executes a macro using a key press which can optionally
 * be repeated.
 * @abstract
 */
export class RepeatableKeyPressMacro extends Macro {
  private repeat_: number;
  /**
   * @param macroName The name of the macro.
   * @param repeat The number of times to repeat the key press. May be 3 or '3'.
   */
  constructor(
      macroName: MacroName, repeat: string|number, checker?: ContextChecker) {
    super(macroName, checker);

    this.repeat_ = parseInt(String(repeat), /*base=*/ 10);
  }

  override checkContext(): CheckContextResult {
    const checkContextResult = super.checkContext();
    if (!checkContextResult.canTryAction) {
      return checkContextResult;
    }

    if (isNaN(this.repeat_)) {
      // This might occur if the numbers grammar did not recognize the
      // spoken number, so we could get a string like "three" instead of
      // the number 3.
      return this.createFailureCheckContextResult_(
          MacroError.INVALID_USER_INTENT, Context.INVALID_INPUT);
    }

    return this.createSuccessCheckContextResult_();
  }

  override run(): RunMacroResult {
    for (let i = 0; i < this.repeat_; i++) {
      this.doKeyPress();
    }
    return this.createRunMacroResult_(/*isSuccess=*/ true);
  }

  doKeyPress(): void {}
}

/** Macro to delete by character. */
export class DeletePreviousCharacterMacro extends RepeatableKeyPressMacro {
  /** @param repeat The number of characters to delete. */
  constructor(inputController: InputController, repeat: number = 1) {
    super(
        MacroName.DELETE_PREV_CHAR, repeat,
        new ContextChecker(inputController).add(Context.EMPTY_EDITABLE));
  }

  override doKeyPress(): void {
    EventGenerator.sendKeyPress(KeyCode.BACK);
  }
}

/** Macro to navigate to the previous character. */
export class NavPreviousCharMacro extends RepeatableKeyPressMacro {
  private isRTLLocale_: boolean;
  /** @param repeat The number of characters to move. */
  constructor(
      inputController: InputController, isRTLLocale: boolean,
      repeat: number = 1) {
    super(
        MacroName.NAV_PREV_CHAR, repeat,
        new ContextChecker(inputController).add(Context.EMPTY_EDITABLE));
    this.isRTLLocale_ = isRTLLocale;
  }

  override doKeyPress(): void {
    EventGenerator.sendKeyPress(
        this.isRTLLocale_ ? KeyCode.RIGHT : KeyCode.LEFT);
  }
}

/** Macro to navigate to the next character. */
export class NavNextCharMacro extends RepeatableKeyPressMacro {
  private isRTLLocale_: boolean;
  /** @param repeat The number of characters to move. */
  constructor(
      inputController: InputController, isRTLLocale: boolean,
      repeat: number = 1) {
    super(
        MacroName.NAV_NEXT_CHAR, repeat,
        new ContextChecker(inputController).add(Context.EMPTY_EDITABLE));
    this.isRTLLocale_ = isRTLLocale;
  }

  override doKeyPress(): void {
    EventGenerator.sendKeyPress(
        this.isRTLLocale_ ? KeyCode.LEFT : KeyCode.RIGHT);
  }
}

/** Macro to navigate to the previous line. */
export class NavPreviousLineMacro extends RepeatableKeyPressMacro {
  /** @param repeat The number of lines to move. */
  constructor(inputController: InputController, repeat: number = 1) {
    super(
        MacroName.NAV_PREV_LINE, repeat,
        new ContextChecker(inputController).add(Context.EMPTY_EDITABLE));
  }

  override doKeyPress(): void {
    EventGenerator.sendKeyPress(KeyCode.UP);
  }
}

/** Macro to navigate to the next line. */
export class NavNextLineMacro extends RepeatableKeyPressMacro {
  /** @param repeat The number of lines to move. */
  constructor(inputController: InputController, repeat: number = 1) {
    super(
        MacroName.NAV_NEXT_LINE, repeat,
        new ContextChecker(inputController).add(Context.EMPTY_EDITABLE));
  }

  override doKeyPress(): void {
    EventGenerator.sendKeyPress(KeyCode.DOWN);
  }
}

/** Macro to copy selected text. */
export class CopySelectedTextMacro extends RepeatableKeyPressMacro {
  constructor(inputController: InputController) {
    super(
        MacroName.COPY_SELECTED_TEXT, /*repeat=*/ 1,
        new ContextChecker(inputController)
            .add(Context.EMPTY_EDITABLE)
            .add(Context.NO_SELECTION));
  }

  override doKeyPress(): void {
    EventGenerator.sendKeyPress(KeyCode.C, {ctrl: true});
  }
}

/** Macro to paste text. */
export class PasteTextMacro extends RepeatableKeyPressMacro {
  constructor() {
    super(MacroName.PASTE_TEXT, /*repeat=*/ 1);
  }

  override doKeyPress(): void {
    EventGenerator.sendKeyPress(KeyCode.V, {ctrl: true});
  }
}

/** Macro to cut selected text. */
export class CutSelectedTextMacro extends RepeatableKeyPressMacro {
  constructor(inputController: InputController) {
    super(
        MacroName.CUT_SELECTED_TEXT, /*repeat=*/ 1,
        new ContextChecker(inputController)
            .add(Context.EMPTY_EDITABLE)
            .add(Context.NO_SELECTION));
  }

  override doKeyPress(): void {
    EventGenerator.sendKeyPress(KeyCode.X, {ctrl: true});
  }
}

/** Macro to undo a text editing action. */
export class UndoTextEditMacro extends RepeatableKeyPressMacro {
  constructor() {
    super(MacroName.UNDO_TEXT_EDIT, /*repeat=*/ 1);
  }

  override doKeyPress(): void {
    EventGenerator.sendKeyPress(KeyCode.Z, {ctrl: true});
  }
}

/** Macro to redo a text editing action. */
export class RedoActionMacro extends RepeatableKeyPressMacro {
  constructor() {
    super(MacroName.REDO_ACTION, /*repeat=*/ 1);
  }

  override doKeyPress(): void {
    EventGenerator.sendKeyPress(KeyCode.Z, {ctrl: true, shift: true});
  }
}

/** Macro to select all text. */
export class SelectAllTextMacro extends RepeatableKeyPressMacro {
  constructor(inputController: InputController) {
    super(
        MacroName.SELECT_ALL_TEXT, /*repeat=*/ 1,
        new ContextChecker(inputController).add(Context.EMPTY_EDITABLE));
  }

  override doKeyPress(): void {
    EventGenerator.sendKeyPress(KeyCode.A, {ctrl: true});
  }
}

/** Macro to unselect text. */
export class UnselectTextMacro extends RepeatableKeyPressMacro {
  private isRTLLocale_: boolean;
  constructor(inputController: InputController, isRTLLocale: boolean) {
    super(
        MacroName.UNSELECT_TEXT, /*repeat=*/ 1,
        new ContextChecker(inputController)
            .add(Context.EMPTY_EDITABLE)
            .add(Context.NO_SELECTION));
    this.isRTLLocale_ = isRTLLocale;
  }

  override doKeyPress(): void {
    EventGenerator.sendKeyPress(
        this.isRTLLocale_ ? KeyCode.LEFT : KeyCode.RIGHT);
  }
}

/** Macro to delete the previous word. */
export class DeletePrevWordMacro extends RepeatableKeyPressMacro {
  /** @param repeat The number of words to delete. */
  constructor(inputController: InputController, repeat = 1) {
    super(
        MacroName.DELETE_PREV_WORD, repeat,
        new ContextChecker(inputController).add(Context.EMPTY_EDITABLE));
  }

  override doKeyPress(): void {
    EventGenerator.sendKeyPress(KeyCode.BACK, {ctrl: true});
  }
}

/** Macro to navigate to the next word. */
export class NavNextWordMacro extends RepeatableKeyPressMacro {
  private isRTLLocale_: boolean;
  /** @param repeat The number of words to move. */
  constructor(
      inputController: InputController, isRTLLocale: boolean, repeat = 1) {
    super(
        MacroName.NAV_NEXT_WORD, repeat,
        new ContextChecker(inputController).add(Context.EMPTY_EDITABLE));
    this.isRTLLocale_ = isRTLLocale;
  }

  override doKeyPress(): void {
    EventGenerator.sendKeyPress(
        this.isRTLLocale_ ? KeyCode.LEFT : KeyCode.RIGHT, {ctrl: true});
  }
}

/** Macro to navigate to the previous word. */
export class NavPrevWordMacro extends RepeatableKeyPressMacro {
  private isRTLLocale_: boolean;
  /** @param repeat The number of words to move. */
  constructor(
      inputController: InputController, isRTLLocale: boolean, repeat = 1) {
    super(
        MacroName.NAV_PREV_WORD, repeat,
        new ContextChecker(inputController).add(Context.EMPTY_EDITABLE));
    this.isRTLLocale_ = isRTLLocale;
  }

  override doKeyPress(): void {
    EventGenerator.sendKeyPress(
        this.isRTLLocale_ ? KeyCode.RIGHT : KeyCode.LEFT, {ctrl: true});
  }
}

/** Macro to delete all text in input field. */
export class DeleteAllText extends RepeatableKeyPressMacro {
  constructor(inputController: InputController) {
    super(
        MacroName.DELETE_ALL_TEXT, 1,
        new ContextChecker(inputController).add(Context.EMPTY_EDITABLE));
  }

  override doKeyPress(): void {
    EventGenerator.sendKeyPress(KeyCode.A, {ctrl: true});
    EventGenerator.sendKeyPress(KeyCode.BACK);
  }
}

/** Macro to move the cursor to the start of the input field. */
export class NavStartText extends RepeatableKeyPressMacro {
  constructor(inputController: InputController) {
    super(
        MacroName.NAV_START_TEXT, 1,
        new ContextChecker(inputController).add(Context.EMPTY_EDITABLE));
  }

  override doKeyPress(): void {
    // TODO(b/259397131): Migrate this implementation to use
    // chrome.automation.setDocumentSelection.
    EventGenerator.sendKeyPress(KeyCode.HOME, {ctrl: true});
  }
}

/** Macro to move the cursor to the end of the input field. */
export class NavEndText extends RepeatableKeyPressMacro {
  constructor(inputController: InputController) {
    super(
        MacroName.NAV_END_TEXT, 1,
        new ContextChecker(inputController).add(Context.EMPTY_EDITABLE));
  }

  override doKeyPress(): void {
    // TODO(b/259397131): Migrate this implementation to use
    // chrome.automation.setDocumentSelection.
    EventGenerator.sendKeyPress(KeyCode.END, {ctrl: true});
  }
}

/** Macro to select the previous word in the input field. */
export class SelectPrevWord extends RepeatableKeyPressMacro {
  /** @param repeat The number of previous words to select. */
  constructor(inputController: InputController, repeat = 1) {
    super(
        MacroName.SELECT_PREV_WORD, repeat,
        new ContextChecker(inputController).add(Context.EMPTY_EDITABLE));
  }

  override doKeyPress(): void {
    EventGenerator.sendKeyPress(KeyCode.LEFT, {ctrl: true, shift: true});
  }
}

/** Macro to select the next word in the input field. */
export class SelectNextWord extends RepeatableKeyPressMacro {
  /** @param repeat The number of next words to select. */
  constructor(inputController: InputController, repeat = 1) {
    super(
        MacroName.SELECT_NEXT_WORD, repeat,
        new ContextChecker(inputController).add(Context.EMPTY_EDITABLE));
  }

  override doKeyPress(): void {
    EventGenerator.sendKeyPress(KeyCode.RIGHT, {ctrl: true, shift: true});
  }
}

/** Macro to select the next character in the input field. */
export class SelectNextChar extends RepeatableKeyPressMacro {
  /** @param repeat The number of next characters to select. */
  constructor(inputController: InputController, repeat = 1) {
    super(
        MacroName.SELECT_NEXT_CHAR, repeat,
        new ContextChecker(inputController).add(Context.EMPTY_EDITABLE));
  }

  override doKeyPress(): void {
    EventGenerator.sendKeyPress(KeyCode.RIGHT, {shift: true});
  }
}

/** Macro to select the previous character in the input field. */
export class SelectPrevChar extends RepeatableKeyPressMacro {
  /** @param repeat The number of previous characters to select. */
  constructor(inputController: InputController, repeat = 1) {
    super(
        MacroName.SELECT_PREV_CHAR, repeat,
        new ContextChecker(inputController).add(Context.EMPTY_EDITABLE));
  }

  override doKeyPress(): void {
    EventGenerator.sendKeyPress(KeyCode.LEFT, {shift: true});
  }
}

TestImportManager.exportForTesting(UnselectTextMacro);