Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix #9203: Drag and drop sections between views #9644

Merged
merged 7 commits into from
Oct 6, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@

- [plugin-ext] add additional startup logging for plugin starting and application loading [#10116](https://github.com/eclipse-theia/theia/pull/10116) - Contributed on behalf of STMicroelectronics

<a name="breaking_changes_1.19.0">[Breaking Changes:](#breaking_changes_1.19.0)</a>

- [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.18.0 - 9/30/2021

[1.18.0 Milestone](https://github.com/eclipse-theia/theia/milestone/24)
Expand Down
4 changes: 3 additions & 1 deletion packages/core/src/browser/shell/application-shell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/browser/shell/tab-bar-decorator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export class TabBarDecoratorService implements FrontendApplicationContribution {
this.contributions.getContributions().map(decorator => decorator.onDidChangeDecorations(this.fireDidChangeDecorations));
}

protected fireDidChangeDecorations = debounce(() => this.onDidChangeDecorationsEmitter.fire(undefined), 150);
fireDidChangeDecorations = debounce(() => this.onDidChangeDecorationsEmitter.fire(undefined), 150);
EstherPerelman marked this conversation as resolved.
Show resolved Hide resolved

/**
* Assign tabs the decorators provided by all the contributions.
Expand Down
111 changes: 103 additions & 8 deletions packages/core/src/browser/shell/tab-bars.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -229,11 +230,11 @@ export class TabBarRenderer extends TabBar.Renderer {
}

renderBadge(data: SideBarRenderData, isInSidePanel?: boolean): VirtualElement {
const badge: number | undefined = this.getDecorationData(data.title, 'badge')[0];
if (!badge) {
const totalBadge = this.getDecorationData(data.title, 'badge').reduce((sum, badge) => sum! + badge!, 0);
if (!totalBadge) {
return h.div({});
}
const limitedBadge = badge >= 100 ? '99+' : badge;
const limitedBadge = totalBadge >= 100 ? '99+' : totalBadge;
return isInSidePanel
? h.div({ className: 'theia-badge-decorator-sidebar' }, `${limitedBadge}`)
: h.div({ className: 'theia-badge-decorator-horizontal' }, `${limitedBadge}`);
Expand Down Expand Up @@ -279,7 +280,6 @@ export class TabBarRenderer extends TabBar.Renderer {
*/
protected getDecorationData<K extends keyof WidgetDecoration.Data>(title: Title<Widget>, key: K): WidgetDecoration.Data[K][] {
return this.getDecorations(title).filter(data => data[key] !== undefined).map(data => data[key]);

}

/**
Expand Down Expand Up @@ -383,8 +383,8 @@ export class TabBarRenderer extends TabBar.Renderer {
* @param {SideBarRenderData} data Data used to render the tab icon.
* @param {boolean} isInSidePanel An optional check which determines if the tab is in the side-panel.
*/
renderIcon(data: SideBarRenderData, inSidePanel?: boolean): VirtualElement {
if (!inSidePanel && this.iconThemeService && this.iconThemeService.current === 'none') {
renderIcon(data: SideBarRenderData, isInSidePanel?: boolean): VirtualElement {
if (!isInSidePanel && this.iconThemeService && this.iconThemeService.current === 'none') {
return h.div();
}
let top: string | undefined;
Expand All @@ -401,13 +401,13 @@ export class TabBarRenderer extends TabBar.Renderer {
if (decorationData.length > 0) {
const baseIcon: VirtualElement = h.div({ className: baseClassName, style }, data.title.iconLabel);
const wrapperClassName: string = WidgetDecoration.Styles.ICON_WRAPPER_CLASS;
const decoratorSizeClassName: string = inSidePanel ? WidgetDecoration.Styles.DECORATOR_SIDEBAR_SIZE_CLASS : WidgetDecoration.Styles.DECORATOR_SIZE_CLASS;
const decoratorSizeClassName: string = isInSidePanel ? WidgetDecoration.Styles.DECORATOR_SIDEBAR_SIZE_CLASS : WidgetDecoration.Styles.DECORATOR_SIZE_CLASS;

decorationData
.filter(notEmpty)
.map(overlay => [overlay.position, overlay] as [WidgetDecoration.IconOverlayPosition, WidgetDecoration.IconOverlay | WidgetDecoration.IconClassOverlay])
.forEach(([position, overlay]) => {
const iconAdditionalClasses: string[] = [decoratorSizeClassName, WidgetDecoration.IconOverlayPosition.getStyle(position, inSidePanel)];
const iconAdditionalClasses: string[] = [decoratorSizeClassName, WidgetDecoration.IconOverlayPosition.getStyle(position, isInSidePanel)];
const overlayIconStyle = (color?: string) => {
if (color === undefined) {
return {};
Expand Down Expand Up @@ -772,6 +772,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 {
Expand Down Expand Up @@ -869,6 +881,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);
}
Expand Down Expand Up @@ -934,4 +955,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);
}));
}
};

}
4 changes: 4 additions & 0 deletions packages/core/src/browser/style/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -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
|----------------------------------------------------------------------------*/
Expand Down
14 changes: 14 additions & 0 deletions packages/core/src/browser/style/tabs.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/*-----------------------------------------------------------------------------
Expand Down Expand Up @@ -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;
Expand Down
18 changes: 9 additions & 9 deletions packages/core/src/browser/style/view-container.css
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
height: 100%;
}

.theia-view-container .part > .header {
.theia-view-container-part-header {
cursor: pointer;
display: flex;
align-items: center;
Expand All @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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;
}
Loading