org.jdesktop.swingx.JXTableHeader Maven / Gradle / Ivy
/*
* $Id: JXTableHeader.java 3960 2011-03-15 19:36:53Z kschaefe $
*
* 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.beans.PropertyChangeListener;
import java.io.Serializable;
import java.util.logging.Logger;
import javax.swing.JTable;
import javax.swing.SortOrder;
import javax.swing.SwingUtilities;
import javax.swing.event.MouseInputListener;
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.plaf.LookAndFeelAddons;
import org.jdesktop.swingx.plaf.TableHeaderAddon;
import org.jdesktop.swingx.sort.SortController;
import org.jdesktop.swingx.table.TableColumnExt;
/**
* TableHeader with extended functionality if associated Table is of
* type JXTable.
*
*
Extended user interaction
*
*
* - Supports column pack (== auto-resize to exactly fit the contents)
* on double-click in resize region.
*
- Configurable to resort a column on the second click of a mouseClicked event
* (feature request #271-swingx)
*
- Does its best to not sort if the mouse click happens in the resize region.
*
- 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).
*
*
* Note: extended sort and resize related functionality is fully effective only if the header's
* table is of type JXTable and has control over the row sorter, that is the row sorter
* is of type SortController.
*
* Extended functionality
*
*
* - Listens to TableColumn propertyChanges to update itself accordingly.
*
- Supports per-column header ToolTips.
*
- Guarantees reasonable minimal height > 0 for header preferred height.
*
*
*
* @author Jeanette Winzenburg
*
* @see JXTable#toggleSortOrder(int)
* @see JXTable#resetSortOrder()
* @see SortGestureRecognizer
*/
public class JXTableHeader extends JTableHeader
implements TableColumnModelExtListener {
@SuppressWarnings("unused")
private static final Logger LOG = Logger.getLogger(JXTableHeader.class
.getName());
static {
LookAndFeelAddons.contribute(new TableHeaderAddon());
}
private transient PropertyChangeListener tablePropertyChangeListener;
private boolean resortsOnDoubleClick;
/**
* Constructs a JTableHeader
with a default
* TableColumnModel
.
*
* @see #createDefaultColumnModel
*/
public JXTableHeader() {
super();
}
/**
* Constructs a JTableHeader
which is initialized with
* cm
as the column model. If cm
is
* null
this method will initialize the table header with a
* default TableColumnModel
.
*
* @param columnModel the column model for the table
* @see #createDefaultColumnModel
*/
public JXTableHeader(TableColumnModel columnModel) {
super(columnModel);
}
/**
* {@inheritDoc}
* Sets the associated JTable. Enables enhanced header
* features if table is of type JXTable.
*
* PENDING: who is responsible for synching the columnModel?
*/
@Override
public void setTable(JTable table) {
uninstallTable();
super.setTable(table);
installTable();
// setColumnModel(table.getColumnModel());
// the additional listening option makes sense only if the table
// actually is a JXTable
if (getXTable() != null) {
installHeaderListener();
} else {
uninstallHeaderListener();
}
}
/**
* Installs the table.
* This implemenation synchs enabled state and installs the PropertyChangeListener.
*/
protected void installTable() {
updateEnabledFromTable();
if (getTable() == null) return;
getTable().addPropertyChangeListener(getTablePropertyChangeListener());
}
/**
* Synchs the header's enabled with the table's enabled property.
*/
protected void updateEnabledFromTable() {
setEnabled(getTable() != null ? getTable().isEnabled() : true);
}
/**
* Uninstalls the table.
* This implementation uninstalls the PropertyChangeListener.
*/
protected void uninstallTable() {
if (getTable() == null) return;
getTable().removePropertyChangeListener(getTablePropertyChangeListener());
}
/**
* Implements TableColumnModelExt to allow internal update after
* column property changes.
*
* 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
*
*
*/
@Override
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.
*
* 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}
*
* 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 resortsOnDoubleClick property.
*
* @return a flag indicating whether or not the second click in a mouseClicked
* event should toggle the sort order again.
*
* @see #setResortsOnDoubleClick(boolean)
*/
public boolean getResortsOnDoubleClick() {
return getXTable() != null && resortsOnDoubleClick;
}
/**
* Sets the resortsOnDoubleClick property. If enabled, the second click
* of a mouseClicked event will toggle the sort order again if the
* column has been unsorted before. This is introduced to support
* feature request #271-swingx. It is effective only if the coupled table
* is of type JXTable and has full control about its RowSorter's properties.
*
* The default value is false.
*
* @param resortsOnDoubleClick a boolean indicating whether or not the
* second click in a mouseClicked event should resort the column.
*
* @see #getResortsOnDoubleClick()
*/
public void setResortsOnDoubleClick(boolean resortsOnDoubleClick) {
boolean old = getResortsOnDoubleClick();
this.resortsOnDoubleClick = resortsOnDoubleClick;
firePropertyChange("resortsOnDoubleClick", old, getResortsOnDoubleClick());
}
/**
* 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}
*
* 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.
*
* 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;
}
/**
* @inherited
*
* Overridden to fire a propertyChange for draggedColumn.
*/
@Override
public void setDraggedColumn(TableColumn column) {
if (getDraggedColumn() == column) return;
TableColumn old = getDraggedColumn();
super.setDraggedColumn(column);
firePropertyChange("draggedColumn", old, getDraggedColumn());
}
/**
* @inherited
*
* Overridden to fire a propertyChange for resizingColumn.
*/
@Override
public void setResizingColumn(TableColumn aColumn) {
if (getResizingColumn() == aColumn) return;
TableColumn old = getResizingColumn();
super.setResizingColumn(aColumn);
firePropertyChange("resizingColumn", old, getResizingColumn());
}
/**
* {@inheritDoc}
*
* Overridden to scroll the table to keep the dragged column visible.
* This side-effect is enabled only if the header's autoscroll property is
* true
and the associated table is of type JXTable.
*
* 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 null
.
*
* @return the dragged column, if a drag is in process and the column is
* visible, otherwise returns null
* @see #getDraggedDistance
*/
@Override
public TableColumn getDraggedColumn() {
return isVisible(draggedColumn) ? draggedColumn : null;
}
/**
* Checks and returns the column's visibility.
*
* @param column the TableColumn
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 PropertyChangeListener to register on the owning table,
* lazily created.
*
* @return the PropertyChangeListener to use on the owning table.
*/
protected PropertyChangeListener getTablePropertyChangeListener() {
if (tablePropertyChangeListener == null) {
tablePropertyChangeListener = createTablePropertyChangeListener();
}
return tablePropertyChangeListener;
}
/**
* Creates and returns the PropertyChangeListener to register on the
* owning table.
*
* This implementation synchs the header's enabled properties with the
* table's enabled.
*
* @return the PropertyChangeListener to register on the owning table.
*/
protected PropertyChangeListener createTablePropertyChangeListener() {
PropertyChangeListener l = new PropertyChangeListener() {
@Override
public void propertyChange(PropertyChangeEvent evt) {
if ("enabled".equals(evt.getPropertyName())) {
updateEnabledFromTable();
}
}
};
return l;
}
/**
* 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;
/**
* A MouseListener implementation to support enhanced tableHeader functionality.
*
* Supports column "packing" by double click in resize region. Works around
* core issue #6862170 (must not sort column by click into resize region).
*
*
* Note that the logic is critical, mostly because it must be independent of
* sequence of listener notification. So we check whether or not a pressed
* happens in the resizing region in both pressed and released, taking the
* header's resizingColumn property as a marker. The inResize flag can only
* be turned on in those. At the end of the released, we check if we are
* in resize and disable core sorting - which happens in clicked - if appropriate.
* In our clicked we hook the pack action (happens only on double click)
* and reset the resizing region flag always. Pressed (and all other methods)
* restore sorting enablement.
*
*
* Supports resort on double click if enabled in the JXTableHeader (Issue #271-swingx).
*
* Is fully effective only if JXTable has control over the row sorter, that is
* if the row sorter is of type SortController.
*
*/
private class HeaderListener implements MouseInputListener, Serializable {
private TableColumn cachedResizingColumn;
private SortOrder[] cachedSortOrderCycle;
private int sortColumn = -1;
/**
* Packs column on double click in resize region. Resorts
* column on double click if enabled and not in resize region.
*/
@Override
public void mouseClicked(MouseEvent e) {
if (shouldIgnore(e)) {
return;
}
doResize(e);
doDoubleSort(e);
uncacheResizingColumn();
}
private void doDoubleSort(MouseEvent e) {
if (!hasCachedSortColumn() || e.getClickCount() % 2 == 1) return;
getXTable().toggleSortOrder(sortColumn);
uncacheSortColumn();
}
private boolean hasCachedSortColumn() {
return sortColumn >= 0;
}
/**
* Resets sort enablement always, set resizing marker if available.
*/
@Override
public void mousePressed(MouseEvent e) {
resetToggleSortOrder(e);
if (shouldIgnore(e)) {
return;
}
cacheResizingColumn(e);
}
/**
* Sets resizing marker if available, disables table sorting if in
* resize region and sort gesture (aka: single click).
*/
@Override
public void mouseReleased(MouseEvent e) {
if (shouldIgnore(e)) {
return;
}
cacheResizingColumn(e);
cacheSortColumn(e);
if (isInResizeRegion(e) && e.getClickCount() % 2 == 1) {
disableToggleSortOrder(e);
}
}
private void cacheSortColumn(MouseEvent e) {
if (!canCacheSortColumn(e)) uncacheSortColumn();
if (e.getClickCount() % 2 == 1) {
int column = columnAtPoint(e.getPoint());
if (column >= 0) {
int primarySortIndex = getXTable().getSortedColumnIndex();
if (primarySortIndex == column) {
column = -1;
}
}
sortColumn = column;
}
}
private void uncacheSortColumn() {
sortColumn = -1;
}
private boolean canCacheSortColumn(MouseEvent e) {
if (hasSortController() && !isInResizeRegion(e) && getResortsOnDoubleClick()) {
return true;
}
return false;
}
/**
* Returns a boolean indication if the mouse event should be ignored.
* Here: returns true if table not enabled or not an event from the left mouse
* button.
*
* @param e
* @return
*/
private boolean shouldIgnore(MouseEvent e) {
return !SwingUtilities.isLeftMouseButton(e)
|| !table.isEnabled();
}
/**
* Packs caches resizing column on double click, if available. Does nothing
* otherwise.
*
* @param e
*/
private void doResize(MouseEvent e) {
if (e.getClickCount() != 2)
return;
int column = getViewIndexForColumn(cachedResizingColumn);
if (column >= 0) {
(getXTable()).packColumn(column, 5);
}
}
/**
*
* @param e
*/
private void disableToggleSortOrder(MouseEvent e) {
if (!hasSortController()) return;
SortController> controller = (SortController>) getXTable().getRowSorter();
cachedSortOrderCycle = controller.getSortOrderCycle();
controller.setSortOrderCycle();
}
/**
* @return
*/
private boolean hasSortController() {
return (getXTable().getRowSorter() instanceof SortController>);
}
/**
*
*/
private void resetToggleSortOrder(MouseEvent e) {
if (cachedSortOrderCycle == null) return;
((SortController>) getXTable().getRowSorter()).setSortOrderCycle(cachedSortOrderCycle);
cachedSortOrderCycle = null;
}
/**
* Caches the resizing column if set. Does nothing if null.
*
* @param e
*/
private void cacheResizingColumn(MouseEvent e) {
TableColumn column = getResizingColumn();
if (column != null) {
cachedResizingColumn = column;
}
}
/**
* Sets the cached resizing column to null.
*/
private void uncacheResizingColumn() {
cachedResizingColumn = null;
}
/**
* Returns true if the mouseEvent happened in the resizing region.
*
* @param e
* @return
*/
private boolean isInResizeRegion(MouseEvent e) {
return cachedResizingColumn != null; // inResize;
}
@Override
public void mouseEntered(MouseEvent e) {
}
/**
* Resets all cached state.
*/
@Override
public void mouseExited(MouseEvent e) {
uncacheSortColumn();
uncacheResizingColumn();
resetToggleSortOrder(e);
}
/**
* Resets all cached state.
*/
@Override
public void mouseDragged(MouseEvent e) {
uncacheSortColumn();
uncacheResizingColumn();
resetToggleSortOrder(e);
}
/**
* Resets all cached state.
*/
@Override
public void mouseMoved(MouseEvent e) {
uncacheSortColumn();
resetToggleSortOrder(e);
}
}
}