Search code examples
javaeventsjavafxtabsdrag-and-drop

How to drag and drop tabs of the SAME tabpane?


I've seen a lot on how to drag tabs of a tabpane to another, which by the way I don't see the point, I guess it can be useful to some extent but the most natural application should be to reorder tabs of the same tabpane, matter which nobody seems to talk about on the internet.

I'd like the behaviour that pretty much all the browsers have implemented.

I've tried something, but it's not doing what I want. The main problem comes from the fact that a Tab cannot be bound with the setOnDragDetected, setOnDragOver etc methods (for some reason that escapes me by the way ...) which results in overcomplicated algorithms to compensate this weakness of the library.

So here's my code, it's not doing what I want because (and it's only a workaround) I only detect the "drop target" AFTER the drop was made, thus making it useless. And yes, for SOME reason, hovering an object while holding the click button doesnt result in a mouseover event ...

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.Tab;
import javafx.scene.control.TabPane;
import javafx.scene.input.ClipboardContent;
import javafx.scene.input.Dragboard;
import javafx.scene.input.TransferMode;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;

public class DragAndDrop extends Application
{
    public void start(Stage primaryStage)
    {
        TabPane tabPane = new TabPane();

        final int[] target = {0};

        for(int i = 0 ; i < 2 ; i ++)
        {
            Tab tab = new Tab();
            tab.setClosable(false);

            int index[] = {i};

            tabPane.getTabs().add(tab);

            Label label = new Label("Tab " + (i + 1), new Rectangle(16, 16, Color.TRANSPARENT));
            tab.setGraphic(label);

            tab.getGraphic().setOnMouseEntered(event ->
            {
                target[0] = index[0];
                System.out.println("target : " + target[0]);
            });
        }

        tabPane.setOnDragDetected(event ->
        {
            Dragboard dragboard = tabPane.startDragAndDrop(TransferMode.ANY);

            ClipboardContent clipboardContent = new ClipboardContent();
            clipboardContent.putString(Integer.toString(tabPane.getSelectionModel().getSelectedIndex()));
            System.out.println("source : " + tabPane.getSelectionModel().getSelectedIndex());
            dragboard.setContent(clipboardContent);

            event.consume();
        });

        tabPane.setOnDragOver(event ->
        {
            if(event.getDragboard().hasString())
            {
                event.acceptTransferModes(TransferMode.COPY_OR_MOVE);
            }

            event.consume();
        });

        tabPane.setOnDragDropped(event ->
        {
            Dragboard dragboard = event.getDragboard();
            boolean success = false;

            if(dragboard.hasString())
            {
                System.out.println("from " + dragboard.getString() + " to " + target[0]);

                success = true;
            }

            event.setDropCompleted(success);
            event.consume();
        });

        tabPane.setOnDragDone(event ->
        {
            if(event.getTransferMode() == TransferMode.MOVE)
            {
                // tabPane.setText("");
            }
            event.consume();
        });

        primaryStage.setScene(new Scene(tabPane, 300, 200));
        primaryStage.show();
    }

    public static void main(String[] args)
    {
        launch(args);
    }
}

Solution

  • I think the easiest way to approach this is to register the drag handlers with the graphic associated with the tab, instead of with the tab pane. This makes it a bit easier to manage the index of the tabs, etc, and you are not relying on the interplay between mouse drag actions and selection (you really have no idea what order those will happen in). The downside to this approach is that you somehow have to ensure that each tab has a graphic (you can use a label as the graphic to display text).

    Here's a general purpose "tab pane dragging support" class. It is not meant to be production quality, by any means, but the basics seem to work. The idea here is that you create a DraggingTabPaneSupport instance and use it to add dragging support to one or more TabPanes. Tabs can then be dragged within or between any of those panes. If you want to be able to only drag within each tab pane, create a DraggingTabPaneSupport instance for each tab pane.

    import java.util.concurrent.atomic.AtomicLong;
    
    import javafx.collections.ListChangeListener.Change;
    import javafx.scene.Node;
    import javafx.scene.control.Label;
    import javafx.scene.control.Tab;
    import javafx.scene.control.TabPane;
    import javafx.scene.input.ClipboardContent;
    import javafx.scene.input.Dragboard;
    import javafx.scene.input.TransferMode;
    
    public class DraggingTabPaneSupport {
    
        private Tab currentDraggingTab ;
    
        private static final AtomicLong idGenerator = new AtomicLong();
    
        private final String draggingID = "DraggingTabPaneSupport-"+idGenerator.incrementAndGet() ;
    
        public void addSupport(TabPane tabPane) {
            tabPane.getTabs().forEach(this::addDragHandlers);
            tabPane.getTabs().addListener((Change<? extends Tab> c) -> {
                while (c.next()) {
                    if (c.wasAdded()) {
                        c.getAddedSubList().forEach(this::addDragHandlers);
                    }
                    if (c.wasRemoved()) {
                        c.getRemoved().forEach(this::removeDragHandlers);
                    }
                }
            });
    
            // if we drag onto a tab pane (but not onto the tab graphic), add the tab to the end of the list of tabs:
            tabPane.setOnDragOver(e -> {
                if (draggingID.equals(e.getDragboard().getString()) && 
                        currentDraggingTab != null &&
                        currentDraggingTab.getTabPane() != tabPane) {
                    e.acceptTransferModes(TransferMode.MOVE);
                }
            });
            tabPane.setOnDragDropped(e -> {
                if (draggingID.equals(e.getDragboard().getString()) && 
                        currentDraggingTab != null &&
                        currentDraggingTab.getTabPane() != tabPane) {
    
                    currentDraggingTab.getTabPane().getTabs().remove(currentDraggingTab);
                    tabPane.getTabs().add(currentDraggingTab);
                    currentDraggingTab.getTabPane().getSelectionModel().select(currentDraggingTab);
                }
            });
        }
    
        private void addDragHandlers(Tab tab) {
    
            // move text to label graphic:
            if (tab.getText() != null && ! tab.getText().isEmpty()) {
                Label label = new Label(tab.getText(), tab.getGraphic());
                tab.setText(null);
                tab.setGraphic(label);
            }
    
            Node graphic = tab.getGraphic();
            graphic.setOnDragDetected(e -> {
                Dragboard dragboard = graphic.startDragAndDrop(TransferMode.MOVE);
                ClipboardContent content = new ClipboardContent();
                // dragboard must have some content, but we need it to be a Tab, which isn't supported
                // So we store it in a local variable and just put arbitrary content in the dragbaord:
                content.putString(draggingID);
                dragboard.setContent(content);
                dragboard.setDragView(graphic.snapshot(null, null));
                currentDraggingTab = tab ;
            });
            graphic.setOnDragOver(e -> {
                if (draggingID.equals(e.getDragboard().getString()) && 
                        currentDraggingTab != null &&
                        currentDraggingTab.getGraphic() != graphic) {
                    e.acceptTransferModes(TransferMode.MOVE);
                }
            });
            graphic.setOnDragDropped(e -> {
                if (draggingID.equals(e.getDragboard().getString()) && 
                        currentDraggingTab != null &&
                        currentDraggingTab.getGraphic() != graphic) {
    
                    int index = tab.getTabPane().getTabs().indexOf(tab) ;
                    currentDraggingTab.getTabPane().getTabs().remove(currentDraggingTab);
                    tab.getTabPane().getTabs().add(index, currentDraggingTab);
                    currentDraggingTab.getTabPane().getSelectionModel().select(currentDraggingTab);
                }
            });
            graphic.setOnDragDone(e -> currentDraggingTab = null);
        }
    
        private void removeDragHandlers(Tab tab) {
            tab.getGraphic().setOnDragDetected(null);
            tab.getGraphic().setOnDragOver(null);
            tab.getGraphic().setOnDragDropped(null);
            tab.getGraphic().setOnDragDone(null);
        }
    }
    

    Here's a quick example of using it. In this example I create three tab panes. I create one dragging support instance for the first two panes, so tabs can be dragged between those two, and a second dragging support instance for the third pane, so in the third pane tabs can be re-ordered by dragging but cannot be dragged to the other two panes.

    import java.util.Random;
    
    import javafx.application.Application;
    import javafx.scene.Scene;
    import javafx.scene.control.Tab;
    import javafx.scene.control.TabPane;
    import javafx.scene.layout.Region;
    import javafx.scene.layout.VBox;
    import javafx.scene.paint.Color;
    import javafx.scene.shape.Rectangle;
    import javafx.stage.Stage;
    
    public class DraggingTabPaneExample extends Application {
    
        private final Random rng = new Random();
    
        @Override
        public void start(Stage primaryStage) {
            TabPane[] panes = new TabPane[] {new TabPane(), new TabPane(), new TabPane() };
            VBox root = new VBox(10, panes);
            for (int i = 1 ; i <= 15; i++) {
                Tab tab = new Tab("Tab "+i);
                tab.setGraphic(new Rectangle(16, 16, randomColor()));
                Region region = new Region();
                region.setMinSize(100, 150);
                tab.setContent(region);
                panes[(i-1) % panes.length].getTabs().add(tab);
            }
    
            DraggingTabPaneSupport support1 = new DraggingTabPaneSupport();
            support1.addSupport(panes[0]);
            support1.addSupport(panes[1]);
            DraggingTabPaneSupport support2 = new DraggingTabPaneSupport();
            support2.addSupport(panes[2]);
    
            Scene scene = new Scene(root, 600, 600);
            primaryStage.setScene(scene);
            primaryStage.show();
        }
    
        private Color randomColor() {
            return Color.rgb(rng.nextInt(256), rng.nextInt(256), rng.nextInt(256));
        }
    
        public static void main(String[] args) {
            launch(args);
        }
    }
    

    Initial screenshot:

    enter image description here

    After dragging within panes: enter image description here

    You can also drag between the first two panes (but not between the third and any others):

    enter image description here