chromium/ash/webui/common/resources/shortcut_input_ui/shortcut_utils.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 {mojoString16ToString} from 'chrome://resources/js/mojo_type_util.js';

import {StandardAcceleratorProperties} from './accelerator_info.mojom-webui.js';
import * as MetaKeyTypes from './meta_key.mojom-webui.js';
import {ShortcutInputKeyElement} from './shortcut_input_key.js';

export interface ShortcutLabelProperties extends StandardAcceleratorProperties {
  shortcutLabelText: TrustedHTML;
  metaKey: MetaKey;
}

/**
 * Refers to the state of an 'shortcut-input-key' item.
 */
export enum KeyInputState {
  NOT_SELECTED = 'not-selected',
  MODIFIER_SELECTED = 'modifier-selected',
  ALPHANUMERIC_SELECTED = 'alpha-numeric-selected',
}

export enum Modifier {
  NONE = 0,
  SHIFT = 1 << 1,
  CONTROL = 1 << 2,
  ALT = 1 << 3,
  COMMAND = 1 << 4,
  FN_KEY = 1 << 5,
}

export const Modifiers: Modifier[] = [
  Modifier.SHIFT,
  Modifier.CONTROL,
  Modifier.ALT,
  Modifier.COMMAND,
  Modifier.FN_KEY,
];

export enum AllowedModifierKeyCodes {
  SHIFT = 16,
  CTRL = 17,
  ALT = 18,
  META_LEFT = 91,
  META_RIGHT = 92,
  FN_KEY = 255,
}

export const ModifierKeyCodes: AllowedModifierKeyCodes[] = [
  AllowedModifierKeyCodes.SHIFT,
  AllowedModifierKeyCodes.ALT,
  AllowedModifierKeyCodes.CTRL,
  AllowedModifierKeyCodes.META_LEFT,
  AllowedModifierKeyCodes.META_RIGHT,
  AllowedModifierKeyCodes.FN_KEY,
];

/**
 * Enumeration of meta key denoting all the possible options deducable from
 * the users keyboard. Used to show the correct key to the user in the settings
 * UI.
 */
export type MetaKey = MetaKeyTypes.MetaKey;
export const MetaKey = MetaKeyTypes.MetaKey;

export const getSortedModifiers = (modifierStrings: string[]): string[] => {
  const sortOrder = ['meta', 'ctrl', 'alt', 'shift', 'fn'];
  if (modifierStrings.length <= 1) {
    return modifierStrings;
  }
  return modifierStrings.sort(
      (a, b) => sortOrder.indexOf(a) - sortOrder.indexOf(b));
};

// The keys in this map are pulled from the file:
// ui/events/keycodes/dom/dom_code_data.inc
export const KeyToIconNameMap: {[key: string]: string|undefined} = {
  'Accessibility': 'accessibility',
  'ArrowDown': 'arrow-down',
  'ArrowLeft': 'arrow-left',
  'ArrowRight': 'arrow-right',
  'ArrowUp': 'arrow-up',
  'AudioVolumeDown': 'volume-down',
  'AudioVolumeMute': 'volume-mute',
  'AudioVolumeUp': 'volume-up',
  'BrightnessDown': 'display-brightness-down',
  'BrightnessUp': 'display-brightness-up',
  'BrowserBack': 'back',
  'BrowserForward': 'forward',
  'BrowserHome': 'browser-home',
  'BrowserRefresh': 'refresh',
  'BrowserSearch': 'browser-search',
  'ContextMenu': 'menu',
  'EmojiPicker': 'emoji-picker',
  'EnableOrToggleDictation': 'dictation-toggle',
  'KeyboardBacklightToggle': 'keyboard-brightness-toggle',
  'KeyboardBrightnessUp': 'keyboard-brightness-up',
  'KeyboardBrightnessDown': 'keyboard-brightness-down',
  'LaunchApplication1': 'overview',
  'LaunchApplication2': 'calculator',
  'LaunchAssistant': 'assistant',
  'LaunchMail': 'launch-mail',
  'MediaFastForward': 'fast-forward',
  'MediaPause': 'pause',
  'MediaPlay': 'play',
  'MediaPlayPause': 'play-pause',
  'MediaTrackNext': 'next-track',
  'MediaTrackPrevious': 'last-track',
  'MicrophoneMuteToggle': 'microphone-mute',
  'ModeChange': 'globe',
  'ViewAllApps': 'view-all-apps',
  'Power': 'power',
  'PrintScreen': 'screenshot',
  'PrivacyScreenToggle': 'electronic-privacy-screen',
  'Settings': 'settings-icon',
  'Standby': 'lock',
  'ZoomToggle': 'fullscreen',
};

// <if expr="_google_chrome" >
export const KeyToInternalIconNameMap: {[key: string]: string|undefined} = {
  'RightAlt': 'right-alt',
};

export const KeyToInternalIconNameRefreshOnlyMap:
    {[key: string]: string|undefined} = {
      'LaunchApplication1': 'overview-refresh',
      'BrightnessUp': 'brightness-up-refresh',
    };
// </if>

/**
 * Map the modifier keys to the bit value. Currently the modifiers only
 * contains the following four.
 */
export const modifierBitMaskToString = new Map<number, string>([
  [Modifier.CONTROL, 'ctrl'],
  [Modifier.SHIFT, 'shift'],
  [Modifier.ALT, 'alt'],
  [Modifier.COMMAND, 'command'],
]);

export function createInputKeyParts(
    shortcutLabelProperties: ShortcutLabelProperties,
    useNarrowLayout: boolean = false): ShortcutInputKeyElement[] {
  const inputKeys: ShortcutInputKeyElement[] = [];
  const pressedModifiers: string[] = [];
  for (const [bitValue, modifierName] of modifierBitMaskToString) {
    if ((shortcutLabelProperties.accelerator.modifiers & bitValue) !== 0) {
      const key: ShortcutInputKeyElement =
          document.createElement('shortcut-input-key');
      key.keyState = KeyInputState.MODIFIER_SELECTED;
      // Current use cases outside keyboard page or shortcut page only consider
      // 'meta' instead of 'command'.
      key.key = modifierName === 'command' ? 'meta' : modifierName;
      key.metaKey = shortcutLabelProperties.metaKey;
      key.narrow = useNarrowLayout;
      inputKeys.push(key);
      pressedModifiers.push(modifierName);
    }
  }

  const keyDisplay = mojoString16ToString(shortcutLabelProperties.keyDisplay);
  if (!pressedModifiers.includes(keyDisplay.toLowerCase())) {
    const key = document.createElement('shortcut-input-key');
    key.keyState = KeyInputState.ALPHANUMERIC_SELECTED;
    key.key = keyDisplay;
    key.narrow = useNarrowLayout;
    inputKeys.push(key);
  }

  return inputKeys;
}

// TODO(b/340609992): Encapsulate this as a new element too.
export function createShortcutAppendedKeyLabel(
    shortcutLabelProperties: ShortcutLabelProperties,
    useNarrowLayout: boolean = false): HTMLDivElement {
  const reminder = document.createElement('div');
  reminder.innerHTML = shortcutLabelProperties.shortcutLabelText;

  // TODO(b/340609992): Move this out of the helper function as a new element.
  const keyCodes = document.createElement('span');
  keyCodes.append(
      ...createInputKeyParts(shortcutLabelProperties, useNarrowLayout));
  reminder.firstElementChild!.replaceWith(keyCodes);
  reminder.classList.add('reminder-label');
  return reminder;
}