View Javadoc

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