/*
 * COPYRIGHT (c) 1998-1999 Jan-Henrik Haukeland, http://www.tildeslash.com/
 */

package ts.ui;

import java.awt.event.*;
import java.beans.*;
import java.util.Vector;
import java.awt.Component;

import javax.swing.*;
import javax.swing.table.*;
import javax.swing.event.*;

/**
 * A table for viewing data in a two-dimensional grid. The table can
 * have any number of columns and rows. Columns can be resized by use of
 * mouse drag. It's possible to hide columns from the grid. The class
 * provide two interfaces: 
 * <OL>
 * <LI>A callback interface which can be implemented by classes that
 * want to receive information about table selections.
 * <LI>An interface allowing the implementor to preprocess a value 
 * before it's set in a cell and either discard or allow the new 
 * value to  be commited.
 * </OL>
 * 
 * @author  Jan-Henrik Haukeland
 * @version $Id: TsTable.java,v 1.6 1999/02/21 08:47:23 hauk Exp $
 *
 * @see TsTableModel
 */
public class TsTable extends JTable
{

  /**
   * Objects implementing this interface can be passed to the
   * TsTable class, to have their singleClick() or doubleClick() 
   * functions called in response to single or double click in 
   * the Table.
   */
  public interface Callback 
  {
    /**
     * Called on a single click on a row
     */
    void singleClick(Object aTable, int row, int col);
    
    /**
     * Called upon double-clicking on a row
     */
    void doubleClick(Object aTable, int row, int col);

  }
  

  /**
   * Objects implementing this interface can be passed to the
   * TsTable class to test a value before it's commited in 
   * a cell.
   */
  public interface Cellmask 
  {
    /**
     * Called before setting values in a cell
     */
    boolean canSetValue( Object value, int row, int col);
    
    /**
     * Returns true if the cell is editable
     */
    boolean  canEditCell( int row, int col);
  }
  


  protected Callback     myCallBack;    // what to call back to 
  protected Cellmask     myCellMask;    // what to call back to 
  protected TsTableModel myTableModel;  // Table Model



  
  //------------------------- Constructors ----------------------------//
 

  /**
   * Create a new empty Table
   */
  public TsTable()
  {
    myTableModel= new TsTableModel();
    super.autoCreateColumnsFromModel= true;
    super.setModel(myTableModel);
    addListeners();
  }

  
  /** 
   * Create a new Table with the submitted titles and dataset
   * @param titles table titles
   * @param data table data
   */
  public TsTable(Object[] titles, Object[][]data)
  {
    myTableModel= new TsTableModel(titles, data);
    super.autoCreateColumnsFromModel= true;
    super.setModel(myTableModel);
    addListeners();
  }


  //---------------------- public methods -----------------------//


  /**
   * Set if cells in the table could be edited. 
   * @param true if the table will allow edit in cells else false
   */
  public void setEditable(boolean b)
  {
    myTableModel.setEditable(b);
  }


  /**
   * Set the number of hidden columns in the Table. Columns can
   * only be hidden from left to right in the column set. I.e. if
   * the number of hidden columns is set to 1, then the first column
   * in the table is hidden, that is, not displayed in the grid. If
   * the number is 2 then the first two columns are hidden and so on.
   * If hidden columns are enabled with this method, you should still
   * work with the Table as if no columns was hidden. This method will
   * fire a TableStructureChanged event which will reset any changes
   * in the table ui, e.g. column size, cell editors etc. (In other
   * words call this method first before you do stuff like that)
   *
   * @param hidden number of columns that should be hidden.
   */
  public void setHiddenColumns(int hidden)
  {
    myTableModel.setHiddenColumns(hidden);
  }


  /**
   * Set a new DataModel for the table. The model include
   * both data for cells and titles
   *
   * @param model A new TsTableModel
   */
  public void setModel(TsTableModel model)
  {
    super.setModel(model);
    myTableModel= model;
  }


  /**
   * Set a new Data set for the table
   *
   * @param data A new data set
   */
  public void setData(Vector data)
  {
    myTableModel.setData(data);
  }


  /**
   * Set new column titles for the Table
   *
   * @param titles Column titles
   */
  public void setTitles(Vector titles)
  {
    myTableModel.setTitles(titles);
  }


  /**
   * Set object to call back to on table selection
   */
  public void setCallBack(Callback c)
  {
    myCallBack= c;
  }


  /**
   * Set object to call back to on edit
   */
  public void setCellMask(Cellmask c)
  {
    myCellMask= c;
  }






  /**
   * Get the row at index. This method will (of course) also return
   * values in any hidden columns.
   *
   * @return A vector representing the row at row index
   */
  public Vector getRowVector(int index)
  {
    return myTableModel.getRowVector(index);
  }


  /**
   * Get the row at index as an Object array
   *
   * @return the row at row index
   */
  public Object[] getRow(int index)
  {
    return myTableModel.getRow(index);
  }


  /**
   * Get the selected row index
   *
   * @return the selected row index
   */
  public int selected()
  {
    return getSelectedRow();
  }


  /**
   * Return true if a row in the grid is selected
   *
   * @return true if a row is selected
   */
  public boolean isSelected()
  {
    return ( getSelectedRow() >= 0 ) ;
  }


  /**
   * Remove all cells from the TsTable and from the dataset 
   */
  public void clear()
  {
    myTableModel.clear(); 
  }


  /** 
   * Compute the minimum size of each column. The algorithm is to
   * loop through every cell in each column and set the width to the
   * widest cell in the column. The table headers is not included in the
   * computing. This is by design. <B>Note!</B>This method should 
   * be used with caution since it's cpu demanding for large dataset. The
   * method could also give some unespected results if the table is
   * put in a viewport (which is normal) and the viewport width is
   * less than the combined width of the columns, then some of the
   * columns may not be visible in the viewport.  
   */
  public void computeColumnWidth()
  {
    TableModel model=   myTableModel;
    TableColumn column= null;
    Component comp=     null;
    int cellWidth=      0;
    int currentWidth=   0;
    int columns=        getColumnCount();
    int rows=           getRowCount();

    // Loop through every column in the table
    for ( int i = 0; i < columns; i++ ) {
      column = getColumnModel().getColumn(i);
      
      // Scan through every cell in the current column and
      // set the cell width to the width of the largest cell
      for ( int j= 0; j < rows; j++ ) {
	comp = getDefaultRenderer(model.getColumnClass(i)).
	    getTableCellRendererComponent(
		this, getValueAt(j,i),
		false, false, 0, i);
	currentWidth= comp.getPreferredSize().width;
	cellWidth=    currentWidth>cellWidth?currentWidth:cellWidth;
      }
      column.setPreferredWidth(cellWidth);
    }

    sizeColumnsToFit(-1);
  } 



  /**
   * Overridden to allow callback object to check if the value is legal
   * before changes in a cell is commited
   *
   * @param aValue - the new value 
   * @param row - the row whose value is to be changed 
   * @param column - the column whose value is to be changed 
   */
  public void setValueAt(Object aValue, int row, int column) 
  {
    if ( myCellMask == null || myCellMask.canSetValue(aValue,row,column) )
        super.setValueAt(aValue, row,column);
  }


  /**
   * Overridden to allow callback object to set edit permission
   * for the cell
   *
   * @param row - the row whose value is to be looked up 
   * @param column - the column whose value is to be looked up 
   */
  public boolean isCellEditable (int row, int column) 
  {
    if ( myCellMask == null )
        return super.isCellEditable(row, column);
    else 
        return myCellMask.canEditCell(row, column);
  }

  
  //----------------------- protected methods -------------------------//


  /**
   * Callback handling
   */
  protected void addListeners()
  {
    
    /*
     * Call the appropriate mouse click handler
     */
    this.addMouseListener( new MouseAdapter() {
      public void mouseClicked(MouseEvent e) {
	if (myCallBack != null) {
	  
	  int aRow= TsTable.this.getSelectedRow();
	  int aCol= TsTable.this.getSelectedColumn();
	
	  if ( aRow != -1 ) {
	    boolean isDclick= e.getClickCount()>1;
	    if (isDclick)
		myCallBack.doubleClick(TsTable.this, aRow, aCol);
	    else
		myCallBack.singleClick(TsTable.this, aRow, aCol);
	  }
	}
      }
    });

    
    /*
     * Simulate a mouse double click if <enter> is pressed.
     * Clear selection if <escape> or <cancel> is pressed.
     */
    this.addKeyListener( new KeyAdapter() {
      public void keyReleased(KeyEvent e) {
	if (myCallBack != null) {
	  
	  if ( e.getKeyCode() == KeyEvent.VK_ENTER ) {
	    int aRow= TsTable.this.getSelectedRow();
	    int aCol= TsTable.this.getSelectedColumn();
	    
	    if ( aRow != -1 ) 
		myCallBack.doubleClick(TsTable.this, aRow, aCol);
	  }
	  
	  if ( e.getKeyCode() == KeyEvent.VK_ESCAPE ||
	       e.getKeyCode() == KeyEvent.VK_CANCEL ) {
	    TsTable.this.clearSelection();
	  }
	}
	super.keyReleased(e);
      }
    });
	
  }
  
}











