diff --git a/packages/core/src/browser/shell/tab-bars.ts b/packages/core/src/browser/shell/tab-bars.ts index d7ee97c36c8c0..05161b5b472f5 100644 --- a/packages/core/src/browser/shell/tab-bars.ts +++ b/packages/core/src/browser/shell/tab-bars.ts @@ -145,7 +145,10 @@ export class TabBarRenderer extends TabBar.Renderer { onauxclick: (e: MouseEvent) => { // If user closes the tab using mouse wheel, nothing should be pasted to an active editor e.preventDefault(); - } + }, + ondragenter: this.handleDragEnterOrLeaveEvent, + ondragover: this.handleDragOverEvent, + ondragleave: this.handleDragEnterOrLeaveEvent }, h.div( { className: 'theia-tab-icon-label' }, @@ -439,11 +442,14 @@ export class TabBarRenderer extends TabBar.Renderer { } }; + protected getTitle(id: string): Title | undefined { + return this.tabBar && this.tabBar.titles.find(t => this.createTabId(t) === id); + } + protected handleDblClickEvent = (event: MouseEvent) => { if (this.tabBar && event.currentTarget instanceof HTMLElement) { const id = event.currentTarget.id; - // eslint-disable-next-line no-null/no-null - const title = this.tabBar.titles.find(t => this.createTabId(t) === id) || null; + const title = this.getTitle(id); const area = title && title.owner.parent; if (area instanceof TheiaDockPanel && (area.id === BOTTOM_AREA_ID || area.id === MAIN_AREA_ID)) { area.toggleMaximized(); @@ -451,6 +457,61 @@ export class TabBarRenderer extends TabBar.Renderer { } }; + isViewContainerDND(event: DragEvent): boolean { + const { dataTransfer } = event; + return !!dataTransfer && dataTransfer.types.indexOf('view-container-dnd') > -1; + } + + isSidebarDNDEvent(event: DragEvent): boolean { + return this.tabBar instanceof SideTabBar && this.isViewContainerDND(event); + } + + toCancelViewContainerDND = new DisposableCollection(); + protected handleDragEnterOrLeaveEvent = (event: DragEvent) => { + if (!this.isSidebarDNDEvent(event)) { + return; + } + this.toCancelViewContainerDND.dispose(); + }; + + protected handleDragOverEvent = (event: DragEvent) => { + if (!this.toCancelViewContainerDND.disposed) { + return; + } + if (!this.isSidebarDNDEvent(event)) { + if (event.dataTransfer) { + event.dataTransfer.dropEffect = 'none'; + } + return; + } + const { currentTarget, clientX, clientY } = event; + if (currentTarget instanceof HTMLElement) { + const { top, bottom, left, right, height } = currentTarget.getBoundingClientRect(); + const mouseOnTop = (clientY - top) < (height / 2); + const dropTargetClass = `drop-target-${mouseOnTop ? 'top' : 'bottom'}`; + currentTarget.className += ' ' + dropTargetClass; + this.toCancelViewContainerDND.push(Disposable.create(() => { + if (currentTarget) { + currentTarget.className = currentTarget.className.replace(dropTargetClass, ''); + } + })); + const openTabTimer = setTimeout(() => { + const title = this.getTitle(currentTarget.id); + if (title) { + const mouseStillOnTab = clientX >= left && clientX <= right && clientY >= top && clientY <= bottom; + if (mouseStillOnTab && this.tabBar) { + this.tabBar.currentTitle = title; + this.tabBar.setHidden(false); + this.tabBar.activate(); + } + } + }, 800); + this.toCancelViewContainerDND.push(Disposable.create(() => { + clearTimeout(openTabTimer); + })); + } + }; + } /** diff --git a/packages/core/src/browser/style/tabs.css b/packages/core/src/browser/style/tabs.css index 112fa4c685405..04f579b2a6469 100644 --- a/packages/core/src/browser/style/tabs.css +++ b/packages/core/src/browser/style/tabs.css @@ -320,6 +320,14 @@ body.theia-editor-highlightModifiedTabs align-items: center; } +.p-TabBar-tab.drop-target-top { + border-top: 2px solid white !important; +} + +.p-TabBar-tab.drop-target-bottom { + border-bottom: 2px solid white !important; +} + /*----------------------------------------------------------------------------- | Tab-bar toolbar |----------------------------------------------------------------------------*/ diff --git a/packages/core/src/browser/style/view-container.css b/packages/core/src/browser/style/view-container.css index 54f69957bf2ee..a07404b6a8cc1 100644 --- a/packages/core/src/browser/style/view-container.css +++ b/packages/core/src/browser/style/view-container.css @@ -98,7 +98,7 @@ } .theia-view-container .part.drop-target { - background: var(--theia-sideBar-dropBackground); + background: var(--theia-list-dropBackground); border: var(--theia-border-width) dashed var(--theia-contrastActiveBorder); transition-property: top, left, right, bottom; transition-duration: 150ms; diff --git a/packages/core/src/browser/view-container.ts b/packages/core/src/browser/view-container.ts index 97d4fb257be26..ccc2ba22e64ac 100644 --- a/packages/core/src/browser/view-container.ts +++ b/packages/core/src/browser/view-container.ts @@ -18,7 +18,7 @@ import { interfaces, injectable, inject, postConstruct } from 'inversify'; import { IIterator, toArray, find, some, every, map } from '@phosphor/algorithm'; import { Widget, EXPANSION_TOGGLE_CLASS, COLLAPSED_CLASS, CODICON_TREE_ITEM_CLASSES, MessageLoop, Message, SplitPanel, - BaseWidget, addEventListener, SplitLayout, LayoutItem, PanelLayout, addKeyListener, waitForRevealed + BaseWidget, addEventListener, SplitLayout, LayoutItem, PanelLayout, addKeyListener, waitForRevealed, DockPanel } from './widgets'; import { Event, Emitter } from '../common/event'; import { Disposable, DisposableCollection } from '../common/disposable'; @@ -33,6 +33,7 @@ import { WidgetManager } from './widget-manager'; import { TabBarToolbarRegistry, TabBarToolbarFactory, TabBarToolbar, TabBarDelegator, TabBarToolbarItem } from './shell/tab-bar-toolbar'; import { Key } from './keys'; import { ProgressBarFactory } from './progress-bar-factory'; +import { isEmpty } from '../common'; export interface ViewContainerTitleOptions { label: string; @@ -59,6 +60,13 @@ export class ViewContainer extends BaseWidget implements StatefulWidget, Applica protected currentPart: ViewContainerPart | undefined; + /** + * Disable dragging parts from/to this view container. + */ + protected get disableDNDBetweenContainers(): boolean { + return false; + } + @inject(FrontendApplicationStateService) protected readonly applicationStateService: FrontendApplicationStateService; @@ -112,6 +120,10 @@ export class ViewContainer extends BaseWidget implements StatefulWidget, Applica const { commandRegistry, menuRegistry, contextMenuRenderer } = this; this.toDispose.pushAll([ + addEventListener(this.node, 'dragenter', event => this.onDragEnterOrLeave(event)), + addEventListener(this.node, 'dragover', event => this.onDragOver(event)), + addEventListener(this.node, 'dragleave', event => this.onDragEnterOrLeave(event)), + addEventListener(this.node, 'drop', event => this.onDrop(event)), addEventListener(this.node, 'contextmenu', event => { if (event.button === 2 && every(this.containerLayout.iter(), part => !!part.isHidden)) { event.stopPropagation(); @@ -191,8 +203,8 @@ export class ViewContainer extends BaseWidget implements StatefulWidget, Applica this.toDisposeOnUpdateTitle.dispose(); this.toDispose.push(this.toDisposeOnUpdateTitle); this.updateTabBarDelegate(); - const title = this.titleOptions; - if (!title) { + let title = Object.assign({}, this.titleOptions); + if (isEmpty(title)) { return; } const allParts = this.getParts(); @@ -202,16 +214,26 @@ export class ViewContainer extends BaseWidget implements StatefulWidget, Applica const part = visibleParts[0]; this.toDisposeOnUpdateTitle.push(part.onTitleChanged(() => this.updateTitle())); const partLabel = part.wrapped.title.label; - if (partLabel) { + // Change the container title if it contains only one part that originally belongs to another container. + if (allParts.length === 1 && part.originalContainerId !== this.id) { + this.title.label = part.originalContainerTitle?.label || ''; + title = Object.assign({}, part.originalContainerTitle); + } + if (partLabel && this.title.label !== partLabel) { this.title.label += ': ' + partLabel; } part.collapsed = false; part.hideTitle(); } else { visibleParts.forEach(part => part.showTitle()); + // If at least one part originally belongs to this container the title should return to its original value. + const originalPart = allParts.find(p => p.originalContainerId === this.id); + if (originalPart && originalPart.originalContainerTitle) { + title = Object.assign({}, originalPart.originalContainerTitle); + } } this.updateToolbarItems(allParts); - const caption = title.caption || title.label; + const caption = title?.caption || title?.label; if (caption) { this.title.caption = caption; if (visibleParts.length === 1) { @@ -276,22 +298,31 @@ export class ViewContainer extends BaseWidget implements StatefulWidget, Applica protected readonly toRemoveWidgets = new Map(); - addWidget(widget: Widget, options?: ViewContainer.Factory.WidgetOptions): Disposable { + createPartId(widget: Widget): string { + const description = this.widgetManager.getDescription(widget); + return JSON.stringify(description) || widget.id; + } + + addWidget(widget: Widget, options?: ViewContainer.Factory.WidgetOptions, + originalContainerId?: string, originalContainerTitle?: ViewContainerTitleOptions): Disposable { const existing = this.toRemoveWidgets.get(widget.id); if (existing) { return existing; } + const partId = this.createPartId(widget); + const newPart = this.createPart(widget, partId, originalContainerId || this.id, originalContainerTitle || this.titleOptions, options); + return this.attachNewPart(newPart); + } + + attachNewPart(newPart: ViewContainerPart, insertIndex?: number): Disposable { const toRemoveWidget = new DisposableCollection(); this.toDispose.push(toRemoveWidget); - this.toRemoveWidgets.set(widget.id, toRemoveWidget); - toRemoveWidget.push(Disposable.create(() => this.toRemoveWidgets.delete(widget.id))); - - const description = this.widgetManager.getDescription(widget); - const partId = description ? JSON.stringify(description) : widget.id; - const newPart = this.createPart(widget, partId, options); + this.toRemoveWidgets.set(newPart.wrapped.id, toRemoveWidget); + toRemoveWidget.push(Disposable.create(() => this.toRemoveWidgets.delete(newPart.wrapped.id))); this.registerPart(newPart); - if (newPart.options && newPart.options.order !== undefined) { - const index = this.getParts().findIndex(part => part.options.order === undefined || part.options.order > newPart.options.order!); + if (insertIndex !== undefined || (newPart.options && newPart.options.order !== undefined)) { + const index = insertIndex !== undefined ? insertIndex + : this.getParts().findIndex(part => part.options.order === undefined || part.options.order > newPart.options.order!); if (index >= 0) { this.containerLayout.insertWidget(index, newPart); } else { @@ -343,8 +374,15 @@ export class ViewContainer extends BaseWidget implements StatefulWidget, Applica return toRemoveWidget; } - protected createPart(widget: Widget, partId: string, options?: ViewContainer.Factory.WidgetOptions | undefined): ViewContainerPart { - return new ViewContainerPart(widget, partId, this.id, this.toolbarRegistry, this.toolbarFactory, options); + protected createPart(widget: Widget, partId: string, originalContainerId: string, originalContainerTitle?: ViewContainerTitleOptions, + options?: ViewContainer.Factory.WidgetOptions): ViewContainerPart { + + return new ViewContainerPart(widget, partId, this.id, originalContainerId, originalContainerTitle, this.toolbarRegistry, this.toolbarFactory, options); + } + + protected createNewPartFromExisting(part: ViewContainerPart): ViewContainerPart { + const partId = this.createPartId(part.wrapped); + return new ViewContainerPart(part.wrapped, partId, this.id, part.originalContainerId, part.originalContainerTitle, this.toolbarRegistry, this.toolbarFactory, part.options); } removeWidget(widget: Widget): boolean { @@ -360,6 +398,13 @@ export class ViewContainer extends BaseWidget implements StatefulWidget, Applica return this.containerLayout.widgets; } + protected getPartIndex(partId: string | undefined): number { + if (partId) { + return this.getParts().findIndex(part => part.id === partId); + } + return -1; + } + getPartFor(widget: Widget): ViewContainerPart | undefined { return this.getParts().find(p => p.wrapped.id === widget.id); } @@ -402,7 +447,9 @@ export class ViewContainer extends BaseWidget implements StatefulWidget, Applica partId: part.partId, collapsed: part.collapsed, hidden: part.isHidden, - relativeSize: size && availableSize ? size / availableSize : undefined + relativeSize: size && availableSize ? size / availableSize : undefined, + originalContainerId: part.originalContainerId, + originalContainerTitle: part.originalContainerTitle }; }); return { parts: partStates, title: this.titleOptions }; @@ -417,7 +464,7 @@ export class ViewContainer extends BaseWidget implements StatefulWidget, Applica // restore widgets for (const part of state.parts) { if (part.widget) { - this.addWidget(part.widget); + this.addWidget(part.widget, undefined, part.originalContainerId, part.originalContainerTitle); } } const partStates = state.parts.filter(partState => some(this.containerLayout.iter(), p => p.partId === partState.partId)); @@ -425,7 +472,7 @@ export class ViewContainer extends BaseWidget implements StatefulWidget, Applica // Reorder the parts according to the stored state for (let index = 0; index < partStates.length; index++) { const partState = partStates[index]; - const currentIndex = this.getParts().findIndex(p => p.partId === partState.partId); + const currentIndex = this.getPartIndex(partState.partId); if (currentIndex > index) { this.containerLayout.moveWidget(currentIndex, index, this.getParts()[currentIndex]); } @@ -531,6 +578,54 @@ export class ViewContainer extends BaseWidget implements StatefulWidget, Applica } } + appendPart(part: ViewContainerPart, index: number): void { + const existing = this.toRemoveWidgets.get(part.wrapped.id); + if (existing) { + return; + } + const newPart = this.createNewPartFromExisting(part); + if (newPart.collapsed !== part.collapsed) { + newPart.collapsed = part.collapsed; + }; + this.attachNewPart(newPart, index); + part.fireOnDidRecreate(newPart, this); + } + + moveBetweenContainers(toMovedId: string, moveBeforeThisId: string | undefined, fromContainer: ViewContainer, fromContainerTitle?: ViewContainerTitleOptions): void { + const fromIndex = fromContainer.getPartIndex(toMovedId); + let toIndex = this.getPartIndex(moveBeforeThisId); + // Increase the index to insert the part after the dropped target but not before it. + toIndex += toIndex > -1 ? 1 : 0; + const partToMove = fromContainer.getParts()[fromIndex]; + fromContainer.removeWidget(partToMove); + this.appendPart(partToMove, toIndex); + fromContainer.updateTitle(); + } + + protected async movePart(fromContainerDescription: string, toMovedId: string, moveBeforeThisId?: string): Promise { + if (!fromContainerDescription) { + return; + } + const fromDescription = JSON.parse(fromContainerDescription); + const currentDescription = this.widgetManager.getDescription(this); + const areBothDebuggerContainers = fromDescription.debugWidgetId && this.parent?.parent?.id === fromDescription.debugWidgetId; + const sameContainers = areBothDebuggerContainers || currentDescription?.factoryId === fromDescription.factoryId; + if (sameContainers) { + return this.moveBefore(toMovedId, moveBeforeThisId!); + } + if (!sameContainers && !this.disableDNDBetweenContainers) { + const widget = await this.widgetManager.getWidget(fromDescription.factoryId || fromDescription.debugWidgetId, fromDescription.options); + if (widget && widget instanceof ViewContainer) { + return this.moveBetweenContainers(toMovedId, moveBeforeThisId, widget, widget.titleOptions); + } + if (widget && 'sessionWidget' in widget) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const debugWidget = widget as any; + return this.moveBetweenContainers(toMovedId, moveBeforeThisId, debugWidget['sessionWidget']['viewContainer'], debugWidget['title']); + } + } + } + getTrackableWidgets(): Widget[] { return this.getParts().map(w => w.wrapped); } @@ -597,49 +692,88 @@ export class ViewContainer extends BaseWidget implements StatefulWidget, Applica protected registerDND(part: ViewContainerPart): Disposable { part['header'].draggable = true; const style = (event: DragEvent) => { - if (!this.draggingPart) { + if ((!ViewContainer.isSupportedDND(event) && !ViewContainer.isInternalDND(event))) { + if (event.dataTransfer) { + event.dataTransfer.dropEffect = 'none'; + } return; } event.preventDefault(); + event.stopPropagation(); + if (event.dataTransfer) { + event.dataTransfer.dropEffect = 'move'; + } const enclosingPartNode = ViewContainerPart.closestPart(event.target); - if (enclosingPartNode && enclosingPartNode !== this.draggingPart.node) { - enclosingPartNode.classList.add('drop-target'); + + if (enclosingPartNode) { + if (!this.draggingPart || (this.draggingPart && this.draggingPart.node !== enclosingPartNode)) { + enclosingPartNode.classList.add('drop-target'); + } } }; const unstyle = (event: DragEvent) => { - if (!this.draggingPart) { + if (!ViewContainer.isSupportedDND(event) && !ViewContainer.isInternalDND(event)) { return; } event.preventDefault(); const enclosingPartNode = ViewContainerPart.closestPart(event.target); if (enclosingPartNode) { enclosingPartNode.classList.remove('drop-target'); + // const partIndex = this.getParts().findIndex(p => p.id === enclosingPartNode.id); + // const enclosingHandleNode = this.containerLayout.handles[partIndex]; + // enclosingHandleNode.classList.remove('drop-target'); } }; return new DisposableCollection( - addEventListener(part['header'], 'dragstart', event => { - const { dataTransfer } = event; - if (dataTransfer) { - this.draggingPart = part; - dataTransfer.effectAllowed = 'move'; - dataTransfer.setData('view-container-dnd', part.id); - const dragImage = document.createElement('div'); - dragImage.classList.add('theia-view-container-drag-image'); - dragImage.innerText = part.wrapped.title.label; - document.body.appendChild(dragImage); - dataTransfer.setDragImage(dragImage, -10, -10); - setTimeout(() => document.body.removeChild(dragImage), 0); + addEventListener(part['header'], 'dragstart', + event => { + const { dataTransfer } = event; + if (dataTransfer) { + this.draggingPart = part; + dataTransfer.effectAllowed = 'move'; + if (part.options.disableDraggingToOtherContainers || this.disableDNDBetweenContainers) { + dataTransfer.setData(ViewContainer.DataTransfers.INTERNAL_DND, part.id); + } else { + dataTransfer.setData(ViewContainer.DataTransfers.DND, part.id); + } + const desc = this.widgetManager.getDescription(this); + if (desc) { + dataTransfer.setData(ViewContainer.DataTransfers.FROM_CONTAINER_DESCRIPTION, JSON.stringify(desc)); + } else { + // The view container inside DebugWidget has no description, Setting `debugWidgetId` property instead. + if (this.parent?.parent && 'sessionWidget' in this.parent.parent) { + dataTransfer.setData(ViewContainer.DataTransfers.FROM_CONTAINER_DESCRIPTION, JSON.stringify({ debugWidgetId: this.parent.parent['id'] })); + } + } + const dragImage = document.createElement('div'); + dragImage.classList.add('theia-view-container-drag-image'); + dragImage.innerText = part.wrapped.title.label; + document.body.appendChild(dragImage); + dataTransfer.setDragImage(dragImage, -10, -10); + setTimeout(() => document.body.removeChild(dragImage), 0); + } } - }, false), + , false), addEventListener(part.node, 'dragend', () => this.draggingPart = undefined, false), addEventListener(part.node, 'dragover', style, false), addEventListener(part.node, 'dragleave', unstyle, false), addEventListener(part.node, 'drop', event => { + event.preventDefault(); + event.stopPropagation(); const { dataTransfer } = event; if (dataTransfer) { - const moveId = dataTransfer.getData('view-container-dnd'); - if (moveId && moveId !== part.id) { - this.moveBefore(moveId, part.id); + if (ViewContainer.isSupportedDND(event)) { + const moveId = dataTransfer.getData(ViewContainer.DataTransfers.DND); + const containerDescription = dataTransfer.getData(ViewContainer.DataTransfers.FROM_CONTAINER_DESCRIPTION); + if (moveId && moveId !== part.id) { + this.movePart(containerDescription, moveId, part.id); + } + this.toDisposeOnDragEnd.dispose(); + } else if (ViewContainer.isInternalDND(event)) { + const moveId = dataTransfer.getData(ViewContainer.DataTransfers.INTERNAL_DND); + if (moveId && moveId !== part.id) { + this.moveBefore(moveId, part.id); + } } unstyle(event); } @@ -647,6 +781,44 @@ export class ViewContainer extends BaseWidget implements StatefulWidget, Applica ); } + toDisposeOnDragEnd = new DisposableCollection(); + protected onDragEnterOrLeave(event: DragEvent): void { + if (!ViewContainer.isSupportedDND(event)) { + return; + } + this.toDisposeOnDragEnd.dispose(); + } + + protected onDragOver(event: DragEvent): void { + if (!ViewContainer.isSupportedDND(event) || this.disableDNDBetweenContainers) { + if (event.dataTransfer) { + event.dataTransfer.dropEffect = 'none'; + } + return; + } + if (this.parent instanceof DockPanel) { + const { overlay } = this.parent; + overlay.show({ top: 0, bottom: 0, right: 0, left: 0 }); + + this.toDisposeOnDragEnd.push(Disposable.create(() => + overlay.hide(100) + )); + } + } + + protected onDrop(event: DragEvent): void { + const { dataTransfer } = event; + if (!dataTransfer || !ViewContainer.isSupportedDND(event) || this.disableDNDBetweenContainers) { + return; + } + const moveId = dataTransfer.getData(ViewContainer.DataTransfers.DND); + const containerDescription = dataTransfer.getData(ViewContainer.DataTransfers.FROM_CONTAINER_DESCRIPTION); + if (moveId) { + this.movePart(containerDescription, moveId); + } + this.toDisposeOnDragEnd.dispose(); + } + } export namespace ViewContainer { @@ -664,6 +836,11 @@ export namespace ViewContainer { readonly initiallyCollapsed?: boolean; readonly canHide?: boolean; readonly initiallyHidden?: boolean; + /** + * Disable dragging this part from its original container to other containers, + * But allow dropping parts from other containers on it. + */ + readonly disableDraggingToOtherContainers?: boolean; } export interface WidgetDescriptor { @@ -684,6 +861,32 @@ export namespace ViewContainer { } return 'vertical'; } + + export function isSupportedDND(event: DragEvent): boolean { + const { dataTransfer } = event; + return !!dataTransfer && dataTransfer.types.indexOf(DataTransfers.DND) > -1; + } + + export function isInternalDND(event: DragEvent): boolean { + const { dataTransfer } = event; + return !!dataTransfer && dataTransfer.types.indexOf(DataTransfers.INTERNAL_DND) > -1; + } + + export const DataTransfers = { + /** + * Dnd between containers (including INTERNAL_DND). + */ + DND: 'view-container-dnd', + /** + * Allow dnd only inside the original container. + */ + INTERNAL_DND: 'view-container-internal-dnd', + /** + * The widget description of the container where the part was dragged from. + * (in case the view container belongs to 'DebugWidget' the description will only include the property: `debugWidgetId`) + */ + FROM_CONTAINER_DESCRIPTION: 'from-container-description', + }; } /** @@ -704,6 +907,8 @@ export class ViewContainerPart extends BaseWidget { readonly onTitleChanged = this.onTitleChangedEmitter.event; protected readonly onDidFocusEmitter = new Emitter(); readonly onDidFocus = this.onDidFocusEmitter.event; + protected readonly onDidRecreatePartEmitter = new Emitter<{ newPart: ViewContainerPart, container: ViewContainer }>(); + readonly onDidRecreatePart = this.onDidRecreatePartEmitter.event; protected _collapsed: boolean; @@ -715,7 +920,9 @@ export class ViewContainerPart extends BaseWidget { constructor( readonly wrapped: Widget, readonly partId: string, - viewContainerId: string, + readonly currentContainerId: string, + readonly originalContainerId: string, + readonly originalContainerTitle: ViewContainerTitleOptions | undefined, protected readonly toolbarRegistry: TabBarToolbarRegistry, protected readonly toolbarFactory: TabBarToolbarFactory, readonly options: ViewContainer.Factory.WidgetOptions = {} @@ -723,7 +930,7 @@ export class ViewContainerPart extends BaseWidget { super(); wrapped.parent = this; wrapped.disposed.connect(() => this.dispose()); - this.id = `${viewContainerId}--${wrapped.id}`; + this.id = `${currentContainerId}--${wrapped.id}`; this.addClass('part'); const fireTitleChanged = () => this.onTitleChangedEmitter.fire(undefined); @@ -782,6 +989,10 @@ export class ViewContainerPart extends BaseWidget { this.collapsedEmitter.fire(collapsed); } + fireOnDidRecreate(newPart: ViewContainerPart, container: ViewContainer): void { + this.onDidRecreatePartEmitter.fire({ newPart, container }); + }; + setHidden(hidden: boolean): void { if (!this.canHide) { return; @@ -884,7 +1095,14 @@ export class ViewContainerPart extends BaseWidget { const title = document.createElement('span'); title.classList.add('label', 'noselect'); - const updateTitle = () => title.innerText = this.wrapped.title.label; + const updateTitle = () => { + if (this.currentContainerId !== this.originalContainerId && this.originalContainerTitle) { + // Creating title in format: : . + title.innerText = this.originalContainerTitle.label + ': ' + this.wrapped.title.label; + } else { + title.innerText = this.wrapped.title.label; + } + }; const updateCaption = () => title.title = this.wrapped.title.caption || this.wrapped.title.label; updateTitle(); updateCaption(); @@ -1032,6 +1250,9 @@ export namespace ViewContainerPart { collapsed: boolean; hidden: boolean; relativeSize?: number; + /** The original container to which this part belongs */ + originalContainerId: string; + originalContainerTitle?: ViewContainerTitleOptions; } export function closestPart(element: Element | EventTarget | null, selector: string = 'div.part'): Element | undefined { @@ -1051,6 +1272,17 @@ export class ViewContainerLayout extends SplitLayout { super(options); } + protected onAfterAttach(msg: Message): void { + this.handles.forEach(handle => { + // In case of `dragover` event on the part `handle`, Need to stop propagation to prevent the parent handler execution. + addEventListener(handle, 'dragover', event => { + event.preventDefault(); + event.stopPropagation(); + }); + }); + super.onAfterAttach(msg); + } + protected get items(): ReadonlyArray { // eslint-disable-next-line @typescript-eslint/no-explicit-any return (this as any)._items as Array; diff --git a/packages/navigator/src/browser/navigator-widget-factory.ts b/packages/navigator/src/browser/navigator-widget-factory.ts index c43ac24dcd329..17d46109dfaec 100644 --- a/packages/navigator/src/browser/navigator-widget-factory.ts +++ b/packages/navigator/src/browser/navigator-widget-factory.ts @@ -50,7 +50,8 @@ export class NavigatorWidgetFactory implements WidgetFactory { order: 1, canHide: false, initiallyCollapsed: false, - weight: 80 + weight: 80, + disableDraggingToOtherContainers: true }; @inject(ViewContainer.Factory) diff --git a/packages/plugin-ext/src/main/browser/view/plugin-view-registry.ts b/packages/plugin-ext/src/main/browser/view/plugin-view-registry.ts index e8b9222b5e115..54b22ef329fd2 100644 --- a/packages/plugin-ext/src/main/browser/view/plugin-view-registry.ts +++ b/packages/plugin-ext/src/main/browser/view/plugin-view-registry.ts @@ -18,7 +18,7 @@ import { injectable, inject, postConstruct, optional } from '@theia/core/shared/ import { ApplicationShell, ViewContainer as ViewContainerWidget, WidgetManager, ViewContainerIdentifier, ViewContainerTitleOptions, Widget, FrontendApplicationContribution, - StatefulWidget, CommonMenus, BaseWidget, TreeViewWelcomeWidget + StatefulWidget, CommonMenus, BaseWidget, TreeViewWelcomeWidget, ViewContainerPart } from '@theia/core/lib/browser'; import { ViewContainer, View, ViewWelcome } from '../../../common'; import { PluginSharedStyle } from '../plugin-shared-style'; @@ -34,13 +34,13 @@ import { MenuModelRegistry } from '@theia/core/lib/common/menu'; import { QuickViewService } from '@theia/core/lib/browser'; import { Emitter } from '@theia/core/lib/common/event'; import { ContextKeyService } from '@theia/core/lib/browser/context-key-service'; -import { SearchInWorkspaceWidget } from '@theia/search-in-workspace/lib/browser/search-in-workspace-widget'; import { ViewContextKeyService } from './view-context-key-service'; import { PROBLEMS_WIDGET_ID } from '@theia/markers/lib/browser/problem/problem-widget'; import { OUTPUT_WIDGET_KIND } from '@theia/output/lib/browser/output-widget'; import { DebugConsoleContribution } from '@theia/debug/lib/browser/console/debug-console-contribution'; import { TERMINAL_WIDGET_FACTORY_ID } from '@theia/terminal/lib/browser/terminal-widget-impl'; import { TreeViewWidget } from './tree-view-widget'; +import { SEARCH_VIEW_CONTAINER_ID } from '@theia/search-in-workspace/lib/browser/search-in-workspace-factory'; export const PLUGIN_VIEW_FACTORY_ID = 'plugin-view'; export const PLUGIN_VIEW_CONTAINER_FACTORY_ID = 'plugin-view-container'; @@ -100,8 +100,8 @@ export class PluginViewRegistry implements FrontendApplicationContribution { protected init(): void { // VS Code Viewlets this.trackVisibleWidget(EXPLORER_VIEW_CONTAINER_ID, { viewletId: 'workbench.view.explorer' }); - this.trackVisibleWidget(SearchInWorkspaceWidget.ID, { viewletId: 'workbench.view.search', sideArea: true }); this.trackVisibleWidget(SCM_VIEW_CONTAINER_ID, { viewletId: 'workbench.view.scm' }); + this.trackVisibleWidget(SEARCH_VIEW_CONTAINER_ID, { viewletId: 'workbench.view.search' }); this.trackVisibleWidget(DebugWidget.ID, { viewletId: 'workbench.view.debug' }); // TODO workbench.view.extensions - Theia does not have a proper extension view yet @@ -111,7 +111,6 @@ export class PluginViewRegistry implements FrontendApplicationContribution { this.trackVisibleWidget(DebugConsoleContribution.options.id, { panelId: 'workbench.panel.repl' }); this.trackVisibleWidget(TERMINAL_WIDGET_FACTORY_ID, { panelId: 'workbench.panel.terminal' }); // TODO workbench.panel.comments - Theia does not have a proper comments view yet - this.trackVisibleWidget(SearchInWorkspaceWidget.ID, { panelId: 'workbench.view.search', sideArea: false }); this.updateFocusedView(); this.shell.onDidChangeActiveWidget(() => this.updateFocusedView()); @@ -123,6 +122,9 @@ export class PluginViewRegistry implements FrontendApplicationContribution { if (factoryId === SCM_VIEW_CONTAINER_ID && widget instanceof ViewContainerWidget) { waitUntil(this.prepareViewContainer('scm', widget)); } + if (factoryId === SEARCH_VIEW_CONTAINER_ID && widget instanceof ViewContainerWidget) { + waitUntil(this.prepareViewContainer('search', widget)); + } if (factoryId === DebugWidget.ID && widget instanceof DebugWidget) { const viewContainer = widget['sessionWidget']['viewContainer']; waitUntil(this.prepareViewContainer('debug', viewContainer)); @@ -288,6 +290,12 @@ export class PluginViewRegistry implements FrontendApplicationContribution { const toDispose = new DisposableCollection(); view.when = view.when?.trim(); + // const existWidget = this.widgetManager.tryGetWidget(PLUGIN_VIEW_FACTORY_ID, this.toPluginViewWidgetIdentifier(view.id)); + // const { lastDroppedContainer } = existWidget || {}; + // if (lastDroppedContainer && this.fromViewContainerIdentifier(lastDroppedContainer) !== viewContainerId) { + // const containerId = this.fromViewContainerIdentifier(lastDroppedContainer); + // viewContainerId = ViewContainerIdentifiers[containerId] || containerId; + // } this.views.set(view.id, [viewContainerId, view]); toDispose.push(Disposable.create(() => this.views.delete(view.id))); @@ -401,7 +409,9 @@ export class PluginViewRegistry implements FrontendApplicationContribution { return; } const [, view] = data; - widget.title.label = view.name; + if (!widget.title.label) { + widget.title.label = view.name; + } const currentDataWidget = widget.widgets[0]; const viewDataWidget = await this.createViewDataWidget(view.id); if (widget.isDisposed) { @@ -466,6 +476,14 @@ export class PluginViewRegistry implements FrontendApplicationContribution { } for (const viewId of this.getContainerViews(viewContainerId)) { const identifier = this.toPluginViewWidgetIdentifier(viewId); + // Keep exising widget in its current container and reregister its part to the plugin view widget events. + const existingWidget = this.widgetManager.tryGetWidget(PLUGIN_VIEW_FACTORY_ID, identifier); + if (existingWidget && existingWidget.currentViewContainerId) { + const currentContainer = await this.getPluginViewContainer(existingWidget.currentViewContainerId); + if (currentContainer && this.registerWidgetPartEvents(existingWidget, containerWidget)) { + continue; + } + } const widget = await this.widgetManager.getOrCreateWidget(PLUGIN_VIEW_FACTORY_ID, identifier); if (containerWidget.getTrackableWidgets().indexOf(widget) === -1) { containerWidget.addWidget(widget, { @@ -473,27 +491,57 @@ export class PluginViewRegistry implements FrontendApplicationContribution { initiallyHidden: !this.isViewVisible(viewId) }); } - const part = containerWidget.getPartFor(widget); - if (part) { - // if a view is explicitly hidden then suppress updating visibility based on `when` closure - part.onDidChangeVisibility(() => widget.suppressUpdateViewVisibility = part.isHidden); + this.registerWidgetPartEvents(widget, containerWidget); + } + } - const tryFireOnDidExpandView = () => { - if (widget.widgets.length === 0) { - if (!part.collapsed && part.isVisible) { - this.onDidExpandViewEmitter.fire(viewId); - } - } else { - toFire.dispose(); - } - }; - const toFire = new DisposableCollection( - part.onCollapsed(tryFireOnDidExpandView), - part.onDidChangeVisibility(tryFireOnDidExpandView) - ); + protected registerWidgetPartEvents(widget: PluginViewWidget, containerWidget: ViewContainerWidget): ViewContainerPart | undefined { + const part = containerWidget.getPartFor(widget); + if (part) { + this.onPartAttached(part, containerWidget); + part.onDidRecreatePart(event => { + this.onPartAttached(event.newPart, event.container); + }); + return part; + } + } + + protected onPartAttached = (part: ViewContainerPart, containerWidget: ViewContainerWidget) => { + const widget = part.wrapped; + if (!(widget instanceof PluginViewWidget)) { + return; + } + widget.currentViewContainerId = this.getViewContainerId(containerWidget); + // if a view is explicitly hidden then suppress updating visibility based on `when` closure + part.onDidChangeVisibility(() => widget.suppressUpdateViewVisibility = part.isHidden); - tryFireOnDidExpandView(); + const tryFireOnDidExpandView = () => { + if (widget.widgets.length === 0) { + if (!part.collapsed && part.isVisible) { + const viewId = this.toViewId(widget.options); + this.onDidExpandViewEmitter.fire(viewId); + } + } else { + toFire.dispose(); } + }; + const toFire = new DisposableCollection( + part.onCollapsed(tryFireOnDidExpandView), + part.onDidChangeVisibility(tryFireOnDidExpandView) + ); + + tryFireOnDidExpandView(); + }; + + protected getViewContainerId(container: ViewContainerWidget): string | undefined { + const description = this.widgetManager.getDescription(container); + switch (description?.factoryId) { + case EXPLORER_VIEW_CONTAINER_ID: return 'explorer'; + case SCM_VIEW_CONTAINER_ID: return 'scm'; + case SEARCH_VIEW_CONTAINER_ID: return 'search'; + case EXPLORER_VIEW_CONTAINER_ID: return 'explorer'; + case undefined: return container.parent?.parent instanceof DebugWidget ? 'debug' : undefined; + default: return description?.factoryId; } } @@ -504,6 +552,9 @@ export class PluginViewRegistry implements FrontendApplicationContribution { if (viewContainerId === 'scm') { return this.widgetManager.getWidget(SCM_VIEW_CONTAINER_ID); } + if (viewContainerId === 'search') { + return this.widgetManager.getWidget(SEARCH_VIEW_CONTAINER_ID); + } if (viewContainerId === 'debug') { const debug = await this.widgetManager.getWidget(DebugWidget.ID); if (debug instanceof DebugWidget) { @@ -546,6 +597,12 @@ export class PluginViewRegistry implements FrontendApplicationContribution { await this.prepareViewContainer('scm', scm); } })().catch(console.error)); + promises.push((async () => { + const search = await this.widgetManager.getWidget(SEARCH_VIEW_CONTAINER_ID); + if (search instanceof ViewContainerWidget) { + await this.prepareViewContainer('search', search); + } + })().catch(console.error)); promises.push((async () => { const debug = await this.widgetManager.getWidget(DebugWidget.ID); if (debug instanceof DebugWidget) { diff --git a/packages/plugin-ext/src/main/browser/view/plugin-view-widget.ts b/packages/plugin-ext/src/main/browser/view/plugin-view-widget.ts index 1a5e323ca18ab..8decdf778bb7e 100644 --- a/packages/plugin-ext/src/main/browser/view/plugin-view-widget.ts +++ b/packages/plugin-ext/src/main/browser/view/plugin-view-widget.ts @@ -44,6 +44,8 @@ export class PluginViewWidget extends Panel implements StatefulWidget { @inject(PluginViewWidgetIdentifier) readonly options: PluginViewWidgetIdentifier; + currentViewContainerId: string | undefined; + constructor() { super(); this.node.tabIndex = -1; @@ -71,7 +73,8 @@ export class PluginViewWidget extends Panel implements StatefulWidget { label: this.title.label, message: this.message, widgets: this.widgets, - suppressUpdateViewVisibility: this._suppressUpdateViewVisibility + suppressUpdateViewVisibility: this._suppressUpdateViewVisibility, + currentViewContainerId: this.currentViewContainerId }; } @@ -79,6 +82,7 @@ export class PluginViewWidget extends Panel implements StatefulWidget { this.title.label = state.label; this.message = state.message; this.suppressUpdateViewVisibility = state.suppressUpdateViewVisibility; + this.currentViewContainerId = state.currentViewContainerId; for (const widget of state.widgets) { this.addWidget(widget); } @@ -136,6 +140,7 @@ export namespace PluginViewWidget { label: string, message?: string, widgets: ReadonlyArray, - suppressUpdateViewVisibility: boolean + suppressUpdateViewVisibility: boolean; + currentViewContainerId: string | undefined; } } diff --git a/packages/scm/src/browser/scm-frontend-module.ts b/packages/scm/src/browser/scm-frontend-module.ts index fceed3cd60ff6..6062156799e27 100644 --- a/packages/scm/src/browser/scm-frontend-module.ts +++ b/packages/scm/src/browser/scm-frontend-module.ts @@ -96,7 +96,8 @@ export default new ContainerModule(bind => { const widget = await container.get(WidgetManager).getOrCreateWidget(SCM_WIDGET_FACTORY_ID); viewContainer.addWidget(widget, { canHide: false, - initiallyCollapsed: false + initiallyCollapsed: false, + disableDraggingToOtherContainers: true }); return viewContainer; } diff --git a/packages/search-in-workspace/src/browser/search-in-workspace-factory.ts b/packages/search-in-workspace/src/browser/search-in-workspace-factory.ts new file mode 100644 index 0000000000000..0a10bd8542892 --- /dev/null +++ b/packages/search-in-workspace/src/browser/search-in-workspace-factory.ts @@ -0,0 +1,60 @@ +/******************************************************************************** + * Copyright (C) 2021 SAP SE or an SAP affiliate company and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import { inject, injectable } from '@theia/core/shared/inversify'; +import { + ViewContainer, + ViewContainerTitleOptions, + WidgetFactory, + WidgetManager +} from '@theia/core/lib/browser'; +import { SearchInWorkspaceWidget } from './search-in-workspace-widget'; + +export const SEARCH_VIEW_CONTAINER_ID = 'search-view-container'; +export const SEARCH_VIEW_CONTAINER_TITLE_OPTIONS: ViewContainerTitleOptions = { + label: 'Search', + iconClass: 'search-in-workspace-tab-icon', + closeable: true +}; + +@injectable() +export class SearchInWorkspaceFactory implements WidgetFactory { + + static ID = SEARCH_VIEW_CONTAINER_ID; + + readonly id = SearchInWorkspaceFactory.ID; + + protected searchWidgetOptions: ViewContainer.Factory.WidgetOptions = { + canHide: false, + initiallyCollapsed: false, + disableDraggingToOtherContainers: true + }; + + @inject(ViewContainer.Factory) + protected readonly viewContainerFactory: ViewContainer.Factory; + @inject(WidgetManager) protected readonly widgetManager: WidgetManager; + + async createWidget(): Promise { + const viewContainer = this.viewContainerFactory({ + id: SEARCH_VIEW_CONTAINER_ID, + progressLocationId: 'search' + }); + viewContainer.setTitleOptions(SEARCH_VIEW_CONTAINER_TITLE_OPTIONS); + const widget = await this.widgetManager.getOrCreateWidget(SearchInWorkspaceWidget.ID); + viewContainer.addWidget(widget, this.searchWidgetOptions); + return viewContainer; + } +} diff --git a/packages/search-in-workspace/src/browser/search-in-workspace-frontend-contribution.ts b/packages/search-in-workspace/src/browser/search-in-workspace-frontend-contribution.ts index 8e63aee1e6c9c..812a0a0698f46 100644 --- a/packages/search-in-workspace/src/browser/search-in-workspace-frontend-contribution.ts +++ b/packages/search-in-workspace/src/browser/search-in-workspace-frontend-contribution.ts @@ -28,6 +28,7 @@ import { TabBarToolbarContribution, TabBarToolbarRegistry } from '@theia/core/li import { EditorManager } from '@theia/editor/lib/browser/editor-manager'; import { Range } from '@theia/core/shared/vscode-languageserver-types'; import { FileService } from '@theia/filesystem/lib/browser/file-service'; +import { SEARCH_VIEW_CONTAINER_ID } from './search-in-workspace-factory'; export namespace SearchInWorkspaceCommands { const SEARCH_CATEGORY = 'Search'; @@ -91,6 +92,7 @@ export class SearchInWorkspaceFrontendContribution extends AbstractViewContribut constructor() { super({ + viewContainerId: SEARCH_VIEW_CONTAINER_ID, widgetId: SearchInWorkspaceWidget.ID, widgetName: SearchInWorkspaceWidget.LABEL, defaultWidgetOptions: { diff --git a/packages/search-in-workspace/src/browser/search-in-workspace-frontend-module.ts b/packages/search-in-workspace/src/browser/search-in-workspace-frontend-module.ts index ee0814cca1bcb..a833c3b9c5271 100644 --- a/packages/search-in-workspace/src/browser/search-in-workspace-frontend-module.ts +++ b/packages/search-in-workspace/src/browser/search-in-workspace-frontend-module.ts @@ -20,7 +20,8 @@ import { ContainerModule, interfaces } from '@theia/core/shared/inversify'; import { SearchInWorkspaceService, SearchInWorkspaceClientImpl } from './search-in-workspace-service'; import { SearchInWorkspaceServer, SIW_WS_PATH } from '../common/search-in-workspace-interface'; import { - WebSocketConnectionProvider, WidgetFactory, createTreeContainer, TreeWidget, bindViewContribution, FrontendApplicationContribution, LabelProviderContribution + WebSocketConnectionProvider, WidgetFactory, createTreeContainer, TreeWidget, bindViewContribution, FrontendApplicationContribution, LabelProviderContribution, + ApplicationShellLayoutMigration } from '@theia/core/lib/browser'; import { SearchInWorkspaceWidget } from './search-in-workspace-widget'; import { SearchInWorkspaceResultTreeWidget } from './search-in-workspace-result-tree-widget'; @@ -29,6 +30,8 @@ import { SearchInWorkspaceContextKeyService } from './search-in-workspace-contex import { TabBarToolbarContribution } from '@theia/core/lib/browser/shell/tab-bar-toolbar'; import { bindSearchInWorkspacePreferences } from './search-in-workspace-preferences'; import { SearchInWorkspaceLabelProvider } from './search-in-workspace-label-provider'; +import { SearchInWorkspaceFactory } from './search-in-workspace-factory'; +import { SearchLayoutVersion3Migration } from './search-layout-migrations'; export default new ContainerModule(bind => { bind(SearchInWorkspaceContextKeyService).toSelf().inSingletonScope(); @@ -39,6 +42,9 @@ export default new ContainerModule(bind => { createWidget: () => ctx.container.get(SearchInWorkspaceWidget) })); bind(SearchInWorkspaceResultTreeWidget).toDynamicValue(ctx => createSearchTreeWidget(ctx.container)); + bind(SearchInWorkspaceFactory).toSelf().inSingletonScope(); + bind(WidgetFactory).toService(SearchInWorkspaceFactory); + bind(ApplicationShellLayoutMigration).to(SearchLayoutVersion3Migration).inSingletonScope(); bindViewContribution(bind, SearchInWorkspaceFrontendContribution); bind(FrontendApplicationContribution).toService(SearchInWorkspaceFrontendContribution); diff --git a/packages/search-in-workspace/src/browser/search-in-workspace-widget.tsx b/packages/search-in-workspace/src/browser/search-in-workspace-widget.tsx index 8161b43ec9c99..f9947cfccea3e 100644 --- a/packages/search-in-workspace/src/browser/search-in-workspace-widget.tsx +++ b/packages/search-in-workspace/src/browser/search-in-workspace-widget.tsx @@ -111,6 +111,7 @@ export class SearchInWorkspaceWidget extends BaseWidget implements StatefulWidge this.searchFormContainer = document.createElement('div'); this.searchFormContainer.classList.add('searchHeader'); this.contentNode.appendChild(this.searchFormContainer); + this.node.tabIndex = 0; this.node.appendChild(this.contentNode); this.matchCaseState = { diff --git a/packages/search-in-workspace/src/browser/search-layout-migrations.ts b/packages/search-in-workspace/src/browser/search-layout-migrations.ts new file mode 100644 index 0000000000000..f0a63b6af314e --- /dev/null +++ b/packages/search-in-workspace/src/browser/search-layout-migrations.ts @@ -0,0 +1,53 @@ +/******************************************************************************** + * Copyright (C) 2021 SAP SE or an SAP affiliate company and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import { injectable } from '@theia/core/shared/inversify'; +import { ApplicationShellLayoutMigration, WidgetDescription, ApplicationShellLayoutMigrationContext } from '@theia/core/lib/browser/shell/shell-layout-restorer'; +import { SearchInWorkspaceWidget } from './search-in-workspace-widget'; +import { SEARCH_VIEW_CONTAINER_ID, SEARCH_VIEW_CONTAINER_TITLE_OPTIONS } from './search-in-workspace-factory'; + +@injectable() +export class SearchLayoutVersion3Migration implements ApplicationShellLayoutMigration { + readonly layoutVersion = 3.0; + onWillInflateWidget(desc: WidgetDescription, { parent }: ApplicationShellLayoutMigrationContext): WidgetDescription | undefined { + if (desc.constructionOptions.factoryId === SearchInWorkspaceWidget.ID && !parent) { + return { + constructionOptions: { + factoryId: SEARCH_VIEW_CONTAINER_ID + }, + innerWidgetState: { + parts: [ + { + widget: { + constructionOptions: { + factoryId: SearchInWorkspaceWidget.ID + }, + innerWidgetState: desc.innerWidgetState + }, + partId: { + factoryId: SearchInWorkspaceWidget.ID + }, + collapsed: false, + hidden: false + } + ], + title: SEARCH_VIEW_CONTAINER_TITLE_OPTIONS + } + }; + } + return undefined; + } +} diff --git a/packages/vsx-registry/src/browser/vsx-extensions-view-container.ts b/packages/vsx-registry/src/browser/vsx-extensions-view-container.ts index 92e1f5710fc90..940dbe74b9ace 100644 --- a/packages/vsx-registry/src/browser/vsx-extensions-view-container.ts +++ b/packages/vsx-registry/src/browser/vsx-extensions-view-container.ts @@ -29,6 +29,10 @@ export class VSXExtensionsViewContainer extends ViewContainer { static ID = 'vsx-extensions-view-container'; static LABEL = 'Extensions'; + protected get disableDNDBetweenContainers(): boolean { + return true; + } + @inject(VSXExtensionsSearchBar) protected readonly searchBar: VSXExtensionsSearchBar;