Java tutorial
/* * Copyright (c) 2000 David Flanagan. All rights reserved. * This code is from the book Java Examples in a Nutshell, 2nd 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. * You may distribute it non-commercially as long as you retain this notice. * For a commercial use license, or to purchase the book (recommended), * visit http://www.davidflanagan.com/javaexamples2. */ import java.awt.BasicStroke; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Cursor; 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.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.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.awt.event.MouseMotionListener; import java.awt.geom.AffineTransform; import java.awt.geom.PathIterator; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.io.Serializable; import java.util.ArrayList; import java.util.StringTokenizer; import javax.swing.ButtonGroup; import javax.swing.JComponent; import javax.swing.JFrame; import javax.swing.JToggleButton; import javax.swing.JToolBar; import javax.swing.border.BevelBorder; import javax.swing.border.Border; /** * This component can operate in two modes. In "draw mode", it allows the user * to scribble with the mouse. In "drag mode", it allows the user to drag * scribbles with the mouse. Regardless of the mode, it always allows scribbles * to be dropped on it from other applications. */ public class ScribbleDragAndDrop extends JComponent implements DragGestureListener, // For recognizing the start of drags DragSourceListener, // For processing drag source events DropTargetListener, // For processing drop target events MouseListener, // For processing mouse clicks MouseMotionListener // For processing mouse drags { ArrayList scribbles = new ArrayList(); // A list of Scribbles to draw Scribble currentScribble; // The scribble in progress Scribble beingDragged; // The scribble being dragged DragSource dragSource; // A central DnD object boolean dragMode; // Are we dragging or scribbling? // These are some constants we use static final int LINEWIDTH = 3; static final BasicStroke linestyle = new BasicStroke(LINEWIDTH); static final Border normalBorder = new BevelBorder(BevelBorder.LOWERED); static final Border dropBorder = new BevelBorder(BevelBorder.RAISED); /** The constructor: set up drag-and-drop stuff */ public ScribbleDragAndDrop() { // Give ourselves a nice default border. // We'll change this border during drag-and-drop. setBorder(normalBorder); // Register listeners to handle drawing addMouseListener(this); addMouseMotionListener(this); // Create a DragSource and DragGestureRecognizer to listen for drags // The DragGestureRecognizer will notify the DragGestureListener // when the user tries to drag an object dragSource = DragSource.getDefaultDragSource(); dragSource.createDefaultDragGestureRecognizer(this, // What component DnDConstants.ACTION_COPY_OR_MOVE, // What drag types? this);// the listener // Create and set up a DropTarget that will listen for drags and // drops over this component, and will notify the DropTargetListener DropTarget dropTarget = new DropTarget(this, // component to monitor this); // listener to notify this.setDropTarget(dropTarget); // Tell the component about it. } /** * The component draws itself by drawing each of the Scribble objects. */ public void paintComponent(Graphics g) { super.paintComponent(g); Graphics2D g2 = (Graphics2D) g; g2.setStroke(linestyle); // Specify wide lines int numScribbles = scribbles.size(); for (int i = 0; i < numScribbles; i++) { Scribble s = (Scribble) scribbles.get(i); g2.draw(s); // Draw the scribble } } public void setDragMode(boolean dragMode) { this.dragMode = dragMode; } public boolean getDragMode() { return dragMode; } /** * This method, and the following four methods are from the MouseListener * interface. If we're in drawing mode, this method handles mouse down * events and starts a new scribble. */ public void mousePressed(MouseEvent e) { if (dragMode) return; currentScribble = new Scribble(); scribbles.add(currentScribble); currentScribble.moveto(e.getX(), e.getY()); } public void mouseReleased(MouseEvent e) { } public void mouseClicked(MouseEvent e) { } public void mouseEntered(MouseEvent e) { } public void mouseExited(MouseEvent e) { } /** * This method and mouseMoved() below are from the MouseMotionListener * interface. If we're in drawing mode, this method adds a new point to the * current scribble and requests a redraw */ public void mouseDragged(MouseEvent e) { if (dragMode) return; currentScribble.lineto(e.getX(), e.getY()); repaint(); } public void mouseMoved(MouseEvent e) { } /** * This method implements the DragGestureListener interface. It will be * invoked when the DragGestureRecognizer thinks that the user has initiated * a drag. If we're not in drawing mode, then this method will try to figure * out which Scribble object is being dragged, and will initiate a drag on * that object. */ public void dragGestureRecognized(DragGestureEvent e) { // Don't drag if we're not in drag mode if (!dragMode) return; // Figure out where the drag started MouseEvent inputEvent = (MouseEvent) e.getTriggerEvent(); int x = inputEvent.getX(); int y = inputEvent.getY(); // Figure out which scribble was clicked on, if any by creating a // small rectangle around the point and testing for intersection. Rectangle r = new Rectangle(x - LINEWIDTH, y - LINEWIDTH, LINEWIDTH * 2, LINEWIDTH * 2); int numScribbles = scribbles.size(); for (int i = 0; i < numScribbles; i++) { // Loop through the scribbles Scribble s = (Scribble) scribbles.get(i); if (s.intersects(r)) { // The user started the drag on top of this scribble, so // start to drag it. // First, remember which scribble is being dragged, so we can // delete it later (if this is a move rather than a copy) beingDragged = s; // Next, create a copy that will be the one dragged Scribble dragScribble = (Scribble) s.clone(); // Adjust the origin to the point the user clicked on. dragScribble.translate(-x, -y); // Choose a cursor based on the type of drag the user initiated Cursor cursor; switch (e.getDragAction()) { case DnDConstants.ACTION_COPY: cursor = DragSource.DefaultCopyDrop; break; case DnDConstants.ACTION_MOVE: cursor = DragSource.DefaultMoveDrop; break; default: return; // We only support move and copys } // Some systems allow us to drag an image along with the // cursor. If so, create an image of the scribble to drag if (dragSource.isDragImageSupported()) { Rectangle scribbleBox = dragScribble.getBounds(); Image dragImage = this.createImage(scribbleBox.width, scribbleBox.height); Graphics2D g = (Graphics2D) dragImage.getGraphics(); g.setColor(new Color(0, 0, 0, 0)); // transparent background g.fillRect(0, 0, scribbleBox.width, scribbleBox.height); g.setColor(Color.black); g.setStroke(linestyle); g.translate(-scribbleBox.x, -scribbleBox.y); g.draw(dragScribble); Point hotspot = new Point(-scribbleBox.x, -scribbleBox.y); // Now start dragging, using the image. e.startDrag(cursor, dragImage, hotspot, dragScribble, this); } else { // Or start the drag without an image e.startDrag(cursor, dragScribble, this); } // After we've started dragging one scribble, stop looking return; } } } /** * This method, and the four unused methods that follow it implement the * DragSourceListener interface. dragDropEnd() is invoked when the user * drops the scribble she was dragging. If the drop was successful, and if * the user did a "move" rather than a "copy", then we delete the dragged * scribble from the list of scribbles to draw. */ public void dragDropEnd(DragSourceDropEvent e) { if (!e.getDropSuccess()) return; int action = e.getDropAction(); if (action == DnDConstants.ACTION_MOVE) { scribbles.remove(beingDragged); beingDragged = null; repaint(); } } // These methods are also part of DragSourceListener. // They are invoked at interesting points during the drag, and can be // used to perform "drag over" effects, such as changing the drag cursor // or drag image. public void dragEnter(DragSourceDragEvent e) { } public void dragExit(DragSourceEvent e) { } public void dropActionChanged(DragSourceDragEvent e) { } public void dragOver(DragSourceDragEvent e) { } // The next five methods implement DropTargetListener /** * This method is invoked when the user first drags something over us. If we * understand the data type being dragged, then call acceptDrag() to tell * the system that we're receptive. Also, we change our border as a "drag * under" effect to signal that we can accept the drop. */ public void dragEnter(DropTargetDragEvent e) { if (e.isDataFlavorSupported(Scribble.scribbleDataFlavor) || e.isDataFlavorSupported(DataFlavor.stringFlavor)) { e.acceptDrag(DnDConstants.ACTION_COPY_OR_MOVE); this.setBorder(dropBorder); } } /** The user is no longer dragging over us, so restore the border */ public void dragExit(DropTargetEvent e) { this.setBorder(normalBorder); } /** * This is the key method of DropTargetListener. It is invoked when the user * drops something on us. */ public void drop(DropTargetDropEvent e) { this.setBorder(normalBorder); // Restore the default border // First, check whether we understand the data that was dropped. // If we supports our data flavors, accept the drop, otherwise reject. if (e.isDataFlavorSupported(Scribble.scribbleDataFlavor) || e.isDataFlavorSupported(DataFlavor.stringFlavor)) { e.acceptDrop(DnDConstants.ACTION_COPY_OR_MOVE); } else { e.rejectDrop(); return; } // We've accepted the drop, so now we attempt to get the dropped data // from the Transferable object. Transferable t = e.getTransferable(); // Holds the dropped data Scribble droppedScribble; // This will hold the Scribble object // First, try to get the data directly as a scribble object try { droppedScribble = (Scribble) t.getTransferData(Scribble.scribbleDataFlavor); } catch (Exception ex) { // unsupported flavor, IO exception, etc. // If that doesn't work, try to get it as a String and parse it try { String s = (String) t.getTransferData(DataFlavor.stringFlavor); droppedScribble = Scribble.parse(s); } catch (Exception ex2) { // If we still couldn't get the data, tell the system we failed e.dropComplete(false); return; } } // If we get here, we've got the Scribble object Point p = e.getLocation(); // Where did the drop happen? droppedScribble.translate(p.getX(), p.getY()); // Move it there scribbles.add(droppedScribble); // add to display list repaint(); // ask for redraw e.dropComplete(true); // signal success! } // These are unused DropTargetListener methods public void dragOver(DropTargetDragEvent e) { } public void dropActionChanged(DropTargetDragEvent e) { } /** * The main method. Creates a simple application using this class. Note the * buttons for switching between draw mode and drag mode. */ public static void main(String[] args) { // Create a frame and put a scribble pane in it JFrame frame = new JFrame("ScribbleDragAndDrop"); final ScribbleDragAndDrop scribblePane = new ScribbleDragAndDrop(); frame.getContentPane().add(scribblePane, BorderLayout.CENTER); // Create two buttons for switching modes JToolBar toolbar = new JToolBar(); ButtonGroup group = new ButtonGroup(); JToggleButton draw = new JToggleButton("Draw"); JToggleButton drag = new JToggleButton("Drag"); draw.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { scribblePane.setDragMode(false); } }); drag.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { scribblePane.setDragMode(true); } }); group.add(draw); group.add(drag); toolbar.add(draw); toolbar.add(drag); frame.getContentPane().add(toolbar, BorderLayout.NORTH); // Start off in drawing mode draw.setSelected(true); scribblePane.setDragMode(false); // Pop up the window frame.setSize(400, 400); frame.setVisible(true); } } class Scribble implements Shape, Transferable, Serializable, Cloneable { protected double[] points = new double[64]; // The scribble data protected int numPoints = 0; // The current number of points double maxX = Double.NEGATIVE_INFINITY; // The bounding box double maxY = Double.NEGATIVE_INFINITY; double minX = Double.POSITIVE_INFINITY; double minY = Double.POSITIVE_INFINITY; /** * Begin a new polyline at (x,y). Note the use of Double.NaN in the points * array to mark the beginning of a new polyline */ public void moveto(double x, double y) { if (numPoints + 3 > points.length) reallocate(); // Mark this as the beginning of a new line points[numPoints++] = Double.NaN; // The rest of this method is just like lineto(); lineto(x, y); } /** * Add the point (x,y) to the end of the current polyline */ public void lineto(double x, double y) { if (numPoints + 2 > points.length) reallocate(); points[numPoints++] = x; points[numPoints++] = y; // See if the point enlarges our bounding box if (x > maxX) maxX = x; if (x < minX) minX = x; if (y > maxY) maxY = y; if (y < minY) minY = y; } /** * Append the Scribble s to this Scribble */ public void append(Scribble s) { int n = numPoints + s.numPoints; double[] newpoints = new double[n]; System.arraycopy(points, 0, newpoints, 0, numPoints); System.arraycopy(s.points, 0, newpoints, numPoints, s.numPoints); points = newpoints; numPoints = n; minX = Math.min(minX, s.minX); maxX = Math.max(maxX, s.maxX); minY = Math.min(minY, s.minY); maxY = Math.max(maxY, s.maxY); } /** * Translate the coordinates of all points in the Scribble by x,y */ public void translate(double x, double y) { for (int i = 0; i < numPoints; i++) { if (Double.isNaN(points[i])) continue; points[i++] += x; points[i] += y; } minX += x; maxX += x; minY += y; maxY += y; } /** An internal method to make more room in the data array */ protected void reallocate() { double[] newpoints = new double[points.length * 2]; System.arraycopy(points, 0, newpoints, 0, numPoints); points = newpoints; } /** Clone a Scribble object and its internal array of data */ public Object clone() { try { Scribble s = (Scribble) super.clone(); // make a copy of all fields s.points = (double[]) points.clone(); // copy the entire array return s; } catch (CloneNotSupportedException e) { // This should never happen return this; } } /** Convert the scribble data to a textual format */ public String toString() { StringBuffer b = new StringBuffer(); for (int i = 0; i < numPoints; i++) { if (Double.isNaN(points[i])) { b.append("m "); } else { b.append(points[i]); b.append(' '); } } return b.toString(); } /** * Create a new Scribble object and initialize it by parsing a string of * coordinate data in the format produced by toString() */ public static Scribble parse(String s) throws NumberFormatException { StringTokenizer st = new StringTokenizer(s); Scribble scribble = new Scribble(); while (st.hasMoreTokens()) { String t = st.nextToken(); if (t.charAt(0) == 'm') { scribble.moveto(Double.parseDouble(st.nextToken()), Double.parseDouble(st.nextToken())); } else { scribble.lineto(Double.parseDouble(t), Double.parseDouble(st.nextToken())); } } return scribble; } // ========= The following methods implement the Shape interface ======== /** Return the bounding box of the Shape */ public Rectangle getBounds() { return new Rectangle((int) (minX - 0.5f), (int) (minY - 0.5f), (int) (maxX - minX + 0.5f), (int) (maxY - minY + 0.5f)); } /** Return the bounding box of the Shape */ public Rectangle2D getBounds2D() { return new Rectangle2D.Double(minX, minY, maxX - minX, maxY - minY); } /** Our shape is an open curve, so it never contains anything */ 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; } /** * Determine if the scribble intersects the specified rectangle by testing * each line segment individually */ public boolean intersects(Rectangle2D r) { if (numPoints < 4) return false; int i = 0; double x1, y1, x2 = 0.0, y2 = 0.0; while (i < numPoints) { if (Double.isNaN(points[i])) { // If we're beginning a new line i++; // Skip the NaN x2 = points[i++]; y2 = points[i++]; } else { x1 = x2; y1 = y2; x2 = points[i++]; y2 = points[i++]; if (r.intersectsLine(x1, y1, x2, y2)) return true; } } return false; } /** Test for intersection by invoking the method above */ public boolean intersects(double x, double y, double w, double h) { return intersects(new Rectangle2D.Double(x, y, w, h)); } /** * Return a PathIterator object that tells Java2D how to draw this scribble */ public PathIterator getPathIterator(AffineTransform at) { return new ScribbleIterator(at); } /** * Return a PathIterator that doesn't include curves. Ours never does. */ public PathIterator getPathIterator(AffineTransform at, double flatness) { return getPathIterator(at); } /** * This inner class implements the PathIterator interface to describe the * shape of a scribble. Since a Scribble is composed of arbitrary movetos * and linetos, we simply return their coordinates */ public class ScribbleIterator implements PathIterator { protected int i = 0; // Position in array protected AffineTransform transform; public ScribbleIterator(AffineTransform transform) { this.transform = transform; } /** How to determine insideness and outsideness for this shape */ public int getWindingRule() { return PathIterator.WIND_NON_ZERO; } /** Have we reached the end of the scribble path yet? */ public boolean isDone() { return i >= numPoints; } /** Move on to the next segment of the path */ public void next() { if (Double.isNaN(points[i])) i += 3; else i += 2; } /** * Get the coordinates of the current moveto or lineto as floats */ public int currentSegment(float[] coords) { int retval; if (Double.isNaN(points[i])) { // If its a moveto coords[0] = (float) points[i + 1]; coords[1] = (float) points[i + 2]; retval = SEG_MOVETO; } else { coords[0] = (float) points[i]; coords[1] = (float) points[i + 1]; retval = SEG_LINETO; } // If a transform was specified, use it on the coordinates if (transform != null) transform.transform(coords, 0, coords, 0, 1); return retval; } /** * Get the coordinates of the current moveto or lineto as doubles */ public int currentSegment(double[] coords) { int retval; if (Double.isNaN(points[i])) { coords[0] = points[i + 1]; coords[1] = points[i + 2]; retval = SEG_MOVETO; } else { coords[0] = points[i]; coords[1] = points[i + 1]; retval = SEG_LINETO; } if (transform != null) transform.transform(coords, 0, coords, 0, 1); return retval; } } //====== The following methods implement the Transferable interface ===== // This is the custom DataFlavor for Scribble objects public static DataFlavor scribbleDataFlavor = new DataFlavor(Scribble.class, "Scribble"); // This is a list of the flavors we know how to work with public static DataFlavor[] supportedFlavors = { scribbleDataFlavor, DataFlavor.stringFlavor }; /** Return the data formats or "flavors" we know how to transfer */ public DataFlavor[] getTransferDataFlavors() { return (DataFlavor[]) supportedFlavors.clone(); } /** Check whether we support a given flavor */ public boolean isDataFlavorSupported(DataFlavor flavor) { return (flavor.equals(scribbleDataFlavor) || flavor.equals(DataFlavor.stringFlavor)); } /** * Return the scribble data in the requested format, or throw an exception * if we don't support the requested format */ public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException { if (flavor.equals(scribbleDataFlavor)) { return this; } else if (flavor.equals(DataFlavor.stringFlavor)) { return toString(); } else throw new UnsupportedFlavorException(flavor); } }