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.gui.main.ExceptionHandler;
18 import net.sf.openrocket.logging.LogHelper;
19 import net.sf.openrocket.rocketcomponent.Rocket;
20 import net.sf.openrocket.rocketcomponent.RocketComponent;
21 import net.sf.openrocket.startup.Application;
22 import net.sf.openrocket.util.BugException;
25 * A TransferHandler that handles dragging components from and to a ComponentTree.
26 * Supports both moving and copying (only copying when dragging to a different rocket).
28 * @author Sampo Niskanen <sampo.niskanen@iki.fi>
30 public class ComponentTreeTransferHandler extends TransferHandler {
32 private static final LogHelper log = Application.getLogger();
34 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 // If drag-dropping to another rocket always copy
94 if (support.getDropAction() == MOVE && data.srcParent.getRoot() != data.destParent.getRoot()) {
95 support.setDropAction(COPY);
104 public boolean importData(TransferHandler.TransferSupport support) {
106 // Sun JRE silently ignores any RuntimeExceptions in importData, yeech!
109 SourceTarget data = getSourceAndTarget(support);
111 // Check what action to perform
112 int action = support.getDropAction();
113 if (data.srcParent.getRoot() != data.destParent.getRoot()) {
114 // If drag-dropping to another rocket always copy
115 log.info("Performing DnD between different rockets, forcing copy action");
116 action = TransferHandler.COPY;
120 // Check whether move action would be a no-op
121 if ((action == MOVE) && (data.srcParent == data.destParent) &&
122 (data.destIndex == data.srcIndex || data.destIndex == data.srcIndex + 1)) {
123 log.user("Dropped component at the same place as previously: " + data);
130 log.user("Performing DnD move operation: " + data);
132 // If parents are the same, check whether removing the child changes the insert position
133 int index = data.destIndex;
134 if (data.srcParent == data.destParent && data.srcIndex < data.destIndex) {
138 // Mark undo and freeze rocket. src and dest are in same rocket, need to freeze only one
140 document.startUndo("Move component");
142 data.srcParent.getRocket().freeze();
143 data.srcParent.removeChild(data.srcIndex);
144 data.destParent.addChild(data.child, index);
146 data.srcParent.getRocket().thaw();
154 log.user("Performing DnD copy operation: " + data);
155 RocketComponent copy = data.child.copy();
157 document.startUndo("Copy component");
158 data.destParent.addChild(copy, data.destIndex);
165 log.warn("Unknown transfer action " + action);
169 } catch (final RuntimeException e) {
170 // Open error dialog later if an exception has occurred
171 SwingUtilities.invokeLater(new Runnable() {
174 ExceptionHandler.handleErrorCondition(e);
184 * Fetch the source and target for the DnD action. This method does not perform
185 * checks on whether this action is allowed based on component positioning rules.
187 * @param support the transfer support
188 * @return the source and targer, or <code>null</code> if invalid.
190 private SourceTarget getSourceAndTarget(TransferHandler.TransferSupport support) {
191 // We currently only support drop, not paste
192 if (!support.isDrop()) {
193 log.warn("Import action is not a drop action");
197 // we only import RocketComponentTransferable
198 if (!support.isDataFlavorSupported(RocketComponentTransferable.ROCKET_COMPONENT_DATA_FLAVOR)) {
199 log.debug("Attempting to import data with data flavors " +
200 Arrays.toString(support.getTransferable().getTransferDataFlavors()));
204 // Fetch the drop location and convert it to work around bug 6560955
205 JTree.DropLocation dl = (JTree.DropLocation) support.getDropLocation();
206 if (dl.getPath() == null) {
207 log.debug("No drop path location available");
210 MyDropLocation location = convertDropLocation((JTree) support.getComponent(), dl);
213 // Fetch the transferred component (child component)
214 Transferable transferable = support.getTransferable();
215 RocketComponent child;
218 child = (RocketComponent) transferable.getTransferData(
219 RocketComponentTransferable.ROCKET_COMPONENT_DATA_FLAVOR);
220 } catch (IOException e) {
221 throw new BugException(e);
222 } catch (UnsupportedFlavorException e) {
223 throw new BugException(e);
227 // Get the source component & index
228 RocketComponent srcParent = child.getParent();
229 if (srcParent == null) {
230 log.debug("Attempting to drag root component");
233 int srcIndex = srcParent.getChildPosition(child);
236 // Get destination component & index
237 RocketComponent destParent = ComponentTreeModel.componentFromPath(location.path);
238 int destIndex = location.index;
243 return new SourceTarget(srcParent, srcIndex, destParent, destIndex, child);
246 private class SourceTarget {
247 private final RocketComponent srcParent;
248 private final int srcIndex;
249 private final RocketComponent destParent;
250 private final int destIndex;
251 private final RocketComponent child;
253 public SourceTarget(RocketComponent srcParent, int srcIndex, RocketComponent destParent, int destIndex,
254 RocketComponent child) {
255 this.srcParent = srcParent;
256 this.srcIndex = srcIndex;
257 this.destParent = destParent;
258 this.destIndex = destIndex;
263 public String toString() {
265 "srcParent=" + srcParent.getComponentName() +
266 ", srcIndex=" + srcIndex +
267 ", destParent=" + destParent.getComponentName() +
268 ", destIndex=" + destIndex +
269 ", child=" + child.getComponentName() +
278 * Convert the JTree drop location in order to work around bug 6560955
279 * (http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6560955).
281 * This method analyzes whether the user is dropping on top of the last component
282 * of a subtree or the next item in the tree. The case to fix must fulfill the following
285 * <li> The node before the current insertion node is not a leaf node
286 * <li> The drop point is on top of the last node of that node
289 * This does not fix the visual clue provided to the user, but fixes the actual drop location.
291 * @param tree the JTree in question
292 * @param location the original drop location
293 * @return the updated drop location
295 private MyDropLocation convertDropLocation(JTree tree, JTree.DropLocation location) {
297 final TreePath originalPath = location.getPath();
298 final int originalIndex = location.getChildIndex();
300 if (originalPath == null || originalIndex <= 0) {
301 return new MyDropLocation(location);
304 // Check whether previous node is a leaf node
305 TreeModel model = tree.getModel();
306 Object previousNode = model.getChild(originalPath.getLastPathComponent(), originalIndex - 1);
307 if (model.isLeaf(previousNode)) {
308 return new MyDropLocation(location);
311 // Find node on top of which the drop occurred
312 Point point = location.getDropPoint();
313 TreePath dropPath = tree.getPathForLocation(point.x, point.y);
314 if (dropPath == null) {
315 return new MyDropLocation(location);
318 // Check whether previousNode is in the ancestry of the actual drop location
319 boolean inAncestry = false;
320 for (Object o : dropPath.getPath()) {
321 if (o == previousNode) {
327 return new MyDropLocation(location);
330 // The bug has occurred - insert after the actual drop location
331 TreePath correctInsertPath = dropPath.getParentPath();
332 int correctInsertIndex = model.getIndexOfChild(correctInsertPath.getLastPathComponent(),
333 dropPath.getLastPathComponent()) + 1;
335 log.verbose("Working around Sun JRE bug 6560955: " +
336 "converted path=" + ComponentTreeModel.pathToString(originalPath) + " index=" + originalIndex +
337 " into path=" + ComponentTreeModel.pathToString(correctInsertPath) +
338 " index=" + correctInsertIndex);
340 return new MyDropLocation(correctInsertPath, correctInsertIndex);
343 private class MyDropLocation {
344 private final TreePath path;
345 private final int index;
347 public MyDropLocation(JTree.DropLocation location) {
348 this(location.getPath(), location.getChildIndex());
351 public MyDropLocation(TreePath path, int index) {