create changelog entry
[debian/openrocket] / core / 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.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;
22
23 /**
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).
26  * 
27  * @author Sampo Niskanen <sampo.niskanen@iki.fi>
28  */
29 public class ComponentTreeTransferHandler extends TransferHandler {
30         
31         private static final LogHelper log = Application.getLogger();
32         
33         private final OpenRocketDocument document;
34         
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                 // 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.");
98                                 allowed = false;
99                                 break;
100                         }
101                         path = path.getParent();
102                 }
103                 
104                 // If drag-dropping to another rocket always copy
105                 if (support.getDropAction() == MOVE && data.srcParent.getRoot() != data.destParent.getRoot()) {
106                         support.setDropAction(COPY);
107                 }
108                 
109                 return allowed;
110         }
111         
112         
113         @Override
114         public boolean importData(TransferHandler.TransferSupport support) {
115                 
116                 // We currently only support drop, not paste
117                 if (!support.isDrop()) {
118                         log.warn("Import action is not a drop action");
119                         return false;
120                 }
121                 
122                 // Sun JRE silently ignores any RuntimeExceptions in importData, yeech!
123                 try {
124                         
125                         SourceTarget data = getSourceAndTarget(support);
126                         
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;
133                         }
134                         
135                         
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);
140                                 return false;
141                         }
142                         
143                         
144                         switch (action) {
145                         case MOVE:
146                                 log.user("Performing DnD move operation: " + data);
147                                 
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) {
151                                         index--;
152                                 }
153                                 
154                                 // Mark undo and freeze rocket.  src and dest are in same rocket, need to freeze only one
155                                 try {
156                                         document.startUndo("Move component");
157                                         try {
158                                                 data.srcParent.getRocket().freeze();
159                                                 data.srcParent.removeChild(data.srcIndex);
160                                                 data.destParent.addChild(data.child, index);
161                                         } finally {
162                                                 data.srcParent.getRocket().thaw();
163                                         }
164                                 } finally {
165                                         document.stopUndo();
166                                 }
167                                 return true;
168                                 
169                         case COPY:
170                                 log.user("Performing DnD copy operation: " + data);
171                                 RocketComponent copy = data.child.copy();
172                                 try {
173                                         document.startUndo("Copy component");
174                                         data.destParent.addChild(copy, data.destIndex);
175                                 } finally {
176                                         document.stopUndo();
177                                 }
178                                 return true;
179                                 
180                         default:
181                                 log.warn("Unknown transfer action " + action);
182                                 return false;
183                         }
184                         
185                 } catch (final RuntimeException e) {
186                         // Open error dialog later if an exception has occurred
187                         SwingUtilities.invokeLater(new Runnable() {
188                                 @Override
189                                 public void run() {
190                                         Application.getExceptionHandler().handleErrorCondition(e);
191                                 }
192                         });
193                         return false;
194                 }
195         }
196         
197         
198         
199         /**
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.
202          * 
203          * @param support       the transfer support
204          * @return                      the source and targer, or <code>null</code> if invalid.
205          */
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");
210                         return null;
211                 }
212                 
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()));
217                         return null;
218                 }
219                 
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");
224                         return null;
225                 }
226                 MyDropLocation location = convertDropLocation((JTree) support.getComponent(), dl);
227                 
228                 
229                 // Fetch the transferred component (child component)
230                 Transferable transferable = support.getTransferable();
231                 RocketComponent child;
232                 
233                 try {
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);
240                 }
241                 
242                 
243                 // Get the source component & index
244                 RocketComponent srcParent = child.getParent();
245                 if (srcParent == null) {
246                         log.debug("Attempting to drag root component");
247                         return null;
248                 }
249                 int srcIndex = srcParent.getChildPosition(child);
250                 
251                 
252                 // Get destination component & index
253                 RocketComponent destParent = ComponentTreeModel.componentFromPath(location.path);
254                 int destIndex = location.index;
255                 if (destIndex < 0) {
256                         destIndex = 0;
257                 }
258                 
259                 return new SourceTarget(srcParent, srcIndex, destParent, destIndex, child);
260         }
261         
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;
268                 
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;
275                         this.child = child;
276                 }
277                 
278                 @Override
279                 public String toString() {
280                         return "[" +
281                                         "srcParent=" + srcParent.getComponentName() +
282                                         ", srcIndex=" + srcIndex +
283                                         ", destParent=" + destParent.getComponentName() +
284                                         ", destIndex=" + destIndex +
285                                         ", child=" + child.getComponentName() +
286                                         "]";
287                 }
288                 
289         }
290         
291         
292         
293         /**
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).
296          * <p>
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
299          * requirements:
300          * <ul>
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
303          * </ul>
304          * <p>
305          * This does not fix the visual clue provided to the user, but fixes the actual drop location.
306          * 
307          * @param tree          the JTree in question
308          * @param location      the original drop location
309          * @return                      the updated drop location
310          */
311         private MyDropLocation convertDropLocation(JTree tree, JTree.DropLocation location) {
312                 
313                 final TreePath originalPath = location.getPath();
314                 final int originalIndex = location.getChildIndex();
315                 
316                 if (originalPath == null || originalIndex <= 0) {
317                         return new MyDropLocation(location);
318                 }
319                 
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);
325                 }
326                 
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);
332                 }
333                 
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) {
338                                 inAncestry = true;
339                                 break;
340                         }
341                 }
342                 if (!inAncestry) {
343                         return new MyDropLocation(location);
344                 }
345                 
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;
350                 
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);
355                 
356                 return new MyDropLocation(correctInsertPath, correctInsertIndex);
357         }
358         
359         private class MyDropLocation {
360                 private final TreePath path;
361                 private final int index;
362                 
363                 public MyDropLocation(JTree.DropLocation location) {
364                         this(location.getPath(), location.getChildIndex());
365                 }
366                 
367                 public MyDropLocation(TreePath path, int index) {
368                         this.path = path;
369                         this.index = index;
370                 }
371                 
372         }
373 }