Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/browser/parts/compositeBar.ts
3296 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { localize } from '../../../nls.js';
7
import { IAction, toAction } from '../../../base/common/actions.js';
8
import { IActivity } from '../../services/activity/common/activity.js';
9
import { IInstantiationService } from '../../../platform/instantiation/common/instantiation.js';
10
import { ActionBar, ActionsOrientation } from '../../../base/browser/ui/actionbar/actionbar.js';
11
import { CompositeActionViewItem, CompositeOverflowActivityAction, CompositeOverflowActivityActionViewItem, CompositeBarAction, ICompositeBar, ICompositeBarColors, IActivityHoverOptions } from './compositeBarActions.js';
12
import { Dimension, $, addDisposableListener, EventType, EventHelper, isAncestor, getWindow } from '../../../base/browser/dom.js';
13
import { StandardMouseEvent } from '../../../base/browser/mouseEvent.js';
14
import { IContextMenuService } from '../../../platform/contextview/browser/contextView.js';
15
import { Widget } from '../../../base/browser/ui/widget.js';
16
import { isUndefinedOrNull } from '../../../base/common/types.js';
17
import { IColorTheme } from '../../../platform/theme/common/themeService.js';
18
import { Emitter } from '../../../base/common/event.js';
19
import { ViewContainerLocation, IViewDescriptorService } from '../../common/views.js';
20
import { IPaneComposite } from '../../common/panecomposite.js';
21
import { IComposite } from '../../common/composite.js';
22
import { CompositeDragAndDropData, CompositeDragAndDropObserver, IDraggedCompositeData, ICompositeDragAndDrop, Before2D, toggleDropEffect, ICompositeDragAndDropObserverCallbacks } from '../dnd.js';
23
import { Gesture, EventType as TouchEventType, GestureEvent } from '../../../base/browser/touch.js';
24
25
export interface ICompositeBarItem {
26
27
readonly id: string;
28
29
name?: string;
30
pinned: boolean;
31
order?: number;
32
visible: boolean;
33
}
34
35
export class CompositeDragAndDrop implements ICompositeDragAndDrop {
36
37
constructor(
38
private viewDescriptorService: IViewDescriptorService,
39
private targetContainerLocation: ViewContainerLocation,
40
private orientation: ActionsOrientation,
41
private openComposite: (id: string, focus?: boolean) => Promise<IPaneComposite | null>,
42
private moveComposite: (from: string, to: string, before?: Before2D) => void,
43
private getItems: () => ICompositeBarItem[]
44
) { }
45
46
drop(data: CompositeDragAndDropData, targetCompositeId: string | undefined, originalEvent: DragEvent, before?: Before2D): void {
47
const dragData = data.getData();
48
49
if (dragData.type === 'composite') {
50
const currentContainer = this.viewDescriptorService.getViewContainerById(dragData.id)!;
51
const currentLocation = this.viewDescriptorService.getViewContainerLocation(currentContainer);
52
let moved = false;
53
54
// ... on the same composite bar
55
if (currentLocation === this.targetContainerLocation) {
56
if (targetCompositeId) {
57
this.moveComposite(dragData.id, targetCompositeId, before);
58
moved = true;
59
}
60
}
61
// ... on a different composite bar
62
else {
63
this.viewDescriptorService.moveViewContainerToLocation(currentContainer, this.targetContainerLocation, this.getTargetIndex(targetCompositeId, before), 'dnd');
64
moved = true;
65
}
66
67
if (moved) {
68
this.openComposite(currentContainer.id, true);
69
}
70
}
71
72
if (dragData.type === 'view') {
73
const viewToMove = this.viewDescriptorService.getViewDescriptorById(dragData.id)!;
74
if (viewToMove && viewToMove.canMoveView) {
75
this.viewDescriptorService.moveViewToLocation(viewToMove, this.targetContainerLocation, 'dnd');
76
77
const newContainer = this.viewDescriptorService.getViewContainerByViewId(viewToMove.id)!;
78
79
if (targetCompositeId) {
80
this.moveComposite(newContainer.id, targetCompositeId, before);
81
}
82
83
this.openComposite(newContainer.id, true).then(composite => {
84
composite?.openView(viewToMove.id, true);
85
});
86
}
87
}
88
}
89
90
onDragEnter(data: CompositeDragAndDropData, targetCompositeId: string | undefined, originalEvent: DragEvent): boolean {
91
return this.canDrop(data, targetCompositeId);
92
}
93
94
onDragOver(data: CompositeDragAndDropData, targetCompositeId: string | undefined, originalEvent: DragEvent): boolean {
95
return this.canDrop(data, targetCompositeId);
96
}
97
98
private getTargetIndex(targetId: string | undefined, before2d: Before2D | undefined): number | undefined {
99
if (!targetId) {
100
return undefined;
101
}
102
103
const items = this.getItems();
104
const before = this.orientation === ActionsOrientation.HORIZONTAL ? before2d?.horizontallyBefore : before2d?.verticallyBefore;
105
return items.filter(item => item.visible).findIndex(item => item.id === targetId) + (before ? 0 : 1);
106
}
107
108
private canDrop(data: CompositeDragAndDropData, targetCompositeId: string | undefined): boolean {
109
const dragData = data.getData();
110
111
if (dragData.type === 'composite') {
112
113
// Dragging a composite
114
const currentContainer = this.viewDescriptorService.getViewContainerById(dragData.id)!;
115
const currentLocation = this.viewDescriptorService.getViewContainerLocation(currentContainer);
116
117
// ... to the same composite location
118
if (currentLocation === this.targetContainerLocation) {
119
return dragData.id !== targetCompositeId;
120
}
121
122
return true;
123
} else {
124
125
// Dragging an individual view
126
const viewDescriptor = this.viewDescriptorService.getViewDescriptorById(dragData.id);
127
128
// ... that cannot move
129
if (!viewDescriptor || !viewDescriptor.canMoveView) {
130
return false;
131
}
132
133
// ... to create a view container
134
return true;
135
}
136
}
137
}
138
139
export interface ICompositeBarOptions {
140
141
readonly icon: boolean;
142
readonly orientation: ActionsOrientation;
143
readonly colors: (theme: IColorTheme) => ICompositeBarColors;
144
readonly compact?: boolean;
145
readonly compositeSize: number;
146
readonly overflowActionSize: number;
147
readonly dndHandler: ICompositeDragAndDrop;
148
readonly activityHoverOptions: IActivityHoverOptions;
149
readonly preventLoopNavigation?: boolean;
150
151
readonly getActivityAction: (compositeId: string) => CompositeBarAction;
152
readonly getCompositePinnedAction: (compositeId: string) => IAction;
153
readonly getCompositeBadgeAction: (compositeId: string) => IAction;
154
readonly getOnCompositeClickAction: (compositeId: string) => IAction;
155
readonly fillExtraContextMenuActions: (actions: IAction[], e?: MouseEvent | GestureEvent) => void;
156
readonly getContextMenuActionsForComposite: (compositeId: string) => IAction[];
157
158
readonly openComposite: (compositeId: string, preserveFocus?: boolean) => Promise<IComposite | null>;
159
readonly getDefaultCompositeId: () => string | undefined;
160
}
161
162
class CompositeBarDndCallbacks implements ICompositeDragAndDropObserverCallbacks {
163
164
private insertDropBefore: Before2D | undefined = undefined;
165
166
constructor(
167
private readonly compositeBarContainer: HTMLElement,
168
private readonly actionBarContainer: HTMLElement,
169
private readonly compositeBarModel: CompositeBarModel,
170
private readonly dndHandler: ICompositeDragAndDrop,
171
private readonly orientation: ActionsOrientation,
172
) { }
173
174
onDragOver(e: IDraggedCompositeData) {
175
176
// don't add feedback if this is over the composite bar actions or there are no actions
177
const visibleItems = this.compositeBarModel.visibleItems;
178
if (!visibleItems.length || (e.eventData.target && isAncestor(e.eventData.target as HTMLElement, this.actionBarContainer))) {
179
this.insertDropBefore = this.updateFromDragging(this.compositeBarContainer, false, false, true);
180
return;
181
}
182
183
const insertAtFront = this.insertAtFront(this.actionBarContainer, e.eventData);
184
const target = insertAtFront ? visibleItems[0] : visibleItems[visibleItems.length - 1];
185
const validDropTarget = this.dndHandler.onDragOver(e.dragAndDropData, target.id, e.eventData);
186
toggleDropEffect(e.eventData.dataTransfer, 'move', validDropTarget);
187
this.insertDropBefore = this.updateFromDragging(this.compositeBarContainer, validDropTarget, insertAtFront, true);
188
}
189
190
onDragLeave(e: IDraggedCompositeData) {
191
this.insertDropBefore = this.updateFromDragging(this.compositeBarContainer, false, false, false);
192
}
193
194
onDragEnd(e: IDraggedCompositeData) {
195
this.insertDropBefore = this.updateFromDragging(this.compositeBarContainer, false, false, false);
196
}
197
198
onDrop(e: IDraggedCompositeData) {
199
const visibleItems = this.compositeBarModel.visibleItems;
200
let targetId = undefined;
201
if (visibleItems.length) {
202
targetId = this.insertAtFront(this.actionBarContainer, e.eventData) ? visibleItems[0].id : visibleItems[visibleItems.length - 1].id;
203
}
204
this.dndHandler.drop(e.dragAndDropData, targetId, e.eventData, this.insertDropBefore);
205
this.insertDropBefore = this.updateFromDragging(this.compositeBarContainer, false, false, false);
206
}
207
208
private insertAtFront(element: HTMLElement, event: DragEvent): boolean {
209
const rect = element.getBoundingClientRect();
210
const posX = event.clientX;
211
const posY = event.clientY;
212
213
switch (this.orientation) {
214
case ActionsOrientation.HORIZONTAL:
215
return posX < rect.left;
216
case ActionsOrientation.VERTICAL:
217
return posY < rect.top;
218
}
219
}
220
221
private updateFromDragging(element: HTMLElement, showFeedback: boolean, front: boolean, isDragging: boolean): Before2D | undefined {
222
element.classList.toggle('dragged-over', isDragging);
223
element.classList.toggle('dragged-over-head', showFeedback && front);
224
element.classList.toggle('dragged-over-tail', showFeedback && !front);
225
226
if (!showFeedback) {
227
return undefined;
228
}
229
230
return { verticallyBefore: front, horizontallyBefore: front };
231
}
232
}
233
234
export class CompositeBar extends Widget implements ICompositeBar {
235
236
private readonly _onDidChange = this._register(new Emitter<void>());
237
readonly onDidChange = this._onDidChange.event;
238
239
private dimension: Dimension | undefined;
240
241
private compositeSwitcherBar: ActionBar | undefined;
242
private compositeOverflowAction: CompositeOverflowActivityAction | undefined;
243
private compositeOverflowActionViewItem: CompositeOverflowActivityActionViewItem | undefined;
244
245
private readonly model: CompositeBarModel;
246
private readonly visibleComposites: string[];
247
private readonly compositeSizeInBar: Map<string, number>;
248
249
constructor(
250
items: ICompositeBarItem[],
251
private readonly options: ICompositeBarOptions,
252
@IInstantiationService private readonly instantiationService: IInstantiationService,
253
@IContextMenuService private readonly contextMenuService: IContextMenuService,
254
@IViewDescriptorService private readonly viewDescriptorService: IViewDescriptorService,
255
) {
256
super();
257
258
this.model = new CompositeBarModel(items, options);
259
this.visibleComposites = [];
260
this.compositeSizeInBar = new Map<string, number>();
261
this.computeSizes(this.model.visibleItems);
262
}
263
264
getCompositeBarItems(): ICompositeBarItem[] {
265
return [...this.model.items];
266
}
267
268
setCompositeBarItems(items: ICompositeBarItem[]): void {
269
this.model.setItems(items);
270
this.updateCompositeSwitcher(true);
271
}
272
273
getPinnedComposites(): ICompositeBarItem[] {
274
return this.model.pinnedItems;
275
}
276
277
getPinnedCompositeIds(): string[] {
278
return this.getPinnedComposites().map(c => c.id);
279
}
280
281
getVisibleComposites(): ICompositeBarItem[] {
282
return this.model.visibleItems;
283
}
284
285
create(parent: HTMLElement): HTMLElement {
286
const actionBarDiv = parent.appendChild($('.composite-bar'));
287
this.compositeSwitcherBar = this._register(new ActionBar(actionBarDiv, {
288
actionViewItemProvider: (action, options) => {
289
if (action instanceof CompositeOverflowActivityAction) {
290
return this.compositeOverflowActionViewItem;
291
}
292
const item = this.model.findItem(action.id);
293
return item && this.instantiationService.createInstance(
294
CompositeActionViewItem,
295
{ ...options, draggable: true, colors: this.options.colors, icon: this.options.icon, hoverOptions: this.options.activityHoverOptions, compact: this.options.compact },
296
action as CompositeBarAction,
297
item.pinnedAction,
298
item.toggleBadgeAction,
299
compositeId => this.options.getContextMenuActionsForComposite(compositeId),
300
() => this.getContextMenuActions(),
301
this.options.dndHandler,
302
this
303
);
304
},
305
orientation: this.options.orientation,
306
ariaLabel: localize('activityBarAriaLabel', "Active View Switcher"),
307
ariaRole: 'tablist',
308
preventLoopNavigation: this.options.preventLoopNavigation,
309
triggerKeys: { keyDown: true }
310
}));
311
312
// Contextmenu for composites
313
this._register(addDisposableListener(parent, EventType.CONTEXT_MENU, e => this.showContextMenu(getWindow(parent), e)));
314
this._register(Gesture.addTarget(parent));
315
this._register(addDisposableListener(parent, TouchEventType.Contextmenu, e => this.showContextMenu(getWindow(parent), e)));
316
317
// Register a drop target on the whole bar to prevent forbidden feedback
318
const dndCallback = new CompositeBarDndCallbacks(parent, actionBarDiv, this.model, this.options.dndHandler, this.options.orientation);
319
this._register(CompositeDragAndDropObserver.INSTANCE.registerTarget(parent, dndCallback));
320
321
return actionBarDiv;
322
}
323
324
focus(index?: number): void {
325
this.compositeSwitcherBar?.focus(index);
326
}
327
328
recomputeSizes(): void {
329
this.computeSizes(this.model.visibleItems);
330
this.updateCompositeSwitcher();
331
}
332
333
layout(dimension: Dimension): void {
334
this.dimension = dimension;
335
336
if (dimension.height === 0 || dimension.width === 0) {
337
// Do not layout if not visible. Otherwise the size measurment would be computed wrongly
338
return;
339
}
340
341
if (this.compositeSizeInBar.size === 0) {
342
// Compute size of each composite by getting the size from the css renderer
343
// Size is later used for overflow computation
344
this.computeSizes(this.model.visibleItems);
345
}
346
347
this.updateCompositeSwitcher();
348
}
349
350
addComposite({ id, name, order, requestedIndex }: { id: string; name: string; order?: number; requestedIndex?: number }): void {
351
if (this.model.add(id, name, order, requestedIndex)) {
352
this.computeSizes([this.model.findItem(id)]);
353
this.updateCompositeSwitcher();
354
}
355
}
356
357
removeComposite(id: string): void {
358
359
// If it pinned, unpin it first
360
if (this.isPinned(id)) {
361
this.unpin(id);
362
}
363
364
// Remove from the model
365
if (this.model.remove(id)) {
366
this.updateCompositeSwitcher();
367
}
368
}
369
370
hideComposite(id: string): void {
371
if (this.model.hide(id)) {
372
this.resetActiveComposite(id);
373
this.updateCompositeSwitcher();
374
}
375
}
376
377
activateComposite(id: string): void {
378
const previousActiveItem = this.model.activeItem;
379
if (this.model.activate(id)) {
380
// Update if current composite is neither visible nor pinned
381
// or previous active composite is not pinned
382
if (this.visibleComposites.indexOf(id) === - 1 || (!!this.model.activeItem && !this.model.activeItem.pinned) || (previousActiveItem && !previousActiveItem.pinned)) {
383
this.updateCompositeSwitcher();
384
}
385
}
386
}
387
388
deactivateComposite(id: string): void {
389
const previousActiveItem = this.model.activeItem;
390
if (this.model.deactivate()) {
391
if (previousActiveItem && !previousActiveItem.pinned) {
392
this.updateCompositeSwitcher();
393
}
394
}
395
}
396
397
async pin(compositeId: string, open?: boolean): Promise<void> {
398
if (this.model.setPinned(compositeId, true)) {
399
this.updateCompositeSwitcher();
400
401
if (open) {
402
await this.options.openComposite(compositeId);
403
this.activateComposite(compositeId); // Activate after opening
404
}
405
}
406
}
407
408
unpin(compositeId: string): void {
409
if (this.model.setPinned(compositeId, false)) {
410
411
this.updateCompositeSwitcher();
412
413
this.resetActiveComposite(compositeId);
414
}
415
}
416
417
areBadgesEnabled(compositeId: string): boolean {
418
return this.viewDescriptorService.getViewContainerBadgeEnablementState(compositeId);
419
}
420
421
toggleBadgeEnablement(compositeId: string): void {
422
this.viewDescriptorService.setViewContainerBadgeEnablementState(compositeId, !this.areBadgesEnabled(compositeId));
423
this.updateCompositeSwitcher();
424
const item = this.model.findItem(compositeId);
425
if (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?
428
item.activityAction.activities = item.activityAction.activities;
429
}
430
}
431
432
private resetActiveComposite(compositeId: string) {
433
const defaultCompositeId = this.options.getDefaultCompositeId();
434
435
// Case: composite is not the active one or the active one is a different one
436
// Solv: we do nothing
437
if (!this.model.activeItem || this.model.activeItem.id !== compositeId) {
438
return;
439
}
440
441
// Deactivate itself
442
this.deactivateComposite(compositeId);
443
444
// Case: composite is not the default composite and default composite is still showing
445
// Solv: we open the default composite
446
if (defaultCompositeId && defaultCompositeId !== compositeId && this.isPinned(defaultCompositeId)) {
447
this.options.openComposite(defaultCompositeId, true);
448
}
449
450
// Case: we closed the default composite
451
// Solv: we open the next visible composite from top
452
else {
453
const visibleComposite = this.visibleComposites.find(cid => cid !== compositeId);
454
if (visibleComposite) {
455
this.options.openComposite(visibleComposite);
456
}
457
}
458
}
459
460
isPinned(compositeId: string): boolean {
461
const item = this.model.findItem(compositeId);
462
return item?.pinned;
463
}
464
465
move(compositeId: string, toCompositeId: string, before?: boolean): void {
466
if (before !== undefined) {
467
const fromIndex = this.model.items.findIndex(c => c.id === compositeId);
468
let toIndex = this.model.items.findIndex(c => c.id === toCompositeId);
469
470
if (fromIndex >= 0 && toIndex >= 0) {
471
if (!before && fromIndex > toIndex) {
472
toIndex++;
473
}
474
475
if (before && fromIndex < toIndex) {
476
toIndex--;
477
}
478
479
if (toIndex < this.model.items.length && toIndex >= 0 && toIndex !== fromIndex) {
480
if (this.model.move(this.model.items[fromIndex].id, this.model.items[toIndex].id)) {
481
// timeout helps to prevent artifacts from showing up
482
setTimeout(() => this.updateCompositeSwitcher(), 0);
483
}
484
}
485
}
486
} else {
487
if (this.model.move(compositeId, toCompositeId)) {
488
// timeout helps to prevent artifacts from showing up
489
setTimeout(() => this.updateCompositeSwitcher(), 0);
490
}
491
}
492
}
493
494
getAction(compositeId: string): CompositeBarAction {
495
const item = this.model.findItem(compositeId);
496
497
return item?.activityAction;
498
}
499
500
private computeSizes(items: ICompositeBarModelItem[]): void {
501
const size = this.options.compositeSize;
502
if (size) {
503
items.forEach(composite => this.compositeSizeInBar.set(composite.id, size));
504
} else {
505
const compositeSwitcherBar = this.compositeSwitcherBar;
506
if (compositeSwitcherBar && this.dimension && this.dimension.height !== 0 && this.dimension.width !== 0) {
507
508
// Compute sizes only if visible. Otherwise the size measurment would be computed wrongly.
509
const currentItemsLength = compositeSwitcherBar.viewItems.length;
510
compositeSwitcherBar.push(items.map(composite => composite.activityAction));
511
items.map((composite, index) => this.compositeSizeInBar.set(composite.id, this.options.orientation === ActionsOrientation.VERTICAL
512
? compositeSwitcherBar.getHeight(currentItemsLength + index)
513
: compositeSwitcherBar.getWidth(currentItemsLength + index)
514
));
515
items.forEach(() => compositeSwitcherBar.pull(compositeSwitcherBar.viewItems.length - 1));
516
}
517
}
518
}
519
520
private updateCompositeSwitcher(donotTrigger?: boolean): void {
521
const compositeSwitcherBar = this.compositeSwitcherBar;
522
if (!compositeSwitcherBar || !this.dimension) {
523
return; // We have not been rendered yet so there is nothing to update.
524
}
525
526
let compositesToShow = this.model.visibleItems.filter(item =>
527
item.pinned
528
|| (this.model.activeItem && this.model.activeItem.id === item.id) /* Show the active composite even if it is not pinned */
529
).map(item => item.id);
530
531
// Ensure we are not showing more composites than we have height for
532
let maxVisible = compositesToShow.length;
533
const totalComposites = compositesToShow.length;
534
let size = 0;
535
const limit = this.options.orientation === ActionsOrientation.VERTICAL ? this.dimension.height : this.dimension.width;
536
537
// Add composites while they fit
538
for (let i = 0; i < compositesToShow.length; i++) {
539
const compositeSize = this.compositeSizeInBar.get(compositesToShow[i])!;
540
// Adding this composite will overflow available size, so don't
541
if (size + compositeSize > limit) {
542
maxVisible = i;
543
break;
544
}
545
546
size += compositeSize;
547
}
548
549
// Remove the tail of composites that did not fit
550
if (totalComposites > maxVisible) {
551
compositesToShow = compositesToShow.slice(0, maxVisible);
552
}
553
554
// We always try show the active composite, so re-add it if it was sliced out
555
if (this.model.activeItem && compositesToShow.every(compositeId => !!this.model.activeItem && compositeId !== this.model.activeItem.id)) {
556
size += this.compositeSizeInBar.get(this.model.activeItem.id)!;
557
compositesToShow.push(this.model.activeItem.id);
558
}
559
560
// The active composite might have pushed us over the limit
561
// Keep popping the composite before the active one until it fits
562
// If even the active one doesn't fit, we will resort to overflow
563
while (size > limit && compositesToShow.length) {
564
const removedComposite = compositesToShow.length > 1 ? compositesToShow.splice(compositesToShow.length - 2, 1)[0] : compositesToShow.pop();
565
size -= this.compositeSizeInBar.get(removedComposite!)!;
566
}
567
568
// We are overflowing, add the overflow size
569
if (totalComposites > compositesToShow.length) {
570
size += this.options.overflowActionSize;
571
}
572
573
// Check if we need to make extra room for the overflow action
574
while (size > limit && compositesToShow.length) {
575
const removedComposite = compositesToShow.length > 1 && compositesToShow[compositesToShow.length - 1] === this.model.activeItem?.id ?
576
compositesToShow.splice(compositesToShow.length - 2, 1)[0] : compositesToShow.pop();
577
size -= this.compositeSizeInBar.get(removedComposite!)!;
578
}
579
580
// Remove the overflow action if there are no overflows
581
if (totalComposites === compositesToShow.length && this.compositeOverflowAction) {
582
compositeSwitcherBar.pull(compositeSwitcherBar.length() - 1);
583
584
this.compositeOverflowAction.dispose();
585
this.compositeOverflowAction = undefined;
586
587
this.compositeOverflowActionViewItem?.dispose();
588
this.compositeOverflowActionViewItem = undefined;
589
}
590
591
// Pull out composites that overflow or got hidden
592
const compositesToRemove: number[] = [];
593
this.visibleComposites.forEach((compositeId, index) => {
594
if (!compositesToShow.includes(compositeId)) {
595
compositesToRemove.push(index);
596
}
597
});
598
compositesToRemove.reverse().forEach(index => {
599
compositeSwitcherBar.pull(index);
600
this.visibleComposites.splice(index, 1);
601
});
602
603
// Update the positions of the composites
604
compositesToShow.forEach((compositeId, newIndex) => {
605
const currentIndex = this.visibleComposites.indexOf(compositeId);
606
if (newIndex !== currentIndex) {
607
if (currentIndex !== -1) {
608
compositeSwitcherBar.pull(currentIndex);
609
this.visibleComposites.splice(currentIndex, 1);
610
}
611
612
compositeSwitcherBar.push(this.model.findItem(compositeId).activityAction, { label: true, icon: this.options.icon, index: newIndex });
613
this.visibleComposites.splice(newIndex, 0, compositeId);
614
}
615
});
616
617
// Add overflow action as needed
618
if (totalComposites > compositesToShow.length && !this.compositeOverflowAction) {
619
this.compositeOverflowAction = this._register(this.instantiationService.createInstance(CompositeOverflowActivityAction, () => {
620
this.compositeOverflowActionViewItem?.showMenu();
621
}));
622
this.compositeOverflowActionViewItem = this._register(this.instantiationService.createInstance(
623
CompositeOverflowActivityActionViewItem,
624
this.compositeOverflowAction,
625
() => this.getOverflowingComposites(),
626
() => this.model.activeItem ? this.model.activeItem.id : undefined,
627
compositeId => {
628
const item = this.model.findItem(compositeId);
629
return item?.activity[0]?.badge;
630
},
631
this.options.getOnCompositeClickAction,
632
this.options.colors,
633
this.options.activityHoverOptions
634
));
635
636
compositeSwitcherBar.push(this.compositeOverflowAction, { label: false, icon: true });
637
}
638
639
if (!donotTrigger) {
640
this._onDidChange.fire();
641
}
642
}
643
644
private getOverflowingComposites(): { id: string; name?: string }[] {
645
let overflowingIds = this.model.visibleItems.filter(item => item.pinned).map(item => item.id);
646
647
// Show the active composite even if it is not pinned
648
if (this.model.activeItem && !this.model.activeItem.pinned) {
649
overflowingIds.push(this.model.activeItem.id);
650
}
651
652
overflowingIds = overflowingIds.filter(compositeId => !this.visibleComposites.includes(compositeId));
653
return this.model.visibleItems.filter(c => overflowingIds.includes(c.id)).map(item => { return { id: item.id, name: this.getAction(item.id)?.label || item.name }; });
654
}
655
656
private showContextMenu(targetWindow: Window, e: MouseEvent | GestureEvent): void {
657
EventHelper.stop(e, true);
658
659
const event = new StandardMouseEvent(targetWindow, e);
660
this.contextMenuService.showContextMenu({
661
getAnchor: () => event,
662
getActions: () => this.getContextMenuActions(e)
663
});
664
}
665
666
getContextMenuActions(e?: MouseEvent | GestureEvent): IAction[] {
667
const actions: IAction[] = this.model.visibleItems
668
.map(({ id, name, activityAction }) => {
669
const isPinned = this.isPinned(id);
670
return toAction({
671
id,
672
label: this.getAction(id).label || name || id,
673
checked: isPinned,
674
enabled: activityAction.enabled && (!isPinned || this.getPinnedCompositeIds().length > 1),
675
run: () => {
676
if (this.isPinned(id)) {
677
this.unpin(id);
678
} else {
679
this.pin(id, true);
680
}
681
}
682
});
683
});
684
685
this.options.fillExtraContextMenuActions(actions, e);
686
687
return actions;
688
}
689
}
690
691
interface ICompositeBarModelItem extends ICompositeBarItem {
692
readonly activityAction: CompositeBarAction;
693
readonly pinnedAction: IAction;
694
readonly toggleBadgeAction: IAction;
695
readonly activity: IActivity[];
696
}
697
698
class CompositeBarModel {
699
700
private _items: ICompositeBarModelItem[] = [];
701
get items(): ICompositeBarModelItem[] { return this._items; }
702
703
private readonly options: ICompositeBarOptions;
704
705
activeItem?: ICompositeBarModelItem;
706
707
constructor(
708
items: ICompositeBarItem[],
709
options: ICompositeBarOptions
710
) {
711
this.options = options;
712
this.setItems(items);
713
}
714
715
setItems(items: ICompositeBarItem[]): void {
716
this._items = [];
717
this._items = items
718
.map(i => this.createCompositeBarItem(i.id, i.name, i.order, i.pinned, i.visible));
719
}
720
721
get visibleItems(): ICompositeBarModelItem[] {
722
return this.items.filter(item => item.visible);
723
}
724
725
get pinnedItems(): ICompositeBarModelItem[] {
726
return this.items.filter(item => item.visible && item.pinned);
727
}
728
729
private createCompositeBarItem(id: string, name: string | undefined, order: number | undefined, pinned: boolean, visible: boolean): ICompositeBarModelItem {
730
const options = this.options;
731
return {
732
id, name, pinned, order, visible,
733
activity: [],
734
get activityAction() {
735
return options.getActivityAction(id);
736
},
737
get pinnedAction() {
738
return options.getCompositePinnedAction(id);
739
},
740
get toggleBadgeAction() {
741
return options.getCompositeBadgeAction(id);
742
}
743
};
744
}
745
746
add(id: string, name: string, order: number | undefined, requestedIndex: number | undefined): boolean {
747
const item = this.findItem(id);
748
if (item) {
749
let changed = false;
750
item.name = name;
751
if (!isUndefinedOrNull(order)) {
752
changed = item.order !== order;
753
item.order = order;
754
}
755
if (!item.visible) {
756
item.visible = true;
757
changed = true;
758
}
759
760
return changed;
761
} else {
762
const item = this.createCompositeBarItem(id, name, order, true, true);
763
if (!isUndefinedOrNull(requestedIndex)) {
764
let index = 0;
765
let rIndex = requestedIndex;
766
while (rIndex > 0 && index < this.items.length) {
767
if (this.items[index++].visible) {
768
rIndex--;
769
}
770
}
771
772
this.items.splice(index, 0, item);
773
} else if (isUndefinedOrNull(order)) {
774
this.items.push(item);
775
} else {
776
let index = 0;
777
while (index < this.items.length && typeof this.items[index].order === 'number' && this.items[index].order! < order) {
778
index++;
779
}
780
this.items.splice(index, 0, item);
781
}
782
783
return true;
784
}
785
}
786
787
remove(id: string): boolean {
788
for (let index = 0; index < this.items.length; index++) {
789
if (this.items[index].id === id) {
790
this.items.splice(index, 1);
791
return true;
792
}
793
}
794
return false;
795
}
796
797
hide(id: string): boolean {
798
for (const item of this.items) {
799
if (item.id === id) {
800
if (item.visible) {
801
item.visible = false;
802
return true;
803
}
804
return false;
805
}
806
}
807
return false;
808
}
809
810
move(compositeId: string, toCompositeId: string): boolean {
811
812
const fromIndex = this.findIndex(compositeId);
813
const toIndex = this.findIndex(toCompositeId);
814
815
// Make sure both items are known to the model
816
if (fromIndex === -1 || toIndex === -1) {
817
return false;
818
}
819
820
const sourceItem = this.items.splice(fromIndex, 1)[0];
821
this.items.splice(toIndex, 0, sourceItem);
822
823
// Make sure a moved composite gets pinned
824
sourceItem.pinned = true;
825
826
return true;
827
}
828
829
setPinned(id: string, pinned: boolean): boolean {
830
for (const item of this.items) {
831
if (item.id === id) {
832
if (item.pinned !== pinned) {
833
item.pinned = pinned;
834
return true;
835
}
836
return false;
837
}
838
}
839
return false;
840
}
841
842
activate(id: string): boolean {
843
if (!this.activeItem || this.activeItem.id !== id) {
844
if (this.activeItem) {
845
this.deactivate();
846
}
847
for (const item of this.items) {
848
if (item.id === id) {
849
this.activeItem = item;
850
this.activeItem.activityAction.activate();
851
return true;
852
}
853
}
854
}
855
return false;
856
}
857
858
deactivate(): boolean {
859
if (this.activeItem) {
860
this.activeItem.activityAction.deactivate();
861
this.activeItem = undefined;
862
return true;
863
}
864
return false;
865
}
866
867
findItem(id: string): ICompositeBarModelItem {
868
return this.items.filter(item => item.id === id)[0];
869
}
870
871
private findIndex(id: string): number {
872
for (let index = 0; index < this.items.length; index++) {
873
if (this.items[index].id === id) {
874
return index;
875
}
876
}
877
878
return -1;
879
}
880
}
881
882