FileDocCategorySizeDatePackage
TransferableScribblePane.javaAPI DocExample14118Sat Jan 24 10:44:38 GMT 2004je3.datatransfer

TransferableScribblePane.java

/*
 * 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.
 */
package je3.datatransfer;
import java.awt.*;                  // Graphics, Rectangle, Stroke, etc.
import java.awt.datatransfer.*;     // Transferable, DataFlavor
import java.awt.dnd.*;              // Drag-and-drop listeners and events
import java.awt.event.*;            // Mouse events
import javax.swing.*;               // JComponent
import javax.swing.border.*;        // LineBorder and BevelBorder
import java.util.*;                 // ArrayList, etc.
import java.util.List;    // Explicit import to disambiguate from java.awt.List
import je3.graphics.PolyLine; // The Shape of scribbles

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

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