I'm experiencing some problems/confusion with DnD between JTrees. After reading the documentation of TransferHandler
and finding the following
canImport( TransferHandler.TransferSupport support )
This method is called repeatedly during a drag and drop operation to allow the developer to configure properties of, and to return the acceptability of transfers; with a return value of true indicating that the transfer represented by the given TransferSupport (which contains all of the details of the transfer) is acceptable at the current time, and a value of false rejecting the transfer.
I implemented the class below (long code, apologies). The idea is to show an information tooltip next to the tree which explains why a drop is or is not possible. The implementation relies on canImport
being called repeatedly and it works nicely on my development platform (windows). However when testing on Linux/Mac it does not work, since the timer I'm using does not get reset (canImport
is called only on mouseMoved events, which, I admit, sounds logical).
Is this normal behavior or a bug in one of the java implementations (or mine)? Any suggestions on how to change my code so it would work as it does now on windows (I'm thinking of temporarily adding a mouse listener to the tree component and hide tooltip on mouseExited
)?
import java.awt.*;
import java.awt.datatransfer.*;
import java.awt.event.*;
import java.io.IOException;
import javax.swing.*;
import javax.swing.tree.*;
public class DndDemo extends JFrame {
private JSplitPane jsppMain;
private JScrollPane jscpTarget;
private JTree jtTarget;
private JScrollPane jscpSource;
private JTree jtSource;
public DndDemo() {
initComponents();
}
public static void main(String[] args) {
EventQueue.invokeLater(new Runnable() {
public void run() {
DndDemo demo = new DndDemo();
demo.setVisible(true);
}
});
}
private void initComponents() {
setLayout(new BorderLayout());
DefaultMutableTreeNode root = new DefaultMutableTreeNode("Drag here");
DefaultTreeModel model = new DefaultTreeModel(root);
for (int i = 0; i < 10; i++) {
DefaultMutableTreeNode child = new DefaultMutableTreeNode("Member" + i);
model.insertNodeInto(child, root, i);
}
jtTarget = new JTree(model);
jscpTarget = new JScrollPane(jtTarget);
root = new DefaultMutableTreeNode("Drag from here");
model = new DefaultTreeModel(root);
for (int i = 0; i < 10; i++) {
DefaultMutableTreeNode child = new DefaultMutableTreeNode("Option" + i);
model.insertNodeInto(child, root, i);
}
jtSource = new JTree(model);
jscpSource = new JScrollPane(jtSource);
jsppMain = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, jscpTarget, jscpSource);
jsppMain.setDividerLocation(150);
add(jsppMain);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
jtTarget.setDragEnabled(true);
jtTarget.setTransferHandler(new TreeTransferHandler());
jtTarget.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
jtSource.setDragEnabled(true);
jtSource.setTransferHandler(new TreeTransferHandler());
jtSource.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
setBounds(0, 0, 300, 300);
setLocationRelativeTo(null);
}
/**
* This is the meat of my code. Everything else is just support code.
*/
private class TreeTransferHandler extends TransferHandler {
private Popup tipWindow;
private TreePath tipPath;
private Timer tooltipTimer;
public TreeTransferHandler() {
// after showing the tip close it when the timer ends
tooltipTimer = new Timer(100, new ActionListener() {
public void actionPerformed(ActionEvent e) {
hideDropTooltip();
}
});
tooltipTimer.setRepeats(false);
}
@Override
public int getSourceActions(JComponent c) {
return TransferHandler.MOVE;
}
@Override
protected Transferable createTransferable(JComponent c) {
JTree tree = (JTree) c;
if (tree == jtTarget) {
return null;
}
TreePath selectionPath = tree.getSelectionPath();
if (selectionPath == null) {
return null;
}
DefaultMutableTreeNode node = (DefaultMutableTreeNode) selectionPath.getLastPathComponent();
return new TreeTransferable(node, TreeTransferable.SOURCE);
}
@Override
public boolean canImport(TransferHandler.TransferSupport support) {
// this method is supposed to get called repeatedly during DnD
// according to it's doc. On windows it is, but linux/mac only
// call it on mouse moves (presumably).
DefaultMutableTreeNode node;
int src;
try {
node = (DefaultMutableTreeNode) support.getTransferable().getTransferData(TreeTransferable.NODE_FLAVOR);
src = (Integer) support.getTransferable().getTransferData(TreeTransferable.SRC_FLAVOR);
} catch (UnsupportedFlavorException ex) {
updateDropTooltip(support, "Unsupported DnD object", false);
return false;
} catch (IOException ex) {
updateDropTooltip(support, "Unsupported DnD object", false);
return false;
}
JTree tree = (JTree) support.getComponent();
TreePath path;
if (support.isDrop()) {
JTree.DropLocation dl = (JTree.DropLocation) support.getDropLocation();
path = dl.getPath();
} else {
path = tree.getSelectionPath();
}
DefaultMutableTreeNode target = (DefaultMutableTreeNode) path.getLastPathComponent();
String nodeName = (String) node.getUserObject();
String targetName = (String) target.getUserObject();
if (targetName.endsWith(nodeName.substring(nodeName.length() - 1))) {
updateDropTooltip(support, "Drop here to add option", true);
return true;
} else {
updateDropTooltip(support, "Unsupported option", false);
return false;
}
}
@Override
public boolean importData(TransferHandler.TransferSupport support) {
return true;
}
private void hideDropTooltip() {
tooltipTimer.stop();
if (tipWindow != null) {
tipWindow.hide();
tipWindow = null;
}
}
private void updateDropTooltip(TransferHandler.TransferSupport support, String message, boolean allowed) {
if (message != null) {
JTree tree = (JTree) support.getComponent();
TreePath path;
if (support.isDrop()) {
JTree.DropLocation dl = (JTree.DropLocation) support.getDropLocation();
path = dl.getPath();
} else {
path = tree.getSelectionPath();
}
if (tipWindow != null) {
if (tipPath == null || !tipPath.equals(path)) {
hideDropTooltip();
}
}
if (tipWindow == null) {
tipPath = path;
JToolTip tip = tree.createToolTip();
tip.setTipText(
"<html>("
+ (allowed
? "yes"
: "no")
+ ")" + message + "</html>");
PopupFactory popupFactory = PopupFactory.getSharedInstance();
Rectangle cellRect = tree.getPathBounds(path);
Point location = tree.getLocationOnScreen();
location.x += cellRect.x;
location.y += cellRect.y;
tipWindow = popupFactory.getPopup(tree, tip, location.x + cellRect.width, location.y);
tipWindow.show();
tooltipTimer.restart();
} else {
tooltipTimer.restart();
}
} else {
hideDropTooltip();
}
}
}
private static class TreeTransferable implements Transferable {
public static final int SOURCE = 0;
public static final int DESTINATION = 0;
public static final DataFlavor NODE_FLAVOR = new DataFlavor(DefaultMutableTreeNode.class, "Tree Node");
public static final DataFlavor SRC_FLAVOR = new DataFlavor(Integer.class, "Source");
private DefaultMutableTreeNode node;
private int src;
private DataFlavor[] flavors = new DataFlavor[] {
NODE_FLAVOR, SRC_FLAVOR
};
public TreeTransferable(DefaultMutableTreeNode node, int src) {
this.node = node;
this.src = src;
}
public DataFlavor[] getTransferDataFlavors() {
return flavors;
}
public boolean isDataFlavorSupported(DataFlavor flavor) {
for (DataFlavor flv : flavors) {
if (flavor.equals(flv)) {
return true;
}
}
return false;
}
public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException, IOException {
if (flavor.equals(NODE_FLAVOR)) {
return node;
} else if (flavor.equals(SRC_FLAVOR)) {
return src;
} else {
throw new UnsupportedFlavorException(flavor);
}
}
}
}
Edit01
It just occurred to me that listening to mouse events will not work while dragging. So the workaround I said I'd try is not really an option.
Still don't know whether the above symptoms are normal or not, but I hacked in a workaround. I changed the implementation of my timer to do the following:
tooltipTimer = new Timer(100, new ActionListener() {
public void actionPerformed(ActionEvent e) {
if (currTree.getMousePosition() == null) {
hideDropTooltip();
} else {
tooltipTimer.restart();
}
}
});
where currTree
field is set within updateDropTooltip(...)
. I now check whether the mouse has left my target tree with getMousePosition()
and restart the timer if it hasn't. Seems to work on all platforms I wish to support.