Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/browser/window.ts
5221 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { isSafari, setFullscreen } from '../../base/browser/browser.js';
7
import { addDisposableListener, EventHelper, EventType, getWindow, getWindowById, getWindows, getWindowsCount, hasAppFocus, windowOpenNoOpener, windowOpenPopup, windowOpenWithSuccess } from '../../base/browser/dom.js';
8
import { DomEmitter } from '../../base/browser/event.js';
9
import { HidDeviceData, requestHidDevice, requestSerialPort, requestUsbDevice, SerialPortData, UsbDeviceData } from '../../base/browser/deviceAccess.js';
10
import { timeout } from '../../base/common/async.js';
11
import { Event } from '../../base/common/event.js';
12
import { Disposable, IDisposable, dispose, toDisposable } from '../../base/common/lifecycle.js';
13
import { matchesScheme, Schemas } from '../../base/common/network.js';
14
import { isIOS, isMacintosh } from '../../base/common/platform.js';
15
import Severity from '../../base/common/severity.js';
16
import { URI } from '../../base/common/uri.js';
17
import { localize } from '../../nls.js';
18
import { CommandsRegistry } from '../../platform/commands/common/commands.js';
19
import { IDialogService, IPromptButton } from '../../platform/dialogs/common/dialogs.js';
20
import { IInstantiationService, ServicesAccessor } from '../../platform/instantiation/common/instantiation.js';
21
import { ILabelService } from '../../platform/label/common/label.js';
22
import { IOpenerService } from '../../platform/opener/common/opener.js';
23
import { IProductService } from '../../platform/product/common/productService.js';
24
import { IBrowserWorkbenchEnvironmentService } from '../services/environment/browser/environmentService.js';
25
import { IWorkbenchLayoutService } from '../services/layout/browser/layoutService.js';
26
import { BrowserLifecycleService } from '../services/lifecycle/browser/lifecycleService.js';
27
import { ILifecycleService, ShutdownReason } from '../services/lifecycle/common/lifecycle.js';
28
import { IHostService } from '../services/host/browser/host.js';
29
import { registerWindowDriver } from '../services/driver/browser/driver.js';
30
import { CodeWindow, isAuxiliaryWindow, mainWindow } from '../../base/browser/window.js';
31
import { createSingleCallFunction } from '../../base/common/functional.js';
32
import { IConfigurationService } from '../../platform/configuration/common/configuration.js';
33
import { IWorkbenchEnvironmentService } from '../services/environment/common/environmentService.js';
34
import { MarkdownString } from '../../base/common/htmlContent.js';
35
import { IContextMenuService } from '../../platform/contextview/browser/contextView.js';
36
37
export abstract class BaseWindow extends Disposable {
38
39
private static TIMEOUT_HANDLES = Number.MIN_SAFE_INTEGER; // try to not compete with the IDs of native `setTimeout`
40
private static readonly TIMEOUT_DISPOSABLES = new Map<number, Set<IDisposable>>();
41
42
constructor(
43
targetWindow: CodeWindow,
44
dom = { getWindowsCount, getWindows }, /* for testing */
45
@IHostService protected readonly hostService: IHostService,
46
@IWorkbenchEnvironmentService protected readonly environmentService: IWorkbenchEnvironmentService,
47
@IContextMenuService protected readonly contextMenuService: IContextMenuService,
48
@IWorkbenchLayoutService protected readonly layoutService: IWorkbenchLayoutService,
49
) {
50
super();
51
52
this.enableWindowFocusOnElementFocus(targetWindow);
53
this.enableMultiWindowAwareTimeout(targetWindow, dom);
54
55
this.registerFullScreenListeners(targetWindow.vscodeWindowId);
56
this.registerContextMenuListeners(targetWindow);
57
}
58
59
//#region focus handling in multi-window applications
60
61
protected enableWindowFocusOnElementFocus(targetWindow: CodeWindow): void {
62
const originalFocus = targetWindow.HTMLElement.prototype.focus;
63
64
const that = this;
65
targetWindow.HTMLElement.prototype.focus = function (this: HTMLElement, options?: FocusOptions | undefined): void {
66
67
// Ensure the window the element belongs to is focused
68
// in scenarios where auxiliary windows are present
69
that.onElementFocus(getWindow(this));
70
71
// Pass to original focus() method
72
originalFocus.apply(this, [options]);
73
};
74
}
75
76
private onElementFocus(targetWindow: CodeWindow): void {
77
78
// Check if focus should transfer: the application currently has focus somewhere, but not in the target window.
79
if (!targetWindow.document.hasFocus() && hasAppFocus()) {
80
81
// Call original focus()
82
targetWindow.focus();
83
84
// In Electron, `window.focus()` fails to bring the window
85
// to the front if multiple windows exist in the same process
86
// group (floating windows). As such, we ask the host service
87
// to focus the window which can take care of bringin the
88
// window to the front.
89
//
90
// To minimise disruption by bringing windows to the front
91
// by accident, we only do this if the window is not already
92
// focused and the active window is not the target window
93
// but has focus. This is an indication that multiple windows
94
// are opened in the same process group while the target window
95
// is not focused.
96
97
if (
98
!this.environmentService.extensionTestsLocationURI &&
99
!targetWindow.document.hasFocus()
100
) {
101
this.hostService.focus(targetWindow);
102
}
103
}
104
}
105
106
//#endregion
107
108
//#region timeout handling in multi-window applications
109
110
protected enableMultiWindowAwareTimeout(targetWindow: Window, dom = { getWindowsCount, getWindows }): void {
111
112
// Override `setTimeout` and `clearTimeout` on the provided window to make
113
// sure timeouts are dispatched to all opened windows. Some browsers may decide
114
// to throttle timeouts in minimized windows, so with this we can ensure the
115
// timeout is scheduled without being throttled (unless all windows are minimized).
116
117
const originalSetTimeout = targetWindow.setTimeout;
118
Object.defineProperty(targetWindow, 'vscodeOriginalSetTimeout', { get: () => originalSetTimeout });
119
120
const originalClearTimeout = targetWindow.clearTimeout;
121
Object.defineProperty(targetWindow, 'vscodeOriginalClearTimeout', { get: () => originalClearTimeout });
122
123
targetWindow.setTimeout = function (this: unknown, handler: TimerHandler, timeout = 0, ...args: unknown[]): number {
124
if (dom.getWindowsCount() === 1 || typeof handler === 'string' || timeout === 0 /* immediates are never throttled */) {
125
return originalSetTimeout.apply(this, [handler, timeout, ...args]);
126
}
127
128
const timeoutDisposables = new Set<IDisposable>();
129
const timeoutHandle = BaseWindow.TIMEOUT_HANDLES++;
130
BaseWindow.TIMEOUT_DISPOSABLES.set(timeoutHandle, timeoutDisposables);
131
132
const handlerFn = createSingleCallFunction(handler, () => {
133
dispose(timeoutDisposables);
134
BaseWindow.TIMEOUT_DISPOSABLES.delete(timeoutHandle);
135
});
136
137
for (const { window, disposables } of dom.getWindows()) {
138
if (isAuxiliaryWindow(window) && window.document.visibilityState === 'hidden') {
139
continue; // skip over hidden windows (but never over main window)
140
}
141
142
// we track didClear in case the browser does not properly clear the timeout
143
// this can happen for timeouts on unfocused windows
144
let didClear = false;
145
146
const handle = (window as { vscodeOriginalSetTimeout?: typeof window.setTimeout }).vscodeOriginalSetTimeout?.apply(this, [(...args: unknown[]) => {
147
if (didClear) {
148
return;
149
}
150
handlerFn(...args);
151
}, timeout, ...args]);
152
153
const timeoutDisposable = toDisposable(() => {
154
didClear = true;
155
(window as { vscodeOriginalClearTimeout?: typeof window.clearTimeout }).vscodeOriginalClearTimeout?.apply(this, [handle]);
156
timeoutDisposables.delete(timeoutDisposable);
157
});
158
159
disposables.add(timeoutDisposable);
160
timeoutDisposables.add(timeoutDisposable);
161
}
162
163
return timeoutHandle;
164
};
165
166
targetWindow.clearTimeout = function (this: unknown, timeoutHandle: number | undefined): void {
167
const timeoutDisposables = typeof timeoutHandle === 'number' ? BaseWindow.TIMEOUT_DISPOSABLES.get(timeoutHandle) : undefined;
168
if (timeoutDisposables) {
169
dispose(timeoutDisposables);
170
BaseWindow.TIMEOUT_DISPOSABLES.delete(timeoutHandle!);
171
} else {
172
originalClearTimeout.apply(this, [timeoutHandle]);
173
}
174
};
175
}
176
177
//#endregion
178
179
//#region Confirm on Shutdown
180
181
static async confirmOnShutdown(accessor: ServicesAccessor, reason: ShutdownReason): Promise<boolean> {
182
const dialogService = accessor.get(IDialogService);
183
const configurationService = accessor.get(IConfigurationService);
184
185
const message = reason === ShutdownReason.QUIT ?
186
(isMacintosh ? localize('quitMessageMac', "Are you sure you want to quit?") : localize('quitMessage', "Are you sure you want to exit?")) :
187
localize('closeWindowMessage', "Are you sure you want to close the window?");
188
const primaryButton = reason === ShutdownReason.QUIT ?
189
(isMacintosh ? localize({ key: 'quitButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Quit") : localize({ key: 'exitButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Exit")) :
190
localize({ key: 'closeWindowButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Close Window");
191
192
const res = await dialogService.confirm({
193
message,
194
primaryButton,
195
checkbox: {
196
label: localize('doNotAskAgain', "Do not ask me again")
197
}
198
});
199
200
// Update setting if checkbox checked
201
if (res.confirmed && res.checkboxChecked) {
202
await configurationService.updateValue('window.confirmBeforeClose', 'never');
203
}
204
205
return res.confirmed;
206
}
207
208
//#endregion
209
210
private registerFullScreenListeners(targetWindowId: number): void {
211
this._register(this.hostService.onDidChangeFullScreen(({ windowId, fullscreen }) => {
212
if (windowId === targetWindowId) {
213
const targetWindow = getWindowById(targetWindowId);
214
if (targetWindow) {
215
setFullscreen(fullscreen, targetWindow.window);
216
}
217
}
218
}));
219
}
220
221
private registerContextMenuListeners(targetWindow: Window): void {
222
if (targetWindow !== mainWindow) {
223
// we only need to listen in the main window as the code
224
// will go by the active container and update accordingly
225
return;
226
}
227
228
const update = (visible: boolean) => this.layoutService.activeContainer.classList.toggle('context-menu-visible', visible);
229
this._register(this.contextMenuService.onDidShowContextMenu(() => update(true)));
230
this._register(this.contextMenuService.onDidHideContextMenu(() => update(false)));
231
}
232
}
233
234
export class BrowserWindow extends BaseWindow {
235
236
constructor(
237
@IOpenerService private readonly openerService: IOpenerService,
238
@ILifecycleService private readonly lifecycleService: BrowserLifecycleService,
239
@IDialogService private readonly dialogService: IDialogService,
240
@ILabelService private readonly labelService: ILabelService,
241
@IProductService private readonly productService: IProductService,
242
@IBrowserWorkbenchEnvironmentService private readonly browserEnvironmentService: IBrowserWorkbenchEnvironmentService,
243
@IWorkbenchLayoutService layoutService: IWorkbenchLayoutService,
244
@IInstantiationService private readonly instantiationService: IInstantiationService,
245
@IHostService hostService: IHostService,
246
@IContextMenuService contextMenuService: IContextMenuService,
247
) {
248
super(mainWindow, undefined, hostService, browserEnvironmentService, contextMenuService, layoutService);
249
250
this.registerListeners();
251
this.create();
252
}
253
254
private registerListeners(): void {
255
256
// Lifecycle
257
this._register(this.lifecycleService.onWillShutdown(() => this.onWillShutdown()));
258
259
// Layout
260
const viewport = isIOS && mainWindow.visualViewport ? mainWindow.visualViewport /** Visual viewport */ : mainWindow /** Layout viewport */;
261
this._register(addDisposableListener(viewport, EventType.RESIZE, () => {
262
this.layoutService.layout();
263
264
// Sometimes the keyboard appearing scrolls the whole workbench out of view, as a workaround scroll back into view #121206
265
if (isIOS) {
266
mainWindow.scrollTo(0, 0);
267
}
268
}));
269
270
// Prevent the back/forward gestures in macOS
271
this._register(addDisposableListener(this.layoutService.mainContainer, EventType.WHEEL, e => e.preventDefault(), { passive: false }));
272
273
// Prevent native context menus in web
274
this._register(addDisposableListener(this.layoutService.mainContainer, EventType.CONTEXT_MENU, e => EventHelper.stop(e, true)));
275
276
// Prevent default navigation on drop
277
this._register(addDisposableListener(this.layoutService.mainContainer, EventType.DROP, e => EventHelper.stop(e, true)));
278
}
279
280
private onWillShutdown(): void {
281
282
// Try to detect some user interaction with the workbench
283
// when shutdown has happened to not show the dialog e.g.
284
// when navigation takes a longer time.
285
Event.toPromise(Event.any(
286
Event.once(new DomEmitter(mainWindow.document.body, EventType.KEY_DOWN, true).event),
287
Event.once(new DomEmitter(mainWindow.document.body, EventType.MOUSE_DOWN, true).event)
288
)).then(async () => {
289
290
// Delay the dialog in case the user interacted
291
// with the page before it transitioned away
292
await timeout(3000);
293
294
// This should normally not happen, but if for some reason
295
// the workbench was shutdown while the page is still there,
296
// inform the user that only a reload can bring back a working
297
// state.
298
await this.dialogService.prompt({
299
type: Severity.Error,
300
message: localize('shutdownError', "An unexpected error occurred that requires a reload of this page."),
301
detail: localize('shutdownErrorDetail', "The workbench was unexpectedly disposed while running."),
302
buttons: [
303
{
304
label: localize({ key: 'reload', comment: ['&& denotes a mnemonic'] }, "&&Reload"),
305
run: () => mainWindow.location.reload() // do not use any services at this point since they are likely not functional at this point
306
}
307
]
308
});
309
});
310
}
311
312
private create(): void {
313
314
// Handle open calls
315
this.setupOpenHandlers();
316
317
// Label formatting
318
this.registerLabelFormatters();
319
320
// Commands
321
this.registerCommands();
322
323
// Smoke Test Driver
324
this.setupDriver();
325
}
326
327
private setupDriver(): void {
328
if (this.environmentService.enableSmokeTestDriver) {
329
registerWindowDriver(this.instantiationService);
330
}
331
}
332
333
private setupOpenHandlers(): void {
334
335
// We need to ignore the `beforeunload` event while
336
// we handle external links to open specifically for
337
// the case of application protocols that e.g. invoke
338
// vscode itself. We do not want to open these links
339
// in a new window because that would leave a blank
340
// window to the user, but using `window.location.href`
341
// will trigger the `beforeunload`.
342
this.openerService.setDefaultExternalOpener({
343
openExternal: async (href: string) => {
344
let isAllowedOpener = false;
345
if (this.browserEnvironmentService.options?.openerAllowedExternalUrlPrefixes) {
346
for (const trustedPopupPrefix of this.browserEnvironmentService.options.openerAllowedExternalUrlPrefixes) {
347
if (href.startsWith(trustedPopupPrefix)) {
348
isAllowedOpener = true;
349
break;
350
}
351
}
352
}
353
354
// HTTP(s): open in new window and deal with potential popup blockers
355
if (matchesScheme(href, Schemas.http) || matchesScheme(href, Schemas.https)) {
356
if (isSafari) {
357
const opened = windowOpenWithSuccess(href, !isAllowedOpener);
358
if (!opened) {
359
await this.dialogService.prompt({
360
type: Severity.Warning,
361
message: localize('unableToOpenExternal', "The browser blocked opening a new tab or window. Press 'Retry' to try again."),
362
custom: {
363
markdownDetails: [{ markdown: new MarkdownString(localize('unableToOpenWindowDetail', "Please allow pop-ups for this website in your [browser settings]({0}).", 'https://aka.ms/allow-vscode-popup'), true) }]
364
},
365
buttons: [
366
{
367
label: localize({ key: 'retry', comment: ['&& denotes a mnemonic'] }, "&&Retry"),
368
run: () => isAllowedOpener ? windowOpenPopup(href) : windowOpenNoOpener(href)
369
}
370
],
371
cancelButton: true
372
});
373
}
374
} else {
375
if (isAllowedOpener) {
376
windowOpenPopup(href);
377
} else {
378
windowOpenNoOpener(href);
379
}
380
}
381
}
382
383
// Anything else: set location to trigger protocol handler in the browser
384
// but make sure to signal this as an expected unload and disable unload
385
// handling explicitly to prevent the workbench from going down.
386
else {
387
const invokeProtocolHandler = () => {
388
this.lifecycleService.withExpectedShutdown({ disableShutdownHandling: true }, () => mainWindow.location.href = href);
389
};
390
391
invokeProtocolHandler();
392
393
const showProtocolUrlOpenedDialog = async () => {
394
const { downloadUrl } = this.productService;
395
let detail: string;
396
397
const buttons: IPromptButton<void>[] = [
398
{
399
label: localize({ key: 'openExternalDialogButtonRetry.v2', comment: ['&& denotes a mnemonic'] }, "&&Try Again"),
400
run: () => invokeProtocolHandler()
401
}
402
];
403
404
if (downloadUrl !== undefined) {
405
detail = localize(
406
'openExternalDialogDetail.v2',
407
"We launched {0} on your computer.\n\nIf {1} did not launch, try again or install it below.",
408
this.productService.nameLong,
409
this.productService.nameLong
410
);
411
412
buttons.push({
413
label: localize({ key: 'openExternalDialogButtonInstall.v3', comment: ['&& denotes a mnemonic'] }, "&&Install"),
414
run: async () => {
415
await this.openerService.open(URI.parse(downloadUrl));
416
417
// Re-show the dialog so that the user can come back after installing and try again
418
showProtocolUrlOpenedDialog();
419
}
420
});
421
} else {
422
detail = localize(
423
'openExternalDialogDetailNoInstall',
424
"We launched {0} on your computer.\n\nIf {1} did not launch, try again below.",
425
this.productService.nameLong,
426
this.productService.nameLong
427
);
428
}
429
430
// While this dialog shows, closing the tab will not display a confirmation dialog
431
// to avoid showing the user two dialogs at once
432
await this.hostService.withExpectedShutdown(() => this.dialogService.prompt({
433
type: Severity.Info,
434
message: localize('openExternalDialogTitle', "All done. You can close this tab now."),
435
detail,
436
buttons,
437
cancelButton: true
438
}));
439
};
440
441
// We cannot know whether the protocol handler succeeded.
442
// Display guidance in case it did not, e.g. the app is not installed locally.
443
if (matchesScheme(href, this.productService.urlProtocol)) {
444
await showProtocolUrlOpenedDialog();
445
}
446
}
447
448
return true;
449
}
450
});
451
}
452
453
private registerLabelFormatters(): void {
454
this._register(this.labelService.registerFormatter({
455
scheme: Schemas.vscodeUserData,
456
priority: true,
457
formatting: {
458
label: '(Settings) ${path}',
459
separator: '/',
460
}
461
}));
462
}
463
464
private registerCommands(): void {
465
466
// Allow extensions to request USB devices in Web
467
CommandsRegistry.registerCommand('workbench.experimental.requestUsbDevice', async (_accessor: ServicesAccessor, options?: { filters?: unknown[] }): Promise<UsbDeviceData | undefined> => {
468
return requestUsbDevice(options);
469
});
470
471
// Allow extensions to request Serial devices in Web
472
CommandsRegistry.registerCommand('workbench.experimental.requestSerialPort', async (_accessor: ServicesAccessor, options?: { filters?: unknown[] }): Promise<SerialPortData | undefined> => {
473
return requestSerialPort(options);
474
});
475
476
// Allow extensions to request HID devices in Web
477
CommandsRegistry.registerCommand('workbench.experimental.requestHidDevice', async (_accessor: ServicesAccessor, options?: { filters?: unknown[] }): Promise<HidDeviceData | undefined> => {
478
return requestHidDevice(options);
479
});
480
}
481
}
482
483