blob: f0fef565d1949ec6d3527906c9da6d69ea74a738 [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
Paul Lewis0fd43712020-01-08 17:07:365import * as Host from '../host/host.js';
Tim van der Lippeaa76aa22020-02-14 14:38:246
Paul Lewis9950e182019-12-16 16:06:077import {Action} from './Action.js'; // eslint-disable-line no-unused-vars
8import {ActionRegistry} from './ActionRegistry.js'; // eslint-disable-line no-unused-vars
9import {Context} from './Context.js';
10import {Dialog} from './Dialog.js';
Jack Lynch94a9b0c2020-03-11 21:45:1411import {Descriptor, KeyboardShortcut, Modifiers, Type} from './KeyboardShortcut.js'; // eslint-disable-line no-unused-vars
Paul Lewis9950e182019-12-16 16:06:0712import {isEditing} from './UIUtils.js';
13
Blink Reformat4c46d092018-04-07 15:32:3714/**
15 * @unrestricted
16 */
Paul Lewis9950e182019-12-16 16:06:0717export class ShortcutRegistry {
Blink Reformat4c46d092018-04-07 15:32:3718 /**
Paul Lewis9950e182019-12-16 16:06:0719 * @param {!ActionRegistry} actionRegistry
Blink Reformat4c46d092018-04-07 15:32:3720 */
Jack Lynch94a9b0c2020-03-11 21:45:1421 constructor(actionRegistry) {
Blink Reformat4c46d092018-04-07 15:32:3722 this._actionRegistry = actionRegistry;
Jack Lynch94a9b0c2020-03-11 21:45:1423 /** @type {!Platform.Multimap.<number, !KeyboardShortcut>} */
24 this._keyToShortcut = new Platform.Multimap();
25 /** @type {!Platform.Multimap.<string, !KeyboardShortcut>} */
26 this._actionToShortcut = new Platform.Multimap();
27 this._registerBindings();
Blink Reformat4c46d092018-04-07 15:32:3728 }
29
30 /**
31 * @param {number} key
Paul Lewis9950e182019-12-16 16:06:0732 * @return {!Array.<!Action>}
Blink Reformat4c46d092018-04-07 15:32:3733 */
34 _applicableActions(key) {
Jack Lynch94a9b0c2020-03-11 21:45:1435 return this._actionRegistry.applicableActions(this.actionsForKey(key), self.UI.context);
Blink Reformat4c46d092018-04-07 15:32:3736 }
37
38 /**
39 * @param {number} key
Jack Lynch94a9b0c2020-03-11 21:45:1440 * @return {!Array.<string>}
Blink Reformat4c46d092018-04-07 15:32:3741 */
Jack Lynch94a9b0c2020-03-11 21:45:1442 actionsForKey(key) {
43 const shortcuts = [...this._keyToShortcut.get(key)];
44 return shortcuts.map(shortcut => shortcut.action);
Blink Reformat4c46d092018-04-07 15:32:3745 }
46
47 /**
Joel Einbinder67f28fb2018-08-02 00:33:4748 * @return {!Array<number>}
49 */
50 globalShortcutKeys() {
51 const keys = [];
Jack Lynch94a9b0c2020-03-11 21:45:1452 for (const key of this._keyToShortcut.keysArray()) {
53 const actions = [...this._keyToShortcut.get(key)];
Paul Lewis9950e182019-12-16 16:06:0754 const applicableActions = this._actionRegistry.applicableActions(actions, new Context());
Tim van der Lippe1d6e57a2019-09-30 11:55:3455 if (applicableActions.length) {
Jack Lynch94a9b0c2020-03-11 21:45:1456 keys.push(key);
Tim van der Lippe1d6e57a2019-09-30 11:55:3457 }
Joel Einbinder67f28fb2018-08-02 00:33:4758 }
59 return keys;
60 }
61
62 /**
Blink Reformat4c46d092018-04-07 15:32:3763 * @param {string} actionId
Tim van der Lippeaa76aa22020-02-14 14:38:2464 * @return {!Array.<!Descriptor>}
Blink Reformat4c46d092018-04-07 15:32:3765 */
66 shortcutDescriptorsForAction(actionId) {
Jack Lynch94a9b0c2020-03-11 21:45:1467 return [...this._actionToShortcut.get(actionId)].map(shortcut => shortcut.descriptor);
Blink Reformat4c46d092018-04-07 15:32:3768 }
69
70 /**
71 * @param {!Array.<string>} actionIds
72 * @return {!Array.<number>}
73 */
74 keysForActions(actionIds) {
75 const result = [];
76 for (let i = 0; i < actionIds.length; ++i) {
77 const descriptors = this.shortcutDescriptorsForAction(actionIds[i]);
Tim van der Lippe1d6e57a2019-09-30 11:55:3478 for (let j = 0; j < descriptors.length; ++j) {
Blink Reformat4c46d092018-04-07 15:32:3779 result.push(descriptors[j].key);
Tim van der Lippe1d6e57a2019-09-30 11:55:3480 }
Blink Reformat4c46d092018-04-07 15:32:3781 }
82 return result;
83 }
84
85 /**
86 * @param {string} actionId
87 * @return {string|undefined}
88 */
89 shortcutTitleForAction(actionId) {
90 const descriptors = this.shortcutDescriptorsForAction(actionId);
Tim van der Lippe1d6e57a2019-09-30 11:55:3491 if (descriptors.length) {
Blink Reformat4c46d092018-04-07 15:32:3792 return descriptors[0].name;
Tim van der Lippe1d6e57a2019-09-30 11:55:3493 }
Blink Reformat4c46d092018-04-07 15:32:3794 }
95
96 /**
97 * @param {!KeyboardEvent} event
98 */
99 handleShortcut(event) {
Paul Lewis9950e182019-12-16 16:06:07100 this.handleKey(KeyboardShortcut.makeKeyFromEvent(event), event.key, event);
Blink Reformat4c46d092018-04-07 15:32:37101 }
102
103 /**
104 * @param {!KeyboardEvent} event
105 * @param {string} actionId
106 * @return {boolean}
107 */
108 eventMatchesAction(event, actionId) {
Jack Lynch94a9b0c2020-03-11 21:45:14109 console.assert(this._actionToShortcut.has(actionId), 'Unknown action ' + actionId);
Paul Lewis9950e182019-12-16 16:06:07110 const key = KeyboardShortcut.makeKeyFromEvent(event);
Jack Lynch94a9b0c2020-03-11 21:45:14111 return [...this._actionToShortcut.get(actionId)].some(shortcut => shortcut.descriptor.key === key);
Blink Reformat4c46d092018-04-07 15:32:37112 }
113
114 /**
115 * @param {!Element} element
116 * @param {string} actionId
117 * @param {function():boolean} listener
118 * @param {boolean=} capture
119 */
120 addShortcutListener(element, actionId, listener, capture) {
Jack Lynch94a9b0c2020-03-11 21:45:14121 console.assert(this._actionToShortcut.has(actionId), 'Unknown action ' + actionId);
Blink Reformat4c46d092018-04-07 15:32:37122 element.addEventListener('keydown', event => {
Tim van der Lippe1d6e57a2019-09-30 11:55:34123 if (!this.eventMatchesAction(/** @type {!KeyboardEvent} */ (event), actionId) || !listener.call(null)) {
Blink Reformat4c46d092018-04-07 15:32:37124 return;
Tim van der Lippe1d6e57a2019-09-30 11:55:34125 }
Blink Reformat4c46d092018-04-07 15:32:37126 event.consume(true);
127 }, capture);
128 }
129
130 /**
131 * @param {number} key
132 * @param {string} domKey
133 * @param {!KeyboardEvent=} event
134 */
Erik Luo9855f612018-10-18 19:11:30135 async handleKey(key, domKey, event) {
Blink Reformat4c46d092018-04-07 15:32:37136 const keyModifiers = key >> 8;
137 const actions = this._applicableActions(key);
Tim van der Lippe1d6e57a2019-09-30 11:55:34138 if (!actions.length || isPossiblyInputKey()) {
Blink Reformat4c46d092018-04-07 15:32:37139 return;
Tim van der Lippe1d6e57a2019-09-30 11:55:34140 }
141 if (event) {
Erik Luo9855f612018-10-18 19:11:30142 event.consume(true);
Tim van der Lippe1d6e57a2019-09-30 11:55:34143 }
Paul Lewis9950e182019-12-16 16:06:07144 if (Dialog.hasInstance()) {
Blink Reformat4c46d092018-04-07 15:32:37145 return;
Tim van der Lippe1d6e57a2019-09-30 11:55:34146 }
Erik Luo9855f612018-10-18 19:11:30147 for (const action of actions) {
Mandy Chenc1c5bec2019-12-10 19:11:31148 try {
149 const result = await action.execute();
150 if (result) {
Jack Lynchad5c5862020-02-27 19:40:41151 Host.userMetrics.keyboardShortcutFired(action.id());
Mandy Chenc1c5bec2019-12-10 19:11:31152 return;
153 }
154 } catch (e) {
155 console.error(e);
156 throw e;
Tim van der Lippe1d6e57a2019-09-30 11:55:34157 }
Blink Reformat4c46d092018-04-07 15:32:37158 }
159
160 /**
161 * @return {boolean}
162 */
163 function isPossiblyInputKey() {
Paul Lewis9950e182019-12-16 16:06:07164 if (!event || !isEditing() || /^F\d+|Control|Shift|Alt|Meta|Escape|Win|U\+001B$/.test(domKey)) {
Blink Reformat4c46d092018-04-07 15:32:37165 return false;
Tim van der Lippe1d6e57a2019-09-30 11:55:34166 }
Blink Reformat4c46d092018-04-07 15:32:37167
Tim van der Lippe1d6e57a2019-09-30 11:55:34168 if (!keyModifiers) {
Blink Reformat4c46d092018-04-07 15:32:37169 return true;
Tim van der Lippe1d6e57a2019-09-30 11:55:34170 }
Blink Reformat4c46d092018-04-07 15:32:37171
Paul Lewis9950e182019-12-16 16:06:07172 const modifiers = Modifiers;
Joel Einbindera66e5bf2018-05-31 01:26:37173 // Undo/Redo will also cause input, so textual undo should take precedence over DevTools undo when editing.
Paul Lewis0fd43712020-01-08 17:07:36174 if (Host.Platform.isMac()) {
Paul Lewis9950e182019-12-16 16:06:07175 if (KeyboardShortcut.makeKey('z', modifiers.Meta) === key) {
Joel Einbindera66e5bf2018-05-31 01:26:37176 return true;
Tim van der Lippe1d6e57a2019-09-30 11:55:34177 }
Paul Lewis9950e182019-12-16 16:06:07178 if (KeyboardShortcut.makeKey('z', modifiers.Meta | modifiers.Shift) === key) {
Joel Einbindera66e5bf2018-05-31 01:26:37179 return true;
Tim van der Lippe1d6e57a2019-09-30 11:55:34180 }
Joel Einbindera66e5bf2018-05-31 01:26:37181 } else {
Paul Lewis9950e182019-12-16 16:06:07182 if (KeyboardShortcut.makeKey('z', modifiers.Ctrl) === key) {
Joel Einbindera66e5bf2018-05-31 01:26:37183 return true;
Tim van der Lippe1d6e57a2019-09-30 11:55:34184 }
Paul Lewis9950e182019-12-16 16:06:07185 if (KeyboardShortcut.makeKey('y', modifiers.Ctrl) === key) {
Joel Einbindera66e5bf2018-05-31 01:26:37186 return true;
Tim van der Lippe1d6e57a2019-09-30 11:55:34187 }
Paul Lewis0fd43712020-01-08 17:07:36188 if (!Host.Platform.isWin() && KeyboardShortcut.makeKey('z', modifiers.Ctrl | modifiers.Shift) === key) {
Joel Einbindera66e5bf2018-05-31 01:26:37189 return true;
Tim van der Lippe1d6e57a2019-09-30 11:55:34190 }
Joel Einbindera66e5bf2018-05-31 01:26:37191 }
192
Tim van der Lippe1d6e57a2019-09-30 11:55:34193 if ((keyModifiers & (modifiers.Ctrl | modifiers.Alt)) === (modifiers.Ctrl | modifiers.Alt)) {
Paul Lewis0fd43712020-01-08 17:07:36194 return Host.Platform.isWin();
Tim van der Lippe1d6e57a2019-09-30 11:55:34195 }
Blink Reformat4c46d092018-04-07 15:32:37196
197 return !hasModifier(modifiers.Ctrl) && !hasModifier(modifiers.Alt) && !hasModifier(modifiers.Meta);
198 }
199
200 /**
201 * @param {number} mod
202 * @return {boolean}
203 */
204 function hasModifier(mod) {
205 return !!(keyModifiers & mod);
206 }
207 }
208
209 /**
Jack Lynch94a9b0c2020-03-11 21:45:14210 * @param {!KeyboardShortcut} shortcut
Blink Reformat4c46d092018-04-07 15:32:37211 */
Jack Lynch94a9b0c2020-03-11 21:45:14212 _registerShortcut(shortcut) {
213 this._actionToShortcut.set(shortcut.action, shortcut);
214 this._keyToShortcut.set(shortcut.descriptor.key, shortcut);
Blink Reformat4c46d092018-04-07 15:32:37215 }
216
Jack Lynch94a9b0c2020-03-11 21:45:14217 _registerBindings() {
Blink Reformat4c46d092018-04-07 15:32:37218 const extensions = self.runtime.extensions('action');
219 extensions.forEach(registerExtension, this);
220
221 /**
Tim van der Lippe99e59b82019-09-30 20:00:59222 * @param {!Root.Runtime.Extension} extension
Tim van der Lippe0830b3d2019-10-03 13:20:07223 * @this {ShortcutRegistry}
Blink Reformat4c46d092018-04-07 15:32:37224 */
225 function registerExtension(extension) {
226 const descriptor = extension.descriptor();
Jack Lynch94a9b0c2020-03-11 21:45:14227 const bindings = descriptor.bindings;
Blink Reformat4c46d092018-04-07 15:32:37228 for (let i = 0; bindings && i < bindings.length; ++i) {
Tim van der Lippe1d6e57a2019-09-30 11:55:34229 if (!platformMatches(bindings[i].platform)) {
Blink Reformat4c46d092018-04-07 15:32:37230 continue;
Tim van der Lippe1d6e57a2019-09-30 11:55:34231 }
Jack Lynch94a9b0c2020-03-11 21:45:14232 const shortcuts = bindings[i].shortcut.split(/\s+/);
233 shortcuts.forEach(shortcut => {
234 const shortcutDescriptor = KeyboardShortcut.makeDescriptorFromBindingShortcut(shortcut);
235 if (shortcutDescriptor) {
236 this._registerShortcut(new KeyboardShortcut(shortcutDescriptor, descriptor.actionId, Type.DefaultShortcut));
237 }
238 });
Blink Reformat4c46d092018-04-07 15:32:37239 }
240 }
241
242 /**
243 * @param {string=} platformsString
244 * @return {boolean}
245 */
246 function platformMatches(platformsString) {
Tim van der Lippe1d6e57a2019-09-30 11:55:34247 if (!platformsString) {
Blink Reformat4c46d092018-04-07 15:32:37248 return true;
Tim van der Lippe1d6e57a2019-09-30 11:55:34249 }
Blink Reformat4c46d092018-04-07 15:32:37250 const platforms = platformsString.split(',');
251 let isMatch = false;
Paul Lewis0fd43712020-01-08 17:07:36252 const currentPlatform = Host.Platform.platform();
Tim van der Lippe1d6e57a2019-09-30 11:55:34253 for (let i = 0; !isMatch && i < platforms.length; ++i) {
Blink Reformat4c46d092018-04-07 15:32:37254 isMatch = platforms[i] === currentPlatform;
Tim van der Lippe1d6e57a2019-09-30 11:55:34255 }
Blink Reformat4c46d092018-04-07 15:32:37256 return isMatch;
257 }
258 }
Tim van der Lippe0830b3d2019-10-03 13:20:07259}
Blink Reformat4c46d092018-04-07 15:32:37260
261/**
262 * @unrestricted
263 */
Tim van der Lippe0830b3d2019-10-03 13:20:07264export class ForwardedShortcut {}
Blink Reformat4c46d092018-04-07 15:32:37265
Tim van der Lippe0830b3d2019-10-03 13:20:07266ForwardedShortcut.instance = new ForwardedShortcut();