Java – How to drag and drop JPanel with its components

This solution works. Some cavets to start with.

I didn’t use the TransferHandler API. I don’t like it, it’s too restrictive, but that’s a personal thing (what it does, it does well), so this might not meet your expectations.

I was testing with BorderLayout. If you want to use other layouts, you’re going to have to try and figure that out. The DnD subsystem does provide information about the mouse point (when moving and dropping).

So what do we need:

A DataFlavor. I chose to do this because it allows a greater deal of restriction

public class PanelDataFlavor extends DataFlavor {

    // This saves me having to make lots of copies of the same thing
    public static final PanelDataFlavor SHARED_INSTANCE = new PanelDataFlavor();

    public PanelDataFlavor() {

        super(JPanel.class, null);

    }

}

A Transferable. Some kind of wrapper that wraps the data (our JPanel) up with a bunch of DataFlavors (in our case, just the PanelDataFlavor)

public class PanelTransferable implements Transferable {

    private DataFlavor[] flavors = new DataFlavor[]{PanelDataFlavor.SHARED_INSTANCE};
    private JPanel panel;

    public PanelTransferable(JPanel panel) {
        this.panel = panel;
    }

    @Override
    public DataFlavor[] getTransferDataFlavors() {
        return flavors;
    }

    @Override
    public boolean isDataFlavorSupported(DataFlavor flavor) {

        // Okay, for this example, this is overkill, but makes it easier
        // to add new flavor support by subclassing
        boolean supported = false;

        for (DataFlavor mine : getTransferDataFlavors()) {

            if (mine.equals(flavor)) {

                supported = true;
                break;

            }

        }

        return supported;

    }

    public JPanel getPanel() {

        return panel;

    }

    @Override
    public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException, IOException {

        Object data = null;
        if (isDataFlavorSupported(flavor)) {

            data = getPanel();

        } else {

            throw new UnsupportedFlavorException(flavor);

        }

        return data;

    }

}

A “DragGestureListener”

For this, I created a simple DragGestureHandler that takes a “JPanel” as the content to be dragged. This allows the gesture handler to become self managed.

public class DragGestureHandler implements DragGestureListener, DragSourceListener {

    private Container parent;
    private JPanel child;

    public DragGestureHandler(JPanel child) {

        this.child = child;

    }

    public JPanel getPanel() {
        return child;
    }

    public void setParent(Container parent) {
        this.parent = parent;
    }

    public Container getParent() {
        return parent;
    }

    @Override
    public void dragGestureRecognized(DragGestureEvent dge) {

        // When the drag begins, we need to grab a reference to the
        // parent container so we can return it if the drop
        // is rejected
        Container parent = getPanel().getParent();

        setParent(parent);

        // Remove the panel from the parent.  If we don't do this, it
        // can cause serialization issues.  We could overcome this
        // by allowing the drop target to remove the component, but that's
        // an argument for another day
        parent.remove(getPanel());

        // Update the display
        parent.invalidate();
        parent.repaint();

        // Create our transferable wrapper
        Transferable transferable = new PanelTransferable(getPanel());

        // Start the "drag" process...
        DragSource ds = dge.getDragSource();
        ds.startDrag(dge, Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR), transferable, this);

    }

    @Override
    public void dragEnter(DragSourceDragEvent dsde) {
    }

    @Override
    public void dragOver(DragSourceDragEvent dsde) {
    }

    @Override
    public void dropActionChanged(DragSourceDragEvent dsde) {
    }

    @Override
    public void dragExit(DragSourceEvent dse) {
    }

    @Override
    public void dragDropEnd(DragSourceDropEvent dsde) {

        // If the drop was not successful, we need to
        // return the component back to it's previous
        // parent
        if (!dsde.getDropSuccess()) {

            getParent().add(getPanel());

            getParent().invalidate();
            getParent().repaint();

        }
    }
}

Okay, so that’s basics. Now we need to wire it all together…

So, in the panel I want to drag, I added:

    private DragGestureRecognizer dgr;
    private DragGestureHandler dragGestureHandler;

    @Override
    public void addNotify() {

        super.addNotify();

        if (dgr == null) {

            dragGestureHandler = new DragGestureHandler(this);
            dgr = DragSource.getDefaultDragSource().createDefaultDragGestureRecognizer(
                    this,
                    DnDConstants.ACTION_MOVE,
                    dragGestureHandler);

        }

    }

    @Override
    public void removeNotify() {

        if (dgr != null) {

            dgr.removeDragGestureListener(dragGestureHandler);
            dragGestureHandler = null;

        }

        dgr = null;

        super.removeNotify();

    }

The reason for using the add/remove notify in this way is to keep the system clean. It helps prevent events from been delivered to our component when we no longer need them. It also provides automatic registration. You may wish to use your own “setDraggable” method.

That’s the drag side, now for the drop side.

First, we need a DropTargetListener:

public class DropHandler implements DropTargetListener {

    @Override
    public void dragEnter(DropTargetDragEvent dtde) {

        // Determine if we can actually process the contents coming in.
        // You could try and inspect the transferable as well, but 
        // there is an issue on the MacOS under some circumstances
        // where it does not actually bundle the data until you accept the
        // drop.
        if (dtde.isDataFlavorSupported(PanelDataFlavor.SHARED_INSTANCE)) {

            dtde.acceptDrag(DnDConstants.ACTION_MOVE);

        } else {

            dtde.rejectDrag();

        }

    }

    @Override
    public void dragOver(DropTargetDragEvent dtde) {
    }

    @Override
    public void dropActionChanged(DropTargetDragEvent dtde) {
    }

    @Override
    public void dragExit(DropTargetEvent dte) {
    }

    @Override
    public void drop(DropTargetDropEvent dtde) {

        boolean success = false;

        // Basically, we want to unwrap the present...
        if (dtde.isDataFlavorSupported(PanelDataFlavor.SHARED_INSTANCE)) {

            Transferable transferable = dtde.getTransferable();
            try {

                Object data = transferable.getTransferData(PanelDataFlavor.SHARED_INSTANCE);
                if (data instanceof JPanel) {

                    JPanel panel = (JPanel) data;

                    DropTargetContext dtc = dtde.getDropTargetContext();
                    Component component = dtc.getComponent();

                    if (component instanceof JComponent) {

                        Container parent = panel.getParent();
                        if (parent != null) {

                            parent.remove(panel);

                        }

                        ((JComponent)component).add(panel);

                        success = true;
                        dtde.acceptDrop(DnDConstants.ACTION_MOVE);

                        invalidate();
                        repaint();

                    } else {

                        success = false;
                        dtde.rejectDrop();

                    }

                } else {

                    success = false;
                    dtde.rejectDrop();

                }

            } catch (Exception exp) {

                success = false;
                dtde.rejectDrop();
                exp.printStackTrace();

            }

        } else {

            success = false;
            dtde.rejectDrop();

        }

        dtde.dropComplete(success);

    }

}

Finally, we need to register the drop target with interested parties… In those containers capable of supporting the drop, you want to add

DropTarget dropTarget;
DropHandler dropHandler;

.
.
.

dropHandler = new DropHandler();
dropTarget = new DropTarget(pnlOne, DnDConstants.ACTION_MOVE, dropHandler, true);

Personally, I initialise in the addNotify and dispose in the removeNotify

dropTarget.removeDropTargetListener(dropHandler);

Just a quick note on addNotify, I have had this been called a number of times in succession, so you may want to double-check that you haven’t already set up the drop targets.

That’s it.

You may also find some of the following of interest

http://rabbit-hole.blogspot.com.au/2006/05/my-drag-image-is-better-than-yours.html

http://rabbit-hole.blogspot.com.au/2006/08/drop-target-navigation-or-you-drag.html

http://rabbit-hole.blogspot.com.au/2006/04/smooth-jlist-drop-target-animation.html

It would be waste not to check them, even if just out of interest.

2018 Update

So, after 4 years since the original code was written, there seems to have been some changes into how the API works, at least under MacOS, which are causing a number of issues 🙄.

First DragGestureHandler was causing a NullPointerException when DragSource#startDrag was been called. This seems to be related to setting the container’s parent reference to null (by removing it from the parent container).

So, instead, I modified the dragGestureRecognized method to remove the panel from the parent AFTER DragSource#startDrag was called…

@Override
public void dragGestureRecognized(DragGestureEvent dge) {
    // When the drag begins, we need to grab a reference to the
    // parent container so we can return it if the drop
    // is rejected
    Container parent = getPanel().getParent();
    System.out.println("parent = " + parent.hashCode());
    setParent(parent);

    // Remove the panel from the parent.  If we don't do this, it
    // can cause serialization issues.  We could overcome this
    // by allowing the drop target to remove the component, but that's
    // an argument for another day
    // This is causing a NullPointerException on MacOS 10.13.3/Java 8
    //      parent.remove(getPanel());
    //      // Update the display
    //      parent.invalidate();
    //      parent.repaint();

    // Create our transferable wrapper
    System.out.println("Drag " + getPanel().hashCode());
    Transferable transferable = new PanelTransferable(getPanel());
    // Start the "drag" process...
    DragSource ds = dge.getDragSource();
    ds.startDrag(dge, null, transferable, this);

    parent.remove(getPanel());
    // Update the display
    parent.invalidate();
    parent.repaint();
}

I also modified the DragGestureHandler#dragDropEnd method

@Override
public void dragDropEnd(DragSourceDropEvent dsde) {
    // If the drop was not successful, we need to
    // return the component back to it's previous
    // parent
    if (!dsde.getDropSuccess()) {
        getParent().add(getPanel());
    } else {
        getPanel().remove(getPanel());
    }
    getParent().invalidate();
    getParent().repaint();
}

And DropHandler#drop

@Override
public void drop(DropTargetDropEvent dtde) {
    boolean success = false;
    // Basically, we want to unwrap the present...
    if (dtde.isDataFlavorSupported(PanelDataFlavor.SHARED_INSTANCE)) {
        Transferable transferable = dtde.getTransferable();
        try {
            Object data = transferable.getTransferData(PanelDataFlavor.SHARED_INSTANCE);
            if (data instanceof JPanel) {
                JPanel panel = (JPanel) data;
                DropTargetContext dtc = dtde.getDropTargetContext();
                Component component = dtc.getComponent();
                if (component instanceof JComponent) {
                    Container parent = panel.getParent();
                    if (parent != null) {
                        parent.remove(panel);
                        parent.revalidate();
                        parent.repaint();
                    }
                    ((JComponent) component).add(panel);
                    success = true;
                    dtde.acceptDrop(DnDConstants.ACTION_MOVE);
                    ((JComponent) component).invalidate();
                    ((JComponent) component).repaint();
                } else {
                    success = false;
                    dtde.rejectDrop();
                }
            } else {
                success = false;
                dtde.rejectDrop();
            }
        } catch (Exception exp) {
            success = false;
            dtde.rejectDrop();
            exp.printStackTrace();
        }
    } else {
        success = false;
        dtde.rejectDrop();
    }
    dtde.dropComplete(success);
}

It’s important to note that these above modifications probably aren’t required, but they existed after the point I got the operations to work again…

Another issue I came across was a bunch of NotSerializableExceptions 🙄

I was required to update the DragGestureHandler and DropHandler classes…

public class DragGestureHandler implements DragGestureListener, DragSourceListener, Serializable {
    //...
}

public public class DropHandler implements DropTargetListener, Serializable {
    //...
}

Runnable example…

import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.GridLayout;
import java.io.Serializable;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;

public class Test implements Serializable {

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

    public Test() {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                try {
                    UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
                } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
                    ex.printStackTrace();
                }

                JFrame frame = new JFrame("Testing");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.add(new TestPane());
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
            }
        });
    }

    public class TestPane extends JPanel {

        public TestPane() {
            setLayout(new GridLayout(1, 2));

            JPanel container = new OutterPane();

            DragPane drag = new DragPane();
            container.add(drag);

            add(container);
            add(new DropPane());
        }

        @Override
        public Dimension getPreferredSize() {
            return new Dimension(200, 200);
        }

    }

    public class OutterPane extends JPanel {

        public OutterPane() {
            setBackground(Color.GREEN);
        }

        @Override
        public Dimension getPreferredSize() {
            return new Dimension(100, 100);
        }

    }

}

DragPane

import java.awt.Color;
import java.awt.Dimension;
import java.awt.dnd.DnDConstants;
import java.awt.dnd.DragGestureRecognizer;
import java.awt.dnd.DragSource;
import javax.swing.JPanel;

public class DragPane extends JPanel {

    private DragGestureRecognizer dgr;
    private DragGestureHandler dragGestureHandler;

    public DragPane() {
        System.out.println("DragPane = " + this.hashCode());
        setBackground(Color.RED);
        dragGestureHandler = new DragGestureHandler(this);
        dgr = DragSource.getDefaultDragSource().createDefaultDragGestureRecognizer(this, DnDConstants.ACTION_MOVE, dragGestureHandler);
    }

    @Override
    public Dimension getPreferredSize() {
        return new Dimension(50, 50);
    }

}

DropPane

import java.awt.Color;
import java.awt.Dimension;
import java.awt.dnd.DnDConstants;
import java.awt.dnd.DropTarget;
import javax.swing.JPanel;

public class DropPane extends JPanel {

    DropTarget dropTarget;
    DropHandler dropHandler;

    public DropPane() {
        setBackground(Color.BLUE);
    }

    @Override
    public Dimension getPreferredSize() {
        return new Dimension(100, 100);
    }

    @Override
    public void addNotify() {
        super.addNotify(); //To change body of generated methods, choose Tools | Templates.
        dropHandler = new DropHandler();
        dropTarget = new DropTarget(this, DnDConstants.ACTION_MOVE, dropHandler, true);
    }

    @Override
    public void removeNotify() {
        super.removeNotify(); //To change body of generated methods, choose Tools | Templates.
        dropTarget.removeDropTargetListener(dropHandler);
    }

}

The DragGestureHandler, DropHandler, PanelDataFlavor and PanelTransferable classes remain the same, except for the changes I’ve mentioned above. All these classes are standalone, external classes, otherwise it causes additional NotSerializableException problems

Notes

It’s possible that having the DragGestureHandler managed by the same component which is been dragged could be causing the over all issues, but I don’t have the time to investigate

It should be noted that, I don’t prompt nor condone manipulating components in this way, as it’s way to easy to end up in situations where a solution might work today, but won’t work tomorrow. I prefer to transfer state or data instead – much more stable.

I had tried a dozen other examples based around the same concept presented in the original answer which simply transferred state and they all worked without issue, it was only when trying to transfer Components it failed – until the above fix was applied 🙄

Leave a Comment