Skip to main content

Top-Level Drop with Swing and Java SE 6

Posted by shan_man on September 15, 2006 at 1:06 PM PDT

Although my blog has been quiet for the last few months, it's certainly not for
lack of content to share. In reality, this blog that you're reading now is one
of three that have been living in various stages of completion in my unposted
blogs folder. For many bloggers, I suspect it doesn't work like this. They have
something worthwhile to say (we can hope) and they post it. In my case, however,
I like to use this space both to keep in touch with you, and also to present
technical content that (we can hope) teaches you something new. As such,
it takes me a bit longer while I construct a demo, grab screenshots, and prepare the
technical discussion. These items I work on in parallel with other blogs and
responsibilities. And then, when they're finally ready, I initiate contact.

This brings me to today's installment. I'd like to present details on
a Swing Drag and Drop RFE that we implemented back in build 53 of
JDK 6. This addition to the
platform, tracked under RFE
4519484,
provides support for dropping onto top-level containers, such as JFrame and
JDialog, by allowing you to set a TransferHandler directly on the
container. I'll walk you through this new feature with a demo showing an application type
where this support is particularly useful.

The type of application I'm referring to is what I call an "editor"—an application
designed for editing one or more documents. These applications include IDEs, image manipulation
programs, and editors for many other types of documents. Such an application typically includes,
at a minimum: a menu, a toolbar, an area for editing documents, and perhaps a list or other
mechanism for switching between open documents. For this demonstration, I've put together just
such an arrangement in the form of a simple text editor. Please ensure that you have a recent build
of JDK 6 (build 76 or later) installed,
and then click
here
to launch the demo. Note that you'll need to accept a security dialog upon launch, as an editor
application just isn't useful without permission to read files. Rest assured that the demo opens
only the files you tell it to, and only for reading; it has no write capability.

The following screen shot shows what the application looks like when it's launched:

As you can see, this demo application contains all of the components that I've mentioned above.
In addition, it also adds something that is particularly useful in this type of
application—drop support for opening files! By allowing drops of files, the user is saved
from having to navigate through a file dialog to open files and instead they can drag files from
their native file system directly to the application for editing. Go ahead and try it now:
drag a file (or a few) from your native desktop or file system, and drop into the document area
at the right. The file(s) will be opened and you'll see something like the following screen-shot:

Supporting drops of files like this is extremely easy. It requires only that you create
a simple TransferHandler to handle the details of importing files,
and that you set the TransferHandler (via the setTransferHandler
method) on the component that you wish to handle the drops.
Here's the source code for the TransferHandler used in this demo:


class FileDropHandler extends TransferHandler {

    public boolean canImport(TransferSupport supp) {
        /* for the demo, we'll only support drops (not clipboard paste) */
        if (!supp.isDrop()) {
            return false;
        }

        /* return true if and only if the drop contains a list of files */
        return supp.isDataFlavorSupported(DataFlavor.javaFileListFlavor);
    }

    public boolean importData(TransferSupport supp) {
        if (!canImport(supp)) {
            return false;
        }

        /* fetch the Transferable */
        Transferable t = support.getTransferable();

        try {
            /* fetch the data from the Transferable */
            Object data = t.getTransferData(DataFlavor.javaFileListFlavor);

            /* data of type javaFileListFlavor is a list of files */
            java.util.List fileList = (java.util.List)data;

            /* loop through the files in the file list */
            for (File file : fileList) {
                /* This is where you place your code for opening the
                 * document represented by the "file" variable.
                 * For example:
                 * - create a new internal frame with a text area to
                 *   represent the document
                 * - use a BufferedReader to read lines of the document
                 *   and append to the text area
                 * - add the internal frame to the desktop pane,
                 *   set its bounds and make it visible
                 */
            }
        } catch (UnsupportedFlavorException e) {
            return false;
        } catch (IOException e) {
            return false;
        }

        return true;
    }
}

Once you have this TransferHandler, you just need to decide which component
to set it on. With this demo, it's initially set it on the JDesktopPane,
which represents the area where documents are edited. While this approach is a decent start,
it has a limitation. Try dragging again over the demo and notice which areas
accept drops. The following screen shot illustrates what you'd see when dragging over
different parts of the demo:

Notice that the only location that accepts drops is the document area. This makes sense,
of course, since the JDesktopPane is the component to which our new
TransferHandler was added. As I mentioned, this is a great start. But
in Java SE 6 we can do better for the user.

Also notice that it's actually only a subset
of the document area that accepts file drops; those areas that are covered by an
editor component do not. The reason for this is that the text area component that we're
using as an editor has it's own default TransferHandler, to deal with
the transfer of text content. The text component's TransferHandler,
which knows nothing of file drops, controls the handling of drag and drop within its
component's bounds. But this is all technical details. Let's take a step towards a
better user experience.

For starters, let's take advantage of the new Java SE 6 support, change one line of code,
and simply change our setTransferHandler call from the JDesktopPane to
the JFrame itself. JFrame—along with JDialog,
JWindow and JApplet—benefits from the addition of a
setTransferHandler method in Java SE 6. By taking advantage of this in the
demo, the user experience is significantly improved. From our demo application's "Demo"
menu, please select "Use Top-Level TransferHandler" to make this change. Try dragging
over the demo again and see how the number of areas that accept drop has increased,
as illustrated by the screen shot below:

Drops are now accepted on almost every area of the application, including the menu bar,
toolbar, and even the frame's title bar! In fact, drops are accepted on any area that isn't
covered by a component with its own TransferHandler. Note that prior to Java
SE 6, similar support can be implemented by setting the TransferHandler on a frame's
JRootPaneframe.getRootPane().setTransferHandler(th)—the
difference being that drop can not be supported on the frame's title bar.

Like the text area component, the document selector at the left (a JList) also comes
with a default TransferHandler, and therefore does not accept drops of files. Here the
solution is easy: In this context, the default TransferHandler on the list
isn't benefiting us, so we can simply remove it (replace it with null). In fact,
for the purposes of our demo, let's remove the TransferHandlers from the text areas
too and look at the results. From the demo application's "Demo" menu, please select
"Remove TransferHandler from List and Text" and then try dragging over the application once
again. You'll see that file drops are now accepted everywhere! This is shown in the following
screen shot:

Fantastic! There's a caveat here, however. While we've decided that it's acceptable to
remove the default TransferHandler from the list component, the story is somewhat
different with the text components. In removing a text component's TransferHandler
you also remove its default support for cut/copy/paste and drag and drop of text.

The correct
solution, for the time being, is to instead provide the text components with their own
custom TransferHandler that supports file drops, and also re-implements the missing
support for handling text transfers. In the future, however, we hope to rememedy the situation
by providing support for adding import support on top of existing
TransferHandlers. This is covered under RFE
4830695
which has the synopsis "Require ability to add data import ability on top of existing TransferHandler."

Before we conclude, I'd like to take you one step further into providing a better user experience
for file drops. If you haven't noticed yet, I'd like to point out that when you drag over the
demo, the mouse cursor changes to the drag and drop "move" cursor. This doesn't exactly make sense,
since you're not really moving the file into the editor. In playing with native applications that
support file drops, I've discovered that they all tend to use "copy" as a better representation
of this situation. That is, when a file is dragged from the file system, where both the "move" and
"copy" actions are supported, the applications I've inspected typically chose "copy".
While, in Java, the results of the drop are identical, it seems worthwhile to provide user
feedback consistent with what users expect. To do so requires that only a few additional
lines be added to the TransferHandler's canImport method to explicitly
chose the action. Let's replace the previous version of canImport with the following:


public boolean canImport(TransferSupport supp) {
    /* for the demo, we'll only support drops (not clipboard paste) */
    if (!supp.isDrop()) {
        return false;
    }

    /* return false if the drop doesn't contain a list of files */
    if (!supp.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) {
        return false;
    }

    /* check to see if the source actions (a bitwise-OR of supported
     * actions) contains the COPY action
     */
    boolean copySupported = (COPY & supp.getSourceDropActions()) == COPY;

    /* if COPY is supported, choose COPY and accept the transfer */
    if (copySupported) {
        support.setDropAction(COPY);
        return true;
    }

    /* COPY isn't supported, so reject the transfer.
     *
     * Note: If you want to accept the transfer with the default
     *       action anyway, you could instead return true.
     */
    return false;
}

You can try this out with the demo now. From the "Demo" menu, please select "Use COPY Action",
and then try dragging over the application again. You'll now see, as illustrated in the
following screen shot, that the drag and drop "copy" cursor is used.

And there you have it—top-level drop support with Java SE 6. So what am I planning next?
Well, I'm right on the heels of another visit to St. Petersburg, Russia and I hope to come
home with more
pictures
and stories to share. And I'm definitely in for many hours of flight time, which gives me the chance
to stop dragging my feet and finish some of my other blogs. Until then, please take care,
and feel free to share your questions and comments.

Related Topics >>