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

'use strict';

J
Joao Moreno 已提交
8 9
import * as path from 'path';
import * as objects from 'vs/base/common/objects';
10
import * as nls from 'vs/nls';
B
Benjamin Pasero 已提交
11
import URI from 'vs/base/common/uri';
B
Benjamin Pasero 已提交
12
import { IStateService } from 'vs/platform/state/common/state';
13
import { shell, screen, BrowserWindow, systemPreferences, app, TouchBar, nativeImage } from 'electron';
J
Joao Moreno 已提交
14
import { TPromise, TValueCallback } from 'vs/base/common/winjs.base';
J
Joao Moreno 已提交
15
import { IEnvironmentService, ParsedArgs } from 'vs/platform/environment/common/environment';
16
import { ILogService } from 'vs/platform/log/common/log';
17
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
J
Joao Moreno 已提交
18
import { parseArgs } from 'vs/platform/environment/node/argv';
19
import product from 'vs/platform/node/product';
20
import { IWindowSettings, MenuBarVisibility, IWindowConfiguration, ReadyState, IRunActionInWindowRequest } from 'vs/platform/windows/common/windows';
21
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
B
Benjamin Pasero 已提交
22
import { isLinux, isMacintosh, isWindows } from 'vs/base/common/platform';
23
import { ICodeWindow, IWindowState, WindowMode } from 'vs/platform/windows/electron-main/windows';
24
import { IWorkspaceIdentifier, IWorkspacesMainService } from 'vs/platform/workspaces/common/workspaces';
B
Benjamin Pasero 已提交
25
import { IBackupMainService } from 'vs/platform/backup/common/backup';
26
import { ICommandAction } from 'vs/platform/actions/common/actions';
27
import { mark, exportEntries } from 'vs/base/common/performance';
28
import { resolveMarketplaceHeaders } from 'vs/platform/extensionManagement/node/extensionGalleryService';
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 implements ICodeWindow {
E
Erich Gamma 已提交
57

M
Matt Bierner 已提交
58 59
	public static readonly themeStorageKey = 'theme';
	public static readonly themeBackgroundStorageKey = 'themeBackground';
60

61 62 63
	private static readonly DEFAULT_BG_LIGHT = '#FFFFFF';
	private static readonly DEFAULT_BG_DARK = '#1E1E1E';
	private static readonly DEFAULT_BG_HC_BLACK = '#000000';
B
Benjamin Pasero 已提交
64

65 66
	private static readonly MIN_WIDTH = 200;
	private static readonly MIN_HEIGHT = 120;
E
Erich Gamma 已提交
67

B
Benjamin Pasero 已提交
68
	private hiddenTitleBarStyle: boolean;
E
Erich Gamma 已提交
69
	private showTimeoutHandle: any;
B
Benjamin Pasero 已提交
70
	private _id: number;
71
	private _win: Electron.BrowserWindow;
E
Erich Gamma 已提交
72 73 74
	private _lastFocusTime: number;
	private _readyState: ReadyState;
	private windowState: IWindowState;
75
	private currentMenuBarVisibility: MenuBarVisibility;
76
	private toDispose: IDisposable[];
77
	private representedFilename: string;
E
Erich Gamma 已提交
78

B
Benjamin Pasero 已提交
79
	private whenReadyCallbacks: TValueCallback<ICodeWindow>[];
E
Erich Gamma 已提交
80 81 82 83

	private currentConfig: IWindowConfiguration;
	private pendingLoadConfig: IWindowConfiguration;

84 85
	private marketplaceHeadersPromise: TPromise<object>;

B
Benjamin Pasero 已提交
86
	private touchBarGroups: Electron.TouchBarSegmentedControl[];
87

J
Joao Moreno 已提交
88 89 90
	constructor(
		config: IWindowCreationOptions,
		@ILogService private logService: ILogService,
91
		@IEnvironmentService private environmentService: IEnvironmentService,
92
		@IConfigurationService private configurationService: IConfigurationService,
B
Benjamin Pasero 已提交
93
		@IStateService private stateService: IStateService,
B
Benjamin Pasero 已提交
94 95
		@IWorkspacesMainService private workspacesMainService: IWorkspacesMainService,
		@IBackupMainService private backupMainService: IBackupMainService
J
Joao Moreno 已提交
96
	) {
B
Benjamin Pasero 已提交
97
		this.touchBarGroups = [];
E
Erich Gamma 已提交
98 99 100
		this._lastFocusTime = -1;
		this._readyState = ReadyState.NONE;
		this.whenReadyCallbacks = [];
101
		this.toDispose = [];
E
Erich Gamma 已提交
102

B
Benjamin Pasero 已提交
103 104 105 106 107 108
		// create browser window
		this.createBrowserWindow(config);

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

B
Benjamin Pasero 已提交
109 110 111
		// macOS: touch bar support
		this.createTouchBar();

112 113 114
		// Request handling
		this.handleMarketplaceRequests();

B
Benjamin Pasero 已提交
115 116 117 118 119 120
		// Eventing
		this.registerListeners();
	}

	private createBrowserWindow(config: IWindowCreationOptions): void {

E
Erich Gamma 已提交
121
		// Load window state
B
Benjamin Pasero 已提交
122
		this.windowState = this.restoreWindowState(config.state);
E
Erich Gamma 已提交
123

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

B
Benjamin Pasero 已提交
127 128 129 130 131
		let backgroundColor = this.getBackgroundColor();
		if (isMacintosh && backgroundColor.toUpperCase() === CodeWindow.DEFAULT_BG_DARK) {
			backgroundColor = '#171717'; // https://github.com/electron/electron/issues/5150
		}

132
		const options: Electron.BrowserWindowConstructorOptions = {
E
Erich Gamma 已提交
133 134 135 136
			width: this.windowState.width,
			height: this.windowState.height,
			x: this.windowState.x,
			y: this.windowState.y,
B
Benjamin Pasero 已提交
137
			backgroundColor,
B
Benjamin Pasero 已提交
138 139
			minWidth: CodeWindow.MIN_WIDTH,
			minHeight: CodeWindow.MIN_HEIGHT,
140
			show: !isFullscreenOrMaximized,
B
Benjamin Pasero 已提交
141
			title: product.nameLong,
142
			webPreferences: {
143 144
				'backgroundThrottling': false, // by default if Code is in the background, intervals and timeouts get throttled,
				disableBlinkFeatures: 'Auxclick' // disable auxclick events (see https://developers.google.com/web/updates/2016/10/auxclick)
145
			}
E
Erich Gamma 已提交
146 147
		};

148
		if (isLinux) {
149
			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 已提交
150 151
		}

152
		const windowConfig = this.configurationService.getValue<IWindowSettings>('window');
153 154 155 156 157 158 159

		let useNativeTabs = false;
		if (windowConfig && windowConfig.nativeTabs) {
			options.tabbingIdentifier = product.nameShort; // this opts in to sierra tabs
			useNativeTabs = true;
		}

160
		let useCustomTitleStyle = false;
161
		if (isMacintosh && (!windowConfig || !windowConfig.titleBarStyle || windowConfig.titleBarStyle === 'custom')) {
162
			const isDev = !this.environmentService.isBuilt || !!config.extensionDevelopmentPath;
B
Benjamin Pasero 已提交
163
			if (!isDev) {
164
				useCustomTitleStyle = true; // not enabled when developing due to https://github.com/electron/electron/issues/3647
B
Benjamin Pasero 已提交
165 166 167
			}
		}

168 169 170 171
		if (useNativeTabs) {
			useCustomTitleStyle = false; // native tabs on sierra do not work with custom title style
		}

172 173 174 175 176
		if (useCustomTitleStyle) {
			options.titleBarStyle = 'hidden';
			this.hiddenTitleBarStyle = true;
		}

E
Erich Gamma 已提交
177 178
		// Create the browser window.
		this._win = new BrowserWindow(options);
B
Benjamin Pasero 已提交
179
		this._id = this._win.id;
E
Erich Gamma 已提交
180

181 182 183 184
		if (useCustomTitleStyle) {
			this._win.setSheetOffset(22); // offset dialogs by the height of the custom title bar if we have any
		}

185
		if (isFullscreenOrMaximized) {
B
Benjamin Pasero 已提交
186
			this._win.maximize();
E
Erich Gamma 已提交
187

B
Benjamin Pasero 已提交
188
			if (this.windowState.mode === WindowMode.Fullscreen) {
B
Benjamin Pasero 已提交
189
				this._win.setFullScreen(true);
190 191
			}

B
Benjamin Pasero 已提交
192 193
			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 已提交
194 195 196
			}
		}

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

200
	public hasHiddenTitleBarStyle(): boolean {
B
Benjamin Pasero 已提交
201
		return this.hiddenTitleBarStyle;
202 203
	}

204
	public get isExtensionDevelopmentHost(): boolean {
205
		return !!this.config.extensionDevelopmentPath;
206 207
	}

208
	public get isExtensionTestHost(): boolean {
209
		return !!this.config.extensionTestsPath;
210 211
	}

212
	public get extensionDevelopmentPath(): string {
213
		return this.config.extensionDevelopmentPath;
E
Erich Gamma 已提交
214 215 216 217 218 219
	}

	public get config(): IWindowConfiguration {
		return this.currentConfig;
	}

B
Benjamin Pasero 已提交
220 221 222 223
	public get id(): number {
		return this._id;
	}

224
	public get win(): Electron.BrowserWindow {
E
Erich Gamma 已提交
225 226 227
		return this._win;
	}

228
	public setRepresentedFilename(filename: string): void {
229
		if (isMacintosh) {
230 231 232 233 234 235 236
			this.win.setRepresentedFilename(filename);
		} else {
			this.representedFilename = filename;
		}
	}

	public getRepresentedFilename(): string {
237
		if (isMacintosh) {
238 239 240 241 242 243
			return this.win.getRepresentedFilename();
		}

		return this.representedFilename;
	}

B
Benjamin Pasero 已提交
244
	public focus(): void {
E
Erich Gamma 已提交
245 246 247 248
		if (!this._win) {
			return;
		}

B
Benjamin Pasero 已提交
249 250
		if (this._win.isMinimized()) {
			this._win.restore();
E
Erich Gamma 已提交
251 252
		}

B
Benjamin Pasero 已提交
253
		this._win.focus();
E
Erich Gamma 已提交
254 255 256 257 258 259
	}

	public get lastFocusTime(): number {
		return this._lastFocusTime;
	}

260 261
	public get backupPath(): string {
		return this.currentConfig ? this.currentConfig.backupPath : void 0;
E
Erich Gamma 已提交
262 263
	}

264
	public get openedWorkspace(): IWorkspaceIdentifier {
265
		return this.currentConfig ? this.currentConfig.workspace : void 0;
266 267
	}

268 269 270 271
	public get openedFolderPath(): string {
		return this.currentConfig ? this.currentConfig.folderPath : void 0;
	}

E
Erich Gamma 已提交
272 273 274 275 276 277 278 279 280
	public setReady(): void {
		this._readyState = ReadyState.READY;

		// inform all waiting promises that we are ready now
		while (this.whenReadyCallbacks.length) {
			this.whenReadyCallbacks.pop()(this);
		}
	}

B
Benjamin Pasero 已提交
281 282
	public ready(): TPromise<ICodeWindow> {
		return new TPromise<ICodeWindow>((c) => {
E
Erich Gamma 已提交
283 284 285 286 287 288 289 290 291 292 293 294 295
			if (this._readyState === ReadyState.READY) {
				return c(this);
			}

			// otherwise keep and call later when we are ready
			this.whenReadyCallbacks.push(c);
		});
	}

	public get readyState(): ReadyState {
		return this._readyState;
	}

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 305 306
			this.marketplaceHeadersPromise.done(headers => {
				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({});
		});

M
Matt Bierner 已提交
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;

B
Benjamin Pasero 已提交
341
				this.pendingLoadConfig = null;
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.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 361 362

		// Handle code that wants to open links
		this._win.webContents.on('new-window', (event: Event, url: string) => {
			event.preventDefault();

B
Benjamin Pasero 已提交
363
			shell.openExternal(url);
E
Erich Gamma 已提交
364 365 366 367
		});

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

371 372 373 374 375 376 377 378 379
		// 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 已提交
380
		// Window Failed to load
381
		this._win.webContents.on('did-fail-load', (event: Electron.Event, errorCode: number, errorDescription: string, validatedURL: string, isMainFrame: boolean) => {
B
Benjamin Pasero 已提交
382
			this.logService.warn('[electron event]: fail to load, ', errorDescription);
E
Erich Gamma 已提交
383 384 385 386
		});

		// Prevent any kind of navigation triggered by the user!
		// But do not touch this in dev version because it will prevent "Reload" from dev tools
387
		if (this.environmentService.isBuilt) {
E
Erich Gamma 已提交
388 389 390 391 392 393
			this._win.webContents.on('will-navigate', (event: Event) => {
				if (event) {
					event.preventDefault();
				}
			});
		}
B
Benjamin Pasero 已提交
394 395

		// Handle configuration changes
396
		this.toDispose.push(this.configurationService.onDidChangeConfiguration(e => this.onConfigurationUpdated()));
397

398
		// Handle Workspace events
B
Benjamin Pasero 已提交
399
		this.toDispose.push(this.workspacesMainService.onUntitledWorkspaceDeleted(e => this.onUntitledWorkspaceDeleted(e)));
400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427

		// TODO@Ben workaround for https://github.com/Microsoft/vscode/issues/13612
		// It looks like smooth scrolling disappears as soon as the window is minimized
		// and maximized again. Touching some window properties "fixes" it, like toggling
		// the visibility of the menu.
		if (isWindows) {
			const windowConfig = this.configurationService.getValue<IWindowSettings>('window');
			if (windowConfig && windowConfig.smoothScrollingWorkaround === true) {
				let minimized = false;

				const restoreSmoothScrolling = () => {
					if (minimized) {
						const visibility = this.getMenuBarVisibility();
						const temporaryVisibility: MenuBarVisibility = (visibility === 'hidden' || visibility === 'toggle') ? 'default' : 'hidden';
						setTimeout(() => {
							this.doSetMenuBarVisibility(temporaryVisibility);
							this.doSetMenuBarVisibility(visibility);
						}, 0);
					}

					minimized = false;
				};

				this._win.on('minimize', () => minimized = true);
				this._win.on('restore', () => restoreSmoothScrolling());
				this._win.on('maximize', () => restoreSmoothScrolling());
			}
		}
428 429
	}

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

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

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

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

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

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

471
	public load(config: IWindowConfiguration, isReload?: boolean): void {
E
Erich Gamma 已提交
472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487

		// 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
		if (this.readyState === ReadyState.NONE) {
			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;
		}

488
		// Clear Document Edited if needed
489
		if (isMacintosh && this._win.isDocumentEdited()) {
B
Benjamin Pasero 已提交
490
			if (!isReload || !this.backupMainService.isHotExitEnabled()) {
491 492 493 494 495 496 497 498 499 500 501
				this._win.setDocumentEdited(false);
			}
		}

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

			this._win.setTitle(product.nameLong);
502 503
		}

E
Erich Gamma 已提交
504
		// Load URL
505
		mark('main:loadWindow');
506
		this._win.loadURL(this.getUrl(config));
E
Erich Gamma 已提交
507 508

		// Make window visible if it did not open in N seconds because this indicates an error
509 510
		// Only do this when running out of sources and not when running tests
		if (!this.environmentService.isBuilt && !this.environmentService.extensionTestsPath) {
E
Erich Gamma 已提交
511 512 513 514
			this.showTimeoutHandle = setTimeout(() => {
				if (this._win && !this._win.isVisible() && !this._win.isMinimized()) {
					this._win.show();
					this._win.focus();
515
					this._win.webContents.openDevTools();
E
Erich Gamma 已提交
516 517 518 519 520
				}
			}, 10000);
		}
	}

521
	public reload(configuration?: IWindowConfiguration, cli?: ParsedArgs): void {
E
Erich Gamma 已提交
522

523 524 525 526 527 528
		// If config is not provided, copy our current one
		if (!configuration) {
			configuration = objects.mixin({}, this.currentConfig);
		}

		// Delete some properties we do not want during reload
E
Erich Gamma 已提交
529 530
		delete configuration.filesToOpen;
		delete configuration.filesToCreate;
531
		delete configuration.filesToDiff;
532
		delete configuration.filesToWait;
533

E
Erich Gamma 已提交
534
		// Some configuration things get inherited if the window is being reloaded and we are
B
Benjamin Pasero 已提交
535
		// in extension development mode. These options are all development related.
536
		if (this.isExtensionDevelopmentHost && cli) {
B
Benjamin Pasero 已提交
537
			configuration.verbose = cli.verbose;
538
			configuration.debugPluginHost = cli.debugPluginHost;
539
			configuration.debugBrkPluginHost = cli.debugBrkPluginHost;
540
			configuration.debugId = cli.debugId;
541
			configuration['extensions-dir'] = cli['extensions-dir'];
E
Erich Gamma 已提交
542 543
		}

544 545 546 547
		if (cli) {
			configuration['disable-extensions'] = cli['disable-extensions'];
		}

548 549
		configuration.isInitialStartup = false; // since this is a reload

E
Erich Gamma 已提交
550
		// Load config
551
		this.load(configuration, true);
E
Erich Gamma 已提交
552 553
	}

554
	private getUrl(windowConfiguration: IWindowConfiguration): string {
E
Erich Gamma 已提交
555

556 557
		// Set window ID
		windowConfiguration.windowId = this._win.id;
S
Sandeep Somavarapu 已提交
558
		windowConfiguration.logLevel = this.logService.getLevel();
559

560
		// Set zoomlevel
561
		const windowConfig = this.configurationService.getValue<IWindowSettings>('window');
562
		const zoomLevel = windowConfig && windowConfig.zoomLevel;
563 564 565 566
		if (typeof zoomLevel === 'number') {
			windowConfiguration.zoomLevel = zoomLevel;
		}

567 568 569
		// Set fullscreen state
		windowConfiguration.fullscreen = this._win.isFullScreen();

570
		// Set Accessibility Config
571 572 573 574 575
		let autoDetectHighContrast = true;
		if (windowConfig && windowConfig.autoDetectHighContrast === false) {
			autoDetectHighContrast = false;
		}
		windowConfiguration.highContrast = isWindows && autoDetectHighContrast && systemPreferences.isInvertedColorScheme();
576 577
		windowConfiguration.accessibilitySupport = app.isAccessibilitySupportEnabled();

578 579
		// Theme
		windowConfiguration.baseTheme = this.getBaseTheme();
580
		windowConfiguration.backgroundColor = this.getBackgroundColor();
M
Martin Aeschlimann 已提交
581

582
		// Perf Counters
583
		windowConfiguration.perfEntries = exportEntries();
584
		windowConfiguration.perfStartTime = (<any>global).perfStartTime;
585
		windowConfiguration.perfWindowLoadTime = Date.now();
586

587 588 589
		// Config (combination of process.argv and window configuration)
		const environment = parseArgs(process.argv);
		const config = objects.assign(environment, windowConfiguration);
590
		for (let key in config) {
S
Sandeep Somavarapu 已提交
591
			if (config[key] === void 0 || config[key] === null || config[key] === '') {
592 593 594
				delete config[key]; // only send over properties that have a true value
			}
		}
595

B
Benjamin Pasero 已提交
596
		return `${require.toUrl('vs/workbench/electron-browser/bootstrap/index.html')}?config=${encodeURIComponent(JSON.stringify(config))}`;
E
Erich Gamma 已提交
597 598
	}

599
	private getBaseTheme(): string {
600
		if (isWindows && systemPreferences.isInvertedColorScheme()) {
601 602
			return 'hc-black';
		}
B
Benjamin Pasero 已提交
603

B
Benjamin Pasero 已提交
604
		const theme = this.stateService.getItem<string>(CodeWindow.themeStorageKey, 'vs-dark');
B
Benjamin Pasero 已提交
605

606 607 608
		return theme.split(' ')[0];
	}

609
	private getBackgroundColor(): string {
610
		if (isWindows && systemPreferences.isInvertedColorScheme()) {
B
Benjamin Pasero 已提交
611
			return CodeWindow.DEFAULT_BG_HC_BLACK;
612 613
		}

B
Benjamin Pasero 已提交
614
		const background = this.stateService.getItem<string>(CodeWindow.themeBackgroundStorageKey, null);
615
		if (!background) {
B
Benjamin Pasero 已提交
616 617
			const baseTheme = this.getBaseTheme();

B
Benjamin Pasero 已提交
618
			return baseTheme === 'hc-black' ? CodeWindow.DEFAULT_BG_HC_BLACK : (baseTheme === 'vs' ? CodeWindow.DEFAULT_BG_LIGHT : CodeWindow.DEFAULT_BG_DARK);
619 620 621 622 623
		}

		return background;
	}

E
Erich Gamma 已提交
624
	public serializeWindowState(): IWindowState {
625 626 627
		if (!this._win) {
			return defaultWindowState();
		}
628 629

		// fullscreen gets special treatment
B
Benjamin Pasero 已提交
630
		if (this._win.isFullScreen()) {
631 632
			const display = screen.getDisplayMatching(this.getBounds());

633
			return {
634
				mode: WindowMode.Fullscreen,
635 636
				display: display ? display.id : void 0,

637 638 639 640 641
				// still carry over window dimensions from previous sessions!
				width: this.windowState.width,
				height: this.windowState.height,
				x: this.windowState.x,
				y: this.windowState.y
642
			};
E
Erich Gamma 已提交
643 644
		}

645
		const state: IWindowState = Object.create(null);
E
Erich Gamma 已提交
646 647 648
		let mode: WindowMode;

		// get window mode
649
		if (!isMacintosh && this._win.isMaximized()) {
E
Erich Gamma 已提交
650 651 652 653 654 655 656 657
			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;
658
		} else {
E
Erich Gamma 已提交
659 660 661 662 663
			state.mode = WindowMode.Normal;
		}

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

666 667 668 669
			state.x = bounds.x;
			state.y = bounds.y;
			state.width = bounds.width;
			state.height = bounds.height;
E
Erich Gamma 已提交
670 671 672 673 674
		}

		return state;
	}

B
Benjamin Pasero 已提交
675
	private restoreWindowState(state?: IWindowState): IWindowState {
E
Erich Gamma 已提交
676 677 678 679
		if (state) {
			try {
				state = this.validateWindowState(state);
			} catch (err) {
J
Joao Moreno 已提交
680
				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 已提交
681 682 683 684 685 686 687
			}
		}

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

B
Benjamin Pasero 已提交
688
		return state;
E
Erich Gamma 已提交
689 690 691 692 693 694 695 696 697 698 699 700 701 702 703
	}

	private validateWindowState(state: IWindowState): IWindowState {
		if (!state) {
			return null;
		}

		if ([state.x, state.y, state.width, state.height].some(n => typeof n !== 'number')) {
			return null;
		}

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

704
		const displays = screen.getAllDisplays();
E
Erich Gamma 已提交
705 706 707

		// Single Monitor: be strict about x/y positioning
		if (displays.length === 1) {
708
			const displayBounds = displays[0].bounds;
E
Erich Gamma 已提交
709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743

			// 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;
		}

744 745 746 747 748 749 750 751 752 753 754 755 756
		// 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
757 758
		const bounds = { x: state.x, y: state.y, width: state.width, height: state.height };
		const display = screen.getDisplayMatching(bounds);
U
unknown 已提交
759 760 761 762 763 764 765
		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 已提交
766
			if (state.mode === WindowMode.Maximized) {
767
				const defaults = defaultWindowState(WindowMode.Maximized); // when maximized, make sure we have good values when the user restores the window
E
Erich Gamma 已提交
768 769 770 771 772 773 774 775 776 777 778 779
				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 已提交
780
	public getBounds(): Electron.Rectangle {
B
Benjamin Pasero 已提交
781 782
		const pos = this._win.getPosition();
		const dimension = this._win.getSize();
E
Erich Gamma 已提交
783 784 785 786 787

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

	public toggleFullScreen(): void {
B
Benjamin Pasero 已提交
788
		const willBeFullScreen = !this._win.isFullScreen();
E
Erich Gamma 已提交
789

B
Benjamin Pasero 已提交
790
		// set fullscreen flag on window
B
Benjamin Pasero 已提交
791
		this._win.setFullScreen(willBeFullScreen);
792

793
		// respect configured menu bar visibility or default to toggle if not set
794
		this.setMenuBarVisibility(this.currentMenuBarVisibility, false);
B
Benjamin Pasero 已提交
795 796
	}

797
	private getMenuBarVisibility(): MenuBarVisibility {
798
		const windowConfig = this.configurationService.getValue<IWindowSettings>('window');
B
Benjamin Pasero 已提交
799
		if (!windowConfig || !windowConfig.menuBarVisibility) {
800
			return 'default';
B
Benjamin Pasero 已提交
801 802 803 804
		}

		let menuBarVisibility = windowConfig.menuBarVisibility;
		if (['visible', 'toggle', 'hidden'].indexOf(menuBarVisibility) < 0) {
805
			menuBarVisibility = 'default';
B
Benjamin Pasero 已提交
806 807 808
		}

		return menuBarVisibility;
E
Erich Gamma 已提交
809 810
	}

811
	private setMenuBarVisibility(visibility: MenuBarVisibility, notify: boolean = true): void {
812
		if (isMacintosh) {
B
Benjamin Pasero 已提交
813 814 815
			return; // ignore for macOS platform
		}

816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836
		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 {
B
Benjamin Pasero 已提交
837
		const isFullscreen = this._win.isFullScreen();
838

839
		switch (visibility) {
840
			case ('default'):
B
Benjamin Pasero 已提交
841 842
				this._win.setMenuBarVisibility(!isFullscreen);
				this._win.setAutoHideMenuBar(isFullscreen);
843 844
				break;

D
David Terry 已提交
845
			case ('visible'):
B
Benjamin Pasero 已提交
846 847
				this._win.setMenuBarVisibility(true);
				this._win.setAutoHideMenuBar(false);
848
				break;
D
David Terry 已提交
849 850

			case ('toggle'):
B
Benjamin Pasero 已提交
851 852
				this._win.setMenuBarVisibility(false);
				this._win.setAutoHideMenuBar(true);
853
				break;
D
David Terry 已提交
854 855

			case ('hidden'):
856 857
				this._win.setMenuBarVisibility(false);
				this._win.setAutoHideMenuBar(false);
858
				break;
859
		}
860 861
	}

862 863 864
	public onWindowTitleDoubleClick(): void {

		// Respect system settings on mac with regards to title click on windows title
865
		if (isMacintosh) {
866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888
			const action = systemPreferences.getUserDefault('AppleActionOnDoubleClick', 'string');
			switch (action) {
				case 'Minimize':
					this.win.minimize();
					break;
				case 'None':
					break;
				case 'Maximize':
				default:
					this.win.maximize();
			}
		}

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

889 890 891 892 893 894
	public close(): void {
		if (this._win) {
			this._win.close();
		}
	}

895 896 897 898 899 900 901
	public sendWhenReady(channel: string, ...args: any[]): void {
		this.ready().then(() => {
			this.send(channel, ...args);
		});
	}

	public send(channel: string, ...args: any[]): void {
B
Benjamin Pasero 已提交
902 903 904
		if (this._win) {
			this._win.webContents.send(channel, ...args);
		}
905 906
	}

907
	public updateTouchBar(groups: ICommandAction[][]): void {
908 909 910 911
		if (!isMacintosh) {
			return; // only supported on macOS
		}

B
Benjamin Pasero 已提交
912 913 914 915 916
		// 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);
917
		});
B
Benjamin Pasero 已提交
918
	}
919

B
Benjamin Pasero 已提交
920 921 922 923
	private createTouchBar(): void {
		if (!isMacintosh) {
			return; // only supported on macOS
		}
924

B
Benjamin Pasero 已提交
925 926 927 928 929 930 931
		// 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);
		}
932

B
Benjamin Pasero 已提交
933
		this._win.setTouchBar(new TouchBar({ items: this.touchBarGroups }));
934
	}
935

B
Benjamin Pasero 已提交
936
	private createTouchBarGroup(items: ICommandAction[] = []): Electron.TouchBarSegmentedControl {
937

B
Benjamin Pasero 已提交
938 939
		// Group Segments
		const segments = this.createTouchBarGroupSegments(items);
940

B
Benjamin Pasero 已提交
941 942 943 944 945 946 947 948 949
		// 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' });
			}
		});
950

B
Benjamin Pasero 已提交
951 952 953 954 955
		return control;
	}

	private createTouchBarGroupSegments(items: ICommandAction[] = []): ITouchBarSegment[] {
		const segments: ITouchBarSegment[] = items.map(item => {
956 957
			let icon: Electron.NativeImage;
			if (item.iconPath) {
B
Benjamin Pasero 已提交
958
				icon = nativeImage.createFromPath(item.iconPath.dark);
959 960 961
				if (icon.isEmpty()) {
					icon = void 0;
				}
962
			}
963 964

			return {
B
Benjamin Pasero 已提交
965
				id: item.id,
966 967 968
				label: !icon ? item.title as string : void 0,
				icon
			};
969 970
		});

B
Benjamin Pasero 已提交
971
		return segments;
972 973
	}

E
Erich Gamma 已提交
974 975 976 977 978
	public dispose(): void {
		if (this.showTimeoutHandle) {
			clearTimeout(this.showTimeoutHandle);
		}

979 980
		this.toDispose = dispose(this.toDispose);

E
Erich Gamma 已提交
981 982
		this._win = null; // Important to dereference the window object to allow for GC
	}
983
}