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