Path: blob/main/src/vs/workbench/browser/parts/compositePart.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import './media/compositepart.css';6import { localize } from '../../../nls.js';7import { defaultGenerator } from '../../../base/common/idGenerator.js';8import { IDisposable, dispose, DisposableStore, MutableDisposable, } from '../../../base/common/lifecycle.js';9import { Emitter } from '../../../base/common/event.js';10import { isCancellationError } from '../../../base/common/errors.js';11import { ActionsOrientation, IActionViewItem, prepareActions } from '../../../base/browser/ui/actionbar/actionbar.js';12import { ProgressBar } from '../../../base/browser/ui/progressbar/progressbar.js';13import { IAction } from '../../../base/common/actions.js';14import { Part, IPartOptions } from '../part.js';15import { Composite, CompositeRegistry } from '../composite.js';16import { IComposite } from '../../common/composite.js';17import { IWorkbenchLayoutService } from '../../services/layout/browser/layoutService.js';18import { IStorageService, StorageScope, StorageTarget } from '../../../platform/storage/common/storage.js';19import { IContextMenuService } from '../../../platform/contextview/browser/contextView.js';20import { IInstantiationService } from '../../../platform/instantiation/common/instantiation.js';21import { ServiceCollection } from '../../../platform/instantiation/common/serviceCollection.js';22import { IProgressIndicator, IEditorProgressService } from '../../../platform/progress/common/progress.js';23import { IKeybindingService } from '../../../platform/keybinding/common/keybinding.js';24import { IThemeService } from '../../../platform/theme/common/themeService.js';25import { INotificationService } from '../../../platform/notification/common/notification.js';26import { Dimension, append, $, hide, show } from '../../../base/browser/dom.js';27import { AnchorAlignment } from '../../../base/browser/ui/contextview/contextview.js';28import { assertReturnsDefined } from '../../../base/common/types.js';29import { createActionViewItem } from '../../../platform/actions/browser/menuEntryActionViewItem.js';30import { AbstractProgressScope, ScopedProgressIndicator } from '../../services/progress/browser/progressIndicator.js';31import { WorkbenchToolBar } from '../../../platform/actions/browser/toolbar.js';32import { defaultProgressBarStyles } from '../../../platform/theme/browser/defaultStyles.js';33import { IBoundarySashes } from '../../../base/browser/ui/sash/sash.js';34import { IBaseActionViewItemOptions } from '../../../base/browser/ui/actionbar/actionViewItems.js';35import { IHoverDelegate } from '../../../base/browser/ui/hover/hoverDelegate.js';36import { createInstantHoverDelegate, getDefaultHoverDelegate } from '../../../base/browser/ui/hover/hoverDelegateFactory.js';37import type { IHoverService } from '../../../platform/hover/browser/hover.js';3839export interface ICompositeTitleLabel {4041/**42* Asks to update the title for the composite with the given ID.43*/44updateTitle(id: string, title: string, keybinding?: string): void;4546/**47* Called when theming information changes.48*/49updateStyles(): void;50}5152interface CompositeItem {53readonly composite: Composite;54readonly disposable: IDisposable;55readonly progress: IProgressIndicator;56}5758export abstract class CompositePart<T extends Composite> extends Part {5960protected readonly onDidCompositeOpen = this._register(new Emitter<{ composite: IComposite; focus: boolean }>());61protected readonly onDidCompositeClose = this._register(new Emitter<IComposite>());6263protected toolBar: WorkbenchToolBar | undefined;64protected titleLabelElement: HTMLElement | undefined;65protected readonly toolbarHoverDelegate: IHoverDelegate;6667private readonly mapCompositeToCompositeContainer = new Map<string, HTMLElement>();68private readonly mapActionsBindingToComposite = new Map<string, () => void>();69private activeComposite: Composite | undefined;70private lastActiveCompositeId: string;71private readonly instantiatedCompositeItems = new Map<string, CompositeItem>();72protected titleLabel: ICompositeTitleLabel | undefined;73private progressBar: ProgressBar | undefined;74private contentAreaSize: Dimension | undefined;75private readonly actionsListener = this._register(new MutableDisposable());76private currentCompositeOpenToken: string | undefined;77private boundarySashes: IBoundarySashes | undefined;7879constructor(80private readonly notificationService: INotificationService,81protected readonly storageService: IStorageService,82protected readonly contextMenuService: IContextMenuService,83layoutService: IWorkbenchLayoutService,84protected readonly keybindingService: IKeybindingService,85private readonly hoverService: IHoverService,86protected readonly instantiationService: IInstantiationService,87themeService: IThemeService,88protected readonly registry: CompositeRegistry<T>,89private readonly activeCompositeSettingsKey: string,90private readonly defaultCompositeId: string,91protected readonly nameForTelemetry: string,92private readonly compositeCSSClass: string,93private readonly titleForegroundColor: string | undefined,94private readonly titleBorderColor: string | undefined,95id: string,96options: IPartOptions97) {98super(id, options, themeService, storageService, layoutService);99100this.lastActiveCompositeId = storageService.get(activeCompositeSettingsKey, StorageScope.WORKSPACE, this.defaultCompositeId);101this.toolbarHoverDelegate = this._register(createInstantHoverDelegate());102}103104protected openComposite(id: string, focus?: boolean): Composite | undefined {105106// Check if composite already visible and just focus in that case107if (this.activeComposite?.getId() === id) {108if (focus) {109this.activeComposite.focus();110}111112// Fullfill promise with composite that is being opened113return this.activeComposite;114}115116// We cannot open the composite if we have not been created yet117if (!this.element) {118return;119}120121// Open122return this.doOpenComposite(id, focus);123}124125private doOpenComposite(id: string, focus: boolean = false): Composite | undefined {126127// Use a generated token to avoid race conditions from long running promises128const currentCompositeOpenToken = defaultGenerator.nextId();129this.currentCompositeOpenToken = currentCompositeOpenToken;130131// Hide current132if (this.activeComposite) {133this.hideActiveComposite();134}135136// Update Title137this.updateTitle(id);138139// Create composite140const composite = this.createComposite(id, true);141142// Check if another composite opened meanwhile and return in that case143if ((this.currentCompositeOpenToken !== currentCompositeOpenToken) || (this.activeComposite && this.activeComposite.getId() !== composite.getId())) {144return undefined;145}146147// Check if composite already visible and just focus in that case148if (this.activeComposite?.getId() === composite.getId()) {149if (focus) {150composite.focus();151}152153this.onDidCompositeOpen.fire({ composite, focus });154return composite;155}156157// Show Composite and Focus158this.showComposite(composite);159if (focus) {160composite.focus();161}162163// Return with the composite that is being opened164if (composite) {165this.onDidCompositeOpen.fire({ composite, focus });166}167168return composite;169}170171protected createComposite(id: string, isActive?: boolean): Composite {172173// Check if composite is already created174const compositeItem = this.instantiatedCompositeItems.get(id);175if (compositeItem) {176return compositeItem.composite;177}178179// Instantiate composite from registry otherwise180const compositeDescriptor = this.registry.getComposite(id);181if (compositeDescriptor) {182const that = this;183const compositeProgressIndicator = new ScopedProgressIndicator(assertReturnsDefined(this.progressBar), new class extends AbstractProgressScope {184constructor() {185super(compositeDescriptor!.id, !!isActive);186this._register(that.onDidCompositeOpen.event(e => this.onScopeOpened(e.composite.getId())));187this._register(that.onDidCompositeClose.event(e => this.onScopeClosed(e.getId())));188}189}());190const compositeInstantiationService = this._register(this.instantiationService.createChild(new ServiceCollection(191[IEditorProgressService, compositeProgressIndicator] // provide the editor progress service for any editors instantiated within the composite192)));193194const composite = compositeDescriptor.instantiate(compositeInstantiationService);195const disposable = new DisposableStore();196197// Remember as Instantiated198this.instantiatedCompositeItems.set(id, { composite, disposable, progress: compositeProgressIndicator });199200// Register to title area update events from the composite201disposable.add(composite.onTitleAreaUpdate(() => this.onTitleAreaUpdate(composite.getId()), this));202disposable.add(compositeInstantiationService);203204return composite;205}206207throw new Error(`Unable to find composite with id ${id}`);208}209210protected showComposite(composite: Composite): void {211212// Remember Composite213this.activeComposite = composite;214215// Store in preferences216const id = this.activeComposite.getId();217if (id !== this.defaultCompositeId) {218this.storageService.store(this.activeCompositeSettingsKey, id, StorageScope.WORKSPACE, StorageTarget.MACHINE);219} else {220this.storageService.remove(this.activeCompositeSettingsKey, StorageScope.WORKSPACE);221}222223// Remember224this.lastActiveCompositeId = this.activeComposite.getId();225226// Composites created for the first time227let compositeContainer = this.mapCompositeToCompositeContainer.get(composite.getId());228if (!compositeContainer) {229230// Build Container off-DOM231compositeContainer = $('.composite');232compositeContainer.classList.add(...this.compositeCSSClass.split(' '));233compositeContainer.id = composite.getId();234235composite.create(compositeContainer);236composite.updateStyles();237238// Remember composite container239this.mapCompositeToCompositeContainer.set(composite.getId(), compositeContainer);240}241242// Fill Content and Actions243// Make sure that the user meanwhile did not open another composite or closed the part containing the composite244if (!this.activeComposite || composite.getId() !== this.activeComposite.getId()) {245return undefined;246}247248// Take Composite on-DOM and show249const contentArea = this.getContentArea();250contentArea?.appendChild(compositeContainer);251show(compositeContainer);252253// Setup action runner254const toolBar = assertReturnsDefined(this.toolBar);255toolBar.actionRunner = composite.getActionRunner();256257// Update title with composite title if it differs from descriptor258const descriptor = this.registry.getComposite(composite.getId());259if (descriptor && descriptor.name !== composite.getTitle()) {260this.updateTitle(composite.getId(), composite.getTitle());261}262263// Handle Composite Actions264let actionsBinding = this.mapActionsBindingToComposite.get(composite.getId());265if (!actionsBinding) {266actionsBinding = this.collectCompositeActions(composite);267this.mapActionsBindingToComposite.set(composite.getId(), actionsBinding);268}269actionsBinding();270271// Action Run Handling272this.actionsListener.value = toolBar.actionRunner.onDidRun(e => {273274// Check for Error275if (e.error && !isCancellationError(e.error)) {276this.notificationService.error(e.error);277}278});279280// Indicate to composite that it is now visible281composite.setVisible(true);282283// Make sure that the user meanwhile did not open another composite or closed the part containing the composite284if (!this.activeComposite || composite.getId() !== this.activeComposite.getId()) {285return;286}287288// Make sure the composite is layed out289if (this.contentAreaSize) {290composite.layout(this.contentAreaSize);291}292293// Make sure boundary sashes are propagated294if (this.boundarySashes) {295composite.setBoundarySashes(this.boundarySashes);296}297}298299protected onTitleAreaUpdate(compositeId: string): void {300301// Title302const composite = this.instantiatedCompositeItems.get(compositeId);303if (composite) {304this.updateTitle(compositeId, composite.composite.getTitle());305}306307// Active Composite308if (this.activeComposite?.getId() === compositeId) {309// Actions310const actionsBinding = this.collectCompositeActions(this.activeComposite);311this.mapActionsBindingToComposite.set(this.activeComposite.getId(), actionsBinding);312actionsBinding();313}314315// Otherwise invalidate actions binding for next time when the composite becomes visible316else {317this.mapActionsBindingToComposite.delete(compositeId);318}319}320321private updateTitle(compositeId: string, compositeTitle?: string): void {322const compositeDescriptor = this.registry.getComposite(compositeId);323if (!compositeDescriptor || !this.titleLabel) {324return;325}326327if (!compositeTitle) {328compositeTitle = compositeDescriptor.name;329}330331const keybinding = this.keybindingService.lookupKeybinding(compositeId);332333this.titleLabel.updateTitle(compositeId, compositeTitle, keybinding?.getLabel() ?? undefined);334335const toolBar = assertReturnsDefined(this.toolBar);336toolBar.setAriaLabel(localize('ariaCompositeToolbarLabel', "{0} actions", compositeTitle));337}338339private collectCompositeActions(composite?: Composite): () => void {340341// From Composite342const menuIds = composite?.getMenuIds();343const primaryActions: IAction[] = composite?.getActions().slice(0) || [];344const secondaryActions: IAction[] = composite?.getSecondaryActions().slice(0) || [];345346// Update context347const toolBar = assertReturnsDefined(this.toolBar);348toolBar.context = this.actionsContextProvider();349350// Return fn to set into toolbar351return () => toolBar.setActions(prepareActions(primaryActions), prepareActions(secondaryActions), menuIds);352}353354protected getActiveComposite(): IComposite | undefined {355return this.activeComposite;356}357358protected getLastActiveCompositeId(): string {359return this.lastActiveCompositeId;360}361362protected hideActiveComposite(): Composite | undefined {363if (!this.activeComposite) {364return undefined; // Nothing to do365}366367const composite = this.activeComposite;368this.activeComposite = undefined;369370const compositeContainer = this.mapCompositeToCompositeContainer.get(composite.getId());371372// Indicate to Composite373composite.setVisible(false);374375// Take Container Off-DOM and hide376if (compositeContainer) {377compositeContainer.remove();378hide(compositeContainer);379}380381// Clear any running Progress382this.progressBar?.stop().hide();383384// Empty Actions385if (this.toolBar) {386this.collectCompositeActions()();387}388this.onDidCompositeClose.fire(composite);389390return composite;391}392393protected override createTitleArea(parent: HTMLElement): HTMLElement {394395// Title Area Container396const titleArea = append(parent, $('.composite'));397titleArea.classList.add('title');398399// Left Title Label400this.titleLabel = this.createTitleLabel(titleArea);401402// Right Actions Container403const titleActionsContainer = append(titleArea, $('.title-actions'));404405// Toolbar406this.toolBar = this._register(this.instantiationService.createInstance(WorkbenchToolBar, titleActionsContainer, {407actionViewItemProvider: (action, options) => this.actionViewItemProvider(action, options),408orientation: ActionsOrientation.HORIZONTAL,409getKeyBinding: action => this.keybindingService.lookupKeybinding(action.id),410anchorAlignmentProvider: () => this.getTitleAreaDropDownAnchorAlignment(),411toggleMenuTitle: localize('viewsAndMoreActions', "Views and More Actions..."),412telemetrySource: this.nameForTelemetry,413hoverDelegate: this.toolbarHoverDelegate414}));415416this.collectCompositeActions()();417418return titleArea;419}420421protected createTitleLabel(parent: HTMLElement): ICompositeTitleLabel {422const titleContainer = append(parent, $('.title-label'));423const titleLabel = append(titleContainer, $('h2'));424this.titleLabelElement = titleLabel;425const hover = this._register(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), titleLabel, ''));426427const $this = this;428return {429updateTitle: (id, title, keybinding) => {430// The title label is shared for all composites in the base CompositePart431if (!this.activeComposite || this.activeComposite.getId() === id) {432titleLabel.textContent = title;433hover.update(keybinding ? localize('titleTooltip', "{0} ({1})", title, keybinding) : title);434}435},436437updateStyles: () => {438titleLabel.style.color = $this.titleForegroundColor ? $this.getColor($this.titleForegroundColor) || '' : '';439const borderColor = $this.titleBorderColor ? $this.getColor($this.titleBorderColor) : undefined;440parent.style.borderBottom = borderColor ? `1px solid ${borderColor}` : '';441}442};443}444445protected createHeaderArea(): HTMLElement {446return $('.composite');447}448449protected createFooterArea(): HTMLElement {450return $('.composite');451}452453override updateStyles(): void {454super.updateStyles();455456// Forward to title label457const titleLabel = assertReturnsDefined(this.titleLabel);458titleLabel.updateStyles();459}460461protected actionViewItemProvider(action: IAction, options: IBaseActionViewItemOptions): IActionViewItem | undefined {462463// Check Active Composite464if (this.activeComposite) {465return this.activeComposite.getActionViewItem(action, options);466}467468return createActionViewItem(this.instantiationService, action, options);469}470471protected actionsContextProvider(): unknown {472473// Check Active Composite474if (this.activeComposite) {475return this.activeComposite.getActionsContext();476}477478return null;479}480481protected override createContentArea(parent: HTMLElement): HTMLElement {482const contentContainer = append(parent, $('.content'));483484this.progressBar = this._register(new ProgressBar(contentContainer, defaultProgressBarStyles));485this.progressBar.hide();486487return contentContainer;488}489490getProgressIndicator(id: string): IProgressIndicator | undefined {491const compositeItem = this.instantiatedCompositeItems.get(id);492493return compositeItem ? compositeItem.progress : undefined;494}495496protected getTitleAreaDropDownAnchorAlignment(): AnchorAlignment {497return AnchorAlignment.RIGHT;498}499500override layout(width: number, height: number, top: number, left: number): void {501super.layout(width, height, top, left);502503// Layout contents504this.contentAreaSize = Dimension.lift(super.layoutContents(width, height).contentSize);505506// Layout composite507this.activeComposite?.layout(this.contentAreaSize);508}509510setBoundarySashes?(sashes: IBoundarySashes): void {511this.boundarySashes = sashes;512this.activeComposite?.setBoundarySashes(sashes);513}514515protected removeComposite(compositeId: string): boolean {516if (this.activeComposite?.getId() === compositeId) {517return false; // do not remove active composite518}519520this.mapCompositeToCompositeContainer.delete(compositeId);521this.mapActionsBindingToComposite.delete(compositeId);522const compositeItem = this.instantiatedCompositeItems.get(compositeId);523if (compositeItem) {524compositeItem.composite.dispose();525dispose(compositeItem.disposable);526this.instantiatedCompositeItems.delete(compositeId);527}528529return true;530}531532override dispose(): void {533this.mapCompositeToCompositeContainer.clear();534this.mapActionsBindingToComposite.clear();535536this.instantiatedCompositeItems.forEach(compositeItem => {537compositeItem.composite.dispose();538dispose(compositeItem.disposable);539});540541this.instantiatedCompositeItems.clear();542543super.dispose();544}545}546547548