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/features/org.eclipse.terminal.feature/feature.xml b/terminal/features/org.eclipse.terminal.feature/feature.xml index 94b37131c25..044c0b1ba2c 100644 --- a/terminal/features/org.eclipse.terminal.feature/feature.xml +++ b/terminal/features/org.eclipse.terminal.feature/feature.xml @@ -2,7 +2,7 @@ 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/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; + } +}