package net.sf.openrocket.gui.main.componenttree;
import java.awt.Point;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.io.IOException;
import java.util.Arrays;
import javax.swing.JComponent;
import javax.swing.JTree;
import javax.swing.SwingUtilities;
import javax.swing.TransferHandler;
import javax.swing.tree.TreeModel;
import javax.swing.tree.TreePath;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import net.sf.openrocket.document.OpenRocketDocument;
import net.sf.openrocket.logging.Markers;
import net.sf.openrocket.rocketcomponent.Rocket;
import net.sf.openrocket.rocketcomponent.RocketComponent;
import net.sf.openrocket.startup.Application;
import net.sf.openrocket.util.BugException;
/**
* A TransferHandler that handles dragging components from and to a ComponentTree.
* Supports both moving and copying (only copying when dragging to a different rocket).
*
* @author Sampo Niskanen <sampo.niskanen@iki.fi>
*/
public class ComponentTreeTransferHandler extends TransferHandler {
private static final Logger log = LoggerFactory.getLogger(ComponentTreeTransferHandler.class);
private final OpenRocketDocument document;
/**
* Sole constructor.
*
* @param document the document this handler will drop to, used for undo actions.
*/
public ComponentTreeTransferHandler(OpenRocketDocument document) {
this.document = document;
}
@Override
public int getSourceActions(JComponent comp) {
return COPY_OR_MOVE;
}
@Override
public Transferable createTransferable(JComponent component) {
if (!(component instanceof JTree)) {
throw new BugException("TransferHandler called with component " + component);
}
JTree tree = (JTree) component;
TreePath path = tree.getSelectionPath();
if (path == null) {
return null;
}
RocketComponent c = ComponentTreeModel.componentFromPath(path);
if (c instanceof Rocket) {
log.info("Attempting to create transferable from Rocket");
return null;
}
log.info("Creating transferable from component " + c.getComponentName());
return new RocketComponentTransferable(c);
}
@Override
public void exportDone(JComponent comp, Transferable trans, int action) {
// Removal from the old place is implemented already in import, so do nothing
}
@Override
public boolean canImport(TransferHandler.TransferSupport support) {
SourceTarget data = getSourceAndTarget(support);
if (data == null) {
return false;
}
boolean allowed = data.destParent.isCompatible(data.child);
log.trace("Checking validity of drag-drop " + data.toString() + " allowed:" + allowed);
// Ensure we're not dropping a component onto a child component
RocketComponent path = data.destParent;
while (path != null) {
if (path.equals(data.child)) {
log.trace("Drop would cause cycle in tree, disallowing.");
allowed = false;
break;
}
path = path.getParent();
}
// If drag-dropping to another rocket always copy
if (support.getDropAction() == MOVE && data.srcParent.getRoot() != data.destParent.getRoot()) {
support.setDropAction(COPY);
}
return allowed;
}
@Override
public boolean importData(TransferHandler.TransferSupport support) {
// We currently only support drop, not paste
if (!support.isDrop()) {
log.warn("Import action is not a drop action");
return false;
}
// Sun JRE silently ignores any RuntimeExceptions in importData, yeech!
try {
SourceTarget data = getSourceAndTarget(support);
// Check what action to perform
int action = support.getDropAction();
if (data.srcParent.getRoot() != data.destParent.getRoot()) {
// If drag-dropping to another rocket always copy
log.info("Performing DnD between different rockets, forcing copy action");
action = TransferHandler.COPY;
}
// Check whether move action would be a no-op
if ((action == MOVE) && (data.srcParent == data.destParent) &&
(data.destIndex == data.srcIndex || data.destIndex == data.srcIndex + 1)) {
log.info(Markers.USER_MARKER, "Dropped component at the same place as previously: " + data);
return false;
}
switch (action) {
case MOVE:
log.info(Markers.USER_MARKER, "Performing DnD move operation: " + data);
// If parents are the same, check whether removing the child changes the insert position
int index = data.destIndex;
if (data.srcParent == data.destParent && data.srcIndex < data.destIndex) {
index--;
}
// Mark undo and freeze rocket. src and dest are in same rocket, need to freeze only one
try {
document.startUndo("Move component");
try {
data.srcParent.getRocket().freeze();
data.srcParent.removeChild(data.srcIndex);
data.destParent.addChild(data.child, index);
} finally {
data.srcParent.getRocket().thaw();
}
} finally {
document.stopUndo();
}
return true;
case COPY:
log.info(Markers.USER_MARKER, "Performing DnD copy operation: " + data);
RocketComponent copy = data.child.copy();
try {
document.startUndo("Copy component");
data.destParent.addChild(copy, data.destIndex);
} finally {
document.stopUndo();
}
return true;
default:
log.warn("Unknown transfer action " + action);
return false;
}
} catch (final RuntimeException e) {
// Open error dialog later if an exception has occurred
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
Application.getExceptionHandler().handleErrorCondition(e);
}
});
return false;
}
}
/**
* Fetch the source and target for the DnD action. This method does not perform
* checks on whether this action is allowed based on component positioning rules.
*
* @param support the transfer support
* @return the source and targer, or <code>null</code> if invalid.
*/
private SourceTarget getSourceAndTarget(TransferHandler.TransferSupport support) {
// We currently only support drop, not paste
if (!support.isDrop()) {
log.warn("Import action is not a drop action");
return null;
}
// we only import RocketComponentTransferable
if (!support.isDataFlavorSupported(RocketComponentTransferable.ROCKET_COMPONENT_DATA_FLAVOR)) {
log.debug("Attempting to import data with data flavors " +
Arrays.toString(support.getTransferable().getTransferDataFlavors()));
return null;
}
// Fetch the drop location and convert it to work around bug 6560955
JTree.DropLocation dl = (JTree.DropLocation) support.getDropLocation();
if (dl.getPath() == null) {
log.debug("No drop path location available");
return null;
}
MyDropLocation location = convertDropLocation((JTree) support.getComponent(), dl);
// Fetch the transferred component (child component)
Transferable transferable = support.getTransferable();
RocketComponent child;
try {
child = (RocketComponent) transferable.getTransferData(
RocketComponentTransferable.ROCKET_COMPONENT_DATA_FLAVOR);
} catch (IOException e) {
throw new BugException(e);
} catch (UnsupportedFlavorException e) {
throw new BugException(e);
}
// Get the source component & index
RocketComponent srcParent = child.getParent();
if (srcParent == null) {
log.debug("Attempting to drag root component");
return null;
}
int srcIndex = srcParent.getChildPosition(child);
// Get destination component & index
RocketComponent destParent = ComponentTreeModel.componentFromPath(location.path);
int destIndex = location.index;
if (destIndex < 0) {
destIndex = 0;
}
return new SourceTarget(srcParent, srcIndex, destParent, destIndex, child);
}
private class SourceTarget {
private final RocketComponent srcParent;
private final int srcIndex;
private final RocketComponent destParent;
private final int destIndex;
private final RocketComponent child;
public SourceTarget(RocketComponent srcParent, int srcIndex, RocketComponent destParent, int destIndex,
RocketComponent child) {
this.srcParent = srcParent;
this.srcIndex = srcIndex;
this.destParent = destParent;
this.destIndex = destIndex;
this.child = child;
}
@Override
public String toString() {
return "[" +
"srcParent=" + srcParent.getComponentName() +
", srcIndex=" + srcIndex +
", destParent=" + destParent.getComponentName() +
", destIndex=" + destIndex +
", child=" + child.getComponentName() +
"]";
}
}
/**
* Convert the JTree drop location in order to work around bug 6560955
* (http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6560955).
* <p>
* This method analyzes whether the user is dropping on top of the last component
* of a subtree or the next item in the tree. The case to fix must fulfill the following
* requirements:
* <ul>
* <li> The node before the current insertion node is not a leaf node
* <li> The drop point is on top of the last node of that node
* </ul>
* <p>
* This does not fix the visual clue provided to the user, but fixes the actual drop location.
*
* @param tree the JTree in question
* @param location the original drop location
* @return the updated drop location
*/
private MyDropLocation convertDropLocation(JTree tree, JTree.DropLocation location) {
final TreePath originalPath = location.getPath();
final int originalIndex = location.getChildIndex();
if (originalPath == null || originalIndex <= 0) {
return new MyDropLocation(location);
}
// Check whether previous node is a leaf node
TreeModel model = tree.getModel();
Object previousNode = model.getChild(originalPath.getLastPathComponent(), originalIndex - 1);
if (model.isLeaf(previousNode)) {
return new MyDropLocation(location);
}
// Find node on top of which the drop occurred
Point point = location.getDropPoint();
TreePath dropPath = tree.getPathForLocation(point.x, point.y);
if (dropPath == null) {
return new MyDropLocation(location);
}
// Check whether previousNode is in the ancestry of the actual drop location
boolean inAncestry = false;
for (Object o : dropPath.getPath()) {
if (o == previousNode) {
inAncestry = true;
break;
}
}
if (!inAncestry) {
return new MyDropLocation(location);
}
// The bug has occurred - insert after the actual drop location
TreePath correctInsertPath = dropPath.getParentPath();
int correctInsertIndex = model.getIndexOfChild(correctInsertPath.getLastPathComponent(),
dropPath.getLastPathComponent()) + 1;
log.trace("Working around Sun JRE bug 6560955: " +
"converted path=" + ComponentTreeModel.pathToString(originalPath) + " index=" + originalIndex +
" into path=" + ComponentTreeModel.pathToString(correctInsertPath) +
" index=" + correctInsertIndex);
return new MyDropLocation(correctInsertPath, correctInsertIndex);
}
private class MyDropLocation {
private final TreePath path;
private final int index;
public MyDropLocation(JTree.DropLocation location) {
this(location.getPath(), location.getChildIndex());
}
public MyDropLocation(TreePath path, int index) {
this.path = path;
this.index = index;
}
}
}