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