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 }