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