14.6 Custom Data Transfer
Example 14-5 is a custom Swing component named
TransferableScribblePane. It allows the user to
scribble with the mouse and to select one scribble at a time. It
defines cut( ), copy( ), and
paste( ) methods that operate on the selected
line, and also allows the selected line to be dragged and dropped
within the component or into another instance of the component. The
scribbles are transferred using the
TransferablePolyLine class defined in Example 14-4, of course.This example does not use the high-level Swing
TransferHandler class, but instead relies on the
underlying data transfer and drag-and-drop architecture of
java.awt.datatransfer and
java.awt.dnd. The code for performing drag-and-drop is
substantially more complex than the code for cut-and-paste. This is
because the asynchronous drag-and-drop model requires a number of
distinct event listeners and their corresponding event objects. The
key interfaces are DragGestureListener, which
triggers a new drag, and DragSourceListener and
DropTargetListener, which notify the source of a
drag and the target of a drop of important events that occur during
the drag-and-drop process. The example implements all three
interfaces as anonymous inner classes. When studying the example, pay
particular attention to the dragGestureRecognized(
) method of the DragSourceListener and
the drop( ) method of the
DropTargetListener. The first is where the drag is
initiated, and the second is where the data transfer actually
transpires.To test this component, display two copies of it using the
ShowBean program. Draw a line with the mouse, then
select the line by shift-clicking on it. Next, drag and drop the
selected line while holding down the Shift or Ctrl
keys. Also, use the Commands menu of
ShowBean to invoke the cut( ),
copy( ), and paste( ) methods.
Example 14-5. TransferableScribblePane.java
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) { }
};
}