From 14b4aee07dc3f69ad75518cf04cafe416e06f0c7 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sun, 17 Jan 2021 16:55:45 -0500 Subject: [PATCH] Hotkey forwarding support (#1263) * Add support for allowing HotkeyHandler to forward hotkeys * Update hotkey registration * Pass HotkeyHandler instance into Display* constructor * Implement hotkey forwarding --- ext/bg/js/search-main.js | 7 +- ext/bg/js/search.js | 4 +- ext/fg/js/float-main.js | 7 +- ext/mixed/js/display.js | 14 +-- ext/mixed/js/hotkey-handler.js | 166 +++++++++++++++++++++++---------- 5 files changed, 138 insertions(+), 60 deletions(-) diff --git a/ext/bg/js/search-main.js b/ext/bg/js/search-main.js index 73bc6750..38c6f4dd 100644 --- a/ext/bg/js/search-main.js +++ b/ext/bg/js/search-main.js @@ -18,6 +18,7 @@ /* global * DisplaySearch * DocumentFocusController + * HotkeyHandler * JapaneseUtil * api * wanakana @@ -32,7 +33,11 @@ await yomichan.backendReady(); const japaneseUtil = new JapaneseUtil(wanakana); - const displaySearch = new DisplaySearch(japaneseUtil, documentFocusController); + + const hotkeyHandler = new HotkeyHandler(); + hotkeyHandler.prepare(); + + const displaySearch = new DisplaySearch(japaneseUtil, documentFocusController, hotkeyHandler); await displaySearch.prepare(); document.documentElement.dataset.loaded = 'true'; diff --git a/ext/bg/js/search.js b/ext/bg/js/search.js index 5963c03b..e2497264 100644 --- a/ext/bg/js/search.js +++ b/ext/bg/js/search.js @@ -23,8 +23,8 @@ */ class DisplaySearch extends Display { - constructor(japaneseUtil, documentFocusController) { - super('search', japaneseUtil, documentFocusController); + constructor(japaneseUtil, documentFocusController, hotkeyHandler) { + super('search', japaneseUtil, documentFocusController, hotkeyHandler); this._searchButton = document.querySelector('#search-button'); this._queryInput = document.querySelector('#search-textbox'); this._introElement = document.querySelector('#intro'); diff --git a/ext/fg/js/float-main.js b/ext/fg/js/float-main.js index f220385e..2b81a08a 100644 --- a/ext/fg/js/float-main.js +++ b/ext/fg/js/float-main.js @@ -19,6 +19,7 @@ * Display * DisplayProfileSelection * DocumentFocusController + * HotkeyHandler * JapaneseUtil * api */ @@ -32,7 +33,11 @@ await yomichan.backendReady(); const japaneseUtil = new JapaneseUtil(null); - const display = new Display('popup', japaneseUtil, documentFocusController); + + const hotkeyHandler = new HotkeyHandler(); + hotkeyHandler.prepare(); + + const display = new Display('popup', japaneseUtil, documentFocusController, hotkeyHandler); await display.prepare(); const displayProfileSelection = new DisplayProfileSelection(display); displayProfileSelection.prepare(); diff --git a/ext/mixed/js/display.js b/ext/mixed/js/display.js index b8baccf9..c3e118c6 100644 --- a/ext/mixed/js/display.js +++ b/ext/mixed/js/display.js @@ -24,7 +24,6 @@ * DocumentUtil * FrameEndpoint * Frontend - * HotkeyHandler * MediaLoader * PopupFactory * QueryParser @@ -35,11 +34,12 @@ */ class Display extends EventDispatcher { - constructor(pageType, japaneseUtil, documentFocusController) { + constructor(pageType, japaneseUtil, documentFocusController, hotkeyHandler) { super(); this._pageType = pageType; this._japaneseUtil = japaneseUtil; this._documentFocusController = documentFocusController; + this._hotkeyHandler = hotkeyHandler; this._container = document.querySelector('#definitions'); this._definitions = []; this._optionsContext = {depth: 0, url: window.location.href}; @@ -60,7 +60,6 @@ class Display extends EventDispatcher { japaneseUtil, mediaLoader: this._mediaLoader }); - this._hotkeyHandler = new HotkeyHandler(this._pageType); this._messageHandlers = new Map(); this._directMessageHandlers = new Map(); this._windowMessageHandlers = new Map(); @@ -199,7 +198,6 @@ class Display extends EventDispatcher { this._audioSystem.prepare(); this._queryParser.prepare(); this._history.prepare(); - this._hotkeyHandler.prepare(); // Event setup this._history.on('stateChanged', this._onStateChanged.bind(this)); @@ -498,6 +496,9 @@ class Display extends EventDispatcher { this._parentPopupId = parentPopupId; this._parentFrameId = parentFrameId; this._ownerFrameId = ownerFrameId; + if (this._pageType === 'popup') { + this._hotkeyHandler.forwardFrameId = ownerFrameId; + } this._childrenSupported = childrenSupported; this._setContentScale(scale); await this.setOptionsContext(optionsContext); @@ -1879,8 +1880,9 @@ class Display extends EventDispatcher { } _updateHotkeys(options) { - this._hotkeyHandler.clearHotkeys(); - this._hotkeyHandler.registerHotkeys(options.inputs.hotkeys); + const scope = this._pageType; + this._hotkeyHandler.clearHotkeys(scope); + this._hotkeyHandler.registerHotkeys(scope, options.inputs.hotkeys); } async _closeTab() { diff --git a/ext/mixed/js/hotkey-handler.js b/ext/mixed/js/hotkey-handler.js index 0a9ec81f..81ec950e 100644 --- a/ext/mixed/js/hotkey-handler.js +++ b/ext/mixed/js/hotkey-handler.js @@ -17,6 +17,7 @@ /* global * DocumentUtil + * api */ /** @@ -25,30 +26,31 @@ class HotkeyHandler extends EventDispatcher { /** * Creates a new instance of the class. - * @param scope The scope required for hotkey definitions. */ - constructor(scope) { + constructor() { super(); - this._scope = scope; - this._hotkeys = new Map(); this._actions = new Map(); + this._hotkeys = new Map(); + this._hotkeyRegistrations = new Map(); this._eventListeners = new EventListenerCollection(); this._isPrepared = false; this._hasEventListeners = false; + this._forwardFrameId = null; } /** - * Gets the scope required for the hotkey definitions. + * Gets the frame ID used for forwarding hotkeys. */ - get scope() { - return this._scope; + get forwardFrameId() { + return this._forwardFrameId; } /** - * Sets the scope required for the hotkey definitions. + * Sets the frame ID used for forwarding hotkeys. */ - set scope(value) { - this._scope = value; + set forwardFrameId(value) { + this._forwardFrameId = value; + this._updateHotkeyRegistrations(); } /** @@ -57,14 +59,9 @@ class HotkeyHandler extends EventDispatcher { prepare() { this._isPrepared = true; this._updateEventHandlers(); - } - - /** - * Stops listening to key press events. - */ - cleanup() { - this._isPrepared = false; - this._updateEventHandlers(); + api.crossFrame.registerHandlers([ + ['hotkeyHandler.forwardHotkey', {async: false, handler: this._onMessageForwardHotkey.bind(this)}] + ]); } /** @@ -78,7 +75,8 @@ class HotkeyHandler extends EventDispatcher { } /** - * Registers a set of hotkeys + * Registers a set of hotkeys for a given scope. + * @param scope The scope that the hotkey definitions must be for in order to be activated. * @param hotkeys An array of hotkey definitions of the format `{action, key, modifiers, scopes, enabled}`. * * `action` - a string indicating which action to perform. * * `key` - a keyboard key code indicating which key needs to be pressed. @@ -86,25 +84,25 @@ class HotkeyHandler extends EventDispatcher { * * `scopes` - an array of scopes for which the hotkey is valid. If this array does not contain `this.scope`, the hotkey will not be registered. * * `enabled` - a boolean indicating whether the hotkey is currently enabled. */ - registerHotkeys(hotkeys) { - for (const {action, key, modifiers, scopes, enabled} of hotkeys) { - if ( - enabled && - key !== null && - action !== '' && - scopes.includes(this._scope) - ) { - this._registerHotkey(key, modifiers, action); - } + registerHotkeys(scope, hotkeys) { + let registrations = this._hotkeyRegistrations.get(scope); + if (typeof registrations === 'undefined') { + registrations = []; + this._hotkeyRegistrations.set(scope, registrations); } - this._updateEventHandlers(); + registrations.push(...hotkeys); + this._updateHotkeyRegistrations(); } /** - * Removes all registered hotkeys. + * Removes all registered hotkeys for a given scope. */ - clearHotkeys() { - this._hotkeys.clear(); + clearHotkeys(scope) { + const registrations = this._hotkeyRegistrations.get(scope); + if (typeof registrations !== 'undefined') { + registrations.length = 0; + } + this._updateHotkeyRegistrations(); } /** @@ -132,37 +130,67 @@ class HotkeyHandler extends EventDispatcher { return result; } + /** + * Attempts to simulate an action for a given combination of key and modifiers. + * @param key A keyboard key code indicating which key needs to be pressed. + * @param modifiers An array of keyboard modifiers which also need to be pressed. Supports: `'alt', 'ctrl', 'shift', 'meta'`. + * @returns `true` if an action was performed, `false` otherwise. + */ + simulate(key, modifiers) { + const hotkeyInfo = this._hotkeys.get(key); + return ( + typeof hotkeyInfo !== 'undefined' && + this._invokeHandlers(key, modifiers, hotkeyInfo, false) + ); + } + + // Message handlers + + _onMessageForwardHotkey({key, modifiers}) { + return this.simulate(key, modifiers); + } + // Private + _onMessage({action, params}, sender, callback) { + const messageHandler = this._messageHandlers.get(action); + if (typeof messageHandler === 'undefined') { return false; } + return yomichan.invokeMessageHandler(messageHandler, params, callback, sender); + } + _onKeyDown(e) { const key = e.code; - const handlers = this._hotkeys.get(key); - if (typeof handlers !== 'undefined') { + const hotkeyInfo = this._hotkeys.get(key); + if (typeof hotkeyInfo !== 'undefined') { const eventModifiers = DocumentUtil.getActiveModifiers(e); - for (const {modifiers, action} of handlers) { - if (!this._areSame(modifiers, eventModifiers)) { continue; } + const canForward = (this._forwardFrameId !== null); + if (this._invokeHandlers(key, eventModifiers, hotkeyInfo, canForward)) { + e.preventDefault(); + return; + } + } + this.trigger('keydownNonHotkey', e); + } - const actionHandler = this._actions.get(action); - if (typeof actionHandler === 'undefined') { continue; } + _invokeHandlers(key, modifiers, hotkeyInfo, canForward) { + for (const {modifiers: handlerModifiers, action} of hotkeyInfo.handlers) { + if (!this._areSame(handlerModifiers, modifiers)) { continue; } - const result = actionHandler(e); + const actionHandler = this._actions.get(action); + if (typeof actionHandler !== 'undefined') { + const result = actionHandler(); if (result !== false) { - e.preventDefault(); return true; } } } - this.trigger('keydownNonHotkey', e); - return false; - } - _registerHotkey(key, modifiers, action) { - let handlers = this._hotkeys.get(key); - if (typeof handlers === 'undefined') { - handlers = []; - this._hotkeys.set(key, handlers); + if (canForward && hotkeyInfo.forward) { + this._forwardHotkey(key, modifiers); + return true; } - handlers.push({modifiers: new Set(modifiers), action}); + + return false; } _areSame(set, array) { @@ -175,6 +203,34 @@ class HotkeyHandler extends EventDispatcher { return true; } + _updateHotkeyRegistrations() { + if (this._hotkeys.size === 0 && this._hotkeyRegistrations.size === 0) { return; } + + const canForward = (this._forwardFrameId !== null); + this._hotkeys.clear(); + for (const [scope, registrations] of this._hotkeyRegistrations.entries()) { + for (const {action, key, modifiers, scopes, enabled} of registrations) { + if (!(enabled && key !== null && action !== '')) { continue; } + + const correctScope = scopes.includes(scope); + if (!correctScope && !canForward) { continue; } + + let hotkeyInfo = this._hotkeys.get(key); + if (typeof hotkeyInfo === 'undefined') { + hotkeyInfo = {handlers: [], forward: false}; + this._hotkeys.set(key, hotkeyInfo); + } + + if (correctScope) { + hotkeyInfo.handlers.push({modifiers: new Set(modifiers), action}); + } else { + hotkeyInfo.forward = true; + } + } + } + this._updateEventHandlers(); + } + _updateHasEventListeners() { this._hasEventListeners = this.hasListeners('keydownNonHotkey'); } @@ -187,4 +243,14 @@ class HotkeyHandler extends EventDispatcher { this._eventListeners.removeAllEventListeners(); } } + + async _forwardHotkey(key, modifiers) { + const frameId = this._forwardFrameId; + if (frameId === null) { throw new Error('No forwarding target'); } + try { + await api.crossFrame.invoke(frameId, 'hotkeyHandler.forwardHotkey', {key, modifiers}); + } catch (e) { + // NOP + } + } }