user5281858
user5281858

Reputation:

JavaFX TabPane System like in a browser

Hello, how can I add a window in JavaFX when I drag it over the tab as tab and double-click again to release it as a window again?

For example the tabs in Firefox or Chrome. Double-Click to get a new Window an drag over to add it to the tabs again.

Sorry for my english :) and thanks for help.

Upvotes: 1

Views: 2067

Answers (2)

kai
kai

Reputation: 905

I have another solution for recent FX Versions, that you might want to adapt to your needs.

Pros:

  • It works with unmodified Tabs and Tabpanes(well you have to set the skin!)
  • It doesn't use OS drag and drop and so Tabs can't be magically swallowed by another application.
  • It creates new windows if draged outside.
  • It is generic - no eclipse dependency or whatever.
  • it keeps the builtin "REORDER" functionality intact.

Cons:

  • It needs a recent FX Version, that supports REORDER.
  • I didn't test what happens, in case the system lags.
  • I didn't test what happen if you chage the reorderpolicy(well: just don't do that!).

Remarks:

  • I put the gesture statically in the skin. As the gesture is kind of part of the mouse and there is only one mouse that should be ok in the majority of cases. In some cases(different groups of tabpanes...) you might to want modify it and pass a gesture instance to the skin instead.
  • It uses Reflection reduced to a minimum- I rather would have avoided that but I need to place the mousehandlers.
  • For the drag the gesture creates a screenshot of the content which fails if there is no content, so I replaced that for stackoverflow with a screenshot of the tabPane. You may want to replace it with whatever you want to show while dragging.
  • You may want to look at Error/Exceptionhandling - I didn't bother about that.

The Skin:

/**
 * Modified skin, that allows detaching. This is a somewhat crude "Hack". But I
 * need access to the private moushandlers. In the skin almost everything is
 * private. And it consumes events I need..
 *
 * @author Kai
 */
public class DragTabPaneSkin extends TabPaneSkin {

    private static final DragPaneDragContext dragContext = new DragPaneDragContext();
    private EventHandler<MouseEvent> orig_headerMouseReleasedHandler;
    private EventHandler<MouseEvent> orig_headerMousePressedHandler;
    private EventHandler<MouseEvent> orig_headerMouseDraggedHandler;
    private StackPane tabHeaderArea;

    public void onMousePressed(MouseEvent event) {
        orig_headerMousePressedHandler.handle(event);
        //don't realy like this but as everything is private....
        //find the tab by looking up the index of the  tab skin in its parent:
        final StackPane tabSkin =(StackPane) event.getSource();
        final int index = tabSkin.getParent().getChildrenUnmodifiable().indexOf(tabSkin);
        final Tab tab = getSkinnable().getTabs().get(index);
        dragContext.start(tabHeaderArea,tab);
        dragContext.onMousePressedHeader(event);
    }

    public void onMouseReleased(MouseEvent event) {
        orig_headerMouseReleasedHandler.handle(event);
        dragContext.finish(tabHeaderArea, event.getScreenX(), event.getScreenY());
    }

    public void onMouseDragged(MouseEvent event) {
        dragContext.set(event.getScreenX(), event.getScreenY());
        if (!dragContext.dragPopup.isShowing()) {
            orig_headerMouseDraggedHandler.handle(event);
        }
    }

    private <T> T hijackField(String name, T newValue) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException {
        final Field field = TabPaneSkin.class.getDeclaredField(name);
        field.setAccessible(true);
        final T result = (T) field.get(this);
        if(null != newValue) field.set(this, newValue);
        return result;
    }

    {
        try {
            //hijack all the mouse handlers and replace them by ours in order to chain them).
            var policy = this.getSkinnable().getTabDragPolicy();
             this.getSkinnable().setTabDragPolicy(FIXED);//remove the listeners
            tabHeaderArea = hijackField("tabHeaderArea", null);
            orig_headerMouseDraggedHandler = hijackField("headerDraggedHandler", (EventHandler<MouseEvent>) this::onMouseDragged);
            orig_headerMousePressedHandler = hijackField("headerMousePressedHandler", (EventHandler<MouseEvent>) this::onMousePressed);
            orig_headerMouseReleasedHandler = hijackField("headerMouseReleasedHandler",(EventHandler<MouseEvent>) this::onMouseReleased);
            this.getSkinnable().setTabDragPolicy(policy);     //add new listeners.
        } catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException ex) {
        }
        this.getSkinnable().setSkin(this);
        dragContext.connectTabPane(this.getSkinnable());
    }

    public DragTabPaneSkin(TabPane tp) {
        super(tp);
    }
}

This is the gesture:

/** Context that does the drag operation.
 *
 */
 public class DragPaneDragContext {
     public Tab tab = null;
     public boolean finishup = false;
     protected final Popup dragPopup;
     protected final ImageView dragView;
     private final EventHandler<MouseEvent> headerMouseExited = this::onMouseExitedHeader;
     private final EventHandler<MouseEvent> headerMouseEntered = this::onMouseEnteredHeader;
     private final EventHandler<MouseEvent> headerDraggedDetected = this::onMouseDraggDetectedHeader;
     private final EventHandler<MouseEvent> mouseExitedTabPane = this::onMouseExitedPane;
     private final EventHandler<MouseEvent> mouseEnteredTabPane = this::onMouseEnteredPane;
     private final EventHandler<MouseEvent> mouseDragReleasedTabPane = this::onMouseDragReleasedPane;
     {
         dragPopup = new Popup();
         dragView = new ImageView();
         dragView.setMouseTransparent(true);
         dragPopup.getContent().setAll(dragView);
     }

     public void set(Tab tab, double x, double y) {
         this.tab = tab;
         this.dragPopup.setX(x);
         this.dragPopup.setY(y);
     }

     public void set(Tab tab) {
         this.tab = tab;
     }

     public void set(double x, double y) {
         this.dragPopup.setX(x);
         this.dragPopup.setY(y);
     }

     public double getX() {
         return this.dragPopup.getX();
     }

     public double getY() {
         return this.dragPopup.getY();
     }

     public void start(StackPane tabHeaderArea, Tab tab) {
         finishup = false;
         this.tab = tab;
         connectHeader(tabHeaderArea);
         dragView.setImage(tab.getTabPane().snapshot(null, null));
         dragView.setFitHeight(100);
         dragView.setFitWidth(100);
     }

     public void complete() {
         if (null == tab) {
             return;
         }
         finishup = false;
         dragPopup.hide();
         this.tab = null;
     }

     public void finish(StackPane tabHeaderArea, double x, double y) {
        disConnectHeader(tabHeaderArea);
        if (null == tab || !dragPopup.isShowing()) {
            complete();
            return;
        }
        finishup = true;
        final SelfClosingTabPane tp = new SelfClosingTabPane();
        set(x, y);
        dragPopup.hide();
        tab.getTabPane().getTabs().remove(tab);
        tp.getTabs().add(tab);
        tp.show(x - 5, y - 5);
        tp.requestFocus();
        tp.getSelectionModel().select(tab);
    }

    private void onMouseExitedHeader(MouseEvent event) {
        if (null == tab) {
            return; //shouldn't happen.
        }
        dragPopup.show(tab.getTabPane(), event.getScreenX(), event.getScreenY());
    }

    private void onMouseEnteredHeader(MouseEvent event) {
        dragPopup.hide();
    }

    private void onMouseDraggDetectedHeader(MouseEvent t) {
        Node source = (Node) t.getSource();
        source.startFullDrag();
    }

    public void onMousePressedHeader(MouseEvent event) {
        this.dragPopup.setX(event.getScreenX());
        this.dragPopup.setY(event.getScreenY());
    }

    private void onMouseEnteredPane(MouseEvent event) {
        if (!finishup || !(event.getSource() instanceof TabPane)) {
            return;
        }
        final TabPane source = (TabPane) event.getSource();
        Point2D local = source.screenToLocal(getX(), getY());
        if (tab != null && tab.getTabPane() != source && source.contains(local.add(5,5)) && source.contains(local.subtract(5,5))) {
            tab.getTabPane().getTabs().remove(tab);
            source.getTabs().add(tab);
            source.getSelectionModel().select(tab); 
            source.requestFocus();
        }
        complete();
    }

    private void onMouseExitedPane(MouseEvent t) {
        if (finishup) {
            complete();
        }
    }

    private void onMouseDragReleasedPane(MouseEvent t) {
        if (!(t.getSource() instanceof TabPane)) {
            return;
        }
        final TabPane source = (TabPane) t.getSource();
        if (null != tab && tab.getTabPane() != source) {
            tab.getTabPane().getTabs().remove(tab);
            source.getTabs().add(tab);
            source.getSelectionModel().select(tab); 
            source.requestFocus();
        }
        complete();
    }

    public void connectTabPane(TabPane pane) {
        pane.addEventHandler(MouseEvent.MOUSE_EXITED, mouseExitedTabPane);
        pane.addEventHandler(MouseEvent.MOUSE_ENTERED, mouseEnteredTabPane);
        pane.addEventHandler(MouseDragEvent.MOUSE_DRAG_RELEASED, mouseDragReleasedTabPane);
    }

    private void connectHeader(StackPane tabHeaderArea) {
        tabHeaderArea.addEventHandler(MouseEvent.MOUSE_EXITED, headerMouseExited);
        tabHeaderArea.addEventHandler(MouseEvent.MOUSE_ENTERED, headerMouseEntered);
        tabHeaderArea.addEventHandler(MouseEvent.DRAG_DETECTED, headerDraggedDetected);
    }

    private void disConnectHeader(StackPane tabHeaderArea){
        tabHeaderArea.removeEventHandler(MouseEvent.MOUSE_EXITED, headerMouseExited);
        tabHeaderArea.removeEventHandler(MouseEvent.MOUSE_ENTERED, headerMouseEntered);
        tabHeaderArea.removeEventHandler(MouseEvent.DRAG_DETECTED, headerDraggedDetected);
    }
}

The Window that is created when a tab is dragged outside:

/**
 * A Window with a Tapbpane, that closes when all Tabs are gone.
 *
 * @author Kai
 */
public class SelfClosingTabPane extends TabPane {

    private final Stage stage;

    private void changeListener(ListChangeListener.Change<? extends Tab> change) {
        if (getTabs().isEmpty()) {
            stage.close();
        }
    }

    public SelfClosingTabPane() {
        super();
        setTabDragPolicy(TabPane.TabDragPolicy.REORDER);
        stage = new Stage();
        stage.initModality(Modality.NONE);
        stage.setScene(new Scene(this));
        this.setSkin(new DragTabPaneSkin(this));
        this.getTabs().addListener(this::changeListener);
    }

    public SelfClosingTabPane(Window owner) {
        this();
        stage.initOwner(owner);
    }

    public SelfClosingTabPane(String title, Window owner) {
        this(title);
        stage.initOwner(owner);
    }

    public SelfClosingTabPane(String title) {
        this();
        stage.setTitle(title);
    }

    public Stage getStage() {
        return this.stage;
    }

    public void show(double x, double y) {
        stage.setX(x);
        stage.setY(y);
        stage.show();
    }
}

Finally a draggable Tabpane(It does nothing but setting the skin) :

/**
 * TabPane that allows Tabs too be detached;
 *
 * @author Kai
 */
public class DetachableTabPane extends TabPane {

    {
        setTabDragPolicy(TabPane.TabDragPolicy.REORDER);
        setSkin(new DragTabPaneSkin(this));
    }

    public DetachableTabPane() {
        super();
    }

    public DetachableTabPane(Tab... tabs) {
        super(tabs);
    }

    public void add(String name,Node node){
      this.getTabs().add(new Tab(name,node));
    }
}

Upvotes: 1

GOXR3PLUS
GOXR3PLUS

Reputation: 7265

🐝

GitHub Library

A library exists in GitHub with name DNDTabPane.Mention that it needs improvements ,although it works well with some modifications.

Link:

https://github.com/sibvisions/javafx.DndTabPane

Demonstration:

https://tomsondev.bestsolution.at/2014/05/12/e4-on-javafx-drag-and-drop-between-stacks-javafx-tabpanes/

Similar question

Javafx drag and drop TabPane

Runnable Code

You can find runnable code here which doesn't use the library above

http://berry120.blogspot.gr/2014/01/draggable-and-detachable-tabs-in-javafx.html?m=1

Finally

As soon as i get back to computer i will provide more..Maybe committing a mix library of the above on GitHub.Be ready!

🐝

Upvotes: 2

Related Questions