window.ts 33.5 KB
Newer Older
E
Erich Gamma 已提交
1 2 3 4 5
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

J
Joao Moreno 已提交
6 7
import * as path from 'path';
import * as objects from 'vs/base/common/objects';
8
import * as nls from 'vs/nls';
9
import { URI } from 'vs/base/common/uri';
B
Benjamin Pasero 已提交
10
import { IStateService } from 'vs/platform/state/common/state';
11
import { screen, BrowserWindow, systemPreferences, app, TouchBar, nativeImage } from 'electron';
J
Joao Moreno 已提交
12
import { IEnvironmentService, ParsedArgs } from 'vs/platform/environment/common/environment';
13
import { ILogService } from 'vs/platform/log/common/log';
14
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
J
Joao Moreno 已提交
15
import { parseArgs } from 'vs/platform/environment/node/argv';
16
import product from 'vs/platform/node/product';
B
Benjamin Pasero 已提交
17
import { IWindowSettings, MenuBarVisibility, IWindowConfiguration, ReadyState, IRunActionInWindowRequest, getTitleBarStyle } from 'vs/platform/windows/common/windows';
B
Benjamin Pasero 已提交
18
import { Disposable, toDisposable } from 'vs/base/common/lifecycle';
B
Benjamin Pasero 已提交
19
import { isLinux, isMacintosh, isWindows } from 'vs/base/common/platform';
20
import { ICodeWindow, IWindowState, WindowMode } from 'vs/platform/windows/electron-main/windows';
21
import { IWorkspaceIdentifier, IWorkspacesMainService } from 'vs/platform/workspaces/common/workspaces';
B
Benjamin Pasero 已提交
22
import { IBackupMainService } from 'vs/platform/backup/common/backup';
B
Benjamin Pasero 已提交
23
import { ISerializableCommandAction } from 'vs/platform/actions/common/actions';
24
import * as perf from 'vs/base/common/performance';
25
import { resolveMarketplaceHeaders } from 'vs/platform/extensionManagement/node/extensionGalleryService';
26
import { getBackgroundColor } from 'vs/code/electron-main/theme';
27
import { IStorageMainService } from 'vs/platform/storage/node/storageMainService';
B
Benjamin Pasero 已提交
28
import { RunOnceScheduler } from 'vs/base/common/async';
29

30 31
export interface IWindowCreationOptions {
	state: IWindowState;
32
	extensionDevelopmentPath?: string;
33
	isExtensionTestHost?: boolean;
34 35
}

B
Benjamin Pasero 已提交
36
export const defaultWindowState = function (mode = WindowMode.Normal): IWindowState {
E
Erich Gamma 已提交
37 38 39
	return {
		width: 1024,
		height: 768,
40
		mode
E
Erich Gamma 已提交
41 42 43
	};
};

B
Benjamin Pasero 已提交
44 45 46 47 48 49 50 51
interface IWorkbenchEditorConfiguration {
	workbench: {
		editor: {
			swipeToNavigate: boolean
		}
	};
}

B
Benjamin Pasero 已提交
52 53 54 55
interface ITouchBarSegment extends Electron.SegmentedControlSegment {
	id: string;
}

B
Benjamin Pasero 已提交
56
export class CodeWindow extends Disposable implements ICodeWindow {
E
Erich Gamma 已提交
57

58 59
	private static readonly MIN_WIDTH = 200;
	private static readonly MIN_HEIGHT = 120;
E
Erich Gamma 已提交
60

B
Benjamin Pasero 已提交
61
	private static readonly MAX_URL_LENGTH = 2 * 1024 * 1024; // https://cs.chromium.org/chromium/src/url/url_constants.cc?l=32
62

B
Benjamin Pasero 已提交
63
	private hiddenTitleBarStyle: boolean;
E
Erich Gamma 已提交
64
	private showTimeoutHandle: any;
B
Benjamin Pasero 已提交
65
	private _id: number;
66
	private _win: Electron.BrowserWindow;
E
Erich Gamma 已提交
67 68 69
	private _lastFocusTime: number;
	private _readyState: ReadyState;
	private windowState: IWindowState;
70
	private currentMenuBarVisibility: MenuBarVisibility;
71
	private representedFilename: string;
E
Erich Gamma 已提交
72

B
Benjamin Pasero 已提交
73
	private whenReadyCallbacks: { (window: ICodeWindow): void }[];
E
Erich Gamma 已提交
74 75

	private currentConfig: IWindowConfiguration;
76
	private pendingLoadConfig?: IWindowConfiguration;
E
Erich Gamma 已提交
77

J
Johannes Rieken 已提交
78
	private marketplaceHeadersPromise: Promise<object>;
79

B
Benjamin Pasero 已提交
80
	private touchBarGroups: Electron.TouchBarSegmentedControl[];
81

J
Joao Moreno 已提交
82 83
	constructor(
		config: IWindowCreationOptions,
84 85 86 87 88 89 90
		@ILogService private readonly logService: ILogService,
		@IEnvironmentService private readonly environmentService: IEnvironmentService,
		@IConfigurationService private readonly configurationService: IConfigurationService,
		@IStateService private readonly stateService: IStateService,
		@IWorkspacesMainService private readonly workspacesMainService: IWorkspacesMainService,
		@IBackupMainService private readonly backupMainService: IBackupMainService,
		@IStorageMainService private readonly storageMainService: IStorageMainService
J
Joao Moreno 已提交
91
	) {
B
Benjamin Pasero 已提交
92 93
		super();

B
Benjamin Pasero 已提交
94
		this.touchBarGroups = [];
E
Erich Gamma 已提交
95 96 97 98
		this._lastFocusTime = -1;
		this._readyState = ReadyState.NONE;
		this.whenReadyCallbacks = [];

B
Benjamin Pasero 已提交
99 100 101 102 103 104
		// create browser window
		this.createBrowserWindow(config);

		// respect configured menu bar visibility
		this.onConfigurationUpdated();

B
Benjamin Pasero 已提交
105 106 107
		// macOS: touch bar support
		this.createTouchBar();

108 109 110
		// Request handling
		this.handleMarketplaceRequests();

B
Benjamin Pasero 已提交
111 112 113 114 115 116
		// Eventing
		this.registerListeners();
	}

	private createBrowserWindow(config: IWindowCreationOptions): void {

E
Erich Gamma 已提交
117
		// Load window state
B
Benjamin Pasero 已提交
118
		this.windowState = this.restoreWindowState(config.state);
E
Erich Gamma 已提交
119

120
		// in case we are maximized or fullscreen, only show later after the call to maximize/fullscreen (see below)
B
Benjamin Pasero 已提交
121
		const isFullscreenOrMaximized = (this.windowState.mode === WindowMode.Maximized || this.windowState.mode === WindowMode.Fullscreen);
122

123
		const options: Electron.BrowserWindowConstructorOptions = {
E
Erich Gamma 已提交
124 125 126 127
			width: this.windowState.width,
			height: this.windowState.height,
			x: this.windowState.x,
			y: this.windowState.y,
128
			backgroundColor: getBackgroundColor(this.stateService),
B
Benjamin Pasero 已提交
129 130
			minWidth: CodeWindow.MIN_WIDTH,
			minHeight: CodeWindow.MIN_HEIGHT,
131
			show: !isFullscreenOrMaximized,
B
Benjamin Pasero 已提交
132
			title: product.nameLong,
133
			webPreferences: {
B
Benjamin Pasero 已提交
134 135 136 137
				// By default if Code is in the background, intervals and timeouts get throttled, so we
				// want to enforce that Code stays in the foreground. This triggers a disable_hidden_
				// flag that Electron provides via patch:
				// https://github.com/electron/libchromiumcontent/blob/master/patches/common/chromium/disable_hidden.patch
138
				'backgroundThrottling': false
139
			}
E
Erich Gamma 已提交
140 141
		};

142
		if (isLinux) {
143
			options.icon = path.join(this.environmentService.appRoot, 'resources/linux/code.png'); // Windows and Mac are better off using the embedded icon(s)
E
Erich Gamma 已提交
144 145
		}

146
		const windowConfig = this.configurationService.getValue<IWindowSettings>('window');
147

148 149 150 151
		if (isMacintosh && !this.useNativeFullScreen()) {
			options.fullscreenable = false; // enables simple fullscreen mode
		}

152 153 154 155 156 157 158 159 160
		if (isMacintosh) {
			options.acceptFirstMouse = true; // enabled by default

			if (windowConfig && windowConfig.clickThroughInactive === false) {
				options.acceptFirstMouse = false;
			}
		}

		if (isMacintosh && windowConfig && windowConfig.nativeTabs === true) {
161 162 163
			options.tabbingIdentifier = product.nameShort; // this opts in to sierra tabs
		}

B
Benjamin Pasero 已提交
164
		const useCustomTitleStyle = getTitleBarStyle(this.configurationService, this.environmentService, !!config.extensionDevelopmentPath) === 'custom';
165 166 167
		if (useCustomTitleStyle) {
			options.titleBarStyle = 'hidden';
			this.hiddenTitleBarStyle = true;
R
Ryan Adolf 已提交
168
			if (!isMacintosh) {
R
Ryan Adolf 已提交
169 170
				options.frame = false;
			}
171 172
		}

E
Erich Gamma 已提交
173 174
		// Create the browser window.
		this._win = new BrowserWindow(options);
B
Benjamin Pasero 已提交
175
		this._id = this._win.id;
E
Erich Gamma 已提交
176

B
Benjamin Pasero 已提交
177
		if (isMacintosh && useCustomTitleStyle) {
178 179 180
			this._win.setSheetOffset(22); // offset dialogs by the height of the custom title bar if we have any
		}

181
		if (isFullscreenOrMaximized) {
B
Benjamin Pasero 已提交
182
			this._win.maximize();
E
Erich Gamma 已提交
183

B
Benjamin Pasero 已提交
184
			if (this.windowState.mode === WindowMode.Fullscreen) {
185
				this.setFullScreen(true);
186 187
			}

B
Benjamin Pasero 已提交
188 189
			if (!this._win.isVisible()) {
				this._win.show(); // to reduce flicker from the default window size to maximize, we only show after maximize
E
Erich Gamma 已提交
190 191 192
			}
		}

193
		this._lastFocusTime = Date.now(); // since we show directly, we need to set the last focus time too
E
Erich Gamma 已提交
194 195
	}

B
Benjamin Pasero 已提交
196
	hasHiddenTitleBarStyle(): boolean {
B
Benjamin Pasero 已提交
197
		return this.hiddenTitleBarStyle;
198 199
	}

B
Benjamin Pasero 已提交
200
	get isExtensionDevelopmentHost(): boolean {
201
		return !!this.config.extensionDevelopmentPath;
202 203
	}

B
Benjamin Pasero 已提交
204
	get isExtensionTestHost(): boolean {
205
		return !!this.config.extensionTestsPath;
206 207
	}

208
	get extensionDevelopmentPath(): string | undefined {
209
		return this.config.extensionDevelopmentPath;
E
Erich Gamma 已提交
210 211
	}

B
Benjamin Pasero 已提交
212
	get config(): IWindowConfiguration {
E
Erich Gamma 已提交
213 214 215
		return this.currentConfig;
	}

B
Benjamin Pasero 已提交
216
	get id(): number {
B
Benjamin Pasero 已提交
217 218 219
		return this._id;
	}

B
Benjamin Pasero 已提交
220
	get win(): Electron.BrowserWindow {
E
Erich Gamma 已提交
221 222 223
		return this._win;
	}

B
Benjamin Pasero 已提交
224
	setRepresentedFilename(filename: string): void {
225
		if (isMacintosh) {
226 227 228 229 230 231
			this.win.setRepresentedFilename(filename);
		} else {
			this.representedFilename = filename;
		}
	}

B
Benjamin Pasero 已提交
232
	getRepresentedFilename(): string {
233
		if (isMacintosh) {
234 235 236 237 238 239
			return this.win.getRepresentedFilename();
		}

		return this.representedFilename;
	}

B
Benjamin Pasero 已提交
240
	focus(): void {
E
Erich Gamma 已提交
241 242 243 244
		if (!this._win) {
			return;
		}

B
Benjamin Pasero 已提交
245 246
		if (this._win.isMinimized()) {
			this._win.restore();
E
Erich Gamma 已提交
247 248
		}

B
Benjamin Pasero 已提交
249
		this._win.focus();
E
Erich Gamma 已提交
250 251
	}

B
Benjamin Pasero 已提交
252
	get lastFocusTime(): number {
E
Erich Gamma 已提交
253 254 255
		return this._lastFocusTime;
	}

256
	get backupPath(): string | undefined {
R
Rob Lourens 已提交
257
		return this.currentConfig ? this.currentConfig.backupPath : undefined;
E
Erich Gamma 已提交
258 259
	}

260
	get openedWorkspace(): IWorkspaceIdentifier | undefined {
R
Rob Lourens 已提交
261
		return this.currentConfig ? this.currentConfig.workspace : undefined;
262 263
	}

264
	get openedFolderUri(): URI | undefined {
R
Rob Lourens 已提交
265
		return this.currentConfig ? this.currentConfig.folderUri : undefined;
266 267
	}

268
	get remoteAuthority(): string | undefined {
R
Rob Lourens 已提交
269
		return this.currentConfig ? this.currentConfig.remoteAuthority : undefined;
M
Martin Aeschlimann 已提交
270 271
	}

B
Benjamin Pasero 已提交
272
	setReady(): void {
E
Erich Gamma 已提交
273 274 275 276
		this._readyState = ReadyState.READY;

		// inform all waiting promises that we are ready now
		while (this.whenReadyCallbacks.length) {
277
			this.whenReadyCallbacks.pop()!(this);
E
Erich Gamma 已提交
278 279 280
		}
	}

J
Johannes Rieken 已提交
281
	ready(): Promise<ICodeWindow> {
B
Benjamin Pasero 已提交
282 283 284
		return new Promise<ICodeWindow>(resolve => {
			if (this.isReady) {
				return resolve(this);
E
Erich Gamma 已提交
285 286 287
			}

			// otherwise keep and call later when we are ready
B
Benjamin Pasero 已提交
288
			this.whenReadyCallbacks.push(resolve);
E
Erich Gamma 已提交
289 290 291
		});
	}

B
Benjamin Pasero 已提交
292 293
	get isReady(): boolean {
		return this._readyState === ReadyState.READY;
E
Erich Gamma 已提交
294 295
	}

296 297 298 299
	private handleMarketplaceRequests(): void {

		// Resolve marketplace headers
		this.marketplaceHeadersPromise = resolveMarketplaceHeaders(this.environmentService);
E
Erich Gamma 已提交
300

301 302
		// Inject headers when requests are incoming
		const urls = ['https://marketplace.visualstudio.com/*', 'https://*.vsassets.io/*'];
M
Matt Bierner 已提交
303
		this._win.webContents.session.webRequest.onBeforeSendHeaders({ urls }, (details: any, cb: any) => {
304
			this.marketplaceHeadersPromise.then(headers => {
305 306
				cb({ cancel: false, requestHeaders: objects.assign(details.requestHeaders, headers) });
			});
307
		});
308 309 310
	}

	private registerListeners(): void {
311 312

		// Prevent loading of svgs
313
		this._win.webContents.session.webRequest.onBeforeRequest(null!, (details, callback) => {
314 315 316 317 318 319 320 321 322 323
			if (details.url.indexOf('.svg') > 0) {
				const uri = URI.parse(details.url);
				if (uri && !uri.scheme.match(/file/i) && (uri.path as any).endsWith('.svg')) {
					return callback({ cancel: true });
				}
			}

			return callback({});
		});

324
		this._win.webContents.session.webRequest.onHeadersReceived(null!, (details: any, callback: any) => {
325 326 327 328 329 330 331 332
			const contentType: string[] = (details.responseHeaders['content-type'] || details.responseHeaders['Content-Type']) as any;
			if (contentType && Array.isArray(contentType) && contentType.some(x => x.toLowerCase().indexOf('image/svg') >= 0)) {
				return callback({ cancel: true });
			}

			return callback({ cancel: false, responseHeaders: details.responseHeaders });
		});

E
Erich Gamma 已提交
333 334 335 336 337 338 339 340
		// Remember that we loaded
		this._win.webContents.on('did-finish-load', () => {
			this._readyState = ReadyState.LOADING;

			// Associate properties from the load request if provided
			if (this.pendingLoadConfig) {
				this.currentConfig = this.pendingLoadConfig;

341
				this.pendingLoadConfig = undefined;
E
Erich Gamma 已提交
342 343
			}

B
Benjamin Pasero 已提交
344
			// To prevent flashing, we set the window visible after the page has finished to load but before Code is loaded
B
Benjamin Pasero 已提交
345
			if (this._win && !this._win.isVisible()) {
B
Benjamin Pasero 已提交
346
				if (this.windowState.mode === WindowMode.Maximized) {
B
Benjamin Pasero 已提交
347
					this._win.maximize();
E
Erich Gamma 已提交
348 349
				}

B
Benjamin Pasero 已提交
350 351
				if (!this._win.isVisible()) { // maximize also makes visible
					this._win.show();
E
Erich Gamma 已提交
352 353 354 355 356
				}
			}
		});

		// App commands support
357
		this.registerNavigationListenerOn('app-command', 'browser-backward', 'browser-forward', false);
E
Erich Gamma 已提交
358 359 360

		// Window Focus
		this._win.on('focus', () => {
B
Benjamin Pasero 已提交
361
			this._lastFocusTime = Date.now();
E
Erich Gamma 已提交
362 363
		});

B
Benjamin Pasero 已提交
364 365 366
		// Simple fullscreen doesn't resize automatically when the resolution changes so as a workaround
		// we need to detect when display metrics change or displays are added/removed and toggle the
		// fullscreen manually.
B
Benjamin Pasero 已提交
367
		if (isMacintosh) {
B
Benjamin Pasero 已提交
368 369 370 371 372 373
			const simpleFullScreenScheduler = this._register(new RunOnceScheduler(() => {
				if (!this._win) {
					return; // disposed
				}

				if (!this.useNativeFullScreen() && this.isFullScreen()) {
B
Benjamin Pasero 已提交
374 375 376
					this.setFullScreen(false);
					this.setFullScreen(true);
				}
B
Benjamin Pasero 已提交
377 378 379 380 381 382 383 384 385
			}, 100));

			const displayChangedListener = () => simpleFullScreenScheduler.schedule();

			screen.on('display-metrics-changed', displayChangedListener);
			this._register(toDisposable(() => screen.removeListener('display-metrics-changed', displayChangedListener)));

			screen.on('display-added', displayChangedListener);
			this._register(toDisposable(() => screen.removeListener('display-added', displayChangedListener)));
B
Benjamin Pasero 已提交
386

B
Benjamin Pasero 已提交
387 388
			screen.on('display-removed', displayChangedListener);
			this._register(toDisposable(() => screen.removeListener('display-removed', displayChangedListener)));
B
Benjamin Pasero 已提交
389
		}
390

S
SteVen Batten 已提交
391
		// Window (Un)Maximize
B
Benjamin Pasero 已提交
392
		this._win.on('maximize', e => {
B
Benjamin Pasero 已提交
393 394 395 396
			if (this.currentConfig) {
				this.currentConfig.maximized = true;
			}

397 398 399
			app.emit('browser-window-maximize', e, this._win);
		});

B
Benjamin Pasero 已提交
400
		this._win.on('unmaximize', e => {
B
Benjamin Pasero 已提交
401 402 403 404
			if (this.currentConfig) {
				this.currentConfig.maximized = false;
			}

405 406
			app.emit('browser-window-unmaximize', e, this._win);
		});
S
SteVen Batten 已提交
407

408 409 410 411 412 413 414 415 416
		// Window Fullscreen
		this._win.on('enter-full-screen', () => {
			this.sendWhenReady('vscode:enterFullScreen');
		});

		this._win.on('leave-full-screen', () => {
			this.sendWhenReady('vscode:leaveFullScreen');
		});

E
Erich Gamma 已提交
417
		// Window Failed to load
418
		this._win.webContents.on('did-fail-load', (event: Electron.Event, errorCode: number, errorDescription: string, validatedURL: string, isMainFrame: boolean) => {
B
Benjamin Pasero 已提交
419
			this.logService.warn('[electron event]: fail to load, ', errorDescription);
E
Erich Gamma 已提交
420 421
		});

B
Benjamin Pasero 已提交
422
		// Handle configuration changes
B
Benjamin Pasero 已提交
423
		this._register(this.configurationService.onDidChangeConfiguration(e => this.onConfigurationUpdated()));
424

425
		// Handle Workspace events
B
Benjamin Pasero 已提交
426
		this._register(this.workspacesMainService.onUntitledWorkspaceDeleted(e => this.onUntitledWorkspaceDeleted(e)));
427 428
	}

429
	private onUntitledWorkspaceDeleted(workspace: IWorkspaceIdentifier): void {
430 431 432

		// Make sure to update our workspace config if we detect that it
		// was deleted
433
		if (this.openedWorkspace && this.openedWorkspace.id === workspace.id) {
R
Rob Lourens 已提交
434
			this.currentConfig.workspace = undefined;
435
		}
E
Erich Gamma 已提交
436 437
	}

B
Benjamin Pasero 已提交
438
	private onConfigurationUpdated(): void {
439
		const newMenuBarVisibility = this.getMenuBarVisibility();
B
Benjamin Pasero 已提交
440 441 442 443
		if (newMenuBarVisibility !== this.currentMenuBarVisibility) {
			this.currentMenuBarVisibility = newMenuBarVisibility;
			this.setMenuBarVisibility(newMenuBarVisibility);
		}
444 445

		// Swipe command support (macOS)
446
		if (isMacintosh) {
447
			const config = this.configurationService.getValue<IWorkbenchEditorConfiguration>();
B
Benjamin Pasero 已提交
448
			if (config && config.workbench && config.workbench.editor && config.workbench.editor.swipeToNavigate) {
449
				this.registerNavigationListenerOn('swipe', 'left', 'right', true);
B
Benjamin Pasero 已提交
450 451 452
			} else {
				this._win.removeAllListeners('swipe');
			}
453
		}
454
	}
B
Benjamin Pasero 已提交
455

B
Benjamin Pasero 已提交
456
	private registerNavigationListenerOn(command: 'swipe' | 'app-command', back: 'left' | 'browser-backward', forward: 'right' | 'browser-forward', acrossEditors: boolean) {
457
		this._win.on(command as 'swipe' /* | 'app-command' */, (e: Electron.Event, cmd: string) => {
B
Benjamin Pasero 已提交
458
			if (!this.isReady) {
B
Benjamin Pasero 已提交
459 460 461 462
				return; // window must be ready
			}

			if (cmd === back) {
463
				this.send('vscode:runAction', { id: acrossEditors ? 'workbench.action.openPreviousRecentlyUsedEditor' : 'workbench.action.navigateBack', from: 'mouse' } as IRunActionInWindowRequest);
B
Benjamin Pasero 已提交
464
			} else if (cmd === forward) {
465
				this.send('vscode:runAction', { id: acrossEditors ? 'workbench.action.openNextRecentlyUsedEditor' : 'workbench.action.navigateForward', from: 'mouse' } as IRunActionInWindowRequest);
B
Benjamin Pasero 已提交
466 467 468 469
			}
		});
	}

470 471 472 473 474 475
	addTabbedWindow(window: ICodeWindow): void {
		if (isMacintosh) {
			this._win.addTabbedWindow(window.win);
		}
	}

B
Benjamin Pasero 已提交
476
	load(config: IWindowConfiguration, isReload?: boolean, disableExtensions?: boolean): void {
E
Erich Gamma 已提交
477 478 479

		// If this is the first time the window is loaded, we associate the paths
		// directly with the window because we assume the loading will just work
B
Benjamin Pasero 已提交
480
		if (this._readyState === ReadyState.NONE) {
E
Erich Gamma 已提交
481 482 483 484 485 486 487 488 489 490 491 492
			this.currentConfig = config;
		}

		// Otherwise, the window is currently showing a folder and if there is an
		// unload handler preventing the load, we cannot just associate the paths
		// because the loading might be vetoed. Instead we associate it later when
		// the window load event has fired.
		else {
			this.pendingLoadConfig = config;
			this._readyState = ReadyState.NAVIGATING;
		}

493 494 495 496 497 498 499
		// Add disable-extensions to the config, but do not preserve it on currentConfig or
		// pendingLoadConfig so that it is applied only on this load
		const configuration = objects.assign({}, config);
		if (disableExtensions !== undefined) {
			configuration['disable-extensions'] = disableExtensions;
		}

500
		// Clear Document Edited if needed
501
		if (isMacintosh && this._win.isDocumentEdited()) {
B
Benjamin Pasero 已提交
502
			if (!isReload || !this.backupMainService.isHotExitEnabled()) {
503 504 505 506 507 508 509 510 511 512 513
				this._win.setDocumentEdited(false);
			}
		}

		// Clear Title and Filename if needed
		if (!isReload) {
			if (this.getRepresentedFilename()) {
				this.setRepresentedFilename('');
			}

			this._win.setTitle(product.nameLong);
514 515
		}

E
Erich Gamma 已提交
516
		// Load URL
517
		perf.mark('main:loadWindow');
518
		this._win.loadURL(this.getUrl(configuration));
E
Erich Gamma 已提交
519 520

		// Make window visible if it did not open in N seconds because this indicates an error
521 522
		// Only do this when running out of sources and not when running tests
		if (!this.environmentService.isBuilt && !this.environmentService.extensionTestsPath) {
E
Erich Gamma 已提交
523 524 525 526
			this.showTimeoutHandle = setTimeout(() => {
				if (this._win && !this._win.isVisible() && !this._win.isMinimized()) {
					this._win.show();
					this._win.focus();
527
					this._win.webContents.openDevTools();
E
Erich Gamma 已提交
528 529 530 531 532
				}
			}, 10000);
		}
	}

533
	reload(configurationIn?: IWindowConfiguration, cli?: ParsedArgs): void {
B
Benjamin Pasero 已提交
534

535
		// If config is not provided, copy our current one
536
		const configuration = configurationIn ? configurationIn : objects.mixin({}, this.currentConfig);
537 538

		// Delete some properties we do not want during reload
E
Erich Gamma 已提交
539 540
		delete configuration.filesToOpen;
		delete configuration.filesToCreate;
541
		delete configuration.filesToDiff;
542
		delete configuration.filesToWait;
543

E
Erich Gamma 已提交
544
		// Some configuration things get inherited if the window is being reloaded and we are
B
Benjamin Pasero 已提交
545
		// in extension development mode. These options are all development related.
546
		if (this.isExtensionDevelopmentHost && cli) {
B
Benjamin Pasero 已提交
547
			configuration.verbose = cli.verbose;
548
			configuration.debugPluginHost = cli.debugPluginHost;
549
			configuration.debugBrkPluginHost = cli.debugBrkPluginHost;
550
			configuration.debugId = cli.debugId;
551
			configuration['extensions-dir'] = cli['extensions-dir'];
E
Erich Gamma 已提交
552 553
		}

554 555
		configuration.isInitialStartup = false; // since this is a reload

E
Erich Gamma 已提交
556
		// Load config
557 558
		const disableExtensions = cli ? cli['disable-extensions'] : undefined;
		this.load(configuration, true, disableExtensions);
E
Erich Gamma 已提交
559 560
	}

561
	private getUrl(windowConfiguration: IWindowConfiguration): string {
E
Erich Gamma 已提交
562

563 564
		// Set window ID
		windowConfiguration.windowId = this._win.id;
S
Sandeep Somavarapu 已提交
565
		windowConfiguration.logLevel = this.logService.getLevel();
566

567
		// Set zoomlevel
568
		const windowConfig = this.configurationService.getValue<IWindowSettings>('window');
569
		const zoomLevel = windowConfig && windowConfig.zoomLevel;
570 571 572 573
		if (typeof zoomLevel === 'number') {
			windowConfiguration.zoomLevel = zoomLevel;
		}

574
		// Set fullscreen state
575
		windowConfiguration.fullscreen = this.isFullScreen();
576

577
		// Set Accessibility Config
578 579 580 581 582
		let autoDetectHighContrast = true;
		if (windowConfig && windowConfig.autoDetectHighContrast === false) {
			autoDetectHighContrast = false;
		}
		windowConfiguration.highContrast = isWindows && autoDetectHighContrast && systemPreferences.isInvertedColorScheme();
583 584
		windowConfiguration.accessibilitySupport = app.isAccessibilitySupportEnabled();

B
Benjamin Pasero 已提交
585 586
		// Title style related
		windowConfiguration.maximized = this._win.isMaximized();
587
		windowConfiguration.frameless = this.hasHiddenTitleBarStyle() && !isMacintosh;
M
Martin Aeschlimann 已提交
588

589 590
		// Dump Perf Counters
		windowConfiguration.perfEntries = perf.exportEntries();
591

592
		// Parts splash
R
Rob Lourens 已提交
593
		windowConfiguration.partsSplashData = this.storageMainService.get('parts-splash-data', undefined);
594

595 596 597
		// Config (combination of process.argv and window configuration)
		const environment = parseArgs(process.argv);
		const config = objects.assign(environment, windowConfiguration);
598
		for (let key in config) {
R
Rob Lourens 已提交
599
			if (config[key] === undefined || config[key] === null || config[key] === '' || config[key] === false) {
600 601 602
				delete config[key]; // only send over properties that have a true value
			}
		}
603

604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622
		// In the unlikely event of the URL becoming larger than 2MB, remove parts of
		// it that are not under our control. Mainly, the user environment can be very
		// large depending on user configuration, so we can only remove it in that case.
		let configUrl = this.doGetUrl(config);
		if (configUrl.length > CodeWindow.MAX_URL_LENGTH) {
			delete config.userEnv;
			this.logService.warn('Application URL exceeds maximum of 2MB and was shortened.');

			configUrl = this.doGetUrl(config);

			if (configUrl.length > CodeWindow.MAX_URL_LENGTH) {
				this.logService.error('Application URL exceeds maximum of 2MB and cannot be loaded.');
			}
		}

		return configUrl;
	}

	private doGetUrl(config: object): string {
623
		return `${require.toUrl('vs/code/electron-browser/workbench/workbench.html')}?config=${encodeURIComponent(JSON.stringify(config))}`;
E
Erich Gamma 已提交
624 625
	}

B
Benjamin Pasero 已提交
626
	serializeWindowState(): IWindowState {
627 628 629
		if (!this._win) {
			return defaultWindowState();
		}
630 631

		// fullscreen gets special treatment
632
		if (this.isFullScreen()) {
633 634
			const display = screen.getDisplayMatching(this.getBounds());

B
Benjamin Pasero 已提交
635 636 637
			const defaultState = defaultWindowState();

			const res = {
638
				mode: WindowMode.Fullscreen,
R
Rob Lourens 已提交
639
				display: display ? display.id : undefined,
640

B
Benjamin Pasero 已提交
641 642 643 644 645 646 647 648 649
				// Still carry over window dimensions from previous sessions
				// if we can compute it in fullscreen state.
				// does not seem possible in all cases on Linux for example
				// (https://github.com/Microsoft/vscode/issues/58218) so we
				// fallback to the defaults in that case.
				width: this.windowState.width || defaultState.width,
				height: this.windowState.height || defaultState.height,
				x: this.windowState.x || 0,
				y: this.windowState.y || 0
650
			};
B
Benjamin Pasero 已提交
651 652

			return res;
E
Erich Gamma 已提交
653 654
		}

655
		const state: IWindowState = Object.create(null);
E
Erich Gamma 已提交
656 657 658
		let mode: WindowMode;

		// get window mode
659
		if (!isMacintosh && this._win.isMaximized()) {
E
Erich Gamma 已提交
660 661 662 663 664 665 666 667
			mode = WindowMode.Maximized;
		} else {
			mode = WindowMode.Normal;
		}

		// we don't want to save minimized state, only maximized or normal
		if (mode === WindowMode.Maximized) {
			state.mode = WindowMode.Maximized;
668
		} else {
E
Erich Gamma 已提交
669 670 671 672 673
			state.mode = WindowMode.Normal;
		}

		// only consider non-minimized window states
		if (mode === WindowMode.Normal || mode === WindowMode.Maximized) {
674
			const bounds = this.getBounds();
E
Erich Gamma 已提交
675

676 677 678 679
			state.x = bounds.x;
			state.y = bounds.y;
			state.width = bounds.width;
			state.height = bounds.height;
E
Erich Gamma 已提交
680 681 682 683 684
		}

		return state;
	}

B
Benjamin Pasero 已提交
685
	private restoreWindowState(state?: IWindowState): IWindowState {
E
Erich Gamma 已提交
686 687
		if (state) {
			try {
688
				state = this.validateWindowState(state) || undefined;
E
Erich Gamma 已提交
689
			} catch (err) {
J
Joao Moreno 已提交
690
				this.logService.warn(`Unexpected error validating window state: ${err}\n${err.stack}`); // somehow display API can be picky about the state to validate
E
Erich Gamma 已提交
691 692 693 694 695 696 697
			}
		}

		if (!state) {
			state = defaultWindowState();
		}

B
Benjamin Pasero 已提交
698
		return state;
E
Erich Gamma 已提交
699 700
	}

701
	private validateWindowState(state: IWindowState): IWindowState | null {
E
Erich Gamma 已提交
702 703 704 705
		if (!state) {
			return null;
		}

706 707 708 709 710
		if (typeof state.x !== 'number'
			|| typeof state.y !== 'number'
			|| typeof state.width !== 'number'
			|| typeof state.height !== 'number'
		) {
E
Erich Gamma 已提交
711 712 713 714 715 716 717
			return null;
		}

		if (state.width <= 0 || state.height <= 0) {
			return null;
		}

718
		const displays = screen.getAllDisplays();
E
Erich Gamma 已提交
719 720 721

		// Single Monitor: be strict about x/y positioning
		if (displays.length === 1) {
722
			const displayBounds = displays[0].bounds;
E
Erich Gamma 已提交
723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757

			// Careful with maximized: in that mode x/y can well be negative!
			if (state.mode !== WindowMode.Maximized && displayBounds.width > 0 && displayBounds.height > 0 /* Linux X11 sessions sometimes report wrong display bounds */) {
				if (state.x < displayBounds.x) {
					state.x = displayBounds.x; // prevent window from falling out of the screen to the left
				}

				if (state.y < displayBounds.y) {
					state.y = displayBounds.y; // prevent window from falling out of the screen to the top
				}

				if (state.x > (displayBounds.x + displayBounds.width)) {
					state.x = displayBounds.x; // prevent window from falling out of the screen to the right
				}

				if (state.y > (displayBounds.y + displayBounds.height)) {
					state.y = displayBounds.y; // prevent window from falling out of the screen to the bottom
				}

				if (state.width > displayBounds.width) {
					state.width = displayBounds.width; // prevent window from exceeding display bounds width
				}

				if (state.height > displayBounds.height) {
					state.height = displayBounds.height; // prevent window from exceeding display bounds height
				}
			}

			if (state.mode === WindowMode.Maximized) {
				return defaultWindowState(WindowMode.Maximized); // when maximized, make sure we have good values when the user restores the window
			}

			return state;
		}

758 759 760 761 762 763 764 765 766 767 768 769 770
		// Multi Montior (fullscreen): try to find the previously used display
		if (state.display && state.mode === WindowMode.Fullscreen) {
			const display = displays.filter(d => d.id === state.display)[0];
			if (display && display.bounds && typeof display.bounds.x === 'number' && typeof display.bounds.y === 'number') {
				const defaults = defaultWindowState(WindowMode.Fullscreen); // make sure we have good values when the user restores the window
				defaults.x = display.bounds.x; // carefull to use displays x/y position so that the window ends up on the correct monitor
				defaults.y = display.bounds.y;

				return defaults;
			}
		}

		// Multi Monitor (non-fullscreen): be less strict because metrics can be crazy
771 772
		const bounds = { x: state.x, y: state.y, width: state.width, height: state.height };
		const display = screen.getDisplayMatching(bounds);
U
unknown 已提交
773 774 775 776 777 778 779
		if (
			display &&												// we have a display matching the desired bounds
			bounds.x < display.bounds.x + display.bounds.width &&	// prevent window from falling out of the screen to the right
			bounds.y < display.bounds.y + display.bounds.height &&	// prevent window from falling out of the screen to the bottom
			bounds.x + bounds.width > display.bounds.x &&			// prevent window from falling out of the screen to the left
			bounds.y + bounds.height > display.bounds.y				// prevent window from falling out of the scree nto the top
		) {
E
Erich Gamma 已提交
780
			if (state.mode === WindowMode.Maximized) {
781
				const defaults = defaultWindowState(WindowMode.Maximized); // when maximized, make sure we have good values when the user restores the window
E
Erich Gamma 已提交
782 783 784 785 786 787 788 789 790 791 792 793
				defaults.x = state.x; // carefull to keep x/y position so that the window ends up on the correct monitor
				defaults.y = state.y;

				return defaults;
			}

			return state;
		}

		return null;
	}

B
Benjamin Pasero 已提交
794
	getBounds(): Electron.Rectangle {
B
Benjamin Pasero 已提交
795 796
		const pos = this._win.getPosition();
		const dimension = this._win.getSize();
E
Erich Gamma 已提交
797 798 799 800

		return { x: pos[0], y: pos[1], width: dimension[0], height: dimension[1] };
	}

B
Benjamin Pasero 已提交
801
	toggleFullScreen(): void {
802 803
		this.setFullScreen(!this.isFullScreen());
	}
E
Erich Gamma 已提交
804

805
	private setFullScreen(fullscreen: boolean): void {
806

807 808 809 810 811 812 813 814 815 816 817
		// Set fullscreen state
		if (this.useNativeFullScreen()) {
			this.setNativeFullScreen(fullscreen);
		} else {
			this.setSimpleFullScreen(fullscreen);
		}

		// Events
		this.sendWhenReady(fullscreen ? 'vscode:enterFullScreen' : 'vscode:leaveFullScreen');

		// Respect configured menu bar visibility or default to toggle if not set
818
		this.setMenuBarVisibility(this.currentMenuBarVisibility, false);
B
Benjamin Pasero 已提交
819 820
	}

821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843
	isFullScreen(): boolean {
		return this._win.isFullScreen() || this._win.isSimpleFullScreen();
	}

	private setNativeFullScreen(fullscreen: boolean): void {
		if (this._win.isSimpleFullScreen()) {
			this._win.setSimpleFullScreen(false);
		}

		this._win.setFullScreen(fullscreen);
	}

	private setSimpleFullScreen(fullscreen: boolean): void {
		if (this._win.isFullScreen()) {
			this._win.setFullScreen(false);
		}

		this._win.setSimpleFullScreen(fullscreen);
		this._win.webContents.focus(); // workaround issue where focus is not going into window
	}

	private useNativeFullScreen(): boolean {
		const windowConfig = this.configurationService.getValue<IWindowSettings>('window');
B
Benjamin Pasero 已提交
844
		if (!windowConfig || typeof windowConfig.nativeFullScreen !== 'boolean') {
B
Benjamin Pasero 已提交
845 846
			return true; // default
		}
847

B
Benjamin Pasero 已提交
848 849 850 851
		if (windowConfig.nativeTabs) {
			return true; // https://github.com/electron/electron/issues/16142
		}

B
Benjamin Pasero 已提交
852
		return windowConfig.nativeFullScreen !== false;
853 854
	}

B
Benjamin Pasero 已提交
855 856 857 858
	isMinimized(): boolean {
		return this._win.isMinimized();
	}

859
	private getMenuBarVisibility(): MenuBarVisibility {
860
		const windowConfig = this.configurationService.getValue<IWindowSettings>('window');
B
Benjamin Pasero 已提交
861
		if (!windowConfig || !windowConfig.menuBarVisibility) {
862
			return 'default';
B
Benjamin Pasero 已提交
863 864 865 866
		}

		let menuBarVisibility = windowConfig.menuBarVisibility;
		if (['visible', 'toggle', 'hidden'].indexOf(menuBarVisibility) < 0) {
867
			menuBarVisibility = 'default';
B
Benjamin Pasero 已提交
868 869 870
		}

		return menuBarVisibility;
E
Erich Gamma 已提交
871 872
	}

873
	private setMenuBarVisibility(visibility: MenuBarVisibility, notify: boolean = true): void {
874
		if (isMacintosh) {
B
Benjamin Pasero 已提交
875 876 877
			return; // ignore for macOS platform
		}

878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898
		if (visibility === 'toggle') {
			if (notify) {
				this.send('vscode:showInfoMessage', nls.localize('hiddenMenuBar', "You can still access the menu bar by pressing the Alt-key."));
			}
		}

		if (visibility === 'hidden') {
			// for some weird reason that I have no explanation for, the menu bar is not hiding when calling
			// this without timeout (see https://github.com/Microsoft/vscode/issues/19777). there seems to be
			// a timing issue with us opening the first window and the menu bar getting created. somehow the
			// fact that we want to hide the menu without being able to bring it back via Alt key makes Electron
			// still show the menu. Unable to reproduce from a simple Hello World application though...
			setTimeout(() => {
				this.doSetMenuBarVisibility(visibility);
			});
		} else {
			this.doSetMenuBarVisibility(visibility);
		}
	}

	private doSetMenuBarVisibility(visibility: MenuBarVisibility): void {
899
		const isFullscreen = this.isFullScreen();
900

901
		switch (visibility) {
902
			case ('default'):
B
Benjamin Pasero 已提交
903 904
				this._win.setMenuBarVisibility(!isFullscreen);
				this._win.setAutoHideMenuBar(isFullscreen);
905 906
				break;

D
David Terry 已提交
907
			case ('visible'):
B
Benjamin Pasero 已提交
908 909
				this._win.setMenuBarVisibility(true);
				this._win.setAutoHideMenuBar(false);
910
				break;
D
David Terry 已提交
911 912

			case ('toggle'):
B
Benjamin Pasero 已提交
913 914
				this._win.setMenuBarVisibility(false);
				this._win.setAutoHideMenuBar(true);
915
				break;
D
David Terry 已提交
916 917

			case ('hidden'):
918 919
				this._win.setMenuBarVisibility(false);
				this._win.setAutoHideMenuBar(false);
920
				break;
921
		}
922 923
	}

B
Benjamin Pasero 已提交
924
	onWindowTitleDoubleClick(): void {
925 926

		// Respect system settings on mac with regards to title click on windows title
927
		if (isMacintosh) {
928 929 930 931 932 933 934 935 936
			const action = systemPreferences.getUserDefault('AppleActionOnDoubleClick', 'string');
			switch (action) {
				case 'Minimize':
					this.win.minimize();
					break;
				case 'None':
					break;
				case 'Maximize':
				default:
B
Benjamin Pasero 已提交
937 938 939 940 941
					if (this.win.isMaximized()) {
						this.win.unmaximize();
					} else {
						this.win.maximize();
					}
942 943 944 945 946 947 948 949 950 951 952 953 954
			}
		}

		// Linux/Windows: just toggle maximize/minimized state
		else {
			if (this.win.isMaximized()) {
				this.win.unmaximize();
			} else {
				this.win.maximize();
			}
		}
	}

B
Benjamin Pasero 已提交
955
	close(): void {
956 957 958 959 960
		if (this._win) {
			this._win.close();
		}
	}

B
Benjamin Pasero 已提交
961
	sendWhenReady(channel: string, ...args: any[]): void {
B
Benjamin Pasero 已提交
962
		if (this.isReady) {
963
			this.send(channel, ...args);
B
Benjamin Pasero 已提交
964 965 966
		} else {
			this.ready().then(() => this.send(channel, ...args));
		}
967 968
	}

B
Benjamin Pasero 已提交
969
	send(channel: string, ...args: any[]): void {
B
Benjamin Pasero 已提交
970 971 972
		if (this._win) {
			this._win.webContents.send(channel, ...args);
		}
973 974
	}

B
Benjamin Pasero 已提交
975
	updateTouchBar(groups: ISerializableCommandAction[][]): void {
976 977 978 979
		if (!isMacintosh) {
			return; // only supported on macOS
		}

B
Benjamin Pasero 已提交
980 981 982 983 984
		// Update segments for all groups. Setting the segments property
		// of the group directly prevents ugly flickering from happening
		this.touchBarGroups.forEach((touchBarGroup, index) => {
			const commands = groups[index];
			touchBarGroup.segments = this.createTouchBarGroupSegments(commands);
985
		});
B
Benjamin Pasero 已提交
986
	}
987

B
Benjamin Pasero 已提交
988 989 990 991
	private createTouchBar(): void {
		if (!isMacintosh) {
			return; // only supported on macOS
		}
992

B
Benjamin Pasero 已提交
993 994 995 996 997 998 999
		// To avoid flickering, we try to reuse the touch bar group
		// as much as possible by creating a large number of groups
		// for reusing later.
		for (let i = 0; i < 10; i++) {
			const groupTouchBar = this.createTouchBarGroup();
			this.touchBarGroups.push(groupTouchBar);
		}
1000

B
Benjamin Pasero 已提交
1001
		this._win.setTouchBar(new TouchBar({ items: this.touchBarGroups }));
1002
	}
1003

B
Benjamin Pasero 已提交
1004
	private createTouchBarGroup(items: ISerializableCommandAction[] = []): Electron.TouchBarSegmentedControl {
1005

B
Benjamin Pasero 已提交
1006 1007
		// Group Segments
		const segments = this.createTouchBarGroupSegments(items);
1008

B
Benjamin Pasero 已提交
1009 1010 1011 1012 1013 1014 1015 1016 1017
		// Group Control
		const control = new TouchBar.TouchBarSegmentedControl({
			segments,
			mode: 'buttons',
			segmentStyle: 'automatic',
			change: (selectedIndex) => {
				this.sendWhenReady('vscode:runAction', { id: (control.segments[selectedIndex] as ITouchBarSegment).id, from: 'touchbar' });
			}
		});
1018

B
Benjamin Pasero 已提交
1019 1020 1021
		return control;
	}

B
Benjamin Pasero 已提交
1022
	private createTouchBarGroupSegments(items: ISerializableCommandAction[] = []): ITouchBarSegment[] {
B
Benjamin Pasero 已提交
1023
		const segments: ITouchBarSegment[] = items.map(item => {
1024
			let icon: Electron.NativeImage | undefined;
1025
			if (item.iconLocation && item.iconLocation.dark.scheme === 'file') {
B
Benjamin Pasero 已提交
1026
				icon = nativeImage.createFromPath(URI.revive(item.iconLocation.dark).fsPath);
1027
				if (icon.isEmpty()) {
R
Rob Lourens 已提交
1028
					icon = undefined;
1029
				}
1030
			}
1031 1032

			return {
B
Benjamin Pasero 已提交
1033
				id: item.id,
R
Rob Lourens 已提交
1034
				label: !icon ? item.title as string : undefined,
1035 1036
				icon
			};
1037 1038
		});

B
Benjamin Pasero 已提交
1039
		return segments;
1040 1041
	}

B
Benjamin Pasero 已提交
1042
	dispose(): void {
B
Benjamin Pasero 已提交
1043 1044
		super.dispose();

E
Erich Gamma 已提交
1045 1046 1047 1048
		if (this.showTimeoutHandle) {
			clearTimeout(this.showTimeoutHandle);
		}

1049
		this._win = null!; // Important to dereference the window object to allow for GC
E
Erich Gamma 已提交
1050
	}
1051
}