window.ts 19.1 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
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
B
Benjamin Pasero 已提交
31
import { ITitleService } from 'vs/workbench/services/title/common/titleService';
32
import { IWorkbenchThemeService, VS_HC_THEME, VS_DARK_THEME } from 'vs/workbench/services/themes/common/workbenchThemeService';
33 34 35
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 已提交
36
import { Position, IResourceInput, IUntitledResourceInput, IEditor } from 'vs/platform/editor/common/editor';
37
import { IExtensionService } from 'vs/platform/extensions/common/extensions';
A
Alex Dima 已提交
38
import { KeyboardMapperFactory } from 'vs/workbench/services/keybinding/electron-browser/keybindingService';
39
import { Themable } from 'vs/workbench/common/theme';
40
import { ipcRenderer as ipc, webFrame } from 'electron';
41
import { IWorkspaceEditingService } from 'vs/workbench/services/workspace/common/workspaceEditing';
42 43 44 45 46
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';
47
import { ConfigurationTarget, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration';
E
Erich Gamma 已提交
48

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

60
export class ElectronWindow extends Themable {
61 62 63

	private static AUTO_SAVE_SETTING = 'files.autoSave';

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

	private previousConfiguredZoomLevel: number;

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

94 95 96 97 98
		this.touchBarDisposables = [];

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

E
Erich Gamma 已提交
99
		this.registerListeners();
100
		this.create();
E
Erich Gamma 已提交
101 102 103 104
	}

	private registerListeners(): void {

105
		// React to editor input changes
106
		this.toUnbind.push(this.editorGroupService.onEditorsChanged(() => {
E
Erich Gamma 已提交
107

108 109
			// Represented File Name
			const file = toResource(this.editorService.getActiveEditorInput(), { supportSideBySide: true, filter: 'file' });
110
			this.titleService.setRepresentedFilename(file ? file.fsPath : '');
111 112 113 114

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

116
		// prevent opening a real URL inside the shell
117 118 119 120
		[DOM.EventType.DRAG_OVER, DOM.EventType.DROP].forEach(event => {
			window.document.body.addEventListener(event, (e: DragEvent) => {
				DOM.EventHelper.stop(e);
			});
E
Erich Gamma 已提交
121 122
		});

123
		// Support runAction event
124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141
		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 已提交
142
				/* __GDPR__
K
kieferrm 已提交
143 144 145 146 147
					"commandExecuted" : {
						"id" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
						"from": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
					}
				*/
148
				this.telemetryService.publicLog('commandExecuted', { id: request.id, from: request.from });
J
Johannes Rieken 已提交
149 150 151
			}, err => {
				this.messageService.show(Severity.Error, err);
			});
152 153 154 155 156 157 158 159 160 161 162 163
		});

		// 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
164
			this.resolveKeybindings(actionIds).done(keybindings => {
165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181
				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));

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

185 186 187 188 189 190
		// Message support
		ipc.on('vscode:showInfoMessage', (event, message: string) => {
			this.messageService.show(Severity.Info, message);
		});

		// Support toggling auto save
191
		ipc.on('vscode.toggleAutoSave', event => {
192 193 194 195
			this.toggleAutoSave();
		});

		// Fullscreen Events
196
		ipc.on('vscode:enterFullScreen', event => {
197 198 199 200 201
			this.partService.joinCreation().then(() => {
				browser.setFullscreen(true);
			});
		});

202
		ipc.on('vscode:leaveFullScreen', event => {
203 204 205 206 207 208
			this.partService.joinCreation().then(() => {
				browser.setFullscreen(false);
			});
		});

		// High Contrast Events
209
		ipc.on('vscode:enterHighContrast', event => {
210
			const windowConfig = this.configurationService.getConfiguration<IWindowSettings>('window');
211
			if (windowConfig && windowConfig.autoDetectHighContrast) {
212
				this.partService.joinCreation().then(() => {
M
Martin Aeschlimann 已提交
213
					this.themeService.setColorTheme(VS_HC_THEME, null);
214 215
				});
			}
216 217
		});

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

A
Alex Dima 已提交
227
		// keyboard layout changed event
228 229
		ipc.on('vscode:keyboardLayoutChanged', event => {
			KeyboardMapperFactory.INSTANCE._onKeyboardLayoutChanged();
A
Alex Dima 已提交
230 231
		});

232 233
		// keyboard layout changed event
		ipc.on('vscode:accessibilitySupportChanged', (event, accessibilitySupportEnabled: boolean) => {
234
			browser.setAccessibilitySupport(accessibilitySupportEnabled ? platform.AccessibilitySupport.Enabled : platform.AccessibilitySupport.Disabled);
235 236
		});

237
		// Configuration changes
238
		this.toUnbind.push(this.configurationService.onDidChangeConfiguration(e => this.onDidUpdateConfiguration(e)));
239

240 241 242
		// Context menu support in input/textarea
		window.document.addEventListener('contextmenu', e => this.onContextMenu(e));
	}
243

244 245 246 247 248 249
	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();
250

251 252 253 254
				this.contextMenuService.showContextMenu({
					getAnchor: () => e,
					getActions: () => TPromise.as(TextInputActions)
				});
255
			}
256 257 258
		}
	}

259 260 261 262 263
	private onDidUpdateConfiguration(event: IConfigurationChangeEvent): void {
		if (!event.affectsConfiguration('window.zoomLevel')) {
			return;
		}

264 265 266 267 268
		const windowConfig: IWindowsConfiguration = this.configurationService.getConfiguration<IWindowsConfiguration>();

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

270 271 272
			// Leave early if the configured zoom level did not change (https://github.com/Microsoft/vscode/issues/1536)
			if (this.previousConfiguredZoomLevel === newZoomLevel) {
				return;
273
			}
274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300

			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 }; })));
301 302
		});

303 304 305 306 307 308 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
		// 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);
355
				}
356 357

				group = [];
358
			}
359 360 361 362 363 364 365 366 367 368 369
		}

		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);
		}
370 371
	}

372
	private resolveKeybindings(actionIds: string[]): TPromise<{ id: string; label: string, isNative: boolean; }[]> {
373
		return TPromise.join([this.partService.joinCreation(), this.extensionService.onReady()]).then(() => {
374
			return arrays.coalesce(actionIds.map(id => {
375
				const binding = this.keybindingService.lookupKeybinding(id);
376 377 378
				if (!binding) {
					return null;
				}
379

380 381 382 383 384
				// first try to resolve a native accelerator
				const electronAccelerator = binding.getElectronAccelerator();
				if (electronAccelerator) {
					return { id, label: electronAccelerator, isNative: true };
				}
385

386 387 388 389
				// 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 };
390 391 392 393 394 395 396
				}

				return null;
			}));
		});
	}

397
	private onAddFolders(request: IAddFoldersRequest): void {
398
		const foldersToAdd = request.foldersToAdd.map(folderToAdd => ({ uri: URI.file(folderToAdd.filePath) }));
399

400
		this.workspaceEditingService.addFolders(foldersToAdd).done(null, errors.onUnexpectedError);
401 402
	}

403
	private onOpenFiles(request: IOpenFileRequest): void {
404 405
		const inputs: IResourceInputType[] = [];
		const diffMode = (request.filesToDiff.length === 2);
406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421

		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);
		}
422 423 424 425 426 427 428 429 430 431 432 433 434 435 436

		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);
				}
			});
		}
437 438
	}

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

442
			// In diffMode we open 2 resources as diff
443
			if (diffMode && resources.length === 2) {
444
				return this.editorService.openEditor({ leftResource: resources[0].resource, rightResource: resources[1].resource, options: { pinned: true } });
445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462
			}

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

463
	private toInputs(paths: IPath[], isNew: boolean): IResourceInputType[] {
464
		return paths.map(p => {
465 466 467 468 469 470 471
			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;
			}
472 473 474 475 476 477 478 479 480 481 482 483 484

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

			return input;
		});
	}

	private toggleAutoSave(): void {
485
		const setting = this.configurationService.inspect(ElectronWindow.AUTO_SAVE_SETTING);
486 487 488 489 490 491 492 493 494 495 496 497
		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;
		}

498
		this.configurationService.updateValue(ElectronWindow.AUTO_SAVE_SETTING, newAutoSaveValue, ConfigurationTarget.USER);
499
	}
500 501 502 503 504 505

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

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