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 // We currently only support drop, not paste
107 if (!support.isDrop()) {
108 log.warn("Import action is not a drop action");
112 // Sun JRE silently ignores any RuntimeExceptions in importData, yeech!
115 SourceTarget data = getSourceAndTarget(support);
117 // Check what action to perform
118 int action = support.getDropAction();
119 if (data.srcParent.getRoot() != data.destParent.getRoot()) {
120 // If drag-dropping to another rocket always copy
121 log.info("Performing DnD between different rockets, forcing copy action");
122 action = TransferHandler.COPY;
126 // Check whether move action would be a no-op
127 if ((action == MOVE) && (data.srcParent == data.destParent) &&
128 (data.destIndex == data.srcIndex || data.destIndex == data.srcIndex + 1)) {
129 log.user("Dropped component at the same place as previously: " + data);
136 log.user("Performing DnD move operation: " + data);
138 // If parents are the same, check whether removing the child changes the insert position
139 int index = data.destIndex;
140 if (data.srcParent == data.destParent && data.srcIndex < data.destIndex) {
144 // Mark undo and freeze rocket. src and dest are in same rocket, need to freeze only one
146 document.startUndo("Move component");
148 data.srcParent.getRocket().freeze();
149 data.srcParent.removeChild(data.srcIndex);
150 data.destParent.addChild(data.child, index);
152 data.srcParent.getRocket().thaw();
160 log.user("Performing DnD copy operation: " + data);
161 RocketComponent copy = data.child.copy();
163 document.startUndo("Copy component");
164 data.destParent.addChild(copy, data.destIndex);
171 log.warn("Unknown transfer action " + action);
175 } catch (final RuntimeException e) {
176 // Open error dialog later if an exception has occurred
177 SwingUtilities.invokeLater(new Runnable() {
180 ExceptionHandler.handleErrorCondition(e);
190 * Fetch the source and target for the DnD action. This method does not perform
191 * checks on whether this action is allowed based on component positioning rules.
193 * @param support the transfer support
194 * @return the source and targer, or <code>null</code> if invalid.
196 private SourceTarget getSourceAndTarget(TransferHandler.TransferSupport support) {
197 // We currently only support drop, not paste
198 if (!support.isDrop()) {
199 log.warn("Import action is not a drop action");
203 // we only import RocketComponentTransferable
204 if (!support.isDataFlavorSupported(RocketComponentTransferable.ROCKET_COMPONENT_DATA_FLAVOR)) {
205 log.debug("Attempting to import data with data flavors " +
206 Arrays.toString(support.getTransferable().getTransferDataFlavors()));
210 // Fetch the drop location and convert it to work around bug 6560955
211 JTree.DropLocation dl = (JTree.DropLocation) support.getDropLocation();
212 if (dl.getPath() == null) {
213 log.debug("No drop path location available");
216 MyDropLocation location = convertDropLocation((JTree) support.getComponent(), dl);
219 // Fetch the transferred component (child component)
220 Transferable transferable = support.getTransferable();
221 RocketComponent child;
224 child = (RocketComponent) transferable.getTransferData(
225 RocketComponentTransferable.ROCKET_COMPONENT_DATA_FLAVOR);
226 } catch (IOException e) {
227 throw new BugException(e);
228 } catch (UnsupportedFlavorException e) {
229 throw new BugException(e);
233 // Get the source component & index
234 RocketComponent srcParent = child.getParent();
235 if (srcParent == null) {
236 log.debug("Attempting to drag root component");
239 int srcIndex = srcParent.getChildPosition(child);
242 // Get destination component & index
243 RocketComponent destParent = ComponentTreeModel.componentFromPath(location.path);
244 int destIndex = location.index;
249 return new SourceTarget(srcParent, srcIndex, destParent, destIndex, child);
252 private class SourceTarget {
253 private final RocketComponent srcParent;
254 private final int srcIndex;
255 private final RocketComponent destParent;
256 private final int destIndex;
257 private final RocketComponent child;
259 public SourceTarget(RocketComponent srcParent, int srcIndex, RocketComponent destParent, int destIndex,
260 RocketComponent child) {
261 this.srcParent = srcParent;
262 this.srcIndex = srcIndex;
263 this.destParent = destParent;
264 this.destIndex = destIndex;
269 public String toString() {
271 "srcParent=" + srcParent.getComponentName() +
272 ", srcIndex=" + srcIndex +
273 ", destParent=" + destParent.getComponentName() +
274 ", destIndex=" + destIndex +
275 ", child=" + child.getComponentName() +
284 * Convert the JTree drop location in order to work around bug 6560955
285 * (http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6560955).
287 * This method analyzes whether the user is dropping on top of the last component
288 * of a subtree or the next item in the tree. The case to fix must fulfill the following
291 * <li> The node before the current insertion node is not a leaf node
292 * <li> The drop point is on top of the last node of that node
295 * This does not fix the visual clue provided to the user, but fixes the actual drop location.
297 * @param tree the JTree in question
298 * @param location the original drop location
299 * @return the updated drop location
301 private MyDropLocation convertDropLocation(JTree tree, JTree.DropLocation location) {
303 final TreePath originalPath = location.getPath();
304 final int originalIndex = location.getChildIndex();
306 if (originalPath == null || originalIndex <= 0) {
307 return new MyDropLocation(location);
310 // Check whether previous node is a leaf node
311 TreeModel model = tree.getModel();
312 Object previousNode = model.getChild(originalPath.getLastPathComponent(), originalIndex - 1);
313 if (model.isLeaf(previousNode)) {
314 return new MyDropLocation(location);
317 // Find node on top of which the drop occurred
318 Point point = location.getDropPoint();
319 TreePath dropPath = tree.getPathForLocation(point.x, point.y);
320 if (dropPath == null) {
321 return new MyDropLocation(location);
324 // Check whether previousNode is in the ancestry of the actual drop location
325 boolean inAncestry = false;
326 for (Object o : dropPath.getPath()) {
327 if (o == previousNode) {
333 return new MyDropLocation(location);
336 // The bug has occurred - insert after the actual drop location
337 TreePath correctInsertPath = dropPath.getParentPath();
338 int correctInsertIndex = model.getIndexOfChild(correctInsertPath.getLastPathComponent(),
339 dropPath.getLastPathComponent()) + 1;
341 log.verbose("Working around Sun JRE bug 6560955: " +
342 "converted path=" + ComponentTreeModel.pathToString(originalPath) + " index=" + originalIndex +
343 " into path=" + ComponentTreeModel.pathToString(correctInsertPath) +
344 " index=" + correctInsertIndex);
346 return new MyDropLocation(correctInsertPath, correctInsertIndex);
349 private class MyDropLocation {
350 private final TreePath path;
351 private final int index;
353 public MyDropLocation(JTree.DropLocation location) {
354 this(location.getPath(), location.getChildIndex());
357 public MyDropLocation(TreePath path, int index) {