[ Team LiB ] Previous Section Next Section

14.6 Custom Data Transfer

Example 14-5 is a custom Swing component named TransferableScribblePane. It allows the user to scribble with the mouse and to select one scribble at a time. It defines cut( ), copy( ), and paste( ) methods that operate on the selected line, and also allows the selected line to be dragged and dropped within the component or into another instance of the component. The scribbles are transferred using the TransferablePolyLine class defined in Example 14-4, of course.

This example does not use the high-level Swing TransferHandler class, but instead relies on the underlying data transfer and drag-and-drop architecture of java.awt.datatransfer and java.awt.dnd. The code for performing drag-and-drop is substantially more complex than the code for cut-and-paste. This is because the asynchronous drag-and-drop model requires a number of distinct event listeners and their corresponding event objects. The key interfaces are DragGestureListener, which triggers a new drag, and DragSourceListener and DropTargetListener, which notify the source of a drag and the target of a drop of important events that occur during the drag-and-drop process. The example implements all three interfaces as anonymous inner classes. When studying the example, pay particular attention to the dragGestureRecognized( ) method of the DragSourceListener and the drop( ) method of the DropTargetListener. The first is where the drag is initiated, and the second is where the data transfer actually transpires.

To test this component, display two copies of it using the ShowBean program. Draw a line with the mouse, then select the line by shift-clicking on it. Next, drag and drop the selected line while holding down the Shift or Ctrl keys. Also, use the Commands menu of ShowBean to invoke the cut( ), copy( ), and paste( ) methods.

Example 14-5. TransferableScribblePane.java
package je3.datatransfer;
import java.awt.*;                  // Graphics, Rectangle, Stroke, etc.
import java.awt.datatransfer.*;     // Transferable, DataFlavor
import java.awt.dnd.*;              // Drag-and-drop listeners and events
import java.awt.event.*;            // Mouse events
import javax.swing.*;               // JComponent
import javax.swing.border.*;        // LineBorder and BevelBorder
import java.util.*;                 // ArrayList, etc.
import java.util.List;    // Explicit import to disambiguate from java.awt.List
import je3.graphics.PolyLine; // The Shape of scribbles

/**
 * This rewrite of ScribblePane allows individual PolyLine lines to be
 * selected, cut, copied, pasted, dragged, and dropped.
 */
public class TransferableScribblePane extends JComponent {
    List lines;             // The PolyLines that comprise this scribble
    PolyLine currentLine;   // The line currently being drawn
    PolyLine selectedLine;  // The line that is current selected 
    boolean canDragImage;   // Can we drag an image of the line?

    // Lines are 3 pixels wide, and the selected line is drawn dashed
    static Stroke stroke = new BasicStroke(3.0f);
    static Stroke selectedStroke = new BasicStroke(3, BasicStroke.CAP_BUTT,
                                                   BasicStroke.JOIN_ROUND, 0f, 
                                                   new float[  ] { 3f, 3f, },0f);

    // Different borders indicate receptivity to drops
    static Border normalBorder = new LineBorder(Color.black, 3);
    static Border canDropBorder = new BevelBorder(BevelBorder.LOWERED);

    // The constructor method
    public TransferableScribblePane( ) {
        setPreferredSize(new Dimension(450,200)); // We need a default size
        setBorder(normalBorder);                  // and a border.
        lines = new ArrayList( );  // Start with an empty list of lines

        // Register interest in mouse button and mouse motion events.
        enableEvents(AWTEvent.MOUSE_EVENT_MASK |
                     AWTEvent.MOUSE_MOTION_EVENT_MASK);

        // Enable drag-and-drop by specifying a listener that will be 
        // notified when a drag begins.  dragGestureListener is defined later.
        DragSource dragSource = DragSource.getDefaultDragSource( );
        dragSource.createDefaultDragGestureRecognizer(this,
                                     DnDConstants.ACTION_COPY_OR_MOVE,
                                                      dragGestureListener);

        // Enable drops on this component by registering a listener to 
        // be notified when something is dragged or dropped over us.
        this.setDropTarget(new DropTarget(this, dropTargetListener));

        // Check whether the system allows us to drag an image of the line
        canDragImage = dragSource.isDragImageSupported( );
    }

    /** We override this method to draw ourselves. */
    public void paintComponent(Graphics g) {
        // Let the superclass do its painting first
        super.paintComponent(g);

        // Make a copy of the Graphics context so we can modify it
        Graphics2D g2 = (Graphics2D) (g.create( )); 

        // Our superclass doesn't paint the background, so do this ourselves.
        g2.setColor(getBackground( ));
        g2.fillRect(0, 0, getWidth( ), getHeight( ));

        // Set the line width and color to use for the foreground
        g2.setStroke(stroke);
        g2.setColor(this.getForeground( ));

        // Now loop through the PolyLine shapes and draw them all
        int numlines = lines.size( );
        for(int i = 0; i < numlines; i++) {
            PolyLine line = (PolyLine)lines.get(i);
            if (line == selectedLine) {        // If it is the selected line
                g2.setStroke(selectedStroke);  // Set dash pattern
                g2.draw(line);                 // Draw the line
                g2.setStroke(stroke);          // Revert to solid lines
            }
            else g2.draw(line);  // Otherwise just draw the line
        }
    }

    /**
     * This method is called on mouse button events.  It begins a new line
     * or tries to select an existing line.
     */
    public void processMouseEvent(MouseEvent e) {
        if (e.getButton( ) == MouseEvent.BUTTON1) {         // Left mouse button
            if (e.getID( ) == MouseEvent.MOUSE_PRESSED) {   // Pressed down 
                if (e.isShiftDown( )) {                     // with Shift key
                    // If the shift key is down, try to select a line
                    int x = e.getX( );
                    int y = e.getY( );
                
                    // Loop through the lines, checking to see if we hit one
                    PolyLine selection = null;
                    int numlines = lines.size( );
                    for(int i = 0; i < numlines; i++) {
                        PolyLine line = (PolyLine)lines.get(i);
                        if (line.intersects(x-2, y-2, 4, 4)) {
                            selection = line;
                            e.consume( );
                            break;
                        }
                    }
                    // If we found an intersecting line, save it and repaint
                    if (selection != selectedLine) { // If selection changed
                        selectedLine = selection; // remember which is selected
                        repaint( );                // will make selection dashed
                    }
                }
                else if (!e.isControlDown( )) {   // no shift key or ctrl key
                    // Start a new line on mouse down without shift or ctrl
                    currentLine = new PolyLine(e.getX( ), e.getY( ));
                    lines.add(currentLine);
                    e.consume( );
                }
            }
            else if (e.getID( ) == MouseEvent.MOUSE_RELEASED) {// Left Button Up
                // End the line on mouse up
                if (currentLine != null) {
                    currentLine = null;
                    e.consume( );
                }
            }
        }

        // The superclass method dispatches to registered event listeners
        super.processMouseEvent(e);
    }

    /**
     * This method is called for mouse motion events.
     * We don't have to detect gestures that initiate a drag in this method.
     * That is the job of the DragGestureRecognizer we created in the 
     * constructor: it will notify the DragGestureListener defined below.
     */
    public void processMouseMotionEvent(MouseEvent e) {
        if (e.getID( ) == MouseEvent.MOUSE_DRAGGED &&     // If we're dragging
            currentLine != null) {                       // and a line exists
            currentLine.addSegment(e.getX( ), e.getY( ));  // Add a line segment
            e.consume( );                                 // Eat the event
            repaint( );                                   // Redisplay all lines
        }
        super.processMouseMotionEvent(e); // Invoke any listeners
    }

    /** Copy the selected line to the clipboard, then delete it */
    public void cut( ) {
        if (selectedLine == null) return; // Only works if a line is selected
        copy( );                           // Do a Copy operation...
        lines.remove(selectedLine);       // and then erase the selected line
        selectedLine = null;
        repaint( );                        // Repaint because a line was removed
    }

    /** Copy the selected line to the clipboard */
    public void copy( ) {
        if (selectedLine == null) return; // Only works if a line is selected
        // Get the system Clipboard object.
        Clipboard c = this.getToolkit( ).getSystemClipboard( );

        // Wrap the selected line in a TransferablePolyLine object
        // and pass it to the clipboard, with an object to receive notification
        // when some other application takes ownership of the clipboard
        c.setContents(new TransferablePolyLine((PolyLine)selectedLine.clone( )),
                      new ClipboardOwner( ) {
                         public void lostOwnership(Clipboard c,Transferable t){
                             // This method is called when something else
                             // is copied to the clipboard.  We could use it
                             // to deselect the selected line, if we wanted.
                         }
                      });
    }

    /** Get a PolyLine from the clipboard, if one exists, and display it */
    public void paste( ) {
        // Get the system Clipboard and ask for its Transferable contents
        Clipboard c = this.getToolkit( ).getSystemClipboard( ); 
        Transferable t = c.getContents(this);

        // See if we can extract a PolyLine from the Transferable object
        PolyLine line;
        try {
            line = (PolyLine)t.getTransferData(TransferablePolyLine.FLAVOR);
        }
        catch(Exception e) {  // UnsupportedFlavorException or IOException
            // If we get here, the clipboard doesn't hold a PolyLine we can use
            getToolkit( ).beep( );   // So beep to indicate the error
            return;
        }
        
        lines.add(line); // We got a line from the clipboard, so add it to list
        repaint( );       // And repaint to make the line appear
    }

    /** Erase all lines and repaint. */
    public void clear( ) {
        lines.clear( );
        repaint( );
    }

    /**
     * This DragGestureListener is notified when the user initiates a drag.
     * We passed it to the DragGestureRecognizer we created in the constructor.
     */
    public DragGestureListener dragGestureListener = new DragGestureListener( ){
            public void dragGestureRecognized(DragGestureEvent e) {
                // Don't start a drag if there isn't a selected line
                if (selectedLine == null) return;

                // Find out where the drag began
                MouseEvent trigger = (MouseEvent)e.getTriggerEvent( );
                int x = trigger.getX( );
                int y = trigger.getY( );

                // Don't do anything if the drag was not near the selected line
                if (!selectedLine.intersects(x-4, y-4, 8, 8)) return;

                // Make a copy of the selected line, adjust the copy so that
                // the point under the mouse is (0,0), and wrap the copy in a 
                // Tranferable wrapper.
                PolyLine copy = (PolyLine)selectedLine.clone( );
                copy.translate(-x, -y);
                Transferable t = new TransferablePolyLine(copy);
                
                // If the system allows custom images to be dragged, make
                // an image of the line on a transparent background
                Image dragImage = null;
                Point hotspot = null;
                if (canDragImage) {
                    Rectangle box = copy.getBounds( );
                    dragImage = createImage(box.width, box.height);
                    Graphics2D g = (Graphics2D)dragImage.getGraphics( );
                    g.setColor(new Color(0,0,0,0));  // transparent bg
                    g.fillRect(0, 0, box.width, box.height);
                    g.setColor(getForeground( ));
                    g.setStroke(selectedStroke);
                    g.translate(-box.x, -box.y);
                    g.draw(copy);
                    hotspot = new Point(-box.x, -box.y);
                    
                }
                
                // Now begin dragging the line, specifying the listener
                // object to receive notifications about the progress of
                // the operation.  Note: the startDrag( ) method is defined by
                // the event object, which is unusual.
                e.startDrag(null,       // Use default drag-and-drop cursors
                            dragImage,  // Use the image, if supported
                            hotspot,    // Ditto for the image hotspot
                            t,          // Drag this object
                            dragSourceListener); // Send notifications here
            }
        };

    /**
     * If this component is the source of a drag, then this DragSourceListener
     * will receive notifications about the progress of the drag.  The only
     * one we use here is dragDropEnd( ) which is called after a drop occurs.
     * We could use the other methods to change cursors or perform other
     * "drag over effects"
     */
    public DragSourceListener dragSourceListener = new DragSourceListener( ) {
            // Invoked when dragging stops
            public void dragDropEnd(DragSourceDropEvent e) {
                if (!e.getDropSuccess( )) return; // Ignore failed drops
                // If the drop was a move, then delete the selected line
                if (e.getDropAction( ) == DnDConstants.ACTION_MOVE) {
                    lines.remove(selectedLine);
                    selectedLine = null;
                    repaint( );
                }
            }
            // The following methods are unused here.  We could implement them
            // to change custom cursors or perform other "drag over effects".
            public void dragEnter(DragSourceDragEvent e) {  }
            public void dragExit(DragSourceEvent e) {  }
            public void dragOver(DragSourceDragEvent e) {  }
            public void dropActionChanged(DragSourceDragEvent e) {  }
        };

    /**
     * This DropTargetListener is notified when something is dragged over
     * this component.
     */
    public DropTargetListener dropTargetListener = new DropTargetListener( ) {
            // This method is called when something is dragged over us.
            // If we understand what is being dragged, then tell the system
            // we can accept it, and change our border to provide extra
            // "drag under" visual feedback to the user to indicate our
            // receptivity to a drop.
            public void dragEnter(DropTargetDragEvent e) {
                if (e.isDataFlavorSupported(TransferablePolyLine.FLAVOR)) {
                    e.acceptDrag(e.getDropAction( ));
                    setBorder(canDropBorder);
                }
            }

            // Revert to our normal border if the drag moves off us.
            public void dragExit(DropTargetEvent e) { setBorder(normalBorder); }
            // This method is called when something is dropped on us.
            public void drop(DropTargetDropEvent e) {
                // If a PolyLine is dropped, accept either a COPY or a MOVE
                if (e.isDataFlavorSupported(TransferablePolyLine.FLAVOR))
                    e.acceptDrop(e.getDropAction( ));
                else {  // Otherwise, reject the drop and return
                    e.rejectDrop( );
                    return;
                }

                // Get the dropped object and extract a PolyLine from it
                Transferable t = e.getTransferable( );
                PolyLine line;
                try {
                    line =
                       (PolyLine)t.getTransferData(TransferablePolyLine.FLAVOR);
                }
                catch(Exception ex) {  // UnsupportedFlavor or IOException
                    getToolkit( ).beep( );   // Something went wrong, so beep
                    e.dropComplete(false); // Tell the system we failed
                    return;
                }

                // Figure out where the drop occurred, and translate so the
                // point that was formerly (0,0) is now at that point.
                Point p = e.getLocation( );
                line.translate((float)p.getX( ), (float)p.getY( ));

                // Add the line to our list, and repaint 
                lines.add(line);
                repaint( );

                // Tell the system that we successfully completed the transfer.
                // This means it is safe for the initiating component to delete
                // its copy of the line
                e.dropComplete(true);
            }

            // We could provide additional drag under effects with this method.
            public void dragOver(DropTargetDragEvent e) {  }

            // If we used custom cursors, we would update them here.
            public void dropActionChanged(DropTargetDragEvent e) {  } 
        };
}
    [ Team LiB ] Previous Section Next Section