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;
38 * @param document the document this handler will drop to, used for undo actions.
40 public ComponentTreeTransferHandler(OpenRocketDocument document) {
41 this.document = document;
45 public int getSourceActions(JComponent comp) {
50 public Transferable createTransferable(JComponent component) {
51 if (!(component instanceof JTree)) {
52 throw new BugException("TransferHandler called with component " + component);
55 JTree tree = (JTree) component;
56 TreePath path = tree.getSelectionPath();
61 RocketComponent c = ComponentTreeModel.componentFromPath(path);
62 if (c instanceof Rocket) {
63 log.info("Attempting to create transferable from Rocket");
67 log.info("Creating transferable from component " + c.getComponentName());
68 return new RocketComponentTransferable(c);
75 public void exportDone(JComponent comp, Transferable trans, int action) {
76 // Removal from the old place is implemented already in import, so do nothing
82 public boolean canImport(TransferHandler.TransferSupport support) {
83 SourceTarget data = getSourceAndTarget(support);
89 boolean allowed = data.destParent.isCompatible(data.child);
90 log.verbose("Checking validity of drag-drop " + data.toString() + " allowed:" + allowed);
92 // If drag-dropping to another rocket always copy
93 if (support.getDropAction() == MOVE && data.srcParent.getRoot() != data.destParent.getRoot()) {
94 support.setDropAction(COPY);
103 public boolean importData(TransferHandler.TransferSupport support) {
105 // We currently only support drop, not paste
106 if (!support.isDrop()) {
107 log.warn("Import action is not a drop action");
111 // Sun JRE silently ignores any RuntimeExceptions in importData, yeech!
114 SourceTarget data = getSourceAndTarget(support);
116 // Check what action to perform
117 int action = support.getDropAction();
118 if (data.srcParent.getRoot() != data.destParent.getRoot()) {
119 // If drag-dropping to another rocket always copy
120 log.info("Performing DnD between different rockets, forcing copy action");
121 action = TransferHandler.COPY;
125 // Check whether move action would be a no-op
126 if ((action == MOVE) && (data.srcParent == data.destParent) &&
127 (data.destIndex == data.srcIndex || data.destIndex == data.srcIndex + 1)) {
128 log.user("Dropped component at the same place as previously: " + data);
135 log.user("Performing DnD move operation: " + data);
137 // If parents are the same, check whether removing the child changes the insert position
138 int index = data.destIndex;
139 if (data.srcParent == data.destParent && data.srcIndex < data.destIndex) {
143 // Mark undo and freeze rocket. src and dest are in same rocket, need to freeze only one
145 document.startUndo("Move component");
147 data.srcParent.getRocket().freeze();
148 data.srcParent.removeChild(data.srcIndex);
149 data.destParent.addChild(data.child, index);
151 data.srcParent.getRocket().thaw();
159 log.user("Performing DnD copy operation: " + data);
160 RocketComponent copy = data.child.copy();
162 document.startUndo("Copy component");
163 data.destParent.addChild(copy, data.destIndex);
170 log.warn("Unknown transfer action " + action);
174 } catch (final RuntimeException e) {
175 // Open error dialog later if an exception has occurred
176 SwingUtilities.invokeLater(new Runnable() {
179 Application.getExceptionHandler().handleErrorCondition(e);
189 * Fetch the source and target for the DnD action. This method does not perform
190 * checks on whether this action is allowed based on component positioning rules.
192 * @param support the transfer support
193 * @return the source and targer, or <code>null</code> if invalid.
195 private SourceTarget getSourceAndTarget(TransferHandler.TransferSupport support) {
196 // We currently only support drop, not paste
197 if (!support.isDrop()) {
198 log.warn("Import action is not a drop action");
202 // we only import RocketComponentTransferable
203 if (!support.isDataFlavorSupported(RocketComponentTransferable.ROCKET_COMPONENT_DATA_FLAVOR)) {
204 log.debug("Attempting to import data with data flavors " +
205 Arrays.toString(support.getTransferable().getTransferDataFlavors()));
209 // Fetch the drop location and convert it to work around bug 6560955
210 JTree.DropLocation dl = (JTree.DropLocation) support.getDropLocation();
211 if (dl.getPath() == null) {
212 log.debug("No drop path location available");
215 MyDropLocation location = convertDropLocation((JTree) support.getComponent(), dl);
218 // Fetch the transferred component (child component)
219 Transferable transferable = support.getTransferable();
220 RocketComponent child;
223 child = (RocketComponent) transferable.getTransferData(
224 RocketComponentTransferable.ROCKET_COMPONENT_DATA_FLAVOR);
225 } catch (IOException e) {
226 throw new BugException(e);
227 } catch (UnsupportedFlavorException e) {
228 throw new BugException(e);
232 // Get the source component & index
233 RocketComponent srcParent = child.getParent();
234 if (srcParent == null) {
235 log.debug("Attempting to drag root component");
238 int srcIndex = srcParent.getChildPosition(child);
241 // Get destination component & index
242 RocketComponent destParent = ComponentTreeModel.componentFromPath(location.path);
243 int destIndex = location.index;
248 return new SourceTarget(srcParent, srcIndex, destParent, destIndex, child);
251 private class SourceTarget {
252 private final RocketComponent srcParent;
253 private final int srcIndex;
254 private final RocketComponent destParent;
255 private final int destIndex;
256 private final RocketComponent child;
258 public SourceTarget(RocketComponent srcParent, int srcIndex, RocketComponent destParent, int destIndex,
259 RocketComponent child) {
260 this.srcParent = srcParent;
261 this.srcIndex = srcIndex;
262 this.destParent = destParent;
263 this.destIndex = destIndex;
268 public String toString() {
270 "srcParent=" + srcParent.getComponentName() +
271 ", srcIndex=" + srcIndex +
272 ", destParent=" + destParent.getComponentName() +
273 ", destIndex=" + destIndex +
274 ", child=" + child.getComponentName() +
283 * Convert the JTree drop location in order to work around bug 6560955
284 * (http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6560955).
286 * This method analyzes whether the user is dropping on top of the last component
287 * of a subtree or the next item in the tree. The case to fix must fulfill the following
290 * <li> The node before the current insertion node is not a leaf node
291 * <li> The drop point is on top of the last node of that node
294 * This does not fix the visual clue provided to the user, but fixes the actual drop location.
296 * @param tree the JTree in question
297 * @param location the original drop location
298 * @return the updated drop location
300 private MyDropLocation convertDropLocation(JTree tree, JTree.DropLocation location) {
302 final TreePath originalPath = location.getPath();
303 final int originalIndex = location.getChildIndex();
305 if (originalPath == null || originalIndex <= 0) {
306 return new MyDropLocation(location);
309 // Check whether previous node is a leaf node
310 TreeModel model = tree.getModel();
311 Object previousNode = model.getChild(originalPath.getLastPathComponent(), originalIndex - 1);
312 if (model.isLeaf(previousNode)) {
313 return new MyDropLocation(location);
316 // Find node on top of which the drop occurred
317 Point point = location.getDropPoint();
318 TreePath dropPath = tree.getPathForLocation(point.x, point.y);
319 if (dropPath == null) {
320 return new MyDropLocation(location);
323 // Check whether previousNode is in the ancestry of the actual drop location
324 boolean inAncestry = false;
325 for (Object o : dropPath.getPath()) {
326 if (o == previousNode) {
332 return new MyDropLocation(location);
335 // The bug has occurred - insert after the actual drop location
336 TreePath correctInsertPath = dropPath.getParentPath();
337 int correctInsertIndex = model.getIndexOfChild(correctInsertPath.getLastPathComponent(),
338 dropPath.getLastPathComponent()) + 1;
340 log.verbose("Working around Sun JRE bug 6560955: " +
341 "converted path=" + ComponentTreeModel.pathToString(originalPath) + " index=" + originalIndex +
342 " into path=" + ComponentTreeModel.pathToString(correctInsertPath) +
343 " index=" + correctInsertIndex);
345 return new MyDropLocation(correctInsertPath, correctInsertIndex);
348 private class MyDropLocation {
349 private final TreePath path;
350 private final int index;
352 public MyDropLocation(JTree.DropLocation location) {
353 this(location.getPath(), location.getChildIndex());
356 public MyDropLocation(TreePath path, int index) {