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