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
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
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
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
546 if (!isSorting())
547 {
548 clearSortingState();
549 fireTableChanged(e);
550 return;
551 }
552
553
554
555
556 if (e.getFirstRow() == TableModelEvent.HEADER_ROW)
557 {
558 cancelSorting();
559 fireTableChanged(e);
560 return;
561 }
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
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
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
613
614 status = status + (e.isShiftDown() ? -1 : 1);
615 status = (status + 4) % 3 - 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
651
652 int dx = (int) (size / 2 * Math.pow(0.8, priority));
653 int dy = descending ? dx : -dx;
654
655 y = y + 5 * size / 6 + (descending ? -dy : 0);
656 int shift = descending ? 1 : -1;
657 g.translate(x, y);
658
659
660 g.setColor(color.darker());
661 g.drawLine(dx / 2, dy, 0, 0);
662 g.drawLine(dx / 2, dy + shift, 0, shift);
663
664
665 g.setColor(color.brighter());
666 g.drawLine(dx / 2, dy, dx, 0);
667 g.drawLine(dx / 2, dy + shift, dx, shift);
668
669
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 }