This commit was manufactured by cvs2svn to create branch 'VamJalview'.
[jalview.git] / src / jalview / util / TableSorter.java
diff --git a/src/jalview/util/TableSorter.java b/src/jalview/util/TableSorter.java
new file mode 100755 (executable)
index 0000000..c03e804
--- /dev/null
@@ -0,0 +1,501 @@
+/*\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
+ */\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 javax.swing.*;\r
+import javax.swing.event.TableModelEvent;\r
+import javax.swing.event.TableModelListener;\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
+ * <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
+ * <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
+ * <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
+ * </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
+ * @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
+    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
+\r
+    public int getColumnCount() {\r
+        return (tableModel == null) ? 0 : tableModel.getColumnCount();\r
+    }\r
+\r
+    public String getColumnName(int column) {\r
+        return tableModel.getColumnName(column);\r
+    }\r
+\r
+    public Class getColumnClass(int column) {\r
+        return tableModel.getColumnClass(column);\r
+    }\r
+\r
+    public boolean isCellEditable(int row, int column) {\r
+        return tableModel.isCellEditable(modelIndex(row), column);\r
+    }\r
+\r
+    public Object getValueAt(int row, int column) {\r
+        return tableModel.getValueAt(modelIndex(row), column);\r
+    }\r
+\r
+    public void setValueAt(Object aValue, int row, int column) {\r
+        tableModel.setValueAt(aValue, modelIndex(row), column);\r
+    }\r
+\r
+    // Helper classes\r
+\r
+    private class Row implements Comparable {\r
+        private int modelIndex;\r
+\r
+        public Row(int index) {\r
+            this.modelIndex = index;\r
+        }\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
+\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
+\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
+\r
+    private static class Arrow implements Icon {\r
+        private boolean descending;\r
+        private int size;\r
+        private int priority;\r
+\r
+        public Arrow(boolean descending, int size, int priority) {\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
+            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
+        }\r
+\r
+        public int getIconWidth() {\r
+            return size;\r
+        }\r
+\r
+        public int getIconHeight() {\r
+            return size;\r
+        }\r
+    }\r
+\r
+    private class SortableHeaderRenderer implements TableCellRenderer {\r
+        private TableCellRenderer tableCellRenderer;\r
+\r
+        public SortableHeaderRenderer(TableCellRenderer tableCellRenderer) {\r
+            this.tableCellRenderer = tableCellRenderer;\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
+        }\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
+    }\r
+}\r