Adding Drop Support to JTree

by John Zukowski

Over the ages, drag and drop with the Swing component set has changed considerably. Early versions had a basic API in the java.awt.dnd package (with support from java.awt.datatransfer), but you had to define all aspects of the drag gesture, from the initial user clicking to the drop operation. J2SE 1.4 updates to the API improved upon the feature set and was described in an earlier tip: Dragging Text and Images with Swing

The earlier API changes made most of the drag and drop tasks much easier because many components had built-in support for drag and drop tasks. For instance, to enable drag operations in a JTextField, you just have to call setDragEnabled(true) on the text component. Users could then drag text out of a text component into some other application that acted as a drop zone, or even within the text field itself.

The text components offer built-in drop support, as does the JColorChooser component, but adding drop support to any of the other Swing components -- like JList, JTable, or JTree -- requires a little bit of extra work. The task might sound complicated, but thanks to the help of the new to Java SE 6 inner DropLocation class of TransferHandler, the task is relatively easy. All you have to do is create a TransferHandler for the JTree that defines what kind of data is droppable on it and what to do with it once dropped. Those operations are provided by the canImport and importData methods, respectively. The TransferSupport inner class is new to Java SE 6 and adds a simpler way to define transfer handlers.

You could create a fancy JTree that accepts images or text to put on a leaf of the tree, but the example here will just accept strings. Feel free to extend the example to images, too. To support strings, you need to define the canImport method with its TransferHandler.TransferSupport argument to check the supported data flavors (string) and operation type. TransferSupport also has a getDropLocation method to get the TransferHandler.DropLocation of the task. As long as the location is a valid spot, the canImport method should return true. Here's the method which returns true for a string-flavored, drop transfer over a non-null tree path.

public boolean canImport(TransferHandler.TransferSupport support) {
    if (!support.isDataFlavorSupported(DataFlavor.stringFlavor) ||
            !support.isDrop()) {
        return false;
    }

    JTree.DropLocation dropLocation =
            (JTree.DropLocation)support.getDropLocation();
    return dropLocation.getPath() != null;
}

The JTree.DropLocation is the predefined implementation of TransferHandler.DropLocation for the JTree component. There is also a JList.DropLocation for working with JList, and another for JTree with JTree.DropLocation. There is a fourth implementation in JTextComponent.DropLocation if you don't like the default text component drop handling behavior.

The other half of adding drop support to a JTree is the importData method. The older version of the importData method -- importData(JComponent comp, Transferable t) -- is still supported, just not called directly. Newer handlers should really implement the importData(TransferHandler.TransferSupport support) version instead. In this method, you need to get the transferred data and place it in the right location in the TreePath.

Getting the transferred data hasn't really changed much going from the old importData method to the new. Instead of having a Transferable argument to the method, you just get it from the TransferSupport with the support.getTransferable method. Then, just get the data for the appropriate flavor:

Transferable transferable = support.getTransferable();
String transferData;
try {
    transferData = (String)transferable.getTransferData(
    DataFlavor.stringFlavor);
} catch (IOException e) {
    return false;
} catch (UnsupportedFlavorException e) 
    return false;
}

For determining the location of the drop task, use the JTree.DropLocation class. Calling the getChildIndex method of DropLocation will give you the location in the tree to add the new node. A child index value of -1 means that the user dropped the node over an empty part of the tree. For this example, this will cause the node to be added to the end. Calling the getPath method of DropLocation returns the TreePath for the drop location. To then find the parent node associated with the drop location, call the path's getLastPathComponent method.

JTree.DropLocation dropLocation =
        (JTree.DropLocation)support.getDropLocation();
TreePath path = dropLocation.getPath();

int childIndex = dropLocation.getChildIndex();
if (childIndex == -1) {
    childIndex = model.getChildCount(path.getLastPathComponent());
}

DefaultMutableTreeNode newNode =
        new DefaultMutableTreeNode(transferData);
DefaultMutableTreeNode parentNode =
        (DefaultMutableTreeNode)path.getLastPathComponent();
model.insertNodeInto(newNode, parentNode, childIndex);

It is also helpful to ensure the new path element is visible. The complete importData method is here:

public boolean importData(TransferHandler.TransferSupport support) {
    if (!canImport(support)) {
        return false;
    }

    JTree.DropLocation dropLocation =
            (JTree.DropLocation)support.getDropLocation();
    TreePath path = dropLocation.getPath();
    Transferable transferable = support.getTransferable();
    String transferData;
    try {
        transferData = (String)transferable.getTransferData(DataFlavor.stringFlavor);
    } catch (IOException e) {
        return false;
    } catch (UnsupportedFlavorException e) {
        return false;
    }

    int childIndex = dropLocation.getChildIndex();
    if (childIndex == -1) {
        childIndex = model.getChildCount(path.getLastPathComponent());
    }

    DefaultMutableTreeNode newNode =
            new DefaultMutableTreeNode(transferData);
    DefaultMutableTreeNode parentNode =
            (DefaultMutableTreeNode)path.getLastPathComponent();
    model.insertNodeInto(newNode, parentNode, childIndex);

    TreePath newPath = path.pathByAddingChild(newNode);
    tree.makeVisible(newPath);
    tree.scrollRectToVisible(tree.getPathBounds(newPath));

    return true;
}

While we've shown a sufficient amount of detail to have a fully working droppable JTree, it is import to mention one more piece of information related to drop support, the DropMode. DropMode is an enumeration of modes related to how the component shows where the dropping is going to happen. Four supported modes are available for JTree:

  • DropMode.USE_SELECTION
  • DropMode.ON
  • DropMode.INSERT
  • DropMode.ON_OR_INSERT

However, it is important to point out that the enumeration is larger for modes specific to other components (like INSERT_COLS or INSERT_ROWS when working with a JTable).

What's the deal with the drop mode? By default, the mode is USE_SELECTION, which means no longer highlight the selected item in the JTree. Instead, use the selection mechanism to highlight the drop location. It is highly recommended that if your JTree is meant to support dropping, change the default. A better mode is ON, which lets you see both the current selection in the JTree and the potential drop location. INSERT mode allows you to insert new nodes between existing nodes, while still seeing the current selection. ON_OR_INSERT combines the latter two. The following four figures shows what the four options look like. The completed program offers a combo box of modes to try out the different behaviors.

The complete droppable tree program is shown next. The program includes a text area at the top for entry of text that can then be selected and dropped onto the JTree in the middle. The drop mode is settable from the combo box on the bottom. The data model for the tree comes from the default model created when one isn't specified when creating the JTree.

import java.awt.\*;
import java.awt.datatransfer.\*;
import java.awt.event.\*;
import java.io.\*;
import javax.swing.\*;
import javax.swing.tree.\*;

public class DndTree {
  public static void main(String args[]) {
    Runnable runner = new Runnable() {
      public void run() {
        JFrame f = new JFrame("D-n-D JTree");
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        JPanel top = new JPanel(new BorderLayout());
        JLabel dragLabel = new JLabel("Drag me:");
        JTextField text = new JTextField();
        text.setDragEnabled(true);
        top.add(dragLabel, BorderLayout.WEST);
        top.add(text, BorderLayout.CENTER);
        f.add(top, BorderLayout.NORTH);

        final JTree tree = new JTree();
        final DefaultTreeModel model = (DefaultTreeModel)tree.getModel();
        tree.setTransferHandler(new TransferHandler() {
          public boolean canImport(TransferHandler.TransferSupport support) {
            if (!support.isDataFlavorSupported(DataFlavor.stringFlavor) ||
                !support.isDrop()) {
              return false;
            }

            JTree.DropLocation dropLocation =
              (JTree.DropLocation)support.getDropLocation();

            return dropLocation.getPath() != null;
          }

          public boolean importData(TransferHandler.TransferSupport support) {
            if (!canImport(support)) {
              return false;
            }

            JTree.DropLocation dropLocation =
              (JTree.DropLocation)support.getDropLocation();

            TreePath path = dropLocation.getPath();

            Transferable transferable = support.getTransferable();

            String transferData;
            try {
              transferData = (String)transferable.getTransferData(
                DataFlavor.stringFlavor);
            } catch (IOException e) {
              return false;
            } catch (UnsupportedFlavorException e) {
              return false;
            }

            int childIndex = dropLocation.getChildIndex();
            if (childIndex == -1) {
              childIndex = model.getChildCount(path.getLastPathComponent());
            }

            DefaultMutableTreeNode newNode = 
              new DefaultMutableTreeNode(transferData);
            DefaultMutableTreeNode parentNode =
              (DefaultMutableTreeNode)path.getLastPathComponent();
            model.insertNodeInto(newNode, parentNode, childIndex);

            TreePath newPath = path.pathByAddingChild(newNode);
            tree.makeVisible(newPath);
            tree.scrollRectToVisible(tree.getPathBounds(newPath));

            return true;
          }
        });

        JScrollPane pane = new JScrollPane(tree);
        f.add(pane, BorderLayout.CENTER);

        JPanel bottom = new JPanel();
        JLabel comboLabel = new JLabel("DropMode");
        String options[] = {"USE_SELECTION",
                "ON", "INSERT", "ON_OR_INSERT"
        };
        final DropMode mode[] = {DropMode.USE_SELECTION,
                DropMode.ON, DropMode.INSERT, DropMode.ON_OR_INSERT};
        final JComboBox combo = new JComboBox(options);
        combo.addActionListener(new ActionListener() {
          public void actionPerformed(ActionEvent e) {
            int selectedIndex = combo.getSelectedIndex();
            tree.setDropMode(mode[selectedIndex]);
          }
        });
        bottom.add(comboLabel);
        bottom.add(combo);
        f.add(bottom, BorderLayout.SOUTH);
        f.setSize(300, 400);
        f.setVisible(true);
      }
    };
    EventQueue.invokeLater(runner);
  }
}

For additional information on the drag and drop support and data transfer APIs, please see the Introduction to Drag and Drop and Data Transfer trail in The Java Tutorial.

Comments:

Post a Comment:
Comments are closed for this entry.
About

John O'Conner

Search

Categories
Archives
« April 2014
SunMonTueWedThuFriSat
  
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
   
       
Today