I have a question about dragging and droping: I can drop labels, text or icon. But I want to drag and drop a JPanel with all its components (Label, Textbox,..etc).
How can I do this ?
I have a question about dragging and droping: I can drop labels, text or icon. But I want to drag and drop a JPanel with all its components (Label, Textbox,..etc).
How can I do this ?
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.
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 NotSerializableException
s 🙄
I was required to update the DragGestureHandler
and DropHandler
classes...
public class DragGestureHandler implements DragGestureListener, DragSourceListener, Serializable {
//...
}
public public class DropHandler implements DropTargetListener, Serializable {
//...
}
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
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 Component
s it failed - until the above fix was applied 🙄
That code is a HUGE help MadProgrammer. For anyone wanting to use those classes, but wants to initiate the drag from a button in the panel you are dragging, I simply replaced the extended JPanel with one for a JButton that takes the panel in the constructor:
public class DragActionButton extends JButton {
private DragGestureRecognizer dgr;
private DragGestureHandler dragGestureHandler;
private JPanel actionPanel;
DragActionButton (JPanel actionPanel, String buttonText)
{
this.setText(buttonText);
this.actionPanel = actionPanel;
}
@Override
public void addNotify() {
super.addNotify();
if (dgr == null) {
dragGestureHandler = new DragGestureHandler(this.actionPanel);
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();
}
}
then you'd do this when creating the button:
this.JButtonDragIt = new DragActionButton(this.JPanel_To_Drag, "button-text-here");