f225f5e5a6fcf438c2ddcf8161505d3dfd210f4d
[debian/openrocket] / src / net / sf / openrocket / gui / main / componenttree / ComponentTreeTransferHandler.java
1 package net.sf.openrocket.gui.main.componenttree;
2
3 import java.awt.Point;
4 import java.awt.datatransfer.Transferable;
5 import java.awt.datatransfer.UnsupportedFlavorException;
6 import java.io.IOException;
7 import java.util.Arrays;
8
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;
15
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;
23
24 /**
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).
27  * 
28  * @author Sampo Niskanen <sampo.niskanen@iki.fi>
29  */
30 public class ComponentTreeTransferHandler extends TransferHandler {
31         
32         private static final LogHelper log = Application.getLogger();
33         
34         private final OpenRocketDocument document;
35         
36         /**
37          * Sole constructor.
38          * 
39          * @param document      the document this handler will drop to, used for undo actions.
40          */
41         public ComponentTreeTransferHandler(OpenRocketDocument document) {
42                 this.document = document;
43         }
44         
45         @Override
46         public int getSourceActions(JComponent comp) {
47                 return COPY_OR_MOVE;
48         }
49         
50         @Override
51         public Transferable createTransferable(JComponent component) {
52                 if (!(component instanceof JTree)) {
53                         throw new BugException("TransferHandler called with component " + component);
54                 }
55                 
56                 JTree tree = (JTree) component;
57                 TreePath path = tree.getSelectionPath();
58                 if (path == null) {
59                         return null;
60                 }
61                 
62                 RocketComponent c = ComponentTreeModel.componentFromPath(path);
63                 if (c instanceof Rocket) {
64                         log.info("Attempting to create transferable from Rocket");
65                         return null;
66                 }
67                 
68                 log.info("Creating transferable from component " + c.getComponentName());
69                 return new RocketComponentTransferable(c);
70         }
71         
72         
73
74
75         @Override
76         public void exportDone(JComponent comp, Transferable trans, int action) {
77                 // Removal from the old place is implemented already in import, so do nothing
78         }
79         
80         
81
82         @Override
83         public boolean canImport(TransferHandler.TransferSupport support) {
84                 SourceTarget data = getSourceAndTarget(support);
85                 
86                 if (data == null) {
87                         return false;
88                 }
89                 
90                 boolean allowed = data.destParent.isCompatible(data.child);
91                 log.verbose("Checking validity of drag-drop " + data.toString() + " allowed:" + allowed);
92                 
93                 // If drag-dropping to another rocket always copy
94                 if (support.getDropAction() == MOVE && data.srcParent.getRoot() != data.destParent.getRoot()) {
95                         support.setDropAction(COPY);
96                 }
97                 
98                 return allowed;
99         }
100         
101         
102
103         @Override
104         public boolean importData(TransferHandler.TransferSupport support) {
105                 
106                 // Sun JRE silently ignores any RuntimeExceptions in importData, yeech!
107                 try {
108                         
109                         SourceTarget data = getSourceAndTarget(support);
110                         
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;
117                         }
118                         
119
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);
124                                 return false;
125                         }
126                         
127
128                         switch (action) {
129                         case MOVE:
130                                 log.user("Performing DnD move operation: " + data);
131                                 
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) {
135                                         index--;
136                                 }
137                                 
138                                 // Mark undo and freeze rocket.  src and dest are in same rocket, need to freeze only one
139                                 try {
140                                         document.startUndo("Move component");
141                                         try {
142                                                 data.srcParent.getRocket().freeze();
143                                                 data.srcParent.removeChild(data.srcIndex);
144                                                 data.destParent.addChild(data.child, index);
145                                         } finally {
146                                                 data.srcParent.getRocket().thaw();
147                                         }
148                                 } finally {
149                                         document.stopUndo();
150                                 }
151                                 return true;
152                                 
153                         case COPY:
154                                 log.user("Performing DnD copy operation: " + data);
155                                 RocketComponent copy = data.child.copy();
156                                 try {
157                                         document.startUndo("Copy component");
158                                         data.destParent.addChild(copy, data.destIndex);
159                                 } finally {
160                                         document.stopUndo();
161                                 }
162                                 return true;
163                                 
164                         default:
165                                 log.warn("Unknown transfer action " + action);
166                                 return false;
167                         }
168                         
169                 } catch (final RuntimeException e) {
170                         // Open error dialog later if an exception has occurred
171                         SwingUtilities.invokeLater(new Runnable() {
172                                 @Override
173                                 public void run() {
174                                         ExceptionHandler.handleErrorCondition(e);
175                                 }
176                         });
177                         return false;
178                 }
179         }
180         
181         
182
183         /**
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.
186          * 
187          * @param support       the transfer support
188          * @return                      the source and targer, or <code>null</code> if invalid.
189          */
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");
194                         return null;
195                 }
196                 
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()));
201                         return null;
202                 }
203                 
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");
208                         return null;
209                 }
210                 MyDropLocation location = convertDropLocation((JTree) support.getComponent(), dl);
211                 
212
213                 // Fetch the transferred component (child component)
214                 Transferable transferable = support.getTransferable();
215                 RocketComponent child;
216                 
217                 try {
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);
224                 }
225                 
226
227                 // Get the source component & index
228                 RocketComponent srcParent = child.getParent();
229                 if (srcParent == null) {
230                         log.debug("Attempting to drag root component");
231                         return null;
232                 }
233                 int srcIndex = srcParent.getChildPosition(child);
234                 
235
236                 // Get destination component & index
237                 RocketComponent destParent = ComponentTreeModel.componentFromPath(location.path);
238                 int destIndex = location.index;
239                 if (destIndex < 0) {
240                         destIndex = 0;
241                 }
242                 
243                 return new SourceTarget(srcParent, srcIndex, destParent, destIndex, child);
244         }
245         
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;
252                 
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;
259                         this.child = child;
260                 }
261                 
262                 @Override
263                 public String toString() {
264                         return "[" +
265                                         "srcParent=" + srcParent.getComponentName() +
266                                         ", srcIndex=" + srcIndex +
267                                         ", destParent=" + destParent.getComponentName() +
268                                         ", destIndex=" + destIndex +
269                                         ", child=" + child.getComponentName() +
270                                         "]";
271                 }
272                 
273         }
274         
275         
276
277         /**
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).
280          * <p>
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
283          * requirements:
284          * <ul>
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
287          * </ul>
288          * <p>
289          * This does not fix the visual clue provided to the user, but fixes the actual drop location.
290          * 
291          * @param tree          the JTree in question
292          * @param location      the original drop location
293          * @return                      the updated drop location
294          */
295         private MyDropLocation convertDropLocation(JTree tree, JTree.DropLocation location) {
296                 
297                 final TreePath originalPath = location.getPath();
298                 final int originalIndex = location.getChildIndex();
299                 
300                 if (originalPath == null || originalIndex <= 0) {
301                         return new MyDropLocation(location);
302                 }
303                 
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);
309                 }
310                 
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);
316                 }
317                 
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) {
322                                 inAncestry = true;
323                                 break;
324                         }
325                 }
326                 if (!inAncestry) {
327                         return new MyDropLocation(location);
328                 }
329                 
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;
334                 
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);
339                 
340                 return new MyDropLocation(correctInsertPath, correctInsertIndex);
341         }
342         
343         private class MyDropLocation {
344                 private final TreePath path;
345                 private final int index;
346                 
347                 public MyDropLocation(JTree.DropLocation location) {
348                         this(location.getPath(), location.getChildIndex());
349                 }
350                 
351                 public MyDropLocation(TreePath path, int index) {
352                         this.path = path;
353                         this.index = index;
354                 }
355                 
356         }
357 }