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