Skip to main content

First Class Drag and Drop Support in Mustang

Posted by shan_man on January 2, 2006 at 6:41 PM EST

Many Thanks!

An expression that my friend and Swing Team colleague in Russia, Michael Knyazev, offers to me; and one I'd like to extend to you now. It's been seven months since my first blog, with it's goofy play on words, discussion of Swing's improved drag gesture, and my promise that I'd write about more exciting Drag and Drop enhancements shortly. Seven months is slightly longer than I intended, and I sincerely apologize for the delay in fulfilling my promise. Even more importantly, I'd like to offer Many Thanks, for both your patience and your visit here today.

In this blog entry I'll introduce some major enhancements that make Swing Drag and Drop much more powerful, flexible, and easy to use: a first class citizen in Mustang! The discussion will include a Java Web Start demo displaying this power in the context of JTree, and source code showing how, with a single method call, you can finally configure JTree to accept drops between nodes. But first, to help excuse the seven month delay with some finesse (and yet delay a little longer), I'll quickly tell you what I've been up to for the last little while.

There's been lots of bug fixing and code reviewing, as we work the release into top shape for beta. I've also been working hard along with my team on implementing and refining major features for the Mustang release. I travelled to California in September to engage in early Dolphin planning sessions with my team. And most incredible, I had the opportunity to visit our team members in Saint Petersburg, Russia. Wow - what an amazing experience! Our colleagues were outstanding hosts, and their city is very beautiful. I've included a few thumbnail pictures below, that you can mouse-over for descriptions, and click for full versions:

Buildings along the Moika canal Buildings along the Moika canal at night Griboedov channel and "Savior on Spilled Blood" Cathedral Griboedov channel and "Savior on Spilled Blood" Cathedral at night Saint Isaac's Cathedral View from the top of Saint Isaac's Cathedral. Peter and Paul fortress in the background, across the Neva River. Hermitage Museum is the green building to the right. A bridge over the Fontanka river

The things I'll remember most about my trip are the wonderful people, the beautiful architecture, Russian pancakes (Blinis) with strawberry jam, refining humor with Chet Haase, and the fact that there's smoking everywhere - which was just fine, since I enjoy the occasional cigarette myself. It was an experience I'll remember forever, and I give Many Thanks to our Russian colleagues for making it so.

With that, let's continue on to the fun technical stuff. There are two themes addressed by our Mustang Drag and Drop feature. The first is the ability to tell a component how to determine drop locations, and to easily and consistently query said locations. The second is for Swing to provide all relevant information on a transfer when asking the TransferHandler if it is acceptable, and when telling it to import data. Together, the enhancements made under these themes open up many doors for Swing Drag and Drop users.

The first theme has a single associated bug number, 4468566, with a synopsis (Swing DnD should not use selection to show drop location) that very clearly states the issue to be solved. Since the initial implementation of Swing Drag and Drop, multiple components (JTree, JTable, JList, JTextComponents) have been capable of visually displaying a drop location during a Drag and Drop operation. Historically this has been done by determining where the mouse is in the component, and then simply changing the component's selection temporarily to be the item under the mouse. Likewise, for text components, the caret is moved to the location under the mouse, temporarily clearing any selection. There is a fundamental problem with this approach in that clearing the selection affects the user experience. After initiating a drag, the user immediately loses the context of what data they are dragging.

Secondarily, this approach can lead to problems for developers, as they are potentially forced to ignore selection events that are fired during Drag and Drop. They also have to deal with the fact that there's no consistent API for fetching the drop location at drop time; and must learn that they have to ask the component directly for it's selected item or index. Finally, and possibly most important, the use of selection to show drop location has a very serious limitation: One cannot drop between items. And so...drum roll please...

Introducing setDropMode(DropMode)! This is a new property on the components mentioned above, allowing you to decide how drop locations are determined. You want to drop on top of items - sure; you want to drop between items - no problem; you want a combination of both - excellent, you can do it. For each of these cases there is a drop mode that can be set on the component, and the location will be automatically calculated and displayed, without affecting the component's selection! The following table lists each drop mode constant, defined in class DropMode, the components that support each mode, and what each one means:

Mode Supported By Description
ON JList, JTree, JTable The drop location should be tracked in terms of the position of existing items. Useful for dropping on top of items.
INSERT JList, JTree, JTable, JTextComponent The drop location should be tracked in terms of the position where new data should be inserted. Useful for inserting or re-arranging content.
ON_OR_INSERT JList, JTree, JTable A combination of ON and INSERT, specifying that data can be dropped on top of existing items, or in insert locations as specified by INSERT.
[ON_OR_]INSERT_ROWS  
[ON_OR_]INSERT_COLS  
JTable Drop modes specific to JTable, indicating that drop locations should be calculated in terms of rows or columns only.
USE_SELECTION JList, JTree, JTable, JTextComponent The default, for backward compatibility. A component's own internal selection mechanism (or caret for text components) should be used to track the drop location.

At this point, let's take a little break from the technical talk to play. Because it's one of the more interesting examples, I've included a Java Web Start enabled demo allowing me to illustrate how you can finally configure a JTree to accept drops between nodes. Of course, the demo will also let you try each of the other drop modes supported by JTree. Click here to launch the demo (requires build 76 or greater of Mustang to work correctly, which you can download now if you don't already have it).

Screen Shot 1 below shows what the demo looks like when it's launched. At the very top is a drag source, with the label "Drag from here:". You'll use this component to initiate drags when playing with the demo. The middle component is a JTree that's been configured to accept drops, and will allow you to try out the different drop modes in the context of trees. Notice that by default, the "blue" node is selected. This allows me to demonstrate how the new drop modes provide drag-over feedback without affecting selection. Finally, at the bottom is a JComboBox used for setting different drop modes on the JTree. By default it is set to INSERT, the subject of this discussion.

Screen Shot 1

The INSERT mode was designed by considering exactly what you need when you want to Drag and Drop to insert new nodes into a tree. That is, the ability to drop on top of existing non-leaf (folder) nodes to insert a child, and the ability to drop between existing nodes to insert between them. This is exactly what you'll see in the demo. Initiate a drag by pressing on top of the drag source and dragging the mouse a short distance. Drag into the tree and start moving down on top of the nodes. As you drag over folder nodes they become highlighted to indicate that the drop can be accepted into that folder. This can be seen in Screen Shot 2 below, which illustrates the effect of dragging over the "names" folder node. Screen Shot 3 shows the effect of releasing the mouse and dropping on top of that node: a new item is inserted as the last child of the "names" node.

Screen Shot 2 Screen Shot 3
 

Similarly, as you drag between nodes a horizontal line is displayed to indicate that the drop can be accepted at that location. The line shows the position and level of the tree where the new node is to be inserted. Screen Shot 4 illustrates this drag-over effect and Screen Shot 5 displays the effect of releasing the mouse at the position indicated: a new node is inserted as a child of "colors" between "red" and "yellow".

Screen Shot 4 Screen Shot 5
 

Notice that throughout this demonstration, selection of the "blue" node is not affected at all! As mentioned previously, all of the new drop modes show the drop location without affecting selection. And speaking of other drop modes, you may have noticed something that you were unable to do with the INSERT mode: namely, dropping on top of a leaf node to add a child to it, effectively converting it to a folder node. This is certainly possible, but a job for ON_OR_INSERT. The difference is that ON_OR_INSERT allows dropping on top of any node, whereas INSERT only allows dropping on top of existing folders. Finally, I'd like to draw your attention to one additional ease of use feature added for JTree Drag and Drop in Mustang: during a drag operation, hovering the mouse over a collapsed folder node for two seconds causes the folder to be expanded, making it possible to drop within its list of children.

Now that you've taken the demo for a spin and had a chance to see the power of setDropMode, it's time to dive into the second theme to see how developers have been empowered to write robust drop handling code, by making the drop location and other important information available to TransferHandler. As you know, the responsibility for deciding the acceptability of drops, and for performing data import, lies with the TransferHandler class. Prior to Mustang, however, some common tasks could not be accomplished due to needed information not being available to TransferHandler's drop related methods. Multiple bugs complained about these problems:

  • 4942851: canImport should carry Transferable in TransferHandler
  • 5029427: Location sensitive dropping
  • 5029432: Provide a way of getting the action in TransferHandler.importData(...)

As described in these bug reports, it wasn't possible to customize drop handling based on the Transferable, drop location, or drop action associated with a particular drop. These limitations have finally been removed in Mustang with the addition of a new TransferHandler.TransferInfo inner class to encapsulate the details of every transfer. An instance of this class is provided to new overloaded versions of TransferHandler's import related methods, canImport and importData, so that developers can now accept or reject transfers, and customize data import, based on all information that can be provided about a transfer. As one example of what this enables, developers can now write a TransferHandler to accepts drops only to particular locations in a component!

The following table outlines the API of TransferInfo:

Method Description
Component getComponent() Returns the target component of the transfer.
DataFlavor[] getDataFlavors() Returns the data flavors for the transfer.
boolean isDataFlavorSupported(DataFlavor) Returns whether or not the given data flavor is supported.
Transferable getTransferable() Returns the Transferable associated with the transfer.
boolean isDrop() Returns whether or not the transfer represents a drop operation.
int getDropAction() If the transfer is a drop, returns the action chosen for the drop, otherwise returns -1.
TransferHandler.DropLocation getDropLocation() If the transfer is a drop, returns the current drop location for the component, otherwise returns null.


Important Note: This blog was published before finalization of the Swing drag and drop API for Java SE 6. As such, there have since been minor modifications. In particular, TransferInfo has been renamed to TransferSupport and now benefits from additional methods. For full details on these changes, please see my later blog entitled Choosing the Drop Action, and Further Changes to Swing Drag and Drop.

The last entry is particularly exciting in terms of the first Mustang theme that I discussed earlier. getDropLocation allows you to query the drop location determined by the component in a consistent manner, regardless of the component you're working with. And the really neat thing is that the return value from this method, which is declared to be of the new type TransferHandler.DropLocation, is actually a more informative subtype when TransferHandler is used with those components having built in drop support. Specifically, the default TransferHandler.DropLocation contains the single method getDropPoint, which returns the point in the component over which the operation is occurring. But when used with JTree, JList, JTable or JTextComponent, a subclass of DropLocation (JTree.DropLocation, JList.DropLocation, JTable.DropLocation or JTextComponent.DropLocation respectively) is returned, giving more information about the drop location in terms of the particular component type; for example, the index for lists or text components, or the tree path for trees.

As we've been doing throughout this blog, let's again take a closer look at this aspect of the new Drag and Drop support in the context of JTree. For Drag and Drop with JTree, the class JTree.DropLocation provides details on the drop location by way of the two methods getPath and getChildIndex. The former dictates the path over which dropped data should be inserted. The latter indicates the index where dropped data should be inserted with respect to the path given. Together these methods make explicit the exact location of the drop in terms that make sense for JTree!

Pulling together the ideas I've discussed in this portion of the blog, I'd like to show things in practice by sharing the source code of the TransferHandler I wrote for the earlier tree demo. To accomplish the import behavior shown by the demo, only two methods require overriding. First, the new version of canImport which takes a TransferInfo, to return the acceptability of transfers:

public boolean canImport(TransferHandler.TransferInfo info) {
    // for demo purposes, we'll only support drops and not clipboard paste
    if (!info.isDrop()) {
        return false;
    }

    // we only support importing Strings
    if (!info.isDataFlavorSupported(DataFlavor.stringFlavor)) {
        return false;
    }

    // fetch the drop location (it's a JTree.DropLocation for JTree)
    JTree.DropLocation dl = (JTree.DropLocation)info.getDropLocation();

    // we only support drops for valid paths in the tree
    return dl.getPath() != null;
}

Extremely simple! We accept imports of Strings, only for valid drop locations, and that's it. Note the use of isDataFlavorSupported for determining whether or not the String flavor is available. This is much simpler than looping through all of the available flavors to look for the String flavor - the only available approach prior to Mustang.

The second method of interest, with an implementation almost as simple as canImport, is the new version of importData, responsible for handling data import:

public boolean importData(TransferHandler.TransferInfo info) {
    // if we can't handle the import, return so
    if (!canImport(info)) {
        return false;
    }

    // fetch the drop location (it's a JTree.DropLocation for JTree)
    JTree.DropLocation dl = (JTree.DropLocation)info.getDropLocation();

    // fetch the path and child index from the drop location
    TreePath path = dl.getPath();
    int childIndex = dl.getChildIndex();

    // fetch the Transferable
    Transferable trans = info.getTransferable();

    // fetch the data, and bail if it fails
    String data;
    try {
        data = (String)trans.getTransferData(DataFlavor.stringFlavor);
    } catch (UnsupportedFlavorException e) {
        return false;
    } catch (IOException e) {
        return false;
    }

    // if the child index is -1, the drop was directly on top of the path,
    // which we'll treat as inserting at the end of the path's child list
    if (childIndex == -1) {
        childIndex = treeModel.getChildCount(path.getLastPathComponent());
    }

    // create a new node to represent the data and insert it into the model
    DefaultMutableTreeNode newNode = new DefaultMutableTreeNode(data);
    DefaultMutableTreeNode parentNode =
        (DefaultMutableTreeNode)path.getLastPathComponent();
    treeModel.insertNodeInto(newNode, parentNode, childIndex);

    // expand and scroll so that the new node is visible
    TreePath newPath = path.pathByAddingChild(newNode);
    tree.makeVisible(newPath);
    tree.scrollRectToVisible(tree.getPathBounds(newPath));

    // return success
    return true;
}

That's it - the full TransferHandler source for accepting Drag and Drop inserts into a JTree. Again, it's extremely simple with the additions made in Mustang. I believe you'll find this to be the case with Mustang Drag and Drop in general - it's been made easier to use, barriers have been eliminated, and new functionality has been made possible. As you can tell, I'm rather excited about this and have lots of information to share on the subject. In fact, I have enough for at least three more blog entries. Which is to say that, although I'm about to conclude this particular lengthy discussion, I think you'll be hearing from me again "shortly", and this time a lot sooner than seven months.

Until then, all the best to you. Oh, and one last time - Many Thanks!