window.ts 19.6 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';
J
Johannes Rieken 已提交
20
import { IPartService } from 'vs/workbench/services/part/common/partService';
21
import { AutoSaveConfiguration, IFileService } from 'vs/platform/files/common/files';
22
import { toResource } from 'vs/workbench/common/editor';
23
import { IWorkbenchEditorService, IResourceInputType } from 'vs/workbench/services/editor/common/editorService';
J
Johannes Rieken 已提交
24
import { IEditorGroupService } from 'vs/workbench/services/group/common/groupService';
25
import { IMessageService } from 'vs/platform/message/common/message';
J
Johannes Rieken 已提交
26
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
27
import { IWorkspaceConfigurationService } from 'vs/workbench/services/configuration/common/configuration';
28
import { IWindowsService, IWindowService, IWindowSettings, IPath, IOpenFileRequest, IWindowsConfiguration, IAddFoldersRequest, IRunActionInWindowRequest } from 'vs/platform/windows/common/windows';
29 30 31
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
B
Benjamin Pasero 已提交
32
import { ITitleService } from 'vs/workbench/services/title/common/titleService';
33
import { IWorkbenchThemeService, VS_HC_THEME, VS_DARK_THEME } from 'vs/workbench/services/themes/common/workbenchThemeService';
34 35 36
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 已提交
37
import { Position, IResourceInput, IUntitledResourceInput, IEditor } from 'vs/platform/editor/common/editor';
38
import { IExtensionService } from 'vs/platform/extensions/common/extensions';
A
Alex Dima 已提交
39
import { KeyboardMapperFactory } from 'vs/workbench/services/keybinding/electron-browser/keybindingService';
40
import { Themable } from 'vs/workbench/common/theme';
41
import { ipcRenderer as ipc, webFrame } from 'electron';
42
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
43
import { IWorkspaceEditingService } from 'vs/workbench/services/workspace/common/workspaceEditing';
44 45 46 47 48
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';
49
import { ConfigurationTarget, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration';
E
Erich Gamma 已提交
50

51 52 53 54 55 56 57 58 59 60 61
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))
];

62
export class ElectronWindow extends Themable {
63 64 65

	private static AUTO_SAVE_SETTING = 'files.autoSave';

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

	private previousConfiguredZoomLevel: number;

E
Erich Gamma 已提交
73 74 75
	constructor(
		shellContainer: HTMLElement,
		@IWorkbenchEditorService private editorService: IWorkbenchEditorService,
76
		@IEditorGroupService private editorGroupService: IEditorGroupService,
77
		@IPartService private partService: IPartService,
78
		@IWindowsService private windowsService: IWindowsService,
B
Benjamin Pasero 已提交
79
		@IWindowService private windowService: IWindowService,
80 81
		@IWorkspaceConfigurationService private configurationService: IWorkspaceConfigurationService,
		@ITitleService private titleService: ITitleService,
82
		@IWorkbenchThemeService protected themeService: IWorkbenchThemeService,
83 84 85 86 87 88
		@IMessageService private messageService: IMessageService,
		@ICommandService private commandService: ICommandService,
		@IExtensionService private extensionService: IExtensionService,
		@IViewletService private viewletService: IViewletService,
		@IContextMenuService private contextMenuService: IContextMenuService,
		@IKeybindingService private keybindingService: IKeybindingService,
89
		// @ts-ignore unused injected service
J
Johannes Rieken 已提交
90
		@IEnvironmentService private environmentService: IEnvironmentService,
91
		@ITelemetryService private telemetryService: ITelemetryService,
92
		// @ts-ignore unused injected service
93
		@IWorkspaceContextService private contextService: IWorkspaceContextService,
94
		@IWorkspaceEditingService private workspaceEditingService: IWorkspaceEditingService,
95 96
		@IFileService private fileService: IFileService,
		@IMenuService private menuService: IMenuService,
97
		// @ts-ignore unused injected service
98
		@IContextKeyService private contextKeyService: IContextKeyService
E
Erich Gamma 已提交
99
	) {
100 101
		super(themeService);

102 103 104 105 106
		this.touchBarDisposables = [];

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

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 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149
		ipc.on('vscode:runAction', (event, request: IRunActionInWindowRequest) => {
			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 163 164 165 166 167 168 169 170 171
		});

		// Support resolve keybindings event
		ipc.on('vscode:resolveKeybindings', (event, rawActionIds: string) => {
			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 179 180 181 182 183 184 185 186 187 188 189
				if (keybindings.length) {
					ipc.send('vscode:keybindingsResolved', JSON.stringify(keybindings));
				}
			}, () => errors.onUnexpectedError);
		});

		ipc.on('vscode:reportError', (event, error) => {
			if (error) {
				const errorParsed = JSON.parse(error);
				errorParsed.mainProcess = true;
				errors.onUnexpectedError(errorParsed);
			}
		});

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

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

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

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

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

210
		ipc.on('vscode:leaveFullScreen', event => {
211 212 213 214 215 216
			this.partService.joinCreation().then(() => {
				browser.setFullscreen(false);
			});
		});

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

226
		ipc.on('vscode:leaveHighContrast', event => {
227
			const windowConfig = this.configurationService.getConfiguration<IWindowSettings>('window');
228
			if (windowConfig && windowConfig.autoDetectHighContrast) {
229
				this.partService.joinCreation().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 237
		ipc.on('vscode:keyboardLayoutChanged', event => {
			KeyboardMapperFactory.INSTANCE._onKeyboardLayoutChanged();
A
Alex Dima 已提交
238 239
		});

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

245
		// Configuration changes
246
		this.toUnbind.push(this.configurationService.onDidChangeConfiguration(e => this.onDidUpdateConfiguration(e)));
247

248 249 250
		// Context menu support in input/textarea
		window.document.addEventListener('contextmenu', e => this.onContextMenu(e));
	}
251

252 253 254 255 256 257
	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();
258

259 260 261 262
				this.contextMenuService.showContextMenu({
					getAnchor: () => e,
					getActions: () => TPromise.as(TextInputActions)
				});
263
			}
264 265 266
		}
	}

267 268 269 270 271
	private onDidUpdateConfiguration(event: IConfigurationChangeEvent): void {
		if (!event.affectsConfiguration('window.zoomLevel')) {
			return;
		}

272 273 274 275 276
		const windowConfig: IWindowsConfiguration = this.configurationService.getConfiguration<IWindowsConfiguration>();

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

278 279 280
			// Leave early if the configured zoom level did not change (https://github.com/Microsoft/vscode/issues/1536)
			if (this.previousConfiguredZoomLevel === newZoomLevel) {
				return;
281
			}
282 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
		this.extensionService.onReady().then(() => {
			ipc.send('vscode:extensionViewlets', JSON.stringify(this.viewletService.getViewlets().filter(v => !!v.extensionId).map(v => { return { id: v.id, label: v.name }; })));
309 310
		});

311 312 313 314 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
		// Emit event when vscode has loaded
		this.partService.joinCreation().then(() => {
			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);
363
				}
364 365

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

		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);
		}
378 379
	}

380
	private resolveKeybindings(actionIds: string[]): TPromise<{ id: string; label: string, isNative: boolean; }[]> {
381
		return TPromise.join([this.partService.joinCreation(), this.extensionService.onReady()]).then(() => {
382
			return arrays.coalesce(actionIds.map(id => {
383
				const binding = this.keybindingService.lookupKeybinding(id);
384 385 386
				if (!binding) {
					return null;
				}
387

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

394 395 396 397
				// 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 };
398 399 400 401 402 403 404
				}

				return null;
			}));
		});
	}

405
	private onAddFolders(request: IAddFoldersRequest): void {
406
		const foldersToAdd = request.foldersToAdd.map(folderToAdd => ({ uri: URI.file(folderToAdd.filePath) }));
407

408
		this.workspaceEditingService.addFolders(foldersToAdd).done(null, errors.onUnexpectedError);
409 410
	}

411
	private onOpenFiles(request: IOpenFileRequest): void {
412 413
		const inputs: IResourceInputType[] = [];
		const diffMode = (request.filesToDiff.length === 2);
414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429

		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) {
			this.openResources(inputs, diffMode).done(null, errors.onUnexpectedError);
		}
430 431 432 433 434 435 436 437 438 439 440 441 442 443 444

		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);
				}
			});
		}
445 446
	}

N
Nick Snyder 已提交
447 448 449
	private openResources(resources: (IResourceInput | IUntitledResourceInput)[], diffMode: boolean): TPromise<IEditor | IEditor[]> {
		return this.partService.joinCreation().then((): TPromise<IEditor | IEditor[]> => {

450
			// In diffMode we open 2 resources as diff
451
			if (diffMode && resources.length === 2) {
452
				return this.editorService.openEditor({ leftResource: resources[0].resource, rightResource: resources[1].resource, options: { pinned: true } });
453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470
			}

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

471
	private toInputs(paths: IPath[], isNew: boolean): IResourceInputType[] {
472
		return paths.map(p => {
473 474 475 476 477 478 479
			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;
			}
480 481 482 483 484 485 486 487 488 489 490 491 492

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

			return input;
		});
	}

	private toggleAutoSave(): void {
493
		const setting = this.configurationService.inspect(ElectronWindow.AUTO_SAVE_SETTING);
494 495 496 497 498 499 500 501 502 503 504 505
		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;
		}

506
		this.configurationService.updateValue(ElectronWindow.AUTO_SAVE_SETTING, newAutoSaveValue, ConfigurationTarget.USER);
507
	}
508 509 510 511 512 513

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

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