@21epub/epub-thirdparty
Version:
epub-thirdparty
226 lines (225 loc) • 10.8 kB
JavaScript
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IntervalTimer, TimeoutTimer } from '../../../base/common/async.js';
import { Emitter, Event } from '../../../base/common/event.js';
import { Disposable } from '../../../base/common/lifecycle.js';
import * as nls from '../../../nls.js';
const HIGH_FREQ_COMMANDS = /^(cursor|delete)/;
export class AbstractKeybindingService extends Disposable {
constructor(_contextKeyService, _commandService, _telemetryService, _notificationService, _logService) {
super();
this._contextKeyService = _contextKeyService;
this._commandService = _commandService;
this._telemetryService = _telemetryService;
this._notificationService = _notificationService;
this._logService = _logService;
this._onDidUpdateKeybindings = this._register(new Emitter());
this._currentChord = null;
this._currentChordChecker = new IntervalTimer();
this._currentChordStatusMessage = null;
this._ignoreSingleModifiers = KeybindingModifierSet.EMPTY;
this._currentSingleModifier = null;
this._currentSingleModifierClearTimeout = new TimeoutTimer();
this._logging = false;
}
get onDidUpdateKeybindings() {
return this._onDidUpdateKeybindings ? this._onDidUpdateKeybindings.event : Event.None; // Sinon stubbing walks properties on prototype
}
dispose() {
super.dispose();
}
_log(str) {
if (this._logging) {
this._logService.info(`[KeybindingService]: ${str}`);
}
}
getKeybindings() {
return this._getResolver().getKeybindings();
}
lookupKeybinding(commandId, context) {
const result = this._getResolver().lookupPrimaryKeybinding(commandId, context || this._contextKeyService);
if (!result) {
return undefined;
}
return result.resolvedKeybinding;
}
dispatchEvent(e, target) {
return this._dispatch(e, target);
}
softDispatch(e, target) {
const keybinding = this.resolveKeyboardEvent(e);
if (keybinding.isChord()) {
console.warn('Unexpected keyboard event mapped to a chord');
return null;
}
const [firstPart,] = keybinding.getDispatchParts();
if (firstPart === null) {
// cannot be dispatched, probably only modifier keys
return null;
}
const contextValue = this._contextKeyService.getContext(target);
const currentChord = this._currentChord ? this._currentChord.keypress : null;
return this._getResolver().resolve(contextValue, currentChord, firstPart);
}
_enterChordMode(firstPart, keypressLabel) {
this._currentChord = {
keypress: firstPart,
label: keypressLabel
};
this._currentChordStatusMessage = this._notificationService.status(nls.localize('first.chord', "({0}) was pressed. Waiting for second key of chord...", keypressLabel));
const chordEnterTime = Date.now();
this._currentChordChecker.cancelAndSet(() => {
if (!this._documentHasFocus()) {
// Focus has been lost => leave chord mode
this._leaveChordMode();
return;
}
if (Date.now() - chordEnterTime > 5000) {
// 5 seconds elapsed => leave chord mode
this._leaveChordMode();
}
}, 500);
}
_leaveChordMode() {
if (this._currentChordStatusMessage) {
this._currentChordStatusMessage.dispose();
this._currentChordStatusMessage = null;
}
this._currentChordChecker.cancel();
this._currentChord = null;
}
_dispatch(e, target) {
return this._doDispatch(this.resolveKeyboardEvent(e), target, /*isSingleModiferChord*/ false);
}
_singleModifierDispatch(e, target) {
const keybinding = this.resolveKeyboardEvent(e);
const [singleModifier,] = keybinding.getSingleModifierDispatchParts();
if (singleModifier) {
if (this._ignoreSingleModifiers.has(singleModifier)) {
this._log(`+ Ignoring single modifier ${singleModifier} due to it being pressed together with other keys.`);
this._ignoreSingleModifiers = KeybindingModifierSet.EMPTY;
this._currentSingleModifierClearTimeout.cancel();
this._currentSingleModifier = null;
return false;
}
this._ignoreSingleModifiers = KeybindingModifierSet.EMPTY;
if (this._currentSingleModifier === null) {
// we have a valid `singleModifier`, store it for the next keyup, but clear it in 300ms
this._log(`+ Storing single modifier for possible chord ${singleModifier}.`);
this._currentSingleModifier = singleModifier;
this._currentSingleModifierClearTimeout.cancelAndSet(() => {
this._log(`+ Clearing single modifier due to 300ms elapsed.`);
this._currentSingleModifier = null;
}, 300);
return false;
}
if (singleModifier === this._currentSingleModifier) {
// bingo!
this._log(`/ Dispatching single modifier chord ${singleModifier} ${singleModifier}`);
this._currentSingleModifierClearTimeout.cancel();
this._currentSingleModifier = null;
return this._doDispatch(keybinding, target, /*isSingleModiferChord*/ true);
}
this._log(`+ Clearing single modifier due to modifier mismatch: ${this._currentSingleModifier} ${singleModifier}`);
this._currentSingleModifierClearTimeout.cancel();
this._currentSingleModifier = null;
return false;
}
// When pressing a modifier and holding it pressed with any other modifier or key combination,
// the pressed modifiers should no longer be considered for single modifier dispatch.
const [firstPart,] = keybinding.getParts();
this._ignoreSingleModifiers = new KeybindingModifierSet(firstPart);
if (this._currentSingleModifier !== null) {
this._log(`+ Clearing single modifier due to other key up.`);
}
this._currentSingleModifierClearTimeout.cancel();
this._currentSingleModifier = null;
return false;
}
_doDispatch(keybinding, target, isSingleModiferChord = false) {
let shouldPreventDefault = false;
if (keybinding.isChord()) {
console.warn('Unexpected keyboard event mapped to a chord');
return false;
}
let firstPart = null; // the first keybinding i.e. Ctrl+K
let currentChord = null; // the "second" keybinding i.e. Ctrl+K "Ctrl+D"
if (isSingleModiferChord) {
const [dispatchKeyname,] = keybinding.getSingleModifierDispatchParts();
firstPart = dispatchKeyname;
currentChord = dispatchKeyname;
}
else {
[firstPart,] = keybinding.getDispatchParts();
currentChord = this._currentChord ? this._currentChord.keypress : null;
}
if (firstPart === null) {
this._log(`\\ Keyboard event cannot be dispatched in keydown phase.`);
// cannot be dispatched, probably only modifier keys
return shouldPreventDefault;
}
const contextValue = this._contextKeyService.getContext(target);
const keypressLabel = keybinding.getLabel();
const resolveResult = this._getResolver().resolve(contextValue, currentChord, firstPart);
this._logService.trace('KeybindingService#dispatch', keypressLabel, resolveResult === null || resolveResult === void 0 ? void 0 : resolveResult.commandId);
if (resolveResult && resolveResult.enterChord) {
shouldPreventDefault = true;
this._enterChordMode(firstPart, keypressLabel);
return shouldPreventDefault;
}
if (this._currentChord) {
if (!resolveResult || !resolveResult.commandId) {
this._notificationService.status(nls.localize('missing.chord', "The key combination ({0}, {1}) is not a command.", this._currentChord.label, keypressLabel), { hideAfter: 10 * 1000 /* 10s */ });
shouldPreventDefault = true;
}
}
this._leaveChordMode();
if (resolveResult && resolveResult.commandId) {
if (!resolveResult.bubble) {
shouldPreventDefault = true;
}
if (typeof resolveResult.commandArgs === 'undefined') {
this._commandService.executeCommand(resolveResult.commandId).then(undefined, err => this._notificationService.warn(err));
}
else {
this._commandService.executeCommand(resolveResult.commandId, resolveResult.commandArgs).then(undefined, err => this._notificationService.warn(err));
}
if (!HIGH_FREQ_COMMANDS.test(resolveResult.commandId)) {
this._telemetryService.publicLog2('workbenchActionExecuted', { id: resolveResult.commandId, from: 'keybinding' });
}
}
return shouldPreventDefault;
}
mightProducePrintableCharacter(event) {
if (event.ctrlKey || event.metaKey) {
// ignore ctrl/cmd-combination but not shift/alt-combinatios
return false;
}
// weak check for certain ranges. this is properly implemented in a subclass
// with access to the KeyboardMapperFactory.
if ((event.keyCode >= 31 /* KeyA */ && event.keyCode <= 56 /* KeyZ */)
|| (event.keyCode >= 21 /* Digit0 */ && event.keyCode <= 30 /* Digit9 */)) {
return true;
}
return false;
}
}
class KeybindingModifierSet {
constructor(source) {
this._ctrlKey = source ? source.ctrlKey : false;
this._shiftKey = source ? source.shiftKey : false;
this._altKey = source ? source.altKey : false;
this._metaKey = source ? source.metaKey : false;
}
has(modifier) {
switch (modifier) {
case 'ctrl': return this._ctrlKey;
case 'shift': return this._shiftKey;
case 'alt': return this._altKey;
case 'meta': return this._metaKey;
}
}
}
KeybindingModifierSet.EMPTY = new KeybindingModifierSet(null);