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