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 | |
Tim van der Lippe | 5905249 | 2020-09-08 16:36:15 | [diff] [blame] | 5 | import * as Common from '../common/common.js'; |
Paul Lewis | 0fd4371 | 2020-01-08 17:07:36 | [diff] [blame] | 6 | import * as Host from '../host/host.js'; |
Tim van der Lippe | ee97fa3 | 2020-04-23 15:20:56 | [diff] [blame] | 7 | import * as Platform from '../platform/platform.js'; |
Tim van der Lippe | 7f2002c | 2020-09-28 14:54:01 | [diff] [blame] | 8 | import * as Root from '../root/root.js'; |
Tim van der Lippe | aa76aa2 | 2020-02-14 14:38:24 | [diff] [blame] | 9 | |
Paul Lewis | 9950e18 | 2019-12-16 16:06:07 | [diff] [blame] | 10 | import {Action} from './Action.js'; // eslint-disable-line no-unused-vars |
| 11 | import {ActionRegistry} from './ActionRegistry.js'; // eslint-disable-line no-unused-vars |
| 12 | import {Context} from './Context.js'; |
| 13 | import {Dialog} from './Dialog.js'; |
Jack Lynch | 94a9b0c | 2020-03-11 21:45:14 | [diff] [blame] | 14 | 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] | 15 | import {isEditing} from './UIUtils.js'; |
| 16 | |
Tim van der Lippe | 9c9fb12 | 2020-09-08 15:06:17 | [diff] [blame] | 17 | /** @type {!ShortcutRegistry} */ |
| 18 | let shortcutRegistryInstance; |
Sigurd Schneider | 46da7db | 2020-05-20 13:45:11 | [diff] [blame] | 19 | |
Paul Lewis | 9950e18 | 2019-12-16 16:06:07 | [diff] [blame] | 20 | export class ShortcutRegistry { |
Blink Reformat | 4c46d09 | 2018-04-07 15:32:37 | [diff] [blame] | 21 | /** |
Paul Lewis | 9950e18 | 2019-12-16 16:06:07 | [diff] [blame] | 22 | * @param {!ActionRegistry} actionRegistry |
Blink Reformat | 4c46d09 | 2018-04-07 15:32:37 | [diff] [blame] | 23 | */ |
Jack Lynch | 94a9b0c | 2020-03-11 21:45:14 | [diff] [blame] | 24 | constructor(actionRegistry) { |
Blink Reformat | 4c46d09 | 2018-04-07 15:32:37 | [diff] [blame] | 25 | this._actionRegistry = actionRegistry; |
Jack Lynch | 94a9b0c | 2020-03-11 21:45:14 | [diff] [blame] | 26 | /** @type {!Platform.Multimap.<string, !KeyboardShortcut>} */ |
| 27 | this._actionToShortcut = new Platform.Multimap(); |
Jack Lynch | 966040b | 2020-04-23 20:11:44 | [diff] [blame] | 28 | this._keyMap = new ShortcutTreeNode(0, 0); |
| 29 | /** @type {?ShortcutTreeNode} */ |
| 30 | this._activePrefixKey = null; |
| 31 | /** @type {?number} */ |
| 32 | this._activePrefixTimeout = null; |
| 33 | /** @type {?function():Promise<void>} */ |
| 34 | this._consumePrefix = null; |
Jack Lynch | 42297a7 | 2020-06-21 01:54:30 | [diff] [blame] | 35 | /** @type {!Set.<string>} */ |
| 36 | this._devToolsDefaultShortcutActions = new Set(); |
Jack Lynch | 5f863da | 2020-09-16 23:18:42 | [diff] [blame] | 37 | /** @type {!Platform.Multimap.<string, !KeyboardShortcut>} */ |
| 38 | this._disabledDefaultShortcutsForAction = new Platform.Multimap(); |
| 39 | this._keybindSetSetting = Common.Settings.Settings.instance().moduleSetting('activeKeybindSet'); |
Jack Lynch | 5f863da | 2020-09-16 23:18:42 | [diff] [blame] | 40 | this._keybindSetSetting.addChangeListener(event => { |
Jack Lynch | 9de6dbb | 2020-06-04 18:22:12 | [diff] [blame] | 41 | Host.userMetrics.keybindSetSettingChanged(event.data); |
| 42 | this._registerBindings(); |
| 43 | }); |
Jack Lynch | 5f863da | 2020-09-16 23:18:42 | [diff] [blame] | 44 | this._userShortcutsSetting = Common.Settings.Settings.instance().moduleSetting('userShortcuts'); |
| 45 | this._userShortcutsSetting.addChangeListener(this._registerBindings, this); |
Jack Lynch | f1f00fa | 2020-05-01 22:16:12 | [diff] [blame] | 46 | |
Jack Lynch | 94a9b0c | 2020-03-11 21:45:14 | [diff] [blame] | 47 | this._registerBindings(); |
Blink Reformat | 4c46d09 | 2018-04-07 15:32:37 | [diff] [blame] | 48 | } |
| 49 | |
| 50 | /** |
Tim van der Lippe | 9c9fb12 | 2020-09-08 15:06:17 | [diff] [blame] | 51 | * @param {{forceNew: ?boolean, actionRegistry: ?ActionRegistry}} opts |
| 52 | */ |
| 53 | static instance(opts = {forceNew: null, actionRegistry: null}) { |
| 54 | const {forceNew, actionRegistry} = opts; |
| 55 | if (!shortcutRegistryInstance || forceNew) { |
| 56 | if (!actionRegistry) { |
| 57 | throw new Error('Missing actionRegistry for shortcutRegistry'); |
| 58 | } |
| 59 | shortcutRegistryInstance = new ShortcutRegistry(actionRegistry); |
| 60 | } |
| 61 | |
| 62 | return shortcutRegistryInstance; |
| 63 | } |
| 64 | |
| 65 | /** |
Blink Reformat | 4c46d09 | 2018-04-07 15:32:37 | [diff] [blame] | 66 | * @param {number} key |
Jack Lynch | 966040b | 2020-04-23 20:11:44 | [diff] [blame] | 67 | * @param {!Object.<string, function():Promise.<boolean>>=} handlers |
Paul Lewis | 9950e18 | 2019-12-16 16:06:07 | [diff] [blame] | 68 | * @return {!Array.<!Action>} |
Blink Reformat | 4c46d09 | 2018-04-07 15:32:37 | [diff] [blame] | 69 | */ |
Jack Lynch | 966040b | 2020-04-23 20:11:44 | [diff] [blame] | 70 | _applicableActions(key, handlers = {}) { |
Benedikt Meurer | 0afca61 | 2020-10-05 10:03:58 | [diff] [blame] | 71 | /** @type {!Array<string>} */ |
Jack Lynch | 966040b | 2020-04-23 20:11:44 | [diff] [blame] | 72 | let actions = []; |
| 73 | const keyMap = this._activePrefixKey || this._keyMap; |
| 74 | const keyNode = keyMap.getNode(key); |
| 75 | if (keyNode) { |
| 76 | actions = keyNode.actions(); |
| 77 | } |
Tim van der Lippe | d1a00aa | 2020-08-19 16:03:56 | [diff] [blame] | 78 | const applicableActions = this._actionRegistry.applicableActions(actions, Context.instance()); |
Jack Lynch | 966040b | 2020-04-23 20:11:44 | [diff] [blame] | 79 | if (keyNode) { |
| 80 | for (const actionId of Object.keys(handlers)) { |
| 81 | if (keyNode.actions().indexOf(actionId) >= 0) { |
| 82 | const action = this._actionRegistry.action(actionId); |
| 83 | if (action) { |
| 84 | applicableActions.push(action); |
| 85 | } |
| 86 | } |
| 87 | } |
| 88 | } |
| 89 | return applicableActions; |
Blink Reformat | 4c46d09 | 2018-04-07 15:32:37 | [diff] [blame] | 90 | } |
| 91 | |
| 92 | /** |
Jack Lynch | 575e9fb | 2020-03-26 22:20:51 | [diff] [blame] | 93 | * @param {string} action |
| 94 | * @return {!Array.<!KeyboardShortcut>} |
| 95 | */ |
| 96 | shortcutsForAction(action) { |
| 97 | return [...this._actionToShortcut.get(action)]; |
| 98 | } |
| 99 | |
| 100 | /** |
Jack Lynch | 5f863da | 2020-09-16 23:18:42 | [diff] [blame] | 101 | * @param {!Array.<!Descriptor>} descriptors |
| 102 | */ |
| 103 | actionsForDescriptors(descriptors) { |
Benedikt Meurer | 0afca61 | 2020-10-05 10:03:58 | [diff] [blame] | 104 | /** @type {?ShortcutTreeNode} */ |
Jack Lynch | 5f863da | 2020-09-16 23:18:42 | [diff] [blame] | 105 | let keyMapNode = this._keyMap; |
| 106 | for (const {key} of descriptors) { |
| 107 | if (!keyMapNode) { |
| 108 | return []; |
| 109 | } |
| 110 | keyMapNode = keyMapNode.getNode(key); |
| 111 | } |
| 112 | return keyMapNode ? keyMapNode.actions() : []; |
| 113 | } |
| 114 | |
| 115 | /** |
Joel Einbinder | 67f28fb | 2018-08-02 00:33:47 | [diff] [blame] | 116 | * @return {!Array<number>} |
| 117 | */ |
| 118 | globalShortcutKeys() { |
| 119 | const keys = []; |
Jack Lynch | 966040b | 2020-04-23 20:11:44 | [diff] [blame] | 120 | for (const node of this._keyMap.chords().values()) { |
| 121 | const actions = node.actions(); |
Tim van der Lippe | d1a00aa | 2020-08-19 16:03:56 | [diff] [blame] | 122 | const applicableActions = this._actionRegistry.applicableActions(actions, Context.instance()); |
Jack Lynch | 966040b | 2020-04-23 20:11:44 | [diff] [blame] | 123 | if (applicableActions.length || node.hasChords()) { |
| 124 | keys.push(node.key()); |
Tim van der Lippe | 1d6e57a | 2019-09-30 11:55:34 | [diff] [blame] | 125 | } |
Joel Einbinder | 67f28fb | 2018-08-02 00:33:47 | [diff] [blame] | 126 | } |
| 127 | return keys; |
| 128 | } |
| 129 | |
| 130 | /** |
Blink Reformat | 4c46d09 | 2018-04-07 15:32:37 | [diff] [blame] | 131 | * @param {!Array.<string>} actionIds |
| 132 | * @return {!Array.<number>} |
| 133 | */ |
| 134 | keysForActions(actionIds) { |
Jack Lynch | b8fb3c7 | 2020-04-21 05:36:16 | [diff] [blame] | 135 | const keys = actionIds.flatMap( |
| 136 | action => [...this._actionToShortcut.get(action)].flatMap( |
| 137 | shortcut => shortcut.descriptors.map(descriptor => descriptor.key))); |
| 138 | return [...(new Set(keys))]; |
Blink Reformat | 4c46d09 | 2018-04-07 15:32:37 | [diff] [blame] | 139 | } |
| 140 | |
| 141 | /** |
| 142 | * @param {string} actionId |
| 143 | * @return {string|undefined} |
| 144 | */ |
| 145 | shortcutTitleForAction(actionId) { |
Benedikt Meurer | 0afca61 | 2020-10-05 10:03:58 | [diff] [blame] | 146 | for (const shortcut of this._actionToShortcut.get(actionId)) { |
| 147 | return shortcut.title(); |
Tim van der Lippe | 1d6e57a | 2019-09-30 11:55:34 | [diff] [blame] | 148 | } |
Benedikt Meurer | 0afca61 | 2020-10-05 10:03:58 | [diff] [blame] | 149 | return undefined; |
Blink Reformat | 4c46d09 | 2018-04-07 15:32:37 | [diff] [blame] | 150 | } |
| 151 | |
| 152 | /** |
| 153 | * @param {!KeyboardEvent} event |
Jack Lynch | 966040b | 2020-04-23 20:11:44 | [diff] [blame] | 154 | * @param {!Object.<string, function():Promise.<boolean>>=} handlers |
Blink Reformat | 4c46d09 | 2018-04-07 15:32:37 | [diff] [blame] | 155 | */ |
Jack Lynch | 966040b | 2020-04-23 20:11:44 | [diff] [blame] | 156 | handleShortcut(event, handlers) { |
| 157 | this.handleKey(KeyboardShortcut.makeKeyFromEvent(event), event.key, event, handlers); |
Blink Reformat | 4c46d09 | 2018-04-07 15:32:37 | [diff] [blame] | 158 | } |
| 159 | |
| 160 | /** |
Jack Lynch | 42297a7 | 2020-06-21 01:54:30 | [diff] [blame] | 161 | * @param {string} actionId |
| 162 | * return {boolean} |
| 163 | */ |
| 164 | actionHasDefaultShortcut(actionId) { |
| 165 | return this._devToolsDefaultShortcutActions.has(actionId); |
| 166 | } |
| 167 | |
| 168 | /** |
Blink Reformat | 4c46d09 | 2018-04-07 15:32:37 | [diff] [blame] | 169 | * @param {!Element} element |
Jack Lynch | 966040b | 2020-04-23 20:11:44 | [diff] [blame] | 170 | * @param {!Object.<string, function():Promise.<boolean>>} handlers |
Jack Lynch | 726aef9 | 2020-10-21 18:14:39 | [diff] [blame] | 171 | * @return {function(!Event): void} |
Blink Reformat | 4c46d09 | 2018-04-07 15:32:37 | [diff] [blame] | 172 | */ |
Jack Lynch | 966040b | 2020-04-23 20:11:44 | [diff] [blame] | 173 | addShortcutListener(element, handlers) { |
| 174 | // We only want keys for these specific actions to get handled this |
Mathias Bynens | 5165a7a | 2020-06-10 05:51:43 | [diff] [blame] | 175 | // way; all others should be allowed to bubble up. |
| 176 | const allowlistKeyMap = new ShortcutTreeNode(0, 0); |
Jack Lynch | 966040b | 2020-04-23 20:11:44 | [diff] [blame] | 177 | const shortcuts = Object.keys(handlers).flatMap(action => [...this._actionToShortcut.get(action)]); |
| 178 | shortcuts.forEach(shortcut => { |
Mathias Bynens | 5165a7a | 2020-06-10 05:51:43 | [diff] [blame] | 179 | allowlistKeyMap.addKeyMapping(shortcut.descriptors.map(descriptor => descriptor.key), shortcut.action); |
Jack Lynch | 966040b | 2020-04-23 20:11:44 | [diff] [blame] | 180 | }); |
| 181 | |
Jack Lynch | 726aef9 | 2020-10-21 18:14:39 | [diff] [blame] | 182 | /** |
| 183 | * @param {!Event} event |
| 184 | */ |
| 185 | const listener = event => { |
Jack Lynch | 966040b | 2020-04-23 20:11:44 | [diff] [blame] | 186 | const key = KeyboardShortcut.makeKeyFromEvent(/** @type {!KeyboardEvent} */ (event)); |
Benedikt Meurer | 0afca61 | 2020-10-05 10:03:58 | [diff] [blame] | 187 | const keyMap = this._activePrefixKey ? allowlistKeyMap.getNode(this._activePrefixKey.key()) : allowlistKeyMap; |
| 188 | if (!keyMap) { |
| 189 | return; |
Tim van der Lippe | 1d6e57a | 2019-09-30 11:55:34 | [diff] [blame] | 190 | } |
Jack Lynch | 966040b | 2020-04-23 20:11:44 | [diff] [blame] | 191 | if (keyMap.getNode(key)) { |
| 192 | this.handleShortcut(/** @type {!KeyboardEvent} */ (event), handlers); |
| 193 | } |
Jack Lynch | 726aef9 | 2020-10-21 18:14:39 | [diff] [blame] | 194 | }; |
| 195 | element.addEventListener('keydown', listener); |
| 196 | return listener; |
Blink Reformat | 4c46d09 | 2018-04-07 15:32:37 | [diff] [blame] | 197 | } |
| 198 | |
| 199 | /** |
| 200 | * @param {number} key |
| 201 | * @param {string} domKey |
| 202 | * @param {!KeyboardEvent=} event |
Jack Lynch | 966040b | 2020-04-23 20:11:44 | [diff] [blame] | 203 | * @param {!Object.<string, function():Promise.<boolean>>=} handlers |
Blink Reformat | 4c46d09 | 2018-04-07 15:32:37 | [diff] [blame] | 204 | */ |
Jack Lynch | 966040b | 2020-04-23 20:11:44 | [diff] [blame] | 205 | async handleKey(key, domKey, event, handlers) { |
Blink Reformat | 4c46d09 | 2018-04-07 15:32:37 | [diff] [blame] | 206 | const keyModifiers = key >> 8; |
Jack Lynch | a0dd1f0 | 2020-05-04 23:33:35 | [diff] [blame] | 207 | const hasHandlersOrPrefixKey = !!handlers || !!this._activePrefixKey; |
Jack Lynch | 4a6f753 | 2020-05-07 00:48:09 | [diff] [blame] | 208 | const keyMapNode = this._keyMap.getNode(key); |
| 209 | const maybeHasActions = this._applicableActions(key, handlers).length > 0 || (keyMapNode && keyMapNode.hasChords()); |
Jack Lynch | a0dd1f0 | 2020-05-04 23:33:35 | [diff] [blame] | 210 | if ((!hasHandlersOrPrefixKey && isPossiblyInputKey()) || !maybeHasActions || |
Jack Lynch | 966040b | 2020-04-23 20:11:44 | [diff] [blame] | 211 | KeyboardShortcut.isModifier(KeyboardShortcut.keyCodeAndModifiersFromKey(key).keyCode)) { |
Blink Reformat | 4c46d09 | 2018-04-07 15:32:37 | [diff] [blame] | 212 | return; |
Tim van der Lippe | 1d6e57a | 2019-09-30 11:55:34 | [diff] [blame] | 213 | } |
Jack Lynch | a0dd1f0 | 2020-05-04 23:33:35 | [diff] [blame] | 214 | if (event) { |
| 215 | event.consume(true); |
| 216 | } |
| 217 | if (!hasHandlersOrPrefixKey && Dialog.hasInstance()) { |
| 218 | return; |
| 219 | } |
Jack Lynch | 966040b | 2020-04-23 20:11:44 | [diff] [blame] | 220 | |
| 221 | if (this._activePrefixTimeout) { |
| 222 | clearTimeout(this._activePrefixTimeout); |
| 223 | const handled = await maybeExecuteActionForKey.call(this); |
| 224 | this._activePrefixKey = null; |
| 225 | this._activePrefixTimeout = null; |
| 226 | if (handled) { |
| 227 | return; |
Tim van der Lippe | 1d6e57a | 2019-09-30 11:55:34 | [diff] [blame] | 228 | } |
Jack Lynch | 966040b | 2020-04-23 20:11:44 | [diff] [blame] | 229 | if (this._consumePrefix) { |
| 230 | await this._consumePrefix(); |
| 231 | } |
| 232 | } |
Jack Lynch | 966040b | 2020-04-23 20:11:44 | [diff] [blame] | 233 | if (keyMapNode && keyMapNode.hasChords()) { |
Jack Lynch | 966040b | 2020-04-23 20:11:44 | [diff] [blame] | 234 | this._activePrefixKey = keyMapNode; |
| 235 | this._consumePrefix = async () => { |
| 236 | this._activePrefixKey = null; |
| 237 | this._activePrefixTimeout = null; |
| 238 | await maybeExecuteActionForKey.call(this); |
| 239 | }; |
Benedikt Meurer | 0afca61 | 2020-10-05 10:03:58 | [diff] [blame] | 240 | this._activePrefixTimeout = window.setTimeout(this._consumePrefix, KeyTimeout); |
Jack Lynch | 966040b | 2020-04-23 20:11:44 | [diff] [blame] | 241 | } else { |
| 242 | await maybeExecuteActionForKey.call(this); |
Blink Reformat | 4c46d09 | 2018-04-07 15:32:37 | [diff] [blame] | 243 | } |
| 244 | |
| 245 | /** |
| 246 | * @return {boolean} |
| 247 | */ |
| 248 | function isPossiblyInputKey() { |
Paul Lewis | 9950e18 | 2019-12-16 16:06:07 | [diff] [blame] | 249 | 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] | 250 | return false; |
Tim van der Lippe | 1d6e57a | 2019-09-30 11:55:34 | [diff] [blame] | 251 | } |
Blink Reformat | 4c46d09 | 2018-04-07 15:32:37 | [diff] [blame] | 252 | |
Tim van der Lippe | 1d6e57a | 2019-09-30 11:55:34 | [diff] [blame] | 253 | if (!keyModifiers) { |
Blink Reformat | 4c46d09 | 2018-04-07 15:32:37 | [diff] [blame] | 254 | return true; |
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 | |
Paul Lewis | 9950e18 | 2019-12-16 16:06:07 | [diff] [blame] | 257 | const modifiers = Modifiers; |
Joel Einbinder | a66e5bf | 2018-05-31 01:26:37 | [diff] [blame] | 258 | // 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] | 259 | if (Host.Platform.isMac()) { |
Paul Lewis | 9950e18 | 2019-12-16 16:06:07 | [diff] [blame] | 260 | if (KeyboardShortcut.makeKey('z', modifiers.Meta) === key) { |
Joel Einbinder | a66e5bf | 2018-05-31 01:26:37 | [diff] [blame] | 261 | return true; |
Tim van der Lippe | 1d6e57a | 2019-09-30 11:55:34 | [diff] [blame] | 262 | } |
Paul Lewis | 9950e18 | 2019-12-16 16:06:07 | [diff] [blame] | 263 | if (KeyboardShortcut.makeKey('z', modifiers.Meta | modifiers.Shift) === key) { |
Joel Einbinder | a66e5bf | 2018-05-31 01:26:37 | [diff] [blame] | 264 | return true; |
Tim van der Lippe | 1d6e57a | 2019-09-30 11:55:34 | [diff] [blame] | 265 | } |
Joel Einbinder | a66e5bf | 2018-05-31 01:26:37 | [diff] [blame] | 266 | } else { |
Paul Lewis | 9950e18 | 2019-12-16 16:06:07 | [diff] [blame] | 267 | if (KeyboardShortcut.makeKey('z', modifiers.Ctrl) === key) { |
Joel Einbinder | a66e5bf | 2018-05-31 01:26:37 | [diff] [blame] | 268 | return true; |
Tim van der Lippe | 1d6e57a | 2019-09-30 11:55:34 | [diff] [blame] | 269 | } |
Paul Lewis | 9950e18 | 2019-12-16 16:06:07 | [diff] [blame] | 270 | if (KeyboardShortcut.makeKey('y', modifiers.Ctrl) === key) { |
Joel Einbinder | a66e5bf | 2018-05-31 01:26:37 | [diff] [blame] | 271 | return true; |
Tim van der Lippe | 1d6e57a | 2019-09-30 11:55:34 | [diff] [blame] | 272 | } |
Paul Lewis | 0fd4371 | 2020-01-08 17:07:36 | [diff] [blame] | 273 | if (!Host.Platform.isWin() && KeyboardShortcut.makeKey('z', modifiers.Ctrl | modifiers.Shift) === key) { |
Joel Einbinder | a66e5bf | 2018-05-31 01:26:37 | [diff] [blame] | 274 | return true; |
Tim van der Lippe | 1d6e57a | 2019-09-30 11:55:34 | [diff] [blame] | 275 | } |
Joel Einbinder | a66e5bf | 2018-05-31 01:26:37 | [diff] [blame] | 276 | } |
| 277 | |
Tim van der Lippe | 1d6e57a | 2019-09-30 11:55:34 | [diff] [blame] | 278 | if ((keyModifiers & (modifiers.Ctrl | modifiers.Alt)) === (modifiers.Ctrl | modifiers.Alt)) { |
Paul Lewis | 0fd4371 | 2020-01-08 17:07:36 | [diff] [blame] | 279 | return Host.Platform.isWin(); |
Tim van der Lippe | 1d6e57a | 2019-09-30 11:55:34 | [diff] [blame] | 280 | } |
Blink Reformat | 4c46d09 | 2018-04-07 15:32:37 | [diff] [blame] | 281 | |
| 282 | return !hasModifier(modifiers.Ctrl) && !hasModifier(modifiers.Alt) && !hasModifier(modifiers.Meta); |
| 283 | } |
| 284 | |
| 285 | /** |
| 286 | * @param {number} mod |
| 287 | * @return {boolean} |
| 288 | */ |
| 289 | function hasModifier(mod) { |
| 290 | return !!(keyModifiers & mod); |
| 291 | } |
Jack Lynch | 966040b | 2020-04-23 20:11:44 | [diff] [blame] | 292 | |
| 293 | /** |
| 294 | * @return {!Promise.<boolean>}; |
| 295 | * @this {!ShortcutRegistry} |
| 296 | */ |
| 297 | async function maybeExecuteActionForKey() { |
| 298 | const actions = this._applicableActions(key, handlers); |
| 299 | if (!actions.length) { |
| 300 | return false; |
| 301 | } |
Jack Lynch | 966040b | 2020-04-23 20:11:44 | [diff] [blame] | 302 | for (const action of actions) { |
| 303 | let handled; |
| 304 | if (handlers && handlers[action.id()]) { |
| 305 | handled = await handlers[action.id()](); |
| 306 | } |
| 307 | if (!handlers) { |
| 308 | handled = await action.execute(); |
| 309 | } |
| 310 | if (handled) { |
| 311 | Host.userMetrics.keyboardShortcutFired(action.id()); |
| 312 | return true; |
| 313 | } |
| 314 | } |
| 315 | return false; |
| 316 | } |
Blink Reformat | 4c46d09 | 2018-04-07 15:32:37 | [diff] [blame] | 317 | } |
| 318 | |
| 319 | /** |
Jack Lynch | 94a9b0c | 2020-03-11 21:45:14 | [diff] [blame] | 320 | * @param {!KeyboardShortcut} shortcut |
Blink Reformat | 4c46d09 | 2018-04-07 15:32:37 | [diff] [blame] | 321 | */ |
Jack Lynch | 5f863da | 2020-09-16 23:18:42 | [diff] [blame] | 322 | registerUserShortcut(shortcut) { |
| 323 | for (const otherShortcut of this._disabledDefaultShortcutsForAction.get(shortcut.action)) { |
| 324 | if (otherShortcut.descriptorsMatch(shortcut.descriptors) && |
| 325 | otherShortcut.hasKeybindSet(this._keybindSetSetting.get())) { |
| 326 | // this user shortcut is the same as a disabled default shortcut, |
| 327 | // so we should just enable the default |
| 328 | this.removeShortcut(otherShortcut); |
| 329 | return; |
| 330 | } |
| 331 | } |
| 332 | for (const otherShortcut of this._actionToShortcut.get(shortcut.action)) { |
| 333 | if (otherShortcut.descriptorsMatch(shortcut.descriptors) && |
| 334 | otherShortcut.hasKeybindSet(this._keybindSetSetting.get())) { |
| 335 | // don't allow duplicate shortcuts |
| 336 | return; |
| 337 | } |
| 338 | } |
| 339 | this._addShortcutToSetting(shortcut); |
| 340 | } |
| 341 | |
| 342 | /** |
| 343 | * @param {!KeyboardShortcut} shortcut |
| 344 | */ |
| 345 | removeShortcut(shortcut) { |
| 346 | if (shortcut.type === Type.DefaultShortcut || shortcut.type === Type.KeybindSetShortcut) { |
| 347 | this._addShortcutToSetting(shortcut.changeType(Type.DisabledDefault)); |
| 348 | } else { |
| 349 | this._removeShortcutFromSetting(shortcut); |
| 350 | } |
| 351 | } |
| 352 | |
| 353 | /** |
Jack Lynch | fa6e27a | 2020-10-26 19:02:05 | [diff] [blame] | 354 | * @param {string} actionId |
| 355 | * @return {!Set.<!KeyboardShortcut>} |
| 356 | */ |
| 357 | disabledDefaultsForAction(actionId) { |
| 358 | return this._disabledDefaultShortcutsForAction.get(actionId); |
| 359 | } |
| 360 | |
| 361 | /** |
Jack Lynch | 5f863da | 2020-09-16 23:18:42 | [diff] [blame] | 362 | * @param {!KeyboardShortcut} shortcut |
| 363 | */ |
| 364 | _addShortcutToSetting(shortcut) { |
| 365 | const userShortcuts = this._userShortcutsSetting.get(); |
| 366 | userShortcuts.push(shortcut); |
| 367 | this._userShortcutsSetting.set(userShortcuts); |
| 368 | } |
| 369 | |
| 370 | /** |
| 371 | * @param {!KeyboardShortcut} shortcut |
| 372 | */ |
| 373 | _removeShortcutFromSetting(shortcut) { |
| 374 | const userShortcuts = this._userShortcutsSetting.get(); |
| 375 | const index = userShortcuts.findIndex(shortcut.equals, shortcut); |
| 376 | if (index !== -1) { |
| 377 | userShortcuts.splice(index, 1); |
| 378 | this._userShortcutsSetting.set(userShortcuts); |
| 379 | } |
| 380 | } |
| 381 | |
| 382 | /** |
| 383 | * @param {!KeyboardShortcut} shortcut |
| 384 | */ |
Jack Lynch | 94a9b0c | 2020-03-11 21:45:14 | [diff] [blame] | 385 | _registerShortcut(shortcut) { |
| 386 | this._actionToShortcut.set(shortcut.action, shortcut); |
Jack Lynch | 966040b | 2020-04-23 20:11:44 | [diff] [blame] | 387 | this._keyMap.addKeyMapping(shortcut.descriptors.map(descriptor => descriptor.key), shortcut.action); |
Blink Reformat | 4c46d09 | 2018-04-07 15:32:37 | [diff] [blame] | 388 | } |
| 389 | |
Jack Lynch | 94a9b0c | 2020-03-11 21:45:14 | [diff] [blame] | 390 | _registerBindings() { |
Jack Lynch | f1f00fa | 2020-05-01 22:16:12 | [diff] [blame] | 391 | this._actionToShortcut.clear(); |
| 392 | this._keyMap.clear(); |
Jack Lynch | 5f863da | 2020-09-16 23:18:42 | [diff] [blame] | 393 | const keybindSet = this._keybindSetSetting.get(); |
Tim van der Lippe | 7f2002c | 2020-09-28 14:54:01 | [diff] [blame] | 394 | const extensions = Root.Runtime.Runtime.instance().extensions('action'); |
Jack Lynch | 5f863da | 2020-09-16 23:18:42 | [diff] [blame] | 395 | this._disabledDefaultShortcutsForAction.clear(); |
| 396 | this._devToolsDefaultShortcutActions.clear(); |
Benedikt Meurer | 0afca61 | 2020-10-05 10:03:58 | [diff] [blame] | 397 | /** @type {!Array<!{keyCode: number, modifiers: number}>} */ |
Jack Lynch | 10d7055 | 2020-06-10 20:30:36 | [diff] [blame] | 398 | const forwardedKeys = []; |
Benedikt Meurer | 973bb96 | 2020-10-05 13:53:34 | [diff] [blame] | 399 | if (Root.Runtime.experiments.isEnabled('keyboardShortcutEditor')) { |
Benedikt Meurer | 0afca61 | 2020-10-05 10:03:58 | [diff] [blame] | 400 | /** @type {!Array<!{action: string, descriptors: !Array.<!Descriptor>, type: !Type}>} */ |
Jack Lynch | 5f863da | 2020-09-16 23:18:42 | [diff] [blame] | 401 | const userShortcuts = this._userShortcutsSetting.get(); |
Benedikt Meurer | 0afca61 | 2020-10-05 10:03:58 | [diff] [blame] | 402 | for (const userShortcut of userShortcuts) { |
Jack Lynch | 5f863da | 2020-09-16 23:18:42 | [diff] [blame] | 403 | const shortcut = KeyboardShortcut.createShortcutFromSettingObject(userShortcut); |
| 404 | if (shortcut.type === Type.DisabledDefault) { |
| 405 | this._disabledDefaultShortcutsForAction.set(shortcut.action, shortcut); |
| 406 | } else { |
| 407 | if (ForwardedActions.has(shortcut.action)) { |
| 408 | forwardedKeys.push( |
Benedikt Meurer | 0afca61 | 2020-10-05 10:03:58 | [diff] [blame] | 409 | ...shortcut.descriptors.map(descriptor => KeyboardShortcut.keyCodeAndModifiersFromKey(descriptor.key))); |
Jack Lynch | 5f863da | 2020-09-16 23:18:42 | [diff] [blame] | 410 | } |
| 411 | this._registerShortcut(shortcut); |
| 412 | } |
Benedikt Meurer | 0afca61 | 2020-10-05 10:03:58 | [diff] [blame] | 413 | } |
Jack Lynch | 5f863da | 2020-09-16 23:18:42 | [diff] [blame] | 414 | } |
Blink Reformat | 4c46d09 | 2018-04-07 15:32:37 | [diff] [blame] | 415 | extensions.forEach(registerExtension, this); |
Jack Lynch | 10d7055 | 2020-06-10 20:30:36 | [diff] [blame] | 416 | Host.InspectorFrontendHost.InspectorFrontendHostInstance.setWhitelistedShortcuts(JSON.stringify(forwardedKeys)); |
Blink Reformat | 4c46d09 | 2018-04-07 15:32:37 | [diff] [blame] | 417 | |
| 418 | /** |
Tim van der Lippe | 99e59b8 | 2019-09-30 20:00:59 | [diff] [blame] | 419 | * @param {!Root.Runtime.Extension} extension |
Tim van der Lippe | 0830b3d | 2019-10-03 13:20:07 | [diff] [blame] | 420 | * @this {ShortcutRegistry} |
Blink Reformat | 4c46d09 | 2018-04-07 15:32:37 | [diff] [blame] | 421 | */ |
| 422 | function registerExtension(extension) { |
| 423 | const descriptor = extension.descriptor(); |
Jack Lynch | 94a9b0c | 2020-03-11 21:45:14 | [diff] [blame] | 424 | const bindings = descriptor.bindings; |
Blink Reformat | 4c46d09 | 2018-04-07 15:32:37 | [diff] [blame] | 425 | for (let i = 0; bindings && i < bindings.length; ++i) { |
Jack Lynch | f1f00fa | 2020-05-01 22:16:12 | [diff] [blame] | 426 | const keybindSets = bindings[i].keybindSets; |
| 427 | if (!platformMatches(bindings[i].platform) || !keybindSetsMatch(keybindSets)) { |
Blink Reformat | 4c46d09 | 2018-04-07 15:32:37 | [diff] [blame] | 428 | continue; |
Tim van der Lippe | 1d6e57a | 2019-09-30 11:55:34 | [diff] [blame] | 429 | } |
Jack Lynch | 966040b | 2020-04-23 20:11:44 | [diff] [blame] | 430 | const keys = bindings[i].shortcut.split(/\s+/); |
| 431 | const shortcutDescriptors = keys.map(KeyboardShortcut.makeDescriptorFromBindingShortcut); |
| 432 | if (shortcutDescriptors.length > 0) { |
Jack Lynch | f1f00fa | 2020-05-01 22:16:12 | [diff] [blame] | 433 | const actionId = /** @type {string} */ (descriptor.actionId); |
Jack Lynch | 5f863da | 2020-09-16 23:18:42 | [diff] [blame] | 434 | |
| 435 | if (this._isDisabledDefault(shortcutDescriptors, actionId)) { |
| 436 | this._devToolsDefaultShortcutActions.add(actionId); |
| 437 | continue; |
| 438 | } |
| 439 | |
Jack Lynch | 10d7055 | 2020-06-10 20:30:36 | [diff] [blame] | 440 | if (ForwardedActions.has(actionId)) { |
| 441 | forwardedKeys.push( |
| 442 | ...shortcutDescriptors.map(shortcut => KeyboardShortcut.keyCodeAndModifiersFromKey(shortcut.key))); |
| 443 | } |
Jack Lynch | f1f00fa | 2020-05-01 22:16:12 | [diff] [blame] | 444 | if (!keybindSets) { |
Jack Lynch | 42297a7 | 2020-06-21 01:54:30 | [diff] [blame] | 445 | this._devToolsDefaultShortcutActions.add(actionId); |
Jack Lynch | f1f00fa | 2020-05-01 22:16:12 | [diff] [blame] | 446 | this._registerShortcut(new KeyboardShortcut(shortcutDescriptors, actionId, Type.DefaultShortcut)); |
| 447 | } else { |
Jack Lynch | 42297a7 | 2020-06-21 01:54:30 | [diff] [blame] | 448 | if (keybindSets.includes(DefaultShortcutSetting)) { |
| 449 | this._devToolsDefaultShortcutActions.add(actionId); |
| 450 | } |
Jack Lynch | f1f00fa | 2020-05-01 22:16:12 | [diff] [blame] | 451 | this._registerShortcut( |
Jack Lynch | 42297a7 | 2020-06-21 01:54:30 | [diff] [blame] | 452 | new KeyboardShortcut(shortcutDescriptors, actionId, Type.KeybindSetShortcut, new Set(keybindSets))); |
Jack Lynch | f1f00fa | 2020-05-01 22:16:12 | [diff] [blame] | 453 | } |
Jack Lynch | f0d1424 | 2020-04-07 22:42:04 | [diff] [blame] | 454 | } |
Blink Reformat | 4c46d09 | 2018-04-07 15:32:37 | [diff] [blame] | 455 | } |
| 456 | } |
| 457 | |
| 458 | /** |
| 459 | * @param {string=} platformsString |
| 460 | * @return {boolean} |
| 461 | */ |
| 462 | function platformMatches(platformsString) { |
Tim van der Lippe | 1d6e57a | 2019-09-30 11:55:34 | [diff] [blame] | 463 | if (!platformsString) { |
Blink Reformat | 4c46d09 | 2018-04-07 15:32:37 | [diff] [blame] | 464 | return true; |
Tim van der Lippe | 1d6e57a | 2019-09-30 11:55:34 | [diff] [blame] | 465 | } |
Blink Reformat | 4c46d09 | 2018-04-07 15:32:37 | [diff] [blame] | 466 | const platforms = platformsString.split(','); |
| 467 | let isMatch = false; |
Paul Lewis | 0fd4371 | 2020-01-08 17:07:36 | [diff] [blame] | 468 | const currentPlatform = Host.Platform.platform(); |
Tim van der Lippe | 1d6e57a | 2019-09-30 11:55:34 | [diff] [blame] | 469 | for (let i = 0; !isMatch && i < platforms.length; ++i) { |
Blink Reformat | 4c46d09 | 2018-04-07 15:32:37 | [diff] [blame] | 470 | isMatch = platforms[i] === currentPlatform; |
Tim van der Lippe | 1d6e57a | 2019-09-30 11:55:34 | [diff] [blame] | 471 | } |
Blink Reformat | 4c46d09 | 2018-04-07 15:32:37 | [diff] [blame] | 472 | return isMatch; |
| 473 | } |
Jack Lynch | f1f00fa | 2020-05-01 22:16:12 | [diff] [blame] | 474 | |
| 475 | /** |
| 476 | * @param {!Array<string>=} keybindSets |
| 477 | */ |
| 478 | function keybindSetsMatch(keybindSets) { |
| 479 | if (!keybindSets) { |
| 480 | return true; |
| 481 | } |
| 482 | return keybindSets.includes(keybindSet); |
| 483 | } |
Blink Reformat | 4c46d09 | 2018-04-07 15:32:37 | [diff] [blame] | 484 | } |
Jack Lynch | 5f863da | 2020-09-16 23:18:42 | [diff] [blame] | 485 | |
Benedikt Meurer | 0afca61 | 2020-10-05 10:03:58 | [diff] [blame] | 486 | /** |
| 487 | * @param {!Array<!{key: number, name: string}>} shortcutDescriptors |
| 488 | * @param {string} action |
| 489 | */ |
Jack Lynch | 5f863da | 2020-09-16 23:18:42 | [diff] [blame] | 490 | _isDisabledDefault(shortcutDescriptors, action) { |
| 491 | const disabledDefaults = this._disabledDefaultShortcutsForAction.get(action); |
| 492 | for (const disabledDefault of disabledDefaults) { |
| 493 | if (disabledDefault.descriptorsMatch(shortcutDescriptors)) { |
| 494 | return true; |
| 495 | } |
| 496 | } |
| 497 | return false; |
| 498 | } |
Tim van der Lippe | 0830b3d | 2019-10-03 13:20:07 | [diff] [blame] | 499 | } |
Blink Reformat | 4c46d09 | 2018-04-07 15:32:37 | [diff] [blame] | 500 | |
Jack Lynch | 966040b | 2020-04-23 20:11:44 | [diff] [blame] | 501 | export class ShortcutTreeNode { |
| 502 | /** |
| 503 | * @param {number} key |
| 504 | * @param {number=} depth |
| 505 | */ |
| 506 | constructor(key, depth = 0) { |
| 507 | this._key = key; |
| 508 | /** @type {!Array.<string>} */ |
| 509 | this._actions = []; |
| 510 | this._chords = new Map(); |
| 511 | this._depth = depth; |
| 512 | } |
| 513 | |
| 514 | /** |
| 515 | * @param {string} action |
| 516 | */ |
| 517 | addAction(action) { |
| 518 | this._actions.push(action); |
| 519 | } |
| 520 | |
| 521 | /** |
| 522 | * @return {number} |
| 523 | */ |
| 524 | key() { |
| 525 | return this._key; |
| 526 | } |
| 527 | |
| 528 | /** |
| 529 | * @return {!Map.<number, !ShortcutTreeNode>} |
| 530 | */ |
| 531 | chords() { |
| 532 | return this._chords; |
| 533 | } |
| 534 | |
| 535 | /** |
| 536 | * @return {boolean} |
| 537 | */ |
| 538 | hasChords() { |
| 539 | return this._chords.size > 0; |
| 540 | } |
| 541 | |
| 542 | /** |
| 543 | * @param {!Array.<number>} keys |
| 544 | * @param {string} action |
| 545 | */ |
| 546 | addKeyMapping(keys, action) { |
| 547 | if (keys.length < this._depth) { |
| 548 | return; |
| 549 | } |
| 550 | |
| 551 | if (keys.length === this._depth) { |
| 552 | this.addAction(action); |
| 553 | } else { |
| 554 | const key = keys[this._depth]; |
| 555 | if (!this._chords.has(key)) { |
| 556 | this._chords.set(key, new ShortcutTreeNode(key, this._depth + 1)); |
| 557 | } |
| 558 | this._chords.get(key).addKeyMapping(keys, action); |
| 559 | } |
| 560 | } |
| 561 | |
| 562 | /** |
| 563 | * @param {number} key |
| 564 | * @return {?ShortcutTreeNode} |
| 565 | */ |
| 566 | getNode(key) { |
| 567 | return this._chords.get(key) || null; |
| 568 | } |
| 569 | |
| 570 | /** |
| 571 | * @return {!Array.<string>} |
| 572 | */ |
| 573 | actions() { |
| 574 | return this._actions; |
| 575 | } |
Jack Lynch | f1f00fa | 2020-05-01 22:16:12 | [diff] [blame] | 576 | |
| 577 | clear() { |
| 578 | this._actions = []; |
| 579 | this._chords = new Map(); |
| 580 | } |
Jack Lynch | 966040b | 2020-04-23 20:11:44 | [diff] [blame] | 581 | } |
| 582 | |
Sigurd Schneider | 46da7db | 2020-05-20 13:45:11 | [diff] [blame] | 583 | |
Tim van der Lippe | 0830b3d | 2019-10-03 13:20:07 | [diff] [blame] | 584 | export class ForwardedShortcut {} |
Blink Reformat | 4c46d09 | 2018-04-07 15:32:37 | [diff] [blame] | 585 | |
Tim van der Lippe | 0830b3d | 2019-10-03 13:20:07 | [diff] [blame] | 586 | ForwardedShortcut.instance = new ForwardedShortcut(); |
Jack Lynch | 966040b | 2020-04-23 20:11:44 | [diff] [blame] | 587 | |
Jack Lynch | 10d7055 | 2020-06-10 20:30:36 | [diff] [blame] | 588 | export const ForwardedActions = new Set([ |
| 589 | 'main.toggle-dock', 'debugger.toggle-breakpoints-active', 'debugger.toggle-pause', 'commandMenu.show', 'console.show' |
| 590 | ]); |
Jack Lynch | 966040b | 2020-04-23 20:11:44 | [diff] [blame] | 591 | export const KeyTimeout = 1000; |
Jack Lynch | f1f00fa | 2020-05-01 22:16:12 | [diff] [blame] | 592 | export const DefaultShortcutSetting = 'devToolsDefault'; |