KickJava   Java API By Example, From Geeks To Geeks.

Java > Open Source Codes > net > mlw > vlh > swing > support > TableSorter


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 JavaDoc;
55 import java.awt.Component JavaDoc;
56 import java.awt.Graphics JavaDoc;
57 import java.awt.event.ActionEvent JavaDoc;
58 import java.awt.event.ActionListener JavaDoc;
59 import java.awt.event.MouseAdapter JavaDoc;
60 import java.awt.event.MouseEvent JavaDoc;
61 import java.awt.event.MouseListener JavaDoc;
62 import java.util.ArrayList JavaDoc;
63 import java.util.Arrays JavaDoc;
64 import java.util.Comparator JavaDoc;
65 import java.util.HashMap JavaDoc;
66 import java.util.Iterator JavaDoc;
67 import java.util.List JavaDoc;
68 import java.util.Map JavaDoc;
69
70 import javax.swing.Icon JavaDoc;
71 import javax.swing.JLabel JavaDoc;
72 import javax.swing.JTable JavaDoc;
73 import javax.swing.event.TableModelEvent JavaDoc;
74 import javax.swing.event.TableModelListener JavaDoc;
75 import javax.swing.table.AbstractTableModel JavaDoc;
76 import javax.swing.table.JTableHeader JavaDoc;
77 import javax.swing.table.TableCellRenderer JavaDoc;
78 import javax.swing.table.TableColumnModel JavaDoc;
79 import javax.swing.table.TableModel JavaDoc;
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 JavaDoc implements ValueListTableModel, ActionListener JavaDoc
86 {
87    protected ValueListTableModel valueListTableModel;
88
89    protected TableModel JavaDoc 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 JavaDoc COMPARABLE_COMAPRATOR = new Comparator JavaDoc()
100    {
101       public int compare(Object JavaDoc o1, Object JavaDoc o2)
102       {
103          return ((Comparable JavaDoc) o1).compareTo(o2);
104       }
105    };
106
107    public static final Comparator JavaDoc LEXICAL_COMPARATOR = new Comparator JavaDoc()
108    {
109       public int compare(Object JavaDoc o1, Object JavaDoc 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 JavaDoc tableHeader;
120
121    private MouseListener JavaDoc mouseListener;
122
123    private TableModelListener JavaDoc tableModelListener;
124
125    private Map JavaDoc columnComparators = new HashMap JavaDoc();
126
127    private List JavaDoc sortingColumns = new ArrayList JavaDoc();
128
129    private ActionListener JavaDoc actionListener;
130
131    public TableSorter()
132    {
133       this.mouseListener = new MouseHandler();
134       this.tableModelListener = new TableModelHandler();
135    }
136
137    public TableSorter(TableModel JavaDoc tableModel)
138    {
139       this();
140       setTableModel(tableModel);
141    }
142
143    public TableSorter(TableModel JavaDoc tableModel, JTableHeader JavaDoc 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 JavaDoc e)
154    {
155       System.out.println("ooops = " + e);
156
157    }
158
159    public void addActionListener(ActionListener JavaDoc actionListener)
160    {
161       this.actionListener = actionListener;
162    }
163
164    private void clearSortingState()
165    {
166       viewToModel = null;
167       modelToView = null;
168    }
169
170    public TableModel JavaDoc getTableModel()
171    {
172       return tableModel;
173    }
174
175    public void setTableModel(TableModel JavaDoc 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 JavaDoc getTableHeader()
199    {
200       return tableHeader;
201    }
202
203    public void setTableHeader(JTableHeader JavaDoc tableHeader)
204    {
205       if (this.tableHeader != null)
206       {
207          this.tableHeader.removeMouseListener(mouseListener);
208          TableCellRenderer JavaDoc 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 JavaDoc 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 JavaDoc 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 JavaDoc type, Comparator JavaDoc 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 JavaDoc getComparator(int column)
306    {
307       Class JavaDoc columnType = tableModel.getColumnClass(column);
308       Comparator JavaDoc comparator = (Comparator JavaDoc) columnComparators.get(columnType);
309       if (comparator != null)
310       {
311          return comparator;
312       }
313       if (Comparable JavaDoc.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 JavaDoc getColumnName(int column)
396    {
397       return tableModel.getColumnName(column);
398    }
399
400    public Class JavaDoc 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 JavaDoc getValueAt(int row, int column)
411    {
412       return tableModel.getValueAt(modelIndex(row), column);
413    }
414
415    public void setValueAt(Object JavaDoc 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 JavaDoc 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 JavaDoc("A ValueListTableModel must be assigned to call this method.");
453       }
454
455    }
456
457    public int addBean(Object JavaDoc 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 JavaDoc("A ValueListTableModel must be assigned to call this method.");
471       }
472    }
473
474    public int removeBean(Object JavaDoc 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 JavaDoc("A ValueListTableModel must be assigned to call this method.");
488       }
489    }
490
491    // Helper classes
492

493    private class Row implements Comparable JavaDoc
494    {
495       private int modelIndex;
496
497       public Row(int index)
498       {
499          this.modelIndex = index;
500       }
501
502       public int compareTo(Object JavaDoc o)
503       {
504          int row1 = modelIndex;
505          int row2 = ((Row) o).modelIndex;
506
507          for (Iterator JavaDoc it = sortingColumns.iterator(); it.hasNext();)
508          {
509             Directive directive = (Directive) it.next();
510             int column = directive.column;
511             Object JavaDoc o1 = tableModel.getValueAt(row1, column);
512             Object JavaDoc 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 JavaDoc
542    {
543       public void tableChanged(TableModelEvent JavaDoc 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 JavaDoc(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 JavaDoc
598    {
599       public void mouseClicked(MouseEvent JavaDoc e)
600       {
601          JTableHeader JavaDoc h = (JTableHeader JavaDoc) e.getSource();
602          TableColumnModel JavaDoc 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 JavaDoc(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 JavaDoc
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 JavaDoc c, Graphics JavaDoc g, int x, int y)
648       {
649          Color JavaDoc 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 JavaDoc
696    {
697       private TableCellRenderer JavaDoc tableCellRenderer;
698
699       public SortableHeaderRenderer(TableCellRenderer JavaDoc tableCellRenderer)
700       {
701          this.tableCellRenderer = tableCellRenderer;
702       }
703
704       public Component JavaDoc getTableCellRendererComponent(JTable JavaDoc table, Object JavaDoc value, boolean isSelected, boolean hasFocus, int row, int column)
705       {
706          Component JavaDoc c = tableCellRenderer.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
707          if (c instanceof JLabel JavaDoc)
708          {
709             JLabel JavaDoc l = (JLabel JavaDoc) 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 JavaDoc 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 JavaDoc bean)
760    {
761       if (valueListTableModel != null)
762       {
763          return valueListTableModel.contains(bean);
764       }
765       else
766       {
767          throw new IllegalStateException JavaDoc("A ValueListTableModel must be assigned to call this method.");
768       }
769    }
770 }
Popular Tags