// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/* eslint-disable @devtools/no-imperative-dom-api */

import * as Common from '../../core/common/common.js';
import * as Host from '../../core/host/host.js';
import * as i18n from '../../core/i18n/i18n.js';
import * as Platform from '../../core/platform/platform.js';
import * as Buttons from '../../ui/components/buttons/buttons.js';
import {createIcon, type Icon, Link} from '../../ui/kit/kit.js';
import * as SettingsUI from '../../ui/legacy/components/settings_ui/settings_ui.js';
import * as UI from '../../ui/legacy/legacy.js';
import * as VisualLogging from '../../ui/visual_logging/visual_logging.js';

import keybindsSettingsTabStyles from './keybindsSettingsTab.css.js';
import settingsScreenStyles from './settingsScreen.css.js';

const UIStrings = {
  /**
   * @description Text for keyboard shortcuts
   */
  shortcuts: 'Shortcuts',
  /**
   * @description Text appearing before a select control offering users their choice of keyboard shortcut presets.
   */
  matchShortcutsFromPreset: 'Shortcut preset',
  /**
   * @description Screen reader label for list of keyboard shortcuts in settings
   */
  keyboardShortcutsList: 'Keyboard shortcuts list',
  /**
   * @description Screen reader label for an icon denoting a shortcut that has been changed from its default
   */
  shortcutModified: 'Shortcut modified',
  /**
   * @description Screen reader label for an empty shortcut cell in custom shortcuts settings tab
   */
  noShortcutForAction: 'No shortcut for action',
  /**
   * @description Link text in the settings pane to add another shortcut for an action
   */
  addAShortcut: 'Add a shortcut',
  /**
   * @description Placeholder text in the settings pane when adding a new shortcut.
   * Explaining that key strokes are going to be recoded.
   */
  recordingKeys: 'Recoding keys',
  /**
   * @description Label for a button in the settings pane that confirms changes to a keyboard shortcut
   */
  confirmChanges: 'Confirm changes',
  /**
   * @description Label for a button in the settings pane that discards changes to the shortcut being edited
   */
  discardChanges: 'Discard changes',
  /**
   * @description Label for a button in the settings pane that removes a keyboard shortcut.
   */
  removeShortcut: 'Remove shortcut',
  /**
   * @description Label for a button in the settings pane that edits a keyboard shortcut
   */
  editShortcut: 'Edit shortcut',
  /**
   * @description Message shown in settings when the user inputs a modifier-only shortcut such as Ctrl+Shift.
   */
  shortcutsCannotContainOnly: 'Shortcuts cannot contain only modifier keys.',
  /**
   * @description Messages shown in shortcuts settings when the user inputs a shortcut that is already in use.
   * @example {Performance} PH1
   * @example {Start/stop recording} PH2
   */
  thisShortcutIsInUseByS: 'This shortcut is in use by {PH1}: {PH2}.',
  /**
   * @description Message shown in settings when to restore default shortcuts.
   */
  RestoreDefaultShortcuts: 'Restore default shortcuts',
  /**
   * @description Message shown in settings to show the full list of keyboard shortcuts.
   */
  FullListOfDevtoolsKeyboard: 'Full list of DevTools keyboard shortcuts and gestures',
  /**
   * @description Label for a button in the shortcut editor that resets all shortcuts for the current action.
   */
  ResetShortcutsForAction: 'Reset shortcuts for action',
  /**
   * @description Screen reader announcement for shortcut removed
   * @example {Start/stop recording} PH1
   */
  shortcutRemoved: '{PH1} Shortcut removed',
  /**
   * @description Screen reader announcement for shortcut restored to default
   */
  shortcutChangesRestored: 'Changes to shortcut restored to default',
  /**
   * @description Screen reader announcement for applied short cut changes
   */
  shortcutChangesApplied: 'Changes to shortcut applied',
  /**
   * @description Screen reader announcement for discarded short cut changes
   */
  shortcutChangesDiscarded: 'Changes to shortcut discarded',
} as const;
const str_ = i18n.i18n.registerUIStrings('panels/settings/KeybindsSettingsTab.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);

export class KeybindsSettingsTab extends UI.Widget.VBox implements UI.ListControl.ListDelegate<KeybindsItem> {
  private readonly items: UI.ListModel.ListModel<KeybindsItem>;
  private list: UI.ListControl.ListControl<string|UI.ActionRegistration.Action>;
  private editingItem: UI.ActionRegistration.Action|null;
  private editingRow: ShortcutListItem|null;

  constructor() {
    super({
      jslog: `${VisualLogging.pane('keybinds')}`,
      useShadowDom: true,
    });
    this.registerRequiredCSS(keybindsSettingsTabStyles, settingsScreenStyles);

    const settingsContent =
        this.contentElement.createChild('div', 'settings-card-container-wrapper').createChild('div');
    settingsContent.classList.add('settings-card-container');

    const keybindsSetSetting = Common.Settings.Settings.instance().moduleSetting('active-keybind-set');
    const userShortcutsSetting = Common.Settings.Settings.instance().moduleSetting('user-shortcuts');
    keybindsSetSetting.addChangeListener(this.update, this);
    const keybindsSetSelect = SettingsUI.SettingsUI.createControlForSetting(
        keybindsSetSetting, i18nString(UIStrings.matchShortcutsFromPreset));

    const card = settingsContent.createChild('devtools-card');
    card.heading = i18nString(UIStrings.shortcuts);

    if (keybindsSetSelect) {
      keybindsSetSelect.classList.add('keybinds-set-select');
    }

    this.items = new UI.ListModel.ListModel();
    this.list = new UI.ListControl.ListControl(this.items, this, UI.ListControl.ListMode.NonViewport);
    this.list.element.classList.add('shortcut-list');
    this.items.replaceAll(this.createListItems());
    UI.ARIAUtils.markAsList(this.list.element);

    UI.ARIAUtils.setLabel(this.list.element, i18nString(UIStrings.keyboardShortcutsList));
    const footer = document.createElement('div');
    footer.classList.add('keybinds-footer');
    const docsLink = Link.create(
        'https://developer.chrome.com/docs/devtools/shortcuts/', i18nString(UIStrings.FullListOfDevtoolsKeyboard),
        undefined, 'learn-more');
    docsLink.classList.add('docs-link');
    footer.appendChild(docsLink);
    const restoreDefaultShortcutsButton =
        UI.UIUtils.createTextButton(i18nString(UIStrings.RestoreDefaultShortcuts), () => {
          userShortcutsSetting.set([]);
          keybindsSetSetting.set(UI.ShortcutRegistry.DefaultShortcutSetting);
        }, {jslogContext: 'restore-default-shortcuts'});
    footer.appendChild(restoreDefaultShortcutsButton);
    this.editingItem = null;
    this.editingRow = null;

    if (keybindsSetSelect) {
      card.append(keybindsSetSelect);
    }
    card.append(this.list.element, footer);

    this.update();
  }

  createElementForItem(item: KeybindsItem): Element {
    const element = document.createElement('div');

    let itemContent;
    if (typeof item === 'string') {
      itemContent = element;
      itemContent.classList.add('keybinds-category-header');
      itemContent.textContent = UI.ActionRegistration.getLocalizedActionCategory(item);
      UI.ARIAUtils.setLevel(itemContent, 1);
    } else {
      const listItem = new ShortcutListItem(item, this, item === this.editingItem);
      itemContent = listItem.element;
      UI.ARIAUtils.setLevel(itemContent, 2);
      if (item === this.editingItem) {
        this.editingRow = listItem;
      }
      itemContent.classList.add('keybinds-list-item');
      element.classList.add('keybinds-list-item-wrapper');
      element.appendChild(itemContent);
    }

    UI.ARIAUtils.markAsListitem(itemContent);
    itemContent.tabIndex = item === this.list.selectedItem() && item !== this.editingItem ? 0 : -1;
    return element;
  }

  commitChanges(
      item: UI.ActionRegistration.Action,
      editedShortcuts: Map<UI.KeyboardShortcut.KeyboardShortcut, UI.KeyboardShortcut.Descriptor[]|null>): void {
    for (const [originalShortcut, newDescriptors] of editedShortcuts) {
      if (originalShortcut.type !== UI.KeyboardShortcut.Type.UNSET_SHORTCUT) {
        UI.ShortcutRegistry.ShortcutRegistry.instance().removeShortcut(originalShortcut);
        if (!newDescriptors) {
          Host.userMetrics.actionTaken(Host.UserMetrics.Action.ShortcutRemoved);
        }
      }
      if (newDescriptors) {
        UI.ShortcutRegistry.ShortcutRegistry.instance().registerUserShortcut(
            originalShortcut.changeKeys(newDescriptors).changeType(UI.KeyboardShortcut.Type.USER_SHORTCUT));
        if (originalShortcut.type === UI.KeyboardShortcut.Type.UNSET_SHORTCUT) {
          Host.userMetrics.actionTaken(Host.UserMetrics.Action.UserShortcutAdded);
        } else {
          Host.userMetrics.actionTaken(Host.UserMetrics.Action.ShortcutModified);
        }
      }
    }
    this.stopEditing(item);
  }

  /**
   * This method will never be called.
   */
  heightForItem(_item: KeybindsItem): number {
    return 0;
  }

  isItemSelectable(item: KeybindsItem): boolean {
    // Category headers (UI.ActionRegistration.ActionCategory) should not be selectable
    return item instanceof UI.ActionRegistration.Action;
  }

  selectedItemChanged(
      _from: KeybindsItem|null, to: KeybindsItem|null, fromElement: HTMLElement|null,
      toElement: HTMLElement|null): void {
    if (fromElement) {
      fromElement.tabIndex = -1;
    }
    if (toElement) {
      if (to === this.editingItem && this.editingRow) {
        this.editingRow.focus();
      } else {
        toElement.tabIndex = 0;
        if (this.list.element.hasFocus()) {
          toElement.focus();
        }
      }
      this.setDefaultFocusedElement(toElement);
    }
  }

  updateSelectedItemARIA(_fromElement: Element|null, _toElement: Element|null): boolean {
    return true;
  }

  startEditing(action: UI.ActionRegistration.Action): void {
    this.list.selectItem(action);

    if (this.editingItem) {
      this.stopEditing(this.editingItem);
    }
    UI.UIUtils.markBeingEdited(this.list.element, true);
    this.editingItem = action;
    this.list.refreshItem(action);
  }

  stopEditing(action: UI.ActionRegistration.Action): void {
    UI.UIUtils.markBeingEdited(this.list.element, false);
    this.editingItem = null;
    this.editingRow = null;
    this.list.refreshItem(action);
    this.focus();
  }

  private createListItems(): KeybindsItem[] {
    const actions = UI.ActionRegistry.ActionRegistry.instance()
                        .actions()
                        .filter(action => action.configurableBindings())
                        .sort((actionA, actionB) => {
                          if (actionA.category() < actionB.category()) {
                            return -1;
                          }
                          if (actionA.category() > actionB.category()) {
                            return 1;
                          }
                          if (actionA.id() < actionB.id()) {
                            return -1;
                          }
                          if (actionA.id() > actionB.id()) {
                            return 1;
                          }
                          return 0;
                        });

    const items: KeybindsItem[] = [];

    let currentCategory: UI.ActionRegistration.ActionCategory;
    actions.forEach(action => {
      if (currentCategory !== action.category()) {
        items.push(action.category());
      }
      items.push(action);
      currentCategory = action.category();
    });
    return items;
  }

  onEscapeKeyPressed(event: Event): void {
    const deepActiveElement = UI.DOMUtilities.deepActiveElement(document);
    if (this.editingRow && deepActiveElement?.nodeName === 'INPUT') {
      this.editingRow.onEscapeKeyPressed(event);
    }
  }

  update(): void {
    if (this.editingItem) {
      this.stopEditing(this.editingItem);
    }
    this.list.refreshAllItems();
    if (!this.list.selectedItem()) {
      this.list.selectFirstItem();
    }
  }

  override willHide(): void {
    super.willHide();
    if (this.editingItem) {
      this.stopEditing(this.editingItem);
    }
  }
}

export class ShortcutListItem {
  private isEditing: boolean;
  private settingsTab: KeybindsSettingsTab;
  private item: UI.ActionRegistration.Action;
  element: HTMLDivElement;
  private editedShortcuts: Map<UI.KeyboardShortcut.KeyboardShortcut, UI.KeyboardShortcut.Descriptor[]|null>;
  private readonly shortcutInputs: Map<UI.KeyboardShortcut.KeyboardShortcut, Element>;
  private readonly shortcuts: UI.KeyboardShortcut.KeyboardShortcut[];
  private elementToFocus: HTMLElement|null;
  private confirmButton: Buttons.Button.Button|null;
  private addShortcutLinkContainer: Element|null;
  private errorMessageElement: Element|null;
  private secondKeyTimeout: number|null;
  constructor(item: UI.ActionRegistration.Action, settingsTab: KeybindsSettingsTab, isEditing?: boolean) {
    this.isEditing = Boolean(isEditing);
    this.settingsTab = settingsTab;
    this.item = item;
    this.element = document.createElement('div');
    this.element.setAttribute(
        'jslog', `${VisualLogging.item().context(item.id()).track({keydown: 'Escape', resize: true})}`);
    this.editedShortcuts = new Map();
    this.shortcutInputs = new Map();
    this.shortcuts = UI.ShortcutRegistry.ShortcutRegistry.instance().shortcutsForAction(item.id());
    this.elementToFocus = null;
    this.confirmButton = null;
    this.addShortcutLinkContainer = null;
    this.errorMessageElement = null;
    this.secondKeyTimeout = null;

    this.update();
  }

  focus(): void {
    if (this.elementToFocus) {
      this.elementToFocus.focus();
    }
  }

  private update(): void {
    this.element.removeChildren();
    this.elementToFocus = null;
    this.shortcutInputs.clear();

    this.element.classList.toggle('keybinds-editing', this.isEditing);
    this.element.createChild('div', 'keybinds-action-name keybinds-list-text').textContent = this.item.title();
    this.shortcuts.forEach(this.createShortcutRow, this);
    if (this.shortcuts.length === 0) {
      this.createEmptyInfo();
    }
    if (this.isEditing) {
      this.setupEditor();
    }
  }

  private createEmptyInfo(): void {
    if (UI.ShortcutRegistry.ShortcutRegistry.instance().actionHasDefaultShortcut(this.item.id())) {
      const icon = createIcon('keyboard-pen', 'keybinds-modified');
      UI.ARIAUtils.setLabel(icon, i18nString(UIStrings.shortcutModified));
      this.element.appendChild(icon);
    }
    if (!this.isEditing) {
      const emptyElement = this.element.createChild('div', 'keybinds-shortcut keybinds-list-text');
      UI.ARIAUtils.setLabel(emptyElement, i18nString(UIStrings.noShortcutForAction));
      this.element.appendChild(this.createEditButton());
    }
  }

  private setupEditor(): void {
    this.addShortcutLinkContainer = this.element.createChild('div', 'keybinds-shortcut');
    const addShortcutButton = UI.UIUtils.createTextButton(
        i18nString(UIStrings.addAShortcut), this.addShortcut.bind(this), {jslogContext: 'add-shortcut'});
    this.addShortcutLinkContainer.appendChild(addShortcutButton);
    if (!this.elementToFocus) {
      this.elementToFocus = addShortcutButton;
    }

    this.errorMessageElement = this.element.createChild('div', 'keybinds-info keybinds-error hidden');
    UI.ARIAUtils.markAsAlert(this.errorMessageElement);
    this.element.appendChild(this.createIconButton(
        i18nString(UIStrings.ResetShortcutsForAction), 'undo', '', 'undo', this.resetShortcutsToDefaults.bind(this)));
    this.confirmButton = this.createIconButton(
        i18nString(UIStrings.confirmChanges), 'checkmark', 'keybinds-confirm-button', 'confirm', () => {
          this.settingsTab.commitChanges(this.item, this.editedShortcuts);
          UI.ARIAUtils.LiveAnnouncer.alert(i18nString(UIStrings.shortcutChangesApplied, {PH1: this.item.title()}));
        });
    this.element.appendChild(this.confirmButton);
    this.element.appendChild(
        this.createIconButton(i18nString(UIStrings.discardChanges), 'cross', 'keybinds-cancel-button', 'cancel', () => {
          this.settingsTab.stopEditing(this.item);
          UI.ARIAUtils.LiveAnnouncer.alert(i18nString(UIStrings.shortcutChangesDiscarded));
        }));
    this.element.addEventListener('keydown', event => {
      if (Platform.KeyboardUtilities.isEscKey(event)) {
        this.settingsTab.stopEditing(this.item);
        event.consume(true);
      }
    });
  }

  private addShortcut(): void {
    const shortcut =
        new UI.KeyboardShortcut.KeyboardShortcut([], this.item.id(), UI.KeyboardShortcut.Type.UNSET_SHORTCUT);
    this.shortcuts.push(shortcut);
    this.update();
    const shortcutInput = this.shortcutInputs.get(shortcut) as HTMLElement;
    if (shortcutInput) {
      shortcutInput.focus();
    }
  }

  private createShortcutRow(shortcut: UI.KeyboardShortcut.KeyboardShortcut, index?: number): void {
    if (this.editedShortcuts.has(shortcut) && !this.editedShortcuts.get(shortcut)) {
      return;
    }
    let icon: Icon;
    if (shortcut.type !== UI.KeyboardShortcut.Type.UNSET_SHORTCUT && !shortcut.isDefault()) {
      icon = createIcon('keyboard-pen', 'keybinds-modified');
      UI.ARIAUtils.setLabel(icon, i18nString(UIStrings.shortcutModified));
      this.element.appendChild(icon);
    }
    const shortcutElement = this.element.createChild('div', 'keybinds-shortcut keybinds-list-text');
    if (this.isEditing) {
      const shortcutInput = shortcutElement.createChild('input', 'harmony-input');
      shortcutInput.setAttribute('jslog', `${VisualLogging.textField().track({change: true})}`);
      shortcutInput.setAttribute('placeholder', i18nString(UIStrings.recordingKeys));
      shortcutInput.spellcheck = false;
      shortcutInput.maxLength = 0;
      this.shortcutInputs.set(shortcut, shortcutInput);
      if (!this.elementToFocus) {
        this.elementToFocus = shortcutInput;
      }
      shortcutInput.value = shortcut.title();
      const userDescriptors = this.editedShortcuts.get(shortcut);
      if (userDescriptors) {
        shortcutInput.value = this.shortcutInputTextForDescriptors(userDescriptors);
      }
      shortcutInput.addEventListener('keydown', this.onShortcutInputKeyDown.bind(this, shortcut, shortcutInput));
      shortcutInput.addEventListener('blur', () => {
        if (this.secondKeyTimeout !== null) {
          clearTimeout(this.secondKeyTimeout);
          this.secondKeyTimeout = null;
        }
      });
      shortcutElement.appendChild(
          this.createIconButton(i18nString(UIStrings.removeShortcut), 'bin', 'keybinds-delete-button', 'delete', () => {
            const index = this.shortcuts.indexOf(shortcut);
            if (!shortcut.isDefault()) {
              this.shortcuts.splice(index, 1);
            }
            this.editedShortcuts.set(shortcut, null);
            this.update();
            this.focus();
            this.validateInputs();
            UI.ARIAUtils.LiveAnnouncer.alert(i18nString(UIStrings.shortcutRemoved, {PH1: this.item.title()}));
          }));
    } else {
      const separator = Host.Platform.isMac() ? '\u2004' : ' + ';
      const keys = shortcut.descriptors.flatMap(descriptor => descriptor.name.split(separator));
      keys.forEach(key => {
        shortcutElement.createChild('div', 'keybinds-key').createChild('span').textContent = key;
      });
      if (index === 0) {
        this.element.appendChild(this.createEditButton());
      }
    }
  }

  private createEditButton(): HTMLElement {
    return this.createIconButton(
        i18nString(UIStrings.editShortcut), 'edit', 'keybinds-edit-button', 'edit',
        () => this.settingsTab.startEditing(this.item));
  }

  private createIconButton(
      label: string, iconName: string, className: string, jslogContext: string,
      listener: () => void): Buttons.Button.Button {
    const button = new Buttons.Button.Button();
    button.data = {variant: Buttons.Button.Variant.ICON, iconName, jslogContext, title: label};
    button.addEventListener('click', listener);
    UI.ARIAUtils.setLabel(button, label);
    if (className) {
      button.classList.add(className);
    }
    return button;
  }

  private onShortcutInputKeyDown(
      shortcut: UI.KeyboardShortcut.KeyboardShortcut, shortcutInput: HTMLInputElement, event: Event): void {
    if ((event as KeyboardEvent).key !== 'Tab') {
      const eventDescriptor = this.descriptorForEvent(event as KeyboardEvent);
      const userDescriptors = this.editedShortcuts.get(shortcut) || [];
      this.editedShortcuts.set(shortcut, userDescriptors);
      const isLastKeyOfShortcut =
          userDescriptors.length === 2 && UI.KeyboardShortcut.KeyboardShortcut.isModifier(userDescriptors[1].key);
      const shouldClearOldShortcut = userDescriptors.length === 2 && !isLastKeyOfShortcut;
      if (shouldClearOldShortcut) {
        userDescriptors.splice(0, 2);
      }
      if (this.secondKeyTimeout) {
        clearTimeout(this.secondKeyTimeout);
        this.secondKeyTimeout = null;
        userDescriptors.push(eventDescriptor);
      } else if (isLastKeyOfShortcut) {
        userDescriptors[1] = eventDescriptor;
      } else if (!UI.KeyboardShortcut.KeyboardShortcut.isModifier(eventDescriptor.key)) {
        userDescriptors[0] = eventDescriptor;
        this.secondKeyTimeout = window.setTimeout(() => {
          this.secondKeyTimeout = null;
        }, UI.ShortcutRegistry.KeyTimeout);
      } else {
        userDescriptors[0] = eventDescriptor;
      }
      shortcutInput.value = this.shortcutInputTextForDescriptors(userDescriptors);
      this.validateInputs();
      event.consume(true);
    }
  }

  private descriptorForEvent(event: KeyboardEvent): UI.KeyboardShortcut.Descriptor {
    const userKey = UI.KeyboardShortcut.KeyboardShortcut.makeKeyFromEvent(event);
    const codeAndModifiers = UI.KeyboardShortcut.KeyboardShortcut.keyCodeAndModifiersFromKey(userKey);
    let key: UI.KeyboardShortcut.Key|string =
        UI.KeyboardShortcut.Keys[event.key] || UI.KeyboardShortcut.KeyBindings[event.key];

    if (!key && !/^[a-z]$/i.test(event.key)) {
      const keyCode = event.code;
      // if we still don't have a key name, let's try the code before falling back to the raw key
      key = UI.KeyboardShortcut.Keys[keyCode] || UI.KeyboardShortcut.KeyBindings[keyCode];
      if (keyCode.startsWith('Digit')) {
        key = keyCode.slice(5);
      } else if (keyCode.startsWith('Key')) {
        key = keyCode.slice(3);
      }
    }

    return UI.KeyboardShortcut.KeyboardShortcut.makeDescriptor(key || event.key, codeAndModifiers.modifiers);
  }

  private shortcutInputTextForDescriptors(descriptors: UI.KeyboardShortcut.Descriptor[]): string {
    return descriptors.map(descriptor => descriptor.name).join(' ');
  }

  private resetShortcutsToDefaults(): void {
    this.editedShortcuts.clear();
    for (const shortcut of this.shortcuts) {
      if (shortcut.type === UI.KeyboardShortcut.Type.UNSET_SHORTCUT) {
        const index = this.shortcuts.indexOf(shortcut);
        this.shortcuts.splice(index, 1);
      } else if (shortcut.type === UI.KeyboardShortcut.Type.USER_SHORTCUT) {
        this.editedShortcuts.set(shortcut, null);
      }
    }
    const disabledDefaults = UI.ShortcutRegistry.ShortcutRegistry.instance().disabledDefaultsForAction(this.item.id());
    disabledDefaults.forEach(shortcut => {
      if (this.shortcuts.includes(shortcut)) {
        return;
      }

      this.shortcuts.push(shortcut);
      this.editedShortcuts.set(shortcut, shortcut.descriptors);
    });
    this.update();
    this.focus();
    UI.ARIAUtils.LiveAnnouncer.alert(i18nString(UIStrings.shortcutChangesRestored, {PH1: this.item.title()}));
  }

  onEscapeKeyPressed(event: Event): void {
    const activeElement = UI.DOMUtilities.deepActiveElement(document);
    for (const [shortcut, shortcutInput] of this.shortcutInputs.entries()) {
      if (activeElement === shortcutInput) {
        this.onShortcutInputKeyDown(shortcut, shortcutInput as HTMLInputElement, event);
      }
    }
  }

  private validateInputs(): void {
    const confirmButton = this.confirmButton;
    const errorMessageElement = this.errorMessageElement;
    if (!confirmButton || !errorMessageElement) {
      return;
    }

    confirmButton.disabled = false;
    errorMessageElement.classList.add('hidden');
    this.shortcutInputs.forEach((shortcutInput, shortcut) => {
      const userDescriptors = this.editedShortcuts.get(shortcut);
      if (!userDescriptors) {
        return;
      }
      if (userDescriptors.some(descriptor => UI.KeyboardShortcut.KeyboardShortcut.isModifier(descriptor.key))) {
        confirmButton.disabled = true;
        shortcutInput.classList.add('error-input');
        UI.ARIAUtils.setInvalid(shortcutInput, true);
        errorMessageElement.classList.remove('hidden');
        errorMessageElement.textContent = i18nString(UIStrings.shortcutsCannotContainOnly);
        return;
      }
      const conflicts = UI.ShortcutRegistry.ShortcutRegistry.instance()
                            .actionsForDescriptors(userDescriptors)
                            .filter(actionId => actionId !== this.item.id());
      if (conflicts.length) {
        confirmButton.disabled = true;
        shortcutInput.classList.add('error-input');
        UI.ARIAUtils.setInvalid(shortcutInput, true);
        errorMessageElement.classList.remove('hidden');
        if (!UI.ActionRegistry.ActionRegistry.instance().hasAction(conflicts[0])) {
          return;
        }
        const action = UI.ActionRegistry.ActionRegistry.instance().getAction(conflicts[0]);
        const actionTitle = action.title();
        const actionCategory = action.category();
        errorMessageElement.textContent =
            i18nString(UIStrings.thisShortcutIsInUseByS, {PH1: actionCategory, PH2: actionTitle});
        return;
      }
      shortcutInput.classList.remove('error-input');
      UI.ARIAUtils.setInvalid(shortcutInput, false);
    });
  }
}

export type KeybindsItem = UI.ActionRegistration.ActionCategory|UI.ActionRegistration.Action;
