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