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