001/* ---------------------------------------------------------------------------- 002 The Kiwi Toolkit - A Java Class Library 003 Copyright (C) 1998-2004 Mark A. Lindner 004 005 This library is free software; you can redistribute it and/or 006 modify it under the terms of the GNU General Public License as 007 published by the Free Software Foundation; either version 2 of the 008 License, or (at your option) any later version. 009 010 This library is distributed in the hope that it will be useful, 011 but WITHOUT ANY WARRANTY; without even the implied warranty of 012 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 013 General Public License for more details. 014 015 You should have received a copy of the GNU General Public License 016 along with this library; if not, write to the Free Software 017 Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 018 02111-1307, USA. 019 020 The author may be contacted at: mark_a_lindner@yahoo.com 021 ---------------------------------------------------------------------------- 022 $Log: TableSorter.java,v $ 023 Revision 1.12 2004/05/31 07:29:26 markl 024 improved sorting capability 025 026 Revision 1.11 2004/03/16 06:43:39 markl 027 LocaleManager method change 028 029 Revision 1.10 2003/01/19 09:32:30 markl 030 Modifications to allow toggle sort (first click causes ascending sort, and 031 second click to same column causes descending sort, etc.). 032 033 Revision 1.9 2001/03/20 00:54:56 markl 034 Fixed deprecated calls. 035 036 Revision 1.8 2001/03/12 04:54:32 markl 037 Source code cleanup. 038 039 Revision 1.7 2000/06/09 01:48:29 markl 040 Modified to use Collator to compare strings. 041 042 Revision 1.6 1999/07/25 13:41:26 markl 043 Minor internal fixes. 044 045 Revision 1.5 1999/07/16 07:11:35 markl 046 Added support for Holder objects. 047 048 Revision 1.4 1999/07/05 08:07:15 markl 049 Added logic to maintain the set of highlighted items when sorting is 050 performed, and disabled sorting if the table is not enabled. 051 052 Revision 1.3 1999/05/05 06:39:24 markl 053 Added logic to sync the highlighted rows with the sort. 054 055 Revision 1.2 1999/01/10 03:12:19 markl 056 added GPL header & RCS tag 057 ---------------------------------------------------------------------------- 058*/ 059 060/* 061 * @(#)TableSorter.java 1.5 97/12/17 062 * 063 * Copyright (c) 1997 Sun Microsystems, Inc. All Rights Reserved. 064 * 065 * This software is the confidential and proprietary information of Sun 066 * Microsystems, Inc. ("Confidential Information"). You shall not 067 * disclose such Confidential Information and shall use it only in 068 * accordance with the terms of the license agreement you entered into 069 * with Sun. 070 * 071 * SUN MAKES NO REPRESENTATIONS OR WARRANTIES ABOUT THE SUITABILITY OF THE 072 * SOFTWARE, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE 073 * IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR 074 * PURPOSE, OR NON-INFRINGEMENT. SUN SHALL NOT BE LIABLE FOR ANY DAMAGES 075 * SUFFERED BY LICENSEE AS A RESULT OF USING, MODIFYING OR DISTRIBUTING 076 * THIS SOFTWARE OR ITS DERIVATIVES. 077 * 078 */ 079 080package kiwi.ui.model; 081 082import java.awt.event.*; 083import java.text.*; 084import java.util.*; 085import javax.swing.JTable; 086import javax.swing.event.TableModelEvent; 087import javax.swing.table.*; 088 089import kiwi.util.*; 090 091/** 092 * A sorter for <code>TableModel</code>s. The sorter has a model (conforming 093 * to <code>TableModel</code>) and itself implements <code>TableModel</code>. 094 * <code>TableSorter</code> does not store or copy the data in the 095 * <code>TableModel</code>; instead it maintains an array of integers which it 096 * keeps the same size as the number of rows in its model. When the model 097 * changes it notifies the sorter that something has changed eg. "rowsAdded" 098 * so that its internal array of integers can be reallocated. As requests are 099 * made of the sorter (like <code>getValueAt(row, col)</code>) it redirects 100 * them to its model via the mapping array. That way the 101 * <code>TableSorter</code> appears to hold another copy of the table with 102 * the rows in a different order. The sorting algorthm used is stable which 103 * means that it does not move around rows when its comparison function 104 * returns 0 to denote that they are equivalent. 105 * 106 * @author Philip Milne 107 * @author Mark Lindner 108 */ 109 110public class TableSorter extends ProxyTableModel 111 { 112 private int indexes[]; 113 private Vector sortingColumns = new Vector(); 114 private boolean ascending = true; 115 private int compares; 116 private JTable tableView; 117 private int sortedColumn = -1; 118 private boolean sortedAscending = true; 119 120 /** Construct a new <code>TableSorter</code>. */ 121 122 public TableSorter() 123 { 124 indexes = new int[0]; // For consistency. 125 } 126 127 /** Construct a new <code>TableSorter</code> with a specified data model.. 128 * 129 * @param model The <code>TableModel</code> to use. 130 */ 131 132 public TableSorter(TableModel model) 133 { 134 setModel(model); 135 } 136 137 /** Return the index of the given row in the <i>unsorted</i> model that this 138 * model wraps. This method is useful for determining which row in the 139 * actual model is mapped to the currently selected row in the sorted 140 * model. 141 * 142 * @see #getReverseRowTranslation 143 */ 144 145 public int getRowTranslation(int row) 146 { 147 return(indexes[row]); 148 } 149 150 /** Return the visible index of the given row in the <i>unsorted</i> model. 151 * This method performs the reverse of <code>getRowTranslation()</code> 152 * 153 * @see #getRowTranslation 154 */ 155 156 public int getReverseRowTranslation(int row) 157 { 158 for(int i = 0; i < indexes.length; i++) 159 if(indexes[i] == row) 160 return(i); 161 162 return(-1); 163 } 164 165 /** Set the <code>TableModel</code> for this table sorter. 166 * 167 * @param model The <code>TableModel</code> to use. 168 */ 169 170 public void setModel(TableModel model) 171 { 172 super.setModel(model); 173 reallocateIndexes(); 174 } 175 176 /** Get the value at the given row and column of the unsorted table. 177 * 178 * @param row The row. 179 * @param col The column. 180 * 181 * @return The object at the given position in the <i>unsorted</i> model. 182 */ 183 184 public Object getValueAt(int row, int col) 185 { 186 // The mapping only affects the contents of the data rows. 187 // Pass all requests to these rows through the mapping array: "indexes". 188 189 checkModel(); 190 return model.getValueAt(indexes[row], col); 191 } 192 193 /** Set the value at the given row and column of the unsorted table. 194 * 195 * @param row The row. 196 * @param col The column. 197 * @param value The new value. 198 * 199 */ 200 201 public void setValueAt(Object value, int row, int col) 202 { 203 checkModel(); 204 model.setValueAt(value, indexes[row], col); 205 } 206 207 /** Sort a column in the table in ascending order. 208 * 209 * @param column The index of the column to sort. 210 */ 211 212 public void sortByColumn(int column) 213 { 214 boolean asc = true; 215 216 // System.out.println("sorting by: " + column + ", and last time col was: " + sortedColumn); 217 218 if(column == sortedColumn) 219 { 220 // System.out.println("asc is now: " + (! sortedAscending)); 221 asc = ! sortedAscending; 222 } 223 224 sortByColumn(column, asc); 225 } 226 227 /** Sort a column in the table. 228 * 229 * @param column The index of the column to sort. 230 * @param ascending If <code>true</code>, sorts in ascending order; 231 * otherwise, sorts in descending order. 232 */ 233 234 public void sortByColumn(int column, boolean ascending) 235 { 236 boolean bigTable = (getRowCount() > 10000); 237 238 if(bigTable) 239 KiwiUtils.busyOn(tableView); 240 241 this.sortedColumn = column; 242 this.sortedAscending = ascending; 243 this.ascending = ascending; 244 sortingColumns.removeAllElements(); 245 sortingColumns.addElement(new Integer(column)); 246 sort(this); 247 248 super.tableChanged(new TableModelEvent(this)); 249 250 if(bigTable) 251 KiwiUtils.busyOff(tableView); 252 } 253 254 /** Get the index of the last column that the table was sorted on. 255 * 256 * @return The column index, or -1 if there was no previous sort. 257 * 258 * @since Kiwi 1.4 259 */ 260 261 public int getSortedColumn() 262 { 263 return(sortedColumn); 264 } 265 266 /** Determine if the last column sort was ascending or descending. 267 * 268 * @return <code>true</code> if the sort was ascending, and 269 * <code>false</code> otherwise. This value is only meaningful if 270 * <code>getSortedColumn()</code> returns a non-negative value. 271 * 272 * @since Kiwi 1.4 273 */ 274 275 public boolean isSortedAscending() 276 { 277 return(sortedAscending); 278 } 279 280 /** Handle <i>table changed</i> events. */ 281 282 public void tableChanged(TableModelEvent e) 283 { 284 // System.out.println("Sorter: tableChanged"); 285 286 reallocateIndexes(); 287 super.tableChanged(e); 288 } 289 290 /** Add a mouse listener to the <code>JTable</code> to trigger a table sort 291 * when a column heading is clicked. A shift click causes the column to 292 * be sorted in descending order, whereas a simple click causes the column 293 * to be sorted in ascending order. 294 * 295 * @param table The <code>JTable</code> to listen for events on. 296 */ 297 298 public void registerTableHeaderListener(JTable table) 299 { 300 final TableSorter sorter = this; 301 tableView = table; 302 303 tableView.setColumnSelectionAllowed(false); 304 MouseAdapter listMouseListener = new MouseAdapter() 305 { 306 public void mouseClicked(MouseEvent e) 307 { 308 // If the table is disabled, we will ignore the mouse event, 309 // effectively preventing sorting. 310 311 if(!tableView.isEnabled() || tableView.isEditing()) 312 return; 313 314 // Save a list of highlighted rows. I don't call getSelectedRows() 315 // directly since that would force me to have *two* working arrays. 316 317 int hrows[] = new int[tableView.getSelectedRowCount()]; 318 int nr = tableView.getRowCount(); 319 for(int c = 0, i = 0; i < nr ; i++) 320 if(tableView.isRowSelected(i)) 321 hrows[c++] = getRowTranslation(i); 322 323 // figure out which column was selected, & sort it 324 325 TableColumnModel columnModel = tableView.getColumnModel(); 326 int viewColumn = columnModel.getColumnIndexAtX(e.getX()); 327 int column = tableView.convertColumnIndexToModel(viewColumn); 328 if(e.getClickCount() == 1 && column != -1) 329 { 330 /* int shiftPressed = e.getModifiers() & InputEvent.SHIFT_MASK; 331 boolean ascending = (shiftPressed == 0); 332 sorter.sortByColumn(column, ascending); */ 333 334 sorter.sortByColumn(column); 335 } 336 337 // now rehighlight the rows in their new positions 338 339 tableView.clearSelection(); 340 for(int i = 0; i < hrows.length; i++) 341 { 342 int r = getReverseRowTranslation(hrows[i]); 343 tableView.addRowSelectionInterval(r, r); 344 } 345 346 } 347 }; 348 349 JTableHeader th = tableView.getTableHeader(); 350 th.addMouseListener(listMouseListener); 351 } 352 353 /* internal code follows */ 354 355 private int compareRowsByColumn(int row1, int row2, int column) 356 { 357 Class type = model.getColumnClass(column); 358 Collator collator = LocaleManager.getDefault().getCollator(); 359 360 TableModel data = model; 361 362 // Check for nulls 363 364 Object o1 = data.getValueAt(row1, column); 365 Object o2 = data.getValueAt(row2, column); 366 367 // If both values are null return 0 368 369 if((o1 == null) && (o2 == null)) 370 { 371 return(0); 372 } 373 else if(o1 == null) 374 { 375 // Define null less than everything. 376 return(-1); 377 } 378 else if(o2 == null) 379 { 380 return(1); 381 } 382 383 /* We copy all returned values from the getValue call in case an optimised 384 * model is reusing one object to return many values. The Number 385 * subclasses in the JDK are immutable and so will not be used in this way 386 * but other subclasses of Number might want to do this to save space and 387 * avoid unnecessary heap allocation. 388 */ 389 390 if(type.getSuperclass() == Number.class) 391 { 392 Number n1 = (Number)o1; 393 double d1 = n1.doubleValue(); 394 Number n2 = (Number)o2; 395 double d2 = n2.doubleValue(); 396 397 return((d1 < d2) ? -1 : ((d1 > d2) ? 1 : 0)); 398 } 399 else if((type.getSuperclass() == ValueHolder.class) 400 || (type.getClass() == Date.class) 401 || (type.getClass() == String.class)) 402 { 403 Comparable c1 = (Comparable)o1; 404 Comparable c2 = (Comparable)o2; 405 406 return(((Comparable)c1).compareTo(c2)); 407 } 408 else if(type == Boolean.class) 409 { 410 Boolean bb1 = (Boolean)o1; 411 Boolean bb2 = (Boolean)o2; 412 413 boolean b1 = bb1.booleanValue(); 414 boolean b2 = bb2.booleanValue(); 415 416 return(b1 == b2 ? 0 : (b1 ? 1 : -1)); 417 } 418 else 419 { 420 if(o1 instanceof Comparable) 421 return(((Comparable)o1).compareTo(o2)); 422 else 423 { 424 String s1 = o1.toString(); 425 String s2 = o2.toString(); 426 427 return(s1.compareTo(s2)); 428 } 429 } 430 } 431 432 private int compare(int row1, int row2) 433 { 434 compares++; 435 for(int level = 0; level < sortingColumns.size(); level++) 436 { 437 Integer column = (Integer)sortingColumns.elementAt(level); 438 int result = compareRowsByColumn(row1, row2, column.intValue()); 439 if(result != 0) 440 return ascending ? result : -result; 441 } 442 return 0; 443 } 444 445 /* 446 */ 447 448 private void reallocateIndexes() 449 { 450 int rowCount = model.getRowCount(); 451 452 // Set up a new array of indexes with the right number of elements 453 // for the new data model. 454 455 indexes = new int[rowCount]; 456 457 // Initialise with the identity mapping. 458 459 for(int row = 0; row < rowCount; row++) 460 indexes[row] = row; 461 } 462 463 /* 464 */ 465 466 private void checkModel() 467 { 468 if(indexes.length != model.getRowCount()) 469 { 470 System.err.println("Sorter not informed of a change in model."); 471 } 472 } 473 474 /* 475 */ 476 477 private void sort(Object sender) 478 { 479 checkModel(); 480 481 compares = 0; 482 483 shuttlesort((int[])indexes.clone(), indexes, 0, indexes.length); 484 } 485 486 /* 487 public void n2sort() 488 { 489 for(int i = 0; i < getRowCount(); i++) { 490 for(int j = i+1; j < getRowCount(); j++) { 491 if (compare(indexes[i], indexes[j]) == -1) { 492 swap(i, j); 493 } 494 } 495 } 496 } 497 */ 498 499 // This is a home-grown implementation which we have not had time to research 500 // - it may perform poorly in some circumstances. It requires twice the space 501 // of an in-place algorithm and makes NlogN assigments shuttling the values 502 // between the two arrays. The number of compares appears to vary between N-1 503 // and NlogN depending on the initial order but the main reason for using it 504 // here is that, unlike qsort, it is stable. 505 506 private void shuttlesort(int from[], int to[], int low, int high) 507 { 508 if (high - low < 2) 509 { 510 return; 511 } 512 513 int middle = (low + high)/2; 514 shuttlesort(to, from, low, middle); 515 shuttlesort(to, from, middle, high); 516 517 int p = low; 518 int q = middle; 519 520 /* This is an optional short-cut; at each recursive call, check to see if 521 * the elements in this subset are already ordered. If so, no further 522 * comparisons are needed; the sub-array can just be copied. The array 523 * must be copied rather than assigned otherwise sister calls in the 524 * recursion might get out of sinc. When the number of elements is three 525 * they are partitioned so that the first set, [low, mid), has one element 526 * and the second, [mid, high), has two. We skip the optimisation when the 527 * number of elements is three or less as the first compare in the normal 528 * merge will produce the same sequence of steps. This optimisation seems 529 * to be worthwhile for partially ordered lists but some analysis is 530 * needed to find out how the performance drops to Nlog(N) as the initial 531 * order diminishes - it may drop very quickly. 532 */ 533 534 if(high - low >= 4 && compare(from[middle-1], from[middle]) <= 0) 535 { 536 for(int i = low; i < high; i++) 537 { 538 to[i] = from[i]; 539 } 540 return; 541 } 542 543 // A normal merge. 544 545 for(int i = low; i < high; i++) 546 { 547 if(q >= high || (p < middle && compare(from[p], from[q]) <= 0)) 548 { 549 to[i] = from[p++]; 550 } 551 else 552 { 553 to[i] = from[q++]; 554 } 555 } 556 } 557 558 /* 559 */ 560 561 private void swap(int i, int j) 562 { 563 int tmp = indexes[i]; 564 indexes[i] = indexes[j]; 565 indexes[j] = tmp; 566 } 567 568 } 569 570/* end of source file */