Skip to content

Commit 06d67f7

Browse files
committed
Fixes microsoft#44847: Leave chord mode after some time or when focus is lost
1 parent 6d04391 commit 06d67f7

4 files changed

Lines changed: 62 additions & 24 deletions

File tree

src/vs/editor/standalone/browser/simpleServices.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -359,7 +359,7 @@ export class StandaloneKeybindingService extends AbstractKeybindingService {
359359
this._cachedResolver = null;
360360
this._dynamicKeybindings = [];
361361

362-
this.toDispose.push(dom.addDisposableListener(domNode, dom.EventType.KEY_DOWN, (e: KeyboardEvent) => {
362+
this._register(dom.addDisposableListener(domNode, dom.EventType.KEY_DOWN, (e: KeyboardEvent) => {
363363
let keyEvent = new StandardKeyboardEvent(e);
364364
let shouldPreventDefault = this._dispatch(keyEvent, keyEvent.target);
365365
if (shouldPreventDefault) {
@@ -420,6 +420,10 @@ export class StandaloneKeybindingService extends AbstractKeybindingService {
420420
return this._cachedResolver;
421421
}
422422

423+
protected _documentHasFocus(): boolean {
424+
return document.hasFocus();
425+
}
426+
423427
private _toNormalizedKeybindingItems(items: IKeybindingItem[], isDefault: boolean): ResolvedKeybindingItem[] {
424428
let result: ResolvedKeybindingItem[] = [], resultLen = 0;
425429
for (let i = 0, len = items.length; i < len; i++) {

src/vs/platform/keybinding/common/abstractKeybindingService.ts

Lines changed: 46 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
import * as nls from 'vs/nls';
88
import { ResolvedKeybinding, Keybinding } from 'vs/base/common/keyCodes';
9-
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
9+
import { IDisposable, Disposable } from 'vs/base/common/lifecycle';
1010
import { ICommandService } from 'vs/platform/commands/common/commands';
1111
import { KeybindingResolver, IResolveResult } from 'vs/platform/keybinding/common/keybindingResolver';
1212
import { IKeybindingEvent, IKeybindingService, IKeyboardEvent } from 'vs/platform/keybinding/common/keybinding';
@@ -16,18 +16,18 @@ import { Event, Emitter } from 'vs/base/common/event';
1616
import { ResolvedKeybindingItem } from 'vs/platform/keybinding/common/resolvedKeybindingItem';
1717
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
1818
import { INotificationService } from 'vs/platform/notification/common/notification';
19+
import { IntervalTimer } from 'vs/base/common/async';
1920

2021
interface CurrentChord {
2122
keypress: string;
2223
label: string;
2324
}
2425

25-
export abstract class AbstractKeybindingService implements IKeybindingService {
26+
export abstract class AbstractKeybindingService extends Disposable implements IKeybindingService {
2627
public _serviceBrand: any;
2728

28-
protected toDispose: IDisposable[] = [];
29-
3029
private _currentChord: CurrentChord;
30+
private _currentChordChecker: IntervalTimer;
3131
private _currentChordStatusMessage: IDisposable;
3232
protected _onDidUpdateKeybindings: Emitter<IKeybindingEvent>;
3333

@@ -44,27 +44,29 @@ export abstract class AbstractKeybindingService implements IKeybindingService {
4444
notificationService: INotificationService,
4545
statusService?: IStatusbarService
4646
) {
47+
super();
4748
this._contextKeyService = contextKeyService;
4849
this._commandService = commandService;
4950
this._telemetryService = telemetryService;
5051
this._statusService = statusService;
5152
this._notificationService = notificationService;
5253

5354
this._currentChord = null;
55+
this._currentChordChecker = new IntervalTimer();
5456
this._currentChordStatusMessage = null;
55-
this._onDidUpdateKeybindings = new Emitter<IKeybindingEvent>();
56-
this.toDispose.push(this._onDidUpdateKeybindings);
57+
this._onDidUpdateKeybindings = this._register(new Emitter<IKeybindingEvent>());
5758
}
5859

5960
public dispose(): void {
60-
this.toDispose = dispose(this.toDispose);
61+
super.dispose();
6162
}
6263

6364
get onDidUpdateKeybindings(): Event<IKeybindingEvent> {
6465
return this._onDidUpdateKeybindings ? this._onDidUpdateKeybindings.event : Event.None; // Sinon stubbing walks properties on prototype
6566
}
6667

6768
protected abstract _getResolver(): KeybindingResolver;
69+
protected abstract _documentHasFocus(): boolean;
6870
public abstract resolveKeybinding(keybinding: Keybinding): ResolvedKeybinding[];
6971
public abstract resolveKeyboardEvent(keyboardEvent: IKeyboardEvent): ResolvedKeybinding;
7072
public abstract resolveUserBinding(userBinding: string): ResolvedKeybinding[];
@@ -114,6 +116,40 @@ export abstract class AbstractKeybindingService implements IKeybindingService {
114116
return this._getResolver().resolve(contextValue, currentChord, firstPart);
115117
}
116118

119+
private _enterChordMode(firstPart: string, keypressLabel: string): void {
120+
this._currentChord = {
121+
keypress: firstPart,
122+
label: keypressLabel
123+
};
124+
if (this._statusService) {
125+
this._currentChordStatusMessage = this._statusService.setStatusMessage(nls.localize('first.chord', "({0}) was pressed. Waiting for second key of chord...", keypressLabel));
126+
}
127+
const chordEnterTime = Date.now();
128+
this._currentChordChecker.cancelAndSet(() => {
129+
130+
if (!this._documentHasFocus()) {
131+
// Focus has been lost => leave chord mode
132+
this._leaveChordMode();
133+
return;
134+
}
135+
136+
if (Date.now() - chordEnterTime > 5000) {
137+
// 5 seconds elapsed => leave chord mode
138+
this._leaveChordMode();
139+
}
140+
141+
}, 500);
142+
}
143+
144+
private _leaveChordMode(): void {
145+
if (this._currentChordStatusMessage) {
146+
this._currentChordStatusMessage.dispose();
147+
this._currentChordStatusMessage = null;
148+
}
149+
this._currentChordChecker.cancel();
150+
this._currentChord = null;
151+
}
152+
117153
protected _dispatch(e: IKeyboardEvent, target: IContextKeyServiceTarget): boolean {
118154
let shouldPreventDefault = false;
119155

@@ -135,13 +171,7 @@ export abstract class AbstractKeybindingService implements IKeybindingService {
135171

136172
if (resolveResult && resolveResult.enterChord) {
137173
shouldPreventDefault = true;
138-
this._currentChord = {
139-
keypress: firstPart,
140-
label: keypressLabel
141-
};
142-
if (this._statusService) {
143-
this._currentChordStatusMessage = this._statusService.setStatusMessage(nls.localize('first.chord', "({0}) was pressed. Waiting for second key of chord...", keypressLabel));
144-
}
174+
this._enterChordMode(firstPart, keypressLabel);
145175
return shouldPreventDefault;
146176
}
147177

@@ -151,11 +181,8 @@ export abstract class AbstractKeybindingService implements IKeybindingService {
151181
shouldPreventDefault = true;
152182
}
153183
}
154-
if (this._currentChordStatusMessage) {
155-
this._currentChordStatusMessage.dispose();
156-
this._currentChordStatusMessage = null;
157-
}
158-
this._currentChord = null;
184+
185+
this._leaveChordMode();
159186

160187
if (resolveResult && resolveResult.commandId) {
161188
if (!resolveResult.bubble) {

src/vs/platform/keybinding/test/common/abstractKeybindingService.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@ suite('AbstractKeybindingService', () => {
4949
return this._resolver;
5050
}
5151

52+
protected _documentHasFocus(): boolean {
53+
return true;
54+
}
55+
5256
public resolveKeybinding(kb: Keybinding): ResolvedKeybinding[] {
5357
return [new USLayoutResolvedKeybinding(kb, OS)];
5458
}

src/vs/workbench/services/keybinding/electron-browser/keybindingService.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -291,8 +291,7 @@ export class WorkbenchKeybindingService extends AbstractKeybindingService {
291291
this._cachedResolver = null;
292292
this._firstTimeComputingResolver = true;
293293

294-
this.userKeybindings = new ConfigWatcher(environmentService.appKeybindingsPath, { defaultConfig: [], onError: error => onUnexpectedError(error) });
295-
this.toDispose.push(this.userKeybindings);
294+
this.userKeybindings = this._register(new ConfigWatcher(environmentService.appKeybindingsPath, { defaultConfig: [], onError: error => onUnexpectedError(error) }));
296295

297296
keybindingsExtPoint.setHandler((extensions) => {
298297
let commandAdded = false;
@@ -306,12 +305,12 @@ export class WorkbenchKeybindingService extends AbstractKeybindingService {
306305
}
307306
});
308307

309-
this.toDispose.push(this.userKeybindings.onDidUpdateConfiguration(event => this.updateResolver({
308+
this._register(this.userKeybindings.onDidUpdateConfiguration(event => this.updateResolver({
310309
source: KeybindingSource.User,
311310
keybindings: event.config
312311
})));
313312

314-
this.toDispose.push(dom.addDisposableListener(windowElement, dom.EventType.KEY_DOWN, (e: KeyboardEvent) => {
313+
this._register(dom.addDisposableListener(windowElement, dom.EventType.KEY_DOWN, (e: KeyboardEvent) => {
315314
let keyEvent = new StandardKeyboardEvent(e);
316315
let shouldPreventDefault = this._dispatch(keyEvent, keyEvent.target);
317316
if (shouldPreventDefault) {
@@ -367,6 +366,10 @@ export class WorkbenchKeybindingService extends AbstractKeybindingService {
367366
return this._cachedResolver;
368367
}
369368

369+
protected _documentHasFocus(): boolean {
370+
return document.hasFocus();
371+
}
372+
370373
private _resolveKeybindingItems(items: IKeybindingItem[], isDefault: boolean): ResolvedKeybindingItem[] {
371374
let result: ResolvedKeybindingItem[] = [], resultLen = 0;
372375
for (let i = 0, len = items.length; i < len; i++) {

0 commit comments

Comments
 (0)