TransferableScribblePane.java Source code

Java tutorial

Introduction

Here is the source code for TransferableScribblePane.java

Source

/*
 * Copyright (c) 2004 David Flanagan.  All rights reserved.
 * This code is from the book Java Examples in a Nutshell, 3nd Edition.
 * It is provided AS-IS, WITHOUT ANY WARRANTY either expressed or implied.
 * You may study, use, and modify it for any non-commercial purpose,
 * including teaching and use in open-source projects.
 * You may distribute it non-commercially as long as you retain this notice.
 * For a commercial use license, or to purchase the book, 
 * please visit http://www.davidflanagan.com/javaexamples3.
 */

import java.awt.AWTEvent;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Shape;
import java.awt.Stroke;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.ClipboardOwner;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.awt.dnd.DnDConstants;
import java.awt.dnd.DragGestureEvent;
import java.awt.dnd.DragGestureListener;
import java.awt.dnd.DragSource;
import java.awt.dnd.DragSourceDragEvent;
import java.awt.dnd.DragSourceDropEvent;
import java.awt.dnd.DragSourceEvent;
import java.awt.dnd.DragSourceListener;
import java.awt.dnd.DropTarget;
import java.awt.dnd.DropTargetDragEvent;
import java.awt.dnd.DropTargetDropEvent;
import java.awt.dnd.DropTargetEvent;
import java.awt.dnd.DropTargetListener;
import java.awt.event.MouseEvent;
import java.awt.geom.AffineTransform;
import java.awt.geom.PathIterator;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.io.Externalizable;
import java.util.ArrayList;
import java.util.List;

import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.border.BevelBorder;
import javax.swing.border.Border;
import javax.swing.border.LineBorder;

/**
 * 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);

    public static void main(String args[]) {
        JFrame f = new JFrame("ColorDrag");
        f.getContentPane().setLayout(new FlowLayout());
        f.getContentPane().add(new TransferableScribblePane());
        f.getContentPane().add(new TransferableScribblePane());
        f.pack();
        f.setVisible(true);
    }

    // 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) {
        }
    };
}

/**
 * This Shape implementation represents a series of connected line segments. It
 * is like a Polygon, but is not closed. This class is used by the ScribblePane
 * class of the GUI chapter. It implements the Cloneable and Externalizable
 * interfaces so it can be used in the Drag-and-Drop examples in the Data
 * Transfer chapter.
 */
class PolyLine implements Shape, Cloneable, Externalizable {
    float x0, y0; // The starting point of the polyline.

    float[] coords; // The x and y coordinates of the end point of each line

    // segment packed into a single array for simplicity:
    // [x1,y1,x2,y2,...] Note that these are relative to x0,y0
    int numsegs; // How many line segments in this PolyLine

    // Coordinates of our bounding box, relative to (x0, y0);
    float xmin = 0f, xmax = 0f, ymin = 0f, ymax = 0f;

    // No arg constructor assumes an origin of (0,0)
    // A no-arg constructor is required for the Externalizable interface
    public PolyLine() {
        this(0f, 0f);
    }

    // The constructor.
    public PolyLine(float x0, float y0) {
        setOrigin(x0, y0); // Record the starting point.
        numsegs = 0; // Note that we have no line segments, so far
    }

    /** Set the origin of the PolyLine. Useful when moving it */
    public void setOrigin(float x0, float y0) {
        this.x0 = x0;
        this.y0 = y0;
    }

    /** Add dx and dy to the origin */
    public void translate(float dx, float dy) {
        this.x0 += dx;
        this.y0 += dy;
    }

    /**
     * Add a line segment to the PolyLine. Note that x and y are absolute
     * coordinates, even though the implementation stores them relative to x0, y0;
     */
    public void addSegment(float x, float y) {
        // Allocate or reallocate the coords[] array when necessary
        if (coords == null)
            coords = new float[32];
        if (numsegs * 2 >= coords.length) {
            float[] newcoords = new float[coords.length * 2];
            System.arraycopy(coords, 0, newcoords, 0, coords.length);
            coords = newcoords;
        }

        // Convert from absolute to relative coordinates
        x = x - x0;
        y = y - y0;

        // Store the data
        coords[numsegs * 2] = x;
        coords[numsegs * 2 + 1] = y;
        numsegs++;

        // Enlarge the bounding box, if necessary
        if (x > xmax)
            xmax = x;
        else if (x < xmin)
            xmin = x;
        if (y > ymax)
            ymax = y;
        else if (y < ymin)
            ymin = y;
    }

    /*------------------ The Shape Interface --------------------- */

    // Return floating-point bounding box
    public Rectangle2D getBounds2D() {
        return new Rectangle2D.Float(x0 + xmin, y0 + ymin, xmax - xmin, ymax - ymin);
    }

    // Return integer bounding box, rounded to outermost pixels.
    public Rectangle getBounds() {
        return new Rectangle((int) (x0 + xmin - 0.5f), // x0
                (int) (y0 + ymin - 0.5f), // y0
                (int) (xmax - xmin + 0.5f), // width
                (int) (ymax - ymin + 0.5f)); // height
    }

    // PolyLine shapes are open curves, with no interior.
    // The Shape interface says that open curves should be implicitly closed
    // for the purposes of insideness testing. For our purposes, however,
    // we define PolyLine shapes to have no interior, and the contains()
    // methods always return false.
    public boolean contains(Point2D p) {
        return false;
    }

    public boolean contains(Rectangle2D r) {
        return false;
    }

    public boolean contains(double x, double y) {
        return false;
    }

    public boolean contains(double x, double y, double w, double h) {
        return false;
    }

    // The intersects methods simply test whether any of the line segments
    // within a polyline intersects the given rectangle. Strictly speaking,
    // the Shape interface requires us to also check whether the rectangle
    // is entirely contained within the shape as well. But the contains()
    // methods for this class alwasy return false.
    // We might improve the efficiency of this method by first checking for
    // intersection with the overall bounding box to rule out cases that
    // aren't even close.
    public boolean intersects(Rectangle2D r) {
        if (numsegs < 1)
            return false;
        float lastx = x0, lasty = y0;
        for (int i = 0; i < numsegs; i++) { // loop through the segments
            float x = coords[i * 2] + x0;
            float y = coords[i * 2 + 1] + y0;
            // See if this line segment intersects the rectangle
            if (r.intersectsLine(x, y, lastx, lasty))
                return true;
            // Otherwise move on to the next segment
            lastx = x;
            lasty = y;
        }
        return false; // No line segment intersected the rectangle
    }

    // This variant method is just defined in terms of the last.
    public boolean intersects(double x, double y, double w, double h) {
        return intersects(new Rectangle2D.Double(x, y, w, h));
    }

    // This is the key to the Shape interface; it tells Java2D how to draw
    // the shape as a series of lines and curves. We use only lines
    public PathIterator getPathIterator(final AffineTransform transform) {
        return new PathIterator() {
            int curseg = -1; // current segment

            // Copy the current segment for thread-safety, so we don't
            // mess up of a segment is added while we're iterating
            int numsegs = PolyLine.this.numsegs;

            public boolean isDone() {
                return curseg >= numsegs;
            }

            public void next() {
                curseg++;
            }

            // Get coordinates and type of current segment as floats
            public int currentSegment(float[] data) {
                int segtype;
                if (curseg == -1) { // First time we're called
                    data[0] = x0; // Data is the origin point
                    data[1] = y0;
                    segtype = SEG_MOVETO; // Returned as a moveto segment
                } else { // Otherwise, the data is a segment endpoint
                    data[0] = x0 + coords[curseg * 2];
                    data[1] = y0 + coords[curseg * 2 + 1];
                    segtype = SEG_LINETO; // Returned as a lineto segment
                }
                // If a tranform was specified, transform point in place
                if (transform != null)
                    transform.transform(data, 0, data, 0, 1);
                return segtype;
            }

            // Same as last method, but use doubles
            public int currentSegment(double[] data) {
                int segtype;
                if (curseg == -1) {
                    data[0] = x0;
                    data[1] = y0;
                    segtype = SEG_MOVETO;
                } else {
                    data[0] = x0 + coords[curseg * 2];
                    data[1] = y0 + coords[curseg * 2 + 1];
                    segtype = SEG_LINETO;
                }
                if (transform != null)
                    transform.transform(data, 0, data, 0, 1);
                return segtype;
            }

            // This only matters for closed shapes
            public int getWindingRule() {
                return WIND_NON_ZERO;
            }
        };
    }

    // PolyLines never contain curves, so we can ignore the flatness limit
    // and implement this method in terms of the one above.
    public PathIterator getPathIterator(AffineTransform at, double flatness) {
        return getPathIterator(at);
    }

    /*------------------ Externalizable --------------------- */

    /**
     * The following two methods implement the Externalizable interface. We use
     * Externalizable instead of Seralizable so we have full control over the data
     * format, and only write out the defined coordinates
     */
    public void writeExternal(java.io.ObjectOutput out) throws java.io.IOException {
        out.writeFloat(x0);
        out.writeFloat(y0);
        out.writeInt(numsegs);
        for (int i = 0; i < numsegs * 2; i++)
            out.writeFloat(coords[i]);
    }

    public void readExternal(java.io.ObjectInput in) throws java.io.IOException, ClassNotFoundException {
        this.x0 = in.readFloat();
        this.y0 = in.readFloat();
        this.numsegs = in.readInt();
        this.coords = new float[numsegs * 2];
        for (int i = 0; i < numsegs * 2; i++)
            coords[i] = in.readFloat();
    }

    /*------------------ Cloneable --------------------- */

    /**
     * Override the Object.clone() method so that the array gets cloned, too.
     */
    public Object clone() {
        try {
            PolyLine copy = (PolyLine) super.clone();
            if (coords != null)
                copy.coords = (float[]) this.coords.clone();
            return copy;
        } catch (CloneNotSupportedException e) {
            throw new AssertionError(); // This should never happen
        }
    }
}

/*
 * Copyright (c) 2004 David Flanagan. All rights reserved. This code is from the
 * book Java Examples in a Nutshell, 3nd Edition. It is provided AS-IS, WITHOUT
 * ANY WARRANTY either expressed or implied. You may study, use, and modify it
 * for any non-commercial purpose, including teaching and use in open-source
 * projects. You may distribute it non-commercially as long as you retain this
 * notice. For a commercial use license, or to purchase the book, please visit
 * http://www.davidflanagan.com/javaexamples3.
 */

/**
 * This class implements the Transferable interface for PolyLine objects. It
 * also defines a DataFlavor used to describe this data type.
 */
class TransferablePolyLine implements Transferable {
    public static DataFlavor FLAVOR = new DataFlavor(PolyLine.class, "PolyLine");

    static DataFlavor[] FLAVORS = new DataFlavor[] { FLAVOR };

    PolyLine line; // This is the PolyLine we wrap.

    public TransferablePolyLine(PolyLine line) {
        this.line = line;
    }

    /** Return the supported flavor */
    public DataFlavor[] getTransferDataFlavors() {
        return FLAVORS;
    }

    /** Check for the one flavor we support */
    public boolean isDataFlavorSupported(DataFlavor f) {
        return f.equals(FLAVOR);
    }

    /** Return the wrapped PolyLine, if the flavor is right */
    public Object getTransferData(DataFlavor f) throws UnsupportedFlavorException {
        if (!f.equals(FLAVOR))
            throw new UnsupportedFlavorException(f);
        return line;
    }
}