titlebarPart.ts 17.2 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';
11
import { Builder, $ } from 'vs/base/browser/builder';
B
Benjamin Pasero 已提交
12
import * as paths from 'vs/base/common/paths';
B
Benjamin Pasero 已提交
13
import { Part } from 'vs/workbench/browser/part';
14
import { ITitleService, ITitleProperties } from 'vs/workbench/services/title/common/titleService';
B
Benjamin Pasero 已提交
15
import { getZoomFactor } from 'vs/base/browser/browser';
B
Benjamin Pasero 已提交
16 17 18 19 20
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';
21
import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration';
22 23 24
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';
25
import * as nls from 'vs/nls';
26
import * as labels from 'vs/base/common/labels';
27
import { EditorInput, toResource } from 'vs/workbench/common/editor';
28
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
29
import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace';
30
import { Verbosity } from 'vs/platform/editor/common/editor';
B
Benjamin Pasero 已提交
31
import { IThemeService } from 'vs/platform/theme/common/themeService';
32
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 已提交
33
import { isMacintosh, isWindows } from 'vs/base/common/platform';
B
Benjamin Pasero 已提交
34
import URI from 'vs/base/common/uri';
R
Ryan Adolf 已提交
35
import { Color } from 'vs/base/common/color';
36
import { ILifecycleService, LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle';
B
Benjamin Pasero 已提交
37
import { trim } from 'vs/base/common/strings';
38
import { addDisposableListener, EventType, EventHelper, Dimension } from 'vs/base/browser/dom';
B
Benjamin Pasero 已提交
39 40 41 42 43

export class TitlebarPart extends Part implements ITitleService {

	public _serviceBrand: any;

44
	private static readonly NLS_UNSUPPORTED = nls.localize('patchedWindowTitle', "[Unsupported]");
45
	private static readonly NLS_USER_IS_ADMIN = isWindows ? nls.localize('userIsAdmin', "[Administrator]") : nls.localize('userIsSudo', "[Superuser]");
46 47 48
	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
49

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

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

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

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

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

		this.init();

81 82 83
		this.registerListeners();
	}

84 85
	private init(): void {

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

90
	private registerListeners(): void {
91 92
		this.toUnbind.push(addDisposableListener(window, EventType.BLUR, () => this.onBlur()));
		this.toUnbind.push(addDisposableListener(window, EventType.FOCUS, () => this.onFocus()));
93
		this.toUnbind.push(this.configurationService.onDidChangeConfiguration(e => this.onConfigurationChanged(e)));
94
		this.toUnbind.push(this.editorGroupService.onEditorsChanged(() => this.onEditorsChanged()));
95 96 97
		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())));
98 99
	}

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

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

110 111
	private onConfigurationChanged(event: IConfigurationChangeEvent): void {
		if (event.affectsConfiguration('window.title')) {
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
			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();
B
Benjamin Pasero 已提交
142
		if (!trim(title)) {
143 144 145
			title = this.environmentService.appNameLong;
		}

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

		if (!this.properties.isPure) {
151 152 153 154 155 156 157 158 159 160 161
			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;
	}

162 163 164 165 166 167 168 169 170 171 172 173
	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());
		}
	}

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

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

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

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

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

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

R
Ryan Adolf 已提交
235
		if (!isMacintosh) {
236 237 238
			$(this.titleContainer).img({
				class: 'window-appicon',
				src: path.join(this.environmentService.appRoot, 'resources/linux/code.png')
239 240
			}).on(EventType.DBLCLICK, (e) => {
				EventHelper.stop(e, true);
241 242
				this.windowService.closeWindow().then(null, errors.onUnexpectedError);
			});
R
Ryan Adolf 已提交
243 244
		}

B
Benjamin Pasero 已提交
245 246 247 248 249 250
		// Title
		this.title = $(this.titleContainer).div({ class: 'window-title' });
		if (this.pendingTitle) {
			this.title.text(this.pendingTitle);
		}

251
		// Maximize/Restore on doubleclick
252 253
		this.titleContainer.on(EventType.DBLCLICK, (e) => {
			EventHelper.stop(e);
254 255 256 257

			this.onTitleDoubleclick();
		});

B
Benjamin Pasero 已提交
258
		// Context menu on title
259 260 261
		this.title.on([EventType.CONTEXT_MENU, EventType.MOUSE_DOWN], (e: MouseEvent) => {
			if (e.type === EventType.CONTEXT_MENU || e.metaKey) {
				EventHelper.stop(e);
B
Benjamin Pasero 已提交
262 263 264 265 266

				this.onContextMenu(e);
			}
		});

R
Ryan Adolf 已提交
267 268 269 270 271 272 273 274
		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;
		};

R
Ryan Adolf 已提交
275
		if (!isMacintosh) {
276
			// The svgs and styles for the titlebar come from the electron-titlebar-windows package
277 278 279 280
			$(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);
281
			}).on(EventType.CLICK, () => {
R
Ryan Adolf 已提交
282
				this.windowService.minimizeWindow().then(null, errors.onUnexpectedError);
283
			});
R
Ryan Adolf 已提交
284

285
			$(this.titleContainer).div({ class: 'window-icon' }, (builder) => {
286
				const svgf = $svg('svg', { class: 'window-maximize', x: 0, y: 0, viewBox: '0 0 10 10' });
287 288 289
				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);

290
				const svgm = $svg('svg', { class: 'window-unmaximize', x: 0, y: 0, viewBox: '0 0 10 10' });
291 292 293 294 295 296 297
				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);
298
			}).on(EventType.CLICK, () => {
299 300 301 302 303 304 305
				this.windowService.isMaximized().then((maximized) => {
					if (maximized) {
						return this.windowService.unmaximizeWindow();
					} else {
						return this.windowService.maximizeWindow();
					}
				}).then(null, errors.onUnexpectedError);
306
			});
R
Ryan Adolf 已提交
307

R
Ryan Adolf 已提交
308
			$(this.titleContainer).div({ class: 'window-icon window-close' }, (builder) => {
309 310 311
				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);
312
			}).on(EventType.CLICK, () => {
R
Ryan Adolf 已提交
313
				this.windowService.closeWindow().then(null, errors.onUnexpectedError);
314
			});
315

316 317
			this.windowService.isMaximized().then((max) => this.onDidChangeMaximized(max), errors.onUnexpectedError);
			this.windowService.onDidChangeMaximize(this.onDidChangeMaximized, this);
318
		}
R
Ryan Adolf 已提交
319

320 321
		// 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.
322
		this.titleContainer.on([EventType.MOUSE_DOWN], () => {
323 324 325 326 327 328 329 330
			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 */);

331
		// Now that there exists a titelbar, we don't need the whole page to be a drag region anymore
R
Ryan Adolf 已提交
332 333
		(document.documentElement.style as any).webkitAppRegion = '';
		document.documentElement.style.height = '';
334

335
		return this.titleContainer.getHTMLElement();
B
Benjamin Pasero 已提交
336 337
	}

338 339 340 341 342 343 344
	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 已提交
345 346 347 348
	protected updateStyles(): void {
		super.updateStyles();

		// Part container
349
		if (this.titleContainer) {
R
Ryan Adolf 已提交
350
			const bgColor = this.getColor(this.isInactive ? TITLE_BAR_INACTIVE_BACKGROUND : TITLE_BAR_ACTIVE_BACKGROUND);
351
			this.titleContainer.style('color', this.getColor(this.isInactive ? TITLE_BAR_INACTIVE_FOREGROUND : TITLE_BAR_ACTIVE_FOREGROUND));
352 353
			this.titleContainer.style('background-color', bgColor);
			this.titleContainer.getHTMLElement().classList.toggle('light', Color.fromHex(bgColor).isLighter());
354 355

			const titleBorder = this.getColor(TITLE_BAR_BORDER);
356
			this.titleContainer.style('border-bottom', titleBorder ? `1px solid ${titleBorder}` : null);
357
		}
B
Benjamin Pasero 已提交
358 359
	}

360
	private onTitleDoubleclick(): void {
361
		this.windowService.onWindowTitleDoubleClick().then(null, errors.onUnexpectedError);
362 363
	}

B
Benjamin Pasero 已提交
364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386
	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--) {
387 388 389 390 391 392 393 394 395
				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 已提交
396
				let label: string;
397
				if (!isFile) {
B
Benjamin Pasero 已提交
398 399 400
					label = labels.getBaseLabel(paths.dirname(path));
				} else {
					label = labels.getBaseLabel(path);
401 402 403
				}

				actions.push(new ShowItemInFolderAction(path, label || paths.sep, this.windowsService));
B
Benjamin Pasero 已提交
404 405 406 407 408 409
			}
		}

		return actions;
	}

410
	public setTitle(title: string): void {
B
Benjamin Pasero 已提交
411 412 413 414 415 416 417 418 419 420 421 422

		// 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 已提交
423 424 425 426 427 428 429 430 431
	public setRepresentedFilename(path: string): void {

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

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

B
Benjamin Pasero 已提交
432 433 434 435 436 437 438 439 440 441
	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 已提交
442 443 444 445
}

class ShowItemInFolderAction extends Action {

446 447
	constructor(private path: string, label: string, private windowsService: IWindowsService) {
		super('showItemInFolder.action.id', label);
B
Benjamin Pasero 已提交
448 449 450 451 452
	}

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