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