001/* 002 * $Id: JXTableHeader.java 3960 2011-03-15 19:36:53Z kschaefe $ 003 * 004 * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle, 005 * Santa Clara, California 95054, U.S.A. All rights reserved. 006 * 007 * This library is free software; you can redistribute it and/or 008 * modify it under the terms of the GNU Lesser General Public 009 * License as published by the Free Software Foundation; either 010 * version 2.1 of the License, or (at your option) any later version. 011 * 012 * This library is distributed in the hope that it will be useful, 013 * but WITHOUT ANY WARRANTY; without even the implied warranty of 014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 015 * Lesser General Public License for more details. 016 * 017 * You should have received a copy of the GNU Lesser General Public 018 * License along with this library; if not, write to the Free Software 019 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 020 */ 021package org.jdesktop.swingx; 022 023import java.awt.Component; 024import java.awt.Dimension; 025import java.awt.event.MouseEvent; 026import java.beans.PropertyChangeEvent; 027import java.beans.PropertyChangeListener; 028import java.io.Serializable; 029import java.util.logging.Logger; 030 031import javax.swing.JTable; 032import javax.swing.SortOrder; 033import javax.swing.SwingUtilities; 034import javax.swing.event.MouseInputListener; 035import javax.swing.table.JTableHeader; 036import javax.swing.table.TableCellRenderer; 037import javax.swing.table.TableColumn; 038import javax.swing.table.TableColumnModel; 039 040import org.jdesktop.swingx.event.TableColumnModelExtListener; 041import org.jdesktop.swingx.plaf.LookAndFeelAddons; 042import org.jdesktop.swingx.plaf.TableHeaderAddon; 043import org.jdesktop.swingx.sort.SortController; 044import org.jdesktop.swingx.table.TableColumnExt; 045 046/** 047 * TableHeader with extended functionality if associated Table is of 048 * type JXTable.<p> 049 * 050 * <h2> Extended user interaction </h2> 051 * 052 * <ul> 053 * <li> Supports column pack (== auto-resize to exactly fit the contents) 054 * on double-click in resize region. 055 * <li> Configurable to resort a column on the second click of a mouseClicked event 056 * (feature request #271-swingx) 057 * <li> Does its best to not sort if the mouse click happens in the resize region. 058 * <li> Supports horizontal auto-scroll if a column is dragged outside visible rectangle. 059 * This feature is enabled if the autoscrolls property is true. The default is false 060 * (because of Issue #788-swingx which still isn't fixed for jdk1.6). 061 * </ul> 062 * 063 * Note: extended sort and resize related functionality is fully effective only if the header's 064 * table is of type JXTable and has control over the row sorter, that is the row sorter 065 * is of type SortController. 066 * 067 * <h2> Extended functionality </h2> 068 * 069 * <ul> 070 * <li> Listens to TableColumn propertyChanges to update itself accordingly. 071 * <li> Supports per-column header ToolTips. 072 * <li> Guarantees reasonable minimal height > 0 for header preferred height. 073* </ul> 074 * 075 * 076 * @author Jeanette Winzenburg 077 * 078 * @see JXTable#toggleSortOrder(int) 079 * @see JXTable#resetSortOrder() 080 * @see SortGestureRecognizer 081 */ 082public class JXTableHeader extends JTableHeader 083 implements TableColumnModelExtListener { 084 085 @SuppressWarnings("unused") 086 private static final Logger LOG = Logger.getLogger(JXTableHeader.class 087 .getName()); 088 089 static { 090 LookAndFeelAddons.contribute(new TableHeaderAddon()); 091 } 092 093 private transient PropertyChangeListener tablePropertyChangeListener; 094 private boolean resortsOnDoubleClick; 095 096 /** 097 * Constructs a <code>JTableHeader</code> with a default 098 * <code>TableColumnModel</code>. 099 * 100 * @see #createDefaultColumnModel 101 */ 102 public JXTableHeader() { 103 super(); 104 } 105 106 /** 107 * Constructs a <code>JTableHeader</code> which is initialized with 108 * <code>cm</code> as the column model. If <code>cm</code> is 109 * <code>null</code> this method will initialize the table header with a 110 * default <code>TableColumnModel</code>. 111 * 112 * @param columnModel the column model for the table 113 * @see #createDefaultColumnModel 114 */ 115 public JXTableHeader(TableColumnModel columnModel) { 116 super(columnModel); 117 } 118 119 120 /** 121 * {@inheritDoc} <p> 122 * Sets the associated JTable. Enables enhanced header 123 * features if table is of type JXTable.<p> 124 * 125 * PENDING: who is responsible for synching the columnModel? 126 */ 127 @Override 128 public void setTable(JTable table) { 129 uninstallTable(); 130 super.setTable(table); 131 installTable(); 132// setColumnModel(table.getColumnModel()); 133 // the additional listening option makes sense only if the table 134 // actually is a JXTable 135 if (getXTable() != null) { 136 installHeaderListener(); 137 } else { 138 uninstallHeaderListener(); 139 } 140 } 141 142 /** 143 * Installs the table. <p> 144 * This implemenation synchs enabled state and installs the PropertyChangeListener. 145 */ 146 protected void installTable() { 147 updateEnabledFromTable(); 148 if (getTable() == null) return; 149 getTable().addPropertyChangeListener(getTablePropertyChangeListener()); 150 } 151 152 /** 153 * Synchs the header's enabled with the table's enabled property. 154 */ 155 protected void updateEnabledFromTable() { 156 setEnabled(getTable() != null ? getTable().isEnabled() : true); 157 } 158 159 /** 160 * Uninstalls the table. <p> 161 * This implementation uninstalls the PropertyChangeListener. 162 */ 163 protected void uninstallTable() { 164 if (getTable() == null) return; 165 getTable().removePropertyChangeListener(getTablePropertyChangeListener()); 166 } 167 168 169 /** 170 * Implements TableColumnModelExt to allow internal update after 171 * column property changes.<p> 172 * 173 * This implementation triggers a resizeAndRepaint on every propertyChange which 174 * doesn't already fire a "normal" columnModelEvent. 175 * 176 * @param event change notification from a contained TableColumn. 177 * @see #isColumnEvent(PropertyChangeEvent) 178 * @see TableColumnModelExtListener 179 * 180 * 181 */ 182 @Override 183 public void columnPropertyChange(PropertyChangeEvent event) { 184 if (isColumnEvent(event)) return; 185 resizeAndRepaint(); 186 } 187 188 189 /** 190 * Returns a boolean indicating if a property change event received 191 * from column changes is expected to be already broadcasted by the 192 * core TableColumnModel. <p> 193 * 194 * This implementation returns true for notification of width, preferredWidth 195 * and visible properties, false otherwise. 196 * 197 * @param event the PropertyChangeEvent received as TableColumnModelExtListener. 198 * @return a boolean to decide whether the same event triggers a 199 * base columnModelEvent. 200 */ 201 protected boolean isColumnEvent(PropertyChangeEvent event) { 202 return "width".equals(event.getPropertyName()) || 203 "preferredWidth".equals(event.getPropertyName()) 204 || "visible".equals(event.getPropertyName()); 205 } 206 207 /** 208 * {@inheritDoc} <p> 209 * 210 * Overridden to respect the column tooltip, if available. 211 * 212 * @return the column tooltip of the column at the mouse position 213 * if not null or super if not available. 214 */ 215 @Override 216 public String getToolTipText(MouseEvent event) { 217 String columnToolTipText = getColumnToolTipText(event); 218 return columnToolTipText != null ? columnToolTipText : super.getToolTipText(event); 219 } 220 221 /** 222 * Returns the column tooltip of the column at the position 223 * of the MouseEvent, if a tooltip is available. 224 * 225 * @param event the mouseEvent representing the mouse location. 226 * @return the column tooltip of the column below the mouse location, 227 * or null if not available. 228 */ 229 protected String getColumnToolTipText(MouseEvent event) { 230 if (getXTable() == null) return null; 231 int column = columnAtPoint(event.getPoint()); 232 if (column < 0) return null; 233 TableColumnExt columnExt = getXTable().getColumnExt(column); 234 return columnExt != null ? columnExt.getToolTipText() : null; 235 } 236 237 /** 238 * Returns the associated table if it is of type JXTable, or null if not. 239 * 240 * @return the associated table if of type JXTable or null if not. 241 */ 242 public JXTable getXTable() { 243 if (!(getTable() instanceof JXTable)) 244 return null; 245 return (JXTable) getTable(); 246 } 247 248 /** 249 * Returns the resortsOnDoubleClick property. 250 * 251 * @return a flag indicating whether or not the second click in a mouseClicked 252 * event should toggle the sort order again. 253 * 254 * @see #setResortsOnDoubleClick(boolean) 255 */ 256 public boolean getResortsOnDoubleClick() { 257 return getXTable() != null && resortsOnDoubleClick; 258 } 259 260 /** 261 * Sets the resortsOnDoubleClick property. If enabled, the second click 262 * of a mouseClicked event will toggle the sort order again if the 263 * column has been unsorted before. This is introduced to support 264 * feature request #271-swingx. It is effective only if the coupled table 265 * is of type JXTable and has full control about its RowSorter's properties. 266 * 267 * The default value is false. 268 * 269 * @param resortsOnDoubleClick a boolean indicating whether or not the 270 * second click in a mouseClicked event should resort the column. 271 * 272 * @see #getResortsOnDoubleClick() 273 */ 274 public void setResortsOnDoubleClick(boolean resortsOnDoubleClick) { 275 boolean old = getResortsOnDoubleClick(); 276 this.resortsOnDoubleClick = resortsOnDoubleClick; 277 firePropertyChange("resortsOnDoubleClick", old, getResortsOnDoubleClick()); 278 } 279 280 /** 281 * Returns the TableCellRenderer to use for the column with the given index. This 282 * implementation returns the column's header renderer if available or this header's 283 * default renderer if not. 284 * 285 * @param columnIndex the index in view coordinates of the column 286 * @return the renderer to use for the column, guaranteed to be not null. 287 */ 288 public TableCellRenderer getCellRenderer(int columnIndex) { 289 TableCellRenderer renderer = getColumnModel().getColumn(columnIndex).getHeaderRenderer(); 290 return renderer != null ? renderer : getDefaultRenderer(); 291 } 292 293 /** 294 * {@inheritDoc} <p> 295 * 296 * Overridden to adjust for a reasonable minimum height. Done to fix Issue 334-swingx, 297 * which actually is a core issue misbehaving in returning a zero height 298 * if the first column has no text. 299 * 300 * @see #getPreferredSize(Dimension) 301 * @see #getMinimumHeight(int). 302 * 303 */ 304 @Override 305 public Dimension getPreferredSize() { 306 Dimension pref = super.getPreferredSize(); 307 pref = getPreferredSize(pref); 308 pref.height = getMinimumHeight(pref.height); 309 return pref; 310 } 311 312 /** 313 * Returns a preferred size which is adjusted to the maximum of all 314 * header renderers' height requirement. 315 * 316 * @param pref an initial preferred size 317 * @return the initial preferred size with its height property adjusted 318 * to the maximum of all renderers preferred height requirement. 319 * 320 * @see #getPreferredSize() 321 * @see #getMinimumHeight(int) 322 */ 323 protected Dimension getPreferredSize(Dimension pref) { 324 int height = pref.height; 325 for (int i = 0; i < getColumnModel().getColumnCount(); i++) { 326 TableCellRenderer renderer = getCellRenderer(i); 327 Component comp = renderer.getTableCellRendererComponent(table, 328 getColumnModel().getColumn(i).getHeaderValue(), false, false, -1, i); 329 height = Math.max(height, comp.getPreferredSize().height); 330 } 331 pref.height = height; 332 return pref; 333 334 } 335 336 /** 337 * Returns a reasonable minimal preferred height for the header. This is 338 * meant as a last straw if all header values are null, renderers report 0 as 339 * their preferred height.<p> 340 * 341 * This implementation returns the default header renderer's preferred height as measured 342 * with a dummy value if the input height is 0, otherwise returns the height 343 * unchanged. 344 * 345 * @param height the initial height. 346 * @return a reasonable minimal preferred height. 347 * 348 * @see #getPreferredSize() 349 * @see #getPreferredSize(Dimension) 350 */ 351 protected int getMinimumHeight(int height) { 352 if ((height == 0)) { 353// && (getXTable() != null) 354// && getXTable().isColumnControlVisible()){ 355 TableCellRenderer renderer = getDefaultRenderer(); 356 Component comp = renderer.getTableCellRendererComponent(getTable(), 357 "dummy", false, false, -1, -1); 358 height = comp.getPreferredSize().height; 359 } 360 return height; 361 } 362 363 364 /** 365 * @inherited <p> 366 * 367 * Overridden to fire a propertyChange for draggedColumn. 368 */ 369 @Override 370 public void setDraggedColumn(TableColumn column) { 371 if (getDraggedColumn() == column) return; 372 TableColumn old = getDraggedColumn(); 373 super.setDraggedColumn(column); 374 firePropertyChange("draggedColumn", old, getDraggedColumn()); 375 } 376 377 378 /** 379 * @inherited <p> 380 * 381 * Overridden to fire a propertyChange for resizingColumn. 382 */ 383 @Override 384 public void setResizingColumn(TableColumn aColumn) { 385 if (getResizingColumn() == aColumn) return; 386 TableColumn old = getResizingColumn(); 387 super.setResizingColumn(aColumn); 388 firePropertyChange("resizingColumn", old, getResizingColumn()); 389 } 390 391 392 393 /** 394 * {@inheritDoc} <p> 395 * 396 * Overridden to scroll the table to keep the dragged column visible. 397 * This side-effect is enabled only if the header's autoscroll property is 398 * <code>true</code> and the associated table is of type JXTable.<p> 399 * 400 * The autoscrolls is disabled by default. With or without - core 401 * issue #6503981 has weird effects (for jdk 1.6 - 1.6u3) on a plain 402 * JTable as well as a JXTable, fixed in 1.6u4. 403 * 404 */ 405 @Override 406 public void setDraggedDistance(int distance) { 407 int old = getDraggedDistance(); 408 super.setDraggedDistance(distance); 409 // fire because super doesn't 410 firePropertyChange("draggedDistance", old, getDraggedDistance()); 411 if (!getAutoscrolls() || (getXTable() == null)) return; 412 TableColumn column = getDraggedColumn(); 413 // fix for #788-swingx: don't try to scroll if we have no dragged column 414 // as doing will confuse the horizontalScrollEnabled on the JXTable. 415 if (column != null) { 416 getXTable().scrollColumnToVisible(getViewIndexForColumn(column)); 417 } 418 } 419 420 /** 421 * Returns the the dragged column if and only if, a drag is in process and 422 * the column is visible, otherwise returns <code>null</code>. 423 * 424 * @return the dragged column, if a drag is in process and the column is 425 * visible, otherwise returns <code>null</code> 426 * @see #getDraggedDistance 427 */ 428 @Override 429 public TableColumn getDraggedColumn() { 430 return isVisible(draggedColumn) ? draggedColumn : null; 431 } 432 433 /** 434 * Checks and returns the column's visibility. 435 * 436 * @param column the <code>TableColumn</code> to check 437 * @return a boolean indicating if the column is visible 438 */ 439 private boolean isVisible(TableColumn column) { 440 return getViewIndexForColumn(column) >= 0; 441 } 442 443 /** 444 * Returns the (visible) view index for the table column 445 * or -1 if not visible or not contained in this header's 446 * columnModel. 447 * 448 * 449 * @param aColumn the TableColumn to find the view index for 450 * @return the view index of the given table column or -1 if not visible 451 * or not contained in the column model. 452 */ 453 private int getViewIndexForColumn(TableColumn aColumn) { 454 if (aColumn == null) 455 return -1; 456 TableColumnModel cm = getColumnModel(); 457 for (int column = 0; column < cm.getColumnCount(); column++) { 458 if (cm.getColumn(column) == aColumn) { 459 return column; 460 } 461 } 462 return -1; 463 } 464 465 /** 466 * Returns the PropertyChangeListener to register on the owning table, 467 * lazily created. 468 * 469 * @return the PropertyChangeListener to use on the owning table. 470 */ 471 protected PropertyChangeListener getTablePropertyChangeListener() { 472 if (tablePropertyChangeListener == null) { 473 tablePropertyChangeListener = createTablePropertyChangeListener(); 474 } 475 return tablePropertyChangeListener; 476 } 477 478 /** 479 * Creates and returns the PropertyChangeListener to register on the 480 * owning table.<p> 481 * 482 * This implementation synchs the header's enabled properties with the 483 * table's enabled. 484 * 485 * @return the PropertyChangeListener to register on the owning table. 486 */ 487 protected PropertyChangeListener createTablePropertyChangeListener() { 488 PropertyChangeListener l = new PropertyChangeListener() { 489 490 @Override 491 public void propertyChange(PropertyChangeEvent evt) { 492 if ("enabled".equals(evt.getPropertyName())) { 493 updateEnabledFromTable(); 494 } 495 } 496 }; 497 return l; 498 } 499 500 501 /** 502 * Creates and installs header listeners to service the extended functionality. 503 * This implementation creates and installs a custom mouse input listener. 504 */ 505 protected void installHeaderListener() { 506 if (headerListener == null) { 507 headerListener = new HeaderListener(); 508 addMouseListener(headerListener); 509 addMouseMotionListener(headerListener); 510 } 511 } 512 513 /** 514 * Uninstalls header listeners to service the extended functionality. 515 * This implementation uninstalls a custom mouse input listener. 516 */ 517 protected void uninstallHeaderListener() { 518 if (headerListener != null) { 519 removeMouseListener(headerListener); 520 removeMouseMotionListener(headerListener); 521 headerListener = null; 522 } 523 } 524 525 private MouseInputListener headerListener; 526 527 /** 528 * A MouseListener implementation to support enhanced tableHeader functionality. 529 * 530 * Supports column "packing" by double click in resize region. Works around 531 * core issue #6862170 (must not sort column by click into resize region). 532 * <p> 533 * 534 * Note that the logic is critical, mostly because it must be independent of 535 * sequence of listener notification. So we check whether or not a pressed 536 * happens in the resizing region in both pressed and released, taking the 537 * header's resizingColumn property as a marker. The inResize flag can only 538 * be turned on in those. At the end of the released, we check if we are 539 * in resize and disable core sorting - which happens in clicked - if appropriate. 540 * In our clicked we hook the pack action (happens only on double click) 541 * and reset the resizing region flag always. Pressed (and all other methods) 542 * restore sorting enablement. 543 * <p> 544 * 545 * Supports resort on double click if enabled in the JXTableHeader (Issue #271-swingx). 546 * 547 * Is fully effective only if JXTable has control over the row sorter, that is 548 * if the row sorter is of type SortController. 549 * 550 */ 551 private class HeaderListener implements MouseInputListener, Serializable { 552 private TableColumn cachedResizingColumn; 553 private SortOrder[] cachedSortOrderCycle; 554 private int sortColumn = -1; 555 556 /** 557 * Packs column on double click in resize region. Resorts 558 * column on double click if enabled and not in resize region. 559 */ 560 @Override 561 public void mouseClicked(MouseEvent e) { 562 if (shouldIgnore(e)) { 563 return; 564 } 565 doResize(e); 566 doDoubleSort(e); 567 uncacheResizingColumn(); 568 } 569 570 private void doDoubleSort(MouseEvent e) { 571 if (!hasCachedSortColumn() || e.getClickCount() % 2 == 1) return; 572 getXTable().toggleSortOrder(sortColumn); 573 uncacheSortColumn(); 574 } 575 576 private boolean hasCachedSortColumn() { 577 return sortColumn >= 0; 578 } 579 580 /** 581 * Resets sort enablement always, set resizing marker if available. 582 */ 583 @Override 584 public void mousePressed(MouseEvent e) { 585 resetToggleSortOrder(e); 586 if (shouldIgnore(e)) { 587 return; 588 } 589 cacheResizingColumn(e); 590 } 591 592 /** 593 * Sets resizing marker if available, disables table sorting if in 594 * resize region and sort gesture (aka: single click). 595 */ 596 @Override 597 public void mouseReleased(MouseEvent e) { 598 if (shouldIgnore(e)) { 599 return; 600 } 601 cacheResizingColumn(e); 602 cacheSortColumn(e); 603 if (isInResizeRegion(e) && e.getClickCount() % 2 == 1) { 604 disableToggleSortOrder(e); 605 } 606 } 607 608 private void cacheSortColumn(MouseEvent e) { 609 if (!canCacheSortColumn(e)) uncacheSortColumn(); 610 if (e.getClickCount() % 2 == 1) { 611 int column = columnAtPoint(e.getPoint()); 612 if (column >= 0) { 613 int primarySortIndex = getXTable().getSortedColumnIndex(); 614 if (primarySortIndex == column) { 615 column = -1; 616 } 617 } 618 sortColumn = column; 619 } 620 621 } 622 623 private void uncacheSortColumn() { 624 sortColumn = -1; 625 } 626 627 private boolean canCacheSortColumn(MouseEvent e) { 628 if (hasSortController() && !isInResizeRegion(e) && getResortsOnDoubleClick()) { 629 return true; 630 } 631 return false; 632 } 633 634 /** 635 * Returns a boolean indication if the mouse event should be ignored. 636 * Here: returns true if table not enabled or not an event from the left mouse 637 * button. 638 * 639 * @param e 640 * @return 641 */ 642 private boolean shouldIgnore(MouseEvent e) { 643 return !SwingUtilities.isLeftMouseButton(e) 644 || !table.isEnabled(); 645 } 646 647 /** 648 * Packs caches resizing column on double click, if available. Does nothing 649 * otherwise. 650 * 651 * @param e 652 */ 653 private void doResize(MouseEvent e) { 654 if (e.getClickCount() != 2) 655 return; 656 int column = getViewIndexForColumn(cachedResizingColumn); 657 if (column >= 0) { 658 (getXTable()).packColumn(column, 5); 659 } 660 } 661 662 663 /** 664 * 665 * @param e 666 */ 667 private void disableToggleSortOrder(MouseEvent e) { 668 if (!hasSortController()) return; 669 SortController<?> controller = (SortController<?>) getXTable().getRowSorter(); 670 cachedSortOrderCycle = controller.getSortOrderCycle(); 671 controller.setSortOrderCycle(); 672 } 673 674 /** 675 * @return 676 */ 677 private boolean hasSortController() { 678 return (getXTable().getRowSorter() instanceof SortController<?>); 679 } 680 681 /** 682 * 683 */ 684 private void resetToggleSortOrder(MouseEvent e) { 685 if (cachedSortOrderCycle == null) return; 686 ((SortController<?>) getXTable().getRowSorter()).setSortOrderCycle(cachedSortOrderCycle); 687 cachedSortOrderCycle = null; 688 } 689 690 691 /** 692 * Caches the resizing column if set. Does nothing if null. 693 * 694 * @param e 695 */ 696 private void cacheResizingColumn(MouseEvent e) { 697 TableColumn column = getResizingColumn(); 698 if (column != null) { 699 cachedResizingColumn = column; 700 } 701 } 702 703 /** 704 * Sets the cached resizing column to null. 705 */ 706 private void uncacheResizingColumn() { 707 cachedResizingColumn = null; 708 } 709 710 /** 711 * Returns true if the mouseEvent happened in the resizing region. 712 * 713 * @param e 714 * @return 715 */ 716 private boolean isInResizeRegion(MouseEvent e) { 717 return cachedResizingColumn != null; // inResize; 718 } 719 720 @Override 721 public void mouseEntered(MouseEvent e) { 722 } 723 724 /** 725 * Resets all cached state. 726 */ 727 @Override 728 public void mouseExited(MouseEvent e) { 729 uncacheSortColumn(); 730 uncacheResizingColumn(); 731 resetToggleSortOrder(e); 732 } 733 734 /** 735 * Resets all cached state. 736 */ 737 @Override 738 public void mouseDragged(MouseEvent e) { 739 uncacheSortColumn(); 740 uncacheResizingColumn(); 741 resetToggleSortOrder(e); 742 } 743 744 /** 745 * Resets all cached state. 746 */ 747 @Override 748 public void mouseMoved(MouseEvent e) { 749 uncacheSortColumn(); 750 resetToggleSortOrder(e); 751 } 752 } 753 754 755 756}