aboutsummaryrefslogtreecommitdiff
path: root/ext
diff options
context:
space:
mode:
authortoasted-nutbread <toasted-nutbread@users.noreply.github.com>2021-01-17 16:55:45 -0500
committerGitHub <noreply@github.com>2021-01-17 16:55:45 -0500
commit14b4aee07dc3f69ad75518cf04cafe416e06f0c7 (patch)
tree407918a74817284cd9f1d474a6c7783033e7a4c6 /ext
parent8ec48456e6d73e10239469c39e7862365db43739 (diff)
Hotkey forwarding support (#1263)
* Add support for allowing HotkeyHandler to forward hotkeys * Update hotkey registration * Pass HotkeyHandler instance into Display* constructor * Implement hotkey forwarding
Diffstat (limited to 'ext')
-rw-r--r--ext/bg/js/search-main.js7
-rw-r--r--ext/bg/js/search.js4
-rw-r--r--ext/fg/js/float-main.js7
-rw-r--r--ext/mixed/js/display.js14
-rw-r--r--ext/mixed/js/hotkey-handler.js166
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
+ }
+ }
}