diff options
| -rw-r--r-- | ext/bg/css/settings2.css | 88 | ||||
| -rw-r--r-- | ext/bg/data/options-schema.json | 72 | ||||
| -rw-r--r-- | ext/bg/js/options.js | 19 | ||||
| -rw-r--r-- | ext/bg/js/settings/keyboard-mouse-input-field.js | 21 | ||||
| -rw-r--r-- | ext/bg/js/settings2/keyboard-shortcuts-controller.js | 294 | ||||
| -rw-r--r-- | ext/bg/js/settings2/settings-main.js | 4 | ||||
| -rw-r--r-- | ext/bg/settings2.html | 91 | ||||
| -rw-r--r-- | ext/mixed/js/display.js | 50 | ||||
| -rw-r--r-- | test/test-options-util.js | 19 | 
9 files changed, 625 insertions, 33 deletions
| 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 <https://www.gnu.org/licenses/>. + */ + +/* 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 @@                  </div>              </div>          </div></div> +        <div class="settings-item settings-item-button" data-modal-action="show,keyboard-shortcuts"><div class="settings-item-inner"> +            <div class="settings-item-left"> +                <div class="settings-item-label">Configure keyboard shortcuts…</div> +            </div> +            <div class="settings-item-right open-panel-button-container"> +                <button class="icon-button"><span class="icon-button-inner"><span class="icon" data-icon="material-right-arrow"></span></span></button> +            </div> +        </div></div>      </div>      <!-- Backup --> @@ -2807,6 +2815,88 @@  </div></div></template> +<!-- Keyboard shortcuts modal --> +<div id="keyboard-shortcuts" class="modal-container" tabindex="-1" role="dialog" hidden><div class="modal-content modal-content-full"> +    <div class="modal-header"> +        <div class="modal-title">Keyboard Shortcuts</div> +        <div class="modal-header-button-container"> +            <div class="modal-header-button-group"> +                <button class="icon-button modal-header-button" data-modal-action="expand"><span class="icon-button-inner"><span class="icon" data-icon="expand"></span></span></button> +                <button class="icon-button modal-header-button" data-modal-action="collapse"><span class="icon-button-inner"><span class="icon" data-icon="collapse"></span></span></button> +            </div> +        </div> +    </div> +    <div class="modal-body"> +        <div class="hotkey-list generic-list" id="hotkey-list"></div> +        <div class="hotkey-list-empty" id="hotkey-list-empty" hidden> +            No keyboard shortcuts defined. +        </div> +    </div> +    <div class="modal-footer"> +        <button class="low-emphasis danger" id="hotkey-list-reset">Reset</button> +        <button class="low-emphasis" id="hotkey-list-add">Add</button> +        <button data-modal-action="hide">Close</button> +    </div> +</div></div> + +<!-- Keyboard shortcuts templates --> +<template id="hotkey-list-item-template"><div class="hotkey-list-item"><div class="hotkey-list-item-grid"> +    <div class="hotkey-list-item-index-cell generic-list-index-prefix"></div> +    <div class="hotkey-list-item-button-cell"> +        <div class="input-height-icon-button-container"> +            <button class="icon-button hotkey-list-item-button" data-menu="hotkey-list-item-menu" data-menu-position="below,left"><span class="icon-button-inner"><span class="icon" data-icon="kebab-menu"></span></span></button> +        </div> +    </div> + +    <div class="hotkey-list-item-input-label-cell">Input:</div> +    <div class="hotkey-list-item-input-cell"> +        <input type="text" class="hotkey-list-item-input" autocomplete="off" spellcheck="false" placeholder="No input" data-property="include"> +        <label class="toggle hotkey-list-item-enabled-label"><input type="checkbox" class="hotkey-list-item-enabled"><span class="toggle-body"><span class="toggle-track"></span><span class="toggle-knob"></span></span></label> +    </div> + +    <div class="hotkey-list-item-action-label-cell">Action:</div> +    <div class="hotkey-list-item-action-cell"> +        <select class="hotkey-list-item-action"> +            <option value="">None</option> +            <option value="close">Close popup</option> +            <option value="nextEntry">Go to next entry</option> +            <option value="nextEntry3">Go to next entry (x3)</option> +            <option value="previousEntry">Go to previous entry</option> +            <option value="previousEntry3">Go to previous entry (x3)</option> +            <option value="lastEntry">Go to last entry</option> +            <option value="firstEntry">Go to first entry</option> +            <option value="historyBackward">Navigate backward in history</option> +            <option value="historyForward">Navigate forward in history</option> +            <option value="addNoteKanji">Add kanji note</option> +            <option value="addNoteTermKanji">Add term note</option> +            <option value="addNoteTermKana">Add term note (reading)</option> +            <option value="viewNote">View note</option> +            <option value="playAudio">Play audio</option> +            <option value="copyHostSelection">Copy selection</option> +        </select> +        <div class="hotkey-list-item-flex-row"> +            <div class="hotkey-list-item-flex-row-label">Scopes:</div> +            <div class="hotkey-list-item-flex-row"> +                <label class="hotkey-scope-checkbox-container"> +                    <label class="checkbox"><input type="checkbox" class="hotkey-scope-checkbox" data-type="popup"><span class="checkbox-body"><span class="checkbox-fill"></span><span class="checkbox-border"></span><span class="checkbox-check"></span></span></label> +                    <span>Popup</span> +                </label> +                <label class="hotkey-scope-checkbox-container"> +                    <label class="checkbox"><input type="checkbox" class="hotkey-scope-checkbox" data-type="search"><span class="checkbox-body"><span class="checkbox-fill"></span><span class="checkbox-border"></span><span class="checkbox-check"></span></span></label> +                    <span>Search</span> +                </label> +            </div> +        </div> +    </div> +</div></div></template> + +<template id="hotkey-list-item-menu-template"><div class="popup-menu-container" tabindex="-1" role="dialog"><div class="popup-menu"> +    <button class="popup-menu-item" data-menu-action="clearInputs">Clear input</button> +    <button class="popup-menu-item" data-menu-action="resetInput">Reset input</button> +    <button class="popup-menu-item" data-menu-action="delete">Delete</button> +</div></div></template> + +  <!-- Scripts -->  <script src="/mixed/lib/jszip.min.js"></script>  <script src="/mixed/lib/wanakana.min.js"></script> @@ -2863,6 +2953,7 @@  <script src="/bg/js/settings/status-footer.js"></script>  <script src="/bg/js/settings/storage-controller.js"></script> +<script src="/bg/js/settings2/keyboard-shortcuts-controller.js"></script>  <script src="/bg/js/settings2/nested-popups-controller.js"></script>  <script src="/bg/js/settings2/secondary-search-dictionary-controller.js"></script>  <script src="/bg/js/settings2/sentence-termination-characters-controller.js"></script> 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} +            ]          }      };  } |