~ubuntu-branches/ubuntu/quantal/netbeans/quantal

« back to all changes in this revision

Viewing changes to versioncontrol/util/src/org/netbeans/modules/versioning/util/TableSorter.java

  • Committer: Bazaar Package Importer
  • Author(s): Marek Slama
  • Date: 2008-01-29 14:11:22 UTC
  • Revision ID: james.westby@ubuntu.com-20080129141122-fnzjbo11ntghxfu7
Tags: upstream-6.0.1
ImportĀ upstreamĀ versionĀ 6.0.1

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
/*
 
2
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 
3
 *
 
4
 * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
 
5
 *
 
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]"
 
23
 *
 
24
 * Contributor(s):
 
25
 *
 
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.
 
29
 *
 
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.
 
40
 */
 
41
 
 
42
package org.netbeans.modules.versioning.util;
 
43
 
 
44
import java.awt.*;
 
45
import java.awt.event.*;
 
46
import java.util.*;
 
47
import java.util.List;
 
48
 
 
49
import javax.swing.*;
 
50
import javax.swing.event.TableModelEvent;
 
51
import javax.swing.event.TableModelListener;
 
52
import javax.swing.table.*;
 
53
 
 
54
/**
 
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.
 
64
 * <p/>
 
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.
 
72
 * <p/>
 
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:
 
79
 * <ul>
 
80
 * <li>
 
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
 
84
 * NOT_SORTED again).
 
85
 * <li>
 
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}.
 
89
 * <li>
 
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
 
93
 * sort.
 
94
 * </ul>
 
95
 * <p/>
 
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.
 
98
 * 
 
99
 * @author Philip Milne
 
100
 * @author Brendon McLean 
 
101
 * @author Dan van Enckevort
 
102
 * @author Parwinder Sekhon
 
103
 * @version 2.0 02/27/04
 
104
 */
 
105
 
 
106
public final class TableSorter extends AbstractTableModel {
 
107
    protected TableModel tableModel;
 
108
 
 
109
    public static final int DESCENDING = -1;
 
110
    public static final int NOT_SORTED = 0;
 
111
    public static final int ASCENDING = 1;
 
112
 
 
113
    private static Directive EMPTY_DIRECTIVE = new Directive(-1, NOT_SORTED);
 
114
 
 
115
    public static final Comparator COMPARABLE_COMAPRATOR = new Comparator<Comparable>() {
 
116
        public int compare(Comparable o1, Comparable o2) {
 
117
            return o1.compareTo(o2);
 
118
        }
 
119
    };
 
120
    public static final Comparator LEXICAL_COMPARATOR = new Comparator() {
 
121
        public int compare(Object o1, Object o2) {
 
122
            return o1.toString().compareTo(o2.toString());
 
123
        }
 
124
    };
 
125
 
 
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
 
128
    
 
129
    private Row[] viewToModel;
 
130
    private int[] modelToView;
 
131
 
 
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>();
 
138
 
 
139
    public TableSorter() {
 
140
        this.mouseListener = new MouseHandler();
 
141
        this.tableModelListener = new TableModelHandler();
 
142
    }
 
143
 
 
144
    public TableSorter(TableModel tableModel) {
 
145
        this();
 
146
        setTableModel(tableModel);
 
147
    }
 
148
 
 
149
    public TableSorter(TableModel tableModel, JTableHeader tableHeader) {
 
150
        this();
 
151
        setTableHeader(tableHeader);
 
152
        setTableModel(tableModel);
 
153
    }
 
154
 
 
155
    private void clearSortingState() {
 
156
        viewToModel = null;
 
157
        modelToView = null;
 
158
    }
 
159
 
 
160
    public TableModel getTableModel() {
 
161
        return tableModel;
 
162
    }
 
163
 
 
164
    public final void setTableModel(TableModel tableModel) {
 
165
        if (this.tableModel != null) {
 
166
            this.tableModel.removeTableModelListener(tableModelListener);
 
167
        }
 
168
 
 
169
        this.tableModel = tableModel;
 
170
        if (this.tableModel != null) {
 
171
            this.tableModel.addTableModelListener(tableModelListener);
 
172
        }
 
173
 
 
174
        clearSortingState();
 
175
        fireTableStructureChanged();
 
176
    }
 
177
 
 
178
    public JTableHeader getTableHeader() {
 
179
        return tableHeader;
 
180
    }
 
181
 
 
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);
 
188
            }
 
189
        }
 
190
        this.tableHeader = tableHeader;
 
191
        if (this.tableHeader != null) {
 
192
            this.tableHeader.addMouseListener(mouseListener);
 
193
            this.tableHeader.setDefaultRenderer(
 
194
                    new SortableHeaderRenderer(this.tableHeader.getDefaultRenderer()));
 
195
        }
 
196
    }
 
197
 
 
198
    public boolean isSorting() {
 
199
        return sortingColumns.size() != 0;
 
200
    }
 
201
 
 
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) {
 
206
                return directive;
 
207
            }
 
208
        }
 
209
        return EMPTY_DIRECTIVE;
 
210
    }
 
211
 
 
212
    public int getSortingStatus(int column) {
 
213
        return getDirective(column).direction;
 
214
    }
 
215
 
 
216
    private void sortingStatusChanged() {
 
217
        clearSortingState();
 
218
        fireTableDataChanged();
 
219
        if (tableHeader != null) {
 
220
            tableHeader.repaint();
 
221
        }
 
222
    }
 
223
 
 
224
    public void setSortingStatus(int column, int status) {
 
225
        Directive directive = getDirective(column);
 
226
        if (directive != EMPTY_DIRECTIVE) {
 
227
            sortingColumns.remove(directive);
 
228
        }
 
229
        if (status != NOT_SORTED) {
 
230
            sortingColumns.add(new Directive(column, status));
 
231
        }
 
232
        sortingStatusChanged();
 
233
    }
 
234
 
 
235
    protected Icon getHeaderRendererIcon(int column, int size) {
 
236
        Directive directive = getDirective(column);
 
237
        if (directive == EMPTY_DIRECTIVE) {
 
238
            return null;
 
239
        }
 
240
        return directive.direction == ASCENDING ? ICON_ASCENDING : ICON_DESCENDING;
 
241
    }
 
242
 
 
243
    private void cancelSorting() {
 
244
        sortingColumns.clear();
 
245
        sortingStatusChanged();
 
246
    }
 
247
 
 
248
    public void setColumnComparator(Class type, Comparator comparator) {
 
249
        if (comparator == null) {
 
250
            columnComparators.remove(type);
 
251
        } else {
 
252
            columnComparators.put(type, comparator);
 
253
        }
 
254
    }
 
255
 
 
256
    /**
 
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.
 
259
     * 
 
260
     * @param column model index of the column to be sorted 
 
261
     * @param comparator comparator that will sort the given column
 
262
     */ 
 
263
    public void setColumnComparator(int column, Comparator comparator) {
 
264
        if (comparator == null) {
 
265
            columnComparators.remove(Integer.valueOf(column));
 
266
        } else {
 
267
            columnComparators.put(Integer.valueOf(column), comparator);
 
268
        }
 
269
    }
 
270
    
 
271
    protected Comparator getComparator(int column) {
 
272
        Class columnType = tableModel.getColumnClass(column);
 
273
        Comparator comparator = columnComparators.get(columnType);
 
274
        if (comparator != null) {
 
275
            return comparator;
 
276
        }
 
277
        if (Comparable.class.isAssignableFrom(columnType)) {
 
278
            return COMPARABLE_COMAPRATOR;
 
279
        }
 
280
        return LEXICAL_COMPARATOR;
 
281
    }
 
282
 
 
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);
 
289
            }
 
290
 
 
291
            if (isSorting()) {
 
292
                Arrays.sort(viewToModel);
 
293
            }
 
294
        }
 
295
        return viewToModel;
 
296
    }
 
297
 
 
298
    public int modelIndex(int viewIndex) {
 
299
        return getViewToModel()[viewIndex].modelIndex;
 
300
    }
 
301
 
 
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;
 
308
            }
 
309
        }
 
310
        return modelToView;
 
311
    }
 
312
 
 
313
    // TableModel interface methods 
 
314
 
 
315
    public int getRowCount() {
 
316
        return (tableModel == null) ? 0 : tableModel.getRowCount();
 
317
    }
 
318
 
 
319
    public int getColumnCount() {
 
320
        return (tableModel == null) ? 0 : tableModel.getColumnCount();
 
321
    }
 
322
 
 
323
    public String getColumnName(int column) {
 
324
        return tableModel.getColumnName(column);
 
325
    }
 
326
 
 
327
    public Class getColumnClass(int column) {
 
328
        return tableModel.getColumnClass(column);
 
329
    }
 
330
 
 
331
    public boolean isCellEditable(int row, int column) {
 
332
        return tableModel.isCellEditable(modelIndex(row), column);
 
333
    }
 
334
 
 
335
    public Object getValueAt(int row, int column) {
 
336
        return tableModel.getValueAt(modelIndex(row), column);
 
337
    }
 
338
 
 
339
    public void setValueAt(Object aValue, int row, int column) {
 
340
        tableModel.setValueAt(aValue, modelIndex(row), column);
 
341
    }
 
342
 
 
343
    // Helper classes
 
344
    
 
345
    private class Row implements Comparable {
 
346
        private int modelIndex;
 
347
 
 
348
        public Row(int index) {
 
349
            this.modelIndex = index;
 
350
        }
 
351
 
 
352
        public int compareTo(Object o) {
 
353
            int row1 = modelIndex;
 
354
            int row2 = ((Row) o).modelIndex;
 
355
 
 
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);
 
361
 
 
362
                int comparison = 0;
 
363
                // Define null less than everything, except null.
 
364
                if (o1 == null && o2 == null) {
 
365
                    comparison = 0;
 
366
                } else if (o1 == null) {
 
367
                    comparison = -1;
 
368
                } else if (o2 == null) {
 
369
                    comparison = 1;
 
370
                } else {
 
371
                    Comparator comparator = columnComparators.get(Integer.valueOf(column));
 
372
                    if (comparator != null) {
 
373
                        comparison = comparator.compare(Integer.valueOf(row1), Integer.valueOf(row2));
 
374
                    } else {
 
375
                        comparison = getComparator(column).compare(o1, o2);
 
376
                    }
 
377
                }
 
378
                if (comparison != 0) {
 
379
                    return directive.direction == DESCENDING ? -comparison : comparison;
 
380
                }
 
381
            }
 
382
            return 0;
 
383
        }
 
384
    }
 
385
 
 
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.             
 
389
            if (!isSorting()) {
 
390
                clearSortingState();
 
391
                fireTableChanged(e);
 
392
                return;
 
393
            }
 
394
                
 
395
            // If the table structure has changed, cancel the sorting; the             
 
396
            // sorting columns may have been either moved or deleted from             
 
397
            // the model. 
 
398
            if (e.getFirstRow() == TableModelEvent.HEADER_ROW) {
 
399
                cancelSorting();
 
400
                fireTableChanged(e);
 
401
                return;
 
402
            }
 
403
 
 
404
            // We can map a cell event through to the view without widening             
 
405
            // when the following conditions apply: 
 
406
            // 
 
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)
 
411
            //
 
412
            // Note: INSERT and DELETE events fail this test as they have column == ALL_COLUMNS.
 
413
            // 
 
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()));
 
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
        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();
 
446
            if (column != -1) {
 
447
                int status = getSortingStatus(column);
 
448
                if (!e.isControlDown()) {
 
449
                    cancelSorting();
 
450
                }
 
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);
 
456
            }
 
457
        }
 
458
    }
 
459
 
 
460
    private class SortableHeaderRenderer implements TableCellRenderer {
 
461
        private TableCellRenderer tableCellRenderer;
 
462
 
 
463
        public SortableHeaderRenderer(TableCellRenderer tableCellRenderer) {
 
464
            this.tableCellRenderer = tableCellRenderer;
 
465
        }
 
466
 
 
467
        public Component getTableCellRendererComponent(JTable table, 
 
468
                                                       Object value,
 
469
                                                       boolean isSelected, 
 
470
                                                       boolean hasFocus,
 
471
                                                       int row, 
 
472
                                                       int column) {
 
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));
 
481
                }
 
482
                l.setHorizontalTextPosition(JLabel.LEFT);
 
483
                l.setIcon(getHeaderRendererIcon(modelColumn, l.getFont().getSize()));
 
484
            }
 
485
            return c;
 
486
        }
 
487
    }
 
488
 
 
489
    private static class Directive {
 
490
        private int column;
 
491
        private int direction;
 
492
 
 
493
        public Directive(int column, int direction) {
 
494
            this.column = column;
 
495
            this.direction = direction;
 
496
        }
 
497
    }
 
498
}