Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/browser/parts/views/viewPaneContainer.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 { $, addDisposableListener, Dimension, DragAndDropObserver, EventType, getWindow, isAncestor } from '../../../../base/browser/dom.js';
7
import { StandardMouseEvent } from '../../../../base/browser/mouseEvent.js';
8
import { EventType as TouchEventType, Gesture } from '../../../../base/browser/touch.js';
9
import { IActionViewItem } from '../../../../base/browser/ui/actionbar/actionbar.js';
10
import { IBoundarySashes, Orientation } from '../../../../base/browser/ui/sash/sash.js';
11
import { IPaneViewOptions, PaneView } from '../../../../base/browser/ui/splitview/paneview.js';
12
import { IAction } from '../../../../base/common/actions.js';
13
import { RunOnceScheduler } from '../../../../base/common/async.js';
14
import { Emitter, Event } from '../../../../base/common/event.js';
15
import { KeyChord, KeyCode, KeyMod } from '../../../../base/common/keyCodes.js';
16
import { combinedDisposable, DisposableStore, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js';
17
import { assertReturnsDefined } from '../../../../base/common/types.js';
18
import './media/paneviewlet.css';
19
import * as nls from '../../../../nls.js';
20
import { createActionViewItem } from '../../../../platform/actions/browser/menuEntryActionViewItem.js';
21
import { Action2, IAction2Options, ISubmenuItem, MenuId, MenuRegistry, registerAction2 } from '../../../../platform/actions/common/actions.js';
22
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
23
import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
24
import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js';
25
import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js';
26
import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js';
27
import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
28
import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';
29
import { activeContrastBorder, asCssVariable } from '../../../../platform/theme/common/colorRegistry.js';
30
import { IThemeService, Themable } from '../../../../platform/theme/common/themeService.js';
31
import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js';
32
import { CompositeDragAndDropObserver, toggleDropEffect } from '../../dnd.js';
33
import { ViewPane } from './viewPane.js';
34
import { IViewletViewOptions } from './viewsViewlet.js';
35
import { Component } from '../../../common/component.js';
36
import { PANEL_SECTION_BORDER, PANEL_SECTION_DRAG_AND_DROP_BACKGROUND, PANEL_SECTION_HEADER_BACKGROUND, PANEL_SECTION_HEADER_BORDER, PANEL_SECTION_HEADER_FOREGROUND, SIDE_BAR_DRAG_AND_DROP_BACKGROUND, SIDE_BAR_SECTION_HEADER_BACKGROUND, SIDE_BAR_SECTION_HEADER_BORDER, SIDE_BAR_SECTION_HEADER_FOREGROUND } from '../../../common/theme.js';
37
import { IAddedViewDescriptorRef, ICustomViewDescriptor, IView, IViewContainerModel, IViewDescriptor, IViewDescriptorRef, IViewDescriptorService, IViewPaneContainer, ViewContainer, ViewContainerLocation, ViewVisibilityState } from '../../../common/views.js';
38
import { IViewsService } from '../../../services/views/common/viewsService.js';
39
import { FocusedViewContext } from '../../../common/contextkeys.js';
40
import { IExtensionService } from '../../../services/extensions/common/extensions.js';
41
import { isHorizontal, IWorkbenchLayoutService, LayoutSettings } from '../../../services/layout/browser/layoutService.js';
42
import { IBaseActionViewItemOptions } from '../../../../base/browser/ui/actionbar/actionViewItems.js';
43
import { ILogService } from '../../../../platform/log/common/log.js';
44
import { ViewContainerMenuActions } from './viewMenuActions.js';
45
46
export const ViewsSubMenu = new MenuId('Views');
47
MenuRegistry.appendMenuItem(MenuId.ViewContainerTitle, {
48
submenu: ViewsSubMenu,
49
title: nls.localize('views', "Views"),
50
order: 1,
51
} satisfies ISubmenuItem);
52
53
export interface IViewPaneContainerOptions extends IPaneViewOptions {
54
mergeViewWithContainerWhenSingleView: boolean;
55
}
56
57
interface IViewPaneItem {
58
pane: ViewPane;
59
disposable: IDisposable;
60
}
61
62
const enum DropDirection {
63
UP,
64
DOWN,
65
LEFT,
66
RIGHT
67
}
68
69
type BoundingRect = { top: number; left: number; bottom: number; right: number };
70
71
class ViewPaneDropOverlay extends Themable {
72
73
private static readonly OVERLAY_ID = 'monaco-pane-drop-overlay';
74
75
private container!: HTMLElement;
76
private overlay!: HTMLElement;
77
78
private _currentDropOperation: DropDirection | undefined;
79
80
// private currentDropOperation: IDropOperation | undefined;
81
private _disposed: boolean | undefined;
82
83
private cleanupOverlayScheduler: RunOnceScheduler;
84
85
get currentDropOperation(): DropDirection | undefined {
86
return this._currentDropOperation;
87
}
88
89
constructor(
90
private paneElement: HTMLElement,
91
private orientation: Orientation | undefined,
92
private bounds: BoundingRect | undefined,
93
protected location: ViewContainerLocation,
94
themeService: IThemeService,
95
) {
96
super(themeService);
97
this.cleanupOverlayScheduler = this._register(new RunOnceScheduler(() => this.dispose(), 300));
98
99
this.create();
100
}
101
102
get disposed(): boolean {
103
return !!this._disposed;
104
}
105
106
private create(): void {
107
108
// Container
109
this.container = $('div', { id: ViewPaneDropOverlay.OVERLAY_ID });
110
this.container.style.top = '0px';
111
112
// Parent
113
this.paneElement.appendChild(this.container);
114
this.paneElement.classList.add('dragged-over');
115
this._register(toDisposable(() => {
116
this.container.remove();
117
this.paneElement.classList.remove('dragged-over');
118
}));
119
120
// Overlay
121
this.overlay = $('.pane-overlay-indicator');
122
this.container.appendChild(this.overlay);
123
124
// Overlay Event Handling
125
this.registerListeners();
126
127
// Styles
128
this.updateStyles();
129
}
130
131
override updateStyles(): void {
132
133
// Overlay drop background
134
this.overlay.style.backgroundColor = this.getColor(this.location === ViewContainerLocation.Panel ? PANEL_SECTION_DRAG_AND_DROP_BACKGROUND : SIDE_BAR_DRAG_AND_DROP_BACKGROUND) || '';
135
136
// Overlay contrast border (if any)
137
const activeContrastBorderColor = this.getColor(activeContrastBorder);
138
this.overlay.style.outlineColor = activeContrastBorderColor || '';
139
this.overlay.style.outlineOffset = activeContrastBorderColor ? '-2px' : '';
140
this.overlay.style.outlineStyle = activeContrastBorderColor ? 'dashed' : '';
141
this.overlay.style.outlineWidth = activeContrastBorderColor ? '2px' : '';
142
143
this.overlay.style.borderColor = activeContrastBorderColor || '';
144
this.overlay.style.borderStyle = 'solid';
145
this.overlay.style.borderWidth = '0px';
146
}
147
148
private registerListeners(): void {
149
this._register(new DragAndDropObserver(this.container, {
150
onDragOver: e => {
151
152
// Position overlay
153
this.positionOverlay(e.offsetX, e.offsetY);
154
155
// Make sure to stop any running cleanup scheduler to remove the overlay
156
if (this.cleanupOverlayScheduler.isScheduled()) {
157
this.cleanupOverlayScheduler.cancel();
158
}
159
},
160
161
onDragLeave: e => this.dispose(),
162
onDragEnd: e => this.dispose(),
163
164
onDrop: e => {
165
// Dispose overlay
166
this.dispose();
167
}
168
}));
169
170
this._register(addDisposableListener(this.container, EventType.MOUSE_OVER, () => {
171
// Under some circumstances we have seen reports where the drop overlay is not being
172
// cleaned up and as such the editor area remains under the overlay so that you cannot
173
// type into the editor anymore. This seems related to using VMs and DND via host and
174
// guest OS, though some users also saw it without VMs.
175
// To protect against this issue we always destroy the overlay as soon as we detect a
176
// mouse event over it. The delay is used to guarantee we are not interfering with the
177
// actual DROP event that can also trigger a mouse over event.
178
if (!this.cleanupOverlayScheduler.isScheduled()) {
179
this.cleanupOverlayScheduler.schedule();
180
}
181
}));
182
}
183
184
private positionOverlay(mousePosX: number, mousePosY: number): void {
185
const paneWidth = this.paneElement.clientWidth;
186
const paneHeight = this.paneElement.clientHeight;
187
188
const splitWidthThreshold = paneWidth / 2;
189
const splitHeightThreshold = paneHeight / 2;
190
191
let dropDirection: DropDirection | undefined;
192
193
if (this.orientation === Orientation.VERTICAL) {
194
if (mousePosY < splitHeightThreshold) {
195
dropDirection = DropDirection.UP;
196
} else if (mousePosY >= splitHeightThreshold) {
197
dropDirection = DropDirection.DOWN;
198
}
199
} else if (this.orientation === Orientation.HORIZONTAL) {
200
if (mousePosX < splitWidthThreshold) {
201
dropDirection = DropDirection.LEFT;
202
} else if (mousePosX >= splitWidthThreshold) {
203
dropDirection = DropDirection.RIGHT;
204
}
205
}
206
207
// Draw overlay based on split direction
208
switch (dropDirection) {
209
case DropDirection.UP:
210
this.doPositionOverlay({ top: '0', left: '0', width: '100%', height: '50%' });
211
break;
212
case DropDirection.DOWN:
213
this.doPositionOverlay({ bottom: '0', left: '0', width: '100%', height: '50%' });
214
break;
215
case DropDirection.LEFT:
216
this.doPositionOverlay({ top: '0', left: '0', width: '50%', height: '100%' });
217
break;
218
case DropDirection.RIGHT:
219
this.doPositionOverlay({ top: '0', right: '0', width: '50%', height: '100%' });
220
break;
221
default: {
222
// const top = this.bounds?.top || 0;
223
// const left = this.bounds?.bottom || 0;
224
225
let top = '0';
226
let left = '0';
227
let width = '100%';
228
let height = '100%';
229
if (this.bounds) {
230
const boundingRect = this.container.getBoundingClientRect();
231
top = `${this.bounds.top - boundingRect.top}px`;
232
left = `${this.bounds.left - boundingRect.left}px`;
233
height = `${this.bounds.bottom - this.bounds.top}px`;
234
width = `${this.bounds.right - this.bounds.left}px`;
235
}
236
237
this.doPositionOverlay({ top, left, width, height });
238
}
239
}
240
241
if ((this.orientation === Orientation.VERTICAL && paneHeight <= 25) ||
242
(this.orientation === Orientation.HORIZONTAL && paneWidth <= 25)) {
243
this.doUpdateOverlayBorder(dropDirection);
244
} else {
245
this.doUpdateOverlayBorder(undefined);
246
}
247
248
// Make sure the overlay is visible now
249
this.overlay.style.opacity = '1';
250
251
// Enable transition after a timeout to prevent initial animation
252
setTimeout(() => this.overlay.classList.add('overlay-move-transition'), 0);
253
254
// Remember as current split direction
255
this._currentDropOperation = dropDirection;
256
}
257
258
private doUpdateOverlayBorder(direction: DropDirection | undefined): void {
259
this.overlay.style.borderTopWidth = direction === DropDirection.UP ? '2px' : '0px';
260
this.overlay.style.borderLeftWidth = direction === DropDirection.LEFT ? '2px' : '0px';
261
this.overlay.style.borderBottomWidth = direction === DropDirection.DOWN ? '2px' : '0px';
262
this.overlay.style.borderRightWidth = direction === DropDirection.RIGHT ? '2px' : '0px';
263
}
264
265
private doPositionOverlay(options: { top?: string; bottom?: string; left?: string; right?: string; width: string; height: string }): void {
266
267
// Container
268
this.container.style.height = '100%';
269
270
// Overlay
271
this.overlay.style.top = options.top || '';
272
this.overlay.style.left = options.left || '';
273
this.overlay.style.bottom = options.bottom || '';
274
this.overlay.style.right = options.right || '';
275
this.overlay.style.width = options.width;
276
this.overlay.style.height = options.height;
277
}
278
279
280
contains(element: HTMLElement): boolean {
281
return element === this.container || element === this.overlay;
282
}
283
284
override dispose(): void {
285
super.dispose();
286
287
this._disposed = true;
288
}
289
}
290
291
export class ViewPaneContainer extends Component implements IViewPaneContainer {
292
293
readonly viewContainer: ViewContainer;
294
private lastFocusedPane: ViewPane | undefined;
295
private lastMergedCollapsedPane: ViewPane | undefined;
296
private paneItems: IViewPaneItem[] = [];
297
private paneview?: PaneView;
298
299
private visible: boolean = false;
300
301
private areExtensionsReady: boolean = false;
302
303
private didLayout = false;
304
private dimension: Dimension | undefined;
305
private _boundarySashes: IBoundarySashes | undefined;
306
307
private readonly visibleViewsCountFromCache: number | undefined;
308
private readonly visibleViewsStorageId: string;
309
protected readonly viewContainerModel: IViewContainerModel;
310
311
private readonly _onTitleAreaUpdate: Emitter<void> = this._register(new Emitter<void>());
312
readonly onTitleAreaUpdate: Event<void> = this._onTitleAreaUpdate.event;
313
314
private readonly _onDidChangeVisibility = this._register(new Emitter<boolean>());
315
readonly onDidChangeVisibility = this._onDidChangeVisibility.event;
316
317
private readonly _onDidAddViews = this._register(new Emitter<IView[]>());
318
readonly onDidAddViews = this._onDidAddViews.event;
319
320
private readonly _onDidRemoveViews = this._register(new Emitter<IView[]>());
321
readonly onDidRemoveViews = this._onDidRemoveViews.event;
322
323
private readonly _onDidChangeViewVisibility = this._register(new Emitter<IView>());
324
readonly onDidChangeViewVisibility = this._onDidChangeViewVisibility.event;
325
326
private readonly _onDidFocusView = this._register(new Emitter<IView>());
327
readonly onDidFocusView = this._onDidFocusView.event;
328
329
private readonly _onDidBlurView = this._register(new Emitter<IView>());
330
readonly onDidBlurView = this._onDidBlurView.event;
331
332
get onDidSashChange(): Event<number> {
333
return assertReturnsDefined(this.paneview).onDidSashChange;
334
}
335
336
get panes(): ViewPane[] {
337
return this.paneItems.map(i => i.pane);
338
}
339
340
get views(): IView[] {
341
return this.panes;
342
}
343
344
get length(): number {
345
return this.paneItems.length;
346
}
347
348
private _menuActions?: ViewContainerMenuActions;
349
get menuActions(): ViewContainerMenuActions | undefined {
350
return this._menuActions;
351
}
352
353
constructor(
354
id: string,
355
private options: IViewPaneContainerOptions,
356
@IInstantiationService protected instantiationService: IInstantiationService,
357
@IConfigurationService protected configurationService: IConfigurationService,
358
@IWorkbenchLayoutService protected layoutService: IWorkbenchLayoutService,
359
@IContextMenuService protected contextMenuService: IContextMenuService,
360
@ITelemetryService protected telemetryService: ITelemetryService,
361
@IExtensionService protected extensionService: IExtensionService,
362
@IThemeService themeService: IThemeService,
363
@IStorageService protected storageService: IStorageService,
364
@IWorkspaceContextService protected contextService: IWorkspaceContextService,
365
@IViewDescriptorService protected viewDescriptorService: IViewDescriptorService,
366
@ILogService protected readonly logService: ILogService,
367
) {
368
369
super(id, themeService, storageService);
370
371
const container = this.viewDescriptorService.getViewContainerById(id);
372
if (!container) {
373
throw new Error('Could not find container');
374
}
375
376
377
this.viewContainer = container;
378
this.visibleViewsStorageId = `${id}.numberOfVisibleViews`;
379
this.visibleViewsCountFromCache = this.storageService.getNumber(this.visibleViewsStorageId, StorageScope.WORKSPACE, undefined);
380
this.viewContainerModel = this.viewDescriptorService.getViewContainerModel(container);
381
}
382
383
create(parent: HTMLElement): void {
384
const options = this.options as IPaneViewOptions;
385
options.orientation = this.orientation;
386
this.paneview = this._register(new PaneView(parent, this.options));
387
388
if (this._boundarySashes) {
389
this.paneview.setBoundarySashes(this._boundarySashes);
390
}
391
392
this._register(this.paneview.onDidDrop(({ from, to }) => this.movePane(from as ViewPane, to as ViewPane)));
393
this._register(this.paneview.onDidScroll(_ => this.onDidScrollPane()));
394
this._register(this.paneview.onDidSashReset((index) => this.onDidSashReset(index)));
395
this._register(addDisposableListener(parent, EventType.CONTEXT_MENU, (e: MouseEvent) => this.showContextMenu(new StandardMouseEvent(getWindow(parent), e))));
396
this._register(Gesture.addTarget(parent));
397
this._register(addDisposableListener(parent, TouchEventType.Contextmenu, (e: MouseEvent) => this.showContextMenu(new StandardMouseEvent(getWindow(parent), e))));
398
399
this._menuActions = this._register(this.instantiationService.createInstance(ViewContainerMenuActions, this.paneview.element, this.viewContainer));
400
this._register(this._menuActions.onDidChange(() => this.updateTitleArea()));
401
402
let overlay: ViewPaneDropOverlay | undefined;
403
const getOverlayBounds: () => BoundingRect = () => {
404
const fullSize = parent.getBoundingClientRect();
405
const lastPane = this.panes[this.panes.length - 1].element.getBoundingClientRect();
406
const top = this.orientation === Orientation.VERTICAL ? lastPane.bottom : fullSize.top;
407
const left = this.orientation === Orientation.HORIZONTAL ? lastPane.right : fullSize.left;
408
409
return {
410
top,
411
bottom: fullSize.bottom,
412
left,
413
right: fullSize.right,
414
};
415
};
416
417
const inBounds = (bounds: BoundingRect, pos: { x: number; y: number }) => {
418
return pos.x >= bounds.left && pos.x <= bounds.right && pos.y >= bounds.top && pos.y <= bounds.bottom;
419
};
420
421
422
let bounds: BoundingRect;
423
424
this._register(CompositeDragAndDropObserver.INSTANCE.registerTarget(parent, {
425
onDragEnter: (e) => {
426
bounds = getOverlayBounds();
427
if (overlay && overlay.disposed) {
428
overlay = undefined;
429
}
430
431
if (!overlay && inBounds(bounds, e.eventData)) {
432
const dropData = e.dragAndDropData.getData();
433
if (dropData.type === 'view') {
434
435
const oldViewContainer = this.viewDescriptorService.getViewContainerByViewId(dropData.id);
436
const viewDescriptor = this.viewDescriptorService.getViewDescriptorById(dropData.id);
437
438
if (oldViewContainer !== this.viewContainer && (!viewDescriptor || !viewDescriptor.canMoveView || this.viewContainer.rejectAddedViews)) {
439
return;
440
}
441
442
overlay = new ViewPaneDropOverlay(parent, undefined, bounds, this.viewDescriptorService.getViewContainerLocation(this.viewContainer)!, this.themeService);
443
}
444
445
if (dropData.type === 'composite' && dropData.id !== this.viewContainer.id) {
446
const container = this.viewDescriptorService.getViewContainerById(dropData.id)!;
447
const viewsToMove = this.viewDescriptorService.getViewContainerModel(container).allViewDescriptors;
448
449
if (!viewsToMove.some(v => !v.canMoveView) && viewsToMove.length > 0) {
450
overlay = new ViewPaneDropOverlay(parent, undefined, bounds, this.viewDescriptorService.getViewContainerLocation(this.viewContainer)!, this.themeService);
451
}
452
}
453
}
454
},
455
onDragOver: (e) => {
456
if (overlay && overlay.disposed) {
457
overlay = undefined;
458
}
459
460
if (overlay && !inBounds(bounds, e.eventData)) {
461
overlay.dispose();
462
overlay = undefined;
463
}
464
465
if (inBounds(bounds, e.eventData)) {
466
toggleDropEffect(e.eventData.dataTransfer, 'move', overlay !== undefined);
467
}
468
},
469
onDragLeave: (e) => {
470
overlay?.dispose();
471
overlay = undefined;
472
},
473
onDrop: (e) => {
474
if (overlay) {
475
const dropData = e.dragAndDropData.getData();
476
const viewsToMove: IViewDescriptor[] = [];
477
478
if (dropData.type === 'composite' && dropData.id !== this.viewContainer.id) {
479
const container = this.viewDescriptorService.getViewContainerById(dropData.id)!;
480
const allViews = this.viewDescriptorService.getViewContainerModel(container).allViewDescriptors;
481
if (!allViews.some(v => !v.canMoveView)) {
482
viewsToMove.push(...allViews);
483
}
484
} else if (dropData.type === 'view') {
485
const oldViewContainer = this.viewDescriptorService.getViewContainerByViewId(dropData.id);
486
const viewDescriptor = this.viewDescriptorService.getViewDescriptorById(dropData.id);
487
if (oldViewContainer !== this.viewContainer && viewDescriptor && viewDescriptor.canMoveView) {
488
this.viewDescriptorService.moveViewsToContainer([viewDescriptor], this.viewContainer, undefined, 'dnd');
489
}
490
}
491
492
const paneCount = this.panes.length;
493
494
if (viewsToMove.length > 0) {
495
this.viewDescriptorService.moveViewsToContainer(viewsToMove, this.viewContainer, undefined, 'dnd');
496
}
497
498
if (paneCount > 0) {
499
for (const view of viewsToMove) {
500
const paneToMove = this.panes.find(p => p.id === view.id);
501
if (paneToMove) {
502
this.movePane(paneToMove, this.panes[this.panes.length - 1]);
503
}
504
}
505
}
506
}
507
508
overlay?.dispose();
509
overlay = undefined;
510
}
511
}));
512
513
this._register(this.onDidSashChange(() => this.saveViewSizes()));
514
this._register(this.viewContainerModel.onDidAddVisibleViewDescriptors(added => this.onDidAddViewDescriptors(added)));
515
this._register(this.viewContainerModel.onDidRemoveVisibleViewDescriptors(removed => this.onDidRemoveViewDescriptors(removed)));
516
const addedViews: IAddedViewDescriptorRef[] = this.viewContainerModel.visibleViewDescriptors.map((viewDescriptor, index) => {
517
const size = this.viewContainerModel.getSize(viewDescriptor.id);
518
const collapsed = this.viewContainerModel.isCollapsed(viewDescriptor.id);
519
return ({ viewDescriptor, index, size, collapsed });
520
});
521
if (addedViews.length) {
522
this.onDidAddViewDescriptors(addedViews);
523
}
524
525
// Update headers after and title contributed views after available, since we read from cache in the beginning to know if the viewlet has single view or not. Ref #29609
526
this.extensionService.whenInstalledExtensionsRegistered().then(() => {
527
this.areExtensionsReady = true;
528
if (this.panes.length) {
529
this.updateTitleArea();
530
this.updateViewHeaders();
531
}
532
this._register(this.configurationService.onDidChangeConfiguration(e => {
533
if (e.affectsConfiguration(LayoutSettings.ACTIVITY_BAR_LOCATION)) {
534
this.updateViewHeaders();
535
}
536
}));
537
});
538
539
this._register(this.viewContainerModel.onDidChangeActiveViewDescriptors(() => this._onTitleAreaUpdate.fire()));
540
}
541
542
getTitle(): string {
543
const containerTitle = this.viewContainerModel.title;
544
545
if (this.isViewMergedWithContainer()) {
546
const singleViewPaneContainerTitle = this.paneItems[0].pane.singleViewPaneContainerTitle;
547
if (singleViewPaneContainerTitle) {
548
return singleViewPaneContainerTitle;
549
}
550
551
const paneItemTitle = this.paneItems[0].pane.title;
552
if (containerTitle === paneItemTitle) {
553
return paneItemTitle;
554
}
555
556
return paneItemTitle ? `${containerTitle}: ${paneItemTitle}` : containerTitle;
557
}
558
559
return containerTitle;
560
}
561
562
private showContextMenu(event: StandardMouseEvent): void {
563
for (const paneItem of this.paneItems) {
564
// Do not show context menu if target is coming from inside pane views
565
if (isAncestor(event.target, paneItem.pane.element)) {
566
return;
567
}
568
}
569
570
event.stopPropagation();
571
event.preventDefault();
572
573
this.contextMenuService.showContextMenu({
574
getAnchor: () => event,
575
getActions: () => this.menuActions?.getContextMenuActions() ?? []
576
});
577
}
578
579
getActionsContext(): unknown {
580
if (this.isViewMergedWithContainer()) {
581
return this.panes[0].getActionsContext();
582
}
583
return undefined;
584
}
585
586
getActionViewItem(action: IAction, options: IBaseActionViewItemOptions): IActionViewItem | undefined {
587
if (this.isViewMergedWithContainer()) {
588
return this.paneItems[0].pane.createActionViewItem(action, options);
589
}
590
return createActionViewItem(this.instantiationService, action, options);
591
}
592
593
focus(): void {
594
let paneToFocus: ViewPane | undefined = undefined;
595
if (this.lastFocusedPane) {
596
paneToFocus = this.lastFocusedPane;
597
} else if (this.paneItems.length > 0) {
598
for (const { pane } of this.paneItems) {
599
if (pane.isExpanded()) {
600
paneToFocus = pane;
601
break;
602
}
603
}
604
}
605
if (paneToFocus) {
606
paneToFocus.focus();
607
}
608
}
609
610
private get orientation(): Orientation {
611
switch (this.viewDescriptorService.getViewContainerLocation(this.viewContainer)) {
612
case ViewContainerLocation.Sidebar:
613
case ViewContainerLocation.AuxiliaryBar:
614
return Orientation.VERTICAL;
615
case ViewContainerLocation.Panel: {
616
return isHorizontal(this.layoutService.getPanelPosition()) ? Orientation.HORIZONTAL : Orientation.VERTICAL;
617
}
618
}
619
620
return Orientation.VERTICAL;
621
}
622
623
layout(dimension: Dimension): void {
624
if (this.paneview) {
625
if (this.paneview.orientation !== this.orientation) {
626
this.paneview.flipOrientation(dimension.height, dimension.width);
627
}
628
629
this.paneview.layout(dimension.height, dimension.width);
630
}
631
632
this.dimension = dimension;
633
if (this.didLayout) {
634
this.saveViewSizes();
635
} else {
636
this.didLayout = true;
637
this.restoreViewSizes();
638
}
639
}
640
641
setBoundarySashes(sashes: IBoundarySashes): void {
642
this._boundarySashes = sashes;
643
this.paneview?.setBoundarySashes(sashes);
644
}
645
646
getOptimalWidth(): number {
647
const additionalMargin = 16;
648
const optimalWidth = Math.max(...this.panes.map(view => view.getOptimalWidth() || 0));
649
return optimalWidth + additionalMargin;
650
}
651
652
addPanes(panes: { pane: ViewPane; size: number; index?: number; disposable: IDisposable }[]): void {
653
const wasMerged = this.isViewMergedWithContainer();
654
655
for (const { pane, size, index, disposable } of panes) {
656
this.addPane(pane, size, disposable, index);
657
}
658
659
this.updateViewHeaders();
660
if (this.isViewMergedWithContainer() !== wasMerged) {
661
this.updateTitleArea();
662
}
663
664
this._onDidAddViews.fire(panes.map(({ pane }) => pane));
665
}
666
667
setVisible(visible: boolean): void {
668
if (this.visible !== !!visible) {
669
this.visible = visible;
670
671
this._onDidChangeVisibility.fire(visible);
672
}
673
674
this.panes.filter(view => view.isVisible() !== visible)
675
.map((view) => view.setVisible(visible));
676
}
677
678
isVisible(): boolean {
679
return this.visible;
680
}
681
682
protected updateTitleArea(): void {
683
this._onTitleAreaUpdate.fire();
684
}
685
686
protected createView(viewDescriptor: IViewDescriptor, options: IViewletViewOptions): ViewPane {
687
return (this.instantiationService as any).createInstance(viewDescriptor.ctorDescriptor.ctor, ...(viewDescriptor.ctorDescriptor.staticArguments || []), options) as ViewPane;
688
}
689
690
getView(id: string): ViewPane | undefined {
691
return this.panes.filter(view => view.id === id)[0];
692
}
693
694
private saveViewSizes(): void {
695
// Save size only when the layout has happened
696
if (this.didLayout) {
697
this.viewContainerModel.setSizes(this.panes.map(view => ({ id: view.id, size: this.getPaneSize(view) })));
698
}
699
}
700
701
private restoreViewSizes(): void {
702
// Restore sizes only when the layout has happened
703
if (this.didLayout) {
704
let initialSizes;
705
for (let i = 0; i < this.viewContainerModel.visibleViewDescriptors.length; i++) {
706
const pane = this.panes[i];
707
const viewDescriptor = this.viewContainerModel.visibleViewDescriptors[i];
708
const size = this.viewContainerModel.getSize(viewDescriptor.id);
709
710
if (typeof size === 'number') {
711
this.resizePane(pane, size);
712
} else {
713
initialSizes = initialSizes ? initialSizes : this.computeInitialSizes();
714
this.resizePane(pane, initialSizes.get(pane.id) || 200);
715
}
716
}
717
}
718
}
719
720
private computeInitialSizes(): Map<string, number> {
721
const sizes: Map<string, number> = new Map<string, number>();
722
if (this.dimension) {
723
const totalWeight = this.viewContainerModel.visibleViewDescriptors.reduce((totalWeight, { weight }) => totalWeight + (weight || 20), 0);
724
for (const viewDescriptor of this.viewContainerModel.visibleViewDescriptors) {
725
if (this.orientation === Orientation.VERTICAL) {
726
sizes.set(viewDescriptor.id, this.dimension.height * (viewDescriptor.weight || 20) / totalWeight);
727
} else {
728
sizes.set(viewDescriptor.id, this.dimension.width * (viewDescriptor.weight || 20) / totalWeight);
729
}
730
}
731
}
732
return sizes;
733
}
734
735
protected override saveState(): void {
736
this.panes.forEach((view) => view.saveState());
737
this.storageService.store(this.visibleViewsStorageId, this.length, StorageScope.WORKSPACE, StorageTarget.MACHINE);
738
}
739
740
private onContextMenu(event: StandardMouseEvent, viewPane: ViewPane): void {
741
event.stopPropagation();
742
event.preventDefault();
743
744
const actions: IAction[] = viewPane.menuActions.getContextMenuActions();
745
746
this.contextMenuService.showContextMenu({
747
getAnchor: () => event,
748
getActions: () => actions
749
});
750
}
751
752
openView(id: string, focus?: boolean): IView | undefined {
753
let view = this.getView(id);
754
if (!view) {
755
this.toggleViewVisibility(id);
756
}
757
view = this.getView(id);
758
if (view) {
759
view.setExpanded(true);
760
if (focus) {
761
view.focus();
762
}
763
}
764
return view;
765
}
766
767
protected onDidAddViewDescriptors(added: IAddedViewDescriptorRef[]): ViewPane[] {
768
const panesToAdd: { pane: ViewPane; size: number; index: number; disposable: IDisposable }[] = [];
769
770
for (const { viewDescriptor, collapsed, index, size } of added) {
771
const pane = this.createView(viewDescriptor,
772
{
773
id: viewDescriptor.id,
774
title: viewDescriptor.name.value,
775
fromExtensionId: (viewDescriptor as Partial<ICustomViewDescriptor>).extensionId,
776
expanded: !collapsed,
777
singleViewPaneContainerTitle: viewDescriptor.singleViewPaneContainerTitle,
778
});
779
780
try {
781
pane.render();
782
} catch (error) {
783
this.logService.error(`Fail to render view ${viewDescriptor.id}`, error);
784
continue;
785
}
786
if (pane.draggableElement) {
787
const contextMenuDisposable = addDisposableListener(pane.draggableElement, 'contextmenu', e => {
788
e.stopPropagation();
789
e.preventDefault();
790
this.onContextMenu(new StandardMouseEvent(getWindow(pane.draggableElement), e), pane);
791
});
792
793
const collapseDisposable = Event.latch(Event.map(pane.onDidChange, () => !pane.isExpanded()))(collapsed => {
794
this.viewContainerModel.setCollapsed(viewDescriptor.id, collapsed);
795
});
796
797
panesToAdd.push({ pane, size: size || pane.minimumSize, index, disposable: combinedDisposable(contextMenuDisposable, collapseDisposable) });
798
}
799
}
800
801
this.addPanes(panesToAdd);
802
this.restoreViewSizes();
803
804
const panes: ViewPane[] = [];
805
for (const { pane } of panesToAdd) {
806
pane.setVisible(this.isVisible());
807
panes.push(pane);
808
}
809
return panes;
810
}
811
812
private onDidRemoveViewDescriptors(removed: IViewDescriptorRef[]): void {
813
removed = removed.sort((a, b) => b.index - a.index);
814
const panesToRemove: ViewPane[] = [];
815
for (const { index } of removed) {
816
const paneItem = this.paneItems[index];
817
if (paneItem) {
818
panesToRemove.push(this.paneItems[index].pane);
819
}
820
}
821
822
if (panesToRemove.length) {
823
this.removePanes(panesToRemove);
824
825
for (const pane of panesToRemove) {
826
pane.setVisible(false);
827
}
828
}
829
}
830
831
toggleViewVisibility(viewId: string): void {
832
// Check if view is active
833
if (this.viewContainerModel.activeViewDescriptors.some(viewDescriptor => viewDescriptor.id === viewId)) {
834
const visible = !this.viewContainerModel.isVisible(viewId);
835
this.viewContainerModel.setVisible(viewId, visible);
836
}
837
}
838
839
private addPane(pane: ViewPane, size: number, disposable: IDisposable, index = this.paneItems.length - 1): void {
840
const onDidFocus = pane.onDidFocus(() => {
841
this._onDidFocusView.fire(pane);
842
this.lastFocusedPane = pane;
843
});
844
const onDidBlur = pane.onDidBlur(() => this._onDidBlurView.fire(pane));
845
const onDidChangeTitleArea = pane.onDidChangeTitleArea(() => {
846
if (this.isViewMergedWithContainer()) {
847
this.updateTitleArea();
848
}
849
});
850
851
const onDidChangeVisibility = pane.onDidChangeBodyVisibility(() => this._onDidChangeViewVisibility.fire(pane));
852
const onDidChange = pane.onDidChange(() => {
853
if (pane === this.lastFocusedPane && !pane.isExpanded()) {
854
this.lastFocusedPane = undefined;
855
}
856
});
857
858
const isPanel = this.viewDescriptorService.getViewContainerLocation(this.viewContainer) === ViewContainerLocation.Panel;
859
pane.style({
860
headerForeground: asCssVariable(isPanel ? PANEL_SECTION_HEADER_FOREGROUND : SIDE_BAR_SECTION_HEADER_FOREGROUND),
861
headerBackground: asCssVariable(isPanel ? PANEL_SECTION_HEADER_BACKGROUND : SIDE_BAR_SECTION_HEADER_BACKGROUND),
862
headerBorder: asCssVariable(isPanel ? PANEL_SECTION_HEADER_BORDER : SIDE_BAR_SECTION_HEADER_BORDER),
863
dropBackground: asCssVariable(isPanel ? PANEL_SECTION_DRAG_AND_DROP_BACKGROUND : SIDE_BAR_DRAG_AND_DROP_BACKGROUND),
864
leftBorder: isPanel ? asCssVariable(PANEL_SECTION_BORDER) : undefined
865
});
866
867
const store = new DisposableStore();
868
store.add(disposable);
869
store.add(combinedDisposable(pane, onDidFocus, onDidBlur, onDidChangeTitleArea, onDidChange, onDidChangeVisibility));
870
const paneItem: IViewPaneItem = { pane, disposable: store };
871
872
this.paneItems.splice(index, 0, paneItem);
873
assertReturnsDefined(this.paneview).addPane(pane, size, index);
874
875
let overlay: ViewPaneDropOverlay | undefined;
876
877
if (pane.draggableElement) {
878
store.add(CompositeDragAndDropObserver.INSTANCE.registerDraggable(pane.draggableElement, () => { return { type: 'view', id: pane.id }; }, {}));
879
}
880
881
store.add(CompositeDragAndDropObserver.INSTANCE.registerTarget(pane.dropTargetElement, {
882
onDragEnter: (e) => {
883
if (!overlay) {
884
const dropData = e.dragAndDropData.getData();
885
if (dropData.type === 'view' && dropData.id !== pane.id) {
886
887
const oldViewContainer = this.viewDescriptorService.getViewContainerByViewId(dropData.id);
888
const viewDescriptor = this.viewDescriptorService.getViewDescriptorById(dropData.id);
889
890
if (oldViewContainer !== this.viewContainer && (!viewDescriptor || !viewDescriptor.canMoveView || this.viewContainer.rejectAddedViews)) {
891
return;
892
}
893
894
overlay = new ViewPaneDropOverlay(pane.dropTargetElement, this.orientation ?? Orientation.VERTICAL, undefined, this.viewDescriptorService.getViewContainerLocation(this.viewContainer)!, this.themeService);
895
}
896
897
if (dropData.type === 'composite' && dropData.id !== this.viewContainer.id && !this.viewContainer.rejectAddedViews) {
898
const container = this.viewDescriptorService.getViewContainerById(dropData.id)!;
899
const viewsToMove = this.viewDescriptorService.getViewContainerModel(container).allViewDescriptors;
900
901
if (!viewsToMove.some(v => !v.canMoveView) && viewsToMove.length > 0) {
902
overlay = new ViewPaneDropOverlay(pane.dropTargetElement, this.orientation ?? Orientation.VERTICAL, undefined, this.viewDescriptorService.getViewContainerLocation(this.viewContainer)!, this.themeService);
903
}
904
}
905
}
906
},
907
onDragOver: (e) => {
908
toggleDropEffect(e.eventData.dataTransfer, 'move', overlay !== undefined);
909
},
910
onDragLeave: (e) => {
911
overlay?.dispose();
912
overlay = undefined;
913
},
914
onDrop: (e) => {
915
if (overlay) {
916
const dropData = e.dragAndDropData.getData();
917
const viewsToMove: IViewDescriptor[] = [];
918
let anchorView: IViewDescriptor | undefined;
919
920
if (dropData.type === 'composite' && dropData.id !== this.viewContainer.id && !this.viewContainer.rejectAddedViews) {
921
const container = this.viewDescriptorService.getViewContainerById(dropData.id)!;
922
const allViews = this.viewDescriptorService.getViewContainerModel(container).allViewDescriptors;
923
924
if (allViews.length > 0 && !allViews.some(v => !v.canMoveView)) {
925
viewsToMove.push(...allViews);
926
anchorView = allViews[0];
927
}
928
} else if (dropData.type === 'view') {
929
const oldViewContainer = this.viewDescriptorService.getViewContainerByViewId(dropData.id);
930
const viewDescriptor = this.viewDescriptorService.getViewDescriptorById(dropData.id);
931
if (oldViewContainer !== this.viewContainer && viewDescriptor && viewDescriptor.canMoveView && !this.viewContainer.rejectAddedViews) {
932
viewsToMove.push(viewDescriptor);
933
}
934
935
if (viewDescriptor) {
936
anchorView = viewDescriptor;
937
}
938
}
939
940
if (viewsToMove) {
941
this.viewDescriptorService.moveViewsToContainer(viewsToMove, this.viewContainer, undefined, 'dnd');
942
}
943
944
if (anchorView) {
945
if (overlay.currentDropOperation === DropDirection.DOWN ||
946
overlay.currentDropOperation === DropDirection.RIGHT) {
947
948
const fromIndex = this.panes.findIndex(p => p.id === anchorView!.id);
949
let toIndex = this.panes.findIndex(p => p.id === pane.id);
950
951
if (fromIndex >= 0 && toIndex >= 0) {
952
if (fromIndex > toIndex) {
953
toIndex++;
954
}
955
956
if (toIndex < this.panes.length && toIndex !== fromIndex) {
957
this.movePane(this.panes[fromIndex], this.panes[toIndex]);
958
}
959
}
960
}
961
962
if (overlay.currentDropOperation === DropDirection.UP ||
963
overlay.currentDropOperation === DropDirection.LEFT) {
964
const fromIndex = this.panes.findIndex(p => p.id === anchorView!.id);
965
let toIndex = this.panes.findIndex(p => p.id === pane.id);
966
967
if (fromIndex >= 0 && toIndex >= 0) {
968
if (fromIndex < toIndex) {
969
toIndex--;
970
}
971
972
if (toIndex >= 0 && toIndex !== fromIndex) {
973
this.movePane(this.panes[fromIndex], this.panes[toIndex]);
974
}
975
}
976
}
977
978
if (viewsToMove.length > 1) {
979
viewsToMove.slice(1).forEach(view => {
980
let toIndex = this.panes.findIndex(p => p.id === anchorView!.id);
981
const fromIndex = this.panes.findIndex(p => p.id === view.id);
982
if (fromIndex >= 0 && toIndex >= 0) {
983
if (fromIndex > toIndex) {
984
toIndex++;
985
}
986
987
if (toIndex < this.panes.length && toIndex !== fromIndex) {
988
this.movePane(this.panes[fromIndex], this.panes[toIndex]);
989
anchorView = view;
990
}
991
}
992
});
993
}
994
}
995
}
996
997
overlay?.dispose();
998
overlay = undefined;
999
}
1000
}));
1001
}
1002
1003
removePanes(panes: ViewPane[]): void {
1004
const wasMerged = this.isViewMergedWithContainer();
1005
1006
panes.forEach(pane => this.removePane(pane));
1007
1008
this.updateViewHeaders();
1009
if (wasMerged !== this.isViewMergedWithContainer()) {
1010
this.updateTitleArea();
1011
}
1012
1013
this._onDidRemoveViews.fire(panes);
1014
}
1015
1016
private removePane(pane: ViewPane): void {
1017
const index = this.paneItems.findIndex(i => i.pane === pane);
1018
1019
if (index === -1) {
1020
return;
1021
}
1022
1023
if (this.lastFocusedPane === pane) {
1024
this.lastFocusedPane = undefined;
1025
}
1026
1027
assertReturnsDefined(this.paneview).removePane(pane);
1028
const [paneItem] = this.paneItems.splice(index, 1);
1029
paneItem.disposable.dispose();
1030
1031
}
1032
1033
movePane(from: ViewPane, to: ViewPane): void {
1034
const fromIndex = this.paneItems.findIndex(item => item.pane === from);
1035
const toIndex = this.paneItems.findIndex(item => item.pane === to);
1036
1037
const fromViewDescriptor = this.viewContainerModel.visibleViewDescriptors[fromIndex];
1038
const toViewDescriptor = this.viewContainerModel.visibleViewDescriptors[toIndex];
1039
1040
if (fromIndex < 0 || fromIndex >= this.paneItems.length) {
1041
return;
1042
}
1043
1044
if (toIndex < 0 || toIndex >= this.paneItems.length) {
1045
return;
1046
}
1047
1048
const [paneItem] = this.paneItems.splice(fromIndex, 1);
1049
this.paneItems.splice(toIndex, 0, paneItem);
1050
1051
assertReturnsDefined(this.paneview).movePane(from, to);
1052
1053
this.viewContainerModel.move(fromViewDescriptor.id, toViewDescriptor.id);
1054
1055
this.updateTitleArea();
1056
}
1057
1058
resizePane(pane: ViewPane, size: number): void {
1059
assertReturnsDefined(this.paneview).resizePane(pane, size);
1060
}
1061
1062
getPaneSize(pane: ViewPane): number {
1063
return assertReturnsDefined(this.paneview).getPaneSize(pane);
1064
}
1065
1066
private updateViewHeaders(): void {
1067
if (this.isViewMergedWithContainer()) {
1068
if (this.paneItems[0].pane.isExpanded()) {
1069
this.lastMergedCollapsedPane = undefined;
1070
} else {
1071
this.lastMergedCollapsedPane = this.paneItems[0].pane;
1072
this.paneItems[0].pane.setExpanded(true);
1073
}
1074
this.paneItems[0].pane.headerVisible = false;
1075
this.paneItems[0].pane.collapsible = true;
1076
} else {
1077
if (this.paneItems.length === 1) {
1078
this.paneItems[0].pane.headerVisible = true;
1079
if (this.paneItems[0].pane === this.lastMergedCollapsedPane) {
1080
this.paneItems[0].pane.setExpanded(false);
1081
}
1082
this.paneItems[0].pane.collapsible = false;
1083
} else {
1084
this.paneItems.forEach(i => {
1085
i.pane.headerVisible = true;
1086
i.pane.collapsible = true;
1087
if (i.pane === this.lastMergedCollapsedPane) {
1088
i.pane.setExpanded(false);
1089
}
1090
});
1091
}
1092
this.lastMergedCollapsedPane = undefined;
1093
}
1094
}
1095
1096
isViewMergedWithContainer(): boolean {
1097
if (!(this.options.mergeViewWithContainerWhenSingleView && this.paneItems.length === 1)) {
1098
return false;
1099
}
1100
if (!this.areExtensionsReady) {
1101
if (this.visibleViewsCountFromCache === undefined) {
1102
return this.paneItems[0].pane.isExpanded();
1103
}
1104
// Check in cache so that view do not jump. See #29609
1105
return this.visibleViewsCountFromCache === 1;
1106
}
1107
return true;
1108
}
1109
1110
private onDidScrollPane() {
1111
for (const pane of this.panes) {
1112
pane.onDidScrollRoot();
1113
}
1114
}
1115
1116
private onDidSashReset(index: number) {
1117
let firstPane = undefined;
1118
let secondPane = undefined;
1119
1120
// Deal with collapsed views: to be clever, we split the space taken by the nearest uncollapsed views
1121
for (let i = index; i >= 0; i--) {
1122
if (this.paneItems[i].pane?.isVisible() && this.paneItems[i]?.pane.isExpanded()) {
1123
firstPane = this.paneItems[i].pane;
1124
break;
1125
}
1126
}
1127
1128
for (let i = index + 1; i < this.paneItems.length; i++) {
1129
if (this.paneItems[i].pane?.isVisible() && this.paneItems[i]?.pane.isExpanded()) {
1130
secondPane = this.paneItems[i].pane;
1131
break;
1132
}
1133
}
1134
1135
if (firstPane && secondPane) {
1136
const firstPaneSize = this.getPaneSize(firstPane);
1137
const secondPaneSize = this.getPaneSize(secondPane);
1138
1139
// Avoid rounding errors and be consistent when resizing
1140
// The first pane always get half rounded up and the second is half rounded down
1141
const newFirstPaneSize = Math.ceil((firstPaneSize + secondPaneSize) / 2);
1142
const newSecondPaneSize = Math.floor((firstPaneSize + secondPaneSize) / 2);
1143
1144
// Shrink the larger pane first, then grow the smaller pane
1145
// This prevents interfering with other view sizes
1146
if (firstPaneSize > secondPaneSize) {
1147
this.resizePane(firstPane, newFirstPaneSize);
1148
this.resizePane(secondPane, newSecondPaneSize);
1149
} else {
1150
this.resizePane(secondPane, newSecondPaneSize);
1151
this.resizePane(firstPane, newFirstPaneSize);
1152
}
1153
}
1154
}
1155
1156
override dispose(): void {
1157
super.dispose();
1158
this.paneItems.forEach(i => i.disposable.dispose());
1159
if (this.paneview) {
1160
this.paneview.dispose();
1161
}
1162
}
1163
}
1164
1165
export abstract class ViewPaneContainerAction<T extends IViewPaneContainer> extends Action2 {
1166
override readonly desc: Readonly<IAction2Options> & { viewPaneContainerId: string };
1167
constructor(desc: Readonly<IAction2Options> & { viewPaneContainerId: string }) {
1168
super(desc);
1169
this.desc = desc;
1170
}
1171
1172
run(accessor: ServicesAccessor, ...args: any[]): unknown {
1173
const viewPaneContainer = accessor.get(IViewsService).getActiveViewPaneContainerWithId(this.desc.viewPaneContainerId);
1174
if (viewPaneContainer) {
1175
return this.runInViewPaneContainer(accessor, <T>viewPaneContainer, ...args);
1176
}
1177
return undefined;
1178
}
1179
1180
abstract runInViewPaneContainer(accessor: ServicesAccessor, viewPaneContainer: T, ...args: any[]): unknown;
1181
}
1182
1183
class MoveViewPosition extends Action2 {
1184
constructor(desc: Readonly<IAction2Options>, private readonly offset: number) {
1185
super(desc);
1186
}
1187
1188
async run(accessor: ServicesAccessor): Promise<void> {
1189
const viewDescriptorService = accessor.get(IViewDescriptorService);
1190
const contextKeyService = accessor.get(IContextKeyService);
1191
1192
const viewId = FocusedViewContext.getValue(contextKeyService);
1193
if (viewId === undefined) {
1194
return;
1195
}
1196
1197
const viewContainer = viewDescriptorService.getViewContainerByViewId(viewId)!;
1198
const model = viewDescriptorService.getViewContainerModel(viewContainer);
1199
1200
const viewDescriptor = model.visibleViewDescriptors.find(vd => vd.id === viewId)!;
1201
const currentIndex = model.visibleViewDescriptors.indexOf(viewDescriptor);
1202
if (currentIndex + this.offset < 0 || currentIndex + this.offset >= model.visibleViewDescriptors.length) {
1203
return;
1204
}
1205
1206
const newPosition = model.visibleViewDescriptors[currentIndex + this.offset];
1207
1208
model.move(viewDescriptor.id, newPosition.id);
1209
}
1210
}
1211
1212
registerAction2(
1213
class MoveViewUp extends MoveViewPosition {
1214
constructor() {
1215
super({
1216
id: 'views.moveViewUp',
1217
title: nls.localize('viewMoveUp', "Move View Up"),
1218
keybinding: {
1219
primary: KeyChord(KeyMod.CtrlCmd + KeyCode.KeyK, KeyCode.UpArrow),
1220
weight: KeybindingWeight.WorkbenchContrib + 1,
1221
when: FocusedViewContext.notEqualsTo('')
1222
}
1223
}, -1);
1224
}
1225
}
1226
);
1227
1228
registerAction2(
1229
class MoveViewLeft extends MoveViewPosition {
1230
constructor() {
1231
super({
1232
id: 'views.moveViewLeft',
1233
title: nls.localize('viewMoveLeft', "Move View Left"),
1234
keybinding: {
1235
primary: KeyChord(KeyMod.CtrlCmd + KeyCode.KeyK, KeyCode.LeftArrow),
1236
weight: KeybindingWeight.WorkbenchContrib + 1,
1237
when: FocusedViewContext.notEqualsTo('')
1238
}
1239
}, -1);
1240
}
1241
}
1242
);
1243
1244
registerAction2(
1245
class MoveViewDown extends MoveViewPosition {
1246
constructor() {
1247
super({
1248
id: 'views.moveViewDown',
1249
title: nls.localize('viewMoveDown', "Move View Down"),
1250
keybinding: {
1251
primary: KeyChord(KeyMod.CtrlCmd + KeyCode.KeyK, KeyCode.DownArrow),
1252
weight: KeybindingWeight.WorkbenchContrib + 1,
1253
when: FocusedViewContext.notEqualsTo('')
1254
}
1255
}, 1);
1256
}
1257
}
1258
);
1259
1260
registerAction2(
1261
class MoveViewRight extends MoveViewPosition {
1262
constructor() {
1263
super({
1264
id: 'views.moveViewRight',
1265
title: nls.localize('viewMoveRight', "Move View Right"),
1266
keybinding: {
1267
primary: KeyChord(KeyMod.CtrlCmd + KeyCode.KeyK, KeyCode.RightArrow),
1268
weight: KeybindingWeight.WorkbenchContrib + 1,
1269
when: FocusedViewContext.notEqualsTo('')
1270
}
1271
}, 1);
1272
}
1273
}
1274
);
1275
1276
1277
registerAction2(class MoveViews extends Action2 {
1278
constructor() {
1279
super({
1280
id: 'vscode.moveViews',
1281
title: nls.localize('viewsMove', "Move Views"),
1282
});
1283
}
1284
1285
async run(accessor: ServicesAccessor, options: { viewIds: string[]; destinationId: string }): Promise<void> {
1286
if (!Array.isArray(options?.viewIds) || typeof options?.destinationId !== 'string') {
1287
return Promise.reject('Invalid arguments');
1288
}
1289
1290
const viewDescriptorService = accessor.get(IViewDescriptorService);
1291
1292
const destination = viewDescriptorService.getViewContainerById(options.destinationId);
1293
if (!destination) {
1294
return;
1295
}
1296
1297
// FYI, don't use `moveViewsToContainer` in 1 shot, because it expects all views to have the same current location
1298
for (const viewId of options.viewIds) {
1299
const viewDescriptor = viewDescriptorService.getViewDescriptorById(viewId);
1300
if (viewDescriptor?.canMoveView) {
1301
viewDescriptorService.moveViewsToContainer([viewDescriptor], destination, ViewVisibilityState.Default, this.desc.id);
1302
}
1303
}
1304
1305
await accessor.get(IViewsService).openViewContainer(destination.id, true);
1306
}
1307
});
1308
1309