Files
react-native-devtools-frontend/front_end/settings/KeybindsSettingsTab.ts
T
Simon Zünd 05f3f0a67d Update more links that were 'moved permanently' (301)
These were found by the `npm run check-external-links` tool after
two small code helpers were removed. The helpers built Urls dynamically
so the tool didn't pick them up before.

R=sigurds@chromium.org

Bug: chromium:1170310
Change-Id: I6d79a864f955d993acb5d2b22a402efad7ffb903
Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/2672025
Commit-Queue: Simon Zünd <szuend@chromium.org>
Commit-Queue: Sigurd Schneider <sigurds@chromium.org>
Auto-Submit: Simon Zünd <szuend@chromium.org>
Reviewed-by: Sigurd Schneider <sigurds@chromium.org>
2021-02-03 12:28:06 +00:00

600 lines
23 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/* eslint-disable rulesdir/no_underscored_properties */
import * as Common from '../common/common.js';
import * as Host from '../host/host.js';
import * as i18n from '../i18n/i18n.js';
import * as Root from '../root/root.js';
import * as UI from '../ui/ui.js';
export 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: 'Match shortcuts from 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 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',
};
const str_ = i18n.i18n.registerUIStrings('settings/KeybindsSettingsTab.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
let keybindsSettingsTabInstance: KeybindsSettingsTab;
export class KeybindsSettingsTab extends UI.Widget.VBox implements UI.ListControl.ListDelegate<KeybindsItem> {
_items: UI.ListModel.ListModel<KeybindsItem>;
_list: UI.ListControl.ListControl<string|UI.ActionRegistration.Action>;
_editingItem: UI.ActionRegistration.Action|null;
_editingRow: ShortcutListItem|null;
constructor() {
super(true);
this.registerRequiredCSS('settings/keybindsSettingsTab.css', {enableLegacyPatching: true});
const header = this.contentElement.createChild('header');
header.createChild('h1').textContent = i18nString(UIStrings.shortcuts);
const keybindsSetSetting = Common.Settings.Settings.instance().moduleSetting('activeKeybindSet');
const userShortcutsSetting = Common.Settings.Settings.instance().moduleSetting('userShortcuts');
userShortcutsSetting.addChangeListener(this.update, this);
keybindsSetSetting.addChangeListener(this.update, this);
const keybindsSetSelect =
UI.SettingsUI.createControlForSetting(keybindsSetSetting, i18nString(UIStrings.matchShortcutsFromPreset));
if (keybindsSetSelect) {
keybindsSetSelect.classList.add('keybinds-set-select');
this.contentElement.appendChild(keybindsSetSelect);
}
this._items = new UI.ListModel.ListModel();
this._list = new UI.ListControl.ListControl(this._items, this, UI.ListControl.ListMode.NonViewport);
this._items.replaceAll(this._createListItems());
UI.ARIAUtils.markAsList(this._list.element);
this.registerRequiredCSS('settings/keybindsSettingsTab.css', {enableLegacyPatching: true});
this.contentElement.appendChild(this._list.element);
UI.ARIAUtils.setAccessibleName(this._list.element, i18nString(UIStrings.keyboardShortcutsList));
const footer = this.contentElement.createChild('div');
footer.classList.add('keybinds-footer');
const docsLink = UI.XLink.XLink.create(
'https://developers.google.com/web/tools/chrome-devtools/shortcuts',
i18nString(UIStrings.FullListOfDevtoolsKeyboard));
docsLink.classList.add('docs-link');
footer.appendChild(docsLink);
footer.appendChild(UI.UIUtils.createTextButton(i18nString(UIStrings.RestoreDefaultShortcuts), () => {
userShortcutsSetting.set([]);
keybindsSetSetting.set(UI.ShortcutRegistry.DefaultShortcutSetting);
}));
this._editingItem = null;
this._editingRow = null;
this.update();
}
static instance(opts = {forceNew: null}): KeybindsSettingsTab {
const {forceNew} = opts;
if (!keybindsSettingsTabInstance || forceNew) {
keybindsSettingsTabInstance = new KeybindsSettingsTab();
}
return keybindsSettingsTabInstance;
}
createElementForItem(item: KeybindsItem): Element {
let itemElement = document.createElement('div');
if (typeof item === 'string') {
UI.ARIAUtils.setLevel(itemElement, 1);
itemElement.classList.add('keybinds-category-header');
itemElement.textContent = item;
} else {
const listItem = new ShortcutListItem(item, this, item === this._editingItem);
itemElement = listItem.element;
UI.ARIAUtils.setLevel(itemElement, 2);
if (item === this._editingItem) {
this._editingRow = listItem;
}
}
itemElement.classList.add('keybinds-list-item');
UI.ARIAUtils.markAsListitem(itemElement);
itemElement.tabIndex = item === this._list.selectedItem() && item !== this._editingItem ? 0 : -1;
return itemElement;
}
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.UnsetShortcut) {
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 as UI.KeyboardShortcut.Descriptor[])
.changeType(UI.KeyboardShortcut.Type.UserShortcut));
if (originalShortcut.type === UI.KeyboardShortcut.Type.UnsetShortcut) {
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 {
return true;
}
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 {
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();
}
_createListItems(): KeybindsItem[] {
const actions = UI.ActionRegistry.ActionRegistry.instance().actions().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: string;
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 = document.deepActiveElement();
if (this._editingRow && deepActiveElement && 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.selectItem(this._items.at(0));
}
}
willHide(): void {
if (this._editingItem) {
this.stopEditing(this._editingItem);
}
}
}
export class ShortcutListItem {
_isEditing: boolean;
_settingsTab: KeybindsSettingsTab;
_item: UI.ActionRegistration.Action;
element: HTMLDivElement;
_editedShortcuts: Map<UI.KeyboardShortcut.KeyboardShortcut, UI.KeyboardShortcut.Descriptor[]|null>;
_shortcutInputs: Map<UI.KeyboardShortcut.KeyboardShortcut, Element>;
_shortcuts: UI.KeyboardShortcut.KeyboardShortcut[];
_elementToFocus: HTMLElement|null;
_confirmButton: HTMLButtonElement|null;
_addShortcutLinkContainer: Element|null;
_errorMessageElement: Element|null;
_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._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();
}
}
_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();
}
}
_createEmptyInfo(): void {
if (UI.ShortcutRegistry.ShortcutRegistry.instance().actionHasDefaultShortcut(this._item.id())) {
const icon = UI.Icon.Icon.create('largeicon-shortcut-changed', 'keybinds-modified');
UI.ARIAUtils.setAccessibleName(icon, i18nString(UIStrings.shortcutModified));
this.element.appendChild(icon);
}
if (!this._isEditing) {
const emptyElement = this.element.createChild('div', 'keybinds-shortcut keybinds-list-text');
UI.ARIAUtils.setAccessibleName(emptyElement, i18nString(UIStrings.noShortcutForAction));
if (Root.Runtime.experiments.isEnabled('keyboardShortcutEditor')) {
this.element.appendChild(this._createEditButton());
}
}
}
_setupEditor(): void {
this._addShortcutLinkContainer = this.element.createChild('div', 'keybinds-shortcut devtools-link');
const addShortcutLink = this._addShortcutLinkContainer.createChild('span', 'devtools-link') as HTMLDivElement;
addShortcutLink.textContent = i18nString(UIStrings.addAShortcut);
addShortcutLink.tabIndex = 0;
UI.ARIAUtils.markAsLink(addShortcutLink);
self.onInvokeElement(addShortcutLink, this._addShortcut.bind(this));
if (!this._elementToFocus) {
this._elementToFocus = addShortcutLink;
}
this._errorMessageElement = this.element.createChild('div', 'keybinds-info keybinds-error hidden');
UI.ARIAUtils.markAsAlert(this._errorMessageElement);
this.element.appendChild(this._createIconButton(
i18nString(UIStrings.ResetShortcutsForAction), 'largeicon-undo', '',
this._resetShortcutsToDefaults.bind(this)));
this._confirmButton = this._createIconButton(
i18nString(UIStrings.confirmChanges), 'largeicon-checkmark', 'keybinds-confirm-button',
() => this._settingsTab.commitChanges(this._item, this._editedShortcuts));
this.element.appendChild(this._confirmButton);
this.element.appendChild(this._createIconButton(
i18nString(UIStrings.discardChanges), 'largeicon-delete', 'keybinds-cancel-button',
() => this._settingsTab.stopEditing(this._item)));
this.element.addEventListener('keydown', event => {
if (isEscKey(event)) {
this._settingsTab.stopEditing(this._item);
event.consume(true);
}
});
}
_addShortcut(): void {
const shortcut =
new UI.KeyboardShortcut.KeyboardShortcut([], this._item.id(), UI.KeyboardShortcut.Type.UnsetShortcut);
this._shortcuts.push(shortcut);
this._update();
const shortcutInput = this._shortcutInputs.get(shortcut) as HTMLElement;
if (shortcutInput) {
shortcutInput.focus();
}
}
_createShortcutRow(shortcut: UI.KeyboardShortcut.KeyboardShortcut, index?: number): void {
if (this._editedShortcuts.has(shortcut) && !this._editedShortcuts.get(shortcut)) {
return;
}
let icon: UI.Icon.Icon;
if (shortcut.type !== UI.KeyboardShortcut.Type.UnsetShortcut && !shortcut.isDefault()) {
icon = UI.Icon.Icon.create('largeicon-shortcut-changed', 'keybinds-modified');
UI.ARIAUtils.setAccessibleName(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') as HTMLInputElement;
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), 'largeicon-trash-bin', 'keybinds-delete-button', () => {
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();
}));
} else {
const keys = shortcut.descriptors.flatMap(descriptor => descriptor.name.split(' + '));
keys.forEach(key => {
shortcutElement.createChild('span', 'keybinds-key').textContent = key;
});
if (Root.Runtime.experiments.isEnabled('keyboardShortcutEditor') && index === 0) {
this.element.appendChild(this._createEditButton());
}
}
}
_createEditButton(): Element {
return this._createIconButton(
i18nString(UIStrings.editShortcut), 'largeicon-edit', 'keybinds-edit-button',
() => this._settingsTab.startEditing(this._item));
}
_createIconButton(label: string, iconName: string, className: string, listener: () => void): HTMLButtonElement {
const button = document.createElement('button') as HTMLButtonElement;
button.appendChild(UI.Icon.Icon.create(iconName));
button.addEventListener('click', listener);
UI.ARIAUtils.setAccessibleName(button, label);
if (className) {
button.classList.add(className);
}
return button;
}
_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);
}
}
_descriptorForEvent(event: KeyboardEvent): UI.KeyboardShortcut.Descriptor {
const userKey = UI.KeyboardShortcut.KeyboardShortcut.makeKeyFromEvent(event as KeyboardEvent);
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);
}
_shortcutInputTextForDescriptors(descriptors: UI.KeyboardShortcut.Descriptor[]): string {
return descriptors.map(descriptor => descriptor.name).join(' ');
}
_resetShortcutsToDefaults(): void {
this._editedShortcuts.clear();
for (const shortcut of this._shortcuts) {
if (shortcut.type === UI.KeyboardShortcut.Type.UnsetShortcut) {
const index = this._shortcuts.indexOf(shortcut);
this._shortcuts.splice(index, 1);
} else if (shortcut.type === UI.KeyboardShortcut.Type.UserShortcut) {
this._editedShortcuts.set(shortcut, null);
}
}
const disabledDefaults = UI.ShortcutRegistry.ShortcutRegistry.instance().disabledDefaultsForAction(this._item.id());
disabledDefaults.forEach(shortcut => {
this._shortcuts.push(shortcut);
this._editedShortcuts.set(shortcut, shortcut.descriptors);
});
this._update();
this.focus();
}
onEscapeKeyPressed(event: Event): void {
const activeElement = document.deepActiveElement();
for (const [shortcut, shortcutInput] of this._shortcutInputs.entries()) {
if (activeElement === shortcutInput) {
this._onShortcutInputKeyDown(
shortcut as UI.KeyboardShortcut.KeyboardShortcut, shortcutInput as HTMLInputElement,
event as KeyboardEvent);
}
}
}
_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');
const action = UI.ActionRegistry.ActionRegistry.instance().action(conflicts[0]);
if (!action) {
return;
}
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 = string|UI.ActionRegistration.Action;