language selector, bug fixed
[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                 // We currently only support drop, not paste
107                 if (!support.isDrop()) {
108                         log.warn("Import action is not a drop action");
109                         return false;
110                 }
111                 
112                 // Sun JRE silently ignores any RuntimeExceptions in importData, yeech!
113                 try {
114                         
115                         SourceTarget data = getSourceAndTarget(support);
116                         
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;
123                         }
124                         
125
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);
130                                 return false;
131                         }
132                         
133
134                         switch (action) {
135                         case MOVE:
136                                 log.user("Performing DnD move operation: " + data);
137                                 
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) {
141                                         index--;
142                                 }
143                                 
144                                 // Mark undo and freeze rocket.  src and dest are in same rocket, need to freeze only one
145                                 try {
146                                         document.startUndo("Move component");
147                                         try {
148                                                 data.srcParent.getRocket().freeze();
149                                                 data.srcParent.removeChild(data.srcIndex);
150                                                 data.destParent.addChild(data.child, index);
151                                         } finally {
152                                                 data.srcParent.getRocket().thaw();
153                                         }
154                                 } finally {
155                                         document.stopUndo();
156                                 }
157                                 return true;
158                                 
159                         case COPY:
160                                 log.user("Performing DnD copy operation: " + data);
161                                 RocketComponent copy = data.child.copy();
162                                 try {
163                                         document.startUndo("Copy component");
164                                         data.destParent.addChild(copy, data.destIndex);
165                                 } finally {
166                                         document.stopUndo();
167                                 }
168                                 return true;
169                                 
170                         default:
171                                 log.warn("Unknown transfer action " + action);
172                                 return false;
173                         }
174                         
175                 } catch (final RuntimeException e) {
176                         // Open error dialog later if an exception has occurred
177                         SwingUtilities.invokeLater(new Runnable() {
178                                 @Override
179                                 public void run() {
180                                         ExceptionHandler.handleErrorCondition(e);
181                                 }
182                         });
183                         return false;
184                 }
185         }
186         
187         
188
189         /**
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.
192          * 
193          * @param support       the transfer support
194          * @return                      the source and targer, or <code>null</code> if invalid.
195          */
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");
200                         return null;
201                 }
202                 
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()));
207                         return null;
208                 }
209                 
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");
214                         return null;
215                 }
216                 MyDropLocation location = convertDropLocation((JTree) support.getComponent(), dl);
217                 
218
219                 // Fetch the transferred component (child component)
220                 Transferable transferable = support.getTransferable();
221                 RocketComponent child;
222                 
223                 try {
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);
230                 }
231                 
232
233                 // Get the source component & index
234                 RocketComponent srcParent = child.getParent();
235                 if (srcParent == null) {
236                         log.debug("Attempting to drag root component");
237                         return null;
238                 }
239                 int srcIndex = srcParent.getChildPosition(child);
240                 
241
242                 // Get destination component & index
243                 RocketComponent destParent = ComponentTreeModel.componentFromPath(location.path);
244                 int destIndex = location.index;
245                 if (destIndex < 0) {
246                         destIndex = 0;
247                 }
248                 
249                 return new SourceTarget(srcParent, srcIndex, destParent, destIndex, child);
250         }
251         
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;
258                 
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;
265                         this.child = child;
266                 }
267                 
268                 @Override
269                 public String toString() {
270                         return "[" +
271                                         "srcParent=" + srcParent.getComponentName() +
272                                         ", srcIndex=" + srcIndex +
273                                         ", destParent=" + destParent.getComponentName() +
274                                         ", destIndex=" + destIndex +
275                                         ", child=" + child.getComponentName() +
276                                         "]";
277                 }
278                 
279         }
280         
281         
282
283         /**
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).
286          * <p>
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
289          * requirements:
290          * <ul>
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
293          * </ul>
294          * <p>
295          * This does not fix the visual clue provided to the user, but fixes the actual drop location.
296          * 
297          * @param tree          the JTree in question
298          * @param location      the original drop location
299          * @return                      the updated drop location
300          */
301         private MyDropLocation convertDropLocation(JTree tree, JTree.DropLocation location) {
302                 
303                 final TreePath originalPath = location.getPath();
304                 final int originalIndex = location.getChildIndex();
305                 
306                 if (originalPath == null || originalIndex <= 0) {
307                         return new MyDropLocation(location);
308                 }
309                 
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);
315                 }
316                 
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);
322                 }
323                 
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) {
328                                 inAncestry = true;
329                                 break;
330                         }
331                 }
332                 if (!inAncestry) {
333                         return new MyDropLocation(location);
334                 }
335                 
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;
340                 
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);
345                 
346                 return new MyDropLocation(correctInsertPath, correctInsertIndex);
347         }
348         
349         private class MyDropLocation {
350                 private final TreePath path;
351                 private final int index;
352                 
353                 public MyDropLocation(JTree.DropLocation location) {
354                         this(location.getPath(), location.getChildIndex());
355                 }
356                 
357                 public MyDropLocation(TreePath path, int index) {
358                         this.path = path;
359                         this.index = index;
360                 }
361                 
362         }
363 }