source: trunk/gli/src/org/greenstone/gatherer/gui/tree/DragTree.java@ 4313

Last change on this file since 4313 was 4313, checked in by jmt12, 21 years ago

allow for drop anywhere on collection tree to mean a copy to the root node

  • Property svn:keywords set to Author Date Id Revision
File size: 23.3 KB
Line 
1package org.greenstone.gatherer.gui.tree;
2
3import java.awt.*;
4import java.awt.datatransfer.*;
5import java.awt.dnd.*;
6import java.awt.geom.AffineTransform;
7import java.awt.image.BufferedImage;
8import java.io.File;
9import java.util.*;
10import javax.swing.*;
11import javax.swing.event.*;
12import javax.swing.tree.*;
13import org.greenstone.gatherer.Gatherer;
14import org.greenstone.gatherer.file.FileNode;
15import org.greenstone.gatherer.file.FileSystemModel;
16import org.greenstone.gatherer.util.ArrayTools;
17import org.greenstone.gatherer.util.DragComponent;
18import org.greenstone.gatherer.util.DragGroup;
19import org.greenstone.gatherer.util.DragTreeSelectionModel;
20import org.greenstone.gatherer.util.Utility;
21
22public class DragTree
23 extends JTree
24 implements Autoscroll, DragGestureListener, DragSourceListener, DropTargetListener, DragComponent, TreeSelectionListener {
25 /** The normal background color. */
26 private Color background_color;
27 /** The normal foreground color. */
28 private Color foreground_color;
29 /** The Group this component belongs to. */
30 private DragGroup group;
31 /** The image to use for the disabled background. */
32 private ImageIcon disabled_background;
33 /** The image to use for a normal background. */
34 private ImageIcon normal_background;
35 /** The icon to use for multiple node drag'n'drops. We decided against using the windows paradigm or a block of x horizontal lines for x files. */
36 private ImageIcon multiple_icon = new ImageIcon("resource"+File.separator+"multiple.gif");
37 /** The default drag action, although its not that important as we provide custom icons during drags. */
38 private int drag_action = DnDConstants.ACTION_MOVE;
39 /** The location of the last ghost drawn, so that we can repair the 'spoilt' area. */
40 private Point pt_last = null;
41 /** The region borderer by the lower cue line. */
42 private Rectangle lower_cue_line;
43 /** The region covered by the drag ghost icon. */
44 private Rectangle ra_ghost = new Rectangle();
45 /** The region borderer by the upper cue line. */
46 private Rectangle upper_cue_line;
47 /** The identifying name of this Tree. */
48 private String name;
49 /** The last tree path the drag was hovered over. */
50 private TreePath previous_path = null;
51 static private final Color TRANSPARENT_COLOR = new Color(0,0,0,0);
52 /** The distance from the edge of the current view within the scroll bar which if entered causes the view to scroll. */
53 static private final int AUTOSCROLL_MARGIN = 12;
54
55 public DragTree(String name, String background_name) {
56 super();
57 init(name, background_name);
58 }
59
60 public DragTree(String name, TreeModel model, String background_name) {
61 super(model);
62 init(name, background_name);
63 // Connection
64 setModel(model);
65 }
66
67 public void init(String name, String background_name) {
68 // Init
69 this.name = name;
70 if(background_name != null) {
71 this.disabled_background = new ImageIcon("background.gif");
72 this.normal_background = new ImageIcon(background_name);
73 }
74
75 // Creation
76 this.putClientProperty("JTree.lineStyle", "Angled");
77 this.setAutoscrolls(true);
78 this.setEditable(false);
79 this.setLargeModel(true);
80 this.setOpaque(true);
81 this.setSelectionModel(new DragTreeSelectionModel(this));
82 // Connection
83 addTreeSelectionListener(this);
84
85 DragTreeCellRenderer renderer = new DragTreeCellRenderer();
86 //make the renderer paint nodes as transparent when not selected
87 //renderer.setBackgroundNonSelectionColor(new Color(0,0,0,0));
88 setCellRenderer(renderer);
89
90 // Drag'n'drop Setup
91 // Drag source setup.
92 DragSource.getDefaultDragSource().createDefaultDragGestureRecognizer(this, drag_action, this);
93 // Drop destination setup.
94 new DropTarget(this, drag_action, this, true);
95 }
96
97 // Autoscroll Interface - Scroll because the mouse cursor is in our scroll zone.<br>
98 // The following code was borrowed from the book:<br>
99 // Java Swing<br>
100 // By Robert Eckstein, Marc Loy & Dave Wood<br>
101 // Paperback - 1221 pages 1 Ed edition (September 1998)<br>
102 // O'Reilly & Associates; ISBN: 156592455X<br>
103 // The relevant chapter of which can be found at:<br>
104 // http://www.oreilly.com/catalog/jswing/chapter/dnd.beta.pdf<br>
105 // But I've probably tortured it beyond all recognition anyway.
106 public void autoscroll(Point pt) {
107 // Figure out which row we're on.
108 int row = getRowForLocation(pt.x, pt.y);
109 // If we are not on a row then ignore this autoscroll request
110 if (row < 0) return;
111 Rectangle bounds = getBounds();// Yes, scroll up one row
112 // Now decide if the row is at the top of the screen or at the bottom. We do this to make the previous row (or the next row) visible as appropriate. If we're at the absolute top or bottom, just return the first or last row respectively.
113 // Is row at top of screen?
114 if(pt.y + bounds.y <= AUTOSCROLL_MARGIN) {
115 // Yes, scroll up one row
116 if(row <= 0) {
117 row = 0;
118 }
119 else {
120 row = row - 1;
121 }
122 }
123 else {
124 // No, scroll down one row
125 if(row < getRowCount() - 1) {
126 row = row + 1;
127 }
128 }
129 this.scrollRowToVisible(row);
130 }
131
132 /** In order for the appearance to be consistant, given we may be in the situation where the pointer has left our focus but the ghost remains, this method allows other members of the GGroup to tell this component to clear its ghost.
133 */
134 public void clearGhost() {
135 // Erase the last ghost image and cue line
136 paintImmediately(ra_ghost.getBounds());
137 }
138
139 /** Any implementation of DragSourceListener must include this method so we can be notified when the drag event ends (somewhere else), which will in turn remove actions.
140 * @param event A <strong>DragSourceDropEvent</strong> containing all the information about the end of the drag event.
141 */
142 public void dragDropEnd(DragSourceDropEvent event) {
143 if(event.getDropSuccess()) {
144 // Do whatever I do when the drop is successful.
145 }
146 }
147 /** Any implementation of DragSourceListener must include this method so we can be notified when the drag focus enters this component.
148 * @param event A <strong>DragSourceDragEvent</strong> containing all the information
149 * about the drag event.
150 */
151 public void dragEnter(DragSourceDragEvent event) {
152 // Handled elsewhere.
153 }
154 /** Any implementation of DropTargetListener must include this method so we can be notified when the drag focus enters this component, which in this case is to grab focus from within our group.
155 * @param event A <strong>DropTargetDragEvent</strong> containing all the information about the drag event.
156 */
157 public void dragEnter(DropTargetDragEvent event) {
158 group.grabFocus(this);
159 }
160 /** Any implementation of DragSourceListener must include this method so we can be notified when the drag focus leaves this component.
161 * @param event A <strong>DragSourceEvent</strong> containing all the information about the drag event.
162 */
163 public void dragExit(DragSourceEvent event) {
164 clearGhost();
165 }
166
167 /** Any implementation of DropTargetListener must include this method
168 * so we can be notified when the drag focus leaves this component.
169 * @param event A DropTargetEvent containing all the information
170 * about the drag event.
171 */
172 public void dragExit(DropTargetEvent event) {
173 clearGhost();
174 }
175
176 /** Any implementation of DragGestureListener must include this method
177 * so we can be notified when a drag action has been noticed, thus a
178 * drag action has begun.
179 * @param event A DragGestureEvent containing all the information about
180 * the drag event.
181 */
182 public void dragGestureRecognized(DragGestureEvent event) {
183 // Disable editing, unless you want to have the edit box pop-up part way through dragging.
184 this.setEditable(false);
185 // We need this to find one of the selected nodes.
186 Point origin = event.getDragOrigin();
187 TreePath path = this.getPathForLocation(origin.x, origin.y);
188 // Taking into account our delayed model of selection, it is possible the user has performed a select and drag in one click. Here we utilize the Windows paradigm like so: If the node at the origin of the drag and drop is already in our selection then we perform multiple drag and drop. Otherwise we recognise that this is a distinct drag-drop and move only the origin node.
189 if(!isPathSelected(path)) {
190 ((DragTreeSelectionModel)selectionModel).setImmediate(true);
191 setSelectionPath(path);
192 }
193 // Now update the selection stored as far as the group is concerned.
194 group.setSelection(getSelectionPaths());
195 group.setSource(this);
196 // First grab ghost.
197 group.grabFocus(this);
198 // Ghost Image stuff.
199 if(path != null) {
200 Rectangle rect = this.getPathBounds(path);
201 group.mouse_offset = new Point(origin.x - rect.x, origin.y - rect.y);
202 // Create the ghost image.
203 // Retrieve the selected files.
204 int selection_count = getSelectionCount();
205 if(selection_count > 0) {
206 JLabel label;
207 if(selection_count == 1) {
208 TreePath node_path = getSelectionPath();
209 FileNode node = (FileNode) path.getLastPathComponent();
210 label = new JLabel(node.toString(), ((DefaultTreeCellRenderer)getCellRenderer()).getLeafIcon(), JLabel.CENTER);
211 }
212 else {
213 String title = getSelectionCount() + " files";
214 label = new JLabel(title, ((DefaultTreeCellRenderer)getCellRenderer()).getClosedIcon(), JLabel.CENTER);
215 title = null;
216 }
217 // The layout manager normally does this.
218 Dimension label_size = label.getPreferredSize();
219 label.setSize(label_size);
220 label.setBackground(TRANSPARENT_COLOR);
221 label.setOpaque(true);
222 // Get a buffered image of the selection for dragging a ghost image.
223 group.image_ghost = new BufferedImage(label_size.width, label_size.height, BufferedImage.TYPE_INT_ARGB_PRE);
224 label_size = null;
225 // Get a graphics context for this image.
226 Graphics2D g2 = group.image_ghost.createGraphics();
227 // Make the image ghostlike
228 g2.setComposite(AlphaComposite.getInstance (AlphaComposite.SRC, 0.5f));
229 // Ask the cell renderer to paint itself into the BufferedImage
230 label.paint(g2);
231 g2 = null;
232 label = null;
233 try {
234 event.startDrag(new Cursor(Cursor.DEFAULT_CURSOR), group.image_ghost, new Point(5,5), new StringSelection("dummy"), this);
235 //dragging = true;
236 }
237 catch(Exception error) {
238 error.printStackTrace();
239 }
240 }
241 }
242 }
243
244 /** Implementation side-effect.
245 * @param event A DragSourceDragEvent containing all the information about the drag event.
246 */
247 public void dragOver(DragSourceDragEvent event) {
248 }
249
250 /** Any implementation of DropTargetListener must include this method
251 * so we can be notified when the drag moves in this component.
252 * @param event A DropTargetDragEvent containing all the information
253 * about the drag event.
254 */
255 public void dragOver(DropTargetDragEvent event) {
256 // Draw the mouse ghost
257 Graphics2D g2 = (Graphics2D) getGraphics();
258 Point pt = event.getLocation();
259 if(pt_last != null && pt.equals(pt_last)) {
260 return;
261 }
262 pt_last = pt;
263 if(!DragSource.isDragImageSupported()) {
264 // Erase the last ghost image and cue line
265 paintImmediately(ra_ghost.getBounds());
266 // Remember where you are about to draw the new ghost image
267 ra_ghost.setRect(pt.x - group.mouse_offset.x, pt.y - group.mouse_offset.y, group.image_ghost.getWidth(), group.image_ghost.getHeight());
268 // Draw the ghost image
269 g2.drawImage(group.image_ghost, AffineTransform.getTranslateInstance(ra_ghost.getX(), ra_ghost.getY()), null);
270 }
271 // Now we highlight the target node if it is a valid drop target. Of course we don't bother if we are still over a node which has already been identified as to whether its a drop target.
272 TreePath target_path = this.getPathForLocation(pt.x, pt.y);
273 if(previous_path == null || target_path != null && !target_path.equals(previous_path)) {
274 // Immediately clear the old cue lines.
275 if(upper_cue_line != null && lower_cue_line != null) {
276 paintImmediately(upper_cue_line.getBounds());
277 paintImmediately(lower_cue_line.getBounds());
278 }
279 if(isValidDrop(target_path)) {
280 ///ystem.err.println("Valid. Painting cues.");
281 // Get the drop target's bounding rectangle
282 Rectangle raPath = getPathBounds(target_path);
283 // Cue line bounds (2 pixels beneath the drop target)
284 upper_cue_line = new Rectangle(0, raPath.y + (int)raPath.getHeight(), getWidth(), 2);
285 lower_cue_line = new Rectangle(0, raPath.y, getWidth(), 2);
286 g2.setColor(((DefaultTreeCellRenderer)cellRenderer).getBackgroundSelectionColor()); // The cue line color
287 g2.fill(upper_cue_line); // Draw the cue line
288 g2.fill(lower_cue_line);
289 }
290 else {
291 upper_cue_line = null;
292 lower_cue_line = null;
293 }
294 }
295 }
296
297 /** Any implementation of DropTargetListener must include this method
298 * so we can be notified when the drag ends, ie the transferable is
299 * dropped.
300 * @param event A DropTargetDropEvent containing all the information
301 * about the end of the drag event.
302 */
303 public void drop(DropTargetDropEvent event) {
304 ///start = System.currentTimeMillis();
305 ///ystem.err.println("Drop target drop: " + this);
306 event.acceptDrop(drag_action);
307 if(!name.equals(Utility.WORKSPACE_TREE)) {
308 // Determine what node we dropped over.
309 Point pt = event.getLocation();
310 TreePath target_path = this.getPathForLocation(pt.x, pt.y);
311 FileNode target = null;
312 if(target_path != null) {
313 if(isValidDrop(target_path)) {
314 target = (FileNode) target_path.getLastPathComponent();
315 }
316 else {
317 // Warn that this is an invalid drop.
318 JOptionPane.showMessageDialog(Gatherer.g_man, Gatherer.dictionary.get("FileActions.InvalidTarget"), Gatherer.dictionary.get("General.Error"), JOptionPane.ERROR_MESSAGE);
319 }
320 }
321 else {
322 target = (FileNode) getModel().getRoot();
323 }
324 target_path = null;
325 pt = null;
326 if(target != null) {
327 ///ystem.err.println("Valid drop.");
328 TreePath[] selection = group.getSelection();
329 if(target != null) {
330 FileNode[] source_nodes = new FileNode[selection.length];
331 for(int i = 0; i < source_nodes.length; i++) {
332 source_nodes[i] = (FileNode) selection[i].getLastPathComponent();
333 }
334 Gatherer.f_man.action(group.getSource(), source_nodes, this, target);
335 source_nodes = null;
336 }
337 group.setSelection(null);
338 group.setSource(null);
339 selection = null;
340 target = null;
341 }
342 }
343 else {
344 // Warn that this is an invalid drop.
345 JOptionPane.showMessageDialog(Gatherer.g_man, Gatherer.dictionary.get("FileActions.ReadOnlyTarget"), Gatherer.dictionary.get("General.Error"), JOptionPane.ERROR_MESSAGE);
346 }
347 // Clear up the group.image_ghost
348 paintImmediately(ra_ghost.getBounds());
349 event.getDropTargetContext().dropComplete(true);
350 }
351
352 /** Any implementation of DragSourceListener must include this method
353 * so we can be notified when the action to be taken upon drop changes.
354 * @param event A DragSourceDragEvent containing all the information
355 * about the drag event.
356 */
357 public void dropActionChanged(DragSourceDragEvent event) {
358 }
359
360 /** Any implementation of DropTargetListener must include this method
361 * so we can be notified when the action to be taken upon drop changes.
362 * @param event A DropTargetDragEvent containing all the information
363 * about the drag event.
364 */
365 public void dropActionChanged(DropTargetDragEvent event) {
366 }
367
368 /** Used to notify this component that it has gained focus. It should
369 * make some effort to inform the user of this.
370 */
371 public void gainFocus() {
372 ///ystem.err.println("Gained focus: " + this);
373 ((DragTreeCellRenderer)cellRenderer).gainFocus();
374 repaint();
375 }
376
377 /** Autoscroll Interface...
378 * The following code was borrowed from the book:
379 * Java Swing
380 * By Robert Eckstein, Marc Loy & Dave Wood
381 * Paperback - 1221 pages 1 Ed edition (September 1998)
382 * O'Reilly & Associates; ISBN: 156592455X
383 *
384 * The relevant chapter of which can be found at:
385 * http://www.oreilly.com/catalog/jswing/chapter/dnd.beta.pdf
386 * Calculate the insets for the *JTREE*, not the viewport
387 * the tree is in. This makes it a bit messy.
388 */
389 public Insets getAutoscrollInsets()
390 {
391 Rectangle raOuter = this.getBounds();
392 Rectangle raInner = this.getParent().getBounds();
393 return new Insets(raInner.y - raOuter.y + AUTOSCROLL_MARGIN,
394 raInner.x - raOuter.x + AUTOSCROLL_MARGIN,
395 raOuter.height - raInner.height - raInner.y + raOuter.y + AUTOSCROLL_MARGIN,
396 raOuter.width - raInner.width - raInner.x + raOuter.x + AUTOSCROLL_MARGIN);
397 }
398
399 public String getSelectionDetails() {
400 return ((DragTreeSelectionModel)selectionModel).getDetails();
401 }
402
403 public FileSystemModel getTreeModel() {
404 return (FileSystemModel) getModel();
405 }
406
407 /** This method is used to inform this component when it loses focus,
408 * and should indicate this somehow.
409 */
410 public void loseFocus() {
411 ///ystem.err.println("Lost focus: " + this);
412 ((DragTreeCellRenderer)cellRenderer).loseFocus();
413 repaint();
414 }
415
416 public void mapDirectory(File file, String title) {
417 try {
418 ((FileSystemModel)treeModel).mapDirectory(file, title);
419 }
420 catch (Exception error) {
421 error.printStackTrace();
422 }
423 }
424
425 public void paint(Graphics g) {
426 if(disabled_background != null) {
427 int height = getSize().height;
428 int offset = 0;
429 ImageIcon background;
430 if(isEnabled()) {
431 background = normal_background;
432 }
433 else {
434 background = disabled_background;
435 }
436 while((height - offset) > 0) {
437 g.drawImage(background.getImage(), 0, offset, null);
438 offset = offset + background.getIconHeight();
439 }
440 background = null;
441 }
442 super.paint(g);
443 }
444
445 public void refresh(TreePath path) {
446 ((FileSystemModel)treeModel).refresh(path);
447 }
448
449 public void setBackgroundNonSelectionColor(Color color) {
450 background_color = color;
451 if(isEnabled()) {
452 setBackground(color);
453 ((DefaultTreeCellRenderer)cellRenderer).setBackgroundNonSelectionColor(color);
454 }
455 else {
456 setBackground(Color.lightGray);
457 ((DefaultTreeCellRenderer)cellRenderer).setBackgroundNonSelectionColor(Color.lightGray);
458 }
459 repaint();
460 }
461
462 public void setBackgroundSelectionColor(Color color) {
463 ((DefaultTreeCellRenderer)cellRenderer).setBackgroundSelectionColor(color);
464 repaint();
465 }
466
467 /** Override the normal setEnabled so the Tree exhibits a little more
468 * change, which in this instance is the background colour changing.
469 * @param state Whether this GTree should be in an enabled state.
470 */
471 public void setEnabled(boolean state) {
472 super.setEnabled(state);
473 clearSelection();
474 // Change some colors
475 if(state) {
476 setBackground(background_color);
477 ((DefaultTreeCellRenderer)cellRenderer).setBackgroundNonSelectionColor(background_color);
478 ((DefaultTreeCellRenderer)cellRenderer).setTextNonSelectionColor(foreground_color);
479 }
480 else {
481 setBackground(Color.lightGray);
482 ((DefaultTreeCellRenderer)cellRenderer).setBackgroundNonSelectionColor(Color.lightGray);
483 ((DefaultTreeCellRenderer)cellRenderer).setTextNonSelectionColor(Color.black);
484 }
485 repaint();
486 }
487
488 public void setGroup(DragGroup group) {
489 this.group = group;
490 }
491
492 /** Determines whether the following selection attempts should go through the normal delayed selection model, or should happen immediately.*/
493 public void setImmediate(boolean state) {
494 ((DragTreeSelectionModel)selectionModel).setImmediate(state);
495 }
496
497 public void setModel(TreeModel model) {
498 super.setModel(model);
499 if(model instanceof FileSystemModel) {
500 FileSystemModel file_system_model = (FileSystemModel) model;
501 file_system_model.setTree(this);
502 addTreeExpansionListener(file_system_model);
503 addTreeWillExpandListener(file_system_model);
504 file_system_model = null;
505 }
506 }
507
508 /** Ensure that that file node denoted by the given file is selected. */
509 public void setSelection(File file) {
510 // We know the file exists, and thus that it must exists somewhere in our tree hierarchy.
511 // 1. Retrieve the root node of our tree.
512 FileNode current = (FileNode) getModel().getRoot();
513 // 2. Find that node in the file parents, keeping track of each intermediate file.
514 ArrayList files = new ArrayList();
515 while(file != null && !current.toString().equals(file.getName())) {
516 files.add(0, file);
517 file = file.getParentFile();
518 }
519 if(file == null) {
520 return;
521 }
522 // 3. While there are still remaining intermediate files.
523 while(files.size() > 0) {
524 file = (File) files.remove(0);
525 // 3a. Find the next file in the current nodes children.
526 boolean found = false;
527 current.map();
528 for(int i = 0; !found && i < current.getChildCount(); i++) {
529 FileNode child = (FileNode) current.getChildAt(i);
530 if(child.toString().equals(file.getName())) {
531 // 3b. Make the current node this node (if found) and continue.
532 found = true;
533 current = child;
534 }
535 }
536 // 3c. If not found then return as this node can't exists somehow.
537 if(!found) {
538 return;
539 }
540 }
541 // 4. We should now have the desired node. Remember to make the selection immediate.
542 TreePath path = new TreePath(current.getPath());
543 setImmediate(true);
544 setSelectionPath(path);
545 setImmediate(false);
546 }
547
548 public void setTextNonSelectionColor(Color color) {
549 foreground_color = color;
550 if(isEnabled()) {
551 ((DefaultTreeCellRenderer)cellRenderer).setTextNonSelectionColor(color);
552 }
553 else {
554 ((DefaultTreeCellRenderer)cellRenderer).setTextNonSelectionColor(Color.black);
555 }
556 repaint();
557 }
558
559 public void setTextSelectionColor(Color color) {
560 ((DefaultTreeCellRenderer)cellRenderer).setTextSelectionColor(color);
561 repaint();
562 }
563
564 public String toString() {
565 return name;
566 }
567
568 public void valueChanged(TreeSelectionEvent e) {
569 if(group == null) {
570 ///ystem.err.println("Oh my god, this tree has no group: " + this);
571 }
572 else {
573 group.grabFocus(this);
574 }
575 Gatherer.g_man.menu_bar.setMetaAuditSuffix(getSelectionDetails());
576 }
577
578 private boolean isValidDrop(TreePath target_path) {
579 boolean valid = false;
580 if(target_path != null) {
581 FileNode target_node = (FileNode) target_path.getLastPathComponent();
582 // We can only continue testing if the node is a folder.
583 if(!target_node.isLeaf()) {
584 // Now we check if the node is readonly.
585 if(!target_node.isReadOnly()) {
586 // Finally we check the target path against the paths in the selection to ensure we are not adding to our own ancestors!
587 TreePath[] selection = group.getSelection();
588 boolean failed = false;
589 for(int i = 0; !failed && selection != null && i < selection.length; i++) {
590 failed = selection[i].isDescendant(target_path);
591 }
592 // Having finally completed all the tests, we can highlight the drop target.
593 if(!failed) {
594 valid = true;
595 }
596 else {
597 ///ystem.err.println("Invalid. Target is descendant of itself.");
598 }
599 }
600 else {
601 ///ystem.err.println("Read only.");
602 }
603 }
604 else {
605 ///ystem.err.println("Leaf node. Children not allowed.");
606 }
607 previous_path = target_path;
608 }
609 else {
610 if(target_path == null) {
611 previous_path = null;
612 }
613 }
614 return valid;
615 }
616}
Note: See TracBrowser for help on using the repository browser.