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