1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 package net.sf.jour.rt.swingmonitor;
22
23 import java.awt.*;
24 import java.awt.event.*;
25 import java.util.*;
26 import java.util.List;
27
28 import javax.swing.*;
29 import javax.swing.event.TableModelEvent;
30 import javax.swing.event.TableModelListener;
31 import javax.swing.table.*;
32
33 /***
34 * TableSorter is a decorator for TableModels; adding sorting
35 * functionality to a supplied TableModel. TableSorter does
36 * not store or copy the data in its TableModel; instead it maintains
37 * a map from the row indexes of the view to the row indexes of the
38 * model. As requests are made of the sorter (like getValueAt(row, col))
39 * they are passed to the underlying model after the row numbers
40 * have been translated via the internal mapping array. This way,
41 * the TableSorter appears to hold another copy of the table
42 * with the rows in a different order.
43 * <p/>
44 * TableSorter registers itself as a listener to the underlying model,
45 * just as the JTable itself would. Events recieved from the model
46 * are examined, sometimes manipulated (typically widened), and then
47 * passed on to the TableSorter's listeners (typically the JTable).
48 * If a change to the model has invalidated the order of TableSorter's
49 * rows, a note of this is made and the sorter will resort the
50 * rows the next time a value is requested.
51 * <p/>
52 * When the tableHeader property is set, either by using the
53 * setTableHeader() method or the two argument constructor, the
54 * table header may be used as a complete UI for TableSorter.
55 * The default renderer of the tableHeader is decorated with a renderer
56 * that indicates the sorting status of each column. In addition,
57 * a mouse listener is installed with the following behavior:
58 * <ul>
59 * <li>
60 * Mouse-click: Clears the sorting status of all other columns
61 * and advances the sorting status of that column through three
62 * values: {NOT_SORTED, ASCENDING, DESCENDING} (then back to
63 * NOT_SORTED again).
64 * <li>
65 * SHIFT-mouse-click: Clears the sorting status of all other columns
66 * and cycles the sorting status of the column through the same
67 * three values, in the opposite order: {NOT_SORTED, DESCENDING, ASCENDING}.
68 * <li>
69 * CONTROL-mouse-click and CONTROL-SHIFT-mouse-click: as above except
70 * that the changes to the column do not cancel the statuses of columns
71 * that are already sorting - giving a way to initiate a compound
72 * sort.
73 * </ul>
74 * <p/>
75 * This is a long overdue rewrite of a class of the same name that
76 * first appeared in the swing table demos in 1997.
77 *
78 * @author Philip Milne
79 * @author Brendon McLean
80 * @author Dan van Enckevort
81 * @author Parwinder Sekhon
82 * @version 2.0 02/27/04
83 */
84
85 public class TableSorter extends AbstractTableModel {
86 protected TableModel tableModel;
87
88 public static final int DESCENDING = -1;
89 public static final int NOT_SORTED = 0;
90 public static final int ASCENDING = 1;
91
92 private static Directive EMPTY_DIRECTIVE = new Directive(-1, NOT_SORTED);
93
94 public static final Comparator COMPARABLE_COMAPRATOR = new Comparator() {
95 public int compare(Object o1, Object o2) {
96 return ((Comparable) o1).compareTo(o2);
97 }
98 };
99 public static final Comparator LEXICAL_COMPARATOR = new Comparator() {
100 public int compare(Object o1, Object o2) {
101 return o1.toString().compareTo(o2.toString());
102 }
103 };
104
105 private Row[] viewToModel;
106 private int[] modelToView;
107
108 private JTableHeader tableHeader;
109 private MouseListener mouseListener;
110 private TableModelListener tableModelListener;
111 private Map columnComparators = new HashMap();
112 private List sortingColumns = new ArrayList();
113
114 public TableSorter() {
115 this.mouseListener = new MouseHandler();
116 this.tableModelListener = new TableModelHandler();
117 }
118
119 public TableSorter(TableModel tableModel) {
120 this();
121 setTableModel(tableModel);
122 }
123
124 public TableSorter(TableModel tableModel, JTableHeader tableHeader) {
125 this();
126 setTableHeader(tableHeader);
127 setTableModel(tableModel);
128 }
129
130 private void clearSortingState() {
131 viewToModel = null;
132 modelToView = null;
133 }
134
135 public TableModel getTableModel() {
136 return tableModel;
137 }
138
139 public void setTableModel(TableModel tableModel) {
140 if (this.tableModel != null) {
141 this.tableModel.removeTableModelListener(tableModelListener);
142 }
143
144 this.tableModel = tableModel;
145 if (this.tableModel != null) {
146 this.tableModel.addTableModelListener(tableModelListener);
147 }
148
149 clearSortingState();
150 fireTableStructureChanged();
151 }
152
153 public JTableHeader getTableHeader() {
154 return tableHeader;
155 }
156
157 public void setTableHeader(JTableHeader tableHeader) {
158 if (this.tableHeader != null) {
159 this.tableHeader.removeMouseListener(mouseListener);
160 TableCellRenderer defaultRenderer = this.tableHeader.getDefaultRenderer();
161 if (defaultRenderer instanceof SortableHeaderRenderer) {
162 this.tableHeader.setDefaultRenderer(((SortableHeaderRenderer) defaultRenderer).tableCellRenderer);
163 }
164 }
165 this.tableHeader = tableHeader;
166 if (this.tableHeader != null) {
167 this.tableHeader.addMouseListener(mouseListener);
168 this.tableHeader.setDefaultRenderer(
169 new SortableHeaderRenderer(this.tableHeader.getDefaultRenderer()));
170 }
171 }
172
173 public boolean isSorting() {
174 return sortingColumns.size() != 0;
175 }
176
177 private Directive getDirective(int column) {
178 for (int i = 0; i < sortingColumns.size(); i++) {
179 Directive directive = (Directive)sortingColumns.get(i);
180 if (directive.column == column) {
181 return directive;
182 }
183 }
184 return EMPTY_DIRECTIVE;
185 }
186
187 public int getSortingStatus(int column) {
188 return getDirective(column).direction;
189 }
190
191 private void sortingStatusChanged() {
192 clearSortingState();
193 fireTableDataChanged();
194 if (tableHeader != null) {
195 tableHeader.repaint();
196 }
197 }
198
199 public void setSortingStatus(int column, int status) {
200 Directive directive = getDirective(column);
201 if (directive != EMPTY_DIRECTIVE) {
202 sortingColumns.remove(directive);
203 }
204 if (status != NOT_SORTED) {
205 sortingColumns.add(new Directive(column, status));
206 }
207 sortingStatusChanged();
208 }
209
210 protected Icon getHeaderRendererIcon(int column, int size) {
211 Directive directive = getDirective(column);
212 if (directive == EMPTY_DIRECTIVE) {
213 return null;
214 }
215 return new Arrow(directive.direction == DESCENDING, size, sortingColumns.indexOf(directive));
216 }
217
218 private void cancelSorting() {
219 sortingColumns.clear();
220 sortingStatusChanged();
221 }
222
223 public void setColumnComparator(Class type, Comparator comparator) {
224 if (comparator == null) {
225 columnComparators.remove(type);
226 } else {
227 columnComparators.put(type, comparator);
228 }
229 }
230
231 protected Comparator getComparator(int column) {
232 Class columnType = tableModel.getColumnClass(column);
233 Comparator comparator = (Comparator) columnComparators.get(columnType);
234 if (comparator != null) {
235 return comparator;
236 }
237 if (Comparable.class.isAssignableFrom(columnType)) {
238 return COMPARABLE_COMAPRATOR;
239 }
240 return LEXICAL_COMPARATOR;
241 }
242
243 private Row[] getViewToModel() {
244 if (viewToModel == null) {
245 int tableModelRowCount = tableModel.getRowCount();
246 viewToModel = new Row[tableModelRowCount];
247 for (int row = 0; row < tableModelRowCount; row++) {
248 viewToModel[row] = new Row(row);
249 }
250
251 if (isSorting()) {
252 Arrays.sort(viewToModel);
253 }
254 }
255 return viewToModel;
256 }
257
258 public int modelIndex(int viewIndex) {
259 return getViewToModel()[viewIndex].modelIndex;
260 }
261
262 private int[] getModelToView() {
263 if (modelToView == null) {
264 int n = getViewToModel().length;
265 modelToView = new int[n];
266 for (int i = 0; i < n; i++) {
267 modelToView[modelIndex(i)] = i;
268 }
269 }
270 return modelToView;
271 }
272
273
274
275 public int getRowCount() {
276 return (tableModel == null) ? 0 : tableModel.getRowCount();
277 }
278
279 public int getColumnCount() {
280 return (tableModel == null) ? 0 : tableModel.getColumnCount();
281 }
282
283 public String getColumnName(int column) {
284 return tableModel.getColumnName(column);
285 }
286
287 public Class getColumnClass(int column) {
288 return tableModel.getColumnClass(column);
289 }
290
291 public boolean isCellEditable(int row, int column) {
292 return tableModel.isCellEditable(modelIndex(row), column);
293 }
294
295 public Object getValueAt(int row, int column) {
296 return tableModel.getValueAt(modelIndex(row), column);
297 }
298
299 public void setValueAt(Object aValue, int row, int column) {
300 tableModel.setValueAt(aValue, modelIndex(row), column);
301 }
302
303
304
305 private class Row implements Comparable {
306 private int modelIndex;
307
308 public Row(int index) {
309 this.modelIndex = index;
310 }
311
312 public int compareTo(Object o) {
313 int row1 = modelIndex;
314 int row2 = ((Row) o).modelIndex;
315
316 for (Iterator it = sortingColumns.iterator(); it.hasNext();) {
317 Directive directive = (Directive) it.next();
318 int column = directive.column;
319 Object o1 = tableModel.getValueAt(row1, column);
320 Object o2 = tableModel.getValueAt(row2, column);
321
322 int comparison = 0;
323
324 if (o1 == null && o2 == null) {
325 comparison = 0;
326 } else if (o1 == null) {
327 comparison = -1;
328 } else if (o2 == null) {
329 comparison = 1;
330 } else {
331 comparison = getComparator(column).compare(o1, o2);
332 }
333 if (comparison != 0) {
334 return directive.direction == DESCENDING ? -comparison : comparison;
335 }
336 }
337 return 0;
338 }
339 }
340
341 private class TableModelHandler implements TableModelListener {
342 public void tableChanged(TableModelEvent e) {
343
344 if (!isSorting()) {
345 clearSortingState();
346 fireTableChanged(e);
347 return;
348 }
349
350
351
352
353 if (e.getFirstRow() == TableModelEvent.HEADER_ROW) {
354 cancelSorting();
355 fireTableChanged(e);
356 return;
357 }
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377 int column = e.getColumn();
378 if (e.getFirstRow() == e.getLastRow()
379 && column != TableModelEvent.ALL_COLUMNS
380 && getSortingStatus(column) == NOT_SORTED
381 && modelToView != null) {
382 int viewIndex = getModelToView()[e.getFirstRow()];
383 fireTableChanged(new TableModelEvent(TableSorter.this,
384 viewIndex, viewIndex,
385 column, e.getType()));
386 return;
387 }
388
389
390 clearSortingState();
391 fireTableDataChanged();
392 return;
393 }
394 }
395
396 private class MouseHandler extends MouseAdapter {
397 public void mouseClicked(MouseEvent e) {
398 JTableHeader h = (JTableHeader) e.getSource();
399 TableColumnModel columnModel = h.getColumnModel();
400 int viewColumn = columnModel.getColumnIndexAtX(e.getX());
401 int column = columnModel.getColumn(viewColumn).getModelIndex();
402 if (column != -1) {
403 int status = getSortingStatus(column);
404 if (!e.isControlDown()) {
405 cancelSorting();
406 }
407
408
409 status = status + (e.isShiftDown() ? -1 : 1);
410 status = (status + 4) % 3 - 1;
411 setSortingStatus(column, status);
412 }
413 }
414 }
415
416 private static class Arrow implements Icon {
417 private boolean descending;
418 private int size;
419 private int priority;
420
421 public Arrow(boolean descending, int size, int priority) {
422 this.descending = descending;
423 this.size = size;
424 this.priority = priority;
425 }
426
427 public void paintIcon(Component c, Graphics g, int x, int y) {
428 Color color = c == null ? Color.gray : c.getBackground();
429
430
431 int dx = (int)(size/2*Math.pow(0.8, priority));
432 int dy = descending ? dx : -dx;
433
434 y = y + 5*size/6 + (descending ? -dy : 0);
435 int shift = descending ? 1 : -1;
436 g.translate(x, y);
437
438
439 g.setColor(color.darker());
440 g.drawLine(dx / 2, dy, 0, 0);
441 g.drawLine(dx / 2, dy + shift, 0, shift);
442
443
444 g.setColor(color.brighter());
445 g.drawLine(dx / 2, dy, dx, 0);
446 g.drawLine(dx / 2, dy + shift, dx, shift);
447
448
449 if (descending) {
450 g.setColor(color.darker().darker());
451 } else {
452 g.setColor(color.brighter().brighter());
453 }
454 g.drawLine(dx, 0, 0, 0);
455
456 g.setColor(color);
457 g.translate(-x, -y);
458 }
459
460 public int getIconWidth() {
461 return size;
462 }
463
464 public int getIconHeight() {
465 return size;
466 }
467 }
468
469 private class SortableHeaderRenderer implements TableCellRenderer {
470 private TableCellRenderer tableCellRenderer;
471
472 public SortableHeaderRenderer(TableCellRenderer tableCellRenderer) {
473 this.tableCellRenderer = tableCellRenderer;
474 }
475
476 public Component getTableCellRendererComponent(JTable table,
477 Object value,
478 boolean isSelected,
479 boolean hasFocus,
480 int row,
481 int column) {
482 Component c = tableCellRenderer.getTableCellRendererComponent(table,
483 value, isSelected, hasFocus, row, column);
484 if (c instanceof JLabel) {
485 JLabel l = (JLabel) c;
486 l.setHorizontalTextPosition(JLabel.LEFT);
487 int modelColumn = table.convertColumnIndexToModel(column);
488 l.setIcon(getHeaderRendererIcon(modelColumn, l.getFont().getSize()));
489 }
490 return c;
491 }
492 }
493
494 private static class Directive {
495 private int column;
496 private int direction;
497
498 public Directive(int column, int direction) {
499 this.column = column;
500 this.direction = direction;
501 }
502 }
503 }