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