Source code: javatools/swing/table/IndexedTableSorter.java
1 /*
2 * IndexedTableSorter.java
3 *
4 * Created on 11 maggio 2002, 12.01
5 Javatools (modified version) - Some useful general classes.
6 Copyright (C) 2002-2003 Chris Bitmead (original) Antonio Petrelli (modified)
7
8 This program is free software; you can redistribute it and/or modify
9 it under the terms of the GNU General Public License as published by
10 the Free Software Foundation; either version 2 of the License, or
11 (at your option) any later version.
12
13 This program is distributed in the hope that it will be useful,
14 but WITHOUT ANY WARRANTY; without even the implied warranty of
15 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 GNU General Public License for more details.
17
18 You should have received a copy of the GNU General Public License
19 along with this program; if not, write to the Free Software
20 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
21
22 Contact me at: brenmcguire@users.sourceforge.net
23 */
24
25 package javatools.swing.table;
26
27 import java.util.*;
28
29 import javax.swing.table.TableModel;
30 import javax.swing.event.TableModelEvent;
31
32 // Imports for picking up mouse events from the JTable.
33
34 import java.awt.event.MouseAdapter;
35 import java.awt.event.MouseEvent;
36 import java.awt.event.InputEvent;
37 import javax.swing.JTable;
38 import javax.swing.table.JTableHeader;
39 import javax.swing.table.TableColumnModel;
40 import javatools.util.FormattedDate;
41
42 /**
43 * It seems a table, it is a table sorter for indexed tables.
44 * Based upon the sorter in the Java tutorial.
45 * @author Antonio Petrelli
46 * @version 0.1.7
47 */
48 public class IndexedTableSorter extends IndexedTableMap {
49
50 /** Creates new IndexedTableSorter */
51 public IndexedTableSorter() {
52 lastSortedColumn = -1;
53 lastSortingDirection = true;
54 indexes = new int[0]; // for consistency
55 }
56
57 /** Creates a new IndexedTableSorter.
58 * @param model The base table model to use.
59 */
60 public IndexedTableSorter(IndexedTableModel model) {
61 lastSortedColumn = -1;
62 lastSortingDirection = true;
63 setModel(model);
64 }
65
66 /** Sets the table model to sort.
67 * @param model The model.
68 */
69 public void setModel(IndexedTableModel model) {
70 super.setModel(model);
71 reallocateIndexes();
72 }
73
74 /** Compares two rows by a column.
75 * @param row1 The first row.
76 * @param row2 The second row.
77 * @param column The column to use as a criterion.
78 * @return The compare result.
79 */
80 public int compareRowsByColumn(int row1, int row2, int column) {
81 Class type = model.getColumnClass(column);
82 TableModel data = model;
83
84 // Check for nulls.
85
86 Object o1 = data.getValueAt(row1, column);
87 Object o2 = data.getValueAt(row2, column);
88
89 // If both values are null, return 0.
90 if (o1 == null && o2 == null) {
91 return 0;
92 } else if (o1 == null) { // Define null less than everything.
93 return -1;
94 } else if (o2 == null) {
95 return 1;
96 }
97
98 /*
99 * We copy all returned values from the getValue call in case
100 * an optimised model is reusing one object to return many
101 * values. The Number subclasses in the JDK are immutable and
102 * so will not be used in this way but other subclasses of
103 * Number might want to do this to save space and avoid
104 * unnecessary heap allocation.
105 */
106
107 if (type.getSuperclass() == java.lang.Number.class) {
108 Number n1 = (Number)data.getValueAt(row1, column);
109 double d1 = n1.doubleValue();
110 Number n2 = (Number)data.getValueAt(row2, column);
111 double d2 = n2.doubleValue();
112
113 if (d1 < d2) {
114 return -1;
115 } else if (d1 > d2) {
116 return 1;
117 } else {
118 return 0;
119 }
120 } else if (type == java.util.Date.class) {
121 Date d1 = (Date)data.getValueAt(row1, column);
122 long n1 = d1.getTime();
123 Date d2 = (Date)data.getValueAt(row2, column);
124 long n2 = d2.getTime();
125
126 if (n1 < n2) {
127 return -1;
128 } else if (n1 > n2) {
129 return 1;
130 } else {
131 return 0;
132 }
133 } else if (type == javatools.util.FormattedDate.class) {
134 FormattedDate d1 = (FormattedDate)data.getValueAt(row1, column);
135 long n1 = d1.getTime();
136 FormattedDate d2 = (FormattedDate)data.getValueAt(row2, column);
137 long n2 = d2.getTime();
138
139 if (n1 < n2) {
140 return -1;
141 } else if (n1 > n2) {
142 return 1;
143 } else {
144 return 0;
145 }
146 } else if (type == String.class) {
147 String s1 = (String)data.getValueAt(row1, column);
148 String s2 = (String)data.getValueAt(row2, column);
149 int result = s1.compareTo(s2);
150
151 if (result < 0) {
152 return -1;
153 } else if (result > 0) {
154 return 1;
155 } else {
156 return 0;
157 }
158 } else if (type == Boolean.class) {
159 Boolean bool1 = (Boolean)data.getValueAt(row1, column);
160 boolean b1 = bool1.booleanValue();
161 Boolean bool2 = (Boolean)data.getValueAt(row2, column);
162 boolean b2 = bool2.booleanValue();
163
164 if (b1 == b2) {
165 return 0;
166 } else if (b1) { // Define false < true
167 return 1;
168 } else {
169 return -1;
170 }
171 } else {
172 Object v1 = data.getValueAt(row1, column);
173 String s1 = v1.toString();
174 Object v2 = data.getValueAt(row2, column);
175 String s2 = v2.toString();
176 int result = s1.compareTo(s2);
177
178 if (result < 0) {
179 return -1;
180 } else if (result > 0) {
181 return 1;
182 } else {
183 return 0;
184 }
185 }
186 }
187
188 /** Compares two columns.
189 * @param row1 The first row.
190 * @param row2 The second row.
191 * @return The compare result.
192 */
193 public int compare(int row1, int row2) {
194 compares++;
195 for (int level = 0; level < sortingColumns.size(); level++) {
196 Integer column = (Integer)sortingColumns.elementAt(level);
197 int result = compareRowsByColumn(row1, row2, column.intValue());
198 if (result != 0) {
199 return ascending ? result : -result;
200 }
201 }
202 return 0;
203 }
204
205 /** Initializes indexes.
206 */
207 public void reallocateIndexes() {
208 int rowCount = model.getRowCount();
209
210 // Set up a new array of indexes with the right number of elements
211 // for the new data model.
212 indexes = new int[rowCount];
213
214 // Initialise with the identity mapping.
215 for (int row = 0; row < rowCount; row++) {
216 indexes[row] = row;
217 }
218 }
219
220 /** Method that is called whenever the table changes.
221 * @param e The event.
222 */
223 public void tableChanged(TableModelEvent e) {
224 //System.out.println("Sorter: tableChanged");
225 reallocateIndexes();
226
227 super.tableChanged(e);
228 }
229
230 /** Checks if the model is OK.
231 */
232 public void checkModel() {
233 if (indexes.length != model.getRowCount()) {
234 System.err.println("Sorter not informed of a change in model.");
235 }
236 }
237
238 /** Sorts the table.
239 * @param sender Ignored (what????).
240 */
241 public void sort(Object sender) {
242 checkModel();
243
244 compares = 0;
245 // n2sort();
246 // qsort(0, indexes.length-1);
247 shuttlesort((int[])indexes.clone(), indexes, 0, indexes.length);
248 //System.out.println("Compares: "+compares);
249 }
250
251 /** Sorts 2 items.
252 */
253 public void n2sort() {
254 for (int i = 0; i < getRowCount(); i++) {
255 for (int j = i+1; j < getRowCount(); j++) {
256 if (compare(indexes[i], indexes[j]) == -1) {
257 swap(i, j);
258 }
259 }
260 }
261 }
262
263 // This is a home-grown implementation which we have not had time
264 // to research - it may perform poorly in some circumstances. It
265 // requires twice the space of an in-place algorithm and makes
266 // NlogN assigments shuttling the values between the two
267 // arrays. The number of compares appears to vary between N-1 and
268 // NlogN depending on the initial order but the main reason for
269 // using it here is that, unlike qsort, it is stable.
270 /** Sorts using shuttlesort.
271 * @param from The origin indexes.
272 * @param to The destination indexes.
273 * @param low The lower limit.
274 * @param high The higher limit.
275 */
276 public void shuttlesort(int from[], int to[], int low, int high) {
277 if (high - low < 2) {
278 return;
279 }
280 int middle = (low + high)/2;
281 shuttlesort(to, from, low, middle);
282 shuttlesort(to, from, middle, high);
283
284 int p = low;
285 int q = middle;
286
287 /* This is an optional short-cut; at each recursive call,
288 check to see if the elements in this subset are already
289 ordered. If so, no further comparisons are needed; the
290 sub-array can just be copied. The array must be copied rather
291 than assigned otherwise sister calls in the recursion might
292 get out of sinc. When the number of elements is three they
293 are partitioned so that the first set, [low, mid), has one
294 element and and the second, [mid, high), has two. We skip the
295 optimisation when the number of elements is three or less as
296 the first compare in the normal merge will produce the same
297 sequence of steps. This optimisation seems to be worthwhile
298 for partially ordered lists but some analysis is needed to
299 find out how the performance drops to Nlog(N) as the initial
300 order diminishes - it may drop very quickly. */
301
302 if (high - low >= 4 && compare(from[middle-1], from[middle]) <= 0) {
303 for (int i = low; i < high; i++) {
304 to[i] = from[i];
305 }
306 return;
307 }
308
309 // A normal merge.
310
311 for (int i = low; i < high; i++) {
312 if (q >= high || (p < middle && compare(from[p], from[q]) <= 0)) {
313 to[i] = from[p++];
314 }
315 else {
316 to[i] = from[q++];
317 }
318 }
319 }
320
321 /** Swaps (logically) two rows.
322 * @param i The first index.
323 * @param j The second index.
324 */
325 public void swap(int i, int j) {
326 int tmp = indexes[i];
327 indexes[i] = indexes[j];
328 indexes[j] = tmp;
329 }
330
331 // The mapping only affects the contents of the data rows.
332 // Pass all requests to these rows through the mapping array: "indexes".
333
334 /** Returns the value in a certain position.
335 * @param aRow The row index.
336 * @param aColumn The column index.
337 * @return The value.
338 */
339 public Object getValueAt(int aRow, int aColumn) {
340 checkModel();
341 return model.getValueAt(indexes[aRow], aColumn);
342 }
343
344 /** Returns the index.
345 * @param row The row index.
346 * @throws ArrayIndexOutOfBoundsException If <CODE>row</CODE> is not correct.
347 * @return The index.
348 */
349 public Object getIndex(int row) throws ArrayIndexOutOfBoundsException {
350 checkModel();
351 return model.getIndex(indexes[row]);
352 }
353
354 /** Sets a value in the table.
355 * @param aValue The value.
356 * @param aRow The row index.
357 * @param aColumn The column index.
358 */
359 public void setValueAt(Object aValue, int aRow, int aColumn) {
360 checkModel();
361 model.setValueAt(aValue, indexes[aRow], aColumn);
362 }
363
364 /** Sets an index.
365 * @param row The row index.
366 * @param index The index.
367 * @throws ArrayIndexOutOfBoundsException If <CODE>row</CODE> is not correct.
368 */
369 public void setIndex(int row, Object index) throws ArrayIndexOutOfBoundsException {
370 checkModel();
371 model.setIndex(indexes[row], index);
372 }
373
374 /** Removes a row.
375 * @param row The row index.
376 */
377 public void removeRow (int row) {
378 checkModel();
379 model.removeRow(indexes[row]);
380 }
381
382 /** Sorts the table by a column.
383 * @param column The column to sort by.
384 */
385 public void sortByColumn(int column) {
386 sortByColumn(column, true);
387 }
388
389 /** Sorts the table by column.
390 * @param column The column to use.
391 * @param ascending <CODE>true</CODE>: ascending order;
392 * <CODE>false</CODE>: descending order.
393 */
394 public void sortByColumn(int column, boolean ascending) {
395 this.ascending = ascending;
396 sortingColumns.removeAllElements();
397 sortingColumns.addElement(new Integer(column));
398 sort(this);
399 super.tableChanged(new TableModelEvent(this));
400 }
401
402 // There is no-where else to put this.
403 // Add a mouse listener to the Table to trigger a table sort
404 // when a column heading is clicked in the JTable.
405 /** Method used to add a mouse listener to catch click on table headers.
406 * @param table The table.
407 */
408 public void addMouseListenerToHeaderInTable(JTable table) {
409 final IndexedTableSorter sorter = this;
410 final JTable tableView = table;
411 tableView.setColumnSelectionAllowed(false);
412 MouseAdapter listMouseListener = new MouseAdapter() {
413 public void mouseClicked(MouseEvent e) {
414 TableColumnModel columnModel = tableView.getColumnModel();
415 int viewColumn = columnModel.getColumnIndexAtX(e.getX());
416 int column = tableView.convertColumnIndexToModel(viewColumn);
417 if (column == lastSortedColumn)
418 lastSortingDirection = !lastSortingDirection;
419 else {
420 lastSortedColumn = column;
421 lastSortingDirection = true;
422 }
423 if (e.getClickCount() == 1 && column != -1) {
424 //System.out.println("Sorting ...");
425 // int shiftPressed = e.getModifiers()&InputEvent.SHIFT_MASK;
426 // boolean ascending = (shiftPressed == 0);
427 sorter.sortByColumn(column, lastSortingDirection);
428 }
429 }
430 };
431 JTableHeader th = tableView.getTableHeader();
432 th.addMouseListener(listMouseListener);
433 }
434
435 /** The indexes to use for correct order.
436 */
437 protected int indexes[];
438 /** Contains the columns to use to sort.
439 */
440 protected Vector sortingColumns = new Vector();
441 /** <CODE>true</CODE>: ascending order;
442 * <CODE>false</CODE>: descending order.
443 */
444 protected boolean ascending = true;
445 /** Contains the number of made comparisons.
446 */
447 protected int compares;
448 /** Contains the last sorted column to alternate ascending and descending order.
449 */
450 protected int lastSortedColumn;
451 /** Contains the last sorting direction, to alternate ascending and descending
452 * order.
453 */
454 protected boolean lastSortingDirection;
455 }