blob: 6b72862fccf3a5a03fb4f7738b7fa03179635f7e [file] [log] [blame]
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* @unrestricted
*/
UI.ShortcutRegistry = class {
/**
* @param {!UI.ActionRegistry} actionRegistry
* @param {!Document} document
*/
constructor(actionRegistry, document) {
this._actionRegistry = actionRegistry;
/** @type {!Multimap.<string, string>} */
this._defaultKeyToActions = new Multimap();
/** @type {!Multimap.<string, !UI.KeyboardShortcut.Descriptor>} */
this._defaultActionToShortcut = new Multimap();
this._registerBindings(document);
}
/**
* @param {number} key
* @return {!Array.<!UI.Action>}
*/
_applicableActions(key) {
return this._actionRegistry.applicableActions(this._defaultActionsForKey(key).valuesArray(), UI.context);
}
/**
* @param {number} key
* @return {!Set.<string>}
*/
_defaultActionsForKey(key) {
return this._defaultKeyToActions.get(String(key));
}
/**
* @param {string} actionId
* @return {!Array.<!UI.KeyboardShortcut.Descriptor>}
*/
shortcutDescriptorsForAction(actionId) {
return this._defaultActionToShortcut.get(actionId).valuesArray();
}
/**
* @param {!Array.<string>} actionIds
* @return {!Array.<number>}
*/
keysForActions(actionIds) {
const result = [];
for (let i = 0; i < actionIds.length; ++i) {
const descriptors = this.shortcutDescriptorsForAction(actionIds[i]);
for (let j = 0; j < descriptors.length; ++j)
result.push(descriptors[j].key);
}
return result;
}
/**
* @param {string} actionId
* @return {string|undefined}
*/
shortcutTitleForAction(actionId) {
const descriptors = this.shortcutDescriptorsForAction(actionId);
if (descriptors.length)
return descriptors[0].name;
}
/**
* @param {!KeyboardEvent} event
*/
handleShortcut(event) {
this.handleKey(UI.KeyboardShortcut.makeKeyFromEvent(event), event.key, event);
}
/**
* @param {!KeyboardEvent} event
* @param {string} actionId
* @return {boolean}
*/
eventMatchesAction(event, actionId) {
console.assert(this._defaultActionToShortcut.has(actionId), 'Unknown action ' + actionId);
const key = UI.KeyboardShortcut.makeKeyFromEvent(event);
return this._defaultActionToShortcut.get(actionId).valuesArray().some(descriptor => descriptor.key === key);
}
/**
* @param {!Element} element
* @param {string} actionId
* @param {function():boolean} listener
* @param {boolean=} capture
*/
addShortcutListener(element, actionId, listener, capture) {
console.assert(this._defaultActionToShortcut.has(actionId), 'Unknown action ' + actionId);
element.addEventListener('keydown', event => {
if (!this.eventMatchesAction(/** @type {!KeyboardEvent} */ (event), actionId) || !listener.call(null))
return;
event.consume(true);
}, capture);
}
/**
* @param {number} key
* @param {string} domKey
* @param {!KeyboardEvent=} event
*/
handleKey(key, domKey, event) {
const keyModifiers = key >> 8;
const actions = this._applicableActions(key);
if (!actions.length)
return;
if (UI.Dialog.hasInstance()) {
if (event && !isPossiblyInputKey())
event.consume(true);
return;
}
if (!isPossiblyInputKey()) {
if (event)
event.consume(true);
processNextAction.call(this, false);
} else {
this._pendingActionTimer = setTimeout(processNextAction.bind(this, false), 0);
}
/**
* @param {boolean} handled
* @this {UI.ShortcutRegistry}
*/
function processNextAction(handled) {
delete this._pendingActionTimer;
const action = actions.shift();
if (!action || handled)
return;
action.execute().then(processNextAction.bind(this));
}
/**
* @return {boolean}
*/
function isPossiblyInputKey() {
if (!event || !UI.isEditing() || /^F\d+|Control|Shift|Alt|Meta|Escape|Win|U\+001B$/.test(domKey))
return false;
if (!keyModifiers)
return true;
const modifiers = UI.KeyboardShortcut.Modifiers;
// Undo/Redo will also cause input, so textual undo should take precedence over DevTools undo when editing.
if (Host.isMac()) {
if (UI.KeyboardShortcut.makeKey('z', modifiers.Meta) === key)
return true;
if (UI.KeyboardShortcut.makeKey('z', modifiers.Meta | modifiers.Shift) === key)
return true;
} else {
if (UI.KeyboardShortcut.makeKey('z', modifiers.Ctrl) === key)
return true;
if (UI.KeyboardShortcut.makeKey('y', modifiers.Ctrl) === key)
return true;
if (!Host.isWin() && UI.KeyboardShortcut.makeKey('z', modifiers.Ctrl | modifiers.Shift) === key)
return true;
}
if ((keyModifiers & (modifiers.Ctrl | modifiers.Alt)) === (modifiers.Ctrl | modifiers.Alt))
return Host.isWin();
return !hasModifier(modifiers.Ctrl) && !hasModifier(modifiers.Alt) && !hasModifier(modifiers.Meta);
}
/**
* @param {number} mod
* @return {boolean}
*/
function hasModifier(mod) {
return !!(keyModifiers & mod);
}
}
/**
* @param {string} actionId
* @param {string} shortcut
*/
registerShortcut(actionId, shortcut) {
const descriptor = UI.KeyboardShortcut.makeDescriptorFromBindingShortcut(shortcut);
if (!descriptor)
return;
this._defaultActionToShortcut.set(actionId, descriptor);
this._defaultKeyToActions.set(String(descriptor.key), actionId);
}
dismissPendingShortcutAction() {
if (this._pendingActionTimer) {
clearTimeout(this._pendingActionTimer);
delete this._pendingActionTimer;
}
}
/**
* @param {!Document} document
*/
_registerBindings(document) {
document.addEventListener('input', this.dismissPendingShortcutAction.bind(this), true);
const extensions = self.runtime.extensions('action');
extensions.forEach(registerExtension, this);
/**
* @param {!Runtime.Extension} extension
* @this {UI.ShortcutRegistry}
*/
function registerExtension(extension) {
const descriptor = extension.descriptor();
const bindings = descriptor['bindings'];
for (let i = 0; bindings && i < bindings.length; ++i) {
if (!platformMatches(bindings[i].platform))
continue;
const shortcuts = bindings[i]['shortcut'].split(/\s+/);
shortcuts.forEach(this.registerShortcut.bind(this, descriptor['actionId']));
}
}
/**
* @param {string=} platformsString
* @return {boolean}
*/
function platformMatches(platformsString) {
if (!platformsString)
return true;
const platforms = platformsString.split(',');
let isMatch = false;
const currentPlatform = Host.platform();
for (let i = 0; !isMatch && i < platforms.length; ++i)
isMatch = platforms[i] === currentPlatform;
return isMatch;
}
}
};
/**
* @unrestricted
*/
UI.ShortcutRegistry.ForwardedShortcut = class {};
UI.ShortcutRegistry.ForwardedShortcut.instance = new UI.ShortcutRegistry.ForwardedShortcut();
/** @type {!UI.ShortcutRegistry} */
UI.shortcutRegistry;