chromium/chrome/browser/resources/ash/settings/device_page/keyboard_remap_modifier_key_row.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
 * 'keyboard-remap-key-row' contains a key with icon label and dropdown menu to
 * allow users to customize the remapped key.
 */

import '/shared/settings/prefs/prefs.js';
import 'chrome://resources/polymer/v3_0/iron-icon/iron-icon.js';
import '../settings_shared.css.js';
import '../controls/settings_dropdown_menu.js';
import '../os_settings_icons.html.js';
// <if expr="_google_chrome" >
import 'chrome://resources/ash/common/internal/ash_internal_icons.html.js';
// </if>

import {I18nMixin} from 'chrome://resources/ash/common/cr_elements/i18n_mixin.js';
import {assertNotReached} from 'chrome://resources/js/assert.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 {DropdownMenuOptionList} from '../controls/settings_dropdown_menu.js';

import {MetaKey, ModifierKey} from './input_device_settings_types.js';
import {getTemplate} from './keyboard_remap_modifier_key_row.html.js';

/**
 * Refers to the state of an 'remap-key' icon.
 */
enum KeyState {
  DEFAULT_REMAPPING = 'default-remapping',
  MODIFIER_REMAPPED = 'modifier-remapped',
}

type KeyIcon = 'cr:search'|'os-settings:launcher'|'os-settings:assistant'|
    'ash-internal:launcher-refresh'|'ash-internal:right-alt'|'';
const KeyboardRemapModifierKeyRowElementBase = I18nMixin(PolymerElement);

export class KeyboardRemapModifierKeyRowElement extends
    KeyboardRemapModifierKeyRowElementBase {
  static get is() {
    return 'keyboard-remap-modifier-key-row';
  }

  static get properties(): PolymerElementProperties {
    return {
      keyLabel: {
        type: String,
        value: '',
        computed: 'getKeyLabel(metaKey)',
      },

      metaKeyLabel: {
        type: String,
        value: '',
        computed: 'getMetaKeyLabel(metaKey)',
      },

      keyState: {
        type: String,
        value: KeyState.DEFAULT_REMAPPING,
        reflectToAttribute: true,
        computed: 'computeKeyState(pref.value, defaultRemappings.*)',
      },

      pref: {
        type: Object,
      },

      metaKey: {
        type: Number,
      },

      key: {
        type: Number,
      },

      defaultRemappings: {
        type: Object,
      },

      keyMapTargets: {
        type: Object,
      },

      keyIcon: {
        type: String,
        value: '',
        computed: 'getKeyIcon(key, metaKey)',
      },

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

      hasFunctionKey: {
        type: Boolean,
        value: false,
      },
    };
  }

  protected keyLabel: string;
  private metaKeyLabel: string;
  private keyMapTargets: DropdownMenuOptionList;
  private keyIcon: KeyIcon;
  private hasFunctionKey: boolean;
  keyState: KeyState;
  pref: chrome.settingsPrivate.PrefObject;
  metaKey: MetaKey;
  key: ModifierKey;
  defaultRemappings: {[key: number]: ModifierKey};

  override ready(): void {
    super.ready();

    this.setUpKeyMapTargets();
  }

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

  /**
   * Whenever the key remapping is changed, update the keyState to change
   * the icon color between default and highlighted.
   */
  private computeKeyState(): KeyState {
    return this.defaultRemappings[this.key] === this.pref.value ?
        KeyState.DEFAULT_REMAPPING :
        KeyState.MODIFIER_REMAPPED;
  }

  /**
   * Populate the metaKey label according to metaKey.
   */
  private getMetaKeyLabel(): string {
    switch (this.metaKey) {
      case MetaKey.kCommand: {
        return this.i18n('perDeviceKeyboardKeyCommand');
      }
      case MetaKey.kExternalMeta: {
        return this.i18n('perDeviceKeyboardKeyMeta');
      }
      // Launcher and Search key will display icon instead of text.
      case MetaKey.kLauncher:
      case MetaKey.kSearch:
      case MetaKey.kLauncherRefresh:
        return this.i18n('perDeviceKeyboardKeySearch');
    }
  }

  /**
   * Populate the key label inside the keyboard key icon.
   */
  private getKeyLabel(): string {
    switch (this.key) {
      case ModifierKey.kAlt: {
        return this.i18n('perDeviceKeyboardKeyAlt');
      }
      case ModifierKey.kAssistant: {
        return this.i18n('perDeviceKeyboardKeyAssistant');
      }
      case ModifierKey.kBackspace: {
        return this.i18n('perDeviceKeyboardKeyBackspace');
      }
      case ModifierKey.kCapsLock: {
        return this.i18n('perDeviceKeyboardKeyCapsLock');
      }
      case ModifierKey.kControl: {
        return this.i18n('perDeviceKeyboardKeyCtrl');
      }
      case ModifierKey.kEscape: {
        return this.i18n('perDeviceKeyboardKeyEscape');
      }
      case ModifierKey.kMeta: {
        return this.getMetaKeyLabel();
      }
      case ModifierKey.kRightAlt: {
        return this.i18n('perDeviceKeyboardKeyRightAlt');
      }
      case ModifierKey.kFunction: {
        return this.i18n('perDeviceKeyboardKeyFunction');
      }
      default:
        assertNotReached('Invalid modifier key: ' + this.key);
    }
  }

  private setUpKeyMapTargets(): void {
    // Ordering is according to UX, but values match ModifierKey.
    this.keyMapTargets = (() => {
      const keyMapTargets = [
        {
          value: ModifierKey.kMeta,
          name: this.i18n('perDeviceKeyboardKeySearch'),
        },
        {
          value: ModifierKey.kControl,
          name: this.i18n('perDeviceKeyboardKeyCtrl'),
        },
        {
          value: ModifierKey.kAlt,
          name: this.i18n('perDeviceKeyboardKeyAlt'),
        },
        {
          value: ModifierKey.kCapsLock,
          name: this.i18n('perDeviceKeyboardKeyCapsLock'),
        },
        {
          value: ModifierKey.kEscape,
          name: this.i18n('perDeviceKeyboardKeyEscape'),
        },
        {
          value: ModifierKey.kBackspace,
          name: this.i18n('perDeviceKeyboardKeyBackspace'),
        },
        {
          value: ModifierKey.kAssistant,
          name: this.i18n('perDeviceKeyboardKeyAssistant'),
        },
      ];

      if (loadTimeData.getBoolean('enableModifierSplit')) {
        keyMapTargets.push({
          value: ModifierKey.kRightAlt,
          name: this.i18n('perDeviceKeyboardKeyRightAlt'),
        });
      }

      if (this.hasFunctionKey) {
        keyMapTargets.push({
          value: ModifierKey.kFunction,
          name: this.i18n('perDeviceKeyboardKeyFunction'),
        });
      }

      // Push void last so that right alt is added before it.
      keyMapTargets.push({
        value: ModifierKey.kVoid,
        name: this.i18n('perDeviceKeyboardKeyDisabled'),
      });

      return keyMapTargets;
    })();
  }

  private getKeyIcon(): KeyIcon {
    if (this.key === ModifierKey.kMeta) {
      if (this.metaKey === MetaKey.kSearch) {
        return 'cr:search';
      }
      if (this.metaKey === MetaKey.kLauncher) {
        return 'os-settings:launcher';
      }
      if (this.metaKey === MetaKey.kLauncherRefresh) {
        return 'ash-internal:launcher-refresh';
      }
    } else if (this.key === ModifierKey.kAssistant) {
      return 'os-settings:assistant';
    } else if (this.key === ModifierKey.kRightAlt) {
      return 'ash-internal:right-alt';
    }

    return '';
  }
}

declare global {
  interface HTMLElementTagNameMap {
    'keyboard-remap-modifier-key-row': KeyboardRemapModifierKeyRowElement;
  }
}

customElements.define(
    KeyboardRemapModifierKeyRowElement.is, KeyboardRemapModifierKeyRowElement);