001: package org.dbbrowser.util;
002:
003: import java.awt.*;
004: import java.awt.event.*;
005: import java.util.*;
006: import java.util.List;
007:
008: import javax.swing.*;
009: import javax.swing.event.TableModelEvent;
010: import javax.swing.event.TableModelListener;
011: import javax.swing.table.*;
012:
013: /**
014: * TableSorter is a decorator for TableModels; adding sorting
015: * functionality to a supplied TableModel. TableSorter does
016: * not store or copy the data in its TableModel; instead it maintains
017: * a map from the row indexes of the view to the row indexes of the
018: * model. As requests are made of the sorter (like getValueAt(row, col))
019: * they are passed to the underlying model after the row numbers
020: * have been translated via the internal mapping array. This way,
021: * the TableSorter appears to hold another copy of the table
022: * with the rows in a different order.
023: * <p/>
024: * TableSorter registers itself as a listener to the underlying model,
025: * just as the JTable itself would. Events recieved from the model
026: * are examined, sometimes manipulated (typically widened), and then
027: * passed on to the TableSorter's listeners (typically the JTable).
028: * If a change to the model has invalidated the order of TableSorter's
029: * rows, a note of this is made and the sorter will resort the
030: * rows the next time a value is requested.
031: * <p/>
032: * When the tableHeader property is set, either by using the
033: * setTableHeader() method or the two argument constructor, the
034: * table header may be used as a complete UI for TableSorter.
035: * The default renderer of the tableHeader is decorated with a renderer
036: * that indicates the sorting status of each column. In addition,
037: * a mouse listener is installed with the following behavior:
038: * <ul>
039: * <li>
040: * Mouse-click: Clears the sorting status of all other columns
041: * and advances the sorting status of that column through three
042: * values: {NOT_SORTED, ASCENDING, DESCENDING} (then back to
043: * NOT_SORTED again).
044: * <li>
045: * SHIFT-mouse-click: Clears the sorting status of all other columns
046: * and cycles the sorting status of the column through the same
047: * three values, in the opposite order: {NOT_SORTED, DESCENDING, ASCENDING}.
048: * <li>
049: * CONTROL-mouse-click and CONTROL-SHIFT-mouse-click: as above except
050: * that the changes to the column do not cancel the statuses of columns
051: * that are already sorting - giving a way to initiate a compound
052: * sort.
053: * </ul>
054: * <p/>
055: * This is a long overdue rewrite of a class of the same name that
056: * first appeared in the swing table demos in 1997.
057: *
058: * @author Philip Milne
059: * @author Brendon McLean
060: * @author Dan van Enckevort
061: * @author Parwinder Sekhon
062: * @version 2.0 02/27/04
063: */
064:
065: public class TableSorter extends AbstractTableModel {
066: protected TableModel tableModel;
067:
068: public static final int DESCENDING = -1;
069: public static final int NOT_SORTED = 0;
070: public static final int ASCENDING = 1;
071:
072: private static Directive EMPTY_DIRECTIVE = new Directive(-1,
073: NOT_SORTED);
074:
075: public static final Comparator COMPARABLE_COMAPRATOR = new Comparator() {
076: public int compare(Object o1, Object o2) {
077: return ((Comparable) o1).compareTo(o2);
078: }
079: };
080: public static final Comparator LEXICAL_COMPARATOR = new Comparator() {
081: public int compare(Object o1, Object o2) {
082: return o1.toString().compareTo(o2.toString());
083: }
084: };
085:
086: private Row[] viewToModel;
087: private int[] modelToView;
088:
089: private JTableHeader tableHeader;
090: private MouseListener mouseListener;
091: private TableModelListener tableModelListener;
092: private Map columnComparators = new HashMap();
093: private List sortingColumns = new ArrayList();
094:
095: public TableSorter() {
096: this .mouseListener = new MouseHandler();
097: this .tableModelListener = new TableModelHandler();
098: }
099:
100: public TableSorter(TableModel tableModel) {
101: this ();
102: setTableModel(tableModel);
103: }
104:
105: public TableSorter(TableModel tableModel, JTableHeader tableHeader) {
106: this ();
107: setTableHeader(tableHeader);
108: setTableModel(tableModel);
109: }
110:
111: private void clearSortingState() {
112: viewToModel = null;
113: modelToView = null;
114: }
115:
116: public TableModel getTableModel() {
117: return tableModel;
118: }
119:
120: public void setTableModel(TableModel tableModel) {
121: if (this .tableModel != null) {
122: this .tableModel
123: .removeTableModelListener(tableModelListener);
124: }
125:
126: this .tableModel = tableModel;
127: if (this .tableModel != null) {
128: this .tableModel.addTableModelListener(tableModelListener);
129: }
130:
131: clearSortingState();
132: fireTableStructureChanged();
133: }
134:
135: public JTableHeader getTableHeader() {
136: return tableHeader;
137: }
138:
139: public void setTableHeader(JTableHeader tableHeader) {
140: if (this .tableHeader != null) {
141: this .tableHeader.removeMouseListener(mouseListener);
142: TableCellRenderer defaultRenderer = this .tableHeader
143: .getDefaultRenderer();
144: if (defaultRenderer instanceof SortableHeaderRenderer) {
145: this .tableHeader
146: .setDefaultRenderer(((SortableHeaderRenderer) defaultRenderer).tableCellRenderer);
147: }
148: }
149: this .tableHeader = tableHeader;
150: if (this .tableHeader != null) {
151: this .tableHeader.addMouseListener(mouseListener);
152: this .tableHeader
153: .setDefaultRenderer(new SortableHeaderRenderer(
154: this .tableHeader.getDefaultRenderer()));
155: }
156: }
157:
158: public boolean isSorting() {
159: return sortingColumns.size() != 0;
160: }
161:
162: private Directive getDirective(int column) {
163: for (int i = 0; i < sortingColumns.size(); i++) {
164: Directive directive = (Directive) sortingColumns.get(i);
165: if (directive.column == column) {
166: return directive;
167: }
168: }
169: return EMPTY_DIRECTIVE;
170: }
171:
172: public int getSortingStatus(int column) {
173: return getDirective(column).direction;
174: }
175:
176: private void sortingStatusChanged() {
177: clearSortingState();
178: fireTableDataChanged();
179: if (tableHeader != null) {
180: tableHeader.repaint();
181: }
182: }
183:
184: public void setSortingStatus(int column, int status) {
185: Directive directive = getDirective(column);
186: if (directive != EMPTY_DIRECTIVE) {
187: sortingColumns.remove(directive);
188: }
189: if (status != NOT_SORTED) {
190: sortingColumns.add(new Directive(column, status));
191: }
192: sortingStatusChanged();
193: }
194:
195: protected Icon getHeaderRendererIcon(int column, int size) {
196: Directive directive = getDirective(column);
197: if (directive == EMPTY_DIRECTIVE) {
198: return null;
199: }
200: return new Arrow(directive.direction == DESCENDING, size,
201: sortingColumns.indexOf(directive));
202: }
203:
204: private void cancelSorting() {
205: sortingColumns.clear();
206: sortingStatusChanged();
207: }
208:
209: public void setColumnComparator(Class type, Comparator comparator) {
210: if (comparator == null) {
211: columnComparators.remove(type);
212: } else {
213: columnComparators.put(type, comparator);
214: }
215: }
216:
217: protected Comparator getComparator(int column) {
218: Class columnType = tableModel.getColumnClass(column);
219: Comparator comparator = (Comparator) columnComparators
220: .get(columnType);
221: if (comparator != null) {
222: return comparator;
223: }
224: if (Comparable.class.isAssignableFrom(columnType)) {
225: return COMPARABLE_COMAPRATOR;
226: }
227: return LEXICAL_COMPARATOR;
228: }
229:
230: private Row[] getViewToModel() {
231: if (viewToModel == null) {
232: int tableModelRowCount = tableModel.getRowCount();
233: viewToModel = new Row[tableModelRowCount];
234: for (int row = 0; row < tableModelRowCount; row++) {
235: viewToModel[row] = new Row(row);
236: }
237:
238: if (isSorting()) {
239: Arrays.sort(viewToModel);
240: }
241: }
242: return viewToModel;
243: }
244:
245: public int modelIndex(int viewIndex) {
246: return getViewToModel()[viewIndex].modelIndex;
247: }
248:
249: private int[] getModelToView() {
250: if (modelToView == null) {
251: int n = getViewToModel().length;
252: modelToView = new int[n];
253: for (int i = 0; i < n; i++) {
254: modelToView[modelIndex(i)] = i;
255: }
256: }
257: return modelToView;
258: }
259:
260: // TableModel interface methods
261:
262: public int getRowCount() {
263: return (tableModel == null) ? 0 : tableModel.getRowCount();
264: }
265:
266: public int getColumnCount() {
267: return (tableModel == null) ? 0 : tableModel.getColumnCount();
268: }
269:
270: public String getColumnName(int column) {
271: return tableModel.getColumnName(column);
272: }
273:
274: public Class getColumnClass(int column) {
275: return tableModel.getColumnClass(column);
276: }
277:
278: public boolean isCellEditable(int row, int column) {
279: return tableModel.isCellEditable(modelIndex(row), column);
280: }
281:
282: public Object getValueAt(int row, int column) {
283: return tableModel.getValueAt(modelIndex(row), column);
284: }
285:
286: public void setValueAt(Object aValue, int row, int column) {
287: tableModel.setValueAt(aValue, modelIndex(row), column);
288: }
289:
290: // Helper classes
291:
292: private class Row implements Comparable {
293: private int modelIndex;
294:
295: public Row(int index) {
296: this .modelIndex = index;
297: }
298:
299: public int compareTo(Object o) {
300: int row1 = modelIndex;
301: int row2 = ((Row) o).modelIndex;
302:
303: for (Iterator it = sortingColumns.iterator(); it.hasNext();) {
304: Directive directive = (Directive) it.next();
305: int column = directive.column;
306: Object o1 = tableModel.getValueAt(row1, column);
307: Object o2 = tableModel.getValueAt(row2, column);
308:
309: int comparison = 0;
310: // Define null less than everything, except null.
311: if (o1 == null && o2 == null) {
312: comparison = 0;
313: } else if (o1 == null) {
314: comparison = -1;
315: } else if (o2 == null) {
316: comparison = 1;
317: } else {
318: comparison = getComparator(column).compare(o1, o2);
319: }
320: if (comparison != 0) {
321: return directive.direction == DESCENDING ? -comparison
322: : comparison;
323: }
324: }
325: return 0;
326: }
327: }
328:
329: private class TableModelHandler implements TableModelListener {
330: public void tableChanged(TableModelEvent e) {
331: // If we're not sorting by anything, just pass the event along.
332: if (!isSorting()) {
333: clearSortingState();
334: fireTableChanged(e);
335: return;
336: }
337:
338: // If the table structure has changed, cancel the sorting; the
339: // sorting columns may have been either moved or deleted from
340: // the model.
341: if (e.getFirstRow() == TableModelEvent.HEADER_ROW) {
342: cancelSorting();
343: fireTableChanged(e);
344: return;
345: }
346:
347: // We can map a cell event through to the view without widening
348: // when the following conditions apply:
349: //
350: // a) all the changes are on one row (e.getFirstRow() == e.getLastRow()) and,
351: // b) all the changes are in one column (column != TableModelEvent.ALL_COLUMNS) and,
352: // c) we are not sorting on that column (getSortingStatus(column) == NOT_SORTED) and,
353: // d) a reverse lookup will not trigger a sort (modelToView != null)
354: //
355: // Note: INSERT and DELETE events fail this test as they have column == ALL_COLUMNS.
356: //
357: // The last check, for (modelToView != null) is to see if modelToView
358: // is already allocated. If we don't do this check; sorting can become
359: // a performance bottleneck for applications where cells
360: // change rapidly in different parts of the table. If cells
361: // change alternately in the sorting column and then outside of
362: // it this class can end up re-sorting on alternate cell updates -
363: // which can be a performance problem for large tables. The last
364: // clause avoids this problem.
365: int column = e.getColumn();
366: if (e.getFirstRow() == e.getLastRow()
367: && column != TableModelEvent.ALL_COLUMNS
368: && getSortingStatus(column) == NOT_SORTED
369: && modelToView != null) {
370: int viewIndex = getModelToView()[e.getFirstRow()];
371: fireTableChanged(new TableModelEvent(TableSorter.this ,
372: viewIndex, viewIndex, column, e.getType()));
373: return;
374: }
375:
376: // Something has happened to the data that may have invalidated the row order.
377: clearSortingState();
378: fireTableDataChanged();
379: return;
380: }
381: }
382:
383: private class MouseHandler extends MouseAdapter {
384: public void mouseClicked(MouseEvent e) {
385: JTableHeader h = (JTableHeader) e.getSource();
386: TableColumnModel columnModel = h.getColumnModel();
387: int viewColumn = columnModel.getColumnIndexAtX(e.getX());
388: int column = columnModel.getColumn(viewColumn)
389: .getModelIndex();
390: if (column != -1) {
391: int status = getSortingStatus(column);
392: if (!e.isControlDown()) {
393: cancelSorting();
394: }
395: // Cycle the sorting states through {NOT_SORTED, ASCENDING, DESCENDING} or
396: // {NOT_SORTED, DESCENDING, ASCENDING} depending on whether shift is pressed.
397: status = status + (e.isShiftDown() ? -1 : 1);
398: status = (status + 4) % 3 - 1; // signed mod, returning {-1, 0, 1}
399: setSortingStatus(column, status);
400: }
401: }
402: }
403:
404: private static class Arrow implements Icon {
405: private boolean descending;
406: private int size;
407: private int priority;
408:
409: public Arrow(boolean descending, int size, int priority) {
410: this .descending = descending;
411: this .size = size;
412: this .priority = priority;
413: }
414:
415: public void paintIcon(Component c, Graphics g, int x, int y) {
416: Color color = c == null ? Color.GRAY : c.getBackground();
417: // In a compound sort, make each succesive triangle 20%
418: // smaller than the previous one.
419: int dx = (int) (size / 2 * Math.pow(0.8, priority));
420: int dy = descending ? dx : -dx;
421: // Align icon (roughly) with font baseline.
422: y = y + 5 * size / 6 + (descending ? -dy : 0);
423: int shift = descending ? 1 : -1;
424: g.translate(x, y);
425:
426: // Right diagonal.
427: g.setColor(color.darker());
428: g.drawLine(dx / 2, dy, 0, 0);
429: g.drawLine(dx / 2, dy + shift, 0, shift);
430:
431: // Left diagonal.
432: g.setColor(color.brighter());
433: g.drawLine(dx / 2, dy, dx, 0);
434: g.drawLine(dx / 2, dy + shift, dx, shift);
435:
436: // Horizontal line.
437: if (descending) {
438: g.setColor(color.darker().darker());
439: } else {
440: g.setColor(color.brighter().brighter());
441: }
442: g.drawLine(dx, 0, 0, 0);
443:
444: g.setColor(color);
445: g.translate(-x, -y);
446: }
447:
448: public int getIconWidth() {
449: return size;
450: }
451:
452: public int getIconHeight() {
453: return size;
454: }
455: }
456:
457: private class SortableHeaderRenderer implements TableCellRenderer {
458: private TableCellRenderer tableCellRenderer;
459:
460: public SortableHeaderRenderer(
461: TableCellRenderer tableCellRenderer) {
462: this .tableCellRenderer = tableCellRenderer;
463: }
464:
465: public Component getTableCellRendererComponent(JTable table,
466: Object value, boolean isSelected, boolean hasFocus,
467: int row, int column) {
468: Component c = tableCellRenderer
469: .getTableCellRendererComponent(table, value,
470: isSelected, hasFocus, row, column);
471: if (c instanceof JLabel) {
472: JLabel l = (JLabel) c;
473: l.setHorizontalTextPosition(JLabel.LEFT);
474: int modelColumn = table
475: .convertColumnIndexToModel(column);
476: l.setIcon(getHeaderRendererIcon(modelColumn, l
477: .getFont().getSize()));
478: }
479: return c;
480: }
481: }
482:
483: private static class Directive {
484: private int column;
485: private int direction;
486:
487: public Directive(int column, int direction) {
488: this.column = column;
489: this.direction = direction;
490: }
491: }
492: }
|