 |
Swing 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
JRootPane—frame.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:
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.
|