content: //  Copyright 2006, Clark N. Hobbie
  //
  //  This file is part of the util library.
  //
  //  The util library is free software; you can redistribute it and/or modify it
  //  under the terms of the Lesser GNU General Public License as published by
  //  the Free Software Foundation; either version 2.1 of the License, or (at
  //  your option) any later version.
  //
  //  The util library is distributed in the hope that it will be useful, but
  //  WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
  //  or FITNESS FOR A PARTICULAR PURPOSE.  See the Lesser GNU General Public
  //  License for more details.
  //
  //  You should have received a copy of the Lesser GNU General Public License
  //  along with the util library; if not, write to the Free Software Foundation,
  //  Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
  //
  /*
   * @(#)JTreeTable.java	1.2 98/10/27
   *
   * Copyright 1997, 1998 by Sun Microsystems, Inc.,
   * 901 San Antonio Road, Palo Alto, California, 94303, U.S.A.
   * All rights reserved.
   *
   * This software is the confidential and proprietary information
   * of Sun Microsystems, Inc. ("Confidential Information").  You
   * shall not disclose such Confidential Information and shall use
   * it only in accordance with the terms of the license agreement
   * you entered into with Sun.
   */
  package com.lts.swing.treetable;
  
  import java.awt.Component;
  import java.awt.Dimension;
  import java.awt.Graphics;
  import java.awt.event.MouseEvent;
  import java.util.ArrayList;
  import java.util.EventObject;
  import java.util.List;
  
  import javax.swing.JTable;
  import javax.swing.JTree;
  import javax.swing.ListSelectionModel;
  import javax.swing.LookAndFeel;
  import javax.swing.UIManager;
  import javax.swing.event.ListSelectionEvent;
  import javax.swing.event.ListSelectionListener;
  import javax.swing.event.TableModelEvent;
  import javax.swing.table.TableCellEditor;
  import javax.swing.table.TableCellRenderer;
  import javax.swing.tree.DefaultTreeCellRenderer;
  import javax.swing.tree.DefaultTreeModel;
  import javax.swing.tree.DefaultTreeSelectionModel;
  import javax.swing.tree.MutableTreeNode;
  import javax.swing.tree.TreeCellRenderer;
  import javax.swing.tree.TreeModel;
  import javax.swing.tree.TreePath;
  
  /**
   * 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.  
   *
   * @version 1.2 10/27/98
   *
   * @author Philip Milne
   * @author Scott Violet
   */
  public class TreeTable 
  	extends JTable 
  {
      /**
  	 * 
  	 */
  	private static final long serialVersionUID = 1L;
  	/** A subclass of JTree. */
      protected TreeTableCellRenderer tree;
      protected TreeTableModelAdapter myAdapter;
  
      public TreeTableModelAdapter getAdapter()
      {
      	return myAdapter;
      }
      
      
      public TreeTable(TreeTableModel treeTableModel) 
      {
  		super();
  		initialize(treeTableModel);
      }
      
      
      public void initialize (TreeTableModel model)
      {
      	setModel(model);
      }
  
  
  	public void setModel(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.
  		myAdapter = new TreeTableModelAdapter(treeTableModel, tree); 
  		super.setModel(myAdapter);
  	
  		// 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);
  		}
  	}
  	
  	
  	public Object getSelectedNode ()
  	{
  		int row = getSelectedRow();
  		
  		if (-1 == row)
  			return null;
  		else
  			return myAdapter.nodeForRow(row);
  	}
  
  
      /**
       * Overridden to message super and forward the method to the tree.
       * Since the tree is not actually in the component hieachy 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;
      }
      
      
      public List getSelectedNodes()
      {
      	List l = new ArrayList();
      	
      	TreePath[] paths = getTree().getSelectionPaths();
      	for (int i = 0; i < paths.length; i++)
      	{
      		TreePath tpath = paths[i];
      		Object o = tpath.getLastPathComponent();
      		l.add(o);
      	}
      	
      	return l;
      }
      
      
      public void removeNode (MutableTreeNode node)
      {
      	DefaultTreeModel dtm = (DefaultTreeModel) getTree().getModel();
      	dtm.removeNodeFromParent(node);
      }
      
  
      /**
       * A TreeCellRenderer that displays a JTree.
       */
      public class TreeTableCellRenderer 
      	extends JTree 
      	implements TableCellRenderer 
      {
  		/**
  		 * 
  		 */
  		private static final long serialVersionUID = 1L;
  		/** 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 (
  					TreeTable.this != null 
  					&& TreeTable.this.getRowHeight() != rowHeight
  				) 
  				{
  				    TreeTable.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, TreeTable.this.getHeight());
  		}
  	
  		/**
  		 * Sublcassed 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. Component returned is the
       * JTree.
       */
      public class TreeTableCellEditor 
      	extends AbstractCellEditor 
      	implements TableCellEditor 
      {
  		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.
       */
      @SuppressWarnings("serial")
  	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();
  		    }
  		}
      }
      
      
  	public void tableChanged(TableModelEvent e)
  	{
  		super.tableChanged(e);
  	}
  }
  