blob: 0b584ff8cdc1c41aae99c9b928ee8f827fdb7603 [file] [log] [blame]
Blink Reformat4c46d092018-04-07 15:32:371// Copyright 2014 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
Paul Lewis9950e182019-12-16 16:06:074
Tim van der Lippeee97fa32020-04-23 15:20:565// @ts-nocheck
6// TODO(crbug.com/1011811): Enable TypeScript compiler checks
7
Paul Lewis0fd43712020-01-08 17:07:368import * as Host from '../host/host.js';
Tim van der Lippeee97fa32020-04-23 15:20:569import * as Platform from '../platform/platform.js';
Tim van der Lippeaa76aa22020-02-14 14:38:2410
Paul Lewis9950e182019-12-16 16:06:0711import {Action} from './Action.js'; // eslint-disable-line no-unused-vars
12import {ActionRegistry} from './ActionRegistry.js'; // eslint-disable-line no-unused-vars
13import {Context} from './Context.js';
14import {Dialog} from './Dialog.js';
Jack Lynch94a9b0c2020-03-11 21:45:1415import {Descriptor, KeyboardShortcut, Modifiers, Type} from './KeyboardShortcut.js'; // eslint-disable-line no-unused-vars
Paul Lewis9950e182019-12-16 16:06:0716import {isEditing} from './UIUtils.js';
17
Sigurd Schneider46da7db2020-05-20 13:45:1118
Paul Lewis9950e182019-12-16 16:06:0719export class ShortcutRegistry {
Blink Reformat4c46d092018-04-07 15:32:3720 /**
Paul Lewis9950e182019-12-16 16:06:0721 * @param {!ActionRegistry} actionRegistry
Blink Reformat4c46d092018-04-07 15:32:3722 */
Jack Lynch94a9b0c2020-03-11 21:45:1423 constructor(actionRegistry) {
Blink Reformat4c46d092018-04-07 15:32:3724 this._actionRegistry = actionRegistry;
Jack Lynch94a9b0c2020-03-11 21:45:1425 /** @type {!Platform.Multimap.<number, !KeyboardShortcut>} */
26 this._keyToShortcut = new Platform.Multimap();
27 /** @type {!Platform.Multimap.<string, !KeyboardShortcut>} */
28 this._actionToShortcut = new Platform.Multimap();
Jack Lynch966040b2020-04-23 20:11:4429 this._keyMap = new ShortcutTreeNode(0, 0);
30 /** @type {?ShortcutTreeNode} */
31 this._activePrefixKey = null;
32 /** @type {?number} */
33 this._activePrefixTimeout = null;
34 /** @type {?function():Promise<void>} */
35 this._consumePrefix = null;
Jack Lynchf1f00fa2020-05-01 22:16:1236 const keybindSetSetting = self.Common.settings.moduleSetting('activeKeybindSet');
37 if (!Root.Runtime.experiments.isEnabled('customKeyboardShortcuts') &&
38 keybindSetSetting.get() !== DefaultShortcutSetting) {
39 keybindSetSetting.set(DefaultShortcutSetting);
40 }
41 keybindSetSetting.addChangeListener(this._registerBindings, this);
42
Jack Lynch94a9b0c2020-03-11 21:45:1443 this._registerBindings();
Blink Reformat4c46d092018-04-07 15:32:3744 }
45
46 /**
47 * @param {number} key
Jack Lynch966040b2020-04-23 20:11:4448 * @param {!Object.<string, function():Promise.<boolean>>=} handlers
Paul Lewis9950e182019-12-16 16:06:0749 * @return {!Array.<!Action>}
Blink Reformat4c46d092018-04-07 15:32:3750 */
Jack Lynch966040b2020-04-23 20:11:4451 _applicableActions(key, handlers = {}) {
52 let actions = [];
53 const keyMap = this._activePrefixKey || this._keyMap;
54 const keyNode = keyMap.getNode(key);
55 if (keyNode) {
56 actions = keyNode.actions();
57 }
58 const applicableActions = this._actionRegistry.applicableActions(actions, self.UI.context);
59 if (keyNode) {
60 for (const actionId of Object.keys(handlers)) {
61 if (keyNode.actions().indexOf(actionId) >= 0) {
62 const action = this._actionRegistry.action(actionId);
63 if (action) {
64 applicableActions.push(action);
65 }
66 }
67 }
68 }
69 return applicableActions;
Blink Reformat4c46d092018-04-07 15:32:3770 }
71
72 /**
Jack Lynch575e9fb2020-03-26 22:20:5173 * @param {string} action
74 * @return {!Array.<!KeyboardShortcut>}
75 */
76 shortcutsForAction(action) {
77 return [...this._actionToShortcut.get(action)];
78 }
79
80 /**
Joel Einbinder67f28fb2018-08-02 00:33:4781 * @return {!Array<number>}
82 */
83 globalShortcutKeys() {
84 const keys = [];
Jack Lynch966040b2020-04-23 20:11:4485 for (const node of this._keyMap.chords().values()) {
86 const actions = node.actions();
Paul Lewis9950e182019-12-16 16:06:0787 const applicableActions = this._actionRegistry.applicableActions(actions, new Context());
Jack Lynch966040b2020-04-23 20:11:4488 if (applicableActions.length || node.hasChords()) {
89 keys.push(node.key());
Tim van der Lippe1d6e57a2019-09-30 11:55:3490 }
Joel Einbinder67f28fb2018-08-02 00:33:4791 }
92 return keys;
93 }
94
95 /**
Jack Lynchb8fb3c72020-04-21 05:36:1696 * @deprecated this function is obsolete and will be removed in the
97 * future along with the legacy shortcuts settings tab
98 * crbug.com/174309
99 *
Blink Reformat4c46d092018-04-07 15:32:37100 * @param {string} actionId
Tim van der Lippeaa76aa22020-02-14 14:38:24101 * @return {!Array.<!Descriptor>}
Blink Reformat4c46d092018-04-07 15:32:37102 */
103 shortcutDescriptorsForAction(actionId) {
Jack Lynchb8fb3c72020-04-21 05:36:16104 return [...this._actionToShortcut.get(actionId)].map(shortcut => shortcut.descriptors[0]);
Blink Reformat4c46d092018-04-07 15:32:37105 }
106
107 /**
108 * @param {!Array.<string>} actionIds
109 * @return {!Array.<number>}
110 */
111 keysForActions(actionIds) {
Jack Lynchb8fb3c72020-04-21 05:36:16112 const keys = actionIds.flatMap(
113 action => [...this._actionToShortcut.get(action)].flatMap(
114 shortcut => shortcut.descriptors.map(descriptor => descriptor.key)));
115 return [...(new Set(keys))];
Blink Reformat4c46d092018-04-07 15:32:37116 }
117
118 /**
119 * @param {string} actionId
120 * @return {string|undefined}
121 */
122 shortcutTitleForAction(actionId) {
Jack Lynchb8fb3c72020-04-21 05:36:16123 const shortcuts = this._actionToShortcut.get(actionId);
124 if (shortcuts.size) {
125 return shortcuts.firstValue().title();
Tim van der Lippe1d6e57a2019-09-30 11:55:34126 }
Blink Reformat4c46d092018-04-07 15:32:37127 }
128
129 /**
130 * @param {!KeyboardEvent} event
Jack Lynch966040b2020-04-23 20:11:44131 * @param {!Object.<string, function():Promise.<boolean>>=} handlers
Blink Reformat4c46d092018-04-07 15:32:37132 */
Jack Lynch966040b2020-04-23 20:11:44133 handleShortcut(event, handlers) {
134 this.handleKey(KeyboardShortcut.makeKeyFromEvent(event), event.key, event, handlers);
Blink Reformat4c46d092018-04-07 15:32:37135 }
136
137 /**
138 * @param {!Element} element
Jack Lynch966040b2020-04-23 20:11:44139 * @param {!Object.<string, function():Promise.<boolean>>} handlers
Blink Reformat4c46d092018-04-07 15:32:37140 */
Jack Lynch966040b2020-04-23 20:11:44141 addShortcutListener(element, handlers) {
142 // We only want keys for these specific actions to get handled this
143 // way; all others should be allowed to bubble up
144 const whitelistKeyMap = new ShortcutTreeNode(0, 0);
145 const shortcuts = Object.keys(handlers).flatMap(action => [...this._actionToShortcut.get(action)]);
146 shortcuts.forEach(shortcut => {
147 whitelistKeyMap.addKeyMapping(shortcut.descriptors.map(descriptor => descriptor.key), shortcut.action);
148 });
149
Blink Reformat4c46d092018-04-07 15:32:37150 element.addEventListener('keydown', event => {
Jack Lynch966040b2020-04-23 20:11:44151 const key = KeyboardShortcut.makeKeyFromEvent(/** @type {!KeyboardEvent} */ (event));
152 let keyMap = whitelistKeyMap;
153 if (this._activePrefixKey) {
154 keyMap = keyMap.getNode(this._activePrefixKey.key());
155 if (!keyMap) {
156 return;
157 }
Tim van der Lippe1d6e57a2019-09-30 11:55:34158 }
Jack Lynch966040b2020-04-23 20:11:44159 if (keyMap.getNode(key)) {
160 this.handleShortcut(/** @type {!KeyboardEvent} */ (event), handlers);
161 }
162 });
Blink Reformat4c46d092018-04-07 15:32:37163 }
164
165 /**
166 * @param {number} key
167 * @param {string} domKey
168 * @param {!KeyboardEvent=} event
Jack Lynch966040b2020-04-23 20:11:44169 * @param {!Object.<string, function():Promise.<boolean>>=} handlers
Blink Reformat4c46d092018-04-07 15:32:37170 */
Jack Lynch966040b2020-04-23 20:11:44171 async handleKey(key, domKey, event, handlers) {
Blink Reformat4c46d092018-04-07 15:32:37172 const keyModifiers = key >> 8;
Jack Lyncha0dd1f02020-05-04 23:33:35173 const hasHandlersOrPrefixKey = !!handlers || !!this._activePrefixKey;
Jack Lynch4a6f7532020-05-07 00:48:09174 const keyMapNode = this._keyMap.getNode(key);
175 const maybeHasActions = this._applicableActions(key, handlers).length > 0 || (keyMapNode && keyMapNode.hasChords());
Jack Lyncha0dd1f02020-05-04 23:33:35176 if ((!hasHandlersOrPrefixKey && isPossiblyInputKey()) || !maybeHasActions ||
Jack Lynch966040b2020-04-23 20:11:44177 KeyboardShortcut.isModifier(KeyboardShortcut.keyCodeAndModifiersFromKey(key).keyCode)) {
Blink Reformat4c46d092018-04-07 15:32:37178 return;
Tim van der Lippe1d6e57a2019-09-30 11:55:34179 }
Jack Lyncha0dd1f02020-05-04 23:33:35180 if (event) {
181 event.consume(true);
182 }
183 if (!hasHandlersOrPrefixKey && Dialog.hasInstance()) {
184 return;
185 }
Jack Lynch966040b2020-04-23 20:11:44186
187 if (this._activePrefixTimeout) {
188 clearTimeout(this._activePrefixTimeout);
189 const handled = await maybeExecuteActionForKey.call(this);
190 this._activePrefixKey = null;
191 this._activePrefixTimeout = null;
192 if (handled) {
193 return;
Tim van der Lippe1d6e57a2019-09-30 11:55:34194 }
Jack Lynch966040b2020-04-23 20:11:44195 if (this._consumePrefix) {
196 await this._consumePrefix();
197 }
198 }
Jack Lynch966040b2020-04-23 20:11:44199 if (keyMapNode && keyMapNode.hasChords()) {
Jack Lynch966040b2020-04-23 20:11:44200 this._activePrefixKey = keyMapNode;
201 this._consumePrefix = async () => {
202 this._activePrefixKey = null;
203 this._activePrefixTimeout = null;
204 await maybeExecuteActionForKey.call(this);
205 };
206 this._activePrefixTimeout = setTimeout(this._consumePrefix, KeyTimeout);
207 } else {
208 await maybeExecuteActionForKey.call(this);
Blink Reformat4c46d092018-04-07 15:32:37209 }
210
211 /**
212 * @return {boolean}
213 */
214 function isPossiblyInputKey() {
Paul Lewis9950e182019-12-16 16:06:07215 if (!event || !isEditing() || /^F\d+|Control|Shift|Alt|Meta|Escape|Win|U\+001B$/.test(domKey)) {
Blink Reformat4c46d092018-04-07 15:32:37216 return false;
Tim van der Lippe1d6e57a2019-09-30 11:55:34217 }
Blink Reformat4c46d092018-04-07 15:32:37218
Tim van der Lippe1d6e57a2019-09-30 11:55:34219 if (!keyModifiers) {
Blink Reformat4c46d092018-04-07 15:32:37220 return true;
Tim van der Lippe1d6e57a2019-09-30 11:55:34221 }
Blink Reformat4c46d092018-04-07 15:32:37222
Paul Lewis9950e182019-12-16 16:06:07223 const modifiers = Modifiers;
Joel Einbindera66e5bf2018-05-31 01:26:37224 // Undo/Redo will also cause input, so textual undo should take precedence over DevTools undo when editing.
Paul Lewis0fd43712020-01-08 17:07:36225 if (Host.Platform.isMac()) {
Paul Lewis9950e182019-12-16 16:06:07226 if (KeyboardShortcut.makeKey('z', modifiers.Meta) === key) {
Joel Einbindera66e5bf2018-05-31 01:26:37227 return true;
Tim van der Lippe1d6e57a2019-09-30 11:55:34228 }
Paul Lewis9950e182019-12-16 16:06:07229 if (KeyboardShortcut.makeKey('z', modifiers.Meta | modifiers.Shift) === key) {
Joel Einbindera66e5bf2018-05-31 01:26:37230 return true;
Tim van der Lippe1d6e57a2019-09-30 11:55:34231 }
Joel Einbindera66e5bf2018-05-31 01:26:37232 } else {
Paul Lewis9950e182019-12-16 16:06:07233 if (KeyboardShortcut.makeKey('z', modifiers.Ctrl) === key) {
Joel Einbindera66e5bf2018-05-31 01:26:37234 return true;
Tim van der Lippe1d6e57a2019-09-30 11:55:34235 }
Paul Lewis9950e182019-12-16 16:06:07236 if (KeyboardShortcut.makeKey('y', modifiers.Ctrl) === key) {
Joel Einbindera66e5bf2018-05-31 01:26:37237 return true;
Tim van der Lippe1d6e57a2019-09-30 11:55:34238 }
Paul Lewis0fd43712020-01-08 17:07:36239 if (!Host.Platform.isWin() && KeyboardShortcut.makeKey('z', modifiers.Ctrl | modifiers.Shift) === key) {
Joel Einbindera66e5bf2018-05-31 01:26:37240 return true;
Tim van der Lippe1d6e57a2019-09-30 11:55:34241 }
Joel Einbindera66e5bf2018-05-31 01:26:37242 }
243
Tim van der Lippe1d6e57a2019-09-30 11:55:34244 if ((keyModifiers & (modifiers.Ctrl | modifiers.Alt)) === (modifiers.Ctrl | modifiers.Alt)) {
Paul Lewis0fd43712020-01-08 17:07:36245 return Host.Platform.isWin();
Tim van der Lippe1d6e57a2019-09-30 11:55:34246 }
Blink Reformat4c46d092018-04-07 15:32:37247
248 return !hasModifier(modifiers.Ctrl) && !hasModifier(modifiers.Alt) && !hasModifier(modifiers.Meta);
249 }
250
251 /**
252 * @param {number} mod
253 * @return {boolean}
254 */
255 function hasModifier(mod) {
256 return !!(keyModifiers & mod);
257 }
Jack Lynch966040b2020-04-23 20:11:44258
259 /**
260 * @return {!Promise.<boolean>};
261 * @this {!ShortcutRegistry}
262 */
263 async function maybeExecuteActionForKey() {
264 const actions = this._applicableActions(key, handlers);
265 if (!actions.length) {
266 return false;
267 }
Jack Lynch966040b2020-04-23 20:11:44268 for (const action of actions) {
269 let handled;
270 if (handlers && handlers[action.id()]) {
271 handled = await handlers[action.id()]();
272 }
273 if (!handlers) {
274 handled = await action.execute();
275 }
276 if (handled) {
277 Host.userMetrics.keyboardShortcutFired(action.id());
278 return true;
279 }
280 }
281 return false;
282 }
Blink Reformat4c46d092018-04-07 15:32:37283 }
284
285 /**
Jack Lynch94a9b0c2020-03-11 21:45:14286 * @param {!KeyboardShortcut} shortcut
Blink Reformat4c46d092018-04-07 15:32:37287 */
Jack Lynch94a9b0c2020-03-11 21:45:14288 _registerShortcut(shortcut) {
289 this._actionToShortcut.set(shortcut.action, shortcut);
Jack Lynch966040b2020-04-23 20:11:44290 this._keyMap.addKeyMapping(shortcut.descriptors.map(descriptor => descriptor.key), shortcut.action);
Blink Reformat4c46d092018-04-07 15:32:37291 }
292
Jack Lynch94a9b0c2020-03-11 21:45:14293 _registerBindings() {
Jack Lynchf1f00fa2020-05-01 22:16:12294 this._keyToShortcut.clear();
295 this._actionToShortcut.clear();
296 this._keyMap.clear();
297 const keybindSet = self.Common.settings.moduleSetting('activeKeybindSet').get();
Blink Reformat4c46d092018-04-07 15:32:37298 const extensions = self.runtime.extensions('action');
299 extensions.forEach(registerExtension, this);
300
301 /**
Tim van der Lippe99e59b82019-09-30 20:00:59302 * @param {!Root.Runtime.Extension} extension
Tim van der Lippe0830b3d2019-10-03 13:20:07303 * @this {ShortcutRegistry}
Blink Reformat4c46d092018-04-07 15:32:37304 */
305 function registerExtension(extension) {
306 const descriptor = extension.descriptor();
Jack Lynch94a9b0c2020-03-11 21:45:14307 const bindings = descriptor.bindings;
Blink Reformat4c46d092018-04-07 15:32:37308 for (let i = 0; bindings && i < bindings.length; ++i) {
Jack Lynchf1f00fa2020-05-01 22:16:12309 const keybindSets = bindings[i].keybindSets;
310 if (!platformMatches(bindings[i].platform) || !keybindSetsMatch(keybindSets)) {
Blink Reformat4c46d092018-04-07 15:32:37311 continue;
Tim van der Lippe1d6e57a2019-09-30 11:55:34312 }
Jack Lynch966040b2020-04-23 20:11:44313 const keys = bindings[i].shortcut.split(/\s+/);
314 const shortcutDescriptors = keys.map(KeyboardShortcut.makeDescriptorFromBindingShortcut);
315 if (shortcutDescriptors.length > 0) {
Jack Lynchf1f00fa2020-05-01 22:16:12316 const actionId = /** @type {string} */ (descriptor.actionId);
317 if (!keybindSets) {
318 this._registerShortcut(new KeyboardShortcut(shortcutDescriptors, actionId, Type.DefaultShortcut));
319 } else {
320 this._registerShortcut(
321 new KeyboardShortcut(shortcutDescriptors, actionId, Type.KeybindSetShortcut, keybindSet));
322 }
Jack Lynchf0d14242020-04-07 22:42:04323 }
Blink Reformat4c46d092018-04-07 15:32:37324 }
325 }
326
327 /**
328 * @param {string=} platformsString
329 * @return {boolean}
330 */
331 function platformMatches(platformsString) {
Tim van der Lippe1d6e57a2019-09-30 11:55:34332 if (!platformsString) {
Blink Reformat4c46d092018-04-07 15:32:37333 return true;
Tim van der Lippe1d6e57a2019-09-30 11:55:34334 }
Blink Reformat4c46d092018-04-07 15:32:37335 const platforms = platformsString.split(',');
336 let isMatch = false;
Paul Lewis0fd43712020-01-08 17:07:36337 const currentPlatform = Host.Platform.platform();
Tim van der Lippe1d6e57a2019-09-30 11:55:34338 for (let i = 0; !isMatch && i < platforms.length; ++i) {
Blink Reformat4c46d092018-04-07 15:32:37339 isMatch = platforms[i] === currentPlatform;
Tim van der Lippe1d6e57a2019-09-30 11:55:34340 }
Blink Reformat4c46d092018-04-07 15:32:37341 return isMatch;
342 }
Jack Lynchf1f00fa2020-05-01 22:16:12343
344 /**
345 * @param {!Array<string>=} keybindSets
346 */
347 function keybindSetsMatch(keybindSets) {
348 if (!keybindSets) {
349 return true;
350 }
351 return keybindSets.includes(keybindSet);
352 }
Blink Reformat4c46d092018-04-07 15:32:37353 }
Tim van der Lippe0830b3d2019-10-03 13:20:07354}
Blink Reformat4c46d092018-04-07 15:32:37355
Jack Lynch966040b2020-04-23 20:11:44356export class ShortcutTreeNode {
357 /**
358 * @param {number} key
359 * @param {number=} depth
360 */
361 constructor(key, depth = 0) {
362 this._key = key;
363 /** @type {!Array.<string>} */
364 this._actions = [];
365 this._chords = new Map();
366 this._depth = depth;
367 }
368
369 /**
370 * @param {string} action
371 */
372 addAction(action) {
373 this._actions.push(action);
374 }
375
376 /**
377 * @return {number}
378 */
379 key() {
380 return this._key;
381 }
382
383 /**
384 * @return {!Map.<number, !ShortcutTreeNode>}
385 */
386 chords() {
387 return this._chords;
388 }
389
390 /**
391 * @return {boolean}
392 */
393 hasChords() {
394 return this._chords.size > 0;
395 }
396
397 /**
398 * @param {!Array.<number>} keys
399 * @param {string} action
400 */
401 addKeyMapping(keys, action) {
402 if (keys.length < this._depth) {
403 return;
404 }
405
406 if (keys.length === this._depth) {
407 this.addAction(action);
408 } else {
409 const key = keys[this._depth];
410 if (!this._chords.has(key)) {
411 this._chords.set(key, new ShortcutTreeNode(key, this._depth + 1));
412 }
413 this._chords.get(key).addKeyMapping(keys, action);
414 }
415 }
416
417 /**
418 * @param {number} key
419 * @return {?ShortcutTreeNode}
420 */
421 getNode(key) {
422 return this._chords.get(key) || null;
423 }
424
425 /**
426 * @return {!Array.<string>}
427 */
428 actions() {
429 return this._actions;
430 }
Jack Lynchf1f00fa2020-05-01 22:16:12431
432 clear() {
433 this._actions = [];
434 this._chords = new Map();
435 }
Jack Lynch966040b2020-04-23 20:11:44436}
437
Sigurd Schneider46da7db2020-05-20 13:45:11438
Tim van der Lippe0830b3d2019-10-03 13:20:07439export class ForwardedShortcut {}
Blink Reformat4c46d092018-04-07 15:32:37440
Tim van der Lippe0830b3d2019-10-03 13:20:07441ForwardedShortcut.instance = new ForwardedShortcut();
Jack Lynch966040b2020-04-23 20:11:44442
443export const KeyTimeout = 1000;
Jack Lynchf1f00fa2020-05-01 22:16:12444export const DefaultShortcutSetting = 'devToolsDefault';