Skip to main content

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

Posted by shan_man on February 15, 2006 at 8:25 AM PST

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.
Related Topics >>