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 }