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