The Source for Java Technology Collaboration
User: Password:



Shannon Hickey's Blog

Community: JavaDesktop Archives


Beans Binding 1.1.1 Beats 1.0's Butt, Bigtime

Posted by shan_man on October 19, 2007 at 01:08 PM | Permalink | Comments (12)

I'm pleased to announced that version 1.1.1 of Beans Binding has just been released at http://beansbinding.dev.java.net/. This release provides a drastic increase in performance over 1.0, the addition of support for binding to a JSpinner's value property, and a small set of bug fixes.

Extreme performance gains in this release are the result of a fix made to the BeanProperty and ELProperty classes. As these classes are central to Beans Binding, most things now perform many orders of magnitude faster. Consider the following simple test case, that fetches the value of a property 100,000 times for the same object:


    Person person = new Person("John", "Smith");
    Property p = BeanProperty.create("firstName");
    p.addPropertyStateListener(person, new PSL());

    long start = System.currentTimeMillis();

    for (int i = 0; i < 100000; i++) {
        p.getValue(person);
    }

    System.out.println(System.currentTimeMillis() - start);

In 1.0, this test takes approximately 13,000 milliseconds on my machine. On the contrary, it takes approximately 8 milliseconds in 1.1.1. How's that for bigtime?! You can imagine how applications with many bindings, or components like JTable and JList with large data sets, will perform much better with this version.

I imagine some of you are curious as to how this incredible change in performance was possible. As described in the JavaDoc for BeanProperty and ELProperty, once a listener is installed on the property, values along the paths are cached, and only updated in respose to property change notifications. Knowing that developers are likely to encounter beans that don't fire the correct notifications, and that resulting problems may pose tricky to debug, I began work on allowing developers to turn on a debug option that would have BeanProperty and ELProperty log a message when they detect such a change without notification. They would do this by re-evaluating the entire path on every method call and comparing each object to what they have in their cache. This would, of course, remove any performance gains accomplished by the caching; but only when the debug option is on.

While the toggle for this debug option hasn't yet been added, it turns out that in 1.0 the logic itself was left turned on, only without the messaging—a code path that served only to remove all caching benefits. In 1.1.1 this code path has been turned off pending completion of the debugging feature.

And since we're talking about performance, I'd like to share something else with those of you looking to get every last drop of speed from the current implementation. Naturally you'd expect BeanProperty to perform better than ELProperty for simple paths, since the former avoids the overhead of EL parsing. This will, of course, be true once the implementation has been further optimized. In the current release, however, ELProperty actually performs better in many cases. Consider the following test case that creates a new BeanProperty 100,000 times, and uses each instance to fetch the value for a new object:


    long start = System.currentTimeMillis();

    for (int i = 0; i < 100000; i++) {
    	Property p = BeanProperty.create("firstName");
        p.getValue(new Person("John", "Smith"));
    }

    System.out.println(System.currentTimeMillis() - start);

This test dramatizes the case of an application that creates and binds a large set of bindings, with similar properties, perhaps on the first initializion of a GUI form. In 1.1.1, this code takes approximately 7000 milliseconds on my machine. But replacing BeanProperty with ELProperty reduces it to 330 milliseconds.

Consider this similar test case that uses a single BeanProperty and fetches its value for 100,000 new objects:


    long start = System.currentTimeMillis();
    
    Property p = BeanProperty.create("firstName");
    
    for (int i = 0; i < 100000; i++) {
        p.getValue(new Person("John", "Smith"));
    }
    
    System.out.println(System.currentTimeMillis() - start);

This test dramatizes what JTable or JList might have to do while scrolling, to fetch the value of a single property for a large set of objects. In 1.1.1, this test takes approximately 6900 milliseconds on my machine. Replacing BeanProperty with ELProperty reduces it to 260 milliseconds.

I raise this discussion as a point of interest, not to encourage you to switch to ELProperty (unless your application demands the performance increase that it currently brings). As I mentioned, BeanProperty will be optimized to be the better choice for simple paths. Profiling indicates that the source of ELProperty's higher performance lies with EL's built-in caching of JavaBean BeanInfos—something that can easily be added to BeanProperty.

One final note before I sign off for the weekend: Some of you might ask why the release number has jumped from 1.0 to 1.1.1. The answer is that I actually first released a 1.1, and before I had finished writing up an announcement, internal testing revealed an existing, but previously unknown bug that I thought was worth fixing quickly. Hence 1.1.1. Enjoy!



Beans Binding 1.0 Released

Posted by shan_man on September 05, 2007 at 04:53 PM | Permalink | Comments (59)

-- From Saint Petersburg, Russia @ 3:30 AM local time

It's finally time! After months of eating, sleeping and drinking Beans Binding, I'm thrilled to finally announce that version 1.0 has been released at http://beansbinding.dev.java.net for your binding pleasure!

1.0 represents a major re-architecture of the Beans Binding API, based on the extremely valuable feedback from members of the expert group and the community. I'd like to say thank you to all of you who have discussed ideas with me and contributed your own. I've tried to combine and build on these ideas and create an API that is powerful, flexible, extensible and easy to use. I hope you'll keep the feedback coming so we can continue this rewarding process.

While the work on this project continues through the JCP process, with feature additions and possible API changes to come, the current state represents the core API that is expected, for the most part, to persist. As such, we're calling it 1.0 and releasing!

Some of the major points of interest in this release:

  • The concept of a property has been factored out into an abstract Property class, with two concrete implementations of interest: BeanProperty and ELProperty.
  • Binding is now an abstract class representing a binding between two Property instances (typically associated with two objects).
  • Binding with automatic syncing is implemented by a new concrete AutoBinding subclass.
  • Bindings to complex Swing components (such as JTable, JList and JComboBox) are now handled by custom Binding subclasses.
  • The synthetic Swing properties that offer multiple possible behaviors are now exposed via multiple versions of the property. For example: "text", "text_ON_FOCUS_LOST" and "text_ON_ACTION_OR_FOCUS_LOST" for JTextField; "selectedElement" and "selectedElement_IGNORE_ADJUSTING" for JList and JTable.
  • Everything has been repackaged into org.jdesktop packages.

It is my hope that the JavaDoc provided with this release will have you well on your way to binding with the new API very quickly. While I would love to take you on a tour through the entire API here, composing such a tutorial would delay this announcement. As such, I'll present a few examples to get you started, let you get going with the new API, and then follow up with additional blog entries, as time allows, on particulars deserving more attention. As always, please feel free to send me your questions and/or feedback at any time.

Examples

For the purposes of these examples, we'll work on Person beans, and assume that Person has the following properties: firstName, lastName, age and mother.

Creating and using BeanProperty and ELProperty:


  // create a BeanProperty representing a bean's firstName
  Property firstP = BeanProperty.create("firstName");

  // create a BeanProperty representing a bean's lastName
  Property lastP = BeanProperty.create("lastName");

  // create a BeanProperty representing the firstName of
  // a bean's mother
  Property motherFirstP = BeanProperty.create("mother.firstName");

  // creata an ELProperty representing a bean's mother's full name:
  Property motherFullP =
      ELProperty.create("${mother.firstName} ${mother.lastName}");

  // create an ELProperty representing whether or not a bean's
  // mother's age is greater than 65
  Property motherOlderP = ELProperty.create("${mother.age > 65}");


  // print Duke's first name
  System.out.println(firstP.getValue(duke));

  // print John's first name, reusing the same property
  System.out.println(firstP.getValue(john));

  // print Duke's mother's full name
  System.out.println(motherFullP.getValue(duke));

  // set Duke's mother's first name
  motherFirstP.setValue(duke, "Jennifer");

  // set John's mother's first name, reusing the same property
  motherFirstP.setValue(john, "Melanie");

  // listen for changes to motherOlderP for Duke
  motherOlderP.addPropertyStateListener(duke, listener);

Creating AutoBindings between two properties:


  // Bind Duke's first name to the text property of a Swing JTextField
  BeanProperty textP = BeanProperty.create("text");
  Binding binding =
      Bindings.createAutoBinding(READ_WRITE, duke, firstP, textfield, textP);
  binding.bind();

  // Bind Duke's mother's first name to the text property of a Swing JTextField,
  // specifying that the JTextField's text property only reports change
  // (thereby updating the source of the READ_WRITE binding) on focus lost
  BeanProperty textP = BeanProperty.create("text_ON_FOCUS_LOST");
  Binding binding =
      Bindings.createAutoBinding(READ_WRITE, duke, motherFirstP, textfield, textP);
  binding.bind();

  // Bind the motherOlderP for Duke to the selection of a display-only Swing JCheckBox
  BeanProperty selectedP = BeanProperty.create("selected");
  Binding binding =
      Bindings.createAutoBinding(READ, duke, motherOlderP, checkBox, selectedP);
  binding.bind();

Using Swing Binding subclasses:


  // Bind a List of Person objects as the elements of a JTable and specify
  // that columns should be shown for the beans' first name, mother's full
  // name and whether or not the mother is older than 65
  JTableBinding tb = SwingBindings.createJTableBinding(READ, personList, personJTable);
  tb.addColumnBinding(firstP)
      .setColumnName("First Name").setColumnClass(String.class);
  tb.addColumnBinding(motherFullP)
      .setColumnName("Mother's Full Name").setColumnClass(String.class);
  tb.addColumnBinding(motherOlderP)
      .setColumnName("Mother older than 65").setColumnClass(Boolean.class);
  tb.bind();

  // Bind the last name of the table's selected element to a JTextField
  Property selectedLastP = BeanProperty.create("selectedElement.lastName");
  Property textP = BeanProperty.create("text");
  Binding selBinding =
      Bindings.createAutoBinding(READ_WRITE, personJTable, selectedLastP,
                                             textfield, textP);
  selBinding.bind();

  // Bind a List of Person objects as the elements of a JList
  // and specify that the list should display the first name of the beans
  JListBinding lb = SwingBindings.createJListBinding(personList, personJList);
  lb.setDetailBinding(firstP);

  // Bind the last name of the list's selected element to a JTextField,
  // specifying that changes to the selected element are ignored while
  // it is adjusting
  Property selectedLastP =
      BeanProperty.create("selectedElement_IGNORE_ADJUSTING.lastName");
  Property textP = BeanProperty.create("text");
  Binding selBinding =
      Bindings.createAutoBinding(READ_WRITE, personJList, selectedLastP,
                                             textfield, textP);
  selBinding.bind();



Beans Binding 0.6.1 Release Available

Posted by shan_man on June 22, 2007 at 02:41 PM | Permalink | Comments (6)

Taking into consideration the feedback that I've received since yesterday's blog on the 0.6 release of Beans Binding, I've just released 0.6.1 with simpler and more intuitive method names and constants. It's available at http://beansbinding.dev.java.net

Full details, straight from the release notes:

API Changes/Additions

  • UpdateStrategy.READ_FROM_SOURCE has been renamed to the shorter UpdateStrategy.READ

  • TextChangeStategy is now a top-level enum. In addition, the values CHANGE_ON_TYPE, CHANGE_ON_ACTION_OR_FOCUS_LOST and CHANGE_ON_FOCUS_LOST have been shortened to ON_TYPE, ON_ACTION_OR_FOCUS_LOST, ON_FOCUS_LOST.

  • Binding.Parameter has been renamed to Binding.ParameterKey and Binding.setValue/getValue have been given the more appropriate names Binding.putParameter/getParameter.

  • All Swing ParameterKeys are now in a top-level ParameterKeys class. They've been renamed as appropriate to reflect the fact that they are constants. A few have also been shortened.

  • SwingBindingSupport has been removed. Documentation for Swing binding is now in the package-level documentation.

To see how some of these changes affect you, let's look at two examples:

Before:


  Binding b = new Binding(list, null, table, "elements");
  b.addChildBinding("${firstName}, null)
     .setValue(SwingBindingSupport.TableColumnParameter, 0)
     .setValue(SwingBindingSupport.TableColumnClassParameter, String.class);

After:


  Binding b = new Binding(list, null, table, "elements");
  b.addChildBinding("${firstName}, null)
     .putParameter(ParameterKeys.COLUMN, 0)
     .putParameter(ParameterKeys.COLUMN_CLASS, String.class);

Likewise, before:


  Binding b = new Binding(object, "${property}", textField, "text");
  b.setValue(SwingBindingSupport.TextChangeStrategyParameter,
             SwingBindingSupport.TextChangeStrategy.CHANGE_ON_TYPE);

After:


  Binding b = new Binding(object, "${property}", textField, "text");
  binding.putParameter(ParameterKeys.TEXT_CHANGE_STRATEGY,
                       TextChangeStrategy.ON_TYPE);



Beans Binding 0.6 Release Available

Posted by shan_man on June 21, 2007 at 09:04 AM | Permalink | Comments (7)

Just a short note to announce that I've posted the 0.6 release of the Beans Binding project at http://beansbinding.dev.java.net

In this release:

  • Improved JTable support
  • More default converters
  • Name your Bindings
  • Compile-time type safety for setting Parameters
  • A handful of method renames

Full details, straight from the release notes:

API Changes/Additions

  • Binding.addBinding/removeBinding/getBindings have been renamed to the more descriptive Binding.addChildBinding/removeChildBinding/getChildBindings

  • Binding.setSourcePath/getSourcePath have been renamed to the more appropriate Binding.setSourceExpression/getSourceExpression

  • To enforce compile-time type safety, the Object varargs parameter has been removed from all constructors and methods in Binding and BindingContext. You must now call setValue directly. To allow for method chaining, setValue now returns the Binding. As an example, replace this binding:

    
      Binding b = new Binding(source, "${property}", target, "property",
                              Parameter1, param1Value, Parameter2, param2Value);
    
    

    with this:

    
      Binding b = new Binding(source, "${property}", target, "property");
      b.setValue(Parameter1, param1Value).setValue(Parameter2, param2Value);
    
    

  • Some Binding and BindingContext methods were updated to throw the more appropriate IllegalArgumentException (rather than IllegalStateException) for certain conditions.

  • Binding now has a name property. The ability to name a binding assists in debugging. Its main goal, however, is to make it possible to fetch bindings by name. This will show its full utility with future changes making it easier to validate and then commit or revert bindings as a group.

    To support naming a binding, the following API additions have been made to Binding:

    • Constructors that take a String name parameter
    • void setName(String name)
    • String getName()
    • addChildBinding methods that take a String name parameter
    • Binding getChildBinding(String name)

    In addition, the following additions have been made to BindingContext:

    • addBinding methods that take a String name parameter
    • Binding getBinding(String name)

  • Added a Parameter to control the editability of a JTable when it is the target of a binding. A new EditableParameter has been added to SwingBindingSupport to control this. It can be used on a top-level binding to control the editability of the entire JTable, and/or on the binding's individual child bindings to control editability of individual columns. For example, to make all columns non-editable, except for the first:

    
      Binding b = new Binding(list, null, table, "elements");
      // whole table is non-editable
      b.setValue(SwingBindingSupport.EditableParameter, false);
      b.addChildBinding("${firstName}, null)
          .setValue(SwingBindingSupport.TableColumnParameter, 0)
          // this column IS editable
          .setValue(SwingBindingSupport.EditableParameter, true);
      b.addChildBinding("${lastName}, null)
          .setValue(SwingBindingSupport.TableColumnParameter, 1);
    
    

Issues Resolved

  • 2: Need converters between various types and String
  • 5: JTable binding support doesn't support sorting and filtering

Other

The JavaDoc in Binding and SwingBindingSupport has been updated to reflect the change to use EL for the source "property".



Top-Level Drop with Swing and Java SE 6

Posted by shan_man on September 15, 2006 at 01:06 PM | Permalink | 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.



Choosing the Drop Action, and Further Changes to Swing Drag and Drop

Posted by shan_man on February 15, 2006 at 08:25 AM | Permalink | Comments (15)

Greetings Gentle Reader; I'm thrilled that you've come back to visit! As you might imagine, we're still working very hard on Mustang, aiming to make it the best Java SE release in history. And now with the Mustang Beta release live on the web, we eagerly await your questions, feedback and more of the excellent code contributions you've been sending in. While we wait, of course, we're still deeply involved in resolving our own bug lists and polishing off the features we've released, and we're already fixing things in post-beta builds. With all of this going on, I'm truly happy for the opportunity to bring you up to date with one of my little corners of Java SE.

In this new world, where you can read blogs about and download Java SE changes as fast as we can make them, an interesting scenario arises: You have access to code that is evolving; stuff that has the potential to change (for the better). This is an extremely positive thing! We want the code in your hands as quickly as possible, allowing you to test drive it, provide feedback, and to help us improve. But this implies a certain obligation for those of us who blog about our evolving technology - we need to keep you current with any changes to what we've already shared. It's only fair :-)

Which brings me to the purpose of this particular blog entry. I'd like to introduce you to the new support for choosing the drop action with Swing Drag and Drop, and narrate how the addition of this support prompted positive change to one aspect of the API I've already spoken of. The story begins with drop actions...

Every drag source (Java based or otherwise) advertises the set of actions it supports when exporting data. If it supports data being copied, it advertises the copy action; if it supports data being moved from it, then it advertises the move action; etc. (With Swing components, source actions are advertised via the TransferHandler's getSourceActions method.) When a drag is initiated, the user has some control over which of the source actions is chosen for the transfer, by way of keyboard modifiers used in conjunction with their drag gesture. Typically, an ordinary drag indicates a preference for moving, holding control while dragging indicates a preference for copying, and holding both shift and control gives a preference to linking. The so-called user action is determined by the system by choosing an appropriate value from the source actions based on this preference.

Prior to Mustang, this nifty little system could only get you so far with Swing Drag and Drop. When accepting a transfer, TransferHandler would always choose the user action as the action for the transfer. This would result in a copy being performed for a user copy action, a move of the data for a move action, and so on. But two things were missing. The first was a way to accept or reject transfers based on the drop action. As I explained in my previous blog entry on First Class Drag and Drop Support in Mustang, this ability was added with the introduction of the TransferInfo class in Mustang. Excellent! The second thing missing was the ability for the developer to explicitly choose an action; for why should the developer be limited to importing solely with the action chosen by the user?

Consider a component that wants to accept copied data, but doesn't want the responsibility that goes with allowing data to be moved to it. And consider a drag source that advertises supporting copy as one of its actions. Shouldn't the developer be able to support dragging from this source to our copy-to-only component, regardless of what the user prefers with their drag gesture? Of course they should! I discovered this limitation while composing an upcoming blog entry, and immediately filed bug number 6379813 (TransferHandler should allow the developer to choose the drop action) on the issue. With the fix for this bug comes the changes I'm about to share with you.

In order to support choosing the drop action, we determined that the following were needed:

  • A method to query the source's drop actions, so you know which actions are supported
  • A method to query the user drop action, which may be of interest when choosing an action
  • A method to query the actual drop action that TransferHandler will use (the explicitly chosen action, if applicable; otherwise the user action)
  • A way to explicitly choose the actual drop action

Where to add the first three methods was immediately obvious - we already had TransferInfo to hold this kind of information. So we added getSourceDropActions and getUserDropAction for the first two items respectively, and then updated the existing getDropAction to reflect the actual drop action. Now, where to add the ability to choose the drop action required some thinking. An initial thought was to provide a new protected method on TransferHandler that developers could override to return their choice of drop action. But when a use case was considered, this seemed less than ideal: Developer overrides canImport to determine the suitability of a transfer; inspecting the source actions for the action they want, and returning true only if that action is supported. Developer also must override our new method to choose and return the action they want, possibly inspecting the source actions again. Yuck! Not very tidy: A need to override two separate methods, and potentially write the same logic twice. No, it became clear that the right solution would allow the developer to do everything in one place.

Knowing that every TransferHandler supporting drop must implement the workhorse canImport method, and that it's the place where developers are already inspecting details of the transfer, it made sense to us that it is also the ideal place for any logic dealing with configuring the drop action. As such, providing support for choosing the drop action became as simple as adding one additional method to TransferInfo, to be called by the developer from canImport. Gentle Reader, it was the addition of this new setDropAction method that began the chain of events leading to the API changes I'm here to update you on.

You see, with this one tiny change, it came about that TransferInfo was no longer just an informational class - it had suddenly gained behavior. As such, referring to it as an "Info" no longer felt appropriate. And so the name TransferSupport was born. With a quick search and replace in our source base, the TransferInfo name has gone to the recycling bin, and the apt TransferSupport name has taken its place. Same little helper, just a little more power and three extra letters to type ;-)

Considering what I've discussed so far, let's see how one implementation of canImport might look in terms of the updated API. This implementation, which uses the new TransferSupport name, accepts drops of Strings and explicitly chooses the COPY action if it's supported by the source; it rejects the transfer otherwise:

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

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

    // check if the source actions (a bitwise-OR of supported actions)
    // contains the COPY action
    boolean copySupported =
        (COPY & support.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
    return false;
}

As I mentioned, this implementation rejects the transfer if COPY isn't a supported action of the drag source. Alternatively, the last line could instead return true, in which case the drop is accepted with the user drop action (the default if an action isn't explicitly chosen) if the COPY action isn't available. Let's try this out with a demo. As these are up to the minute changes that I'm discussing, you'll need to wait until build 76 or later of Mustang is available to run the demo. At the time of this writing, the current weekly build available for download is 71. Once you have the correct build, click here to launch the demo.

When it launches, you're presented with a frame containing three JList components. The larger list on the left, labelled "Drag from here" acts as a drag source for the demo, supporting both copying and moving of its data, and advertising this via its TransferHandler's getSourceActions method. On the right side are two smaller lists that act as targets for drops. The top one, labelled "Drop to COPY here", has a TransferHandler that always chooses a copy action, with a canImport implementation exactly the same as the code snippet I've shown above. Similarly, the bottom one, labelled "Drop to MOVE here", always chooses a move action, with the same code, but using MOVE in place of COPY.

Go ahead and initiate a drag from the source list and drag into the upper target list. Notice while you're dragging over the target that the use of the copy action is indicated by the copy-drop mouse cursor, which shows a little plus sign. This can be seen in Screen Shot 1 below. Screen Shot 2 illustrates the effect of dropping into this list: The dragged item is inserted into the target, but not removed from the source - a perfect copy drop.

Screen Shot 1 Screen Shot 2
 

Now drag again from the source list, and into the lower target list. While you're dragging over this target, notice that the mouse cursor indicates a drag action without the plus sign - indicating that the move action is in use (see Note below - a known, and soon to be fixed, bug may cause a no-drop cursor to be shown instead). This is shown in Screen Shot 3 below. When you drop into this list, notice that the dragged item is inserted, and removed from the source - the effect of a move drop. The result is illustrated in Screen Shot 4 below.

Screen Shot 3 Screen Shot 4
 

For a little bit of added fun, try dragging a snippet of text from your favorite native text editor into the target lists to see the same results. Most text editors support dragging text as both copy and move, and the support I've discussed works for both Java and non-Java components alike!

Note: Before we move on, one final note on behavior you may encounter while trying out this demo, depending on what build you are using to run it. I've just discovered a small bug in the underlying drag and drop subsystem, such that explicitly choosing an action other than the user action causes the no-drop mouse cursor to be shown when the drag source is Java based. Until this bug is fixed, you'll unfortunately see the no-drop cursor instead of the copy-drop cursor when dragging into the bottom target list. This does not affect the functionality of the demo, or the ability to actually drop into the component. I've filed this as a high priority bug and expect to see it fixed shortly. For your reference, the bug number is 6385534 (For Java drag source, choosing an action other than the user action shows a no-drop cursor).

Okay, so now you've learned about choosing the drop action, and the name change from TransferInfo to TransferSupport. Recalling what I mentioned earlier about upcoming changes to API that has already been released, you may be wondering if I've already covered it - and if a search and replace of the term TransferInfo in your code will bring you up to date. Well, the answer is "it depends". If you happen to have read my blog entry on Location-Sensitive Drag and Drop in Mustang, and have already started taking advantage of the new shouldIndicate method in TransferHandler, to specify whether or not the drop location should be indicated by the target of a transfer, you'll need to prepare for another small change.

You see, the line of thought that saw setDropAction added to TransferSupport, rather than providing another overridable method to TransferHandler for choosing the drop action, led to a re-evaluation of our placement of the recently added shouldIndicate method. Like choosing a drop action, deciding whether or not to show the drop location likely involves inspecting many of the same things that a developer would already be looking at in canImport. With a fresh look, it now seemed silly to force the developer to override a separate method for the purpose of changing the drop location indication. Do you see where I'm going?

TransferHandler.shouldIndicate has been removed and in its place a new method, setShowDropLocation(boolean), has been added to TransferSupport. This new method is to be called from TransferHandler.canImport any time the developer wishes to change the default displayability of the drop location. Let's look at an example of how this simplifies things by contrasting the code for a TransferHandler under the old API and the new API. Consider a TransferHandler implemented to accept drops into a JList only when the data is a String and the drop location is on top of items 0 through 5. Furthermore, let's say that it also wishes to always indicate the drop location, regardless of whether or not it accepts the transfer. Here's the code with the old API:

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

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

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

    // return true for index 0 through 5, and false otherwise
    return (dl.getIndex() >= 0 && dl.getIndex() <= 5) {
}

public boolean shouldIndicate(TransferInfo info, boolean canImport) {
    // always show the drop location
    return true;
}

It's not too bad in this case, but remember that in reality shouldIndicate and canImport will likely have more in common, such as conditionalizing on the drop location or the data flavors available. This makes the following new approach much more appealing:

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

    // always show the drop location
    support.setShowDropLocation(true);

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

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

    // return true for index 0 through, 5 and false otherwise
    return (dl.getIndex() >= 0 && dl.getIndex() <= 5) {
}

Simplicity - removing an override and keeping associated logic together. I like it and I like it a lot! One of the benefits to regular blogging about ongoing Java SE development is that it keeps me constantly re-evaluating and using our new features. Hopefully, it also keeps you excited, so you can help do the same. The end result being a more feature-complete, bug free API. And of course, the best release in history.


For your reference, the updated TransferSupport API:

Method Description
Component getComponent() Returns the target component for 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.
     Note: The following methods throw IllegalStateException if the transfer is not a drop.
TransferHandler.DropLocation getDropLocation() Returns the current drop location for the component
void setShowDropLocation(boolean) Sets whether or not the drop location should be visually indicated for the transfer.
int getUserDropAction() Returns the user action for the drop.
int getSourceDropActions() Returns the drag source's supported actions.
int getDropAction() Returns the action chosen for the drop (the user action unless explicitly set).
void setDropAction(int) Sets the drop action for the transfer to the given action. Throws IllegalArgumentException if the action isn't supported.


Enable Dropping into Empty JTables

Posted by shan_man on January 19, 2006 at 01:46 PM | Permalink | Comments (12)

So you've created an empty JTable, you've given it a TransferHandler to accept drops, and you've added it to a JScrollPane. But when you launch your application and drag some completely valid data into the table, it rejects you! I'm not kidding; go ahead and try it yourself with this demo (requires build 76 or greater of Mustang). Drag from the component labelled "Drag from here" into the JTable, and you'll see the following:

What's going on? Without peeking at the demo's "Options" menu (which would likely give it away), can you figure it out? How about a hint: The background color of a JTable in the Ocean Look and Feel is supposed to be white. In this demo, I haven't changed it, and yet you aren't seeing that white color. Perhaps there's a connection...Are you ready for a second hint? Using the demo, double click a couple of times on the drag source to have some rows added to the table. Now try dragging again and you'll see that this time you can indeed drop into the table. But, the drop line is only shown when the mouse is over an existing row; if you drag below the existing rows, you'll see something like the following:

After seeing this, have you yet started to wonder if maybe that gray "no-drop" area is not actually a part of the table? If so, you've figured out what's going on! You see, unlike JList and JTree, for some reason JTable is not implemented to be automatically stretched to fill the height of a JScrollPane's viewport; it only takes up as much vertical room as needed for the rows that it contains. As such, when you're dragging over that gray area, you're actually not over the table. This issue has been known for some time under bug number 4310721 (JTable is not stretched to fill a viewport's height). Let's have a look at how this has been resolved in Mustang, and how you can easily work around it in earlier releases.

The behavior of a component in this capacity is controlled by the component's implementation of the getScrollableTracksViewportHeight method, declared in the Scrollable interface. This method tells the viewport whether or not it should force the height of the contained component to match the height of the viewport. JList and JTree, which implement Scrollable, do something intelligent in this method and tell the viewport that they want to be stretched any time their preferred height is smaller than the viewport height. JTable on the other hand, prior to Mustang, always returns false.

Determining this to be something that developers often want to change, we've made it easier to do so in Mustang; it is now very simple to configure JTable to act like the other two components in this regard. Striving to maintain backward compatibility and to not break the previous clearly specified behavior (however poor), we've added a new property to JTable to enable solving this problem: "fillsViewportHeight". By default the property is false, so that JTable acts exactly as it did previously. By setting the property to true, with the method JTable.setFillsViewportHeight(boolean), JTable behaves like lists and trees and is stretched to fill the viewport height when appropriate.

Using the demo again, let's see what this looks like. If you've closed the demo, please launch it again. If you still have it running, please select the "Reset" item from the "Options" menu to reset the state. In the demo, the new "fillsViewportHeight" property can be changed via the "Fill Viewport Height" item in the "Options" menu. Please enable this option now. As you'll see, and as indicated in the following screen shots, the table is now stretched to fill the viewport. This results in two positive things: 1) The white background is now visible; 2) You can now drop anywhere within the viewport.

 

Incidentally, since it's already apparent in the demo and screen shots, let me quickly draw your attention to the solid line indicating the drop location in the JTable. This is also new in Mustang, and the result of using JTable in the INSERT_ROWS drop mode, a subject which I began talking about in my earlier blog entry. There's actually a lot more to say about the new drop modes with respect to JTable, and for that reason I'll leave further discussion on the subject to a future blog entry.

For now, let's conclude with me fulfilling my promise of demonstrating how to make JTable stretch to fill the viewport in pre-Mustang releases. It's extremely simple, with a variation of the following override in a JTable subclass being all that is needed :

public boolean getScrollableTracksViewportHeight() {
    // fetch the table's parent
    Container viewport = getParent();

    // if the parent is not a viewport, calling this isn't useful
    if (!viewport instanceof JViewport) {
        return false;
    }

    // return true if the table's preferred height is smaller
    // than the viewport height, else false
    return getPreferredSize().height < viewport.getHeight();
}

With this code you'll acheive identical results to turning the new property on in Mustang. And there's no need to wait since it's most likely the behavior you want, is extremely easy, and will not interfere when you upgrade in the future. That's the best kind of work-around in my opinion!

That's all I have for today, but please stay tuned. I'm finding it extremely rewarding to communicate with you via this blog and I'm on a roll, with the words just flowing. Potential upcoming topics include the new drop modes as they pertain to JTable, and fancy customization of drop mode indication in JTree - something that was a big hit, and received audience applause at the last JavaOne conference. In fact, I may just have to write that one first, as I'm getting quite excited about it. Until next time, take care!



Location-Sensitive Drag and Drop in Mustang

Posted by shan_man on January 17, 2006 at 12:19 PM | Permalink | Comments (14)

Hello again friends!

Prompted by an excellent question from wrandelshofer in the comments section of my recent Swing Drag and Drop blog entry, I'm jumping on the opportunity to tell you more about the enhancements made in Mustang. In particular, I'd like to talk about how these changes have enabled location-sensitive Drag and Drop.

In order to clarify the concept, I'll paraphrase the question that I've just referred to: Is it possible to allow the user to drop into some locations of a JTree and not others, and have Swing indicate this by only showing the drop location when the mouse is over acceptable drop locations? The answer is a resounding YES; this is the default behavior in Mustang! Let's take a look at how this has been enabled.

Prior to Mustang, developers could not implement this very important behavior due to oversights in the Swing implementation. The first problem, as I discussed in my previous blog entry, was that the drop location wasn't easily available to the TransferHandler during a drag operation. The astute developer could often overcome this limitation, however, after discovering that Swing changed the selection instead. But it was actually a second more subtle and hidden problem in the implementation that was the limiting factor: a problem with when TransferHandler called the canImport method. As you know, the canImport method gives you control over whether or not a component should accept a particular drag operation. Prior to Mustang, though, canImport was only called when a drag operation entered a component, and the returned value was then used for the duration of the mouse's visit to that component. This gave the developer absolutely no mechanism to return different values based on the drop location, or any other factors that might change throughout the operation.

Enter Mustang! With the recent enhancements made to Swing Drag and Drop, canImport is now called continuously by a component's TransferHandler while a drag and drop operation is occuring over it. As such, the acceptability of a transfer can change as many times, and based on whatever criteria (including the drop location), you choose. You need only return a different value from canImport and Swing will respond. I'll elaborate on what that response is. When canImport returns true to accept a transfer, Swing shows the normal drag and drop mouse cursor and visually indicates the drop location, as discussed in my previous blog entry. When canImport rejects a drag, on the other hand, the "no-drag" mouse cursor is shown and no drop location is indicated. Let's see this in action - click here to launch a Java Webstart demo (requires build 76 or greater of Mustang).

Below you'll see two screen shots of how the demo appears. 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 on any node except for "names" and its descendants. At the bottom is a JComboBox that we'll talk about a bit later, when I introduce another new method in Mustang to allow further customization of when drop locations are shown. For now, simply note that it is set to "Default", meaning that nothing has been changed from the default behavior of showing drop locations when accepted, and not showing them when rejected.

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 hover the mouse over most of the nodes, the drag acceptibility is indicated by both the mouse cursor and by the node becoming highlighted. This is portrayed in Screen Shot 1, which shows an acceptable drag over the "yellow" node. Likewise, Screen Shot 2 shows what happens when you drag over the "names" node or any descendant, which I've mentioned have been configured to reject the drag. The "no-drop" cursor is shown, and the drop location isn't indicated.

Screen Shot 1 Screen Shot 2
 

Here's the implementation of canImport from the demo, showing how drops on top of "names" and descendants has been prevented:


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, as shown in the following code snippet, has been renamed to TransferSupport. For full details on these changes, please see my later blog entitled Choosing the Drop Action, and Further Changes to Swing Drag and Drop.

public boolean canImport(TransferHandler.TransferInfo info) {
    // for the demo, we'll only support drops (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();

    // fetch the path from the drop location
    TreePath dropPath = dl.getPath();

    // we'll reject invalid paths, and descendants of the "names" node
    if (dropPath == null || NAMESPATH.isDescendant(dropPath)) {
        return false;
    }

    return true;
}

So what if you want to change an aspect of this behavior from the default? What if you want to always show the potential drop location, regardless of the acceptability of the transfer, so that the user can see what item they're hovering over? Or say for some reason you prefer not to indicate the drop location for some items, even though you want to accept the transfer itself?


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, shouldIndicate, as mentioned in the remainder of this document, has been removed in favor of a new setShowDropLocation method on TransferSupport. For full details on these changes, please see my later blog entitled
Choosing the Drop Action, and Further Changes to Swing Drag and Drop.

TransferHandler now calls a new method, allowing you to control this too:

boolean shouldIndicate(TransferInfo info, boolean canImport)

This method returns whether or not the drop location should be indicated for the given transfer, and is called after the canImport method. The arguments are the TransferInfo for the transfer in question, and the return value from the preceding call to canImport. The default implementation simply returns the value of the canImport argument, causing the drop location to be shown if and only if the TransferHandler has already said that it can accept the import; the behavior that I've described above. By simply overriding this method, you can customize the drop location indication as it works for your application. For example, the following simple override will cause the drop location to always be shown, even if the canImport method rejects the transfer:

public boolean shouldIndicate(TransferInfo info, boolean canImport) {
    return true;
}

Likewise, returning false causes the drop location to never be shown. Of course, the implementation can be more involved, possibly inspecting the TransferInfo, depending on how specific your logic needs to be. An important thing to note is that this method is designed specifically for configuring the drop location indication, and it does not affect how the mouse cursor appears. The mouse cursor needs to tell the user whether or not the drag is acceptable and, as such, needs to stay true to the return value of canImport. Let's have a look at this in practice, by bringing up the demo again.

This time we will play with the JComboBox at the bottom, which has been set up to dictate how shouldIndicate will return. Switch the option to "Always" to have shouldIndicate always return true. Dragging over the JTree you'll see that the drop location is shown for all nodes, including "names" and its descendants which won't actually accept the transfer. This can be seen in Screen Shot 3 below. Switching the JComboBox to "Never" configures shouldIndicate to always return false. This causes the drop location to not be indicated for any node, regardless of whether or not the transfer is acceptable. Screen Shot 4 shows this case, where the "yellow" node is not highlighted even though the mouse cursor is showing an acceptable transfer.

Screen Shot 3 Screen Shot 4
 

Pretty neat! Location-sensitive Drag and Drop for Swing, new in Mustang. I encourage you to keep those questions and comments coming. You may just prompt my next blog entry! All the best :)



toto, The Francophone foo

Posted by shan_man on January 16, 2006 at 08:22 AM | Permalink | Comments (14)

Do the toto!

Shortly before the holidays, I had the opportunity to visit a customer site in the wonderful city of Montreal, Canada. I was an eater of excellent food, a seer of new sites, and a lucky fan at Centre Bell when the Montreal Canadians won their first overtime shootout under the new NHL rules! And that was just in the off hours. I also truly enjoyed myself during the day in the company of the folks whom I met, who, by the way, are all very savvy and are doing powerful things with Java and Swing. I thank them for their friendly hospitality, for switching to English during my visit, and for encouraging my multiple attempts at expanding my French vocabulary (Je voudrais des frites, s'il vous plait). I also thank them for introducing me to toto. The introduction went something like this:

We were sitting in a conference room, where it was my turn at the whiteboard to demonstrate code snippets. So I began: boolean foo = ... And then I stopped, for suddenly (and I don't what prompted it) I was unsure if they'd ever met foo. Feeling it was important to know before continuing, I asked them straight out. Gesturing over my shoulders with both hands towards the board, I asked them: "Do you foo?" Well I assume they took my gestures out of context, for the next few seconds were extremely comical. I watched as they looked at each other, and some non-verbal communication passsed between them. Then, looking back at me akwardly, they slowly pushed back their chairs and rose. Beginning a cute little dance, they responded in the negative: "Non...nous ne foo pas. But we toto!"

Alright, okay, so the true story is a lot less entertaining. In actuality, I did ask about the use of foo in French programming and, in fact, these gentlemen had not seen it before. But, being that these were smart guys, they deduced exactly what it represented. And that's how it came about that they introduced me to foo's French counterpart toto. You see, where we'd normally talk using "foo", "bar" or "baz" in English, as a proper francophone you should correctly use the French "toto", "titi" or "tata". And while I personally couldn't get Dorothy's little puppy out of my head for the first hour, I eventually became very comfortable with this. Due to their pattern that has you alternating hands between letters (LEFT: t - RIGHT: o - LEFT: t - RIGHT: o), some of these little guys are exceptionally easy to type - and can be entered even faster than the three-letter foo! Give it a try.

But what about the words themselves? Is "toto" as steeped in culture as the English "foo"? Is "titi" a direct translation of our "bar", the place where programmers rejuvinate on coffee between late-night coding sessions? And what about "tata", a word I've actually used myself when speaking with my two-year-old sons; as in "tata honey, please give Daddy back the digital camera that you're not supposed to be playing with." Well it turns out that these words are designed to have no inherent meaning at all when we use them to talk about computer systems. With a little googling, I've discovered that all of these words, English and French alike, belong to a group with the easy-to-remember name of metasyntactic variables. They are so-called, according to Wikipedia and The Jargon File, because (a) "They are variables in the metalanguage used to talk about programs, etc." and (b) "They are variables whose values are often variables (as in usages like 'the value of f(foo, bar) is the sum of foo and bar')."

Nifty - so someone's named some of the nonsense that we programmers instinctively understand! But what I find particularly interesting, since I'd never considered it before, is that the implementation of the concept varies across cultures and languages. Again reading from Wikipedia, I've learned that when programming in Estonia you may need to refer to the "kalatehas (fish factory)" or maybe "oxe (misspelled vomit)". And in Italy, according to The Jargon File, be prepared to be introduced to "pippo (Goofy)" and "paperino (Donald Duck)".

Cool! By happening to discover the non-universality of foo, I met his francophone twin toto, became more cultured, and learned about a universal concept. I'd say my visit to Montreal was quite succesful!



First Class Drag and Drop Support in Mustang

Posted by shan_man on January 02, 2006 at 03:41 PM | Permalink | Comments (32)

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!



Improved Drag Gesture in Swing

Posted by shan_man on June 07, 2005 at 12:22 PM | Permalink | Comments (29)

One small step for Shan, one exciting leap for Swing drag and drop.

Fantastic! The java.net folks kindly set me up a place to share my thoughts, I sit down and begin typing, and out comes a play on the monumental Neil Armstrong quote. I consider: "Why would I start my blog space with a goofy play on words, appearing to compare Swing drag and drop to humankind landing on the moon?" Well, the answer is twofold: First, as a software engineer, my mind often travels into odd or complex places, and seems to stick there for extended periods of time (like the months I disappeared into fixing the drag and drop bug which I'm about to tell you about). Driving to work this morning, thinking about how to start off this blog that I've been planning for some time, in popped the "One small step..." jingle. As I've just described, I got stuck on it. Although I got a kick out of my own [not so] wittiness, I realized it was rather cheezy. So I tried playing a game with myself to pass the commute - if I could forget about it by the time I arrived at work, I'd find something else to start with. You know how successful trying to forget about something can be...

But the more important reason I started as I did is that it perfectly summarizes this morning for me. I'm taking my very first step into the world of blogging, and to mark the occasion I'm announcing a very exciting bug fix to Swing drag and drop. While I wouldn't even think to seriously compare this to landing on the moon, I know it will mean the world to a rather large group of Swing drag and drop users.

Until very recently, the sixth highest vote getter on Sun's list of Top 25 Bugs was bug 4521075 with a total of 239 votes. This bug reports that the recognition of a drag gesture in Swing is unlike what is typically expected by users. More specifically, it complains that Swing requires an item to first be selected with one mouse click, before pressing on it again and dragging to begin drag and drop. The much more common paradigm expected by users is that a single mouse press should be able to select the item and also begin recognition of a drag gesture.

I am extremely pleased to announce that this important bug has been fixed! In fact, it has already been released to the public in build 36 of Mustang, which you can download now. And let me top this with some even better news: This fix is also targeted to be released in update 5 of J2SE 5.0, due out in the not too distant future.

Originally, the fix for this bug was targeted to be the default behavior in FCS of J2SE 5.0. However, it turns out that the changes involved were larger and had more potential for backward incompatibility than was orginally expected. It was a sad thing to do after all of the effort we'd invested, but we finally decided that this needed more investigation to eliminate the risk of breaking existing applications, and therefore it couldn't make 5.0. But that's history! Today, the fix is available in Mustang with very few potential points of incompatibility, and we'll make the entire fix available in 5.0 update 5 safely by conditionalizing around a system property, "sun.swing.enableImprovedDragGesture". So in update 5 you'd launch your application like this:

java -Dsun.swing.enableImprovedDragGesture APPCLASSNAME

(In Mustang the system property disappears and it just works!)

Most of the details regarding this fix are available in the evaluation of the bug report. But please let me point out the highlights:

  • JList, JTree, and JFileChooser have been fixed such that a selection is no longer required before pressing on an item to begin a drag operation. More importantly, we've carefully studied this behavior and are confident that the many different combinations of mouse gestures will work as expected (ex. ctrl-press, ctrl-press then drag, etc).

  • JTable has also been fixed to work as described, but (for good reason) only in SINGLE_SELECTION mode. The reason behind this decision is that JTable has always allowed the user to select ranges of cells by dragging the mouse. The new drag gesture would be identical to the existing gesture for selecting a range of cells, and this conflict prevented the change from being be made in modes supporting multiple selections. In these modes, users must continue to select a range of cells first, before clicking on them to start drag and drop. This behavior is consistent with the spreadsheet applications we compared with during development.

Of course, the description of this change wouldn't be complete without a quick summary of any potential points of incompatibility:

  • Consolidation of selection and gesture recognition logic: Prior to this fix, selection logic and drag gesture recognition logic was separated into two distinct mouse listeners installed by the UI on each component. The two listeners knew nothing about each other, and therefore it wasn't possible to make them do the right thing. In some cases selection needs to happen before considering for a drag gesture, and in other cases drag and drop should begin without processing selection changes. Without coordination between the two listeners, this process couldn't work.

    The fix was to put all selection and drag gesture recognition logic into a single mouse listener to be installed by the UI on each component. This has worked extremely well and has facilitated the fix of this bug as desired. It has also introduced the first two very minor points of potential incompatibility. First, I have seen numerous work-arounds posted to forums for this bug, and some of them include removing or playing with the private listeners on the components. It is unknown how these work-arounds will fare under the re-designed drag recognition. The good news is that any problem can be easily fixed by removing the work-around. The second potential for incompatibility is for developers that have created custom UI implementations supplying their own mouse listeners that replace the behavior of Swing's defaults. Any such implementation would simply suffer from no longer having built-in drag support. We don't expect this to be a major issue though, as most UIs replace the look of a component and leave the listeners alone.

  • Handling of cell editors: The last thing of significance that had to be modified to facilitate this change is the way that cell editors are handled in JTable and JTree when drag recognition is turned on. While selection and drag recognition will be considered on mouse press events, cell editing will now not be considered until after the mouse has been released. This will not affect most developers at all (after the mouse release, Swing re-processes the press event - and things will appear to developer code the same as before). The only easily noticeable result of this is that JComboBoxes in JTables now pop up on the release rather than the press. This is considered a benefit in that users can now begin drag and drop or drag to make selections on top of a JComboBox cell without it popping up. Finally, the value of CellEditor.shouldSelectCell() will now be ignored for table and tree editors when drag is enabled, on the grounds that cells that aren't selected can never be dragged.

Phew, that covers it! I can't believe I let my mind back into those last couple of points again. But seriously folks, I absolutely love working on Swing drag and drop and there's some more exciting changes in the works that I'll be writing about shortly. So please, if you're interested in Swing drag and drop, Swing in general, or cheesy plays on words, please come back and visit soon. Otherwise, life would really be a drag. Ha ha h...OK, I think it's about time I drop it.



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