diff --git a/CHANGELOG.md b/CHANGELOG.md
index 18a5cb905b259..e8e78a42d5de8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,7 @@
[Breaking Changes:](#breaking_changes_1.18.0)
- [core] added `BreadcrumbsRendererFactory` to constructor arguments of `DockPanelRenderer` and `ToolbarAwareTabBar`. [#9920](https://github.com/eclipse-theia/theia/pull/9920)
+- [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 ff0026676d375..2fd6f0ef2943a 100644
--- a/packages/core/src/browser/shell/application-shell.ts
+++ b/packages/core/src/browser/shell/application-shell.ts
@@ -57,7 +57,9 @@ export type ApplicationShellLayoutVersion =
/** view containers are introduced, backward compatible to 2.0 */
3.0 |
/** git history view is replaced by a more generic scm history view, backward compatible to 3.0 */
- 4.0;
+ 4.0 |
+ /** added the ability to drag and drop view parts between view containers */
+ 5.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 005d39a3bb2de..d32092fe4675d 100644
--- a/packages/core/src/browser/style/index.css
+++ b/packages/core/src/browser/style/index.css
@@ -225,6 +225,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 e369c48720421..feef2f782b6b1 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 d2ebd97af2edd..38039e1368c49 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
+ Message, ApplicationShell, Widget, BaseWidget, PanelLayout, StatefulWidget, ViewContainer, ViewContainerTitleOptions
} from '@theia/core/lib/browser';
import { DebugThreadsWidget } from './debug-threads-widget';
import { DebugStackFramesWidget } from './debug-stack-frames-widget';
@@ -25,9 +25,15 @@ import { DebugVariablesWidget } from './debug-variables-widget';
import { DebugToolBar } from './debug-toolbar-widget';
import { DebugViewModel, DebugViewOptions } from './debug-view-model';
import { DebugWatchWidget } from './debug-watch-widget';
+import { DebugWidget } from './debug-widget';
export const DebugSessionWidgetFactory = Symbol('DebugSessionWidgetFactory');
export type DebugSessionWidgetFactory = (options: DebugViewOptions) => DebugSessionWidget;
+export const DEBUG_VIEW_CONTAINER_TITLE_OPTIONS: ViewContainerTitleOptions = {
+ label: 'debug',
+ iconClass: 'debug-tab-icon',
+ closeable: true
+};
@injectable()
export class DebugSessionWidget extends BaseWidget implements StatefulWidget, ApplicationShell.TrackableWidgetProvider {
@@ -82,17 +88,18 @@ export class DebugSessionWidget extends BaseWidget implements StatefulWidget, Ap
this.title.label = this.model.label;
this.title.caption = this.model.label;
this.title.closable = true;
- this.title.iconClass = 'debug-tab-icon';
+ this.title.iconClass = DebugWidget.ICON_CLASS;
this.addClass('theia-session-container');
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/debug/src/browser/view/debug-widget.ts b/packages/debug/src/browser/view/debug-widget.ts
index 78f6efb82a56e..0553efafbe02f 100644
--- a/packages/debug/src/browser/view/debug-widget.ts
+++ b/packages/debug/src/browser/view/debug-widget.ts
@@ -39,6 +39,7 @@ export class DebugWidget extends BaseWidget implements StatefulWidget, Applicati
static ID = 'debug';
static LABEL = 'Debug';
+ static ICON_CLASS = 'debug-tab-icon';
@inject(DebugViewModel)
readonly model: DebugViewModel;
@@ -61,7 +62,7 @@ export class DebugWidget extends BaseWidget implements StatefulWidget, Applicati
this.title.label = DebugWidget.LABEL;
this.title.caption = DebugWidget.LABEL;
this.title.closable = true;
- this.title.iconClass = 'debug-tab-icon';
+ this.title.iconClass = DebugWidget.ICON_CLASS;
this.addClass('theia-debug-container');
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 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..0bd0fa9ec702c 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));
@@ -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..f5bf1265bd868
--- /dev/null
+++ b/packages/search-in-workspace/src/browser/search-in-workspace-factory.ts
@@ -0,0 +1,59 @@
+/********************************************************************************
+ * 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
+ };
+
+ @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..12353f7e0e9cf
--- /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 = 5.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 c350f7926bee5..27de106215040 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 92e1f5710fc90..53222b889d9a9 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;