001 package util.swing;
002
003 import java.util.*;
004
005 import javax.swing.table.TableModel;
006 import javax.swing.event.TableModelEvent;
007
008 import java.awt.event.MouseAdapter;
009 import java.awt.event.MouseEvent;
010 import javax.swing.JTable;
011 import javax.swing.table.JTableHeader;
012 import javax.swing.table.TableColumnModel;
013
014 /**
015 * A sorter for TableModels. The sorter has a model (conforming to TableModel)
016 * and itself implements TableModel. TableSorter does not store or copy
017 * the data in the TableModel, instead it maintains an array of
018 * integers which it keeps the same size as the number of rows in its
019 * model. When the model changes it notifies the sorter that something
020 * has changed eg. "rowsAdded" so that its internal array of integers
021 * can be reallocated. As requests are made of the sorter (like
022 * getValueAt(row, col) it redirects them to its model via the mapping
023 * array. That way the TableSorter appears to hold another copy of the table
024 * with the rows in a different order. The sorting algorthm used is stable
025 * which means that it does not move around rows when its comparison
026 * function returns 0 to denote that they are equivalent.
027 *
028 * @version 1.6 2003-03-25
029 * @author Philip Milne
030 * @author Thomas Medack
031 * @author Andreas Bartho
032 */
033 public class TableSorter extends TableMap {
034
035 /**
036 * ID for serialization.
037 */
038 private static final long serialVersionUID = 5975884042981229023L;
039
040 /**
041 * Field of indexes.
042 */
043 private int indexes[], reverseIndexes[];
044
045 /**
046 * Sorting vector.
047 */
048 private Vector<Integer> sortingColumns = new Vector<Integer>();
049
050 /**
051 * Sort ascending or descending.
052 */
053 private boolean ascending = true;
054
055 /**
056 * The column to be sorted.
057 */
058 private int actualColumn = 0;
059
060 /**
061 * Internal helper variable.
062 */
063 private int compares;
064
065 /**
066 * Constructor.
067 */
068 public TableSorter() {
069 indexes = new int[0]; // for consistency
070 }
071
072 /**
073 * Constructor.
074 * @param model the wrapped {@link TableModel}
075 */
076 public TableSorter(AbstractTableModel model) {
077 setModel(model);
078 }
079
080 /**
081 * Sets the {@link TableModel}.
082 *
083 * @param model the model.
084 */
085 public void setModel(AbstractTableModel model) {
086 super.setModel(model);
087 reallocateIndexes();
088 }
089
090 /**
091 * Compares columns of two specific rows.
092 *
093 * <p><b>Attention:</b><br />
094 * This code is not 100% Java 1.5 compatible. Had to avoid the warning with SuppressWarnings
095 * because there is no clean solution for this in 1.5 yet.</p>
096 *
097 * @param row1 first row.
098 * @param row2 second row.
099 * @param column the column index.
100 * @return a comparative value.
101 */
102 @SuppressWarnings("unchecked")
103 public int compareRowsByColumn(int row1, int row2, int column) {
104 TableModel data = model;
105 // Check for nulls.
106 Object o1 = data.getValueAt(row1, column);
107 Object o2 = data.getValueAt(row2, column);
108 // If both values are null, return 0.
109 if (o1 == null && o2 == null) {
110 return 0;
111 } else {
112 if (o1 == null) { // Define null less than everything.
113 return -1;
114 } else {
115 if (o2 == null) {
116 return 1;
117 }
118 }
119 }
120 String s1 = o1.toString();
121 String s2 = o2.toString();
122
123 if ((o1 instanceof Comparable) && (o2 instanceof Comparable)) {
124 java.util.Comparator<Object> cmp = model.getEntryDescriptor().getColumnOrder(column);
125 return (cmp == null) ?
126 ((Comparable<Object>)o1).compareTo(o2) :
127 cmp.compare(model.getRecord(row1), model.getRecord(row2));
128 } else {
129 int result = s1.compareTo(s2);
130 if (result < 0) {
131 return -1;
132 } else {
133 if (result > 0) {
134 return 1;
135 } else {
136 return 0;
137 }
138 }
139 }
140 }
141
142 /**
143 * Comparison of two rows.
144 *
145 * @param row1 first row.
146 * @param row2 second row.
147 * @return a comparative value.
148 */
149 public int compare(int row1, int row2) {
150 compares++;
151 for (int level = 0; level < sortingColumns.size(); level++) {
152 Integer column = (Integer)sortingColumns.elementAt(level);
153 int result = compareRowsByColumn(row1, row2, column.intValue());
154 if (result != 0) {
155 return ascending ? result : -result;
156 }
157 }
158 return 0;
159 }
160
161 /**
162 * Helper method.
163 */
164 private void reallocateIndexes() {
165 int rowCount = model.getRowCount();
166 // Set up a new array of indexes with the right number of elements
167 // for the new data model.
168 indexes = new int[rowCount];
169 // Initialize with the identity mapping.
170 for (int row = 0; row < rowCount; row++) {
171 indexes[row] = row;
172 }
173 }
174
175 /**
176 * Reacts on TableChangeEvents, either converts them as needed or passes them to the model.
177 * @param e the event.
178 */
179 public void tableChanged(TableModelEvent e) {
180 if (indexes.length == getModel().getRowCount() && indexes.length != 0) {
181 createReverseIndexes();
182 super.tableChanged(new TableModelEvent(this, reverseIndexes[e.getFirstRow()]));
183 } else {
184 //if big changes happened (row add or remove) pass event to parent
185 reallocateIndexes();
186 //sortByColumn(actualColumn, ascending);
187 super.tableChanged(e);
188 }
189 }
190
191 /**
192 * Creates a reverse index table.
193 *
194 * This method maps the line number for each table to have the view updated
195 *
196 * Example: indexes = [2 3 0 1 4]<br>
197 * reverseIndex stores in i, where i appeares in indexes:<br>
198 * 0 is on position 2, 1 is on position 3, 2 is on position 0 ... => reverseIndex = [2 3 0 1 4]<br>
199 * Mapping this array with the row number sent by a TableChangeEvent event, all tables have the
200 * correct lines updated (has only effect on CountingStocks)
201 */
202 private void createReverseIndexes() {
203 reverseIndexes = new int[indexes.length];
204 for (int i = 0; i < indexes.length; i++) {
205 for (int j = 0; j < indexes.length; j++) {
206 if (indexes[j] == i) {
207 reverseIndexes[i] = j;
208 }
209 }
210 }
211 }
212
213 /**
214 * Helper method.
215 */
216 private void checkModel() {
217 if (indexes.length != model.getRowCount()) {
218 System.err.println("Sorter not informed of a change in model.");
219 }
220 }
221
222 /**
223 * Sorts the model. It uses {@link shuttlesort(int, int, int, int) shuttlesort}.
224 */
225 private void sort() {
226 checkModel();
227 compares = 0;
228 shuttlesort((int[])indexes.clone(), indexes, 0, indexes.length);
229 }
230
231 // This is a home-grown implementation which we have not had time
232 // to research - it may perform poorly in some circumstances. It
233 // requires twice the space of an in-place algorithm and makes
234 // NlogN assigments shuttling the values between the two
235 // arrays. The number of compares appears to vary between N-1 and
236 // NlogN depending on the initial order but the main reason for
237 // using it here is that, unlike qsort, it is stable.
238 /**
239 * Sorting method.
240 *
241 * @param from the unsorted index array.
242 * @param to the sorted index array (i.e. the result).
243 * @param low lowest field to be taken into consideration on sorting.
244 * @param high highest field to be taken into consideration on sorting.
245 */
246 private void shuttlesort(int from[], int to[], int low, int high) {
247 if (high - low < 2) {
248 return;
249 }
250 int middle = (low + high) / 2;
251 shuttlesort(to, from, low, middle);
252 shuttlesort(to, from, middle, high);
253
254 int p = low;
255 int q = middle;
256
257 if (high - low >= 4 && compare(from[middle - 1], from[middle]) <= 0) {
258 for (int i = low; i < high; i++) {
259 to[i] = from[i];
260 }
261 return;
262 }
263
264 // A normal merge.
265
266 for (int i = low; i < high; i++) {
267 if (q >= high || (p < middle && compare(from[p], from[q]) <= 0)) {
268 to[i] = from[p++];
269 } else {
270 to[i] = from[q++];
271 }
272 }
273 }
274
275 /**
276 * Helper method. Swaps two ints.
277 *
278 * @param i first int.
279 * @param j secont int
280 */
281 /*
282 private void swap(int i, int j) {
283 int tmp = indexes[i];
284 indexes[i] = indexes[j];
285 indexes[j] = tmp;
286 }
287 */
288
289 // The mapping only affects the contents of the data rows.
290 // Pass all requests to these rows through the mapping array: "indexes".
291
292 /**
293 * Get the value of a table cell.
294 *
295 * @param aRow the row of the TableCell to get.
296 * @param aColumn the column of the table cell to get.
297 */
298 public Object getValueAt(int aRow, int aColumn) {
299 checkModel();
300 if (aRow >= 0 && model.getRowCount() > 0) {
301 return model.getValueAt(indexes[aRow], aColumn);
302 } else {
303 return null;
304 }
305 }
306
307 /**
308 * Gets the record.
309 *
310 * @param row the affected table row.
311 * @return the appropriate record.
312 */
313 public Object getRecord(int row) {
314 checkModel();
315 if (row >= 0 && model.getRowCount() > 0 && row < model.getRowCount()) { //prevent ArrayIndexOutOfBoundException when last table entry has
316 // disappered but is yet queried (e.g. all elements of the last
317 // row of a TTFS table have been moved)
318 return model.getRecord(indexes[row]);
319 } else {
320 return null;
321 }
322 }
323
324 /**
325 * Changes the value of a table cell.
326 *
327 * @param aValue the value to set.
328 * @param aRow the row of the TableCell to be changed.
329 * @param aColumn the column of the table cell to be changed.
330 */
331 public void setValueAt(Object aValue, int aRow, int aColumn) {
332 checkModel();
333 if (aRow >= 0) {
334 model.setValueAt(aValue, indexes[aRow], aColumn);
335 }
336 }
337
338 /**
339 * Sorts the table ascending by a column.
340 *
341 * @param column the column by which the table should be sorted.
342 */
343 public void sortByColumn(int column) {
344 sortByColumn(column, true);
345 }
346
347 /**
348 * Sorts the table by a column.
349 *
350 * @param column the column by which the table should be sorted.
351 * @param ascending if <code>true</code> sort sequence is ascending, otherwise descending.
352 */
353 public void sortByColumn(int column, boolean ascending) {
354 this.ascending = ascending;
355 sortingColumns.removeAllElements();
356 sortingColumns.addElement(new Integer(column));
357 sort();
358 super.tableChanged(new TableModelEvent(this));
359 }
360
361 // There is no-where else to put this.
362 // Add a mouse listener to the Table to trigger a table sort
363 // when a column heading is clicked in the JTable.
364
365 /**
366 * Adds a mouse listener to the table header.
367 *
368 * @param table the table to which header the listener is to be added
369 */
370 public void addMouseListenerToHeaderInTable(JTable table, final Object[] ao) {
371 final TableSorter sorter = this;
372 final JTable tableView = table;
373 final TableEntryDescriptor ted = getModel().getEntryDescriptor();
374 ascending = true;
375 tableView.setColumnSelectionAllowed(false);
376 MouseAdapter listMouseListener = new MouseAdapter() {
377 int activeColumn = -1; //no active column yet (prevent table from being reverse ordered)
378 public void mouseClicked(MouseEvent e) {
379 TableColumnModel columnModel = tableView.getColumnModel();
380 int viewColumn = columnModel.getColumnIndexAtX(e.getX());
381 int column = tableView.convertColumnIndexToModel(viewColumn);
382 //initialize ascending everytime a different column is chosen
383 if (column != activeColumn) {
384 ascending = false;
385 activeColumn = column;
386 }
387 if (e.getClickCount() == 1 && column != -1) {
388 // check TED if sorting by this column is allowed
389 if (ted.canSortByColumn(column)) {
390 actualColumn = column;
391 ascending = !ascending;
392 ao[0] = new Integer(column);
393 ao[1] = new Boolean(ascending);
394 sorter.sortByColumn(actualColumn, ascending);
395 // Fire Tableheader changed
396 tableView.getTableHeader().resizeAndRepaint();
397 }
398 }
399 }
400 };
401 JTableHeader th = tableView.getTableHeader();
402 th.addMouseListener(listMouseListener);
403 }
404 }