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