Path: blob/main/src/vs/workbench/browser/parts/compositeBar.ts
5263 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 { localize } from '../../../nls.js';6import { IAction, toAction } from '../../../base/common/actions.js';7import { IActivity } from '../../services/activity/common/activity.js';8import { IInstantiationService } from '../../../platform/instantiation/common/instantiation.js';9import { ActionBar, ActionsOrientation } from '../../../base/browser/ui/actionbar/actionbar.js';10import { CompositeActionViewItem, CompositeOverflowActivityAction, CompositeOverflowActivityActionViewItem, CompositeBarAction, ICompositeBar, ICompositeBarColors, IActivityHoverOptions } from './compositeBarActions.js';11import { Dimension, $, addDisposableListener, EventType, EventHelper, isAncestor, getWindow } from '../../../base/browser/dom.js';12import { StandardMouseEvent } from '../../../base/browser/mouseEvent.js';13import { IContextMenuService } from '../../../platform/contextview/browser/contextView.js';14import { Widget } from '../../../base/browser/ui/widget.js';15import { isUndefinedOrNull } from '../../../base/common/types.js';16import { IColorTheme } from '../../../platform/theme/common/themeService.js';17import { Emitter } from '../../../base/common/event.js';18import { ViewContainerLocation, IViewDescriptorService } from '../../common/views.js';19import { IPaneComposite } from '../../common/panecomposite.js';20import { IComposite } from '../../common/composite.js';21import { CompositeDragAndDropData, CompositeDragAndDropObserver, IDraggedCompositeData, ICompositeDragAndDrop, Before2D, toggleDropEffect, ICompositeDragAndDropObserverCallbacks } from '../dnd.js';22import { Gesture, EventType as TouchEventType, GestureEvent } from '../../../base/browser/touch.js';23import { MutableDisposable } from '../../../base/common/lifecycle.js';2425export interface ICompositeBarItem {2627readonly id: string;2829name?: string;30pinned: boolean;31order?: number;32visible: boolean;33}3435export class CompositeDragAndDrop implements ICompositeDragAndDrop {3637constructor(38private viewDescriptorService: IViewDescriptorService,39private targetContainerLocation: ViewContainerLocation,40private orientation: ActionsOrientation,41private openComposite: (id: string, focus?: boolean) => Promise<IPaneComposite | null>,42private moveComposite: (from: string, to: string, before?: Before2D) => void,43private getItems: () => ICompositeBarItem[]44) { }4546drop(data: CompositeDragAndDropData, targetCompositeId: string | undefined, originalEvent: DragEvent, before?: Before2D): void {47const dragData = data.getData();4849if (dragData.type === 'composite') {50const currentContainer = this.viewDescriptorService.getViewContainerById(dragData.id)!;51const currentLocation = this.viewDescriptorService.getViewContainerLocation(currentContainer);52let moved = false;5354// ... on the same composite bar55if (currentLocation === this.targetContainerLocation) {56if (targetCompositeId) {57this.moveComposite(dragData.id, targetCompositeId, before);58moved = true;59}60}61// ... on a different composite bar62else {63this.viewDescriptorService.moveViewContainerToLocation(currentContainer, this.targetContainerLocation, this.getTargetIndex(targetCompositeId, before), 'dnd');64moved = true;65}6667if (moved) {68this.openComposite(currentContainer.id, true);69}70}7172if (dragData.type === 'view') {73const viewToMove = this.viewDescriptorService.getViewDescriptorById(dragData.id)!;74if (viewToMove.canMoveView) {75this.viewDescriptorService.moveViewToLocation(viewToMove, this.targetContainerLocation, 'dnd');7677const newContainer = this.viewDescriptorService.getViewContainerByViewId(viewToMove.id)!;7879if (targetCompositeId) {80this.moveComposite(newContainer.id, targetCompositeId, before);81}8283this.openComposite(newContainer.id, true).then(composite => {84composite?.openView(viewToMove.id, true);85});86}87}88}8990onDragEnter(data: CompositeDragAndDropData, targetCompositeId: string | undefined, originalEvent: DragEvent): boolean {91return this.canDrop(data, targetCompositeId);92}9394onDragOver(data: CompositeDragAndDropData, targetCompositeId: string | undefined, originalEvent: DragEvent): boolean {95return this.canDrop(data, targetCompositeId);96}9798private getTargetIndex(targetId: string | undefined, before2d: Before2D | undefined): number | undefined {99if (!targetId) {100return undefined;101}102103const items = this.getItems();104const before = this.orientation === ActionsOrientation.HORIZONTAL ? before2d?.horizontallyBefore : before2d?.verticallyBefore;105return items.filter(item => item.visible).findIndex(item => item.id === targetId) + (before ? 0 : 1);106}107108private canDrop(data: CompositeDragAndDropData, targetCompositeId: string | undefined): boolean {109const dragData = data.getData();110111if (dragData.type === 'composite') {112113// Dragging a composite114const currentContainer = this.viewDescriptorService.getViewContainerById(dragData.id)!;115const currentLocation = this.viewDescriptorService.getViewContainerLocation(currentContainer);116117// ... to the same composite location118if (currentLocation === this.targetContainerLocation) {119return dragData.id !== targetCompositeId;120}121122return true;123} else {124125// Dragging an individual view126const viewDescriptor = this.viewDescriptorService.getViewDescriptorById(dragData.id);127128// ... that cannot move129if (!viewDescriptor?.canMoveView) {130return false;131}132133// ... to create a view container134return true;135}136}137}138139export interface ICompositeBarOptions {140141readonly icon: boolean;142readonly orientation: ActionsOrientation;143readonly colors: (theme: IColorTheme) => ICompositeBarColors;144readonly compact?: boolean;145readonly compositeSize: number;146readonly overflowActionSize: number;147readonly dndHandler: ICompositeDragAndDrop;148readonly activityHoverOptions: IActivityHoverOptions;149readonly preventLoopNavigation?: boolean;150151readonly getActivityAction: (compositeId: string) => CompositeBarAction;152readonly getCompositePinnedAction: (compositeId: string) => IAction;153readonly getCompositeBadgeAction: (compositeId: string) => IAction;154readonly getOnCompositeClickAction: (compositeId: string) => IAction;155readonly fillExtraContextMenuActions: (actions: IAction[], e?: MouseEvent | GestureEvent) => void;156readonly getContextMenuActionsForComposite: (compositeId: string) => IAction[];157158readonly openComposite: (compositeId: string, preserveFocus?: boolean) => Promise<IComposite | null>;159readonly getDefaultCompositeId: () => string | undefined;160}161162class CompositeBarDndCallbacks implements ICompositeDragAndDropObserverCallbacks {163164private insertDropBefore: Before2D | undefined = undefined;165166constructor(167private readonly compositeBarContainer: HTMLElement,168private readonly actionBarContainer: HTMLElement,169private readonly compositeBarModel: CompositeBarModel,170private readonly dndHandler: ICompositeDragAndDrop,171private readonly orientation: ActionsOrientation,172) { }173174onDragOver(e: IDraggedCompositeData) {175176// don't add feedback if this is over the composite bar actions or there are no actions177const visibleItems = this.compositeBarModel.visibleItems;178if (!visibleItems.length || (e.eventData.target && isAncestor(e.eventData.target as HTMLElement, this.actionBarContainer))) {179this.insertDropBefore = this.updateFromDragging(this.compositeBarContainer, false, false, true);180return;181}182183const insertAtFront = this.insertAtFront(this.actionBarContainer, e.eventData);184const target = insertAtFront ? visibleItems[0] : visibleItems[visibleItems.length - 1];185const validDropTarget = this.dndHandler.onDragOver(e.dragAndDropData, target.id, e.eventData);186toggleDropEffect(e.eventData.dataTransfer, 'move', validDropTarget);187this.insertDropBefore = this.updateFromDragging(this.compositeBarContainer, validDropTarget, insertAtFront, true);188}189190onDragLeave(e: IDraggedCompositeData) {191this.insertDropBefore = this.updateFromDragging(this.compositeBarContainer, false, false, false);192}193194onDragEnd(e: IDraggedCompositeData) {195this.insertDropBefore = this.updateFromDragging(this.compositeBarContainer, false, false, false);196}197198onDrop(e: IDraggedCompositeData) {199const visibleItems = this.compositeBarModel.visibleItems;200let targetId = undefined;201if (visibleItems.length) {202targetId = this.insertAtFront(this.actionBarContainer, e.eventData) ? visibleItems[0].id : visibleItems[visibleItems.length - 1].id;203}204this.dndHandler.drop(e.dragAndDropData, targetId, e.eventData, this.insertDropBefore);205this.insertDropBefore = this.updateFromDragging(this.compositeBarContainer, false, false, false);206}207208private insertAtFront(element: HTMLElement, event: DragEvent): boolean {209const rect = element.getBoundingClientRect();210const posX = event.clientX;211const posY = event.clientY;212213switch (this.orientation) {214case ActionsOrientation.HORIZONTAL:215return posX < rect.left;216case ActionsOrientation.VERTICAL:217return posY < rect.top;218}219}220221private updateFromDragging(element: HTMLElement, showFeedback: boolean, front: boolean, isDragging: boolean): Before2D | undefined {222element.classList.toggle('dragged-over', isDragging);223element.classList.toggle('dragged-over-head', showFeedback && front);224element.classList.toggle('dragged-over-tail', showFeedback && !front);225226if (!showFeedback) {227return undefined;228}229230return { verticallyBefore: front, horizontallyBefore: front };231}232}233234export class CompositeBar extends Widget implements ICompositeBar {235236private readonly _onDidChange = this._register(new Emitter<void>());237readonly onDidChange = this._onDidChange.event;238239private dimension: Dimension | undefined;240241private compositeSwitcherBar: ActionBar | undefined;242private compositeOverflowAction = this._register(new MutableDisposable<CompositeOverflowActivityAction>());243private compositeOverflowActionViewItem = this._register(new MutableDisposable<CompositeOverflowActivityActionViewItem>());244245private readonly model: CompositeBarModel;246private readonly visibleComposites: string[];247private readonly compositeSizeInBar: Map<string, number>;248249constructor(250items: ICompositeBarItem[],251private readonly options: ICompositeBarOptions,252@IInstantiationService private readonly instantiationService: IInstantiationService,253@IContextMenuService private readonly contextMenuService: IContextMenuService,254@IViewDescriptorService private readonly viewDescriptorService: IViewDescriptorService,255) {256super();257258this.model = new CompositeBarModel(items, options);259this.visibleComposites = [];260this.compositeSizeInBar = new Map<string, number>();261this.computeSizes(this.model.visibleItems);262}263264getCompositeBarItems(): ICompositeBarItem[] {265return [...this.model.items];266}267268setCompositeBarItems(items: ICompositeBarItem[]): void {269this.model.setItems(items);270this.updateCompositeSwitcher(true);271}272273getPinnedComposites(): ICompositeBarItem[] {274return this.model.pinnedItems;275}276277getPinnedCompositeIds(): string[] {278return this.getPinnedComposites().map(c => c.id);279}280281getVisibleComposites(): ICompositeBarItem[] {282return this.model.visibleItems;283}284285create(parent: HTMLElement): HTMLElement {286const actionBarDiv = parent.appendChild($('.composite-bar'));287this.compositeSwitcherBar = this._register(new ActionBar(actionBarDiv, {288actionViewItemProvider: (action, options) => {289if (action instanceof CompositeOverflowActivityAction) {290return this.compositeOverflowActionViewItem.value;291}292const item = this.model.findItem(action.id);293return item && this.instantiationService.createInstance(294CompositeActionViewItem,295{ ...options, draggable: true, colors: this.options.colors, icon: this.options.icon, hoverOptions: this.options.activityHoverOptions, compact: this.options.compact },296action as CompositeBarAction,297item.pinnedAction,298item.toggleBadgeAction,299compositeId => this.options.getContextMenuActionsForComposite(compositeId),300() => this.getContextMenuActions(),301this.options.dndHandler,302this303);304},305orientation: this.options.orientation,306ariaLabel: localize('activityBarAriaLabel', "Active View Switcher"),307ariaRole: 'tablist',308preventLoopNavigation: this.options.preventLoopNavigation,309triggerKeys: { keyDown: true }310}));311312// Contextmenu for composites313this._register(addDisposableListener(parent, EventType.CONTEXT_MENU, e => this.showContextMenu(getWindow(parent), e)));314this._register(Gesture.addTarget(parent));315this._register(addDisposableListener(parent, TouchEventType.Contextmenu, e => this.showContextMenu(getWindow(parent), e)));316317// Register a drop target on the whole bar to prevent forbidden feedback318const dndCallback = new CompositeBarDndCallbacks(parent, actionBarDiv, this.model, this.options.dndHandler, this.options.orientation);319this._register(CompositeDragAndDropObserver.INSTANCE.registerTarget(parent, dndCallback));320321return actionBarDiv;322}323324focus(index?: number): void {325this.compositeSwitcherBar?.focus(index);326}327328recomputeSizes(): void {329this.computeSizes(this.model.visibleItems);330this.updateCompositeSwitcher();331}332333layout(dimension: Dimension): void {334this.dimension = dimension;335336if (dimension.height === 0 || dimension.width === 0) {337// Do not layout if not visible. Otherwise the size measurment would be computed wrongly338return;339}340341if (this.compositeSizeInBar.size === 0) {342// Compute size of each composite by getting the size from the css renderer343// Size is later used for overflow computation344this.computeSizes(this.model.visibleItems);345}346347this.updateCompositeSwitcher();348}349350addComposite({ id, name, order, requestedIndex }: { id: string; name: string; order?: number; requestedIndex?: number }): void {351if (this.model.add(id, name, order, requestedIndex)) {352this.computeSizes([this.model.findItem(id)]);353this.updateCompositeSwitcher();354}355}356357removeComposite(id: string): void {358359// If it pinned, unpin it first360if (this.isPinned(id)) {361this.unpin(id);362}363364// Remove from the model365if (this.model.remove(id)) {366this.updateCompositeSwitcher();367}368}369370hideComposite(id: string): void {371if (this.model.hide(id)) {372this.resetActiveComposite(id);373this.updateCompositeSwitcher();374}375}376377activateComposite(id: string): void {378const previousActiveItem = this.model.activeItem;379if (this.model.activate(id)) {380// Update if current composite is neither visible nor pinned381// or previous active composite is not pinned382if (this.visibleComposites.indexOf(id) === - 1 || (!!this.model.activeItem && !this.model.activeItem.pinned) || (previousActiveItem && !previousActiveItem.pinned)) {383this.updateCompositeSwitcher();384}385}386}387388deactivateComposite(id: string): void {389const previousActiveItem = this.model.activeItem;390if (this.model.deactivate()) {391if (previousActiveItem && !previousActiveItem.pinned) {392this.updateCompositeSwitcher();393}394}395}396397async pin(compositeId: string, open?: boolean): Promise<void> {398if (this.model.setPinned(compositeId, true)) {399this.updateCompositeSwitcher();400401if (open) {402await this.options.openComposite(compositeId);403this.activateComposite(compositeId); // Activate after opening404}405}406}407408unpin(compositeId: string): void {409if (this.model.setPinned(compositeId, false)) {410411this.updateCompositeSwitcher();412413this.resetActiveComposite(compositeId);414}415}416417areBadgesEnabled(compositeId: string): boolean {418return this.viewDescriptorService.getViewContainerBadgeEnablementState(compositeId);419}420421toggleBadgeEnablement(compositeId: string): void {422this.viewDescriptorService.setViewContainerBadgeEnablementState(compositeId, !this.areBadgesEnabled(compositeId));423this.updateCompositeSwitcher();424const item = this.model.findItem(compositeId);425if (item) {426// TODO @lramos15 how do we tell the activity to re-render the badge? This triggers an onDidChange but isn't the right way to do it.427// I could add another specific function like `activity.updateBadgeEnablement` would then the activity store the sate?428item.activityAction.activities = item.activityAction.activities;429}430}431432private resetActiveComposite(compositeId: string) {433const defaultCompositeId = this.options.getDefaultCompositeId();434435// Case: composite is not the active one or the active one is a different one436// Solv: we do nothing437if (!this.model.activeItem || this.model.activeItem.id !== compositeId) {438return;439}440441// Deactivate itself442this.deactivateComposite(compositeId);443444// Case: composite is not the default composite and default composite is still showing445// Solv: we open the default composite446if (defaultCompositeId && defaultCompositeId !== compositeId && this.isPinned(defaultCompositeId)) {447this.options.openComposite(defaultCompositeId, true);448}449450// Case: we closed the default composite451// Solv: we open the next visible composite from top452else {453const visibleComposite = this.visibleComposites.find(cid => cid !== compositeId);454if (visibleComposite) {455this.options.openComposite(visibleComposite);456}457}458}459460isPinned(compositeId: string): boolean {461const item = this.model.findItem(compositeId);462return item?.pinned;463}464465move(compositeId: string, toCompositeId: string, before?: boolean): void {466if (before !== undefined) {467const fromIndex = this.model.items.findIndex(c => c.id === compositeId);468let toIndex = this.model.items.findIndex(c => c.id === toCompositeId);469470if (fromIndex >= 0 && toIndex >= 0) {471if (!before && fromIndex > toIndex) {472toIndex++;473}474475if (before && fromIndex < toIndex) {476toIndex--;477}478479if (toIndex < this.model.items.length && toIndex >= 0 && toIndex !== fromIndex) {480if (this.model.move(this.model.items[fromIndex].id, this.model.items[toIndex].id)) {481// timeout helps to prevent artifacts from showing up482setTimeout(() => this.updateCompositeSwitcher(), 0);483}484}485}486} else {487if (this.model.move(compositeId, toCompositeId)) {488// timeout helps to prevent artifacts from showing up489setTimeout(() => this.updateCompositeSwitcher(), 0);490}491}492}493494getAction(compositeId: string): CompositeBarAction {495const item = this.model.findItem(compositeId);496497return item?.activityAction;498}499500private computeSizes(items: ICompositeBarModelItem[]): void {501const size = this.options.compositeSize;502if (size) {503items.forEach(composite => this.compositeSizeInBar.set(composite.id, size));504} else {505const compositeSwitcherBar = this.compositeSwitcherBar;506if (compositeSwitcherBar && this.dimension && this.dimension.height !== 0 && this.dimension.width !== 0) {507508// Compute sizes only if visible. Otherwise the size measurment would be computed wrongly.509const currentItemsLength = compositeSwitcherBar.viewItems.length;510compositeSwitcherBar.push(items.map(composite => composite.activityAction));511items.map((composite, index) => this.compositeSizeInBar.set(composite.id, this.options.orientation === ActionsOrientation.VERTICAL512? compositeSwitcherBar.getHeight(currentItemsLength + index)513: compositeSwitcherBar.getWidth(currentItemsLength + index)514));515items.forEach(() => compositeSwitcherBar.pull(compositeSwitcherBar.viewItems.length - 1));516}517}518}519520private updateCompositeSwitcher(donotTrigger?: boolean): void {521const compositeSwitcherBar = this.compositeSwitcherBar;522if (!compositeSwitcherBar || !this.dimension) {523return; // We have not been rendered yet so there is nothing to update.524}525526let compositesToShow = this.model.visibleItems.filter(item =>527item.pinned528|| (this.model.activeItem && this.model.activeItem.id === item.id) /* Show the active composite even if it is not pinned */529).map(item => item.id);530531// Ensure we are not showing more composites than we have height for532let maxVisible = compositesToShow.length;533const totalComposites = compositesToShow.length;534let size = 0;535const limit = this.options.orientation === ActionsOrientation.VERTICAL ? this.dimension.height : this.dimension.width;536537// Add composites while they fit538for (let i = 0; i < compositesToShow.length; i++) {539const compositeSize = this.compositeSizeInBar.get(compositesToShow[i])!;540// Adding this composite will overflow available size, so don't541if (size + compositeSize > limit) {542maxVisible = i;543break;544}545546size += compositeSize;547}548549// Remove the tail of composites that did not fit550if (totalComposites > maxVisible) {551compositesToShow = compositesToShow.slice(0, maxVisible);552}553554// We always try show the active composite, so re-add it if it was sliced out555if (this.model.activeItem && compositesToShow.every(compositeId => !!this.model.activeItem && compositeId !== this.model.activeItem.id)) {556size += this.compositeSizeInBar.get(this.model.activeItem.id)!;557compositesToShow.push(this.model.activeItem.id);558}559560// The active composite might have pushed us over the limit561// Keep popping the composite before the active one until it fits562// If even the active one doesn't fit, we will resort to overflow563while (size > limit && compositesToShow.length) {564const removedComposite = compositesToShow.length > 1 ? compositesToShow.splice(compositesToShow.length - 2, 1)[0] : compositesToShow.pop();565size -= this.compositeSizeInBar.get(removedComposite!)!;566}567568// We are overflowing, add the overflow size569if (totalComposites > compositesToShow.length) {570size += this.options.overflowActionSize;571}572573// Check if we need to make extra room for the overflow action574while (size > limit && compositesToShow.length) {575const removedComposite = compositesToShow.length > 1 && compositesToShow[compositesToShow.length - 1] === this.model.activeItem?.id ?576compositesToShow.splice(compositesToShow.length - 2, 1)[0] : compositesToShow.pop();577size -= this.compositeSizeInBar.get(removedComposite!)!;578}579580// Remove the overflow action if there are no overflows581if (totalComposites === compositesToShow.length && this.compositeOverflowAction.value) {582compositeSwitcherBar.pull(compositeSwitcherBar.length() - 1);583584this.compositeOverflowAction.value = undefined;585this.compositeOverflowActionViewItem.value = undefined;586}587588// Pull out composites that overflow or got hidden589const compositesToRemove: number[] = [];590this.visibleComposites.forEach((compositeId, index) => {591if (!compositesToShow.includes(compositeId)) {592compositesToRemove.push(index);593}594});595compositesToRemove.reverse().forEach(index => {596compositeSwitcherBar.pull(index);597this.visibleComposites.splice(index, 1);598});599600// Update the positions of the composites601compositesToShow.forEach((compositeId, newIndex) => {602const currentIndex = this.visibleComposites.indexOf(compositeId);603if (newIndex !== currentIndex) {604if (currentIndex !== -1) {605compositeSwitcherBar.pull(currentIndex);606this.visibleComposites.splice(currentIndex, 1);607}608609compositeSwitcherBar.push(this.model.findItem(compositeId).activityAction, { label: true, icon: this.options.icon, index: newIndex });610this.visibleComposites.splice(newIndex, 0, compositeId);611}612});613614// Add overflow action as needed615if (totalComposites > compositesToShow.length && !this.compositeOverflowAction.value) {616this.compositeOverflowAction.value = this.instantiationService.createInstance(CompositeOverflowActivityAction, () => {617this.compositeOverflowActionViewItem.value?.showMenu();618});619this.compositeOverflowActionViewItem.value = this.instantiationService.createInstance(620CompositeOverflowActivityActionViewItem,621this.compositeOverflowAction.value,622() => this.getOverflowingComposites(),623() => this.model.activeItem ? this.model.activeItem.id : undefined,624compositeId => {625const item = this.model.findItem(compositeId);626return item?.activity[0]?.badge;627},628this.options.getOnCompositeClickAction,629this.options.colors,630this.options.activityHoverOptions631);632633compositeSwitcherBar.push(this.compositeOverflowAction.value, { label: false, icon: true });634}635636if (!donotTrigger) {637this._onDidChange.fire();638}639}640641private getOverflowingComposites(): { id: string; name?: string }[] {642let overflowingIds = this.model.visibleItems.filter(item => item.pinned).map(item => item.id);643644// Show the active composite even if it is not pinned645if (this.model.activeItem && !this.model.activeItem.pinned) {646overflowingIds.push(this.model.activeItem.id);647}648649overflowingIds = overflowingIds.filter(compositeId => !this.visibleComposites.includes(compositeId));650return this.model.visibleItems.filter(c => overflowingIds.includes(c.id)).map(item => { return { id: item.id, name: this.getAction(item.id)?.label || item.name }; });651}652653private showContextMenu(targetWindow: Window, e: MouseEvent | GestureEvent): void {654EventHelper.stop(e, true);655656const event = new StandardMouseEvent(targetWindow, e);657this.contextMenuService.showContextMenu({658getAnchor: () => event,659getActions: () => this.getContextMenuActions(e)660});661}662663getContextMenuActions(e?: MouseEvent | GestureEvent): IAction[] {664const actions: IAction[] = this.model.visibleItems665.map(({ id, name, activityAction }) => {666const isPinned = this.isPinned(id);667return toAction({668id,669label: this.getAction(id).label || name || id,670checked: isPinned,671enabled: activityAction.enabled && (!isPinned || this.getPinnedCompositeIds().length > 1),672run: () => {673if (this.isPinned(id)) {674this.unpin(id);675} else {676this.pin(id, true);677}678}679});680});681682this.options.fillExtraContextMenuActions(actions, e);683684return actions;685}686}687688interface ICompositeBarModelItem extends ICompositeBarItem {689readonly activityAction: CompositeBarAction;690readonly pinnedAction: IAction;691readonly toggleBadgeAction: IAction;692readonly activity: IActivity[];693}694695class CompositeBarModel {696697private _items: ICompositeBarModelItem[] = [];698get items(): ICompositeBarModelItem[] { return this._items; }699700private readonly options: ICompositeBarOptions;701702activeItem?: ICompositeBarModelItem;703704constructor(705items: ICompositeBarItem[],706options: ICompositeBarOptions707) {708this.options = options;709this.setItems(items);710}711712setItems(items: ICompositeBarItem[]): void {713this._items = [];714this._items = items715.map(i => this.createCompositeBarItem(i.id, i.name, i.order, i.pinned, i.visible));716}717718get visibleItems(): ICompositeBarModelItem[] {719return this.items.filter(item => item.visible);720}721722get pinnedItems(): ICompositeBarModelItem[] {723return this.items.filter(item => item.visible && item.pinned);724}725726private createCompositeBarItem(id: string, name: string | undefined, order: number | undefined, pinned: boolean, visible: boolean): ICompositeBarModelItem {727const options = this.options;728return {729id, name, pinned, order, visible,730activity: [],731get activityAction() {732return options.getActivityAction(id);733},734get pinnedAction() {735return options.getCompositePinnedAction(id);736},737get toggleBadgeAction() {738return options.getCompositeBadgeAction(id);739}740};741}742743add(id: string, name: string, order: number | undefined, requestedIndex: number | undefined): boolean {744const item = this.findItem(id);745if (item) {746let changed = false;747item.name = name;748if (!isUndefinedOrNull(order)) {749changed = item.order !== order;750item.order = order;751}752if (!item.visible) {753item.visible = true;754changed = true;755}756757return changed;758} else {759const item = this.createCompositeBarItem(id, name, order, true, true);760if (!isUndefinedOrNull(requestedIndex)) {761let index = 0;762let rIndex = requestedIndex;763while (rIndex > 0 && index < this.items.length) {764if (this.items[index++].visible) {765rIndex--;766}767}768769this.items.splice(index, 0, item);770} else if (isUndefinedOrNull(order)) {771this.items.push(item);772} else {773let index = 0;774while (index < this.items.length && typeof this.items[index].order === 'number' && this.items[index].order! < order) {775index++;776}777this.items.splice(index, 0, item);778}779780return true;781}782}783784remove(id: string): boolean {785for (let index = 0; index < this.items.length; index++) {786if (this.items[index].id === id) {787this.items.splice(index, 1);788return true;789}790}791return false;792}793794hide(id: string): boolean {795for (const item of this.items) {796if (item.id === id) {797if (item.visible) {798item.visible = false;799return true;800}801return false;802}803}804return false;805}806807move(compositeId: string, toCompositeId: string): boolean {808809const fromIndex = this.findIndex(compositeId);810const toIndex = this.findIndex(toCompositeId);811812// Make sure both items are known to the model813if (fromIndex === -1 || toIndex === -1) {814return false;815}816817const sourceItem = this.items.splice(fromIndex, 1)[0];818this.items.splice(toIndex, 0, sourceItem);819820// Make sure a moved composite gets pinned821sourceItem.pinned = true;822823return true;824}825826setPinned(id: string, pinned: boolean): boolean {827for (const item of this.items) {828if (item.id === id) {829if (item.pinned !== pinned) {830item.pinned = pinned;831return true;832}833return false;834}835}836return false;837}838839activate(id: string): boolean {840if (!this.activeItem || this.activeItem.id !== id) {841if (this.activeItem) {842this.deactivate();843}844for (const item of this.items) {845if (item.id === id) {846this.activeItem = item;847this.activeItem.activityAction.activate();848return true;849}850}851}852return false;853}854855deactivate(): boolean {856if (this.activeItem) {857this.activeItem.activityAction.deactivate();858this.activeItem = undefined;859return true;860}861return false;862}863864findItem(id: string): ICompositeBarModelItem {865return this.items.filter(item => item.id === id)[0];866}867868private findIndex(id: string): number {869for (let index = 0; index < this.items.length; index++) {870if (this.items[index].id === id) {871return index;872}873}874875return -1;876}877}878879880