Using Table Editors

Java > Core SWING advanced programming > 8. DRAG-AND-DROP > Implementing a Drop Target

 

Implementing a Drop Target

Now that you've had an overview of the drag-and-drop subsystem and seen the major classes and how they work together, it's time to start looking at some real code. In this section, you'll see how to implement a working drop target that can be used with a JEditorPane. The first version of this example will be very rudimentary, but will show the basic features of a working drop target. In the sections that follow, we'll enhance this example by adding more facilities until the fourth version has everything you need to be able to drop files and text onto a JEditorPane including appropriate feedback to the user and the ability to scroll the JEditorPane until the required location for the drop comes into view.

Because at this point we don't have a drag source written in Java, we'll assume that you have suitable platform-native applications that can act in this role. On the Windows platform, for example, you can use the Windows Explorer as a source for files and either WordPad or Microsoft Word for dragging text.

A Simple Drop Target

The aim of our first example is to create a class that can be used as a drop target for a JEditorPane. Before we start looking at the code that implements the drop target support, there is an important choice that we need to make namely whether we should create a whole new component derived from JEditorPane with drop target support included, or whether this support should be part of a separate class that can be connected as required to existing JEditorPanes. How you make this choice is probably determined mainly by the environment that you are working in. If you build a new component derived from JEditorPane that automatically behaves as a drop target, you won't need to worry about this feature as you build your application just slot in your component and let it do its job. The flip side of this picture is what you should do if you already have an application to which you have been asked to add drag-and-drop support. If you're in that position, you might find it simpler to have the drop target functionality as a separate item and connect the drop target to each JEditorPane as required. The benefits of this are clearer if your application uses subclassed JEditorPanes: Because Java does not have multiple inheritance, if you have created your drop target as a subclass of JEditorPane, you have to do some extra, perhaps nontrivial, work if you want to add the same feature to a class that is already subclassed from JEditorPane. In this book, we take the view that it is generally more useful to implement the drop target support in a separate object that you can use in conjunction with a JEditorPane or any subclass of JEditorPane. If you prefer the other approach, you can always build your own JEditorPane subclass that incorporates the code that you see here by instantiating a drop target object in its constructor and connecting it to the JEditorPane.

The drop target that we're going to implement in this section will demonstrate how to interact with the drag-and-drop subsystem to allow a single file to be dragged from elsewhere on the desktop and dropped onto a JEditorPane, where it will be opened and displayed. You've already seen in this book how to arrange for a JEditorPane to open a file and you'll know that, if you can obtain a URL to describe the location of the file, the JEditorPane will work out for itself the file's content type and will install the correct Editor-Kit to display its content properly. In essence, then, all the drop target needs to do when the file is dropped on the JEditorPane is to get the location of the file as a URL and call the JEditorPane setPage method and that's exactly what the code shown in Listing 8-1 does. There is, of course, slightly more to it than that but, although it may appear that there is quite a lot of code to implement something that sounds so straightforward, most of it is pretty simple.

Listing 8-1 A Drop Target for a JEditorPane
 package AdvancedSwing.Chapter8; import javax.swing.*; import java.awt.*; import java.awt.event.*; import java.awt.datatransfer.*; import java.awt.dnd.* ; import java.io.*; import java.net.*; import java.util.List; public class EditorDropTarget implements DropTargetListener {    public EditorDropTarget(JEditorPane pane) {       this.pane = pane;       // Create the DropTarget and register       //it with the JEditorPane.       dropTarget = new DropTarget(pane,                    DnDConstants.ACTION_COPY_OR_MOVE,                    this, true, null);    }    // Implementation of the DropTargetListener interface    public void dragEnter(DropTargetDragEvent dtde) {       DnDUtils.debugPrintln("dragEnter, drop action = "                 + DnDUtils.showActions(dtde.getDropAction()));       // Get the type of object being transferred and       // determine whether it is appropriate.       checkTransferType(dtde);       // Accept or reject the drag.       acceptOrRejectDrag(dtde);    }    public void dragExit(DropTargetEvent dte) {       DnDUtils.debugPrintln("DropTarget dragExit");    }    public void dragOver(DropTargetDragEvent dtde) {       DnDUtils.debugPrintln("DropTarget dragOver,                     drop action = " + DnDUtils.showActions(                     dtde.getDropAction()));       // Accept or reject the drag       acceptOrRejectDrag(dtde);    }    public void dropActionChanged(DropTargetDragEvent dtde) {       DnDUtils.debugPrintln("DropTarget dropActionChanged,                     drop action = " + DnDUtils.showActions(                     dtde.getDropAction()));       // Accept or reject the drag       acceptOrRejectDrag(dtde);    }    public void drop(DropTargetDropEvent dtde) {       DnDUtils.debugPrintln("DropTarget drop, drop action = "                 + DnDUtils.showActions(dtde.getDropAction()));       // Check the 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();          try {             boolean result = dropFile(transferable);             dtde.dropComplete(result);             DnDUtils.debugPrintln("Drop completed,                                   success: " + result);          } catch (Exception e) {             DnDUtils.debugPrintln("Exception while handling                                   drop " + e);             dtde.dropComplete(false);             }          } else {            DnDUtils.debugPrintln("Drop target rejected drop");             dtde.rejectDrop();          }    }    // Internal methods start here    protected boolean acceptOrRejectDrag(                               DropTargetDragEvent dtde) {       int dropAction = dtde .getDropAction ();       int sourceActions = dtde.getSourceActions();       boolean acceptedDrag = false;       DnDUtils.debugPrintln("\tSource actions are " +                      DnDUtils.showActions(sourceActions) + ",                       drop action is " +                       DnDUtils.showActions(dropAction));       // Reject if the object being transferred       // or the operations available are not acceptable.       if (!acceptableType ||          (sourceActions &                   DnDConstants.ACTION_COPY_OR_MOVE) == 0) {           DnDUtils.debugPrintln("Drop target rejecting drag");           dtde.rejectDrag();       } else if ((dropAction & DnDConstants.ACTION_COPY_OR_MOVE)                                                         = = 0) {          // Not offering copy or move - suggest a copy          DnDUtils.debugPrintln("Drop target offering COPY");          dtde.acceptDrag(DnDConstants.ACTION_COPY);          acceptedDrag = true;       } else {          // Offering an acceptable operation: accept          DnDUtils.debugPrintln("Drop target accepting drag");          dtde.acceptDrag(dropAction);          acceptedDrag = true;       }       return acceptedDrag;    }    protected void checkTransferType(DropTargetDragEvent dtde) {       // Only accept a list of files       acceptableType =       dtde.isDataFlavorSupported(DataFlavor.javaFileListFlavor);       DnDUtils.debugPrintln("File type acceptable - " +                             acceptableType);    }    // This method handles a drop for a list of files    protected boolean dropFile(Transferable transferable)                 throws IOException, UnsupportedFlavorException,                 MalformedURLException {       List fileList = (List)transferable.getTransferData(                            DataFlavor.JavaFileListFlavor);       File transferFile = (File)fileList.get(0);       final URL transferURL = transferFile.toURL();       DnDUtils.debugPrintln("File URL is " + transferURL);       pane.setPage(transferURL);       return true;    }    public static void main(String[] args) {       final JFrame f = new JFrame("JEditor Pane Drop Target                                   Example 1");       JEditorPane pane = new JEditorPane();       // Add a drop target to the JEditorPane       EditorDropTarget target = new EditorDropTarget(pane);       f.addWindowListener(new WindowAdapter() {          public void windowClosing(WindowEvent evt) {             System.exit(0);          }       }) ;       f.getContentPane().add(new JScrollPane(pane),                              BorderLayout.CENTER);       f.setSize(500, 400);       f.setVisible(true);    }    protected JEditorPane pane;    protected DropTarget dropTarget;    protected boolean acceptableType; // Indicates whether                                      // data is acceptable } 

You can try out this example using the command

 java AdvancedSwing.Chapters.EditorDropTarget 

which creates a frame with an empty JEditorPane. You'll also need to start Windows Explorer (or, on Solaris, an equivalent graphical file display program such as dtfile) and select a file containing some plain text (such as a Java source file) or an HTML file. With the file selected, use whatever your platform recognizes as the gesture necessary to initiate a drag to drag the file over the JEditorPane and then drop it. In the case of Windows Explorer, you can use either the left of right mouse button to drag the file over the desktop; when you're ready to drop it on the JEditorPane, just release the mouse button. As you drag the file, you should get feedback from the cursor to indicate that a drag is in progress. What exactly you see depends not only on the operating system you are using, but also on whether the application thinks you are requesting a copy, move, or a link operation. The standard cursors that you'll see when using Windows are shown in Figure 8-5.

Figure 8-5. Drag cursors on the Windows platform.
graphics/08fig05.gif

In order from left to right, the leftmost three cursors are used when the user has selected a copy, move, or a link operation and the cursor is currently over a location (that is, over a drop target) that could perform that operation on the data selected from the drag source. The rightmost cursor, which looks like no-entry sign, is used when the cursor is not over a drop target or is over a drop target that has rejected the drop for some reason. The drag source is responsible for selecting the correct cursor at all times, based on feedback from the drag-and-drop subsystem. When the drag source is a native application, you can't control the cursor that is displayed but, if you write a drag source for a Java component, you have the choice of using the default cursors shown previously or using your own. You'll see how to use custom drag cursors later in this chapter.

Creating and Using the DropTarget

The EditorDropTarget class shown in this example provides all the code needed to implement a basic drop target for a JEditorPane. As you can see from the main method, all you need to do to add this functionality to an existing editor is to create an instance of it, passing the JEditorPane as a constructor argument:

 JEditorPane pane = new JEditorPane ( ); EditorDropTarget target = new EditorDropTarget (pane); 

The constructor creates a DropTarget object that is associated with the component that is passed to it, thus activating that component as a drop site within the drag-and-drop subsystem. In this simple example, the drop site is always active and will accept copy or move operations; in a more sophisticated implementation, you would probably make the acceptable actions a constructor parameter and you would want to arrange for the drop site to be active only when the editor is enabled. In later versions of this example, we'll add more sophisticated features.

Core Note

In this example, the connection between the Droptarget and the JEditorPane is made by passing the latter as a constructor argument when creating the DropTarget. Alternatively, you can set the drop target component either by calling the setcomponent method of DropTargetor by invoking the Component method setDropTarget, passing the DropTarget as the argument



Apart from instantiating a DropTarget object and connecting it to the JEditorPane, the only other thing you need to supply is an implementation of the DropTargetListener interface, a reference to which you pass when creating the DropTarget (refer to Figure 8-3 to see how these classes fit together). The DropTargetListener could be implemented as a separate class but, for simplicity, we implement the code as part of the EditorDropTarget class. This has the advantage of keeping all our code in one class, which makes life easier for the developer using EditorDropTarget. An alternative and equally acceptable approach would be to implement the DropTargetListener as an inner class of EditorDropTarget, but there seems to be little reason to do that. Implementing DropTargetListener requires us to supply code for the dragEnter, dragOver, dragExit, dropActionChanged, and drop methods that we described under "The Drop Target". These methods are all relatively simple and, in fact, because three of them are almost exclusively concerned with deciding whether the operation that the user is performing should be allowed, these three methods share a lot of common code.

The dragEnter Method

When the user drags the cursor over the JEditorPane, the first DropTargetListener method that gets called is dragEnter. The implementation of this method in this example looks like this:

 public void dragEnter (DropTargetDragEvent dtde) {    DnDUtils .debugPrintln ("dragEnter, drop action = "                 + DnDUtils.showActions(dtde.getDropAction ( )));    // Get the type of object being transferred and determine    // whether it is appropriate.    checkTransferType (dtde);    // Accept or reject the drag.    acceptOrRejectDrag (dtde); } 

The first method that dragEnter calls is a utility routine that prints debug output to the window from which the application was started. You can use this debugging information to see when the various DropTargetListener methods are invoked and what the result of calling each of them is. To enable the debugging, you need to define a property called DnDExamples.debug, which you can do by invoking the example like this:

 java -DDnDExamples.debug AdvancedSwing.Chapter8.EditorDropTarget 

You might find it useful to start the example program with debugging enabled and try dragging a file over the JEditorPane to become familiar with the sequence of events that are delivered to the DropTargetListener. You'll also see what happens when you do different things such as dragging the mouse away from the drop target without performing a drop, or pressing the ESC key, which cancels the drag, while the cursor is over the JEditorPane. Here's some sample output obtained in this way:

 dragEnter, drop action = Move File type acceptable - true Source actions are Copy Move Link, drop action is Move Drop target accepting drag DropTarget dragOver, drop action = Move Source actions are Copy Move Link, drop action is Move Drop target accepting drag DropTarget dragOver, drop action = Move Source actions are Copy Move Link, drop action is Move Drop target accepting drag DropTarget dragExit 

The start of this trace shows the dragEnter method being called, with the user's drop action being ACTION_MOVE; the drop action is obtained from the getDropAction method of the DropTargetDragEvent that is created by the drag-and-drop subsystem and passed to the drop target. A little further down the trace, you can see that the dragOver method was invoked twice as the cursor was moved over the JEditorPane, followed finally by dragExit, which ends the interaction between the JEditorPane drop target and the drag-and-drop subsystem for this drag. In fact, this call resulted from the ESC key being pressed which terminated the drag, but it is impossible to distinguish this from the case where the user simply drags the cursor out of the JEditorPane's screen space (and, of course, the drop target doesn't need to be able to tell these two cases apart).

Ultimately, the dragEnter method must decide whether to accept or reject the drag operation by invoking either the acceptDrag or rejectDrag method of DropTargetDropEvent, both of which are covered for the same methods in the DropTargetContext object associated with the drop. The dragEnter method decides which method to call based on:

  1. whether any of the DataFlavors offered by the drag source is acceptable, and

  2. whether the drag operation is acceptable to the drop target.

Because the first check is only really required in the dragEnter method while the second will also be made from dragOver, dropActionChanged and drop, the code that implements these two cases is separated into two separate methods, both of which the dragEnter method invokes (see Listing 8-1).

The DataFlavor check is performed in the checkTransferType method, which, in the case of this example, is very simple:

 protected void checkTransferType (DropTargetDragEvent dtde) {    // Only accept a list of files    acceptableType =       dtde.isDataFlavorSupported (DataFlavor.javaFileListFlavor);    DnDUtils.debugPrintln ("File type acceptable - " +                           acceptableType); } 

The drop target in this implementation is only going to allow us to drop a file onto the JEditorPane it won't permit you to drag text from another editor, although you will see how to allow this in the next section. To check whether a particular DataFlavor is available from the drag source, you call the DropTargetDragEvent method isDataFlavorSupported, passing it a reference to the DataFlavor type that you would like to use, in this case the well-known value DataFlavor.javaFileListFlavor. If this type is available, the drag source and the drop target have a compatible transfer format for the date. To record this fact, the instance variable acceptableType is set to true. On the other hand, if this flavor is not being offered by the drag source, acceptableType is set to false. Before returning, checkTransferType writes the result of the check to your output window if you started the program with debugging enabled, as you can see from the trace extract shown earlier.

Next, dragEnter invokes the acceptOrRejectDrag method, which actually determines whether the drag operation will be accepted or rejected. Here is how the acceptOrRejectDrag method is implemented:

 protected boolean acceptOrRejectDrag(DropTargetDragEvent dtde) {    int dropAction = dtde.getDropAction();    int sourceActions = dtde.getSourceActions();    boolean acceptedDrag = false;    DnDUtils.debugPrintln("\tSource actions are " +                       DnDUtils.showActions(sourceActions) +                       ", drop action is " +                       DnDUtils.showActions(dropAction));    // Reject if the object being transferred    // or the operations available are not acceptable.    if (!acceptableType ||       (sourceActions & DnDConstants.ACTION_COPY_OR_MOVE) == 0) {       DnDUtils.debugPrintln("Drop target rejecting drag");       dtde.rejectDrag();    } else if ((dropAction & DnDConstants.ACTION_COPY_OR_MOVE) ==                                                              0)  {       // Not offering copy or move - suggest a copy       DnDUtils.debugPrintln("Drop target offering COPY");       dtde.acceptDrag(DnDConstants.ACTION_COPY);       acceptedDrag = true;    } else {       // Offering an acceptable operation: accept       DnDUtils.debugPrintln("Drop target accepting drag");       dtde.acceptDrag(dropAction);       acceptedDrag = true;    }    return acceptedDrag; } 

The first step is to check whether the checkTransferType method found an acceptable transfer type; if it did not, rejectDrag is called immediately. This test is performed here rather than in checkTransferType itself because checkTransferType is called only once each time a drag operation moves over the drop target component, whereas acceptOrRejectDrag is invoked continuously while the drag is in progress. Because the data flavors offered cannot change once the drag has started, the result of the tests performed by checkTransferType will remain valid throughout the drag. Furthermore, this check must be made each time acceptOrRejectDrag is called because calling rejectDrag does not terminate the drag, or even prevent the other DropTargetListener methods being called; invoking rejectDrag once in dragEnter is therefore not sufficient.

Assuming that there is a compatible data transfer flavor, the next test is whether the drag source is prepared to carry out either a copy or a move operation. Note that this check involves the value returned by the getSourceActions method, which is also constant for the duration of a drag. Because this drop target will only perform a copy or move, if neither of these is available from the drag source, there is no possibility that the drop would succeed, so rejectDrag should be called. This test would result in the drag being the rejected if the drag source offers only a link operation.

At this point, we know that the drop could succeed because the drag source is prepared to transfer a file list and perform either a copy or move operation. The only remaining thing to check is the action implied by the user's current gesture, which is obtained from the DropTargetDropEvent getDropAction method. The drop would succeed, and should be accepted, if the user has requested either a copy or a move operation but if both the CTRL and SHIFT keys are pressed to indicate a link, the drop would not be accepted. Because we know that the drag source is prepared to perform either a copy or a move, both of which would be acceptable, if a link operation is requested, instead of calling acceptDrag with the currently selected operation, we instead call acceptDrag with argument ACTION_COPY, which expresses our preference to perform a copy instead.

Core Note

As noted earlier, calling acceptDrag with an operation that does not reflect the one in progress causes the cursor change to the "no entry" sign shown in Figure 8-5 (or its equivalent on your platform) if the drag source is a Java implementation, but may or may not have any effect in the case of a native drag source.



The last thing that acceptOrRejectDrag does is return true if it accepted the drag (whether or not it suggested an alternative operation) and false if it did not. If you look at the dragEnter method, however, you'll see that having done its job, it just returns when it receives control back from acceptOrRejectDrag, without inspecting the return value. In fact, nothing in this example uses this return value it's there only so that we can use it when we enhance this code to provide feedback from the drop target to the user later in this chapter.

Returning from the dragEnter method returns control to the drag-and-drop subsystem. There is no need to return any kind of status from the dragEnter method (and, indeed, there is no way to do so) the only requirement is that dragEnter invoke either acceptDrag or rejectDrag as appropriate.

The dragOver, dragExit, and dropActionChanged Methods

While the user continues to drag the cursor over the drop target component, the DropTargetListener dragOver method will be called in response to movement of the mouse. As with dragEnter, the main job of this method is simply to call either acceptDrag or rejectDrag depending on the parameters of the drag. The only difference between dragEnter and dragOver is that there is no need for the latter to examine the list of DataFlavors available from the drag source because this will not change, so the initial setting of the acceptableType instance variable will apply throughout the drag operation. To implement dragOver in this simple case, then, it is sufficient to simply call acceptOrRejectDrag again. In more complex cases, however, the implementation of dragOver might not be quite so simple. As an example, whereas the exact location of the cursor is not important when dragging a new file over a JEditorPane, this would not be the case if you were instead trying to copy or move a file by dragging it over a graphical file viewer such as the one shown in Figure 8-2. Furthermore, in addition to the checks made by acceptOrRejectDrag, if the drag cursor were placed over a node in the JTree that represents a directory to which the user does not have write access, you would probably want to call rejectDrag.

The same rationale applies to the dropActionChanged method here, there is the possibility that the new drop action will be more acceptable than the previous one or will not be acceptable at all. Either way, just calling acceptOrRejectDrag is still the correct thing to do. The dragExit method implementation in this example is extremely simple. As we'll see later in this chapter, you can use the dragExit method to remove any drag-under feedback effects that were applied during dragEnter or any of the other DropTargetListener methods. In this simple case, however, there is nothing more to do.

Transferring the Data the drop Method

In general, when the drop method is entered, you know two things:

  1. There is at least one DataFlavor supported by both the drag source and the drop target that could be used to transfer the data being dragged by the user.

  2. At least one of the source actions available from the drag source is acceptable to the drop target.

The first task of the drop method is to decide whether the drop will be accepted or rejected and to select the format in which the data will be transferred. Because there is a common data format between the drag source and the drop target, there is no need to recheck this, but it is necessary to ensure that the selected drop operation is acceptable. The drop method is passed as its only argument a DropTargetDropEvent, the methods of which you saw earlier in this chapter. This event, like DropTargetDragEvent, has a getDropAction method that returns the operation that the user last selected before the drop. If this operation is not compatible with the drop target, the drop should be rejected by calling the DropTargetDropEvent rejectDrop method. In this example, the drop target is prepared to perform a copy or a move operation so it checks that the value returned by getDropAction is one of these. For ease of reference, the implementation of the drop method is repeated here.

 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 ( );       try {          boolean result = dropFile(transferable);          dtde.dropComplete(result);          DnDUtils.debugPrintln("Drop completed, success: " +                                result);       } catch (Exception e) {          DnDUtils.debugPrintln("Exception while handling drop                                " + e) ;          dtde.dropComplete (false);       }    } else {       DnDUtils.debugPrintln ("Drop target rejected drop");       dtde.rejectDrop ( );    } } 

Before the data can be transferred, a reference to the Transferable that gives access to it must be obtained from the drag-and-drop subsystem via the getTransferable method of the DropTargetDropEvent. However, before you can call this method, you must accept the drop operation by calling acceptDrop. Notice that acceptDrop, unlike acceptDrag, has no arguments, because there is no scope at this late stage for offering an alternative operation if the one suggested by the user is not acceptable. Once you call acceptDrop, you are not committed to actually accepting the drop data or performing the requested operation on it, but you are obliged to call dropComplete before the drop method ends to inform the drag source that the drop has ended, successfully or otherwise.

In this example, we already know that the data that will be returned from the Transferable will be a list of File objects representing the files that were dragged by the user from the drag source, so the drop method immediately calls another method called dropFile, the code for which we'll examine shortly, to perform the drop. This method returns a boolean indicating whether the drop succeeded, which is used as the argument to the dropComplete method. If an exception occurs, dropComplete is invoked with argument false. In more complex cases, there will be more than one possible DataFlavor for the transfer and the drop method will need to select one and act appropriately. You'll see an example of this in the next section.

The dropFile method (which is part of the implementation of this example and not a drag-and-drop method) is very straightforward. To obtain the list of files from the Transferable, the getTransferData method is called with argument DataFlavor.javaFileListFlavor. Because we know that this flavor is available, it is safe to do this without further checks and to cast the returned Object to type java.util.List, which is the type used by DataFlavor.javaFileList:

 List fileList = (List)transferable.getTransferData (                 DataFlavor.javaFileListFlavor); File transferFile = (File) fileList.get (0); final URL transferURL = transferFile.toURL ( ); 

Because the user may have selected more than one file, the List may have more than one entry, but this code simply extracts the first item in the list. Once we have the file, we need to invoke the JEditorPane's setPage method to have it load and display the file content, for which we need a URL. Fortunately, the items of the List returned by getTransferData are of type java.io.File, and, as of Java 2, this class has a toURL method from which you can obtain a URL that refers to the same file as the File object itself. The last step is to pass this URL to the setPage method. Of course, you can only safely call Swing component methods from the AWT event thread. This, however, is not a problem, because the drag-and-drop subsystem always arranges to invoke the methods of all its listeners from the event thread. This means that you don't have to take any special steps to ensure thread safety when using drag-and-drop with Swing components.

Having called setPage, the dropFile method returns true to the drop method so that it can signal a successful drop to the drag source. Strictlyspeaking, of course, there is still a possibility that the JEditorPane will fail to open or display the file, but it is not practical to attempt to determine whether this will happen.

A Multi-Functional Drop Target

The example that you have just seen allows you to drag a single file from Windows Explorer onto a JEditorPane. Because there is only one possible transfer operation here, the code is fairly simple and demonstrates without too much complication how to implement a basic drop target. In the real world, however, drop targets are rarely as simple as this, and here and in the next two sections we'll complicate the code more and more by introducing extra features, while keeping the same overall code layout so that you can see more easily how the implementation changes as we enhance the functionality. The first change we're going to make is to enhance the drop target so that, as well as dragging an entire file onto the editor, you can also select text from another application and drag it directly into the JEditorPane's text flow. Adding this capability requires us to make several changes to the code, of which the most obvious are the following:

  • As well as accepting a complete file transfer using the Java file list DataFlavor, we have to be able to handle the transfer of blocks of text from an external source. Text, of course, uses a different DataFlavor from files, so we'll need to be able to enhance the checkTransferType method to accept a drop that offers one of the possible flavors for text as well as DataFlavor.javaFileListFlavor.

  • When the user completes a drop with text, we need to select the appropriate DataFlavor from the offered set, extract the text itself, and then paste it into the JEditorPane. As you'll see, this is a little more involved than arranging for a complete new file to be opened.

In the last example, although we implemented all the DropTargetListener methods, most of the code was actually contained in helper methods that dealt with checking the available data transfer flavors (checkTransferType), choosing whether to accept or reject a drag based on the user's selected action (acceptOrRejectDrag), and completing the drop by opening a new file in the JEditorPane (dropFile). Adding the ability to support a new DataFlavor affects only the first of these three methods and requires us to write an additional method that is similar to dropFile. It must handle text instead, while leaving the rest of the code almost unchanged. Because the changes are confined to well-defined areas of the original example, in this section we'll show only those methods that are affected by the enhancements that we're making; if you want to see all of the source code, you can find it on the CD-ROM that accompanies this book.

Text and Data Flavors

Before we look at the code for this example, we need to examine the way in which the drag-and-drop subsystem handles transferring text. Drag-and-drop relies entirely on the java.awt.datatransfer package for the means to describe the data that a drag source wishes to export and for transferring that data to the drop target. The mechanism that this package supplies is the DataFlavor class. Up to now, you have only seen one instance of this class in use, DataFlavor.javaFileListFlavor, which represents a list of File objects. The code in Listing 8-1 simply used DataFlavor.javaFileListFlavor as a constant, which is exactly what it is. Using this constant, you can easily find out whether the drag source can supply a list of files simply by passing it as an argument to the DropTargetDragEvent isDataFlavorSupported method:

 if (dtde.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) {    // Drag source can supply a file list } 

or by obtaining a list of available flavors from the DropTargetDragEvent getCurrentDataFlavors method and walking down the list looking for the correct type:

 DataFlavor[] flavors = dtde.getCurrentDataFlavors(); for (int i = 0; i < flavors.length; i++) {    if (flavors[i].equals(DataFlavor.javaFileListFlavor)) {       // File list flavor is available    } } 

If you want to find out whether the drag source supports the transfer of text, you would probably look for a similar constant in the DataFlavor class that looks like it might represent text and then write code like that shown earlier to see if the drag source supports the transfer of data in that flavor. In fact, there is such a constant (DataFlavor.plainTextFlavor), but there is actually a little more to managing the transfer of text than there is to presenting a list of files. The complication arises from the fact is that there isn't just one type of text, so a single DataFlavor constant just won't do.

Suppose you open a basic file editor such as the UNIX vi editor or the Windows Notepad program. These programs deal only in plain, unformatted text they don't concern themselves with presentation issues such as font, text color, subscripting, and so on. When you save a file created by one of these editors, you just get a simple sequence of bytes in which each byte maps to a single character in the file. There are, however, several more sophisticated forms of text in common use. Two that we've seen quite a lot of in this book are Hypertext Markup Language (HTML) and Rich Text Format (RTF). Both of these formats store not only the text itself, but also attributes that indicate how the text should be displayed and, possibly, graphics and hyperlink information as well. Now suppose that you start a Web browser and a plain text editor on your machine and load a Web page into the browser, select some text from the Web page, and copy it onto the system clipboard with a view to importing it into the text editor. When you copy the text onto the clipboard, the browser exports an object (the platform-specific equivalent of a Transferable) that carries with it information describing the formats in which the selected data can be made available. These formats actually depend on the Web browser that you are using. Internet Explorer 5, for example, can export text from a Web page as plain text, HTML, or enriched text. Assuming that your browser supported export in all these formats, what would happen when you attempt to paste the data from the clipboard into your text editor? Because the editor understands only simple plain text, it would naturally choose to receive the data in plain text format. The result of this, of course, would be that any formatting applied by the HTML from which the text was originally created will be lost and there is little point in the browser supplying any of this text when it is asked for the data in plain text form, because the HTML tags would just be interpreted as part of the text.

Suppose, on the other hand, that you wanted to paste the same selection into an HTML editor, which, of course, understands HTML as well as plain text. In this case, you would expect the receiving editor to ask for the selection in HTML format. This time, the browser would export the text and the HTML tags so that the editor could display it in the same form as the browser, or in the form of raw HTML for the user to edit directly.

Expressing this in terms of the java.awt.datatransfer package, there need to be different DataFlavor objects corresponding to the different types of text that are available. In fact, there are two constants in the DataFlavor class that represent text without formatting:

DataFlavor.plainTextFlavor Plain text encoded in Unicode and presented as an InputStream.
DataFlavor.stringFlavor Plain text encoded in Unicode and returned as a String object.

Although these are the only predefined text DataFlavors, it is possible to create others. The key to creating new ones is to realize that each DataFlavor is distinguished by three characteristics:

  1. The type of data that is represents.

  2. The class used to return the data when the getTransferData method of a Transferable is called, referred to as its representation class.

  3. A human-presentable name, which can be used for display purposes.

The data format is expressed as a multipurpose Internet mail extension (MIME) type and may include optional parameters such as the character set in which the data is encoded. The plainTextFlavor, for example, is described by the following MIME type:

 text/plain; charset=unicode 

and its representation class is java.io.InputStream. By contrast, the MIME type of stringFlavor is

 application/x-Java-serialized-object 

and its representation class is java.lang.String in other words, the getTransferData method of a Transferable that handles data of type stringFlavor would return the String directly to the caller.

Creating a new DataFlavor is simply a matter of invoking one of the DataFlavor constructors and passing the required values for these three attributes. As a developer working with drag-and-drop at the application level, you will need to create a new DataFlavor when you implement a drag source if you need to supply the data in a form for which there is no existing DataFlavor. Note that DataFlavor only describes the format and representation of data it does not actually contain the data itself. As a result, inventing a new DataFlavor does not require you to write the code that actually transfers data in that form that job belongs to the Transferable, which needs to be able to encapsulate the data in the form described by the DataFlavor's representation class and return an instance of that class when its getTransferData method is called.

When implementing a DropTarget, you don't need to be concerned about creating a DataFlavor your problem will be interpreting it to discover the format of the data that it contains. DataFlavor has several methods that you can use to do this:

  • public String getMimeType ( ) ;

  • public String getPrimaryType ( );

  • public String getSubType ( );

  • public Class getRepresentationClass ( );

  • public String getParameter (String paramName);

  • public boolean isMimeTypeEqual (String mimeType);

  • public boolean equals(Object o);

  • public boolean equals(String s);

The getMimeType method returns the complete MIME definition of the data type starting with the type and subtype, followed by any accompanying parameters; if you just want the primary type or the subtype, use getPrimaryType and getSubType instead. The getRepresentationClass method gives you a Class object for the type of object that will be returned by the getTransferData method of any Transferable when this DataFlavor is passed as the argument. This method is most often used when you need to carry out the data transfer implied by the drop operation, as you'll see later in this section. The values returned by these four methods for the built-in DataFlavor plainTextFlavor are as follows:

getMimeType Text /plain; class=java.io.Inputstream; charset=unicode
getPrimaryType Text
getSubType Plain
getRepresentationClass Java.io.InputStream

whereas the values returned when these methods are invoked against the DataFlavor. stringFlavor object are:

getMimeType Application/x-java-serialized-object ; class=java.lang.String
getPrimaryType application
getSubType x-java-serialized-object
GetRepresentationClass java.lang.String

The getParameter method returns the value of a specific parameter from the MIME description of the DataFlavor. This method is commonly used when the representation class of the data is java.io.Inputstream to get the character set used to encode the data, so that it can be properly converted to Unicode as it is read. Typical usage for this method would something like this:

 DataFlavor flavor = DataFlavor.plainTextFlavor; String charSet = flavor.getParameter ("charset"); 

In this case, the value returned would be Unicode. You'll see this method in use later in this section when we show the code that implements the transfer of text to the JEditorPane.

The last three methods in the earlier list are useful for comparing a DataFlavor to a MIME type or another DataFlavor. For example, if you want to know whether a specific DataFlavor is the same as the built-in DataFlavor.plainTextFlavor, you could use the following expression:

 flavor.equals (DataFlavor.plainTextFlavor) 

This expression evaluates to true if flavor has the same representation class as plainTextFlavor (that is, java.io.Inputstream) and the MIME types match according to the isMimeTypeEqual method. The isMimeTypeEqual method is a weaker test that does not require the representation class of the two DataFlavors to match. It returns true if the primary of the two DataFlavors are the same and either the subtypes match or one of them is a wildcard (represented by a "*"), so that for example, the MIME types text / plain and text /* match according to this method. Note that, because only the MIME type and subtype are taken into account, any optional parameters are ignored, so that DataFlavors with the following MIME descriptions:

 text/plain; charset="unicode" text/plain; charset="cp1251" 

would be considered equal, even though they do not use the same character encoding. We'll make use of this fact later in this section.

The two built-in DataFlavors that represent plain text aren't the only DataFlavors that you are likely to come across when attempting to drag text from a native platform application to a Java drop target. To see some of the other types of text available, type the following command:

 java -DDnDExamples.debug AdvancedSwing.Chapter8.EditorDropTarget2 

This command runs the example program whose code we'll be looking at shortly. In addition to allowing you to drop files onto the JEditorPane, this program also allows you to drag and drop text from a platform-native application. When debugging is enabled, as it is by the command line shown previously, the checkTransferType method prints the MIME types of all the DataFlavors that are offered by the drag source when the drop target's dragEnter method is called. To try this out, start an editor, type and select some text, and drag it over the JEditorPane. Using WordPad on the Windows platform, you should get a result something like this:

 Drop MIME type text/enriched; class=java.io.InputStream; charset=ascii                                                          is available Drop MIME type text/plain; class=java.io.Inputstream; charset=ascii                                                           is available 

Internet Explorer 5 offers an even larger set of transfer flavors if you select some text from a Web page that it's displaying. Here is some typical output:

 Drop MIME type text/enriched; class=java.io.InputStream; charset=ascii                                                           is available Drop MIME type text/html; class=java.io.Inputstream; charset=unicode                                                           is available Drop MIME type text/plain; class=Java.io.Inputstream; charset=ascii                                                           is available Drop MIME type text/plain; class=java.io.InputStream; charset=unicode                                                           is available 

As you might expect of a Web browser, IE5 is prepared to supply HTML, rich text, and plain text, depending on the capabilities of the receiving program. Notice that two different forms of plain text are offered, one encoded in Unicode, the other in ascii, whatever that means. There is actually no Java-supported character encoding called ascii in fact, if you tried to convert the data from an InputStream with data of this type by creating an InputstreamReader with this character encoding, you'll get an UnsupportedEncodingException. In fact, describing the encoding as ascii is some what misleading what it actually means is that the input stream from a Transferable using a DataFlavor with this MIME definition will use the platform's default encoding.

FlavorMap and the SystemFlavorMap

Of the DataFlavors in the set returned by IE5, only the last one looks like one of the built-in flavors that describe plain text although, in fact, even that is not actually DataFlavor.plainTextFlavor. Given that IE5 is a platform-native application, it can't create new DataFlavor objects, so where did these come from?

Core Note

The discussion that follows covers an advanced topic that you don't need a complete understanding of to make use of the drag-and-drop subsystem and the rest of this chapter does not depend on it. If you are not interested in how the set of DataFlavors that is available for a specific drag operation is obtained, you should skip forward to the next section.



The drag-and-drop subsystem is responsible for converting the data formats offered by native applications into DataFlavor objects that Java programs can understand. The native drag-and-drop mechanism has a platform-dependent way of describing data formats that is much like the DataFlavor scheme. When the Java VM is started, it creates a FlavorMap that maps the platform-native representation of the supported data transfer types from the local platform into the corresponding DataFlavor. This FlavorMap is used to map the types offered by the drag source when a drag operation begins into a list of DataFlavor objects. By default, all DragSources and DropTargets use this system-created FlavorMap, but you can supply your own if you want to change the mapping from platform type to DataFlavor. The constructor of the DragSource class provides a parameter that sets a new FlavorMap for that drag source, while DropTarget has both a constructor argument and a setFlavorMap method. The FlavorMap in use for a given drag operation can be obtained from the getFlavorMap method of the DragSource or the DropTarget objects that are managing it.

FlavorMap is actually an interface that defines two methods:

 public Map getFlavorsForNatives (String [ ] natives); public Map getNativesForFlavors (DataFlavor [ ] flavors); 

The drag-and-drop subsystem uses the first of these methods to convert the available platform data types into DataFlavor objects; application code can use the second to get the list of native data types from one or more DataFlavors (although this information is probably only of use to highly specialized applications with native code that interacts directly with the platform's drag-and-drop provider). Supplying null as the argument for either of these methods returns a Map of all the known values of that type, so that the call

 Map flavorMap = getFlavorsForNatives (null); 

gives a Map with the DataFlavors corresponding to all the native drag-and-drop data formats on the platform that the Java drag-and-drop subsystem supports.

The FlavorMap interface does not specify how the mapping that it describes is stored or how it is initially created. The system default Flavor-Map is an instance of the class SystemFlavorMap, which initializes itself from a text file called flavormap.properties that is stored in the jre/lib directory of the Java installation on your machine. This file simply maps platform-specific data type names to the MIME definitions that will be used to build the corresponding DataFlavor objects. Its content is, of course, platform-specific; here's what the Windows version of this file looks like:

 TEXT=text/plain; charset=ascii UNICODE\ TEXT=text/plain;charset=unicode HTML\ Format=text/html;charset=unicode Rich\ Text\ Format=text/enriched;charset=ascii HDROP=application/x-java-file-list;class=java.util.List 

You should recognize from this file the DataFlavors that were shown as being returned from IE5 when dragging text over a Java drop site and the standard DataFlavor that represents a list of files that we used in the first example in this chapter. You can extend the default SystemFlavorMap by creating another file in the same format as the one shown previously and setting the property AWT.DnD.flavorMapFileURL to a string URL which points to it. Entries from this file that have the same key as those from the system default file will overwrite the default entry, which allows you to redefine the MIME type returned for any of the standard types or to add new definitions.

Establishing a Transfer Format when Several DataFlavors Are Available

Now let's look at the code for our second drag-and-drop example. The main difference between this program and the one shown in Listing 8-1 is that we want to be able to allow the user to drag text from a native application (or any Java application that can act as a drag source and supply text) onto a JEditorPane, where it will be inserted in place of anything currently selected in the editor. As you've already seen in this section, dragging text onto a drop target can result in a choice of DataFlavors. In the most general implementation, you might want to make the choice of transfer flavor dependent on the type of data that the JEditorPane is currently displaying. As an example of this, suppose the text is being dragged from IE5, so that you have a choice of two different encodings of plain text, enriched text, and HTML. JEditorPane can, of course, display all these formats. If it currently has an HTML page loaded, you might prefer to transfer the text from the drag source in HTML format if it is available and accept plain text if it isn't. Similarly, when the JEditorPane has an RTF document installed, you would probably want to select enriched text in preference to plain text and, finally, if the document is itself plain text then you would naturally elect to accept plain text from the drag source.

Because you can easily obtain the content type of the document in the JEditorPane from its getContentType method, it is a relatively easy matter to work out the most appropriate transfer format given access to the list of flavors available from the drag source. However, for the purposes of this example, we are not going to attempt to select the best possible transfer format. The reason for this choice is to avoid the complexity that would be involved when actually transferring the data during the execution of the DropTargetListener drop method, which would overly complicate the implementation to the point that it would be difficult to see the most important details. In fact, to keep things simple, we're going to accept only three flavors DataFlavor.javaFileListFlavor (to preserve the ability to drop files), DataFlavor.stringFlavor, and DataFlavor.plainTextFlavor. In other words, we're always going to import either an entire file or a string selection in the form of plain text.

Core Note

The complexity involved in importing more general forms of text arises partly from the fact that neither JEditorPane nor the underlying EditorKit implementations offer a single method of importing data to an arbitrary location in the document that works equally well for plain text, RTF, and HTML. Because of this, the drop method would have to be implemented with special case code for all three text types. Showing this code would not, of course, help you to understand drag-and-drop.



In the previous example, we checked whether there was an acceptable DataFlavor in the checkTransferType method, which was invoked from the DropTargetListener dragEnter method. We keep the same structure for this example, but this time we need slightly more complicated code, as shown in Listing 8-2.

Listing 8-2 Working with Multiple Transfer Flavors
 protected void checkTransferType(DropTargetDragEvent dtde) {    // Accept a list of files, or data content that    // amounts to plain text or a Unicode text string    acceptableType = false;    draggingFile = false;    if (DnDUtils.isDebugEnabled()) {       DataFlavor[] flavors = dtde.getCurrentDataFlavors();       for (int i = 0; i < flavors.length; i++) {          DataFlavor flavor = flavors[i];          DnDUtils.debugPrintln("Drop MIME type "                   + flavor.getMimeType() + " is available");       }    }    if ((dtde.isDataFlavorSupported(                    DataFlavor.javaFileListFlavor)) {       acceptableType = true;       draggingFile = true;    } else if (dtde.isDataFlavorSupported(                   DataFlavor.plainTextFlavor)      || dtde.isDataFlavorSupported(DataFlavor.stringFlavor)) {      acceptableType = true;    }    DnDUtils.debugPrintln("File type acceptable - " +                           acceptableType); } 

Forgetting about the debugging code, this method is changed from the previous version shown in Listing 8-1 in two respects:

  1. As well as checking whether the user is dragging a file, it also allows for the possibility of plain text or a Unicode string. If any of these three flavors is available, it will set the instance variable acceptableType to true.

  2. In addition to determining whether there is a common transfer flavor, this method also initializes another instance variable called draggingFile, which it sets to true if DataFlavor.javaFile-ListFlavor is available and false if we are dragging text.

As with the previous example, the value of acceptableType is used in the decision to accept or reject the drag. The draggingFile variable is used to allow the DropTargetListener to use different criteria for accepting a drag or a drop depending on whether the user is dragging text or a file. You'll see why this is necessary shortly.

As before, the checkTransferType method is only called when the cursor moves into the space occupied by the drop target component and makes a once-and-for-all decision as to whether it is feasible to transfer the data offered by the current drag operation. The acceptOrRejectDrag method is again the one that determines whether the drag will be accepted or rejected. Like checkTransferType, there is a little extra complication in this method because of the need to support both importing of text and reading entire files. The modified version of this method is shown in Listing 8-3.

Listing 8-3 Determining Whether to Accept or Reject a Drag Operation
 protected boolean acceptOrRejectDrag(DropTargetDragEvent dtde) {    int dropAction = dtde.getDropAction();    int sourceActions = dtde.getSourceActions();    boolean acceptedDrag = false;    DnDUtils.debugPrintln("\tSource actions are " +              DnDUtils.showActions(sourceActions) +              ", drop action is " +              DnDUtils.showActions(dropAction));    // Reject if the object being transferred    // or the operations available are not acceptable    if (!acceptableType ||       (sourceActions & DnDConstants.ACTION_COPY_OR_MOVE) == 0) {        DnDUtils.debugPrintln("Drop target rejecting drag");        dtde.rejectDrag();   } else if (!draggingFile && !pane.isEditable()) {       // Can't drag text to a read-only JEditorPane       DnDUtils.debugPrintln("Drop target rejecting drag");       dtde.rejectDrag();    } else if ((dropAction & DnDConstants.ACTION_COPY_OR_MOVE) ==                                                             0) {       // Not offering copy or move - suggest a copy       DnDUtils.debugPrintln("Drop target offering COPY");       dtde.acceptDrag(DnDConstants.ACTION_COPY);       acceptedDrag = true;    } else {       // Offering an acceptable operation: accept       DnDUtils.debugPrintln("Drop target accepting drag");       dtde.acceptDrag(dropAction);       acceptedDrag = true;    }    return acceptedDrag; } 

The code that has been added to this method is highlighted in bold. The rationale behind this modification is simple. A JEditorPane can be in either an editable or noneditable state; when it is editable, the document that it is displaying can be modified by the user but, when it setEditable (false) is called, the document should be considered read-only. We didn't worry about this issue in our first example, because it is perfectly acceptable to open different documents in a read-only editor the read-only state only stops us changing the content of the document once it has been opened. Therefore, when dropping a file onto a JEditorPane, you don't need to check whether it is read-only. On the other hand, dragging text into an editor amounts to changing the content of the document and should be prohibited if the editor is read-only. The extra code that has been added to the acceptOrRejectDrag method will reject the drag if the editor is not editable and the user is not dragging a file onto it. The need to make this test conditional on whether the user is dragging a file is, of course, the reason the checkTransferType method sets the new instance variable draggingFile.

Core Note

Another issue that we have sidestepped here is whether the JEditorPane is enabled. Strictly speaking, if the editor is disabled, the drop should be rejected unconditionally. We'll attend to this detail in the next version of this example drop target.



Transferring Text from the Drag Source to the Drop Target

When the user finally commits to the drop, the DropTargetListener's drop method is called. In Listing 8-1, the drop method simply verified that the user's drop action was either a move or a copy, called acceptDrop, obtained a reference to the Transferable and then passed it to the helper method dropFile to actually open the file in the editor. Most of that code still applies in version of the example. The only difference is that now there is the possibility that the user is dragging text instead of a file. To cater for this, the drop method checks the draggingFile variable and calls the original dropFile method if it is true or the new dropContent method if it is not. Here's the affected part of the code, with the changes highlighted:

 try {    boolean result = false;   if (draggingFile) {          result = dropFile(transferable);   } else {          result = dropContent(transferable, dtde);   }    dtde.dropComplete(result);    DnDUtils.debugPrintln("Drop completed, success: " + result); } catch (Exception e) {    DnDUtils.debugPrintln("Exception while handling drop " + e);    dtde.dropComplete(false); } 

The actual transfer of text from the drag source to the drop target takes place in dropContent. The most important things that this method has to do are:

  1. Determine which flavor to use for the data transfer (either stringFlavor or plainTextFlavor).

  2. Obtain the data from the Transferable, converting it to Unicode if necessary.

  3. Insert the text into the JEditorPane.

The implementation of this method is shown in Listing 8-4.

The first thing that this method does is check that the JEditorPane is editable if it is not, it returns immediately without attempting to transfer any data. You may be wondering whether it is necessary to make this check here because the drag will have been rejected in the dragEnter and dragOver methods if the editor is not editable and, as we said earlier, if the last call to dragOver before the user initiated the drop calls rejectDrop, then the drop method will not be called at all.

Listing 8-4 Dropping Text onto a JEditorPane
 protected boolean dropContent(Transferable transferable,                               DropTargetDropEvent dtde) {    if (!pane.isEditable()) {       // Can't drop content on a read-only text control       return false;    }    try {       // Check for a match with the current content type       DataFlavor[] flavors = dtde.getCurrentDataFlavors();       DataFlavor selectedFlavor = null;       // Look for either plain text or a String.       for (int i = 0; i < flavors.length; i++) {         DataFlavor flavor = flavors[i];         if (flavor.equals(DataFlavor.plainTextFlavor)            || flavor.equals(DataFlavor.stringFlavor)) {            selectedFlavor = flavor;            break;         }    }         if (selectedFlavor == null) {            //No compatible flavor - should never happen            return false;         }         DnDUtils.debugPrintln("Selected flavor is " +                  selectedFlavor.getHumanPresentableName());         // Get the transferable and then obtain the data         Object data =               transferable.getTransferData(selectedFlavor);         DnDUtils.debugPrintln("Transfer data type is " +                  data.getClass().getName());         String insertData = null;         if (data instanceof InputStream) {         // Plain text flavor         String charSet =                   selectedFlavor.getParameter("charset");         InputStream is = (InputStream)data;         byte[] bytes = new byte[is.available()];         is.read(bytes);         try {            insertData = new String(bytes, charSet);         } catch (UnsupportedEncodingException e) {            // Use the platform default encoding            insertData = new String(bytes);         }      } else if (data instanceof String) {         // String flavor         insertData = (String)data;      }      if (insertData != null) {         int selectionStart = pane.getCaretPosition();         pane.replaceSelection(insertData);         pane.select(selectionStart,            selectionStart + insertData.length());         return true;      }      return false;    } catch (Exception e) {       return false;    } } 

This logic is almost flawless, except for the fact that the drag operation is performed asynchronously from the execution of the application itself. While the user drags the mouse over the drop target component, the application continues to execute and could, theoretically, change the state of the JEditorPane from editable to read-only after the last dragOver call but before drop is entered. If this happens, the last call to dragOver will invoke acceptDrop, so that drop will eventually be called, by which time the JEditorPane will have been switched to read-only mode. Of course, this is very unlikely to happen, but the window of opportunity does exist.

Having verified that the JEditorPane is in an appropriate state to import the data, the next step is to determine the transfer flavor. Because we are only going to accept stringFlavor or plainTextFlavor, this method gets the list of available DataFlavors from the DropTargetDropEvent and iterates through each flavor, looking for one that matches either stringFlavor or plainTextFlavor and uses whichever of these appears first in the list.

Core Note

This algorithm represents an arbitrary method of choice between stringFlavor and plainTextFlavor (and between various different types of plainTextFlavor if more than one is offered as is the case with the list of formats offered by IES that you saw earlier). If you prefer, you could change this loop to select stringFlavor in preference to plainTextFlavor if both are available from the drag source by traversing the list remembering any plain text flavor that you see as you go and stopping as soon as you find stringFlavor or at the end of the list. If you reach the end of the list, you would use the plain text flavor.



What may not be immediately obvious from the code is that there is a subtlety about the way in which we check for the availability of a plain text flavor. Here is the code that does this:

 if (flavor.equals(DataFlavor.plainTextFlavor)       || flavor.equals(DataFlavor.stringFlavor)) {    selectedFlavor = flavor;    break; } 

Notice that we use the equals operator to test whether a flavor from the available flavors list matches DataFlavor.plainTextFlavor. It would clearly be presumptuous to replace this by a test of this form:

 if (flavor == DataFlavor.plainTextFlavor) 

because there is no guarantee that the DataFlavor object that the drag-and-drop subsystem supplies in the list of flavors available from the drag source is exactly the same object as the constant DataFlavor.plainTextFlavor. That, however, is not the subtlety in this test. The fact of the matter is that the constant object DataFlavor.plainTextFlavor is a flavor that has the following properties:

  • It has primary MIME type text with subtype plain.

  • Its representation class is java.io.Inputstream.

  • Its MIME type has a charset parameter set to unicode, indicating that the data stream is encoded in Unicode.

These attributes match exactly those of the DataFlavor that will be created by default by the drag-and-drop subsystem for text from a native application that claims to supply Unicode text. However, many applications do not offer Unicode text they provide the data in the platform's default encoding. In this case, the drag-and-drop subsystem creates a DataFlavor which matches the first two attributes of plainTextFlavor, but with the charset parameter set to ascii instead of Unicode. Nevertheless, this still represents plain text and our drop target can handle it just as easily as if it were encoded in Unicode. Obviously we don't want to have to complicate the dropContent method by extracting the MIME type and subtype from the DataFlavors offered and explicitly checking to see if they are text and plain respectively, thereby ignoring the character encoding to match on any kind of plain text. Fortunately, we don't need to do this because, as we noted under "Text and Data Flavors", the DataFlavor equals operator performs exactly this check, ignoring the charset parameter, if it is present. Therefore, the test

 flavor.equals(DataFlavor.plainTextFlavor) 

evaluates to true if flavor is plain text in any encoding, which is exactly what we want here.

Having selected the appropriate transfer format, the next step is to obtain the data by invoking the getTransferData method of the Transferable returned by the drag-and-drop subsystem, passing it the DataFlavor that we have chosen to use. This method returns an Object, the type of which matches the representation class of the DataFlavor. To insert the data into the JEditorPane, we need to have it in the form of a String. If the data is being transferred as DataFlavor.stringFlavor, getTransferData returns it in the form of a String, so there is nothing more to do in this case. However, if we are using DataFlavor.plainTextFlavor, the getTransferData method will return a java.io.inputstream. To get the data in the form of a String, we need to read it from the Inputstream and convert it to Unicode. As you know, an Inputstream delivers a sequence of bytes with a particular encoding and the byte stream must be converted to Unicode using the appropriate character set converter. One way to do this is to create an InputStreamReader with the correct encoding and wrap it around the Inputstream. We've already seen several example of this in earlier chapters. Here, we use an alternative method provided by the String class, through the following constructor:

 public String (byte [ ] bytes, String encoding); 

which constructs a String from an array of bytes and an encoding name. As Listing 8-4 shows, the first step is to create a byte array of the appropriate size (using the Inputstream available method to find out how many bytes of data there are) and then read the data from the InputStream into the array. The remaining problem is to get the correct encoding. The encoding can be obtained by asking for the charset parameter of the DataFlavor's MIME type using the getParameter method, so the code should look something like this, assuming that the data has been read into an array called bytes:

 String charSet = selectedFlavor.getParameter ("charset"); String insertData = new String (bytes, charSet); 

However, there is a small problem with this. As we noted earlier, the default DataFlavor for plain text specifies the character set ascii, which is not a valid Java character encoding. Passing ascii to the String constructor will cause an UnsupportedEncodingException. However, this charset value actually means that the data is in the platform's default encoding, which could be handled automatically by creating a String using the byte array and without specifying an encoding:

 insertData = new String (bytes); // OK if "bytes" in                                  // platform default encoding 

Rather than explicitly checking for the value ascii, we simply allow the exception to be thrown and then revert to using this simpler constructor:

 String insertData; try {    String charSet = selectedFlavor.getParameter("charset");    String insertData = new String(bytes, charSet); } catch (UnsupportedEncodingException e) {    insertData = new String(bytes); } 

Incidentally, this method also works for those applications (such as IE5) that offer data in Unicode. In this case, the data is still read from an Input-Stream, in which each byte pair represents a Unicode character. Creating a String with Unicode as the character set will correctly convert such an Inputstream to Unicode characters, simply by assigning each pair of bytes to a Java char.

Finally, having obtained the transfer data as a String, we insert it into the JEditorPane using its replaceSelection method, which replaces whatever is selected with the new data, or inserts it at the location of the cursor if there is no selection. So that you can see exactly what has been dragged, the new text is highlighted by using the JTextComponent select method. In a production environment, you would almost certainly not want to do this.

You can try this example using the command

 java AdvancedSwing.Chapter8.EditorDropTarget2 

Once the program has started, drag a file from Windows Explorer onto it to show that the functionality of the first example is still available. Next, start a text editor or a Web browser, select some text from it and drag that over the JEditorPane. You'll find that the drag cursor indicates that a drop could be performed, provided you are not holding down both the CTRL and SHIFT keys to indicate a link operation. When you release the mouse buttons, the text will be transferred to the JEditorPane, as shown in Figure 8-6. Note that if you drag the text from a text editor and indicate a move operation, the text in the editor will be deleted after being dropped on the JEditorPane. Deleting the text is the job of the drag source; it should only be done if the drop target accepted the move operation and the subsequent transfer of data worked. This is why the DropTargetListener drop method must invoke dropCornplete with the appropriate success or failure indication.

Figure 8-6. Text dragged into JEditorPane.
graphics/08fig06.gif

Incidentally, this example incorporates a checkbox that you can use to switch the JEditorPane between editable and read-only modes. If you make the JEditorPane read-only by clearing this checkbox, you'll find that you can still drag a new file from Windows Explorer but, if you try to drag text, the drag cursor will indicate that the operation is not acceptable and an attempt to drop the text will be ignored.

Providing Drag-Under Feedback

As far as managing the transfer of files or text from an external application to a JEditorPane is concerned, our example drop target is now basically complete. However, we are not finished with it yet. In this section and the next, we're going to add a couple of features that make the drop target more useful from the user's point of view, starting with adding some feedback to indicate that the cursor is over a valid location for the drop. As you know, the drag cursor is automatically changed to indicate whether the currently selected operation is valid, but this isn't always sufficient. Although the drag-and-drop subsystem operates at the component level and therefore the drop target also has the same physical scope, it isn't always the case that the whole area occupied by a component should be considered valid as a drop site.

Consider the case of a JTree displaying a view of a file system, as shown in Figure 8-2. If the user wants to move a file from one location in the file system to another, it is usual to highlight the node of the tree that the file would be dropped into as the user moves the cursor over the view of the file system. When the cursor is not over (or at least near) a node representing a directory, there should be no visible highlighting, so that the user can see that a drop would not succeed. In a proper implementation, of course, in this case, rejectDrag would be called and the cursor would be changed, so that it will be clear to the user that the drop location is unacceptable.

Later in this chapter, you'll see the implementation of a drop target for a JTree and the code that provides the appropriate drop target or drag-under feedback, as it is usually called. In this section, we'll demonstrate how to implement drag-under feedback in the context of the JEditorPane. When it comes to implementing the drop target for the JTree, we'll place the code that triggers the feedback in the same place but the details will, of course, be different.

The JEditorPane drop target can now cope with two different types of data transfer. These two modes of operation are sufficiently different that they merit completely different kinds of drag-under feedback. What we'll implement is the following:

  • If the user is dragging a file, the entire JEditorPane is the drop target. In a case like this, it makes sense either to provide no drag-under feedback and to rely solely on the change of cursor shape to indicate to the user that the drop would succeed, or to make a change to the drop target's state that clearly indicates that the whole component would be affected by the drop. In this example, we'll provide the feedback by changing the background color of the editor when the cursor is over it and the drag operation is acceptable; and we'll revert to the original color when the operation is not acceptable, when the drop occurs, or when the user drags the file out of the bounds of the JEditorPane.

  • When text is being dragged, the feedback should reflect exactly what will happen when the text is dropped. In the previous version of this example, it was necessary to select the text in the JEditorPane that was to be replaced, or click with the mouse at the point at which the text was to be dropped. Because you can't do this while a drag is in progress, it was necessary to select the insertion point before the drag operation starts. This is not the most convenient interface for the user. In this example, we're going to improve on this by having the insertion point track the cursor as the user drags the text over the JEditorPane. The feedback to the user will be the movement of the caret through the text as the user performs the drag.

The feedback that we give to the user depends on the type of transfer being performed and whether the user's selected operation is acceptable. Because we want to move the editor's insertion caret to track the mouse if the user is dragging text, the drag-under feedback for this case also depends on the location of the mouse. In general, then, we need to be able to switch the feedback on and off when the drag enters the component, each time the mouse moves, when the user's selected operation changes, and when the drag operation leaves the component. To make it easier to replace the details of the feedback should you decide to subclass EditorDropTarget or, more likely, if you decide to use it as the basis for your own drop target, we'll implement the code that provides the feedback in a separate method and call it from dragEnter, dragOver, dropActionChanged, and dragExit. Listing 8-5 shows the code that provides the drag-under feedback for this example.

Listing 8-5 Drag-Under Feedback for a JEditorPane
 protected void dragUnderFeedback(DropTargetDragEvent dtde,                                  boolean acceptedDrag) {    if (draggingFile) {       // When dragging a file, change the background color       Color newColor = (dtde != null && acceptedDrag ?                         feedbackColor : backgroundColor);       if (newColor.equals(pane.getBackground ( ) ) == false) {           changingBackground = true;          pane.setBackground(newColor);          changingBackground = false;          pane.repaint ( );       }    } else {       if (dtde != null && acceptedDrag) {          // Dragging text - move the insertion cursor          Point location = dtde.getLocation ( );          pane.getCaret ( ).setVisible(true);          pane.setCaretPosition(pane.viewToModel (location) );       } else {          pane.getCaret ( ).setVisible (false);       }    } } 

The arguments supplied to the dragUnderFeedback method represent the information that we would need to pass to it in the most general case. Most of the information about the drag operation is made available to the DropTargetListener methods that will invoke dragUnderFeedback in the DropTargetDragEvent that they receive, including the location of the drag cursor relative to the drop target component and the user's current action. For this reason, we pass the DropTargetDragEvent directly to dragUnderFeedback. There is a small problem with this, however the dragExit event is passed a DropTargetEvent, not a DropTargetDragEvent. DropTargetEvent is the superclass of DropTargetDragEvent and carries much less state. Fortunately, in dragExit all we need to do is remove whatever drag-under feedback effects have been applied, which does not require context-dependent information available from a DropTargetDragEvent. Therefore, when dragUnderFeedback is invoked from dragExit, we'll pass null for the first argument. There is no issue with the drop method, which receives a DropTargetDropEvent, because you don't concern yourself with drag-under feedback in this method; in fact, by the time drop is called, dragExit will already have removed the drag-under effects. The second argument to this method indicates whether the drag operation in progress is acceptable to the drop target and is used to decide whether to show or remove the drag-under effects.

Core Note

Purists might prefer to pass a DropTargetEvent as the first argument to this method to avoid the use of a null reference for the special case of dragExit. The drawback with this approach is that it requires you to cast the DropTargetEvent to a DropTargetDragEvent if you want to get the cursor location or any other context-dependent information from it, which is likely to offend other purists.



The dragUnderFeedback method behaves differently depending on whether the object being dragged is a file or some text, which it can determine from the draggingFile instance variable. Providing feedback for file dragging is the simpler case here, all that is necessary is to set the JEditorPane's background color. If the drag has not been accepted or we are being called from dragExit, which is the case when the first argument is null, we need to revert to the component's original background color, which is obtained and stored in the drop target class' constructor. Otherwise, we set a (fixed) background color that indicates that the drop is valid. In the case of this example, the background is changed to gray. Note that before and after the setBackground call we toggle the state of an instance variable called changingBackground. You'll see later why this is necessary.

When the user is dragging text, a little more work is involved. Here, we want to move the insertion caret to the nearest valid location in the editor to the position of the mouse. To do that, we get the mouse location relative to the JEditorPane using the getLocation method of DropTargetDragEvent, and then use the JEditorPane viewToModel method to convert the mouse position to an offset within the editor pane's document. Finally, the caret is moved to this location using setCaretPosition. There is one other point to take note of. Swing text components hide the insertion caret when they don't have the focus, which means that the caret will not be visible during the drag operation. For this reason, before setting the caret location we make the caret visible; conversely, if we are removing the drag-under effect, we switch the caret off again.

With the implementation of the dragUnderFeedback method complete, the only other change is to call it at the appropriate times. As we said earlier, we need to call this method whenever the selected drop operation or the position of the cursor might have changed and when the drag operation is terminated. This means that we need to call it from dragEnter, dragOver, dropActionChanged, and dragExit. In all cases, this requires the addition of one line of code. Because nothing else in those methods needs to change for this example, we're not going to bother to reproduce the entire source listing here. Instead, for the purposes of illustration we'll show only the change to the dragExit method:

 public void dragExit(DropTargetEvent dte) {    DnDUtils.debugPrintln("DropTarget dragExit");    // Do drag-under feedback    dragUnderFeedback(null, false); } 

The dragExit method is, as noted earlier, a special case because it doesn't receive a DropTargetDragEvent, so the first argument passed to dragUnderFeedback here is null. The second argument is also fixed it's false to indicate that the drag has not been accepted. Both of these settings indicate to dragUnderFeedback that it should remove any drag-under effects previously applied.

You can try this example by typing the command:

 java AdvancedSwing.Chapter8.EditorDropTarget3 

and then dragging a file over the editor component. As you do so, the component's blank background will change from white to gray, as shown in Figure 8-7; if you drag the cursor out of the component without dropping the file, you'll see that it will revert to its original color. The same thing happens if you cancel the drag while the file is over the editor by pressing the ESCAPE key. If you now drop a file onto the editor, and then drag some text over it, you'll see that the insertion caret tracks the mouse as you move it over the component and, when you drop the text, it will be inserted where the caret last appeared. Because our drag-under feedback code moves the insertion caret as you move the mouse, there is no need to modify the dropContent method to have the text inserted at this location.

Figure 8-7. Drag-under feedback while dragging a file over JEditorPane.
graphics/08fig07.gif

Like the last example, this one has a checkbox that enables you to make the editor editable or read-only and it also has a second one that you can use to enable or disable the JEditorPane. The effect of making the editor readonly depends on the drag operation being performed that is, we do not allow text to be dragged onto a read-only editor, but it is acceptable to drag a file and open it in read-only mode. When the editor is disabled, however, it should not be possible to do either of these things. If you look back to the previous example, you'll see that we explicitly took into account whether the drop target component was editable when deciding to accept or reject both the drag and the drop operations. We could handle a disabled component by adding similar checks to the same methods, but there is a better way. The DropTarget object itself has an active attribute that you can set when it is created and subsequently change as required. If you make the DropTarget inactive, it behaves as if it is not a drop target at all. In other words, the DropTargetListener's dragEnter, dragOver, dragExit, dropAction-Changed, and drop methods will never be called for an inactive DropTarget. We take advantage of this by setting the DropTarget's active attribute in its constructor, as shown in Listing 8-6.

Listing 8-6 Setting the Active Attribute for a DropTarget
 public EditorDropTarget3(JEditorPane pane) {    this.pane = pane;    // Listen for changes in the enabled property    pane.addPropertyChangeListener(this);    // Save the JEditorPane's background color    backgroundColor = pane.getBackground();    // Create the DropTarget and register    // it with the JEditorPane.    dropTarget = new DropTarget(pane,       DnDConstants.ACTION_COPY_OR_MOVE,       this,      pane.isEnabled(), null); } 

Here, the DropTarget will be active if the editor is enabled and inactive if it is not. This, however, is not sufficient because the enabled state of the editor could change later and we need the active attribute of the DropTarget to track the change. Fortunately, the enabled state of a JComponent is exposed as a bound property, which allows us to receive notification of changes to it by registering a PropertyChangeListener, as you can see from Listing 8-6. The propertyChange method then simply reflects the state of the enabled attribute of the component in the active state of the DropTarget, as shown in Listing 8-7.

Listing 8-7 Tracking the Enabled and Background Attributes of a Component
 public void propertyChange(PropertyChangeEvent evt) {    String propertyName = evt.getPropertyName();    if (propertyName.equals("enabled")) {       // Enable the drop target if the JEditorPane is enabled       // and vice versa.       dropTarget.setActive(pane.isEnabled());    } else if (!changingBackground &&              propertyName.equals("background")) {        backgroundColor = pane.getBackground();    } } 

This method also serves another purpose. As noted earlier, to toggle the background color of the drop target component between its original color and the color needed for drag-under feedback, we store the initial background color as an instance variable of our drop target class. This, of course, is not sufficient because the program could change the background color after the original value has been saved. The solution for this problem is the same as for the DropTarget active state because the background color is also a bound property of the editor, so we can update the saved background color from the propertyChange method as the change is made. There is a catch, however, because the drag-under feedback causes the background color to change, which will result in propertyChange being invoked. If we did nothing about this, we would store the feedback color as the original background color. This is why the dragUnderFeedback method used the changingBackground instance variable to indicate that this background color change is to be ignored; when the background property changes, it is ignored if changingBackground is true. You can try this by clearing the enabled checkbox and then attempting to drag both text and a file onto the JEditorPane. In both cases, you'll find that the drag cursor will show that the operation is invalid, there will be no drag-under feedback, and dropping the file or text will be ignored.

Scrolling the Drop Target

The final enhancement that we're going to make to our drop target example will make it possible for the user to drag text to a location in the editor that is not actually visible when the drag operation begins. With the current implementation, as you drag text over the JEditorPane, the insertion caret tracks the mouse so that you can indicate exactly where you would like to drop the text. However, a JEditorPane is usually enclosed in a JScrollPane and, if there is enough text in the editor, at any given time a certain amount of it will not be visible. When this is the case, the user will try to drag the text in the direction of the part of the document where the insertion should take place by moving the mouse to the top, bottom, left, or right of the JScrollPane, in the expectation that this will cause the editor's content to be scrolled to bring the drop position into view. Unfortunately, although the drag-and-drop subsystem has autoscrolling built in, it does not work automatically you have to implement an interface called java.awt.dnd.Autoscroll through which you can be notified when the user's drag gesture might require to scroll the drop target component. It is your responsibility to decide the direction and distance of the scroll and to carry it out.

In the first three drop target examples in this chapter, to maximize reusability, the code was part of a separate class that could be used to provide the drop target functionality for any JEditorPane. unfortunately, autoscrolling is different and cannot easily be made part of a separate class in this way. Autoscrolling is supported by the DropTarget and is enabled only if the Component that you register via its constructor or its setComponent method directly implements the Autoscroll interface. In other words, an unmodifed JEditorPane cannot have autoscrolling switched on simply by using another class, which provides the autoscrolling capability, as an adapter as we have been doing in this chapter to add its drop target functionality. Therefore for this example, instead of modifying the EditorDropTarget class, we'll be developing a subclass of JEditorPane, called AutoScrollingEditorPane, which implements the Autoscroll interface, and using that instead of an ordinary JEditorPane. By doing this, we'll be able to keep the example simple enough so that we can concentrate on showing how the drag-and-drop mechanism interacts with the component to tell it when to scroll.

The Autoscroll interface has only two methods:

 public interface Autoscroll {    public Insets getAutoscrollInsets();    public void autoscroll(Point cursorLocn); } 

The idea is that when the drag cursor is close to the edge of the component, the autoscroll method is called to allow the component to be scrolled. The getAutoscrollInsets method is used to determine how close the cursor must be to the edge of the component before the DropTarget will call the autoscroll method; in fact, there are also other constraints that must be satisfied but, to avoid complicating things, we'll postpone looking at those until later. Let's first see how this works in practice by looking at an example. Type the following command:

 java AdvancedSwing.Chapter8.EditorDropTarget4 

and then drag a fairly large file onto the editor, such as its own source code from the file EditorDropTarget4.java, as shown in Figure 8-8.

Figure 8-8. Supporting drop target autoscroll.
graphics/08fig08.gif

With the source file displayed in the editor, select some text from another application and drag it over the editor. The drag cursor should indicate that the drop operation is valid. Now drag the text down toward the bottom of the visible area. As you reach the last line of the text, providing you don't drag the mouse too quickly, the content of the editor will begin to scroll upward, bring text from further down the file into view. This will continue to happen as long as you keep the drag cursor relatively motionless near the position that it occupied when scrolling commenced. If you drag the cursor up to the top of the editor, you'll find that the source file will scroll in the opposite direction. Similarly, dragging the text to the right will scroll the content a very small distance horizontally, an effect that can be reversed by dragging the text back over to the left. This is the autoscroll feature in action if you try to do this with the previous example (EditorDropTarget3), which does not have autoscroll suport, you'll find that the editor does not scroll when you drag text to its edges.

To trigger the scrolling action, you have to drag the text close to the edge of the editor's visible area. Let's look at the components involved in this example to see exactly how this works.

Figure 8-9 shows a JEditorPane wrapped in a JScrollPane. This is a rather unusual view of this arrangement; when you see this on the screen, only the part of the JEditorPane that falls within the viewing area of the JScrollPane is actually visible, whereas Figure 8-9 shows the whole JEditorPane, including the parts of it that are not in view. For most purposes, the JEditorPane neither knows nor cares that it is enclosed in a JScrollPane. When mouse events are reported to the JEditorPane, for example, the coordinates are given relative to the top left of the JEditorPane, rather than relative to the JScrollPane. When it comes to autoscrolling, this is a very important point. The user expects autoscrolling to be triggered when the mouse approaches the edge of the visible area of the scrolled area, which is the region shown darkly shaded in Figure 8-9, and the Autoscroll interface provides the getAutoscrollInsets method, which returns the insets that represent the critical area within which the autoscrolling operation should be performed.

Figure 8-9. The apparent insets that trigger drop target autoscrolling.
graphics/08fig09.gif

You might at first think that this would be the highlighted area shown in Figure 8-9, but that is not the case. The drag-and-drop mechanism works with respect to the drop target component, not the JScrollPane it is, in fact, unaware of the JScrollPane. Suppose your intended effect is for autoscrolling to happen when the mouse is within eight pixels of the boundary of the JScrollPane. The obvious thing to do would be to implement the getAutoscrollInsets method like this:

 public Insets getAutoscrollInsets() {    return new Insets (8, 8, 8, 8); } 

If you do this, however, the autoscrolling region you set up will be as shown in Figure 8-10.

Figure 8-10. Incorrect autoscrolling insets.
graphics/08fig10.gif

The eight-pixel insets returned from getAutoscrollInsets are applied to the JEditorPane itself, not to the visible area of the JScrollPane. This means that, in general, most or all of the autoscrolling region will be inaccessible to the user. In fact, the correct insets to return from getAutoscrollinsets are illustrated in Figure 8-11.

Figure 8-11. Correct autoscrolling insets.
graphics/08fig11.gif

As you can see, to calculate the correct insets, you need to take into account both the position of the origin of the JEditorPane relative to the viewable area of the JScrollPane and the size of the viewable area, as well as the apparent eight-pixel insets that you want the user to perceive. Because of this, the required insets change as the user scrolls the viewable area of the JEditorPane, because the scrolling action moves the location of the JScrollPane relative to the JEditorPane. Fortunately, the getAutoscrollinsets method is invoked each time the drag-and-drop subsystem tries to determine whether the mouse is within the autoscroll area and you can return different insets on each call if necessary.

Now let's look at the code for the AutoScrollingEditorPane class to see how the getAutoscrollInsets method is actually implemented; the code is shown in Listing 8-8.

Listing 8-8 A JEditorPane with Autoscrolling Support
 package AdvancedSwing.Chapter8; import javax.swing.*; import java.awt.*; import java.awt.dnd.*; public class AutoScrollingEditorPane extends JEditorPane                                         implements Autoscroll {    public static final Insets defaultScrollInsets =                                          new Insets(8, 8, 8, 8) ;    protected Insets scrollInsets = defaultScrollInsets;    public AutoScrollingEditorPane() {    }    public void setScrollInsets(Insets insets) {       this.scrollInsets = insets;    }    public Insets getScrollInsets() {       return scrollInsets;    }    // Implementation of Autoscroll interface    public Insets getAutoscrollInsets() {       Rectangle r = getVisibleRect();       Dimension size = getSize();       Insets i = new Insets(r.y + scrollInsets.top,              r.x + scrollInsets.left,              size.height - r.y - r.height + scrollInsets.bottom,              size.width - r.x - r.width + scrollInsets.right);       return i;    }    public void autoscroll(Point location) {       JScrollPane scroller =              (JScrollPane)SwingUtilities.getAncestorOfClass(              JScrollPane.class, this);       if (scroller != null) {           JScrollBar hBar = scroller.getHorizontalScrollBar();           JScrollBar vBar = scroller.getVerticalScrollBar();           Rectangle r = getVisibleRect();           if (location.x <= r.x + scrollInsets.left) {              // Need to scroll left              hBar.setValue(hBar.getValue() -                                       hBar.getUnitIncrement(-1));           }           if (location.y <= r.y + scrollInsets.top) {              // Need to scroll up              vBar.setValue(vBar.getValue() -                                       vBar.getUnitIncrement(-1));           }           if (location.x >= r.x + r.width - scrolllnsets.right) {              // Need to scroll right              hBar.setValue(hBar.getValue() +                                        hBar.getUnitIncrement(1));           }          if (location.y >= r.y + r.height - scrollInsets.bottom) {              // Need to scroll down              vBar.setValue(vBar.getValue() +                                        vBar.getUnitIncrement(1));           }       }    } } 

AutoScrollingEditorPane is a subclass of JEditorPane that has a default autoscroll insets of eight pixels on each side of the JScrollPane that it is wrapped in. The setScrollInsets method can be used to set a different value for this property on a per-component basis. The only other difference between this component and JEditorPane is the fact that it implements the Autoscroll interface. When you construct a DropTarget and pass it a component that implements Autoscroll, or use the DropTarget setComponent method to associate it with such a component, autoscrolling is automatically enabled and the getAutoscrollInsets and autoscroll methods of the Autoscroll interface will be called at the appropriate times.

Let's look first at the getAutoscrollInsets method, which must return an Insets object that corresponds to the darker shaded area of Figure 8-11. To calculate the correct insets, you need to know the coordinates of the visible area of the JEditorPane relative to its origin, which corresponds to the dark-outlined rectangle in Figure 8-11. You can obtain this from the JEditorPane getvisibleRect method, which returns a Rectangle describing the shape of the visible area. In terms of the variables used in the implementation of getAutoscrollInsets shown in Listing 8-8, the attributes of this Rectangle are illustrated in Figure 8-12.

Figure 8-12. Calculating autoscrolling insets.
graphics/08fig12.gif

In the implementation of getAutoscrollInsets, the variables used are as follows:

r A Rectangle describing the visible area of the JEditorPane. The x and y coordinates are measured relative to the origin of the JEditorPane and are shown as the values r.x and r.y in Figure 8-12. Similarly, the values r.width and r.height correspond to the width and height of the visible area and measure the size of the dark-outlined area in the diagram.
size A Dimension object that holds the actual size of the JEditorPane as returned by the getSize method. The size.width value, for example, gives the width of the JEditorPane, as indicated in Figure 8-12.
scrollInsets An Insets object that describes the apparent insets of the autoscroll area as seen by the user. Figure 8-12 shows the measurements represented by two instance variables of this object, scrollInsets.bottom and scrollInsets.right.As you can see, these inset values describe the autoscroll area by reference to the dark-outlined area, which represents the JScrollPane.

From the previous definitions and Figure 8-12, it is easy to see how to calculate the insets of the autoscroll area relative to the JEditorPane, which is what getAutoscrollInsets needs to return. The left inset, for example, is the sum of the horizontal distance from the left of the JEditorPane to the left side of the JScrollPane's viewing area (represented by the dark outline) and the left inset of the region represented by scrollInsets or, in other words

 r.x + scrollInsets.left 

Similarly, the top inset is the sum of the distance from the top of the JEditorPane to the top of the JScrollPane and the top inset of scrollinsets, that is

 r.y + scrollInsets.top 

These are exactly the values used in the implementation of the getAutoscrollinsets method in Listing 8-8. The right and bottom insets values are slightly more difficult to compute. The right inset is the distance from the right edge of the JEditorPane to the right edge of the area shown in the JScrollPane, plus the right inset of scrollInsets. From Figure 8-12, you can see that the distance to the right edge of the JEditorPane is given by size.width, while the distance to the right edge of the JScrollPane is (r.x + r.width), from which it follows that the right inset is

 (size.width - (r.x + r.width)) + scrollInsets.right 

which simplifies (on removal of the parentheses) to the value used in Listing 8-8. Similarly, the bottom inset is given by

 (size.height - (r.y.+ r.height)) + scrollInsets.bottom 

which again matches with the expression used in Listing 8-8.

The other method that you need to implement is the autoscroll method, which is responsible for actually moving the visible area of the JEditorPane within the window of the JScrollPane so that it scrolls in the direction in which the user is trying to drag the text to be dropped. The autoscroll method is passed only a single Point object that describes the location of the drag cursor, relative to the JEditorPane. Given only this single piece of information, the autoscroll method needs to determine:

  1. In which direction (or directions) the JEditorPane should be scrolled.

  2. By how much the JEditorPane should scroll, if at all.

The answer to the first of these questions can be expressed as follows:

  • If the drag cursor is near the top of the visible area, scroll the content downward.

  • If the drag cursor is near the bottom of the visible area, scroll the content upward.

  • If the drag cursor is near the left of the visible area, scroll the content to the right.

  • If the drag cursor is near the right of the visible area, scroll the content to the left.

These descriptions are all expressed in terms of the visible area of the JEditorPane, but the location that the autoscroll method is given is relative to the origin of the JEditorPane, not relative to its visible region. However, we know that it is within the autoscroll insets area, which is also expressed relative to the origin of the JEditorPane, so it is possible to compare the cursor position with the insets returned by getAutoscrollInsets. As an example, the drag cursor is near the top of the visible area if its y coordinate places it within the top inset of the dark shaded area in Figure 8-12, which, from the calculation used for the left inset returned by getAutoscrollInsets, is given by the value r.y + scrollInsets.top. In other words, we should scroll downward if the condition

 location.y <= r.y + scrollInsets.top 

is true. Similarly, the drag cursor is near the left of the visible area if its x coordinate is less than r.x + scrollInsets.right. The expressions used for determining whether the drag cursor is near the bottom or right of the visible area can similarly be deduced from the expressions used to compute the right and bottom insets in getAutoscrollInsets. Note that, if the drag cursor is in a corner of the visible area, it will be close enough to two sides to cause more than one of these conditions to be satisfied which will cause the JEditorPane to scroll both horizontally and vertically.

Having determined in which direction or directions the JEditorPane must be scrolled, the only remaining question is how far to scroll it. The obvious answer to this question is to scroll up and down by the height of one line of text, or left and right by the width of a character. However, while this could be made to work for JEditorPane, it is not a very generic solution it would be better if the autoscroll method could be made independent of such concepts as lines or characters of text, so that the same code could be reused for other components. So far, all the code that you have seen in connection with implementing autoscrolling depends only on the drop target component being a JComponent (because getVisibleRect is actually a JComponent method and so is not specific to JEditorPane). Fortunately this is quite easy to do. The JScrollBar component has two attributes that determine how far the component that it contains is moved when the user clicks on the scrollbar the unit increment and the page increment. The former corresponds most closely to the concepts of line and character for a text component. In fact, the Swing text components implement an interface called Scrollable that allows scrollbars to interrogate them to find out how far they should be scrolled each time a scrolling movement is necessary. This makes it possible for them to scroll by different amounts depending on the font in use, or even to continue to scroll a line at a time if different lines use different fonts and so require different scroll distances. By using the scrollbar getUnitIncrement method to obtain the scrolling distance, we allow the component itself to determine how far it will be scrolled, and we have a solution that will work for any drop target component, whether or not it implements the Scrollable interface.

Core Note

For a component that does not implement Scrollable, the scrolling distances can be set as properties of the scrollbars. See Core Java Foundation Classes (Prentice Hall) for a complete discussion of scrolling.



For convenience, the code for the autoscroll method is reproduced next. This method is, of course, an instance method of the JEditorPane and has no direct knowledge of the JScrollPane that the editor might be wrapped in. The first task, therefore, is to obtain a reference to that JScrollPane, which is done using the SwingUtilities getAncestorOfClass method. This method searches the component hierarchy of the component passed as its second argument to find an ancestor component of the class given as the first argument. The code shown here will find the first JScrollPane enclosing the AutoScrollingEditorPane. From here, the scrollbars themselves can be found using the getHorizontalScrollBar and getVerticalScrollBar methods.

 public void autoscroll (Point location) {    JScrollPane scroller =                 (JScrollPane) SwingUtilities.getAncestorOfClass (                 JScrollPane.class, this);    if (scroller != null) {       JScrollBar hBar = scroller.getHorizontalScrollBar ();       JScrollBar vBar = scroller.getVerticalScrollBar ();       Rectangle r = getVisibleRect ();       if (location.x <= r.x + scrollInsets.left) {          // Need to scroll left          hBar.setValue (hBar.getValue () -                         hBar.getUnitIncrement(-1));       }       if (location.y <= r.y + scrollInsets.top) {          // Need to scroll up          vBar.setValue(vBar.getValue() -                        vBar.getUnitIncrement(-1));       }       if (location.x >= r.x + r.width - scrollInsets.right) {          // Need to scroll right          hBar.setValue(hBar.getValue() +          hBar.getUnitIncrement(1));       }       if (location.y >= r.y + r.height - scrollInsets.bottom) {           // Need to scroll down.setValue(vBar.getValue() +                                        vBar.getUnitIncrement(1));       }    } } 

Once the scrollbars have been located, the code uses the getUnitIncrement method with argument +1 if it needs to scroll the JEditorPane right or down or -1 to scroll left or up. This method returns the amount by which the scrollbars value should be changed to move a distance of one unit in the requested direction. The returned value is, however, always positive and is either added to (to move right or down) or subtracted from (to move left or up) the current value of the scrollbar to properly adjust the view of the JEditorPane.

Note that the value +1 or -1 passed to the getUnitIncrement method does not determine the sign of the returned value, which is always zero or positive. Instead, it is used to allow the scrollbar to return a different value for movement in different directions. This might be required, for example, if the JScrollPane contained a Scrollable that manages a text component in which each line is a different height, so that moving up one line would require a movement by a different distance than would moving one line downward.

 

 



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