Path: blob/main/src/vs/workbench/browser/parts/compositePart.ts
5259 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 interface ICompositePartOptions extends IPartOptions {59readonly trailingSeparator?: boolean;60}6162export abstract class CompositePart<T extends Composite, MementoType extends object = object> extends Part<MementoType> {6364protected readonly onDidCompositeOpen = this._register(new Emitter<{ composite: IComposite; focus: boolean }>());65protected readonly onDidCompositeClose = this._register(new Emitter<IComposite>());6667protected toolBar: WorkbenchToolBar | undefined;68protected titleLabelElement: HTMLElement | undefined;69protected readonly toolbarHoverDelegate: IHoverDelegate;7071private readonly mapCompositeToCompositeContainer = new Map<string, HTMLElement>();72private readonly mapActionsBindingToComposite = new Map<string, () => void>();73private activeComposite: Composite | undefined;74private lastActiveCompositeId: string;75private readonly instantiatedCompositeItems = new Map<string, CompositeItem>();76protected titleLabel: ICompositeTitleLabel | undefined;77private progressBar: ProgressBar | undefined;78private contentAreaSize: Dimension | undefined;79private readonly actionsListener = this._register(new MutableDisposable());80private currentCompositeOpenToken: string | undefined;81private boundarySashes: IBoundarySashes | undefined;82private readonly trailingSeparator: boolean;8384constructor(85private readonly notificationService: INotificationService,86protected readonly storageService: IStorageService,87protected readonly contextMenuService: IContextMenuService,88layoutService: IWorkbenchLayoutService,89protected readonly keybindingService: IKeybindingService,90private readonly hoverService: IHoverService,91protected readonly instantiationService: IInstantiationService,92themeService: IThemeService,93protected readonly registry: CompositeRegistry<T>,94private readonly activeCompositeSettingsKey: string,95private readonly defaultCompositeId: string,96protected readonly nameForTelemetry: string,97private readonly compositeCSSClass: string,98private readonly titleForegroundColor: string | undefined,99private readonly titleBorderColor: string | undefined,100id: string,101options: ICompositePartOptions102) {103super(id, options, themeService, storageService, layoutService);104105this.lastActiveCompositeId = storageService.get(activeCompositeSettingsKey, StorageScope.WORKSPACE, this.defaultCompositeId);106this.toolbarHoverDelegate = this._register(createInstantHoverDelegate());107this.trailingSeparator = options.trailingSeparator ?? false;108}109110protected openComposite(id: string, focus?: boolean): Composite | undefined {111112// Check if composite already visible and just focus in that case113if (this.activeComposite?.getId() === id) {114if (focus) {115this.activeComposite.focus();116}117118// Fullfill promise with composite that is being opened119return this.activeComposite;120}121122// We cannot open the composite if we have not been created yet123if (!this.element) {124return;125}126127// Open128return this.doOpenComposite(id, focus);129}130131private doOpenComposite(id: string, focus: boolean = false): Composite | undefined {132133// Use a generated token to avoid race conditions from long running promises134const currentCompositeOpenToken = defaultGenerator.nextId();135this.currentCompositeOpenToken = currentCompositeOpenToken;136137// Hide current138if (this.activeComposite) {139this.hideActiveComposite();140}141142// Update Title143this.updateTitle(id);144145// Create composite146const composite = this.createComposite(id, true);147148// Check if another composite opened meanwhile and return in that case149if ((this.currentCompositeOpenToken !== currentCompositeOpenToken) || (this.activeComposite && this.activeComposite.getId() !== composite.getId())) {150return undefined;151}152153// Check if composite already visible and just focus in that case154if (this.activeComposite?.getId() === composite.getId()) {155if (focus) {156composite.focus();157}158159this.onDidCompositeOpen.fire({ composite, focus });160return composite;161}162163// Show Composite and Focus164this.showComposite(composite);165if (focus) {166composite.focus();167}168169// Return with the composite that is being opened170if (composite) {171this.onDidCompositeOpen.fire({ composite, focus });172}173174return composite;175}176177protected createComposite(id: string, isActive?: boolean): Composite {178179// Check if composite is already created180const compositeItem = this.instantiatedCompositeItems.get(id);181if (compositeItem) {182return compositeItem.composite;183}184185// Instantiate composite from registry otherwise186const compositeDescriptor = this.registry.getComposite(id);187if (compositeDescriptor) {188const that = this;189const compositeProgressIndicator = new ScopedProgressIndicator(assertReturnsDefined(this.progressBar), this._register(new class extends AbstractProgressScope {190constructor() {191super(compositeDescriptor!.id, !!isActive);192this._register(that.onDidCompositeOpen.event(e => this.onScopeOpened(e.composite.getId())));193this._register(that.onDidCompositeClose.event(e => this.onScopeClosed(e.getId())));194}195}()));196const compositeInstantiationService = this._register(this.instantiationService.createChild(new ServiceCollection(197[IEditorProgressService, compositeProgressIndicator] // provide the editor progress service for any editors instantiated within the composite198)));199200const composite = compositeDescriptor.instantiate(compositeInstantiationService);201const disposable = new DisposableStore();202203// Remember as Instantiated204this.instantiatedCompositeItems.set(id, { composite, disposable, progress: compositeProgressIndicator });205206// Register to title area update events from the composite207disposable.add(composite.onTitleAreaUpdate(() => this.onTitleAreaUpdate(composite.getId()), this));208disposable.add(compositeInstantiationService);209210return composite;211}212213throw new Error(`Unable to find composite with id ${id}`);214}215216protected showComposite(composite: Composite): void {217218// Remember Composite219this.activeComposite = composite;220221// Store in preferences222const id = this.activeComposite.getId();223if (id !== this.defaultCompositeId) {224this.storageService.store(this.activeCompositeSettingsKey, id, StorageScope.WORKSPACE, StorageTarget.MACHINE);225} else {226this.storageService.remove(this.activeCompositeSettingsKey, StorageScope.WORKSPACE);227}228229// Remember230this.lastActiveCompositeId = this.activeComposite.getId();231232// Composites created for the first time233let compositeContainer = this.mapCompositeToCompositeContainer.get(composite.getId());234if (!compositeContainer) {235236// Build Container off-DOM237compositeContainer = $('.composite');238compositeContainer.classList.add(...this.compositeCSSClass.split(' '));239compositeContainer.id = composite.getId();240241composite.create(compositeContainer);242composite.updateStyles();243244// Remember composite container245this.mapCompositeToCompositeContainer.set(composite.getId(), compositeContainer);246}247248// Fill Content and Actions249// Make sure that the user meanwhile did not open another composite or closed the part containing the composite250if (!this.activeComposite || composite.getId() !== this.activeComposite.getId()) {251return undefined;252}253254// Take Composite on-DOM and show255this.contentArea?.appendChild(compositeContainer);256show(compositeContainer);257258// Setup action runner259const toolBar = assertReturnsDefined(this.toolBar);260toolBar.actionRunner = composite.getActionRunner();261262// Update title with composite title if it differs from descriptor263const descriptor = this.registry.getComposite(composite.getId());264if (descriptor && descriptor.name !== composite.getTitle()) {265this.updateTitle(composite.getId(), composite.getTitle());266}267268// Handle Composite Actions269let actionsBinding = this.mapActionsBindingToComposite.get(composite.getId());270if (!actionsBinding) {271actionsBinding = this.collectCompositeActions(composite);272this.mapActionsBindingToComposite.set(composite.getId(), actionsBinding);273}274actionsBinding();275276// Action Run Handling277this.actionsListener.value = toolBar.actionRunner.onDidRun(e => {278279// Check for Error280if (e.error && !isCancellationError(e.error)) {281this.notificationService.error(e.error);282}283});284285// Indicate to composite that it is now visible286composite.setVisible(true);287288// Make sure that the user meanwhile did not open another composite or closed the part containing the composite289if (!this.activeComposite || composite.getId() !== this.activeComposite.getId()) {290return;291}292293// Make sure the composite is layed out294if (this.contentAreaSize) {295composite.layout(this.contentAreaSize);296}297298// Make sure boundary sashes are propagated299if (this.boundarySashes) {300composite.setBoundarySashes(this.boundarySashes);301}302}303304protected onTitleAreaUpdate(compositeId: string): void {305306// Title307const composite = this.instantiatedCompositeItems.get(compositeId);308if (composite) {309this.updateTitle(compositeId, composite.composite.getTitle());310}311312// Active Composite313if (this.activeComposite?.getId() === compositeId) {314// Actions315const actionsBinding = this.collectCompositeActions(this.activeComposite);316this.mapActionsBindingToComposite.set(this.activeComposite.getId(), actionsBinding);317actionsBinding();318}319320// Otherwise invalidate actions binding for next time when the composite becomes visible321else {322this.mapActionsBindingToComposite.delete(compositeId);323}324}325326private updateTitle(compositeId: string, compositeTitle?: string): void {327const compositeDescriptor = this.registry.getComposite(compositeId);328if (!compositeDescriptor || !this.titleLabel) {329return;330}331332if (!compositeTitle) {333compositeTitle = compositeDescriptor.name;334}335336const keybinding = this.keybindingService.lookupKeybinding(compositeId);337338this.titleLabel.updateTitle(compositeId, compositeTitle, keybinding?.getLabel() ?? undefined);339340const toolBar = assertReturnsDefined(this.toolBar);341toolBar.setAriaLabel(localize('ariaCompositeToolbarLabel', "{0} actions", compositeTitle));342}343344private collectCompositeActions(composite?: Composite): () => void {345346// From Composite347const menuIds = composite?.getMenuIds();348const primaryActions: IAction[] = composite?.getActions().slice(0) || [];349const secondaryActions: IAction[] = composite?.getSecondaryActions().slice(0) || [];350351// Update context352const toolBar = assertReturnsDefined(this.toolBar);353toolBar.context = this.actionsContextProvider();354355// Return fn to set into toolbar356return () => {357toolBar.setActions(prepareActions(primaryActions), prepareActions(secondaryActions), menuIds);358this.titleArea?.classList.toggle('has-actions', primaryActions.length > 0 || secondaryActions.length > 0);359};360}361362protected getActiveComposite(): IComposite | undefined {363return this.activeComposite;364}365366protected getLastActiveCompositeId(): string {367return this.lastActiveCompositeId;368}369370protected hideActiveComposite(): Composite | undefined {371if (!this.activeComposite) {372return undefined; // Nothing to do373}374375const composite = this.activeComposite;376this.activeComposite = undefined;377378const compositeContainer = this.mapCompositeToCompositeContainer.get(composite.getId());379380// Indicate to Composite381composite.setVisible(false);382383// Take Container Off-DOM and hide384if (compositeContainer) {385compositeContainer.remove();386hide(compositeContainer);387}388389// Clear any running Progress390this.progressBar?.stop().hide();391392// Empty Actions393if (this.toolBar) {394this.collectCompositeActions()();395}396this.onDidCompositeClose.fire(composite);397398return composite;399}400401protected override createTitleArea(parent: HTMLElement): HTMLElement {402403// Title Area Container404const titleArea = append(parent, $('.composite'));405titleArea.classList.add('title');406407// Left Title Label408this.titleLabel = this.createTitleLabel(titleArea);409410// Right Actions Container411const titleActionsContainer = append(titleArea, $('.title-actions'));412413// Toolbar414this.toolBar = this._register(this.instantiationService.createInstance(WorkbenchToolBar, titleActionsContainer, {415actionViewItemProvider: (action, options) => this.actionViewItemProvider(action, options),416orientation: ActionsOrientation.HORIZONTAL,417getKeyBinding: action => this.keybindingService.lookupKeybinding(action.id),418anchorAlignmentProvider: () => this.getTitleAreaDropDownAnchorAlignment(),419toggleMenuTitle: localize('viewsAndMoreActions', "Views and More Actions..."),420telemetrySource: this.nameForTelemetry,421hoverDelegate: this.toolbarHoverDelegate,422trailingSeparator: this.trailingSeparator,423}));424425this.collectCompositeActions()();426427return titleArea;428}429430protected createTitleLabel(parent: HTMLElement): ICompositeTitleLabel {431const titleContainer = append(parent, $('.title-label'));432const titleLabel = append(titleContainer, $('h2'));433this.titleLabelElement = titleLabel;434const hover = this._register(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), titleLabel, ''));435436const $this = this;437return {438updateTitle: (id, title, keybinding) => {439// The title label is shared for all composites in the base CompositePart440if (!this.activeComposite || this.activeComposite.getId() === id) {441titleLabel.textContent = title;442hover.update(keybinding ? localize('titleTooltip', "{0} ({1})", title, keybinding) : title);443}444},445446updateStyles: () => {447titleLabel.style.color = $this.titleForegroundColor ? $this.getColor($this.titleForegroundColor) || '' : '';448const borderColor = $this.titleBorderColor ? $this.getColor($this.titleBorderColor) : undefined;449parent.style.borderBottom = borderColor ? `1px solid ${borderColor}` : '';450}451};452}453454protected createHeaderArea(): HTMLElement {455return $('.composite');456}457458protected createFooterArea(): HTMLElement {459return $('.composite');460}461462override updateStyles(): void {463super.updateStyles();464465// Forward to title label466const titleLabel = assertReturnsDefined(this.titleLabel);467titleLabel.updateStyles();468}469470protected actionViewItemProvider(action: IAction, options: IBaseActionViewItemOptions): IActionViewItem | undefined {471472// Check Active Composite473if (this.activeComposite) {474return this.activeComposite.getActionViewItem(action, options);475}476477return createActionViewItem(this.instantiationService, action, options);478}479480protected actionsContextProvider(): unknown {481482// Check Active Composite483if (this.activeComposite) {484return this.activeComposite.getActionsContext();485}486487return null;488}489490protected override createContentArea(parent: HTMLElement): HTMLElement {491const contentContainer = append(parent, $('.content'));492493this.progressBar = this._register(new ProgressBar(contentContainer, defaultProgressBarStyles));494this.progressBar.hide();495496return contentContainer;497}498499getProgressIndicator(id: string): IProgressIndicator | undefined {500const compositeItem = this.instantiatedCompositeItems.get(id);501502return compositeItem ? compositeItem.progress : undefined;503}504505protected getTitleAreaDropDownAnchorAlignment(): AnchorAlignment {506return AnchorAlignment.RIGHT;507}508509override layout(width: number, height: number, top: number, left: number): void {510super.layout(width, height, top, left);511512// Layout contents513this.contentAreaSize = Dimension.lift(super.layoutContents(width, height).contentSize);514515// Layout composite516this.activeComposite?.layout(this.contentAreaSize);517}518519setBoundarySashes?(sashes: IBoundarySashes): void {520this.boundarySashes = sashes;521this.activeComposite?.setBoundarySashes(sashes);522}523524protected removeComposite(compositeId: string): boolean {525if (this.activeComposite?.getId() === compositeId) {526return false; // do not remove active composite527}528529this.mapCompositeToCompositeContainer.delete(compositeId);530this.mapActionsBindingToComposite.delete(compositeId);531const compositeItem = this.instantiatedCompositeItems.get(compositeId);532if (compositeItem) {533compositeItem.composite.dispose();534dispose(compositeItem.disposable);535this.instantiatedCompositeItems.delete(compositeId);536}537538return true;539}540541override dispose(): void {542this.mapCompositeToCompositeContainer.clear();543this.mapActionsBindingToComposite.clear();544545this.instantiatedCompositeItems.forEach(compositeItem => {546compositeItem.composite.dispose();547dispose(compositeItem.disposable);548});549550this.instantiatedCompositeItems.clear();551552super.dispose();553}554}555556557