window.ts 20.0 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';

8
import nls = require('vs/nls');
E
Erich Gamma 已提交
9
import platform = require('vs/base/common/platform');
10
import URI from 'vs/base/common/uri';
11 12
import errors = require('vs/base/common/errors');
import types = require('vs/base/common/types');
J
Johannes Rieken 已提交
13
import { TPromise } from 'vs/base/common/winjs.base';
14
import arrays = require('vs/base/common/arrays');
15
import objects = require('vs/base/common/objects');
16
import DOM = require('vs/base/browser/dom');
17 18 19
import Severity from 'vs/base/common/severity';
import { Separator } from 'vs/base/browser/ui/actionbar/actionbar';
import { IAction, Action } from 'vs/base/common/actions';
20
import { AutoSaveConfiguration, IFileService } from 'vs/platform/files/common/files';
21
import { toResource } from 'vs/workbench/common/editor';
22
import { IWorkbenchEditorService, IResourceInputType } from 'vs/workbench/services/editor/common/editorService';
J
Johannes Rieken 已提交
23
import { IEditorGroupService } from 'vs/workbench/services/group/common/groupService';
24
import { IMessageService } from 'vs/platform/message/common/message';
J
Johannes Rieken 已提交
25
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
26
import { IWorkspaceConfigurationService } from 'vs/workbench/services/configuration/common/configuration';
27
import { IWindowsService, IWindowService, IWindowSettings, IPath, IOpenFileRequest, IWindowsConfiguration, IAddFoldersRequest, IRunActionInWindowRequest } from 'vs/platform/windows/common/windows';
28 29
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
B
Benjamin Pasero 已提交
30
import { ITitleService } from 'vs/workbench/services/title/common/titleService';
31
import { IWorkbenchThemeService, VS_HC_THEME, VS_DARK_THEME } from 'vs/workbench/services/themes/common/workbenchThemeService';
32 33 34
import * as browser from 'vs/base/browser/browser';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet';
N
Nick Snyder 已提交
35
import { Position, IResourceInput, IUntitledResourceInput, IEditor } from 'vs/platform/editor/common/editor';
36
import { IExtensionService } from 'vs/platform/extensions/common/extensions';
A
Alex Dima 已提交
37
import { KeyboardMapperFactory } from 'vs/workbench/services/keybinding/electron-browser/keybindingService';
38
import { Themable } from 'vs/workbench/common/theme';
39
import { ipcRenderer as ipc, webFrame } from 'electron';
40
import { IWorkspaceEditingService } from 'vs/workbench/services/workspace/common/workspaceEditing';
41 42 43 44 45
import { IMenuService, MenuId, IMenu, MenuItemAction, ICommandAction } from 'vs/platform/actions/common/actions';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { fillInActions } from 'vs/platform/actions/browser/menuItemActionItem';
import { RunOnceScheduler } from 'vs/base/common/async';
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
I
isidor 已提交
46
import { ConfigurationTarget } from 'vs/platform/configuration/common/configuration';
47
import { LifecyclePhase, ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle';
48
import { IWorkspaceFolderCreationData } from 'vs/platform/workspaces/common/workspaces';
E
Erich Gamma 已提交
49

50 51 52 53 54 55 56 57 58 59 60
const TextInputActions: IAction[] = [
	new Action('undo', nls.localize('undo', "Undo"), null, true, () => document.execCommand('undo') && TPromise.as(true)),
	new Action('redo', nls.localize('redo', "Redo"), null, true, () => document.execCommand('redo') && TPromise.as(true)),
	new Separator(),
	new Action('editor.action.clipboardCutAction', nls.localize('cut', "Cut"), null, true, () => document.execCommand('cut') && TPromise.as(true)),
	new Action('editor.action.clipboardCopyAction', nls.localize('copy', "Copy"), null, true, () => document.execCommand('copy') && TPromise.as(true)),
	new Action('editor.action.clipboardPasteAction', nls.localize('paste', "Paste"), null, true, () => document.execCommand('paste') && TPromise.as(true)),
	new Separator(),
	new Action('editor.action.selectAll', nls.localize('selectAll', "Select All"), null, true, () => document.execCommand('selectAll') && TPromise.as(true))
];

61
export class ElectronWindow extends Themable {
62

63
	private static readonly AUTO_SAVE_SETTING = 'files.autoSave';
64

65 66 67 68 69 70 71
	private touchBarUpdater: RunOnceScheduler;
	private touchBarMenu: IMenu;
	private touchBarDisposables: IDisposable[];
	private lastInstalledTouchedBar: ICommandAction[][];

	private previousConfiguredZoomLevel: number;

72 73 74
	private addFoldersScheduler: RunOnceScheduler;
	private pendingFoldersToAdd: IAddFoldersRequest[];

E
Erich Gamma 已提交
75 76 77
	constructor(
		shellContainer: HTMLElement,
		@IWorkbenchEditorService private editorService: IWorkbenchEditorService,
78
		@IEditorGroupService private editorGroupService: IEditorGroupService,
79
		@IWindowsService private windowsService: IWindowsService,
B
Benjamin Pasero 已提交
80
		@IWindowService private windowService: IWindowService,
81 82
		@IWorkspaceConfigurationService private configurationService: IWorkspaceConfigurationService,
		@ITitleService private titleService: ITitleService,
83
		@IWorkbenchThemeService protected themeService: IWorkbenchThemeService,
84 85 86 87 88 89
		@IMessageService private messageService: IMessageService,
		@ICommandService private commandService: ICommandService,
		@IExtensionService private extensionService: IExtensionService,
		@IViewletService private viewletService: IViewletService,
		@IContextMenuService private contextMenuService: IContextMenuService,
		@IKeybindingService private keybindingService: IKeybindingService,
90
		@ITelemetryService private telemetryService: ITelemetryService,
91
		@IWorkspaceEditingService private workspaceEditingService: IWorkspaceEditingService,
92
		@IFileService private fileService: IFileService,
93 94
		@IMenuService private menuService: IMenuService,
		@ILifecycleService private lifecycleService: ILifecycleService
E
Erich Gamma 已提交
95
	) {
96 97
		super(themeService);

98 99 100 101 102
		this.touchBarDisposables = [];

		this.touchBarUpdater = new RunOnceScheduler(() => this.doSetupTouchbar(), 300);
		this.toUnbind.push(this.touchBarUpdater);

103 104 105 106
		this.pendingFoldersToAdd = [];
		this.addFoldersScheduler = new RunOnceScheduler(() => this.doAddFolders(), 100);
		this.toUnbind.push(this.addFoldersScheduler);

E
Erich Gamma 已提交
107
		this.registerListeners();
108
		this.create();
E
Erich Gamma 已提交
109 110 111 112
	}

	private registerListeners(): void {

113
		// React to editor input changes
114
		this.toUnbind.push(this.editorGroupService.onEditorsChanged(() => {
E
Erich Gamma 已提交
115

116 117
			// Represented File Name
			const file = toResource(this.editorService.getActiveEditorInput(), { supportSideBySide: true, filter: 'file' });
118
			this.titleService.setRepresentedFilename(file ? file.fsPath : '');
119 120 121 122

			// Touch Bar
			this.updateTouchbarMenu();
		}));
E
Erich Gamma 已提交
123

124
		// prevent opening a real URL inside the shell
125 126 127 128
		[DOM.EventType.DRAG_OVER, DOM.EventType.DROP].forEach(event => {
			window.document.body.addEventListener(event, (e: DragEvent) => {
				DOM.EventHelper.stop(e);
			});
E
Erich Gamma 已提交
129 130
		});

131
		// Support runAction event
132
		ipc.on('vscode:runAction', (_event: any, request: IRunActionInWindowRequest) => {
133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149
			const args: any[] = [];

			// If we run an action from the touchbar, we fill in the currently active resource
			// as payload because the touch bar items are context aware depending on the editor
			if (request.from === 'touchbar') {
				const activeEditor = this.editorService.getActiveEditor();
				if (activeEditor) {
					const resource = toResource(activeEditor.input, { supportSideBySide: true });
					if (resource) {
						args.push(resource);
					}
				}
			} else {
				args.push({ from: request.from }); // TODO@telemetry this is a bit weird to send this to every action?
			}

			this.commandService.executeCommand(request.id, ...args).done(_ => {
K
kieferrm 已提交
150
				/* __GDPR__
K
kieferrm 已提交
151 152 153 154 155
					"commandExecuted" : {
						"id" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
						"from": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
					}
				*/
156
				this.telemetryService.publicLog('commandExecuted', { id: request.id, from: request.from });
J
Johannes Rieken 已提交
157 158 159
			}, err => {
				this.messageService.show(Severity.Error, err);
			});
160 161 162
		});

		// Support resolve keybindings event
163
		ipc.on('vscode:resolveKeybindings', (_event: any, rawActionIds: string) => {
164 165 166 167 168 169 170 171
			let actionIds: string[] = [];
			try {
				actionIds = JSON.parse(rawActionIds);
			} catch (error) {
				// should not happen
			}

			// Resolve keys using the keybinding service and send back to browser process
172
			this.resolveKeybindings(actionIds).done(keybindings => {
173 174 175 176 177 178
				if (keybindings.length) {
					ipc.send('vscode:keybindingsResolved', JSON.stringify(keybindings));
				}
			}, () => errors.onUnexpectedError);
		});

179
		ipc.on('vscode:reportError', (_event: any, error: string) => {
180 181 182 183 184 185 186 187
			if (error) {
				const errorParsed = JSON.parse(error);
				errorParsed.mainProcess = true;
				errors.onUnexpectedError(errorParsed);
			}
		});

		// Support openFiles event for existing and new files
188
		ipc.on('vscode:openFiles', (_event: any, request: IOpenFileRequest) => this.onOpenFiles(request));
189

190
		// Support addFolders event if we have a workspace opened
191
		ipc.on('vscode:addFolders', (_event: any, request: IAddFoldersRequest) => this.onAddFoldersRequest(request));
192

193
		// Message support
194
		ipc.on('vscode:showInfoMessage', (_event: any, message: string) => {
195 196 197 198
			this.messageService.show(Severity.Info, message);
		});

		// Support toggling auto save
199
		ipc.on('vscode.toggleAutoSave', () => {
200 201 202 203
			this.toggleAutoSave();
		});

		// Fullscreen Events
204
		ipc.on('vscode:enterFullScreen', () => {
205
			this.lifecycleService.when(LifecyclePhase.Running).then(() => {
206 207 208 209
				browser.setFullscreen(true);
			});
		});

210
		ipc.on('vscode:leaveFullScreen', () => {
211
			this.lifecycleService.when(LifecyclePhase.Running).then(() => {
212 213 214 215 216
				browser.setFullscreen(false);
			});
		});

		// High Contrast Events
217
		ipc.on('vscode:enterHighContrast', () => {
218
			const windowConfig = this.configurationService.getValue<IWindowSettings>('window');
219
			if (windowConfig && windowConfig.autoDetectHighContrast) {
220
				this.lifecycleService.when(LifecyclePhase.Running).then(() => {
M
Martin Aeschlimann 已提交
221
					this.themeService.setColorTheme(VS_HC_THEME, null);
222 223
				});
			}
224 225
		});

226
		ipc.on('vscode:leaveHighContrast', () => {
227
			const windowConfig = this.configurationService.getValue<IWindowSettings>('window');
228
			if (windowConfig && windowConfig.autoDetectHighContrast) {
229
				this.lifecycleService.when(LifecyclePhase.Running).then(() => {
M
Martin Aeschlimann 已提交
230
					this.themeService.setColorTheme(VS_DARK_THEME, null);
231 232
				});
			}
233 234
		});

A
Alex Dima 已提交
235
		// keyboard layout changed event
236
		ipc.on('vscode:keyboardLayoutChanged', () => {
237
			KeyboardMapperFactory.INSTANCE._onKeyboardLayoutChanged();
A
Alex Dima 已提交
238 239
		});

240
		// keyboard layout changed event
241
		ipc.on('vscode:accessibilitySupportChanged', (_event: any, accessibilitySupportEnabled: boolean) => {
242
			browser.setAccessibilitySupport(accessibilitySupportEnabled ? platform.AccessibilitySupport.Enabled : platform.AccessibilitySupport.Disabled);
243 244
		});

S
Sandeep Somavarapu 已提交
245 246 247 248 249 250 251
		// Zoom level changes
		this.updateWindowZoomLevel();
		this.toUnbind.push(this.configurationService.onDidChangeConfiguration(e => {
			if (e.affectsConfiguration('window.zoomLevel')) {
				this.updateWindowZoomLevel();
			}
		}));
252

253 254 255
		// Context menu support in input/textarea
		window.document.addEventListener('contextmenu', e => this.onContextMenu(e));
	}
256

257 258 259 260 261 262
	private onContextMenu(e: PointerEvent): void {
		if (e.target instanceof HTMLElement) {
			const target = <HTMLElement>e.target;
			if (target.nodeName && (target.nodeName.toLowerCase() === 'input' || target.nodeName.toLowerCase() === 'textarea')) {
				e.preventDefault();
				e.stopPropagation();
263

264 265 266 267
				this.contextMenuService.showContextMenu({
					getAnchor: () => e,
					getActions: () => TPromise.as(TextInputActions)
				});
268
			}
269 270 271
		}
	}

S
Sandeep Somavarapu 已提交
272
	private updateWindowZoomLevel(): void {
273

274
		const windowConfig: IWindowsConfiguration = this.configurationService.getValue<IWindowsConfiguration>();
275 276 277 278

		let newZoomLevel = 0;
		if (windowConfig.window && typeof windowConfig.window.zoomLevel === 'number') {
			newZoomLevel = windowConfig.window.zoomLevel;
279

280 281 282
			// Leave early if the configured zoom level did not change (https://github.com/Microsoft/vscode/issues/1536)
			if (this.previousConfiguredZoomLevel === newZoomLevel) {
				return;
283
			}
284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308

			this.previousConfiguredZoomLevel = newZoomLevel;
		}

		if (webFrame.getZoomLevel() !== newZoomLevel) {
			webFrame.setZoomLevel(newZoomLevel);
			browser.setZoomFactor(webFrame.getZoomFactor());
			// See https://github.com/Microsoft/vscode/issues/26151
			// Cannot be trusted because the webFrame might take some time
			// until it really applies the new zoom level
			browser.setZoomLevel(webFrame.getZoomLevel(), /*isTrusted*/false);
		}
	}

	private create(): void {

		// Handle window.open() calls
		const $this = this;
		(<any>window).open = function (url: string, target: string, features: string, replace: boolean): any {
			$this.windowsService.openExternal(url);

			return null;
		};

		// Send over all extension viewlets when extensions are ready
309
		this.extensionService.whenInstalledExtensionsRegistered().then(() => {
310
			ipc.send('vscode:extensionViewlets', JSON.stringify(this.viewletService.getViewlets().filter(v => !!v.extensionId).map(v => { return { id: v.id, label: v.name }; })));
311 312
		});

313
		// Emit event when vscode has loaded
314
		this.lifecycleService.when(LifecyclePhase.Running).then(() => {
315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364
			ipc.send('vscode:workbenchLoaded', this.windowService.getCurrentWindowId());
		});

		// Touchbar Support
		this.updateTouchbarMenu();
	}

	private updateTouchbarMenu(): void {
		if (!platform.isMacintosh) {
			return; // macOS only
		}

		// Dispose old
		this.touchBarDisposables = dispose(this.touchBarDisposables);

		// Create new
		this.touchBarMenu = this.editorGroupService.invokeWithinEditorContext(accessor => this.menuService.createMenu(MenuId.TouchBarContext, accessor.get(IContextKeyService)));
		this.touchBarDisposables.push(this.touchBarMenu);
		this.touchBarDisposables.push(this.touchBarMenu.onDidChange(() => {
			this.scheduleSetupTouchbar();
		}));

		this.scheduleSetupTouchbar();
	}

	private scheduleSetupTouchbar(): void {
		this.touchBarUpdater.schedule();
	}

	private doSetupTouchbar(): void {
		const actions: (MenuItemAction | Separator)[] = [];

		// Fill actions into groups respecting order
		fillInActions(this.touchBarMenu, void 0, actions);

		// Convert into command action multi array
		const items: ICommandAction[][] = [];
		let group: ICommandAction[] = [];
		for (let i = 0; i < actions.length; i++) {
			const action = actions[i];

			// Command
			if (action instanceof MenuItemAction) {
				group.push(action.item);
			}

			// Separator
			else if (action instanceof Separator) {
				if (group.length) {
					items.push(group);
365
				}
366 367

				group = [];
368
			}
369 370 371 372 373 374 375 376 377 378 379
		}

		if (group.length) {
			items.push(group);
		}

		// Only update if the actions have changed
		if (!objects.equals(this.lastInstalledTouchedBar, items)) {
			this.lastInstalledTouchedBar = items;
			this.windowService.updateTouchBar(items);
		}
380 381
	}

382
	private resolveKeybindings(actionIds: string[]): TPromise<{ id: string; label: string, isNative: boolean; }[]> {
383
		return TPromise.join([this.lifecycleService.when(LifecyclePhase.Running), this.extensionService.whenInstalledExtensionsRegistered()]).then(() => {
384
			return arrays.coalesce(actionIds.map(id => {
385
				const binding = this.keybindingService.lookupKeybinding(id);
386 387 388
				if (!binding) {
					return null;
				}
389

390 391 392 393 394
				// first try to resolve a native accelerator
				const electronAccelerator = binding.getElectronAccelerator();
				if (electronAccelerator) {
					return { id, label: electronAccelerator, isNative: true };
				}
395

396 397 398 399
				// we need this fallback to support keybindings that cannot show in electron menus (e.g. chords)
				const acceleratorLabel = binding.getLabel();
				if (acceleratorLabel) {
					return { id, label: acceleratorLabel, isNative: false };
400 401 402 403 404 405 406
				}

				return null;
			}));
		});
	}

407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425
	private onAddFoldersRequest(request: IAddFoldersRequest): void {

		// Buffer all pending requests
		this.pendingFoldersToAdd.push(request);

		// Delay the adding of folders a bit to buffer in case more requests are coming
		if (!this.addFoldersScheduler.isScheduled()) {
			this.addFoldersScheduler.schedule();
		}
	}

	private doAddFolders(): void {
		const foldersToAdd: IWorkspaceFolderCreationData[] = [];

		this.pendingFoldersToAdd.forEach(request => {
			foldersToAdd.push(...request.foldersToAdd.map(folderToAdd => ({ uri: URI.file(folderToAdd.filePath) })));
		});

		this.pendingFoldersToAdd = [];
426

427
		this.workspaceEditingService.addFolders(foldersToAdd).done(null, errors.onUnexpectedError);
428 429
	}

430
	private onOpenFiles(request: IOpenFileRequest): void {
431 432
		const inputs: IResourceInputType[] = [];
		const diffMode = (request.filesToDiff.length === 2);
433 434 435 436 437 438 439 440 441 442 443 444 445 446

		if (!diffMode && request.filesToOpen) {
			inputs.push(...this.toInputs(request.filesToOpen, false));
		}

		if (!diffMode && request.filesToCreate) {
			inputs.push(...this.toInputs(request.filesToCreate, true));
		}

		if (diffMode) {
			inputs.push(...this.toInputs(request.filesToDiff, false));
		}

		if (inputs.length) {
447
			this.openResources(inputs, diffMode).then(null, errors.onUnexpectedError);
448
		}
449 450 451 452 453 454 455 456 457 458 459 460 461 462 463

		if (request.filesToWait && inputs.length) {
			// In wait mode, listen to changes to the editors and wait until the files
			// are closed that the user wants to wait for. When this happens we delete
			// the wait marker file to signal to the outside that editing is done.
			const resourcesToWaitFor = request.filesToWait.paths.map(p => URI.file(p.filePath));
			const waitMarkerFile = URI.file(request.filesToWait.waitMarkerFilePath);
			const stacks = this.editorGroupService.getStacksModel();
			const unbind = stacks.onEditorClosed(() => {
				if (resourcesToWaitFor.every(r => !stacks.isOpen(r))) {
					unbind.dispose();
					this.fileService.del(waitMarkerFile).done(null, errors.onUnexpectedError);
				}
			});
		}
464 465
	}

466 467
	private openResources(resources: (IResourceInput | IUntitledResourceInput)[], diffMode: boolean): Thenable<IEditor | IEditor[]> {
		return this.lifecycleService.when(LifecyclePhase.Running).then((): TPromise<IEditor | IEditor[]> => {
N
Nick Snyder 已提交
468

469
			// In diffMode we open 2 resources as diff
470
			if (diffMode && resources.length === 2) {
471
				return this.editorService.openEditor({ leftResource: resources[0].resource, rightResource: resources[1].resource, options: { pinned: true } });
472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489
			}

			// For one file, just put it into the current active editor
			if (resources.length === 1) {
				return this.editorService.openEditor(resources[0]);
			}

			// Otherwise open all
			const activeEditor = this.editorService.getActiveEditor();
			return this.editorService.openEditors(resources.map((r, index) => {
				return {
					input: r,
					position: activeEditor ? activeEditor.position : Position.ONE
				};
			}));
		});
	}

490
	private toInputs(paths: IPath[], isNew: boolean): IResourceInputType[] {
491
		return paths.map(p => {
492 493 494 495 496 497 498
			const resource = URI.file(p.filePath);
			let input: IResourceInput | IUntitledResourceInput;
			if (isNew) {
				input = { filePath: resource.fsPath, options: { pinned: true } } as IUntitledResourceInput;
			} else {
				input = { resource, options: { pinned: true } } as IResourceInput;
			}
499 500 501 502 503 504 505 506 507 508 509 510 511

			if (!isNew && p.lineNumber) {
				input.options.selection = {
					startLineNumber: p.lineNumber,
					startColumn: p.columnNumber
				};
			}

			return input;
		});
	}

	private toggleAutoSave(): void {
512
		const setting = this.configurationService.inspect(ElectronWindow.AUTO_SAVE_SETTING);
513 514 515 516 517 518 519 520 521 522 523 524
		let userAutoSaveConfig = setting.user;
		if (types.isUndefinedOrNull(userAutoSaveConfig)) {
			userAutoSaveConfig = setting.default; // use default if setting not defined
		}

		let newAutoSaveValue: string;
		if ([AutoSaveConfiguration.AFTER_DELAY, AutoSaveConfiguration.ON_FOCUS_CHANGE, AutoSaveConfiguration.ON_WINDOW_CHANGE].some(s => s === userAutoSaveConfig)) {
			newAutoSaveValue = AutoSaveConfiguration.OFF;
		} else {
			newAutoSaveValue = AutoSaveConfiguration.AFTER_DELAY;
		}

525
		this.configurationService.updateValue(ElectronWindow.AUTO_SAVE_SETTING, newAutoSaveValue, ConfigurationTarget.USER);
526
	}
527 528 529 530 531 532

	public dispose(): void {
		this.touchBarDisposables = dispose(this.touchBarDisposables);

		super.dispose();
	}
J
Johannes Rieken 已提交
533
}