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