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

// 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.

import {EventGenerator} from '../../event_generator.js';

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

import ScreenPoint = chrome.accessibilityPrivate.ScreenPoint;
import SyntheticMouseEventButton = chrome.accessibilityPrivate.SyntheticMouseEventButton;

/**
 * Class that implements a macro to send a synthetic mouse click.
 */
export class MouseClickMacro extends Macro {
  private leftClick_: boolean;
  private location_: ScreenPoint|undefined;
  private runCount_ = 0;
  private clickImmediately_: boolean;

  /**
   * Pass the location in density-independent pixels. Defaults to left click and
   * clicking immediately.
   */
  constructor(
      location: ScreenPoint|undefined, leftClick = true,
      clickImmediately = true) {
    super(
        leftClick ? (clickImmediately ? MacroName.MOUSE_CLICK_LEFT :
                                        MacroName.MOUSE_LONG_CLICK_LEFT) :
                    MacroName.MOUSE_CLICK_RIGHT);
    this.leftClick_ = leftClick;
    this.location_ = location;
    this.clickImmediately_ = clickImmediately;
  }

  /**
   * The mouse click macro should be run twice, once when the click begins and
   * again when it ends.
   */
  override triggersAtActionStartAndEnd(): boolean {
    return !this.clickImmediately_;
  }

  /** Invalid context if location isn't set. */
  override checkContext(): CheckContextResult {
    if (!this.location_) {
      return this.createFailureCheckContextResult_(MacroError.BAD_CONTEXT);
    }
    if ((this.clickImmediately_ && this.runCount_ > 0) ||
        (!this.clickImmediately_ && this.runCount_ > 2)) {
      return this.createFailureCheckContextResult_(
          MacroError.INVALID_USER_INTENT);
    } else {
      return this.createSuccessCheckContextResult_();
    }
  }

  updateLocation(location?: ScreenPoint): void {
    this.location_ = location;
  }

  override run(): RunMacroResult {
    if (!this.location_ || this.runCount_ > 2) {
      return this.createRunMacroResult_(/*isSuccess=*/ false);
    }
    const mouseButton = this.leftClick_ ? SyntheticMouseEventButton.LEFT :
                                          SyntheticMouseEventButton.RIGHT;

    if (this.runCount_ === 0) {
      EventGenerator.sendMousePress(
          this.location_.x, this.location_.y, mouseButton);
      if (this.clickImmediately_) {
        // Increment run count so we perform the click release.
        this.runCount_++;
      }
    }

    if (this.runCount_ === 1) {
      EventGenerator.sendMouseRelease(this.location_.x, this.location_.y);
    }

    this.runCount_++;
    return this.createRunMacroResult_(/*isSuccess=*/ true);
  }
}

/** Class that implements a macro to send a double left click. */
export class MouseClickLeftDoubleMacro extends Macro {
  private location_: ScreenPoint|undefined;

  constructor(location: ScreenPoint|undefined) {
    super(MacroName.MOUSE_CLICK_LEFT_DOUBLE);
    this.location_ = location;
  }

  override checkContext(): CheckContextResult {
    if (!this.location_) {
      return this.createFailureCheckContextResult_(MacroError.BAD_CONTEXT);
    }

    return this.createSuccessCheckContextResult_();
  }

  override run(): RunMacroResult {
    if (!this.location_) {
      return this.createRunMacroResult_(/*isSuccess=*/ false);
    }

    const mouseButton = SyntheticMouseEventButton.LEFT;
    EventGenerator.sendMousePress(
        this.location_.x, this.location_.y, mouseButton,
        /*isDoubleClick=*/ true);
    EventGenerator.sendMouseRelease(
        this.location_.x, this.location_.y, /*isDoubleClick=*/ true);

    return this.createRunMacroResult_(/*isSuccess=*/ true);
  }
}