123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225 |
- /*---------------------------------------------------------------------------------------------
- * 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);
|