org.jdesktop.swingx.JXTable Maven / Gradle / Ivy
Show all versions of swingx-all Show documentation
/*
* $Id: JXTable.java 4266 2012-12-05 16:34:37Z kleopatra $
*
* 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 org.jdesktop.beans.JavaBean;
import org.jdesktop.swingx.action.AbstractActionExt;
import org.jdesktop.swingx.action.BoundAction;
import org.jdesktop.swingx.decorator.ComponentAdapter;
import org.jdesktop.swingx.decorator.CompoundHighlighter;
import org.jdesktop.swingx.decorator.Highlighter;
import org.jdesktop.swingx.decorator.ResetDTCRColorHighlighter;
import org.jdesktop.swingx.event.TableColumnModelExtListener;
import org.jdesktop.swingx.hyperlink.HyperlinkAction;
import org.jdesktop.swingx.plaf.LookAndFeelAddons;
import org.jdesktop.swingx.plaf.TableAddon;
import org.jdesktop.swingx.plaf.UIAction;
import org.jdesktop.swingx.plaf.UIDependent;
import org.jdesktop.swingx.plaf.UIManagerExt;
import org.jdesktop.swingx.renderer.AbstractRenderer;
import org.jdesktop.swingx.renderer.CheckBoxProvider;
import org.jdesktop.swingx.renderer.DefaultTableRenderer;
import org.jdesktop.swingx.renderer.HyperlinkProvider;
import org.jdesktop.swingx.renderer.IconValues;
import org.jdesktop.swingx.renderer.MappedValue;
import org.jdesktop.swingx.renderer.StringValue;
import org.jdesktop.swingx.renderer.StringValues;
import org.jdesktop.swingx.rollover.RolloverProducer;
import org.jdesktop.swingx.rollover.TableRolloverController;
import org.jdesktop.swingx.rollover.TableRolloverProducer;
import org.jdesktop.swingx.search.AbstractSearchable;
import org.jdesktop.swingx.search.SearchFactory;
import org.jdesktop.swingx.search.Searchable;
import org.jdesktop.swingx.search.TableSearchable;
import org.jdesktop.swingx.sort.DefaultSortController;
import org.jdesktop.swingx.sort.SortController;
import org.jdesktop.swingx.sort.SortUtils;
import org.jdesktop.swingx.sort.StringValueRegistry;
import org.jdesktop.swingx.sort.TableSortController;
import org.jdesktop.swingx.table.ColumnControlButton;
import org.jdesktop.swingx.table.ColumnFactory;
import org.jdesktop.swingx.table.DefaultTableColumnModelExt;
import org.jdesktop.swingx.table.NumberEditorExt;
import org.jdesktop.swingx.table.TableColumnExt;
import org.jdesktop.swingx.table.TableColumnModelExt;
import javax.swing.*;
import javax.swing.RowSorter.SortKey;
import javax.swing.border.LineBorder;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.RowSorterEvent;
import javax.swing.event.TableColumnModelEvent;
import javax.swing.event.TableModelEvent;
import javax.swing.table.JTableHeader;
import javax.swing.table.TableCellEditor;
import javax.swing.table.TableCellRenderer;
import javax.swing.table.TableColumn;
import javax.swing.table.TableColumnModel;
import javax.swing.table.TableModel;
import java.awt.Color;
import java.awt.Component;
import java.awt.ComponentOrientation;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.KeyboardFocusManager;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Window;
import java.awt.event.ActionEvent;
import java.awt.print.PrinterException;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.lang.reflect.Constructor;
import java.net.URI;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.EventObject;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Vector;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Enhanced Table component with support for general SwingX sorting/filtering,
* rendering, highlighting, rollover and search functionality. Table specific
* enhancements include runtime configuration options like toggle column
* visibility, column sizing, PENDING JW ...
*
* Sorting and Filtering
*
* JXTable supports sorting and filtering of rows (switched to core sorting).
*
* Additionally, it provides api to apply
* a specific sort order, to toggle the sort order of columns identified
* by view index or column identifier and to reset all sorts. F.i:
*
*
* table.setSortOrder("PERSON_ID", SortOrder.DESCENDING);
* table.toggleSortOder(4);
* table.resetSortOrder();
*
*
* Sorting sequence can be configured per column by setting the TableColumnExt's
* comparator
property. Sorting can be disabled per column - setting the TableColumnExt's
* sortable
or per table by {@link #setSortable(boolean)}.
* The table takes responsibility to propagate these
* properties to the current sorter, if available
*
* Note that the enhanced sorting controls are effective only if the RowSorter is
* of type SortController, which it is by default. Different from core JTable, the
* autoCreateRowSorter property is enabled by default. If on, the JXTable creates and
* uses a default row sorter as returned by the createDefaultRowSorter method.
*
*
* Typically, a JXTable is sortable by left clicking on column headers. By default, each
* subsequent click on a header reverses the order of the sort, and a sort arrow
* icon is automatically drawn on the header.
*
*
*
*
Rendering and Highlighting
*
* As all SwingX collection views, a JXTable is a HighlighterClient (PENDING JW:
* formally define and implement, like in AbstractTestHighlighter), that is it
* provides consistent api to add and remove Highlighters which can visually
* decorate the rendering component.
*
*
* An example multiple highlighting (default striping as appropriate for the
* current LookAndFeel, cell foreground on matching pattern, and shading a
* column):
*
*
*
* Highlighter simpleStriping = HighlighterFactory.createSimpleStriping();
* PatternPredicate patternPredicate = new PatternPredicate("ˆM", 1);
* ColorHighlighter magenta = new ColorHighlighter(patternPredicate, null,
* Color.MAGENTA, null, Color.MAGENTA);
* Highlighter shading = new ShadingColorHighlighter(
* new HighlightPredicate.ColumnHighlightPredicate(1));
*
* table.setHighlighters(simpleStriping,
* magenta,
* shading);
*
*
*
* To fully support, JXTable registers SwingX default table renderers instead of
* core defaults (see {@link DefaultTableRenderer}) The recommended approach for
* customizing rendered content it to intall a DefaultTableRenderer configured
* with a custom String- and/or IconValue. F.i. assuming the cell value is a
* File and should be rendered by showing its name followed and date of last
* change:
*
*
* StringValue sv = new StringValue() {
* public String getString(Object value) {
* if (!(value instanceof File)) return StringValues.TO_STRING.getString(value);
* return StringValues.FILE_NAME.getString(value) + ", "
* + StringValues.DATE_TO_STRING.getString(((File) value).lastModified());
* }};
* table.setCellRenderer(File.class, new DefaultTableRenderer(sv));
*
*
* In addition to super default per-class registration, JXTable registers a default
* renderer for URI
s which opens the default application to view the related
* document as supported by Desktop
. Note: this action is triggered only if
* rolloverEnabled is true (default value) and the cell is not editable.
*
*
* Note: DefaultTableCellRenderer and subclasses require a hack to play
* nicely with Highlighters because it has an internal "color memory" in
* setForeground/setBackground. The hack is applied by default which might lead
* to unexpected side-effects in custom renderers subclassing DTCR. See
* {@link #resetDefaultTableCellRendererHighlighter} for details.
*
*
* Note: by default JXTable disables the alternate row striping provided
* by Nimbus, instead it does use the color provided by Nimbus to configure the
* UIColorHighlighter. Like in any other LAF without striping support,
* client code has to explicitly turn on striping by
* setting a Highlighter like:
*
*
* table.addHighlighter(HighlighterFactory.createSimpleStriping());
*
*
* Alternatively, if client code wants to rely on the LAF provided striping
* support, it can set a property in the UIManager ("early" in the application
* lifetime to prevent JXTable to disable Nimbus handling it. In this case it is
* recommended to not any of the ui-dependent Highlighters provided by the
* HighlighterFactory.
*
*
* UIManager.put("Nimbus.keepAlternateRowColor", Boolean.TRUE);
*
*
* Rollover
*
* As all SwingX collection views, a JXTable supports per-cell rollover which is
* enabled by default. If enabled, the component fires rollover events on
* enter/exit of a cell which by default is promoted to the renderer if it
* implements RolloverRenderer, that is simulates live behaviour. The rollover
* events can be used by client code as well, f.i. to decorate the rollover row
* using a Highlighter.
*
*
* JXTable table = new JXTable();
* table.addHighlighter(new ColorHighlighter(HighlightPredicate.ROLLOVER_ROW,
* null, Color.RED);
*
*
* Search
*
* As all SwingX collection views, a JXTable is searchable. A search action is
* registered in its ActionMap under the key "find". The default behaviour is to
* ask the SearchFactory to open a search component on this component. The
* default keybinding is retrieved from the SearchFactory, typically ctrl-f (or
* cmd-f for Mac). Client code can register custom actions and/or bindings as
* appropriate.
*
*
* JXTable provides api to vend a renderer-controlled String representation of
* cell content. This allows the Searchable and Highlighters to use WYSIWYM
* (What-You-See-Is-What-You-Match), that is pattern matching against the actual
* string as seen by the user.
*
*
Column Configuration
*
* JXTable's default column model
* is of type TableColumnModelExt which allows management of hidden columns.
* Furthermore, it guarantees to delegate creation and configuration of table columns
* to its ColumnFactory. The factory is meant as the central place to
* customize column configuration.
*
*
* Columns can be hidden or shown by setting the visible property on the
* TableColumnExt using {@link TableColumnExt#setVisible(boolean)}. Columns can
* also be shown or hidden from the column control popup.
*
*
* The column control popup is triggered by an icon drawn to the far right of
* the column headers, above the table's scrollbar (when installed in a
* JScrollPane). The popup allows the user to select which columns should be
* shown or hidden, as well as to pack columns and turn on horizontal scrolling.
* To show or hide the column control, use the
* {@link #setColumnControlVisible(boolean show)}method.
*
*
* You can resize all columns, selected columns, or a single column using the
* methods like {@link #packAll()}. Packing combines several other aspects of a
* JXTable. If horizontal scrolling is enabled using
* {@link #setHorizontalScrollEnabled(boolean)}, then the scrollpane will allow
* the table to scroll right-left, and columns will be sized to their preferred
* size. To control the preferred sizing of a column, you can provide a
* prototype value for the column in the TableColumnExt using
* {@link TableColumnExt#setPrototypeValue(Object)}. The prototype is used as an
* indicator of the preferred size of the column. This can be useful if some
* data in a given column is very long, but where the resize algorithm would
* normally not pick this up.
*
*
*
*
*
* Keys/Actions registered with this component:
*
*
* - "find" - open an appropriate search widget for searching cell content.
* The default action registeres itself with the SearchFactory as search target.
*
- "print" - print the table
*
- {@link JXTable#HORIZONTALSCROLL_ACTION_COMMAND} - toggle the horizontal
* scrollbar
*
- {@link JXTable#PACKSELECTED_ACTION_COMMAND} - resize the selected column
* to fit the widest cell content
*
- {@link JXTable#PACKALL_ACTION_COMMAND} - resize all columns to fit the
* widest cell content in each column
*
*
*
*
* Key bindings.
*
*
* - "control F" - bound to actionKey "find".
*
*
*
* Client Properties.
*
*
* - {@link JXTable#MATCH_HIGHLIGHTER} - set to Boolean.TRUE to use a
* SearchHighlighter to mark a cell as matching.
*
*
* @author Ramesh Gupta
* @author Amy Fowler
* @author Mark Davidson
* @author Jeanette Winzenburg
*/
@JavaBean
public class JXTable extends JTable implements TableColumnModelExtListener {
/**
*
*/
public static final String FOCUS_PREVIOUS_COMPONENT = "focusPreviousComponent";
/**
*
*/
public static final String FOCUS_NEXT_COMPONENT = "focusNextComponent";
private static final Logger LOG = Logger.getLogger(JXTable.class.getName());
/**
* Identifier of show horizontal scroll action, used in JXTable's
* ActionMap
.
*/
public static final String HORIZONTALSCROLL_ACTION_COMMAND = ColumnControlButton.COLUMN_CONTROL_MARKER + "horizontalScroll";
/**
* Identifier of pack table action, used in JXTable's ActionMap
* .
*/
public static final String PACKALL_ACTION_COMMAND = ColumnControlButton.COLUMN_CONTROL_MARKER + "packAll";
/**
* Identifier of pack selected column action, used in JXTable's
* ActionMap
.
*/
public static final String PACKSELECTED_ACTION_COMMAND = ColumnControlButton.COLUMN_CONTROL_MARKER + "packSelected";
/**
* The prefix marker to find table related properties in the
* ResourceBundle
.
*/
public static final String UIPREFIX = "JXTable.";
/**
* key for client property to use SearchHighlighter as match marker.
*/
public static final String MATCH_HIGHLIGHTER = AbstractSearchable.MATCH_HIGHLIGHTER;
static {
// Hack: make sure the resource bundle is loaded
LookAndFeelAddons.getAddon();
LookAndFeelAddons.contribute(new TableAddon());
}
/**
* The CompoundHighlighter for the table.
*/
protected CompoundHighlighter compoundHighlighter;
/**
* The key for the client property deciding about whether the color memory
* hack for DefaultTableCellRenderer should be used.
*
* @see #resetDefaultTableCellRendererHighlighter
*/
public static final String USE_DTCR_COLORMEMORY_HACK = "useDTCRColorMemoryHack";
/**
* The Highlighter used to hack around DefaultTableCellRenderer's color
* memory.
*/
protected Highlighter resetDefaultTableCellRendererHighlighter;
/**
* The ComponentAdapter for model data access.
*/
protected ComponentAdapter dataAdapter;
/**
* Listens for changes from the highlighters.
*/
private ChangeListener highlighterChangeListener;
/**
* the factory to use for column creation and configuration.
*/
private ColumnFactory columnFactory;
/**
* The default number of visible rows (in a ScrollPane).
*/
private int visibleRowCount = 20;
/**
* The default number of visible columns (in a ScrollPane).
*/
private int visibleColumnCount = -1;
/**
* Flag to indicate if the column control is visible.
*/
private boolean columnControlVisible;
/**
* ScrollPane's original vertical scroll policy. If the column control is
* visible the policy is set to ALWAYS.
*/
private int verticalScrollPolicy;
/**
* The component used a column control in the upper trailing corner of an
* enclosing JScrollPane
.
*/
private JComponent columnControlButton;
/**
* Mouse/Motion/Listener keeping track of mouse moved in cell coordinates.
*/
private transient RolloverProducer rolloverProducer;
/**
* RolloverController: listens to cell over events and repaints
* entered/exited rows.
*/
private transient TableRolloverController linkController;
/**
* field to store the autoResizeMode while interactively setting horizontal
* scrollbar to visible.
*/
private int oldAutoResizeMode;
/**
* flag to indicate enhanced auto-resize-off behaviour is on. This is
* set/reset in setHorizontalScrollEnabled.
*/
private boolean intelliMode;
/**
* internal flag indicating that we are in super.doLayout(). (used in
* columnMarginChanged to not update the resizingCol's prefWidth).
*/
private boolean inLayout;
/**
* Flag to distinguish internal settings of row height from client code
* settings. The rowHeight will be internally adjusted to font size on
* instantiation and in updateUI if the height has not been set explicitly
* by the application.
*
* @see #adminSetRowHeight(int)
* @see #setRowHeight(int)
*/
protected boolean isXTableRowHeightSet;
/**
* property to control search behaviour.
*/
protected Searchable searchable;
/**
* property to control table's editability as a whole.
*/
private boolean editable;
private Dimension calculatedPrefScrollableViewportSize;
/**
* flag to indicate whether the rowSorter is auto-created.
*/
private boolean autoCreateRowSorter;
/**
* flag to indicate if table is interactively sortable.
*/
private boolean sortable;
/**
* flag to indicate whether model update events should trigger resorts.
*/
private boolean sortsOnUpdates;
/**
* flag to indicate that it's unsafe to update sortable-related sorter properties.
*/
private boolean ignoreAddColumn;
/**
* Registry of per-cell string representation.
*/
private transient StringValueRegistry stringValueRegistry;
private SortOrder[] sortOrderCycle;
/**
* Instantiates a JXTable with a default table model, no data.
*/
public JXTable() {
init();
}
/**
* Instantiates a JXTable with a specific table model.
*
* @param dm The model to use.
*/
public JXTable(TableModel dm) {
super(dm);
init();
}
/**
* Instantiates a JXTable with a specific table model.
*
* @param dm The model to use.
*/
public JXTable(TableModel dm, TableColumnModel cm) {
super(dm, cm);
init();
}
/**
* Instantiates a JXTable with a specific table model, column model, and
* selection model.
*
* @param dm The table model to use.
* @param cm The column model to use.
* @param sm The list selection model to use.
*/
public JXTable(TableModel dm, TableColumnModel cm, ListSelectionModel sm) {
super(dm, cm, sm);
init();
}
/**
* Instantiates a JXTable for a given number of columns and rows.
*
* @param numRows Count of rows to accommodate.
* @param numColumns Count of columns to accommodate.
*/
public JXTable(int numRows, int numColumns) {
super(numRows, numColumns);
init();
}
/**
* Instantiates a JXTable with data in a vector or rows and column names.
*
* @param rowData Row data, as a Vector of Objects.
* @param columnNames Column names, as a Vector of Strings.
*/
public JXTable(Vector extends Vector> rowData, Vector> columnNames) {
super(rowData, columnNames);
init();
}
/**
* Instantiates a JXTable with data in a array or rows and column names.
*
* @param rowData Row data, as a two-dimensional Array of Objects (by row,
* for column).
* @param columnNames Column names, as a Array of Strings.
*/
public JXTable(Object[][] rowData, Object[] columnNames) {
super(rowData, columnNames);
init();
}
/**
* Initializes the table for use.
*/
private void init() {
putClientProperty(USE_DTCR_COLORMEMORY_HACK, Boolean.TRUE);
initDefaultStringValues();
sortOrderCycle = DefaultSortController.getDefaultSortOrderCycle();
setSortsOnUpdates(true);
setSortable(true);
setAutoCreateRowSorter(true);
setRolloverEnabled(true);
setEditable(true);
setTerminateEditOnFocusLost(true);
initActionsAndBindings();
initFocusBindings();
// instantiate row height depending ui setting or font size.
updateRowHeightUI(false);
// set to null - don't want hard-coded pixel sizes.
setPreferredScrollableViewportSize(null);
// PENDING: need to duplicate here..
// why doesn't the call in tableChanged work?
initializeColumnWidths();
setFillsViewportHeight(true);
updateLocaleState(getLocale());
}
//--------------- Rollover support
/**
* Sets the property to enable/disable rollover support. If enabled, this component
* fires property changes on per-cell mouse rollover state, i.e.
* when the mouse enters/leaves a list cell.
*
* This can be enabled to show "live" rollover behaviour, f.i. the cursor over a cell
* rendered by a JXHyperlink.
*
* The default value is true.
*
* @param rolloverEnabled a boolean indicating whether or not the rollover
* functionality should be enabled.
* @see #isRolloverEnabled()
* @see #getLinkController()
* @see #createRolloverProducer()
* @see org.jdesktop.swingx.rollover.RolloverRenderer
*/
public void setRolloverEnabled(boolean rolloverEnabled) {
boolean old = isRolloverEnabled();
if (rolloverEnabled == old)
return;
if (rolloverEnabled) {
rolloverProducer = createRolloverProducer();
rolloverProducer.install(this);
getLinkController().install(this);
} else {
rolloverProducer.release(this);
rolloverProducer = null;
getLinkController().release();
}
firePropertyChange("rolloverEnabled", old, isRolloverEnabled());
}
/**
* Returns a boolean indicating whether or not rollover support is enabled.
*
* @return a boolean indicating whether or not rollover support is enabled.
* @see #setRolloverEnabled(boolean)
*/
public boolean isRolloverEnabled() {
return rolloverProducer != null;
}
/**
* Returns the RolloverController for this component. Lazyly creates the
* controller if necessary, that is the return value is guaranteed to be
* not null.
*
* PENDING JW: rename to getRolloverController
*
* @return the RolloverController for this tree, guaranteed to be not null.
* @see #setRolloverEnabled(boolean)
* @see #createLinkController()
* @see org.jdesktop.swingx.rollover.RolloverController
*/
protected TableRolloverController getLinkController() {
if (linkController == null) {
linkController = createLinkController();
}
return linkController;
}
/**
* Creates and returns a RolloverController appropriate for this component.
*
* @return a RolloverController appropriate for this component.
* @see #getLinkController()
* @see org.jdesktop.swingx.rollover.RolloverController
*/
protected TableRolloverController createLinkController() {
return new TableRolloverController<>();
}
/**
* Creates and returns the RolloverProducer to use with this component.
*
*
* @return RolloverProducer
to use with this component
* @see #setRolloverEnabled(boolean)
*/
protected RolloverProducer createRolloverProducer() {
return new TableRolloverProducer();
}
/**
* Returns the column control visible property.
*
*
* @return boolean to indicate whether the column control is visible.
* @see #setColumnControlVisible(boolean)
* @see #setColumnControl(JComponent)
*/
public boolean isColumnControlVisible() {
return columnControlVisible;
}
/**
* Sets the column control visible property. If true and
* JXTable
is contained in a JScrollPane
, the
* table adds the column control to the trailing corner of the scroll pane.
*
*
* Note: if the table is not inside a JScrollPane
the column
* control is not shown even if this returns true. In this case it's the
* responsibility of the client code to actually show it.
*
*
* The default value is false
.
*
* @param visible boolean to indicate if the column control should be shown
* @see #isColumnControlVisible()
* @see #setColumnControl(JComponent)
*/
public void setColumnControlVisible(boolean visible) {
if (isColumnControlVisible() == visible)
return;
boolean old = isColumnControlVisible();
if (old) {
unconfigureColumnControl();
}
this.columnControlVisible = visible;
if (isColumnControlVisible()) {
configureColumnControl();
}
firePropertyChange("columnControlVisible", old, !old);
}
/**
* Returns the component used as column control. Lazily creates the control
* to the default if it is null
.
*
* @return component for column control, guaranteed to be != null.
* @see #setColumnControl(JComponent)
* @see #createDefaultColumnControl()
*/
public JComponent getColumnControl() {
if (columnControlButton == null) {
columnControlButton = createDefaultColumnControl();
}
return columnControlButton;
}
/**
* Sets the component used as column control. Updates the enclosing
* JScrollPane
if appropriate. Passing a null
* parameter restores the column control to the default.
*
* The component is automatically visible only if the
* columnControlVisible
property is true
and the
* table is contained in a JScrollPane
.
*
*
* NOTE: from the table's perspective, the column control is simply a
* JComponent
to add to and keep in the trailing corner of the
* scrollpane. (if any). It's up the concrete control to configure itself
* from and keep synchronized to the columns' states.
*
*
* @param columnControl the JComponent
to use as columnControl.
* @see #getColumnControl()
* @see #createDefaultColumnControl()
* @see #setColumnControlVisible(boolean)
*/
public void setColumnControl(JComponent columnControl) {
// PENDING JW: release old column control? who's responsible?
// Could implement CCB.autoRelease()?
JComponent old = columnControlButton;
this.columnControlButton = columnControl;
configureColumnControl();
firePropertyChange("columnControl", old, getColumnControl());
}
/**
* Creates the default column control used by this table. This
* implementation returns a ColumnControlButton
configured with
* default ColumnControlIcon
.
*
* @return the default component used as column control.
* @see #setColumnControl(JComponent)
* @see ColumnControlButton
* @see org.jdesktop.swingx.icon.ColumnControlIcon
*/
protected JComponent createDefaultColumnControl() {
return new ColumnControlButton(this);
}
/**
* Sets the language-sensitive orientation that is to be used to order the
* elements or text within this component.
*
*
* Overridden to work around a core bug: JScrollPane
can't cope
* with corners when changing component orientation at runtime. This method
* explicitly re-configures the column control.
*
*
* @param o the ComponentOrientation for this table.
* @see Component#setComponentOrientation(ComponentOrientation)
*/
@Override
public void setComponentOrientation(ComponentOrientation o) {
removeColumnControlFromCorners();
super.setComponentOrientation(o);
configureColumnControl();
}
/**
* Sets upper corners in JScrollPane to null if same as getColumnControl().
* This is a hack around core not coping correctly with component orientation.
*
* @see #setComponentOrientation(ComponentOrientation)
*/
protected void removeColumnControlFromCorners() {
JScrollPane scrollPane = getEnclosingScrollPane();
if (scrollPane == null || !isColumnControlVisible())
return;
removeColumnControlFromCorners(scrollPane, JScrollPane.UPPER_LEFT_CORNER, JScrollPane.UPPER_RIGHT_CORNER);
}
private void removeColumnControlFromCorners(JScrollPane scrollPane, String... corners) {
for (String corner : corners) {
if (scrollPane.getCorner(corner) == getColumnControl()) {
scrollPane.setCorner(corner, null);
}
}
}
/**
* Configures the enclosing JScrollPane
.
*
*
* Overridden to addionally configure the upper trailing corner with the
* column control.
*
* @see #configureColumnControl()
*/
@Override
protected void configureEnclosingScrollPane() {
super.configureEnclosingScrollPane();
configureColumnControl();
}
/**
* Unconfigures the enclosing JScrollPane
.
*
*
* Overridden to addionally unconfigure the upper trailing corner with the
* column control.
*
* @see #unconfigureColumnControl()
*/
@Override
protected void unconfigureEnclosingScrollPane() {
unconfigureColumnControl();
super.unconfigureEnclosingScrollPane();
}
/**
* /** Unconfigures the upper trailing corner of an enclosing
* JScrollPane
.
*
* Here: removes the upper trailing corner and resets.
*
* @see #setColumnControlVisible(boolean)
* @see #setColumnControl(JComponent)
*/
protected void unconfigureColumnControl() {
JScrollPane scrollPane = getEnclosingScrollPane();
if (scrollPane == null)
return;
if (verticalScrollPolicy != 0) {
// Fix #155-swingx: reset only if we had forced always before
// PENDING: JW - doesn't cope with dynamically changing the
// policy
// shouldn't be much of a problem because doesn't happen too
// often??
scrollPane.setVerticalScrollBarPolicy(verticalScrollPolicy);
verticalScrollPolicy = 0;
}
if (isColumnControlVisible()) {
scrollPane.setCorner(JScrollPane.UPPER_TRAILING_CORNER, null);
}
}
/**
* Configures the upper trailing corner of an enclosing
* JScrollPane
.
*
* Adds the ColumnControl
if the
* columnControlVisible
property is true.
*
*
* @see #setColumnControlVisible(boolean)
* @see #setColumnControl(JComponent)
*/
protected void configureColumnControl() {
if (!isColumnControlVisible())
return;
JScrollPane scrollPane = getEnclosingScrollPane();
if (scrollPane == null)
return;
if (verticalScrollPolicy == 0) {
verticalScrollPolicy = scrollPane.getVerticalScrollBarPolicy();
}
scrollPane.setCorner(JScrollPane.UPPER_TRAILING_CORNER, getColumnControl());
scrollPane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS);
}
/**
* Returns the enclosing JScrollPane of this table, or null if not
* contained in a JScrollPane or not the main view of the scrollPane.
*
* @return the enclosing JScrollPane if this table is the main view or
* null if not.
*/
protected JScrollPane getEnclosingScrollPane() {
Container p = getParent();
if (p instanceof JViewport) {
Container gp = p.getParent();
if (gp instanceof JScrollPane) {
JScrollPane scrollPane = (JScrollPane) gp;
// Make certain we are the viewPort's view and not, for
// example, the rowHeaderView of the scrollPane -
// an implementor of fixed columns might do this.
JViewport viewport = scrollPane.getViewport();
if (viewport == null || viewport.getView() != this) {
return null;
}
return scrollPane;
}
}
return null;
}
// --------------------- actions
/**
* Take over ctrl-tab.
*/
private void initFocusBindings() {
setFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS, Collections.emptySet());
setFocusTraversalKeys(KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS, Collections.emptySet());
getInputMap(WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(KeyStroke.getKeyStroke("ctrl TAB"), FOCUS_NEXT_COMPONENT);
getInputMap(WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(KeyStroke.getKeyStroke("shift ctrl TAB"), FOCUS_PREVIOUS_COMPONENT);
getActionMap().put(FOCUS_NEXT_COMPONENT, createFocusTransferAction(true));
getActionMap().put(FOCUS_PREVIOUS_COMPONENT, createFocusTransferAction(false));
}
/**
* Creates and returns an action for forward/backward focus transfer,
* depending on the given flag.
*
* @param forward a boolean indicating the direction of the required focus
* transfer
* @return the action bound to focusTraversal.
*/
private Action createFocusTransferAction(boolean forward) {
BoundAction action = new BoundAction(null, forward ? FOCUS_NEXT_COMPONENT : FOCUS_PREVIOUS_COMPONENT);
action.registerCallback(this, forward ? "transferFocus" : "transferFocusBackward");
return action;
}
/**
* A small class which dispatches actions.
*
* TODO (?): Is there a way that we can make this static?
*
*
* PENDING JW: don't use UIAction ... we are in OO-land!
*/
private class Actions extends UIAction {
Actions(String name) {
super(name);
}
@Override
public void actionPerformed(ActionEvent evt) {
if ("print".equals(getName())) {
try {
print();
} catch (PrinterException ex) {
// REMIND(aim): should invoke pluggable application error
// handler
LOG.log(Level.WARNING, "", ex);
}
} else if ("find".equals(getName())) {
doFind();
}
}
}
/**
* Registers additional, per-instance Action
s to the this
* table's ActionMap. Binds the search accelerator (as returned by the
* SearchFactory) to the find action.
*/
private void initActionsAndBindings() {
// Register the actions that this class can handle.
ActionMap map = getActionMap();
map.put("print", new Actions("print"));
map.put("find", new Actions("find"));
// hack around core bug: cancel editing doesn't fire
// reported against SwingX as of #610-swingx
map.put("cancel", createCancelAction());
map.put(PACKALL_ACTION_COMMAND, createPackAllAction());
map.put(PACKSELECTED_ACTION_COMMAND, createPackSelectedAction());
map.put(HORIZONTALSCROLL_ACTION_COMMAND, createHorizontalScrollAction());
KeyStroke findStroke = SearchFactory.getInstance().getSearchAccelerator();
getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(findStroke, "find");
}
/**
* Creates and returns an Action which cancels an ongoing edit correctly.
* Note: the correct thing to do is to call the editor's cancelEditing, the
* wrong thing to do is to call table removeEditor (as core JTable does...).
* So this is a quick hack around a core bug, reported against SwingX in
* #610-swingx.
*
* @return an Action which cancels an edit.
*/
private Action createCancelAction() {
return new AbstractActionExt() {
@Override
public void actionPerformed(ActionEvent e) {
if (!isEditing())
return;
getCellEditor().cancelCellEditing();
}
@Override
public boolean isEnabled() {
return isEditing();
}
};
}
/**
* Creates and returns the default Action
for toggling the
* horizontal scrollBar.
*/
private Action createHorizontalScrollAction() {
BoundAction action = new BoundAction(null, HORIZONTALSCROLL_ACTION_COMMAND);
action.setStateAction();
action.registerCallback(this, "setHorizontalScrollEnabled");
action.setSelected(isHorizontalScrollEnabled());
return action;
}
/**
* Returns a potentially localized value from the UIManager. The given key
* is prefixed by this table's UIPREFIX
before doing the
* lookup. The lookup respects this table's current locale
* property. Returns the key, if no value is found.
*
* @param key the bare key to look up in the UIManager.
* @return the value mapped to UIPREFIX + key or key if no value is found.
*/
protected String getUIString(String key) {
return getUIString(key, getLocale());
}
/**
* Returns a potentially localized value from the UIManager for the given
* locale. The given key is prefixed by this table's UIPREFIX
* before doing the lookup. Returns the key, if no value is found.
*
* @param key the bare key to look up in the UIManager.
* @param locale the locale use for lookup
* @return the value mapped to UIPREFIX + key in the given locale, or key if
* no value is found.
*/
protected String getUIString(String key, Locale locale) {
String text = UIManagerExt.getString(UIPREFIX + key, locale);
return text != null ? text : key;
}
/**
* Creates and returns the default Action
for packing the
* selected column.
*/
private Action createPackSelectedAction() {
BoundAction action = new BoundAction(null, PACKSELECTED_ACTION_COMMAND);
action.registerCallback(this, "packSelected");
action.setEnabled(getSelectedColumnCount() > 0);
return action;
}
/**
* Creates and returns the default Action for packing all columns.
*/
private Action createPackAllAction() {
BoundAction action = new BoundAction(null, PACKALL_ACTION_COMMAND);
action.registerCallback(this, "packAll");
return action;
}
/**
* {@inheritDoc}
*
* Overridden to update locale-dependent properties.
*
* @see #updateLocaleState(Locale)
*/
@Override
public void setLocale(Locale locale) {
updateLocaleState(locale);
super.setLocale(locale);
}
/**
* Updates locale-dependent state to the given Locale
.
*
* Here: updates registered column actions' locale-dependent state.
*
*
* PENDING: Try better to find all column actions including custom
* additions? Or move to columnControl?
*
* @param locale the Locale to use for value lookup
* @see #setLocale(Locale)
* @see #updateLocaleActionState(String, Locale)
*/
protected void updateLocaleState(Locale locale) {
updateLocaleActionState(HORIZONTALSCROLL_ACTION_COMMAND, locale);
updateLocaleActionState(PACKALL_ACTION_COMMAND, locale);
updateLocaleActionState(PACKSELECTED_ACTION_COMMAND, locale);
}
/**
* Updates locale-dependent state of action registered with key in
* ActionMap
. Does nothing if no action with key is found.
*
*
* Here: updates the Action
's name property.
*
* @param key the string for lookup in this table's ActionMap
* @see #updateLocaleState(Locale)
*/
protected void updateLocaleActionState(String key, Locale locale) {
Action action = getActionMap().get(key);
if (action == null)
return;
action.putValue(Action.NAME, getUIString(key, locale));
}
// ------------------ bound action callback methods
/**
* Resizes all columns to fit their content.
*
*
* By default this method is bound to the pack all columns
* Action
and registered in the table's ActionMap
.
*/
public void packAll() {
packTable(-1);
}
/**
* Resizes the lead column to fit its content.
*
*
* By default this method is bound to the pack selected column
* Action
and registered in the table's ActionMap
.
*/
public void packSelected() {
int selected = getColumnModel().getSelectionModel().getLeadSelectionIndex();
if (selected >= 0) {
packColumn(selected, -1);
}
}
/**
* {@inheritDoc}
*
*
* Overridden to update the enabled state of the pack selected column
* Action
.
*/
@Override
public void columnSelectionChanged(ListSelectionEvent e) {
super.columnSelectionChanged(e);
if (e.getValueIsAdjusting())
return;
Action packSelected = getActionMap().get(PACKSELECTED_ACTION_COMMAND);
if (packSelected != null) {
packSelected.setEnabled(!((ListSelectionModel) e.getSource()).isSelectionEmpty());
}
}
// ----------------------- scrollable control
/**
* Sets the enablement of enhanced horizontal scrolling. If enabled, it
* toggles an auto-resize mode which always fills the JViewport
* horizontally and shows the horizontal scrollbar if necessary.
*
*
* The default value is false
.
*
*
* Note: this is not a bound property, though it follows
* bean naming conventions.
*
* PENDING: Probably should be... If so, could be taken by a listening
* Action as in the app-framework.
*
* PENDING JW: the name is mis-leading?
*
* @param enabled a boolean indicating whether enhanced auto-resize mode is
* enabled.
* @see #isHorizontalScrollEnabled()
*/
public void setHorizontalScrollEnabled(boolean enabled) {
/*
* PENDING JW: add a "real" mode? Problematic because there are several
* places in core which check for #AUTO_RESIZE_OFF, can't use different
* value without unwanted side-effects. The current solution with
* tagging the #AUTO_RESIZE_OFF by a boolean flag #intelliMode is
* brittle - need to be very careful to turn off again ... Another
* problem is to keep the horizontalScrollEnabled toggling action in
* synch with this property. Yet another problem is the change
* notification: currently this is _not_ a bound property.
*/
if (enabled == isHorizontalScrollEnabled()) {
return;
}
boolean old = isHorizontalScrollEnabled();
if (enabled) {
// remember the resizeOn mode if any
if (getAutoResizeMode() != AUTO_RESIZE_OFF) {
oldAutoResizeMode = getAutoResizeMode();
}
setAutoResizeMode(AUTO_RESIZE_OFF);
// setAutoResizeModel always disables the intelliMode
// must set after calling and update the action again
intelliMode = true;
updateHorizontalAction();
} else {
setAutoResizeMode(oldAutoResizeMode);
}
firePropertyChange("horizontalScrollEnabled", old, isHorizontalScrollEnabled());
}
/**
* Returns the current setting for horizontal scrolling.
*
* @return the enablement of enhanced horizontal scrolling.
* @see #setHorizontalScrollEnabled(boolean)
*/
public boolean isHorizontalScrollEnabled() {
return intelliMode && getAutoResizeMode() == AUTO_RESIZE_OFF;
}
/**
* {@inheritDoc}
*
*
* Overridden for internal bookkeeping related to the enhanced auto-resize
* behaviour.
*
*
* Note: to enable/disable the enhanced auto-resize mode use exclusively
* setHorizontalScrollEnabled
, this method can't cope with it.
*
* @see #setHorizontalScrollEnabled(boolean)
*/
@Override
public void setAutoResizeMode(int mode) {
if (mode != AUTO_RESIZE_OFF) {
oldAutoResizeMode = mode;
}
intelliMode = false;
super.setAutoResizeMode(mode);
updateHorizontalAction();
}
/**
* Synchs selected state of horizontal scrolling Action
to
* enablement of enhanced auto-resize behaviour.
*/
protected void updateHorizontalAction() {
Action showHorizontal = getActionMap().get(HORIZONTALSCROLL_ACTION_COMMAND);
if (showHorizontal instanceof BoundAction) {
((BoundAction) showHorizontal).setSelected(isHorizontalScrollEnabled());
}
}
/**
* {@inheritDoc}
*
*
* Overridden to support enhanced auto-resize behaviour enabled and
* necessary.
*
* @see #setHorizontalScrollEnabled(boolean)
*/
@Override
public boolean getScrollableTracksViewportWidth() {
boolean shouldTrack = super.getScrollableTracksViewportWidth();
if (isHorizontalScrollEnabled()) {
return hasExcessWidth();
}
return shouldTrack;
}
/**
* Layouts column width. The exact behaviour depends on the
* autoResizeMode
property.
*
* Overridden to support enhanced auto-resize behaviour enabled and
* necessary.
*
* @see #setAutoResizeMode(int)
* @see #setHorizontalScrollEnabled(boolean)
*/
@Override
public void doLayout() {
int resizeMode = getAutoResizeMode();
// fool super...
if (isHorizontalScrollEnabled() && hasRealizedParent() && hasExcessWidth()) {
autoResizeMode = oldAutoResizeMode;
}
inLayout = true;
super.doLayout();
inLayout = false;
autoResizeMode = resizeMode;
}
/**
* @return boolean to indicate whether the table has a realized parent.
*/
private boolean hasRealizedParent() {
return getWidth() > 0 && getParent() != null && getParent().getWidth() > 0;
}
/**
* PRE: hasRealizedParent()
*
* @return boolean to indicate whether the table has widths excessing
* parent's width
*/
private boolean hasExcessWidth() {
return getPreferredSize().width < getParent().getWidth();
}
/**
* {@inheritDoc}
*
*
* Overridden to support enhanced auto-resize behaviour enabled and
* necessary.
*
* @see #setHorizontalScrollEnabled(boolean)
*/
@Override
public void columnMarginChanged(ChangeEvent e) {
if (isEditing()) {
removeEditor();
}
TableColumn resizingColumn = getResizingColumn();
// Need to do this here, before the parent's
// layout manager calls getPreferredSize().
if (resizingColumn != null && autoResizeMode == AUTO_RESIZE_OFF && !inLayout) {
resizingColumn.setPreferredWidth(resizingColumn.getWidth());
}
resizeAndRepaint();
}
/**
* Returns the column which is interactively resized. The return value is
* null if the header is null or has no resizing column.
*
* @return the resizing column.
*/
private TableColumn getResizingColumn() {
return tableHeader == null ? null : tableHeader.getResizingColumn();
}
/**
* {@inheritDoc}
*
* Overridden for documentation reasons only: same behaviour but different default value.
*
*
* The default value is true
.
*
*/
@Override
public void setFillsViewportHeight(boolean fillsViewportHeight) {
if (fillsViewportHeight == getFillsViewportHeight())
return;
super.setFillsViewportHeight(fillsViewportHeight);
}
// ------------------------ override super because of filter-awareness
/**
* {@inheritDoc}
* Overridden to respect the cell's editability, that is it has no effect if
* !isCellEditable(row, column)
.
*
* @see #isCellEditable(int, int)
*/
@Override
public void setValueAt(Object aValue, int row, int column) {
if (!isCellEditable(row, column))
return;
super.setValueAt(aValue, row, column);
}
/**
* Returns true if the cell at row
and column
is
* editable. Otherwise, invoking setValueAt
on the cell will
* have no effect.
*
* Overridden to account for row index mapping and to support a layered
* editability control:
*
* - per-table:
JXTable.isEditable()
* - per-column:
TableColumnExt.isEditable()
* - per-cell: controlled by the model
*
TableModel.isCellEditable()
*
* The view cell is considered editable only if all three layers are
* enabled.
*
* @param row the row index in view coordinates
* @param column the column index in view coordinates
* @return true if the cell is editable
* @see #setValueAt(Object, int, int)
* @see #isEditable()
* @see TableColumnExt#isEditable
* @see TableModel#isCellEditable
*/
@Override
public boolean isCellEditable(int row, int column) {
if (!isEditable())
return false;
boolean editable = super.isCellEditable(row, column);
if (editable) {
TableColumnExt tableColumn = getColumnExt(column);
if (tableColumn != null) {
editable = tableColumn.isEditable();
}
}
return editable;
}
/**
* {@inheritDoc}
*
*
* Overridden for documentation clarification. The property has the same
* meaning as super, that is if true to re-create all table columns on
* either setting a new TableModel or receiving a structureChanged from the
* existing. The most obvious visual effect is that custom column properties
* appear to be "lost".
*
*
* JXTable does support additonal custom configuration (via a custom
* ColumnFactory) which can (and incorrectly was) called independently from
* the creation. Setting this property to false guarantees that no column
* configuration is applied.
*
* @see #tableChanged(TableModelEvent)
* @see ColumnFactory
*/
@Override
public boolean getAutoCreateColumnsFromModel() {
return super.getAutoCreateColumnsFromModel();
}
/**
* {@inheritDoc}
*
*
* Overridden to update internal state related to enhanced functionality and
* hack around core bugs.
*
* - re-calculate intialize column width and preferred
* scrollable size after a structureChanged if autocreateColumnsFromModel is
* true.
*
- update string representation control after structureChanged
*
- core bug #6791934 logic to force revalidate if appropriate
*
*
*/
@Override
public void tableChanged(TableModelEvent e) {
preprocessModelChange(e);
super.tableChanged(e);
if (isStructureChanged(e) && getAutoCreateColumnsFromModel()) {
initializeColumnWidths();
resetCalculatedScrollableSize(true);
}
if (isStructureChanged(e)) {
updateStringValueRegistryColumnClasses();
}
postprocessModelChange(e);
}
//----> start hack around core issue 6791934:
// table not updated correctly after updating model
// while having a sorter with filter.
/**
* Overridden to hack around core bug
* http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6791934
*/
@Override
public void sorterChanged(RowSorterEvent e) {
super.sorterChanged(e);
postprocessSorterChanged(e);
}
/**
* flag to indicate if forced revalidate is needed.
*/
protected boolean forceRevalidate;
/**
* flag to indicate if a sortOrderChanged has happened between pre- and postProcessModelChange.
*/
protected boolean filteredRowCountChanged;
/**
* Hack around core issue 6791934: sets flags to force revalidate if appropriate.
* Called before processing the event.
*
* @param e the TableModelEvent received from the model
*/
protected void preprocessModelChange(TableModelEvent e) {
forceRevalidate = getSortsOnUpdates() && getRowFilter() != null && isUpdate(e);
}
/**
* Hack around core issue 6791934: forces a revalidate if appropriate and resets
* internal flags.
* Called after processing the event.
*
* @param e the TableModelEvent received from the model
*/
protected void postprocessModelChange(TableModelEvent e) {
if (forceRevalidate && filteredRowCountChanged) {
resizeAndRepaint();
}
filteredRowCountChanged = false;
forceRevalidate = false;
}
/**
* Hack around core issue 6791934: sets the sorter changed flag if appropriate.
* Called after processing the event.
*
* @param e the sorter event received from the sorter
*/
protected void postprocessSorterChanged(RowSorterEvent e) {
filteredRowCountChanged = false;
if (forceRevalidate && e.getType() == RowSorterEvent.Type.SORTED) {
filteredRowCountChanged = e.getPreviousRowCount() != getRowCount();
}
}
//----> end hack around core issue 6791934:
/**
* {@inheritDoc}
*
* Overridden to prevent super from creating RowSorter.
*/
@Override
public void setModel(TableModel dataModel) {
boolean old = getAutoCreateRowSorter();
try {
this.autoCreateRowSorter = false;
this.ignoreAddColumn = true;
super.setModel(dataModel);
} finally {
this.autoCreateRowSorter = old;
this.ignoreAddColumn = false;
}
if (getAutoCreateRowSorter()) {
setRowSorter(createDefaultRowSorter());
}
}
/**
* {@inheritDoc}
*
* Overridden to synch sorter state from columns.
*/
@Override
public void setColumnModel(TableColumnModel columnModel) {
super.setColumnModel(columnModel);
configureSorterProperties();
initPerColumnStringValues();
}
/**
* {@inheritDoc}
*
* Overridden to
*
* - fix core bug: replaces sorter even if flag doesn't change.
*
- use xflag (need because super's RowSorter creation is hard-coded.
*
*/
@Override
public void setAutoCreateRowSorter(boolean autoCreateRowSorter) {
if (getAutoCreateRowSorter() == autoCreateRowSorter)
return;
boolean oldValue = getAutoCreateRowSorter();
this.autoCreateRowSorter = autoCreateRowSorter;
if (autoCreateRowSorter) {
setRowSorter(createDefaultRowSorter());
}
firePropertyChange("autoCreateRowSorter", oldValue, getAutoCreateRowSorter());
}
/**
* {@inheritDoc}
*
* Overridden to return xflag
*/
@Override
public boolean getAutoCreateRowSorter() {
return autoCreateRowSorter;
}
/**
* {@inheritDoc}
*
* Overridden propagate sort-related properties to the sorter after calling super,
* if the given RowSorter is of type SortController. Does nothing additional otherwise.
*/
@Override
public void setRowSorter(RowSorter extends TableModel> sorter) {
super.setRowSorter(sorter);
configureSorterProperties();
}
/**
* Propagates sort-related properties from table/columns to the sorter if it
* is of type SortController, does nothing otherwise.
*/
protected void configureSorterProperties() {
// need to hack: if a structureChange is the result of a setModel
// the rowsorter is not yet updated
if (ignoreAddColumn || !getControlsSorterProperties())
return;
getSortController().setStringValueProvider(getStringValueRegistry());
// configure from table properties
getSortController().setSortable(sortable);
getSortController().setSortsOnUpdates(sortsOnUpdates);
getSortController().setSortOrderCycle(getSortOrderCycle());
// configure from column properties
List columns = getColumns(true);
for (TableColumn tableColumn : columns) {
int modelIndex = tableColumn.getModelIndex();
getSortController().setSortable(
modelIndex,
tableColumn instanceof TableColumnExt ? ((TableColumnExt) tableColumn).isSortable() : true
);
getSortController().setComparator(
modelIndex,
tableColumn instanceof TableColumnExt ? ((TableColumnExt) tableColumn).getComparator() : null
);
}
}
/**
* Creates and returns the default RowSorter. Note that this is already
* configured to the current TableModel - no api in the base class to set
* the model?
*
* PENDING JW: review method signature - better expose the need for the
* model by adding a parameter?
*
* @return the default RowSorter.
*/
protected RowSorter extends TableModel> createDefaultRowSorter() {
return new TableSortController<>(getModel());
}
/**
* Convenience method to detect dataChanged table event type.
*
* @param e the event to examine.
* @return true if the event is of type dataChanged, false else.
*/
protected boolean isDataChanged(TableModelEvent e) {
if (e == null)
return false;
return e.getType() == TableModelEvent.UPDATE && e.getFirstRow() == 0 && e.getLastRow() == Integer.MAX_VALUE;
}
/**
* Convenience method to detect update table event type.
*
* @param e the event to examine.
* @return true if the event is of type update and not dataChanged, false
* else.
*/
protected boolean isUpdate(TableModelEvent e) {
if (isStructureChanged(e))
return false;
return e.getType() == TableModelEvent.UPDATE && e.getLastRow() < Integer.MAX_VALUE;
}
/**
* Convenience method to detect a structureChanged table event type.
*
* @param e the event to examine.
* @return true if the event is of type structureChanged or null, false
* else.
*/
protected boolean isStructureChanged(TableModelEvent e) {
return e == null || e.getFirstRow() == TableModelEvent.HEADER_ROW;
}
// -------------------------------- sorting: configure sorter
/**
* Sets "sortable" property indicating whether or not this table
* supports sortable columns. If sortable
is true
* then sorting will be enabled on all columns whose sortable
* property is true
. If sortable
is
* false
then sorting will be disabled for all columns,
* regardless of each column's individual sorting
property. The
* default is true
.
*
* Note: as of post-1.0 this property is propagated to the SortController
* if controlsSorterProperties is true.
* Whether or not a change triggers a re-sort is up to either the concrete controller
* implementation (the default doesn't) or client code. This behaviour is
* different from old SwingX style sorting.
*
* @param sortable boolean indicating whether or not this table supports
* sortable columns
* @see #getControlsSorterProperties()
*/
public void setSortable(boolean sortable) {
boolean old = isSortable();
this.sortable = sortable;
if (getControlsSorterProperties()) {
getSortController().setSortable(sortable);
}
firePropertyChange("sortable", old, isSortable());
}
/**
* Returns the table's sortable property.
*
* @return true if the table is sortable.
* @see #setSortable(boolean)
*/
public boolean isSortable() {
return sortable;
}
/**
* If true, specifies that a sort should happen when the underlying
* model is updated (rowsUpdated
is invoked). For
* example, if this is true and the user edits an entry the
* location of that item in the view may change.
* This property is propagated to the SortController
* if controlsSorterProperties is true.
*
*
* The default value is true.
*
* @param sortsOnUpdates whether or not to sort on update events
* @see #getSortsOnUpdates()
* @see #getControlsSorterProperties()
*/
public void setSortsOnUpdates(boolean sortsOnUpdates) {
boolean old = getSortsOnUpdates();
this.sortsOnUpdates = sortsOnUpdates;
if (getControlsSorterProperties()) {
getSortController().setSortsOnUpdates(sortsOnUpdates);
}
firePropertyChange("sortsOnUpdates", old, getSortsOnUpdates());
}
/**
* Returns true if a sort should happen when the underlying
* model is updated; otherwise, returns false.
*
* @return whether or not to sort when the model is updated
*/
public boolean getSortsOnUpdates() {
return sortsOnUpdates;
}
/**
* Sets the sortorder cycle used when toggle sorting this table's columns.
* This property is propagated to the SortController
* if controlsSorterProperties is true.
*
* @param cycle the sequence of zero or more not-null SortOrders to cycle through.
* @throws NullPointerException if the array or any of its elements are null
*/
public void setSortOrderCycle(SortOrder... cycle) {
SortOrder[] old = getSortOrderCycle();
if (getControlsSorterProperties()) {
getSortController().setSortOrderCycle(cycle);
}
this.sortOrderCycle = Arrays.copyOf(cycle, cycle.length);
firePropertyChange("sortOrderCycle", old, getSortOrderCycle());
}
/**
* Returns the sortOrder cycle used when toggle sorting this table's columns, guaranteed
* to be not null.
*
* @return the sort order cycle used in toggle sort, not null
*/
public SortOrder[] getSortOrderCycle() {
return Arrays.copyOf(sortOrderCycle, sortOrderCycle.length);
}
//------------------------- sorting: sort/filter
/**
* Sets the filter to the sorter, if available and of type SortController.
* Does nothing otherwise.
*
*
* @param filter the filter used to determine what entries should be
* included
*/
@SuppressWarnings("unchecked")
public void setRowFilter(RowFilter super R, ? super Integer> filter) {
if (hasSortController()) {
// all fine, because R is a TableModel (R extends TableModel)
SortController controller = (SortController) getSortController();
controller.setRowFilter(filter);
}
}
/**
* Returns the filter of the sorter, if available and of type SortController.
* Returns null otherwise.
*
* PENDING JW: generics? had to remove return type from getSortController to
* make this compilable, so probably wrong.
*
* @return the filter used in the sorter.
*/
public RowFilter, ?> getRowFilter() {
return hasSortController() ? getSortController().getRowFilter() : null;
}
/**
* Resets sorting of all columns.
* Delegates to the SortController if available, or does nothing if not.
*
* PENDING JW: method name - consistent in SortController and here.
*/
public void resetSortOrder() {
if (!hasSortController())
return;
getSortController().resetSortOrders();
// JW PENDING: think about notification instead of manual repaint.
if (getTableHeader() != null) {
getTableHeader().repaint();
}
}
/**
* Toggles the sort order of the column at columnIndex.
* Delegates to the SortController if available, or does nothing if not.
*
*
* The exact behaviour is defined by the SortController's toggleSortOrder
* implementation. Typically a unsorted column is sorted in ascending order,
* a sorted column's order is reversed.
*
*
* PRE: {@code 0 <= columnIndex < getColumnCount()}
*
* @param columnIndex the columnIndex in view coordinates.
*/
public void toggleSortOrder(int columnIndex) {
if (hasSortController()) {
getSortController().toggleSortOrder(convertColumnIndexToModel(columnIndex));
}
}
/**
* Sorts the table by the given column using SortOrder.
* Delegates to the SortController if available, or does nothing if not.
*
* PRE: {@code 0 <= columnIndex < getColumnCount()}
*
*
* @param columnIndex the column index in view coordinates.
* @param sortOrder the sort order to use.
*/
public void setSortOrder(int columnIndex, SortOrder sortOrder) {
if (hasSortController()) {
getSortController().setSortOrder(convertColumnIndexToModel(columnIndex), sortOrder);
}
}
/**
* Returns the SortOrder of the given column.
* Delegates to the SortController if available, or returns SortOrder.UNSORTED if not.
*
* @param columnIndex the column index in view coordinates.
* @return the interactive sorter's SortOrder if matches the column or
* SortOrder.UNSORTED
*/
public SortOrder getSortOrder(int columnIndex) {
if (hasSortController()) {
return getSortController().getSortOrder(convertColumnIndexToModel(columnIndex));
}
return SortOrder.UNSORTED;
}
/**
* Toggles the sort order of the column with identifier.
* Delegates to the SortController if available, or does nothing if not.
*
* The exact behaviour of a toggle is defined by the SortController's toggleSortOrder
* implementation. Typically a unsorted column is sorted in ascending order,
* a sorted column's order is reversed.
*
*
* PENDING: JW - define the behaviour if the identifier is not found. This
* can happen if either there's no column at all with the identifier or if
* there's no column of type TableColumnExt. Currently does nothing, that is
* does not change sort state.
*
* @param identifier the column identifier.
*/
public void toggleSortOrder(Object identifier) {
if (!hasSortController())
return;
TableColumn columnExt = getColumnByIdentifier(identifier);
if (columnExt == null)
return;
getSortController().toggleSortOrder(columnExt.getModelIndex());
}
/**
* Sorts the table by the given column using the SortOrder.
* Delegates to the SortController, if available or does nothing if not.
*
*
* PENDING: JW - define the behaviour if the identifier is not found. This
* can happen if either there's no column at all with the identifier or if
* there's no column of type TableColumnExt. Currently does nothing, that is
* does not change sort state.
*
* @param identifier the column's identifier.
* @param sortOrder the sort order to use. If null or SortOrder.UNSORTED,
* this method has the same effect as resetSortOrder();
*/
public void setSortOrder(Object identifier, SortOrder sortOrder) {
if (!hasSortController())
return;
TableColumn columnExt = getColumnByIdentifier(identifier);
if (columnExt == null)
return;
getSortController().setSortOrder(columnExt.getModelIndex(), sortOrder);
}
/**
* Returns the SortOrder of the given column.
* Delegates to the SortController if available, or returns SortOrder.UNSORTED if not.
*
* PENDING: JW - define the behaviour if the identifier is not found. This
* can happen if either there's no column at all with the identifier or if
* there's no column of type TableColumnExt. Currently returns
* SortOrder.UNSORTED.
*
* @param identifier the column's identifier.
* @return the interactive sorter's SortOrder if matches the column or
* SortOrder.UNSORTED
*/
public SortOrder getSortOrder(Object identifier) {
if (!hasSortController())
return SortOrder.UNSORTED;
TableColumn columnExt = getColumnByIdentifier(identifier);
if (columnExt == null)
return SortOrder.UNSORTED;
int modelIndex = columnExt.getModelIndex();
return getSortController().getSortOrder(modelIndex);
}
/**
* Returns a contained TableColumn with the given identifier.
*
* Note that this is a hack around weird columnModel.getColumn(Object) contract in
* core TableColumnModel (throws exception if not found).
*
* @param identifier the column identifier
* @return a TableColumn with the identifier if found, or null if not found.
*/
private TableColumn getColumnByIdentifier(Object identifier) {
TableColumn columnExt;
try {
columnExt = getColumn(identifier);
} catch (IllegalArgumentException e) {
// hacking around weird getColumn(Object) behaviour -
// PENDING JW: revisit and override
columnExt = getColumnExt(identifier);
}
return columnExt;
}
/**
* Returns the currently active SortController. May be null, if the current RowSorter
* is not an instance of SortController.
*
* PENDING JW: generics - can't get the
* RowFilter getter signature correct with having controller typed here.
*
* PENDING JW: swaying about hiding or not - currently the only way to
* make the view not configure a RowSorter of type SortController is to
* let this return null.
*
* @return the currently active SortController
may be null
*/
@SuppressWarnings("unchecked")
protected SortController extends TableModel> getSortController() {
if (hasSortController()) {
// JW: the RowSorter is always of type extends TableModel>
// so the unchecked cast is safe
return (SortController extends TableModel>) getRowSorter();
}
return null;
}
/**
* Returns a boolean indicating whether the table has a SortController.
* If true, the call to getSortController is guaranteed to return a not-null
* value.
*
* @return a boolean indicating whether the table has a SortController.
* @see #getSortController()
*/
protected boolean hasSortController() {
return getRowSorter() instanceof SortController>;
}
/**
* Returns a boolean indicating whether the table configures the sorter's
* properties. If true, guaranteed that table's and the columns' sort related
* properties are propagated to the sorter. If false, guaranteed to not
* touch the sorter's configuration.
*
* This implementation returns true if the sorter is of type SortController.
*
* Note: the synchronization is unidirection from the table to the sorter.
* Changing the sorter under the table's feet might lead to undefined
* behaviour.
*
* @return a boolean indicating whether the table configurers the sorter's
* properties.
*/
protected boolean getControlsSorterProperties() {
return hasSortController() && getAutoCreateRowSorter();
}
/**
* Returns the primary sort column, or null if nothing sorted or no sortKey
* corresponds to a TableColumn currently contained in the TableColumnModel.
*
* @return the currently interactively sorted TableColumn or null if there
* is not sorter active or if the sorted column index does not
* correspond to any column in the TableColumnModel.
*/
public TableColumn getSortedColumn() {
// bloody hack: get primary SortKey and
// check if there's a column with it available
RowSorter> controller = getRowSorter();
if (controller != null) {
// PENDING JW: must use RowSorter?
SortKey sortKey = SortUtils.getFirstSortingKey(controller.getSortKeys());
if (sortKey != null) {
int sorterColumn = sortKey.getColumn();
List columns = getColumns(true);
for (TableColumn column : columns) {
if (column.getModelIndex() == sorterColumn) {
return column;
}
}
}
}
return null;
}
/**
* Returns the view column index of the primary sort column.
*
* @return the view column index of the primary sort column or -1 if nothing
* sorted or the primary sort column not visible.
*/
public int getSortedColumnIndex() {
RowSorter> controller = getRowSorter();
if (controller != null) {
SortKey sortKey = SortUtils.getFirstSortingKey(controller.getSortKeys());
if (sortKey != null) {
return convertColumnIndexToView(sortKey.getColumn());
}
}
return -1;
}
/**
* {@inheritDoc}
*
* Overridden to propagate sort-related column properties to the SortController and
* to update string representation of column.
*
* PENDING JW: check correct update on visibility change!
* PENDING JW: need cleanup of string rep after column removed (if it's a real remove)
*/
@Override
public void columnAdded(TableColumnModelEvent e) {
super.columnAdded(e);
// PENDING JW: check for visibility event?
TableColumn column = getColumn(e.getToIndex());
updateStringValueForColumn(column, column.getCellRenderer());
if (ignoreAddColumn)
return;
updateSortableAfterColumnChanged(column, column instanceof TableColumnExt ? ((TableColumnExt) column).isSortable() : true);
updateComparatorAfterColumnChanged(column, column instanceof TableColumnExt ? ((TableColumnExt) column).getComparator() : null);
}
// ----------------- enhanced column support: delegation to TableColumnModel
/**
* Returns the TableColumn
at view position
* columnIndex
. The return value is not null
.
*
*
* NOTE: This delegate method is added to protect developer's from
* unexpected exceptions in jdk1.5+. Super does not expose the
* TableColumn
access by index which may lead to unexpected
* IllegalArgumentException
: If client code assumes the
* delegate method is available, autoboxing will convert the given int to an
* Integer which will call the getColumn(Object) method.
*
* @param viewColumnIndex index of the column with the object in question
* @return the TableColumn
object that matches the column index
* @throws ArrayIndexOutOfBoundsException if viewColumnIndex out of allowed
* range.
* @see #getColumn(Object)
* @see #getColumnExt(int)
* @see TableColumnModel#getColumn(int)
*/
public TableColumn getColumn(int viewColumnIndex) {
return getColumnModel().getColumn(viewColumnIndex);
}
/**
* Returns a List
of visible TableColumn
s.
*
* @return a List
of visible columns.
* @see #getColumns(boolean)
*/
public List getColumns() {
return Collections.list(getColumnModel().getColumns());
}
/**
* Returns the margin between columns.
*
*
* Convenience to expose column model properties through
* JXTable
api.
*
* @return the margin between columns
* @see #setColumnMargin(int)
* @see TableColumnModel#getColumnMargin()
*/
public int getColumnMargin() {
return getColumnModel().getColumnMargin();
}
/**
* Sets the margin between columns.
*
* Convenience to expose column model properties through
* JXTable
api.
*
* @param value margin between columns; must be greater than or equal to
* zero.
* @see #getColumnMargin()
* @see TableColumnModel#setColumnMargin(int)
*/
public void setColumnMargin(int value) {
getColumnModel().setColumnMargin(value);
}
// ----------------- enhanced column support: delegation to
// TableColumnModelExt
/**
* Returns the number of contained columns. The count includes or excludes
* invisible columns, depending on whether the includeHidden
is
* true or false, respectively. If false, this method returns the same count
* as getColumnCount()
. If the columnModel is not of type
* TableColumnModelExt
, the parameter value has no effect.
*
* @param includeHidden a boolean to indicate whether invisible columns
* should be included
* @return the number of contained columns, including or excluding the
* invisible as specified.
* @see #getColumnCount()
* @see TableColumnModelExt#getColumnCount(boolean)
*/
public int getColumnCount(boolean includeHidden) {
if (getColumnModel() instanceof TableColumnModelExt) {
return ((TableColumnModelExt) getColumnModel()).getColumnCount(includeHidden);
}
return getColumnCount();
}
/**
* Returns a List
of contained TableColumn
s.
* Includes or excludes invisible columns, depending on whether the
* includeHidden
is true or false, respectively. If false, an
* Iterator
over the List is equivalent to the
* Enumeration
returned by getColumns()
. If the
* columnModel is not of type TableColumnModelExt
, the
* parameter value has no effect.
*
*
* NOTE: the order of columns in the List depends on whether or not the
* invisible columns are included, in the former case it's the insertion
* order in the latter it's the current order of the visible columns.
*
* @param includeHidden a boolean to indicate whether invisible columns
* should be included
* @return a List
of contained columns.
* @see #getColumns()
* @see TableColumnModelExt#getColumns(boolean)
*/
public List getColumns(boolean includeHidden) {
if (getColumnModel() instanceof TableColumnModelExt) {
return ((TableColumnModelExt) getColumnModel()).getColumns(includeHidden);
}
return getColumns();
}
/**
* Returns the first TableColumnExt
with the given
* identifier
. The return value is null if there is no
* contained column with identifier or if the column with
* identifier
is not of type TableColumnExt
. The
* returned column may be visible or hidden.
*
* @param identifier the object used as column identifier
* @return first TableColumnExt
with the given identifier or
* null if none is found
* @see #getColumnExt(int)
* @see #getColumn(Object)
* @see TableColumnModelExt#getColumnExt(Object)
*/
public TableColumnExt getColumnExt(Object identifier) {
if (getColumnModel() instanceof TableColumnModelExt) {
return ((TableColumnModelExt) getColumnModel()).getColumnExt(identifier);
} else {
// PENDING: not tested!
try {
TableColumn column = getColumn(identifier);
if (column instanceof TableColumnExt) {
return (TableColumnExt) column;
}
} catch (Exception e) {
// TODO: handle exception
}
}
return null;
}
/**
* Returns the TableColumnExt
at view position
* columnIndex
. The return value is null, if the column at
* position columnIndex
is not of type
* TableColumnExt
. The returned column is visible.
*
* @param viewColumnIndex the index of the column desired
* @return the TableColumnExt
object that matches the column
* index
* @throws ArrayIndexOutOfBoundsException if columnIndex out of allowed
* range, that is if
* {@code (columnIndex < 0) || (columnIndex >= getColumnCount())}
* @see #getColumnExt(Object)
* @see #getColumn(int)
* @see TableColumnModelExt#getColumnExt(int)
*/
public TableColumnExt getColumnExt(int viewColumnIndex) {
TableColumn column = getColumn(viewColumnIndex);
if (column instanceof TableColumnExt) {
return (TableColumnExt) column;
}
return null;
}
// ---------------------- enhanced TableColumn/Model support: convenience
/**
* Reorders the columns in the sequence given array. Logical names that do
* not correspond to any column in the model will be ignored. Columns with
* logical names not contained are added at the end.
*
* PENDING JW - do we want this? It's used by JNTable.
*
* @param identifiers array of logical column names
* @see #getColumns(boolean)
*/
public void setColumnSequence(Object[] identifiers) {
/*
* JW: not properly tested (not in all in fact) ...
*/
List columns = getColumns(true);
Map