chromium/chrome/browser/resources/ash/settings/device_page/key_combination_input_dialog.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.

import 'chrome://resources/ash/common/shortcut_input_ui/shortcut_input_key.js';
import 'chrome://resources/ash/common/shortcut_input_ui/shortcut_input.js';
import 'chrome://resources/ash/common/cr_elements/cr_button/cr_button.js';
import 'chrome://resources/ash/common/cr_elements/cr_dialog/cr_dialog.js';
import 'chrome://resources/ash/common/cr_elements/cr_shared_vars.css.js';
import './input_device_settings_shared.css.js';
import '../settings_shared.css.js';
// <if expr="_google_chrome" >
import 'chrome://resources/ash/common/internal/ash_internal_icons.html.js';
// </if>

import {CrDialogElement} from 'chrome://resources/ash/common/cr_elements/cr_dialog/cr_dialog.js';
import {I18nMixin} from 'chrome://resources/ash/common/cr_elements/i18n_mixin.js';
import {ShortcutInputElement} from 'chrome://resources/ash/common/shortcut_input_ui/shortcut_input.js';
import {EventTracker} from 'chrome://resources/js/event_tracker.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 {ButtonRemapping, KeyEvent, MetaKey, ShortcutInputProviderInterface} from './input_device_settings_types.js';
import {keyEventsAreEqual} from './input_device_settings_utils.js';
import {getTemplate} from './key_combination_input_dialog.html.js';
import {getShortcutInputProvider} from './shortcut_input_mojo_interface_provider.js';

/**
 * @fileoverview
 * 'key-combination-input-dialog' is a dialog that pops up after clicking the
 * 'Key combination' choice in the dropdown menu to allow users to input a
 * combination of keyboard keys as a button remapping action.
 */

export interface KeyCombinationInputDialogElement {
  $: {
    keyCombinationInputDialog: CrDialogElement,
    shortcutInput: ShortcutInputElement,
  };
}

export type ShortcutInputCompleteEvent = CustomEvent<{keyEvent: KeyEvent}>;
export type ShortcutInputCaptureStateEvent = CustomEvent<{capturing: boolean}>;

declare global {
  interface HTMLElementEventMap {
    'shortcut-input-event': ShortcutInputCompleteEvent;
  }
}

const KeyCombinationInputDialogElementBase = I18nMixin(PolymerElement);

export class KeyCombinationInputDialogElement extends
    KeyCombinationInputDialogElementBase {
  static get is() {
    return 'key-combination-input-dialog' as const;
  }

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

  static get properties(): PolymerElementProperties {
    return {
      buttonRemappingList: {
        type: Array,
      },

      remappingIndex: {
        type: Number,
      },

      buttonRemapping_: {
        type: Object,
      },

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

      isCapturing: {
        type: Boolean,
      },

      inputKeyEvent: {
        type: Object,
      },

      metaKey: Object,
    };
  }

  static get observers(): string[] {
    return [
      'initializeDialog(buttonRemappingList.*, remappingIndex)',
    ];
  }

  buttonRemappingList: ButtonRemapping[];
  remappingIndex: number;
  isOpen: boolean;
  shortcutInput: ShortcutInputElement;
  inputKeyEvent: KeyEvent|undefined;
  isCapturing: boolean = false;
  metaKey: MetaKey = MetaKey.kSearch;
  private buttonRemapping_: ButtonRemapping;
  private eventTracker_: EventTracker = new EventTracker();

  override connectedCallback(): void {
    super.connectedCallback();
    this.eventTracker_.add(
        this, 'shortcut-input-event', this.onShortcutInputEvent_);
    this.eventTracker_.add(
        this, 'shortcut-input-capture-state', this.onShortcutInputUpdate_);
    // Set window as the eventTarget to exclude blur event from shortcut-input.
    this.eventTracker_.add(window, 'blur', this.onBlur_.bind(this));
    this.shortcutInput = this.$.shortcutInput;
  }

  override disconnectedCallback(): void {
    super.disconnectedCallback();
    this.eventTracker_.removeAll();
  }

  /**
   * Initialize the button remapping content and set up fake pref.
   */
  private initializeDialog(): void {
    if (!this.buttonRemappingList ||
        !this.buttonRemappingList[this.remappingIndex]) {
      return;
    }
    this.buttonRemapping_ = this.buttonRemappingList[this.remappingIndex];
  }

  showModal(): void {
    this.initializeDialog();
    const keyCombinationInputDialog = this.$.keyCombinationInputDialog;
    keyCombinationInputDialog.showModal();
    this.isOpen = keyCombinationInputDialog.open;
    this.shortcutInput.reset();
    this.shortcutInput.startObserving();
  }

  close(): void {
    const keyCombinationInputDialog = this.$.keyCombinationInputDialog;
    keyCombinationInputDialog.close();
    this.isOpen = keyCombinationInputDialog.open;

    this.shortcutInput.reset();
    this.shortcutInput.stopObserving();
  }

  getShortcutProvider(): ShortcutInputProviderInterface {
    return getShortcutInputProvider();
  }

  private onBlur_(): void {
    if (this.isCapturing && !this.inputKeyEvent && this.isOpen) {
      this.close();
    }
  }

  private cancelDialogClicked_(): void {
    this.close();
  }

  private saveDialogClicked_(): void {
    if (!this.inputKeyEvent) {
      return;
    }
    const prevKeyEvent: KeyEvent|undefined =
        this.buttonRemapping_.remappingAction?.keyEvent;
    if (!prevKeyEvent ||
        !keyEventsAreEqual(this.inputKeyEvent, prevKeyEvent!)) {
      this.set(
          `buttonRemappingList.${this.remappingIndex}`,
          this.getUpdatedButtonRemapping_());
      this.dispatchEvent(new CustomEvent('button-remapping-changed', {
        bubbles: true,
        composed: true,
      }));
    }
    this.close();
  }

  /**
   * @returns Button remapping with updated remapping action based on
   * users' key combination input.
   */
  private getUpdatedButtonRemapping_(): ButtonRemapping {
    return {
      ...this.buttonRemapping_,
      remappingAction: {
        keyEvent: this.inputKeyEvent,
      },
    };
  }

  /**
   * Listens for ShortcutInputCompleteEvent to store users' input keyEvent.
   */
  private onShortcutInputEvent_(e: ShortcutInputCompleteEvent): void {
    this.inputKeyEvent = e.detail.keyEvent;
    this.shortcutInput.stopObserving();
  }

  private onShortcutInputUpdate_(e: ShortcutInputCaptureStateEvent): void {
    this.isCapturing = e.detail.capturing;
  }

  private onEditButtonClicked_(): void {
    this.inputKeyEvent = undefined;
    this.shortcutInput.reset();
    this.shortcutInput.startObserving();
  }

  private shouldDisableSaveButton_(): boolean {
    return this.inputKeyEvent === undefined;
  }

  private shouldShowEditButton_(): boolean {
    return !this.isCapturing;
  }
}

declare global {
  interface HTMLElementTagNameMap {
    [KeyCombinationInputDialogElement.is]: KeyCombinationInputDialogElement;
  }
}

customElements.define(
    KeyCombinationInputDialogElement.is, KeyCombinationInputDialogElement);