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