2
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
4
* Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
6
* The contents of this file are subject to the terms of either the GNU
7
* General Public License Version 2 only ("GPL") or the Common
8
* Development and Distribution License("CDDL") (collectively, the
9
* "License"). You may not use this file except in compliance with the
10
* License. You can obtain a copy of the License at
11
* http://www.netbeans.org/cddl-gplv2.html
12
* or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
13
* specific language governing permissions and limitations under the
14
* License. When distributing the software, include this License Header
15
* Notice in each file and include the License file at
16
* nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this
17
* particular file as subject to the "Classpath" exception as provided
18
* by Sun in the GPL Version 2 section of the License file that
19
* accompanied this code. If applicable, add the following below the
20
* License Header, with the fields enclosed by brackets [] replaced by
21
* your own identifying information:
22
* "Portions Copyrighted [year] [name of copyright owner]"
26
* The Original Software is NetBeans. The Initial Developer of the Original
27
* Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun
28
* Microsystems, Inc. All Rights Reserved.
30
* If you wish your version of this file to be governed by only the CDDL
31
* or only the GPL Version 2, indicate your decision by adding
32
* "[Contributor] elects to include this software in this distribution
33
* under the [CDDL or GPL Version 2] license." If you do not indicate a
34
* single choice of license, a recipient has the option to distribute
35
* your version of this file under either the CDDL, the GPL Version 2 or
36
* to extend the choice of license to its licensees as provided above.
37
* However, if you add GPL Version 2 code and therefore, elected the GPL
38
* Version 2 license, then the option applies only if the new code is
39
* made subject to such option by the copyright holder.
42
package org.netbeans.modules.versioning.util;
45
import java.awt.event.*;
47
import java.util.List;
50
import javax.swing.event.TableModelEvent;
51
import javax.swing.event.TableModelListener;
52
import javax.swing.table.*;
55
* TableSorter is a decorator for TableModels; adding sorting
56
* functionality to a supplied TableModel. TableSorter does
57
* not store or copy the data in its TableModel; instead it maintains
58
* a map from the row indexes of the view to the row indexes of the
59
* model. As requests are made of the sorter (like getValueAt(row, col))
60
* they are passed to the underlying model after the row numbers
61
* have been translated via the internal mapping array. This way,
62
* the TableSorter appears to hold another copy of the table
63
* with the rows in a different order.
65
* TableSorter registers itself as a listener to the underlying model,
66
* just as the JTable itself would. Events recieved from the model
67
* are examined, sometimes manipulated (typically widened), and then
68
* passed on to the TableSorter's listeners (typically the JTable).
69
* If a change to the model has invalidated the order of TableSorter's
70
* rows, a note of this is made and the sorter will resort the
71
* rows the next time a value is requested.
73
* When the tableHeader property is set, either by using the
74
* setTableHeader() method or the two argument constructor, the
75
* table header may be used as a complete UI for TableSorter.
76
* The default renderer of the tableHeader is decorated with a renderer
77
* that indicates the sorting status of each column. In addition,
78
* a mouse listener is installed with the following behavior:
81
* Mouse-click: Clears the sorting status of all other columns
82
* and advances the sorting status of that column through three
83
* values: {NOT_SORTED, ASCENDING, DESCENDING} (then back to
86
* SHIFT-mouse-click: Clears the sorting status of all other columns
87
* and cycles the sorting status of the column through the same
88
* three values, in the opposite order: {NOT_SORTED, DESCENDING, ASCENDING}.
90
* CONTROL-mouse-click and CONTROL-SHIFT-mouse-click: as above except
91
* that the changes to the column do not cancel the statuses of columns
92
* that are already sorting - giving a way to initiate a compound
96
* This is a long overdue rewrite of a class of the same name that
97
* first appeared in the swing table demos in 1997.
99
* @author Philip Milne
100
* @author Brendon McLean
101
* @author Dan van Enckevort
102
* @author Parwinder Sekhon
103
* @version 2.0 02/27/04
106
public final class TableSorter extends AbstractTableModel {
107
protected TableModel tableModel;
109
public static final int DESCENDING = -1;
110
public static final int NOT_SORTED = 0;
111
public static final int ASCENDING = 1;
113
private static Directive EMPTY_DIRECTIVE = new Directive(-1, NOT_SORTED);
115
public static final Comparator COMPARABLE_COMAPRATOR = new Comparator<Comparable>() {
116
public int compare(Comparable o1, Comparable o2) {
117
return o1.compareTo(o2);
120
public static final Comparator LEXICAL_COMPARATOR = new Comparator() {
121
public int compare(Object o1, Object o2) {
122
return o1.toString().compareTo(o2.toString());
126
private final Icon ICON_ASCENDING = new ImageIcon(org.openide.util.Utilities.loadImage("org/netbeans/modules/openide/explorer/columnsSortedAsc.gif", true)); // NOI18N
127
private final Icon ICON_DESCENDING = new ImageIcon(org.openide.util.Utilities.loadImage("org/netbeans/modules/openide/explorer/columnsSortedDesc.gif", true)); // NOI18N
129
private Row[] viewToModel;
130
private int[] modelToView;
132
private JTableHeader tableHeader;
133
private MouseListener mouseListener;
134
private TableModelListener tableModelListener;
135
// key is either Class or Integer
136
private Map<Object, Comparator> columnComparators = new HashMap<Object, Comparator>();
137
private List<Directive> sortingColumns = new ArrayList<Directive>();
139
public TableSorter() {
140
this.mouseListener = new MouseHandler();
141
this.tableModelListener = new TableModelHandler();
144
public TableSorter(TableModel tableModel) {
146
setTableModel(tableModel);
149
public TableSorter(TableModel tableModel, JTableHeader tableHeader) {
151
setTableHeader(tableHeader);
152
setTableModel(tableModel);
155
private void clearSortingState() {
160
public TableModel getTableModel() {
164
public final void setTableModel(TableModel tableModel) {
165
if (this.tableModel != null) {
166
this.tableModel.removeTableModelListener(tableModelListener);
169
this.tableModel = tableModel;
170
if (this.tableModel != null) {
171
this.tableModel.addTableModelListener(tableModelListener);
175
fireTableStructureChanged();
178
public JTableHeader getTableHeader() {
182
public void setTableHeader(JTableHeader tableHeader) {
183
if (this.tableHeader != null) {
184
this.tableHeader.removeMouseListener(mouseListener);
185
TableCellRenderer defaultRenderer = this.tableHeader.getDefaultRenderer();
186
if (defaultRenderer instanceof SortableHeaderRenderer) {
187
this.tableHeader.setDefaultRenderer(((SortableHeaderRenderer) defaultRenderer).tableCellRenderer);
190
this.tableHeader = tableHeader;
191
if (this.tableHeader != null) {
192
this.tableHeader.addMouseListener(mouseListener);
193
this.tableHeader.setDefaultRenderer(
194
new SortableHeaderRenderer(this.tableHeader.getDefaultRenderer()));
198
public boolean isSorting() {
199
return sortingColumns.size() != 0;
202
private Directive getDirective(int column) {
203
for (int i = 0; i < sortingColumns.size(); i++) {
204
Directive directive = sortingColumns.get(i);
205
if (directive.column == column) {
209
return EMPTY_DIRECTIVE;
212
public int getSortingStatus(int column) {
213
return getDirective(column).direction;
216
private void sortingStatusChanged() {
218
fireTableDataChanged();
219
if (tableHeader != null) {
220
tableHeader.repaint();
224
public void setSortingStatus(int column, int status) {
225
Directive directive = getDirective(column);
226
if (directive != EMPTY_DIRECTIVE) {
227
sortingColumns.remove(directive);
229
if (status != NOT_SORTED) {
230
sortingColumns.add(new Directive(column, status));
232
sortingStatusChanged();
235
protected Icon getHeaderRendererIcon(int column, int size) {
236
Directive directive = getDirective(column);
237
if (directive == EMPTY_DIRECTIVE) {
240
return directive.direction == ASCENDING ? ICON_ASCENDING : ICON_DESCENDING;
243
private void cancelSorting() {
244
sortingColumns.clear();
245
sortingStatusChanged();
248
public void setColumnComparator(Class type, Comparator comparator) {
249
if (comparator == null) {
250
columnComparators.remove(type);
252
columnComparators.put(type, comparator);
257
* Sets comparator that will be used to compare values in the specified column. Note that the Comparator will obtain
258
* row indices (as Integer objects) to compare and NOT actual cell values.
260
* @param column model index of the column to be sorted
261
* @param comparator comparator that will sort the given column
263
public void setColumnComparator(int column, Comparator comparator) {
264
if (comparator == null) {
265
columnComparators.remove(Integer.valueOf(column));
267
columnComparators.put(Integer.valueOf(column), comparator);
271
protected Comparator getComparator(int column) {
272
Class columnType = tableModel.getColumnClass(column);
273
Comparator comparator = columnComparators.get(columnType);
274
if (comparator != null) {
277
if (Comparable.class.isAssignableFrom(columnType)) {
278
return COMPARABLE_COMAPRATOR;
280
return LEXICAL_COMPARATOR;
283
private Row[] getViewToModel() {
284
if (viewToModel == null) {
285
int tableModelRowCount = tableModel.getRowCount();
286
viewToModel = new Row[tableModelRowCount];
287
for (int row = 0; row < tableModelRowCount; row++) {
288
viewToModel[row] = new Row(row);
292
Arrays.sort(viewToModel);
298
public int modelIndex(int viewIndex) {
299
return getViewToModel()[viewIndex].modelIndex;
302
private int[] getModelToView() {
303
if (modelToView == null) {
304
int n = getViewToModel().length;
305
modelToView = new int[n];
306
for (int i = 0; i < n; i++) {
307
modelToView[modelIndex(i)] = i;
313
// TableModel interface methods
315
public int getRowCount() {
316
return (tableModel == null) ? 0 : tableModel.getRowCount();
319
public int getColumnCount() {
320
return (tableModel == null) ? 0 : tableModel.getColumnCount();
323
public String getColumnName(int column) {
324
return tableModel.getColumnName(column);
327
public Class getColumnClass(int column) {
328
return tableModel.getColumnClass(column);
331
public boolean isCellEditable(int row, int column) {
332
return tableModel.isCellEditable(modelIndex(row), column);
335
public Object getValueAt(int row, int column) {
336
return tableModel.getValueAt(modelIndex(row), column);
339
public void setValueAt(Object aValue, int row, int column) {
340
tableModel.setValueAt(aValue, modelIndex(row), column);
345
private class Row implements Comparable {
346
private int modelIndex;
348
public Row(int index) {
349
this.modelIndex = index;
352
public int compareTo(Object o) {
353
int row1 = modelIndex;
354
int row2 = ((Row) o).modelIndex;
356
for (Iterator<Directive> it = sortingColumns.iterator(); it.hasNext();) {
357
Directive directive = it.next();
358
int column = directive.column;
359
Object o1 = tableModel.getValueAt(row1, column);
360
Object o2 = tableModel.getValueAt(row2, column);
363
// Define null less than everything, except null.
364
if (o1 == null && o2 == null) {
366
} else if (o1 == null) {
368
} else if (o2 == null) {
371
Comparator comparator = columnComparators.get(Integer.valueOf(column));
372
if (comparator != null) {
373
comparison = comparator.compare(Integer.valueOf(row1), Integer.valueOf(row2));
375
comparison = getComparator(column).compare(o1, o2);
378
if (comparison != 0) {
379
return directive.direction == DESCENDING ? -comparison : comparison;
386
private class TableModelHandler implements TableModelListener {
387
public void tableChanged(TableModelEvent e) {
388
// If we're not sorting by anything, just pass the event along.
395
// If the table structure has changed, cancel the sorting; the
396
// sorting columns may have been either moved or deleted from
398
if (e.getFirstRow() == TableModelEvent.HEADER_ROW) {
404
// We can map a cell event through to the view without widening
405
// when the following conditions apply:
407
// a) all the changes are on one row (e.getFirstRow() == e.getLastRow()) and,
408
// b) all the changes are in one column (column != TableModelEvent.ALL_COLUMNS) and,
409
// c) we are not sorting on that column (getSortingStatus(column) == NOT_SORTED) and,
410
// d) a reverse lookup will not trigger a sort (modelToView != null)
412
// Note: INSERT and DELETE events fail this test as they have column == ALL_COLUMNS.
414
// The last check, for (modelToView != null) is to see if modelToView
415
// is already allocated. If we don't do this check; sorting can become
416
// a performance bottleneck for applications where cells
417
// change rapidly in different parts of the table. If cells
418
// change alternately in the sorting column and then outside of
419
// it this class can end up re-sorting on alternate cell updates -
420
// which can be a performance problem for large tables. The last
421
// clause avoids this problem.
422
int column = e.getColumn();
423
if (e.getFirstRow() == e.getLastRow()
424
&& column != TableModelEvent.ALL_COLUMNS
425
&& getSortingStatus(column) == NOT_SORTED
426
&& modelToView != null) {
427
int viewIndex = getModelToView()[e.getFirstRow()];
428
fireTableChanged(new TableModelEvent(TableSorter.this,
429
viewIndex, viewIndex,
430
column, e.getType()));
434
// Something has happened to the data that may have invalidated the row order.
436
fireTableDataChanged();
440
private class MouseHandler extends MouseAdapter {
441
public void mouseClicked(MouseEvent e) {
442
JTableHeader h = (JTableHeader) e.getSource();
443
TableColumnModel columnModel = h.getColumnModel();
444
int viewColumn = columnModel.getColumnIndexAtX(e.getX());
445
int column = columnModel.getColumn(viewColumn).getModelIndex();
447
int status = getSortingStatus(column);
448
if (!e.isControlDown()) {
451
// Cycle the sorting states through {NOT_SORTED, ASCENDING, DESCENDING} or
452
// {NOT_SORTED, DESCENDING, ASCENDING} depending on whether shift is pressed.
453
status += e.isShiftDown() ? -1 : 1;
454
status = (status + 4) % 3 - 1; // signed mod, returning {-1, 0, 1}
455
setSortingStatus(column, status);
460
private class SortableHeaderRenderer implements TableCellRenderer {
461
private TableCellRenderer tableCellRenderer;
463
public SortableHeaderRenderer(TableCellRenderer tableCellRenderer) {
464
this.tableCellRenderer = tableCellRenderer;
467
public Component getTableCellRendererComponent(JTable table,
473
Component c = tableCellRenderer.getTableCellRendererComponent(table,
474
value, isSelected, hasFocus, row, column);
475
if (c instanceof JLabel) {
476
JLabel l = (JLabel) c;
477
int modelColumn = table.convertColumnIndexToModel(column);
478
Directive directive = getDirective(modelColumn);
479
if (directive != EMPTY_DIRECTIVE) {
480
l.setFont(l.getFont().deriveFont(Font.BOLD));
482
l.setHorizontalTextPosition(JLabel.LEFT);
483
l.setIcon(getHeaderRendererIcon(modelColumn, l.getFont().getSize()));
489
private static class Directive {
491
private int direction;
493
public Directive(int column, int direction) {
494
this.column = column;
495
this.direction = direction;