Swing 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.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;
  }
}