1 package net.sf.openrocket.gui.main.componenttree;
4 import java.awt.datatransfer.Transferable;
5 import java.awt.datatransfer.UnsupportedFlavorException;
6 import java.io.IOException;
7 import java.util.Arrays;
9 import javax.swing.JComponent;
10 import javax.swing.JTree;
11 import javax.swing.SwingUtilities;
12 import javax.swing.TransferHandler;
13 import javax.swing.tree.TreeModel;
14 import javax.swing.tree.TreePath;
16 import net.sf.openrocket.document.OpenRocketDocument;
17 import net.sf.openrocket.logging.LogHelper;
18 import net.sf.openrocket.rocketcomponent.Rocket;
19 import net.sf.openrocket.rocketcomponent.RocketComponent;
20 import net.sf.openrocket.startup.Application;
21 import net.sf.openrocket.util.BugException;
24 * A TransferHandler that handles dragging components from and to a ComponentTree.
25 * Supports both moving and copying (only copying when dragging to a different rocket).
27 * @author Sampo Niskanen <sampo.niskanen@iki.fi>
29 public class ComponentTreeTransferHandler extends TransferHandler {
31 private static final LogHelper log = Application.getLogger();
33 private final OpenRocketDocument document;
39 * @param document the document this handler will drop to, used for undo actions.
41 public ComponentTreeTransferHandler(OpenRocketDocument document) {
42 this.document = document;
46 public int getSourceActions(JComponent comp) {
51 public Transferable createTransferable(JComponent component) {
52 if (!(component instanceof JTree)) {
53 throw new BugException("TransferHandler called with component " + component);
56 JTree tree = (JTree) component;
57 TreePath path = tree.getSelectionPath();
62 RocketComponent c = ComponentTreeModel.componentFromPath(path);
63 if (c instanceof Rocket) {
64 log.info("Attempting to create transferable from Rocket");
68 log.info("Creating transferable from component " + c.getComponentName());
69 return new RocketComponentTransferable(c);
76 public void exportDone(JComponent comp, Transferable trans, int action) {
77 // Removal from the old place is implemented already in import, so do nothing
83 public boolean canImport(TransferHandler.TransferSupport support) {
84 SourceTarget data = getSourceAndTarget(support);
90 boolean allowed = data.destParent.isCompatible(data.child);
91 log.verbose("Checking validity of drag-drop " + data.toString() + " allowed:" + allowed);
93 // Ensure we're not dropping a component onto a child component
94 RocketComponent path = data.destParent;
95 while (path != null) {
96 if (path.equals(data.child)) {
97 log.verbose("Drop would cause cycle in tree, disallowing.");
101 path = path.getParent();
104 // If drag-dropping to another rocket always copy
105 if (support.getDropAction() == MOVE && data.srcParent.getRoot() != data.destParent.getRoot()) {
106 support.setDropAction(COPY);
114 public boolean importData(TransferHandler.TransferSupport support) {
116 // We currently only support drop, not paste
117 if (!support.isDrop()) {
118 log.warn("Import action is not a drop action");
122 // Sun JRE silently ignores any RuntimeExceptions in importData, yeech!
125 SourceTarget data = getSourceAndTarget(support);
127 // Check what action to perform
128 int action = support.getDropAction();
129 if (data.srcParent.getRoot() != data.destParent.getRoot()) {
130 // If drag-dropping to another rocket always copy
131 log.info("Performing DnD between different rockets, forcing copy action");
132 action = TransferHandler.COPY;
136 // Check whether move action would be a no-op
137 if ((action == MOVE) && (data.srcParent == data.destParent) &&
138 (data.destIndex == data.srcIndex || data.destIndex == data.srcIndex + 1)) {
139 log.user("Dropped component at the same place as previously: " + data);
146 log.user("Performing DnD move operation: " + data);
148 // If parents are the same, check whether removing the child changes the insert position
149 int index = data.destIndex;
150 if (data.srcParent == data.destParent && data.srcIndex < data.destIndex) {
154 // Mark undo and freeze rocket. src and dest are in same rocket, need to freeze only one
156 document.startUndo("Move component");
158 data.srcParent.getRocket().freeze();
159 data.srcParent.removeChild(data.srcIndex);
160 data.destParent.addChild(data.child, index);
162 data.srcParent.getRocket().thaw();
170 log.user("Performing DnD copy operation: " + data);
171 RocketComponent copy = data.child.copy();
173 document.startUndo("Copy component");
174 data.destParent.addChild(copy, data.destIndex);
181 log.warn("Unknown transfer action " + action);
185 } catch (final RuntimeException e) {
186 // Open error dialog later if an exception has occurred
187 SwingUtilities.invokeLater(new Runnable() {
190 Application.getExceptionHandler().handleErrorCondition(e);
200 * Fetch the source and target for the DnD action. This method does not perform
201 * checks on whether this action is allowed based on component positioning rules.
203 * @param support the transfer support
204 * @return the source and targer, or <code>null</code> if invalid.
206 private SourceTarget getSourceAndTarget(TransferHandler.TransferSupport support) {
207 // We currently only support drop, not paste
208 if (!support.isDrop()) {
209 log.warn("Import action is not a drop action");
213 // we only import RocketComponentTransferable
214 if (!support.isDataFlavorSupported(RocketComponentTransferable.ROCKET_COMPONENT_DATA_FLAVOR)) {
215 log.debug("Attempting to import data with data flavors " +
216 Arrays.toString(support.getTransferable().getTransferDataFlavors()));
220 // Fetch the drop location and convert it to work around bug 6560955
221 JTree.DropLocation dl = (JTree.DropLocation) support.getDropLocation();
222 if (dl.getPath() == null) {
223 log.debug("No drop path location available");
226 MyDropLocation location = convertDropLocation((JTree) support.getComponent(), dl);
229 // Fetch the transferred component (child component)
230 Transferable transferable = support.getTransferable();
231 RocketComponent child;
234 child = (RocketComponent) transferable.getTransferData(
235 RocketComponentTransferable.ROCKET_COMPONENT_DATA_FLAVOR);
236 } catch (IOException e) {
237 throw new BugException(e);
238 } catch (UnsupportedFlavorException e) {
239 throw new BugException(e);
243 // Get the source component & index
244 RocketComponent srcParent = child.getParent();
245 if (srcParent == null) {
246 log.debug("Attempting to drag root component");
249 int srcIndex = srcParent.getChildPosition(child);
252 // Get destination component & index
253 RocketComponent destParent = ComponentTreeModel.componentFromPath(location.path);
254 int destIndex = location.index;
259 return new SourceTarget(srcParent, srcIndex, destParent, destIndex, child);
262 private class SourceTarget {
263 private final RocketComponent srcParent;
264 private final int srcIndex;
265 private final RocketComponent destParent;
266 private final int destIndex;
267 private final RocketComponent child;
269 public SourceTarget(RocketComponent srcParent, int srcIndex, RocketComponent destParent, int destIndex,
270 RocketComponent child) {
271 this.srcParent = srcParent;
272 this.srcIndex = srcIndex;
273 this.destParent = destParent;
274 this.destIndex = destIndex;
279 public String toString() {
281 "srcParent=" + srcParent.getComponentName() +
282 ", srcIndex=" + srcIndex +
283 ", destParent=" + destParent.getComponentName() +
284 ", destIndex=" + destIndex +
285 ", child=" + child.getComponentName() +
294 * Convert the JTree drop location in order to work around bug 6560955
295 * (http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6560955).
297 * This method analyzes whether the user is dropping on top of the last component
298 * of a subtree or the next item in the tree. The case to fix must fulfill the following
301 * <li> The node before the current insertion node is not a leaf node
302 * <li> The drop point is on top of the last node of that node
305 * This does not fix the visual clue provided to the user, but fixes the actual drop location.
307 * @param tree the JTree in question
308 * @param location the original drop location
309 * @return the updated drop location
311 private MyDropLocation convertDropLocation(JTree tree, JTree.DropLocation location) {
313 final TreePath originalPath = location.getPath();
314 final int originalIndex = location.getChildIndex();
316 if (originalPath == null || originalIndex <= 0) {
317 return new MyDropLocation(location);
320 // Check whether previous node is a leaf node
321 TreeModel model = tree.getModel();
322 Object previousNode = model.getChild(originalPath.getLastPathComponent(), originalIndex - 1);
323 if (model.isLeaf(previousNode)) {
324 return new MyDropLocation(location);
327 // Find node on top of which the drop occurred
328 Point point = location.getDropPoint();
329 TreePath dropPath = tree.getPathForLocation(point.x, point.y);
330 if (dropPath == null) {
331 return new MyDropLocation(location);
334 // Check whether previousNode is in the ancestry of the actual drop location
335 boolean inAncestry = false;
336 for (Object o : dropPath.getPath()) {
337 if (o == previousNode) {
343 return new MyDropLocation(location);
346 // The bug has occurred - insert after the actual drop location
347 TreePath correctInsertPath = dropPath.getParentPath();
348 int correctInsertIndex = model.getIndexOfChild(correctInsertPath.getLastPathComponent(),
349 dropPath.getLastPathComponent()) + 1;
351 log.verbose("Working around Sun JRE bug 6560955: " +
352 "converted path=" + ComponentTreeModel.pathToString(originalPath) + " index=" + originalIndex +
353 " into path=" + ComponentTreeModel.pathToString(correctInsertPath) +
354 " index=" + correctInsertIndex);
356 return new MyDropLocation(correctInsertPath, correctInsertIndex);
359 private class MyDropLocation {
360 private final TreePath path;
361 private final int index;
363 public MyDropLocation(JTree.DropLocation location) {
364 this(location.getPath(), location.getChildIndex());
367 public MyDropLocation(TreePath path, int index) {