| 1 | /* | 
|---|
| 2 |  *    This program is free software; you can redistribute it and/or modify | 
|---|
| 3 |  *    it under the terms of the GNU General Public License as published by | 
|---|
| 4 |  *    the Free Software Foundation; either version 2 of the License, or | 
|---|
| 5 |  *    (at your option) any later version. | 
|---|
| 6 |  * | 
|---|
| 7 |  *    This program is distributed in the hope that it will be useful, | 
|---|
| 8 |  *    but WITHOUT ANY WARRANTY; without even the implied warranty of | 
|---|
| 9 |  *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | 
|---|
| 10 |  *    GNU General Public License for more details. | 
|---|
| 11 |  * | 
|---|
| 12 |  *    You should have received a copy of the GNU General Public License | 
|---|
| 13 |  *    along with this program; if not, write to the Free Software | 
|---|
| 14 |  *    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. | 
|---|
| 15 |  */ | 
|---|
| 16 |  | 
|---|
| 17 | /* | 
|---|
| 18 |  * ArffTable.java | 
|---|
| 19 |  * Copyright (C) 2005 University of Waikato, Hamilton, New Zealand | 
|---|
| 20 |  * | 
|---|
| 21 |  */ | 
|---|
| 22 |  | 
|---|
| 23 | package weka.gui.arffviewer; | 
|---|
| 24 |  | 
|---|
| 25 | import weka.core.Attribute; | 
|---|
| 26 | import weka.core.Instances; | 
|---|
| 27 | import weka.gui.ComponentHelper; | 
|---|
| 28 | import weka.gui.JTableHelper; | 
|---|
| 29 | import weka.gui.ViewerDialog; | 
|---|
| 30 |  | 
|---|
| 31 | import java.awt.Component; | 
|---|
| 32 | import java.awt.event.ActionEvent; | 
|---|
| 33 | import java.awt.event.ActionListener; | 
|---|
| 34 | import java.awt.datatransfer.StringSelection; | 
|---|
| 35 | import java.util.Enumeration; | 
|---|
| 36 | import java.util.HashSet; | 
|---|
| 37 | import java.util.Iterator; | 
|---|
| 38 |  | 
|---|
| 39 | import javax.swing.AbstractCellEditor; | 
|---|
| 40 | import javax.swing.DefaultCellEditor; | 
|---|
| 41 | import javax.swing.JButton; | 
|---|
| 42 | import javax.swing.JComboBox; | 
|---|
| 43 | import javax.swing.JOptionPane; | 
|---|
| 44 | import javax.swing.JTable; | 
|---|
| 45 | import javax.swing.event.ChangeEvent; | 
|---|
| 46 | import javax.swing.event.ChangeListener; | 
|---|
| 47 | import javax.swing.event.TableModelEvent; | 
|---|
| 48 | import javax.swing.table.TableCellEditor; | 
|---|
| 49 | import javax.swing.table.TableModel; | 
|---|
| 50 |  | 
|---|
| 51 | /** | 
|---|
| 52 |  * A specialized JTable for the Arff-Viewer. | 
|---|
| 53 |  * | 
|---|
| 54 |  * | 
|---|
| 55 |  * @author FracPete (fracpete at waikato dot ac dot nz) | 
|---|
| 56 |  * @version $Revision: 1.8 $  | 
|---|
| 57 |  */ | 
|---|
| 58 | public class ArffTable | 
|---|
| 59 |   extends JTable { | 
|---|
| 60 |    | 
|---|
| 61 |   /** for serialization */ | 
|---|
| 62 |   static final long serialVersionUID = -2016200506908637967L; | 
|---|
| 63 |  | 
|---|
| 64 |   /** | 
|---|
| 65 |    * a special Editor for editing the relation attribute. | 
|---|
| 66 |    */ | 
|---|
| 67 |   protected class RelationalCellEditor | 
|---|
| 68 |     extends AbstractCellEditor | 
|---|
| 69 |     implements TableCellEditor { | 
|---|
| 70 |  | 
|---|
| 71 |     /** for serialization */ | 
|---|
| 72 |     private static final long serialVersionUID = 657969163293205963L; | 
|---|
| 73 |      | 
|---|
| 74 |     /** the button for opening the dialog */ | 
|---|
| 75 |     protected JButton m_Button; | 
|---|
| 76 |      | 
|---|
| 77 |     /** the current instances */ | 
|---|
| 78 |     protected Instances m_CurrentInst; | 
|---|
| 79 |      | 
|---|
| 80 |     /** the row index this editor is for */ | 
|---|
| 81 |     protected int m_RowIndex; | 
|---|
| 82 |      | 
|---|
| 83 |     /** the column index this editor is for */ | 
|---|
| 84 |     protected int m_ColumnIndex; | 
|---|
| 85 |      | 
|---|
| 86 |     /** | 
|---|
| 87 |      * initializes the editor | 
|---|
| 88 |      *  | 
|---|
| 89 |      * @param rowIndex          the row index | 
|---|
| 90 |      * @param columnIndex       the column index | 
|---|
| 91 |      */ | 
|---|
| 92 |     public RelationalCellEditor(int rowIndex, int columnIndex) { | 
|---|
| 93 |       super(); | 
|---|
| 94 |  | 
|---|
| 95 |       m_CurrentInst = getInstancesAt(rowIndex, columnIndex); | 
|---|
| 96 |       m_RowIndex    = rowIndex; | 
|---|
| 97 |       m_ColumnIndex = columnIndex; | 
|---|
| 98 |        | 
|---|
| 99 |       m_Button = new JButton("..."); | 
|---|
| 100 |       m_Button.addActionListener(new ActionListener() { | 
|---|
| 101 |         public void actionPerformed(ActionEvent evt) { | 
|---|
| 102 |           ViewerDialog        dialog; | 
|---|
| 103 |           int                 result; | 
|---|
| 104 |            | 
|---|
| 105 |           dialog = new ViewerDialog(null); | 
|---|
| 106 |           dialog.setTitle( | 
|---|
| 107 |               "Relational attribute Viewer - "  | 
|---|
| 108 |               + ((ArffSortedTableModel) getModel()).getInstances().attribute(m_ColumnIndex - 1).name()); | 
|---|
| 109 |           result = dialog.showDialog(m_CurrentInst); | 
|---|
| 110 |           if (result == ViewerDialog.APPROVE_OPTION) { | 
|---|
| 111 |             m_CurrentInst = dialog.getInstances(); | 
|---|
| 112 |             fireEditingStopped(); | 
|---|
| 113 |           } | 
|---|
| 114 |           else { | 
|---|
| 115 |             fireEditingCanceled(); | 
|---|
| 116 |           } | 
|---|
| 117 |         } | 
|---|
| 118 |       }); | 
|---|
| 119 |     } | 
|---|
| 120 |  | 
|---|
| 121 |     /** | 
|---|
| 122 |      * returns the underlying instances at the given position | 
|---|
| 123 |      *  | 
|---|
| 124 |      * @param rowIndex          the row index | 
|---|
| 125 |      * @param columnIndex       the column index | 
|---|
| 126 |      * @return                  the corresponding instances | 
|---|
| 127 |      */ | 
|---|
| 128 |     protected Instances getInstancesAt(int rowIndex, int columnIndex) { | 
|---|
| 129 |       Instances                 result; | 
|---|
| 130 |       ArffSortedTableModel      model; | 
|---|
| 131 |       double                    value; | 
|---|
| 132 |        | 
|---|
| 133 |       model = (ArffSortedTableModel) getModel(); | 
|---|
| 134 |       value = model.getInstancesValueAt(rowIndex, columnIndex); | 
|---|
| 135 |       result = model.getInstances().attribute(columnIndex - 1).relation((int) value); | 
|---|
| 136 |        | 
|---|
| 137 |       return result; | 
|---|
| 138 |     } | 
|---|
| 139 |      | 
|---|
| 140 |     /** | 
|---|
| 141 |      * Sets an initial value for the editor. This will cause the editor to  | 
|---|
| 142 |      * stopEditing and lose any partially edited value if the editor is  | 
|---|
| 143 |      * editing when this method is called. | 
|---|
| 144 |      *  | 
|---|
| 145 |      * @param table             the table this editor belongs to | 
|---|
| 146 |      * @param value             the value to edit | 
|---|
| 147 |      * @param isSelected        whether the cell is selected | 
|---|
| 148 |      * @param row               the row index | 
|---|
| 149 |      * @param column            the column index | 
|---|
| 150 |      * @return                  the  | 
|---|
| 151 |      */ | 
|---|
| 152 |     public Component getTableCellEditorComponent(JTable table, | 
|---|
| 153 |                                                  Object value, | 
|---|
| 154 |                                                  boolean isSelected, | 
|---|
| 155 |                                                  int row, | 
|---|
| 156 |                                                  int column) { | 
|---|
| 157 |       return m_Button; | 
|---|
| 158 |     } | 
|---|
| 159 |  | 
|---|
| 160 |     /** | 
|---|
| 161 |      * Returns the value contained in the editor. | 
|---|
| 162 |      *  | 
|---|
| 163 |      * @return          the value contained in the editor | 
|---|
| 164 |      */ | 
|---|
| 165 |     public Object getCellEditorValue() { | 
|---|
| 166 |       return m_CurrentInst; | 
|---|
| 167 |     } | 
|---|
| 168 |   } | 
|---|
| 169 |    | 
|---|
| 170 |   /** the search string */ | 
|---|
| 171 |   private String m_SearchString; | 
|---|
| 172 |   /** the listeners for changes */ | 
|---|
| 173 |   private HashSet m_ChangeListeners; | 
|---|
| 174 |    | 
|---|
| 175 |   /** | 
|---|
| 176 |    * initializes with no model | 
|---|
| 177 |    */ | 
|---|
| 178 |   public ArffTable() { | 
|---|
| 179 |     this(new ArffSortedTableModel("")); | 
|---|
| 180 |   } | 
|---|
| 181 |    | 
|---|
| 182 |   /** | 
|---|
| 183 |    * initializes with the given model | 
|---|
| 184 |    *  | 
|---|
| 185 |    * @param model               the model to use | 
|---|
| 186 |    */ | 
|---|
| 187 |   public ArffTable(TableModel model) { | 
|---|
| 188 |     super(model); | 
|---|
| 189 |      | 
|---|
| 190 |     setAutoResizeMode(JTable.AUTO_RESIZE_OFF); | 
|---|
| 191 |   } | 
|---|
| 192 |    | 
|---|
| 193 |   /** | 
|---|
| 194 |    * sets the new model | 
|---|
| 195 |    *  | 
|---|
| 196 |    * @param model               the model to use | 
|---|
| 197 |    */ | 
|---|
| 198 |   public void setModel(TableModel model) { | 
|---|
| 199 |     ArffSortedTableModel      arffModel; | 
|---|
| 200 |      | 
|---|
| 201 |     // initialize the search | 
|---|
| 202 |     m_SearchString = null; | 
|---|
| 203 |      | 
|---|
| 204 |     // init the listeners | 
|---|
| 205 |     if (m_ChangeListeners == null) | 
|---|
| 206 |       m_ChangeListeners = new HashSet(); | 
|---|
| 207 |      | 
|---|
| 208 |     super.setModel(model); | 
|---|
| 209 |      | 
|---|
| 210 |     if (model == null) | 
|---|
| 211 |       return; | 
|---|
| 212 |      | 
|---|
| 213 |     if (!(model instanceof ArffSortedTableModel)) | 
|---|
| 214 |       return; | 
|---|
| 215 |      | 
|---|
| 216 |     arffModel = (ArffSortedTableModel) model; | 
|---|
| 217 |     arffModel.addMouseListenerToHeader(this); | 
|---|
| 218 |     arffModel.addTableModelListener(this); | 
|---|
| 219 |     arffModel.sort(0); | 
|---|
| 220 |     setLayout(); | 
|---|
| 221 |     setSelectedColumn(0); | 
|---|
| 222 |      | 
|---|
| 223 |     // disable column moving | 
|---|
| 224 |     if (getTableHeader() != null) | 
|---|
| 225 |       getTableHeader().setReorderingAllowed(false); | 
|---|
| 226 |   } | 
|---|
| 227 |  | 
|---|
| 228 |   /** | 
|---|
| 229 |    * returns the cell editor for the given cell | 
|---|
| 230 |    *  | 
|---|
| 231 |    * @param row         the row index | 
|---|
| 232 |    * @param column      the column index | 
|---|
| 233 |    * @return            the cell editor | 
|---|
| 234 |    */ | 
|---|
| 235 |   public TableCellEditor getCellEditor(int row, int column) { | 
|---|
| 236 |     TableCellEditor             result; | 
|---|
| 237 |      | 
|---|
| 238 |     // relational attribute? | 
|---|
| 239 |     if (    (getModel() instanceof ArffSortedTableModel)  | 
|---|
| 240 |          && (((ArffSortedTableModel) getModel()).getType(column) == Attribute.RELATIONAL) ) | 
|---|
| 241 |       result = new RelationalCellEditor(row, column); | 
|---|
| 242 |     // default | 
|---|
| 243 |     else | 
|---|
| 244 |       result = super.getCellEditor(row, column); | 
|---|
| 245 |      | 
|---|
| 246 |     return result; | 
|---|
| 247 |   } | 
|---|
| 248 |  | 
|---|
| 249 |   /** | 
|---|
| 250 |    * returns whether the model is read-only | 
|---|
| 251 |    *  | 
|---|
| 252 |    * @return            true if model is read-only | 
|---|
| 253 |    */ | 
|---|
| 254 |   public boolean isReadOnly() { | 
|---|
| 255 |     return ((ArffSortedTableModel) getModel()).isReadOnly(); | 
|---|
| 256 |   } | 
|---|
| 257 |    | 
|---|
| 258 |   /** | 
|---|
| 259 |    * sets whether the model is read-only | 
|---|
| 260 |    *  | 
|---|
| 261 |    * @param value       if true the model is set to read-only | 
|---|
| 262 |    */ | 
|---|
| 263 |   public void setReadOnly(boolean value) { | 
|---|
| 264 |     ((ArffSortedTableModel) getModel()).setReadOnly(value); | 
|---|
| 265 |   } | 
|---|
| 266 |    | 
|---|
| 267 |   /** | 
|---|
| 268 |    * sets the cell renderer and calcs the optimal column width | 
|---|
| 269 |    */ | 
|---|
| 270 |   private void setLayout() { | 
|---|
| 271 |     ArffSortedTableModel      arffModel; | 
|---|
| 272 |     int                  i; | 
|---|
| 273 |     JComboBox            combo; | 
|---|
| 274 |     Enumeration          enm; | 
|---|
| 275 |      | 
|---|
| 276 |     arffModel = (ArffSortedTableModel) getModel(); | 
|---|
| 277 |      | 
|---|
| 278 |     for (i = 0; i < getColumnCount(); i++) { | 
|---|
| 279 |       // optimal colwidths (only according to header!) | 
|---|
| 280 |       JTableHelper.setOptimalHeaderWidth(this, i); | 
|---|
| 281 |        | 
|---|
| 282 |       // CellRenderer | 
|---|
| 283 |       getColumnModel().getColumn(i).setCellRenderer( | 
|---|
| 284 |           new ArffTableCellRenderer()); | 
|---|
| 285 |        | 
|---|
| 286 |       // CellEditor | 
|---|
| 287 |       if (i > 0) { | 
|---|
| 288 |         if (arffModel.getType(i) == Attribute.NOMINAL) { | 
|---|
| 289 |           combo = new JComboBox(); | 
|---|
| 290 |           combo.addItem(null); | 
|---|
| 291 |           enm  = arffModel.getInstances().attribute(i - 1).enumerateValues(); | 
|---|
| 292 |           while (enm.hasMoreElements()) | 
|---|
| 293 |             combo.addItem(enm.nextElement()); | 
|---|
| 294 |           getColumnModel().getColumn(i).setCellEditor(new DefaultCellEditor(combo)); | 
|---|
| 295 |         } | 
|---|
| 296 |         else { | 
|---|
| 297 |           getColumnModel().getColumn(i).setCellEditor(null); | 
|---|
| 298 |         } | 
|---|
| 299 |       } | 
|---|
| 300 |     } | 
|---|
| 301 |   } | 
|---|
| 302 |    | 
|---|
| 303 |   /** | 
|---|
| 304 |    * returns the basically the attribute name of the column and not the | 
|---|
| 305 |    * HTML column name via getColumnName(int) | 
|---|
| 306 |    *  | 
|---|
| 307 |    * @param columnIndex         the column index | 
|---|
| 308 |    * @return                    the plain name | 
|---|
| 309 |    */ | 
|---|
| 310 |   public String getPlainColumnName(int columnIndex) { | 
|---|
| 311 |     ArffSortedTableModel      arffModel; | 
|---|
| 312 |     String               result; | 
|---|
| 313 |      | 
|---|
| 314 |     result = ""; | 
|---|
| 315 |      | 
|---|
| 316 |     if (getModel() == null) | 
|---|
| 317 |       return result; | 
|---|
| 318 |     if (!(getModel() instanceof ArffSortedTableModel))   | 
|---|
| 319 |       return result; | 
|---|
| 320 |      | 
|---|
| 321 |     arffModel = (ArffSortedTableModel) getModel(); | 
|---|
| 322 |      | 
|---|
| 323 |     if ( (columnIndex >= 0) && (columnIndex < getColumnCount()) ) { | 
|---|
| 324 |       if (columnIndex == 0) | 
|---|
| 325 |         result = "No."; | 
|---|
| 326 |       else | 
|---|
| 327 |         result = arffModel.getAttributeAt(columnIndex).name(); | 
|---|
| 328 |     } | 
|---|
| 329 |      | 
|---|
| 330 |     return result; | 
|---|
| 331 |   } | 
|---|
| 332 |    | 
|---|
| 333 |   /** | 
|---|
| 334 |    * returns the selected content in a StringSelection that can be copied to | 
|---|
| 335 |    * the clipboard and used in Excel, if nothing is selected the whole table | 
|---|
| 336 |    * is copied to the clipboard | 
|---|
| 337 |    *  | 
|---|
| 338 |    * @return                    the current selection | 
|---|
| 339 |    */ | 
|---|
| 340 |   public StringSelection getStringSelection() { | 
|---|
| 341 |     StringSelection         result; | 
|---|
| 342 |     int[]                   indices; | 
|---|
| 343 |     int                     i; | 
|---|
| 344 |     int                     n; | 
|---|
| 345 |     StringBuffer            tmp; | 
|---|
| 346 |      | 
|---|
| 347 |     result = null; | 
|---|
| 348 |      | 
|---|
| 349 |     // nothing selected? -> all | 
|---|
| 350 |     if (getSelectedRow() == -1) { | 
|---|
| 351 |       // really? | 
|---|
| 352 |       if (ComponentHelper.showMessageBox( | 
|---|
| 353 |             getParent(), | 
|---|
| 354 |             "Question...", | 
|---|
| 355 |             "Do you really want to copy the whole table?", | 
|---|
| 356 |             JOptionPane.YES_NO_OPTION, | 
|---|
| 357 |             JOptionPane.QUESTION_MESSAGE ) != JOptionPane.YES_OPTION) | 
|---|
| 358 |         return result; | 
|---|
| 359 |        | 
|---|
| 360 |       indices = new int[getRowCount()]; | 
|---|
| 361 |       for (i = 0; i < indices.length; i++) | 
|---|
| 362 |         indices[i] = i; | 
|---|
| 363 |     } | 
|---|
| 364 |     else { | 
|---|
| 365 |       indices = getSelectedRows(); | 
|---|
| 366 |     } | 
|---|
| 367 |      | 
|---|
| 368 |     // get header | 
|---|
| 369 |     tmp = new StringBuffer(); | 
|---|
| 370 |     for (i = 0; i < getColumnCount(); i++) { | 
|---|
| 371 |       if (i > 0) | 
|---|
| 372 |         tmp.append("\t"); | 
|---|
| 373 |       tmp.append(getPlainColumnName(i)); | 
|---|
| 374 |     } | 
|---|
| 375 |     tmp.append("\n"); | 
|---|
| 376 |      | 
|---|
| 377 |     // get content | 
|---|
| 378 |     for (i = 0; i < indices.length; i++) { | 
|---|
| 379 |       for (n = 0; n < getColumnCount(); n++) { | 
|---|
| 380 |         if (n > 0) | 
|---|
| 381 |           tmp.append("\t"); | 
|---|
| 382 |         tmp.append(getValueAt(indices[i], n).toString()); | 
|---|
| 383 |       } | 
|---|
| 384 |       tmp.append("\n"); | 
|---|
| 385 |     } | 
|---|
| 386 |      | 
|---|
| 387 |     result = new StringSelection(tmp.toString()); | 
|---|
| 388 |      | 
|---|
| 389 |     return result; | 
|---|
| 390 |   } | 
|---|
| 391 |    | 
|---|
| 392 |   /** | 
|---|
| 393 |    * sets the search string to look for in the table, NULL or "" disables | 
|---|
| 394 |    * the search | 
|---|
| 395 |    *  | 
|---|
| 396 |    * @param searchString        the search string to use | 
|---|
| 397 |    */ | 
|---|
| 398 |   public void setSearchString(String searchString) { | 
|---|
| 399 |     this.m_SearchString = searchString; | 
|---|
| 400 |     repaint(); | 
|---|
| 401 |   } | 
|---|
| 402 |    | 
|---|
| 403 |   /** | 
|---|
| 404 |    * returns the search string, can be NULL if no search string is set | 
|---|
| 405 |    *  | 
|---|
| 406 |    * @return                    the current search string | 
|---|
| 407 |    */ | 
|---|
| 408 |   public String getSearchString() { | 
|---|
| 409 |     return m_SearchString; | 
|---|
| 410 |   } | 
|---|
| 411 |    | 
|---|
| 412 |   /** | 
|---|
| 413 |    * sets the selected column | 
|---|
| 414 |    *  | 
|---|
| 415 |    * @param index               the column to select | 
|---|
| 416 |    */ | 
|---|
| 417 |   public void setSelectedColumn(int index) { | 
|---|
| 418 |     getColumnModel().getSelectionModel().clearSelection(); | 
|---|
| 419 |     getColumnModel().getSelectionModel().setSelectionInterval(index, index); | 
|---|
| 420 |     resizeAndRepaint(); | 
|---|
| 421 |     if (getTableHeader() != null) | 
|---|
| 422 |       getTableHeader().resizeAndRepaint(); | 
|---|
| 423 |   } | 
|---|
| 424 |    | 
|---|
| 425 |   /** | 
|---|
| 426 |    * This fine grain notification tells listeners the exact range of cells,  | 
|---|
| 427 |    * rows, or columns that changed. | 
|---|
| 428 |    *  | 
|---|
| 429 |    * @param e           the table event | 
|---|
| 430 |    */ | 
|---|
| 431 |   public void tableChanged(TableModelEvent e) { | 
|---|
| 432 |     super.tableChanged(e); | 
|---|
| 433 |      | 
|---|
| 434 |     setLayout(); | 
|---|
| 435 |     notifyListener(); | 
|---|
| 436 |   } | 
|---|
| 437 |    | 
|---|
| 438 |   /** | 
|---|
| 439 |    * notfies all listener of the change | 
|---|
| 440 |    */ | 
|---|
| 441 |   private void notifyListener() { | 
|---|
| 442 |     Iterator                iter; | 
|---|
| 443 |      | 
|---|
| 444 |     iter = m_ChangeListeners.iterator(); | 
|---|
| 445 |     while (iter.hasNext()) | 
|---|
| 446 |       ((ChangeListener) iter.next()).stateChanged(new ChangeEvent(this)); | 
|---|
| 447 |   } | 
|---|
| 448 |    | 
|---|
| 449 |   /** | 
|---|
| 450 |    * Adds a ChangeListener to the panel | 
|---|
| 451 |    *  | 
|---|
| 452 |    * @param l                   the listener to add | 
|---|
| 453 |    */ | 
|---|
| 454 |   public void addChangeListener(ChangeListener l) { | 
|---|
| 455 |     m_ChangeListeners.add(l); | 
|---|
| 456 |   } | 
|---|
| 457 |    | 
|---|
| 458 |   /** | 
|---|
| 459 |    * Removes a ChangeListener from the panel | 
|---|
| 460 |    *  | 
|---|
| 461 |    * @param l                   the listener to remove | 
|---|
| 462 |    */ | 
|---|
| 463 |   public void removeChangeListener(ChangeListener l) { | 
|---|
| 464 |     m_ChangeListeners.remove(l); | 
|---|
| 465 |   } | 
|---|
| 466 | } | 
|---|