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';
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
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';
26
import * as nls from 'vs/nls';
27
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
import { trim } from 'vs/base/common/strings';
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 {
B
Benjamin Pasero 已提交
91 92
		this.toUnbind.push(DOM.addDisposableListener(window, DOM.EventType.BLUR, () => this.onBlur()));
		this.toUnbind.push(DOM.addDisposableListener(window, DOM.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 }
		});
	}

B
Benjamin Pasero 已提交
231
	public createContentArea(parent: Builder): Builder {
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 239 240 241 242
			$(this.titleContainer).img({
				class: 'window-appicon',
				src: path.join(this.environmentService.appRoot, 'resources/linux/code.png')
			}).on(DOM.EventType.DBLCLICK, (e) => {
				DOM.EventHelper.stop(e, true);
				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 252 253 254 255 256 257
		// Maximize/Restore on doubleclick
		this.titleContainer.on(DOM.EventType.DBLCLICK, (e) => {
			DOM.EventHelper.stop(e);

			this.onTitleDoubleclick();
		});

B
Benjamin Pasero 已提交
258 259 260 261 262 263 264 265 266
		// 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 已提交
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(DOM.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 299 300 301 302 303 304 305
			}).on(DOM.EventType.CLICK, () => {
				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(DOM.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 322 323 324 325 326 327 328 329 330
		// 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 */);

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

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

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 349
	protected updateStyles(): void {
		super.updateStyles();

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

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

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

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

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

		return actions;
	}

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

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

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

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

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

class ShowItemInFolderAction extends Action {

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

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