diff --git a/ext/bg/css/settings2.css b/ext/bg/css/settings2.css index 6ae9e335..b41ea7ea 100644 --- a/ext/bg/css/settings2.css +++ b/ext/bg/css/settings2.css @@ -1931,6 +1931,94 @@ input.sentence-termination-character-input2 { margin-top: 0.5em; } +.hotkey-list { + margin: 0 calc(var(--modal-padding-horizontal) * -1); +} +.hotkey-list-item { + margin: 0.5em 0; +} +.hotkey-list-item+.hotkey-list-item { + border-top: var(--thin-border-size) solid var(--separator-color2); +} +.hotkey-list-item-grid { + display: grid; + grid-template-columns: auto auto 1fr auto; + grid-template-rows: auto; + grid-template-areas: + 'index input-label input button' + '. action-label action .'; + width: 100%; + column-gap: 0.25em; + row-gap: 0.25em; + margin: 0.5em 0; + padding: 0 var(--modal-padding-horizontal); + box-sizing: border-box; +} +.hotkey-list-item-index-cell { + grid-area: index; + align-self: center; + text-align: center; + width: 2em; +} +.hotkey-list-item-button-cell { + grid-area: button; + align-self: center; +} +.hotkey-list-item-input-label-cell { + grid-area: input-label; + align-self: center; +} +.hotkey-list-item-input-cell { + grid-area: input; + display: flex; + flex-flow: row nowrap; + width: 100%; + align-items: stretch; + align-self: center; +} +.hotkey-list-item-input { + flex: 1 1 auto; +} +.hotkey-list-item-action-label-cell { + grid-area: action-label; + align-self: center; +} +.hotkey-list-item-action-cell { + grid-area: action; + align-self: center; + display: flex; + flex-flow: row nowrap; + width: 100%; + align-items: center; +} +.hotkey-list-item-action { + flex: 1 1 auto; +} +.hotkey-list-item-enabled-label { + align-self: center; + margin-left: 1em; +} +.hotkey-list-item-flex-row { + display: flex; + flex-flow: row nowrap; + align-items: center; +} +.hotkey-list-item-flex-row-label { + margin: 0 0.5em 0 1em; +} +.hotkey-scope-checkbox-container { + display: flex; + flex-flow: row nowrap; + align-items: center; + cursor: pointer; +} +.hotkey-scope-checkbox-container:not(:last-child) { + margin-right: 0.75em; +} +.hotkey-scope-checkbox-container>span { + padding-left: 0.375em; +} + /* Generic layouts */ .margin-above { diff --git a/ext/bg/data/options-schema.json b/ext/bg/data/options-schema.json index 5d23df02..44bff10a 100644 --- a/ext/bg/data/options-schema.json +++ b/ext/bg/data/options-schema.json @@ -69,7 +69,8 @@ "dictionaries", "parsing", "anki", - "sentenceParsing" + "sentenceParsing", + "inputs" ], "properties": { "general": { @@ -914,6 +915,75 @@ ] } } + }, + "inputs": { + "type": "object", + "required": [ + "hotkeys" + ], + "properties": { + "hotkeys": { + "type": "array", + "items": { + "type": "object", + "required": [ + "action", + "key", + "modifiers", + "scopes", + "enabled" + ], + "properties": { + "action": { + "type": "string", + "default": "" + }, + "key": { + "type": ["string", "null"], + "default": null + }, + "modifiers": { + "type": "array", + "items": { + "type": "string", + "enum": ["alt", "ctrl", "shift", "meta"], + "default": "alt" + } + }, + "scopes": { + "type": "array", + "items": { + "type": "string", + "enum": ["popup", "search"], + "default": "popup" + }, + "default": ["popup", "search"] + }, + "enabled": { + "type": "boolean", + "default": true + } + } + }, + "default": [ + {"action": "close", "key": "Escape", "modifiers": [], "scopes": ["popup", "search"], "enabled": true}, + {"action": "previousEntry3", "key": "PageUp", "modifiers": ["alt"], "scopes": ["popup", "search"], "enabled": true}, + {"action": "nextEntry3", "key": "PageDown", "modifiers": ["alt"], "scopes": ["popup", "search"], "enabled": true}, + {"action": "lastEntry", "key": "End", "modifiers": ["alt"], "scopes": ["popup", "search"], "enabled": true}, + {"action": "firstEntry", "key": "Home", "modifiers": ["alt"], "scopes": ["popup", "search"], "enabled": true}, + {"action": "previousEntry", "key": "ArrowUp", "modifiers": ["alt"], "scopes": ["popup", "search"], "enabled": true}, + {"action": "nextEntry", "key": "ArrowDown", "modifiers": ["alt"], "scopes": ["popup", "search"], "enabled": true}, + {"action": "historyBackward", "key": "KeyB", "modifiers": ["alt"], "scopes": ["popup", "search"], "enabled": true}, + {"action": "historyForward", "key": "KeyF", "modifiers": ["alt"], "scopes": ["popup", "search"], "enabled": true}, + {"action": "addNoteKanji", "key": "KeyK", "modifiers": ["alt"], "scopes": ["popup", "search"], "enabled": true}, + {"action": "addNoteTermKanji", "key": "KeyE", "modifiers": ["alt"], "scopes": ["popup", "search"], "enabled": true}, + {"action": "addNoteTermKana", "key": "KeyR", "modifiers": ["alt"], "scopes": ["popup", "search"], "enabled": true}, + {"action": "playAudio", "key": "KeyP", "modifiers": ["alt"], "scopes": ["popup", "search"], "enabled": true}, + {"action": "viewNote", "key": "KeyV", "modifiers": ["alt"], "scopes": ["popup", "search"], "enabled": true}, + {"action": "copyHostSelection", "key": "KeyC", "modifiers": ["ctrl"], "scopes": ["popup", "search"], "enabled": true} + ] + } + } } } } diff --git a/ext/bg/js/options.js b/ext/bg/js/options.js index 86f76698..0d3e42a1 100644 --- a/ext/bg/js/options.js +++ b/ext/bg/js/options.js @@ -712,6 +712,25 @@ class OptionsUtil { }; delete profile.options.anki.sentenceExt; profile.options.general.popupActionBarLocation = 'top'; + profile.options.inputs = { + hotkeys: [ + {action: 'close', key: 'Escape', modifiers: [], scopes: ['popup', 'search'], enabled: true}, + {action: 'previousEntry3', key: 'PageUp', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true}, + {action: 'nextEntry3', key: 'PageDown', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true}, + {action: 'lastEntry', key: 'End', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true}, + {action: 'firstEntry', key: 'Home', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true}, + {action: 'previousEntry', key: 'ArrowUp', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true}, + {action: 'nextEntry', key: 'ArrowDown', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true}, + {action: 'historyBackward', key: 'KeyB', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true}, + {action: 'historyForward', key: 'KeyF', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true}, + {action: 'addNoteKanji', key: 'KeyK', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true}, + {action: 'addNoteTermKanji', key: 'KeyE', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true}, + {action: 'addNoteTermKana', key: 'KeyR', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true}, + {action: 'playAudio', key: 'KeyP', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true}, + {action: 'viewNote', key: 'KeyV', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true}, + {action: 'copyHostSelection', key: 'KeyC', modifiers: ['ctrl'], scopes: ['popup', 'search'], enabled: true} + ] + }; } return options; } diff --git a/ext/bg/js/settings/keyboard-mouse-input-field.js b/ext/bg/js/settings/keyboard-mouse-input-field.js index 5e7a2f2e..d48b130f 100644 --- a/ext/bg/js/settings/keyboard-mouse-input-field.js +++ b/ext/bg/js/settings/keyboard-mouse-input-field.js @@ -49,11 +49,9 @@ class KeyboardMouseInputField extends EventDispatcher { prepare(key, modifiers, mouseModifiersSupported=false, keySupported=false) { this.cleanup(); - this._key = key; - this._modifiers = this._sortModifiers(modifiers); this._mouseModifiersSupported = mouseModifiersSupported; this._keySupported = keySupported; - this._updateDisplayString(); + this.setInput(key, modifiers); const events = [ [this._inputNode, 'keydown', this._onModifierKeyDown.bind(this), false] ]; @@ -73,6 +71,12 @@ class KeyboardMouseInputField extends EventDispatcher { } } + setInput(key, modifiers) { + this._key = key; + this._modifiers = this._sortModifiers(modifiers); + this._updateDisplayString(); + } + cleanup() { this._eventListeners.removeAllEventListeners(); this._modifiers = []; @@ -131,11 +135,20 @@ class KeyboardMouseInputField extends EventDispatcher { } if (this._key !== null) { if (!first) { displayValue += this._keySeparator; } - displayValue += this._key; + displayValue += this._getDisplayKey(this._key); } this._inputNode.value = displayValue; } + _getDisplayKey(key) { + if (typeof key === 'string') { + if (key.length === 4 && key.startsWith('Key')) { + key = key.substring(3); + } + } + return key; + } + _getModifierName(modifier) { const pattern = this._mouseInputNamePattern; const match = pattern.exec(modifier); diff --git a/ext/bg/js/settings2/keyboard-shortcuts-controller.js b/ext/bg/js/settings2/keyboard-shortcuts-controller.js new file mode 100644 index 00000000..83b457c8 --- /dev/null +++ b/ext/bg/js/settings2/keyboard-shortcuts-controller.js @@ -0,0 +1,294 @@ +/* + * Copyright (C) 2021 Yomichan Authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/* global + * KeyboardMouseInputField + * api + */ + +class KeyboardShortcutController { + constructor(settingsController) { + this._settingsController = settingsController; + this._entries = []; + this._os = null; + this._addButton = null; + this._resetButton = null; + this._listContainer = null; + this._emptyIndicator = null; + this._stringComparer = new Intl.Collator('en-US'); // Invariant locale + } + + get settingsController() { + return this._settingsController; + } + + async prepare() { + const {platform: {os}} = await api.getEnvironmentInfo(); + this._os = os; + + this._addButton = document.querySelector('#hotkey-list-add'); + this._resetButton = document.querySelector('#hotkey-list-reset'); + this._listContainer = document.querySelector('#hotkey-list'); + this._emptyIndicator = document.querySelector('#hotkey-list-empty'); + + this._addButton.addEventListener('click', this._onAddClick.bind(this)); + this._resetButton.addEventListener('click', this._onResetClick.bind(this)); + this._settingsController.on('optionsChanged', this._onOptionsChanged.bind(this)); + + await this._updateOptions(); + } + + async addEntry(terminationCharacterEntry) { + const options = await this._settingsController.getOptions(); + const {inputs: {hotkeys}} = options; + + await this._settingsController.modifyProfileSettings([{ + action: 'splice', + path: 'inputs.hotkeys', + start: hotkeys.length, + deleteCount: 0, + items: [terminationCharacterEntry] + }]); + + await this._updateOptions(); + } + + async deleteEntry(index) { + const options = await this._settingsController.getOptions(); + const {inputs: {hotkeys}} = options; + + if (index < 0 || index >= hotkeys.length) { return false; } + + await this._settingsController.modifyProfileSettings([{ + action: 'splice', + path: 'inputs.hotkeys', + start: index, + deleteCount: 1, + items: [] + }]); + + await this._updateOptions(); + return true; + } + + async modifyProfileSettings(targets) { + return await this._settingsController.modifyProfileSettings(targets); + } + + async getDefaultHotkeys() { + const defaultOptions = await this._settingsController.getDefaultOptions(); + return defaultOptions.profiles[0].options.inputs.hotkeys; + } + + // Private + + _onOptionsChanged({options}) { + for (const entry of this._entries) { + entry.cleanup(); + } + + this._entries = []; + const {inputs: {hotkeys}} = options; + const fragment = document.createDocumentFragment(); + + for (let i = 0, ii = hotkeys.length; i < ii; ++i) { + const hotkeyEntry = hotkeys[i]; + const node = this._settingsController.instantiateTemplate('hotkey-list-item'); + fragment.appendChild(node); + const entry = new KeyboardShortcutHotkeyEntry(this, hotkeyEntry, i, node, this._os, this._stringComparer); + this._entries.push(entry); + entry.prepare(); + } + + this._listContainer.appendChild(fragment); + this._listContainer.hidden = (hotkeys.length === 0); + this._emptyIndicator.hidden = (hotkeys.length !== 0); + } + + _onAddClick(e) { + e.preventDefault(); + this._addNewEntry(); + } + + _onResetClick(e) { + e.preventDefault(); + this._reset(); + } + + async _addNewEntry() { + const newEntry = { + action: '', + key: null, + modifiers: [], + scopes: ['popup', 'search'], + enabled: true + }; + return await this.addEntry(newEntry); + } + + async _updateOptions() { + const options = await this._settingsController.getOptions(); + this._onOptionsChanged({options}); + } + + async _reset() { + const value = await this.getDefaultHotkeys(); + await this._settingsController.setProfileSetting('inputs.hotkeys', value); + await this._updateOptions(); + } +} + +class KeyboardShortcutHotkeyEntry { + constructor(parent, data, index, node, os, stringComparer) { + this._parent = parent; + this._data = data; + this._index = index; + this._node = node; + this._os = os; + this._eventListeners = new EventListenerCollection(); + this._inputField = null; + this._basePath = `inputs.hotkeys[${this._index}]`; + this._stringComparer = stringComparer; + } + + prepare() { + const node = this._node; + + const menuButton = node.querySelector('.hotkey-list-item-button'); + const input = node.querySelector('.hotkey-list-item-input'); + const action = node.querySelector('.hotkey-list-item-action'); + const scopeCheckboxes = node.querySelectorAll('.hotkey-scope-checkbox'); + const enabledToggle = node.querySelector('.hotkey-list-item-enabled'); + + this._inputField = new KeyboardMouseInputField(input, null, this._os); + this._inputField.prepare(this._data.key, this._data.modifiers, false, true); + + action.value = this._data.action; + action.dataset.setting = `${this._basePath}.action`; + + enabledToggle.checked = this._data.enabled; + enabledToggle.dataset.setting = `${this._basePath}.enabled`; + + const scopes = this._data.scopes; + for (const scopeCheckbox of scopeCheckboxes) { + scopeCheckbox.checked = scopes.includes(scopeCheckbox.dataset.type); + this._eventListeners.addEventListener(scopeCheckbox, 'change', this._onScopeCheckboxChange.bind(this), false); + } + + this._eventListeners.addEventListener(menuButton, 'menuClosed', this._onMenuClosed.bind(this), false); + this._eventListeners.on(this._inputField, 'change', this._onInputFieldChange.bind(this)); + } + + cleanup() { + this._eventListeners.removeAllEventListeners(); + this._inputField.cleanup(); + if (this._node.parentNode !== null) { + this._node.parentNode.removeChild(this._node); + } + } + + // Private + + _onMenuClosed(e) { + const {detail: {action}} = e; + switch (action) { + case 'delete': + this._delete(); + break; + case 'clearInputs': + this._inputField.clearInputs(); + break; + case 'resetInput': + this._resetInput(); + break; + } + } + + _onInputFieldChange({key, modifiers}) { + this._setKeyAndModifiers(key, modifiers); + } + + _onScopeCheckboxChange(e) { + const node = e.currentTarget; + const {type} = node.dataset; + if (typeof type !== 'string') { return; } + this._setScopeEnabled(type, node.checked); + } + + async _delete() { + this._parent.deleteEntry(this._index); + } + + async _setKeyAndModifiers(key, modifiers) { + this._data.key = key; + this._data.modifiers = modifiers; + await this._modifyProfileSettings([ + { + action: 'set', + path: `${this._basePath}.key`, + value: key + }, + { + action: 'set', + path: `${this._basePath}.modifiers`, + value: modifiers + } + ]); + } + + async _setScopeEnabled(scope, enabled) { + const scopes = this._data.scopes; + const index = scopes.indexOf(scope); + if ((index >= 0) === enabled) { return; } + + if (enabled) { + scopes.push(scope); + const stringComparer = this._stringComparer; + scopes.sort((scope1, scope2) => stringComparer.compare(scope1, scope2)); + } else { + scopes.splice(index, 1); + } + + await this._modifyProfileSettings([{ + action: 'set', + path: `${this._basePath}.scopes`, + value: scopes + }]); + } + + async _modifyProfileSettings(targets) { + return await this._parent.settingsController.modifyProfileSettings(targets); + } + + async _resetInput() { + const defaultHotkeys = await this._parent.getDefaultHotkeys(); + const defaultValue = this._getDefaultKeyAndModifiers(defaultHotkeys, this._data.action); + if (defaultValue === null) { return; } + + const {key, modifiers} = defaultValue; + await this._setKeyAndModifiers(key, modifiers); + this._inputField.setInput(key, modifiers); + } + + _getDefaultKeyAndModifiers(defaultHotkeys, action) { + for (const {action: action2, key, modifiers} of defaultHotkeys) { + if (action2 !== action) { continue; } + return {modifiers, key}; + } + return null; + } +} diff --git a/ext/bg/js/settings2/settings-main.js b/ext/bg/js/settings2/settings-main.js index fc003ac8..f2852ab1 100644 --- a/ext/bg/js/settings2/settings-main.js +++ b/ext/bg/js/settings2/settings-main.js @@ -25,6 +25,7 @@ * DictionaryImportController * DocumentFocusController * GenericSettingController + * KeyboardShortcutController * ModalController * NestedPopupsController * PopupPreviewController @@ -128,6 +129,9 @@ async function setupGenericSettingsController(genericSettingController) { const sentenceTerminationCharactersController = new SentenceTerminationCharactersController(settingsController); sentenceTerminationCharactersController.prepare(); + const keyboardShortcutController = new KeyboardShortcutController(settingsController); + keyboardShortcutController.prepare(); + await Promise.all(preparePromises); document.documentElement.dataset.loaded = 'true'; diff --git a/ext/bg/settings2.html b/ext/bg/settings2.html index 28db236b..181739d5 100644 --- a/ext/bg/settings2.html +++ b/ext/bg/settings2.html @@ -1482,6 +1482,14 @@ +
+
+
Configure keyboard shortcuts…
+
+
+ +
+
@@ -2807,6 +2815,88 @@ + + + + + + + + + @@ -2863,6 +2953,7 @@ + diff --git a/ext/mixed/js/display.js b/ext/mixed/js/display.js index e361a3a1..45019039 100644 --- a/ext/mixed/js/display.js +++ b/ext/mixed/js/display.js @@ -132,23 +132,6 @@ class Display extends EventDispatcher { ['playAudio', () => { this._playAudioCurrent(); }], ['copyHostSelection', () => this._copyHostSelection()] ]); - this.registerHotkeys([ - {key: 'Escape', modifiers: [], action: 'close'}, - {key: 'PageUp', modifiers: ['alt'], action: 'previousEntry3'}, - {key: 'PageDown', modifiers: ['alt'], action: 'nextEntry3'}, - {key: 'End', modifiers: ['alt'], action: 'lastEntry'}, - {key: 'Home', modifiers: ['alt'], action: 'firstEntry'}, - {key: 'ArrowUp', modifiers: ['alt'], action: 'previousEntry'}, - {key: 'ArrowDown', modifiers: ['alt'], action: 'nextEntry'}, - {key: 'KeyB', modifiers: ['alt'], action: 'historyBackward'}, - {key: 'KeyF', modifiers: ['alt'], action: 'historyForward'}, - {key: 'KeyK', modifiers: ['alt'], action: 'addNoteKanji'}, - {key: 'KeyE', modifiers: ['alt'], action: 'addNoteTermKanji'}, - {key: 'KeyR', modifiers: ['alt'], action: 'addNoteTermKana'}, - {key: 'KeyP', modifiers: ['alt'], action: 'playAudio'}, - {key: 'KeyV', modifiers: ['alt'], action: 'viewNote'}, - {key: 'KeyC', modifiers: ['ctrl'], action: 'copyHostSelection'} - ]); this.registerMessageHandlers([ ['setMode', {async: false, handler: this._onMessageSetMode.bind(this)}] ]); @@ -313,6 +296,7 @@ class Display extends EventDispatcher { this._options = options; this._ankiFieldTemplates = templates; + this._updateHotkeys(options); this._updateDocumentOptions(options); this._updateTheme(options.general.popupTheme); this.setCustomCss(options.general.customPopupCss); @@ -401,17 +385,6 @@ class Display extends EventDispatcher { } } - registerHotkeys(hotkeys) { - for (const {key, modifiers, action} of hotkeys) { - let handlers = this._hotkeys.get(key); - if (typeof handlers === 'undefined') { - handlers = []; - this._hotkeys.set(key, handlers); - } - handlers.push({modifiers: new Set(modifiers), action}); - } - } - registerMessageHandlers(handlers) { for (const [name, handlerInfo] of handlers) { this._messageHandlers.set(name, handlerInfo); @@ -1902,4 +1875,25 @@ class Display extends EventDispatcher { height = Math.max(Math.max(0, handleSize.height), height); await this._invokeOwner('setFrameSize', {width, height}); } + + _registerHotkey(key, modifiers, action) { + if (!this._actions.has(action)) { return false; } + + let handlers = this._hotkeys.get(key); + if (typeof handlers === 'undefined') { + handlers = []; + this._hotkeys.set(key, handlers); + } + handlers.push({modifiers: new Set(modifiers), action}); + return true; + } + + _updateHotkeys(options) { + this._hotkeys.clear(); + + for (const {action, key, modifiers, scopes, enabled} of options.inputs.hotkeys) { + if (!enabled || !scopes.includes(this._pageType)) { continue; } + this._registerHotkey(key, modifiers, action); + } + } } diff --git a/test/test-options-util.js b/test/test-options-util.js index c4a6addd..beef6f80 100644 --- a/test/test-options-util.js +++ b/test/test-options-util.js @@ -436,6 +436,25 @@ function createProfileOptionsUpdatedTestData1() { {enabled: true, character1: '?', character2: null, includeCharacterAtStart: false, includeCharacterAtEnd: true}, {enabled: true, character1: '…', character2: null, includeCharacterAtStart: false, includeCharacterAtEnd: true} ] + }, + inputs: { + hotkeys: [ + {action: 'close', key: 'Escape', modifiers: [], scopes: ['popup', 'search'], enabled: true}, + {action: 'previousEntry3', key: 'PageUp', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true}, + {action: 'nextEntry3', key: 'PageDown', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true}, + {action: 'lastEntry', key: 'End', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true}, + {action: 'firstEntry', key: 'Home', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true}, + {action: 'previousEntry', key: 'ArrowUp', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true}, + {action: 'nextEntry', key: 'ArrowDown', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true}, + {action: 'historyBackward', key: 'KeyB', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true}, + {action: 'historyForward', key: 'KeyF', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true}, + {action: 'addNoteKanji', key: 'KeyK', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true}, + {action: 'addNoteTermKanji', key: 'KeyE', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true}, + {action: 'addNoteTermKana', key: 'KeyR', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true}, + {action: 'playAudio', key: 'KeyP', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true}, + {action: 'viewNote', key: 'KeyV', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true}, + {action: 'copyHostSelection', key: 'KeyC', modifiers: ['ctrl'], scopes: ['popup', 'search'], enabled: true} + ] } }; }