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