compositePart.ts 17.3 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,
87
		private titleForegroundColor: string,
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
		// 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
108
			return this.activeComposite;
109 110 111 112 113 114
		}

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

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

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

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

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

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

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

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

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

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

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

158
		return composite;
159 160
	}

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

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

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

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

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

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

184
			return composite;
185 186
		}

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

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

		// Remember Composite
		this.activeComposite = composite;

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

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

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

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

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

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

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

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

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

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

I
isidor 已提交
244 245 246
		// 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 已提交
247
			this.updateTitle(composite.getId(), composite.getTitle() || undefined);
I
isidor 已提交
248
		}
249

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

344 345 346 347
		// Return fn to set into toolbar
		return this.toolBar.setActions(prepareActions(primaryActions), prepareActions(secondaryActions));
	}

M
Matt Bierner 已提交
348
	protected getActiveComposite(): IComposite | null {
349 350 351 352 353 354 355
		return this.activeComposite;
	}

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

M
Matt Bierner 已提交
356
	protected hideActiveComposite(): Composite | undefined {
357
		if (!this.activeComposite) {
358
			return undefined; // Nothing to do
359 360
		}

361
		const composite = this.activeComposite;
362 363
		this.activeComposite = null;

364
		const compositeContainer = this.mapCompositeToCompositeContainer[composite.getId()];
365 366

		// Indicate to Composite
367
		composite.setVisible(false);
368

369 370 371
		// Take Container Off-DOM and hide
		compositeContainer.remove();
		hide(compositeContainer);
372

373 374
		// Clear any running Progress
		this.progressBar.stop().hide();
375

376 377
		// Empty Actions
		this.toolBar.setActions([])();
B
Benjamin Pasero 已提交
378
		this.onDidCompositeClose.fire(composite);
379

380
		return composite;
381 382
	}

B
Benjamin Pasero 已提交
383
	createTitleArea(parent: HTMLElement): HTMLElement {
384 385

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

I
isidor 已提交
389
		// Left Title Label
B
Benjamin Pasero 已提交
390
		this.titleLabel = this.createTitleLabel(titleArea);
I
isidor 已提交
391

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

B
Benjamin Pasero 已提交
403
		return titleArea;
404 405
	}

406
	protected createTitleLabel(parent: HTMLElement): ICompositeTitleLabel {
B
Benjamin Pasero 已提交
407 408
		const titleContainer = append(parent, $('.title-label'));
		const titleLabel = append(titleContainer, $('h2'));
409

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

417
			updateStyles: () => {
B
Benjamin Pasero 已提交
418
				titleLabel.style.color = $this.getColor($this.titleForegroundColor);
419 420 421 422
			}
		};
	}

423 424 425 426 427 428 429
	protected updateStyles(): void {
		super.updateStyles();

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

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

432 433
		// Check Active Composite
		if (this.activeComposite) {
434
			return this.activeComposite.getActionItem(action);
435 436
		}

M
Matt Bierner 已提交
437
		return null;
438 439
	}

B
Benjamin Pasero 已提交
440
	createContentArea(parent: HTMLElement): HTMLElement {
B
Benjamin Pasero 已提交
441 442 443 444 445 446 447
		const contentContainer = append(parent, $('.content'));

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

		return contentContainer;
448 449
	}

450 451 452
	getProgressIndicator(id: string): IProgressService | null {
		const compositeItem = this.instantiatedCompositeItems.get(id);
		return compositeItem ? compositeItem.progressService : null;
453
	}
454

I
isidor 已提交
455 456 457 458 459 460 461 462
	protected getActions(): IAction[] {
		return [];
	}

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

463 464 465 466
	protected getTitleAreaDropDownAnchorAlignment(): AnchorAlignment {
		return AnchorAlignment.RIGHT;
	}

467 468 469
	layout(dimension: Dimension): Dimension[];
	layout(width: number, height: number): void;
	layout(dim1: Dimension | number, dim2?: number): Dimension[] | void {
470
		// Pass to super
471
		const sizes = super.layout(dim1 instanceof Dimension ? dim1 : new Dimension(dim1, dim2!));
472 473 474 475 476 477 478

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

479 480 481
		if (dim1 instanceof Dimension) {
			return sizes;
		}
482 483
	}

484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500
	protected removeComposite(compositeId: string): boolean {
		if (this.activeComposite && this.activeComposite.getId() === compositeId) {
			// do not remove active compoiste
			return false;
		}

		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);
		}
		return true;
	}

B
Benjamin Pasero 已提交
501
	dispose(): void {
M
Matt Bierner 已提交
502 503
		this.mapCompositeToCompositeContainer = null!; // StrictNullOverride: nulling out ok in dispose
		this.mapActionsBindingToComposite = null!; // StrictNullOverride: nulling out ok in dispose
504

505 506 507 508
		this.instantiatedCompositeItems.forEach(compositeItem => {
			compositeItem.composite.dispose();
			dispose(compositeItem.disposable);
		});
509

510
		this.instantiatedCompositeItems.clear();
511 512 513 514

		super.dispose();
	}
}