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