Compound Edits

Java > Core SWING advanced programming > 8. DRAG-AND-DROP > Using Drag-and-Drop with the JTree Component

 

Using Drag-and-Drop with the JTree Component

To conclude this chapter, we're going to look at how to implement in Java one of the most common uses of drag-and-drop the ability to copy and move files using a graphical file system viewer. In creating this example, we'll use most of the techniques that you've seen in this chapter and we'll also take a closer look at some of the detail that has so far only been mentioned only in passing.

The FileTree Component

To implement drag-and-drop functionality for a component that displays a file system, we first need such a component. The most natural way to display the relationship between files and folders graphically is to use a tree, in which each file or folder maps to a node; a folder would be modeled as a branch node with files or other folders as its children, while files would always be leaf nodes. Building a tree from a file system is, in principle, a relatively simple matter you just have to create one node per object and arrange them to mirror the actual parent-child relationships in the file system itself. Such a component was implemented in Chapter 10 of Core Java Foundation Classes and we will use that component, called FileTree, as the basis for this example.

The original implementation of the FileTree class is described in detail in Core Java Foundation Classes. To build drag sources and drop targets for FileTree, we will need to make a few minor changes to the original code, which we'll cover in this section, but we're not going to repeat here the description of the basic implementation of the tree. If you have a copy of Core Java Foundation Classes, it would be a good idea to re-read Chapter 10 before proceeding with this section. If you don't have this book, a copy of Chapter 10 in PDF format has been included on the CD-ROM that accompanies this book.

Modifications to the FileTree Component

The original FileTree component was created to show how to represent a hierarchical file system using a JTree. A typical view of the file system that the FileTree component produces was shown in Figure 8-2 earlier in this chapter. While the implementation in Core Java Foundation Classes was perfectly adequate for its purpose, for the sake of this more advanced example, a few modifications were required. If you would like to compare the source code for the original example with that for the drag-and-drop example, you'll find both among the example code for this chapter; the file names are as follows:

FileTree.java The new source code, modified for this example
CoreJFC/FileTree.java The original source code, as created for Core Java Foundation Classes

The changes that have been made to FileTree are summarized below.

Inclusion of files The original FileTree component only showed directories in the file system. Because it would not be very useful to restrict ourselves to dragging and dropping directories, the modified component also shows files.
Ordering of files and directories As you can see from Figure 8-2, the ordering of nodes in the tree is essentially random. In fact, it matches the order in which the java.io.File list method returns the contents of each directory. This is not very satisfactory, so the new version of this component orders the nodes so that all directories precede all files and both files and directories are sorted into alphabetical order. This produces a much more useful representation of the file system.
Autoscrolling Autoscrolling support has been added. As we said earlier in this chapter, autoscrolling support needs to be built into the target component itself rather than being a feature provided by the code that will be used to support drag and/or drop for the component. The code that was added to FileTree is identical to that added to JEditorPane and shown in Listing 8-8.
FileTreeNode made public Each file or directory in the view mapped by the FileTree is represented by an instance of the inner class FileTree.FileTreeNode, which is derived from DefaultMutableTreeNode. In the original implementation, there was no reason to expose these nodes, so they were made protected. For the purposes of this example, it was necessary to directly manipulate the nodes that represent the file system objects that will be dragged around, so this class was made public.
Support for changing the file system view The original implementation provided a static view of the file system. Because Java does not provide a way to receive notification of changes to the underlying file system (for example the removal or renaming of files), no provision was made to incorporate changes into the JTree view. It is still not possible to detect changes to the file system made externally, but it is necessary to update the view when we move or copy a file or directory from one location to another by dragging it from or dropping it onto a FileTree and it is feasible to do this because the code that operates on the file system is aware of the FileTree. Consequently, code has been added that allows the FileTree to update itself as the result of an object being added to or removed from the part of the file system that it is mapping.
FileTreeNode API extended As well as making FileTreeNode public, a couple of extra methods were added to it. One of these (isDir) returns a boolean indicating whether the node represents a directory (true) or a file (false). The other (getFullName) returns the full path name of the file or folder that the node represents as a String.

If you'd like to try out the modified FileTree on its own (that is, without any code for drag-and-drop being present in the example), you can do so by typing the following command:

 java AdvancedSwing.Chapter8.FileTreeTest pathname 

where pathname is the full path name of the directory to appear at the root of the tree. If you are using the Windows platform and you want the root directory of a drive to appear as the tree's root, be sure to supply the backslash as well as the drive name that is, type c:\ instead of simply c:. A typical example of what you'll see is shown in Figure 8-18. If you compare this with Figure 8-2, you'll see that the tree now shows files as well as directories and that the files and directories appear in alphabetical order.

Figure 8-18. An improved file system view component.
graphics/08fig18.gif

The Drag Source Implementation

As before in this chapter, we are going to implement the drag-and-drop support for FileTree as separate classes that we connect to instances of the tree rather than subclassing FileTree and adding the code directly to it. We'll develop the drag source and the drop target separately and we'll explain them in isolation although, in reality, they do have dependencies upon each other. These dependencies, however, are not very close dependencies all we need to ensure is that both implementations obey the usual drag-and-drop protocol in the sense that the drag source exposes the objects to be dragged in a form that is acceptable to the drop target and that the drop target properly communicates the result of the drop operation back to the drag source. In theory, because both drag source and drop targets are conforming to rules imposed by the drag-and-drop subsystem, it should be possible to use either the drag source or the drop target from this book together with the other of the pair obtained from another source.

The FileTreeDragSource Class

The drag source is by far the simpler of the two main pieces of code that we'll show you in this section. The drag source is implemented in a class called (not surprisingly) FileTreeDragSource and looks very much like the one that we created for JLabel in Listing 8-9. The code for FileTreeDragSource is shown in Listing 8-12.

Listing 8-12 A Drag Source for a Graphical File Viewer
 package AdvancedSwing.Chapter8; import java.awt.*; import java.awt.event.*; import java.awt.datatransfar.*; import java.awt.dnd.*; import java.io.*; import java.util.*; import javax.swing.*; import javax.swing.tree.*; public class FileTreeDragSource implements DragGestureListener,                                 DragSourceListener {    public FileTreeDragSource(FileTree tree) {       this.tree = tree;       // Use the default DragSource       DragSource dragSource =                  DragSource.getDefaultDragSource();       // Create a DragGestureRecognizer and       // register as the listener       dragSource.createDefaultDragGestureRecognizer(                  tree, DnDConstants.ACTION_COPY_OR_MOVE, this);    }    // Implementation of DragGestureListener interface.    public void dragGestureRecognized(DragGestureEvent dge) {       // Get the mouse location and convert it to       // a location within the tree       Point location = dge.getDragOrigin();       TreePath dragPath =              tree.getPathForLocation(location.x, location.y);       if (dragPath != null && tree.isPathSelected(dragPath)) {          // Get the list of selected files and create a          // Transferable. The list of files and the is saved          // for use when the drop completes.          paths = tree.getSelectionPaths();          if (paths != null && paths.length > 0) {             dragFiles = new File[paths.length];             for (int i = 0; i < paths.length; i++) {                String pathName = tree.getPathName(paths[i]);                dragFiles[i] = new File(pathName);             }             Transferable transferable =                          new FileListTransferable(dragFiles);             dge.startDrag(null, transferable, this);       }    } } // Implementation of DragSourceListener interface public void dragEnter(DragSourceDragEvent dsde) {    DnDUtils.debugPrintln("Drag Source: dragEnter,                  drop action = " + DnDUtils.showActions(                  dsde.getDropAction())); } public void dragOver(DragSourceDragEvent dsde)    { DnDUtils.debugPrintln("Drag Source: dragOver,                    drop action = " + DnDUtils.showActions(                    dsde.getDropAction())); } public void dragExit(DragSourceEvent dse) {    DnDUtils.debugPrintln("Drag Source: dragExit"); } public void dropActionChanged(DragSourceDragEvent dsde) {    DnDUtils.debugPrintln("Drag Source:             dropActionChanged,drop action = "             + DnDUtils.showActions(dsde.getDropAction())); } public void dragDropEnd(DragSourceDropEvent dsde) {    DnDUtils.debugPrintln("Drag Source: drop completed,             drop action = " + DnDUtils.showActions(             dsde.getDropAction()) + ",             success: " + dsde.getDropSuccess());    // If the drop action was ACTION_MOVE,    // the tree might need to be updated.    if (dsde.getDropAction() == DnDConstants.ACTION_MOVE) {       final File[] draggedFiles = dragFiles;       final TreePath[] draggedPaths = paths;       Timer tm = new Timer(200, new ActionListener() {          public void actionPerformed(ActionEvent evt) {            // Check whether each of the dragged files exists.             // If it does not, we need to remove the node             // that represents it from the tree.             for (int i = 0; i < draggedFiles.length; i++) {                 if (draggedFiles[i].exists() == false) {                    // Remove this node                       DefaultMutableTreeNode node =                     (DefaultMutableTreeNode)draggedPaths[i].                             getLastPathComponent();                       ((DefaultTreeModel)tree.getModel()).                             removeNodeFromParent(node);                 }             }         }      });      tm.setRepeats(false);      tm.start();   } } public static void main(String[] args) {    JFrame f = new JFrame("Draggable File Tree");    try {       FileTree tree = new FileTree(args[0]);       f.getContentPane().add(new JScrollPane(tree));       // Attach the drag source       FileTreeDragSource dragSource = new                                    FileTreeDragSource(tree);    } catch (Exception e) {    }    f.pack();    f.setVisible(true) ; } protected FileTree tree;       // The associated tree protected File[] dragFiles;    // Dragged files protected TreePath[] paths;    // Dragged paths } 

The tasks that FileTreeDragSource must perform are:

  • Associate a DragSource with the FileTree and assign a DragGestureListener to receive notification when the user attempts to initiate a drag. This job is performed in the constructor and uses the same code as we saw in the JLabelDragSource class. As before, the DragGestureListener is directly implemented by the FileTreeDragSource class.

  • When a drag gesture is recognized, check whether the drag should be allowed and call startDrag if it should.

  • Create a Transferable for the drop target.

  • When the drop completes, check whether it succeeded. If it did and the action was ACTION_MOVE, update the view of the file system in the FileTree to reflect the fact that the files and/or directories that have moved should no longer be visible in their original locations.

Each of these tasks is self-contained, so we'll describe them separately in the sections that follow.

Checking Whether the Drag Operation Is Valid

In the case of the drag source for JLabel, the drag operation was always considered valid because the whole surface of the component was considered to be the object being dragged. With a tree, however, this is not the case. The expected protocol when moving files using a graphical file viewer is that the user will select one or more objects from the file system and then, with the cursor over one of those objects, drag them to the drop target. In terms of this example, this means that the dragGestureRecognized method must check that the following conditions are satisfied before calling startDrag:

  • The mouse is positioned over a node of the tree.

  • The node over which the mouse is positioned must be selected.

If either of these criteria is not satisfied, the drag gesture will be ignored. These checks ensure, for example, that the user cannot start a drag by pressing the mouse over an area of unoccupied space in the tree.

You might be wondering how it is possible for the user to attempt to drag one or more selected items from the tree without the mouse being physically placed over one of them the fact that we have to check that the node over which the mouse is positioned when the drag begins is selected implies that it might be possible for that node to be under the cursor and not selected. In fact, this is perfectly possible. To start a drag, you have to click the mouse over a valid node and then drag it. The act of clicking the mouse will cause the node under the mouse to be selected if it is not already selected, so both of the conditions above will be satisfied. But suppose that the node was only one of several nodes that were selected. Clicking on the node will result in that node and all the others being deselected, which means that, because there is now no selection, the drag will be rejected. But suppose the user holds down the CTRL key and then presses the mouse over one of the selected nodes, and then starts to drag the mouse. Holding down CTRL while clicking on a selected node will deselect that node, but leave the others selected. Dragging the mouse with CTRL held down is a valid drag gesture, so it will be recognized by the DragGestureRecognizer and the dragGestureRecognized method will be invoked. Now, the drag will have been started while the mouse was over a node that was not selected, but there still exists at least one other node that is selected. Although we could allow this, it seems counter-intuitive to allow the drag to start if the user is not dragging one of the selected items.

Everything that the FileTreeDragSource has to do to verify that the drag conditions are satisfied can be achieved using facilities provided by the JTree component. To work out whether the mouse is placed over a node of the tree, the position of the cursor at the time that the drag gesture started is obtained from the DragGestureEvent using its getDragOrigin method. This position is then converted to the TreePath for the corresponding node in the tree using the JTree getPathForLocation method. Because the coordinates of the drag location are specified relative the origin of the JTree component, it doesn't matter that the tree is actually mounted in a JScrollPane and that the origin of the tree may not be visible in the viewport, because getPathForLocation requires the position to be specified in the tree's coordinate system. If getPathForLocation returns a non-null value, the drag operation began over a node of the tree in other words, over a directory or file in the file system.

Next, we need to work out whether the node under the mouse is selected. This is simple the isPathSelected method can be used to find out if the TreePath that it is given as an argument is among the selected set, so we just pass it the TreePath returned by getPathForLocation and, if it returns true, both of the drag conditions have been met.

Creating the Transferable

Once the DragGestureListener has determined that the drag is valid, it must call the DragGestureEvent startDrag method, for which it will need a Transferable that will eventually be given to the DropTarget. The drag source is going to pass to the drop target a list of files and directories selected by the user. We've already seen that the java.awt.datatransfer package includes a DataFlavor called javaFileListFlavor that describes data in this form and we saw it in use when we developed the drop target for JEditorPane earlier in this chapter. In that example, however, the drag source was a native application and the Transferable that provided the list of files in the javaFileListFlavor was created internally by the drag-and-drop subsystem. In fact, there is (at the time of writing) no public implementation of such a Transferable in the JDK, so we need to create one of our own. Fortunately, it is very simple and the code is shown in Listing 8-13.

Listing 8-13 A Transferable for a List of File System Objects
 package AdvancedSwing.Chapter8; import java.awt.datatransfer.*; import java.io.*; import java.util.*; public class FileListTransferable implements Transferable {    public FileListTransferable(File[] files) {       fileList = new ArrayList();       for (int i = 0; i < files.length; i++) {          fileList.add(files[i]);       }    }    // Implementation of the Transferable interface    public DataFlavor[] getTransferDataFlavors() {       return new DataFlavor[]                       { DataFlavor.javaFileListFlavor };    }    public boolean isDataFlavorSupported(DataFlavor fl) {       return fl.equals(DataFlavor.javaFileListFlavor);    }    public Object getTransferData(DataFlavor fl) {       if (!isDataFlavorSupported(f1)) {          return null;       }       return fileList;    }    List fileList;            // The list of files } 

The representation class for javaFileListFlavor is defined to be java.util.List and each entry in the list must be a File object corresponding to one file or directory that is part of the data being transferred. The constructor for the FileListTransferable class therefore takes an array of File objects and then converts it to an object of type ArrayList, which implements the java.util.List interface. The ArrayList is held within the Transferable until the drop target asks for it by invoking the getTransferData method that, as you can see, simply verifies that the caller is requesting the data in the appropriate flavor and then returns the store ArrayList. To implement the Transferable interface, this class must also provide implementations of the getTransferDataFlavors and isDataFlavorSupported methods. Because only one flavor is supported, both of these methods are, as you can see, very trivial.

If you return to the dragGestureRecognized method in Listing 8-12, you'll see that after checking the drag conditions, it obtains a list of all the nodes in the tree that are selected using the JTree getSelectionPaths method, which returns the selected nodes as an array of TreePath objects, which is stored within the FileTreeDragSource class for later use. Because the FileListTransferable constructor needs the selected files as an array of File references, it is necessary to convert each TreePath into the corresponding File object. To do this, we need the full file system path name of the node represented by the TreePath. The mapping between TreePath and file system name is known only within the FileTree class. To make it possible to perform the conversion, FileTree provides a method called getpathName, which we call for each TreePath object, and then pass the resulting name to the File constructor to create the appropriate File object.

Once the FileListTransferable has been obtained, the startDrag method is called and the drag operation begins.

Note that this implementation passes the value DnDConstants.ACTION_COPY_OR_MOVE to startDrag, implying that it does not support the link operation. The reason for this is twofold. First, supporting three operations would complicate the example without making it any more useful as a demonstration of how to implement drag-and-drop for a JTree. The second reason is more important however Java just doesn't provide a platform-independent way to link files! If you want to implement this operation, you'll have to write some native code to invoke the underlying operating system services.

Updating the FileTree on Completion of the Drag Operation

After the drop has been completed, the drag source may need to update the FileTree that it is associated with. The reason for doing this should be clear from the following example. Suppose the FileTree shows the content of the directory c:\temp which contains files a and b and that the user selects files a and b and drags them over a drop target using a gesture that indicates a move operation. If the drop target accepts this operation, it will move the files from their current location and put them somewhere else. The process of carrying out this operation will actually remove the files a and b in the directory c:\temp, but the drag source's FileTree will still show them as present. It is the job of the drag source to update the FileTree to show the correct state. This task is performed in the dragDropEnd method in Listing 8-12.

The dragDropEnd method first checks whether the drop action selected by the user was a move; if the user performed a copy, the source files in the underlying file system will not have changed, so there would be no need to change the content of the FileTree. However, some care is required here. Suppose that the drop operation moved file a but failed to move file b, perhaps because there was no room in the file system to which b was being moved. In this case, the drop target may or may not report success, depending on its implementation. Because we can't tell what the drop target will do when it only manages to perform part of the move, the dragDropEnd method does not call the getDropSuccess method of the DragSourceDropEvent, but instead checks whether the files that were the subject of the operation have actually been moved, by making use of the list of File objects that was created by the dragGestureRecognized method it loops through this list, checking each entry to see whether it has been removed and, if it has, removes the file's node from its parent. To update the FileTree, the dragDropEnd method needs to locate the nodes for the files that have actually been removed and remove them from their parent node (the one that represents c:\temp). Locating the node for each file is simple, because the dragGestureRecognized method saves the TreePath objects for the files being dragged; from the TreePath object, the file node itself can be located by calling the getLastPathComponent method and then removed using the DefaultTreeModel removeNodeFromParent method, which will cause the JTree to redraw itself as a side effect.

Core Alert

You'll notice that the dragoropEnd method does not directly check whether the dragged files still exist instead, it starts a timer for a short interval and performs the check when the timer expires. This is an unfortunate trick made necessary by the fact that when dragging files to Windows Explorer, the dragDropEnd method is entered before the files have actually been moved. As a result of this it looks, in the dragDropEnd method, as if the source files still exist as a result of which the FileTree view would not get updated.



You can try out the FileTree drag source by typing command

 java AdvancedSwing.Chapters.FileTreeDragSource pathname 

where pathname is the absolute path name of a directory in your file system. If you select a file or a folder and start dragging it, you'll find that the cursor changes to indicate that the drop would not be accepted, which shows that the drag source is active. You can demonstrate that the drag source actually works by starting Windows Explorer (or dtfile on Solaris) and dragging some files from the FileTree onto it, or start one of the JEditorPane examples from earlier in this chapter and drag a text file or an HTML file onto the editor to see that they are opened successfully.

The Drop Target

Having seen how to implement the FileTree drag source, we now move on to the drop target. You won't be surprised to discover that much of the source code for this drop target is very similar to that for the EditorDropTarget classes that were developed earlier in this chapter. As far as interfacing with the drag-and-drop subsystem is concerned, there is very little difference between JEditorPane and FileTree. Most of the work in this class actually concerns the job of moving or copying the files from the drag source, which has little to do with drag-and-drop itself. To keep the distinction clear, we'll discuss these two aspects separately.

The FileTreeDropTarget Class

As we did with the JEditorPane drop target, we can reduce the job of implementing a drop target for the FileTree component into several smaller pieces:

  • Ensuring that the data type of the object being dragged is acceptable. As in earlier examples, this task is performed by the checkTransferType method. Here, this method is trivial because only the javaFileListFlavor is accepted.

  • Determining whether the cursor is over a valid drop location.

  • Providing drag-under feedback to indicate where the drop would occur, if it is considered valid.

  • When the drop occurs, obtaining the list of files involved and copying or moving them to their new locations.

We'll discuss the last three of these tasks and show the source code for them separately in the sections that follow. The overall structure of the FileTreeDropTarget class is the same as that of EditorDropTarget, so we're not going to show all the source code here. If you want to see the complete implementation, you'll find the source file on the CD-ROM that accompanies this book.

You can try out the drop target implementation by typing the command

 java AdvancedSwing.Chapter8.FileTreeDropTarget pathname 

where, as usual, pathname is the root of the directory tree to be displayed. You can try dragging from one copy of the tree to another by running both of the following commands from separate windows:

 java AdvancedSwing.Chapter8.FileTreeDragSource c:\ java AdvancedSwing.Chapter8.FileTreeDropTarget c:\ 

Note that although you can use a move gesture when dragging files from the drag source onto the drop target, when you use FileTreeDropTarget as the drop target, it will just copy the files and directories that you drag it does not delete the originals. If you want the drop target to implement the move operation properly, you need to define the property DnDExamples.allowRemove, like this:

 java -DDnDExamples.allowRemove                 AdvancedSwing.Chapter8.FileTreeDropTarget c:\ 

Be careful when you enable this property that you only use the move gesture when you really intend to.

The CD-ROM also contains an example that attaches both a drag source and a drop target to the same tree. You can run this example using the command

 java AdvancedSwing.Chapter8.DragDropTreeExample pathname 

and then drag and drop files and folders in a single window.

Checking for a Valid Drop Location

When the user drags the mouse over the drop target, the determination as to whether the cursor is over a valid drop location is made by the acceptOrRejectDrag method, which is called from dragEnter, dragOver, and dropActionChanged. For the user to be able to drop a file into the file system, the destination must be a directory to which the user has write access. To make this determination, the drop target must:

  1. Convert the mouse location to a FileTree node.

  2. Verify that the node represents a directory.

  3. Check that the user has permission to write in that directory.

Because the user can attempt a drop event when the drop target has rejected it because the cursor location does not correspond to a writable directory, this functionality is also required during drop processing, so it is implemented in a separate method that can be called from either place. The code for this method, called isAcceptableDropLocation, is shown in Listing 8-14. As you can see, this method itself delegates responsibility to another method called findTargetDirectory, which returns a File object for the directory that will be the target for the drop if, and only if, the three conditions listed above are met. The findTargetDirectory method itself is used in more than one place, which is why its code is not included directly into isAcceptableDropLocation.

Listing 8-14 Determining the Validity of the Drop Location
 protected boolean isAcceptableDropLocation(Point location) {    return findTargetDirectory(location) != null; } protected File findTargetDirectory(Point location)    { TreePath treePath =             tree.getPathForLocation(location.x, location.y);    if(treePath != null) {       FileTree.FileTreeNode node =    (FileTree.FileTreeNode)treePath.getLastPathComponent();       // Only allow a drop on a writable directory       if (node.isDir()) {          try {             File f = new File(node.getFullName());             if (f .canWrite()) {                return f;             }          } catch (Exception e) {          }       }    }    return null; } 
Drag-Under Feedback

Drag-under feedback is very important when the user is moving or copying files using drag-and-drop, because it is essential that the user can unambiguously see the drop location. In this example, we provide drag-under feedback by selecting the node in the tree that represents the current drop target; as the user drags the cursor over the tree, the node that would act as the drop target will appear selected only if it represents a directory to which the user has write access.

By using the selection in this way, however, we are creating a conflict with the tree itself, which may already have nodes selected. This is certainly the case if we attach both a drag source and a drop target to the same tree and the user drags files from one part of the tree to another, because the file or files being dragged will, of course, be selected. To remove all ambiguity, we need to deselect the nodes that are not under the mouse cursor while the drag is in progress and reselect them when the drag operation has completed. This means:

  1. When dragEnter is called, the current selection must be saved and all nodes in the tree must be deselected.

  2. As the user moves the cursor, the dragOver method must determine whether the node under the cursor represents a valid drop site and select it if it does.

  3. When dragExit is entered, the node for the drop target (if there is one) should be deselected and the nodes that were selected before dragEnter was called must be reselected.

The code that provides the drag-under feedback is shown in Listing 8-15.

Listing 8-15 Providing Drag-Under Feedback for the FileTree Drop Target
 protected void saveTreeSelection() {    selections = tree.getSelectionPaths();    leadSelection = tree.getLeadSelectionPath();    tree.clearSelection(); } protected void restoreTreeSelection() {    tree.setSelectionPaths(selections);    // Restore the lead selection    if (leadSelection != null) {       tree.removeSelectionPath(leadSelection);       tree.addSelectionPath(leadSelection);    } } protected void dragUnderFeedback(DropTargetDragEvent dtde,                                  boolean acceptedDrag) {    if (dtde != null && acceptedDrag) {       Point location = dtde.getLocation();       if (isAcceptableDropLocation(location)) {          tree.setSelectionRow(            tree.getRowForLocation(location.x, location.y));       } else {          tree.clearSelection();       }    } else {       tree.clearSelection();    } } ... TreePath[] selections;     // Initially selected rows TreePath leadSelection;    // Initial lead selection 

The saveTreeSelection method is called from dragEnter and uses JTree methods to get the list of selected TreePaths, which it saves for later restoration. It is important to also save the lead selection, which is obtained using the getLeadSelectionPath method. The lead selection is usually the last path to have been selected by the user and it is rendered with a highlighting border when the tree has the focus. Having saved all necessary selection information, the clearSelection method is called to deselect everything.

The restoreTreeSelection restores the original selection state by reselecting everything originally stored by saveTreeSelection, and then specifically reselecting the previous lead selection path. This code works even if nothing in the tree was selected when saveTreeSelection was called, because it explicitly checks for the lead selection being null and does nothing if it is.

When the cursor first enters the space occupied by the FileTree component, the dragEnter method will be called. This method calls saveTreeSelection. As a result, any existing selection will be saved and then cleared, so the selection can safely be manipulated while the cursor is still over the drop target. If the drag operation is canceled by the user (by pressing the ESCAPE key) or the cursor is dragged out of the FileTree, the dragExit method will be invoked, which will result in the original selections being restored by restoreTreeSelection. This also happens when the user commits to the drop in other words, when the drop targets drop method is entered, the selection will have been restored to the state it was in before the drag started.

The dragEnter, dragOver, dropActionChanged, and dragExit methods all call dragUnderFeedback. In all cases apart from dragExit, the first argument to this method is the DropTargetDragEvent that indicates where the mouse is relative to the FileTree. This argument is passed as null to dragExit, which results in the tree selection being cleared and no further action being taken. In the other cases, the mouse location is passed to the isAcceptableDropLocation method which works out whether the cursor is over a writable directory. If it is, the drop would succeed, so the path under the mouse is selected to indicate where the file (or files) being transferred would be moved or copied to by calling the setSelectionRow method; this has the effect of clearing any previous selection, so there will only ever be at most one node selected as the user drags the cursor over the drop target. If the cursor is not over a valid drop site, perhaps because it moved from a node representing a writable directory to one representing a file, the previous node must be deselected without the new one being selected; the clearSelection method is used to do this.

Performing the Drop

If the user commits to the drop operation while the cursor is over the FileTree, the drop method will be called. This method delegates most of its work to another method called dropFile, which deals with the details of transferring the files or directories that the user is dragging. However, it does carry out some housekeeping functions before and after performing the drop processing itself, as you can see from Listing 8-16.

Listing 8-16 The FileTreeDropTarget drop Method
 public void drop(DropTargetDropEvent dtde) {    DnDUtils.debugPrintln("DropTarget drop, drop action = "              + DnDUtils.showActions(dtde.getDropAction()));    // Check the drop action    if ((dtde.getDropAction() &              DnDConstants.ACTION_COPY_OR_MOVE) != 0) {       // Accept the drop and get the transfer data       dtde.acceptDrop(dtde.getDropAction());       Transferable transferable = dtde.getTransferable();       boolean dropSucceeded = false;       try {          tree.setCursor(Cursor.getPredefinedCursor(                         Cursor.WAIT_CURSOR));          // Save the user's selections          saveTreeSelection();          dropSucceeded = dropFile(dtde.getDropAction(),                         transferable, dtde.getLocation());          DnDUtils.debugPrintln("Drop completed, success: "                       + dropSucceeded);    } catch (Exception e) {         DnDUtils.debugPrintln("Exception while handling                                              drop " + e) ;    } finally {          tree.setCursor(Cursor.getDefaultCursor());          // Restore the user's selections          restoreTreeSelection ( );          dtde.dropComplete (dropSucceeded);       }    } else {       DnDUtils.debugPrintln ("Drop target rejected drop");       dtde.dropComplete (false);    } } 

The first thing that this method does is to verify that the drop operation is acceptable and then accepts it. Having accepted the drop, the getTransferable method can be called to get the Transferable, which will eventually be used by dropFile to get the list of files to be moved. As we said earlier, when the drop method is called, the dragExit method will already have been invoked, so the drag-under feedback will have been removed and the original tree selections restored. However, because the process of moving the affecting files may take a short time, we want to continue to provide feedback to the user, so the saveTreeSelection method is used to save (again) the user's own selection and then the file transfer is performed by calling the dropFile method.

Core Note

It would be nice to be able to avoid restoring the tree selection in the dragExit method only to remove it again while handling the drop, but there is no way for the drop target code to know whether dragExit is being called immediately prior to an invocation of drop or because the user has moved the drag cursor away from the FileTree. Because of this, it is not possible to perform any optimization.



When the transfer is complete, there are two things to do:

  • Restore the user's tree selection by calling restoreTreeSelection.

  • Report on the success of the drop operation, for the benefit of the drag source, by calling dropComplete.

Both of these operations must be performed whether the drop proceeds normally or fails. It is also important to invoke the dropComplete method even if, for some reason, the dropFile method throws an exception. Because of this, most of this method is wrapped in a try block, with the operations that must be performed at the end placed in the finally clause to ensure that they are completed no matter what the outcome of the drop.

Performing the Movement of Files

The last piece of the puzzle as far as interaction with the drag-and-drop subsystem is concerned, is the dropFile method, which will actually control the movement of the files and/or directories that the user dragged onto the FileTree. This method has several tasks to perform:

  • Obtaining the list of files to transfer.

  • Getting the target directory as a FileTreeNode and as a File object. The FileTreeNode will be used when updating the tree's visual representation, while the File object will be used when actually moving the data.

  • Highlighting the target directory in the tree to provide drag-under feedback to the user.

  • Moving the source files to the target directory.

The code for this method is shown in Listing 8-17.

Listing 8-17 Handling the Drop Operation for the FileTree Component
 // This method handles a drop for a list of files protected boolean dropFile(int action, Transferable              transferable, Point location)              throws IOException, UnsupportedFlavorException,              MalformedURLException {    List files = (List)transferable.getTransferData(                 DataFlavor.javaFileListFlavor);    TreePath treePath = tree.getPathForLocation(                 location.x, location.y);    File targetDirectory = findTargetDirectory(location);    if (treePath == null || targetDirectory == null) {        return false;    }    FileTree.FileTreeNode node =       (FileTree.FileTreeNode)treePath.getLastPathComponent();    // Highlight the drop location while we perform the drop    tree.setSelectionPath(treePath);    // Get File objects for all files being    // transferred, eliminating duplicates.    File[] fileList = getFileList(files);    // Don't overwrite files by default    copyOverExistingFiles = false;    // Copy or move each source object to the target    for (int i = 0; i < fileList.length; i++) {       File f = fileList[i];       if (f.isDirectory()) {          transferDirectory(action, f, targetDirectory,                            node);       } else {          try {             transferFile(action, fileList[i],                          targetDirectory, node);          } catch (IllegalStateException e) {             // Cancelled by user             return false;          }       }    }    return true; } 

Most of this code should be self-explanatory. The mouse location passed from the drop method is given to the JTree getPathForLocation method to get the TreePath for the directory into which the user wants to drop the files and is also passed to the findTargetDirectory method that we saw earlier to get a File object for the same directory. In practice, given all the checks that have been made while the drag was being carried out, neither of these methods should return null because the mouse should be over a node of the tree that represents a writable directory. However, a check is added here to ensure that both of the returned references are valid and, in the event of any problem, the dropFile method returns false to indicate that it failed. The values returned by these two methods give us everything we need to know about the directory that is the target of the drop operation.

The rest of this method is a loop that moves or copies one file or directory at a time. This, however, is not quite as simple as it sounds. To see what complications might arise, let's look at some typical cases. Consider the directory structure shown in Figure 8-18 and suppose that the user selects and drags the files c:\jdk1.2.1\COPYRIGHT, c:\jdk1.2.1\LICENSE, and c:\jdk1.2.1\README and drops them onto the directory c:\temp. When the dropFile method is entered, the List object returned from the Transferable getTransferData method will return File objects for each of these three files. Assuming that the user selected copy as the drag operation, the loop in the dropFile method needs to copy the COPYRIGHT, LICENSE, and README files directly into the target directory c:\temp. In principle, all it has to do is create the new files in the c:\temp directory and use the File objects to read the content of the source files and write it to the new files. When the copy is complete, if the operation chosen was ACTION_MOVE, the source file should be removed. This simple process works as long as the objects being dragged are files.

The situation is more complex when the user selects a directory, however. Copying a directory implies copying the directory itself and all the files and directories it contains. If the directory does contain subdirectories, this is a recursive operation that terminates when all the subdirectories have been processed. Furthermore, in the case of a move operation, after each directory's content has been copied, all its files and subdirectories must be deleted and the directory itself must be removed. You'll see the details of this later in this section.

The fact that handling directories is more complex than handling files is the reason why the loop in Listing 8-17 handles files and directories differently by calling a specific method for each. This, however, is not the only problem associated with copying directories. Returning to Figure 8-18, suppose the user selects and drags the following nodes from the FileTree:

  • c:\jdk1.2.1

  • c:\jdk1.2.1\COPYRIGHT

This, of course, is redundant, because dragging the node c:\jdk1.2.1 implies copying that directory and all its content, which includes the file c:\jdk1.2.1\COPYRlGHT. If we did nothing about this and simply copied the directory and then the COPYRIGHT file, we would be doing extra work. The problem would get worse if the user selected many files from a directory that was also selected, or if subdirectories with lots of files in them were selected but, beside the extra time taken, no real harm would be done. The real problem occurs when the user makes a redundant selection and then performs a move operation. In this case, when the directory c:\jdk1 .2.1 has been copied, it will be removed, so the next pass of the loop in the dropFile method will not find a c:\jdk1.2.1\COPYRlGHT file to operate on. This looks like an error but, of course, it is not. To avoid both this problem and the redundant copying operations, before starting its loop, dropFile calls the getFileList method, passing it the java.awt.List returned from the Transferable. This method turns the List into an array of File objects, which is easier to iterate through than a List; it also scans the list of files and directories that the user has selected and removes redundant entries such as the COPYRIGHT file in the case shown earlier.

Removing Redundant File and Directory Paths

The process of removing redundant items from the set of File objects returned from the Transferable is a relatively simple one, but it requires a reasonable amount of code to implement it. The task itself is easy enough in principle given two file system objects A and B, the path B is redundant if the absolute path names of A and B are the same or if the absolute path name of B starts with the absolute path name of A with the underlying file system's file separator character added to the end of it. Therefore, in the previous example, c:\jdk1.2.1\COPYRIGHT is redundant because it starts with the string c:\jdk1.2.l\, which is the other path name plus the file separator character. It is important to add the file separator to avoid false matches such as c:\temp and c:\tempfiles, which would otherwise meet the condition for c:\tempfiles to be considered redundant.

The difficulty arises because the File objects from the Transferable can theoretically be delivered in any order. In practice, they will probably appear in the order in which you see them in the tree, but you cannot rely on this. To find all the redundant paths, you have to check each one against every other one, testing the longer name against the shorter one. You can make this job easier by sorting the list of files alphabetically. This ensures that longer names appear after shorter ones, so you only have to compare each entry with the ones that follow it in the list. This is exactly what the getFileList method does, as you can see from Listing 8-18.

Listing 8-18 Removing Redundant Paths from the Set of Files Being Dragged
 // Get the list of files being transferred and // remove any duplicates. For example, if the // list contains /a/b/c and /a/b/c/d, the // second entry is removed. protected File[] getFileList(List files) {    int size = files.size();      // Get the files into an array for sorting      File[] f = new File[size];      Iterator iter = files.iterator();      int count = 0;      while (iter.hasNext()) {         f[count++] = (File)iter.next();      }      // Sort the files into alphabetical order      // based on pathnames.      Arrays.sort(f, new Comparator() {         public boolean equals(Object o1) {            return false;         }         public int compare(Object o1, Object o2) {            return ((File)o1).getAbsolutePath().compareTo(                       ((File)o2).getAbsolutePath());         }    });      // Remove duplicates, retaining the results in a Vector      Vector v = new Vector();      char separator = System.getProperty(                       "file.separator").charAt(0);   outer:      for (int i = f.length - 1 ; i >= 0; i--) {         String secondPath = f[i].getAbsolutePath();         int secondLength = secondPath.length();         for (int j = i - 1 ; j >= 0; j--) {            String firstPath = f[j].getAbsolutePath();            int firstLength = firstPath.length();            if (secondPath.startsWith(firstPath)              && firstLength != secondLength              && secondPath.charAt(firstLength) == separator){              continue outer;           }        }        v.add(f[i]);    }      // Copy the retained files into an array      f = new File[v.size()];      v.copyInto(f);      return f;   } 

The easiest way to sort objects in Java 2 is to put them into an array and then use the sort method of the Arrays class to do the sorting, so getFileList extracts the File objects from the List that it is passed and loads them into an array of the appropriate size. There are many overloaded variants of the Arrays sort method that take arrays of different kind of objects as their first argument, along with a Comparator. Comparator is an interface with two methods defined:

  • public boolean equals (Object o);

  • public int compare (Object o1, Object o2);

The first of these methods allows two Comparators to be compared, which is not of interest to us here. The second method compares two values and returns a negative if the first object is "less than" the second, zero if they are equal, and a positive integer if first object is "greater than" the second one. What this means, of course, depends on the type of the objects being compared and the context of the operation being performed. In terms of our example, the compare method is passed a pair of objects from the array that it is sorting which, in this case, will be File objects. We need to supply a Comparator that can sort File objects based on the alphabetical ordering of the absolute path name of the file system objects that they represent. Given a pair of these objects, we can extract the path names using the getAbsolutePath method and then use the String compareTo method to do the comparison. This method returns the correct value required by the definition of the compare method that is, for example, given the two paths

  • c:\jdk1.2.1

  • c:\jdk1.2.1\COPYRIGHT

the first is "less than" the second because it is a substring of it, so compareTo will return a negative value.

When the sort method completes, the list of Files will be sorted according to their absolute path names and all we need to do is go through the list from the end to the beginning (that is, backward) comparing the path name of each entry with those that precede it in the list, looking for entries that match some leading part of it. If we find such a path, the File object that we are currently working with must be redundant, so we do not need to keep it. If we reach the start of the list without finding a match, a copy of the File object is added to a Vector. Only File objects that do not have parent directories in the list will appear in this Vector. This operation is performed by the nested "for" loops in the code shown in Listing 8-18.

To see how this works in practice, suppose that the getFileList method were passed a List containing File objects for the following paths:

  • c:\jdk1.2.1\COPYRIGHT

  • c:\jdk1.1.8\src\javax

  • c:\jdk1.1.8\src\javax\swing

  • c:\jdk1.1.8\src

  • c:\temp\page1.html

  • c:\jdk1.2.1

The first step is to sort these entries according to path name, giving the following result:

  • c:\jdk1.1.8\src

  • c:\jdk1.1.8\src\javax

  • c:\jdk1.1.8\src\javax\swing

  • c:\jdk1.2.1\src

  • c:\jdk1.2.1\src\COPYRIGHT

  • c:\temp\page1.html

Now this list is processed from the bottom looking for redundancy. This process starts with c:\temp\page1.html. Clearly, none of the other match the beginning of this string, so this entry will be copied to the result Vector and will be one of the files involved in the drag operation. Next, we look at c:\jdk1.2.1\src\COPYRIGHT and compare it with the entries above it in the list. Here, redundancy is detected immediately, because the string before it, when the file separator character is added, becomes c:\jdk1.2.1\src\, which matches the beginning of the path name we are looking it. This causes c:\jdk1.2.l\src\CDPYRIGHT to be discarded, with no further checks required. On the other hand, c:\jdk1.2.l\src will be copied to the result Vector. Of the remaining three strings, the first two are redundant because c:\jdk1.1.8\src\javax\ matches the beginning of c:\jdk1.l.8\src\javax\swing, causing the latter to be discarded and similarly with c:\jdk1.1.8\src\javax itself, which is eliminated because of c:\jdk1.1.8\src. Thus, only the File objects with the following paths are copied to the results Vector:

  • c:\jdk1.1.8\src

  • c:\jdk1.2.1\src

  • c:\temp\page1.html

Finally, the Vector of File objects that remain is converted to an array and is returned to the caller.

Copying a Directory

Once the dropFile method has the correct list of file system objects involved in the drop, it loops through them copying or moving them. A move operation is different from a copy only in that the move requires the original object to be removed when the copy is complete, so in this section we'll deal mainly with the details of the copy operation. Each item in the array returned by getFileList is either a directory or an array. The copying in process for these two object types is very different, so we'll examine them separately, starting in this section with how directories are handled.

Suppose we need to copy the directory c:\a\b\c to the directory c:\temp. Obviously, the first thing to do is to create the target directory c:\temp\c, if it does not already exist. What next? Copying a directory implies copying all its content as well, so we need to get a list of the objects in the directory c:\a\b\c and copy those to c:\temp\c. Suppose that the directory c:\a\b\c contains the following objects:

  • A file called c:\a\b\c\README

  • A directory called c:\a\b\c\src

To copy the directory, we create a loop, handling one object from the directory at a time. The first pass of the loop will copy c : \a\b\c\README to c:\temp\c\README. This is a file copy operation, which will be covered in "Copying a File" and will call the same method that dropFile itself calls to copy files from its list of objects to be dragged. The second pass of the loop encounters a directory. Copying this directory involves creating the target node c:\temp\c\src and then copying the contents of c:\a\b\c\src into it. This is, of course, the same task as we are already describing, so it is performed by recursion. If this directory itself has subdirectories, there will be further levels of recursion until the deepest level of nesting is reached. The implementation of this whole process is shown in Listing 8-19.

Listing 8-19 Copying a Directory
 protected void transferDirectory(int action, File srcDir,              File targetDirectory,              FileTree.FileTreeNode targetNode) {    DnDUtils.debugPrintln(              (action == DnDConstants.ACTION_COPY ? "Copy" :              "Move") + " directory " +              srcDir.getAbsolutePathf) +              " to " + targetDirectory.getAbsolutePath());    //Do not copy a directory into itself or    //a subdirectory of itself.    File parentDir = targetDirectory;    while (parentDir != null) {       if (parentDir.equals(srcDir)) {          DnDUtils.debugPrintln("-- SUPPRESSED");          return;       }       parentDir = parentDir.getParentFile();    }    // Copy the directory itself, then its contents    // Create a File entry for the target    String name = srcDir.getName();    File newDir = new File(targetDirectory, name);    if (newDir.exists()) {       // Already exists - is it the same directory?       if (newDir.equals(srcDir)) {          // Exactly the same file - ignore          return;       }    } else {       // Directory does not exist - create it       if (newDir.mkdir() == false) {           // Failed to create - abandon this directory           JOptionPane.showMessageDialog(tree,               "Failed to create target directory\n " +               newDir.getAbsolutePath(),               "Directory creation Failed",               JOptionPane.ERROR_MESSAGE);            return;        }     }     // Add a node for the new directory     if (targetNode != null) {     targetNode = tree.addNode(targetNode, name);     }     // Now copy the directory content.     File[] files = srcDir.listFiles();     for (int i = 0; i < files.length; i++) {        File f = files[i];        if (f.isFile()) {           transferFile(action, f, newDir, targetNode);        } else if (f.isDirectory()) {           transferDirectory(action, f, newDir, targetNode);        }     }     // Remove the source directory after moving     if (action == DnDConstants.ACTION_MOVE &&        System.getProperty (                     "DnDExamples.allowRemove") != null) {        srcDir.delete ( );     } } 

As you can see, this method does broadly what was outlined earlier. There are, however, a couple of interesting points to note.

First, there is a check for the relationship between the source and target directories to make sure that the user is not trying to drag a directory "inside" itself. As an example of this would be trying to drag the directory c:\jdk1.2.1 into the subdirectory c:\jdk1.2.1\src, which is illegal because it is an operation that would recurse until the file system filled up. This is easy to check we just take the target directory and compare it with the source. If they don't match, we move to the parent of the target and compare that with the source, repeating this operation until a match occurs, in which case the operation will be abandoned, or we reach the root of the file system. In the case of this example, we would first compare c:\jdk1.2.1\src with c:\jdk1.2.1, which does not match, and then move to the parent of c:\jdk1.2.1\src, which is c:\jdk1.2.1, at which point we match with the target and abandon the operation.

Next, the target directory is created if it does not exist and a node for the new directory is added to the FileTree, which will make it visible to the user when the FileTree repaints itself. The FileTree itself provides the addNode method that is provided here. We're not going to describe how this method works in detail, except to say that it creates a new FileTreeNode for the new directory and adds it to the tree, causing an event that will make the tree repaint itself. You'll find a discussion of the topic of adding nodes to a TreeModel and arranging for the tree to update its screen display in Chapter 10 of Core Java Foundation Classes on the CD-ROM that accompanies this book.

The directory contents are copied using the loop that we described earlier, recursing if necessary to copy subdirectories. Finally, if the operation is ACTION_MOVE, the source directory is deleted. Deleting the original directory is actually conditional on the property DnDExamples.allowRemove being defined. As noted earlier, for the purposes of this example, you have to explicitly define this property when you run it to authorize removal of files or directories. This is a safeguard against accidents when running the test program.

There is also a subtlety surrounding the single line of code that deletes the source directory. In general, you can only delete a directory that is empty and this code makes no such check. In fact, the underlying platform does this for you. How do we know that the source directory is empty at this point? Consider an example in which we move the directory c:\jdk1.2.1 to another location. This directory contains several files and subdirectories, such as:

  • c:\jdk1.2.1\bin (directory)

  • c:\jdk1.2.1\include (directory)

  • c:\jdk1.2.1\jre (directory)

  • c:\jdk1.2.1\lib (directory)

  • c:\jdk1.2.1\COPYRIGHT (file)

  • c:\jdk1.2.1\License (file)

and so on. As this directory is processed by the transferDirectory method, it will invoke itself recursively to move the four subdirectories and will invoke the transferFile method (described in the next section) to move the two files. The last thing that these methods do when performing a move operation is, of course, to remove the original object. Therefore, when the c:\jdk1.2.1\bin directory move operation completes, it will have been removed. The same happens with the other three directories and the two files. After the loop shown in Listing 8-19 completes, therefore, the directory should be empty. If any errors occur, perhaps because the user is trying to move a directory that he does not have permission to remove, that directory will remain and the parent directory will not be empty, which will cause the attempt to remove it to fail. This is, of course, exactly what you would expect.

Copying a File

Copying a file from the drag source is, in principle, simply a case of creating a new, empty file at the target location, reading the content of the original file and writing it to the new one, and then closing both files. Finally, if the file is being moved, the original should be removed. That is, indeed, exactly what the FileTreeDropTarget transferFile method does but, as with copying directories, there are a few interesting points to look out for. The implementation is shown in Listing 8-20.

Listing 8-20 Copying a File
   // Copy or move a file   protected void transferFile(int action, File srcFile, File         targetDirectory, FileTree.FileTreeNode targetNode) {     DnDUtils.debugPrintln(        (action == DnDConstants.ACTION_COPY ? "Copy" :              "Move") + " file " +              srcFile.getAbsolutePath() +              " to " + targetDirectory.getAbsolutePath());     // Create a File entry for the target     String name = srcFile.getName();     File newFile = new File(targetDirectory, name);     if (newFile.exists()) {        // Already exists - is it the same file?        if (newFile.equals(srcFile)) {           // Exactly the same file - ignore           return;        }        // File of this name exists in this directory        if (copyOverExistingFiles == false) {           int res = JOptionPane.showOptionDialog(tree,               "A file called\n " + name +               "\nalready exists in the directory\n " +               targetDirectory.getAbsolutePath() +               "\nOverwrite it?", "File Exists",               JOptionPane.DEFAULT_OPTION,               JOptionPane.QUESTION_MESSAGE,               null, new String[] {"Yes", "Yes to All", "No",               "Cancel"}, No");            switch (res) {            case 1:// Yes to all               copyOverExistingFiles = true;            case O:// Yes               break;            case 2:// No               return;            default: // Cancel               throw new IllegalStateException("Cancelled");            }       }    } else {       // New file - create it       try {          newFile.createNewFile();       } catch (IOException e) {           JOptionPane.showMessageDialog(tree,                 "Failed to create new file\n " +                 newFile.getAbsolutePath(),                 "File Creation Failed",                 JOptionPane.ERROR_MESSAGE);           return;       }    }    // Copy the data and close file.    BufferedInputStream is = null;    BufferedOutputStream os = null;    try {       is = new BufferedInputStream(                             new FilelnputStream(srcFile));       os = new BufferedOutputStream(                             new FileOutputStream(newFile));       int size = 4096;       byte[] buffer = new byte[size];       int len;       while ((len = is.read(buffer, 0, size)) > 0) {          os.write(buffer, 0, len);       }    } catch (IOException e) {        JOptionPane.showMessageDialog(tree,             "Failed to copy file\n " + name + "\nto             directory\n " +             targetDirectory.getAbsolutePath(),             "File Copy Failed",             JOptionPane.ERROR_MESSAGE);        return;     } finally {        try {           if (is != null) {              is.close();           }           if (os != null) {              os.close() ;           }        } catch (IOException e) {        }     }     // Remove the source if this is a move operation.     if (action == DnDConstants.ACTION_MOVE &&        System.getProperty("DnDExamples.allowRemove")                                                   != null) {        srcFile.delete();     }     // Update the tree display      if (targetNode != null) {         tree.addNode(targetNode, name);      } } 

The first part of this method checks whether the target file already exists. If it doesn't, the File method createNewFile is used to create it and any problems encountered in doing so cause an error message to be displayed in a modal dialog and the operation to be aborted. If the file already exists, there is the possibility that the user is dragging the file onto itself by, for example, dragging the file c:\jdk1.2.1\COPYRIGHT into the directory c:\jdk1.2.1. If this is the case, there is nothing to do and trying to treat this as anything other than a special case by reading and writing the file to itself would be disastrous the first write to the file would lose any unread data!

If the file already exists, we prompt the user for confirmation that it is to be overwritten by posting a modal dialog. If the user is copying one directory tree over another and the target tree contains older versions of the files being copied from the source directory, it may be that there will be many file copy operations for which the target file will already exists. Because of this, we give the user the ability to agree that all such copies will go ahead, even if the files already exist. A typical example of the dialog that appears is shown in Figure 8-19.

Figure 8-19. Prompting for permission to overwrite existing files.
graphics/08fig19.gif

This dialog allows the user four possible options:

Yes The current file is copied. If another attempt is made to copy over an existing file, another dialog will appear.
Yes to All All attempts to copy over existing files are assumed to be approved without further prompting.
No This file should not be copied. Prompt again if another file to be copied already exists.
Cancel Do not copy this file or any other files. In other words, abort the entire drag operation from this point.

If the user replies Yes or Yes to All, the copy is performed, overwriting the original content of the file. The difference between Yes and Yes to All is that we do not want to prompt the user again if the latter is selected. To arrange for this, the instance variable copyOverExistingFiles is set to true in this case; if this variable is true when an existing file is being copied, the copy will take place without a dialog being displayed. This variable is initialized to false in the dropFile method, so permission to overwrite existing files extends only for the duration of a single drop operation.

Selecting No aborts the current file copy or move operation. This is simple to arrange the transferFile method just returns. Cancel is more difficult to implement. Here, the intention is to abort the current operation and to copy or move any other files involved in the operation that have not yet been processed. The problem with this is that the loop that handles the remaining files is outside the transferFile method, so it cannot be aborted directly by transferFile. To make this operation possible, the transferFile method throws an IllegalStateException when Cancel is selected, to notify dropFile that it should terminate its copying loop without processing any more files or directory, as shown in Listing 8-17.

 

 



Core Swing
Core Swing: Advanced Programming
ISBN: 0130832928
EAN: 2147483647
Year: 1999
Pages: 55
Authors: Kim Topley

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