compositePart.ts 17.6 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';
9
import { IDisposable, dispose, toDisposable } 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
import { AnchorAlignment } from 'vs/base/browser/ui/contextview/contextview';
35

36 37 38 39 40 41
export interface ICompositeTitleLabel {

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

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

49 50 51 52 53 54
interface CompositeItem {
	composite: Composite;
	disposable: IDisposable;
	progressService: IProgressService;
}

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

B
Benjamin Pasero 已提交
57 58
	protected readonly onDidCompositeOpen = this._register(new Emitter<{ composite: IComposite, focus: boolean }>());
	protected readonly onDidCompositeClose = this._register(new Emitter<IComposite>());
B
Benjamin Pasero 已提交
59 60 61

	protected toolBar: ToolBar;

B
Benjamin Pasero 已提交
62
	private mapCompositeToCompositeContainer: { [compositeId: string]: HTMLElement; };
63
	private mapActionsBindingToComposite: { [compositeId: string]: () => void; };
M
Matt Bierner 已提交
64
	private activeComposite: Composite | null;
65
	private lastActiveCompositeId: string;
66
	private instantiatedCompositeItems: Map<string, CompositeItem>;
67
	private titleLabel: ICompositeTitleLabel;
68 69
	private progressBar: ProgressBar;
	private contentAreaSize: Dimension;
M
Matt Bierner 已提交
70
	private telemetryActionsListener: IDisposable | null;
71
	private currentCompositeOpenToken: string;
72 73

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

		this.mapCompositeToCompositeContainer = {};
		this.mapActionsBindingToComposite = {};
		this.activeComposite = null;
96
		this.instantiatedCompositeItems = new Map<string, CompositeItem>();
97
		this.lastActiveCompositeId = storageService.get(activeCompositeSettingsKey, StorageScope.WORKSPACE, this.defaultCompositeId);
98 99
	}

M
Matt Bierner 已提交
100
	protected openComposite(id: string, focus?: boolean): Composite | undefined {
101

102 103 104 105 106 107 108
		// 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
109
			return this.activeComposite;
110 111 112 113 114 115
		}

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

M
Matt Bierner 已提交
116
	private doOpenComposite(id: string, focus: boolean = false): Composite | undefined {
117 118

		// Use a generated token to avoid race conditions from long running promises
119
		const currentCompositeOpenToken = defaultGenerator.nextId();
120 121 122 123
		this.currentCompositeOpenToken = currentCompositeOpenToken;

		// Hide current
		if (this.activeComposite) {
124
			this.hideActiveComposite();
125 126
		}

127 128
		// Update Title
		this.updateTitle(id);
129

130 131
		// Create composite
		const composite = this.createComposite(id, true);
132

133 134 135 136
		// Check if another composite opened meanwhile and return in that case
		if ((this.currentCompositeOpenToken !== currentCompositeOpenToken) || (this.activeComposite && this.activeComposite.getId() !== composite.getId())) {
			return undefined;
		}
137

138 139 140 141
		// Check if composite already visible and just focus in that case
		if (this.activeComposite && this.activeComposite.getId() === composite.getId()) {
			if (focus) {
				composite.focus();
142 143
			}

B
Benjamin Pasero 已提交
144
			this.onDidCompositeOpen.fire({ composite, focus });
145 146
			return composite;
		}
147

148 149 150 151 152
		// Show Composite and Focus
		this.showComposite(composite);
		if (focus) {
			composite.focus();
		}
153

154 155
		// Return with the composite that is being opened
		if (composite) {
B
Benjamin Pasero 已提交
156
			this.onDidCompositeOpen.fire({ composite, focus });
157
		}
158

159
		return composite;
160 161
	}

162
	protected createComposite(id: string, isActive?: boolean): Composite {
163 164

		// Check if composite is already created
165 166 167
		const compositeItem = this.instantiatedCompositeItems.get(id);
		if (compositeItem) {
			return compositeItem.composite;
168 169 170
		}

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

176
			const composite = compositeDescriptor.instantiate(compositeInstantiationService);
177
			const disposables: IDisposable[] = [];
178

179
			// Remember as Instantiated
180
			this.instantiatedCompositeItems.set(id, { composite, disposable: toDisposable(() => dispose(disposables)), progressService });
181

182
			// Register to title area update events from the composite
183
			composite.onTitleAreaUpdate(() => this.onTitleAreaUpdate(composite.getId()), this, disposables);
184

185
			return composite;
186 187
		}

188
		throw new Error(`Unable to find composite with id ${id}`);
189 190
	}

191
	protected showComposite(composite: Composite): void {
192 193 194 195 196

		// Remember Composite
		this.activeComposite = composite;

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

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

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

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

I
isidor 已提交
216 217
			composite.create(compositeContainer);
			composite.updateStyles();
218 219 220 221 222

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

B
Benjamin Pasero 已提交
223
		// Report progress for slow loading composites (but only if we did not create the composites before already)
224 225 226
		const compositeItem = this.instantiatedCompositeItems.get(composite.getId());
		if (compositeItem && !compositeContainer) {
			compositeItem.progressService.showWhile(Promise.resolve(), this.partService.isRestored() ? 800 : 3200 /* less ugly initial startup */);
227 228 229
		}

		// Fill Content and Actions
I
isidor 已提交
230 231
		// 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()) {
R
Rob Lourens 已提交
232
			return undefined;
I
isidor 已提交
233
		}
234

I
isidor 已提交
235
		// Take Composite on-DOM and show
M
Matt Bierner 已提交
236 237 238 239
		const contentArea = this.getContentArea();
		if (contentArea) {
			contentArea.appendChild(compositeContainer);
		}
I
isidor 已提交
240
		show(compositeContainer);
241

I
isidor 已提交
242 243
		// Setup action runner
		this.toolBar.actionRunner = composite.getActionRunner();
244

I
isidor 已提交
245 246 247
		// Update title with composite title if it differs from descriptor
		const descriptor = this.registry.getComposite(composite.getId());
		if (descriptor && descriptor.name !== composite.getTitle()) {
M
Matt Bierner 已提交
248
			this.updateTitle(composite.getId(), composite.getTitle() || undefined);
I
isidor 已提交
249
		}
250

I
isidor 已提交
251 252 253 254 255 256 257
		// Handle Composite Actions
		let actionsBinding = this.mapActionsBindingToComposite[composite.getId()];
		if (!actionsBinding) {
			actionsBinding = this.collectCompositeActions(composite);
			this.mapActionsBindingToComposite[composite.getId()] = actionsBinding;
		}
		actionsBinding();
258

I
isidor 已提交
259 260 261 262
		if (this.telemetryActionsListener) {
			this.telemetryActionsListener.dispose();
			this.telemetryActionsListener = null;
		}
263

I
isidor 已提交
264 265
		// Action Run Handling
		this.telemetryActionsListener = this.toolBar.actionRunner.onDidRun(e => {
266

I
isidor 已提交
267 268 269 270
			// Check for Error
			if (e.error && !errors.isPromiseCanceledError(e.error)) {
				this.notificationService.error(e.error);
			}
271

I
isidor 已提交
272 273 274 275 276 277 278 279 280 281 282
			// 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 });
			}
		});
283

I
isidor 已提交
284
		// Indicate to composite that it is now visible
285
		composite.setVisible(true);
286

287 288 289 290
		// 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;
		}
291

292 293 294 295
		// Make sure the composite is layed out
		if (this.contentAreaSize) {
			composite.layout(this.contentAreaSize);
		}
296 297
	}

298
	protected onTitleAreaUpdate(compositeId: string): void {
299 300

		// Active Composite
301
		if (this.activeComposite && this.activeComposite.getId() === compositeId) {
302 303

			// Title
M
Matt Bierner 已提交
304
			this.updateTitle(this.activeComposite.getId(), this.activeComposite.getTitle() || undefined);
305 306

			// Actions
307
			const actionsBinding = this.collectCompositeActions(this.activeComposite);
308 309 310 311 312 313
			this.mapActionsBindingToComposite[this.activeComposite.getId()] = actionsBinding;
			actionsBinding();
		}

		// Otherwise invalidate actions binding for next time when the composite becomes visible
		else {
314
			delete this.mapActionsBindingToComposite[compositeId];
315 316 317
		}
	}

318
	private updateTitle(compositeId: string, compositeTitle?: string): void {
319
		const compositeDescriptor = this.registry.getComposite(compositeId);
I
isidor 已提交
320
		if (!compositeDescriptor || !this.titleLabel) {
321 322 323 324 325 326 327
			return;
		}

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

328
		const keybinding = this.keybindingService.lookupKeybinding(compositeId);
329

M
Matt Bierner 已提交
330
		this.titleLabel.updateTitle(compositeId, compositeTitle, (keybinding && keybinding.getLabel()) || undefined);
331

B
Benjamin Pasero 已提交
332
		this.toolBar.setAriaLabel(nls.localize('ariaCompositeToolbarLabel', "{0} actions", compositeTitle));
333 334 335 336 337
	}

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

		// From Composite
338 339
		const primaryActions: IAction[] = composite.getActions().slice(0);
		const secondaryActions: IAction[] = composite.getSecondaryActions().slice(0);
340

I
isidor 已提交
341 342 343 344
		// From Part
		primaryActions.push(...this.getActions());
		secondaryActions.push(...this.getSecondaryActions());

J
Joao Moreno 已提交
345 346 347
		// Update context
		this.toolBar.context = this.actionsContextProvider();

348 349 350 351
		// Return fn to set into toolbar
		return this.toolBar.setActions(prepareActions(primaryActions), prepareActions(secondaryActions));
	}

M
Matt Bierner 已提交
352
	protected getActiveComposite(): IComposite | null {
353 354 355 356 357 358 359
		return this.activeComposite;
	}

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

M
Matt Bierner 已提交
360
	protected hideActiveComposite(): Composite | undefined {
361
		if (!this.activeComposite) {
362
			return undefined; // Nothing to do
363 364
		}

365
		const composite = this.activeComposite;
366 367
		this.activeComposite = null;

368
		const compositeContainer = this.mapCompositeToCompositeContainer[composite.getId()];
369 370

		// Indicate to Composite
371
		composite.setVisible(false);
372

373 374 375
		// Take Container Off-DOM and hide
		compositeContainer.remove();
		hide(compositeContainer);
376

377 378
		// Clear any running Progress
		this.progressBar.stop().hide();
379

380 381
		// Empty Actions
		this.toolBar.setActions([])();
B
Benjamin Pasero 已提交
382
		this.onDidCompositeClose.fire(composite);
383

384
		return composite;
385 386
	}

B
Benjamin Pasero 已提交
387
	createTitleArea(parent: HTMLElement): HTMLElement {
388 389

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

I
isidor 已提交
393
		// Left Title Label
B
Benjamin Pasero 已提交
394
		this.titleLabel = this.createTitleLabel(titleArea);
I
isidor 已提交
395

396
		// Right Actions Container
B
Benjamin Pasero 已提交
397 398 399 400 401 402
		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,
403 404
			getKeyBinding: action => this.keybindingService.lookupKeybinding(action.id),
			anchorAlignmentProvider: () => this.getTitleAreaDropDownAnchorAlignment()
B
Benjamin Pasero 已提交
405
		}));
406

B
Benjamin Pasero 已提交
407
		return titleArea;
408 409
	}

410
	protected createTitleLabel(parent: HTMLElement): ICompositeTitleLabel {
B
Benjamin Pasero 已提交
411 412
		const titleContainer = append(parent, $('.title-label'));
		const titleLabel = append(titleContainer, $('h2'));
413

414
		const $this = this;
415 416
		return {
			updateTitle: (id, title, keybinding) => {
B
Benjamin Pasero 已提交
417 418
				titleLabel.innerHTML = strings.escape(title);
				titleLabel.title = keybinding ? nls.localize('titleTooltip', "{0} ({1})", title, keybinding) : title;
419
			},
B
Benjamin Pasero 已提交
420

421
			updateStyles: () => {
M
Matt Bierner 已提交
422
				titleLabel.style.color = $this.titleForegroundColor ? $this.getColor($this.titleForegroundColor) : null;
423 424 425 426
			}
		};
	}

427 428 429 430 431 432 433
	protected updateStyles(): void {
		super.updateStyles();

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

M
Matt Bierner 已提交
434
	protected actionItemProvider(action: Action): IActionItem | null {
435

436 437
		// Check Active Composite
		if (this.activeComposite) {
438
			return this.activeComposite.getActionItem(action);
439 440
		}

M
Matt Bierner 已提交
441
		return null;
442 443
	}

J
Joao Moreno 已提交
444 445 446 447 448 449 450 451 452 453
	protected actionsContextProvider(): any {

		// Check Active Composite
		if (this.activeComposite) {
			return this.activeComposite.getActionsContext();
		}

		return null;
	}

B
Benjamin Pasero 已提交
454
	createContentArea(parent: HTMLElement): HTMLElement {
B
Benjamin Pasero 已提交
455 456 457 458 459 460 461
		const contentContainer = append(parent, $('.content'));

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

		return contentContainer;
462 463
	}

464 465
	getProgressIndicator(id: string): IProgressService | null {
		const compositeItem = this.instantiatedCompositeItems.get(id);
466

467
		return compositeItem ? compositeItem.progressService : null;
468
	}
469

I
isidor 已提交
470 471 472 473 474 475 476 477
	protected getActions(): IAction[] {
		return [];
	}

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

478 479 480 481
	protected getTitleAreaDropDownAnchorAlignment(): AnchorAlignment {
		return AnchorAlignment.RIGHT;
	}

482 483 484
	layout(dimension: Dimension): Dimension[];
	layout(width: number, height: number): void;
	layout(dim1: Dimension | number, dim2?: number): Dimension[] | void {
485

486
		// Pass to super
487
		const sizes = super.layout(dim1 instanceof Dimension ? dim1 : new Dimension(dim1, dim2!));
488 489 490 491 492 493 494

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

495 496 497
		if (dim1 instanceof Dimension) {
			return sizes;
		}
498 499
	}

500 501
	protected removeComposite(compositeId: string): boolean {
		if (this.activeComposite && this.activeComposite.getId() === compositeId) {
502
			return false; // do not remove active composite
503 504 505 506 507 508 509 510 511 512
		}

		delete this.mapCompositeToCompositeContainer[compositeId];
		delete this.mapActionsBindingToComposite[compositeId];
		const compositeItem = this.instantiatedCompositeItems.get(compositeId);
		if (compositeItem) {
			compositeItem.composite.dispose();
			dispose(compositeItem.disposable);
			this.instantiatedCompositeItems.delete(compositeId);
		}
513

514 515 516
		return true;
	}

B
Benjamin Pasero 已提交
517
	dispose(): void {
M
Matt Bierner 已提交
518 519
		this.mapCompositeToCompositeContainer = null!; // StrictNullOverride: nulling out ok in dispose
		this.mapActionsBindingToComposite = null!; // StrictNullOverride: nulling out ok in dispose
520

521 522 523 524
		this.instantiatedCompositeItems.forEach(compositeItem => {
			compositeItem.composite.dispose();
			dispose(compositeItem.disposable);
		});
525

526
		this.instantiatedCompositeItems.clear();
527 528 529 530

		super.dispose();
	}
}