0
votes

I am experiencing some difficulty with a drag and drop in the JTree component. My drag and drop is to allow user rearrange nodes in the tree. My implementation works mostly fine, but in some situations there is a null pointer exception when moving a node in between of its children, like on the picture below I am dragging C under B (between B and A):

Drag between children

The direct cause of the exception seems to be that while performing cleanup of DnD framework, path to the item being dragged was invalidated by the changes I have performed on the tree to implement the required move.

More specifically, I get a callstack like this:

javax.swing.plaf.basic.BasicTreeUI#getDropLineRect
java.beans.PropertyChangeSupport#firePropertyChange
...
javax.swing.TransferHandler.DropHandler#cleanup
...
javax.swing.TransferHandler.SwingDropTarget#drop

The getDropLineRect is clearly attempting to evaluate a rectangle for a path in the tree which no longer exists, as the node was already deleted when the move was performed in my importData function.

My question is: is this expected? Is it considered unsafe to perform data changes while DnD has not completed yet, and should they be queued and performed later? I did not see any such requirement in any documentation, and I did not see such async implementation in any of the examples or tutorials.

2

2 Answers

0
votes

IMHO it would not only be saver - as you stated yourself - to do any data modifications AFTER the drop occured, but also easier to implement. If you already remove the node on the drag event, you need to have a rollback at hand to handle the situation of removing the node from you window or making an illegal drop. On the other hand, if you use the drag event to just gain the needed information for a successful drop and don't change anything in your data yet, the previously stated problems dissapear. On drop, you would use the remembered information from the drag and complete the whole operation, or discard the drop, which in return rollbacks the visual actions (though I'm not 100% sure here), thus remaining a consistent state all the time.

Edit: "My question is: is this expected?": I think the Exception occurs from your logic of changing the tree model in between the drag and drop, making it inconsitent at a certain time for the event dispatching thread trying to redraw it from the model. Apologies for not being able to state "this is how it's done", maybe someone else finds a quote from the swing tree dnd doc.

0
votes

I still can not give a general statement, but I think it's always a good idea to look at the standard implementations, in this case, at the TreeModel. You did not state what kind of TreeModel you use. If you a custom one, you have to fire the "tree changed event" yourself, to tell the tree something changed. I found this excerpt from an example for dnd, using the DefaultTreeModel. Its ready to run and I tried to build the error you described but it seemed to work fine. The line "model.insertNodeInto" fires the appropriate event in the DefaultTreeModel. http://www.coderanch.com/t/346509/GUI/java/JTree-drag-drop-tree-Java

Notice that the event is fired during the importData and before the clean up calling, where your exception occurs.

I remember that these events only differ in the value of the arguements passed to the same constructor. I also had problems getting to fire the right event for the tree to understand me. At some point I ended up copying the calling lines from the DefaultTreeModel source and used it in my code so it just worked. The docu on this is not very specific.

public boolean importData(TransferHandler.TransferSupport support) {
    if(!canImport(support)) {
        return false;
    }
    // Extract transfer data.
    DefaultMutableTreeNode[] nodes = null;
    try {
        Transferable t = support.getTransferable();
        nodes = (DefaultMutableTreeNode[])t.getTransferData(nodesFlavor);
    } catch(UnsupportedFlavorException ufe) {
        System.out.println("UnsupportedFlavor: " + ufe.getMessage());
    } catch(java.io.IOException ioe) {
        System.out.println("I/O error: " + ioe.getMessage());
    }
    // Get drop location info.
    JTree.DropLocation dl =
            (JTree.DropLocation)support.getDropLocation();
    int childIndex = dl.getChildIndex();
    TreePath dest = dl.getPath();
    DefaultMutableTreeNode parent =
        (DefaultMutableTreeNode)dest.getLastPathComponent();
    JTree tree = (JTree)support.getComponent();
    DefaultTreeModel model = (DefaultTreeModel)tree.getModel();
    // Configure for drop mode.
    int index = childIndex;    // DropMode.INSERT
    if(childIndex == -1) {     // DropMode.ON
        index = parent.getChildCount();
    }
    // Add data to model.
    for(int i = 0; i < nodes.length; i++) {
        model.insertNodeInto(nodes[i], parent, index++);
    }
    return true;
}

Default tree model source: http://developer.classpath.org/doc/javax/swing/tree/DefaultTreeModel-source.html

Edit: I think what also worked for me was firing a general "whole tree structure changed" event, which causes the tree to be built up from scratch from the model state. I think it was the Event constructor with all the specifics having a null value. Of course this might take some more time that just firing the specific event for the actual changes.