001: //
002: // The Swing JTable tutorial includes the TableSorter class in this file saying
003: // "You can use TableSorter as-is to provide sorting functionality...". I've
004: // added the sample code license text I found on the tutorial site. I also
005: // added the trivial viewIndex() method. -- ab
006: //
007: // Useful links:
008: //
009: // "How to Use Tables":
010: // http://java.sun.com/docs/books/tutorial/uiswing/components/table.html
011: //
012: // TableSorter.java:
013: // http://java.sun.com/docs/books/tutorial/uiswing/components/example-1dot4/TableSorter.java
014: //
015: // "Building GUIs with JFC/Swing APIs Code Samples":
016: // http://java.sun.com/developer/codesamples/swing.html
017: //
018: // License pointed at by "Building GUIs..." document.
019: // http://developers.sun.com/license/berkeley_license.html
020: //
021:
022: /*/////////////////////////////////////////////////////////////////////
023:
024: Copyright 1994-2006 Sun Microsystems, Inc. All Rights Reserved.
025:
026: Redistribution and use in source and binary forms, with or without
027: modification, are permitted provided that the following conditions are met:
028:
029:
030: * Redistribution of source code must retain the above copyright notice,
031: this list of conditions and the following disclaimer.
032:
033: * Redistribution in binary form must reproduce the above copyright notice,
034: this list of conditions and the following disclaimer in the documentation
035: and/or other materials provided with the distribution.
036:
037:
038: Neither the name of Sun Microsystems, Inc. or the names of contributors may be
039: used to endorse or promote products derived from this software without specific
040: prior written permission.
041:
042: This software is provided "AS IS," without a warranty of any kind. ALL EXPRESS
043: OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES, INCLUDING ANY IMPLIED
044: WARRANTY OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE OR
045: NON-INFRINGEMENT, ARE HEREBY EXCLUDED. SUN MICROSYSTEMS, INC. ("SUN") AND ITS
046: LICENSORS SHALL NOT BE LIABLE FOR ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT
047: OF USING, MODIFYING OR DISTRIBUTING THIS SOFTWARE OR ITS DERIVATIVES. IN NO
048: EVENT WILL SUN OR ITS LICENSORS BE LIABLE FOR ANY LOST REVENUE, PROFIT OR DATA,
049: OR FOR DIRECT, INDIRECT, SPECIAL, CONSEQUENTIAL, INCIDENTAL OR PUNITIVE
050: DAMAGES, HOWEVER CAUSED AND REGARDLESS OF THE THEORY OF LIABILITY, ARISING OUT
051: OF THE USE OF OR INABILITY TO USE THIS SOFTWARE, EVEN IF SUN HAS BEEN ADVISED
052: OF THE POSSIBILITY OF SUCH DAMAGES.
053:
054: You acknowledge that this software is not designed, licensed or intended for
055: use in the design, construction, operation or maintenance of any nuclear
056: facility.
057:
058: /////////////////////////////////////////////////////////////////////*/
059:
060: package com.tivo.jipviewer;
061:
062: import java.awt.*;
063: import java.awt.event.*;
064: import java.util.*;
065: import java.util.List;
066:
067: import javax.swing.*;
068: import javax.swing.event.TableModelEvent;
069: import javax.swing.event.TableModelListener;
070: import javax.swing.table.*;
071:
072: /**
073: * TableSorter is a decorator for TableModels; adding sorting
074: * functionality to a supplied TableModel. TableSorter does
075: * not store or copy the data in its TableModel; instead it maintains
076: * a map from the row indexes of the view to the row indexes of the
077: * model. As requests are made of the sorter (like getValueAt(row, col))
078: * they are passed to the underlying model after the row numbers
079: * have been translated via the internal mapping array. This way,
080: * the TableSorter appears to hold another copy of the table
081: * with the rows in a different order.
082: * <p/>
083: * TableSorter registers itself as a listener to the underlying model,
084: * just as the JTable itself would. Events recieved from the model
085: * are examined, sometimes manipulated (typically widened), and then
086: * passed on to the TableSorter's listeners (typically the JTable).
087: * If a change to the model has invalidated the order of TableSorter's
088: * rows, a note of this is made and the sorter will resort the
089: * rows the next time a value is requested.
090: * <p/>
091: * When the tableHeader property is set, either by using the
092: * setTableHeader() method or the two argument constructor, the
093: * table header may be used as a complete UI for TableSorter.
094: * The default renderer of the tableHeader is decorated with a renderer
095: * that indicates the sorting status of each column. In addition,
096: * a mouse listener is installed with the following behavior:
097: * <ul>
098: * <li>
099: * Mouse-click: Clears the sorting status of all other columns
100: * and advances the sorting status of that column through three
101: * values: {NOT_SORTED, ASCENDING, DESCENDING} (then back to
102: * NOT_SORTED again).
103: * <li>
104: * SHIFT-mouse-click: Clears the sorting status of all other columns
105: * and cycles the sorting status of the column through the same
106: * three values, in the opposite order: {NOT_SORTED, DESCENDING, ASCENDING}.
107: * <li>
108: * CONTROL-mouse-click and CONTROL-SHIFT-mouse-click: as above except
109: * that the changes to the column do not cancel the statuses of columns
110: * that are already sorting - giving a way to initiate a compound
111: * sort.
112: * </ul>
113: * <p/>
114: * This is a long overdue rewrite of a class of the same name that
115: * first appeared in the swing table demos in 1997.
116: *
117: * @author Philip Milne
118: * @author Brendon McLean
119: * @author Dan van Enckevort
120: * @author Parwinder Sekhon
121: * @version 2.0 02/27/04
122: */
123:
124: public class TableSorter extends AbstractTableModel {
125: protected TableModel tableModel;
126:
127: public static final int DESCENDING = -1;
128: public static final int NOT_SORTED = 0;
129: public static final int ASCENDING = 1;
130:
131: private static Directive EMPTY_DIRECTIVE = new Directive(-1,
132: NOT_SORTED);
133:
134: public static final Comparator COMPARABLE_COMAPRATOR = new Comparator() {
135: public int compare(Object o1, Object o2) {
136: return ((Comparable) o1).compareTo(o2);
137: }
138: };
139: public static final Comparator LEXICAL_COMPARATOR = new Comparator() {
140: public int compare(Object o1, Object o2) {
141: return o1.toString().compareTo(o2.toString());
142: }
143: };
144:
145: private Row[] viewToModel;
146: private int[] modelToView;
147:
148: private JTableHeader tableHeader;
149: private MouseListener mouseListener;
150: private TableModelListener tableModelListener;
151: private Map columnComparators = new HashMap();
152: private List sortingColumns = new ArrayList();
153:
154: public TableSorter() {
155: this .mouseListener = new MouseHandler();
156: this .tableModelListener = new TableModelHandler();
157: }
158:
159: public TableSorter(TableModel tableModel) {
160: this ();
161: setTableModel(tableModel);
162: }
163:
164: public TableSorter(TableModel tableModel, JTableHeader tableHeader) {
165: this ();
166: setTableHeader(tableHeader);
167: setTableModel(tableModel);
168: }
169:
170: private void clearSortingState() {
171: viewToModel = null;
172: modelToView = null;
173: }
174:
175: public TableModel getTableModel() {
176: return tableModel;
177: }
178:
179: public void setTableModel(TableModel tableModel) {
180: if (this .tableModel != null) {
181: this .tableModel
182: .removeTableModelListener(tableModelListener);
183: }
184:
185: this .tableModel = tableModel;
186: if (this .tableModel != null) {
187: this .tableModel.addTableModelListener(tableModelListener);
188: }
189:
190: clearSortingState();
191: fireTableStructureChanged();
192: }
193:
194: public JTableHeader getTableHeader() {
195: return tableHeader;
196: }
197:
198: public void setTableHeader(JTableHeader tableHeader) {
199: if (this .tableHeader != null) {
200: this .tableHeader.removeMouseListener(mouseListener);
201: TableCellRenderer defaultRenderer = this .tableHeader
202: .getDefaultRenderer();
203: if (defaultRenderer instanceof SortableHeaderRenderer) {
204: this .tableHeader
205: .setDefaultRenderer(((SortableHeaderRenderer) defaultRenderer).tableCellRenderer);
206: }
207: }
208: this .tableHeader = tableHeader;
209: if (this .tableHeader != null) {
210: this .tableHeader.addMouseListener(mouseListener);
211: this .tableHeader
212: .setDefaultRenderer(new SortableHeaderRenderer(
213: this .tableHeader.getDefaultRenderer()));
214: }
215: }
216:
217: public boolean isSorting() {
218: return sortingColumns.size() != 0;
219: }
220:
221: private Directive getDirective(int column) {
222: for (int i = 0; i < sortingColumns.size(); i++) {
223: Directive directive = (Directive) sortingColumns.get(i);
224: if (directive.column == column) {
225: return directive;
226: }
227: }
228: return EMPTY_DIRECTIVE;
229: }
230:
231: public int getSortingStatus(int column) {
232: return getDirective(column).direction;
233: }
234:
235: private void sortingStatusChanged() {
236: clearSortingState();
237: fireTableDataChanged();
238: if (tableHeader != null) {
239: tableHeader.repaint();
240: }
241: }
242:
243: public void setSortingStatus(int column, int status) {
244: Directive directive = getDirective(column);
245: if (directive != EMPTY_DIRECTIVE) {
246: sortingColumns.remove(directive);
247: }
248: if (status != NOT_SORTED) {
249: sortingColumns.add(new Directive(column, status));
250: }
251: sortingStatusChanged();
252: }
253:
254: protected Icon getHeaderRendererIcon(int column, int size) {
255: Directive directive = getDirective(column);
256: if (directive == EMPTY_DIRECTIVE) {
257: return null;
258: }
259: return new Arrow(directive.direction == DESCENDING, size,
260: sortingColumns.indexOf(directive));
261: }
262:
263: private void cancelSorting() {
264: sortingColumns.clear();
265: sortingStatusChanged();
266: }
267:
268: public void setColumnComparator(Class type, Comparator comparator) {
269: if (comparator == null) {
270: columnComparators.remove(type);
271: } else {
272: columnComparators.put(type, comparator);
273: }
274: }
275:
276: protected Comparator getComparator(int column) {
277: Class columnType = tableModel.getColumnClass(column);
278: Comparator comparator = (Comparator) columnComparators
279: .get(columnType);
280: if (comparator != null) {
281: return comparator;
282: }
283: if (Comparable.class.isAssignableFrom(columnType)) {
284: return COMPARABLE_COMAPRATOR;
285: }
286: return LEXICAL_COMPARATOR;
287: }
288:
289: private Row[] getViewToModel() {
290: if (viewToModel == null) {
291: int tableModelRowCount = tableModel.getRowCount();
292: viewToModel = new Row[tableModelRowCount];
293: for (int row = 0; row < tableModelRowCount; row++) {
294: viewToModel[row] = new Row(row);
295: }
296:
297: if (isSorting()) {
298: Arrays.sort(viewToModel);
299: }
300: }
301: return viewToModel;
302: }
303:
304: public int modelIndex(int viewIndex) {
305: return getViewToModel()[viewIndex].modelIndex;
306: }
307:
308: public int viewIndex(int modelIndex) {
309: return getModelToView()[modelIndex];
310: }
311:
312: private int[] getModelToView() {
313: if (modelToView == null) {
314: int n = getViewToModel().length;
315: modelToView = new int[n];
316: for (int i = 0; i < n; i++) {
317: modelToView[modelIndex(i)] = i;
318: }
319: }
320: return modelToView;
321: }
322:
323: // TableModel interface methods
324:
325: public int getRowCount() {
326: return (tableModel == null) ? 0 : tableModel.getRowCount();
327: }
328:
329: public int getColumnCount() {
330: return (tableModel == null) ? 0 : tableModel.getColumnCount();
331: }
332:
333: public String getColumnName(int column) {
334: return tableModel.getColumnName(column);
335: }
336:
337: public Class getColumnClass(int column) {
338: return tableModel.getColumnClass(column);
339: }
340:
341: public boolean isCellEditable(int row, int column) {
342: return tableModel.isCellEditable(modelIndex(row), column);
343: }
344:
345: public Object getValueAt(int row, int column) {
346: return tableModel.getValueAt(modelIndex(row), column);
347: }
348:
349: public void setValueAt(Object aValue, int row, int column) {
350: tableModel.setValueAt(aValue, modelIndex(row), column);
351: }
352:
353: // Helper classes
354:
355: private class Row implements Comparable {
356: private int modelIndex;
357:
358: public Row(int index) {
359: this .modelIndex = index;
360: }
361:
362: public int compareTo(Object o) {
363: int row1 = modelIndex;
364: int row2 = ((Row) o).modelIndex;
365:
366: for (Iterator it = sortingColumns.iterator(); it.hasNext();) {
367: Directive directive = (Directive) it.next();
368: int column = directive.column;
369: Object o1 = tableModel.getValueAt(row1, column);
370: Object o2 = tableModel.getValueAt(row2, column);
371:
372: int comparison = 0;
373: // Define null less than everything, except null.
374: if (o1 == null && o2 == null) {
375: comparison = 0;
376: } else if (o1 == null) {
377: comparison = -1;
378: } else if (o2 == null) {
379: comparison = 1;
380: } else {
381: comparison = getComparator(column).compare(o1, o2);
382: }
383: if (comparison != 0) {
384: return directive.direction == DESCENDING ? -comparison
385: : comparison;
386: }
387: }
388: return 0;
389: }
390: }
391:
392: private class TableModelHandler implements TableModelListener {
393: public void tableChanged(TableModelEvent e) {
394: // If we're not sorting by anything, just pass the event along.
395: if (!isSorting()) {
396: clearSortingState();
397: fireTableChanged(e);
398: return;
399: }
400:
401: // If the table structure has changed, cancel the sorting; the
402: // sorting columns may have been either moved or deleted from
403: // the model.
404: if (e.getFirstRow() == TableModelEvent.HEADER_ROW) {
405: cancelSorting();
406: fireTableChanged(e);
407: return;
408: }
409:
410: // We can map a cell event through to the view without widening
411: // when the following conditions apply:
412: //
413: // a) all the changes are on one row (e.getFirstRow() == e.getLastRow()) and,
414: // b) all the changes are in one column (column != TableModelEvent.ALL_COLUMNS) and,
415: // c) we are not sorting on that column (getSortingStatus(column) == NOT_SORTED) and,
416: // d) a reverse lookup will not trigger a sort (modelToView != null)
417: //
418: // Note: INSERT and DELETE events fail this test as they have column == ALL_COLUMNS.
419: //
420: // The last check, for (modelToView != null) is to see if modelToView
421: // is already allocated. If we don't do this check; sorting can become
422: // a performance bottleneck for applications where cells
423: // change rapidly in different parts of the table. If cells
424: // change alternately in the sorting column and then outside of
425: // it this class can end up re-sorting on alternate cell updates -
426: // which can be a performance problem for large tables. The last
427: // clause avoids this problem.
428: int column = e.getColumn();
429: if (e.getFirstRow() == e.getLastRow()
430: && column != TableModelEvent.ALL_COLUMNS
431: && getSortingStatus(column) == NOT_SORTED
432: && modelToView != null) {
433: int viewIndex = getModelToView()[e.getFirstRow()];
434: fireTableChanged(new TableModelEvent(TableSorter.this ,
435: viewIndex, viewIndex, column, e.getType()));
436: return;
437: }
438:
439: // Something has happened to the data that may have invalidated the row order.
440: clearSortingState();
441: fireTableDataChanged();
442: return;
443: }
444: }
445:
446: private class MouseHandler extends MouseAdapter {
447: public void mouseClicked(MouseEvent e) {
448: JTableHeader h = (JTableHeader) e.getSource();
449: TableColumnModel columnModel = h.getColumnModel();
450: int viewColumn = columnModel.getColumnIndexAtX(e.getX());
451: int column = columnModel.getColumn(viewColumn)
452: .getModelIndex();
453: if (column != -1) {
454: int status = getSortingStatus(column);
455: if (!e.isControlDown()) {
456: cancelSorting();
457: }
458: // Cycle the sorting states through {NOT_SORTED, ASCENDING, DESCENDING} or
459: // {NOT_SORTED, DESCENDING, ASCENDING} depending on whether shift is pressed.
460: status = status + (e.isShiftDown() ? -1 : 1);
461: status = (status + 4) % 3 - 1; // signed mod, returning {-1, 0, 1}
462: setSortingStatus(column, status);
463: }
464: }
465: }
466:
467: private static class Arrow implements Icon {
468: private boolean descending;
469: private int size;
470: private int priority;
471:
472: public Arrow(boolean descending, int size, int priority) {
473: this .descending = descending;
474: this .size = size;
475: this .priority = priority;
476: }
477:
478: public void paintIcon(Component c, Graphics g, int x, int y) {
479: Color color = c == null ? Color.GRAY : c.getBackground();
480: // In a compound sort, make each succesive triangle 20%
481: // smaller than the previous one.
482: int dx = (int) (size / 2 * Math.pow(0.8, priority));
483: int dy = descending ? dx : -dx;
484: // Align icon (roughly) with font baseline.
485: y = y + 5 * size / 6 + (descending ? -dy : 0);
486: int shift = descending ? 1 : -1;
487: g.translate(x, y);
488:
489: // Right diagonal.
490: g.setColor(color.darker());
491: g.drawLine(dx / 2, dy, 0, 0);
492: g.drawLine(dx / 2, dy + shift, 0, shift);
493:
494: // Left diagonal.
495: g.setColor(color.brighter());
496: g.drawLine(dx / 2, dy, dx, 0);
497: g.drawLine(dx / 2, dy + shift, dx, shift);
498:
499: // Horizontal line.
500: if (descending) {
501: g.setColor(color.darker().darker());
502: } else {
503: g.setColor(color.brighter().brighter());
504: }
505: g.drawLine(dx, 0, 0, 0);
506:
507: g.setColor(color);
508: g.translate(-x, -y);
509: }
510:
511: public int getIconWidth() {
512: return size;
513: }
514:
515: public int getIconHeight() {
516: return size;
517: }
518: }
519:
520: private class SortableHeaderRenderer implements TableCellRenderer {
521: private TableCellRenderer tableCellRenderer;
522:
523: public SortableHeaderRenderer(
524: TableCellRenderer tableCellRenderer) {
525: this .tableCellRenderer = tableCellRenderer;
526: }
527:
528: public Component getTableCellRendererComponent(JTable table,
529: Object value, boolean isSelected, boolean hasFocus,
530: int row, int column) {
531: Component c = tableCellRenderer
532: .getTableCellRendererComponent(table, value,
533: isSelected, hasFocus, row, column);
534: if (c instanceof JLabel) {
535: JLabel l = (JLabel) c;
536: l.setHorizontalTextPosition(JLabel.LEFT);
537: int modelColumn = table
538: .convertColumnIndexToModel(column);
539: l.setIcon(getHeaderRendererIcon(modelColumn, l
540: .getFont().getSize()));
541: }
542: return c;
543: }
544: }
545:
546: private static class Directive {
547: private int column;
548: private int direction;
549:
550: public Directive(int column, int direction) {
551: this.column = column;
552: this.direction = direction;
553: }
554: }
555: }
|