1   /*
2    *  Copyright (c) 1998-2001, 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    *  Valentin Tablan 23/01/2001
10   *
11   *  $Id: XJTable.java,v 1.11 2001/11/16 17:32:22 valyt Exp $
12   *
13   */
14  
15  package gate.swing;
16  
17  import java.awt.*;
18  import java.awt.event.*;
19  import java.util.*;
20  import javax.swing.*;
21  import javax.swing.table.*;
22  import javax.swing.event.*;
23  
24  import gate.util.*;
25  
26  /**
27   * A "smarter" JTable. Feaures include:
28   * <ul>
29   * <li>sorting the table using the values from a column as keys</li>
30   * <li>updating the widths of the columns so they accommodate the contents to
31   * their preferred sizes.
32   * </ul>
33   * It uses a custom made model that stands between the table model set by the
34   * user and the gui component. This middle model is responsible for sorting the
35   * rows.
36   */
37  public class XJTable extends JTable {
38  
39    /**Default constructor*/
40    public XJTable() {
41      init();
42    }
43  
44    /**Constructor from model*/
45    public XJTable(TableModel model) {
46      init();
47      setModel(model);
48    }
49  
50    public void setModel(TableModel model){
51      if(sorter != null) sorter.setModel(model);
52      else{
53        sorter = new TableSorter(model);
54        super.setModel(sorter);
55      }
56    }// void setModel(TableModel model)
57  
58    /**
59     * Returns the actual table model. Note that gateModel() will return the
60     * middle model used for sorting. This cannot be avoided because JTable
61     * expects to find the model used for the component when calling getModel().
62     */
63    public TableModel getActualModel(){
64      if(sorter != null)return sorter.getModel();
65      else return super.getModel();
66    }// public TableModel getActualModel()
67  
68    /**
69     * Get the row in the table for a row in the model.
70     */
71    public int getTableRow(int modelRow){
72      for(int i = 0; i < sorter.indexes.length; i ++){
73        if(sorter.indexes[i] == modelRow) return i;
74      }
75      return -1;
76    }
77  
78    public void tableChanged(TableModelEvent e){
79      super.tableChanged(e);
80      adjustSizes();
81    }
82  
83    /**Should the soring facility be enabled*/
84    public void setSortable(boolean isSortable){
85      this.sortable = isSortable;
86    }
87  
88    protected void init(){
89      //make sure we have a model
90      if(sorter == null){
91        sorter = new TableSorter(super.getModel());
92        super.setModel(sorter);
93      }
94      //read the arrows icons
95      upIcon = new ImageIcon(getClass().getResource(Files.getResourcePath() +
96                                                    "/img/up.gif"));
97      downIcon = new ImageIcon(getClass().getResource(Files.getResourcePath() +
98                                                      "/img/down.gif"));
99  
100     setColumnSelectionAllowed(false);
101     headerMouseListener = new MouseAdapter() {
102       public void mouseClicked(MouseEvent e) {
103         if(!sortable) return;
104         TableColumnModel columnModel = getColumnModel();
105         int viewColumn = columnModel.getColumnIndexAtX(e.getX());
106         int column = convertColumnIndexToModel(viewColumn);
107         if (column != -1) {
108           if(column != sortedColumn) ascending = true;
109           else ascending = !ascending;
110           sorter.sortByColumn(column);
111           sortedColumn = column;
112         }
113         adjustSizes();
114       }
115     };
116     if(sortable) getTableHeader().addMouseListener(headerMouseListener);
117     setAutoResizeMode(AUTO_RESIZE_OFF);
118     headerRenderer =
119       new CustomHeaderRenderer(getTableHeader().getDefaultRenderer());
120 
121     getTableHeader().setDefaultRenderer(headerRenderer);
122   }//init()
123 
124 
125   protected void configureEnclosingScrollPane(){
126     super.configureEnclosingScrollPane();
127     //if we're into a scroll pane resize with it
128     Container p = getParent();
129     if (p instanceof JViewport) {
130       Container gp = p.getParent();
131       if (gp instanceof JScrollPane) {
132         JScrollPane scrollPane = (JScrollPane)gp;
133         // Make certain we are the viewPort's view and not, for
134         // example, the rowHeaderView of the scrollPane -
135         // an implementor of fixed columns might do this.
136         JViewport viewport = scrollPane.getViewport();
137         if (viewport != null && viewport.getView() == this) {
138           scrollPane.addComponentListener(new ComponentAdapter() {
139             public void componentResized(ComponentEvent e) {
140               adjustSizes();
141             }
142 
143             public void componentShown(ComponentEvent e) {
144               adjustSizes();
145             }
146           });
147         }//if
148       }//if
149     }//if
150   }// void configureEnclosingScrollPane()
151 
152 
153   /**Resizes all the cells so they accommodate the components at their
154    * preferred sizes.
155    */
156   protected void adjustSizes(){
157     int totalWidth = 0;
158     TableColumn tCol = null;
159     Dimension dim;
160     int cellWidth;
161     int cellHeight;
162     int rowMargin = getRowMargin();
163 
164     //delete the current rowModel in order to get a new updated one
165     //this way we fix a bug in JTable
166     setRowHeight(Math.max(getRowHeight(0), 1));
167     for(int column = 0; column < getColumnCount(); column ++){
168       int width;
169       tCol = getColumnModel().getColumn(column);
170       //compute the sizes
171       width = headerRenderer.getTableCellRendererComponent(
172                   this, tCol.getHeaderValue(), false, false,0,column
173               ).getPreferredSize().width;
174       for(int row = 0; row < getRowCount(); row ++){
175         TableCellRenderer renderer = getCellRenderer(row,column);
176         if(renderer == null){
177           renderer = getDefaultRenderer(getModel().getColumnClass(column));
178         }
179         if(renderer != null){
180           dim = renderer.
181                       getTableCellRendererComponent(
182                         this, getValueAt(row, column), false, false, row, column
183                       ).getPreferredSize();
184           cellWidth = dim.width;
185           cellHeight = dim.height;
186           width = Math.max(width, cellWidth);
187           //width = Math.max(width, tCol.getPreferredWidth());
188           if((cellHeight + rowMargin) > getRowHeight(row)){
189            setRowHeight(row, cellHeight + rowMargin);
190           }
191         }//if(renderer != null)
192       }//for
193 
194       width += getColumnModel().getColumnMargin();
195       tCol.setPreferredWidth(width);
196       tCol.setWidth(width);
197       totalWidth += width;
198     }
199     int totalHeight = 0;
200     for (int row = 0; row < getRowCount(); row++)
201       totalHeight += getRowHeight(row);
202     dim = new Dimension(totalWidth, totalHeight);
203     setPreferredScrollableViewportSize(dim);
204 
205     //extend the last column
206     Container p = getParent();
207     if (p instanceof JViewport) {
208       Container gp = p.getParent();
209       if (gp instanceof JScrollPane) {
210         JScrollPane scrollPane = (JScrollPane)gp;
211         // Make certain we are the viewPort's view and not, for
212         // example, the rowHeaderView of the scrollPane -
213         // an implementor of fixed columns might do this.
214         JViewport viewport = scrollPane.getViewport();
215         if (viewport == null || viewport.getView() != this) {
216             return;
217         }
218         int portWidth = scrollPane.getSize().width -
219                         scrollPane.getInsets().left -
220                         scrollPane.getInsets().right;
221         if(scrollPane.getVerticalScrollBar().isVisible())
222           portWidth -= scrollPane.getVerticalScrollBar().getWidth();
223         if(totalWidth < portWidth){
224           int width = tCol.getWidth() + portWidth - totalWidth - 2;
225           tCol.setPreferredWidth(width);
226           tCol.setWidth(width);
227         }//if(totalWidth < portWidth)
228       }//if (gp instanceof JScrollPane)
229     }//if (p instanceof JViewport)
230   }//protected void adjustSizes()
231 
232   /**
233    * Sets the column to be used as key for sorting. This column changes
234    * automatically when the user click a column header.
235    */
236   public void setSortedColumn(int column){
237     sortedColumn = column;
238     sorter.sortByColumn(sortedColumn);
239   }
240 
241   /**Should the sorting be ascending or descending*/
242   public void setAscending(boolean ascending){
243     this.ascending = ascending;
244   }
245 
246   public void setAutoResizeMode(int resizeMode){
247     /*
248     throw new UnsupportedOperationException(
249         "Auto resize mode not supported for " + getClass().getName() + ".\n"
250         "The default mode is AUTO_RESIZE_LAST_COLUMN");
251     */
252   }
253 
254   protected TableSorter sorter;
255 
256   protected Icon upIcon;
257   protected Icon downIcon;
258   int sortedColumn = -1;
259 //  int oldSortedColumn = -1;
260   boolean ascending = true;
261   protected TableCellRenderer headerRenderer;
262   protected boolean sortable = true;
263   MouseListener headerMouseListener;
264 //  protected TableCellRenderer savedHeaderRenderer;
265 
266 //classes
267 
268   /**
269    * A sorter for TableModels. The sorter has a model (conforming to TableModel)
270    * and itself implements TableModel. TableSorter does not store or copy
271    * the data in the TableModel, instead it maintains an array of
272    * integers which it keeps the same size as the number of rows in its
273    * model. When the model changes it notifies the sorter that something
274    * has changed eg. "rowsAdded" so that its internal array of integers
275    * can be reallocated. As requests are made of the sorter (like
276    * getValueAt(row, col) it redirects them to its model via the mapping
277    * array. That way the TableSorter appears to hold another copy of the table
278    * with the rows in a different order. The sorting algorthm used is stable
279    * which means that it does not move around rows when its comparison
280    * function returns 0 to denote that they are equivalent.
281    *
282    * @version 1.5 12/17/97
283    * @author Philip Milne
284    */
285 
286   class TableSorter extends TableMap {
287     int             indexes[];
288     Vector          sortingColumns = new Vector();
289 
290     public TableSorter() {
291       indexes = new int[0]; // for consistency
292     }
293 
294     public TableSorter(TableModel model) {
295       setModel(model);
296     }
297 
298     public void setModel(TableModel model) {
299       super.setModel(model);
300       reallocateIndexes();
301     }
302 
303     public int compareRowsByColumn(int row1, int row2, int column) {
304       Class type = model.getColumnClass(column);
305       TableModel data = model;
306 
307       // Check for nulls.
308 
309       Object o1 = data.getValueAt(row1, column);
310       Object o2 = data.getValueAt(row2, column);
311 
312       // If both values are null, return 0.
313       if (o1 == null && o2 == null) {
314         return 0;
315       } else if (o1 == null) { // Define null less than everything.
316         return -1;
317       } else if (o2 == null) {
318         return 1;
319       }
320 
321       /*
322        * We copy all returned values from the getValue call in case
323        * an optimised model is reusing one object to return many
324        * values.  The Number subclasses in the JDK are immutable and
325        * so will not be used in this way but other subclasses of
326        * Number might want to do this to save space and avoid
327        * unnecessary heap allocation.
328        */
329 
330       if (type.getSuperclass() == java.lang.Number.class) {
331         Number n1 = (Number)data.getValueAt(row1, column);
332         double d1 = n1.doubleValue();
333         Number n2 = (Number)data.getValueAt(row2, column);
334         double d2 = n2.doubleValue();
335 
336         if (d1 < d2) {
337           return -1;
338         } else if (d1 > d2) {
339           return 1;
340         } else {
341           return 0;
342         }
343       } else if (type == java.util.Date.class) {
344         Date d1 = (Date)data.getValueAt(row1, column);
345         long n1 = d1.getTime();
346         Date d2 = (Date)data.getValueAt(row2, column);
347         long n2 = d2.getTime();
348 
349         if (n1 < n2) {
350           return -1;
351         } else if (n1 > n2) {
352           return 1;
353         } else {
354           return 0;
355         }
356       } else if (type == String.class) {
357         String s1 = (String)data.getValueAt(row1, column);
358         String s2    = (String)data.getValueAt(row2, column);
359         int result = s1.compareTo(s2);
360 
361         if (result < 0) {
362           return -1;
363         } else if (result > 0) {
364           return 1;
365         } else {
366           return 0;
367         }
368       } else if (type == Boolean.class) {
369         Boolean bool1 = (Boolean)data.getValueAt(row1, column);
370         boolean b1 = bool1.booleanValue();
371         Boolean bool2 = (Boolean)data.getValueAt(row2, column);
372         boolean b2 = bool2.booleanValue();
373 
374         if (b1 == b2) {
375           return 0;
376         } else if (b1) { // Define false < true
377           return 1;
378         } else {
379           return -1;
380         }
381       } else {
382         Object v1 = data.getValueAt(row1, column);
383         Object v2 = data.getValueAt(row2, column);
384         int result;
385         if(v1 instanceof Comparable){
386           try {
387             result = ((Comparable)v1).compareTo(v2);
388           } catch(ClassCastException cce) {
389             String s1 = v1.toString();
390             String s2 = v2.toString();
391             result = s1.compareTo(s2);
392           }
393         } else {
394           String s1 = v1.toString();
395           String s2 = v2.toString();
396           result = s1.compareTo(s2);
397         }
398 
399         if (result < 0) {
400           return -1;
401         } else if (result > 0) {
402           return 1;
403         } else {
404           return 0;
405         }
406       }
407     }
408 
409     public int compare(int row1, int row2) {
410      // compares++;
411       for (int level = 0; level < sortingColumns.size(); level++) {
412         Integer column = (Integer)sortingColumns.elementAt(level);
413         int result = compareRowsByColumn(row1, row2, column.intValue());
414         if (result != 0) {
415           return ascending ? result : -result;
416         }
417       }
418       return 0;
419     }
420 
421     public void reallocateIndexes() {
422       int rowCount = model.getRowCount();
423 
424       // Set up a new array of indexes with the right number of elements
425       // for the new data model.
426       indexes = new int[rowCount];
427 
428       // Initialise with the identity mapping.
429       for (int row = 0; row < rowCount; row++) {
430         indexes[row] = row;
431       }
432     }
433 
434     public void tableChanged(TableModelEvent e) {
435       reallocateIndexes();
436       sort(sorter);
437       super.tableChanged(e);
438     }
439 
440     public void checkModel() {
441       if (indexes.length != model.getRowCount()) {
442         tableChanged(null);
443         //System.err.println("Sorter not informed of a change in model.");
444       }
445     }
446 
447     public void sort(Object sender) {
448       checkModel();
449       shuttlesort((int[])indexes.clone(), indexes, 0, indexes.length);
450     }
451 
452     // This is a home-grown implementation which we have not had time
453     // to research - it may perform poorly in some circumstances. It
454     // requires twice the space of an in-place algorithm and makes
455     // NlogN assigments shuttling the values between the two
456     // arrays. The number of compares appears to vary between N-1 and
457     // NlogN depending on the initial order but the main reason for
458     // using it here is that, unlike qsort, it is stable.
459     public void shuttlesort(int from[], int to[], int low, int high) {
460       if (high - low < 2) {
461           return;
462       }
463       int middle = (low + high)/2;
464       shuttlesort(to, from, low, middle);
465       shuttlesort(to, from, middle, high);
466 
467       int p = low;
468       int q = middle;
469 
470       /* This is an optional short-cut; at each recursive call,
471       check to see if the elements in this subset are already
472       ordered.  If so, no further comparisons are needed; the
473       sub-array can just be copied.  The array must be copied rather
474       than assigned otherwise sister calls in the recursion might
475       get out of sinc.  When the number of elements is three they
476       are partitioned so that the first set, [low, mid), has one
477       element and and the second, [mid, high), has two. We skip the
478       optimisation when the number of elements is three or less as
479       the first compare in the normal merge will produce the same
480       sequence of steps. This optimisation seems to be worthwhile
481       for partially ordered lists but some analysis is needed to
482       find out how the performance drops to Nlog(N) as the initial
483       order diminishes - it may drop very quickly.  */
484 
485       if (high - low >= 4 && compare(from[middle-1], from[middle]) <= 0) {
486         for (int i = low; i < high; i++) {
487           to[i] = from[i];
488         }
489         return;
490       }
491 
492       // A normal merge.
493 
494       for (int i = low; i < high; i++) {
495         if (q >= high || (p < middle && compare(from[p], from[q]) <= 0)) {
496           to[i] = from[p++];
497         }
498         else {
499           to[i] = from[q++];
500         }
501       }
502     }
503 
504     public void swap(int i, int j) {
505       int tmp = indexes[i];
506       indexes[i] = indexes[j];
507       indexes[j] = tmp;
508     }
509 
510     // The mapping only affects the contents of the data rows.
511     // Pass all requests to these rows through the mapping array: "indexes".
512 
513     public Object getValueAt(int aRow, int aColumn) {
514       checkModel();
515       return model.getValueAt(indexes[aRow], aColumn);
516     }
517 
518     public void setValueAt(Object aValue, int aRow, int aColumn) {
519       checkModel();
520       model.setValueAt(aValue, indexes[aRow], aColumn);
521     }
522 
523     public boolean isCellEditable(int aRow, int aColumn) {
524       checkModel();
525       return model.isCellEditable(indexes[aRow], aColumn);
526     }
527 
528     public void sortByColumn(int column) {
529       sortingColumns.removeAllElements();
530       sortingColumns.addElement(new Integer(column));
531       sort(this);
532       super.tableChanged(new TableModelEvent(this));
533       getTableHeader().repaint();
534     }
535   }//class TableSorter extends TableMap
536 
537   class CustomHeaderRenderer extends DefaultTableCellRenderer{
538     public CustomHeaderRenderer(TableCellRenderer oldRenderer){
539       this.oldRenderer = oldRenderer;
540     }
541 
542     public Component getTableCellRendererComponent(JTable table,
543                                              Object value,
544                                              boolean isSelected,
545                                              boolean hasFocus,
546                                              int row,
547                                              int column){
548 
549       Component res = oldRenderer.getTableCellRendererComponent(
550                             table, value, isSelected, hasFocus, row, column);
551       if(res instanceof JLabel){
552         if(convertColumnIndexToModel(column) == sortedColumn){
553           ((JLabel)res).setIcon(ascending?upIcon:downIcon);
554         } else {
555           ((JLabel)res).setIcon(null);
556         }
557         ((JLabel)res).setHorizontalTextPosition(JLabel.LEFT);
558       }
559       return res;
560     }// Component getTableCellRendererComponent
561     protected TableCellRenderer oldRenderer;
562 
563   }// class CustomHeaderRenderer extends DefaultTableCellRenderer
564 }