How to drag and drop with Java 2, Part 1

Explore the Java platform's new drag and drop classes

1 2 3 Page 2
Page 2 of 3
  1. DragGestureRecognizer recognizes a gesture, notifies the DragGestureListener.

  2. Assuming the actions and/or flavors are OK, DragGestureListener asks DragSource to startDrag().

  3. DragSource creates a DragSourceContext and a DragSourceContextPeer. The DragSourceContext adds itself as a DragSourceListener to the DragSourceContextPeer.

  4. DragSourceContextPeer receives state notifications (component entered/exited/is over) from the native system and delegates them to the DragSourceContext.

  5. The DragSourceContext notifies the DragSourceListener, which provides drag over feedback (if the DropTargetListener accepts the action). Typical feedback includes asking the DragSourceContext to change the cursor.

  6. When the drop is complete, the DragSourceListener receives a dragDropEnd notification message.

Creating a droppable component

For a GUI component to act as the destination of a D&D operation, it must be associated with two objects:

  • java.awt.dnd.DropTarget
  • java.awt.dnd.DropTargetListener

The DropTarget

The DropTarget's constructor makes associations with DropTarget and DropTargetListener objects. Alternatively, the DropTarget has setComponent and addDropTargetListener methods. The DropTarget's constructors use newly defined java.awt.Component methods to inform the Component that it is now associated with the DropTarget.

Here are the additions to java.awt.Component:

Component methods
public void setDropTarget(DropTarget dt)
public DropTarget getDropTarget()

Let's define a droppable component. Here we create a DropTarget in our Component's constructor. The Component associates itself and a DropTargetListener with the DropTarget via the DropTarget's constructor:

public class DropLabel extends JLabel { public DropLabel(String s) { this.setText(s); // not strictly necessary to maintain this reference this.dtListener = new DTListener();

// component, ops, listener, accepting this.dropTarget = new DropTarget(this, this.acceptableActions, this.dtListener, true); } private DropTarget dropTarget; private DropTargetListener dtListener; private int acceptableActions = DnDConstants.ACTION_COPY; }

The DropTargetListener

The DropTargetListener needs an association with the Component so that the Component can notify the DropTargetListener to display "drag under" effects during the operation. This listener, which can be conveniently created as an inner class, transfers the data when the drop occurs. Warning: The Component itself shouldn't be the listener, since this implies its availability for use as some other Component's listener.

The dragEnter, dragOver, and dropActionChanged methods must accept or reject the current operation after they inspect the action and flavor. Only if the operation is accepted will the DragSourceListener call its dragEnter and dragOver methods.

Additionally, the DropTargetListener invokes the dragExit method when the user leaves the droppable area. The listener may undo any drag under effects here.

Here's a DropTargetListener implementation that provides drag under feedback by displaying a green border if it can handle the transfer (we'll discuss how isDragOk determines this next) and a red border if it cannot:

  class DTListener implements DropTargetListener {
    public void dragEnter(DropTargetDragEvent e) {
      if(isDragOk(e) == false) {
    e.rejectDrag();      
    return;
      }
      DropLabel.this.borderColor=Color.green;      
      showBorder(true);
      e.acceptDrag(DropLabel.this.acceptableActions);
    }
    public void dragOver(DropTargetDragEvent e) {
      if(isDragOk(e) == false) {
    e.rejectDrag();      
    return;
      }
      e.acceptDrag(DropLabel.this.acceptableActions);      
    }
    public void dropActionChanged(DropTargetDragEvent e) {
      if(isDragOk(e) == false) {
    e.rejectDrag();      
    return;
      }
      e.acceptDrag(DropLabel.this.acceptableActions);      
    }
    public void dragExit(DropTargetEvent e) {
      DropLabel.this.borderColor=Color.green;            
      showBorder(false);
    }

In action, the complex flow of these messages remains hidden. For the curious, the event objects maintain a reference to their DropTargetContext. The DropTargetContext in turn maintains a reference to the native DropTargetContextPeer. When the listener sends the various accept or reject messages, they go all the way to the native system.

Now let's discuss how the listener can determine the validity of the current drag operation. Methods dragEnter, dragOver, and dropActionChanged use method isDragOk to determine whether the DataFlavor being dragged is compatible with the flavors the target component can accept. In this example, any of the flavors defined in StringTransferable will be accepted. In addition, a class variable referring to a collection of DataFlavors defined in the drop component might also be used.

If the flavor is compatible, the DropTargetListener compares the actions supported by the DragSource to the actions acceptable to the DropTarget. The actions specified when the Component created the DragGestureRecognizer may be retrieved through the DropTargetDragEvent's getSourceActions method. If both the action and the flavor are OK, a true value is returned.

Let's examine the definition of isDragOk. The DataFlavors and actions offered by the DragSource are inspected:

private boolean isDragOk(DropTargetDragEvent e) { DataFlavor[] flavors = StringTransferable.flavors; DataFlavor chosen = null; for (int i = 0; i < flavors.length; i++) { if (e.isDataFlavorSupported(flavors[i])) { chosen = flavors[i]; break; } } /* * the src does not support any of the StringTransferable * flavors */ if (chosen == null) { return false; } // the actions specified when the source // created the DragGestureRecognizer int sa = e.getSourceActions();

// we're saying that these actions are necessary if ((sa & DropLabel.this.acceptableActions) == 0) return false; return true; }

Data transfer

The DropTargetListener's drop method is responsible for the data transfer. There are three parts to the drop method:

  1. The DropTargetListener checks the validity of the dropped action and DataFlavor. If necessary, the drop is rejected.
  2. The DropTargetListener accepts the drop with a specified action and the Transferable's data object is retrieved.

  3. The DropTargetListener reads and uses the data.

Drop method part 1: validation

The DropTargetListener chooses the DataFlavor and checks the operation before the transfer takes place. If none of the DataFlavors and/or operations are acceptable, the DropTargetListener sends a rejectDrop message to the DropTargetDropEvent. The event object may be queried with isLocalTransfer if the drag source is in the same JVM. If so, the DropTargetListener can choose an appropriate DataFlavor:

    public void drop(DropTargetDropEvent e) {
      DataFlavor[] flavors = StringTransferable.flavors;
      DataFlavor chosen = null;    
      if (e.isLocalTransfer() == false) {
    chosen = StringTransferable.plainTextFlavor; 
      } else {
    for (int i = 0; i < flavors.length; i++ ) {
      if (e.isDataFlavorSupported(flavors[i])) {
        chosen = flavors[i];
        break;
      }
    }
      }
      if (chosen == null) {
    e.rejectDrop();         
    return;
      }
      // the actions that the source has specified with DragGestureRecognizer
      int sa = e.getSourceActions();      
      if ( ( sa & DropLabel.this.acceptableActions ) == 0 ) {
    e.rejectDrop();             
    showBorder(false);              
    return;
      }

Drop method part 2: accepting the drop

Once the drop passes the validation, the DropTargetListener sends an acceptDrop message -- with the desired operation specified -- to the DragSourceListener in its dragDropEnd method. After the DropTargetListener accepts the drop, it retrieves the Transferable and requests its data.

Here's the continuation of the drop method:

Object data=null; try { e.acceptDrop(DropLabel.this.acceptableActions); data = e.getTransferable().getTransferData(chosen); if (data == null) throw new NullPointerException(); } catch ( Throwable t ) { t.printStackTrace(); e.dropComplete(false); showBorder(false); return; }

Drop method part 3: data transfer

In the case of native-to-Java or inter-JVM transfers, the data's representation class should be a subclass of java.io.InputStream, since references to a Java object make sense only in the JVM. (There's a bug in Java 2's StringSelection class: it returns a StringReader, which isn't a subclass of InputStream.)

For local transfers, the Transferable returns an object reference.

If there are any exceptions thrown during data transfer, the DropTargetListener must send a dropComplete(false) message to the DropTargetDropEvent.

However, once the data has been successfully transferred, the DropTargetListener sends the DropTargetDropEvent a dropComplete(true) message.

Note: Unfortunately, Java 2 still contains a bug that prevents some native-to-Java transfers. Java-to-native transfers, however, do not crash despite a few inserted garbage characters. (For more information about the Java 2 bug, see Resources.)

Continuing with our definition of the drop method, we finally retrieve the data:

if (data instanceof String ) { String s = (String) data; DropLabel.this.setText(s); } else if (data instanceof InputStream) { String charset = chosen.getParameter("charset").trim(); InputStream input = (InputStream)data; InputStreamReader isr = null; try { isr=new InputStreamReader(input,charset); } catch(UnsupportedEncodingException uee) { isr=new InputStreamReader(input); }

StringBuffer str = new StringBuffer(); int in=-1; try { while((in = isr.read()) >= 0 ) { if (in != 0) str.append((char)in); } DropLabel.this.setText(str.toString()); } catch(IOException ioe) { /* bug #4094987 sun.io.MalformedInputException: Missing byte-order mark e.g. if dragging from MS Word 97 to Java still a bug in 1.2 final */ System.err.println( "cannot read" + ioe); e.dropComplete(false); showBorder(false); String message = "Bad drop\n" + ioe.getMessage(); JOptionPane.showMessageDialog(DropLabel.this, message, "Error", JOptionPane.ERROR_MESSAGE); return; } } else { System.out.println( "drop: rejecting"); e.dropComplete(false); showBorder(false); return; } e.dropComplete(true); showBorder(false); }

Path of the Transferable

Here are the gory details of the path taken by the Transferable:

The DragGestureListener gets the Transferable (probably from the GUI component) and sends it to the DragSource via the startDrag message.

The DragSource sends the same message to its DragSourceContext, which in turn forwards it to its peer (passing itself as a parameter). The native system thus has access to the Transferable through the DragSourceContext.

The DropTargetContextPeer now has access to this Transferable. When the DropTargetContext receives the getTransferable message, it creates and returns a TransferableProxy object from the DropTargetContextPeer's Transferable.

The TransferableProxy is returned to the DropTargetListener when the DropTargetListener asks the DropTargetDropEvent for the Transferable. (The Event forwards the Listener's getTransferable message to its DropTargetContext.)

Conclusion

Java 2 has given us the long-awaited ability to transfer data using a drag and drop metaphor available on many platforms. Each platform has its own native APIs for drag and drop. Fortunately, these platform dependencies are well hidden. However, Java 2 introduces a different layer of complexity by the sheer number of drag and drop classes and interfaces and their sometimes rather circuitous message-passing methods. Hopefully, this article has taken the mystery out of the use of the Java Drag and Drop API.

We've seen how to create components that can act as drag sources and drop sites to transfer text data in different flavors.

Unfortunately there are still a few bugs -- some in supporting packages such as java.io and java.awt.datatransfer. Our examples have shown how to work around these deficiencies.

In a future article, we 'll discuss D&D with applets, custom DataFlavors including transfer of image data, transfer of filenames from the native system (e.g., dropping files from the Windows Explorer into a Java program), autoscrolling, and the details of using other Swing components such as JTable, JList and JTree for D&D.

For an explanation of drag and drop terms used in this article, see the D&D Glossary.

Gene De Lisa is a senior consultant, instructor, and curriculum developer for Rockhopper Technologies Inc. He has been developing and teaching Java since the fall of 1995. Gene is currently teaching and developing using EJB and Swing. Since 1986, his activities have centered around OOAD, first using C++ and Objective-C, and now Java. He'll be delivering a session on drag and drop at JavaOne '99. He's also an amateur spheniscologist. Watch him turn into his favorite spheniscid species here.
1 2 3 Page 2
Page 2 of 3