// JTreeTable.java
//
// Created: December 2005
//
// This file is a component of the BioMoby project.
// Copyright Martin Senger (martin.senger@gmail.com).
//

package org.biomoby.service.dashboard.treetable;

import javax.swing.JTable;
import javax.swing.JTree;
import javax.swing.UIManager;
import javax.swing.LookAndFeel;
import javax.swing.ListSelectionModel;
import javax.swing.JScrollPane;
import javax.swing.AbstractCellEditor;
import javax.swing.table.TableCellEditor;
import javax.swing.table.TableCellRenderer;
import javax.swing.tree.TreePath;
import javax.swing.tree.TreeModel;
import javax.swing.tree.DefaultTreeCellRenderer;
import javax.swing.tree.DefaultTreeSelectionModel;
import javax.swing.tree.TreeCellRenderer;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;

import java.awt.Dimension;
import java.awt.Component;
import java.awt.Graphics;
import java.awt.event.MouseEvent;

import java.util.EventObject;

/**
 * This example shows how to create a simple JTreeTable component, by
 * using a JTree as a renderer (and editor) for the cells in a
 * particular column in the JTable. <p>
 *
 * Inspired by <a
 * href="http://java.sun.com/products/jfc/tsc/articles/treetable1/index.html">http://java.sun.com/products/jfc/tsc/articles/treetable1/index.html</a>. <p>
 *
 * @author Philip Milne 
 * @author Scott Violet
 * @author <A HREF="mailto:martin.senger@gmail.com">Martin Senger</A>
 * @version $Id: JTreeTable.java,v 1.7 2006/02/04 09:24:42 senger Exp $
 */

public class JTreeTable extends JTable {

    // a tree component (sub-class of JTree)
    protected TreeTableCellRenderer tree;

    // an inner model
    protected TreeTableModel treeTableModel;

    /*********************************************************************
     * A default constructor.
     ********************************************************************/
    public JTreeTable (TreeTableModel treeTableModel) {
	super();

	this.treeTableModel = treeTableModel;

	// Create the tree. It will be used as a renderer and editor. 
	tree = new TreeTableCellRenderer (treeTableModel);

	// Install a tableModel representing the visible rows in the tree. 
	super.setModel (new TreeTableModelAdapter (treeTableModel, tree));

	// Force the JTable and JTree to share their row selection models. 
	ListToTreeSelectionModelWrapper selectionWrapper =
	    new ListToTreeSelectionModelWrapper();
	tree.setSelectionModel (selectionWrapper);
	setSelectionModel (selectionWrapper.getListSelectionModel()); 

	// Install the tree editor renderer and editor. 
	setDefaultRenderer (TreeTableModel.class, tree); 
	setDefaultEditor (TreeTableModel.class, new TreeTableCellEditor());

	// No grid.
	setShowGrid (false);

	// No intercell spacing
	setIntercellSpacing (new Dimension (0, 0));	

	// And update the height of the trees row to match that of the
	// table
	if (tree.getRowHeight() < 1) {
	    // Metal looks better like this.
	    setRowHeight (18);
	}
    }

    /*********************************************************************
     * Put this table in a scrollable pane.
     ********************************************************************/
    public JScrollPane scrollable() {
 	return new JScrollPane (this);
    }

    /*********************************************************************
     * Overridden to message super and forward the method to the tree.
     * Since the tree is not actually in the component hierarchy it
     * will never receive this unless we forward it in this manner.
     ********************************************************************/
    public void updateUI() {
	super.updateUI();
	if(tree != null) {
	    tree.updateUI();
	}
	// Use the tree's default foreground and background colors in the
	// table. 
        LookAndFeel.installColorsAndFont(this, "Tree.background",
                                         "Tree.foreground", "Tree.font");
    }

    /* Workaround for BasicTableUI anomaly. Make sure the UI never tries to 
     * paint the editor. The UI currently uses different techniques to 
     * paint the renderers and editors and overriding setBounds() below 
     * is not the right thing to do for an editor. Returning -1 for the 
     * editing row in this case, ensures the editor is never painted. 
     */
    public int getEditingRow() {
        return (getColumnClass(editingColumn) == TreeTableModel.class) ? -1 :
	        editingRow;  
    }

    /**
     * Overridden to pass the new rowHeight to the tree.
     */
    public void setRowHeight(int rowHeight) { 
        super.setRowHeight(rowHeight); 
	if (tree != null && tree.getRowHeight() != rowHeight) {
            tree.setRowHeight(getRowHeight()); 
	}
    }

    /**
     * Returns the tree that is being shared between the model.
     */
    public JTree getTree() {
	return tree;
    }

    /*********************************************************************
     * Returns the model that has been passed to this table in its
     * constructor. Note that this is not the same as a normal model
     * created for this table and passed to its super class. See
     * source code of a constructor for more details.
     ********************************************************************/
    public TreeTableModel getInnerModel() {
	return treeTableModel;
    }

    /*********************************************************************
     *
     * A TreeCellRenderer that displays a JTree.
     *
     ********************************************************************/
    public class TreeTableCellRenderer
	extends JTree
	implements TableCellRenderer {

	/** Last table/tree row asked to renderer. */
	protected int visibleRow;

	public TreeTableCellRenderer(TreeModel model) {
	    super(model); 
	}

	/**
	 * updateUI is overridden to set the colors of the Tree's renderer
	 * to match that of the table.
	 */
	public void updateUI() {
	    super.updateUI();
	    // Make the tree's cell renderer use the table's cell selection
	    // colors. 
	    TreeCellRenderer tcr = getCellRenderer();
	    if (tcr instanceof DefaultTreeCellRenderer) {
		DefaultTreeCellRenderer dtcr = ((DefaultTreeCellRenderer)tcr); 
		// For 1.1 uncomment this, 1.2 has a bug that will cause an
		// exception to be thrown if the border selection color is
		// null.
		// dtcr.setBorderSelectionColor(null);
		dtcr.setTextSelectionColor(UIManager.getColor
					   ("Table.selectionForeground"));
		dtcr.setBackgroundSelectionColor(UIManager.getColor
						("Table.selectionBackground"));
	    }
	}

	/**
	 * Sets the row height of the tree, and forwards the row height to
	 * the table.
	 */
	public void setRowHeight(int rowHeight) { 
	    if (rowHeight > 0) {
		super.setRowHeight(rowHeight); 
		if (JTreeTable.this != null &&
		    JTreeTable.this.getRowHeight() != rowHeight) {
		    JTreeTable.this.setRowHeight(getRowHeight()); 
		}
	    }
	}

	/**
	 * This is overridden to set the height to match that of the JTable.
	 */
	public void setBounds(int x, int y, int w, int h) {
	    super.setBounds(x, 0, w, JTreeTable.this.getHeight());
	}

	/**
	 * Subclassed to translate the graphics such that the last visible
	 * row will be drawn at 0,0.
	 */
	public void paint(Graphics g) {
	    g.translate(0, -visibleRow * getRowHeight());
	    super.paint(g);
	}

	/**
	 * TreeCellRenderer method. Overridden to update the visible row.
	 */
	public Component getTableCellRendererComponent(JTable table,
						       Object value,
						       boolean isSelected,
						       boolean hasFocus,
						       int row, int column) {
	    if(isSelected)
		setBackground(table.getSelectionBackground());
	    else
		setBackground(table.getBackground());

	    visibleRow = row;
	    return this;
	}
    }


    /*********************************************************************
     *
     * TreeTableCellEditor implementation.
     *
     ********************************************************************/
    public class TreeTableCellEditor
	extends AbstractCellEditor
	implements TableCellEditor {

	public Object getCellEditorValue() { return null; }

	// note that component returned is the JTree
	public Component getTableCellEditorComponent(JTable table,
						     Object value,
						     boolean isSelected,
						     int r, int c) {
	    return tree;
	}

	/**
	 * Overridden to return false, and if the event is a mouse event
	 * it is forwarded to the tree.<p>
	 * The behavior for this is debatable, and should really be offered
	 * as a property. By returning false, all keyboard actions are
	 * implemented in terms of the table. By returning true, the
	 * tree would get a chance to do something with the keyboard
	 * events. For the most part this is ok. But for certain keys,
	 * such as left/right, the tree will expand/collapse where as
	 * the table focus should really move to a different column. Page
	 * up/down should also be implemented in terms of the table.
	 * By returning false this also has the added benefit that clicking
	 * outside of the bounds of the tree node, but still in the tree
	 * column will select the row, whereas if this returned true
	 * that wouldn't be the case.
	 * <p>By returning false we are also enforcing the policy that
	 * the tree will never be editable (at least by a key sequence).
	 */
	public boolean isCellEditable(EventObject e) {
	    if (e instanceof MouseEvent) {
		for (int counter = getColumnCount() - 1; counter >= 0;
		     counter--) {
		    if (getColumnClass(counter) == TreeTableModel.class) {
			MouseEvent me = (MouseEvent)e;
			MouseEvent newME = new MouseEvent(tree, me.getID(),
				   me.getWhen(), me.getModifiers(),
				   me.getX() - getCellRect(0, counter, true).x,
				   me.getY(), me.getClickCount(),
                                   me.isPopupTrigger());
			tree.dispatchEvent(newME);
			break;
		    }
		}
	    }
	    return false;
	}
    }


    /*********************************************************************
     *
     * ListToTreeSelectionModelWrapper extends
     * DefaultTreeSelectionModel to listen for changes in the
     * ListSelectionModel it maintains. Once a change in the
     * ListSelectionModel happens, the paths are updated in the
     * DefaultTreeSelectionModel.
     *
     ********************************************************************/
    class ListToTreeSelectionModelWrapper
	extends DefaultTreeSelectionModel { 

	/** Set to true when we are updating the ListSelectionModel. */
	protected boolean         updatingListSelectionModel;

	public ListToTreeSelectionModelWrapper() {
	    super();
	    getListSelectionModel().addListSelectionListener
	                            (createListSelectionListener());
	}

	/**
	 * Returns the list selection model. ListToTreeSelectionModelWrapper
	 * listens for changes to this model and updates the selected paths
	 * accordingly.
	 */
	ListSelectionModel getListSelectionModel() {
	    return listSelectionModel; 
	}

	/**
	 * This is overridden to set <code>updatingListSelectionModel</code>
	 * and message super. This is the only place DefaultTreeSelectionModel
	 * alters the ListSelectionModel.
	 */
	public void resetRowSelection() {
	    if(!updatingListSelectionModel) {
		updatingListSelectionModel = true;
		try {
		    super.resetRowSelection();
		}
		finally {
		    updatingListSelectionModel = false;
		}
	    }
	    // Notice how we don't message super if
	    // updatingListSelectionModel is true. If
	    // updatingListSelectionModel is true, it implies the
	    // ListSelectionModel has already been updated and the
	    // paths are the only thing that needs to be updated.
	}

	/**
	 * Creates and returns an instance of ListSelectionHandler.
	 */
	protected ListSelectionListener createListSelectionListener() {
	    return new ListSelectionHandler();
	}

	/**
	 * If <code>updatingListSelectionModel</code> is false, this will
	 * reset the selected paths from the selected rows in the list
	 * selection model.
	 */
	protected void updateSelectedPathsFromSelectedRows() {
	    if(!updatingListSelectionModel) {
		updatingListSelectionModel = true;
		try {
		    // This is way expensive, ListSelectionModel needs an
		    // enumerator for iterating.
		    int        min = listSelectionModel.getMinSelectionIndex();
		    int        max = listSelectionModel.getMaxSelectionIndex();

		    clearSelection();
		    if(min != -1 && max != -1) {
			for(int counter = min; counter <= max; counter++) {
			    if(listSelectionModel.isSelectedIndex(counter)) {
				TreePath     selPath = tree.getPathForRow
				                            (counter);

				if(selPath != null) {
				    addSelectionPath(selPath);
				}
			    }
			}
		    }
		}
		finally {
		    updatingListSelectionModel = false;
		}
	    }
	}

	/**
	 * Class responsible for calling updateSelectedPathsFromSelectedRows
	 * when the selection of the list changse.
	 */
	class ListSelectionHandler implements ListSelectionListener {
	    public void valueChanged(ListSelectionEvent e) {
		updateSelectedPathsFromSelectedRows();
	    }
	}
    }
}
