/* * $Id: JXTableHeader.java,v 1.34 2009/05/06 10:42:59 kleopatra Exp $ * * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle, * Santa Clara, California 95054, U.S.A. All rights reserved. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ package org.jdesktop.swingx; import java.awt.Component; import java.awt.Dimension; import java.awt.event.MouseEvent; import java.beans.PropertyChangeEvent; import java.io.Serializable; import javax.swing.JTable; import javax.swing.SwingUtilities; import javax.swing.event.MouseInputListener; import javax.swing.plaf.UIResource; import javax.swing.table.JTableHeader; import javax.swing.table.TableCellRenderer; import javax.swing.table.TableColumn; import javax.swing.table.TableColumnModel; import org.jdesktop.swingx.event.TableColumnModelExtListener; import org.jdesktop.swingx.table.ColumnHeaderRenderer; import org.jdesktop.swingx.table.TableColumnExt; /** * TableHeader with extended functionality if associated Table is of * type JXTable.<p> * * <h2> Extended user interaction </h2> * * <ul> * <li> Supports column sorting by mouse clicks into a header cell * (outside the resize region). The concrete gestures are configurable * by providing a custom SortGestureRecognizer. The default recognizer * toggles sort order on mouseClicked. On shift-mouseClicked, it resets any column sorting. * Both are done by invoking the corresponding methods of JXTable, * <code> toggleSortOrder(int) </code> and <code> resetSortOrder() </code> * <li> Supports column pack (== auto-resize to exactly fit the contents) * on double-click in resize region. * <li> Supports horizontal auto-scroll if a column is dragged outside visible rectangle. * This feature is enabled if the autoscrolls property is true. The default is false * (because of Issue #788-swingx which still isn't fixed for jdk1.6). * </ul> * * <h2> Extended functionality </h2> * * <ul> * <li> Installs a default header renderer which is able to show sort icons. * LAF provided special effects are uneffected. * <li> Listens to TableColumn propertyChanges to update itself accordingly. * <li> Supports per-column header ToolTips. * <li> Guarantees reasonable minimal height > 0 for header preferred height. * </ul> * * * @author Jeanette Winzenburg * * @see JXTable#toggleSortOrder(int) * @see JXTable#resetSortOrder() * @see SortGestureRecognizer * @see ColumnHeaderRenderer */ public class JXTableHeader extends JTableHeader implements TableColumnModelExtListener { /** * The recognizer used for interpreting mouse events as sorting user gestures. */ private SortGestureRecognizer sortGestureRecognizer; /** * Constructs a <code>JTableHeader</code> with a default * <code>TableColumnModel</code>. * * @see #createDefaultColumnModel */ public JXTableHeader() { super(); } /** * Constructs a <code>JTableHeader</code> which is initialized with * <code>cm</code> as the column model. If <code>cm</code> is * <code>null</code> this method will initialize the table header with a * default <code>TableColumnModel</code>. * * @param columnModel the column model for the table * @see #createDefaultColumnModel */ public JXTableHeader(TableColumnModel columnModel) { super(columnModel); } /** * {@inheritDoc} <p> * Sets the associated JTable. Enables enhanced header * features if table is of type JXTable.<p> * * PENDING: who is responsible for synching the columnModel? */ @Override public void setTable(JTable table) { super.setTable(table); // setColumnModel(table.getColumnModel()); // the additional listening option makes sense only if the table // actually is a JXTable if (getXTable() != null) { installHeaderListener(); } else { uninstallHeaderListener(); } } /** * Implements TableColumnModelExt to allow internal update after * column property changes.<p> * * This implementation triggers a resizeAndRepaint on every propertyChange which * doesn't already fire a "normal" columnModelEvent. * * @param event change notification from a contained TableColumn. * @see #isColumnEvent(PropertyChangeEvent) * @see TableColumnModelExtListener * * */ public void columnPropertyChange(PropertyChangeEvent event) { if (isColumnEvent(event)) return; resizeAndRepaint(); } /** * Returns a boolean indicating if a property change event received * from column changes is expected to be already broadcasted by the * core TableColumnModel. <p> * * This implementation returns true for notification of width, preferredWidth * and visible properties, false otherwise. * * @param event the PropertyChangeEvent received as TableColumnModelExtListener. * @return a boolean to decide whether the same event triggers a * base columnModelEvent. */ protected boolean isColumnEvent(PropertyChangeEvent event) { return "width".equals(event.getPropertyName()) || "preferredWidth".equals(event.getPropertyName()) || "visible".equals(event.getPropertyName()); } /** * {@inheritDoc} <p> * * Overridden to respect the column tooltip, if available. * * @return the column tooltip of the column at the mouse position * if not null or super if not available. */ @Override public String getToolTipText(MouseEvent event) { String columnToolTipText = getColumnToolTipText(event); return columnToolTipText != null ? columnToolTipText : super.getToolTipText(event); } /** * Returns the column tooltip of the column at the position * of the MouseEvent, if a tooltip is available. * * @param event the mouseEvent representing the mouse location. * @return the column tooltip of the column below the mouse location, * or null if not available. */ protected String getColumnToolTipText(MouseEvent event) { if (getXTable() == null) return null; int column = columnAtPoint(event.getPoint()); if (column < 0) return null; TableColumnExt columnExt = getXTable().getColumnExt(column); return columnExt != null ? columnExt.getToolTipText() : null; } /** * Returns the associated table if it is of type JXTable, or null if not. * * @return the associated table if of type JXTable or null if not. */ public JXTable getXTable() { if (!(getTable() instanceof JXTable)) return null; return (JXTable) getTable(); } /** * Returns the TableCellRenderer to use for the column with the given index. This * implementation returns the column's header renderer if available or this header's * default renderer if not. * * @param columnIndex the index in view coordinates of the column * @return the renderer to use for the column, guaranteed to be not null. */ public TableCellRenderer getCellRenderer(int columnIndex) { TableCellRenderer renderer = getColumnModel().getColumn(columnIndex).getHeaderRenderer(); return renderer != null ? renderer : getDefaultRenderer(); } /** * {@inheritDoc} <p> * * Overridden to adjust for a reasonable minimum height. Done to fix Issue 334-swingx, * which actually is a core issue misbehaving in returning a zero height * if the first column has no text. * * @see #getPreferredSize(Dimension) * @see #getMinimumHeight(int). * */ @Override public Dimension getPreferredSize() { Dimension pref = super.getPreferredSize(); pref = getPreferredSize(pref); pref.height = getMinimumHeight(pref.height); return pref; } /** * Returns a preferred size which is adjusted to the maximum of all * header renderers' height requirement. * * @param pref an initial preferred size * @return the initial preferred size with its height property adjusted * to the maximum of all renderers preferred height requirement. * * @see #getPreferredSize() * @see #getMinimumHeight(int) */ protected Dimension getPreferredSize(Dimension pref) { int height = pref.height; for (int i = 0; i < getColumnModel().getColumnCount(); i++) { TableCellRenderer renderer = getCellRenderer(i); Component comp = renderer.getTableCellRendererComponent(table, getColumnModel().getColumn(i).getHeaderValue(), false, false, -1, i); height = Math.max(height, comp.getPreferredSize().height); } pref.height = height; return pref; } /** * Returns a reasonable minimal preferred height for the header. This is * meant as a last straw if all header values are null, renderers report 0 as * their preferred height.<p> * * This implementation returns the default header renderer's preferred height as measured * with a dummy value if the input height is 0, otherwise returns the height * unchanged. * * @param height the initial height. * @return a reasonable minimal preferred height. * * @see #getPreferredSize() * @see #getPreferredSize(Dimension) */ protected int getMinimumHeight(int height) { if ((height == 0)) { // && (getXTable() != null) // && getXTable().isColumnControlVisible()){ TableCellRenderer renderer = getDefaultRenderer(); Component comp = renderer.getTableCellRendererComponent(getTable(), "dummy", false, false, -1, -1); height = comp.getPreferredSize().height; } return height; } /** * {@inheritDoc} <p> * * Overridden to update the default renderer. * * @see #preUpdateRendererUI() * @see #postUpdateRendererUI(TableCellRenderer) * @see ColumnHeaderRenderer */ @Override public void updateUI() { TableCellRenderer oldRenderer = preUpdateRendererUI(); super.updateUI(); postUpdateRendererUI(oldRenderer); } /** * Prepares the default renderer and internal state for updateUI. * Returns the default renderer set when entering this method. * Called from updateUI before calling super.updateUI to * allow UIDelegate to cleanup, if necessary. This implementation * does so by restoring the header's default renderer to the * <code>ColumnHeaderRenderer</code>'s delegate. * * @return the current default renderer * @see #updateUI() */ protected TableCellRenderer preUpdateRendererUI() { TableCellRenderer oldRenderer = getDefaultRenderer(); // reset the default to the original to give S if (oldRenderer instanceof ColumnHeaderRenderer) { setDefaultRenderer(((ColumnHeaderRenderer)oldRenderer).getDelegateRenderer()); } return oldRenderer; } /** * Cleans up after the UIDelegate has updated the default renderer. * Called from <code>updateUI</code> after calling <code>super.updateUI</code>. * This implementation wraps a <code>UIResource</code> default renderer into a * <code>ColumnHeaderRenderer</code>. * * @param oldRenderer the default renderer before updateUI * * @see #updateUI() * * */ protected void postUpdateRendererUI(TableCellRenderer oldRenderer) { TableCellRenderer current = getDefaultRenderer(); if (!(current instanceof ColumnHeaderRenderer) && (current instanceof UIResource)) { ColumnHeaderRenderer renderer; if (oldRenderer instanceof ColumnHeaderRenderer) { renderer = (ColumnHeaderRenderer) oldRenderer; renderer.updateUI(this); } else { renderer = new ColumnHeaderRenderer(this); } setDefaultRenderer(renderer); } } /** * {@inheritDoc} <p> * * Overridden to scroll the table to keep the dragged column visible. * This side-effect is enabled only if the header's autoscroll property is * <code>true</code> and the associated table is of type JXTable.<p> * * The autoscrolls is disabled by default. With or without - core * issue #6503981 has weird effects (for jdk 1.6 - 1.6u3) on a plain * JTable as well as a JXTable, fixed in 1.6u4. * */ @Override public void setDraggedDistance(int distance) { int old = getDraggedDistance(); super.setDraggedDistance(distance); // fire because super doesn't firePropertyChange("draggedDistance", old, getDraggedDistance()); if (!getAutoscrolls() || (getXTable() == null)) return; TableColumn column = getDraggedColumn(); // fix for #788-swingx: don't try to scroll if we have no dragged column // as doing will confuse the horizontalScrollEnabled on the JXTable. if (column != null) { getXTable().scrollColumnToVisible(getViewIndexForColumn(column)); } } /** * Returns the the dragged column if and only if, a drag is in process and * the column is visible, otherwise returns <code>null</code>. * * @return the dragged column, if a drag is in process and the column is * visible, otherwise returns <code>null</code> * @see #getDraggedDistance */ @Override public TableColumn getDraggedColumn() { return isVisible(draggedColumn) ? draggedColumn : null; } /** * Checks and returns the column's visibility. * * @param column the <code>TableColumn</code> to check * @return a boolean indicating if the column is visible */ private boolean isVisible(TableColumn column) { return getViewIndexForColumn(column) >= 0; } /** * Returns the (visible) view index for the table column * or -1 if not visible or not contained in this header's * columnModel. * * * @param aColumn the TableColumn to find the view index for * @return the view index of the given table column or -1 if not visible * or not contained in the column model. */ private int getViewIndexForColumn(TableColumn aColumn) { if (aColumn == null) return -1; TableColumnModel cm = getColumnModel(); for (int column = 0; column < cm.getColumnCount(); column++) { if (cm.getColumn(column) == aColumn) { return column; } } return -1; } /** * Returns the SortGestureRecognizer to use. If none available, lazily * creates a default. * * @return the SortGestureRecognizer to use for interpreting mouse events * as sort gestures. * * @see #setSortGestureRecognizer(SortGestureRecognizer) * @see #createSortGestureRecognizer() */ public SortGestureRecognizer getSortGestureRecognizer() { if (sortGestureRecognizer == null) { sortGestureRecognizer = createSortGestureRecognizer(); } return sortGestureRecognizer; } /** * Sets the SortGestureRecognizer to use for interpreting mouse events * as sort gestures. If null, a default as returned by createSortGestureRecognizer * is used.<p> * * This is a bound property. * * @param recognizer the SortGestureRecognizer to use for interpreting mouse events * as sort gestures * * @see #getSortGestureRecognizer() * @see #createSortGestureRecognizer() */ public void setSortGestureRecognizer(SortGestureRecognizer recognizer) { SortGestureRecognizer old = getSortGestureRecognizer(); this.sortGestureRecognizer = recognizer; firePropertyChange("sortGestureRecognizer", old, getSortGestureRecognizer()); } /** * Creates and returns the default SortGestureRecognizer. * @return the default SortGestureRecognizer to use for interpreting mouse events * as sort gestures. * * @see #getSortGestureRecognizer() * @see #setSortGestureRecognizer(SortGestureRecognizer) */ protected SortGestureRecognizer createSortGestureRecognizer() { return new SortGestureRecognizer(); } /** * Controller for mapping left mouse clicks to sort/-unsort gestures for use * in interested mouse listeners. This base class interprets a single click * for toggling sort order, and a single SHIFT-left click for unsort. * <p> * * A custom implementation which doesn't allow unsort. * * <pre> * <code> * public class CustomRecognizer extends SortGestureRecognizer { * // Disable reset gesture. * @Override * public boolean isResetSortOrderGesture(MouseEvent e) { * return false; * } * } * tableHeader.setSortGestureRecognizer(new CustomRecognizer()); * </code> * </pre> * * <b>Note</b>: Unsort as of SwingX means to reset the sort of all columns. * Which currently doesn't make a difference because it supports single * column sorts only. Might become significant after switching to JDK 1.6 * which supports multiple column sorting (if we can keep up the pluggable * control). * * */ public static class SortGestureRecognizer { /** * Returns a boolean indicating whether the mouse event should be interpreted * as an unsort trigger or not. * @param e a mouseEvent representing a left mouse click. * @return true if the mouse click should be used as a unsort gesture */ public boolean isResetSortOrderGesture(MouseEvent e) { return isSortOrderGesture(e) && isResetModifier(e); } /** * Returns a boolean indicating whether the mouse event should be interpreted * as a toggle sort trigger or not. * @param e a mouseEvent representing a left mouse click. * @return true if the mouse click should be used as a toggle sort gesture */ public boolean isToggleSortOrderGesture(MouseEvent e) { return isSortOrderGesture(e) && !isResetModifier(e); } /** * Returns a boolean indicating whether the mouse event should be interpreted * as any type of sort change trigger. * @param e a mouseEvent representing a left mouse click. * @return true if the mouse click should be used as a sort/unsort gesture */ public boolean isSortOrderGesture(MouseEvent e) { return e.getClickCount() == 1; } /** * Returns a boolean indicating whether the mouse event's modifier should be interpreted * as a unsort or not. * * @param e a mouseEvent representing a left mouse click. * @return true if the mouse click's modifier should be interpreted as a reset. * */ protected boolean isResetModifier(MouseEvent e) { return ((e.getModifiersEx() & MouseEvent.SHIFT_DOWN_MASK) == MouseEvent.SHIFT_DOWN_MASK); } } /** * Creates and installs header listeners to service the extended functionality. * This implementation creates and installs a custom mouse input listener. */ protected void installHeaderListener() { if (headerListener == null) { headerListener = new HeaderListener(); addMouseListener(headerListener); addMouseMotionListener(headerListener); } } /** * Uninstalls header listeners to service the extended functionality. * This implementation uninstalls a custom mouse input listener. */ protected void uninstallHeaderListener() { if (headerListener != null) { removeMouseListener(headerListener); removeMouseMotionListener(headerListener); headerListener = null; } } private MouseInputListener headerListener; private class HeaderListener implements MouseInputListener, Serializable { private TableColumn cachedResizingColumn; public void mouseClicked(MouseEvent e) { if (shouldIgnore(e)) { return; } if (isInResizeRegion(e)) { doResize(e); } else { doSort(e); } } private boolean shouldIgnore(MouseEvent e) { return !SwingUtilities.isLeftMouseButton(e) || !table.isEnabled(); } private void doSort(MouseEvent e) { JXTable table = getXTable(); if (!table.isSortable()) return; if (getSortGestureRecognizer().isResetSortOrderGesture(e)) { table.resetSortOrder(); repaint(); } else if (getSortGestureRecognizer().isToggleSortOrderGesture(e)){ int column = columnAtPoint(e.getPoint()); if (column >= 0) { table.toggleSortOrder(column); } uncacheResizingColumn(); repaint(); } } private void doResize(MouseEvent e) { if (e.getClickCount() != 2) return; int column = getViewIndexForColumn(cachedResizingColumn); if (column >= 0) { (getXTable()).packColumn(column, 5); } uncacheResizingColumn(); } public void mouseReleased(MouseEvent e) { cacheResizingColumn(e); } public void mousePressed(MouseEvent e) { cacheResizingColumn(e); } private void cacheResizingColumn(MouseEvent e) { if (!getSortGestureRecognizer().isSortOrderGesture(e)) return; TableColumn column = getResizingColumn(); if (column != null) { cachedResizingColumn = column; } } private void uncacheResizingColumn() { cachedResizingColumn = null; } private boolean isInResizeRegion(MouseEvent e) { return cachedResizingColumn != null; // inResize; } public void mouseEntered(MouseEvent e) { } public void mouseExited(MouseEvent e) { uncacheResizingColumn(); } public void mouseDragged(MouseEvent e) { uncacheResizingColumn(); } public void mouseMoved(MouseEvent e) { } } }