How to drag and drop with Java 2, Part 2

Create a library of drag and drop-enabled Swing components

1 2 3 Page 2
Page 2 of 3

Selecting the item under the pointer provides intuitive drag-under feedback. The pointer location can be retrieved from the DropTargetDragEvent object by its getLocation() method. This location can be turned into an index with the JList locationToIndex() method. If the returned index is -1, we need to clear the selection, since the pointer isn't over any item. Otherwise, we select the item at the returned index. If the drop cannot take place at the selected index, we can change the background color to red and reset it to the default selection color when the drop is OK. To undo the drag-under feedback, we can simply clear the selection.

Drag-over feedback

With the isDragOk() method, we can determine if the pointer is currently over an item or not. If the value returned from the locationToIndex() method is -1, the return value should be false. The adapter will then set the no-drop cursor. One special case is that if the model is empty, the locationToIndex() method will always return -1, since there are no items over which the pointer can be. Therefore, we should check to see if the model size is zero and return true if it is.

The drop

The combination of our drag-over and drag-under feedback ensures that there will be an item selected when the drop occurs. Should we insert the data before or after the item selected by our drag-under feedback?

One way to determine this is to make it a user preference. We need to remember which index we insert the data into, since it will be used in the move() method.

The Transferable

Since the JList may contain many different kinds of data, we rely on the TransferableListModel to provide the Transferable. The JList only needs code to find the selected index and to get the Transferable from the model at that index. Here, too, we must remember the selected index, since it will be needed in the case of a move operation.

Here's the code:

this.fromIndex = this.getSelectedIndex();
return this.model.getTransferable(this.fromIndex);

The move operation

We need to remember both the index of the item used to create the Transferable and the index where the item was inserted. If the same JList is both the source and destination, we can't simply remove the item at that index. The dropped data is added before the move operation. If the dropped data is inserted at an index that is less than the original index, it will be off by one.

If the move operation isn't local, we can simply remove the element at that index.

Let's look at this in detail:

public void move() {
     if(this.local) {
         if(this.insertIndex < this.fromIndex)
          this.fromIndex++;
     }
     this.model.remove(this.fromIndex);
}

JTree

For each of the D&D issues, we will now discuss possible solutions for a D&D-enabled JTree.

Starting the drag

There's no easy way to get the node at the current pointer location. To do so, you need to get the TreePath at the location and get the last path component from it. If the user is currently editing a node, stop the edit before starting the drag.

Now, let's examine how this works:

protected DefaultMutableTreeNode getTreeNode(Point p) {
     TreePath path = this.getPathForLocation(p.x, p.y);
     if(path != null) {
         return (DefaultMutableTreeNode)path.getLastPathComponent();
     } else
         return null;
    }
}

Drag-under feedback

You might want to expand the node under the pointer and select the node that the pointer is over. Display invalid feedback if this node doesn't accept children.

Drag-over feedback

You can check to see if the node under the pointer allows children. If not, return false from the isDragOk() method to show the no-drop cursor.

The drop

If the drop is local and the operation is to copy, you probably want to create a copy of the node rather than simply insert the node. Otherwise, when either of the nodes (the original or the copy) is modified, the other will be as well. You might also want to make a deep copy of the dropped node. The default clone method makes a shallow copy. You need to write your own deep-copy code. You can do this with the getChildCount() and getChildAt() methods.

The Transferable

Create a TreeTransferable object from the node under the current pointer location. Then save a reference to this node.

The move operation

Remove the node that was used to create the Transferable object with method removeNodeFromParent().

JTable

The JTable has many of the same issues as the JList.

The table is a view of the data in its model. If each table cell represents an object in the model, you may want to create a table in which the user can drag and drop individual cells. If the table maps each object to a row and the table columns to the object's attributes, you might want the user to be able to drag and drop a table row.

One frequent question that comes up in such situations is how to drop onto an empty table or a table with empty cells. For example, the table could represent the innings in a baseball game. If the game is in progress, the table's later innings would be empty. (This is more of a model question than a D&D issue.) One way to solve the problem is for the game model to start with nine default inning objects. At the end of each inning the user can drop the new inning onto the table to replace the default one. A more efficient answer for a situation with a large number of cells is to create a model that uses a sparse collection.

Starting the drag

In addition to verifying that the data under the pointer is draggable, you need to check to see if an edit is in progress. If so, stop the edit with the editingStopped() method.

Drag-under feedback

You can select the cell or row under the pointer for drag-under feedback. To accomplish this, use methods columnAtPoint() and/or rowAtPoint() to determine which row or cell the pointer is over.

Drag-over feedback

You may want to check the row and column at the current point and veto some cells. If you save the row and column that you used to create the Transferable object, you can prevent dragging and dropping on the same cell. You can also retrieve the row and column counts from the model. If the calculated row or column is greater than the model's row or column count, you may want to veto the drop.

The drop

Does the drop replace the selected row or cell, or does it insert a new row or cell? If it isn't a local drop, it is possible that an edit is in progress. Stop the editing, then either use the model.setValueAt(row, col) method to replace the selection or create a method in the model that allows insertion before or after the selection.

Transferable

Retrieve the Transferable object from the model. Use the selected row and/or column to allow the model to determine how to make the Transferable. If the table drags and drops rows, the selected column is ignored.

The move operation

Save the row and column of the node used to create the Transferable. Use removeNodeFromParent() to remove the node.

Composite components

Components such as the JFileChooser and JColorChooser are composed of several other components. In this section, we discuss the issues involved with adding D&D capability to these components

JFileChooser and GlassPanes

The JFileChooser contains JComboBoxes, a JList, JButtons, and a JTextField. If you create a JFileChooser subclass and use either inner class listeners or adapters, the D&D operation may occur only where there are no children visible. This situation will not be intuitive for the user.

As a solution, we create a special glass pane that implements the drag and/or drop listeners. When visible, the glass pane would need to forward mouse events to the child components. For the JFileChooser, the glass pane can be created and installed in the showDialog() method. The DataFlavors that this glass pane should accept include DataFlavor.javaFileListFlavor. For a drag operation, the JFileChooser's selected file is used to create an instance of our custom FileTransferable class.

Now, let's take a look in detail:

     LinkedList list = new LinkedList();
     list.add( this.filechooser.getSelectedFile() );
     return new FileTransferable( list );

To retarget mouse events, you can get a head start by borrowing parts of the GlassPaneDispatcher inner class from the BasicInternalFrameUI class. However, the GlassPaneDispatcher must be modified to work with JPopupMenus. The JComboBox children of the JFileChooser would not work otherwise, since combination boxes use a JPopupMenu. Moreover, menus in a menu bar won't work either. We need to make the glass pane invisible when a popup is visible in order to make popups work correctly. The MenuSelectionManager class can help to determine if a popup is visible:

MenuSelectionManager msm =
MenuSelectionManager.defaultManager();
MenuElement[] p = msm.getSelectedPath();

If the selected path is greater than zero and the mouse event is a press (the instant in time when the user depresses the mouse button, but has not yet released it) or a click (a press followed by a release), you need to set the glass pane as invisible. The selected path can search for an instance of a JPopupMenu. If one is found, a PopupMenuListener can be installed that will make the glass pane visible again when the popup becomes invisible.

JColorChooser

The glass pane technique doesn't work well with a JColorChooser component. The BasicColorChooserUI object uses a JTabbedPane, which doesn't handle mouse events correctly on tabs beyond the first one. The JTabbedPane may be modified to work correctly, but that doesn't help with the JColorChooser unless a new UI is installed. Also, since the glass pane forwards events to the children, the JSlider children would interfere with the drag gesture.

The current solution is to write a new UI delegate with corrected JTabbedPane and D&D-enabled components. You can install your D&D-enabled versions of the SwatchChooserPanel, HSBChooserPanel, and RGBChooserPanels classes either by your own ColorComponentChooserFactory class or by the ColorChooser's setChooserPanels() method. It would be convenient if you were able to set the ChooserFactory, but that isn't currently possible. This JTabbedPane bug (actually it's a java.awt.Component bug) is in the list of outstanding bugs and will be fixed in a future release of the JDK.

InternalFrame

A common problem with the JInternalFrame is that D&D seems to work at first, but breaks down when an internal frame is selected. The mystery is that when an internal frame is not selected, its glass pane becomes visible and catches events. This bug results from the user's need to be able to select an inactive frame by pressing the mouse anywhere on the internal frame. This bug was supposedly fixed in the JDK 1.2.2 release. In this release you were, in fact, able to drop onto the frame's children. However, the fix seems to have introduced a selection-repaint bug where the previous selection isn't completely cleared during a drag.

One solution is to install another special glass pane that is a DropTargetListener on each internal frame. This glass pane will forward mouse events to the internal frame's child components. We can reuse our modified GlassPaneDispatcher for this. The only modification to the dispatcher is to select the glass pane's internal frame on mouse-press events. A complication is that this glass pane cannot dispatch D&D events (as it can MouseEvents) since they are not AWTEvent subclasses.

JApplet

You can use D&D in an applet if your browser supports Java 2 or if you can use the Java 2 plugin. There are two steps to enable D&D in an applet. First, the applet needs permissions granted in a policy file:

grant {
  permission java.awt.AWTPermission "accessEventQueue";
  permission java.awt.AWTPermission "setDropTarget";
  permission java.awt.AWTPermission "accessClipboard";
  permission java.awt.AWTPermission "acceptDropBetweenAccessControllerContexts";
  permission java.awt.AWTPermission "listenToAllAWTEvents";
};

To use this policy (in a file named policy) with appletviewer, use this command:

 
appletviewer -J-Djava.security.policy=policy index.html 

(Policy files have been discussed elsewhere in JavaWorld, see Resources for URLs.)

Second, DropTargets will not work if you create them in either the init() or start() methods. If you do create them in these methods, you will be able to drag, but not drop.

Below is an example of a JApplet that contains D&D-enabled children. The createComponents() method will create these components and add them to the content pane. You will also want to check to see if this is the first time that the start message has been received. Otherwise, you'd be adding many components!

public void  start() {
     // do this only the first time start is called
     Thread kicker = new Thread( this );
     kicker.start();
    }
    public void  run() {
     this.createComponents();
     JPanel p = (JPanel)getContentPane();
     p.revalidate();
    }

If the JApplet itself is a drop site, you'll need to create the DropTarget the same way -- in a separate thread. In addition, you'll need to specify the content pane as the DropTarget's component:

public void  run() {
     this.inidDnD();
     JPanel p = (JPanel)getContentPane();
     p.revalidate();
    }
void initDnD() {
     this.dropTargetAdapter = new DropTargetAdapter( etc...
     this.dropTarget = new DropTarget(this.getContentPane(),
                          DnDConstants.ACTION_COPY_OR_MOVE,
                          this.dropTargetAdapter,
                          true);
    }
Related:
1 2 3 Page 2
Page 2 of 3