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