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