From df4c5bc194116a46023ae44774101595ef736fb0 Mon Sep 17 00:00:00 2001 From: Lars Vogel Date: Mon, 1 Jun 2026 10:18:52 +0200 Subject: [PATCH] Allow reordering terminal tabs via drag and drop Dragging a terminal tab within the same Terminals view now reorders it to the drop position, matching the drag-to-reorder behavior of editor and view tabs. The drop within the same view was previously rejected. The reorder uses the new CTabFolder.moveItem(int, int) API, so the live terminal control stays attached and no longer has to be recreated. The existing cross-view move (which transfers a terminal to a different Terminals view) is unchanged, as it still requires re-parenting. Requires SWT 3.135. Fixes https://github.com/eclipse-platform/eclipse.platform/issues/2679 --- .../META-INF/MANIFEST.MF | 3 +- .../view/ui/internal/view/TerminalsView.java | 70 ++++++++++- .../META-INF/MANIFEST.MF | 3 +- .../build.properties | 2 + .../terminal/test/AutomatedTestSuite.java | 1 + .../ui/tests/TerminalsViewReorderTest.java | 111 ++++++++++++++++++ 6 files changed, 182 insertions(+), 8 deletions(-) create mode 100644 terminal/tests/org.eclipse.terminal.test/src/org/eclipse/terminal/view/ui/tests/TerminalsViewReorderTest.java diff --git a/terminal/bundles/org.eclipse.terminal.view.ui/META-INF/MANIFEST.MF b/terminal/bundles/org.eclipse.terminal.view.ui/META-INF/MANIFEST.MF index 76a78133ca0..e8dfd9f7c67 100644 --- a/terminal/bundles/org.eclipse.terminal.view.ui/META-INF/MANIFEST.MF +++ b/terminal/bundles/org.eclipse.terminal.view.ui/META-INF/MANIFEST.MF @@ -2,7 +2,7 @@ Manifest-Version: 1.0 Bundle-ManifestVersion: 2 Bundle-Name: %pluginName Bundle-SymbolicName: org.eclipse.terminal.view.ui;singleton:=true -Bundle-Version: 1.1.100.qualifier +Bundle-Version: 1.1.200.qualifier Bundle-Activator: org.eclipse.terminal.view.ui.internal.UIPlugin Bundle-Vendor: %providerName Require-Bundle: org.eclipse.core.expressions;bundle-version="[3.9.0,4.0.0)", @@ -10,6 +10,7 @@ Require-Bundle: org.eclipse.core.expressions;bundle-version="[3.9.0,4.0.0)", org.eclipse.core.resources;bundle-version="[3.22.0,4.0.0)";resolution:=optional, org.eclipse.core.variables;bundle-version="[3.6.0,4.0.0)", org.eclipse.debug.ui;bundle-version="[3.18.0,4.0.0)";resolution:=optional, + org.eclipse.swt;bundle-version="[3.135.0,4.0.0)", org.eclipse.ui;bundle-version="[3.208.0,4.0.0)", org.eclipse.ui.ide;bundle-version="[3.22.0,4.0.0)";resolution:=optional, org.eclipse.ui.editors;bundle-version="[3.20.0,4.0.0)";resolution:=optional, diff --git a/terminal/bundles/org.eclipse.terminal.view.ui/src/org/eclipse/terminal/view/ui/internal/view/TerminalsView.java b/terminal/bundles/org.eclipse.terminal.view.ui/src/org/eclipse/terminal/view/ui/internal/view/TerminalsView.java index ef41338cbd9..f9faac493d4 100644 --- a/terminal/bundles/org.eclipse.terminal.view.ui/src/org/eclipse/terminal/view/ui/internal/view/TerminalsView.java +++ b/terminal/bundles/org.eclipse.terminal.view.ui/src/org/eclipse/terminal/view/ui/internal/view/TerminalsView.java @@ -261,12 +261,9 @@ private void addDropSupport() { target.addDropListener(new DropTargetListener() { @Override public void dragEnter(DropTargetEvent event) { - // only if the drop target is different then the drag source - if (TerminalTransfer.getInstance().getTabFolderManager() == tabFolderManager) { - event.detail = DND.DROP_NONE; - } else { - event.detail = DND.DROP_MOVE; - } + // Accept the move both for a different terminals view (the terminal is moved + // to the other view) and for the same view (the tab is reordered). + event.detail = DND.DROP_MOVE; } @Override @@ -290,6 +287,12 @@ public void drop(DropTargetEvent event) { if (TerminalTransfer.getInstance().getDraggedFolderItem() != null && tabFolderManager != null) { CTabItem draggedItem = TerminalTransfer.getInstance().getDraggedFolderItem(); + // Drop within the same terminals view: reorder the dragged tab in place. + if (TerminalTransfer.getInstance().getTabFolderManager() == tabFolderManager) { + reorderTabItem(draggedItem, event.x, event.y); + return; + } + CTabItem item = tabFolderManager.cloneTabItemAfterDrop(draggedItem); tabFolderManager.bringToTop(item); switchToTabFolderControl(); @@ -313,6 +316,61 @@ public void drop(DropTargetEvent event) { }); } + /** + * Reorder the dragged tab item within its own tab folder so that it is dropped at the position + * the mouse points to. + * + * @param draggedItem the tab item being dragged, must not be null. + * @param x the x coordinate of the drop, in display-relative coordinates. + * @param y the y coordinate of the drop, in display-relative coordinates. + */ + private void reorderTabItem(CTabItem draggedItem, int x, int y) { + if (tabFolderControl == null || tabFolderControl.isDisposed()) { + return; + } + + int from = tabFolderControl.indexOf(draggedItem); + if (from == -1) { + return; + } + + // Map the display-relative drop coordinates to the tab folder and find the tab below them. + // A drop next to the tabs (e.g. on the trailing empty space) targets the last position. + Point point = tabFolderControl.toControl(x, y); + CTabItem targetItem = tabFolderControl.getItem(point); + int indexUnderCursor = targetItem != null ? tabFolderControl.indexOf(targetItem) : -1; + + int to = computeReorderIndex(from, indexUnderCursor, tabFolderControl.getItemCount()); + if (to != -1) { + tabFolderControl.moveItem(from, to); + } + + // Keep the moved terminal selected and focused. + tabFolderManager.bringToTop(draggedItem); + setFocus(); + } + + /** + * Computes the destination index for a tab reorder triggered by a drop. + *

+ * This method is internal and only exposed for testing. + *

+ * + * @param from the current index of the dragged tab. + * @param indexUnderCursor the index of the tab below the drop location, or -1 if the + * drop did not happen over a tab (for example on the empty space following the last tab). + * @param itemCount the total number of tabs in the folder. + * @return the index the dragged tab should be moved to, or -1 if no move is required + * because the tab would keep its position. + */ + public static int computeReorderIndex(int from, int indexUnderCursor, int itemCount) { + int to = indexUnderCursor != -1 ? indexUnderCursor : itemCount - 1; + if (to < 0 || to == from) { + return -1; + } + return to; + } + @Override public void dispose() { // Dispose the tab folder manager diff --git a/terminal/tests/org.eclipse.terminal.test/META-INF/MANIFEST.MF b/terminal/tests/org.eclipse.terminal.test/META-INF/MANIFEST.MF index f7415294243..09adac293ec 100644 --- a/terminal/tests/org.eclipse.terminal.test/META-INF/MANIFEST.MF +++ b/terminal/tests/org.eclipse.terminal.test/META-INF/MANIFEST.MF @@ -8,7 +8,8 @@ Bundle-Localization: plugin Require-Bundle: org.eclipse.core.runtime;bundle-version="[3.33.0,4)", org.eclipse.ui;bundle-version="[3.208.0,4)", org.opentest4j;bundle-version="[1.3.0,2)", - org.eclipse.terminal.control;bundle-version="1.0.0" + org.eclipse.terminal.control;bundle-version="1.0.0", + org.eclipse.terminal.view.ui;bundle-version="[1.1.200,2.0.0)" Bundle-RequiredExecutionEnvironment: JavaSE-21 Export-Package: org.eclipse.terminal.internal.connector;x-internal:=true, org.eclipse.terminal.internal.emulator;x-internal:=true, diff --git a/terminal/tests/org.eclipse.terminal.test/build.properties b/terminal/tests/org.eclipse.terminal.test/build.properties index 8dc3df69b6b..b160f62e77d 100644 --- a/terminal/tests/org.eclipse.terminal.test/build.properties +++ b/terminal/tests/org.eclipse.terminal.test/build.properties @@ -21,3 +21,5 @@ bin.includes = META-INF/,\ src.includes = teamConfig/,\ about.html pom.model.property.testClass = org.eclipse.terminal.test.AutomatedIntegrationSuite +pom.model.property.tycho.surefire.useUIHarness = true +pom.model.property.tycho.surefire.useUIThread = true diff --git a/terminal/tests/org.eclipse.terminal.test/src/org/eclipse/terminal/test/AutomatedTestSuite.java b/terminal/tests/org.eclipse.terminal.test/src/org/eclipse/terminal/test/AutomatedTestSuite.java index d07b27e91f6..c146029c61a 100644 --- a/terminal/tests/org.eclipse.terminal.test/src/org/eclipse/terminal/test/AutomatedTestSuite.java +++ b/terminal/tests/org.eclipse.terminal.test/src/org/eclipse/terminal/test/AutomatedTestSuite.java @@ -25,6 +25,7 @@ org.eclipse.terminal.model.AllTestSuite.class, // org.eclipse.terminal.internal.connector.TerminalConnectorTest.class, // org.eclipse.terminal.internal.connector.TerminalToRemoteInjectionOutputStreamTest.class, // + org.eclipse.terminal.view.ui.tests.TerminalsViewReorderTest.class, // }) public class AutomatedTestSuite { diff --git a/terminal/tests/org.eclipse.terminal.test/src/org/eclipse/terminal/view/ui/tests/TerminalsViewReorderTest.java b/terminal/tests/org.eclipse.terminal.test/src/org/eclipse/terminal/view/ui/tests/TerminalsViewReorderTest.java new file mode 100644 index 00000000000..95c412e8eaa --- /dev/null +++ b/terminal/tests/org.eclipse.terminal.test/src/org/eclipse/terminal/view/ui/tests/TerminalsViewReorderTest.java @@ -0,0 +1,111 @@ +/******************************************************************************* + * Copyright (c) 2026 Eclipse contributors and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + *******************************************************************************/ +package org.eclipse.terminal.view.ui.tests; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.custom.CTabFolder; +import org.eclipse.swt.custom.CTabItem; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.terminal.view.ui.internal.view.TerminalsView; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +/** + * Tests for reordering terminal tabs in the terminals view (see + * issue 2679). + */ +public class TerminalsViewReorderTest { + + private static Display display = null; + + @BeforeAll + public static void createDisplay() { + if (Display.getCurrent() == null) { + display = new Display(); + } + } + + @AfterAll + public static void disposeDisplay() { + if (display != null) { + display.dispose(); + display = null; + } + } + + @Test + public void dropOverAnotherTabTargetsThatTab() { + assertEquals(2, TerminalsView.computeReorderIndex(0, 2, 4)); + } + + @Test + public void dropOverItselfIsNoOp() { + assertEquals(-1, TerminalsView.computeReorderIndex(2, 2, 4)); + } + + @Test + public void dropNextToTheTabsTargetsTheLastPosition() { + assertEquals(3, TerminalsView.computeReorderIndex(1, -1, 4)); + } + + @Test + public void dropNextToTheTabsWhileAlreadyLastIsNoOp() { + assertEquals(-1, TerminalsView.computeReorderIndex(3, -1, 4)); + } + + @Test + public void singleTabIsNeverReordered() { + assertEquals(-1, TerminalsView.computeReorderIndex(0, -1, 1)); + } + + /** + * Verifies the {@link CTabFolder#moveItem(int, int)} contract the reorder feature relies on: + * the items are reordered and the previously selected item stays selected. + */ + @Test + public void moveItemReordersAndKeepsSelection() { + Shell shell = new Shell(display); + try { + CTabFolder folder = new CTabFolder(shell, SWT.NONE); + CTabItem a = newItem(folder, "A"); + CTabItem b = newItem(folder, "B"); + newItem(folder, "C"); + newItem(folder, "D"); + + folder.setSelection(b); + + // Move "A" (index 0) to position 2: expected order is B, C, A, D. + folder.moveItem(0, 2); + + assertEquals("B", folder.getItem(0).getText()); + assertEquals("C", folder.getItem(1).getText()); + assertEquals("A", folder.getItem(2).getText()); + assertEquals("D", folder.getItem(3).getText()); + assertEquals(2, folder.indexOf(a)); + + // The selected item is unchanged even though its index moved. + assertSame(b, folder.getSelection()); + } finally { + shell.dispose(); + } + } + + private static CTabItem newItem(CTabFolder folder, String text) { + CTabItem item = new CTabItem(folder, SWT.CLOSE); + item.setText(text); + return item; + } +}