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