blob: c017e8b73274bf1dc641a11b0e62bf9d91fa92f3 [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.
4/**
5 * @unrestricted
6 */
7UI.ShortcutRegistry = class {
8 /**
9 * @param {!UI.ActionRegistry} actionRegistry
10 * @param {!Document} document
11 */
12 constructor(actionRegistry, document) {
13 this._actionRegistry = actionRegistry;
14 /** @type {!Multimap.<string, string>} */
15 this._defaultKeyToActions = new Multimap();
16 /** @type {!Multimap.<string, !UI.KeyboardShortcut.Descriptor>} */
17 this._defaultActionToShortcut = new Multimap();
18 this._registerBindings(document);
19 }
20
21 /**
22 * @param {number} key
23 * @return {!Array.<!UI.Action>}
24 */
25 _applicableActions(key) {
26 return this._actionRegistry.applicableActions(this._defaultActionsForKey(key).valuesArray(), UI.context);
27 }
28
29 /**
30 * @param {number} key
31 * @return {!Set.<string>}
32 */
33 _defaultActionsForKey(key) {
34 return this._defaultKeyToActions.get(String(key));
35 }
36
37 /**
Joel Einbinder67f28fb2018-08-02 00:33:4738 * @return {!Array<number>}
39 */
40 globalShortcutKeys() {
41 const keys = [];
42 for (const key of this._defaultKeyToActions.keysArray()) {
43 const actions = this._defaultKeyToActions.get(key).valuesArray();
44 const applicableActions = this._actionRegistry.applicableActions(actions, new UI.Context());
Tim van der Lippe1d6e57a2019-09-30 11:55:3445 if (applicableActions.length) {
Joel Einbinder67f28fb2018-08-02 00:33:4746 keys.push(Number(key));
Tim van der Lippe1d6e57a2019-09-30 11:55:3447 }
Joel Einbinder67f28fb2018-08-02 00:33:4748 }
49 return keys;
50 }
51
52 /**
Blink Reformat4c46d092018-04-07 15:32:3753 * @param {string} actionId
54 * @return {!Array.<!UI.KeyboardShortcut.Descriptor>}
55 */
56 shortcutDescriptorsForAction(actionId) {
57 return this._defaultActionToShortcut.get(actionId).valuesArray();
58 }
59
60 /**
61 * @param {!Array.<string>} actionIds
62 * @return {!Array.<number>}
63 */
64 keysForActions(actionIds) {
65 const result = [];
66 for (let i = 0; i < actionIds.length; ++i) {
67 const descriptors = this.shortcutDescriptorsForAction(actionIds[i]);
Tim van der Lippe1d6e57a2019-09-30 11:55:3468 for (let j = 0; j < descriptors.length; ++j) {
Blink Reformat4c46d092018-04-07 15:32:3769 result.push(descriptors[j].key);
Tim van der Lippe1d6e57a2019-09-30 11:55:3470 }
Blink Reformat4c46d092018-04-07 15:32:3771 }
72 return result;
73 }
74
75 /**
76 * @param {string} actionId
77 * @return {string|undefined}
78 */
79 shortcutTitleForAction(actionId) {
80 const descriptors = this.shortcutDescriptorsForAction(actionId);
Tim van der Lippe1d6e57a2019-09-30 11:55:3481 if (descriptors.length) {
Blink Reformat4c46d092018-04-07 15:32:3782 return descriptors[0].name;
Tim van der Lippe1d6e57a2019-09-30 11:55:3483 }
Blink Reformat4c46d092018-04-07 15:32:3784 }
85
86 /**
87 * @param {!KeyboardEvent} event
88 */
89 handleShortcut(event) {
90 this.handleKey(UI.KeyboardShortcut.makeKeyFromEvent(event), event.key, event);
91 }
92
93 /**
94 * @param {!KeyboardEvent} event
95 * @param {string} actionId
96 * @return {boolean}
97 */
98 eventMatchesAction(event, actionId) {
99 console.assert(this._defaultActionToShortcut.has(actionId), 'Unknown action ' + actionId);
100 const key = UI.KeyboardShortcut.makeKeyFromEvent(event);
101 return this._defaultActionToShortcut.get(actionId).valuesArray().some(descriptor => descriptor.key === key);
102 }
103
104 /**
105 * @param {!Element} element
106 * @param {string} actionId
107 * @param {function():boolean} listener
108 * @param {boolean=} capture
109 */
110 addShortcutListener(element, actionId, listener, capture) {
111 console.assert(this._defaultActionToShortcut.has(actionId), 'Unknown action ' + actionId);
112 element.addEventListener('keydown', event => {
Tim van der Lippe1d6e57a2019-09-30 11:55:34113 if (!this.eventMatchesAction(/** @type {!KeyboardEvent} */ (event), actionId) || !listener.call(null)) {
Blink Reformat4c46d092018-04-07 15:32:37114 return;
Tim van der Lippe1d6e57a2019-09-30 11:55:34115 }
Blink Reformat4c46d092018-04-07 15:32:37116 event.consume(true);
117 }, capture);
118 }
119
120 /**
121 * @param {number} key
122 * @param {string} domKey
123 * @param {!KeyboardEvent=} event
124 */
Erik Luo9855f612018-10-18 19:11:30125 async handleKey(key, domKey, event) {
Blink Reformat4c46d092018-04-07 15:32:37126 const keyModifiers = key >> 8;
127 const actions = this._applicableActions(key);
Tim van der Lippe1d6e57a2019-09-30 11:55:34128 if (!actions.length || isPossiblyInputKey()) {
Blink Reformat4c46d092018-04-07 15:32:37129 return;
Tim van der Lippe1d6e57a2019-09-30 11:55:34130 }
131 if (event) {
Erik Luo9855f612018-10-18 19:11:30132 event.consume(true);
Tim van der Lippe1d6e57a2019-09-30 11:55:34133 }
134 if (UI.Dialog.hasInstance()) {
Blink Reformat4c46d092018-04-07 15:32:37135 return;
Tim van der Lippe1d6e57a2019-09-30 11:55:34136 }
Erik Luo9855f612018-10-18 19:11:30137 for (const action of actions) {
Tim van der Lippe1d6e57a2019-09-30 11:55:34138 if (await action.execute()) {
Blink Reformat4c46d092018-04-07 15:32:37139 return;
Tim van der Lippe1d6e57a2019-09-30 11:55:34140 }
Blink Reformat4c46d092018-04-07 15:32:37141 }
142
143 /**
144 * @return {boolean}
145 */
146 function isPossiblyInputKey() {
Tim van der Lippe1d6e57a2019-09-30 11:55:34147 if (!event || !UI.isEditing() || /^F\d+|Control|Shift|Alt|Meta|Escape|Win|U\+001B$/.test(domKey)) {
Blink Reformat4c46d092018-04-07 15:32:37148 return false;
Tim van der Lippe1d6e57a2019-09-30 11:55:34149 }
Blink Reformat4c46d092018-04-07 15:32:37150
Tim van der Lippe1d6e57a2019-09-30 11:55:34151 if (!keyModifiers) {
Blink Reformat4c46d092018-04-07 15:32:37152 return true;
Tim van der Lippe1d6e57a2019-09-30 11:55:34153 }
Blink Reformat4c46d092018-04-07 15:32:37154
155 const modifiers = UI.KeyboardShortcut.Modifiers;
Joel Einbindera66e5bf2018-05-31 01:26:37156 // Undo/Redo will also cause input, so textual undo should take precedence over DevTools undo when editing.
157 if (Host.isMac()) {
Tim van der Lippe1d6e57a2019-09-30 11:55:34158 if (UI.KeyboardShortcut.makeKey('z', modifiers.Meta) === key) {
Joel Einbindera66e5bf2018-05-31 01:26:37159 return true;
Tim van der Lippe1d6e57a2019-09-30 11:55:34160 }
161 if (UI.KeyboardShortcut.makeKey('z', modifiers.Meta | modifiers.Shift) === key) {
Joel Einbindera66e5bf2018-05-31 01:26:37162 return true;
Tim van der Lippe1d6e57a2019-09-30 11:55:34163 }
Joel Einbindera66e5bf2018-05-31 01:26:37164 } else {
Tim van der Lippe1d6e57a2019-09-30 11:55:34165 if (UI.KeyboardShortcut.makeKey('z', modifiers.Ctrl) === key) {
Joel Einbindera66e5bf2018-05-31 01:26:37166 return true;
Tim van der Lippe1d6e57a2019-09-30 11:55:34167 }
168 if (UI.KeyboardShortcut.makeKey('y', modifiers.Ctrl) === key) {
Joel Einbindera66e5bf2018-05-31 01:26:37169 return true;
Tim van der Lippe1d6e57a2019-09-30 11:55:34170 }
171 if (!Host.isWin() && UI.KeyboardShortcut.makeKey('z', modifiers.Ctrl | modifiers.Shift) === key) {
Joel Einbindera66e5bf2018-05-31 01:26:37172 return true;
Tim van der Lippe1d6e57a2019-09-30 11:55:34173 }
Joel Einbindera66e5bf2018-05-31 01:26:37174 }
175
Tim van der Lippe1d6e57a2019-09-30 11:55:34176 if ((keyModifiers & (modifiers.Ctrl | modifiers.Alt)) === (modifiers.Ctrl | modifiers.Alt)) {
Blink Reformat4c46d092018-04-07 15:32:37177 return Host.isWin();
Tim van der Lippe1d6e57a2019-09-30 11:55:34178 }
Blink Reformat4c46d092018-04-07 15:32:37179
180 return !hasModifier(modifiers.Ctrl) && !hasModifier(modifiers.Alt) && !hasModifier(modifiers.Meta);
181 }
182
183 /**
184 * @param {number} mod
185 * @return {boolean}
186 */
187 function hasModifier(mod) {
188 return !!(keyModifiers & mod);
189 }
190 }
191
192 /**
193 * @param {string} actionId
194 * @param {string} shortcut
195 */
196 registerShortcut(actionId, shortcut) {
197 const descriptor = UI.KeyboardShortcut.makeDescriptorFromBindingShortcut(shortcut);
Tim van der Lippe1d6e57a2019-09-30 11:55:34198 if (!descriptor) {
Blink Reformat4c46d092018-04-07 15:32:37199 return;
Tim van der Lippe1d6e57a2019-09-30 11:55:34200 }
Blink Reformat4c46d092018-04-07 15:32:37201 this._defaultActionToShortcut.set(actionId, descriptor);
202 this._defaultKeyToActions.set(String(descriptor.key), actionId);
203 }
204
Blink Reformat4c46d092018-04-07 15:32:37205 /**
206 * @param {!Document} document
207 */
208 _registerBindings(document) {
Blink Reformat4c46d092018-04-07 15:32:37209 const extensions = self.runtime.extensions('action');
210 extensions.forEach(registerExtension, this);
211
212 /**
213 * @param {!Runtime.Extension} extension
214 * @this {UI.ShortcutRegistry}
215 */
216 function registerExtension(extension) {
217 const descriptor = extension.descriptor();
218 const bindings = descriptor['bindings'];
219 for (let i = 0; bindings && i < bindings.length; ++i) {
Tim van der Lippe1d6e57a2019-09-30 11:55:34220 if (!platformMatches(bindings[i].platform)) {
Blink Reformat4c46d092018-04-07 15:32:37221 continue;
Tim van der Lippe1d6e57a2019-09-30 11:55:34222 }
Blink Reformat4c46d092018-04-07 15:32:37223 const shortcuts = bindings[i]['shortcut'].split(/\s+/);
224 shortcuts.forEach(this.registerShortcut.bind(this, descriptor['actionId']));
225 }
226 }
227
228 /**
229 * @param {string=} platformsString
230 * @return {boolean}
231 */
232 function platformMatches(platformsString) {
Tim van der Lippe1d6e57a2019-09-30 11:55:34233 if (!platformsString) {
Blink Reformat4c46d092018-04-07 15:32:37234 return true;
Tim van der Lippe1d6e57a2019-09-30 11:55:34235 }
Blink Reformat4c46d092018-04-07 15:32:37236 const platforms = platformsString.split(',');
237 let isMatch = false;
238 const currentPlatform = Host.platform();
Tim van der Lippe1d6e57a2019-09-30 11:55:34239 for (let i = 0; !isMatch && i < platforms.length; ++i) {
Blink Reformat4c46d092018-04-07 15:32:37240 isMatch = platforms[i] === currentPlatform;
Tim van der Lippe1d6e57a2019-09-30 11:55:34241 }
Blink Reformat4c46d092018-04-07 15:32:37242 return isMatch;
243 }
244 }
245};
246
247/**
248 * @unrestricted
249 */
250UI.ShortcutRegistry.ForwardedShortcut = class {};
251
252UI.ShortcutRegistry.ForwardedShortcut.instance = new UI.ShortcutRegistry.ForwardedShortcut();
253
254/** @type {!UI.ShortcutRegistry} */
255UI.shortcutRegistry;