001: package org.underworldlabs.swing.table;
002:
003: import java.awt.event.MouseAdapter;
004: import java.awt.event.MouseEvent;
005: import java.awt.event.MouseListener;
006:
007: import java.util.ArrayList;
008: import java.util.Arrays;
009: import java.util.Comparator;
010: import java.util.Date;
011: import java.util.HashMap;
012: import java.util.Iterator;
013: import java.util.List;
014: import java.util.Map;
015:
016: import javax.swing.Icon;
017: import javax.swing.event.TableModelEvent;
018: import javax.swing.event.TableModelListener;
019: import javax.swing.table.AbstractTableModel;
020: import javax.swing.table.JTableHeader;
021: import javax.swing.table.TableColumnModel;
022: import javax.swing.table.TableModel;
023: import org.underworldlabs.swing.util.SwingWorker;
024:
025: /*
026: * TableSorter.java
027: *
028: * Copyright (C) 2002, 2003, 2004, 2005, 2006 Takis Diakoumis
029: *
030: * This program is free software; you can redistribute it and/or
031: * modify it under the terms of the GNU General Public License
032: * as published by the Free Software Foundation; either version 2
033: * of the License, or any later version.
034: *
035: * This program is distributed in the hope that it will be useful,
036: * but WITHOUT ANY WARRANTY; without even the implied warranty of
037: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
038: * GNU General Public License for more details.
039: *
040: * You should have received a copy of the GNU General Public License
041: * along with this program; if not, write to the Free Software
042: * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
043: *
044: */
045:
046: /* ----------------------------------------------------------
047: * CVS NOTE: Changes to the CVS repository prior to the
048: * release of version 3.0.0beta1 has meant a
049: * resetting of CVS revision numbers.
050: * ----------------------------------------------------------
051: */
052:
053: /**
054: * TableSorter is a decorator for TableModels; adding sorting
055: * functionality to a supplied TableModel. TableSorter does
056: * not store or copy the data in its TableModel; instead it maintains
057: * a map from the row indexes of the view to the row indexes of the
058: * model. As requests are made of the sorter (like getValueAt(row, col))
059: * they are passed to the underlying model after the row numbers
060: * have been translated via the internal mapping array. This way,
061: * the TableSorter appears to hold another copy of the table
062: * with the rows in a different order.
063: * <p/>
064: * TableSorter registers itself as a listener to the underlying model,
065: * just as the JTable itself would. Events recieved from the model
066: * are examined, sometimes manipulated (typically widened), and then
067: * passed on to the TableSorter's listeners (typically the JTable).
068: * If a change to the model has invalidated the order of TableSorter's
069: * rows, a note of this is made and the sorter will resort the
070: * rows the next time a value is requested.
071: * <p/>
072: * When the tableHeader property is set, either by using the
073: * setTableHeader() method or the two argument constructor, the
074: * table header may be used as a complete UI for TableSorter.
075: * The default renderer of the tableHeader is decorated with a renderer
076: * that indicates the sorting status of each column. In addition,
077: * a mouse listener is installed with the following behavior:
078: * <ul>
079: * <li>
080: * Mouse-click: Clears the sorting status of all other columns
081: * and advances the sorting status of that column through three
082: * values: {NOT_SORTED, ASCENDING, DESCENDING} (then back to
083: * NOT_SORTED again).
084: * <li>
085: * SHIFT-mouse-click: Clears the sorting status of all other columns
086: * and cycles the sorting status of the column through the same
087: * three values, in the opposite order: {NOT_SORTED, DESCENDING, ASCENDING}.
088: * <li>
089: * CONTROL-mouse-click and CONTROL-SHIFT-mouse-click: as above except
090: * that the changes to the column do not cancel the statuses of columns
091: * that are already sorting - giving a way to initiate a compound
092: * sort.
093: * </ul>
094: * <p/>
095: * This is a long overdue rewrite of a class of the same name that
096: * first appeared in the swing table demos in 1997.
097: *
098: * @author Philip Milne
099: * @author Brendon McLean
100: * @author Dan van Enckevort
101: * @author Parwinder Sekhon
102: *
103: * @author Takis Diakoumis
104: * @version $Revision: 1.5 $
105: * @date $Date: 2006/05/14 06:56:07 $
106: */
107: public class TableSorter extends AbstractTableModel {
108:
109: protected TableModel tableModel;
110:
111: public static final int DESCENDING = -1;
112: public static final int NOT_SORTED = 0;
113: public static final int ASCENDING = 1;
114:
115: private static Directive EMPTY_DIRECTIVE = new Directive(-1,
116: NOT_SORTED);
117:
118: private SortableHeaderRenderer headerRenderer;
119:
120: public static final Comparator COMPARABLE_COMAPRATOR = new Comparator() {
121: public int compare(Object o1, Object o2) {
122: return ((Comparable) o1).compareTo(o2);
123: }
124: };
125: public static final Comparator LEXICAL_COMPARATOR = new Comparator() {
126: public int compare(Object o1, Object o2) {
127: return o1.toString().compareTo(o2.toString());
128: }
129: };
130:
131: private Row[] viewToModel;
132: private int[] modelToView;
133:
134: private JTableHeader tableHeader;
135: private MouseListener mouseListener;
136: private TableModelListener tableModelListener;
137: private Map columnComparators = new HashMap();
138: private List sortingColumns = new ArrayList();
139:
140: public TableSorter() {
141: this .mouseListener = new MouseHandler();
142: this .tableModelListener = new TableModelHandler();
143: this .headerRenderer = new SortableHeaderRenderer(this );
144: }
145:
146: public TableSorter(TableModel tableModel) {
147: this ();
148: setTableModel(tableModel);
149: }
150:
151: public TableSorter(TableModel tableModel, JTableHeader tableHeader) {
152: this ();
153: setTableHeader(tableHeader);
154: setTableModel(tableModel);
155: }
156:
157: private void clearSortingState() {
158: viewToModel = null;
159: modelToView = null;
160: }
161:
162: public TableModel getTableModel() {
163: return tableModel;
164: }
165:
166: public void setTableModel(TableModel _tableModel) {
167:
168: if (tableModel != null) {
169: tableModel.removeTableModelListener(tableModelListener);
170: }
171: tableModel = _tableModel;
172:
173: if (tableModel != null) {
174: tableModel.addTableModelListener(tableModelListener);
175: }
176: /*
177: if (tableHeader != null) {
178: tableHeader.removeMouseListener(mouseListener);
179: tableHeader = null;
180: }
181: */
182: sortingColumns.clear();
183: clearSortingState();
184: fireTableStructureChanged();
185:
186: }
187:
188: public JTableHeader getTableHeader() {
189: return tableHeader;
190: }
191:
192: public void setTableHeader(JTableHeader _tableHeader) {
193:
194: tableHeader = _tableHeader;
195:
196: if (tableHeader != null) {
197: tableHeader.addMouseListener(mouseListener);
198: tableHeader.setDefaultRenderer(headerRenderer);//new SortableHeaderRenderer(this));
199: }
200:
201: }
202:
203: public boolean isSorting() {
204: return sortingColumns.size() != 0;
205: }
206:
207: private Directive getDirective(int column) {
208: for (int i = 0; i < sortingColumns.size(); i++) {
209: Directive directive = (Directive) sortingColumns.get(i);
210: if (directive.column == column) {
211: return directive;
212: }
213: }
214: return EMPTY_DIRECTIVE;
215: }
216:
217: public int getSortingStatus(int column) {
218: return getDirective(column).direction;
219: }
220:
221: private void sortingStatusChanged() {
222: clearSortingState();
223: fireTableDataChanged();
224:
225: if (tableHeader != null) {
226: tableHeader.repaint();
227: }
228: }
229:
230: public void setSortingStatus(int column, int status) {
231: Directive directive = getDirective(column);
232: if (directive != EMPTY_DIRECTIVE) {
233: sortingColumns.remove(directive);
234: }
235: if (status != NOT_SORTED) {
236: sortingColumns.add(new Directive(column, status));
237: }
238: sortingStatusChanged();
239: }
240:
241: public Icon getHeaderRendererIcon(int column, int size) {
242: Directive directive = getDirective(column);
243:
244: if (directive == EMPTY_DIRECTIVE) {
245: return null;
246: }
247:
248: boolean isAscend = directive.direction == ASCENDING;
249: return new ArrowIcon(isAscend ? ArrowIcon.DOWN : ArrowIcon.UP);
250: }
251:
252: public int getHeaderRendererIcon(int column) {
253: Directive directive = getDirective(column);
254: if (directive == EMPTY_DIRECTIVE) {
255: return -1;
256: }
257:
258: boolean isAscend = (directive.direction == ASCENDING);
259: return isAscend ? ArrowIcon.DOWN : ArrowIcon.UP;
260: }
261:
262: private void cancelSorting() {
263: sortingColumns.clear();
264: sortingStatusChanged();
265: }
266:
267: public void setColumnComparator(Class type, Comparator comparator) {
268: if (comparator == null) {
269: columnComparators.remove(type);
270: } else {
271: columnComparators.put(type, comparator);
272: }
273: }
274:
275: protected Comparator getComparator(int column) {
276:
277: Class columnType = tableModel.getColumnClass(column);
278: Comparator comparator = (Comparator) columnComparators
279: .get(columnType);
280:
281: if (comparator != null) {
282: return comparator;
283: }
284:
285: if (Comparable.class.isAssignableFrom(columnType)) {
286: return COMPARABLE_COMAPRATOR;
287: }
288:
289: return LEXICAL_COMPARATOR;
290: }
291:
292: private Row[] getViewToModel() {
293:
294: if (viewToModel == null) {
295: int tableModelRowCount = tableModel.getRowCount();
296: viewToModel = new Row[tableModelRowCount];
297:
298: if (viewToModel == null)
299: return null;
300:
301: for (int row = 0; row < tableModelRowCount; row++) {
302: viewToModel[row] = new Row(row);
303: }
304:
305: if (isSorting())
306: Arrays.sort(viewToModel);
307:
308: }
309:
310: return viewToModel;
311:
312: }
313:
314: public int modelIndex(int viewIndex) {
315: return getViewToModel()[viewIndex].modelIndex;
316: }
317:
318: private int[] getModelToView() {
319: if (modelToView == null) {
320: int n = getViewToModel().length;
321: modelToView = new int[n];
322: for (int i = 0; i < n; i++) {
323: modelToView[modelIndex(i)] = i;
324: }
325: }
326: return modelToView;
327: }
328:
329: // TableModel interface methods
330:
331: public int getRowCount() {
332: return (tableModel == null) ? 0 : tableModel.getRowCount();
333: }
334:
335: public int getColumnCount() {
336: return (tableModel == null) ? 0 : tableModel.getColumnCount();
337: }
338:
339: public String getColumnName(int column) {
340: return tableModel.getColumnName(column);
341: }
342:
343: public Class getColumnClass(int column) {
344: return tableModel.getColumnClass(column);
345: }
346:
347: public boolean isCellEditable(int row, int column) {
348: return tableModel.isCellEditable(modelIndex(row), column);
349: }
350:
351: public Object getValueAt(int row, int column) {
352: if (row >= getRowCount() || column >= getColumnCount()) {
353: return null;
354: }
355: return tableModel.getValueAt(modelIndex(row), column);
356: }
357:
358: public void setValueAt(Object aValue, int row, int column) {
359: tableModel.setValueAt(aValue, modelIndex(row), column);
360: }
361:
362: // Helper classes
363:
364: private class Row implements Comparable {
365: private int modelIndex;
366:
367: public Row(int index) {
368: this .modelIndex = index;
369: }
370:
371: public int compareTo(Object o) {
372: int row1 = modelIndex;
373: int row2 = ((Row) o).modelIndex;
374:
375: for (Iterator it = sortingColumns.iterator(); it.hasNext();) {
376: Directive directive = (Directive) it.next();
377: int column = directive.column;
378: Object o1 = tableModel.getValueAt(row1, column);
379: Object o2 = tableModel.getValueAt(row2, column);
380:
381: int comparison = 0;
382:
383: // Define null less than everything, except null.
384: if (o1 == null && o2 == null)
385: comparison = 0;
386: else if (o1 == null)
387: comparison = -1;
388: else if (o2 == null)
389: comparison = 1;
390: else {
391:
392: Class type = tableModel.getColumnClass(column);
393:
394: if (columnComparators.containsKey(type))
395: comparison = ((Comparator) columnComparators
396: .get(type)).compare(o1, o2);
397:
398: else
399: comparison = compareByColumn(type, o1, o2);
400:
401: // comparison = getComparator(column).compare(o1, o2);
402:
403: }
404:
405: if (comparison != 0)
406: return directive.direction == DESCENDING ? -comparison
407: : comparison;
408:
409: }
410:
411: return 0;
412:
413: }
414:
415: private int compareByColumn(Class type, Object o1, Object o2) {
416:
417: if (type.getSuperclass() == java.lang.Number.class) {
418: Number n1 = (Number) o1;
419: double d1 = n1.doubleValue();
420: Number n2 = (Number) o2;
421: double d2 = n2.doubleValue();
422:
423: if (d1 < d2)
424: return -1;
425: else if (d1 > d2)
426: return 1;
427: else
428: return 0;
429:
430: }
431:
432: else if (type == java.util.Date.class) {
433: Date d1 = (Date) o1;
434: long n1 = d1.getTime();
435: Date d2 = (Date) o2;
436: long n2 = d2.getTime();
437:
438: if (n1 < n2)
439: return -1;
440: else if (n1 > n2)
441: return 1;
442: else
443: return 0;
444:
445: }
446:
447: else if (type == String.class) {
448: String s1 = (String) o1;
449: String s2 = (String) o2;
450: int result = s1.compareTo(s2);
451:
452: if (result < 0)
453: return -1;
454: else if (result > 0)
455: return 1;
456: else
457: return 0;
458:
459: }
460:
461: else if (type == Boolean.class) {
462: Boolean bool1 = (Boolean) o1;
463: boolean b1 = bool1.booleanValue();
464: Boolean bool2 = (Boolean) o2;
465: boolean b2 = bool2.booleanValue();
466:
467: if (b1 == b2)
468: return 0;
469: else if (b1) // Define false < true
470: return 1;
471: else
472: return -1;
473:
474: }
475:
476: else {
477:
478: try {
479: Number n1 = (Number) o1;
480: double d1 = n1.doubleValue();
481: Number n2 = (Number) o2;
482: double d2 = n2.doubleValue();
483:
484: if (d1 < d2)
485: return -1;
486: else if (d1 > d2)
487: return 1;
488: else
489: return 0;
490:
491: }
492:
493: catch (ClassCastException cExc) {
494: Object v1 = o1;
495: String s1 = v1.toString();
496: Object v2 = o2;
497: String s2 = v2.toString();
498: int result = s1.compareTo(s2);
499:
500: if (result < 0)
501: return -1;
502: else if (result > 0)
503: return 1;
504: else
505: return 0;
506:
507: }
508:
509: }
510:
511: }
512:
513: } // class Row
514:
515: private class TableModelHandler implements TableModelListener {
516: public void tableChanged(TableModelEvent e) {
517:
518: // If we're not sorting by anything, just pass the event along.
519: if (!isSorting()) {
520: clearSortingState();
521: fireTableChanged(e);
522: return;
523: }
524:
525: // If the table structure has changed, cancel the sorting; the
526: // sorting columns may have been either moved or deleted from
527: // the model.
528: if (e.getFirstRow() == TableModelEvent.HEADER_ROW) {
529: cancelSorting();
530: fireTableChanged(e);
531: return;
532: }
533:
534: // We can map a cell event through to the view without widening
535: // when the following conditions apply:
536: //
537: // a) all the changes are on one row (e.getFirstRow() == e.getLastRow()) and,
538: // b) all the changes are in one column (column != TableModelEvent.ALL_COLUMNS) and,
539: // c) we are not sorting on that column (getSortingStatus(column) == NOT_SORTED) and,
540: // d) a reverse lookup will not trigger a sort (modelToView != null)
541: //
542: // Note: INSERT and DELETE events fail this test as they have column == ALL_COLUMNS.
543: //
544: // The last check, for (modelToView != null) is to see if modelToView
545: // is already allocated. If we don't do this check; sorting can become
546: // a performance bottleneck for applications where cells
547: // change rapidly in different parts of the table. If cells
548: // change alternately in the sorting column and then outside of
549: // it this class can end up re-sorting on alternate cell updates -
550: // which can be a performance problem for large tables. The last
551: // clause avoids this problem.
552: int column = e.getColumn();
553: if (e.getFirstRow() == e.getLastRow()
554: && column != TableModelEvent.ALL_COLUMNS
555: && getSortingStatus(column) == NOT_SORTED
556: && modelToView != null) {
557: int viewIndex = getModelToView()[e.getFirstRow()];
558: fireTableChanged(new TableModelEvent(TableSorter.this ,
559: viewIndex, viewIndex, column, e.getType()));
560: return;
561: }
562:
563: // Something has happened to the data that may have invalidated the row order.
564: clearSortingState();
565: fireTableDataChanged();
566: return;
567: }
568: }
569:
570: private class MouseHandler extends MouseAdapter {
571:
572: int column, status;
573:
574: public void mouseClicked(MouseEvent e) {
575:
576: JTableHeader h = (JTableHeader) e.getSource();
577: TableColumnModel columnModel = h.getColumnModel();
578: int viewColumn = columnModel.getColumnIndexAtX(e.getX());
579: column = columnModel.getColumn(viewColumn).getModelIndex();
580:
581: if (column != -1) {
582: status = getSortingStatus(column);
583:
584: if (!e.isControlDown())
585: cancelSorting();
586:
587: // Cycle the sorting states through {NOT_SORTED, ASCENDING, DESCENDING} or
588: // {NOT_SORTED, DESCENDING, ASCENDING} depending on whether shift is pressed.
589: status = status + (e.isShiftDown() ? -1 : 1);
590: status = (status + 4) % 3 - 1; // signed mod, returning {-1, 0, 1}
591:
592: SwingWorker worker = new SwingWorker() {
593: public Object construct() {
594: setSortingStatus(column, status);
595: return "done";
596: }
597: };
598:
599: worker.start();
600:
601: }
602:
603: }
604:
605: } // class MouseHandler
606:
607: /*
608: private class SortableHeaderRenderer extends JButton
609: implements TableCellRenderer {
610:
611: public SortableHeaderRenderer() {
612: setMargin(btnMargin);
613: }
614:
615: public Component getTableCellRendererComponent(JTable table,
616: Object value,
617: boolean isSelected,
618: boolean hasFocus,
619: int row,
620: int column) {
621:
622: String label = value.toString();
623: setText(label);
624: setToolTipText(label);
625:
626: setHorizontalTextPosition(SwingConstants.LEFT);
627: int modelColumn = table.convertColumnIndexToModel(column);
628: setIcon(getHeaderRendererIcon(modelColumn, getFont().getSize()));
629:
630: return this;
631:
632: }
633:
634: } // class SortableHeaderRenderer
635: */
636: private static class Directive {
637: private int column;
638: private int direction;
639:
640: public Directive(int column, int direction) {
641: this.column = column;
642: this.direction = direction;
643: }
644: }
645: }
|