Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/browser/parts/compositePart.ts
5259 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import './media/compositepart.css';
7
import { localize } from '../../../nls.js';
8
import { defaultGenerator } from '../../../base/common/idGenerator.js';
9
import { IDisposable, dispose, DisposableStore, MutableDisposable, } from '../../../base/common/lifecycle.js';
10
import { Emitter } from '../../../base/common/event.js';
11
import { isCancellationError } from '../../../base/common/errors.js';
12
import { ActionsOrientation, IActionViewItem, prepareActions } from '../../../base/browser/ui/actionbar/actionbar.js';
13
import { ProgressBar } from '../../../base/browser/ui/progressbar/progressbar.js';
14
import { IAction } from '../../../base/common/actions.js';
15
import { Part, IPartOptions } from '../part.js';
16
import { Composite, CompositeRegistry } from '../composite.js';
17
import { IComposite } from '../../common/composite.js';
18
import { IWorkbenchLayoutService } from '../../services/layout/browser/layoutService.js';
19
import { IStorageService, StorageScope, StorageTarget } from '../../../platform/storage/common/storage.js';
20
import { IContextMenuService } from '../../../platform/contextview/browser/contextView.js';
21
import { IInstantiationService } from '../../../platform/instantiation/common/instantiation.js';
22
import { ServiceCollection } from '../../../platform/instantiation/common/serviceCollection.js';
23
import { IProgressIndicator, IEditorProgressService } from '../../../platform/progress/common/progress.js';
24
import { IKeybindingService } from '../../../platform/keybinding/common/keybinding.js';
25
import { IThemeService } from '../../../platform/theme/common/themeService.js';
26
import { INotificationService } from '../../../platform/notification/common/notification.js';
27
import { Dimension, append, $, hide, show } from '../../../base/browser/dom.js';
28
import { AnchorAlignment } from '../../../base/browser/ui/contextview/contextview.js';
29
import { assertReturnsDefined } from '../../../base/common/types.js';
30
import { createActionViewItem } from '../../../platform/actions/browser/menuEntryActionViewItem.js';
31
import { AbstractProgressScope, ScopedProgressIndicator } from '../../services/progress/browser/progressIndicator.js';
32
import { WorkbenchToolBar } from '../../../platform/actions/browser/toolbar.js';
33
import { defaultProgressBarStyles } from '../../../platform/theme/browser/defaultStyles.js';
34
import { IBoundarySashes } from '../../../base/browser/ui/sash/sash.js';
35
import { IBaseActionViewItemOptions } from '../../../base/browser/ui/actionbar/actionViewItems.js';
36
import { IHoverDelegate } from '../../../base/browser/ui/hover/hoverDelegate.js';
37
import { createInstantHoverDelegate, getDefaultHoverDelegate } from '../../../base/browser/ui/hover/hoverDelegateFactory.js';
38
import type { IHoverService } from '../../../platform/hover/browser/hover.js';
39
40
export interface ICompositeTitleLabel {
41
42
/**
43
* Asks to update the title for the composite with the given ID.
44
*/
45
updateTitle(id: string, title: string, keybinding?: string): void;
46
47
/**
48
* Called when theming information changes.
49
*/
50
updateStyles(): void;
51
}
52
53
interface CompositeItem {
54
readonly composite: Composite;
55
readonly disposable: IDisposable;
56
readonly progress: IProgressIndicator;
57
}
58
59
export interface ICompositePartOptions extends IPartOptions {
60
readonly trailingSeparator?: boolean;
61
}
62
63
export abstract class CompositePart<T extends Composite, MementoType extends object = object> extends Part<MementoType> {
64
65
protected readonly onDidCompositeOpen = this._register(new Emitter<{ composite: IComposite; focus: boolean }>());
66
protected readonly onDidCompositeClose = this._register(new Emitter<IComposite>());
67
68
protected toolBar: WorkbenchToolBar | undefined;
69
protected titleLabelElement: HTMLElement | undefined;
70
protected readonly toolbarHoverDelegate: IHoverDelegate;
71
72
private readonly mapCompositeToCompositeContainer = new Map<string, HTMLElement>();
73
private readonly mapActionsBindingToComposite = new Map<string, () => void>();
74
private activeComposite: Composite | undefined;
75
private lastActiveCompositeId: string;
76
private readonly instantiatedCompositeItems = new Map<string, CompositeItem>();
77
protected titleLabel: ICompositeTitleLabel | undefined;
78
private progressBar: ProgressBar | undefined;
79
private contentAreaSize: Dimension | undefined;
80
private readonly actionsListener = this._register(new MutableDisposable());
81
private currentCompositeOpenToken: string | undefined;
82
private boundarySashes: IBoundarySashes | undefined;
83
private readonly trailingSeparator: boolean;
84
85
constructor(
86
private readonly notificationService: INotificationService,
87
protected readonly storageService: IStorageService,
88
protected readonly contextMenuService: IContextMenuService,
89
layoutService: IWorkbenchLayoutService,
90
protected readonly keybindingService: IKeybindingService,
91
private readonly hoverService: IHoverService,
92
protected readonly instantiationService: IInstantiationService,
93
themeService: IThemeService,
94
protected readonly registry: CompositeRegistry<T>,
95
private readonly activeCompositeSettingsKey: string,
96
private readonly defaultCompositeId: string,
97
protected readonly nameForTelemetry: string,
98
private readonly compositeCSSClass: string,
99
private readonly titleForegroundColor: string | undefined,
100
private readonly titleBorderColor: string | undefined,
101
id: string,
102
options: ICompositePartOptions
103
) {
104
super(id, options, themeService, storageService, layoutService);
105
106
this.lastActiveCompositeId = storageService.get(activeCompositeSettingsKey, StorageScope.WORKSPACE, this.defaultCompositeId);
107
this.toolbarHoverDelegate = this._register(createInstantHoverDelegate());
108
this.trailingSeparator = options.trailingSeparator ?? false;
109
}
110
111
protected openComposite(id: string, focus?: boolean): Composite | undefined {
112
113
// Check if composite already visible and just focus in that case
114
if (this.activeComposite?.getId() === id) {
115
if (focus) {
116
this.activeComposite.focus();
117
}
118
119
// Fullfill promise with composite that is being opened
120
return this.activeComposite;
121
}
122
123
// We cannot open the composite if we have not been created yet
124
if (!this.element) {
125
return;
126
}
127
128
// Open
129
return this.doOpenComposite(id, focus);
130
}
131
132
private doOpenComposite(id: string, focus: boolean = false): Composite | undefined {
133
134
// Use a generated token to avoid race conditions from long running promises
135
const currentCompositeOpenToken = defaultGenerator.nextId();
136
this.currentCompositeOpenToken = currentCompositeOpenToken;
137
138
// Hide current
139
if (this.activeComposite) {
140
this.hideActiveComposite();
141
}
142
143
// Update Title
144
this.updateTitle(id);
145
146
// Create composite
147
const composite = this.createComposite(id, true);
148
149
// Check if another composite opened meanwhile and return in that case
150
if ((this.currentCompositeOpenToken !== currentCompositeOpenToken) || (this.activeComposite && this.activeComposite.getId() !== composite.getId())) {
151
return undefined;
152
}
153
154
// Check if composite already visible and just focus in that case
155
if (this.activeComposite?.getId() === composite.getId()) {
156
if (focus) {
157
composite.focus();
158
}
159
160
this.onDidCompositeOpen.fire({ composite, focus });
161
return composite;
162
}
163
164
// Show Composite and Focus
165
this.showComposite(composite);
166
if (focus) {
167
composite.focus();
168
}
169
170
// Return with the composite that is being opened
171
if (composite) {
172
this.onDidCompositeOpen.fire({ composite, focus });
173
}
174
175
return composite;
176
}
177
178
protected createComposite(id: string, isActive?: boolean): Composite {
179
180
// Check if composite is already created
181
const compositeItem = this.instantiatedCompositeItems.get(id);
182
if (compositeItem) {
183
return compositeItem.composite;
184
}
185
186
// Instantiate composite from registry otherwise
187
const compositeDescriptor = this.registry.getComposite(id);
188
if (compositeDescriptor) {
189
const that = this;
190
const compositeProgressIndicator = new ScopedProgressIndicator(assertReturnsDefined(this.progressBar), this._register(new class extends AbstractProgressScope {
191
constructor() {
192
super(compositeDescriptor!.id, !!isActive);
193
this._register(that.onDidCompositeOpen.event(e => this.onScopeOpened(e.composite.getId())));
194
this._register(that.onDidCompositeClose.event(e => this.onScopeClosed(e.getId())));
195
}
196
}()));
197
const compositeInstantiationService = this._register(this.instantiationService.createChild(new ServiceCollection(
198
[IEditorProgressService, compositeProgressIndicator] // provide the editor progress service for any editors instantiated within the composite
199
)));
200
201
const composite = compositeDescriptor.instantiate(compositeInstantiationService);
202
const disposable = new DisposableStore();
203
204
// Remember as Instantiated
205
this.instantiatedCompositeItems.set(id, { composite, disposable, progress: compositeProgressIndicator });
206
207
// Register to title area update events from the composite
208
disposable.add(composite.onTitleAreaUpdate(() => this.onTitleAreaUpdate(composite.getId()), this));
209
disposable.add(compositeInstantiationService);
210
211
return composite;
212
}
213
214
throw new Error(`Unable to find composite with id ${id}`);
215
}
216
217
protected showComposite(composite: Composite): void {
218
219
// Remember Composite
220
this.activeComposite = composite;
221
222
// Store in preferences
223
const id = this.activeComposite.getId();
224
if (id !== this.defaultCompositeId) {
225
this.storageService.store(this.activeCompositeSettingsKey, id, StorageScope.WORKSPACE, StorageTarget.MACHINE);
226
} else {
227
this.storageService.remove(this.activeCompositeSettingsKey, StorageScope.WORKSPACE);
228
}
229
230
// Remember
231
this.lastActiveCompositeId = this.activeComposite.getId();
232
233
// Composites created for the first time
234
let compositeContainer = this.mapCompositeToCompositeContainer.get(composite.getId());
235
if (!compositeContainer) {
236
237
// Build Container off-DOM
238
compositeContainer = $('.composite');
239
compositeContainer.classList.add(...this.compositeCSSClass.split(' '));
240
compositeContainer.id = composite.getId();
241
242
composite.create(compositeContainer);
243
composite.updateStyles();
244
245
// Remember composite container
246
this.mapCompositeToCompositeContainer.set(composite.getId(), compositeContainer);
247
}
248
249
// Fill Content and Actions
250
// Make sure that the user meanwhile did not open another composite or closed the part containing the composite
251
if (!this.activeComposite || composite.getId() !== this.activeComposite.getId()) {
252
return undefined;
253
}
254
255
// Take Composite on-DOM and show
256
this.contentArea?.appendChild(compositeContainer);
257
show(compositeContainer);
258
259
// Setup action runner
260
const toolBar = assertReturnsDefined(this.toolBar);
261
toolBar.actionRunner = composite.getActionRunner();
262
263
// Update title with composite title if it differs from descriptor
264
const descriptor = this.registry.getComposite(composite.getId());
265
if (descriptor && descriptor.name !== composite.getTitle()) {
266
this.updateTitle(composite.getId(), composite.getTitle());
267
}
268
269
// Handle Composite Actions
270
let actionsBinding = this.mapActionsBindingToComposite.get(composite.getId());
271
if (!actionsBinding) {
272
actionsBinding = this.collectCompositeActions(composite);
273
this.mapActionsBindingToComposite.set(composite.getId(), actionsBinding);
274
}
275
actionsBinding();
276
277
// Action Run Handling
278
this.actionsListener.value = toolBar.actionRunner.onDidRun(e => {
279
280
// Check for Error
281
if (e.error && !isCancellationError(e.error)) {
282
this.notificationService.error(e.error);
283
}
284
});
285
286
// Indicate to composite that it is now visible
287
composite.setVisible(true);
288
289
// Make sure that the user meanwhile did not open another composite or closed the part containing the composite
290
if (!this.activeComposite || composite.getId() !== this.activeComposite.getId()) {
291
return;
292
}
293
294
// Make sure the composite is layed out
295
if (this.contentAreaSize) {
296
composite.layout(this.contentAreaSize);
297
}
298
299
// Make sure boundary sashes are propagated
300
if (this.boundarySashes) {
301
composite.setBoundarySashes(this.boundarySashes);
302
}
303
}
304
305
protected onTitleAreaUpdate(compositeId: string): void {
306
307
// Title
308
const composite = this.instantiatedCompositeItems.get(compositeId);
309
if (composite) {
310
this.updateTitle(compositeId, composite.composite.getTitle());
311
}
312
313
// Active Composite
314
if (this.activeComposite?.getId() === compositeId) {
315
// Actions
316
const actionsBinding = this.collectCompositeActions(this.activeComposite);
317
this.mapActionsBindingToComposite.set(this.activeComposite.getId(), actionsBinding);
318
actionsBinding();
319
}
320
321
// Otherwise invalidate actions binding for next time when the composite becomes visible
322
else {
323
this.mapActionsBindingToComposite.delete(compositeId);
324
}
325
}
326
327
private updateTitle(compositeId: string, compositeTitle?: string): void {
328
const compositeDescriptor = this.registry.getComposite(compositeId);
329
if (!compositeDescriptor || !this.titleLabel) {
330
return;
331
}
332
333
if (!compositeTitle) {
334
compositeTitle = compositeDescriptor.name;
335
}
336
337
const keybinding = this.keybindingService.lookupKeybinding(compositeId);
338
339
this.titleLabel.updateTitle(compositeId, compositeTitle, keybinding?.getLabel() ?? undefined);
340
341
const toolBar = assertReturnsDefined(this.toolBar);
342
toolBar.setAriaLabel(localize('ariaCompositeToolbarLabel', "{0} actions", compositeTitle));
343
}
344
345
private collectCompositeActions(composite?: Composite): () => void {
346
347
// From Composite
348
const menuIds = composite?.getMenuIds();
349
const primaryActions: IAction[] = composite?.getActions().slice(0) || [];
350
const secondaryActions: IAction[] = composite?.getSecondaryActions().slice(0) || [];
351
352
// Update context
353
const toolBar = assertReturnsDefined(this.toolBar);
354
toolBar.context = this.actionsContextProvider();
355
356
// Return fn to set into toolbar
357
return () => {
358
toolBar.setActions(prepareActions(primaryActions), prepareActions(secondaryActions), menuIds);
359
this.titleArea?.classList.toggle('has-actions', primaryActions.length > 0 || secondaryActions.length > 0);
360
};
361
}
362
363
protected getActiveComposite(): IComposite | undefined {
364
return this.activeComposite;
365
}
366
367
protected getLastActiveCompositeId(): string {
368
return this.lastActiveCompositeId;
369
}
370
371
protected hideActiveComposite(): Composite | undefined {
372
if (!this.activeComposite) {
373
return undefined; // Nothing to do
374
}
375
376
const composite = this.activeComposite;
377
this.activeComposite = undefined;
378
379
const compositeContainer = this.mapCompositeToCompositeContainer.get(composite.getId());
380
381
// Indicate to Composite
382
composite.setVisible(false);
383
384
// Take Container Off-DOM and hide
385
if (compositeContainer) {
386
compositeContainer.remove();
387
hide(compositeContainer);
388
}
389
390
// Clear any running Progress
391
this.progressBar?.stop().hide();
392
393
// Empty Actions
394
if (this.toolBar) {
395
this.collectCompositeActions()();
396
}
397
this.onDidCompositeClose.fire(composite);
398
399
return composite;
400
}
401
402
protected override createTitleArea(parent: HTMLElement): HTMLElement {
403
404
// Title Area Container
405
const titleArea = append(parent, $('.composite'));
406
titleArea.classList.add('title');
407
408
// Left Title Label
409
this.titleLabel = this.createTitleLabel(titleArea);
410
411
// Right Actions Container
412
const titleActionsContainer = append(titleArea, $('.title-actions'));
413
414
// Toolbar
415
this.toolBar = this._register(this.instantiationService.createInstance(WorkbenchToolBar, titleActionsContainer, {
416
actionViewItemProvider: (action, options) => this.actionViewItemProvider(action, options),
417
orientation: ActionsOrientation.HORIZONTAL,
418
getKeyBinding: action => this.keybindingService.lookupKeybinding(action.id),
419
anchorAlignmentProvider: () => this.getTitleAreaDropDownAnchorAlignment(),
420
toggleMenuTitle: localize('viewsAndMoreActions', "Views and More Actions..."),
421
telemetrySource: this.nameForTelemetry,
422
hoverDelegate: this.toolbarHoverDelegate,
423
trailingSeparator: this.trailingSeparator,
424
}));
425
426
this.collectCompositeActions()();
427
428
return titleArea;
429
}
430
431
protected createTitleLabel(parent: HTMLElement): ICompositeTitleLabel {
432
const titleContainer = append(parent, $('.title-label'));
433
const titleLabel = append(titleContainer, $('h2'));
434
this.titleLabelElement = titleLabel;
435
const hover = this._register(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), titleLabel, ''));
436
437
const $this = this;
438
return {
439
updateTitle: (id, title, keybinding) => {
440
// The title label is shared for all composites in the base CompositePart
441
if (!this.activeComposite || this.activeComposite.getId() === id) {
442
titleLabel.textContent = title;
443
hover.update(keybinding ? localize('titleTooltip', "{0} ({1})", title, keybinding) : title);
444
}
445
},
446
447
updateStyles: () => {
448
titleLabel.style.color = $this.titleForegroundColor ? $this.getColor($this.titleForegroundColor) || '' : '';
449
const borderColor = $this.titleBorderColor ? $this.getColor($this.titleBorderColor) : undefined;
450
parent.style.borderBottom = borderColor ? `1px solid ${borderColor}` : '';
451
}
452
};
453
}
454
455
protected createHeaderArea(): HTMLElement {
456
return $('.composite');
457
}
458
459
protected createFooterArea(): HTMLElement {
460
return $('.composite');
461
}
462
463
override updateStyles(): void {
464
super.updateStyles();
465
466
// Forward to title label
467
const titleLabel = assertReturnsDefined(this.titleLabel);
468
titleLabel.updateStyles();
469
}
470
471
protected actionViewItemProvider(action: IAction, options: IBaseActionViewItemOptions): IActionViewItem | undefined {
472
473
// Check Active Composite
474
if (this.activeComposite) {
475
return this.activeComposite.getActionViewItem(action, options);
476
}
477
478
return createActionViewItem(this.instantiationService, action, options);
479
}
480
481
protected actionsContextProvider(): unknown {
482
483
// Check Active Composite
484
if (this.activeComposite) {
485
return this.activeComposite.getActionsContext();
486
}
487
488
return null;
489
}
490
491
protected override createContentArea(parent: HTMLElement): HTMLElement {
492
const contentContainer = append(parent, $('.content'));
493
494
this.progressBar = this._register(new ProgressBar(contentContainer, defaultProgressBarStyles));
495
this.progressBar.hide();
496
497
return contentContainer;
498
}
499
500
getProgressIndicator(id: string): IProgressIndicator | undefined {
501
const compositeItem = this.instantiatedCompositeItems.get(id);
502
503
return compositeItem ? compositeItem.progress : undefined;
504
}
505
506
protected getTitleAreaDropDownAnchorAlignment(): AnchorAlignment {
507
return AnchorAlignment.RIGHT;
508
}
509
510
override layout(width: number, height: number, top: number, left: number): void {
511
super.layout(width, height, top, left);
512
513
// Layout contents
514
this.contentAreaSize = Dimension.lift(super.layoutContents(width, height).contentSize);
515
516
// Layout composite
517
this.activeComposite?.layout(this.contentAreaSize);
518
}
519
520
setBoundarySashes?(sashes: IBoundarySashes): void {
521
this.boundarySashes = sashes;
522
this.activeComposite?.setBoundarySashes(sashes);
523
}
524
525
protected removeComposite(compositeId: string): boolean {
526
if (this.activeComposite?.getId() === compositeId) {
527
return false; // do not remove active composite
528
}
529
530
this.mapCompositeToCompositeContainer.delete(compositeId);
531
this.mapActionsBindingToComposite.delete(compositeId);
532
const compositeItem = this.instantiatedCompositeItems.get(compositeId);
533
if (compositeItem) {
534
compositeItem.composite.dispose();
535
dispose(compositeItem.disposable);
536
this.instantiatedCompositeItems.delete(compositeId);
537
}
538
539
return true;
540
}
541
542
override dispose(): void {
543
this.mapCompositeToCompositeContainer.clear();
544
this.mapActionsBindingToComposite.clear();
545
546
this.instantiatedCompositeItems.forEach(compositeItem => {
547
compositeItem.composite.dispose();
548
dispose(compositeItem.disposable);
549
});
550
551
this.instantiatedCompositeItems.clear();
552
553
super.dispose();
554
}
555
}
556
557