package org.greenstone.gatherer.gui.tree;

import java.awt.*;
import java.awt.datatransfer.*;
import java.awt.dnd.*;
import java.awt.event.*;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.io.File;
import java.util.*;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.plaf.basic.BasicTreeUI;
import javax.swing.tree.*;
import org.greenstone.gatherer.Dictionary;
import org.greenstone.gatherer.Gatherer;
import org.greenstone.gatherer.file.FileNode;
import org.greenstone.gatherer.file.FileSystemModel;
import org.greenstone.gatherer.gui.Filter;
import org.greenstone.gatherer.util.DragComponent;
import org.greenstone.gatherer.util.DragGroup;
import org.greenstone.gatherer.util.DragTreeSelectionModel;
import org.greenstone.gatherer.util.Utility;

public abstract class DragTree
    extends JTree
    implements Autoscroll, DragGestureListener, DragSourceListener, DropTargetListener, DragComponent, TreeSelectionListener {
    /** The normal background color. */
    private Color background_color;
    /** The normal foreground color. */
    private Color foreground_color;
    /** The Group this component belongs to. */
    private DragGroup group;
    /** The filter for this tree. */
    protected Filter filter = null;
    /** The image to use for the disabled background. */
    private ImageIcon disabled_background;
    /** The image to use for a normal background. */
    private ImageIcon normal_background;
    /** 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. */
    private ImageIcon multiple_icon = new ImageIcon("resource"+File.separator+"multiple.gif");
    /** The default drag action, although its not that important as we provide custom icons during drags. */
    private int drag_action = DnDConstants.ACTION_MOVE;
    /** The location of the last ghost drawn, so that we can repair the 'spoilt' area. */
    private Point pt_last = null;
    /** The region borderer by the lower cue line. */
    private Rectangle lower_cue_line;
    /** The region covered by the drag ghost icon. */
    private Rectangle ra_ghost = new Rectangle();
    /** The region borderer by the upper cue line. */
    private Rectangle upper_cue_line;
    /** The last tree path the drag was hovered over. */
    private TreePath previous_path = null;

    static private Cursor NO_DRAG_CURSOR = null;

    static private final Color TRANSPARENT_COLOR = new Color(0,0,0,0);
    /** The distance from the edge of the current view within the scroll bar which if entered causes the view to scroll. */
    static private final int AUTOSCROLL_MARGIN = 12;

    static public final int TREE_DISPLAY_CHANGED        = 0;
    static public final int LOADED_COLLECTION_CHANGED   = 1;
    static public final int COLLECTION_CONTENTS_CHANGED = 2;


    public DragTree(TreeModel model, boolean mixed_selection)
    {
	super();

	// For some reason these aren't set with Java 1.4.2 and the GTK look and feel?
	if (UIManager.get("Tree.leftChildIndent") == null) {
	    UIManager.put("Tree.leftChildIndent", Integer.valueOf(7));
	}
	if (UIManager.get("Tree.rightChildIndent") == null) {
	    UIManager.put("Tree.rightChildIndent", Integer.valueOf(13));
	}

	init(mixed_selection);
	if (model != null) {
	    setModel(model);
	}
    }

    public void init(boolean mixed_selection) {
	if (NO_DRAG_CURSOR == null) {
	    NO_DRAG_CURSOR = DragSource.DefaultMoveNoDrop;
	}

	// Init
	this.filter = new Filter(this, null);

	// Creation
	this.putClientProperty("JTree.lineStyle", "Angled");
	this.setAutoscrolls(true);
	this.setEditable(false);
	this.setLargeModel(true);
	this.setOpaque(true);
	this.setRootVisible(false);
	this.setSelectionModel(new DragTreeSelectionModel(this, mixed_selection));
	this.setShowsRootHandles(true);
	this.setUI(new BasicTreeUI());

	// Connection
	addKeyListener(new DragTreeKeyListener());
	addMouseListener(Gatherer.g_man.foa_listener);
	addTreeExpansionListener(Gatherer.g_man.foa_listener);
	addTreeSelectionListener(this);

	DragTreeCellRenderer renderer = new DragTreeCellRenderer();
	//make the renderer paint nodes as transparent when not selected
	//renderer.setBackgroundNonSelectionColor(new Color(0,0,0,0));
	setCellRenderer(renderer);
	// It turns out VariableHeightLayoutCache is buggy under MacOS, so I'll force it to use FixedHeightLayoutCache. To do that I have to set a cell height, ala below, and set the large model property to true, ala above. And buggy VariableHeightLayoutCache goes away. Plus this actually provides a minor performance boost as the layout manager doesn't have to calculate new bounds each time (well... I suppose any gain is actually pretty much eclipsed by the time taken for file access while we determine what a certain nodes children are).
	// And once we have the cell renderer, use it to determine a fixed height for the rows
	Component prototype_row = renderer.getTreeCellRendererComponent(this, "Prototype", true, true, false, 0, true);
	this.setRowHeight(prototype_row.getSize().height);
	prototype_row = null;


	// Drag'n'drop Setup
	// Drag source setup.
	DragSource.getDefaultDragSource().createDefaultDragGestureRecognizer(this, drag_action, this);
	// Drop destination setup.
	new DropTarget(this, drag_action, this, true);
    }

    // Autoscroll Interface - Scroll because the mouse cursor is in our scroll zone.<br>
    // The following code was borrowed from the book:<br>
    //              Java Swing<br>
    //              By Robert Eckstein, Marc Loy & Dave Wood<br>
    //              Paperback - 1221 pages 1 Ed edition (September 1998)<br>
    //              O'Reilly & Associates; ISBN: 156592455X<br>
    // The relevant chapter of which can be found at:<br>
    //              http://www.oreilly.com/catalog/jswing/chapter/dnd.beta.pdf<br>
    // But I've probably tortured it beyond all recognition anyway.
    public void autoscroll(Point pt) {
	// Figure out which row we're on.
	int row = getRowForLocation(pt.x, pt.y);
	// If we are not on a row then ignore this autoscroll request
	if (row < 0) return;
	Rectangle bounds = getBounds();// Yes, scroll up one row
	// 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.
	// Is row at top of screen?
	if(pt.y + bounds.y <= AUTOSCROLL_MARGIN) {
	    // Yes, scroll up one row
	    if(row <= 0) {
		row = 0;
	    }
	    else {
		row = row - 1;
	    }
	}
	else {
	    // No, scroll down one row
	    if(row < getRowCount() - 1) {
		row = row + 1;
	    }
	}
	this.scrollRowToVisible(row);
    }

    /** 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.
     */
    public void clearGhost() {
	// Erase the last ghost image and cue line
	paintImmediately(ra_ghost.getBounds());
    }

    /** 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.
     * @param event A <strong>DragSourceDropEvent</strong> containing all the information about the end of the drag event.
     */
    public void dragDropEnd(DragSourceDropEvent event) {
    }

    /** Any implementation of DragSourceListener must include this method so we can be notified when the drag focus enters this component.
     * @param event A <strong>DragSourceDragEvent</strong> containing all the information
     * about the drag event.
     */
    public void dragEnter(DragSourceDragEvent event) {
	// Handled elsewhere.
    }
    /** 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.
     * @param event A <strong>DropTargetDragEvent</strong> containing all the information about the drag event.
     */
    public void dragEnter(DropTargetDragEvent event) {
	group.grabFocus(this);
    }
    /** Any implementation of DragSourceListener must include this method so we can be notified when the drag focus leaves this component.
     * @param event A <strong>DragSourceEvent</strong> containing all the information about the drag event.
     */
    public void dragExit(DragSourceEvent event) {
	clearGhost();
    }

    /** Any implementation of DropTargetListener must include this method
     * so we can be notified when the drag focus leaves this component.
     * @param event A DropTargetEvent containing all the information
     * about the drag event.
     */
    public void dragExit(DropTargetEvent event) {
	clearGhost();
    }

    /** Any implementation of DragGestureListener must include this method
     * so we can be notified when a drag action has been noticed, thus a
     * drag action has begun.
     * @param event A DragGestureEvent containing all the information about
     * the drag event.
     */
    public void dragGestureRecognized(DragGestureEvent event)
    {
	// Check that the tree is draggable
	if (!isDraggable()) {
	    return;
	}

	// Disable editing, unless you want to have the edit box pop-up part way through dragging.
	this.setEditable(false);
	// We need this to find one of the selected nodes.
	Point origin = event.getDragOrigin();
	TreePath path = this.getPathForLocation(origin.x, origin.y);
	// 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.
	if(!isPathSelected(path)) {
	    ((DragTreeSelectionModel)selectionModel).setImmediate(true);
	    setSelectionPath(path);
	    ((DragTreeSelectionModel)selectionModel).setImmediate(false);
	}
	if(path == null) {
	    return;
	}
	if (!isValidDrag()) {
	    try {
		event.startDrag(NO_DRAG_CURSOR, new StringSelection("dummy"), this);
		//dragging = true;
	    }
	    catch(Exception error) {
		error.printStackTrace();
	    }
	    return;
	}
	// Now update the selection stored as far as the group is concerned.
	group.setSelection(getSelectionPaths());
	group.setSource(this);
	// First grab ghost.
	group.grabFocus(this);
	// Ghost Image stuff.
	Rectangle rect = this.getPathBounds(path);
	group.mouse_offset = new Point(origin.x - rect.x, origin.y - rect.y);
	// Create the ghost image.
	// Retrieve the selected files.
	int selection_count = getSelectionCount();
	if(selection_count > 0) {
	    JLabel label;
	    if(selection_count == 1) {
		TreePath node_path = getSelectionPath();
		FileNode node = (FileNode) path.getLastPathComponent();
		label = new JLabel(node.toString(), ((DefaultTreeCellRenderer)getCellRenderer()).getLeafIcon(), JLabel.CENTER);
	    }
	    else {
		String title = getSelectionCount() + " " + Dictionary.get("Tree.Files");
		label = new JLabel(title, ((DefaultTreeCellRenderer)getCellRenderer()).getClosedIcon(), JLabel.CENTER);
		title = null;
	    }
	    // The layout manager normally does this.
	    Dimension label_size = label.getPreferredSize();
	    label.setSize(label_size);
	    label.setBackground(TRANSPARENT_COLOR);
	    label.setOpaque(true);
	    // Get a buffered image of the selection for dragging a ghost image.
	    group.image_ghost = new BufferedImage(label_size.width, label_size.height, BufferedImage.TYPE_INT_ARGB_PRE);
	    label_size = null;
	    // Get a graphics context for this image.
	    Graphics2D g2 = group.image_ghost.createGraphics();
	    // Make the image ghostlike
	    g2.setComposite(AlphaComposite.getInstance (AlphaComposite.SRC, 0.5f));
	    // Ask the cell renderer to paint itself into the BufferedImage
	    label.paint(g2);
	    g2 = null;
	    label = null;
	    try {
		event.startDrag(new Cursor(Cursor.DEFAULT_CURSOR), group.image_ghost, group.mouse_offset, new StringSelection("dummy"), this);
		//dragging = true;
	    }
	    catch(Exception error) {
		error.printStackTrace();
	    }
	}

    }

    /** Implementation side-effect.
     * @param event A DragSourceDragEvent containing all the information about the drag event.
     */
    public void dragOver(DragSourceDragEvent event) {
    }

    /** Any implementation of DropTargetListener must include this method
     * so we can be notified when the drag moves in this component.
     * @param event A DropTargetDragEvent containing all the information
     * about the drag event.
     */
    public void dragOver(DropTargetDragEvent event) {
	// Draw the mouse ghost
	Graphics2D g2 = (Graphics2D) getGraphics();
	Point pt = event.getLocation();
	if(pt_last != null && pt.equals(pt_last)) {
	    return;
	}
	pt_last = pt;
	if(!DragSource.isDragImageSupported() && group.image_ghost != null) {
	    // Erase the last ghost image and cue line
	    paintImmediately(ra_ghost.getBounds());
	    // Remember where you are about to draw the new ghost image
	    ra_ghost.setRect(pt.x - group.mouse_offset.x, pt.y - group.mouse_offset.y, group.image_ghost.getWidth(), group.image_ghost.getHeight());
	    // Draw the ghost image
	    g2.drawImage(group.image_ghost, AffineTransform.getTranslateInstance(ra_ghost.getX(), ra_ghost.getY()), null);
	}
	// 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
	TreePath target_path = this.getPathForLocation(pt.x, pt.y);
	
	if (target_path == null) {
	    // the user has moved the mouse into background area - need to remove any existing cue lines
	    if(upper_cue_line != null && lower_cue_line != null) {
		paintImmediately(upper_cue_line.getBounds());
		paintImmediately(lower_cue_line.getBounds());
		previous_path = null;
	    }
	    // don't need to display anything
	    return;
	} 
	
	if(previous_path == null || target_path != null && !target_path.equals(previous_path)) {
	    // Immediately clear the old cue lines.
	    if(upper_cue_line != null && lower_cue_line != null) {
		paintImmediately(upper_cue_line.getBounds());
		paintImmediately(lower_cue_line.getBounds());
	    }
	    if(isValidDrop(target_path)) {
		// Get the drop target's bounding rectangle
		Rectangle raPath = getPathBounds(target_path);
		// Cue line bounds (2 pixels beneath the drop target)
		upper_cue_line = new Rectangle(0, raPath.y + (int)raPath.getHeight(), getWidth(), 2);
		lower_cue_line = new Rectangle(0, raPath.y, getWidth(), 2);
		g2.setColor(((DefaultTreeCellRenderer)cellRenderer).getBackgroundSelectionColor()); // The cue line color
		g2.fill(upper_cue_line);         // Draw the cue line
		g2.fill(lower_cue_line);
	    }
	    else {
		upper_cue_line = null;
		lower_cue_line = null;
	    }
	}
    }

    /** Any implementation of DropTargetListener must include this method
	  * so we can be notified when the drag ends, ie the transferable is
	  * dropped.
	  * @param event A DropTargetDropEvent containing all the information
	  * about the end of the drag event.
	  */
    public void drop(DropTargetDropEvent event) {
	///start = System.currentTimeMillis();
	///ystem.err.println("Drop target drop: " + this);
	if (!isDroppable()) {
	    return;
	}

	event.acceptDrop(drag_action);

	// Determine what node we dropped over.
	Point pt = event.getLocation();
	TreePath target_path = this.getPathForLocation(pt.x, pt.y);
	FileNode target = null;
	if(target_path != null) {
	    if(isValidDrop(target_path)) {
		target = (FileNode) target_path.getLastPathComponent();
	    } else if (isValidDropOntoFile(target_path)) {
		target = (FileNode) target_path.getParentPath().getLastPathComponent();
	    }
	}
	else {
	    TreeModel m = getModel();
	    // check that we have a model and is a FileSystemModel (ie check that a collection is loaded
	    if (m != null && m instanceof FileSystemModel) {
		target = (FileNode) m.getRoot();
	    }
	}
	target_path = null;
	pt = null;
	if(target != null) {
	    ///ystem.err.println("Valid drop.");
	    TreePath[] selection = group.getSelection();
	    if(target != null && selection != null) {
		FileNode[] source_nodes = new FileNode[selection.length];
		for(int i = 0; i < source_nodes.length; i++) {
		    source_nodes[i] = (FileNode) selection[i].getLastPathComponent();
		}
		Gatherer.f_man.action(group.getSource(), source_nodes, this, target);
		source_nodes = null;
	    }
	    group.setSelection(null);
	    group.setSource(null);
	    selection = null;
	    target = null;
	}

	// Clear up the group.image_ghost
	paintImmediately(ra_ghost.getBounds());
	event.getDropTargetContext().dropComplete(true);
	group.image_ghost = null;
    }


    /** Any implementation of DragSourceListener must include this method
     * so we can be notified when the action to be taken upon drop changes.
     * @param event A DragSourceDragEvent containing all the information
     * about the drag event.
     */
    public void dropActionChanged(DragSourceDragEvent event) {
    }

    /** Any implementation of DropTargetListener must include this method
     * so we can be notified when the action to be taken upon drop changes.
     * @param event A DropTargetDragEvent containing all the information
     * about the drag event.
     */
    public void dropActionChanged(DropTargetDragEvent event) {
    }

    /** Used to notify this component that it has gained focus. It should
     * make some effort to inform the user of this.
     */
    public void gainFocus() {
	///ystem.err.println("Gained focus: " + this);
	((DragTreeCellRenderer)cellRenderer).gainFocus();
	repaint();
    }

    /** Autoscroll Interface...
     * The following code was borrowed from the book:
     *              Java Swing
     *              By Robert Eckstein, Marc Loy & Dave Wood
     *              Paperback - 1221 pages 1 Ed edition (September 1998)
     *              O'Reilly & Associates; ISBN: 156592455X
     *
     * The relevant chapter of which can be found at:
     *              http://www.oreilly.com/catalog/jswing/chapter/dnd.beta.pdf
     * Calculate the insets for the *JTREE*, not the viewport
     * the tree is in. This makes it a bit messy.
     */
    public Insets getAutoscrollInsets()
    {
	Rectangle raOuter = this.getBounds();
	Rectangle raInner = this.getParent().getBounds();
	return new Insets(raInner.y - raOuter.y + AUTOSCROLL_MARGIN,
			  raInner.x - raOuter.x + AUTOSCROLL_MARGIN,
			  raOuter.height - raInner.height - raInner.y + raOuter.y + AUTOSCROLL_MARGIN,
			  raOuter.width - raInner.width - raInner.x + raOuter.x + AUTOSCROLL_MARGIN);
    }

    public Filter getFilter() {
	return filter;
    }

    public String getSelectionDetails() {
	return ((DragTreeSelectionModel)selectionModel).getDetails();
    }

    public FileSystemModel getTreeModel() {
	return (FileSystemModel) getModel();
    }


    protected abstract boolean isDraggable();


    protected abstract boolean isDroppable();


    /** This method is used to inform this component when it loses focus,
     * and should indicate this somehow.
     */
    public void loseFocus() {
	///ystem.err.println("Lost focus: " + this);
	((DragTreeCellRenderer)cellRenderer).loseFocus();
	repaint();
    }


    public void paint(Graphics g) {
	if(disabled_background != null) {
	    int height = getSize().height;
	    int offset = 0;
	    ImageIcon background;
	    if(isEnabled()) {
		background = normal_background;
	    }
	    else {
		background = disabled_background;
	    }
	    while((height - offset) > 0) {
		g.drawImage(background.getImage(), 0, offset, null);
		offset = offset + background.getIconHeight();
	    }
	    background = null;
	}
	super.paint(g);
    }

    public void refresh(TreePath path) {
	if (treeModel instanceof FileSystemModel) {
	    ((FileSystemModel)treeModel).refresh(path);
	}
	else {
	    // System.err.println("DragTree::refresh - Tree model is " + treeModel);
	}
    }

    public void setBackgroundNonSelectionColor(Color color) {
	background_color = color;
	if(isEnabled()) {
	    setBackground(color);
	    ((DefaultTreeCellRenderer)cellRenderer).setBackgroundNonSelectionColor(color);
	}
	else {
	    setBackground(Color.lightGray);
	    ((DefaultTreeCellRenderer)cellRenderer).setBackgroundNonSelectionColor(Color.lightGray);
	}
	repaint();
    }

    public void setBackgroundSelectionColor(Color color) {
	((DefaultTreeCellRenderer)cellRenderer).setBackgroundSelectionColor(color);
	repaint();
    }

    /** Override the normal setEnabled so the Tree exhibits a little more
     * change, which in this instance is the background colour changing.
     * @param state Whether this GTree should be in an enabled state.
     */
    public void setEnabled(boolean state) {
	super.setEnabled(state);

	// Change some colors
	if(state) {
	    setBackground(background_color);
	    ((DefaultTreeCellRenderer)cellRenderer).setBackgroundNonSelectionColor(background_color);
	    ((DefaultTreeCellRenderer)cellRenderer).setTextNonSelectionColor(foreground_color);
	}
	else {
	    setBackground(Color.lightGray);
	    ((DefaultTreeCellRenderer)cellRenderer).setBackgroundNonSelectionColor(Color.lightGray);
	    ((DefaultTreeCellRenderer)cellRenderer).setTextNonSelectionColor(Color.black);
	}
	repaint();
    }

    public void setGroup(DragGroup group) {
	this.group = group;
    }

    /** Determines whether the following selection attempts should go through the normal delayed selection model, or should happen immediately.*/
    public void setImmediate(boolean state) {
	((DragTreeSelectionModel)selectionModel).setImmediate(state);
    }

    public void setModel(TreeModel model) {
	super.setModel(model);
	if(model instanceof FileSystemModel) {
	    FileSystemModel file_system_model = (FileSystemModel) model;
	    file_system_model.setTree(this);
	    removeTreeExpansionListener(file_system_model);
	    addTreeExpansionListener(file_system_model);
	    removeTreeWillExpandListener(file_system_model);
	    addTreeWillExpandListener(file_system_model);
	    file_system_model = null;
	}
    }

    /** Ensure that that file node denoted by the given file is selected. */
    public void setSelection(File file) {
	// We know the file exists, and thus that it must exists somewhere in our tree hierarchy.
	// 1. Retrieve the root node of our tree.
	FileNode current = (FileNode) getModel().getRoot();
	// 2. Find that node in the file parents, keeping track of each intermediate file.
	ArrayList files = new ArrayList();
	while(file != null && !current.toString().equals(file.getName())) {
	    files.add(0, file);
	    file = file.getParentFile();
	}
	if(file == null) {
	    return;
	}
	// 3. While there are still remaining intermediate files.
	while(files.size() > 0) {
	    file = (File) files.remove(0);
				// 3a. Find the next file in the current nodes children.
	    boolean found = false;
	    current.map();
	    for(int i = 0; !found && i < current.getChildCount(); i++) {
		FileNode child = (FileNode) current.getChildAt(i);
		if(child.toString().equals(file.getName())) {
		    // 3b. Make the current node this node (if found) and continue.
		    found = true;
		    current = child;
		}
	    }
	    // 3c. If not found then return as this node can't exists somehow.
	    if(!found) {
		return;
	    }
	}
	// 4. We should now have the desired node. Remember to make the selection immediate.
	TreePath path = new TreePath(current.getPath());
	setImmediate(true);
	setSelectionPath(path);
	setImmediate(false);
    }

    public void setTextNonSelectionColor(Color color) {
	foreground_color = color;
	if(isEnabled()) {
	    ((DefaultTreeCellRenderer)cellRenderer).setTextNonSelectionColor(color);
	}
	else {
	    ((DefaultTreeCellRenderer)cellRenderer).setTextNonSelectionColor(Color.black);
	}
	repaint();
    }

    public void setTextSelectionColor(Color color) {
	((DefaultTreeCellRenderer)cellRenderer).setTextSelectionColor(color);
	repaint();
    }

    public void valueChanged(TreeSelectionEvent event) {
	if(group == null) {
	    ///ystem.err.println("Oh my god, this tree has no group: " + this);
	}
	else {
	    group.grabFocus(this);
	}
    }

    /** returns false for dummy nodes (ones without files), and system root
     * nodes */
    private boolean isValidDrag() {
	//because you cant select nodes that are children of another selection, and we use a contiguous selection model, we just test the first selection path
	TreePath node_path = getSelectionPath();
	FileNode node = (FileNode) node_path.getLastPathComponent();

	if (node.getFile() == null) {
	    return false;
	}
	if (node.isFileSystemRoot()) {
	    return false;
	}
	// We also don't allow the user to select files that reside within the currently loaded collection
	TreePath[] paths = getSelectionPaths();
	for(int i = 0; i < paths.length; i++) {
	    FileNode child = (FileNode) paths[i].getLastPathComponent();
	    if (child.isInLoadedCollection()) {
		return false;
	    }
	}
	return true;
    }

    private boolean isValidDropOntoFile(TreePath target_path) {
	boolean valid = false;
	if(target_path != null) {
	    FileNode target_node = (FileNode) target_path.getLastPathComponent();
	    if (target_node.isLeaf()) {
		// check the parent node for being a valid drop
		return isValidDrop(target_path.getParentPath());
	    }
	}
	return false;
    }

		
    private boolean isValidDrop(TreePath target_path) {
	boolean valid = false;
	if(target_path == null) {
	    previous_path = null;
	} 
	else {
	    FileNode target_node = (FileNode) target_path.getLastPathComponent();
	    // We can only continue testing if the node is a folder.
	    if(!target_node.isLeaf()) {
		// Now we check if the node is readonly.
		if(!target_node.isReadOnly()) {
		    // Finally we check the target path against the paths in the selection to ensure we are not adding to our own ancestors!
		    TreePath[] selection = group.getSelection();
		    boolean failed = false;
		    for(int i = 0; !failed && selection != null && i < selection.length; i++) {
			failed = selection[i].isDescendant(target_path);
		    }
		    // Having finally completed all the tests, we can highlight the drop target.
		    if(!failed) {
			valid = true;
		    }
		    else {
			///ystem.err.println("Invalid. Target is descendant of itself.");
		    }
		}
		else {
		    ///ystem.err.println("Read only.");
		}
	    }
	    else {
		///ystem.err.println("Leaf node. Children not allowed.");
	    }
	    previous_path = target_path;
	}
	return valid;
    }


    /** This class listens for certain key presses, such as [Enter] or [Delete], and responds appropriately. */
    private class DragTreeKeyListener
	extends KeyAdapter
    {
	private boolean vk_left_pressed = false;
	/** Called whenever a key that was pressed is released, it is this action that will cause the desired effects (this allows for the key event itself to be processed prior to this listener dealing with it). */
	public void keyReleased(KeyEvent event) {
	    ///ystem.err.println("Key Release detected. " + event.getKeyCode());
	    if(event.getKeyCode() == KeyEvent.VK_DELETE) {
		// Get the selected files from the tree and removal them using the default dnd removal method.
		// Find the active tree (you've made selections in).
		DragTree tree = (DragTree) group.getActive();
		// Fudge things a bit
		group.setSource(tree);
		// Determine the selection.
		TreePath paths[] = tree.getSelectionPaths();
		if(paths != null) {
		    FileNode[] source_nodes = new FileNode[paths.length];
		    for(int i = 0; i < source_nodes.length; i++) {
			source_nodes[i] = (FileNode) paths[i].getLastPathComponent();
		    }
		    Gatherer.f_man.action(tree, source_nodes, Gatherer.recycle_bin, null);
		    source_nodes = null;
		}
	    }
	    else if(event.getKeyCode() == KeyEvent.VK_ENTER) {
		// Get the first selected file.
		DragTree tree = (DragTree)event.getSource();
		TreePath path = tree.getSelectionPath();
		if(path != null) {
		    File file = ((FileNode)path.getLastPathComponent()).getFile();
		    if (file != null && file.isFile()) {
			Gatherer.f_man.openFileInExternalApplication(file);
		    }
		    else {
			if(!tree.isExpanded(path)) {
			    tree.expandPath(path);
			}
			else {
			    tree.collapsePath(path);
			}
		    }
		}
	    } else if (event.getKeyCode() == KeyEvent.VK_UP || event.getKeyCode() == KeyEvent.VK_DOWN) {
		DragTree tree = (DragTree)event.getSource();
		// we need to manually do the up and down selections
		boolean up = (event.getKeyCode() == KeyEvent.VK_UP);
		int current_row = tree.getLeadSelectionRow();
		tree.setImmediate(true);
		if (up) {
		    if (current_row > 0) {
			tree.clearSelection();
			tree.setSelectionRow(current_row-1);
		    }
		} else {
		    if (current_row < tree.getRowCount()-1){
			tree.clearSelection();
			tree.setSelectionRow(current_row+1);
		    }
		}
		tree.setImmediate(false);
	    } else if (event.getKeyCode() == KeyEvent.VK_LEFT) {
		// left key on a file shifts the selection to the parent folder
		DragTree tree = (DragTree)event.getSource();
		TreePath path = tree.getLeadSelectionPath();
		if(path != null) {
		    File file = ((FileNode)path.getLastPathComponent()).getFile();
		    if(file != null) {
			if (file.isFile() || vk_left_pressed) {
			    vk_left_pressed = false;
			    TreePath parent_path = path.getParentPath();
			    if (parent_path != null && parent_path.getParentPath() != null) {
				// if this file is in the top level folder, don't move the focus 
				tree.setImmediate(true);
				tree.clearSelection();
				tree.setSelectionPath(parent_path);
				tree.setImmediate(false);
			    }
			}
		    }
		}
	    }
	}
	
	// we need to watch for left clicks on an unopened folder - should shift the focus to teh parent folder. But because there is some other mysterious key listener that does opening and closing folders on right and left clicks, we must detect the situation before the other handler has done its job, and process it after. 
	public void keyPressed(KeyEvent event) {
	    if (event.getKeyCode() == KeyEvent.VK_LEFT) {
		// left key on  closed directory shifts the selection to the parent folder
		DragTree tree = (DragTree)event.getSource();
		TreePath path = tree.getLeadSelectionPath();
		if(path == null) return; 
		File file = ((FileNode)path.getLastPathComponent()).getFile();
		if(file == null) return;
		
		if (file.isDirectory() && tree.isCollapsed(path)) {
		    vk_left_pressed = true;
		}
	    }
	}
    }
}
