update author list in license for (JAL-826)
[jalview.git] / src / jalview / util / TableSorter.java
index c03e804..5e9556e 100755 (executable)
@@ -1,78 +1,68 @@
 /*\r
- * Jalview - A Sequence Alignment Editor and Viewer\r
- * Copyright (C) 2006 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
+ * Jalview - A Sequence Alignment Editor and Viewer (Version 2.7)\r
+ * Copyright (C) 2011 J Procter, AM Waterhouse, J Engelhardt, LM Lui, G Barton, M Clamp, S Searle\r
+ * \r
+ * This file is part of Jalview.\r
+ * \r
+ * Jalview 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 3 of the License, or (at your option) any later version.\r
+ * \r
+ * Jalview is distributed in the hope that it will be useful, but \r
+ * WITHOUT ANY WARRANTY; without even the implied warranty \r
+ * of MERCHANTABILITY or FITNESS FOR A PARTICULAR \r
+ * PURPOSE.  See the GNU General Public License for more details.\r
+ * \r
+ * You should have received a copy of the GNU General Public License along with Jalview.  If not, see <http://www.gnu.org/licenses/>.\r
  */\r
 package jalview.util;\r
 \r
-import java.awt.*;\r
-import java.awt.event.*;\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.TableModelEvent;\r
-import javax.swing.event.TableModelListener;\r
+import javax.swing.event.*;\r
 import javax.swing.table.*;\r
 \r
 /**\r
- * TableSorter is a decorator for TableModels; adding sorting\r
- * functionality to a supplied TableModel. TableSorter does\r
- * not store or copy the data in its TableModel; instead it maintains\r
- * a map from the row indexes of the view to the row indexes of the\r
- * model. As requests are made of the sorter (like getValueAt(row, col))\r
- * they are passed to the underlying model after the row numbers\r
- * have been translated via the internal mapping array. This way,\r
- * the TableSorter appears to hold another copy of the table\r
- * with the rows in a different order.\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.\r
  * <p/>\r
- * TableSorter registers itself as a listener to the underlying model,\r
- * just as the JTable itself would. Events recieved from the model\r
- * are examined, sometimes manipulated (typically widened), and then\r
- * passed on to the TableSorter's listeners (typically the JTable).\r
- * If a change to the model has invalidated the order of TableSorter's\r
- * rows, a note of this is made and the sorter will resort the\r
- * rows the next time a value is requested.\r
+ * TableSorter registers itself as a listener to the underlying model, just as\r
+ * the JTable itself would. Events recieved from the model are examined,\r
+ * sometimes manipulated (typically widened), and then passed on to the\r
+ * TableSorter's listeners (typically the JTable). If a change to the model has\r
+ * invalidated the order of TableSorter's rows, a note of this is made and the\r
+ * sorter will resort the rows the next time a value is requested.\r
  * <p/>\r
- * When the tableHeader property is set, either by using the\r
- * setTableHeader() method or the two argument constructor, the\r
- * table header may be used as a complete UI for TableSorter.\r
- * The default renderer of the tableHeader is decorated with a renderer\r
- * that indicates the sorting status of each column. In addition,\r
- * a mouse listener is installed with the following behavior:\r
+ * When the tableHeader property is set, either by using the setTableHeader()\r
+ * method or the two argument constructor, the table header may be used as a\r
+ * complete UI for TableSorter. The default renderer of the tableHeader is\r
+ * decorated with a renderer that indicates the sorting status of each column.\r
+ * In addition, a mouse listener is installed with the following behavior:\r
  * <ul>\r
- * <li>\r
- * Mouse-click: Clears the sorting status of all other columns\r
- * and advances the sorting status of that column through three\r
- * values: {NOT_SORTED, ASCENDING, DESCENDING} (then back to\r
- * NOT_SORTED again).\r
- * <li>\r
- * SHIFT-mouse-click: Clears the sorting status of all other columns\r
- * and cycles the sorting status of the column through the same\r
- * three values, in the opposite order: {NOT_SORTED, DESCENDING, ASCENDING}.\r
- * <li>\r
- * CONTROL-mouse-click and CONTROL-SHIFT-mouse-click: as above except\r
- * that the changes to the column do not cancel the statuses of columns\r
- * that are already sorting - giving a way to initiate a compound\r
- * sort.\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/>\r
- * This is a long overdue rewrite of a class of the same name that\r
- * first appeared in the swing table demos in 1997.\r
- *\r
+ * 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
@@ -80,422 +70,528 @@ import javax.swing.table.*;
  * @version 2.0 02/27/04\r
  */\r
 \r
-public class TableSorter extends AbstractTableModel {\r
-    protected TableModel tableModel;\r
-\r
-    public static final int DESCENDING = -1;\r
-    public static final int NOT_SORTED = 0;\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
-        public int compare(Object o1, Object o2) {\r
-            return ((Comparable) o1).compareTo(o2);\r
-        }\r
-    };\r
-    public static final Comparator LEXICAL_COMPARATOR = new Comparator() {\r
-        public int compare(Object o1, Object o2) {\r
-            return o1.toString().compareTo(o2.toString());\r
-        }\r
-    };\r
-\r
-    private Row[] viewToModel;\r
-    private int[] modelToView;\r
-\r
-    private JTableHeader tableHeader;\r
-    private MouseListener mouseListener;\r
-    private TableModelListener tableModelListener;\r
-    private Map columnComparators = new HashMap();\r
-    private List sortingColumns = new ArrayList();\r
-\r
-    public TableSorter() {\r
-        this.mouseListener = new MouseHandler();\r
-        this.tableModelListener = new TableModelHandler();\r
-    }\r
-\r
-    public TableSorter(TableModel tableModel) {\r
-        this();\r
-        setTableModel(tableModel);\r
-    }\r
-\r
-    public TableSorter(TableModel tableModel, JTableHeader tableHeader) {\r
-        this();\r
-        setTableHeader(tableHeader);\r
-        setTableModel(tableModel);\r
-    }\r
-\r
-    private void clearSortingState() {\r
-        viewToModel = null;\r
-        modelToView = null;\r
-    }\r
-\r
-    public TableModel getTableModel() {\r
-        return tableModel;\r
-    }\r
-\r
-    public void setTableModel(TableModel tableModel) {\r
-        if (this.tableModel != null) {\r
-            this.tableModel.removeTableModelListener(tableModelListener);\r
-        }\r
-\r
-        this.tableModel = tableModel;\r
-        if (this.tableModel != null) {\r
-            this.tableModel.addTableModelListener(tableModelListener);\r
-        }\r
-\r
-        clearSortingState();\r
-        fireTableStructureChanged();\r
-    }\r
-\r
-    public JTableHeader getTableHeader() {\r
-        return tableHeader;\r
-    }\r
-\r
-    public void setTableHeader(JTableHeader tableHeader) {\r
-        if (this.tableHeader != null) {\r
-            this.tableHeader.removeMouseListener(mouseListener);\r
-            TableCellRenderer defaultRenderer = this.tableHeader.getDefaultRenderer();\r
-            if (defaultRenderer instanceof SortableHeaderRenderer) {\r
-                this.tableHeader.setDefaultRenderer(((SortableHeaderRenderer) defaultRenderer).tableCellRenderer);\r
-            }\r
-        }\r
-        this.tableHeader = tableHeader;\r
-        if (this.tableHeader != null) {\r
-            this.tableHeader.addMouseListener(mouseListener);\r
-            this.tableHeader.setDefaultRenderer(\r
-                    new SortableHeaderRenderer(this.tableHeader.getDefaultRenderer()));\r
-        }\r
-    }\r
-\r
-    public boolean isSorting() {\r
-        return sortingColumns.size() != 0;\r
-    }\r
-\r
-    private Directive getDirective(int column) {\r
-        for (int i = 0; i < sortingColumns.size(); i++) {\r
-            Directive directive = (Directive)sortingColumns.get(i);\r
-            if (directive.column == column) {\r
-                return directive;\r
-            }\r
-        }\r
-        return EMPTY_DIRECTIVE;\r
-    }\r
-\r
-    public int getSortingStatus(int column) {\r
-        return getDirective(column).direction;\r
-    }\r
-\r
-    private void sortingStatusChanged() {\r
-        clearSortingState();\r
-        fireTableDataChanged();\r
-        if (tableHeader != null) {\r
-            tableHeader.repaint();\r
-        }\r
-    }\r
-\r
-    public void setSortingStatus(int column, int status) {\r
-        Directive directive = getDirective(column);\r
-        if (directive != EMPTY_DIRECTIVE) {\r
-            sortingColumns.remove(directive);\r
-        }\r
-        if (status != NOT_SORTED) {\r
-            sortingColumns.add(new Directive(column, status));\r
-        }\r
-        sortingStatusChanged();\r
-    }\r
-\r
-    protected Icon getHeaderRendererIcon(int column, int size) {\r
-        Directive directive = getDirective(column);\r
-        if (directive == EMPTY_DIRECTIVE) {\r
-            return null;\r
-        }\r
-        return new Arrow(directive.direction == DESCENDING, size, sortingColumns.indexOf(directive));\r
-    }\r
-\r
-    private void cancelSorting() {\r
-        sortingColumns.clear();\r
-        sortingStatusChanged();\r
-    }\r
-\r
-    public void setColumnComparator(Class type, Comparator comparator) {\r
-        if (comparator == null) {\r
-            columnComparators.remove(type);\r
-        } else {\r
-            columnComparators.put(type, comparator);\r
-        }\r
-    }\r
-\r
-    protected Comparator getComparator(int column) {\r
-        Class columnType = tableModel.getColumnClass(column);\r
-        Comparator comparator = (Comparator) columnComparators.get(columnType);\r
-        if (comparator != null) {\r
-            return comparator;\r
-        }\r
-        if (Comparable.class.isAssignableFrom(columnType)) {\r
-            return COMPARABLE_COMAPRATOR;\r
-        }\r
-        return LEXICAL_COMPARATOR;\r
-    }\r
-\r
-    private Row[] getViewToModel() {\r
-        if (viewToModel == null) {\r
-            int tableModelRowCount = tableModel.getRowCount();\r
-            viewToModel = new Row[tableModelRowCount];\r
-            for (int row = 0; row < tableModelRowCount; row++) {\r
-                viewToModel[row] = new Row(row);\r
-            }\r
-\r
-            if (isSorting()) {\r
-                Arrays.sort(viewToModel);\r
-            }\r
-        }\r
-        return viewToModel;\r
-    }\r
-\r
-    public int modelIndex(int viewIndex) {\r
-        return getViewToModel()[viewIndex].modelIndex;\r
-    }\r
-\r
-    private int[] getModelToView() {\r
-        if (modelToView == null) {\r
-            int n = getViewToModel().length;\r
-            modelToView = new int[n];\r
-            for (int i = 0; i < n; i++) {\r
-                modelToView[modelIndex(i)] = i;\r
-            }\r
-        }\r
-        return modelToView;\r
-    }\r
-\r
-    // TableModel interface methods\r
-\r
-    public int getRowCount() {\r
-        return (tableModel == null) ? 0 : tableModel.getRowCount();\r
-    }\r
+public class TableSorter extends AbstractTableModel\r
+{\r
+  protected TableModel tableModel;\r
 \r
-    public int getColumnCount() {\r
-        return (tableModel == null) ? 0 : tableModel.getColumnCount();\r
-    }\r
+  public static final int DESCENDING = -1;\r
 \r
-    public String getColumnName(int column) {\r
-        return tableModel.getColumnName(column);\r
-    }\r
+  public static final int NOT_SORTED = 0;\r
 \r
-    public Class getColumnClass(int column) {\r
-        return tableModel.getColumnClass(column);\r
-    }\r
+  public static final int ASCENDING = 1;\r
 \r
-    public boolean isCellEditable(int row, int column) {\r
-        return tableModel.isCellEditable(modelIndex(row), column);\r
-    }\r
+  private static Directive EMPTY_DIRECTIVE = new Directive(-1, NOT_SORTED);\r
 \r
-    public Object getValueAt(int row, int column) {\r
-        return tableModel.getValueAt(modelIndex(row), column);\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 void setValueAt(Object aValue, int row, int column) {\r
-        tableModel.setValueAt(aValue, modelIndex(row), column);\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
-    // Helper classes\r
-\r
-    private class Row implements Comparable {\r
-        private int modelIndex;\r
+  private Row[] viewToModel;\r
 \r
-        public Row(int index) {\r
-            this.modelIndex = index;\r
-        }\r
+  private int[] modelToView;\r
 \r
-        public int compareTo(Object o) {\r
-            int row1 = modelIndex;\r
-            int row2 = ((Row) o).modelIndex;\r
-\r
-            for (Iterator it = sortingColumns.iterator(); it.hasNext();) {\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
-                    comparison = 0;\r
-                } else if (o1 == null) {\r
-                    comparison = -1;\r
-                } else if (o2 == null) {\r
-                    comparison = 1;\r
-                } else {\r
-                    comparison = getComparator(column).compare(o1, o2);\r
-                }\r
-                if (comparison != 0) {\r
-                    return directive.direction == DESCENDING ? -comparison : comparison;\r
-                }\r
-            }\r
-            return 0;\r
-        }\r
-    }\r
+  private JTableHeader tableHeader;\r
 \r
-    private class TableModelHandler implements TableModelListener {\r
-        public void tableChanged(TableModelEvent e) {\r
-            // If we're not sorting by anything, just pass the event along.\r
-            if (!isSorting()) {\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
-                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()) and,\r
-            // b) all the changes are in one column (column != TableModelEvent.ALL_COLUMNS) and,\r
-            // c) we are not sorting on that column (getSortingStatus(column) == 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 == 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
-                int viewIndex = getModelToView()[e.getFirstRow()];\r
-                fireTableChanged(new TableModelEvent(TableSorter.this,\r
-                                                     viewIndex, viewIndex,\r
-                                                     column, e.getType()));\r
-                return;\r
-            }\r
-\r
-            // Something has happened to the data that may have invalidated the row order.\r
-            clearSortingState();\r
-            fireTableDataChanged();\r
-            return;\r
-        }\r
-    }\r
+  private MouseListener mouseListener;\r
 \r
-    private class MouseHandler extends MouseAdapter {\r
-        public void mouseClicked(MouseEvent e) {\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
-                int status = getSortingStatus(column);\r
-                if (!e.isControlDown()) {\r
-                    cancelSorting();\r
-                }\r
-                // Cycle the sorting states through {NOT_SORTED, ASCENDING, DESCENDING} or\r
-                // {NOT_SORTED, DESCENDING, ASCENDING} depending on whether shift is 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
+  private TableModelListener tableModelListener;\r
 \r
-    private static class Arrow implements Icon {\r
-        private boolean descending;\r
-        private int size;\r
-        private int priority;\r
+  private Map columnComparators = new HashMap();\r
 \r
-        public Arrow(boolean descending, int size, int priority) {\r
-            this.descending = descending;\r
-            this.size = size;\r
-            this.priority = priority;\r
-        }\r
+  private List sortingColumns = new ArrayList();\r
 \r
-        public void paintIcon(Component c, Graphics g, int x, int y) {\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
-                g.setColor(color.darker().darker());\r
-            } else {\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
+  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
-\r
-        public int getIconWidth() {\r
-            return size;\r
+        else if (o1 == null)\r
+        {\r
+          comparison = -1;\r
         }\r
-\r
-        public int getIconHeight() {\r
-            return size;\r
+        else if (o2 == null)\r
+        {\r
+          comparison = 1;\r
         }\r
-    }\r
-\r
-    private class SortableHeaderRenderer implements TableCellRenderer {\r
-        private TableCellRenderer tableCellRenderer;\r
-\r
-        public SortableHeaderRenderer(TableCellRenderer tableCellRenderer) {\r
-            this.tableCellRenderer = tableCellRenderer;\r
+        else\r
+        {\r
+          comparison = getComparator(column).compare(o1, o2);\r
         }\r
-\r
-        public Component getTableCellRendererComponent(JTable table,\r
-                                                       Object value,\r
-                                                       boolean isSelected,\r
-                                                       boolean hasFocus,\r
-                                                       int row,\r
-                                                       int column) {\r
-            Component c = tableCellRenderer.getTableCellRendererComponent(table,\r
-                    value, isSelected, hasFocus, row, column);\r
-            if (c instanceof JLabel) {\r
-                JLabel l = (JLabel) c;\r
-                l.setHorizontalTextPosition(JLabel.LEFT);\r
-                int modelColumn = table.convertColumnIndexToModel(column);\r
-                l.setIcon(getHeaderRendererIcon(modelColumn, l.getFont().getSize()));\r
-            }\r
-            return c;\r
+        if (comparison != 0)\r
+        {\r
+          return directive.direction == DESCENDING ? -comparison\r
+                  : comparison;\r
         }\r
-    }\r
-\r
-    private static class Directive {\r
-        private int column;\r
-        private int direction;\r
-\r
-        public Directive(int column, int direction) {\r
-            this.column = column;\r
-            this.direction = direction;\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
-    }\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.setIcon(getHeaderRendererIcon(modelColumn, l.getFont().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