Classes in this File | Line Coverage | Branch Coverage | Complexity | ||||
DnDExplorerTree |
|
| 6.8;6.8 | ||||
DnDExplorerTree$ArgoDropTargetListener |
|
| 6.8;6.8 | ||||
DnDExplorerTree$ArgoDropTargetListener$1 |
|
| 6.8;6.8 | ||||
DnDExplorerTree$DnDTreeSelectionListener |
|
| 6.8;6.8 |
1 | /* $Id: DnDExplorerTree.java 17843 2010-01-12 19:23:29Z linus $ | |
2 | ***************************************************************************** | |
3 | * Copyright (c) 2009 Contributors - see below | |
4 | * All rights reserved. This program and the accompanying materials | |
5 | * are made available under the terms of the Eclipse Public License v1.0 | |
6 | * which accompanies this distribution, and is available at | |
7 | * http://www.eclipse.org/legal/epl-v10.html | |
8 | * | |
9 | * Contributors: | |
10 | * tfmorris | |
11 | ***************************************************************************** | |
12 | * | |
13 | * Some portions of this file was previously release using the BSD License: | |
14 | */ | |
15 | ||
16 | // Copyright (c) 1996-2008 The Regents of the University of California. All | |
17 | // Rights Reserved. Permission to use, copy, modify, and distribute this | |
18 | // software and its documentation without fee, and without a written | |
19 | // agreement is hereby granted, provided that the above copyright notice | |
20 | // and this paragraph appear in all copies. This software program and | |
21 | // documentation are copyrighted by The Regents of the University of | |
22 | // California. The software program and documentation are supplied "AS | |
23 | // IS", without any accompanying services from The Regents. The Regents | |
24 | // does not warrant that the operation of the program will be | |
25 | // uninterrupted or error-free. The end-user understands that the program | |
26 | // was developed for research purposes and is advised not to rely | |
27 | // exclusively on the program for any reason. IN NO EVENT SHALL THE | |
28 | // UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT, | |
29 | // SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING LOST PROFITS, | |
30 | // ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF | |
31 | // THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE POSSIBILITY OF | |
32 | // SUCH DAMAGE. THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY | |
33 | // WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF | |
34 | // MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE | |
35 | // PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THE UNIVERSITY OF | |
36 | // CALIFORNIA HAS NO OBLIGATIONS TO PROVIDE MAINTENANCE, SUPPORT, | |
37 | // UPDATES, ENHANCEMENTS, OR MODIFICATIONS. | |
38 | ||
39 | package org.argouml.ui.explorer; | |
40 | ||
41 | import java.awt.AlphaComposite; | |
42 | import java.awt.Color; | |
43 | import java.awt.GradientPaint; | |
44 | import java.awt.Graphics2D; | |
45 | import java.awt.Insets; | |
46 | import java.awt.Point; | |
47 | import java.awt.Rectangle; | |
48 | import java.awt.SystemColor; | |
49 | import java.awt.datatransfer.Transferable; | |
50 | import java.awt.datatransfer.UnsupportedFlavorException; | |
51 | import java.awt.dnd.Autoscroll; | |
52 | import java.awt.dnd.DnDConstants; | |
53 | import java.awt.dnd.DragGestureEvent; | |
54 | import java.awt.dnd.DragGestureListener; | |
55 | import java.awt.dnd.DragGestureRecognizer; | |
56 | import java.awt.dnd.DragSource; | |
57 | import java.awt.dnd.DragSourceDragEvent; | |
58 | import java.awt.dnd.DragSourceDropEvent; | |
59 | import java.awt.dnd.DragSourceEvent; | |
60 | import java.awt.dnd.DragSourceListener; | |
61 | import java.awt.dnd.DropTarget; | |
62 | import java.awt.dnd.DropTargetDragEvent; | |
63 | import java.awt.dnd.DropTargetDropEvent; | |
64 | import java.awt.dnd.DropTargetEvent; | |
65 | import java.awt.dnd.DropTargetListener; | |
66 | import java.awt.event.ActionEvent; | |
67 | import java.awt.event.ActionListener; | |
68 | import java.awt.event.InputEvent; | |
69 | import java.awt.geom.AffineTransform; | |
70 | import java.awt.geom.Rectangle2D; | |
71 | import java.awt.image.BufferedImage; | |
72 | import java.io.IOException; | |
73 | import java.util.ArrayList; | |
74 | import java.util.Collection; | |
75 | ||
76 | import javax.swing.Icon; | |
77 | import javax.swing.JLabel; | |
78 | import javax.swing.KeyStroke; | |
79 | import javax.swing.Timer; | |
80 | import javax.swing.event.TreeSelectionEvent; | |
81 | import javax.swing.event.TreeSelectionListener; | |
82 | import javax.swing.tree.DefaultMutableTreeNode; | |
83 | import javax.swing.tree.TreePath; | |
84 | ||
85 | import org.apache.log4j.Logger; | |
86 | import org.argouml.model.Model; | |
87 | import org.argouml.ui.TransferableModelElements; | |
88 | import org.argouml.ui.targetmanager.TargetManager; | |
89 | import org.argouml.uml.diagram.Relocatable; | |
90 | import org.argouml.uml.diagram.ui.ActionSaveDiagramToClipboard; | |
91 | ||
92 | /** | |
93 | * This class extends the default Argo JTree with Drag and drop capabilities.<p> | |
94 | * See <a | |
95 | * href="http://java.sun.com/j2se/1.4.2/docs/guide/dragndrop/spec/dnd1.html"> | |
96 | * dnd1</a> and <a | |
97 | * href="http://java.sun.com/products/jfc/tsc/articles/dragndrop/index.html"> | |
98 | * dnd2</a><p> | |
99 | * | |
100 | * And it adds the 'copy to clipboard' capability for diagrams. See | |
101 | * <a href="http://java.sun.com/j2se/1.3/docs/guide/swing/KeyBindChanges.html"> | |
102 | * KeyBindChanges</a><p> | |
103 | * | |
104 | * The ghosted images code originates from <p><a | |
105 | * href="http://www.javaworld.com/javaworld/javatips/jw-javatip114.html"> | |
106 | * javatip114</a><p> | |
107 | * | |
108 | * Interesting may also be the following: <p><a | |
109 | * href="http://forum.java.sun.com/thread.jspa?threadID=296255&start=30"> | |
110 | * thread</a> | |
111 | * | |
112 | * @author alexb | |
113 | * @since Created on 16 April 2003 | |
114 | */ | |
115 | 19 | public class DnDExplorerTree |
116 | extends ExplorerTree | |
117 | implements DragGestureListener, | |
118 | DragSourceListener, | |
119 | Autoscroll { | |
120 | /** | |
121 | * Logger. | |
122 | */ | |
123 | 900 | private static final Logger LOG = |
124 | Logger.getLogger(DnDExplorerTree.class); | |
125 | ||
126 | private static final String DIAGRAM_TO_CLIPBOARD_ACTION = | |
127 | "export Diagram as GIF"; | |
128 | ||
129 | /** | |
130 | * Where, in the drag image, the mouse was clicked. | |
131 | */ | |
132 | 900 | private Point clickOffset = new Point(); |
133 | /** | |
134 | * The path being dragged. | |
135 | */ | |
136 | private TreePath sourcePath; | |
137 | /** | |
138 | * The 'drag image'. | |
139 | */ | |
140 | private BufferedImage ghostImage; | |
141 | ||
142 | ||
143 | /** | |
144 | * The selected node. | |
145 | */ | |
146 | private TreePath selectedTreePath; | |
147 | ||
148 | /** | |
149 | * dnd source. | |
150 | */ | |
151 | private DragSource dragSource; | |
152 | ||
153 | /** | |
154 | * Creates a new instance of DnDArgoJTree. | |
155 | */ | |
156 | public DnDExplorerTree() { | |
157 | ||
158 | 900 | super(); |
159 | ||
160 | 900 | this.addTreeSelectionListener(new DnDTreeSelectionListener()); |
161 | ||
162 | 900 | dragSource = DragSource.getDefaultDragSource(); |
163 | ||
164 | /* | |
165 | * The drag gesture recognizer fires events | |
166 | * in response to drag gestures in a component. | |
167 | */ | |
168 | 900 | DragGestureRecognizer dgr = |
169 | dragSource | |
170 | .createDefaultDragGestureRecognizer( | |
171 | this, | |
172 | DnDConstants.ACTION_COPY_OR_MOVE, //specifies valid actions | |
173 | this); | |
174 | ||
175 | // Eliminates right mouse clicks as valid actions | |
176 | 900 | dgr.setSourceActions( |
177 | dgr.getSourceActions() & ~InputEvent.BUTTON3_MASK); | |
178 | ||
179 | // First argument: Component to associate the target with | |
180 | // Second argument: DropTargetListener | |
181 | 900 | new DropTarget(this, new ArgoDropTargetListener()); |
182 | ||
183 | 900 | KeyStroke ctrlC = KeyStroke.getKeyStroke("control C"); |
184 | 900 | this.getInputMap().put(ctrlC, DIAGRAM_TO_CLIPBOARD_ACTION); |
185 | 900 | this.getActionMap().put(DIAGRAM_TO_CLIPBOARD_ACTION, |
186 | new ActionSaveDiagramToClipboard()); | |
187 | 900 | } |
188 | ||
189 | /** | |
190 | * The drag gesture listener is notified of drag gestures by a recognizer. | |
191 | * The typical response is to initiate a drag by invoking | |
192 | * DragSource.startDrag(). | |
193 | * <p> | |
194 | * | |
195 | * TODO: find a way to show a different image when multiple elements are | |
196 | * dragged. | |
197 | * | |
198 | * @param dragGestureEvent | |
199 | * the DragGestureEvent describing the gesture that has just | |
200 | * occurred | |
201 | * @see java.awt.dnd.DragGestureListener#dragGestureRecognized(java.awt.dnd.DragGestureEvent) | |
202 | */ | |
203 | public void dragGestureRecognized( | |
204 | DragGestureEvent dragGestureEvent) { | |
205 | ||
206 | /* | |
207 | * Get the selected targets (UML ModelElements) | |
208 | * from the TargetManager. | |
209 | */ | |
210 | 0 | Collection targets = TargetManager.getInstance().getModelTargets(); |
211 | 0 | if (targets.size() < 1) { |
212 | 0 | return; |
213 | } | |
214 | 0 | if (LOG.isDebugEnabled()) { |
215 | 0 | LOG.debug("Drag: start transferring " + targets.size() |
216 | + " targets."); | |
217 | } | |
218 | 0 | TransferableModelElements tf = |
219 | new TransferableModelElements(targets); | |
220 | ||
221 | 0 | Point ptDragOrigin = dragGestureEvent.getDragOrigin(); |
222 | 0 | TreePath path = |
223 | getPathForLocation(ptDragOrigin.x, ptDragOrigin.y); | |
224 | 0 | if (path == null) { |
225 | 0 | return; |
226 | } | |
227 | 0 | Rectangle raPath = getPathBounds(path); |
228 | 0 | clickOffset.setLocation(ptDragOrigin.x - raPath.x, |
229 | ptDragOrigin.y - raPath.y); | |
230 | ||
231 | /* | |
232 | * Get the cell renderer (which is a JLabel) | |
233 | * for the path being dragged. | |
234 | */ | |
235 | 0 | JLabel lbl = |
236 | (JLabel) getCellRenderer().getTreeCellRendererComponent( | |
237 | this, // tree | |
238 | path.getLastPathComponent(), // value | |
239 | false, // isSelected (dont want a colored background) | |
240 | isExpanded(path), // isExpanded | |
241 | getModel().isLeaf(path.getLastPathComponent()), // isLeaf | |
242 | 0, // row (not important for rendering) | |
243 | false // hasFocus (dont want a focus rectangle) | |
244 | ); | |
245 | /* The layout manager would normally do this: */ | |
246 | 0 | lbl.setSize((int) raPath.getWidth(), (int) raPath.getHeight()); |
247 | ||
248 | // Get a buffered image of the selection for dragging a ghost image | |
249 | 0 | ghostImage = |
250 | new BufferedImage( | |
251 | (int) raPath.getWidth(), (int) raPath.getHeight(), | |
252 | BufferedImage.TYPE_INT_ARGB_PRE); | |
253 | 0 | Graphics2D g2 = ghostImage.createGraphics(); |
254 | ||
255 | /* | |
256 | * Ask the cell renderer to paint itself into the BufferedImage. | |
257 | * Make the image ghostlike. | |
258 | */ | |
259 | 0 | g2.setComposite(AlphaComposite.getInstance( |
260 | AlphaComposite.SRC, 0.5f)); | |
261 | 0 | lbl.paint(g2); |
262 | ||
263 | /* | |
264 | * Now paint a gradient UNDER the ghosted JLabel text | |
265 | * (but not under the icon if any). | |
266 | */ | |
267 | 0 | Icon icon = lbl.getIcon(); |
268 | 0 | int nStartOfText = |
269 | (icon == null) ? 0 | |
270 | : icon.getIconWidth() + lbl.getIconTextGap(); | |
271 | /* Make the gradient ghostlike: */ | |
272 | 0 | g2.setComposite(AlphaComposite.getInstance( |
273 | AlphaComposite.DST_OVER, 0.5f)); | |
274 | 0 | g2.setPaint(new GradientPaint(nStartOfText, 0, |
275 | SystemColor.controlShadow, | |
276 | getWidth(), 0, new Color(255, 255, 255, 0))); | |
277 | 0 | g2.fillRect(nStartOfText, 0, getWidth(), ghostImage.getHeight()); |
278 | ||
279 | 0 | g2.dispose(); |
280 | ||
281 | /* | |
282 | * Remember the path being dragged (because if it is being moved, | |
283 | * we will have to delete it later). | |
284 | */ | |
285 | 0 | sourcePath = path; |
286 | ||
287 | /* | |
288 | * We pass our drag image just in case | |
289 | * it IS supported by the platform. | |
290 | */ | |
291 | 0 | dragGestureEvent.startDrag(null, ghostImage, |
292 | new Point(5, 5), tf, this); | |
293 | 0 | } |
294 | ||
295 | private boolean isValidDrag(TreePath destinationPath, | |
296 | Transferable tf) { | |
297 | 0 | if (destinationPath == null) { |
298 | 0 | LOG.debug("No valid Drag: no destination found."); |
299 | 0 | return false; |
300 | } | |
301 | 0 | if (selectedTreePath.isDescendant(destinationPath)) { |
302 | 0 | LOG.debug("No valid Drag: move to descendent."); |
303 | 0 | return false; |
304 | } | |
305 | 0 | if (!tf.isDataFlavorSupported( |
306 | TransferableModelElements.UML_COLLECTION_FLAVOR)) { | |
307 | 0 | LOG.debug("No valid Drag: flavor not supported."); |
308 | 0 | return false; |
309 | } | |
310 | 0 | Object dest = |
311 | ((DefaultMutableTreeNode) destinationPath | |
312 | .getLastPathComponent()).getUserObject(); | |
313 | ||
314 | /* TODO: support other types of drag. | |
315 | * Here you set the owner by dragging into a namespace. | |
316 | * An alternative could be to drag states into composite states... | |
317 | */ | |
318 | ||
319 | /* If the destination is not a NameSpace, then abort: */ | |
320 | 0 | if (!Model.getFacade().isANamespace(dest)) { |
321 | 0 | LOG.debug("No valid Drag: not a namespace."); |
322 | 0 | return false; |
323 | } | |
324 | ||
325 | /* We are sure "dest" is a Namespace now. */ | |
326 | 0 | if (Model.getModelManagementHelper().isReadOnly(dest)) { |
327 | 0 | LOG.debug("No valid Drag: " |
328 | + "this is not an editable UML element (profile?)."); | |
329 | 0 | return false; |
330 | } | |
331 | ||
332 | /* If the destination is a DataType, then abort: */ | |
333 | ||
334 | // TODO: Any Namespace can contain other elements. Why don't we allow | |
335 | // this? - tfm | |
336 | /* | |
337 | * MVW: These are the WFRs for DataType: | |
338 | * [1] A DataType can only contain Operations, | |
339 | * which all must be queries. | |
340 | * self.allFeatures->forAll(f | | |
341 | * f.oclIsKindOf(Operation) and f.oclAsType(Operation).isQuery) | |
342 | * [2] A DataType cannot contain any other ModelElements. | |
343 | * self.allContents->isEmpty | |
344 | * IMHO we should enforce these WFRs here. | |
345 | * ... so it is still possible to copy or move query operations, | |
346 | * hence we should allow this. | |
347 | */ | |
348 | 0 | if (Model.getFacade().isADataType(dest)) { |
349 | 0 | LOG.debug("No valid Drag: destination is a DataType."); |
350 | 0 | return false; |
351 | } | |
352 | ||
353 | /* | |
354 | * Let's check all dragged elements - if one of these | |
355 | * may be dropped, then the drag is valid. | |
356 | * The others will be ignored when dropping. | |
357 | */ | |
358 | try { | |
359 | 0 | Collection transfers = |
360 | (Collection) tf.getTransferData( | |
361 | TransferableModelElements.UML_COLLECTION_FLAVOR); | |
362 | 0 | for (Object element : transfers) { |
363 | 0 | if (Model.getFacade().isAUMLElement(element)) { |
364 | 0 | if (!Model.getModelManagementHelper().isReadOnly(element)) { |
365 | 0 | if (Model.getFacade().isAModelElement(dest) |
366 | && Model.getFacade().isANamespace(element) | |
367 | && Model.getCoreHelper().isValidNamespace( | |
368 | element, dest)) { | |
369 | 0 | LOG.debug("Valid Drag: namespace " + dest); |
370 | 0 | return true; |
371 | } | |
372 | 0 | if (Model.getFacade().isAFeature(element) |
373 | && Model.getFacade().isAClassifier(dest)) { | |
374 | 0 | return true; |
375 | } | |
376 | } | |
377 | } | |
378 | 0 | if (element instanceof Relocatable) { |
379 | 0 | Relocatable d = (Relocatable) element; |
380 | 0 | if (d.isRelocationAllowed(dest)) { |
381 | 0 | LOG.debug("Valid Drag: diagram " + dest); |
382 | 0 | return true; |
383 | } | |
384 | 0 | } |
385 | } | |
386 | 0 | } catch (UnsupportedFlavorException e) { |
387 | 0 | LOG.debug(e); |
388 | 0 | } catch (IOException e) { |
389 | 0 | LOG.debug(e); |
390 | 0 | } |
391 | 0 | LOG.debug("No valid Drag: not a valid namespace."); |
392 | 0 | return false; |
393 | } | |
394 | ||
395 | /* | |
396 | * @see java.awt.dnd.DragSourceListener#dragDropEnd(java.awt.dnd.DragSourceDropEvent) | |
397 | */ | |
398 | public void dragDropEnd( | |
399 | DragSourceDropEvent dragSourceDropEvent) { | |
400 | 0 | sourcePath = null; |
401 | 0 | ghostImage = null; |
402 | 0 | } |
403 | ||
404 | /* | |
405 | * @see java.awt.dnd.DragSourceListener#dragEnter(java.awt.dnd.DragSourceDragEvent) | |
406 | */ | |
407 | public void dragEnter(DragSourceDragEvent dragSourceDragEvent) { | |
408 | // empty implementation - not used. | |
409 | 0 | } |
410 | ||
411 | /* | |
412 | * @see java.awt.dnd.DragSourceListener#dragExit(java.awt.dnd.DragSourceEvent) | |
413 | */ | |
414 | public void dragExit(DragSourceEvent dragSourceEvent) { | |
415 | // empty implementation - not used. | |
416 | 0 | } |
417 | ||
418 | /* | |
419 | * This is not the correct location to set the cursor. | |
420 | * The commented out code illustrates the calculation | |
421 | * of coordinates. | |
422 | * | |
423 | * @see java.awt.dnd.DragSourceListener#dragOver(java.awt.dnd.DragSourceDragEvent) | |
424 | */ | |
425 | public void dragOver(DragSourceDragEvent dragSourceDragEvent) { | |
426 | // Transferable tf = | |
427 | // dragSourceDragEvent.getDragSourceContext().getTransferable(); | |
428 | // /* This is the mouse location on the screen: */ | |
429 | // Point dragLoc = dragSourceDragEvent.getLocation(); | |
430 | // /* This is the JTree location on the screen: */ | |
431 | // Point treeLoc = getLocationOnScreen(); | |
432 | // /* Now substract to find the location within the JTree: */ | |
433 | // dragLoc.translate(- treeLoc.x, - treeLoc.y); | |
434 | // TreePath destinationPath = | |
435 | // getPathForLocation(dragLoc.x, dragLoc.y); | |
436 | // if (isValidDrag(destinationPath, tf)) { | |
437 | //// dragSourceDragEvent.getDragSourceContext() | |
438 | //// .setCursor(DragSource.DefaultMoveDrop); | |
439 | // } else { | |
440 | //// dragSourceDragEvent.getDragSourceContext() | |
441 | //// .setCursor(DragSource.DefaultCopyNoDrop); | |
442 | // } | |
443 | 0 | } |
444 | ||
445 | /* | |
446 | * @see java.awt.dnd.DragSourceListener#dropActionChanged(java.awt.dnd.DragSourceDragEvent) | |
447 | */ | |
448 | public void dropActionChanged( | |
449 | DragSourceDragEvent dragSourceDragEvent) { | |
450 | // empty implementation - not used. | |
451 | 0 | } |
452 | ||
453 | /** | |
454 | * records the selected path for later use during dnd. | |
455 | */ | |
456 | 900 | class DnDTreeSelectionListener implements TreeSelectionListener { |
457 | public void valueChanged( | |
458 | TreeSelectionEvent treeSelectionEvent) { | |
459 | 19 | selectedTreePath = treeSelectionEvent.getNewLeadSelectionPath(); |
460 | 19 | } |
461 | } | |
462 | ||
463 | ||
464 | // Autoscroll Interface... | |
465 | // The following code was borrowed from the book: | |
466 | // Java Swing | |
467 | // By Robert Eckstein, Marc Loy & Dave Wood | |
468 | // Paperback - 1221 pages 1 Ed edition (September 1998) | |
469 | // O'Reilly & Associates; ISBN: 156592455X | |
470 | // | |
471 | // The relevant chapter of which can be found at: | |
472 | // http://www.oreilly.com/catalog/jswing/chapter/dnd.beta.pdf | |
473 | ||
474 | private static final int AUTOSCROLL_MARGIN = 12; | |
475 | ||
476 | /* | |
477 | * Ok, we've been told to scroll because the mouse cursor is in our | |
478 | * scroll zone. | |
479 | * @see java.awt.dnd.Autoscroll#autoscroll(java.awt.Point) | |
480 | */ | |
481 | public void autoscroll(Point pt) { | |
482 | // Figure out which row we're on. | |
483 | 0 | int nRow = getRowForLocation(pt.x, pt.y); |
484 | ||
485 | // If we are not on a row then ignore this autoscroll request | |
486 | 0 | if (nRow < 0) { |
487 | 0 | return; |
488 | } | |
489 | ||
490 | 0 | Rectangle raOuter = getBounds(); |
491 | // Now decide if the row is at the top of the screen or at the | |
492 | // bottom. We do this to make the previous row (or the next | |
493 | // row) visible as appropriate. If were at the absolute top or | |
494 | // bottom, just return the first or last row respectively. | |
495 | ||
496 | // Is row at top of screen? | |
497 | 0 | nRow = |
498 | (pt.y + raOuter.y <= AUTOSCROLL_MARGIN) | |
499 | ? | |
500 | // Yes, scroll up one row | |
501 | (nRow <= 0 ? 0 : nRow - 1) | |
502 | : | |
503 | // No, scroll down one row | |
504 | (nRow < getRowCount() - 1 ? nRow + 1 : nRow); | |
505 | ||
506 | 0 | scrollRowToVisible(nRow); |
507 | 0 | } |
508 | ||
509 | /* | |
510 | * Calculate the insets for the *JTREE*, not the viewport the tree is in. | |
511 | * This makes it a bit messy. | |
512 | * | |
513 | * @see java.awt.dnd.Autoscroll#getAutoscrollInsets() | |
514 | */ | |
515 | public Insets getAutoscrollInsets() { | |
516 | 0 | Rectangle raOuter = getBounds(); |
517 | 0 | Rectangle raInner = getParent().getBounds(); |
518 | 0 | return new Insets( |
519 | raInner.y - raOuter.y + AUTOSCROLL_MARGIN, | |
520 | raInner.x - raOuter.x + AUTOSCROLL_MARGIN, | |
521 | raOuter.height - raInner.height | |
522 | - raInner.y + raOuter.y + AUTOSCROLL_MARGIN, | |
523 | raOuter.width - raInner.width | |
524 | - raInner.x + raOuter.x + AUTOSCROLL_MARGIN); | |
525 | } | |
526 | ||
527 | /** | |
528 | * The DropTargetListener. | |
529 | * Handles drop target events including the drop itself. | |
530 | */ | |
531 | 0 | class ArgoDropTargetListener implements DropTargetListener { |
532 | ||
533 | private TreePath lastPath; | |
534 | 900 | private Rectangle2D cueLine = new Rectangle2D.Float(); |
535 | 900 | private Rectangle2D ghostRectangle = new Rectangle2D.Float(); |
536 | private Color cueLineColor; | |
537 | 900 | private Point lastMouseLocation = new Point(); |
538 | private Timer hoverTimer; | |
539 | ||
540 | /** | |
541 | * The constructor. | |
542 | */ | |
543 | 900 | public ArgoDropTargetListener() { |
544 | 900 | cueLineColor = |
545 | new Color( | |
546 | SystemColor.controlShadow.getRed(), | |
547 | SystemColor.controlShadow.getGreen(), | |
548 | SystemColor.controlShadow.getBlue(), | |
549 | 64 | |
550 | ); | |
551 | ||
552 | /* Set up a hover timer, so that a node will be | |
553 | * automatically expanded or collapsed | |
554 | * if the user lingers on it for more than a short time. | |
555 | */ | |
556 | 900 | hoverTimer = |
557 | 900 | new Timer(1000, new ActionListener() { |
558 | /* | |
559 | * @see java.awt.event.ActionListener#actionPerformed(java.awt.event.ActionEvent) | |
560 | */ | |
561 | public void actionPerformed(ActionEvent e) { | |
562 | 0 | if (getPathForRow(0).equals/*isRootPath*/(lastPath)) { |
563 | 0 | return; |
564 | } | |
565 | 0 | if (isExpanded(lastPath)) { |
566 | 0 | collapsePath(lastPath); |
567 | } else { | |
568 | 0 | expandPath(lastPath); |
569 | } | |
570 | 0 | } |
571 | }); | |
572 | 900 | hoverTimer.setRepeats(false); // Set timer to one-shot mode |
573 | 900 | } |
574 | ||
575 | /* | |
576 | * @see java.awt.dnd.DropTargetListener#dragEnter(java.awt.dnd.DropTargetDragEvent) | |
577 | */ | |
578 | public void dragEnter( | |
579 | DropTargetDragEvent dropTargetDragEvent) { | |
580 | 0 | LOG.debug("dragEnter"); |
581 | 0 | if (!isDragAcceptable(dropTargetDragEvent)) { |
582 | 0 | dropTargetDragEvent.rejectDrag(); |
583 | } else { | |
584 | 0 | dropTargetDragEvent.acceptDrag( |
585 | dropTargetDragEvent.getDropAction()); | |
586 | } | |
587 | 0 | } |
588 | ||
589 | /* | |
590 | * @see java.awt.dnd.DropTargetListener#dragExit(java.awt.dnd.DropTargetEvent) | |
591 | */ | |
592 | public void dragExit(DropTargetEvent dropTargetEvent) { | |
593 | 0 | LOG.debug("dragExit"); |
594 | 0 | if (!DragSource.isDragImageSupported()) { |
595 | 0 | repaint(ghostRectangle.getBounds()); |
596 | } | |
597 | 0 | } |
598 | ||
599 | /** | |
600 | * Called when a drag operation is ongoing, while the mouse pointer | |
601 | * is still over the operable part of the drop site | |
602 | * for the <code>DropTarget</code> registered with this listener. | |
603 | * | |
604 | * @see java.awt.dnd.DropTargetListener#dragOver(java.awt.dnd.DropTargetDragEvent) | |
605 | */ | |
606 | public void dragOver(DropTargetDragEvent dropTargetDragEvent) { | |
607 | 0 | Point pt = dropTargetDragEvent.getLocation(); |
608 | 0 | if (pt.equals(lastMouseLocation)) { |
609 | 0 | return; |
610 | } | |
611 | /* Many many of these events .. this slows things down: */ | |
612 | // LOG.debug("dragOver"); | |
613 | ||
614 | 0 | lastMouseLocation = pt; |
615 | ||
616 | 0 | Graphics2D g2 = (Graphics2D) getGraphics(); |
617 | ||
618 | /* | |
619 | * The next condition becomes false when dragging in | |
620 | * something from another application. | |
621 | */ | |
622 | 0 | if (ghostImage != null) { |
623 | /* | |
624 | * If a drag image is not supported by the platform, | |
625 | * then draw my own drag image. | |
626 | */ | |
627 | 0 | if (!DragSource.isDragImageSupported()) { |
628 | /* Rub out the last ghost image and cue line: */ | |
629 | 0 | paintImmediately(ghostRectangle.getBounds()); |
630 | /* And remember where we are about to draw | |
631 | * the new ghost image: | |
632 | */ | |
633 | 0 | ghostRectangle.setRect(pt.x - clickOffset.x, |
634 | pt.y - clickOffset.y, | |
635 | ghostImage.getWidth(), | |
636 | ghostImage.getHeight()); | |
637 | 0 | g2.drawImage(ghostImage, |
638 | AffineTransform.getTranslateInstance( | |
639 | ghostRectangle.getX(), | |
640 | ghostRectangle.getY()), null); | |
641 | } else { | |
642 | // Just rub out the last cue line | |
643 | 0 | paintImmediately(cueLine.getBounds()); |
644 | } | |
645 | } | |
646 | ||
647 | 0 | TreePath path = getPathForLocation(pt.x, pt.y); |
648 | 0 | if (!(path == lastPath)) { |
649 | 0 | lastPath = path; |
650 | 0 | hoverTimer.restart(); |
651 | } | |
652 | ||
653 | /* | |
654 | * In any case draw (over the ghost image if necessary) | |
655 | * a cue line indicating where a drop will occur. | |
656 | */ | |
657 | 0 | Rectangle raPath = getPathBounds(path); |
658 | 0 | if (raPath != null) { |
659 | 0 | cueLine.setRect(0, |
660 | raPath.y + (int) raPath.getHeight(), | |
661 | getWidth(), | |
662 | 2); | |
663 | } | |
664 | ||
665 | 0 | g2.setColor(cueLineColor); |
666 | 0 | g2.fill(cueLine); |
667 | ||
668 | // And include the cue line in the area to be rubbed out next time | |
669 | 0 | ghostRectangle = ghostRectangle.createUnion(cueLine); |
670 | ||
671 | /* Testcase: drag something from another | |
672 | * application into ArgoUML, | |
673 | * and the explorer shows the drop icon, instead of the noDrop. | |
674 | */ | |
675 | try { | |
676 | 0 | if (!dropTargetDragEvent.isDataFlavorSupported( |
677 | TransferableModelElements.UML_COLLECTION_FLAVOR)) { | |
678 | 0 | dropTargetDragEvent.rejectDrag(); |
679 | 0 | return; |
680 | } | |
681 | 0 | } catch (NullPointerException e) { |
682 | 0 | dropTargetDragEvent.rejectDrag(); |
683 | 0 | return; |
684 | 0 | } |
685 | ||
686 | 0 | if (path == null) { |
687 | 0 | dropTargetDragEvent.rejectDrag(); |
688 | 0 | return; |
689 | } | |
690 | // to prohibit dropping onto the drag source: | |
691 | 0 | if (path.equals(sourcePath)) { |
692 | 0 | dropTargetDragEvent.rejectDrag(); |
693 | 0 | return; |
694 | } | |
695 | 0 | if (selectedTreePath.isDescendant(path)) { |
696 | 0 | dropTargetDragEvent.rejectDrag(); |
697 | 0 | return; |
698 | } | |
699 | ||
700 | 0 | Object dest = |
701 | ((DefaultMutableTreeNode) path | |
702 | .getLastPathComponent()).getUserObject(); | |
703 | ||
704 | /* If the destination is not a NameSpace, then reject: */ | |
705 | 0 | if (!Model.getFacade().isANamespace(dest)) { |
706 | 0 | if (LOG.isDebugEnabled()) { |
707 | String name; | |
708 | 0 | if (Model.getFacade().isAUMLElement(dest)) { |
709 | 0 | name = Model.getFacade().getName(dest); |
710 | 0 | } else if (dest == null) { |
711 | 0 | name = "<null>"; |
712 | } else { | |
713 | 0 | name = dest.toString(); |
714 | } | |
715 | 0 | LOG.debug("No valid Drag: " |
716 | + (Model.getFacade().isAUMLElement(dest) | |
717 | ? name + " not a namespace." | |
718 | : " not a UML element.")); | |
719 | } | |
720 | 0 | dropTargetDragEvent.rejectDrag(); |
721 | 0 | return; |
722 | } | |
723 | /* We are sure "dest" is a Namespace now. */ | |
724 | ||
725 | 0 | if (Model.getModelManagementHelper().isReadOnly(dest)) { |
726 | 0 | LOG.debug("No valid Drag: " |
727 | + "not an editable UML element (profile?)."); | |
728 | 0 | return; |
729 | } | |
730 | ||
731 | /* If the destination is a DataType, then reject: */ | |
732 | 0 | if (Model.getFacade().isADataType(dest)) { |
733 | 0 | LOG.debug("No valid Drag: destination is a DataType."); |
734 | 0 | dropTargetDragEvent.rejectDrag(); |
735 | 0 | return; |
736 | } | |
737 | ||
738 | 0 | dropTargetDragEvent.acceptDrag( |
739 | dropTargetDragEvent.getDropAction()); | |
740 | 0 | } |
741 | ||
742 | /** | |
743 | * The drop: what is done when the mousebutton is released. | |
744 | * | |
745 | * @see java.awt.dnd.DropTargetListener#drop(java.awt.dnd.DropTargetDropEvent) | |
746 | */ | |
747 | public void drop(DropTargetDropEvent dropTargetDropEvent) { | |
748 | 0 | LOG.debug("dropping ... "); |
749 | /* Prevent hover timer from doing an unwanted | |
750 | * expandPath or collapsePath: | |
751 | */ | |
752 | 0 | hoverTimer.stop(); |
753 | ||
754 | /* Clear the ghost image: */ | |
755 | 0 | repaint(ghostRectangle.getBounds()); |
756 | ||
757 | 0 | if (!isDropAcceptable(dropTargetDropEvent)) { |
758 | 0 | dropTargetDropEvent.rejectDrop(); |
759 | 0 | return; |
760 | } | |
761 | ||
762 | try { | |
763 | 0 | Transferable tr = dropTargetDropEvent.getTransferable(); |
764 | //get new parent node | |
765 | 0 | Point loc = dropTargetDropEvent.getLocation(); |
766 | 0 | TreePath destinationPath = getPathForLocation(loc.x, loc.y); |
767 | 0 | if (LOG.isDebugEnabled()) { |
768 | 0 | LOG.debug("Drop location: x=" + loc.x + " y=" + loc.y); |
769 | } | |
770 | ||
771 | 0 | if (!isValidDrag(destinationPath, tr)) { |
772 | 0 | dropTargetDropEvent.rejectDrop(); |
773 | 0 | return; |
774 | } | |
775 | ||
776 | //get the model elements that are being transfered. | |
777 | 0 | Collection modelElements = |
778 | (Collection) tr.getTransferData( | |
779 | TransferableModelElements.UML_COLLECTION_FLAVOR); | |
780 | 0 | if (LOG.isDebugEnabled()) { |
781 | 0 | LOG.debug("transfer data = " + modelElements); |
782 | } | |
783 | ||
784 | 0 | Object dest = |
785 | ((DefaultMutableTreeNode) destinationPath | |
786 | .getLastPathComponent()).getUserObject(); | |
787 | 0 | Object src = |
788 | ((DefaultMutableTreeNode) sourcePath | |
789 | .getLastPathComponent()).getUserObject(); | |
790 | ||
791 | 0 | int action = dropTargetDropEvent.getDropAction(); |
792 | /* The user-DropActions are: | |
793 | * Ctrl + Shift -> ACTION_LINK | |
794 | * Ctrl -> ACTION_COPY | |
795 | * Shift -> ACTION_MOVE | |
796 | * (none) -> ACTION_MOVE | |
797 | */ | |
798 | 0 | boolean copyAction = |
799 | (action == DnDConstants.ACTION_COPY); | |
800 | 0 | boolean moveAction = |
801 | (action == DnDConstants.ACTION_MOVE); | |
802 | ||
803 | 0 | if (!(moveAction || copyAction)) { |
804 | 0 | dropTargetDropEvent.rejectDrop(); |
805 | 0 | return; |
806 | } | |
807 | ||
808 | 0 | if (Model.getFacade().isAUMLElement(dest)) { |
809 | 0 | if (Model.getModelManagementHelper().isReadOnly(dest)) { |
810 | 0 | dropTargetDropEvent.rejectDrop(); |
811 | 0 | return; |
812 | } | |
813 | } | |
814 | 0 | if (Model.getFacade().isAUMLElement(src)) { |
815 | 0 | if (Model.getModelManagementHelper().isReadOnly(src)) { |
816 | 0 | dropTargetDropEvent.rejectDrop(); |
817 | 0 | return; |
818 | } | |
819 | } | |
820 | ||
821 | // TODO: Really should be Element/ModelElement, but we don't | |
822 | // have a type which is portable for this | |
823 | 0 | Collection<Object> newTargets = new ArrayList<Object>(); |
824 | try { | |
825 | 0 | dropTargetDropEvent.acceptDrop(action); |
826 | 0 | for (Object me : modelElements) { |
827 | 0 | if (Model.getFacade().isAUMLElement(me)) { |
828 | 0 | if (Model.getModelManagementHelper().isReadOnly(me)) { |
829 | 0 | continue; |
830 | } | |
831 | } | |
832 | 0 | if (LOG.isDebugEnabled()) { |
833 | 0 | LOG.debug((moveAction ? "move " : "copy ") + me); |
834 | } | |
835 | 0 | if (Model.getCoreHelper().isValidNamespace(me, dest)) { |
836 | 0 | if (moveAction) { |
837 | 0 | Model.getCoreHelper().setNamespace(me, dest); |
838 | 0 | newTargets.add(me); |
839 | } | |
840 | 0 | if (copyAction) { |
841 | try { | |
842 | 0 | newTargets.add(Model.getCopyHelper() |
843 | .copy(me, dest)); | |
844 | 0 | } catch (RuntimeException e) { |
845 | /* TODO: The copy function is not yet | |
846 | * completely implemented - so we will | |
847 | * have some exceptions here and there.*/ | |
848 | 0 | LOG.error("Exception", e); |
849 | 0 | } |
850 | } | |
851 | } | |
852 | 0 | if (me instanceof Relocatable) { |
853 | 0 | Relocatable d = (Relocatable) me; |
854 | 0 | if (d.isRelocationAllowed(dest)) { |
855 | 0 | if (d.relocate(dest)) { |
856 | 0 | ExplorerEventAdaptor.getInstance() |
857 | .modelElementChanged(src); | |
858 | 0 | ExplorerEventAdaptor.getInstance() |
859 | .modelElementChanged(dest); | |
860 | /*TODO: Make the tree refresh and expand | |
861 | * really work in all cases! | |
862 | */ | |
863 | 0 | makeVisible(destinationPath); |
864 | 0 | expandPath(destinationPath); |
865 | 0 | newTargets.add(me); |
866 | } | |
867 | } | |
868 | } | |
869 | 0 | if (Model.getFacade().isAFeature(me) |
870 | && Model.getFacade().isAClassifier(dest)) { | |
871 | 0 | if (moveAction) { |
872 | 0 | Model.getCoreHelper().removeFeature( |
873 | Model.getFacade().getOwner(me), me); | |
874 | 0 | Model.getCoreHelper().addFeature(dest, me); |
875 | 0 | newTargets.add(me); |
876 | } | |
877 | 0 | if (copyAction) { |
878 | 0 | newTargets.add( |
879 | Model.getCopyHelper().copy(me, dest)); | |
880 | } | |
881 | } | |
882 | } | |
883 | 0 | dropTargetDropEvent.getDropTargetContext() |
884 | .dropComplete(true); | |
885 | 0 | TargetManager.getInstance().setTargets(newTargets); |
886 | 0 | } catch (java.lang.IllegalStateException ils) { |
887 | 0 | LOG.debug("drop IllegalStateException"); |
888 | 0 | dropTargetDropEvent.rejectDrop(); |
889 | 0 | } |
890 | ||
891 | 0 | dropTargetDropEvent.getDropTargetContext() |
892 | .dropComplete(true); | |
893 | 0 | } catch (IOException io) { |
894 | 0 | LOG.debug("drop IOException"); |
895 | 0 | dropTargetDropEvent.rejectDrop(); |
896 | 0 | } catch (UnsupportedFlavorException ufe) { |
897 | 0 | LOG.debug("drop UnsupportedFlavorException"); |
898 | 0 | dropTargetDropEvent.rejectDrop(); |
899 | 0 | } |
900 | 0 | } |
901 | ||
902 | /* | |
903 | * @see java.awt.dnd.DropTargetListener#dropActionChanged(java.awt.dnd.DropTargetDragEvent) | |
904 | */ | |
905 | public void dropActionChanged( | |
906 | DropTargetDragEvent dropTargetDragEvent) { | |
907 | 0 | if (!isDragAcceptable(dropTargetDragEvent)) { |
908 | 0 | dropTargetDragEvent.rejectDrag(); |
909 | } else { | |
910 | 0 | dropTargetDragEvent.acceptDrag( |
911 | dropTargetDragEvent.getDropAction()); | |
912 | } | |
913 | 0 | } |
914 | ||
915 | /** | |
916 | * @param dropTargetEvent the droptargetevent | |
917 | * @return true if the drag is acceptable | |
918 | */ | |
919 | public boolean isDragAcceptable( | |
920 | DropTargetDragEvent dropTargetEvent) { | |
921 | // Only accept COPY or MOVE gestures (ie LINK is not supported) | |
922 | 0 | if ((dropTargetEvent.getDropAction() |
923 | & DnDConstants.ACTION_COPY_OR_MOVE) == 0) { | |
924 | 0 | return false; |
925 | } | |
926 | ||
927 | // Do this if you want to prohibit dropping onto the drag source... | |
928 | 0 | Point pt = dropTargetEvent.getLocation(); |
929 | 0 | TreePath path = getPathForLocation(pt.x, pt.y); |
930 | 0 | if (path == null) { |
931 | 0 | return false; |
932 | } | |
933 | 0 | if (path.equals(sourcePath)) { |
934 | 0 | return false; |
935 | } | |
936 | 0 | return true; |
937 | } | |
938 | ||
939 | /** | |
940 | * @param dropTargetDropEvent the droptargetdropevent | |
941 | * @return true if the drop is acceptable | |
942 | */ | |
943 | public boolean isDropAcceptable( | |
944 | DropTargetDropEvent dropTargetDropEvent) { | |
945 | // Only accept COPY or MOVE gestures (ie LINK is not supported) | |
946 | 0 | if ((dropTargetDropEvent.getDropAction() |
947 | & DnDConstants.ACTION_COPY_OR_MOVE) == 0) { | |
948 | 0 | return false; |
949 | } | |
950 | ||
951 | // Do this if you want to prohibit dropping onto the drag source... | |
952 | 0 | Point pt = dropTargetDropEvent.getLocation(); |
953 | 0 | TreePath path = getPathForLocation(pt.x, pt.y); |
954 | 0 | if (path == null) { |
955 | 0 | return false; |
956 | } | |
957 | 0 | if (path.equals(sourcePath)) { |
958 | 0 | return false; |
959 | } | |
960 | 0 | return true; |
961 | } | |
962 | ||
963 | } /* end class */ | |
964 | ||
965 | ||
966 | /** | |
967 | * The UID. | |
968 | */ | |
969 | private static final long serialVersionUID = 6207230394860016617L; | |
970 | } | |
971 | ||
972 |