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