package com.sun.java.forums; import java.awt.Color; import java.awt.Component; import java.awt.Graphics; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import javax.swing.Icon; import javax.swing.JLabel; import javax.swing.JTable; import javax.swing.SwingConstants; import javax.swing.event.TableModelEvent; import javax.swing.event.TableModelListener; import javax.swing.table.AbstractTableModel; import javax.swing.table.JTableHeader; import javax.swing.table.TableCellRenderer; import javax.swing.table.TableColumnModel; import javax.swing.table.TableModel; /** * TableSorter is a decorator for TableModels; adding sorting functionality to a supplied TableModel. TableSorter does * not store or copy the data in its TableModel; instead it maintains a map from the row indexes of the view to the row * indexes of the model. As requests are made of the sorter (like getValueAt(row, col)) they are passed to the * underlying model after the row numbers have been translated via the internal mapping array. This way, the TableSorter * appears to hold another copy of the table with the rows in a different order. <p/> TableSorter registers itself as a * listener to the underlying model, just as the JTable itself would. Events recieved from the model are examined, * sometimes manipulated (typically widened), and then passed on to the TableSorter's listeners (typically the JTable). * If a change to the model has invalidated the order of TableSorter's rows, a note of this is made and the sorter will * resort the rows the next time a value is requested. <p/> When the tableHeader property is set, either by using the * setTableHeader() method or the two argument constructor, the table header may be used as a complete UI for * TableSorter. The default renderer of the tableHeader is decorated with a renderer that indicates the sorting status * of each column. In addition, a mouse listener is installed with the following behavior: * <ul> * <li> Mouse-click: Clears the sorting status of all other columns and advances the sorting status of that column * through three values: {NOT_SORTED, ASCENDING, DESCENDING} (then back to NOT_SORTED again). * <li> SHIFT-mouse-click: Clears the sorting status of all other columns and cycles the sorting status of the column * through the same three values, in the opposite order: {NOT_SORTED, DESCENDING, ASCENDING}. * <li> CONTROL-mouse-click and CONTROL-SHIFT-mouse-click: as above except that the changes to the column do not cancel * the statuses of columns that are already sorting - giving a way to initiate a compound sort. * </ul> * <p/> This is a long overdue rewrite of a class of the same name that first appeared in the swing table demos in 1997. * * @author Philip Milne * @author Brendon McLean * @author Dan van Enckevort * @author Parwinder Sekhon * @version 2.0 02/27/04 */ public class TableSorter extends AbstractTableModel { protected TableModel tableModel; public static final int DESCENDING = -1; public static final int NOT_SORTED = 0; public static final int ASCENDING = 1; private static Directive EMPTY_DIRECTIVE = new Directive( -1, TableSorter.NOT_SORTED ); public static final Comparator COMPARABLE_COMAPRATOR = new Comparator() { public int compare( Object o1, Object o2 ) { return ( (Comparable) o1 ).compareTo( o2 ); } }; public static final Comparator LEXICAL_COMPARATOR = new Comparator() { public int compare( Object o1, Object o2 ) { return o1.toString().toLowerCase().compareTo( o2.toString().toLowerCase() ); } }; private Row[] viewToModel; private int[] modelToView; private JTableHeader tableHeader; private final MouseListener mouseListener; private final TableModelListener tableModelListener; private final Map columnComparators = new HashMap(); private final ArrayList sortingColumns = new ArrayList(); public TableSorter() { this.mouseListener = new MouseHandler(); this.tableModelListener = new TableModelHandler(); } public TableSorter( final TableModel tableModel ) { this(); this.setTableModel( tableModel ); } public TableSorter( final TableModel tableModel, final JTableHeader tableHeader ) { this(); this.setTableHeader( tableHeader ); this.setTableModel( tableModel ); } private void clearSortingState() { this.viewToModel = null; this.modelToView = null; } public TableModel getTableModel() { return this.tableModel; } public void setTableModel( final TableModel tableModel ) { if ( this.tableModel != null ) { this.tableModel.removeTableModelListener( this.tableModelListener ); } this.tableModel = tableModel; if ( this.tableModel != null ) { this.tableModel.addTableModelListener( this.tableModelListener ); } this.clearSortingState(); this.fireTableStructureChanged(); } public JTableHeader getTableHeader() { return this.tableHeader; } public void setTableHeader( final JTableHeader tableHeader ) { if ( this.tableHeader != null ) { this.tableHeader.removeMouseListener( this.mouseListener ); TableCellRenderer defaultRenderer = this.tableHeader.getDefaultRenderer(); if ( defaultRenderer instanceof SortableHeaderRenderer ) { this.tableHeader.setDefaultRenderer( ( (SortableHeaderRenderer) defaultRenderer ).tableCellRenderer ); } } this.tableHeader = tableHeader; if ( this.tableHeader != null ) { this.tableHeader.addMouseListener( this.mouseListener ); this.tableHeader.setDefaultRenderer( new SortableHeaderRenderer( this.tableHeader.getDefaultRenderer() ) ); } } public boolean isSorting() { return this.sortingColumns.size() != 0; } private Directive getDirective( final int column ) { for ( int i = 0; i < this.sortingColumns.size(); i++ ) { Directive directive = (Directive) this.sortingColumns.get( i ); if ( directive.column == column ) { return directive; } } return TableSorter.EMPTY_DIRECTIVE; } public int getSortingStatus( final int column ) { return this.getDirective( column ).direction; } private void sortingStatusChanged() { this.clearSortingState(); this.fireTableDataChanged(); if ( this.tableHeader != null ) { this.tableHeader.repaint(); } } public void setSortingStatus( final int column, final int status ) { Directive directive = this.getDirective( column ); if ( directive != TableSorter.EMPTY_DIRECTIVE ) { this.sortingColumns.remove( directive ); } if ( status != TableSorter.NOT_SORTED ) { this.sortingColumns.add( new Directive( column, status ) ); } this.sortingStatusChanged(); } protected Icon getHeaderRendererIcon( final int column, final int size ) { Directive directive = this.getDirective( column ); if ( directive == TableSorter.EMPTY_DIRECTIVE ) { return null; } return new Arrow( directive.direction == TableSorter.DESCENDING, size, this.sortingColumns.indexOf( directive ) ); } private void cancelSorting() { this.sortingColumns.clear(); this.sortingStatusChanged(); } public void setColumnComparator( final Class type, final Comparator comparator ) { if ( comparator == null ) { this.columnComparators.remove( type ); } else { this.columnComparators.put( type, comparator ); } } protected Comparator getComparator( final int column ) { Class columnType = this.tableModel.getColumnClass( column ); Comparator comparator = (Comparator) this.columnComparators.get( columnType ); if ( comparator != null ) { return comparator; } if ( Comparable.class.isAssignableFrom( columnType ) ) { return TableSorter.COMPARABLE_COMAPRATOR; } return TableSorter.LEXICAL_COMPARATOR; } private Row[] getViewToModel() { if ( this.viewToModel == null ) { int tableModelRowCount = this.tableModel.getRowCount(); this.viewToModel = new Row[ tableModelRowCount ]; for ( int row = 0; row < tableModelRowCount; row++ ) { this.viewToModel[ row ] = new Row( row ); } if ( this.isSorting() ) { Arrays.sort( this.viewToModel ); } } return this.viewToModel; } public int modelIndex( final int viewIndex ) { return this.getViewToModel()[ viewIndex ].modelIndex; } private int[] getModelToView() { if ( this.modelToView == null ) { int n = this.getViewToModel().length; this.modelToView = new int[ n ]; for ( int i = 0; i < n; i++ ) { this.modelToView[ this.modelIndex( i ) ] = i; } } return this.modelToView; } // TableModel interface methods public int getRowCount() { return this.tableModel == null ? 0 : this.tableModel.getRowCount(); } public int getColumnCount() { return this.tableModel == null ? 0 : this.tableModel.getColumnCount(); } @Override public String getColumnName( final int column ) { return this.tableModel.getColumnName( column ); } @Override public Class getColumnClass( final int column ) { return this.tableModel.getColumnClass( column ); } @Override public boolean isCellEditable( final int row, final int column ) { return this.tableModel.isCellEditable( this.modelIndex( row ), column ); } public Object getValueAt( final int row, final int column ) { return this.tableModel.getValueAt( this.modelIndex( row ), column ); } @Override public void setValueAt( final Object aValue, final int row, final int column ) { this.tableModel.setValueAt( aValue, this.modelIndex( row ), column ); } // Helper classes private class Row implements Comparable { private final int modelIndex; public Row( final int index ) { this.modelIndex = index; } public int compareTo( final Object o ) { int row1 = this.modelIndex; int row2 = ( (Row) o ).modelIndex; for ( Iterator it = TableSorter.this.sortingColumns.iterator(); it.hasNext(); ) { Directive directive = (Directive) it.next(); int column = directive.column; Object o1 = TableSorter.this.tableModel.getValueAt( row1, column ); Object o2 = TableSorter.this.tableModel.getValueAt( row2, column ); int comparison = 0; // Define null less than everything, except null. if ( o1 == null && o2 == null ) { comparison = 0; } else if ( o1 == null ) { comparison = -1; } else if ( o2 == null ) { comparison = 1; } else { comparison = TableSorter.this.getComparator( column ).compare( o1, o2 ); } if ( comparison != 0 ) { return directive.direction == TableSorter.DESCENDING ? -comparison : comparison; } } return 0; } } private class TableModelHandler implements TableModelListener { public void tableChanged( final TableModelEvent e ) { // If we're not sorting by anything, just pass the event along. if ( !TableSorter.this.isSorting() ) { TableSorter.this.clearSortingState(); TableSorter.this.fireTableChanged( e ); return; } // If the table structure has changed, cancel the sorting; the // sorting columns may have been either moved or deleted from // the model. if ( e.getFirstRow() == TableModelEvent.HEADER_ROW ) { TableSorter.this.cancelSorting(); TableSorter.this.fireTableChanged( e ); return; } // We can map a cell event through to the view without widening // when the following conditions apply: // // a) all the changes are on one row (e.getFirstRow() == e.getLastRow()) and, // b) all the changes are in one column (column != TableModelEvent.ALL_COLUMNS) and, // c) we are not sorting on that column (getSortingStatus(column) == NOT_SORTED) and, // d) a reverse lookup will not trigger a sort (modelToView != null) // // Note: INSERT and DELETE events fail this test as they have column == ALL_COLUMNS. // // The last check, for (modelToView != null) is to see if modelToView // is already allocated. If we don't do this check; sorting can become // a performance bottleneck for applications where cells // change rapidly in different parts of the table. If cells // change alternately in the sorting column and then outside of // it this class can end up re-sorting on alternate cell updates - // which can be a performance problem for large tables. The last // clause avoids this problem. int column = e.getColumn(); if ( e.getFirstRow() == e.getLastRow() && column != TableModelEvent.ALL_COLUMNS && TableSorter.this.getSortingStatus( column ) == TableSorter.NOT_SORTED && TableSorter.this.modelToView != null ) { int viewIndex = TableSorter.this.getModelToView()[ e.getFirstRow() ]; TableSorter.this.fireTableChanged( new TableModelEvent( TableSorter.this, viewIndex, viewIndex, column, e.getType() ) ); return; } // Something has happened to the data that may have invalidated the row order. TableSorter.this.clearSortingState(); TableSorter.this.fireTableDataChanged(); return; } } private class MouseHandler extends MouseAdapter { @Override public void mouseClicked( final MouseEvent e ) { JTableHeader h = (JTableHeader) e.getSource(); TableColumnModel columnModel = h.getColumnModel(); int viewColumn = columnModel.getColumnIndexAtX( e.getX() ); int column = columnModel.getColumn( viewColumn ).getModelIndex(); if ( column != -1 ) { int status = TableSorter.this.getSortingStatus( column ); if ( !e.isControlDown() ) { TableSorter.this.cancelSorting(); } // Cycle the sorting states through {NOT_SORTED, ASCENDING, DESCENDING} or // {NOT_SORTED, DESCENDING, ASCENDING} depending on whether shift is pressed. status = status + ( e.isShiftDown() ? -1 : 1 ); status = ( status + 4 ) % 3 - 1; // signed mod, returning {-1, 0, 1} TableSorter.this.setSortingStatus( column, status ); } } } private static class Arrow implements Icon { private final boolean descending; private final int size; private final int priority; public Arrow( final boolean descending, final int size, final int priority ) { this.descending = descending; this.size = size; this.priority = priority; } public void paintIcon( final Component c, final Graphics g, int x, int y ) { Color color = c == null ? Color.GRAY : c.getBackground(); // In a compound sort, make each succesive triangle 20% // smaller than the previous one. int dx = (int) ( this.size / 2 * Math.pow( 0.8, this.priority ) ); int dy = this.descending ? dx : -dx; // Align icon (roughly) with font baseline. y = y + 5 * this.size / 6 + ( this.descending ? -dy : 0 ); int shift = this.descending ? 1 : -1; g.translate( x, y ); // Right diagonal. g.setColor( color.darker() ); g.drawLine( dx / 2, dy, 0, 0 ); g.drawLine( dx / 2, dy + shift, 0, shift ); // Left diagonal. g.setColor( color.brighter() ); g.drawLine( dx / 2, dy, dx, 0 ); g.drawLine( dx / 2, dy + shift, dx, shift ); // Horizontal line. if ( this.descending ) { g.setColor( color.darker().darker() ); } else { g.setColor( color.brighter().brighter() ); } g.drawLine( dx, 0, 0, 0 ); g.setColor( color ); g.translate( -x, -y ); } public int getIconWidth() { return this.size; } public int getIconHeight() { return this.size; } } private class SortableHeaderRenderer implements TableCellRenderer { private final TableCellRenderer tableCellRenderer; public SortableHeaderRenderer( final TableCellRenderer tableCellRenderer ) { this.tableCellRenderer = tableCellRenderer; } public Component getTableCellRendererComponent( final JTable table, final Object value, final boolean isSelected, final boolean hasFocus, final int row, final int column ) { Component c = this.tableCellRenderer.getTableCellRendererComponent( table, value, isSelected, hasFocus, row, column ); if ( c instanceof JLabel ) { JLabel l = (JLabel) c; l.setHorizontalTextPosition( SwingConstants.LEFT ); int modelColumn = table.convertColumnIndexToModel( column ); l.setIcon( TableSorter.this.getHeaderRendererIcon( modelColumn, l.getFont().getSize() ) ); } return c; } } private static class Directive { private final int column; private final int direction; public Directive( final int column, final int direction ) { this.column = column; this.direction = direction; } } }