24.6 Finishing Touches


With the DnD API, we can add a few more features to help us achieve the goal of user interface maturity. Since we can't drop our objects on other leaves, we really should keep the drag cursor consistent. If we're over a folder, we can accept the drag and show the "ok to drop here" cursor. If we're anywhere else, we'll reject the drag and show the "no drop" version. The current implementation of our handlers allows only nodes to be moved. We can include support for "copy or move" drags. (We can also keep our cursor in sync with the new support so users know whether they're copying or moving.) Once all this is working, we should also set up autoscrolling on our tree to give the user access to any off-screen parts of the tree.

24.6.1 Dynamic Cursors

If your application can use the new DnD support built into the 1.4 release, you shouldn't have to worry about cursor management. If you're using one of the older SDKs or building fancier custom support, read on.

The DnD package ships with some standard cursors for indicating "ok to drop" and "not ok to drop." These cursors are displayed automatically when you accept( ) or reject( ) an item as it is dragged over potential drop targets. Here's a look at the dragEnter( ) and dragOver( ) event handlers that check to see whether the mouse is over a folder in the tree. If it's not, we reject the drag. This rejection causes the cursor to update its appearance. (Note that with the new cursor features in Java 2, you can define your own cursors and use them here if you don't like the cursors supplied by your windowing system.)

  private TreeNode getNodeForEvent(DropTargetDragEvent dtde) {     // Use the same logic from the drop( ) method to see if the cursor is over a     // folder in the target tree.     Point p = dtde.getLocation( );     TreePath path = targetTree.getClosestPathForLocation(p.x, p.y);     return (TreeNode)path.getLastPathComponent( );   }   public void dragEnter(DropTargetDragEvent dtde) {     dragOver(dtde);   }   public void dragOver(DropTargetDragEvent dtde) {     TreeNode node = getNodeForEvent(dtde);     if (node.isLeaf( )) {       dtde.rejectDrag( );     }     else {       // Start by supporting move operations.       dtde.acceptDrag(DnDConstants.ACTION_MOVE);     }   }

24.6.2 Changing the Drop Action

To support both move and copy operations, we need to make two modifications. Contrary to what you might infer from the names of the event-handling methods, we do not need to override the dropActionChanged( ) method. (You would override this method if you want to react to the change during the drag operation. We just want to react to the change when the data is finally dropped.) All we really need to do is make sure that our acceptDrag( ) methods handle both copy and move actions. Regrettably, you cannot accept the ACTION_MOVE_OR_COPY action to encompass both types of drops. You need to decide which type of drop occurred and accept that drop specifically. However, if you know you'll accept any drop the user can make, you can try the follwing trick. In TreeDropTarget.java, we had:

dtde.acceptDrag(DnDConstants.ACTION_MOVE);

We now have:

dtde.acceptDrag(dtde.getDropAction( ));

We just accept whatever the user's desired action is.

To complete the transaction properly, we need to make sure that during a copy operation we don't remove the original node. In TreeDropSource.java, we need a slightly smarter dragDropEnd( ) method:

public void dragDropEnd(DragSourceDropEvent dsde) {   // To support move or copy, we have to check which occurred,   // and remove the node only if it was a move operation.   if (dsde.getDropSuccess( ) &&       (dsde.getDropAction( ) == DnDConstants.ACTION_MOVE))    {     ((DefaultTreeModel)sourceTree.getModel( ))           .removeNodeFromParent(oldNode);   } }  

So now, if the user performs a copy rather than a move, we leave the source tree intact. Otherwise, we remove the node from the source tree just as we did before.

24.6.3 Autoscrolling

Often, a drop target is contained in a scrollable window. Word processors with text that can't fit on the screen and file managers with a large hierarchy of files and folders are common examples. If you want, you can create drop targets that set up an autoscroll boundary. This boundary allows a user in the middle of a drag operation to hold the mouse near an edge of your component, which causes the component scroll itself to show the previously hidden area.

Regretfully, this is not something that is implemented by default on components such as JTree and JTextArea. However, you can create autoscrolling versions of these classes by implementing the Autoscroll interface. (See Figure 24-9.)

Figure 24-9. The editable tree with autoscroll boundaries drawn in
figs/swng2.2409.gif

24.6.4 The Autoscroll Interface

For any component that can appear in a JViewport or JScrollPane, you can extend it and implement this interface. This allows DropTarget objects to monitor where your cursor is and initiate an autoscroll if necessary. Autoscrolling is handled by an autoscroller, which is discussed in the next section.

24.6.4.1 Methods

The Autoscroll interface has only two methods:

public Insets getAutoscrollInsets( )

This method returns an Insets object that describes the autoscroll activation areas of a target. Figure 24-9 shows a JTree with an outline of the insets drawn over the tree.

public void autoscroll(Point cursorLocn)

This method is called by the autoscroller to force the component to reposition itself. Exactly where the component moves to (how fast, in which direction, etc.) is up to you. The cursorLocn argument gives you the location of the mouse cursor that prompted the scrolling request. You get full control over the scrolling. You can jump to a new location or use a loop to incrementally inch your way to the target.

We can add an inner class that extends JTree and implements this interface to achieve our autoscrolling, rearrangeable tree. Pay special attention to the AutoscrollingJTree class.

// AutoscrollTest.java import java.awt.*; import java.awt.dnd.*; import java.awt.datatransfer.*; import java.awt.event.*; import java.io.*; import java.util.*; import javax.swing.*; import javax.swing.tree.*; public class AutoscrollTest extends JFrame {   TreeDragSource ds;   TreeDropTarget dt;   JTree tree;   public AutoscrollTest( ) {     super("Rearrangeable Tree");     setSize(300,200);     setDefaultCloseOperation(EXIT_ON_CLOSE);     // If you want autoscrolling, use this line:     //     tree = new AutoScrollingJTree( );     // Otherwise, use this line:     //     tree = new JTree( );     getContentPane( ).add(new JScrollPane(tree), BorderLayout.CENTER);     ds = new TreeDragSource(tree, DnDConstants.ACTION_COPY_OR_MOVE);     dt = new TreeDropTarget(tree);     setVisible(true);   }   public class AutoScrollingJTree extends JTree implements Autoscroll {     private int margin = 12;     public AutoScrollingJTree( ) { super( ); }     // You've been told to scroll because the mouse cursor is in your scroll zone.     public void autoscroll(Point p) {       // Figure out which row you're on.       int realrow = getRowForLocation(p.x, p.y);       Rectangle outer = getBounds( );       // Now decide if the row is at the top of the screen or at the bottom. Do this       // so that the previous row (or the next row) is visible. If you're at the       // absolute top or bottom, just return the first or last row, respectively.       realrow = (p.y + outer.y <= margin ?                   realrow < 1 ? 0 : realrow - 1 :                   realrow < getRowCount( ) - 1 ? realrow + 1 : realrow);       scrollRowToVisible(realrow);     }     // Calculate the insets for the *JTREE*, not the viewport the tree is in. This     // makes it a bit messy.     public Insets getAutoscrollInsets( ) {       Rectangle outer = getBounds( );       Rectangle inner = getParent( ).getBounds( );       return new Insets(inner.y - outer.y + margin, inner.x - outer.x + margin,         outer.height - inner.height - inner.y + outer.y + margin,         outer.width - inner.width - inner.x + outer.x + margin);     }     // Use this method if you want to see the boundaries of the autoscroll active     // region. Toss it out otherwise.     public void paintComponent(Graphics g) {       super.paintComponent(g);       Rectangle outer = getBounds( );       Rectangle inner = getParent( ).getBounds( );       g.setColor(Color.red);       g.drawRect(-outer.x + 12, -outer.y + 12, inner.width - 24, inner.height - 24);     }   }   public static void main(String args[]) {     new AutoscrollTest( );   } }

In the autoscroll( ) method, we check to see if the cursor is near the top edge or the bottom edge. We use JTree's getRowForLocation( ) method to find the row closest to the cursor and then make the next (or previous) row visible. If you implemented this functionality for a component that did not have something like the scrollRowToVisi-ble( ) method, you can grab the component's container and scroll it. (Most likely, the parent container is a JViewport, so you can code for this. Remember that even in a JScrollPane, the component is housed in a JViewport that is tied to the scrollbars.)

The getAutoscrollInsets( ) method looks a bit ugly since we need to supply insets that match the tree, not the viewport that contains the tree. As we expand the tree, it gets taller. As it gets taller, we need to make the bottom inset larger. Figure 24-10 illustrates the problem we're facing.

Figure 24-10. Real insets versus visible insets for a component in a JViewport
figs/swng2.2410.gif

So, all the extra work in that method will take the varying size of the tree into account. Fortunately, this also works when the user resizes the application and changes the space allotted to the viewport. The performance of these calculations is surprisingly quick and should suffice for many applications. However, for performance fanatics, you can certainly modify this class to listen to component events on both the tree and the viewport. If either of these changes, you can reset the values of the insets, which do not change during the getAutoscrollInsets( ) method.

24.6.5 The DropTarget.DropTargetAutoScroller Class

As we mentioned earlier, there is a separate helper class involved in listening to the mouse cursor's position and causing a target to autoscroll when the cursor waits long enough in the active autoscroll region. The DropTarget class has an inner class called DropTargetAutoScroller devoted to this task. When you register a component with a drop target, the target checks to see if the component is an instance of Autoscroll. If so, it creates an autoscroller for you. You can, of course, subclass if you want different behavior from the autoscroller.

24.6.5.1 Constructor

The only constructor for the autoscroller is protected:

protected DropTarget.DropTargetAutoScroller(Component c, Point p)

This creates an autoscroller for the component c and stores an initial value for the mouse location p. (This value is used to determine whether the mouse moved since the last time an autoscroll was performed.)

24.6.5.2 Methods

Three methods are present to start a scroll, stop a scroll, and update the mouse's location:

public void actionPerformed(ActionEvent e)

The autoscroller works by timing the mouse. If this method sits in an active autoscroll region long enough, an autoscroll is started. To do this, it uses a Timer object that reports its ActionEvent to this class. This method then calls the autoscroll( ) method of your component.

protected void stop( )

This method stops the autoscroller's timer.

protected void updateLocation(Point newLocn)

This method keeps the autoscroller in sync with the mouse as it moves around, and restarts the timer if necessary.



Java Swing
Graphic Java 2: Mastering the Jfc, By Geary, 3Rd Edition, Volume 2: Swing
ISBN: 0130796670
EAN: 2147483647
Year: 2001
Pages: 289
Authors: David Geary

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net