chromium/chrome/browser/resources/ash/settings/device_page/per_device_touchpad_subsection.ts

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

/**
 * @fileoverview
 * 'per-device-touchpad-subsection' allow users to configure their
 * per-device-touchpad subsection settings in system settings.
 */

import '../icons.html.js';
import '../settings_shared.css.js';
import 'chrome://resources/ash/common/cr_elements/localized_link/localized_link.js';
import 'chrome://resources/ash/common/cr_elements/cr_link_row/cr_link_row.js';
import 'chrome://resources/ash/common/cr_elements/cr_radio_button/cr_radio_button.js';
import 'chrome://resources/ash/common/cr_elements/cr_shared_vars.css.js';
import '../controls/settings_dropdown_menu.js';
import '../controls/settings_radio_group.js';
import '../controls/settings_slider.js';
import '../controls/settings_toggle_button.js';
import './input_device_settings_shared.css.js';
import './per_device_app_installed_row.js';
import './per_device_install_row.js';
import './per_device_subsection_header.js';
import 'chrome://resources/ash/common/cr_elements/cr_slider/cr_slider.js';

import {I18nMixin} from 'chrome://resources/ash/common/cr_elements/i18n_mixin.js';
import {loadTimeData} from 'chrome://resources/js/load_time_data.js';
import {PolymerElementProperties} from 'chrome://resources/polymer/v3_0/polymer/interfaces.js';
import {PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';

import {DeepLinkingMixin} from '../common/deep_linking_mixin.js';
import {isRevampWayfindingEnabled} from '../common/load_time_booleans.js';
import {RouteObserverMixin} from '../common/route_observer_mixin.js';
import {Setting} from '../mojom-webui/setting.mojom-webui.js';
import {Route, routes} from '../router.js';

import {getInputDeviceSettingsProvider} from './input_device_mojo_interface_provider.js';
import {CompanionAppState, InputDeviceSettingsProviderInterface, SimulateRightClickModifier, Touchpad, TouchpadSettings} from './input_device_settings_types.js';
import {settingsAreEqual} from './input_device_settings_utils.js';
import {getTemplate} from './per_device_touchpad_subsection.html.js';

const SettingsPerDeviceTouchpadSubsectionElementBase =
    DeepLinkingMixin(RouteObserverMixin(I18nMixin(PolymerElement)));
export class SettingsPerDeviceTouchpadSubsectionElement extends
    SettingsPerDeviceTouchpadSubsectionElementBase {
  static get is() {
    return 'settings-per-device-touchpad-subsection';
  }

  static get template(): HTMLTemplateElement {
    return getTemplate();
  }

  static get properties(): PolymerElementProperties {
    return {
      enableTapToClickPref: {
        type: Object,
        value() {
          return {
            key: 'fakeEnableTapToClickPref',
            type: chrome.settingsPrivate.PrefType.BOOLEAN,
            value: true,
          };
        },
      },

      enableTapDraggingPref: {
        type: Object,
        value() {
          return {
            key: 'fakeEnableTapDraggingPref',
            type: chrome.settingsPrivate.PrefType.BOOLEAN,
            value: false,
          };
        },
      },

      accelerationPref: {
        type: Object,
        value() {
          return {
            key: 'fakeAccelerationPref',
            type: chrome.settingsPrivate.PrefType.BOOLEAN,
            value: true,
          };
        },
      },

      sensitivityPref: {
        type: Object,
        value() {
          return {
            key: 'fakeSensitivityPref',
            type: chrome.settingsPrivate.PrefType.NUMBER,
            value: 3,
          };
        },
      },

      hapticClickSensitivityPref: {
        type: Object,
        value() {
          return {
            key: 'fakeHapticClickSensitivityPref',
            type: chrome.settingsPrivate.PrefType.NUMBER,
            value: 3,
          };
        },
      },

      simulateRightClickPref: {
        type: Object,
        value() {
          return {
            key: 'fakeSimulateRightClickPref',
            type: chrome.settingsPrivate.PrefType.NUMBER,
            value: SimulateRightClickModifier.kNone,
          };
        },
      },

      simulateRightClickOptions: {
        readOnly: true,
        type: Array,
        value() {
          return [
            {
              value: SimulateRightClickModifier.kNone,
              name: loadTimeData.getString(
                  'touchpadSimulateRightClickOptionDisabled'),
            },
            {
              value: SimulateRightClickModifier.kSearch,
              name: loadTimeData.getString(
                  'touchpadSimulateRightClickOptionSearch'),
            },
            {
              value: SimulateRightClickModifier.kAlt,
              name:
                  loadTimeData.getString('touchpadSimulateRightClickOptionAlt'),
            },
          ];
        },
      },

      /**
       * TODO(michaelpg): settings-slider should optionally take a min and max
       * so we don't have to generate a simple range of natural numbers
       * ourselves. These values match the TouchpadSensitivity enum in
       * enums.xml.
       */
      sensitivityValues_: {
        type: Array,
        value: [1, 2, 3, 4, 5],
        readOnly: true,
      },

      /**
       * The click sensitivity values from prefs are [1,3,5] but ChromeVox needs
       * to announce them as [1,2,3].
       */
      hapticClickSensitivityValues_: {
        type: Array,
        value() {
          return [
            {value: 1, ariaValue: 1},
            {value: 3, ariaValue: 2},
            {value: 5, ariaValue: 3},
          ];
        },
        readOnly: true,
      },

      isRevampWayfindingEnabled_: {
        type: Boolean,
        value: () => {
          return isRevampWayfindingEnabled();
        },
      },

      reverseScrollValue: {
        type: Boolean,
        value: false,
      },

      hapticFeedbackValue: {
        type: Boolean,
        value: true,
      },

      touchpad: {type: Object},

      /**
       * Used by DeepLinkingMixin to focus this page's deep links.
       */
      supportedSettingIds: {
        type: Object,
        value: () => new Set<Setting>([
          Setting.kTouchpadTapToClick,
          Setting.kTouchpadTapDragging,
          Setting.kTouchpadReverseScrolling,
          Setting.kTouchpadAcceleration,
          Setting.kTouchpadScrollAcceleration,
          Setting.kTouchpadSpeed,
          Setting.kTouchpadHapticFeedback,
          Setting.kTouchpadHapticClickSensitivity,
        ]),
      },

      touchpadIndex: {
        type: Number,
      },

      isLastDevice: {
        type: Boolean,
        reflectToAttribute: true,
      },

      isAltClickAndSixPackCustomizationEnabled: {
        type: Boolean,
        value() {
          return loadTimeData.getBoolean(
              'enableAltClickAndSixPackCustomization');
        },
        readOnly: true,
      },
    };
  }

  static get observers(): string[] {
    return [
      'onSettingsChanged(enableTapToClickPref.value,' +
          'enableTapDraggingPref.value,' +
          'accelerationPref.value,' +
          'sensitivityPref.value,' +
          'hapticClickSensitivityPref.value,' +
          'simulateRightClickPref.value,' +
          'reverseScrollValue,' +
          'hapticFeedbackValue)',
      'updateSettingsToCurrentPrefs(touchpad)',
    ];
  }

  override currentRouteChanged(route: Route): void {
    // Does not apply to this page.
    if (route !== routes.PER_DEVICE_TOUCHPAD) {
      return;
    }

    // If multiple touchpads are available, focus on the first one.
    if (this.touchpadIndex === 0) {
      this.attemptDeepLink();
    }
  }

  private touchpad: Touchpad;
  private enableTapToClickPref: chrome.settingsPrivate.PrefObject;
  private enableTapDraggingPref: chrome.settingsPrivate.PrefObject;
  private accelerationPref: chrome.settingsPrivate.PrefObject;
  private sensitivityPref: chrome.settingsPrivate.PrefObject;
  private hapticClickSensitivityPref: chrome.settingsPrivate.PrefObject;
  private simulateRightClickPref: chrome.settingsPrivate.PrefObject;
  private reverseScrollValue: boolean;
  private hapticFeedbackValue: boolean;
  private isInitialized: boolean = false;
  private inputDeviceSettingsProvider: InputDeviceSettingsProviderInterface =
      getInputDeviceSettingsProvider();
  private touchpadIndex: number;
  private isLastDevice: boolean;
  isAltClickAndSixPackCustomizationEnabled: boolean;
  private isRevampWayfindingEnabled_: boolean;

  private showInstallAppRow(): boolean {
    return this.touchpad.appInfo?.state === CompanionAppState.kAvailable;
  }

  private updateSettingsToCurrentPrefs(): void {
    // `updateSettingsToCurrentPrefs` gets called when the `keyboard` object
    // gets updated. This subsection element can be reused multiple times so we
    // need to reset `isInitialized` so we do not make unneeded API calls.
    this.isInitialized = false;
    this.set(
        'enableTapToClickPref.value', this.touchpad.settings.tapToClickEnabled);
    this.set(
        'simulateRightClickPref.value',
        this.touchpad.settings.simulateRightClick);
    this.set(
        'enableTapDraggingPref.value',
        this.touchpad.settings.tapDraggingEnabled);
    this.set(
        'accelerationPref.value', this.touchpad.settings.accelerationEnabled);
    this.set('sensitivityPref.value', this.touchpad.settings.sensitivity);
    this.set(
        'hapticClickSensitivityPref.value',
        this.touchpad.settings.hapticSensitivity);
    this.reverseScrollValue = this.touchpad.settings.reverseScrolling;
    this.hapticFeedbackValue = this.touchpad.settings.hapticEnabled;
    this.isInitialized = true;
  }

  private onLearnMoreLinkClicked_(event: Event): void {
    const path = event.composedPath();
    if (!Array.isArray(path) || !path.length) {
      return;
    }

    if ((path[0] as HTMLElement).tagName === 'A') {
      // Do not toggle reverse scrolling if the contained link is clicked.
      event.stopPropagation();
    }
  }

  private onTouchpadReverseScrollRowClicked_(): void {
    this.reverseScrollValue = !this.reverseScrollValue;
  }

  private onTouchpadHapticFeedbackRowClicked_(): void {
    this.hapticFeedbackValue = !this.hapticFeedbackValue;
  }

  private onSettingsChanged(): void {
    if (!this.isInitialized) {
      return;
    }

    const newSettings: TouchpadSettings = {
      ...this.touchpad.settings,
      tapToClickEnabled: this.enableTapToClickPref.value,
      tapDraggingEnabled: this.enableTapDraggingPref.value,
      accelerationEnabled: this.accelerationPref.value,
      sensitivity: this.sensitivityPref.value,
      hapticSensitivity: this.hapticClickSensitivityPref.value,
      simulateRightClick: this.simulateRightClickPref.value,
      reverseScrolling: this.reverseScrollValue,
      hapticEnabled: this.hapticFeedbackValue,
    };

    if (settingsAreEqual(newSettings, this.touchpad.settings)) {
      return;
    }

    this.touchpad.settings = newSettings;
    this.inputDeviceSettingsProvider.setTouchpadSettings(
        this.touchpad.id, this.touchpad.settings);
  }

  private getTouchpadName(): string {
    return this.touchpad.isExternal ? this.touchpad.name :
                                      this.i18n('builtInTouchpadName');
  }

  private getLabelWithoutLearnMore(stringName: string): string|TrustedHTML {
    const tempEl = document.createElement('div');
    const localizedString = this.i18nAdvanced(stringName);
    tempEl.innerHTML = localizedString;

    const nodesToDelete: Node[] = [];
    tempEl.childNodes.forEach((node) => {
      // Remove elements with the <a> tag
      if (node.nodeType === Node.ELEMENT_NODE && node.nodeName === 'A') {
        nodesToDelete.push(node);
        return;
      }
    });

    nodesToDelete.forEach((node) => {
      tempEl.removeChild(node);
    });

    return tempEl.innerHTML;
  }

  private getTouchpadAccelerationDescription(): string {
    if (this.isRevampWayfindingEnabled_) {
      return this.i18n('touchpadAccelerationDescription');
    }
    return '';
  }

  private getTouchpadTapDraggingDescription(): string {
    if (this.isRevampWayfindingEnabled_) {
      return this.i18n('tapDraggingDescription');
    }
    return '';
  }

  private getTouchpadTapToClickDescription(): string {
    if (this.isRevampWayfindingEnabled_) {
      return this.i18n('touchpadTapToClickDescription');
    }
    return '';
  }

  private isCompanionAppInstalled(): boolean {
    return this.touchpad.appInfo?.state === CompanionAppState.kInstalled;
  }
}

declare global {
  interface HTMLElementTagNameMap {
    'settings-per-device-touchpad-subsection':
        SettingsPerDeviceTouchpadSubsectionElement;
  }
}

customElements.define(
    SettingsPerDeviceTouchpadSubsectionElement.is,
    SettingsPerDeviceTouchpadSubsectionElement);