001 package net.provision.soap; 002 003 import java.awt.Color; 004 import java.awt.Component; 005 import java.awt.FontMetrics; 006 import java.awt.Graphics; 007 import java.awt.Polygon; 008 import java.awt.event.*; 009 010 import java.util.ArrayList; 011 import java.util.Arrays; 012 import java.util.Comparator; 013 import java.util.Date; 014 import java.util.HashMap; 015 import java.util.Iterator; 016 import java.util.List; 017 import java.util.Map; 018 019 import javax.swing.*; 020 import javax.swing.event.*; 021 import javax.swing.table.*; 022 023 024 /** 025 * TableSorter is a decorator for TableModels; adding sorting functionality to a 026 * supplied TableModel. TableSorter does not store or copy the data in its TableModel; 027 * instead it maintains a map from the row indexes of the view to the row indexes of 028 * the model. As requests are made of the sorter (like getValueAt(row, col)) they are 029 * passed to the underlying model after the row numbers have been translated via the 030 * internal mapping array. This way, the TableSorter appears to hold another copy of 031 * the table with the rows in a different order. TableSorter registers itself as a 032 * listener to the underlying model, just as the JTable itself would. Events recieved 033 * from the model are examined, sometimes manipulated (typically widened), and then 034 * passed on to the TableSorter's listeners (typically the JTable). If a change to the 035 * model has invalidated the order of TableSorter's rows, a note of this is made and 036 * the sorter will resort the rows the next time a value is requested. When the 037 * tableHeader property is set, either by using the setTableHeader() method or the two 038 * argument constructor, the table header may be used as a complete UI for 039 * TableSorter. The default renderer of the tableHeader is decorated with a renderer 040 * that indicates the sorting status of each column. In addition, a mouse listener is 041 * installed with the following behavior: 042 * 043 * <ul> 044 * <li> 045 * Mouse-click: Clears the sorting status of all other columns and advances the sorting 046 * status of that column through three values: {NOT_SORTED, ASCENDING, DESCENDING} 047 * (then back to NOT_SORTED again). 048 * </li> 049 * <li> 050 * SHIFT-mouse-click: Clears the sorting status of all other columns and cycles the 051 * sorting status of the column through the same three values, in the opposite order: 052 * {NOT_SORTED, DESCENDING, ASCENDING}. 053 * </li> 054 * <li> 055 * CONTROL-mouse-click and CONTROL-SHIFT-mouse-click: as above except that the changes 056 * to the column do not cancel the statuses of columns that are already sorting - 057 * giving a way to initiate a compound sort. 058 * </li> 059 * </ul> 060 * 061 * This is a long overdue rewrite of a class of the same name that first appeared in 062 * the swing table demos in 1997. 063 * 064 * @author Philip Milne 065 * @author Brendon McLean 066 * @author Dan van Enckevort 067 * @author Parwinder Sekhon 068 * @version 2.0 02/27/04 069 */ 070 public class TableSorter extends AbstractTableModel { 071 public static final int DESCENDING = -1; 072 public static final int NOT_SORTED = 0; 073 public static final int ASCENDING = 1; 074 private static Directive EMPTY_DIRECTIVE = new Directive(-1, NOT_SORTED); 075 public static final Comparator COMPARABLE_COMAPRATOR = new Comparator() { 076 public int compare(Object o1, Object o2) { 077 return ((Comparable)o1).compareTo(o2); 078 } 079 }; 080 081 public static final Comparator LEXICAL_COMPARATOR = new Comparator() { 082 public int compare(Object o1, Object o2) { 083 if(o1 instanceof Date) { 084 return ((Date)o1).compareTo((Date)o2); 085 } 086 087 return o1.toString().compareTo(o2.toString()); 088 } 089 }; 090 091 protected TableModel tableModel; 092 private JTableHeader tableHeader; 093 private List sortingColumns = new ArrayList(); 094 private Map columnComparators = new HashMap(); 095 private MouseListener mouseListener; 096 private TableModelListener tableModelListener; 097 private int[] modelToView; 098 private Row[] viewToModel; 099 100 public TableSorter() { 101 this.mouseListener = new MouseHandler(); 102 this.tableModelListener = new TableModelHandler(); 103 } 104 105 public TableSorter(TableModel tableModel) { 106 this(); 107 setTableModel(tableModel); 108 } 109 110 public TableSorter(TableModel tableModel, JTableHeader tableHeader) { 111 this(); 112 setTableHeader(tableHeader); 113 setTableModel(tableModel); 114 } 115 116 /** 117 * DOCUMENT ME! 118 * 119 * @param row DOCUMENT ME! 120 * @param column DOCUMENT ME! 121 * 122 * @return DOCUMENT ME! 123 */ 124 public boolean isCellEditable(int row, int column) { 125 return tableModel.isCellEditable(modelIndex(row), column); 126 } 127 128 /** 129 * DOCUMENT ME! 130 * 131 * @param column DOCUMENT ME! 132 * 133 * @return DOCUMENT ME! 134 */ 135 public Class getColumnClass(int column) { 136 return tableModel.getColumnClass(column); 137 } 138 139 /** 140 * DOCUMENT ME! 141 * 142 * @param type DOCUMENT ME! 143 * @param comparator DOCUMENT ME! 144 */ 145 public void setColumnComparator(Class type, Comparator comparator) { 146 if(comparator == null) { 147 columnComparators.remove(type); 148 } else { 149 columnComparators.put(type, comparator); 150 } 151 } 152 153 /** 154 * DOCUMENT ME! 155 * 156 * @return DOCUMENT ME! 157 */ 158 public int getColumnCount() { 159 return (tableModel == null) ? 0 : tableModel.getColumnCount(); 160 } 161 162 /** 163 * DOCUMENT ME! 164 * 165 * @param column DOCUMENT ME! 166 * 167 * @return DOCUMENT ME! 168 */ 169 public String getColumnName(int column) { 170 return tableModel.getColumnName(column); 171 } 172 173 // TableModel interface methods 174 public int getRowCount() { 175 return (tableModel == null) ? 0 : tableModel.getRowCount(); 176 } 177 178 /** 179 * DOCUMENT ME! 180 * 181 * @return DOCUMENT ME! 182 */ 183 public boolean isSorting() { 184 return sortingColumns.size() != 0; 185 } 186 187 /** 188 * DOCUMENT ME! 189 * 190 * @param column DOCUMENT ME! 191 * @param status DOCUMENT ME! 192 */ 193 public void setSortingStatus(int column, int status) { 194 Directive directive = getDirective(column); 195 196 if(directive != EMPTY_DIRECTIVE) { 197 sortingColumns.remove(directive); 198 } 199 200 if(status != NOT_SORTED) { 201 sortingColumns.add(new Directive(column, status)); 202 } 203 204 sortingStatusChanged(); 205 } 206 207 /** 208 * DOCUMENT ME! 209 * 210 * @param column DOCUMENT ME! 211 * 212 * @return DOCUMENT ME! 213 */ 214 public int getSortingStatus(int column) { 215 return getDirective(column).direction; 216 } 217 218 /** 219 * DOCUMENT ME! 220 * 221 * @param tableHeader DOCUMENT ME! 222 */ 223 public void setTableHeader(JTableHeader tableHeader) { 224 if(this.tableHeader != null) { 225 this.tableHeader.removeMouseListener(mouseListener); 226 227 TableCellRenderer defaultRenderer = this.tableHeader.getDefaultRenderer(); 228 229 if(defaultRenderer instanceof SortableHeaderRenderer) { 230 this.tableHeader.setDefaultRenderer(((SortableHeaderRenderer)defaultRenderer).tableCellRenderer); 231 } 232 } 233 234 this.tableHeader = tableHeader; 235 236 if(this.tableHeader != null) { 237 this.tableHeader.addMouseListener(mouseListener); 238 this.tableHeader.setDefaultRenderer(new SortableHeaderRenderer( 239 this.tableHeader.getDefaultRenderer())); 240 } 241 } 242 243 /** 244 * DOCUMENT ME! 245 * 246 * @return DOCUMENT ME! 247 */ 248 public JTableHeader getTableHeader() { 249 return tableHeader; 250 } 251 252 /** 253 * DOCUMENT ME! 254 * 255 * @param tableModel DOCUMENT ME! 256 */ 257 public void setTableModel(TableModel tableModel) { 258 if(this.tableModel != null) { 259 this.tableModel.removeTableModelListener(tableModelListener); 260 } 261 262 this.tableModel = tableModel; 263 264 if(this.tableModel != null) { 265 this.tableModel.addTableModelListener(tableModelListener); 266 } 267 268 clearSortingState(); 269 fireTableStructureChanged(); 270 } 271 272 /** 273 * DOCUMENT ME! 274 * 275 * @return DOCUMENT ME! 276 */ 277 public TableModel getTableModel() { 278 return tableModel; 279 } 280 281 /** 282 * DOCUMENT ME! 283 * 284 * @param aValue DOCUMENT ME! 285 * @param row DOCUMENT ME! 286 * @param column DOCUMENT ME! 287 */ 288 public void setValueAt(Object aValue, int row, int column) { 289 tableModel.setValueAt(aValue, modelIndex(row), column); 290 } 291 292 /** 293 * DOCUMENT ME! 294 * 295 * @param row DOCUMENT ME! 296 * @param column DOCUMENT ME! 297 * 298 * @return DOCUMENT ME! 299 */ 300 public Object getValueAt(int row, int column) { 301 return tableModel.getValueAt(modelIndex(row), column); 302 } 303 304 /** 305 * DOCUMENT ME! 306 * 307 * @param viewIndex DOCUMENT ME! 308 * 309 * @return DOCUMENT ME! 310 */ 311 public int modelIndex(int viewIndex) { 312 return getViewToModel()[viewIndex].modelIndex; 313 } 314 315 /** 316 * DOCUMENT ME! 317 * 318 * @param column DOCUMENT ME! 319 * 320 * @return DOCUMENT ME! 321 */ 322 protected Comparator getComparator(int column) { 323 Class columnType = tableModel.getColumnClass(column); 324 Comparator comparator = (Comparator)columnComparators.get(columnType); 325 326 if(comparator != null) { 327 return comparator; 328 } 329 330 if(Comparable.class.isAssignableFrom(columnType)) { 331 return COMPARABLE_COMAPRATOR; 332 } 333 334 return LEXICAL_COMPARATOR; 335 } 336 337 /** 338 * DOCUMENT ME! 339 * 340 * @param column DOCUMENT ME! 341 * @param size DOCUMENT ME! 342 * 343 * @return DOCUMENT ME! 344 */ 345 protected Icon getHeaderRendererIcon(int column, int size) { 346 Directive directive = getDirective(column); 347 348 if(directive == EMPTY_DIRECTIVE) { 349 return null; 350 } 351 352 return new Arrow(directive.direction == DESCENDING, size, 353 sortingColumns.indexOf(directive)); 354 } 355 356 /** 357 * DOCUMENT ME! 358 * 359 * @param column DOCUMENT ME! 360 * 361 * @return DOCUMENT ME! 362 */ 363 private Directive getDirective(int column) { 364 for(int i = 0; i < sortingColumns.size(); i++) { 365 Directive directive = (Directive)sortingColumns.get(i); 366 367 if(directive.column == column) { 368 return directive; 369 } 370 } 371 372 return EMPTY_DIRECTIVE; 373 } 374 375 /** 376 * DOCUMENT ME! 377 * 378 * @return DOCUMENT ME! 379 */ 380 private int[] getModelToView() { 381 if(modelToView == null) { 382 int n = getViewToModel().length; 383 modelToView = new int[n]; 384 385 for(int i = 0; i < n; i++) { 386 modelToView[modelIndex(i)] = i; 387 } 388 } 389 390 return modelToView; 391 } 392 393 /** 394 * DOCUMENT ME! 395 * 396 * @return DOCUMENT ME! 397 */ 398 private Row[] getViewToModel() { 399 if(viewToModel == null) { 400 int tableModelRowCount = tableModel.getRowCount(); 401 viewToModel = new Row[tableModelRowCount]; 402 403 for(int row = 0; row < tableModelRowCount; row++) { 404 viewToModel[row] = new Row(row); 405 } 406 407 if(isSorting()) { 408 Arrays.sort(viewToModel); 409 } 410 } 411 412 return viewToModel; 413 } 414 415 /** 416 * DOCUMENT ME! 417 */ 418 private void cancelSorting() { 419 sortingColumns.clear(); 420 sortingStatusChanged(); 421 } 422 423 /** 424 * DOCUMENT ME! 425 */ 426 private void clearSortingState() { 427 viewToModel = null; 428 modelToView = null; 429 } 430 431 /** 432 * DOCUMENT ME! 433 */ 434 private void sortingStatusChanged() { 435 clearSortingState(); 436 fireTableDataChanged(); 437 438 if(tableHeader != null) { 439 tableHeader.repaint(); 440 } 441 } 442 443 /** 444 * DOCUMENT ME! 445 * 446 * @author $author$ 447 * @version $Revision$ 448 */ 449 private static class Arrow implements Icon { 450 private boolean descending; 451 private int priority; 452 private int size; 453 454 public Arrow(boolean descending, int size, int priority) { 455 this.descending = descending; 456 this.size = size; 457 this.priority = priority; 458 } 459 460 /** 461 * DOCUMENT ME! 462 * 463 * @return DOCUMENT ME! 464 */ 465 public int getIconHeight() { 466 return size; 467 } 468 469 /** 470 * DOCUMENT ME! 471 * 472 * @return DOCUMENT ME! 473 */ 474 public int getIconWidth() { 475 return size; 476 } 477 478 /** 479 * DOCUMENT ME! 480 * 481 * @param c DOCUMENT ME! 482 * @param g DOCUMENT ME! 483 * @param x DOCUMENT ME! 484 * @param y DOCUMENT ME! 485 */ 486 public void paintIcon(Component c, Graphics g, int x, int y) { 487 // modified by Brett on 7/14/2004 to make arrow a better color 488 // Color color = c == null ? Color.GRAY : c.getBackground(); 489 // // In a compound sort, make each succesive triangle 20% 490 // // smaller than the previous one. 491 // int dx = (int)(size/2*Math.pow(0.8, priority)); 492 // int dy = descending ? dx : -dx; 493 // // Align icon (roughly) with font baseline. 494 // y = y + 5*size/6 + (descending ? -dy : 0); 495 // int shift = descending ? 1 : -1; 496 // g.translate(x, y); 497 // 498 // // Right diagonal. 499 // g.setColor(color.darker()); 500 // g.drawLine(dx / 2, dy, 0, 0); 501 // g.drawLine(dx / 2, dy + shift, 0, shift); 502 // 503 // // Left diagonal. 504 // g.setColor(color.brighter()); 505 // g.drawLine(dx / 2, dy, dx, 0); 506 // g.drawLine(dx / 2, dy + shift, dx, shift); 507 // 508 // // Horizontal line. 509 // if (descending) { 510 // g.setColor(color.darker().darker()); 511 // } else { 512 // g.setColor(color.brighter().brighter()); 513 // } 514 // g.drawLine(dx, 0, 0, 0); 515 // 516 // g.setColor(color); 517 // g.translate(-x, -y); 518 // Override base size with a value calculated from the 519 // component's font. 520 updateSize(c); 521 522 Color color = (c == null) ? Color.BLACK : c.getForeground(); 523 g.setColor(color); 524 525 int npoints = 3; 526 int[] xpoints = new int[]{ 0, size / 2, size }; 527 int[] ypoints = descending ? new int[]{ 0, size, 0 } : new int[]{ size, 0, size }; 528 529 Polygon triangle = new Polygon(xpoints, ypoints, npoints); 530 531 // Center icon vertically within the column heading label. 532 int dy = (c.getHeight() - size) / 2; 533 534 g.translate(x, dy); 535 g.drawPolygon(triangle); 536 g.fillPolygon(triangle); 537 g.translate(-x, -dy); 538 } 539 540 /** 541 * DOCUMENT ME! 542 * 543 * @param c DOCUMENT ME! 544 */ 545 private void updateSize(Component c) { 546 if(c != null) { 547 FontMetrics fm = c.getFontMetrics(c.getFont()); 548 int baseHeight = fm.getAscent(); 549 550 // In a compound sort, make each succesive triangle 20% 551 // smaller than the previous one. 552 size = (int)((baseHeight * 3) / 4 * Math.pow(0.8, priority)); 553 } 554 } 555 } 556 557 /** 558 * DOCUMENT ME! 559 * 560 * @author $author$ 561 * @version $Revision$ 562 */ 563 private static class Directive { 564 private int column; 565 private int direction; 566 567 public Directive(int column, int direction) { 568 this.column = column; 569 this.direction = direction; 570 } 571 } 572 573 /** 574 * DOCUMENT ME! 575 * 576 * @author $author$ 577 * @version $Revision$ 578 */ 579 private class MouseHandler extends MouseAdapter { 580 /** 581 * DOCUMENT ME! 582 * 583 * @param e DOCUMENT ME! 584 */ 585 public void mouseClicked(MouseEvent e) { 586 JTableHeader h = (JTableHeader)e.getSource(); 587 TableColumnModel columnModel = h.getColumnModel(); 588 int viewColumn = columnModel.getColumnIndexAtX(e.getX()); 589 int column = columnModel.getColumn(viewColumn).getModelIndex(); 590 591 if(column != -1) { 592 int status = getSortingStatus(column); 593 594 if(!e.isControlDown()) { 595 cancelSorting(); 596 } 597 598 // Cycle the sorting states through {NOT_SORTED, ASCENDING, DESCENDING} or 599 // {NOT_SORTED, DESCENDING, ASCENDING} depending on whether shift is pressed. 600 status = status + (e.isShiftDown() ? (-1) : 1); 601 status = ((status + 4) % 3) - 1; // signed mod, returning {-1, 0, 1} 602 setSortingStatus(column, status); 603 } 604 } 605 } 606 607 // Helper classes 608 private class Row implements Comparable { 609 private int modelIndex; 610 611 public Row(int index) { 612 this.modelIndex = index; 613 } 614 615 /** 616 * DOCUMENT ME! 617 * 618 * @param o DOCUMENT ME! 619 * 620 * @return DOCUMENT ME! 621 */ 622 public int compareTo(Object o) { 623 int row1 = modelIndex; 624 int row2 = ((Row)o).modelIndex; 625 626 for(Iterator it = sortingColumns.iterator(); it.hasNext();) { 627 Directive directive = (Directive)it.next(); 628 int column = directive.column; 629 Object o1 = tableModel.getValueAt(row1, column); 630 Object o2 = tableModel.getValueAt(row2, column); 631 632 int comparison = 0; 633 634 // Define null less than everything, except null. 635 if((o1 == null) && (o2 == null)) { 636 comparison = 0; 637 } else if(o1 == null) { 638 comparison = -1; 639 } else if(o2 == null) { 640 comparison = 1; 641 } else { 642 comparison = getComparator(column).compare(o1, o2); 643 } 644 645 if(comparison != 0) { 646 return (directive.direction == DESCENDING) ? (-comparison) : comparison; 647 } 648 } 649 650 return 0; 651 } 652 } 653 654 /** 655 * DOCUMENT ME! 656 * 657 * @author $author$ 658 * @version $Revision$ 659 */ 660 private class SortableHeaderRenderer implements TableCellRenderer { 661 private TableCellRenderer tableCellRenderer; 662 663 public SortableHeaderRenderer(TableCellRenderer tableCellRenderer) { 664 this.tableCellRenderer = tableCellRenderer; 665 } 666 667 /** 668 * DOCUMENT ME! 669 * 670 * @param table DOCUMENT ME! 671 * @param value DOCUMENT ME! 672 * @param isSelected DOCUMENT ME! 673 * @param hasFocus DOCUMENT ME! 674 * @param row DOCUMENT ME! 675 * @param column DOCUMENT ME! 676 * 677 * @return DOCUMENT ME! 678 */ 679 public Component getTableCellRendererComponent(JTable table, Object value, 680 boolean isSelected, boolean hasFocus, int row, int column) { 681 Component c = tableCellRenderer.getTableCellRendererComponent(table, value, 682 isSelected, hasFocus, row, column); 683 684 if(c instanceof JLabel) { 685 JLabel l = (JLabel)c; 686 l.setHorizontalTextPosition(JLabel.LEFT); 687 688 int modelColumn = table.convertColumnIndexToModel(column); 689 l.setIcon(getHeaderRendererIcon(modelColumn, l.getFont().getSize())); 690 } 691 692 return c; 693 } 694 } 695 696 /** 697 * DOCUMENT ME! 698 * 699 * @author $author$ 700 * @version $Revision$ 701 */ 702 private class TableModelHandler implements TableModelListener { 703 /** 704 * DOCUMENT ME! 705 * 706 * @param e DOCUMENT ME! 707 */ 708 public void tableChanged(TableModelEvent e) { 709 // If we're not sorting by anything, just pass the event along. 710 if(!isSorting()) { 711 clearSortingState(); 712 fireTableChanged(e); 713 714 return; 715 } 716 717 // If the table structure has changed, cancel the sorting; the 718 // sorting columns may have been either moved or deleted from 719 // the model. 720 if(e.getFirstRow() == TableModelEvent.HEADER_ROW) { 721 cancelSorting(); 722 fireTableChanged(e); 723 724 return; 725 } 726 727 // We can map a cell event through to the view without widening 728 // when the following conditions apply: 729 // 730 // a) all the changes are on one row (e.getFirstRow() == e.getLastRow()) and, 731 // b) all the changes are in one column (column != TableModelEvent.ALL_COLUMNS) and, 732 // c) we are not sorting on that column (getSortingStatus(column) == NOT_SORTED) and, 733 // d) a reverse lookup will not trigger a sort (modelToView != null) 734 // 735 // Note: INSERT and DELETE events fail this test as they have column == ALL_COLUMNS. 736 // 737 // The last check, for (modelToView != null) is to see if modelToView 738 // is already allocated. If we don't do this check; sorting can become 739 // a performance bottleneck for applications where cells 740 // change rapidly in different parts of the table. If cells 741 // change alternately in the sorting column and then outside of 742 // it this class can end up re-sorting on alternate cell updates - 743 // which can be a performance problem for large tables. The last 744 // clause avoids this problem. 745 int column = e.getColumn(); 746 747 if((e.getFirstRow() == e.getLastRow()) && 748 (column != TableModelEvent.ALL_COLUMNS) && 749 (getSortingStatus(column) == NOT_SORTED) && (modelToView != null)) { 750 int viewIndex = getModelToView()[e.getFirstRow()]; 751 fireTableChanged(new TableModelEvent(TableSorter.this, viewIndex, 752 viewIndex, column, e.getType())); 753 754 return; 755 } 756 757 // Something has happened to the data that may have invalidated the row order. 758 clearSortingState(); 759 fireTableDataChanged(); 760 761 return; 762 } 763 } 764 }