diff --git a/CHANGELOG.md b/CHANGELOG.md index c158c6621db42..5adae97ac6a35 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ - [core] added `BreadcrumbsRendererFactory` to constructor arguments of `DockPanelRenderer` and `ToolbarAwareTabBar`. [#9920](https://github.com/eclipse-theia/theia/pull/9920) - [task] `TaskDefinition.properties.required` is now optional to align with the specification [#10015](https://github.com/eclipse-theia/theia/pull/10015) - [core] `setTopPanelVisibily` renamed to `setTopPanelVisibility` [#10020](https://github.com/eclipse-theia/theia/pull/10020) +- [view-container] `ViewContainerPart` constructor takes new 2 parameters: `originalContainerId` and `originalContainerTitle`. The existing `viewContainerId` parameter has been renamed to `currentContainerId` to enable drag & drop views. [#9644](https://github.com/eclipse-theia/theia/pull/9644) ## v1.17.2 - 9/1/2021 diff --git a/packages/core/src/browser/shell/application-shell.ts b/packages/core/src/browser/shell/application-shell.ts index 30c3271ec5e60..9c60175ec8ed1 100644 --- a/packages/core/src/browser/shell/application-shell.ts +++ b/packages/core/src/browser/shell/application-shell.ts @@ -59,7 +59,9 @@ export type ApplicationShellLayoutVersion = /** git history view is replaced by a more generic scm history view, backward compatible to 3.0 */ 4.0 | /** Replace custom/font-awesome icons with codicons */ - 5.0; + 5.0 | + /** added the ability to drag and drop view parts between view containers */ + 6.0; /** * When a version is increased, make sure to introduce a migration (ApplicationShellLayoutMigration) to this version. diff --git a/packages/core/src/browser/shell/tab-bars.ts b/packages/core/src/browser/shell/tab-bars.ts index 65768bbad632a..18cde12665711 100644 --- a/packages/core/src/browser/shell/tab-bars.ts +++ b/packages/core/src/browser/shell/tab-bars.ts @@ -30,6 +30,7 @@ import { TabBarDecoratorService } from './tab-bar-decorator'; import { IconThemeService } from '../icon-theme-service'; import { BreadcrumbsRenderer, BreadcrumbsRendererFactory } from '../breadcrumbs/breadcrumbs-renderer'; import { NavigatableWidget } from '../navigatable-types'; +import { IDragEvent } from '@phosphor/dragdrop'; /** The class name added to hidden content nodes, which are required to render vertical side bars. */ const HIDDEN_CONTENT_CLASS = 'theia-TabBar-hidden-content'; @@ -772,6 +773,18 @@ export class SideTabBar extends ScrollableTabBar { protected onAfterAttach(msg: Message): void { super.onAfterAttach(msg); this.renderTabBar(); + this.node.addEventListener('p-dragenter', this); + this.node.addEventListener('p-dragover', this); + this.node.addEventListener('p-dragleave', this); + document.addEventListener('p-drop', this); + } + + protected onAfterDetach(msg: Message): void { + super.onAfterDetach(msg); + this.node.removeEventListener('p-dragenter', this); + this.node.removeEventListener('p-dragover', this); + this.node.removeEventListener('p-dragleave', this); + document.removeEventListener('p-drop', this); } protected onUpdateRequest(msg: Message): void { @@ -869,6 +882,15 @@ export class SideTabBar extends ScrollableTabBar { this.onMouseMove(event as MouseEvent); super.handleEvent(event); break; + case 'p-dragenter': + this.onDragEnter(event as IDragEvent); + break; + case 'p-dragover': + this.onDragOver(event as IDragEvent); + break; + case 'p-dragleave': case 'p-drop': + this.cancelViewContainerDND(); + break; default: super.handleEvent(event); } @@ -934,4 +956,78 @@ export class SideTabBar extends ScrollableTabBar { } } + toCancelViewContainerDND = new DisposableCollection(); + protected cancelViewContainerDND = () => { + this.toCancelViewContainerDND.dispose(); + }; + + /** + * handles `viewContainerPart` drag enter. + */ + protected onDragEnter = (event: IDragEvent) => { + this.cancelViewContainerDND(); + if (event.mimeData.getData('application/vnd.phosphor.view-container-factory')) { + event.preventDefault(); + event.stopPropagation(); + } + }; + + /** + * Handle `viewContainerPart` drag over, + * Defines the appropriate `drpAction` and opens the tab on which the mouse stands on for more than 800 ms. + */ + protected onDragOver = (event: IDragEvent) => { + const factory = event.mimeData.getData('application/vnd.phosphor.view-container-factory'); + const widget = factory && factory(); + if (!widget) { + event.dropAction = 'none'; + return; + } + event.preventDefault(); + event.stopPropagation(); + if (!this.toCancelViewContainerDND.disposed) { + event.dropAction = event.proposedAction; + return; + } + + const { target, clientX, clientY } = event; + if (target instanceof HTMLElement) { + if (widget.options.disableDraggingToOtherContainers || widget.viewContainer.disableDNDBetweenContainers) { + event.dropAction = 'none'; + target.classList.add('theia-cursor-no-drop'); + this.toCancelViewContainerDND.push(Disposable.create(() => { + target.classList.remove('theia-cursor-no-drop'); + })); + } else { + event.dropAction = event.proposedAction; + } + const { top, bottom, left, right, height } = target.getBoundingClientRect(); + const mouseOnTop = (clientY - top) < (height / 2); + const dropTargetClass = `drop-target-${mouseOnTop ? 'top' : 'bottom'}`; + const tabs = this.contentNode.children; + const targetTab = ArrayExt.findFirstValue(tabs, t => ElementExt.hitTest(t, clientX, clientY)); + if (!targetTab) { + return; + } + targetTab.classList.add(dropTargetClass); + this.toCancelViewContainerDND.push(Disposable.create(() => { + if (targetTab) { + targetTab.classList.remove(dropTargetClass); + } + })); + const openTabTimer = setTimeout(() => { + const title = this.titles.find(t => (this.renderer as TabBarRenderer).createTabId(t) === targetTab.id); + if (title) { + const mouseStillOnTab = clientX >= left && clientX <= right && clientY >= top && clientY <= bottom; + if (mouseStillOnTab) { + this.currentTitle = title; + } + } + }, 800); + this.toCancelViewContainerDND.push(Disposable.create(() => { + clearTimeout(openTabTimer); + })); + } + }; + } diff --git a/packages/core/src/browser/style/index.css b/packages/core/src/browser/style/index.css index 1a9fbe36f299f..51feecfab5f6a 100644 --- a/packages/core/src/browser/style/index.css +++ b/packages/core/src/browser/style/index.css @@ -235,6 +235,10 @@ button.secondary[disabled], .theia-button.secondary[disabled] { z-index: 999; } +.theia-cursor-no-drop, .theia-cursor-no-drop:active { + cursor: no-drop; +} + /*----------------------------------------------------------------------------- | Import children style files |----------------------------------------------------------------------------*/ diff --git a/packages/core/src/browser/style/tabs.css b/packages/core/src/browser/style/tabs.css index b93adff2aec46..e8023dbc8db29 100644 --- a/packages/core/src/browser/style/tabs.css +++ b/packages/core/src/browser/style/tabs.css @@ -10,6 +10,7 @@ --theia-tabbar-toolbar-z-index: 1001; --theia-toolbar-active-transform-scale: 1.272019649; --theia-horizontal-toolbar-height: calc(var(--theia-private-horizontal-tab-height) + var(--theia-private-horizontal-tab-scrollbar-rail-height) / 2); + --theia-dragover-tab-border-width: 2px; } /*----------------------------------------------------------------------------- @@ -39,6 +40,19 @@ align-items: center; } +.p-TabBar[data-orientation='vertical'] .p-TabBar-tab { + border-top: var(--theia-dragover-tab-border-width) solid transparent !important; + border-bottom: var(--theia-dragover-tab-border-width) solid transparent !important; +} + +.p-TabBar[data-orientation='vertical'] .p-TabBar-tab.drop-target-top { + border-top-color: var(--theia-activityBar-activeBorder) !important; +} + +.p-TabBar[data-orientation='vertical'] .p-TabBar-tab.drop-target-bottom { + border-bottom-color: var(--theia-activityBar-activeBorder) !important; +} + .p-TabBar[data-orientation='horizontal'] .p-TabBar-tab .theia-tab-icon-label, .p-TabBar-tab.p-mod-drag-image .theia-tab-icon-label { display: flex; diff --git a/packages/core/src/browser/style/view-container.css b/packages/core/src/browser/style/view-container.css index dd133b772b492..7c54b48cf67c8 100644 --- a/packages/core/src/browser/style/view-container.css +++ b/packages/core/src/browser/style/view-container.css @@ -50,7 +50,7 @@ height: 100%; } -.theia-view-container .part > .header { +.theia-view-container-part-header { cursor: pointer; display: flex; align-items: center; @@ -61,26 +61,26 @@ font-weight: 700; } -.theia-view-container .part > .header .theia-ExpansionToggle { +.theia-view-container-part-header .theia-ExpansionToggle { padding-left: 4px; } -.theia-view-container > .p-SplitPanel[data-orientation='horizontal'] .part > .header .theia-ExpansionToggle::before { +.theia-view-container > .p-SplitPanel[data-orientation='horizontal'] .part > .theia-header .theia-ExpansionToggle::before { display: none; padding-left: 0px; } -.theia-view-container > .p-SplitPanel[data-orientation='horizontal'] .part > .header .theia-ExpansionToggle { +.theia-view-container > .p-SplitPanel[data-orientation='horizontal'] .part > .theia-header .theia-ExpansionToggle { padding-left: 0px; } -.theia-view-container .part > .header .label { +.theia-view-container-part-header .label { flex: 0; white-space: nowrap; text-overflow: ellipsis; } -.theia-view-container .part > .header .description { +.theia-view-container-part-header .description { flex: 1; overflow: hidden; white-space: nowrap; @@ -107,7 +107,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; @@ -143,7 +143,7 @@ } .theia-view-container-part-title.menu-open, -.p-Widget.part:not(.collapsed):hover .header .theia-view-container-part-title, -.p-Widget.part:not(.collapsed):focus-within .header .theia-view-container-part-title { +.p-Widget.part:not(.collapsed):hover .theia-view-container-part-header .theia-view-container-part-title, +.p-Widget.part:not(.collapsed):focus-within .theia-view-container-part-header .theia-view-container-part-title { display: flex; } diff --git a/packages/core/src/browser/view-container.ts b/packages/core/src/browser/view-container.ts index 41ed0468535cd..e0786c3580c1f 100644 --- a/packages/core/src/browser/view-container.ts +++ b/packages/core/src/browser/view-container.ts @@ -15,12 +15,12 @@ ********************************************************************************/ import { interfaces, injectable, inject, postConstruct } from 'inversify'; -import { IIterator, toArray, find, some, every, map } from '@phosphor/algorithm'; +import { IIterator, toArray, find, some, every, map, ArrayExt } from '@phosphor/algorithm'; import { Widget, EXPANSION_TOGGLE_CLASS, COLLAPSED_CLASS, CODICON_TREE_ITEM_CLASSES, MessageLoop, Message, SplitPanel, - BaseWidget, addEventListener, SplitLayout, LayoutItem, PanelLayout, addKeyListener, waitForRevealed, UnsafeWidgetUtilities + BaseWidget, addEventListener, SplitLayout, LayoutItem, PanelLayout, addKeyListener, waitForRevealed, UnsafeWidgetUtilities, DockPanel } from './widgets'; -import { Event, Emitter } from '../common/event'; +import { Event as CommonEvent, Emitter } from '../common/event'; import { Disposable, DisposableCollection } from '../common/disposable'; import { CommandRegistry } from '../common/command'; import { MenuModelRegistry, MenuPath, MenuAction } from '../common/menu'; @@ -29,10 +29,14 @@ import { MAIN_AREA_ID, BOTTOM_AREA_ID } from './shell/theia-dock-panel'; import { FrontendApplicationStateService } from './frontend-application-state'; import { ContextMenuRenderer, Anchor } from './context-menu-renderer'; import { parseCssMagnitude } from './browser'; -import { WidgetManager } from './widget-manager'; import { TabBarToolbarRegistry, TabBarToolbarFactory, TabBarToolbar, TabBarDelegator, TabBarToolbarItem } from './shell/tab-bar-toolbar'; +import { isEmpty } from '../common'; +import { WidgetManager } from './widget-manager'; import { Key } from './keys'; import { ProgressBarFactory } from './progress-bar-factory'; +import { Drag, IDragEvent } from '@phosphor/dragdrop'; +import { MimeData } from '@phosphor/coreutils'; +import { ElementExt } from '@phosphor/domutils'; export interface ViewContainerTitleOptions { label: string; @@ -70,6 +74,11 @@ export class ViewContainer extends BaseWidget implements StatefulWidget, Applica protected currentPart: ViewContainerPart | undefined; + /** + * Disable dragging parts from/to this view container. + */ + disableDNDBetweenContainers = false; + @inject(FrontendApplicationStateService) protected readonly applicationStateService: FrontendApplicationStateService; @@ -103,6 +112,9 @@ export class ViewContainer extends BaseWidget implements StatefulWidget, Applica @inject(ProgressBarFactory) protected readonly progressBarFactory: ProgressBarFactory; + @inject(ApplicationShell) + protected readonly shell: ApplicationShell; + @postConstruct() protected init(): void { this.id = this.options.id; @@ -202,27 +214,46 @@ 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(); const visibleParts = allParts.filter(part => !part.isHidden); this.title.label = title.label; - if (visibleParts.length === 1) { + // If there's only one visible part - inline it's title into the container title except in case the part + // isn't originally belongs to this container but there are other **original** hidden parts. + if (visibleParts.length === 1 && (visibleParts[0].originalContainerId === this.id || !this.findOriginalPart())) { const part = visibleParts[0]; this.toDisposeOnUpdateTitle.push(part.onTitleChanged(() => this.updateTitle())); const partLabel = part.wrapped.title.label; + // 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.isCurrentTitle(part.originalContainerTitle)) { + title = Object.assign({}, part.originalContainerTitle); + this.setTitleOptions(title); + return; + } if (partLabel) { - this.title.label += ': ' + partLabel; + if (this.title.label && this.title.label !== partLabel) { + this.title.label += ': ' + partLabel; + } else { + 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 = this.findOriginalPart(); + if (originalPart && !this.isCurrentTitle(originalPart.originalContainerTitle)) { + title = Object.assign({}, originalPart.originalContainerTitle); + this.setTitleOptions(title); + return; + } } 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) { @@ -274,6 +305,16 @@ export class ViewContainer extends BaseWidget implements StatefulWidget, Applica } } + protected findOriginalPart(): ViewContainerPart | undefined { + return this.getParts().find(part => part.originalContainerId === this.id); + } + + protected isCurrentTitle(titleOptions: ViewContainerTitleOptions | undefined): boolean { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return (!!titleOptions && !!this.titleOptions && Object.keys(titleOptions).every(key => (titleOptions as any)[key] === (this.titleOptions as any)[key])) + || (!titleOptions && !this.titleOptions); + } + protected findPartForAnchor(anchor: Anchor): ViewContainerPart | undefined { const element = document.elementFromPoint(anchor.x, anchor.y); if (element instanceof Element) { @@ -287,22 +328,29 @@ export class ViewContainer extends BaseWidget implements StatefulWidget, Applica protected readonly toRemoveWidgets = new Map(); - addWidget(widget: Widget, options?: ViewContainer.Factory.WidgetOptions): Disposable { + protected createPartId(widget: Widget): string { + const description = this.widgetManager.getDescription(widget); + return widget.id || JSON.stringify(description); + } + + 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); + } + + protected 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 ?? this.getParts().findIndex(part => part.options.order === undefined || part.options.order > newPart.options.order!); if (index >= 0) { this.containerLayout.insertWidget(index, newPart); } else { @@ -317,10 +365,12 @@ export class ViewContainer extends BaseWidget implements StatefulWidget, Applica this.update(); this.fireDidChangeTrackableWidgets(); toRemoveWidget.pushAll([ - newPart, Disposable.create(() => { + if (newPart.currentViewContainerId === this.id) { + newPart.dispose(); + } this.unregisterPart(newPart); - if (!newPart.isDisposed) { + if (!newPart.isDisposed && this.getPartIndex(newPart.id) > -1) { this.containerLayout.removeWidget(newPart); } if (!this.isDisposed) { @@ -354,8 +404,10 @@ 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); } removeWidget(widget: Widget): boolean { @@ -371,6 +423,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); } @@ -413,7 +472,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 }; @@ -428,7 +489,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 || {} as ViewContainerTitleOptions); } } const partStates = state.parts.filter(partState => some(this.containerLayout.iter(), p => p.partId === partState.partId)); @@ -436,7 +497,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.getParts().findIndex(part => part.partId === partState.partId); if (currentIndex > index) { this.containerLayout.moveWidget(currentIndex, index, this.getParts()[currentIndex]); } @@ -600,62 +661,173 @@ export class ViewContainer extends BaseWidget implements StatefulWidget, Applica protected onAfterShow(msg: Message): void { super.onAfterShow(msg); + this.updateTitle(); this.lastVisibleState = undefined; } - protected draggingPart: ViewContainerPart | undefined; + protected onBeforeAttach(msg: Message): void { + super.onBeforeAttach(msg); + this.node.addEventListener('p-dragenter', this, true); + this.node.addEventListener('p-dragover', this, true); + this.node.addEventListener('p-dragleave', this, true); + this.node.addEventListener('p-drop', this, true); + } - protected registerDND(part: ViewContainerPart): Disposable { - part['header'].draggable = true; - const style = (event: DragEvent) => { - if (!this.draggingPart) { - return; - } + protected onAfterDetach(msg: Message): void { + super.onAfterDetach(msg); + this.node.removeEventListener('p-dragenter', this, true); + this.node.removeEventListener('p-dragover', this, true); + this.node.removeEventListener('p-dragleave', this, true); + this.node.removeEventListener('p-drop', this, true); + } + + handleEvent(event: Event): void { + switch (event.type) { + case 'p-dragenter': + this.handleDragEnter(event as IDragEvent); + break; + case 'p-dragover': + this.handleDragOver(event as IDragEvent); + break; + case 'p-dragleave': + this.handleDragLeave(event as IDragEvent); + break; + case 'p-drop': + this.handleDrop(event as IDragEvent); + break; + } + } + + handleDragEnter(event: IDragEvent): void { + if (event.mimeData.hasData('application/vnd.phosphor.view-container-factory')) { event.preventDefault(); - const enclosingPartNode = ViewContainerPart.closestPart(event.target); - if (enclosingPartNode && enclosingPartNode !== this.draggingPart.node) { - enclosingPartNode.classList.add('drop-target'); - } - }; - const unstyle = (event: DragEvent) => { - if (!this.draggingPart) { - return; + event.stopPropagation(); + } + } + + toDisposeOnDragEnd = new DisposableCollection(); + handleDragOver(event: IDragEvent): void { + const factory = event.mimeData.getData('application/vnd.phosphor.view-container-factory'); + const widget = factory && factory(); + if (!(widget instanceof ViewContainerPart)) { + return; + } + event.preventDefault(); + event.stopPropagation(); + + const sameContainers = this.id === widget.currentViewContainerId; + const targetPart = ArrayExt.findFirstValue(this.getParts(), (p => ElementExt.hitTest(p.node, event.clientX, event.clientY))); + if (!targetPart && sameContainers) { + event.dropAction = 'none'; + return; + } + if (targetPart) { + // add overlay class style to the `targetPart` node. + targetPart.node.classList.add('drop-target'); + this.toDisposeOnDragEnd.push(Disposable.create(() => targetPart.node.classList.remove('drop-target'))); + } else { + // show panel overlay. + const dockPanel = this.getDockPanel(); + if (dockPanel) { + dockPanel.overlay.show({ top: 0, bottom: 0, right: 0, left: 0 }); + this.toDisposeOnDragEnd.push(Disposable.create(() => dockPanel.overlay.hide(100))); } - event.preventDefault(); - const enclosingPartNode = ViewContainerPart.closestPart(event.target); - if (enclosingPartNode) { - enclosingPartNode.classList.remove('drop-target'); + } + + const draggingOutsideisDisabled = this.disableDNDBetweenContainers || widget.viewContainer?.disableDNDBetweenContainers + || widget.options.disableDraggingToOtherContainers; + if (draggingOutsideisDisabled && !sameContainers) { + const { target } = event; + if (target instanceof HTMLElement) { + target.classList.add('theia-cursor-no-drop'); + this.toDisposeOnDragEnd.push(Disposable.create(() => { + target.classList.remove('theia-cursor-no-drop'); + })); } + event.dropAction = 'none'; + return; }; + + event.dropAction = event.proposedAction; + }; + + handleDragLeave(event: IDragEvent): void { + this.toDisposeOnDragEnd.dispose(); + if (event.mimeData.hasData('application/vnd.phosphor.view-container-factory')) { + event.preventDefault(); + event.stopPropagation(); + } + }; + + handleDrop(event: IDragEvent): void { + this.toDisposeOnDragEnd.dispose(); + const factory = event.mimeData.getData('application/vnd.phosphor.view-container-factory'); + const draggedPart = factory && factory(); + if (!(draggedPart instanceof ViewContainerPart)) { + event.dropAction = 'none'; + return; + } + event.preventDefault(); + event.stopPropagation(); + const parts = this.getParts(); + const toIndex = ArrayExt.findFirstIndex(parts, part => ElementExt.hitTest(part.node, event.clientX, event.clientY)); + if (draggedPart.currentViewContainerId !== this.id) { + this.attachNewPart(draggedPart, toIndex > -1 ? toIndex + 1 : toIndex); + draggedPart.onPartMoved(this); + } else { + this.moveBefore(draggedPart.id, parts[toIndex].id); + } + event.dropAction = event.proposedAction; + } + + protected registerDND(part: ViewContainerPart): Disposable { + part.headerElement.draggable = true; + 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); - } - }, 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 => { - const { dataTransfer } = event; - if (dataTransfer) { - const moveId = dataTransfer.getData('view-container-dnd'); - if (moveId && moveId !== part.id) { - this.moveBefore(moveId, part.id); - } - unstyle(event); - } - }, false) - ); + addEventListener(part.headerElement, 'dragstart', + event => { + const mimeData = new MimeData(); + mimeData.setData('application/vnd.phosphor.view-container-factory', () => part); + const clonedHeader = part.headerElement.cloneNode(true) as HTMLElement; + clonedHeader.style.width = part.node.style.width; + clonedHeader.style.opacity = '0.6'; + const drag = new Drag({ + mimeData, + dragImage: clonedHeader, + proposedAction: 'move', + supportedActions: 'move' + }); + part.node.classList.add('p-mod-hidden'); + drag.start(event.clientX, event.clientY).then(dropAction => { + // The promise is resolved when the drag has ended + if (dropAction === 'move' && part.currentViewContainerId !== this.id) { + this.removeWidget(part.wrapped); + this.lastVisibleState = this.doStoreState(); + } + }); + setTimeout(() => { part.node.classList.remove('p-mod-hidden'); }, 0); + }, false)); + } + + protected getDockPanel(): DockPanel | undefined { + let panel: DockPanel | undefined; + let parent = this.parent; + while (!panel && parent) { + if (this.isSideDockPanel(parent)) { + panel = parent as DockPanel; + } else { + parent = parent.parent; + } + } + return panel; + } + + protected isSideDockPanel(widget: Widget): boolean { + const { leftPanelHandler, rightPanelHandler } = this.shell; + if (widget instanceof DockPanel && (widget.id === rightPanelHandler.dockPanel.id || widget.id === leftPanelHandler.dockPanel.id)) { + return true; + } + return false; } } @@ -675,6 +847,12 @@ 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, + * This option only applies to the `ViewContainerPart` and has no effect on the ViewContainer. + */ + readonly disableDraggingToOtherContainers?: boolean; } export interface WidgetDescriptor { @@ -715,6 +893,8 @@ export class ViewContainerPart extends BaseWidget { readonly onTitleChanged = this.onTitleChangedEmitter.event; protected readonly onDidFocusEmitter = new Emitter(); readonly onDidFocus = this.onDidFocusEmitter.event; + protected readonly onPartMovedEmitter = new Emitter(); + readonly onDidMove = this.onPartMovedEmitter.event; protected readonly onDidChangeDescriptionEmitter = new Emitter(); readonly onDidChangeDescription = this.onDidChangeDescriptionEmitter.event; @@ -730,7 +910,9 @@ export class ViewContainerPart extends BaseWidget { constructor( readonly wrapped: Widget, readonly partId: string, - viewContainerId: string, + protected currentContainerId: string, + readonly originalContainerId: string, + readonly originalContainerTitle: ViewContainerTitleOptions | undefined, protected readonly toolbarRegistry: TabBarToolbarRegistry, protected readonly toolbarFactory: TabBarToolbarFactory, readonly options: ViewContainer.Factory.WidgetOptions = {} @@ -738,7 +920,7 @@ export class ViewContainerPart extends BaseWidget { super(); wrapped.parent = this; wrapped.disposed.connect(() => this.dispose()); - this.id = `${viewContainerId}--${wrapped.id}`; + this.id = `${originalContainerId}--${wrapped.id}`; this.addClass('part'); const fireTitleChanged = () => this.onTitleChangedEmitter.fire(undefined); @@ -781,6 +963,18 @@ export class ViewContainerPart extends BaseWidget { } } + get viewContainer(): ViewContainer | undefined { + return this.parent ? this.parent.parent as ViewContainer : undefined; + } + + get currentViewContainerId(): string { + return this.currentContainerId; + } + + get headerElement(): HTMLElement { + return this.header; + } + get collapsed(): boolean { return this._collapsed; } @@ -809,6 +1003,11 @@ export class ViewContainerPart extends BaseWidget { this.collapsedEmitter.fire(collapsed); } + onPartMoved(newContainer: ViewContainer): void { + this.currentContainerId = newContainer.id; + this.onPartMovedEmitter.fire(newContainer); + } + setHidden(hidden: boolean): void { if (!this.canHide) { return; @@ -820,11 +1019,11 @@ export class ViewContainerPart extends BaseWidget { return this.options.canHide === undefined || this.options.canHide; } - get onCollapsed(): Event { + get onCollapsed(): CommonEvent { return this.collapsedEmitter.event; } - get onContextMenu(): Event { + get onContextMenu(): CommonEvent { return this.contextMenuEmitter.event; } @@ -891,7 +1090,7 @@ export class ViewContainerPart extends BaseWidget { const disposable = new DisposableCollection(); const header = document.createElement('div'); header.tabIndex = 0; - header.classList.add('theia-header', 'header'); + header.classList.add('theia-header', 'header', 'theia-view-container-part-header'); disposable.push(addEventListener(header, 'click', event => { if (this.toolbar && this.toolbar.shouldHandleMouseEvent(event)) { return; @@ -915,7 +1114,14 @@ export class ViewContainerPart extends BaseWidget { const description = document.createElement('span'); description.classList.add('description'); - const updateTitle = () => title.innerText = this.wrapped.title.label; + const updateTitle = () => { + if (this.currentContainerId !== this.originalContainerId && this.originalContainerTitle?.label) { + // Creating a 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; const updateDescription = () => { description.innerText = DescriptionWidget.is(this.wrapped) && !this.collapsed && this.wrapped.description || ''; @@ -928,8 +1134,9 @@ export class ViewContainerPart extends BaseWidget { disposable.pushAll([ this.onTitleChanged(updateTitle), this.onTitleChanged(updateCaption), + this.onDidMove(updateTitle), this.onDidChangeDescription(updateDescription), - this.onCollapsed(updateDescription), + this.onCollapsed(updateDescription) ]); header.appendChild(title); header.appendChild(description); @@ -1032,6 +1239,9 @@ export namespace ViewContainerPart { hidden: boolean; relativeSize?: number; description?: string; + /** 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 { @@ -1064,9 +1274,24 @@ export class ViewContainerLayout extends SplitLayout { return toArray(this.iter()); } + attachWidget(index: number, widget: ViewContainerPart): void { + super.attachWidget(index, widget); + if (index > -1 && this.parent && this.parent.node.contains(this.widgets[index + 1]?.node)) { + // Set the correct attach index to the DOM elements. + const ref = this.widgets[index + 1].node; + this.parent.node.insertBefore(widget.node, ref); + this.parent.node.insertBefore(this.handles[index], ref); + this.parent.fit(); + } + } + moveWidget(fromIndex: number, toIndex: number, widget: Widget): void { const ref = this.widgets[toIndex < fromIndex ? toIndex : toIndex + 1]; super.moveWidget(fromIndex, toIndex, widget); + // Keep the order of `_widgets` array just as done before (by `super`) for the `_items` array - + // to prevent later bugs relying on index. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ArrayExt.move((this as any)._widgets, fromIndex, toIndex); if (ref) { this.parent!.node.insertBefore(this.handles[toIndex], ref.node); } else { diff --git a/packages/debug/src/browser/view/debug-session-widget.ts b/packages/debug/src/browser/view/debug-session-widget.ts index 46fe25a9fe2c7..8fc55607e8fe6 100644 --- a/packages/debug/src/browser/view/debug-session-widget.ts +++ b/packages/debug/src/browser/view/debug-session-widget.ts @@ -16,7 +16,7 @@ import { inject, injectable, postConstruct, interfaces, Container } from '@theia/core/shared/inversify'; import { - Message, ApplicationShell, Widget, BaseWidget, PanelLayout, StatefulWidget, ViewContainer, codicon + Message, ApplicationShell, Widget, BaseWidget, PanelLayout, StatefulWidget, ViewContainer, codicon, ViewContainerTitleOptions } from '@theia/core/lib/browser'; import { DebugThreadsWidget } from './debug-threads-widget'; import { DebugStackFramesWidget } from './debug-stack-frames-widget'; @@ -28,6 +28,11 @@ import { DebugWatchWidget } from './debug-watch-widget'; export const DebugSessionWidgetFactory = Symbol('DebugSessionWidgetFactory'); export type DebugSessionWidgetFactory = (options: DebugViewOptions) => DebugSessionWidget; +export const DEBUG_VIEW_CONTAINER_TITLE_OPTIONS: ViewContainerTitleOptions = { + label: 'debug', + iconClass: codicon('debug-alt'), + closeable: true +}; @injectable() export class DebugSessionWidget extends BaseWidget implements StatefulWidget, ApplicationShell.TrackableWidgetProvider { @@ -88,11 +93,12 @@ export class DebugSessionWidget extends BaseWidget implements StatefulWidget, Ap this.viewContainer = this.viewContainerFactory({ id: 'debug:view-container:' + this.model.id }); - this.viewContainer.addWidget(this.threads, { weight: 30 }); - this.viewContainer.addWidget(this.stackFrames, { weight: 20 }); - this.viewContainer.addWidget(this.variables, { weight: 10 }); - this.viewContainer.addWidget(this.watch, { weight: 10 }); - this.viewContainer.addWidget(this.breakpoints, { weight: 10 }); + this.viewContainer.setTitleOptions(DEBUG_VIEW_CONTAINER_TITLE_OPTIONS); + this.viewContainer.addWidget(this.threads, { weight: 30, disableDraggingToOtherContainers: true }); + this.viewContainer.addWidget(this.stackFrames, { weight: 20, disableDraggingToOtherContainers: true }); + this.viewContainer.addWidget(this.variables, { weight: 10, disableDraggingToOtherContainers: true }); + this.viewContainer.addWidget(this.watch, { weight: 10, disableDraggingToOtherContainers: true }); + this.viewContainer.addWidget(this.breakpoints, { weight: 10, disableDraggingToOtherContainers: true }); this.toDispose.pushAll([ this.toolbar, diff --git a/packages/navigator/src/browser/navigator-widget-factory.ts b/packages/navigator/src/browser/navigator-widget-factory.ts index f1f4ba3e181c9..44c333f09a95e 100644 --- a/packages/navigator/src/browser/navigator-widget-factory.ts +++ b/packages/navigator/src/browser/navigator-widget-factory.ts @@ -51,7 +51,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 1e05b1f9b38b6..31c58273b63a0 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, codicon + StatefulWidget, CommonMenus, BaseWidget, TreeViewWelcomeWidget, codicon, 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)); @@ -401,7 +403,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 +470,14 @@ export class PluginViewRegistry implements FrontendApplicationContribution { } for (const viewId of this.getContainerViews(viewContainerId)) { const identifier = this.toPluginViewWidgetIdentifier(viewId); + // Keep existing 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, currentContainer)) { + continue; + } + } const widget = await this.widgetManager.getOrCreateWidget(PLUGIN_VIEW_FACTORY_ID, identifier); if (containerWidget.getTrackableWidgets().indexOf(widget) === -1) { containerWidget.addWidget(widget, { @@ -473,27 +485,49 @@ 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(); + protected registerWidgetPartEvents(widget: PluginViewWidget, containerWidget: ViewContainerWidget): ViewContainerPart | undefined { + const part = containerWidget.getPartFor(widget); + if (part) { + + widget.currentViewContainerId = this.getViewContainerId(containerWidget); + part.onDidMove(event => { widget.currentViewContainerId = this.getViewContainerId(event); }); + + // if a view is explicitly hidden then suppress updating visibility based on `when` closure + part.onDidChangeVisibility(() => widget.suppressUpdateViewVisibility = part.isHidden); + + const tryFireOnDidExpandView = () => { + if (widget.widgets.length === 0) { + if (!part.collapsed && part.isVisible) { + const viewId = this.toViewId(widget.options); + this.onDidExpandViewEmitter.fire(viewId); } - }; - const toFire = new DisposableCollection( - part.onCollapsed(tryFireOnDidExpandView), - part.onDidChangeVisibility(tryFireOnDidExpandView) - ); + } else { + toFire.dispose(); + } + }; + const toFire = new DisposableCollection( + part.onCollapsed(tryFireOnDidExpandView), + part.onDidChangeVisibility(tryFireOnDidExpandView) + ); - tryFireOnDidExpandView(); - } + tryFireOnDidExpandView(); + return part; + } + }; + + 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 undefined: return container.parent?.parent instanceof DebugWidget ? 'debug' : container.id; + case PLUGIN_VIEW_CONTAINER_FACTORY_ID: return this.toViewContainerId(description.options); + default: return container.id; } } @@ -504,6 +538,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 +583,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 77dba92d0d382..90d65b892dfb0 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 @@ -46,6 +46,8 @@ export class PluginViewWidget extends Panel implements StatefulWidget, Descripti @inject(PluginViewWidgetIdentifier) readonly options: PluginViewWidgetIdentifier; + currentViewContainerId: string | undefined; + constructor() { super(); this.node.tabIndex = -1; @@ -75,7 +77,8 @@ export class PluginViewWidget extends Panel implements StatefulWidget, Descripti label: this.title.label, message: this.message, widgets: this.widgets, - suppressUpdateViewVisibility: this._suppressUpdateViewVisibility + suppressUpdateViewVisibility: this._suppressUpdateViewVisibility, + currentViewContainerId: this.currentViewContainerId }; } @@ -83,6 +86,7 @@ export class PluginViewWidget extends Panel implements StatefulWidget, Descripti 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); } @@ -150,6 +154,7 @@ export namespace PluginViewWidget { label: string, message?: string, widgets: ReadonlyArray, - suppressUpdateViewVisibility: boolean + suppressUpdateViewVisibility: boolean; + currentViewContainerId: string | undefined; } } 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..196fa7a08b8ae --- /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 { + codicon, + 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: codicon('search'), + 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 + }; + + @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 172cf518f0f1a..c53e46b5d4012 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 d31b17056e1f7..5dd6485b7ad10 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..18b0a7c5a7c41 --- /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 = 6.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/search-in-workspace/src/browser/styles/index.css b/packages/search-in-workspace/src/browser/styles/index.css index 84a07e6dbeb57..a38d9a8e43a8b 100644 --- a/packages/search-in-workspace/src/browser/styles/index.css +++ b/packages/search-in-workspace/src/browser/styles/index.css @@ -14,6 +14,10 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ + #search-in-workspace { + height: 100%; +} + .t-siw-search-container { padding: 5px ; display: flex; 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 bab463fd8313d..479f2936e32ed 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,8 @@ export class VSXExtensionsViewContainer extends ViewContainer { static ID = 'vsx-extensions-view-container'; static LABEL = 'Extensions'; + disableDNDBetweenContainers = true; + @inject(VSXExtensionsSearchBar) protected readonly searchBar: VSXExtensionsSearchBar;