Path: blob/main/src/vs/workbench/browser/parts/compositeBar.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 { 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';2324export interface ICompositeBarItem {2526readonly id: string;2728name?: string;29pinned: boolean;30order?: number;31visible: boolean;32}3334export class CompositeDragAndDrop implements ICompositeDragAndDrop {3536constructor(37private viewDescriptorService: IViewDescriptorService,38private targetContainerLocation: ViewContainerLocation,39private orientation: ActionsOrientation,40private openComposite: (id: string, focus?: boolean) => Promise<IPaneComposite | null>,41private moveComposite: (from: string, to: string, before?: Before2D) => void,42private getItems: () => ICompositeBarItem[]43) { }4445drop(data: CompositeDragAndDropData, targetCompositeId: string | undefined, originalEvent: DragEvent, before?: Before2D): void {46const dragData = data.getData();4748if (dragData.type === 'composite') {49const currentContainer = this.viewDescriptorService.getViewContainerById(dragData.id)!;50const currentLocation = this.viewDescriptorService.getViewContainerLocation(currentContainer);51let moved = false;5253// ... on the same composite bar54if (currentLocation === this.targetContainerLocation) {55if (targetCompositeId) {56this.moveComposite(dragData.id, targetCompositeId, before);57moved = true;58}59}60// ... on a different composite bar61else {62this.viewDescriptorService.moveViewContainerToLocation(currentContainer, this.targetContainerLocation, this.getTargetIndex(targetCompositeId, before), 'dnd');63moved = true;64}6566if (moved) {67this.openComposite(currentContainer.id, true);68}69}7071if (dragData.type === 'view') {72const viewToMove = this.viewDescriptorService.getViewDescriptorById(dragData.id)!;73if (viewToMove && viewToMove.canMoveView) {74this.viewDescriptorService.moveViewToLocation(viewToMove, this.targetContainerLocation, 'dnd');7576const newContainer = this.viewDescriptorService.getViewContainerByViewId(viewToMove.id)!;7778if (targetCompositeId) {79this.moveComposite(newContainer.id, targetCompositeId, before);80}8182this.openComposite(newContainer.id, true).then(composite => {83composite?.openView(viewToMove.id, true);84});85}86}87}8889onDragEnter(data: CompositeDragAndDropData, targetCompositeId: string | undefined, originalEvent: DragEvent): boolean {90return this.canDrop(data, targetCompositeId);91}9293onDragOver(data: CompositeDragAndDropData, targetCompositeId: string | undefined, originalEvent: DragEvent): boolean {94return this.canDrop(data, targetCompositeId);95}9697private getTargetIndex(targetId: string | undefined, before2d: Before2D | undefined): number | undefined {98if (!targetId) {99return undefined;100}101102const items = this.getItems();103const before = this.orientation === ActionsOrientation.HORIZONTAL ? before2d?.horizontallyBefore : before2d?.verticallyBefore;104return items.filter(item => item.visible).findIndex(item => item.id === targetId) + (before ? 0 : 1);105}106107private canDrop(data: CompositeDragAndDropData, targetCompositeId: string | undefined): boolean {108const dragData = data.getData();109110if (dragData.type === 'composite') {111112// Dragging a composite113const currentContainer = this.viewDescriptorService.getViewContainerById(dragData.id)!;114const currentLocation = this.viewDescriptorService.getViewContainerLocation(currentContainer);115116// ... to the same composite location117if (currentLocation === this.targetContainerLocation) {118return dragData.id !== targetCompositeId;119}120121return true;122} else {123124// Dragging an individual view125const viewDescriptor = this.viewDescriptorService.getViewDescriptorById(dragData.id);126127// ... that cannot move128if (!viewDescriptor || !viewDescriptor.canMoveView) {129return false;130}131132// ... to create a view container133return true;134}135}136}137138export interface ICompositeBarOptions {139140readonly icon: boolean;141readonly orientation: ActionsOrientation;142readonly colors: (theme: IColorTheme) => ICompositeBarColors;143readonly compact?: boolean;144readonly compositeSize: number;145readonly overflowActionSize: number;146readonly dndHandler: ICompositeDragAndDrop;147readonly activityHoverOptions: IActivityHoverOptions;148readonly preventLoopNavigation?: boolean;149150readonly getActivityAction: (compositeId: string) => CompositeBarAction;151readonly getCompositePinnedAction: (compositeId: string) => IAction;152readonly getCompositeBadgeAction: (compositeId: string) => IAction;153readonly getOnCompositeClickAction: (compositeId: string) => IAction;154readonly fillExtraContextMenuActions: (actions: IAction[], e?: MouseEvent | GestureEvent) => void;155readonly getContextMenuActionsForComposite: (compositeId: string) => IAction[];156157readonly openComposite: (compositeId: string, preserveFocus?: boolean) => Promise<IComposite | null>;158readonly getDefaultCompositeId: () => string | undefined;159}160161class CompositeBarDndCallbacks implements ICompositeDragAndDropObserverCallbacks {162163private insertDropBefore: Before2D | undefined = undefined;164165constructor(166private readonly compositeBarContainer: HTMLElement,167private readonly actionBarContainer: HTMLElement,168private readonly compositeBarModel: CompositeBarModel,169private readonly dndHandler: ICompositeDragAndDrop,170private readonly orientation: ActionsOrientation,171) { }172173onDragOver(e: IDraggedCompositeData) {174175// don't add feedback if this is over the composite bar actions or there are no actions176const visibleItems = this.compositeBarModel.visibleItems;177if (!visibleItems.length || (e.eventData.target && isAncestor(e.eventData.target as HTMLElement, this.actionBarContainer))) {178this.insertDropBefore = this.updateFromDragging(this.compositeBarContainer, false, false, true);179return;180}181182const insertAtFront = this.insertAtFront(this.actionBarContainer, e.eventData);183const target = insertAtFront ? visibleItems[0] : visibleItems[visibleItems.length - 1];184const validDropTarget = this.dndHandler.onDragOver(e.dragAndDropData, target.id, e.eventData);185toggleDropEffect(e.eventData.dataTransfer, 'move', validDropTarget);186this.insertDropBefore = this.updateFromDragging(this.compositeBarContainer, validDropTarget, insertAtFront, true);187}188189onDragLeave(e: IDraggedCompositeData) {190this.insertDropBefore = this.updateFromDragging(this.compositeBarContainer, false, false, false);191}192193onDragEnd(e: IDraggedCompositeData) {194this.insertDropBefore = this.updateFromDragging(this.compositeBarContainer, false, false, false);195}196197onDrop(e: IDraggedCompositeData) {198const visibleItems = this.compositeBarModel.visibleItems;199let targetId = undefined;200if (visibleItems.length) {201targetId = this.insertAtFront(this.actionBarContainer, e.eventData) ? visibleItems[0].id : visibleItems[visibleItems.length - 1].id;202}203this.dndHandler.drop(e.dragAndDropData, targetId, e.eventData, this.insertDropBefore);204this.insertDropBefore = this.updateFromDragging(this.compositeBarContainer, false, false, false);205}206207private insertAtFront(element: HTMLElement, event: DragEvent): boolean {208const rect = element.getBoundingClientRect();209const posX = event.clientX;210const posY = event.clientY;211212switch (this.orientation) {213case ActionsOrientation.HORIZONTAL:214return posX < rect.left;215case ActionsOrientation.VERTICAL:216return posY < rect.top;217}218}219220private updateFromDragging(element: HTMLElement, showFeedback: boolean, front: boolean, isDragging: boolean): Before2D | undefined {221element.classList.toggle('dragged-over', isDragging);222element.classList.toggle('dragged-over-head', showFeedback && front);223element.classList.toggle('dragged-over-tail', showFeedback && !front);224225if (!showFeedback) {226return undefined;227}228229return { verticallyBefore: front, horizontallyBefore: front };230}231}232233export class CompositeBar extends Widget implements ICompositeBar {234235private readonly _onDidChange = this._register(new Emitter<void>());236readonly onDidChange = this._onDidChange.event;237238private dimension: Dimension | undefined;239240private compositeSwitcherBar: ActionBar | undefined;241private compositeOverflowAction: CompositeOverflowActivityAction | undefined;242private compositeOverflowActionViewItem: CompositeOverflowActivityActionViewItem | undefined;243244private readonly model: CompositeBarModel;245private readonly visibleComposites: string[];246private readonly compositeSizeInBar: Map<string, number>;247248constructor(249items: ICompositeBarItem[],250private readonly options: ICompositeBarOptions,251@IInstantiationService private readonly instantiationService: IInstantiationService,252@IContextMenuService private readonly contextMenuService: IContextMenuService,253@IViewDescriptorService private readonly viewDescriptorService: IViewDescriptorService,254) {255super();256257this.model = new CompositeBarModel(items, options);258this.visibleComposites = [];259this.compositeSizeInBar = new Map<string, number>();260this.computeSizes(this.model.visibleItems);261}262263getCompositeBarItems(): ICompositeBarItem[] {264return [...this.model.items];265}266267setCompositeBarItems(items: ICompositeBarItem[]): void {268this.model.setItems(items);269this.updateCompositeSwitcher(true);270}271272getPinnedComposites(): ICompositeBarItem[] {273return this.model.pinnedItems;274}275276getPinnedCompositeIds(): string[] {277return this.getPinnedComposites().map(c => c.id);278}279280getVisibleComposites(): ICompositeBarItem[] {281return this.model.visibleItems;282}283284create(parent: HTMLElement): HTMLElement {285const actionBarDiv = parent.appendChild($('.composite-bar'));286this.compositeSwitcherBar = this._register(new ActionBar(actionBarDiv, {287actionViewItemProvider: (action, options) => {288if (action instanceof CompositeOverflowActivityAction) {289return this.compositeOverflowActionViewItem;290}291const item = this.model.findItem(action.id);292return item && this.instantiationService.createInstance(293CompositeActionViewItem,294{ ...options, draggable: true, colors: this.options.colors, icon: this.options.icon, hoverOptions: this.options.activityHoverOptions, compact: this.options.compact },295action as CompositeBarAction,296item.pinnedAction,297item.toggleBadgeAction,298compositeId => this.options.getContextMenuActionsForComposite(compositeId),299() => this.getContextMenuActions(),300this.options.dndHandler,301this302);303},304orientation: this.options.orientation,305ariaLabel: localize('activityBarAriaLabel', "Active View Switcher"),306ariaRole: 'tablist',307preventLoopNavigation: this.options.preventLoopNavigation,308triggerKeys: { keyDown: true }309}));310311// Contextmenu for composites312this._register(addDisposableListener(parent, EventType.CONTEXT_MENU, e => this.showContextMenu(getWindow(parent), e)));313this._register(Gesture.addTarget(parent));314this._register(addDisposableListener(parent, TouchEventType.Contextmenu, e => this.showContextMenu(getWindow(parent), e)));315316// Register a drop target on the whole bar to prevent forbidden feedback317const dndCallback = new CompositeBarDndCallbacks(parent, actionBarDiv, this.model, this.options.dndHandler, this.options.orientation);318this._register(CompositeDragAndDropObserver.INSTANCE.registerTarget(parent, dndCallback));319320return actionBarDiv;321}322323focus(index?: number): void {324this.compositeSwitcherBar?.focus(index);325}326327recomputeSizes(): void {328this.computeSizes(this.model.visibleItems);329this.updateCompositeSwitcher();330}331332layout(dimension: Dimension): void {333this.dimension = dimension;334335if (dimension.height === 0 || dimension.width === 0) {336// Do not layout if not visible. Otherwise the size measurment would be computed wrongly337return;338}339340if (this.compositeSizeInBar.size === 0) {341// Compute size of each composite by getting the size from the css renderer342// Size is later used for overflow computation343this.computeSizes(this.model.visibleItems);344}345346this.updateCompositeSwitcher();347}348349addComposite({ id, name, order, requestedIndex }: { id: string; name: string; order?: number; requestedIndex?: number }): void {350if (this.model.add(id, name, order, requestedIndex)) {351this.computeSizes([this.model.findItem(id)]);352this.updateCompositeSwitcher();353}354}355356removeComposite(id: string): void {357358// If it pinned, unpin it first359if (this.isPinned(id)) {360this.unpin(id);361}362363// Remove from the model364if (this.model.remove(id)) {365this.updateCompositeSwitcher();366}367}368369hideComposite(id: string): void {370if (this.model.hide(id)) {371this.resetActiveComposite(id);372this.updateCompositeSwitcher();373}374}375376activateComposite(id: string): void {377const previousActiveItem = this.model.activeItem;378if (this.model.activate(id)) {379// Update if current composite is neither visible nor pinned380// or previous active composite is not pinned381if (this.visibleComposites.indexOf(id) === - 1 || (!!this.model.activeItem && !this.model.activeItem.pinned) || (previousActiveItem && !previousActiveItem.pinned)) {382this.updateCompositeSwitcher();383}384}385}386387deactivateComposite(id: string): void {388const previousActiveItem = this.model.activeItem;389if (this.model.deactivate()) {390if (previousActiveItem && !previousActiveItem.pinned) {391this.updateCompositeSwitcher();392}393}394}395396async pin(compositeId: string, open?: boolean): Promise<void> {397if (this.model.setPinned(compositeId, true)) {398this.updateCompositeSwitcher();399400if (open) {401await this.options.openComposite(compositeId);402this.activateComposite(compositeId); // Activate after opening403}404}405}406407unpin(compositeId: string): void {408if (this.model.setPinned(compositeId, false)) {409410this.updateCompositeSwitcher();411412this.resetActiveComposite(compositeId);413}414}415416areBadgesEnabled(compositeId: string): boolean {417return this.viewDescriptorService.getViewContainerBadgeEnablementState(compositeId);418}419420toggleBadgeEnablement(compositeId: string): void {421this.viewDescriptorService.setViewContainerBadgeEnablementState(compositeId, !this.areBadgesEnabled(compositeId));422this.updateCompositeSwitcher();423const item = this.model.findItem(compositeId);424if (item) {425// 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.426// I could add another specific function like `activity.updateBadgeEnablement` would then the activity store the sate?427item.activityAction.activities = item.activityAction.activities;428}429}430431private resetActiveComposite(compositeId: string) {432const defaultCompositeId = this.options.getDefaultCompositeId();433434// Case: composite is not the active one or the active one is a different one435// Solv: we do nothing436if (!this.model.activeItem || this.model.activeItem.id !== compositeId) {437return;438}439440// Deactivate itself441this.deactivateComposite(compositeId);442443// Case: composite is not the default composite and default composite is still showing444// Solv: we open the default composite445if (defaultCompositeId && defaultCompositeId !== compositeId && this.isPinned(defaultCompositeId)) {446this.options.openComposite(defaultCompositeId, true);447}448449// Case: we closed the default composite450// Solv: we open the next visible composite from top451else {452const visibleComposite = this.visibleComposites.find(cid => cid !== compositeId);453if (visibleComposite) {454this.options.openComposite(visibleComposite);455}456}457}458459isPinned(compositeId: string): boolean {460const item = this.model.findItem(compositeId);461return item?.pinned;462}463464move(compositeId: string, toCompositeId: string, before?: boolean): void {465if (before !== undefined) {466const fromIndex = this.model.items.findIndex(c => c.id === compositeId);467let toIndex = this.model.items.findIndex(c => c.id === toCompositeId);468469if (fromIndex >= 0 && toIndex >= 0) {470if (!before && fromIndex > toIndex) {471toIndex++;472}473474if (before && fromIndex < toIndex) {475toIndex--;476}477478if (toIndex < this.model.items.length && toIndex >= 0 && toIndex !== fromIndex) {479if (this.model.move(this.model.items[fromIndex].id, this.model.items[toIndex].id)) {480// timeout helps to prevent artifacts from showing up481setTimeout(() => this.updateCompositeSwitcher(), 0);482}483}484}485} else {486if (this.model.move(compositeId, toCompositeId)) {487// timeout helps to prevent artifacts from showing up488setTimeout(() => this.updateCompositeSwitcher(), 0);489}490}491}492493getAction(compositeId: string): CompositeBarAction {494const item = this.model.findItem(compositeId);495496return item?.activityAction;497}498499private computeSizes(items: ICompositeBarModelItem[]): void {500const size = this.options.compositeSize;501if (size) {502items.forEach(composite => this.compositeSizeInBar.set(composite.id, size));503} else {504const compositeSwitcherBar = this.compositeSwitcherBar;505if (compositeSwitcherBar && this.dimension && this.dimension.height !== 0 && this.dimension.width !== 0) {506507// Compute sizes only if visible. Otherwise the size measurment would be computed wrongly.508const currentItemsLength = compositeSwitcherBar.viewItems.length;509compositeSwitcherBar.push(items.map(composite => composite.activityAction));510items.map((composite, index) => this.compositeSizeInBar.set(composite.id, this.options.orientation === ActionsOrientation.VERTICAL511? compositeSwitcherBar.getHeight(currentItemsLength + index)512: compositeSwitcherBar.getWidth(currentItemsLength + index)513));514items.forEach(() => compositeSwitcherBar.pull(compositeSwitcherBar.viewItems.length - 1));515}516}517}518519private updateCompositeSwitcher(donotTrigger?: boolean): void {520const compositeSwitcherBar = this.compositeSwitcherBar;521if (!compositeSwitcherBar || !this.dimension) {522return; // We have not been rendered yet so there is nothing to update.523}524525let compositesToShow = this.model.visibleItems.filter(item =>526item.pinned527|| (this.model.activeItem && this.model.activeItem.id === item.id) /* Show the active composite even if it is not pinned */528).map(item => item.id);529530// Ensure we are not showing more composites than we have height for531let maxVisible = compositesToShow.length;532const totalComposites = compositesToShow.length;533let size = 0;534const limit = this.options.orientation === ActionsOrientation.VERTICAL ? this.dimension.height : this.dimension.width;535536// Add composites while they fit537for (let i = 0; i < compositesToShow.length; i++) {538const compositeSize = this.compositeSizeInBar.get(compositesToShow[i])!;539// Adding this composite will overflow available size, so don't540if (size + compositeSize > limit) {541maxVisible = i;542break;543}544545size += compositeSize;546}547548// Remove the tail of composites that did not fit549if (totalComposites > maxVisible) {550compositesToShow = compositesToShow.slice(0, maxVisible);551}552553// We always try show the active composite, so re-add it if it was sliced out554if (this.model.activeItem && compositesToShow.every(compositeId => !!this.model.activeItem && compositeId !== this.model.activeItem.id)) {555size += this.compositeSizeInBar.get(this.model.activeItem.id)!;556compositesToShow.push(this.model.activeItem.id);557}558559// The active composite might have pushed us over the limit560// Keep popping the composite before the active one until it fits561// If even the active one doesn't fit, we will resort to overflow562while (size > limit && compositesToShow.length) {563const removedComposite = compositesToShow.length > 1 ? compositesToShow.splice(compositesToShow.length - 2, 1)[0] : compositesToShow.pop();564size -= this.compositeSizeInBar.get(removedComposite!)!;565}566567// We are overflowing, add the overflow size568if (totalComposites > compositesToShow.length) {569size += this.options.overflowActionSize;570}571572// Check if we need to make extra room for the overflow action573while (size > limit && compositesToShow.length) {574const removedComposite = compositesToShow.length > 1 && compositesToShow[compositesToShow.length - 1] === this.model.activeItem?.id ?575compositesToShow.splice(compositesToShow.length - 2, 1)[0] : compositesToShow.pop();576size -= this.compositeSizeInBar.get(removedComposite!)!;577}578579// Remove the overflow action if there are no overflows580if (totalComposites === compositesToShow.length && this.compositeOverflowAction) {581compositeSwitcherBar.pull(compositeSwitcherBar.length() - 1);582583this.compositeOverflowAction.dispose();584this.compositeOverflowAction = undefined;585586this.compositeOverflowActionViewItem?.dispose();587this.compositeOverflowActionViewItem = undefined;588}589590// Pull out composites that overflow or got hidden591const compositesToRemove: number[] = [];592this.visibleComposites.forEach((compositeId, index) => {593if (!compositesToShow.includes(compositeId)) {594compositesToRemove.push(index);595}596});597compositesToRemove.reverse().forEach(index => {598compositeSwitcherBar.pull(index);599this.visibleComposites.splice(index, 1);600});601602// Update the positions of the composites603compositesToShow.forEach((compositeId, newIndex) => {604const currentIndex = this.visibleComposites.indexOf(compositeId);605if (newIndex !== currentIndex) {606if (currentIndex !== -1) {607compositeSwitcherBar.pull(currentIndex);608this.visibleComposites.splice(currentIndex, 1);609}610611compositeSwitcherBar.push(this.model.findItem(compositeId).activityAction, { label: true, icon: this.options.icon, index: newIndex });612this.visibleComposites.splice(newIndex, 0, compositeId);613}614});615616// Add overflow action as needed617if (totalComposites > compositesToShow.length && !this.compositeOverflowAction) {618this.compositeOverflowAction = this._register(this.instantiationService.createInstance(CompositeOverflowActivityAction, () => {619this.compositeOverflowActionViewItem?.showMenu();620}));621this.compositeOverflowActionViewItem = this._register(this.instantiationService.createInstance(622CompositeOverflowActivityActionViewItem,623this.compositeOverflowAction,624() => this.getOverflowingComposites(),625() => this.model.activeItem ? this.model.activeItem.id : undefined,626compositeId => {627const item = this.model.findItem(compositeId);628return item?.activity[0]?.badge;629},630this.options.getOnCompositeClickAction,631this.options.colors,632this.options.activityHoverOptions633));634635compositeSwitcherBar.push(this.compositeOverflowAction, { label: false, icon: true });636}637638if (!donotTrigger) {639this._onDidChange.fire();640}641}642643private getOverflowingComposites(): { id: string; name?: string }[] {644let overflowingIds = this.model.visibleItems.filter(item => item.pinned).map(item => item.id);645646// Show the active composite even if it is not pinned647if (this.model.activeItem && !this.model.activeItem.pinned) {648overflowingIds.push(this.model.activeItem.id);649}650651overflowingIds = overflowingIds.filter(compositeId => !this.visibleComposites.includes(compositeId));652return this.model.visibleItems.filter(c => overflowingIds.includes(c.id)).map(item => { return { id: item.id, name: this.getAction(item.id)?.label || item.name }; });653}654655private showContextMenu(targetWindow: Window, e: MouseEvent | GestureEvent): void {656EventHelper.stop(e, true);657658const event = new StandardMouseEvent(targetWindow, e);659this.contextMenuService.showContextMenu({660getAnchor: () => event,661getActions: () => this.getContextMenuActions(e)662});663}664665getContextMenuActions(e?: MouseEvent | GestureEvent): IAction[] {666const actions: IAction[] = this.model.visibleItems667.map(({ id, name, activityAction }) => {668const isPinned = this.isPinned(id);669return toAction({670id,671label: this.getAction(id).label || name || id,672checked: isPinned,673enabled: activityAction.enabled && (!isPinned || this.getPinnedCompositeIds().length > 1),674run: () => {675if (this.isPinned(id)) {676this.unpin(id);677} else {678this.pin(id, true);679}680}681});682});683684this.options.fillExtraContextMenuActions(actions, e);685686return actions;687}688}689690interface ICompositeBarModelItem extends ICompositeBarItem {691readonly activityAction: CompositeBarAction;692readonly pinnedAction: IAction;693readonly toggleBadgeAction: IAction;694readonly activity: IActivity[];695}696697class CompositeBarModel {698699private _items: ICompositeBarModelItem[] = [];700get items(): ICompositeBarModelItem[] { return this._items; }701702private readonly options: ICompositeBarOptions;703704activeItem?: ICompositeBarModelItem;705706constructor(707items: ICompositeBarItem[],708options: ICompositeBarOptions709) {710this.options = options;711this.setItems(items);712}713714setItems(items: ICompositeBarItem[]): void {715this._items = [];716this._items = items717.map(i => this.createCompositeBarItem(i.id, i.name, i.order, i.pinned, i.visible));718}719720get visibleItems(): ICompositeBarModelItem[] {721return this.items.filter(item => item.visible);722}723724get pinnedItems(): ICompositeBarModelItem[] {725return this.items.filter(item => item.visible && item.pinned);726}727728private createCompositeBarItem(id: string, name: string | undefined, order: number | undefined, pinned: boolean, visible: boolean): ICompositeBarModelItem {729const options = this.options;730return {731id, name, pinned, order, visible,732activity: [],733get activityAction() {734return options.getActivityAction(id);735},736get pinnedAction() {737return options.getCompositePinnedAction(id);738},739get toggleBadgeAction() {740return options.getCompositeBadgeAction(id);741}742};743}744745add(id: string, name: string, order: number | undefined, requestedIndex: number | undefined): boolean {746const item = this.findItem(id);747if (item) {748let changed = false;749item.name = name;750if (!isUndefinedOrNull(order)) {751changed = item.order !== order;752item.order = order;753}754if (!item.visible) {755item.visible = true;756changed = true;757}758759return changed;760} else {761const item = this.createCompositeBarItem(id, name, order, true, true);762if (!isUndefinedOrNull(requestedIndex)) {763let index = 0;764let rIndex = requestedIndex;765while (rIndex > 0 && index < this.items.length) {766if (this.items[index++].visible) {767rIndex--;768}769}770771this.items.splice(index, 0, item);772} else if (isUndefinedOrNull(order)) {773this.items.push(item);774} else {775let index = 0;776while (index < this.items.length && typeof this.items[index].order === 'number' && this.items[index].order! < order) {777index++;778}779this.items.splice(index, 0, item);780}781782return true;783}784}785786remove(id: string): boolean {787for (let index = 0; index < this.items.length; index++) {788if (this.items[index].id === id) {789this.items.splice(index, 1);790return true;791}792}793return false;794}795796hide(id: string): boolean {797for (const item of this.items) {798if (item.id === id) {799if (item.visible) {800item.visible = false;801return true;802}803return false;804}805}806return false;807}808809move(compositeId: string, toCompositeId: string): boolean {810811const fromIndex = this.findIndex(compositeId);812const toIndex = this.findIndex(toCompositeId);813814// Make sure both items are known to the model815if (fromIndex === -1 || toIndex === -1) {816return false;817}818819const sourceItem = this.items.splice(fromIndex, 1)[0];820this.items.splice(toIndex, 0, sourceItem);821822// Make sure a moved composite gets pinned823sourceItem.pinned = true;824825return true;826}827828setPinned(id: string, pinned: boolean): boolean {829for (const item of this.items) {830if (item.id === id) {831if (item.pinned !== pinned) {832item.pinned = pinned;833return true;834}835return false;836}837}838return false;839}840841activate(id: string): boolean {842if (!this.activeItem || this.activeItem.id !== id) {843if (this.activeItem) {844this.deactivate();845}846for (const item of this.items) {847if (item.id === id) {848this.activeItem = item;849this.activeItem.activityAction.activate();850return true;851}852}853}854return false;855}856857deactivate(): boolean {858if (this.activeItem) {859this.activeItem.activityAction.deactivate();860this.activeItem = undefined;861return true;862}863return false;864}865866findItem(id: string): ICompositeBarModelItem {867return this.items.filter(item => item.id === id)[0];868}869870private findIndex(id: string): number {871for (let index = 0; index < this.items.length; index++) {872if (this.items[index].id === id) {873return index;874}875}876877return -1;878}879}880881882