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:
parent
8ec48456e6
commit
14b4aee07d
@ -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';
|
||||||
|
@ -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');
|
||||||
|
@ -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();
|
||||||
|
@ -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() {
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user