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