titlebarPart.ts 17.0 KB
Newer Older
B
Benjamin Pasero 已提交
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';

R
Ryan Adolf 已提交
8
import * as path from 'path';
B
Benjamin Pasero 已提交
9
import 'vs/css!./media/titlebarpart';
B
Benjamin Pasero 已提交
10
import { TPromise } from 'vs/base/common/winjs.base';
B
Benjamin Pasero 已提交
11
import { Builder, $, Dimension } from 'vs/base/browser/builder';
B
Benjamin Pasero 已提交
12 13
import * as DOM from 'vs/base/browser/dom';
import * as paths from 'vs/base/common/paths';
B
Benjamin Pasero 已提交
14
import { Part } from 'vs/workbench/browser/part';
15
import { ITitleService, ITitleProperties } from 'vs/workbench/services/title/common/titleService';
B
Benjamin Pasero 已提交
16
import { getZoomFactor } from 'vs/base/browser/browser';
B
Benjamin Pasero 已提交
17 18 19 20 21
import { IWindowService, IWindowsService } from 'vs/platform/windows/common/windows';
import * as errors from 'vs/base/common/errors';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { StandardMouseEvent } from 'vs/base/browser/mouseEvent';
import { IAction, Action } from 'vs/base/common/actions';
22
import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration';
23 24 25 26 27
import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService';
import { IEditorGroupService } from 'vs/workbench/services/group/common/groupService';
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
import nls = require('vs/nls');
import * as labels from 'vs/base/common/labels';
28
import { EditorInput, toResource } from 'vs/workbench/common/editor';
29
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
30
import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace';
31
import { Verbosity } from 'vs/platform/editor/common/editor';
B
Benjamin Pasero 已提交
32
import { IThemeService } from 'vs/platform/theme/common/themeService';
33
import { TITLE_BAR_ACTIVE_BACKGROUND, TITLE_BAR_ACTIVE_FOREGROUND, TITLE_BAR_INACTIVE_FOREGROUND, TITLE_BAR_INACTIVE_BACKGROUND, TITLE_BAR_BORDER } from 'vs/workbench/common/theme';
R
Ryan Adolf 已提交
34
import { isMacintosh, isWindows } from 'vs/base/common/platform';
B
Benjamin Pasero 已提交
35
import URI from 'vs/base/common/uri';
R
Ryan Adolf 已提交
36
import { Color } from 'vs/base/common/color';
37
import { ILifecycleService, LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle';
B
Benjamin Pasero 已提交
38 39 40 41 42

export class TitlebarPart extends Part implements ITitleService {

	public _serviceBrand: any;

43
	private static readonly NLS_UNSUPPORTED = nls.localize('patchedWindowTitle', "[Unsupported]");
44
	private static readonly NLS_USER_IS_ADMIN = isWindows ? nls.localize('userIsAdmin', "[Administrator]") : nls.localize('userIsSudo', "[Superuser]");
45 46 47
	private static readonly NLS_EXTENSION_HOST = nls.localize('devExtensionWindowTitlePrefix', "[Extension Development Host]");
	private static readonly TITLE_DIRTY = '\u25cf ';
	private static readonly TITLE_SEPARATOR = isMacintosh ? '' : ' - '; // macOS uses special - separator
48

B
Benjamin Pasero 已提交
49 50 51 52
	private titleContainer: Builder;
	private title: Builder;
	private pendingTitle: string;
	private initialTitleFontSize: number;
B
Benjamin Pasero 已提交
53
	private representedFileName: string;
B
Benjamin Pasero 已提交
54

B
Benjamin Pasero 已提交
55 56
	private isInactive: boolean;

57
	private properties: ITitleProperties;
58 59
	private activeEditorListeners: IDisposable[];

60 61
	constructor(
		id: string,
B
Benjamin Pasero 已提交
62 63
		@IContextMenuService private contextMenuService: IContextMenuService,
		@IWindowService private windowService: IWindowService,
64 65 66 67 68
		@IConfigurationService private configurationService: IConfigurationService,
		@IWindowsService private windowsService: IWindowsService,
		@IWorkbenchEditorService private editorService: IWorkbenchEditorService,
		@IEditorGroupService private editorGroupService: IEditorGroupService,
		@IEnvironmentService private environmentService: IEnvironmentService,
B
Benjamin Pasero 已提交
69
		@IWorkspaceContextService private contextService: IWorkspaceContextService,
70
		@IThemeService themeService: IThemeService,
71
		@ILifecycleService private lifecycleService: ILifecycleService
72
	) {
B
Benjamin Pasero 已提交
73
		super(id, { hasTitle: false }, themeService);
74

75
		this.properties = { isPure: true, isAdmin: false };
76 77 78 79
		this.activeEditorListeners = [];

		this.init();

80 81 82
		this.registerListeners();
	}

83 84
	private init(): void {

85
		// Initial window title when loading is done
86
		this.lifecycleService.when(LifecyclePhase.Running).then(() => this.setTitle(this.getWindowTitle()));
87 88
	}

89
	private registerListeners(): void {
B
Benjamin Pasero 已提交
90 91
		this.toUnbind.push(DOM.addDisposableListener(window, DOM.EventType.BLUR, () => this.onBlur()));
		this.toUnbind.push(DOM.addDisposableListener(window, DOM.EventType.FOCUS, () => this.onFocus()));
92
		this.toUnbind.push(this.configurationService.onDidChangeConfiguration(e => this.onConfigurationChanged(e)));
93
		this.toUnbind.push(this.editorGroupService.onEditorsChanged(() => this.onEditorsChanged()));
94 95 96
		this.toUnbind.push(this.contextService.onDidChangeWorkspaceFolders(() => this.setTitle(this.getWindowTitle())));
		this.toUnbind.push(this.contextService.onDidChangeWorkbenchState(() => this.setTitle(this.getWindowTitle())));
		this.toUnbind.push(this.contextService.onDidChangeWorkspaceName(() => this.setTitle(this.getWindowTitle())));
97 98
	}

B
Benjamin Pasero 已提交
99 100 101 102 103 104 105 106 107 108
	private onBlur(): void {
		this.isInactive = true;
		this.updateStyles();
	}

	private onFocus(): void {
		this.isInactive = false;
		this.updateStyles();
	}

109 110
	private onConfigurationChanged(event: IConfigurationChangeEvent): void {
		if (event.affectsConfiguration('window.title')) {
111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144
			this.setTitle(this.getWindowTitle());
		}
	}

	private onEditorsChanged(): void {

		// Dispose old listeners
		dispose(this.activeEditorListeners);
		this.activeEditorListeners = [];

		const activeEditor = this.editorService.getActiveEditor();
		const activeInput = activeEditor ? activeEditor.input : void 0;

		// Calculate New Window Title
		this.setTitle(this.getWindowTitle());

		// Apply listener for dirty and label changes
		if (activeInput instanceof EditorInput) {
			this.activeEditorListeners.push(activeInput.onDidChangeDirty(() => {
				this.setTitle(this.getWindowTitle());
			}));

			this.activeEditorListeners.push(activeInput.onDidChangeLabel(() => {
				this.setTitle(this.getWindowTitle());
			}));
		}
	}

	private getWindowTitle(): string {
		let title = this.doGetWindowTitle();
		if (!title) {
			title = this.environmentService.appNameLong;
		}

145 146 147 148 149
		if (this.properties.isAdmin) {
			title = `${title} ${TitlebarPart.NLS_USER_IS_ADMIN}`;
		}

		if (!this.properties.isPure) {
150 151 152 153 154 155 156 157 158 159 160
			title = `${title} ${TitlebarPart.NLS_UNSUPPORTED}`;
		}

		// Extension Development Host gets a special title to identify itself
		if (this.environmentService.isExtensionDevelopment) {
			title = `${TitlebarPart.NLS_EXTENSION_HOST} - ${title}`;
		}

		return title;
	}

161 162 163 164 165 166 167 168 169 170 171 172
	public updateProperties(properties: ITitleProperties): void {
		const isAdmin = typeof properties.isAdmin === 'boolean' ? properties.isAdmin : this.properties.isAdmin;
		const isPure = typeof properties.isPure === 'boolean' ? properties.isPure : this.properties.isPure;

		if (isAdmin !== this.properties.isAdmin || isPure !== this.properties.isPure) {
			this.properties.isAdmin = isAdmin;
			this.properties.isPure = isPure;

			this.setTitle(this.getWindowTitle());
		}
	}

173 174 175
	/**
	 * Possible template values:
	 *
B
Benjamin Pasero 已提交
176 177 178
	 * {activeEditorLong}: e.g. /Users/Development/myProject/myFolder/myFile.txt
	 * {activeEditorMedium}: e.g. myFolder/myFile.txt
	 * {activeEditorShort}: e.g. myFile.txt
179
	 * {rootName}: e.g. myFolder1, myFolder2, myFolder3
180
	 * {rootPath}: e.g. /Users/Development/myProject
181 182
	 * {folderName}: e.g. myFolder
	 * {folderPath}: e.g. /Users/Development/myFolder
183 184 185 186 187 188
	 * {appName}: e.g. VS Code
	 * {dirty}: indiactor
	 * {separator}: conditional separator
	 */
	private doGetWindowTitle(): string {
		const input = this.editorService.getActiveEditorInput();
B
Benjamin Pasero 已提交
189
		const workspace = this.contextService.getWorkspace();
190

191
		let root: URI;
192
		if (workspace.configuration) {
193
			root = workspace.configuration;
194 195
		} else if (workspace.folders.length) {
			root = workspace.folders[0].uri;
196 197 198 199
		}

		// Compute folder resource
		// Single Root Workspace: always the root single workspace in this case
200
		// Otherwise: root folder of the currently active file if any
201
		let folder = this.contextService.getWorkbenchState() === WorkbenchState.FOLDER ? workspace.folders[0] : this.contextService.getWorkspaceFolder(toResource(input, { supportSideBySide: true }));
202

203
		// Variables
204 205 206
		const activeEditorShort = input ? input.getTitle(Verbosity.SHORT) : '';
		const activeEditorMedium = input ? input.getTitle(Verbosity.MEDIUM) : activeEditorShort;
		const activeEditorLong = input ? input.getTitle(Verbosity.LONG) : activeEditorMedium;
207
		const rootName = workspace.name;
208
		const rootPath = root ? labels.getPathLabel(root, void 0, this.environmentService) : '';
B
Benjamin Pasero 已提交
209
		const folderName = folder ? folder.name : '';
210
		const folderPath = folder ? labels.getPathLabel(folder.uri, void 0, this.environmentService) : '';
211 212 213
		const dirty = input && input.isDirty() ? TitlebarPart.TITLE_DIRTY : '';
		const appName = this.environmentService.appNameLong;
		const separator = TitlebarPart.TITLE_SEPARATOR;
214
		const titleTemplate = this.configurationService.getValue<string>('window.title');
215

216
		return labels.template(titleTemplate, {
B
Benjamin Pasero 已提交
217 218 219
			activeEditorShort,
			activeEditorLong,
			activeEditorMedium,
220 221
			rootName,
			rootPath,
222 223
			folderName,
			folderPath,
224 225 226 227 228 229
			dirty,
			appName,
			separator: { label: separator }
		});
	}

B
Benjamin Pasero 已提交
230
	public createContentArea(parent: Builder): Builder {
R
Ryan Adolf 已提交
231
		const SVGNS = 'http://www.w3.org/2000/svg';
B
Benjamin Pasero 已提交
232 233
		this.titleContainer = $(parent);

R
Ryan Adolf 已提交
234 235 236 237
		if (isWindows) {
			$(this.titleContainer).img({ class: 'window-appicon', src: path.join(this.environmentService.appRoot, 'resources/linux/code.png') });
		}

B
Benjamin Pasero 已提交
238 239 240 241 242 243
		// Title
		this.title = $(this.titleContainer).div({ class: 'window-title' });
		if (this.pendingTitle) {
			this.title.text(this.pendingTitle);
		}

244 245 246 247 248 249 250
		// Maximize/Restore on doubleclick
		this.titleContainer.on(DOM.EventType.DBLCLICK, (e) => {
			DOM.EventHelper.stop(e);

			this.onTitleDoubleclick();
		});

B
Benjamin Pasero 已提交
251 252 253 254 255 256 257 258 259
		// Context menu on title
		this.title.on([DOM.EventType.CONTEXT_MENU, DOM.EventType.MOUSE_DOWN], (e: MouseEvent) => {
			if (e.type === DOM.EventType.CONTEXT_MENU || e.metaKey) {
				DOM.EventHelper.stop(e);

				this.onContextMenu(e);
			}
		});

R
Ryan Adolf 已提交
260 261 262 263 264 265 266 267
		const $svg = (name: string, props: { [name: string]: any }) => {
			const el = document.createElementNS(SVGNS, name);
			Object.keys(props).forEach((prop) => {
				el.setAttribute(prop, props[prop]);
			});
			return el;
		};

268
		if (isWindows) {
269
			// The svgs and styles for the titlebar come from the electron-titlebar-windows package
270 271 272 273
			$(this.titleContainer).div({ class: 'window-icon' }, (builder) => {
				const svg = $svg('svg', { x: 0, y: 0, viewBox: '0 0 10 1' });
				svg.appendChild($svg('rect', { fill: 'currentColor', width: 10, height: 1 }));
				builder.getHTMLElement().appendChild(svg);
274
			}).on(DOM.EventType.CLICK, () => {
R
Ryan Adolf 已提交
275
				this.windowService.minimizeWindow().then(null, errors.onUnexpectedError);
276
			});
R
Ryan Adolf 已提交
277

278
			$(this.titleContainer).div({ class: 'window-icon' }, (builder) => {
279
				const svgf = $svg('svg', { class: 'window-maximize', x: 0, y: 0, viewBox: '0 0 10 10' });
280 281 282
				svgf.appendChild($svg('path', { fill: 'currentColor', d: 'M 0 0 L 0 10 L 10 10 L 10 0 L 0 0 z M 1 1 L 9 1 L 9 9 L 1 9 L 1 1 z' }));
				builder.getHTMLElement().appendChild(svgf);

283
				const svgm = $svg('svg', { class: 'window-unmaximize', x: 0, y: 0, viewBox: '0 0 10 10' });
284 285 286 287 288 289 290
				const mask = $svg('mask', { id: 'Mask' });
				mask.appendChild($svg('rect', { fill: '#fff', width: 10, height: 10 }));
				mask.appendChild($svg('path', { fill: '#000', d: 'M 3 1 L 9 1 L 9 7 L 8 7 L 8 2 L 3 2 L 3 1 z' }));
				mask.appendChild($svg('path', { fill: '#000', d: 'M 1 3 L 7 3 L 7 9 L 1 9 L 1 3 z' }));
				svgm.appendChild(mask);
				svgm.appendChild($svg('path', { fill: 'currentColor', d: 'M 2 0 L 10 0 L 10 8 L 8 8 L 8 10 L 0 10 L 0 2 L 2 2 L 2 0 z', mask: 'url(#Mask)' }));
				builder.getHTMLElement().appendChild(svgm);
291 292 293 294 295 296 297 298
			}).on(DOM.EventType.CLICK, () => {
				this.windowService.isMaximized().then((maximized) => {
					if (maximized) {
						return this.windowService.unmaximizeWindow();
					} else {
						return this.windowService.maximizeWindow();
					}
				}).then(null, errors.onUnexpectedError);
299
			});
R
Ryan Adolf 已提交
300

R
Ryan Adolf 已提交
301
			$(this.titleContainer).div({ class: 'window-icon window-close' }, (builder) => {
302 303 304
				const svg = $svg('svg', { x: '0', y: '0', viewBox: '0 0 10 10' });
				svg.appendChild($svg('polygon', { fill: 'currentColor', points: '10,1 9,0 5,4 1,0 0,1 4,5 0,9 1,10 5,6 9,10 10,9 6,5' }));
				builder.getHTMLElement().appendChild(svg);
305
			}).on(DOM.EventType.CLICK, () => {
R
Ryan Adolf 已提交
306
				this.windowService.closeWindow().then(null, errors.onUnexpectedError);
307
			});
308

309 310
			this.windowService.isMaximized().then((max) => this.onDidChangeMaximized(max), errors.onUnexpectedError);
			this.windowService.onDidChangeMaximize(this.onDidChangeMaximized, this);
311
		}
R
Ryan Adolf 已提交
312

313 314 315 316 317 318 319 320 321 322 323
		// Since the title area is used to drag the window, we do not want to steal focus from the
		// currently active element. So we restore focus after a timeout back to where it was.
		this.titleContainer.on([DOM.EventType.MOUSE_DOWN], () => {
			const active = document.activeElement;
			setTimeout(() => {
				if (active instanceof HTMLElement) {
					active.focus();
				}
			}, 0 /* need a timeout because we are in capture phase */);
		}, void 0, true /* use capture to know the currently active element properly */);

324 325 326
		// Now that there exists a titelbar, we don't need the whole page to be a drag region anymore
		(document.body.style as any).webkitAppRegion = '';

B
Benjamin Pasero 已提交
327 328 329
		return this.titleContainer;
	}

330 331 332 333 334 335 336
	private onDidChangeMaximized(maximized: boolean) {
		($(this.titleContainer).getHTMLElement().querySelector('.window-maximize') as SVGElement).style.display = maximized ? 'none' : 'inline';
		($(this.titleContainer).getHTMLElement().querySelector('.window-unmaximize') as SVGElement).style.display = maximized ? 'inline' : 'none';
		$(this.titleContainer).getHTMLElement().style.paddingLeft = maximized ? '0.15em' : '0.5em';
		$(this.titleContainer).getHTMLElement().style.paddingRight = maximized ? 'calc(2em / 12)' : '0';
	}

B
Benjamin Pasero 已提交
337 338 339 340 341
	protected updateStyles(): void {
		super.updateStyles();

		// Part container
		const container = this.getContainer();
342
		if (container) {
R
Ryan Adolf 已提交
343
			const bgColor = this.getColor(this.isInactive ? TITLE_BAR_INACTIVE_BACKGROUND : TITLE_BAR_ACTIVE_BACKGROUND);
344
			container.style('color', this.getColor(this.isInactive ? TITLE_BAR_INACTIVE_FOREGROUND : TITLE_BAR_ACTIVE_FOREGROUND));
R
Ryan Adolf 已提交
345 346
			container.style('background-color', bgColor);
			container.getHTMLElement().classList.toggle('light', Color.fromHex(bgColor).isLighter());
347 348 349

			const titleBorder = this.getColor(TITLE_BAR_BORDER);
			container.style('border-bottom', titleBorder ? `1px solid ${titleBorder}` : null);
350
		}
B
Benjamin Pasero 已提交
351 352
	}

353
	private onTitleDoubleclick(): void {
354
		this.windowService.onWindowTitleDoubleClick().then(null, errors.onUnexpectedError);
355 356
	}

B
Benjamin Pasero 已提交
357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379
	private onContextMenu(e: MouseEvent): void {

		// Find target anchor
		const event = new StandardMouseEvent(e);
		const anchor = { x: event.posx, y: event.posy };

		// Show menu
		const actions = this.getContextMenuActions();
		if (actions.length) {
			this.contextMenuService.showContextMenu({
				getAnchor: () => anchor,
				getActions: () => TPromise.as(actions),
				onHide: () => actions.forEach(a => a.dispose())
			});
		}
	}

	private getContextMenuActions(): IAction[] {
		const actions: IAction[] = [];

		if (this.representedFileName) {
			const segments = this.representedFileName.split(paths.sep);
			for (let i = segments.length; i > 0; i--) {
380 381 382 383 384 385 386 387 388
				const isFile = (i === segments.length);

				let pathOffset = i;
				if (!isFile) {
					pathOffset++; // for segments which are not the file name we want to open the folder
				}

				const path = segments.slice(0, pathOffset).join(paths.sep);

B
Benjamin Pasero 已提交
389
				let label: string;
390
				if (!isFile) {
B
Benjamin Pasero 已提交
391 392 393
					label = labels.getBaseLabel(paths.dirname(path));
				} else {
					label = labels.getBaseLabel(path);
394 395 396
				}

				actions.push(new ShowItemInFolderAction(path, label || paths.sep, this.windowsService));
B
Benjamin Pasero 已提交
397 398 399 400 401 402
			}
		}

		return actions;
	}

403
	public setTitle(title: string): void {
B
Benjamin Pasero 已提交
404 405 406 407 408 409 410 411 412 413 414 415

		// Always set the native window title to identify us properly to the OS
		window.document.title = title;

		// Apply if we can
		if (this.title) {
			this.title.text(title);
		} else {
			this.pendingTitle = title;
		}
	}

B
Benjamin Pasero 已提交
416 417 418 419 420 421 422 423 424
	public setRepresentedFilename(path: string): void {

		// Apply to window
		this.windowService.setRepresentedFilename(path);

		// Keep for context menu
		this.representedFileName = path;
	}

B
Benjamin Pasero 已提交
425 426 427 428 429 430 431 432 433 434
	public layout(dimension: Dimension): Dimension[] {

		// To prevent zooming we need to adjust the font size with the zoom factor
		if (typeof this.initialTitleFontSize !== 'number') {
			this.initialTitleFontSize = parseInt(this.titleContainer.getComputedStyle().fontSize, 10);
		}
		this.titleContainer.style({ fontSize: `${this.initialTitleFontSize / getZoomFactor()}px` });

		return super.layout(dimension);
	}
B
Benjamin Pasero 已提交
435 436 437 438
}

class ShowItemInFolderAction extends Action {

439 440
	constructor(private path: string, label: string, private windowsService: IWindowsService) {
		super('showItemInFolder.action.id', label);
B
Benjamin Pasero 已提交
441 442 443 444 445
	}

	public run(): TPromise<void> {
		return this.windowsService.showItemInFolder(this.path);
	}
R
Ryan Adolf 已提交
446
}