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