001: package org.beryl.gui.table;
002:
003: /**
004: * Enhancements:
005: * - reverse lookups
006: * - supports the Comparable interface
007: * - ascending / descending can be toggled by clicking on a column head twice
008: * @author Wenzel Jakob
009: */
010:
011: /*
012: * @(#)TableSorter.java 1.8 99/04/23
013: *
014: * Copyright (c) 1997-1999 by Sun Microsystems, Inc. All Rights Reserved.
015: *
016: * Sun grants you ("Licensee") a non-exclusive, royalty free, license to use,
017: * modify and redistribute this software in source and binary code form,
018: * provided that i) this copyright notice and license appear on all copies of
019: * the software; and ii) Licensee does not utilize the software in a manner
020: * which is disparaging to Sun.
021: *
022: * This software is provided "AS IS," without a warranty of any kind. ALL
023: * EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES, INCLUDING ANY
024: * IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE OR
025: * NON-INFRINGEMENT, ARE HEREBY EXCLUDED. SUN AND ITS LICENSORS SHALL NOT BE
026: * LIABLE FOR ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT OF USING, MODIFYING
027: * OR DISTRIBUTING THE SOFTWARE OR ITS DERIVATIVES. IN NO EVENT WILL SUN OR ITS
028: * LICENSORS BE LIABLE FOR ANY LOST REVENUE, PROFIT OR DATA, OR FOR DIRECT,
029: * INDIRECT, SPECIAL, CONSEQUENTIAL, INCIDENTAL OR PUNITIVE DAMAGES, HOWEVER
030: * CAUSED AND REGARDLESS OF THE THEORY OF LIABILITY, ARISING OUT OF THE USE OF
031: * OR INABILITY TO USE SOFTWARE, EVEN IF SUN HAS BEEN ADVISED OF THE
032: * POSSIBILITY OF SUCH DAMAGES.
033: *
034: * This software is not designed or intended for use in on-line control of
035: * aircraft, air traffic, aircraft navigation or aircraft communications; or in
036: * the design, construction, operation or maintenance of any nuclear
037: * facility. Licensee represents and warrants that it will not use or
038: * redistribute the Software for such purposes.
039: */
040:
041: /**
042: * A sorter for TableModels. The sorter has a model (conforming to TableModel)
043: * and itself implements TableModel. TableSorter does not store or copy
044: * the data in the TableModel, instead it maintains an array of
045: * integers which it keeps the same size as the number of rows in its
046: * model. When the model changes it notifies the sorter that something
047: * has changed eg. "rowsAdded" so that its internal array of integers
048: * can be reallocated. As requests are made of the sorter (like
049: * getValueAt(row, col) it redirects them to its model via the mapping
050: * array. That way the TableSorter appears to hold another copy of the table
051: * with the rows in a different order. The sorting algorthm used is stable
052: * which means that it does not move around rows when its comparison
053: * function returns 0 to denote that they are equivalent.
054: *
055: * @version 1.8 04/23/99
056: * @author Philip Milne
057: */
058:
059: import java.awt.Component;
060: import java.awt.event.MouseAdapter;
061: import java.awt.event.MouseEvent;
062: import java.util.Date;
063: import java.util.Vector;
064:
065: import javax.swing.Icon;
066: import javax.swing.JTable;
067: import javax.swing.SwingConstants;
068: import javax.swing.UIManager;
069: import javax.swing.event.TableModelEvent;
070: import javax.swing.table.DefaultTableCellRenderer;
071: import javax.swing.table.JTableHeader;
072: import javax.swing.table.TableColumnModel;
073: import javax.swing.table.TableModel;
074:
075: import org.beryl.gui.GUIException;
076: import org.beryl.gui.ImageIconFactory;
077:
078: public class TableSorter extends TableMap {
079: private int indexes[];
080: private int inverseIndexes[];
081: private Vector sortingColumns = new Vector();
082: private int singleSortingColumn = -1;
083: private boolean ascending = true;
084: private int compares;
085:
086: private SortedTableHeaderRenderer unSortedRenderer = null;
087: private SortedTableHeaderRenderer sortedRenderer = null;
088:
089: public TableSorter() throws GUIException {
090: indexes = new int[0]; // For consistency.
091: inverseIndexes = new int[0];
092:
093: sortedRenderer = new SortedTableHeaderRenderer();
094: unSortedRenderer = new SortedTableHeaderRenderer();
095: }
096:
097: public void setModel(TableModel model) {
098: singleSortingColumn = -1;
099: super .setModel(model);
100: reallocateIndexes();
101: fireTableChanged(new TableModelEvent(this ,
102: TableModelEvent.HEADER_ROW));
103: }
104:
105: public int compareRowsByColumn(int row1, int row2, int column) {
106: Class type = model.getColumnClass(column);
107: TableModel data = model;
108:
109: // Check for nulls
110:
111: Object o1 = data.getValueAt(row1, column);
112: Object o2 = data.getValueAt(row2, column);
113:
114: // If both values are null return 0
115: if (o1 == null && o2 == null) {
116: return 0;
117: } else if (o1 == null) { // Define null less than everything.
118: return -1;
119: } else if (o2 == null) {
120: return 1;
121: }
122:
123: /* We copy all returned values from the getValue call in case
124: an optimised model is reusing one object to return many values.
125: The Number subclasses in the JDK are immutable and so will not be used in
126: this way but other subclasses of Number might want to do this to save
127: space and avoid unnecessary heap allocation.
128: */
129: if (type.getSuperclass() == java.lang.Number.class) {
130: Number n1 = (Number) data.getValueAt(row1, column);
131: double d1 = n1.doubleValue();
132: Number n2 = (Number) data.getValueAt(row2, column);
133: double d2 = n2.doubleValue();
134:
135: if (d1 < d2)
136: return -1;
137: else if (d1 > d2)
138: return 1;
139: else
140: return 0;
141: } else if (type == java.util.Date.class) {
142: Date d1 = (Date) data.getValueAt(row1, column);
143: long n1 = d1.getTime();
144: Date d2 = (Date) data.getValueAt(row2, column);
145: long n2 = d2.getTime();
146:
147: if (n1 < n2)
148: return -1;
149: else if (n1 > n2)
150: return 1;
151: else
152: return 0;
153: } else if (type == String.class) {
154: String s1 = (String) data.getValueAt(row1, column);
155: String s2 = (String) data.getValueAt(row2, column);
156: int result = s1.compareTo(s2);
157:
158: if (result < 0)
159: return -1;
160: else if (result > 0)
161: return 1;
162: else
163: return 0;
164: } else if (type == Boolean.class) {
165: Boolean bool1 = (Boolean) data.getValueAt(row1, column);
166: boolean b1 = bool1.booleanValue();
167: Boolean bool2 = (Boolean) data.getValueAt(row2, column);
168: boolean b2 = bool2.booleanValue();
169:
170: if (b1 == b2)
171: return 0;
172: else if (b1) // Define false < true
173: return 1;
174: else
175: return -1;
176: } else if (type == Comparable.class) {
177: Comparable c1 = (Comparable) data.getValueAt(row1, column);
178: Comparable c2 = (Comparable) data.getValueAt(row2, column);
179: return c1.compareTo(c2);
180: } else {
181: Object v1 = data.getValueAt(row1, column);
182: String s1 = v1.toString();
183: Object v2 = data.getValueAt(row2, column);
184: String s2 = v2.toString();
185: int result = s1.compareTo(s2);
186:
187: if (result < 0)
188: return -1;
189: else if (result > 0)
190: return 1;
191: else
192: return 0;
193: }
194: }
195:
196: public int compare(int row1, int row2) {
197: compares++;
198: for (int level = 0; level < sortingColumns.size(); level++) {
199: Integer column = (Integer) sortingColumns.elementAt(level);
200: int result = compareRowsByColumn(row1, row2, column
201: .intValue());
202: if (result != 0)
203: return ascending ? result : -result;
204: }
205: return 0;
206: }
207:
208: public void reallocateIndexes() {
209: int rowCount = model.getRowCount();
210:
211: // Set up a new array of indexes with the right number of elements
212: // for the new data model.
213: indexes = new int[rowCount];
214: inverseIndexes = new int[rowCount];
215:
216: // Initialise with the identity mapping.
217: for (int row = 0; row < rowCount; row++) {
218: indexes[row] = row;
219: inverseIndexes[row] = row;
220: }
221: }
222:
223: public void tableChanged(TableModelEvent e) {
224: reallocateIndexes();
225:
226: if (singleSortingColumn != -1) {
227: /** this is __expensive__ if many single rows are added to an existing table **/
228: sortByColumn(singleSortingColumn);
229: }
230: super .tableChanged(e);
231: }
232:
233: public void checkModel() {
234: if (indexes.length != model.getRowCount()) {
235: System.err
236: .println("Sorter not informed of a change in model.");
237: }
238: }
239:
240: public void sort(Object sender) {
241: checkModel();
242:
243: compares = 0;
244: shuttlesort((int[]) indexes.clone(), indexes, 0, indexes.length);
245:
246: /* Fill inverse index array */
247: for (int i = 0; i < indexes.length; i++) {
248: int inverseRow = indexes[i];
249: inverseIndexes[inverseRow] = i;
250: }
251: }
252:
253: // This is a home-grown implementation which we have not had time
254: // to research - it may perform poorly in some circumstances. It
255: // requires twice the space of an in-place algorithm and makes
256: // NlogN assigments shuttling the values between the two
257: // arrays. The number of compares appears to vary between N-1 and
258: // NlogN depending on the initial order but the main reason for
259: // using it here is that, unlike qsort, it is stable.
260: public void shuttlesort(int from[], int to[], int low, int high) {
261: if (high - low < 2) {
262: return;
263: }
264: int middle = (low + high) / 2;
265: shuttlesort(to, from, low, middle);
266: shuttlesort(to, from, middle, high);
267:
268: int p = low;
269: int q = middle;
270:
271: /* This is an optional short-cut; at each recursive call,
272: check to see if the elements in this subset are already
273: ordered. If so, no further comparisons are needed; the
274: sub-array can just be copied. The array must be copied rather
275: than assigned otherwise sister calls in the recursion might
276: get out of sinc. When the number of elements is three they
277: are partitioned so that the first set, [low, mid), has one
278: element and and the second, [mid, high), has two. We skip the
279: optimisation when the number of elements is three or less as
280: the first compare in the normal merge will produce the same
281: sequence of steps. This optimisation seems to be worthwhile
282: for partially ordered lists but some analysis is needed to
283: find out how the performance drops to Nlog(N) as the initial
284: order diminishes - it may drop very quickly. */
285:
286: if (high - low >= 4
287: && compare(from[middle - 1], from[middle]) <= 0) {
288: for (int i = low; i < high; i++) {
289: to[i] = from[i];
290: }
291: return;
292: }
293:
294: // A normal merge.
295:
296: for (int i = low; i < high; i++) {
297: if (q >= high
298: || (p < middle && compare(from[p], from[q]) <= 0)) {
299: to[i] = from[p++];
300: } else {
301: to[i] = from[q++];
302: }
303: }
304: }
305:
306: public void swap(int i, int j) {
307: int tmp = indexes[i];
308: indexes[i] = indexes[j];
309: indexes[j] = tmp;
310: }
311:
312: // The mapping only affects the contents of the data rows.
313: // Pass all requests to these rows through the mapping array: "indexes".
314:
315: public Object getValueAt(int aRow, int aColumn) {
316: checkModel();
317: return model.getValueAt(indexes[aRow], aColumn);
318: }
319:
320: public void setValueAt(Object aValue, int aRow, int aColumn) {
321: checkModel();
322: try {
323: model.setValueAt(aValue, indexes[aRow], aColumn);
324: } catch (IndexOutOfBoundsException e) {
325: /* Ignore. This is sometimes thrown when a new table model is set and
326: * editingStopped() gets called */
327: }
328: }
329:
330: public void sortByColumn(int column) {
331: sortingColumns.removeAllElements();
332: sortingColumns.addElement(new Integer(column));
333: sort(this );
334: super .tableChanged(new TableModelEvent(this ));
335: }
336:
337: // There is no-where else to put this.
338: // Add a mouse listener to the Table to trigger a table sort
339: // when a column heading is clicked in the JTable.
340: public void addMouseListenerToHeaderInTable(JTable table) {
341: final TableSorter sorter = this ;
342: final JTable tableView = table;
343:
344: tableView.getTableHeader().setDefaultRenderer(unSortedRenderer);
345:
346: tableView.setColumnSelectionAllowed(false);
347: MouseAdapter listMouseListener = new MouseAdapter() {
348: public void mouseClicked(MouseEvent e) {
349: TableColumnModel columnModel = tableView
350: .getColumnModel();
351: int viewColumn = columnModel
352: .getColumnIndexAtX(e.getX());
353: int column = tableView
354: .convertColumnIndexToModel(viewColumn);
355: if (e.getClickCount() == 1 && column != -1) {
356: if (singleSortingColumn == column)
357: ascending = !ascending;
358: else
359: ascending = true;
360: sorter.sortByColumn(column);
361: singleSortingColumn = column;
362:
363: // setting renderer
364: for (int i = 0; i < columnModel.getColumnCount(); i++)
365: columnModel.getColumn(i).setHeaderRenderer(
366: unSortedRenderer);
367: if (ascending)
368: sortedRenderer
369: .setIconType(SortedTableHeaderRenderer.ASCENDING);
370: else
371: sortedRenderer
372: .setIconType(SortedTableHeaderRenderer.DESCENDING);
373: columnModel.getColumn(column).setHeaderRenderer(
374: sortedRenderer);
375: }
376: }
377: };
378: JTableHeader th = tableView.getTableHeader();
379: th.addMouseListener(listMouseListener);
380: }
381:
382: public int getSortedRowForRow(int row) {
383: return inverseIndexes[row];
384: }
385:
386: public int getRowForSortedRow(int row) {
387: return indexes[row];
388: }
389:
390: public class SortedTableHeaderRenderer extends
391: DefaultTableCellRenderer {
392: private Icon upIcon = null;
393: private Icon downIcon = null;
394: private Icon icon = null;
395: private static final int ASCENDING = 1;
396: private static final int DESCENDING = 2;
397:
398: public SortedTableHeaderRenderer() throws GUIException {
399: upIcon = ImageIconFactory.getIcon("sort_up");
400: downIcon = ImageIconFactory.getIcon("sort_down");
401: }
402:
403: public void setIconType(int type) {
404: if (type == ASCENDING)
405: icon = downIcon;
406: if (type == DESCENDING)
407: icon = upIcon;
408: }
409:
410: public Component getTableCellRendererComponent(JTable table,
411: Object value, boolean isSelected, boolean hasFocus,
412: int row, int column) {
413: setFont(UIManager.getFont("TableHeader.font"));
414: setForeground(UIManager.getColor("TableHeader.foreground"));
415: setBackground(UIManager.getColor("TableHeader.background"));
416: setBorder(UIManager.getBorder("TableHeader.cellBorder"));
417: setValue(value);
418: setIcon(icon);
419: setHorizontalTextPosition(SwingConstants.LEADING);
420: setHorizontalAlignment(SwingConstants.CENTER);
421: return this;
422: }
423: }
424: }
|