The Source for Java Technology Collaboration
User: Password:



Shannon Hickey

Shannon Hickey's Blog

Top-Level Drop with Swing and Java SE 6

Posted by shan_man on September 15, 2006 at 01:06 PM | Comments (12)

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.


Bookmark blog post: del.icio.us del.icio.us Digg Digg DZone DZone Furl Furl Reddit Reddit
Comments
Comments are listed in date ascending order (oldest first) | Post Comment

  • I get this error when trying to launch the demo:

    An error occurred while launching/running the application.

    Title: TopLevelTransferHandlerDemo
    Vendor: Shannon Hickey, Sun Microsystems, Inc.
    Category: Download Error

    Unable to load resource: http://www.java.net/download/javadesktop/blogs/shan_man/2006.09.15/TopLevelTransferHandlerDemo.jnlp

    Posted by: geertjan on September 18, 2006 at 08:37 AM

  • Hi geertjan! The only thing that I can think of here is an issue with proxy settings on your system. Java Web Start uses Internet Explorer's proxy settings regardless of which method you use to launch the jnlp file (including launch from Firefox). As such, please make sure IE has the correct proxy settings for your situation and try again. Also, while I don't believe it affects your case, note that I orginally posted the jnlp with inaccessible permissions, but then fixed it early in the weekend. Please let me know if you continue to have trouble. Thanks!

    Posted by: shan_man on September 18, 2006 at 10:18 AM

  • It would be great if something more advanced that just cursor change were implemeted. For example, check this out, a very short dnd demo (QuickTime, 500Kb)

    I was forced to implement that myself with many hacks since nothing like this exists in Swing. The hacks are inevitable since currently many components have too mauch knowledge about built-in dnd, which is really poor. So I would vote make dnd more extensible first of all to avoid hacks and probable maintenance headaches.

    Posted by: kirillk on September 27, 2006 at 01:34 AM

  • I missed the opportunity to submit a question during the week of Swing topics. Since you're discussing drag and drop, I was wondering if you had any idea whether or not Bug ID: 6242241 will ever be fixed? When trying to drag an attachment from MS Outlook 2000, 2002, etc. to a Java application, it fails. The work around is either to use Outlook Express 6 or drag the attachment to MS Explorer and then to the Java application. The work around is not very elegant and a turn off to user's.

    Posted by: jimwong on November 03, 2006 at 02:57 PM

  • shan_man!

    You didn't get snowed in up there in Canada did you? Not sure if it is that or you had 2 of those Canadian beers again. ;-)


    Really nice article. JBuilder has a really sweet feature where you can drag a tabbed panel with Java code in it to the desktop. It creates a native frame just like the parent with only the toolbar, menu bar and JTabbed Panel.
    Gives great visual cues. The really wild thing is it even creates a new 'icon - text item' in the task bar at the bottom of Windows. So the first time you do this you end up with two JBuilder icons one for each native frame. If you exit the application they are all running in the same JVM and exit as a group. Very cool effect for multiple desktop machines. Some of my users want to be able to drag out a JTable and expand the details in a native JFrame. I haven't figured it out yet but I hope to nail it soon.

    Cheers,
    </p.

    Posted by: cupofjoe on November 08, 2006 at 08:05 PM

  • What's the deal with the preview? I previewed, hit post, and lost my message.... Let's try again. Hey Shannon how the hell are you, long time no speak. I'm glad to see that all is going well for you way out west. I did a search on your name in google and lo and behold your mug popped up on my screen after hitting the #1 match. Let me know how I can get your private email address so we can catch up sometime .... was that your little tyke I saw running around one of these pages with you? Anyway, I've gotta run, but I look forward to hearing from you soon, if you're not too busy. Take care of yourself.

    Ciao for now,
    Mark Kristensen (UofT) :-)

    Posted by: markkristensen on November 13, 2006 at 11:00 AM


  • Mark! Wow - how the heck are you? This is nuts! I was actually wondering, within the last couple of weeks, what you were up to these days. And I had every intention of trying to track you down, starting with old phone numbers and e-mail addresses, as soon as I had a few minutes for detective work. You've just made it one step easier! I can't wait to catch up.


    You asked about the little tyke...yep - that's one of my twin boys! Lots of changes. And hey, I'm living (and working remotely) back in the Great White North now, if that makes it easier for us to catch up. I've divined your contact information out of your blog post and sent off an e-mail to you. Hope it reaches you soon. All the best until then!

    Posted by: shan_man on November 13, 2006 at 01:23 PM


  • kirillk: I wonder if you've read all of my blogs on the new Swing drag and drop support. Certainly, there's much more possible than "just cursor change". I also disagree with you that "many components have too much knowledge about built-in dnd". It seems to me that the more knowledge they have, the easier they are to use. Perhaps there are certain areas that can be opened up to be more extensible, but please consider the hoops you had to go through prior to Java SE 6 to drop between nodes in a JTree. I can tell you that we didn't solve this problem by "removing" knowledge of drag and drop.


    With respect to the things that you show in your demo, much of that IS possible WITHOUT hacks. Those components that know about drag and drop have support for custom drop location rendering (I just haven't blogged about it yet). For more advanced custom rendering across an entire frame, one can simply use the glasspane to render an image. This is extremely simple. I covered it at JavaOne 2006, and plan a blog in the future.

    Posted by: shan_man on November 22, 2006 at 01:11 PM

  • jimwong: I'll ask someone from the AWT team, who owns this bug, to post a response here for you. Thanks!

    Posted by: shan_man on November 22, 2006 at 01:12 PM

  • cupofjoe: Only a little snow so far, and no beers lately - I'm a Coca Cola man. That feature you describe with JBuilder sounds really cool! I'll admit that I've never heard of that before, and I love the idea. If you nail it, would you mind sharing details with us? Thanks!

    Posted by: shan_man on November 22, 2006 at 01:18 PM

  • Shannon, did you have a chance to check with the person in charge of the Bug ID: 6242241? It'd be nice to find out if anyone plans to fix the drag bug from Microsoft Outlook...

    Posted by: jimwong on December 09, 2006 at 08:53 PM

  • Hi. We moved one of our main projects to java 6, with Drag-and-drop as one of the primary motivations. The code is surely a lot cleaner and maintainable now. There is however one remaining problem. As in the examples, all accept/reject logic for a Transferable is now in the canImport method. But we need to be a bit more selective and can only accept files of a given extension. So we first check the DataFlavor to reject non-files. If that check is passed, we obtain the file list from the Transferable and check the actual file names for the proper extension. If the extension doesn't match, canImport rejects the Transferable by returning false. So far, so good and it works as expected for the drag-over behaviour. The mouse icon shows whether the file can be dropped or not. However, the moment I release the mouse button to initiate a drop action, the canImport method is called once more and that invocation fails with an "InvalidDnDOperationException: No drop current" exception at the point where the file list is obtained from the Transferable. Googling a bit hinted that getting data from the Transferable isn't allowed until the drop is accepted by calling event.acceptDrop() in the proper listener. That, however, is something that's hidden from me deep inside the Swing code and I would rather keep it that way. Nevertheless, I took a peek and found that there's indeed code down there that first checks the TransferHandler's canImport and - if that calls gives the ok - calls the acceptDrop. Sounds reasonable: only accept it if it can be imported. But it apparently leads to a catch-22 situation: in our case, canImport can only validate the data if its accepted first, but to accept it, it must be validated. I have the feeling I'm missing something. But what? Do you have a suggestion?

    Posted by: hansabbink on January 24, 2008 at 03:54 AM



Only logged in users may post comments. Login Here.


Powered by
Movable Type 3.01D
 Feed java.net RSS Feeds