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, th