compositePart.ts 16.1 KB
Newer Older
1 2 3 4 5
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

B
Benjamin Pasero 已提交
6
import 'vs/css!./media/compositepart';
7
import * as nls from 'vs/nls';
8
import { defaultGenerator } from 'vs/base/common/idGenerator';
J
Johannes Rieken 已提交
9
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
10
import * as strings from 'vs/base/common/strings';
J
Johannes Rieken 已提交
11
import { Emitter } from 'vs/base/common/event';
12
import * as errors from 'vs/base/common/errors';
13
import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar';
S
Sandeep Somavarapu 已提交
14
import { IActionItem, ActionsOrientation } from 'vs/base/browser/ui/actionbar/actionbar';
J
Johannes Rieken 已提交
15
import { ProgressBar } from 'vs/base/browser/ui/progressbar/progressbar';
16
import { prepareActions } from 'vs/workbench/browser/actions';
17
import { Action, IAction } from 'vs/base/common/actions';
18
import { Part, IPartOptions } from 'vs/workbench/browser/part';
J
Johannes Rieken 已提交
19 20
import { Composite, CompositeRegistry } from 'vs/workbench/browser/composite';
import { IComposite } from 'vs/workbench/common/composite';
21
import { ScopedProgressService } from 'vs/workbench/services/progress/browser/progressService';
J
Johannes Rieken 已提交
22
import { IPartService } from 'vs/workbench/services/part/common/partService';
B
Benjamin Pasero 已提交
23
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
J
Johannes Rieken 已提交
24 25 26 27 28 29
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection';
import { IProgressService } from 'vs/platform/progress/common/progress';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
30
import { IThemeService } from 'vs/platform/theme/common/themeService';
31
import { attachProgressBarStyler } from 'vs/platform/theme/common/styler';
32
import { INotificationService } from 'vs/platform/notification/common/notification';
33
import { Dimension, append, $, addClass, hide, show, addClasses } from 'vs/base/browser/dom';
34

35 36 37 38 39 40
export interface ICompositeTitleLabel {

	/**
	 * Asks to update the title for the composite with the given ID.
	 */
	updateTitle(id: string, title: string, keybinding?: string): void;
41 42 43 44 45

	/**
	 * Called when theming information changes.
	 */
	updateStyles(): void;
46 47
}

48
export abstract class CompositePart<T extends Composite> extends Part {
B
Benjamin Pasero 已提交
49

50
	protected _onDidCompositeOpen = this._register(new Emitter<{ composite: IComposite, focus: boolean }>());
B
Benjamin Pasero 已提交
51 52 53 54
	protected _onDidCompositeClose = this._register(new Emitter<IComposite>());

	protected toolBar: ToolBar;

A
Alex Dima 已提交
55
	private instantiatedCompositeListeners: IDisposable[];
B
Benjamin Pasero 已提交
56
	private mapCompositeToCompositeContainer: { [compositeId: string]: HTMLElement; };
57 58 59 60
	private mapActionsBindingToComposite: { [compositeId: string]: () => void; };
	private mapProgressServiceToComposite: { [compositeId: string]: IProgressService; };
	private activeComposite: Composite;
	private lastActiveCompositeId: string;
B
Benjamin Pasero 已提交
61
	private instantiatedComposites: Composite[];
62
	private titleLabel: ICompositeTitleLabel;
63 64 65
	private progressBar: ProgressBar;
	private contentAreaSize: Dimension;
	private telemetryActionsListener: IDisposable;
66
	private currentCompositeOpenToken: string;
67 68

	constructor(
69
		private notificationService: INotificationService,
70
		private storageService: IStorageService,
71
		private telemetryService: ITelemetryService,
I
isidor 已提交
72
		protected contextMenuService: IContextMenuService,
73
		protected partService: IPartService,
74
		protected keybindingService: IKeybindingService,
75
		protected instantiationService: IInstantiationService,
76
		themeService: IThemeService,
77
		protected readonly registry: CompositeRegistry<T>,
78
		private activeCompositeSettingsKey: string,
79
		private defaultCompositeId: string,
80
		private nameForTelemetry: string,
Y
Yuki Ueda 已提交
81
		private compositeCSSClass: string,
82
		private titleForegroundColor: string,
83 84
		id: string,
		options: IPartOptions
85
	) {
86
		super(id, options, themeService, storageService);
87 88 89 90 91 92

		this.instantiatedCompositeListeners = [];
		this.mapCompositeToCompositeContainer = {};
		this.mapActionsBindingToComposite = {};
		this.mapProgressServiceToComposite = {};
		this.activeComposite = null;
B
Benjamin Pasero 已提交
93
		this.instantiatedComposites = [];
94
		this.lastActiveCompositeId = storageService.get(activeCompositeSettingsKey, StorageScope.WORKSPACE, this.defaultCompositeId);
95 96
	}

97
	protected openComposite(id: string, focus?: boolean): Composite {
98 99 100 101 102 103 104
		// Check if composite already visible and just focus in that case
		if (this.activeComposite && this.activeComposite.getId() === id) {
			if (focus) {
				this.activeComposite.focus();
			}

			// Fullfill promise with composite that is being opened
105
			return this.activeComposite;
106 107 108 109 110 111
		}

		// Open
		return this.doOpenComposite(id, focus);
	}

112
	private doOpenComposite(id: string, focus?: boolean): Composite {
113 114

		// Use a generated token to avoid race conditions from long running promises
115
		const currentCompositeOpenToken = defaultGenerator.nextId();
116 117 118 119
		this.currentCompositeOpenToken = currentCompositeOpenToken;

		// Hide current
		if (this.activeComposite) {
120
			this.hideActiveComposite();
121 122
		}

123 124
		// Update Title
		this.updateTitle(id);
125

126 127
		// Create composite
		const composite = this.createComposite(id, true);
128

129 130 131 132
		// Check if another composite opened meanwhile and return in that case
		if ((this.currentCompositeOpenToken !== currentCompositeOpenToken) || (this.activeComposite && this.activeComposite.getId() !== composite.getId())) {
			return undefined;
		}
133

134 135 136 137
		// Check if composite already visible and just focus in that case
		if (this.activeComposite && this.activeComposite.getId() === composite.getId()) {
			if (focus) {
				composite.focus();
138 139
			}

140 141 142
			this._onDidCompositeOpen.fire({ composite, focus });
			return composite;
		}
143

144 145 146 147 148
		// Show Composite and Focus
		this.showComposite(composite);
		if (focus) {
			composite.focus();
		}
149

150 151 152 153
		// Return with the composite that is being opened
		if (composite) {
			this._onDidCompositeOpen.fire({ composite, focus });
		}
154

155
		return composite;
156 157
	}

158
	protected createComposite(id: string, isActive?: boolean): Composite {
159 160

		// Check if composite is already created
B
Benjamin Pasero 已提交
161 162
		for (let i = 0; i < this.instantiatedComposites.length; i++) {
			if (this.instantiatedComposites[i].getId() === id) {
163
				return this.instantiatedComposites[i];
164 165 166 167
			}
		}

		// Instantiate composite from registry otherwise
168
		const compositeDescriptor = this.registry.getComposite(id);
169
		if (compositeDescriptor) {
170
			const progressService = this.instantiationService.createInstance(ScopedProgressService, this.progressBar, compositeDescriptor.id, isActive);
171
			const compositeInstantiationService = this.instantiationService.createChild(new ServiceCollection([IProgressService, progressService]));
172

173 174
			const composite = compositeDescriptor.instantiate(compositeInstantiationService);
			this.mapProgressServiceToComposite[composite.getId()] = progressService;
175

176 177
			// Remember as Instantiated
			this.instantiatedComposites.push(composite);
178

179 180
			// Register to title area update events from the composite
			this.instantiatedCompositeListeners.push(composite.onTitleAreaUpdate(() => this.onTitleAreaUpdate(composite.getId())));
181

182
			return composite;
183 184 185 186 187
		}

		throw new Error(strings.format('Unable to find composite with id {0}', id));
	}

188
	protected showComposite(composite: Composite): void {
189 190 191 192 193

		// Remember Composite
		this.activeComposite = composite;

		// Store in preferences
194 195
		const id = this.activeComposite.getId();
		if (id !== this.defaultCompositeId) {
B
Benjamin Pasero 已提交
196
			this.storageService.store(this.activeCompositeSettingsKey, id, StorageScope.WORKSPACE);
197
		} else {
B
Benjamin Pasero 已提交
198
			this.storageService.remove(this.activeCompositeSettingsKey, StorageScope.WORKSPACE);
199
		}
200 201 202 203

		// Remember
		this.lastActiveCompositeId = this.activeComposite.getId();

B
Benjamin Pasero 已提交
204
		// Composites created for the first time
205 206 207 208
		let compositeContainer = this.mapCompositeToCompositeContainer[composite.getId()];
		if (!compositeContainer) {

			// Build Container off-DOM
B
Benjamin Pasero 已提交
209 210 211 212
			compositeContainer = $('.composite');
			addClasses(compositeContainer, this.compositeCSSClass);
			compositeContainer.id = composite.getId();

I
isidor 已提交
213 214
			composite.create(compositeContainer);
			composite.updateStyles();
215 216 217 218 219

			// Remember composite container
			this.mapCompositeToCompositeContainer[composite.getId()] = compositeContainer;
		}

B
Benjamin Pasero 已提交
220
		// Report progress for slow loading composites (but only if we did not create the composites before already)
221
		const progressService = this.mapProgressServiceToComposite[composite.getId()];
222
		if (progressService && !compositeContainer) {
I
isidor 已提交
223
			this.mapProgressServiceToComposite[composite.getId()].showWhile(Promise.resolve(), this.partService.isCreated() ? 800 : 3200 /* less ugly initial startup */);
224 225 226
		}

		// Fill Content and Actions
I
isidor 已提交
227 228 229 230
		// Make sure that the user meanwhile did not open another composite or closed the part containing the composite
		if (!this.activeComposite || composite.getId() !== this.activeComposite.getId()) {
			return void 0;
		}
231

I
isidor 已提交
232 233 234
		// Take Composite on-DOM and show
		this.getContentArea().appendChild(compositeContainer);
		show(compositeContainer);
235

I
isidor 已提交
236 237
		// Setup action runner
		this.toolBar.actionRunner = composite.getActionRunner();
238

I
isidor 已提交
239 240 241 242 243
		// Update title with composite title if it differs from descriptor
		const descriptor = this.registry.getComposite(composite.getId());
		if (descriptor && descriptor.name !== composite.getTitle()) {
			this.updateTitle(composite.getId(), composite.getTitle());
		}
244

I
isidor 已提交
245 246 247 248 249 250 251
		// Handle Composite Actions
		let actionsBinding = this.mapActionsBindingToComposite[composite.getId()];
		if (!actionsBinding) {
			actionsBinding = this.collectCompositeActions(composite);
			this.mapActionsBindingToComposite[composite.getId()] = actionsBinding;
		}
		actionsBinding();
252

I
isidor 已提交
253 254 255 256
		if (this.telemetryActionsListener) {
			this.telemetryActionsListener.dispose();
			this.telemetryActionsListener = null;
		}
257

I
isidor 已提交
258 259
		// Action Run Handling
		this.telemetryActionsListener = this.toolBar.actionRunner.onDidRun(e => {
260

I
isidor 已提交
261 262 263 264
			// Check for Error
			if (e.error && !errors.isPromiseCanceledError(e.error)) {
				this.notificationService.error(e.error);
			}
265

I
isidor 已提交
266 267 268 269 270 271 272 273 274 275 276
			// Log in telemetry
			if (this.telemetryService) {
				/* __GDPR__
					"workbenchActionExecuted" : {
						"id" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
						"from": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
					}
				*/
				this.telemetryService.publicLog('workbenchActionExecuted', { id: e.action.id, from: this.nameForTelemetry });
			}
		});
277

I
isidor 已提交
278
		// Indicate to composite that it is now visible
279
		composite.setVisible(true);
280

281 282 283 284
		// Make sure that the user meanwhile did not open another composite or closed the part containing the composite
		if (!this.activeComposite || composite.getId() !== this.activeComposite.getId()) {
			return;
		}
285

286 287 288 289
		// Make sure the composite is layed out
		if (this.contentAreaSize) {
			composite.layout(this.contentAreaSize);
		}
290 291
	}

292
	protected onTitleAreaUpdate(compositeId: string): void {
293 294

		// Active Composite
295
		if (this.activeComposite && this.activeComposite.getId() === compositeId) {
296 297 298 299 300

			// Title
			this.updateTitle(this.activeComposite.getId(), this.activeComposite.getTitle());

			// Actions
301
			const actionsBinding = this.collectCompositeActions(this.activeComposite);
302 303 304 305 306 307
			this.mapActionsBindingToComposite[this.activeComposite.getId()] = actionsBinding;
			actionsBinding();
		}

		// Otherwise invalidate actions binding for next time when the composite becomes visible
		else {
308
			delete this.mapActionsBindingToComposite[compositeId];
309 310 311
		}
	}

312
	private updateTitle(compositeId: string, compositeTitle?: string): void {
313
		const compositeDescriptor = this.registry.getComposite(compositeId);
I
isidor 已提交
314
		if (!compositeDescriptor || !this.titleLabel) {
315 316 317 318 319 320 321
			return;
		}

		if (!compositeTitle) {
			compositeTitle = compositeDescriptor.name;
		}

322
		const keybinding = this.keybindingService.lookupKeybinding(compositeId);
323

324
		this.titleLabel.updateTitle(compositeId, compositeTitle, keybinding ? keybinding.getLabel() : undefined);
325

B
Benjamin Pasero 已提交
326
		this.toolBar.setAriaLabel(nls.localize('ariaCompositeToolbarLabel', "{0} actions", compositeTitle));
327 328 329 330 331
	}

	private collectCompositeActions(composite: Composite): () => void {

		// From Composite
332 333
		const primaryActions: IAction[] = composite.getActions().slice(0);
		const secondaryActions: IAction[] = composite.getSecondaryActions().slice(0);
334

I
isidor 已提交
335 336 337 338
		// From Part
		primaryActions.push(...this.getActions());
		secondaryActions.push(...this.getSecondaryActions());

339 340 341 342 343 344 345 346 347 348 349 350
		// Return fn to set into toolbar
		return this.toolBar.setActions(prepareActions(primaryActions), prepareActions(secondaryActions));
	}

	protected getActiveComposite(): IComposite {
		return this.activeComposite;
	}

	protected getLastActiveCompositetId(): string {
		return this.lastActiveCompositeId;
	}

351
	protected hideActiveComposite(): Composite {
352
		if (!this.activeComposite) {
353
			return undefined; // Nothing to do
354 355
		}

356
		const composite = this.activeComposite;
357 358
		this.activeComposite = null;

359
		const compositeContainer = this.mapCompositeToCompositeContainer[composite.getId()];
360 361

		// Indicate to Composite
362
		composite.setVisible(false);
363

364 365 366
		// Take Container Off-DOM and hide
		compositeContainer.remove();
		hide(compositeContainer);
367

368 369
		// Clear any running Progress
		this.progressBar.stop().hide();
370

371 372 373
		// Empty Actions
		this.toolBar.setActions([])();
		this._onDidCompositeClose.fire(composite);
374

375
		return composite;
376 377
	}

B
Benjamin Pasero 已提交
378
	createTitleArea(parent: HTMLElement): HTMLElement {
379 380

		// Title Area Container
B
Benjamin Pasero 已提交
381 382
		const titleArea = append(parent, $('.composite'));
		addClass(titleArea, 'title');
383

I
isidor 已提交
384
		// Left Title Label
B
Benjamin Pasero 已提交
385
		this.titleLabel = this.createTitleLabel(titleArea);
I
isidor 已提交
386

387
		// Right Actions Container
B
Benjamin Pasero 已提交
388 389 390 391 392 393 394 395
		const titleActionsContainer = append(titleArea, $('.title-actions'));

		// Toolbar
		this.toolBar = this._register(new ToolBar(titleActionsContainer, this.contextMenuService, {
			actionItemProvider: action => this.actionItemProvider(action as Action),
			orientation: ActionsOrientation.HORIZONTAL,
			getKeyBinding: action => this.keybindingService.lookupKeybinding(action.id)
		}));
396

B
Benjamin Pasero 已提交
397
		return titleArea;
398 399
	}

400
	protected createTitleLabel(parent: HTMLElement): ICompositeTitleLabel {
B
Benjamin Pasero 已提交
401 402
		const titleContainer = append(parent, $('.title-label'));
		const titleLabel = append(titleContainer, $('h2'));
403

404
		const $this = this;
405 406
		return {
			updateTitle: (id, title, keybinding) => {
B
Benjamin Pasero 已提交
407 408
				titleLabel.innerHTML = strings.escape(title);
				titleLabel.title = keybinding ? nls.localize('titleTooltip', "{0} ({1})", title, keybinding) : title;
409
			},
B
Benjamin Pasero 已提交
410

411
			updateStyles: () => {
B
Benjamin Pasero 已提交
412
				titleLabel.style.color = $this.getColor($this.titleForegroundColor);
413 414 415 416
			}
		};
	}

417 418 419 420 421 422 423
	protected updateStyles(): void {
		super.updateStyles();

		// Forward to title label
		this.titleLabel.updateStyles();
	}

424
	protected actionItemProvider(action: Action): IActionItem {
425

426 427
		// Check Active Composite
		if (this.activeComposite) {
428
			return this.activeComposite.getActionItem(action);
429 430
		}

431
		return undefined;
432 433
	}

B
Benjamin Pasero 已提交
434
	createContentArea(parent: HTMLElement): HTMLElement {
B
Benjamin Pasero 已提交
435 436 437 438 439 440 441
		const contentContainer = append(parent, $('.content'));

		this.progressBar = this._register(new ProgressBar(contentContainer));
		this._register(attachProgressBarStyler(this.progressBar, this.themeService));
		this.progressBar.hide();

		return contentContainer;
442 443
	}

B
Benjamin Pasero 已提交
444
	getProgressIndicator(id: string): IProgressService {
445 446
		return this.mapProgressServiceToComposite[id];
	}
447

I
isidor 已提交
448 449 450 451 452 453 454 455
	protected getActions(): IAction[] {
		return [];
	}

	protected getSecondaryActions(): IAction[] {
		return [];
	}

B
Benjamin Pasero 已提交
456
	layout(dimension: Dimension): Dimension[] {
457 458

		// Pass to super
459
		const sizes = super.layout(dimension);
460 461 462 463 464 465 466 467 468 469

		// Pass Contentsize to composite
		this.contentAreaSize = sizes[1];
		if (this.activeComposite) {
			this.activeComposite.layout(this.contentAreaSize);
		}

		return sizes;
	}

B
Benjamin Pasero 已提交
470
	dispose(): void {
471 472 473 474
		this.mapCompositeToCompositeContainer = null;
		this.mapProgressServiceToComposite = null;
		this.mapActionsBindingToComposite = null;

B
Benjamin Pasero 已提交
475 476
		for (let i = 0; i < this.instantiatedComposites.length; i++) {
			this.instantiatedComposites[i].dispose();
477 478
		}

B
Benjamin Pasero 已提交
479
		this.instantiatedComposites = [];
A
Alex Dima 已提交
480
		this.instantiatedCompositeListeners = dispose(this.instantiatedCompositeListeners);
481 482 483 484

		super.dispose();
	}
}