001: /*--
002:
003: Copyright (C) 2000-2003 Anthony Eden.
004: All rights reserved.
005:
006: Redistribution and use in source and binary forms, with or without
007: modification, are permitted provided that the following conditions
008: are met:
009:
010: 1. Redistributions of source code must retain the above copyright
011: notice, this list of conditions, and the following disclaimer.
012:
013: 2. Redistributions in binary form must reproduce the above copyright
014: notice, this list of conditions, and the disclaimer that follows
015: these conditions in the documentation and/or other materials
016: provided with the distribution.
017:
018: 3. The name "EdenLib" must not be used to endorse or promote products
019: derived from this software without prior written permission. For
020: written permission, please contact me@anthonyeden.com.
021:
022: 4. Products derived from this software may not be called "EdenLib", nor
023: may "EdenLib" appear in their name, without prior written permission
024: from Anthony Eden (me@anthonyeden.com).
025:
026: In addition, I request (but do not require) that you include in the
027: end-user documentation provided with the redistribution and/or in the
028: software itself an acknowledgement equivalent to the following:
029: "This product includes software developed by
030: Anthony Eden (http://www.anthonyeden.com/)."
031:
032: THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
033: WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
034: OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
035: DISCLAIMED. IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT,
036: INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
037: (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
038: SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
039: HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
040: STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
041: IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
042: POSSIBILITY OF SUCH DAMAGE.
043:
044: For more information on EdenLib, please see <http://edenlib.sf.net/>.
045:
046: */
047:
048: package com.anthonyeden.lib.util;
049:
050: import java.awt.event.MouseAdapter;
051: import java.awt.event.MouseEvent;
052: import java.awt.event.InputEvent;
053: import java.util.*;
054:
055: import javax.swing.JTable;
056: import javax.swing.event.TableModelEvent;
057: import javax.swing.table.TableModel;
058: import javax.swing.table.JTableHeader;
059: import javax.swing.table.TableColumnModel;
060:
061: /**
062: * A sorter for TableModels. The sorter has a model (conforming to TableModel)
063: * and itself implements TableModel. TableSorter does not store or copy
064: * the data in the TableModel, instead it maintains an array of
065: * integers which it keeps the same size as the number of rows in its
066: * model. When the model changes it notifies the sorter that something
067: * has changed eg. "rowsAdded" so that its internal array of integers
068: * can be reallocated. As requests are made of the sorter (like
069: * getValueAt(row, col) it redirects them to its model via the mapping
070: * array. That way the TableSorter appears to hold another copy of the table
071: * with the rows in a different order. The sorting algorthm used is stable
072: * which means that it does not move around rows when its comparison
073: * function returns 0 to denote that they are equivalent.
074: *
075: * @version 1.5 12/17/97
076: * @author Philip Milne, Anthony Eden
077: */
078:
079: public class TableSorter extends TableMap {
080:
081: int indexes[];
082: Vector sortingColumns = new Vector();
083: boolean ascending = true;
084: int column = -1;
085: int compares;
086:
087: /** Construct a TableSorter with no TableModel. */
088:
089: public TableSorter() {
090: indexes = new int[0]; // for consistency
091: }
092:
093: /** Construct a TableSorter with the given TableModel.
094:
095: @param model The TableModel
096: */
097:
098: public TableSorter(TableModel model) {
099: setModel(model);
100: }
101:
102: /** Set the TableModel.
103:
104: @param model The TableModel
105: */
106:
107: public void setModel(TableModel model) {
108: super .setModel(model);
109: reallocateIndexes();
110: }
111:
112: /** Compare values row by row. This method can compare Strings, Booleans,
113: Dates, or Numbers. It compares objects by calling each object's toString()
114: method and then comparing the Strings. It can also compare objects which
115: implement the <code>java.lang.Comparable</code> interface.
116:
117: @param row1 Row 1
118: @param row2 Row 2
119: @param column The column to sort
120: @return 1 If row 1 is greater than row 2, -1 if row 1 is less than
121: row 2, or 0 if the values match
122: */
123:
124: public int compareRowsByColumn(int row1, int row2, int column) {
125: Class type = model.getColumnClass(column);
126: TableModel data = model;
127:
128: // Check for nulls.
129:
130: Object o1 = data.getValueAt(row1, column);
131: Object o2 = data.getValueAt(row2, column);
132:
133: // If both values are null, return 0.
134: if (o1 == null && o2 == null) {
135: return 0;
136: } else if (o1 == null) { // Define null less than everything.
137: return -1;
138: } else if (o2 == null) {
139: return 1;
140: }
141:
142: /*
143: * We copy all returned values from the getValue call in case
144: * an optimised model is reusing one object to return many
145: * values. The Number subclasses in the JDK are immutable and
146: * so will not be used in this way but other subclasses of
147: * Number might want to do this to save space and avoid
148: * unnecessary heap allocation.
149: */
150:
151: if (type.getSuperclass() == java.lang.Number.class) {
152: Number n1 = (Number) data.getValueAt(row1, column);
153: double d1 = n1.doubleValue();
154: Number n2 = (Number) data.getValueAt(row2, column);
155: double d2 = n2.doubleValue();
156:
157: if (d1 < d2) {
158: return -1;
159: } else if (d1 > d2) {
160: return 1;
161: } else {
162: return 0;
163: }
164: } else if (type == java.util.Date.class) {
165: Date d1 = (Date) data.getValueAt(row1, column);
166: long n1 = d1.getTime();
167: Date d2 = (Date) data.getValueAt(row2, column);
168: long n2 = d2.getTime();
169:
170: if (n1 < n2) {
171: return -1;
172: } else if (n1 > n2) {
173: return 1;
174: } else {
175: return 0;
176: }
177: } else if (type == String.class) {
178: String s1 = (String) data.getValueAt(row1, column);
179: String s2 = (String) data.getValueAt(row2, column);
180: int result = s1.compareTo(s2);
181:
182: if (result < 0) {
183: return -1;
184: } else if (result > 0) {
185: return 1;
186: } else {
187: return 0;
188: }
189: } else if (type == Boolean.class) {
190: Boolean bool1 = (Boolean) data.getValueAt(row1, column);
191: boolean b1 = bool1.booleanValue();
192: Boolean bool2 = (Boolean) data.getValueAt(row2, column);
193: boolean b2 = bool2.booleanValue();
194:
195: if (b1 == b2) {
196: return 0;
197: } else if (b1) { // Define false < true
198: return 1;
199: } else {
200: return -1;
201: }
202: } else {
203: Object v1 = data.getValueAt(row1, column);
204: Comparable comp = null;
205: if (v1 instanceof Comparable) {
206: comp = (Comparable) v1;
207: } else {
208: comp = v1.toString();
209: }
210:
211: int result = comp.compareTo(o2);
212:
213: if (result < 0) {
214: return -1;
215: } else if (result > 0) {
216: return 1;
217: } else {
218: return 0;
219: }
220: }
221: }
222:
223: /** Compare two rows. All sorting columns will be sorted.
224:
225: @param row1 Row 1
226: @param row2 Row 2
227: @return 1, 0, or -1
228: */
229:
230: public int compare(int row1, int row2) {
231: compares++;
232: for (int level = 0; level < sortingColumns.size(); level++) {
233: Integer column = (Integer) sortingColumns.elementAt(level);
234: int result = compareRowsByColumn(row1, row2, column
235: .intValue());
236: if (result != 0) {
237: return ascending ? result : -result;
238: }
239: }
240: return 0;
241: }
242:
243: /** Reallocate the array which holds sorted indexes. */
244:
245: public void reallocateIndexes() {
246: int rowCount = model.getRowCount();
247:
248: // Set up a new array of indexes with the right number of elements
249: // for the new data model.
250: indexes = new int[rowCount];
251:
252: // Initialise with the identity mapping.
253: for (int row = 0; row < rowCount; row++) {
254: indexes[row] = row;
255: }
256: }
257:
258: /** Fire a TableModelEvent signaling that the table has changed.
259:
260: @param e The TableModelEvent
261: */
262:
263: public void tableChanged(TableModelEvent e) {
264: reallocateIndexes();
265: if (column >= 0) {
266: sortByColumn(column, ascending);
267: }
268:
269: super .tableChanged(e);
270: }
271:
272: /** Check to see if the index length matches the model row count. If
273: not then the sorter was never informed of a change in the model.
274: */
275:
276: public void checkModel() {
277: if (indexes.length != model.getRowCount()) {
278: // System.err.println("Sorter not informed of a change in model.");
279: }
280: }
281:
282: /** Sort the table data.
283:
284: @param sender The object which invoked the sort
285: */
286:
287: public void sort(Object sender) {
288: checkModel();
289:
290: compares = 0;
291: // n2sort();
292: // qsort(0, indexes.length-1);
293: shuttlesort((int[]) indexes.clone(), indexes, 0, indexes.length);
294: //System.out.println("Compares: "+compares);
295: }
296:
297: public void n2sort() {
298: for (int i = 0; i < getRowCount(); i++) {
299: for (int j = i + 1; j < getRowCount(); j++) {
300: if (compare(indexes[i], indexes[j]) == -1) {
301: swap(i, j);
302: }
303: }
304: }
305: }
306:
307: // This is a home-grown implementation which we have not had time
308: // to research - it may perform poorly in some circumstances. It
309: // requires twice the space of an in-place algorithm and makes
310: // NlogN assigments shuttling the values between the two
311: // arrays. The number of compares appears to vary between N-1 and
312: // NlogN depending on the initial order but the main reason for
313: // using it here is that, unlike qsort, it is stable.
314: public void shuttlesort(int from[], int to[], int low, int high) {
315: if (high - low < 2) {
316: return;
317: }
318: int middle = (low + high) / 2;
319: shuttlesort(to, from, low, middle);
320: shuttlesort(to, from, middle, high);
321:
322: int p = low;
323: int q = middle;
324:
325: /* This is an optional short-cut; at each recursive call,
326: check to see if the elements in this subset are already
327: ordered. If so, no further comparisons are needed; the
328: sub-array can just be copied. The array must be copied rather
329: than assigned otherwise sister calls in the recursion might
330: get out of sinc. When the number of elements is three they
331: are partitioned so that the first set, [low, mid), has one
332: element and and the second, [mid, high), has two. We skip the
333: optimisation when the number of elements is three or less as
334: the first compare in the normal merge will produce the same
335: sequence of steps. This optimisation seems to be worthwhile
336: for partially ordered lists but some analysis is needed to
337: find out how the performance drops to Nlog(N) as the initial
338: order diminishes - it may drop very quickly. */
339:
340: if (high - low >= 4
341: && compare(from[middle - 1], from[middle]) <= 0) {
342: for (int i = low; i < high; i++) {
343: to[i] = from[i];
344: }
345: return;
346: }
347:
348: // A normal merge.
349:
350: for (int i = low; i < high; i++) {
351: if (q >= high
352: || (p < middle && compare(from[p], from[q]) <= 0)) {
353: to[i] = from[p++];
354: } else {
355: to[i] = from[q++];
356: }
357: }
358: }
359:
360: public void swap(int i, int j) {
361: int tmp = indexes[i];
362: indexes[i] = indexes[j];
363: indexes[j] = tmp;
364: }
365:
366: // The mapping only affects the contents of the data rows.
367: // Pass all requests to these rows through the mapping array: "indexes".
368:
369: public Object getValueAt(int aRow, int aColumn) {
370: checkModel();
371: return model.getValueAt(indexes[aRow], aColumn);
372: }
373:
374: public void setValueAt(Object aValue, int aRow, int aColumn) {
375: checkModel();
376: model.setValueAt(aValue, indexes[aRow], aColumn);
377: }
378:
379: /** Sort by the given column in ascending order.
380:
381: @param column The column
382: */
383:
384: public void sortByColumn(int column) {
385: sortByColumn(column, true);
386: }
387:
388: /** Sort by the given column and order.
389:
390: @param column The column
391: @param ascending True to sort in ascending order
392: */
393:
394: public void sortByColumn(int column, boolean ascending) {
395: this .column = column;
396: this .ascending = ascending;
397: sortingColumns.removeAllElements();
398: sortingColumns.addElement(new Integer(column));
399: sort(this );
400: super .tableChanged(new TableModelEvent(this ));
401: }
402:
403: /** Get the column which is being sorted.
404:
405: @return The sorted column
406: */
407:
408: public int getColumn() {
409: return column;
410: }
411:
412: /** Get the real index of the given row.
413:
414: @param row The row index
415: @return The index
416: */
417:
418: public int getRealIndex(int row) {
419: return indexes[row];
420: }
421:
422: /** Return true if the data is sorted in ascending order.
423:
424: @return True if sorted in ascending order
425: */
426:
427: public boolean isAscending() {
428: return ascending;
429: }
430:
431: // There is no-where else to put this.
432: // Add a mouse listener to the Table to trigger a table sort
433: // when a column heading is clicked in the JTable.
434:
435: /** Add a MouseListener to the given table which will cause the table
436: to be sorted when the header is clicked. The table will be sorted
437: in ascending order initially. If the table was already sorted in
438: ascending order and the same column is clicked then the order will
439: be reversed.
440:
441: @param table The JTable
442: */
443:
444: public void addMouseListenerToHeaderInTable(JTable table) {
445: final TableSorter sorter = this ;
446: final JTable tableView = table;
447: tableView.setColumnSelectionAllowed(false);
448: MouseAdapter listMouseListener = new MouseAdapter() {
449: public void mouseClicked(MouseEvent e) {
450: TableColumnModel columnModel = tableView
451: .getColumnModel();
452: int viewColumn = columnModel
453: .getColumnIndexAtX(e.getX());
454: int column = tableView
455: .convertColumnIndexToModel(viewColumn);
456: if (e.getClickCount() == 1 && column != -1) {
457: //System.out.println("Sorting ...");
458: //int shiftPressed = e.getModifiers()&InputEvent.SHIFT_MASK;
459: //boolean ascending = (shiftPressed == 0);
460: if (TableSorter.this .column == column) {
461: ascending = !ascending;
462: } else {
463: ascending = true;
464: }
465: sorter.sortByColumn(column, ascending);
466: }
467: }
468: };
469: JTableHeader th = tableView.getTableHeader();
470: th.addMouseListener(listMouseListener);
471: }
472:
473: }
|