View Javadoc

1   package baseCode.gui.table;
2   
3   import java.awt.Color;
4   import java.awt.Component;
5   import java.awt.Graphics;
6   import java.awt.Point;
7   import java.awt.event.MouseAdapter;
8   import java.awt.event.MouseEvent;
9   import java.awt.event.MouseListener;
10  import java.util.ArrayList;
11  import java.util.Arrays;
12  import java.util.Comparator;
13  import java.util.HashMap;
14  import java.util.Iterator;
15  import java.util.List;
16  import java.util.Map;
17  import java.util.Vector;
18  
19  import javax.swing.Icon;
20  import javax.swing.JLabel;
21  import javax.swing.JTable;
22  import javax.swing.SwingConstants;
23  import javax.swing.event.TableModelEvent;
24  import javax.swing.event.TableModelListener;
25  import javax.swing.table.AbstractTableModel;
26  import javax.swing.table.JTableHeader;
27  import javax.swing.table.TableCellRenderer;
28  import javax.swing.table.TableColumnModel;
29  import javax.swing.table.TableModel;
30  
31  import baseCode.gui.JMatrixDisplay;
32  
33  /***
34   * TableSorter is a decorator for TableModels; adding sorting functionality to a supplied TableModel. TableSorter does
35   * not store or copy the data in its TableModel; instead it maintains a map from the row indexes of the view to the row
36   * indexes of the model. As requests are made of the sorter (like getValueAt(row, col)) they are passed to the
37   * underlying model after the row numbers have been translated via the internal mapping array. This way, the TableSorter
38   * appears to hold another copy of the table with the rows in a different order. <p/>TableSorter registers itself as a
39   * listener to the underlying model, just as the JTable itself would. Events recieved from the model are examined,
40   * sometimes manipulated (typically widened), and then passed on to the TableSorter's listeners (typically the JTable).
41   * If a change to the model has invalidated the order of TableSorter's rows, a note of this is made and the sorter will
42   * resort the rows the next time a value is requested. <p/>When the tableHeader property is set, either by using the
43   * setTableHeader() method or the two argument constructor, the table header may be used as a complete UI for
44   * TableSorter. The default renderer of the tableHeader is decorated with a renderer that indicates the sorting status
45   * of each column. In addition, a mouse listener is installed with the following behavior:
46   * <ul>
47   * <li>Mouse-click: Clears the sorting status of all other columns and advances the sorting status of that column
48   * through three values: {NOT_SORTED, ASCENDING, DESCENDING} (then back to NOT_SORTED again).
49   * <li>SHIFT-mouse-click: Clears the sorting status of all other columns and cycles the sorting status of the column
50   * through the same three values, in the opposite order: {NOT_SORTED, DESCENDING, ASCENDING}.
51   * <li>CONTROL-mouse-click and CONTROL-SHIFT-mouse-click: as above except that the changes to the column do not cancel
52   * the statuses of columns that are already sorting - giving a way to initiate a compound sort.
53   * </ul>
54   * <p/>This is a long overdue rewrite of a class of the same name that first appeared in the swing table demos in 1997.
55   * 
56   * @author Philip Milne
57   * @author Brendon McLean
58   * @author Dan van Enckevort
59   * @author Parwinder Sekhon
60   * @author Paul Pavlidis (minor)
61   * @version 2.0 02/27/04
62   * @version $Id: TableSorter.java,v 1.10 2004/09/20 22:19:03 pavlidis Exp $
63   */
64  
65  public class TableSorter extends AbstractTableModel {
66     protected TableModel tableModel;
67     JMatrixDisplay m_matrixDisplay; // get rid of this!
68  
69     public static final int DESCENDING = -1;
70     public static final int NOT_SORTED = 0;
71     public static final int ASCENDING = 1;
72  
73     private static Directive EMPTY_DIRECTIVE = new Directive( -1, NOT_SORTED );
74  
75     public static final Comparator COMPARABLE_COMAPRATOR = new Comparator() {
76        public int compare( Object o1, Object o2 ) {
77           return ( ( Comparable ) o1 ).compareTo( o2 );
78        }
79     };
80     public static final Comparator LEXICAL_COMPARATOR = new Comparator() {
81        public int compare( Object o1, Object o2 ) {
82           return o1.toString().compareTo( o2.toString() );
83        }
84     };
85  
86     private Row[] viewToModel;
87     int[] modelToView;
88  
89     private JTableHeader tableHeader;
90     private MouseListener mouseListener;
91     private TableModelListener tableModelListener;
92     private Map columnComparators = new HashMap();
93     List sortingColumns = new ArrayList();
94  
95     public TableSorter() {
96        this.mouseListener = new MouseHandler();
97        this.tableModelListener = new TableModelHandler();
98     }
99  
100    public TableSorter( TableModel tableModel ) {
101       this();
102       setTableModel( tableModel );
103    }
104 
105    public TableSorter( TableModel tableModel, JTableHeader tableHeader ) {
106       this();
107       setTableHeader( tableHeader );
108       setTableModel( tableModel );
109    }
110 
111    public TableSorter( TableModel tableModel, JMatrixDisplay matrixDisplay ) {
112       this( tableModel );
113       m_matrixDisplay = matrixDisplay;
114    }
115 
116    void clearSortingState() {
117       viewToModel = null;
118       modelToView = null;
119    }
120 
121    public TableModel getTableModel() {
122       return tableModel;
123    }
124 
125    public void setTableModel( TableModel tableModel ) {
126       if ( this.tableModel != null ) {
127          this.tableModel.removeTableModelListener( tableModelListener );
128       }
129 
130       this.tableModel = tableModel;
131       if ( this.tableModel != null ) {
132          this.tableModel.addTableModelListener( tableModelListener );
133       }
134 
135       clearSortingState();
136       fireTableStructureChanged();
137    }
138 
139    public JTableHeader getTableHeader() {
140       return tableHeader;
141    }
142 
143    public void setTableHeader( JTableHeader tableHeader ) {
144       if ( this.tableHeader != null ) {
145          this.tableHeader.removeMouseListener( mouseListener );
146          TableCellRenderer defaultRenderer = this.tableHeader
147                .getDefaultRenderer();
148          if ( defaultRenderer instanceof SortableHeaderRenderer ) {
149             this.tableHeader
150                   .setDefaultRenderer( ( ( SortableHeaderRenderer ) defaultRenderer ).tableCellRenderer );
151          }
152       }
153       this.tableHeader = tableHeader;
154       if ( this.tableHeader != null ) {
155          this.tableHeader.addMouseListener( mouseListener );
156          this.tableHeader.setDefaultRenderer( new SortableHeaderRenderer(
157                this.tableHeader.getDefaultRenderer() ) );
158       }
159    }
160 
161    public boolean isSorting() {
162       return sortingColumns.size() != 0;
163    }
164 
165    private Directive getDirective( int column ) {
166       for ( int i = 0; i < sortingColumns.size(); i++ ) {
167          Directive directive = ( Directive ) sortingColumns.get( i );
168          if ( directive.column == column ) {
169             return directive;
170          }
171       }
172       return EMPTY_DIRECTIVE;
173    }
174 
175    public int getSortingStatus( int column ) {
176       return getDirective( column ).direction;
177    }
178 
179    private void sortingStatusChanged() {
180       clearSortingState();
181       fireTableDataChanged();
182       if ( tableHeader != null ) {
183          tableHeader.repaint();
184       }
185    }
186 
187    public void setSortingStatus( int column, int status ) {
188       Directive directive = getDirective( column );
189       if ( directive != EMPTY_DIRECTIVE ) {
190          sortingColumns.remove( directive );
191       }
192       if ( status != NOT_SORTED ) {
193          sortingColumns.add( new Directive( column, status ) );
194       }
195       sortingStatusChanged();
196    }
197 
198    protected Icon getHeaderRendererIcon( int column, int size ) {
199       Directive directive = getDirective( column );
200       if ( directive == EMPTY_DIRECTIVE ) {
201          return null;
202       }
203       return new Arrow( directive.direction == DESCENDING, size, sortingColumns
204             .indexOf( directive ) );
205    }
206 
207    public void cancelSorting() {
208       sortingColumns.clear();
209       sortingStatusChanged();
210    }
211 
212    public void setColumnComparator( Class type, Comparator comparator ) {
213       if ( comparator == null ) {
214          columnComparators.remove( type );
215       } else {
216          columnComparators.put( type, comparator );
217       }
218    }
219 
220    protected Comparator getComparator( int column ) {
221       Class columnType = tableModel.getColumnClass( column );
222       Comparator comparator = ( Comparator ) columnComparators.get( columnType );
223       if ( comparator != null ) {
224          return comparator;
225       }
226       if ( Comparable.class.isAssignableFrom( columnType ) ) {
227          return COMPARABLE_COMAPRATOR;
228       }
229       return LEXICAL_COMPARATOR;
230    }
231 
232    protected Comparator getComparator( Class columnClass ) {
233       Class columnType = columnClass;
234       Comparator comparator = ( Comparator ) columnComparators.get( columnType );
235       if ( comparator != null ) {
236          return comparator;
237       }
238       if ( Comparable.class.isAssignableFrom( columnType ) ) {
239          return COMPARABLE_COMAPRATOR;
240       }
241       return LEXICAL_COMPARATOR;
242    }
243 
244    private Row[] getViewToModel() {
245       if ( viewToModel == null ) {
246          int tableModelRowCount = tableModel.getRowCount();
247          viewToModel = new Row[tableModelRowCount];
248          for ( int row = 0; row < tableModelRowCount; row++ ) {
249             viewToModel[row] = new Row( row );
250          }
251 
252          if ( isSorting() ) {
253             Arrays.sort( viewToModel );
254          }
255       }
256       return viewToModel;
257    }
258 
259    public int modelIndex( int viewIndex ) {
260       return getViewToModel()[viewIndex].modelIndex;
261    }
262 
263    int[] getModelToView() {
264       if ( modelToView == null ) {
265          int n = getViewToModel().length;
266          modelToView = new int[n];
267          for ( int i = 0; i < n; i++ ) {
268             modelToView[modelIndex( i )] = i;
269          }
270       }
271       return modelToView;
272    }
273 
274    // TableModel interface methods
275 
276    public int getRowCount() {
277       return ( tableModel == null ) ? 0 : tableModel.getRowCount();
278    }
279 
280    public int getColumnCount() {
281       return ( tableModel == null ) ? 0 : tableModel.getColumnCount();
282    }
283 
284    public String getColumnName( int column ) {
285       return tableModel.getColumnName( column );
286    }
287 
288    public Class getColumnClass( int column ) {
289       return tableModel.getColumnClass( column );
290    }
291 
292    public boolean isCellEditable( int row, int column ) {
293       return tableModel.isCellEditable( modelIndex( row ), column );
294    }
295 
296    public Object getValueAt( int row, int column ) {
297       return tableModel.getValueAt( modelIndex( row ), column );
298    }
299 
300    public void setValueAt( Object aValue, int row, int column ) {
301       tableModel.setValueAt( aValue, modelIndex( row ), column );
302    }
303 
304    // Helper classes
305 
306    private class Row implements Comparable {
307       int modelIndex;
308 
309       public Row( int index ) {
310          this.modelIndex = index;
311       }
312 
313       public int compareTo( Object o ) {
314          int row1 = modelIndex;
315          int row2 = ( ( Row ) o ).modelIndex;
316 
317          for ( Iterator it = sortingColumns.iterator(); it.hasNext(); ) {
318             Directive directive = ( Directive ) it.next();
319             int column = directive.column;
320             Object o1 = tableModel.getValueAt( row1, column );
321             Object o2 = tableModel.getValueAt( row2, column );
322             Comparator comparator = null;
323             int comparison = 0;
324             boolean favor = false;
325 
326             if ( o1 == null && o2 == null ) {
327                comparison = 0;
328             } else if ( o1 == null ) {
329                if ( directive.direction == DESCENDING ) // Define null less than everything, except null.
330                   comparison = -1;
331                else
332                   // Define null greater than everything, except null.
333                   comparison = 1;
334             } else if ( o2 == null ) {
335                if ( directive.direction == DESCENDING )
336                   comparison = 1;
337                else
338                   comparison = -1;
339             } else if ( o1 != null && o2 != null ) {
340                if ( o1.getClass().equals( Double.class ) ) {
341                   comparator = getComparator( Double.class );
342                } else if ( o1.getClass().equals( Integer.class ) ) {
343                   comparator = getComparator( Integer.class );
344                } else if ( o1.getClass().equals( Point.class ) ) {
345                   comparator = getComparator( Double.class );
346 
347                   // If sortColumn is in the matrix display, then model.getValueAt()
348                   // returns a Point object that represents a coordinate into the
349                   // display matrix. This is done so that the display matrix object
350                   // can be asked for both the color and the value. We are here only
351                   // interested in the value.
352 
353                   /***
354                    * @todo we shouldn't include a dependency on JMatrixDisplay here. Find a workaround.
355                    */
356                   if ( m_matrixDisplay != null ) {
357 
358                      Point p1 = ( Point ) o1;
359                      Point p2 = ( Point ) o2;
360 
361                      o1 = new Double( m_matrixDisplay.getValue( p1.x, p1.y ) );
362                      o2 = new Double( m_matrixDisplay.getValue( p2.x, p2.y ) );
363                   }
364                } else if ( o1.getClass().equals( ArrayList.class ) ) {
365                   if ( ( ( ArrayList ) o1 ).get( 0 ).getClass().equals(
366                         Double.class ) ) {
367                      comparator = getComparator( Double.class );
368                      //only comparing the first member of the list
369                      Double a = ( Double ) ( ( ArrayList ) o1 ).get( 0 );
370                      Double b = ( Double ) ( ( ArrayList ) o2 ).get( 0 );
371 
372                      // yes, we did get an array list, but we want to
373                      // compare the Double values the array lists contain,
374                      // not the array lists themselves.
375                      o1 = a;
376                      o2 = b;
377                   }
378                } else if ( o1.getClass().equals( Vector.class ) ) {
379                   if ( ( ( Vector ) o1 ).get( 0 ).getClass().equals(
380                         String.class ) ) {
381                      if ( ( ( Vector ) o1 ).size() == 2 ) {
382                         comparison = -1;
383                         favor = true;
384                      } else if ( ( ( Vector ) o2 ).size() == 2 ) {
385                         comparison = 1;
386                         favor = true;
387                      } else //compare normally
388                      {
389                         comparator = getComparator( String.class );
390                         String a = ( String ) ( ( Vector ) o1 ).get( 0 );
391                         String b = ( String ) ( ( Vector ) o2 ).get( 0 );
392                         o1 = a;
393                         o2 = b;
394                      }
395                   } else if ( ( ( Vector ) o1 ).get( 0 ).getClass().equals(
396                         Double.class ) ) {
397 
398                      //only comparing the first member of the list
399                      Double a = ( Double ) ( ( ArrayList ) o1 ).get( 0 );
400                      Double b = ( Double ) ( ( ArrayList ) o2 ).get( 0 );
401                      comparator = getComparator( Double.class );
402 
403                      // yes, we did get a Vector, but we want to
404                      // compare the Double values the Vectors contain,
405                      // not the Vectors themselves
406                      o1 = a;
407                      o2 = b;
408                   }
409                } else {
410                   comparator = getComparator( column );
411                }
412                if ( favor != true ) { //we're not favoring anyone
413                   comparison = comparator.compare( o1, o2 );
414                }
415             }
416 
417             if ( comparison != 0 ) {
418                return directive.direction == DESCENDING ? -comparison
419                      : comparison;
420             }
421          }
422          return 0;
423       }
424    }
425 
426    private class TableModelHandler implements TableModelListener {
427       public void tableChanged( TableModelEvent e ) {
428          // If we're not sorting by anything, just pass the event along.
429          if ( !isSorting() ) {
430             clearSortingState();
431             fireTableChanged( e );
432             return;
433          }
434 
435          // If the table structure has changed, cancel the sorting; the
436          // sorting columns may have been either moved or deleted from
437          // the model.
438          if ( e.getFirstRow() == TableModelEvent.HEADER_ROW ) {
439             cancelSorting();
440             fireTableChanged( e );
441             return;
442          }
443 
444          // We can map a cell event through to the view without widening
445          // when the following conditions apply:
446          //
447          // a) all the changes are on one row (e.getFirstRow() == e.getLastRow()) and,
448          // b) all the changes are in one column (column != TableModelEvent.ALL_COLUMNS) and,
449          // c) we are not sorting on that column (getSortingStatus(column) == NOT_SORTED) and,
450          // d) a reverse lookup will not trigger a sort (modelToView != null)
451          //
452          // Note: INSERT and DELETE events fail this test as they have column == ALL_COLUMNS.
453          //
454          // The last check, for (modelToView != null) is to see if modelToView
455          // is already allocated. If we don't do this check; sorting can become
456          // a performance bottleneck for applications where cells
457          // change rapidly in different parts of the table. If cells
458          // change alternately in the sorting column and then outside of
459          // it this class can end up re-sorting on alternate cell updates -
460          // which can be a performance problem for large tables. The last
461          // clause avoids this problem.
462          int column = e.getColumn();
463          if ( e.getFirstRow() == e.getLastRow()
464                && column != TableModelEvent.ALL_COLUMNS
465                && getSortingStatus( column ) == NOT_SORTED
466                && modelToView != null ) {
467             int viewIndex = getModelToView()[e.getFirstRow()];
468             fireTableChanged( new TableModelEvent( TableSorter.this, viewIndex,
469                   viewIndex, column, e.getType() ) );
470             return;
471          }
472 
473          // Something has happened to the data that may have invalidated the row order.
474          clearSortingState();
475          fireTableDataChanged();
476          return;
477       }
478    }
479 
480    private class MouseHandler extends MouseAdapter {
481       public void mouseClicked( MouseEvent e ) {
482          if ( e.getButton() == MouseEvent.BUTTON1 ) {
483             JTableHeader h = ( JTableHeader ) e.getSource();
484             TableColumnModel columnModel = h.getColumnModel();
485             int viewColumn = columnModel.getColumnIndexAtX( e.getX() );
486             int column = columnModel.getColumn( viewColumn ).getModelIndex();
487             if ( column != -1 ) {
488                int status = getSortingStatus( column );
489                if ( !e.isControlDown() ) {
490                   cancelSorting();
491                }
492                // Cycle the sorting states through {ASCENDING, DESCENDING} ignoring NOT_SORTED
493                status = ( status == ASCENDING ) ? DESCENDING : ASCENDING;
494                setSortingStatus( column, status );
495             }
496          }
497       }
498    }
499 
500    private static class Arrow implements Icon {
501       private boolean descending;
502       private int size;
503       private int priority;
504 
505       public Arrow( boolean descending, int size, int priority ) {
506          this.descending = descending;
507          this.size = size;
508          this.priority = priority;
509       }
510 
511       public void paintIcon( Component c, Graphics g, int x, int y ) {
512          Color color = c == null ? Color.GRAY : c.getBackground();
513          // In a compound sort, make each succesive triangle 20%
514          // smaller than the previous one.
515          int dx = ( int ) ( size / 2 * Math.pow( 0.8, priority ) );
516          int dy = descending ? dx : -dx;
517          // Align icon (roughly) with font baseline.
518          y = y + 5 * size / 6 + ( descending ? -dy : 0 );
519          int shift = descending ? 1 : -1;
520          g.translate( x, y );
521 
522          // Right diagonal.
523          g.setColor( color.darker() );
524          g.drawLine( dx / 2, dy, 0, 0 );
525          g.drawLine( dx / 2, dy + shift, 0, shift );
526 
527          // Left diagonal.
528          g.setColor( color.brighter() );
529          g.drawLine( dx / 2, dy, dx, 0 );
530          g.drawLine( dx / 2, dy + shift, dx, shift );
531 
532          // Horizontal line.
533          if ( descending ) {
534             g.setColor( color.darker().darker() );
535          } else {
536             g.setColor( color.brighter().brighter() );
537          }
538          g.drawLine( dx, 0, 0, 0 );
539 
540          g.setColor( color );
541          g.translate( -x, -y );
542       }
543 
544       public int getIconWidth() {
545          return size;
546       }
547 
548       public int getIconHeight() {
549          return size;
550       }
551    }
552 
553    private class SortableHeaderRenderer implements TableCellRenderer {
554       TableCellRenderer tableCellRenderer;
555 
556       public SortableHeaderRenderer( TableCellRenderer tableCellRenderer ) {
557          this.tableCellRenderer = tableCellRenderer;
558       }
559 
560       public Component getTableCellRendererComponent( JTable table,
561             Object value, boolean isSelected, boolean hasFocus, int row,
562             int column ) {
563          Component c = tableCellRenderer.getTableCellRendererComponent( table,
564                value, isSelected, hasFocus, row, column );
565          if ( c instanceof JLabel ) {
566             JLabel l = ( JLabel ) c;
567             l.setHorizontalTextPosition( SwingConstants.LEFT );
568             l.setVerticalAlignment( SwingConstants.BOTTOM );
569             int modelColumn = table.convertColumnIndexToModel( column );
570             l.setIcon( getHeaderRendererIcon( modelColumn, l.getFont()
571                   .getSize() ) );
572          }
573          return c;
574       }
575    }
576 
577    private static class Directive {
578       int column;
579       int direction;
580 
581       public Directive( int column, int direction ) {
582          this.column = column;
583          this.direction = direction;
584       }
585    }
586 }