Mega Code Archive

 
Categories / Java Tutorial / Swing
 

ScribblePane allows individual PolyLine lines to be selected, cut, copied, pasted, dragged, and dropped

/*  * 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.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;   } }