content: /*
   *    This program is free software; you can redistribute it and/or modify
   *    it under the terms of the GNU General Public License as published by
   *    the Free Software Foundation; either version 2 of the License, or
   *    (at your option) any later version.
   *
   *    This program 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
   *    GNU General Public License for more details.
   *
   *    You should have received a copy of the GNU General Public License
   *    along with this program; if not, write to the Free Software
   *    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
   */
  
  /*
   * ArffTable.java
   * Copyright (C) 2005 University of Waikato, Hamilton, New Zealand
   *
   */
  
  package weka.gui.arffviewer;
  
  import weka.core.Attribute;
  import weka.core.Instances;
  import weka.gui.ComponentHelper;
  import weka.gui.JTableHelper;
  import weka.gui.ViewerDialog;
  
  import java.awt.Component;
  import java.awt.event.ActionEvent;
  import java.awt.event.ActionListener;
  import java.awt.datatransfer.StringSelection;
  import java.util.Enumeration;
  import java.util.HashSet;
  import java.util.Iterator;
  
  import javax.swing.AbstractCellEditor;
  import javax.swing.DefaultCellEditor;
  import javax.swing.JButton;
  import javax.swing.JComboBox;
  import javax.swing.JOptionPane;
  import javax.swing.JTable;
  import javax.swing.event.ChangeEvent;
  import javax.swing.event.ChangeListener;
  import javax.swing.event.TableModelEvent;
  import javax.swing.table.TableCellEditor;
  import javax.swing.table.TableModel;
  
  /**
   * A specialized JTable for the Arff-Viewer.
   *
   *
   * @author FracPete (fracpete at waikato dot ac dot nz)
   * @version $Revision: 1.8 $ 
   */
  public class ArffTable
    extends JTable {
    
    /** for serialization */
    static final long serialVersionUID = -2016200506908637967L;
  
    /**
     * a special Editor for editing the relation attribute.
     */
    protected class RelationalCellEditor
      extends AbstractCellEditor
      implements TableCellEditor {
  
      /** for serialization */
      private static final long serialVersionUID = 657969163293205963L;
      
      /** the button for opening the dialog */
      protected JButton m_Button;
      
      /** the current instances */
      protected Instances m_CurrentInst;
      
      /** the row index this editor is for */
      protected int m_RowIndex;
      
      /** the column index this editor is for */
      protected int m_ColumnIndex;
      
      /**
       * initializes the editor
       * 
       * @param rowIndex		the row index
       * @param columnIndex	the column index
       */
      public RelationalCellEditor(int rowIndex, int columnIndex) {
        super();
  
        m_CurrentInst = getInstancesAt(rowIndex, columnIndex);
        m_RowIndex    = rowIndex;
        m_ColumnIndex = columnIndex;
        
        m_Button = new JButton("...");
        m_Button.addActionListener(new ActionListener() {
          public void actionPerformed(ActionEvent evt) {
            ViewerDialog        dialog;
            int                 result;
            
            dialog = new ViewerDialog(null);
            dialog.setTitle(
                "Relational attribute Viewer - " 
                + ((ArffSortedTableModel) getModel()).getInstances().attribute(m_ColumnIndex - 1).name());
            result = dialog.showDialog(m_CurrentInst);
            if (result == ViewerDialog.APPROVE_OPTION) {
              m_CurrentInst = dialog.getInstances();
              fireEditingStopped();
            }
            else {
              fireEditingCanceled();
            }
          }
        });
      }
  
      /**
       * returns the underlying instances at the given position
       * 
       * @param rowIndex		the row index
       * @param columnIndex	the column index
       * @return 			the corresponding instances
       */
      protected Instances getInstancesAt(int rowIndex, int columnIndex) {
        Instances			result;
        ArffSortedTableModel	model;
        double			value;
        
        model = (ArffSortedTableModel) getModel();
        value = model.getInstancesValueAt(rowIndex, columnIndex);
        result = model.getInstances().attribute(columnIndex - 1).relation((int) value);
        
        return result;
      }
      
      /**
       * Sets an initial value for the editor. This will cause the editor to 
       * stopEditing and lose any partially edited value if the editor is 
       * editing when this method is called.
       * 
       * @param table		the table this editor belongs to
       * @param value		the value to edit
       * @param isSelected	whether the cell is selected
       * @param row		the row index
       * @param column		the column index
       * @return			the 
       */
      public Component getTableCellEditorComponent(JTable table,
                                                   Object value,
                                                   boolean isSelected,
                                                   int row,
                                                   int column) {
        return m_Button;
      }
  
      /**
       * Returns the value contained in the editor.
       * 
       * @return		the value contained in the editor
       */
      public Object getCellEditorValue() {
        return m_CurrentInst;
      }
    }
    
    /** the search string */
    private String m_SearchString;
    /** the listeners for changes */
    private HashSet m_ChangeListeners;
    
    /**
     * initializes with no model
     */
    public ArffTable() {
      this(new ArffSortedTableModel(""));
    }
    
    /**
     * initializes with the given model
     * 
     * @param model		the model to use
     */
    public ArffTable(TableModel model) {
      super(model);
      
      setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
    }
    
    /**
     * sets the new model
     * 
     * @param model		the model to use
     */
    public void setModel(TableModel model) {
      ArffSortedTableModel      arffModel;
      
      // initialize the search
      m_SearchString = null;
      
      // init the listeners
      if (m_ChangeListeners == null)
        m_ChangeListeners = new HashSet();
      
      super.setModel(model);
      
      if (model == null)
        return;
      
      if (!(model instanceof ArffSortedTableModel))
        return;
      
      arffModel = (ArffSortedTableModel) model;
      arffModel.addMouseListenerToHeader(this);
      arffModel.addTableModelListener(this);
      arffModel.sort(0);
      setLayout();
      setSelectedColumn(0);
      
      // disable column moving
      if (getTableHeader() != null)
        getTableHeader().setReorderingAllowed(false);
    }
  
    /**
     * returns the cell editor for the given cell
     * 
     * @param row		the row index
     * @param column	the column index
     * @return		the cell editor
     */
    public TableCellEditor getCellEditor(int row, int column) {
      TableCellEditor		result;
      
      // relational attribute?
      if (    (getModel() instanceof ArffSortedTableModel) 
  	 && (((ArffSortedTableModel) getModel()).getType(column) == Attribute.RELATIONAL) )
        result = new RelationalCellEditor(row, column);
      // default
      else
        result = super.getCellEditor(row, column);
      
      return result;
    }
  
    /**
     * returns whether the model is read-only
     * 
     * @return 		true if model is read-only
     */
    public boolean isReadOnly() {
      return ((ArffSortedTableModel) getModel()).isReadOnly();
    }
    
    /**
     * sets whether the model is read-only
     * 
     * @param value	if true the model is set to read-only
     */
    public void setReadOnly(boolean value) {
      ((ArffSortedTableModel) getModel()).setReadOnly(value);
    }
    
    /**
     * sets the cell renderer and calcs the optimal column width
     */
    private void setLayout() {
      ArffSortedTableModel      arffModel;
      int                  i;
      JComboBox            combo;
      Enumeration          enm;
      
      arffModel = (ArffSortedTableModel) getModel();
      
      for (i = 0; i < getColumnCount(); i++) {
        // optimal colwidths (only according to header!)
        JTableHelper.setOptimalHeaderWidth(this, i);
        
        // CellRenderer
        getColumnModel().getColumn(i).setCellRenderer(
            new ArffTableCellRenderer());
        
        // CellEditor
        if (i > 0) {
          if (arffModel.getType(i) == Attribute.NOMINAL) {
            combo = new JComboBox();
            combo.addItem(null);
            enm  = arffModel.getInstances().attribute(i - 1).enumerateValues();
            while (enm.hasMoreElements())
              combo.addItem(enm.nextElement());
            getColumnModel().getColumn(i).setCellEditor(new DefaultCellEditor(combo));
          }
          else {
            getColumnModel().getColumn(i).setCellEditor(null);
          }
        }
      }
    }
    
    /**
     * returns the basically the attribute name of the column and not the
     * HTML column name via getColumnName(int)
     * 
     * @param columnIndex		the column index
     * @return 			the plain name
     */
    public String getPlainColumnName(int columnIndex) {
      ArffSortedTableModel      arffModel;
      String               result;
      
      result = "";
      
      if (getModel() == null)
        return result;
      if (!(getModel() instanceof ArffSortedTableModel))  
        return result;
      
      arffModel = (ArffSortedTableModel) getModel();
      
      if ( (columnIndex >= 0) && (columnIndex < getColumnCount()) ) {
        if (columnIndex == 0)
          result = "No.";
        else
          result = arffModel.getAttributeAt(columnIndex).name();
      }
      
      return result;
    }
    
    /**
     * returns the selected content in a StringSelection that can be copied to
     * the clipboard and used in Excel, if nothing is selected the whole table
     * is copied to the clipboard
     * 
     * @return			the current selection
     */
    public StringSelection getStringSelection() {
      StringSelection         result;
      int[]                   indices;
      int                     i;
      int                     n;
      StringBuffer            tmp;
      
      result = null;
      
      // nothing selected? -> all
      if (getSelectedRow() == -1) {
        // really?
        if (ComponentHelper.showMessageBox(
              getParent(),
              "Question...",
              "Do you really want to copy the whole table?",
              JOptionPane.YES_NO_OPTION,
              JOptionPane.QUESTION_MESSAGE ) != JOptionPane.YES_OPTION)
          return result;
        
        indices = new int[getRowCount()];
        for (i = 0; i < indices.length; i++)
          indices[i] = i;
      }
      else {
        indices = getSelectedRows();
      }
      
      // get header
      tmp = new StringBuffer();
      for (i = 0; i < getColumnCount(); i++) {
        if (i > 0)
          tmp.append("\t");
        tmp.append(getPlainColumnName(i));
      }
      tmp.append("\n");
      
      // get content
      for (i = 0; i < indices.length; i++) {
        for (n = 0; n < getColumnCount(); n++) {
          if (n > 0)
            tmp.append("\t");
          tmp.append(getValueAt(indices[i], n).toString());
        }
        tmp.append("\n");
      }
      
      result = new StringSelection(tmp.toString());
      
      return result;
    }
    
    /**
     * sets the search string to look for in the table, NULL or "" disables
     * the search
     * 
     * @param searchString	the search string to use
     */
    public void setSearchString(String searchString) {
      this.m_SearchString = searchString;
      repaint();
    }
    
    /**
     * returns the search string, can be NULL if no search string is set
     * 
     * @return			the current search string
     */
    public String getSearchString() {
      return m_SearchString;
    }
    
    /**
     * sets the selected column
     * 
     * @param index		the column to select
     */
    public void setSelectedColumn(int index) {
      getColumnModel().getSelectionModel().clearSelection();
      getColumnModel().getSelectionModel().setSelectionInterval(index, index);
      resizeAndRepaint();
      if (getTableHeader() != null)
        getTableHeader().resizeAndRepaint();
    }
    
    /**
     * This fine grain notification tells listeners the exact range of cells, 
     * rows, or columns that changed.
     * 
     * @param e		the table event
     */
    public void tableChanged(TableModelEvent e) {
      super.tableChanged(e);
      
      setLayout();
      notifyListener();
    }
    
    /**
     * notfies all listener of the change
     */
    private void notifyListener() {
      Iterator                iter;
      
      iter = m_ChangeListeners.iterator();
      while (iter.hasNext())
        ((ChangeListener) iter.next()).stateChanged(new ChangeEvent(this));
    }
    
    /**
     * Adds a ChangeListener to the panel
     * 
     * @param l			the listener to add
     */
    public void addChangeListener(ChangeListener l) {
      m_ChangeListeners.add(l);
    }
    
    /**
     * Removes a ChangeListener from the panel
     * 
     * @param l			the listener to remove
     */
    public void removeChangeListener(ChangeListener l) {
      m_ChangeListeners.remove(l);
    }
  }
  