Blink Reformat | 4c46d09 | 2018-04-07 15:32:37 | [diff] [blame] | 1 | // 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 Lewis | 9950e18 | 2019-12-16 16:06:07 | [diff] [blame] | 4 | |
Paul Lewis | 0fd4371 | 2020-01-08 17:07:36 | [diff] [blame] | 5 | import * as Host from '../host/host.js'; |
Tim van der Lippe | aa76aa2 | 2020-02-14 14:38:24 | [diff] [blame] | 6 | |
Paul Lewis | 9950e18 | 2019-12-16 16:06:07 | [diff] [blame] | 7 | import {Action} from './Action.js'; // eslint-disable-line no-unused-vars |
| 8 | import {ActionRegistry} from './ActionRegistry.js'; // eslint-disable-line no-unused-vars |
| 9 | import {Context} from './Context.js'; |
| 10 | import {Dialog} from './Dialog.js'; |
Jack Lynch | 94a9b0c | 2020-03-11 21:45:14 | [diff] [blame^] | 11 | import {Descriptor, KeyboardShortcut, Modifiers, Type} from './KeyboardShortcut.js'; // eslint-disable-line no-unused-vars |
Paul Lewis | 9950e18 | 2019-12-16 16:06:07 | [diff] [blame] | 12 | import {isEditing} from './UIUtils.js'; |
| 13 | |
Blink Reformat | 4c46d09 | 2018-04-07 15:32:37 | [diff] [blame] | 14 | /** |
| 15 | * @unrestricted |
| 16 | */ |
Paul Lewis | 9950e18 | 2019-12-16 16:06:07 | [diff] [blame] | 17 | export class ShortcutRegistry { |
Blink Reformat | 4c46d09 | 2018-04-07 15:32:37 | [diff] [blame] | 18 | /** |
Paul Lewis | 9950e18 | 2019-12-16 16:06:07 | [diff] [blame] | 19 | * @param {!ActionRegistry} actionRegistry |
Blink Reformat | 4c46d09 | 2018-04-07 15:32:37 | [diff] [blame] | 20 | */ |
Jack Lynch | 94a9b0c | 2020-03-11 21:45:14 | [diff] [blame^] | 21 | constructor(actionRegistry) { |
Blink Reformat | 4c46d09 | 2018-04-07 15:32:37 | [diff] [blame] | 22 | this._actionRegistry = actionRegistry; |
Jack Lynch | 94a9b0c | 2020-03-11 21:45:14 | [diff] [blame^] | 23 | /** @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 Reformat | 4c46d09 | 2018-04-07 15:32:37 | [diff] [blame] | 28 | } |
| 29 | |
| 30 | /** |
| 31 | * @param {number} key |
Paul Lewis | 9950e18 | 2019-12-16 16:06:07 | [diff] [blame] | 32 | * @return {!Array.<!Action>} |
Blink Reformat | 4c46d09 | 2018-04-07 15:32:37 | [diff] [blame] | 33 | */ |
| 34 | _applicableActions(key) { |
Jack Lynch | 94a9b0c | 2020-03-11 21:45:14 | [diff] [blame^] | 35 | return this._actionRegistry.applicableActions(this.actionsForKey(key), self.UI.context); |
Blink Reformat | 4c46d09 | 2018-04-07 15:32:37 | [diff] [blame] | 36 | } |
| 37 | |
| 38 | /** |
| 39 | * @param {number} key |
Jack Lynch | 94a9b0c | 2020-03-11 21:45:14 | [diff] [blame^] | 40 | * @return {!Array.<string>} |
Blink Reformat | 4c46d09 | 2018-04-07 15:32:37 | [diff] [blame] | 41 | */ |
Jack Lynch | 94a9b0c | 2020-03-11 21:45:14 | [diff] [blame^] | 42 | actionsForKey(key) { |
| 43 | const shortcuts = [...this._keyToShortcut.get(key)]; |
| 44 | return shortcuts.map(shortcut => shortcut.action); |
Blink Reformat | 4c46d09 | 2018-04-07 15:32:37 | [diff] [blame] | 45 | } |
| 46 | |
| 47 | /** |
Joel Einbinder | 67f28fb | 2018-08-02 00:33:47 | [diff] [blame] | 48 | * @return {!Array<number>} |
| 49 | */ |
| 50 | globalShortcutKeys() { |
| 51 | const keys = []; |
Jack Lynch | 94a9b0c | 2020-03-11 21:45:14 | [diff] [blame^] | 52 | for (const key of this._keyToShortcut.keysArray()) { |
| 53 | const actions = [...this._keyToShortcut.get(key)]; |
Paul Lewis | 9950e18 | 2019-12-16 16:06:07 | [diff] [blame] | 54 | const applicableActions = this._actionRegistry.applicableActions(actions, new Context()); |
Tim van der Lippe | 1d6e57a | 2019-09-30 11:55:34 | [diff] [blame] | 55 | if (applicableActions.length) { |
Jack Lynch | 94a9b0c | 2020-03-11 21:45:14 | [diff] [blame^] | 56 | keys.push(key); |
Tim van der Lippe | 1d6e57a | 2019-09-30 11:55:34 | [diff] [blame] | 57 | } |
Joel Einbinder | 67f28fb | 2018-08-02 00:33:47 | [diff] [blame] | 58 | } |
| 59 | return keys; |
| 60 | } |
| 61 | |
| 62 | /** |
Blink Reformat | 4c46d09 | 2018-04-07 15:32:37 | [diff] [blame] | 63 | * @param {string} actionId |
Tim van der Lippe | aa76aa2 | 2020-02-14 14:38:24 | [diff] [blame] | 64 | * @return {!Array.<!Descriptor>} |
Blink Reformat | 4c46d09 | 2018-04-07 15:32:37 | [diff] [blame] | 65 | */ |
| 66 | shortcutDescriptorsForAction(actionId) { |
Jack Lynch | 94a9b0c | 2020-03-11 21:45:14 | [diff] [blame^] | 67 | return [...this._actionToShortcut.get(actionId)].map(shortcut => shortcut.descriptor); |
Blink Reformat | 4c46d09 | 2018-04-07 15:32:37 | [diff] [blame] | 68 | } |
| 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 Lippe | 1d6e57a | 2019-09-30 11:55:34 | [diff] [blame] | 78 | for (let j = 0; j < descriptors.length; ++j) { |
Blink Reformat | 4c46d09 | 2018-04-07 15:32:37 | [diff] [blame] | 79 | result.push(descriptors[j].key); |
Tim van der Lippe | 1d6e57a | 2019-09-30 11:55:34 | [diff] [blame] | 80 | } |
Blink Reformat | 4c46d09 | 2018-04-07 15:32:37 | [diff] [blame] | 81 | } |
| 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 Lippe | 1d6e57a | 2019-09-30 11:55:34 | [diff] [blame] | 91 | if (descriptors.length) { |
Blink Reformat | 4c46d09 | 2018-04-07 15:32:37 | [diff] [blame] | 92 | return descriptors[0].name; |
Tim van der Lippe | 1d6e57a | 2019-09-30 11:55:34 | [diff] [blame] | 93 | } |
Blink Reformat | 4c46d09 | 2018-04-07 15:32:37 | [diff] [blame] | 94 | } |
| 95 | |
| 96 | /** |
| 97 | * @param {!KeyboardEvent} event |
| 98 | */ |
| 99 | handleShortcut(event) { |
Paul Lewis | 9950e18 | 2019-12-16 16:06:07 | [diff] [blame] | 100 | this.handleKey(KeyboardShortcut.makeKeyFromEvent(event), event.key, event); |
Blink Reformat | 4c46d09 | 2018-04-07 15:32:37 | [diff] [blame] | 101 | } |
| 102 | |
| 103 | /** |
| 104 | * @param {!KeyboardEvent} event |
| 105 | * @param {string} actionId |
| 106 | * @return {boolean} |
| 107 | */ |
| 108 | eventMatchesAction(event, actionId) { |
Jack Lynch | 94a9b0c | 2020-03-11 21:45:14 | [diff] [blame^] | 109 | console.assert(this._actionToShortcut.has(actionId), 'Unknown action ' + actionId); |
Paul Lewis | 9950e18 | 2019-12-16 16:06:07 | [diff] [blame] | 110 | const key = KeyboardShortcut.makeKeyFromEvent(event); |
Jack Lynch | 94a9b0c | 2020-03-11 21:45:14 | [diff] [blame^] | 111 | return [...this._actionToShortcut.get(actionId)].some(shortcut => shortcut.descriptor.key === key); |
Blink Reformat | 4c46d09 | 2018-04-07 15:32:37 | [diff] [blame] | 112 | } |
| 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 Lynch | 94a9b0c | 2020-03-11 21:45:14 | [diff] [blame^] | 121 | console.assert(this._actionToShortcut.has(actionId), 'Unknown action ' + actionId); |
Blink Reformat | 4c46d09 | 2018-04-07 15:32:37 | [diff] [blame] | 122 | element.addEventListener('keydown', event => { |
Tim van der Lippe | 1d6e57a | 2019-09-30 11:55:34 | [diff] [blame] | 123 | if (!this.eventMatchesAction(/** @type {!KeyboardEvent} */ (event), actionId) || !listener.call(null)) { |
Blink Reformat | 4c46d09 | 2018-04-07 15:32:37 | [diff] [blame] | 124 | return; |
Tim van der Lippe | 1d6e57a | 2019-09-30 11:55:34 | [diff] [blame] | 125 | } |
Blink Reformat | 4c46d09 | 2018-04-07 15:32:37 | [diff] [blame] | 126 | event.consume(true); |
| 127 | }, capture); |
| 128 | } |
| 129 | |
| 130 | /** |
| 131 | * @param {number} key |
| 132 | * @param {string} domKey |
| 133 | * @param {!KeyboardEvent=} event |
| 134 | */ |
Erik Luo | 9855f61 | 2018-10-18 19:11:30 | [diff] [blame] | 135 | async handleKey(key, domKey, event) { |
Blink Reformat | 4c46d09 | 2018-04-07 15:32:37 | [diff] [blame] | 136 | const keyModifiers = key >> 8; |
| 137 | const actions = this._applicableActions(key); |
Tim van der Lippe | 1d6e57a | 2019-09-30 11:55:34 | [diff] [blame] | 138 | if (!actions.length || isPossiblyInputKey()) { |
Blink Reformat | 4c46d09 | 2018-04-07 15:32:37 | [diff] [blame] | 139 | return; |
Tim van der Lippe | 1d6e57a | 2019-09-30 11:55:34 | [diff] [blame] | 140 | } |
| 141 | if (event) { |
Erik Luo | 9855f61 | 2018-10-18 19:11:30 | [diff] [blame] | 142 | event.consume(true); |
Tim van der Lippe | 1d6e57a | 2019-09-30 11:55:34 | [diff] [blame] | 143 | } |
Paul Lewis | 9950e18 | 2019-12-16 16:06:07 | [diff] [blame] | 144 | if (Dialog.hasInstance()) { |
Blink Reformat | 4c46d09 | 2018-04-07 15:32:37 | [diff] [blame] | 145 | return; |
Tim van der Lippe | 1d6e57a | 2019-09-30 11:55:34 | [diff] [blame] | 146 | } |
Erik Luo | 9855f61 | 2018-10-18 19:11:30 | [diff] [blame] | 147 | for (const action of actions) { |
Mandy Chen | c1c5bec | 2019-12-10 19:11:31 | [diff] [blame] | 148 | try { |
| 149 | const result = await action.execute(); |
| 150 | if (result) { |
Jack Lynch | ad5c586 | 2020-02-27 19:40:41 | [diff] [blame] | 151 | Host.userMetrics.keyboardShortcutFired(action.id()); |
Mandy Chen | c1c5bec | 2019-12-10 19:11:31 | [diff] [blame] | 152 | return; |
| 153 | } |
| 154 | } catch (e) { |
| 155 | console.error(e); |
| 156 | throw e; |
Tim van der Lippe | 1d6e57a | 2019-09-30 11:55:34 | [diff] [blame] | 157 | } |
Blink Reformat | 4c46d09 | 2018-04-07 15:32:37 | [diff] [blame] | 158 | } |
| 159 | |
| 160 | /** |
| 161 | * @return {boolean} |
| 162 | */ |
| 163 | function isPossiblyInputKey() { |
Paul Lewis | 9950e18 | 2019-12-16 16:06:07 | [diff] [blame] | 164 | if (!event || !isEditing() || /^F\d+|Control|Shift|Alt|Meta|Escape|Win|U\+001B$/.test(domKey)) { |
Blink Reformat | 4c46d09 | 2018-04-07 15:32:37 | [diff] [blame] | 165 | return false; |
Tim van der Lippe | 1d6e57a | 2019-09-30 11:55:34 | [diff] [blame] | 166 | } |
Blink Reformat | 4c46d09 | 2018-04-07 15:32:37 | [diff] [blame] | 167 | |
Tim van der Lippe | 1d6e57a | 2019-09-30 11:55:34 | [diff] [blame] | 168 | if (!keyModifiers) { |
Blink Reformat | 4c46d09 | 2018-04-07 15:32:37 | [diff] [blame] | 169 | return true; |
Tim van der Lippe | 1d6e57a | 2019-09-30 11:55:34 | [diff] [blame] | 170 | } |
Blink Reformat | 4c46d09 | 2018-04-07 15:32:37 | [diff] [blame] | 171 | |
Paul Lewis | 9950e18 | 2019-12-16 16:06:07 | [diff] [blame] | 172 | const modifiers = Modifiers; |
Joel Einbinder | a66e5bf | 2018-05-31 01:26:37 | [diff] [blame] | 173 | // Undo/Redo will also cause input, so textual undo should take precedence over DevTools undo when editing. |
Paul Lewis | 0fd4371 | 2020-01-08 17:07:36 | [diff] [blame] | 174 | if (Host.Platform.isMac()) { |
Paul Lewis | 9950e18 | 2019-12-16 16:06:07 | [diff] [blame] | 175 | if (KeyboardShortcut.makeKey('z', modifiers.Meta) === key) { |
Joel Einbinder | a66e5bf | 2018-05-31 01:26:37 | [diff] [blame] | 176 | return true; |
Tim van der Lippe | 1d6e57a | 2019-09-30 11:55:34 | [diff] [blame] | 177 | } |
Paul Lewis | 9950e18 | 2019-12-16 16:06:07 | [diff] [blame] | 178 | if (KeyboardShortcut.makeKey('z', modifiers.Meta | modifiers.Shift) === key) { |
Joel Einbinder | a66e5bf | 2018-05-31 01:26:37 | [diff] [blame] | 179 | return true; |
Tim van der Lippe | 1d6e57a | 2019-09-30 11:55:34 | [diff] [blame] | 180 | } |
Joel Einbinder | a66e5bf | 2018-05-31 01:26:37 | [diff] [blame] | 181 | } else { |
Paul Lewis | 9950e18 | 2019-12-16 16:06:07 | [diff] [blame] | 182 | if (KeyboardShortcut.makeKey('z', modifiers.Ctrl) === key) { |
Joel Einbinder | a66e5bf | 2018-05-31 01:26:37 | [diff] [blame] | 183 | return true; |
Tim van der Lippe | 1d6e57a | 2019-09-30 11:55:34 | [diff] [blame] | 184 | } |
Paul Lewis | 9950e18 | 2019-12-16 16:06:07 | [diff] [blame] | 185 | if (KeyboardShortcut.makeKey('y', modifiers.Ctrl) === key) { |
Joel Einbinder | a66e5bf | 2018-05-31 01:26:37 | [diff] [blame] | 186 | return true; |
Tim van der Lippe | 1d6e57a | 2019-09-30 11:55:34 | [diff] [blame] | 187 | } |
Paul Lewis | 0fd4371 | 2020-01-08 17:07:36 | [diff] [blame] | 188 | if (!Host.Platform.isWin() && KeyboardShortcut.makeKey('z', modifiers.Ctrl | modifiers.Shift) === key) { |
Joel Einbinder | a66e5bf | 2018-05-31 01:26:37 | [diff] [blame] | 189 | return true; |
Tim van der Lippe | 1d6e57a | 2019-09-30 11:55:34 | [diff] [blame] | 190 | } |
Joel Einbinder | a66e5bf | 2018-05-31 01:26:37 | [diff] [blame] | 191 | } |
| 192 | |
Tim van der Lippe | 1d6e57a | 2019-09-30 11:55:34 | [diff] [blame] | 193 | if ((keyModifiers & (modifiers.Ctrl | modifiers.Alt)) === (modifiers.Ctrl | modifiers.Alt)) { |
Paul Lewis | 0fd4371 | 2020-01-08 17:07:36 | [diff] [blame] | 194 | return Host.Platform.isWin(); |
Tim van der Lippe | 1d6e57a | 2019-09-30 11:55:34 | [diff] [blame] | 195 | } |
Blink Reformat | 4c46d09 | 2018-04-07 15:32:37 | [diff] [blame] | 196 | |
| 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 Lynch | 94a9b0c | 2020-03-11 21:45:14 | [diff] [blame^] | 210 | * @param {!KeyboardShortcut} shortcut |
Blink Reformat | 4c46d09 | 2018-04-07 15:32:37 | [diff] [blame] | 211 | */ |
Jack Lynch | 94a9b0c | 2020-03-11 21:45:14 | [diff] [blame^] | 212 | _registerShortcut(shortcut) { |
| 213 | this._actionToShortcut.set(shortcut.action, shortcut); |
| 214 | this._keyToShortcut.set(shortcut.descriptor.key, shortcut); |
Blink Reformat | 4c46d09 | 2018-04-07 15:32:37 | [diff] [blame] | 215 | } |
| 216 | |
Jack Lynch | 94a9b0c | 2020-03-11 21:45:14 | [diff] [blame^] | 217 | _registerBindings() { |
Blink Reformat | 4c46d09 | 2018-04-07 15:32:37 | [diff] [blame] | 218 | const extensions = self.runtime.extensions('action'); |
| 219 | extensions.forEach(registerExtension, this); |
| 220 | |
| 221 | /** |
Tim van der Lippe | 99e59b8 | 2019-09-30 20:00:59 | [diff] [blame] | 222 | * @param {!Root.Runtime.Extension} extension |
Tim van der Lippe | 0830b3d | 2019-10-03 13:20:07 | [diff] [blame] | 223 | * @this {ShortcutRegistry} |
Blink Reformat | 4c46d09 | 2018-04-07 15:32:37 | [diff] [blame] | 224 | */ |
| 225 | function registerExtension(extension) { |
| 226 | const descriptor = extension.descriptor(); |
Jack Lynch | 94a9b0c | 2020-03-11 21:45:14 | [diff] [blame^] | 227 | const bindings = descriptor.bindings; |
Blink Reformat | 4c46d09 | 2018-04-07 15:32:37 | [diff] [blame] | 228 | for (let i = 0; bindings && i < bindings.length; ++i) { |
Tim van der Lippe | 1d6e57a | 2019-09-30 11:55:34 | [diff] [blame] | 229 | if (!platformMatches(bindings[i].platform)) { |
Blink Reformat | 4c46d09 | 2018-04-07 15:32:37 | [diff] [blame] | 230 | continue; |
Tim van der Lippe | 1d6e57a | 2019-09-30 11:55:34 | [diff] [blame] | 231 | } |
Jack Lynch | 94a9b0c | 2020-03-11 21:45:14 | [diff] [blame^] | 232 | 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 Reformat | 4c46d09 | 2018-04-07 15:32:37 | [diff] [blame] | 239 | } |
| 240 | } |
| 241 | |
| 242 | /** |
| 243 | * @param {string=} platformsString |
| 244 | * @return {boolean} |
| 245 | */ |
| 246 | function platformMatches(platformsString) { |
Tim van der Lippe | 1d6e57a | 2019-09-30 11:55:34 | [diff] [blame] | 247 | if (!platformsString) { |
Blink Reformat | 4c46d09 | 2018-04-07 15:32:37 | [diff] [blame] | 248 | return true; |
Tim van der Lippe | 1d6e57a | 2019-09-30 11:55:34 | [diff] [blame] | 249 | } |
Blink Reformat | 4c46d09 | 2018-04-07 15:32:37 | [diff] [blame] | 250 | const platforms = platformsString.split(','); |
| 251 | let isMatch = false; |
Paul Lewis | 0fd4371 | 2020-01-08 17:07:36 | [diff] [blame] | 252 | const currentPlatform = Host.Platform.platform(); |
Tim van der Lippe | 1d6e57a | 2019-09-30 11:55:34 | [diff] [blame] | 253 | for (let i = 0; !isMatch && i < platforms.length; ++i) { |
Blink Reformat | 4c46d09 | 2018-04-07 15:32:37 | [diff] [blame] | 254 | isMatch = platforms[i] === currentPlatform; |
Tim van der Lippe | 1d6e57a | 2019-09-30 11:55:34 | [diff] [blame] | 255 | } |
Blink Reformat | 4c46d09 | 2018-04-07 15:32:37 | [diff] [blame] | 256 | return isMatch; |
| 257 | } |
| 258 | } |
Tim van der Lippe | 0830b3d | 2019-10-03 13:20:07 | [diff] [blame] | 259 | } |
Blink Reformat | 4c46d09 | 2018-04-07 15:32:37 | [diff] [blame] | 260 | |
| 261 | /** |
| 262 | * @unrestricted |
| 263 | */ |
Tim van der Lippe | 0830b3d | 2019-10-03 13:20:07 | [diff] [blame] | 264 | export class ForwardedShortcut {} |
Blink Reformat | 4c46d09 | 2018-04-07 15:32:37 | [diff] [blame] | 265 | |
Tim van der Lippe | 0830b3d | 2019-10-03 13:20:07 | [diff] [blame] | 266 | ForwardedShortcut.instance = new ForwardedShortcut(); |