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