1   /*
2    *  Copyright (c) 1998-2005, The University of Sheffield.
3    *
4    *  This file is part of GATE (see http://gate.ac.uk/), and is free
5    *  software, licenced under the GNU Library General Public License,
6    *  Version 2, June 1991 (in the distribution as file licence.html,
7    *  and also available at http://gate.ac.uk/gate/licence.html).
8    *
9    *  XXJTable.java
10   *
11   *  Valentin Tablan, 25-Jun-2004
12   *
13   *  $Id: XJTable.java,v 1.33 2005/01/11 13:51:37 ian Exp $
14   */
15  
16  package gate.swing;
17  
18  import java.awt.Component;
19  import java.awt.Dimension;
20  import java.awt.event.*;
21  import java.awt.event.MouseAdapter;
22  import java.awt.event.MouseEvent;
23  import java.util.*;
24  import java.util.ArrayList;
25  import java.util.List;
26  import javax.swing.*;
27  import javax.swing.Timer;
28  import javax.swing.event.TableModelEvent;
29  import javax.swing.event.TableModelListener;
30  import javax.swing.table.*;
31  import javax.swing.table.AbstractTableModel;
32  import javax.swing.table.TableModel;
33  import gate.util.ObjectComparator;
34  
35  /**
36   * A "smarter" JTable. Feaures include:
37   * <ul>
38   * <li>sorting the table using the values from a column as keys</li>
39   * <li>updating the widths of the columns so they accommodate the contents to
40   * their preferred sizes.</li>
41   * <li>sizing the rows according to the preferred sizes of the renderers</li>
42   * <li>ability to hide columns</li>
43   * </ul>
44   * It uses a custom made model that stands between the table model set by the
45   * user and the gui component. This middle model is responsible for sorting the
46   * rows.
47   */
48  public class XJTable extends JTable{
49    
50    public XJTable(){
51      super();
52    }
53    
54    public XJTable(TableModel model){
55      super();
56      setModel(model);
57    }
58    
59    public void setModel(TableModel dataModel) {
60      sortingModel = new SortingModel(dataModel);
61      super.setModel(sortingModel);
62      newColumns();
63    }
64    
65    /**
66     * Called when the columns have changed.
67     */
68    protected void newColumns(){
69      columnData = new ArrayList(dataModel.getColumnCount());
70      for(int i = 0; i < dataModel.getColumnCount(); i++)
71        columnData.add(new ColumnData(i));
72      adjustSizes();
73    }
74    
75    /**
76     * This is called whenever the UI is initialised or changed
77     */
78    public void updateUI() {
79      super.updateUI();
80      getTableHeader().addMouseListener(new HeaderMouseListener());
81      adjustSizes();
82    }
83    
84    public Dimension getPreferredSize(){
85      int width = 0;
86      for(int i = 0; i < getColumnModel().getColumnCount(); i++)
87        width += getColumnModel().getColumn(i).getPreferredWidth();
88      int height = 0;
89      for(int i = 0; i < getRowCount(); i++)
90        height += getRowHeight(i);
91      return new Dimension(width, height);
92    }
93    
94    public Dimension getPreferredScrollableViewportSize(){
95      return getPreferredSize();
96    }
97    
98    /**
99     * Sets the preferred widths for the columns and rows based or the preferred
100    * sizes of the renderers.
101    *
102    */
103   public void adjustSizes(){
104     Iterator colIter = columnData.iterator();
105     while(colIter.hasNext()){
106       ((ColumnData)colIter.next()).adjustColumnWidth();
107     }
108     repaint();
109   }
110   
111   /**
112    * Converts a row number from the model co-ordinates system to the view's. 
113    * @param modelRow the row number in the model
114    * @return the corresponding row number in the view. 
115    */
116   public int rowModelToView(int modelRow){
117     return sortingModel.sourceToTarget(modelRow);
118   }
119 
120   /**
121    * @return Returns the ascending.
122    */
123   public boolean isAscending() {
124     return ascending;
125   }
126   
127   /**
128    * Gets the hidden state for a column
129    * @param columnIndex the column
130    * @return the hidden state
131    */
132   public boolean isColumnHidden(int columnIndex){
133     return ((ColumnData)columnData.get(columnIndex)).isHidden();
134   }
135   
136   /**
137    * @param ascending The ascending to set.
138    */
139   public void setAscending(boolean ascending) {
140     this.ascending = ascending;
141   }
142   /**
143    * Converts a row number from the view co-ordinates system to the model's. 
144    * @param viewRow the row number in the view.
145    * @return the corresponding row number in the model. 
146    */
147   public int rowViewToModel(int viewRow){
148     return sortingModel.targetToSource(viewRow);
149   }
150   
151   /**
152    * Sets the custom comparator to be used for a particular column. Columns that
153    * don't have a custom comparator will be sorted using the natural order.
154    * @param column the column index.
155    * @param comparator the comparator to be used.
156    */
157   public void setComparator(int column, Comparator comparator){
158     ((ColumnData)columnData.get(column)).comparator = comparator;
159   }
160     
161   /**
162    * @return Returns the sortable.
163    */
164   public boolean isSortable(){
165     return sortable;
166   }
167   /**
168    * @param sortable The sortable to set.
169    */
170   public void setSortable(boolean sortable){
171     this.sortable = sortable;
172   }
173   /**
174    * @return Returns the sortColumn.
175    */
176   public int getSortedColumn(){
177     return sortedColumn;
178   }
179   /**
180    * @param sortColumn The sortColumn to set.
181    */
182   public void setSortedColumn(int sortColumn){
183     this.sortedColumn = sortColumn;
184   }
185   
186   /**
187    * Get the row in the table for a row in the model.
188    */
189   public int getTableRow(int modelRow){
190     return sortingModel.sourceToTarget(modelRow);
191   }
192 
193   /**
194    * Handles translations between an indexed data source and a permutation of 
195    * itself (like the translations between the rows in sorted table and the
196    * rows in the actual unsorted model).  
197    */
198   protected class SortingModel extends AbstractTableModel 
199       implements TableModelListener{
200     
201     public SortingModel(TableModel sourceModel){
202       init(sourceModel);
203     }
204     
205     protected void init(TableModel sourceModel){
206       if(this.sourceModel != null) 
207         this.sourceModel.removeTableModelListener(this);
208       this.sourceModel = sourceModel;
209       //start with the identity order
210       int size = sourceModel.getRowCount();
211       sourceToTarget = new int[size];
212       targetToSource = new int[size];
213       for(int i = 0; i < size; i++) {
214         sourceToTarget[i] = i;
215         targetToSource[i] = i;
216       }
217       sourceModel.addTableModelListener(this);
218       if(isSortable() && sortedColumn == -1) setSortedColumn(0);
219     }
220     
221     /**
222      * This gets events from the source model and forwards them to the UI
223      */
224     public void tableChanged(TableModelEvent e){
225       int type = e.getType();
226       int firstRow = e.getFirstRow();
227       int lastRow = e.getLastRow();
228       int column = e.getColumn();
229       
230       //now deal with the changes in the data
231       //we have no way to "repair" the sorting on data updates so we will need
232       //to rebuild the order every time
233       
234       switch(type){
235         case TableModelEvent.UPDATE:
236           if(firstRow == TableModelEvent.HEADER_ROW){
237             //complete structure change -> reallocate the data
238             init(sourceModel);
239             fireTableStructureChanged();
240             if(isSortable()) sort();
241             newColumns();
242             adjustSizes();
243           }else if(lastRow == Integer.MAX_VALUE){
244             //all data changed (including the number of rows)
245             init(sourceModel);
246             fireTableDataChanged();
247             if(isSortable()) sort();
248             adjustSizes();
249           }else{
250             //the rows should have normal values
251             //if the sortedColumn is not affected we don't care
252             if(isSortable() &&
253                (column == sortedColumn || 
254                 column == TableModelEvent.ALL_COLUMNS)){
255                 //re-sorting will also fire the event upwards
256                 sort();
257             }else{
258               fireTableChanged(new TableModelEvent(this,  
259                       sourceToTarget(firstRow), 
260                       sourceToTarget(lastRow), column, type));
261               
262             }
263             //resize the updated column(s)
264             if(column == TableModelEvent.ALL_COLUMNS){
265               adjustSizes();
266             }else{
267               ((ColumnData)columnData.get(column)).adjustColumnWidth();
268             }
269           }
270           break;
271         case TableModelEvent.INSERT:
272           //rows were inserted -> we need to rebuild
273           init(sourceModel);
274           if(firstRow == lastRow){  
275             fireTableChanged(new TableModelEvent(this,  
276                     sourceToTarget(firstRow), 
277                     sourceToTarget(lastRow), column, type));
278           }else{
279             //the real rows are not in sequence
280             fireTableDataChanged();
281           }
282           if(isSortable()) sort();
283           //resize the updated column(s)
284           if(column == TableModelEvent.ALL_COLUMNS) adjustSizes();
285           else ((ColumnData)columnData.get(column)).adjustColumnWidth();
286           break;
287         case TableModelEvent.DELETE:
288           //rows were deleted -> we need to rebuild
289           init(sourceModel);
290           fireTableDataChanged();
291           if(isSortable()) sort();
292       }
293     }
294     
295     public int getRowCount(){
296       return sourceModel.getRowCount();
297     }
298     
299     public int getColumnCount(){
300       return sourceModel.getColumnCount();
301     }
302     
303     public String getColumnName(int columnIndex){
304       return sourceModel.getColumnName(columnIndex);
305     }
306     public Class getColumnClass(int columnIndex){
307       return sourceModel.getColumnClass(columnIndex);
308     }
309     
310     public boolean isCellEditable(int rowIndex, int columnIndex){
311       return sourceModel.isCellEditable(targetToSource(rowIndex),
312               columnIndex);
313     }
314     public void setValueAt(Object aValue, int rowIndex, int columnIndex){
315       sourceModel.setValueAt(aValue, targetToSource(rowIndex), 
316               columnIndex);
317     }
318     public Object getValueAt(int row, int column){
319       return sourceModel.getValueAt(targetToSource(row), column);
320     }
321     
322     /**
323      * Sorts the table using the values in the specified column and sorting order.
324      * @param sortedColumn the column used for sorting the data.
325      * @param ascending the sorting order.
326      */
327     public void sort(){
328       //save the selection
329       int[] rows = getSelectedRows();
330       //convert to model co-ordinates
331       for(int i = 0; i < rows.length; i++) rows[i] = rowViewToModel(rows[i]);
332       clearSelection();
333       
334       List sourceData = new ArrayList(sourceModel.getRowCount());
335       //get the data in the source order
336       for(int i = 0; i < sourceModel.getRowCount(); i++){
337         sourceData.add(sourceModel.getValueAt(i, sortedColumn));
338       }
339       //get an appropriate comparator
340       Comparator comparator = ((ColumnData)columnData.
341               get(sortedColumn)).comparator;
342       if(comparator == null){
343         //use the default comparator
344         if(defaultComparator == null) 
345           defaultComparator = new ObjectComparator();
346         comparator = defaultComparator;
347       }
348       for(int i = 0; i < sourceData.size() - 1; i++){
349         for(int j = i + 1; j < sourceData.size(); j++){
350           Object o1 = sourceData.get(targetToSource(i));
351           Object o2 = sourceData.get(targetToSource(j));
352           boolean swap = ascending ?
353                   (comparator.compare(o1, o2) > 0) :
354                   (comparator.compare(o1, o2) < 0);
355           if(swap){
356             int aux = targetToSource[i];
357             targetToSource[i] = targetToSource[j];
358             targetToSource[j] = aux;
359             
360             sourceToTarget[targetToSource[i]] = i;
361             sourceToTarget[targetToSource[j]] = j;
362           }
363         }
364       }
365       fireTableRowsUpdated(0, sourceData.size() -1);
366       //restore selection
367       //convert to model co-ordinates
368       for(int i = 0; i < rows.length; i++){
369         rows[i] = rowModelToView(rows[i]);
370         getSelectionModel().addSelectionInterval(rows[i], rows[i]);
371       }
372     }
373 
374     
375     /**
376      * Converts an index from the source coordinates to the target ones.
377      * Used to propagate events from the data source (table model) to the view. 
378      * @param index the index in the source coordinates.
379      * @return the corresponding index in the target coordinates.
380      */
381     public int sourceToTarget(int index){
382       return sourceToTarget[index];
383     }
384 
385     /**
386      * Converts an index from the target coordinates to the source ones. 
387      * @param index the index in the target coordinates.
388      * Used to propagate events from the view (e.g. editing) to the source
389      * data source (table model).
390      * @return the corresponding index in the source coordinates.
391      */
392     public int targetToSource(int index){
393       return targetToSource[index];
394     }
395     
396     /**
397      * Builds the reverse index based on the new sorting order.
398      */
399     protected void buildTargetToSourceIndex(){
400       targetToSource = new int[sourceToTarget.length];
401       for(int i = 0; i < sourceToTarget.length; i++)
402         targetToSource[sourceToTarget[i]] = i;
403     }
404     
405     /**
406      * The direct index
407      */
408     protected int[] sourceToTarget;
409     
410     /**
411      * The reverse index.
412      */
413     protected int[] targetToSource;
414     
415     protected TableModel sourceModel;
416   }
417   
418   protected class HeaderMouseListener extends MouseAdapter{
419     public HeaderMouseListener(){
420     }
421     
422     public void mouseClicked(MouseEvent e){
423       process(e);
424     }
425     
426     public void mousePressed(MouseEvent e){
427       process(e);
428     }
429     
430     public void mouseReleased(MouseEvent e){
431       process(e);
432     }
433     
434     protected void process(MouseEvent e){
435       int viewColumn = columnModel.getColumnIndexAtX(e.getX());
436       final int column = convertColumnIndexToModel(viewColumn);
437       ColumnData cData = (ColumnData)columnData.get(column);
438       if((e.getID() == MouseEvent.MOUSE_PRESSED || 
439           e.getID() == MouseEvent.MOUSE_RELEASED) && 
440          e.isPopupTrigger()){
441         //show pop-up
442         cData.popup.show(e.getComponent(), e.getX(), e.getY());
443       }else if(e.getID() == MouseEvent.MOUSE_CLICKED &&
444                e.getButton() == MouseEvent.BUTTON1){
445         //normal click 
446         if(e.getClickCount() >= 2){
447           //double click -> resize
448           if(singleClickTimer != null){
449             singleClickTimer.stop();
450             singleClickTimer = null;
451           }
452           cData.adjustColumnWidth();
453         }else {
454           //possible single click -> resort
455           singleClickTimer = new Timer(CLICK_DELAY, new ActionListener(){
456             public void actionPerformed(ActionEvent evt){
457               //this is the action to be done for single click.
458               if(sortable && column != -1) {
459                 ascending = (column == sortedColumn) ? !ascending : true;
460                 sortedColumn = column;
461                 sortingModel.sort();
462               }
463             }
464           });
465           singleClickTimer.setRepeats(false);
466           singleClickTimer.start();
467         }
468       }
469     }
470     /**
471      * How long should we wait for a second click until deciding the it's 
472      * actually a single click.
473      */
474     private static final int CLICK_DELAY = 300;
475     protected Timer singleClickTimer;
476   }
477   
478   protected class ColumnData{
479     public ColumnData(int column){
480       this.column = column;
481       popup = new JPopupMenu();
482       hideMenuItem = new JCheckBoxMenuItem("Hide", false);
483       popup.add(hideMenuItem);
484       autoSizeMenuItem = new JCheckBoxMenuItem("Autosize", true);
485 //      popup.add(autoSizeMenuItem);
486       hidden = false;
487       initListeners();
488     }
489     
490     protected void initListeners(){
491       hideMenuItem.addActionListener(new ActionListener(){
492         public void actionPerformed(ActionEvent evt){
493           TableColumn tCol = getColumnModel().getColumn(column);
494           if(hideMenuItem.isSelected()){
495             //hide column
496             colWidth = tCol.getWidth();
497             colPreferredWidth = tCol.getPreferredWidth();
498             colMaxWidth = tCol.getMaxWidth();
499             tCol.setPreferredWidth(HIDDEN_WIDTH);
500             tCol.setMaxWidth(HIDDEN_WIDTH);
501             tCol.setWidth(HIDDEN_WIDTH);
502           }else{
503             //show column
504             tCol.setMaxWidth(colMaxWidth);
505             tCol.setPreferredWidth(colPreferredWidth);
506             tCol.setWidth(colWidth);
507           }
508         }
509       });
510       
511       autoSizeMenuItem.addActionListener(new ActionListener(){
512         public void actionPerformed(ActionEvent evt){
513           if(autoSizeMenuItem.isSelected()){
514             //adjust the size for this column
515             adjustColumnWidth();
516           }
517         }
518       });
519       
520     }
521     
522     public boolean isHidden(){
523       return hideMenuItem.isSelected();
524     }
525     
526     public void adjustColumnWidth(){
527       int viewColumn = convertColumnIndexToView(column);
528       TableColumn tCol = getColumnModel().getColumn(column);
529       Dimension dim;
530       int width, height;
531       TableCellRenderer renderer;
532       //compute the sizes
533       if(getTableHeader() != null){
534         renderer = tCol.getHeaderRenderer();
535         if(renderer == null) renderer = getTableHeader().getDefaultRenderer();
536         dim = renderer.getTableCellRendererComponent(XJTable.this, 
537                 tCol.getHeaderValue(), true, true ,0 , viewColumn).
538                 getPreferredSize(); 
539         width = dim.width;
540         //make sure the table header gets sized correctly
541         height = dim.height;
542         if(height + getRowMargin() > getTableHeader().getPreferredSize().height){
543           getTableHeader().setPreferredSize(
544                   new Dimension(getTableHeader().getPreferredSize().width, 
545                   height));
546         }
547         int marginWidth = getColumnModel().getColumnMargin(); 
548         if(marginWidth > 0) width += marginWidth;         
549       }else{
550         width = 0;
551       }
552       renderer = tCol.getCellRenderer();
553       if(renderer == null) renderer = getDefaultRenderer(getColumnClass(column));
554       for(int row = 0; row < getRowCount(); row ++){
555         if(renderer != null){
556           dim = renderer. getTableCellRendererComponent(XJTable.this, 
557                   getValueAt(row, column), false, false, row, viewColumn).
558                   getPreferredSize();
559           width = Math.max(width, dim.width);
560           height = dim.height;
561           if((height + getRowMargin()) > getRowHeight(row)){
562             setRowHeight(row, height + getRowMargin());
563            }          
564         }
565       }
566 
567       int marginWidth = getColumnModel().getColumnMargin(); 
568       if(marginWidth > 0) width += marginWidth; 
569       tCol.setPreferredWidth(width);
570     }
571     
572     JCheckBoxMenuItem autoSizeMenuItem;
573     JCheckBoxMenuItem hideMenuItem;
574     JPopupMenu popup;
575     int column;
576     boolean hidden;
577     int colPreferredWidth;
578     int colMaxWidth;
579     int colWidth;
580     Comparator comparator;
581     private static final int HIDDEN_WIDTH = 5;
582   }
583   
584   protected SortingModel sortingModel;
585   protected ObjectComparator defaultComparator;
586   
587   /**
588    * The column currently being sorted.
589    */
590   protected int sortedColumn = -1;
591   
592   /**
593    * is the current sort order ascending (or descending)?
594    */
595   protected boolean ascending = true;
596   /**
597    * Should this table be sortable.
598    */
599   protected boolean sortable = true;
600   
601   /**
602    * A list of {@link ColumnData} objects.
603    */
604   protected List columnData;
605 }
606