4855112b77482f12cfe1652712dba3fb0acab3df
[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          * Sole constructor.
37          * 
38          * @param document      the document this handler will drop to, used for undo actions.
39          */
40         public ComponentTreeTransferHandler(OpenRocketDocument document) {
41                 this.document = document;
42         }
43         
44         @Override
45         public int getSourceActions(JComponent comp) {
46                 return COPY_OR_MOVE;
47         }
48         
49         @Override
50         public Transferable createTransferable(JComponent component) {
51                 if (!(component instanceof JTree)) {
52                         throw new BugException("TransferHandler called with component " + component);
53                 }
54                 
55                 JTree tree = (JTree) component;
56                 TreePath path = tree.getSelectionPath();
57                 if (path == null) {
58                         return null;
59                 }
60                 
61                 RocketComponent c = ComponentTreeModel.componentFromPath(path);
62                 if (c instanceof Rocket) {
63                         log.info("Attempting to create transferable from Rocket");
64                         return null;
65                 }
66                 
67                 log.info("Creating transferable from component " + c.getComponentName());
68                 return new RocketComponentTransferable(c);
69         }
70         
71         
72
73
74         @Override
75         public void exportDone(JComponent comp, Transferable trans, int action) {
76                 // Removal from the old place is implemented already in import, so do nothing
77         }
78         
79         
80
81         @Override
82         public boolean canImport(TransferHandler.TransferSupport support) {
83                 SourceTarget data = getSourceAndTarget(support);
84                 
85                 if (data == null) {
86                         return false;
87                 }
88                 
89                 boolean allowed = data.destParent.isCompatible(data.child);
90                 log.verbose("Checking validity of drag-drop " + data.toString() + " allowed:" + allowed);
91                 
92                 // If drag-dropping to another rocket always copy
93                 if (support.getDropAction() == MOVE && data.srcParent.getRoot() != data.destParent.getRoot()) {
94                         support.setDropAction(COPY);
95                 }
96                 
97                 return allowed;
98         }
99         
100         
101
102         @Override
103         public boolean importData(TransferHandler.TransferSupport support) {
104                 
105                 // We currently only support drop, not paste
106                 if (!support.isDrop()) {
107                         log.warn("Import action is not a drop action");
108                         return false;
109                 }
110                 
111                 // Sun JRE silently ignores any RuntimeExceptions in importData, yeech!
112                 try {
113                         
114                         SourceTarget data = getSourceAndTarget(support);
115                         
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;
122                         }
123                         
124
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);
129                                 return false;
130                         }
131                         
132
133                         switch (action) {
134                         case MOVE:
135                                 log.user("Performing DnD move operation: " + data);
136                                 
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) {
140                                         index--;
141                                 }
142                                 
143                                 // Mark undo and freeze rocket.  src and dest are in same rocket, need to freeze only one
144                                 try {
145                                         document.startUndo("Move component");
146                                         try {
147                                                 data.srcParent.getRocket().freeze();
148                                                 data.srcParent.removeChild(data.srcIndex);
149                                                 data.destParent.addChild(data.child, index);
150                                         } finally {
151                                                 data.srcParent.getRocket().thaw();
152                                         }
153                                 } finally {
154                                         document.stopUndo();
155                                 }
156                                 return true;
157                                 
158                         case COPY:
159                                 log.user("Performing DnD copy operation: " + data);
160                                 RocketComponent copy = data.child.copy();
161                                 try {
162                                         document.startUndo("Copy component");
163                                         data.destParent.addChild(copy, data.destIndex);
164                                 } finally {
165                                         document.stopUndo();
166                                 }
167                                 return true;
168                                 
169                         default:
170                                 log.warn("Unknown transfer action " + action);
171                                 return false;
172                         }
173                         
174                 } catch (final RuntimeException e) {
175                         // Open error dialog later if an exception has occurred
176                         SwingUtilities.invokeLater(new Runnable() {
177                                 @Override
178                                 public void run() {
179                                         Application.getExceptionHandler().handleErrorCondition(e);
180                                 }
181                         });
182                         return false;
183                 }
184         }
185         
186         
187
188         /**
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.
191          * 
192          * @param support       the transfer support
193          * @return                      the source and targer, or <code>null</code> if invalid.
194          */
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");
199                         return null;
200                 }
201                 
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()));
206                         return null;
207                 }
208                 
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");
213                         return null;
214                 }
215                 MyDropLocation location = convertDropLocation((JTree) support.getComponent(), dl);
216                 
217
218                 // Fetch the transferred component (child component)
219                 Transferable transferable = support.getTransferable();
220                 RocketComponent child;
221                 
222                 try {
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);
229                 }
230                 
231
232                 // Get the source component & index
233                 RocketComponent srcParent = child.getParent();
234                 if (srcParent == null) {
235                         log.debug("Attempting to drag root component");
236                         return null;
237                 }
238                 int srcIndex = srcParent.getChildPosition(child);
239                 
240
241                 // Get destination component & index
242                 RocketComponent destParent = ComponentTreeModel.componentFromPath(location.path);
243                 int destIndex = location.index;
244                 if (destIndex < 0) {
245                         destIndex = 0;
246                 }
247                 
248                 return new SourceTarget(srcParent, srcIndex, destParent, destIndex, child);
249         }
250         
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;
257                 
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;
264                         this.child = child;
265                 }
266                 
267                 @Override
268                 public String toString() {
269                         return "[" +
270                                         "srcParent=" + srcParent.getComponentName() +
271                                         ", srcIndex=" + srcIndex +
272                                         ", destParent=" + destParent.getComponentName() +
273                                         ", destIndex=" + destIndex +
274                                         ", child=" + child.getComponentName() +
275                                         "]";
276                 }
277                 
278         }
279         
280         
281
282         /**
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).
285          * <p>
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
288          * requirements:
289          * <ul>
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
292          * </ul>
293          * <p>
294          * This does not fix the visual clue provided to the user, but fixes the actual drop location.
295          * 
296          * @param tree          the JTree in question
297          * @param location      the original drop location
298          * @return                      the updated drop location
299          */
300         private MyDropLocation convertDropLocation(JTree tree, JTree.DropLocation location) {
301                 
302                 final TreePath originalPath = location.getPath();
303                 final int originalIndex = location.getChildIndex();
304                 
305                 if (originalPath == null || originalIndex <= 0) {
306                         return new MyDropLocation(location);
307                 }
308                 
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);
314                 }
315                 
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);
321                 }
322                 
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) {
327                                 inAncestry = true;
328                                 break;
329                         }
330                 }
331                 if (!inAncestry) {
332                         return new MyDropLocation(location);
333                 }
334                 
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;
339                 
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);
344                 
345                 return new MyDropLocation(correctInsertPath, correctInsertIndex);
346         }
347         
348         private class MyDropLocation {
349                 private final TreePath path;
350                 private final int index;
351                 
352                 public MyDropLocation(JTree.DropLocation location) {
353                         this(location.getPath(), location.getChildIndex());
354                 }
355                 
356                 public MyDropLocation(TreePath path, int index) {
357                         this.path = path;
358                         this.index = index;
359                 }
360                 
361         }
362 }