Hotkey forwarding support (#1263)

* Add support for allowing HotkeyHandler to forward hotkeys

* Update hotkey registration

* Pass HotkeyHandler instance into Display* constructor

* Implement hotkey forwarding
This commit is contained in:
toasted-nutbread 2021-01-17 16:55:45 -05:00 committed by GitHub
parent 8ec48456e6
commit 14b4aee07d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 138 additions and 60 deletions

View File

@ -18,6 +18,7 @@
/* global /* global
* DisplaySearch * DisplaySearch
* DocumentFocusController * DocumentFocusController
* HotkeyHandler
* JapaneseUtil * JapaneseUtil
* api * api
* wanakana * wanakana
@ -32,7 +33,11 @@
await yomichan.backendReady(); await yomichan.backendReady();
const japaneseUtil = new JapaneseUtil(wanakana); 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(); await displaySearch.prepare();
document.documentElement.dataset.loaded = 'true'; document.documentElement.dataset.loaded = 'true';

View File

@ -23,8 +23,8 @@
*/ */
class DisplaySearch extends Display { class DisplaySearch extends Display {
constructor(japaneseUtil, documentFocusController) { constructor(japaneseUtil, documentFocusController, hotkeyHandler) {
super('search', japaneseUtil, documentFocusController); super('search', japaneseUtil, documentFocusController, hotkeyHandler);
this._searchButton = document.querySelector('#search-button'); this._searchButton = document.querySelector('#search-button');
this._queryInput = document.querySelector('#search-textbox'); this._queryInput = document.querySelector('#search-textbox');
this._introElement = document.querySelector('#intro'); this._introElement = document.querySelector('#intro');

View File

@ -19,6 +19,7 @@
* Display * Display
* DisplayProfileSelection * DisplayProfileSelection
* DocumentFocusController * DocumentFocusController
* HotkeyHandler
* JapaneseUtil * JapaneseUtil
* api * api
*/ */
@ -32,7 +33,11 @@
await yomichan.backendReady(); await yomichan.backendReady();
const japaneseUtil = new JapaneseUtil(null); 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(); await display.prepare();
const displayProfileSelection = new DisplayProfileSelection(display); const displayProfileSelection = new DisplayProfileSelection(display);
displayProfileSelection.prepare(); displayProfileSelection.prepare();

View File

@ -24,7 +24,6 @@
* DocumentUtil * DocumentUtil
* FrameEndpoint * FrameEndpoint
* Frontend * Frontend
* HotkeyHandler
* MediaLoader * MediaLoader
* PopupFactory * PopupFactory
* QueryParser * QueryParser
@ -35,11 +34,12 @@
*/ */
class Display extends EventDispatcher { class Display extends EventDispatcher {
constructor(pageType, japaneseUtil, documentFocusController) { constructor(pageType, japaneseUtil, documentFocusController, hotkeyHandler) {
super(); super();
this._pageType = pageType; this._pageType = pageType;
this._japaneseUtil = japaneseUtil; this._japaneseUtil = japaneseUtil;
this._documentFocusController = documentFocusController; this._documentFocusController = documentFocusController;
this._hotkeyHandler = hotkeyHandler;
this._container = document.querySelector('#definitions'); this._container = document.querySelector('#definitions');
this._definitions = []; this._definitions = [];
this._optionsContext = {depth: 0, url: window.location.href}; this._optionsContext = {depth: 0, url: window.location.href};
@ -60,7 +60,6 @@ class Display extends EventDispatcher {
japaneseUtil, japaneseUtil,
mediaLoader: this._mediaLoader mediaLoader: this._mediaLoader
}); });
this._hotkeyHandler = new HotkeyHandler(this._pageType);
this._messageHandlers = new Map(); this._messageHandlers = new Map();
this._directMessageHandlers = new Map(); this._directMessageHandlers = new Map();
this._windowMessageHandlers = new Map(); this._windowMessageHandlers = new Map();
@ -199,7 +198,6 @@ class Display extends EventDispatcher {
this._audioSystem.prepare(); this._audioSystem.prepare();
this._queryParser.prepare(); this._queryParser.prepare();
this._history.prepare(); this._history.prepare();
this._hotkeyHandler.prepare();
// Event setup // Event setup
this._history.on('stateChanged', this._onStateChanged.bind(this)); this._history.on('stateChanged', this._onStateChanged.bind(this));
@ -498,6 +496,9 @@ class Display extends EventDispatcher {
this._parentPopupId = parentPopupId; this._parentPopupId = parentPopupId;
this._parentFrameId = parentFrameId; this._parentFrameId = parentFrameId;
this._ownerFrameId = ownerFrameId; this._ownerFrameId = ownerFrameId;
if (this._pageType === 'popup') {
this._hotkeyHandler.forwardFrameId = ownerFrameId;
}
this._childrenSupported = childrenSupported; this._childrenSupported = childrenSupported;
this._setContentScale(scale); this._setContentScale(scale);
await this.setOptionsContext(optionsContext); await this.setOptionsContext(optionsContext);
@ -1879,8 +1880,9 @@ class Display extends EventDispatcher {
} }
_updateHotkeys(options) { _updateHotkeys(options) {
this._hotkeyHandler.clearHotkeys(); const scope = this._pageType;
this._hotkeyHandler.registerHotkeys(options.inputs.hotkeys); this._hotkeyHandler.clearHotkeys(scope);
this._hotkeyHandler.registerHotkeys(scope, options.inputs.hotkeys);
} }
async _closeTab() { async _closeTab() {

View File

@ -17,6 +17,7 @@
/* global /* global
* DocumentUtil * DocumentUtil
* api
*/ */
/** /**
@ -25,30 +26,31 @@
class HotkeyHandler extends EventDispatcher { class HotkeyHandler extends EventDispatcher {
/** /**
* Creates a new instance of the class. * Creates a new instance of the class.
* @param scope The scope required for hotkey definitions.
*/ */
constructor(scope) { constructor() {
super(); super();
this._scope = scope;
this._hotkeys = new Map();
this._actions = new Map(); this._actions = new Map();
this._hotkeys = new Map();
this._hotkeyRegistrations = new Map();
this._eventListeners = new EventListenerCollection(); this._eventListeners = new EventListenerCollection();
this._isPrepared = false; this._isPrepared = false;
this._hasEventListeners = 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() { get forwardFrameId() {
return this._scope; return this._forwardFrameId;
} }
/** /**
* Sets the scope required for the hotkey definitions. * Sets the frame ID used for forwarding hotkeys.
*/ */
set scope(value) { set forwardFrameId(value) {
this._scope = value; this._forwardFrameId = value;
this._updateHotkeyRegistrations();
} }
/** /**
@ -57,14 +59,9 @@ class HotkeyHandler extends EventDispatcher {
prepare() { prepare() {
this._isPrepared = true; this._isPrepared = true;
this._updateEventHandlers(); this._updateEventHandlers();
} api.crossFrame.registerHandlers([
['hotkeyHandler.forwardHotkey', {async: false, handler: this._onMessageForwardHotkey.bind(this)}]
/** ]);
* Stops listening to key press events.
*/
cleanup() {
this._isPrepared = false;
this._updateEventHandlers();
} }
/** /**
@ -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}`. * @param hotkeys An array of hotkey definitions of the format `{action, key, modifiers, scopes, enabled}`.
* * `action` - a string indicating which action to perform. * * `action` - a string indicating which action to perform.
* * `key` - a keyboard key code indicating which key needs to be pressed. * * `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. * * `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. * * `enabled` - a boolean indicating whether the hotkey is currently enabled.
*/ */
registerHotkeys(hotkeys) { registerHotkeys(scope, hotkeys) {
for (const {action, key, modifiers, scopes, enabled} of hotkeys) { let registrations = this._hotkeyRegistrations.get(scope);
if ( if (typeof registrations === 'undefined') {
enabled && registrations = [];
key !== null && this._hotkeyRegistrations.set(scope, registrations);
action !== '' &&
scopes.includes(this._scope)
) {
this._registerHotkey(key, modifiers, action);
} }
} registrations.push(...hotkeys);
this._updateEventHandlers(); this._updateHotkeyRegistrations();
} }
/** /**
* Removes all registered hotkeys. * Removes all registered hotkeys for a given scope.
*/ */
clearHotkeys() { clearHotkeys(scope) {
this._hotkeys.clear(); 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; 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 // 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) { _onKeyDown(e) {
const key = e.code; const key = e.code;
const handlers = this._hotkeys.get(key); const hotkeyInfo = this._hotkeys.get(key);
if (typeof handlers !== 'undefined') { if (typeof hotkeyInfo !== 'undefined') {
const eventModifiers = DocumentUtil.getActiveModifiers(e); const eventModifiers = DocumentUtil.getActiveModifiers(e);
for (const {modifiers, action} of handlers) { const canForward = (this._forwardFrameId !== null);
if (!this._areSame(modifiers, eventModifiers)) { continue; } if (this._invokeHandlers(key, eventModifiers, hotkeyInfo, canForward)) {
e.preventDefault();
return;
}
}
this.trigger('keydownNonHotkey', e);
}
_invokeHandlers(key, modifiers, hotkeyInfo, canForward) {
for (const {modifiers: handlerModifiers, action} of hotkeyInfo.handlers) {
if (!this._areSame(handlerModifiers, modifiers)) { continue; }
const actionHandler = this._actions.get(action); const actionHandler = this._actions.get(action);
if (typeof actionHandler === 'undefined') { continue; } if (typeof actionHandler !== 'undefined') {
const result = actionHandler();
const result = actionHandler(e);
if (result !== false) { if (result !== false) {
e.preventDefault();
return true; return true;
} }
} }
} }
this.trigger('keydownNonHotkey', e);
return false; if (canForward && hotkeyInfo.forward) {
this._forwardHotkey(key, modifiers);
return true;
} }
_registerHotkey(key, modifiers, 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});
} }
_areSame(set, array) { _areSame(set, array) {
@ -175,6 +203,34 @@ class HotkeyHandler extends EventDispatcher {
return true; 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() { _updateHasEventListeners() {
this._hasEventListeners = this.hasListeners('keydownNonHotkey'); this._hasEventListeners = this.hasListeners('keydownNonHotkey');
} }
@ -187,4 +243,14 @@ class HotkeyHandler extends EventDispatcher {
this._eventListeners.removeAllEventListeners(); 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
}
}
} }