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. |
| 4 | /** |
| 5 | * @unrestricted |
| 6 | */ |
| 7 | UI.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 Einbinder | 67f28fb | 2018-08-02 00:33:47 | [diff] [blame] | 38 | * @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 Lippe | 1d6e57a | 2019-09-30 11:55:34 | [diff] [blame^] | 45 | if (applicableActions.length) { |
Joel Einbinder | 67f28fb | 2018-08-02 00:33:47 | [diff] [blame] | 46 | keys.push(Number(key)); |
Tim van der Lippe | 1d6e57a | 2019-09-30 11:55:34 | [diff] [blame^] | 47 | } |
Joel Einbinder | 67f28fb | 2018-08-02 00:33:47 | [diff] [blame] | 48 | } |
| 49 | return keys; |
| 50 | } |
| 51 | |
| 52 | /** |
Blink Reformat | 4c46d09 | 2018-04-07 15:32:37 | [diff] [blame] | 53 | * @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 Lippe | 1d6e57a | 2019-09-30 11:55:34 | [diff] [blame^] | 68 | for (let j = 0; j < descriptors.length; ++j) { |
Blink Reformat | 4c46d09 | 2018-04-07 15:32:37 | [diff] [blame] | 69 | result.push(descriptors[j].key); |
Tim van der Lippe | 1d6e57a | 2019-09-30 11:55:34 | [diff] [blame^] | 70 | } |
Blink Reformat | 4c46d09 | 2018-04-07 15:32:37 | [diff] [blame] | 71 | } |
| 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 Lippe | 1d6e57a | 2019-09-30 11:55:34 | [diff] [blame^] | 81 | if (descriptors.length) { |
Blink Reformat | 4c46d09 | 2018-04-07 15:32:37 | [diff] [blame] | 82 | return descriptors[0].name; |
Tim van der Lippe | 1d6e57a | 2019-09-30 11:55:34 | [diff] [blame^] | 83 | } |
Blink Reformat | 4c46d09 | 2018-04-07 15:32:37 | [diff] [blame] | 84 | } |
| 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 Lippe | 1d6e57a | 2019-09-30 11:55:34 | [diff] [blame^] | 113 | if (!this.eventMatchesAction(/** @type {!KeyboardEvent} */ (event), actionId) || !listener.call(null)) { |
Blink Reformat | 4c46d09 | 2018-04-07 15:32:37 | [diff] [blame] | 114 | return; |
Tim van der Lippe | 1d6e57a | 2019-09-30 11:55:34 | [diff] [blame^] | 115 | } |
Blink Reformat | 4c46d09 | 2018-04-07 15:32:37 | [diff] [blame] | 116 | event.consume(true); |
| 117 | }, capture); |
| 118 | } |
| 119 | |
| 120 | /** |
| 121 | * @param {number} key |
| 122 | * @param {string} domKey |
| 123 | * @param {!KeyboardEvent=} event |
| 124 | */ |
Erik Luo | 9855f61 | 2018-10-18 19:11:30 | [diff] [blame] | 125 | async handleKey(key, domKey, event) { |
Blink Reformat | 4c46d09 | 2018-04-07 15:32:37 | [diff] [blame] | 126 | const keyModifiers = key >> 8; |
| 127 | const actions = this._applicableActions(key); |
Tim van der Lippe | 1d6e57a | 2019-09-30 11:55:34 | [diff] [blame^] | 128 | if (!actions.length || isPossiblyInputKey()) { |
Blink Reformat | 4c46d09 | 2018-04-07 15:32:37 | [diff] [blame] | 129 | return; |
Tim van der Lippe | 1d6e57a | 2019-09-30 11:55:34 | [diff] [blame^] | 130 | } |
| 131 | if (event) { |
Erik Luo | 9855f61 | 2018-10-18 19:11:30 | [diff] [blame] | 132 | event.consume(true); |
Tim van der Lippe | 1d6e57a | 2019-09-30 11:55:34 | [diff] [blame^] | 133 | } |
| 134 | if (UI.Dialog.hasInstance()) { |
Blink Reformat | 4c46d09 | 2018-04-07 15:32:37 | [diff] [blame] | 135 | return; |
Tim van der Lippe | 1d6e57a | 2019-09-30 11:55:34 | [diff] [blame^] | 136 | } |
Erik Luo | 9855f61 | 2018-10-18 19:11:30 | [diff] [blame] | 137 | for (const action of actions) { |
Tim van der Lippe | 1d6e57a | 2019-09-30 11:55:34 | [diff] [blame^] | 138 | if (await action.execute()) { |
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 | } |
Blink Reformat | 4c46d09 | 2018-04-07 15:32:37 | [diff] [blame] | 141 | } |
| 142 | |
| 143 | /** |
| 144 | * @return {boolean} |
| 145 | */ |
| 146 | function isPossiblyInputKey() { |
Tim van der Lippe | 1d6e57a | 2019-09-30 11:55:34 | [diff] [blame^] | 147 | if (!event || !UI.isEditing() || /^F\d+|Control|Shift|Alt|Meta|Escape|Win|U\+001B$/.test(domKey)) { |
Blink Reformat | 4c46d09 | 2018-04-07 15:32:37 | [diff] [blame] | 148 | return false; |
Tim van der Lippe | 1d6e57a | 2019-09-30 11:55:34 | [diff] [blame^] | 149 | } |
Blink Reformat | 4c46d09 | 2018-04-07 15:32:37 | [diff] [blame] | 150 | |
Tim van der Lippe | 1d6e57a | 2019-09-30 11:55:34 | [diff] [blame^] | 151 | if (!keyModifiers) { |
Blink Reformat | 4c46d09 | 2018-04-07 15:32:37 | [diff] [blame] | 152 | return true; |
Tim van der Lippe | 1d6e57a | 2019-09-30 11:55:34 | [diff] [blame^] | 153 | } |
Blink Reformat | 4c46d09 | 2018-04-07 15:32:37 | [diff] [blame] | 154 | |
| 155 | const modifiers = UI.KeyboardShortcut.Modifiers; |
Joel Einbinder | a66e5bf | 2018-05-31 01:26:37 | [diff] [blame] | 156 | // Undo/Redo will also cause input, so textual undo should take precedence over DevTools undo when editing. |
| 157 | if (Host.isMac()) { |
Tim van der Lippe | 1d6e57a | 2019-09-30 11:55:34 | [diff] [blame^] | 158 | if (UI.KeyboardShortcut.makeKey('z', modifiers.Meta) === key) { |
Joel Einbinder | a66e5bf | 2018-05-31 01:26:37 | [diff] [blame] | 159 | return true; |
Tim van der Lippe | 1d6e57a | 2019-09-30 11:55:34 | [diff] [blame^] | 160 | } |
| 161 | if (UI.KeyboardShortcut.makeKey('z', modifiers.Meta | modifiers.Shift) === key) { |
Joel Einbinder | a66e5bf | 2018-05-31 01:26:37 | [diff] [blame] | 162 | return true; |
Tim van der Lippe | 1d6e57a | 2019-09-30 11:55:34 | [diff] [blame^] | 163 | } |
Joel Einbinder | a66e5bf | 2018-05-31 01:26:37 | [diff] [blame] | 164 | } else { |
Tim van der Lippe | 1d6e57a | 2019-09-30 11:55:34 | [diff] [blame^] | 165 | if (UI.KeyboardShortcut.makeKey('z', modifiers.Ctrl) === key) { |
Joel Einbinder | a66e5bf | 2018-05-31 01:26:37 | [diff] [blame] | 166 | return true; |
Tim van der Lippe | 1d6e57a | 2019-09-30 11:55:34 | [diff] [blame^] | 167 | } |
| 168 | if (UI.KeyboardShortcut.makeKey('y', modifiers.Ctrl) === key) { |
Joel Einbinder | a66e5bf | 2018-05-31 01:26:37 | [diff] [blame] | 169 | return true; |
Tim van der Lippe | 1d6e57a | 2019-09-30 11:55:34 | [diff] [blame^] | 170 | } |
| 171 | if (!Host.isWin() && UI.KeyboardShortcut.makeKey('z', modifiers.Ctrl | modifiers.Shift) === key) { |
Joel Einbinder | a66e5bf | 2018-05-31 01:26:37 | [diff] [blame] | 172 | return true; |
Tim van der Lippe | 1d6e57a | 2019-09-30 11:55:34 | [diff] [blame^] | 173 | } |
Joel Einbinder | a66e5bf | 2018-05-31 01:26:37 | [diff] [blame] | 174 | } |
| 175 | |
Tim van der Lippe | 1d6e57a | 2019-09-30 11:55:34 | [diff] [blame^] | 176 | if ((keyModifiers & (modifiers.Ctrl | modifiers.Alt)) === (modifiers.Ctrl | modifiers.Alt)) { |
Blink Reformat | 4c46d09 | 2018-04-07 15:32:37 | [diff] [blame] | 177 | return Host.isWin(); |
Tim van der Lippe | 1d6e57a | 2019-09-30 11:55:34 | [diff] [blame^] | 178 | } |
Blink Reformat | 4c46d09 | 2018-04-07 15:32:37 | [diff] [blame] | 179 | |
| 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 Lippe | 1d6e57a | 2019-09-30 11:55:34 | [diff] [blame^] | 198 | if (!descriptor) { |
Blink Reformat | 4c46d09 | 2018-04-07 15:32:37 | [diff] [blame] | 199 | return; |
Tim van der Lippe | 1d6e57a | 2019-09-30 11:55:34 | [diff] [blame^] | 200 | } |
Blink Reformat | 4c46d09 | 2018-04-07 15:32:37 | [diff] [blame] | 201 | this._defaultActionToShortcut.set(actionId, descriptor); |
| 202 | this._defaultKeyToActions.set(String(descriptor.key), actionId); |
| 203 | } |
| 204 | |
Blink Reformat | 4c46d09 | 2018-04-07 15:32:37 | [diff] [blame] | 205 | /** |
| 206 | * @param {!Document} document |
| 207 | */ |
| 208 | _registerBindings(document) { |
Blink Reformat | 4c46d09 | 2018-04-07 15:32:37 | [diff] [blame] | 209 | 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 Lippe | 1d6e57a | 2019-09-30 11:55:34 | [diff] [blame^] | 220 | if (!platformMatches(bindings[i].platform)) { |
Blink Reformat | 4c46d09 | 2018-04-07 15:32:37 | [diff] [blame] | 221 | continue; |
Tim van der Lippe | 1d6e57a | 2019-09-30 11:55:34 | [diff] [blame^] | 222 | } |
Blink Reformat | 4c46d09 | 2018-04-07 15:32:37 | [diff] [blame] | 223 | 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 Lippe | 1d6e57a | 2019-09-30 11:55:34 | [diff] [blame^] | 233 | if (!platformsString) { |
Blink Reformat | 4c46d09 | 2018-04-07 15:32:37 | [diff] [blame] | 234 | return true; |
Tim van der Lippe | 1d6e57a | 2019-09-30 11:55:34 | [diff] [blame^] | 235 | } |
Blink Reformat | 4c46d09 | 2018-04-07 15:32:37 | [diff] [blame] | 236 | const platforms = platformsString.split(','); |
| 237 | let isMatch = false; |
| 238 | const currentPlatform = Host.platform(); |
Tim van der Lippe | 1d6e57a | 2019-09-30 11:55:34 | [diff] [blame^] | 239 | for (let i = 0; !isMatch && i < platforms.length; ++i) { |
Blink Reformat | 4c46d09 | 2018-04-07 15:32:37 | [diff] [blame] | 240 | isMatch = platforms[i] === currentPlatform; |
Tim van der Lippe | 1d6e57a | 2019-09-30 11:55:34 | [diff] [blame^] | 241 | } |
Blink Reformat | 4c46d09 | 2018-04-07 15:32:37 | [diff] [blame] | 242 | return isMatch; |
| 243 | } |
| 244 | } |
| 245 | }; |
| 246 | |
| 247 | /** |
| 248 | * @unrestricted |
| 249 | */ |
| 250 | UI.ShortcutRegistry.ForwardedShortcut = class {}; |
| 251 | |
| 252 | UI.ShortcutRegistry.ForwardedShortcut.instance = new UI.ShortcutRegistry.ForwardedShortcut(); |
| 253 | |
| 254 | /** @type {!UI.ShortcutRegistry} */ |
| 255 | UI.shortcutRegistry; |