org.jdesktop.swingx.JXTable Maven / Gradle / Ivy
Show all versions of swingx Show documentation
/*
* $Id: JXTable.java,v 1.226 2007/11/19 17:52:57 kschaefe 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.applet.Applet;
import java.awt.Color;
import java.awt.Component;
import java.awt.ComponentOrientation;
import java.awt.Container;
import java.awt.Cursor;
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.Field;
import java.text.DateFormat;
import java.text.NumberFormat;
import java.util.Collections;
import java.util.Date;
import java.util.Enumeration;
import java.util.EventObject;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.TreeSet;
import java.util.Vector;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.swing.Action;
import javax.swing.ActionMap;
import javax.swing.DefaultCellEditor;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JCheckBox;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JPopupMenu;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.JTextField;
import javax.swing.JViewport;
import javax.swing.KeyStroke;
import javax.swing.ListSelectionModel;
import javax.swing.ScrollPaneConstants;
import javax.swing.SizeSequence;
import javax.swing.SwingUtilities;
import javax.swing.UIDefaults;
import javax.swing.UIManager;
import javax.swing.border.Border;
import javax.swing.border.EmptyBorder;
import javax.swing.border.LineBorder;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.TableColumnModelEvent;
import javax.swing.event.TableModelEvent;
import javax.swing.table.DefaultTableCellRenderer;
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 org.jdesktop.swingx.action.AbstractActionExt;
import org.jdesktop.swingx.action.BoundAction;
import org.jdesktop.swingx.decorator.AbstractHighlighter;
import org.jdesktop.swingx.decorator.ColorHighlighter;
import org.jdesktop.swingx.decorator.ComponentAdapter;
import org.jdesktop.swingx.decorator.CompoundHighlighter;
import org.jdesktop.swingx.decorator.DefaultSelectionMapper;
import org.jdesktop.swingx.decorator.FilterPipeline;
import org.jdesktop.swingx.decorator.HighlightPredicate;
import org.jdesktop.swingx.decorator.Highlighter;
import org.jdesktop.swingx.decorator.PipelineEvent;
import org.jdesktop.swingx.decorator.PipelineListener;
import org.jdesktop.swingx.decorator.ResetDTCRColorHighlighter;
import org.jdesktop.swingx.decorator.SearchPredicate;
import org.jdesktop.swingx.decorator.SelectionMapper;
import org.jdesktop.swingx.decorator.SizeSequenceMapper;
import org.jdesktop.swingx.decorator.SortController;
import org.jdesktop.swingx.decorator.SortKey;
import org.jdesktop.swingx.decorator.SortOrder;
import org.jdesktop.swingx.event.TableColumnModelExtListener;
import org.jdesktop.swingx.icon.ColumnControlIcon;
import org.jdesktop.swingx.plaf.LookAndFeelAddons;
import org.jdesktop.swingx.plaf.UIManagerExt;
import org.jdesktop.swingx.renderer.ButtonProvider;
import org.jdesktop.swingx.renderer.DefaultTableRenderer;
import org.jdesktop.swingx.renderer.FormatStringValue;
import org.jdesktop.swingx.renderer.IconValue;
import org.jdesktop.swingx.renderer.MappedValue;
import org.jdesktop.swingx.renderer.StringValue;
import org.jdesktop.swingx.table.ColumnControlButton;
import org.jdesktop.swingx.table.ColumnFactory;
import org.jdesktop.swingx.table.DefaultTableColumnModelExt;
import org.jdesktop.swingx.table.TableColumnExt;
import org.jdesktop.swingx.table.TableColumnModelExt;
/**
*
* A JXTable is a JTable with built-in support for row sorting, filtering, and
* highlighting, column visibility and a special popup control on the column
* header for quick access to table configuration. You can instantiate a JXTable
* just as you would a JTable, using a TableModel. However, a JXTable
* automatically wraps TableColumns inside a TableColumnExt instance.
* TableColumnExt supports visibility, sortability, and prototype values for
* column sizing, none of which are available in TableColumn. You can retrieve
* the TableColumnExt instance for a column using {@link #getColumnExt(Object)}
* or {@link #getColumnExt(int colnumber)}.
*
*
* A JXTable is, by default, sortable by clicking on column headers; each
* subsequent click on a header reverses the order of the sort, and a sort arrow
* icon is automatically drawn on the header. Sorting can be disabled using
* {@link #setSortable(boolean)}. Sorting on columns is handled by a Sorter
* instance which contains a Comparator used to compare values in two rows of a
* column. You can replace the Comparator for a given column by using
* getColumnExt("column").setComparator(customComparator)
*
*
* 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.
*
*
* Rows can be filtered from a JXTable using a Filter class and a
* FilterPipeline. One assigns a FilterPipeline to the table using
* {@link #setFilters(FilterPipeline)}. Filtering hides, but does not delete or
* permanently remove rows from a JXTable. Filters are used to provide sorting
* to the table--rows are not removed, but the table is made to believe rows in
* the model are in a sorted order.
*
*
* One can automatically highlight certain rows in a JXTable by attaching
* Highlighters with setHighlighters(Highlighter) method. An
* example would be a Highlighter that colors alternate rows in the table for
* readability; AlternateRowHighlighter does this. Again, like Filters,
* Highlighters can be chained together in a CompoundHighlighter to achieve more
* interesting effects.
*
*
* 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.
*
*
* JXTable guarantees to delegate creation and configuration of TableColumnExt
* to a ColumnFactory. By default, the application-wide shared ColumnFactory is used.
* You can install a custom ColumnFactory, either application-wide by
* {@link ColumnFactory#setInstance(ColumnFactory)} or per table instance by
* {@link #setColumnFactory(ColumnFactory)}.
*
*
* Last, you can also provide searches on a JXTable using the Searchable property.
*
*
* 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
*/
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();
}
/** The FilterPipeline for the table. */
protected FilterPipeline filters;
/** 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()
*/
protected 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;
/** The handler for mapping view/model coordinates of row selection. */
private SelectionMapper selectionMapper;
/** flag to indicate if table is interactively sortable. */
private boolean sortable;
/** Listens for changes from the filters. */
private PipelineListener pipelineListener;
/** 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;
private SizeSequenceMapper rowModelMapper;
private Field rowModelField;
private boolean rowHeightEnabled;
/**
* 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 RolloverProducer rolloverProducer;
/**
* RolloverController: listens to cell over events and repaints
* entered/exited rows.
*/
private TableRolloverController linkController;
/** field to store the autoResizeMode while interactively setting
* horizontal scrollbar to visible.
*/
private int oldAutoResizeMode;
/** property to control the tracksViewportHeight behaviour. */
private boolean fillsViewportHeight;
/** 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 rowheight 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;
/** 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 colomn 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 accomodate.
* @param numColumns Count of columns to accomodate.
*/
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 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);
setEditable(true);
setSortable(true);
setRolloverEnabled(true);
setTerminateEditOnFocusLost(true);
// guarantee getFilters() to return != null
setFilters(null);
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());
}
/**
* Property to enable/disable rollover support. This can be enabled to show
* "live" rollover behaviour, f.i. the cursor over LinkModel cells. Default
* is enabled. If rollover effects are not used, this property should be
* disabled.
*
* @param rolloverEnabled
*/
public void setRolloverEnabled(boolean rolloverEnabled) {
boolean old = isRolloverEnabled();
if (rolloverEnabled == old)
return;
if (rolloverEnabled) {
rolloverProducer = createRolloverProducer();
addMouseListener(rolloverProducer);
addMouseMotionListener(rolloverProducer);
getLinkController().install(this);
} else {
removeMouseListener(rolloverProducer);
removeMouseMotionListener(rolloverProducer);
rolloverProducer = null;
getLinkController().release();
}
firePropertyChange("rolloverEnabled", old, isRolloverEnabled());
}
protected TableRolloverController getLinkController() {
if (linkController == null) {
linkController = createLinkController();
}
return linkController;
}
protected TableRolloverController createLinkController() {
return new TableRolloverController();
}
/**
* creates and returns the RolloverProducer to use.
*
* @return RolloverProducer
*/
protected RolloverProducer createRolloverProducer() {
return new RolloverProducer() {
@Override
protected void updateRolloverPoint(JComponent component,
Point mousePoint) {
JTable table = (JTable) component;
int col = table.columnAtPoint(mousePoint);
int row = table.rowAtPoint(mousePoint);
if ((col < 0) || (row < 0)) {
row = -1;
col = -1;
}
rollover.x = col;
rollover.y = row;
}
};
}
/**
* Returns the rolloverEnabled property.
*
* @return true
if rollover is enabled
*/
public boolean isRolloverEnabled() {
return rolloverProducer != null;
}
/**
* listens to rollover properties.
* Repaints effected component regions.
* Updates link cursor.
*
* @author Jeanette Winzenburg
*/
public static class TableRolloverController extends RolloverController {
private Cursor oldCursor;
// --------------------------- JTable rollover
@Override
protected void rollover(Point oldLocation, Point newLocation) {
if (oldLocation != null) {
Rectangle r = component.getCellRect(oldLocation.y, oldLocation.x, false);
r.x = 0;
r.width = component.getWidth();
component.repaint(r);
}
if (newLocation != null) {
Rectangle r = component.getCellRect(newLocation.y, newLocation.x, false);
r.x = 0;
r.width = component.getWidth();
component.repaint(r);
}
setRolloverCursor(newLocation);
}
/**
* overridden to return false if cell editable.
*/
@Override
protected boolean isClickable(Point location) {
return super.isClickable(location) && !component.isCellEditable(location.y, location.x);
}
@Override
protected RolloverRenderer getRolloverRenderer(Point location, boolean prepare) {
TableCellRenderer renderer = component.getCellRenderer(location.y, location.x);
RolloverRenderer rollover = renderer instanceof RolloverRenderer ?
(RolloverRenderer) renderer : null;
if ((rollover != null) && !rollover.isEnabled()) {
rollover = null;
}
if ((rollover != null) && prepare) {
component.prepareRenderer(renderer, location.y, location.x);
}
return rollover;
}
private void setRolloverCursor(Point location) {
if (hasRollover(location)) {
if (oldCursor == null) {
oldCursor = component.getCursor();
component.setCursor(Cursor
.getPredefinedCursor(Cursor.HAND_CURSOR));
}
} else {
if (oldCursor != null) {
component.setCursor(oldCursor);
oldCursor = null;
}
}
}
@Override
protected Point getFocusedCell() {
int leadRow = component.getSelectionModel()
.getLeadSelectionIndex();
int leadColumn = component.getColumnModel().getSelectionModel()
.getLeadSelectionIndex();
return new Point(leadColumn, leadRow);
}
}
//--------------------------------- ColumnControl && Viewport
/**
* 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) {
boolean old = isColumnControlVisible();
this.columnControlVisible = visible;
if (old != 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 org.jdesktop.swingx.table.ColumnControlButton
* @see org.jdesktop.swingx.icon.ColumnControlIcon
*/
protected JComponent createDefaultColumnControl() {
return new ColumnControlButton(this, new ColumnControlIcon());
}
/**
* 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 java.awt.Component#setComponentOrientation(ComponentOrientation)
*/
@Override
public void setComponentOrientation(ComponentOrientation o) {
super.setComponentOrientation(o);
configureColumnControl();
}
/**
* 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();
}
/**
* Configures the upper trailing corner of an enclosing
* JScrollPane
.
*
* Adds/removes the ColumnControl
depending on the
* columnControlVisible
property.
*
* @see #setColumnControlVisible(boolean)
* @see #setColumnControl(JComponent)
*/
protected void configureColumnControl() {
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;
}
if (isColumnControlVisible()) {
verticalScrollPolicy = scrollPane
.getVerticalScrollBarPolicy();
scrollPane.setCorner(JScrollPane.UPPER_TRAILING_CORNER,
getColumnControl());
scrollPane
.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS);
} else {
if (verticalScrollPolicy != 0) {
// Fix #155-swingx: reset only if we had force 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);
}
try {
scrollPane.setCorner(JScrollPane.UPPER_TRAILING_CORNER,
null);
} catch (Exception ex) {
// Ignore spurious exception thrown by JScrollPane. This
// is a Swing bug!
}
}
}
}
}
//--------------------- actions
/**
* Take over ctrl-tab.
*
*/
private void initFocusBindings() {
setFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS,
new TreeSet());
setFocusTraversalKeys(KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS,
new TreeSet());
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(final 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);
}
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())) {
find();
}
}
}
/**
* 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() {
Action action = new AbstractActionExt() {
public void actionPerformed(ActionEvent e) {
if (!isEditing()) return;
getCellEditor().cancelCellEditing();
}
};
return action;
}
/**
* 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)
*/
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()
*/
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;
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);
}
}
/**
* 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();
}
/**
* Sets the flag which controls the scrollableTracksViewportHeight property.
* If true the table's height will be always at least as large as the
* containing parent, if false the table's height will be independent of
* parent's height.
*
*
* The default value is true
.
*
*
* Note: this a backport from Mustang's JTable
.
*
* @param fillsViewportHeight boolean to indicate whether the table should
* always fill parent's height.
* @see #getFillsViewportHeight()
* @see #getScrollableTracksViewportHeight()
*/
public void setFillsViewportHeight(boolean fillsViewportHeight) {
if (fillsViewportHeight == getFillsViewportHeight()) return;
boolean old = getFillsViewportHeight();
this.fillsViewportHeight = fillsViewportHeight;
firePropertyChange("fillsViewportHeight", old, getFillsViewportHeight());
revalidate();
}
/**
* Returns the flag which controls the scrollableTracksViewportHeight
* property.
*
* @return true if the table's height will always be at least as large
* as the containing parent, false if it is independent
* @see #setFillsViewportHeight(boolean)
* @see #getScrollableTracksViewportHeight()
*/
public boolean getFillsViewportHeight() {
return fillsViewportHeight;
}
/**
* {@inheritDoc}
*
* Overridden to control the tracksHeight property depending on
* fillsViewportHeight and relative size to containing parent.
*
* @return true if the control flag is true and the containing parent
* height > prefHeight, else returns false.
* @see #setFillsViewportHeight(boolean)
*
*/
@Override
public boolean getScrollableTracksViewportHeight() {
return getFillsViewportHeight()
&& getParent() instanceof JViewport
&& (((JViewport)getParent()).getHeight() > getPreferredSize().height);
}
//------------------------ override super because of filter-awareness
/**
* Returns the row count in the table; if filters are applied, this is the
* filtered row count.
*/
@Override
public int getRowCount() {
// RG: If there are no filters, call superclass version rather than
// accessing model directly
return filters == null ?
super.getRowCount() : filters.getOutputSize();
}
/**
* Convert row index from view coordinates to model coordinates accounting
* for the presence of sorters and filters.
*
* @param row
* row index in view coordinates
* @return row index in model coordinates
*/
public int convertRowIndexToModel(int row) {
return getFilters() != null ? getFilters().convertRowIndexToModel(row): row;
}
/**
* Convert row index from model coordinates to view coordinates accounting
* for the presence of sorters and filters.
*
* @param row
* row index in model coordinates
* @return row index in view coordinates
*/
public int convertRowIndexToView(int row) {
return getFilters() != null ? getFilters().convertRowIndexToView(row): row;
}
/**
* Overridden to account for row index mapping.
* {@inheritDoc}
*/
@Override
public Object getValueAt(int row, int column) {
return getModel().getValueAt(convertRowIndexToModel(row),
convertColumnIndexToModel(column));
}
/**
* Overridden to account for row index mapping. This implementation
* respects the cell's editability, that is it has no effect if
* !isCellEditable(row, column)
.
*
* {@inheritDoc}
* @see #isCellEditable(int, int)
*/
@Override
public void setValueAt(Object aValue, int row, int column) {
if (!isCellEditable(row, column)) return;
getModel().setValueAt(aValue, convertRowIndexToModel(row),
convertColumnIndexToModel(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 = getModel().isCellEditable(convertRowIndexToModel(row),
convertColumnIndexToModel(column));
if (editable) {
TableColumnExt tableColumn = getColumnExt(column);
if (tableColumn != null) {
editable = tableColumn.isEditable();
}
}
return editable;
}
/**
* Overridden to update selectionMapper
*/
@Override
public void setSelectionModel(ListSelectionModel newModel) {
super.setSelectionModel(newModel);
getSelectionMapper().setViewSelectionModel(getSelectionModel());
}
/**
* {@inheritDoc}
*/
@Override
public void setModel(TableModel newModel) {
// JW: need to look here? is done in tableChanged as well.
boolean wasEnabled = getSelectionMapper().isEnabled();
getSelectionMapper().setEnabled(false);
try {
super.setModel(newModel);
} finally {
getSelectionMapper().setEnabled(wasEnabled);
}
}
/**
* additionally updates filtered state.
* {@inheritDoc}
*/
@Override
public void tableChanged(TableModelEvent e) {
if (getSelectionModel().getValueIsAdjusting()) {
// this may happen if the uidelegate/editor changed selection
// and adjusting state
// before firing a editingStopped
// need to enforce update of model selection
getSelectionModel().setValueIsAdjusting(false);
}
// JW: make SelectionMapper deaf ... super doesn't know about row
// mapping and sets rowSelection in model coordinates
// causing complete confusion.
boolean wasEnabled = getSelectionMapper().isEnabled();
getSelectionMapper().setEnabled(false);
try {
super.tableChanged(e);
updateSelectionAndRowModel(e);
} finally {
getSelectionMapper().setEnabled(wasEnabled);
}
if (shouldSortOnChange(e)) {
use(filters);
}
if (isStructureChanged(e)) {
initializeColumnWidths();
resetCalculatedScrollableSize(true);
}
}
/**
* Returns a boolean to indicate whether the table should be
* resorted after receiving the given event. This implementation
* returns true always.
*
* NOTE: this is a quick hack to give subclasses a hook to
* experiment with conditional keeping the view unsorted, f.i.
* after edits. It's untested ... and will not receive much
* work because in Mustang the DefaultRowSorter has the functionality.
*
* @param e the event which might trigger a resort.
* @return a boolean indicating whether the event should
* trigger a re-sort, here true always.
*/
protected boolean shouldSortOnChange(TableModelEvent e) {
return true;
}
/**
* reset model selection coordinates in SelectionMapper after
* model events.
*
* @param e
*/
private void updateSelectionAndRowModel(TableModelEvent e) {
if (isStructureChanged(e) || isDataChanged(e)) {
// JW fixing part of #172 - trying to adjust lead/anchor to valid
// indices (at least in model coordinates) after super's default clearSelection
// in dataChanged/structureChanged.
hackLeadAnchor(e);
getSelectionMapper().clearModelSelection();
getRowModelMapper().clearModelSizes();
updateViewSizeSequence();
// JW: c&p from JTable
} else if (e.getType() == TableModelEvent.INSERT) {
int start = e.getFirstRow();
int end = e.getLastRow();
if (start < 0) {
start = 0;
}
if (end < 0) {
end = getModel().getRowCount() - 1;
}
// Adjust the selectionMapper to account for the new rows.
int length = end - start + 1;
getSelectionMapper().insertIndexInterval(start, length, true);
getRowModelMapper().insertIndexInterval(start, length, getRowHeight());
} else if (e.getType() == TableModelEvent.DELETE) {
int start = e.getFirstRow();
int end = e.getLastRow();
if (start < 0) {
start = 0;
}
if (end < 0) {
end = getModel().getRowCount() - 1;
}
int deletedCount = end - start + 1;
// Adjust the selectionMapper to account for the new rows
getSelectionMapper().removeIndexInterval(start, end);
getRowModelMapper().removeIndexInterval(start, deletedCount);
}
// nothing to do on TableEvent.updated
}
/**
* 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;
}
/**
* Trying to hack around #172-swingx: lead/anchor of row selection model
* is not adjusted to valid (not even model indices!) in the
* usual clearSelection after dataChanged/structureChanged.
*
* Note: as of jdk1.5U6 the anchor/lead of the view selectionModel is
* unconditionally set to -1 after data/structureChanged.
*
* @param e
*/
private void hackLeadAnchor(TableModelEvent e) {
int lead = getSelectionModel().getLeadSelectionIndex();
int anchor = getSelectionModel().getAnchorSelectionIndex();
int lastRow = getModel().getRowCount() - 1;
if ((lead > lastRow) || (anchor > lastRow)) {
lead = anchor = lastRow;
getSelectionModel().setAnchorSelectionIndex(lead);
getSelectionModel().setLeadSelectionIndex(lead);
}
}
/**
* Called if individual row height mapping need to be updated.
* This implementation guards against unnessary access of
* super's private rowModel field.
*/
protected void updateViewSizeSequence() {
SizeSequence sizeSequence = null;
if (isRowHeightEnabled()) {
sizeSequence = getSuperRowModel();
}
getRowModelMapper().setViewSizeSequence(sizeSequence, getRowHeight());
}
/**
* @return SelectionMapper
*/
public SelectionMapper getSelectionMapper() {
// JW: why is this public? Probably made so accidentally?
// maybe not: was introduced in version 1.148 when applying
// Jesse's patch to #386-swingx (added functionality to
// turn off the mapping
if (selectionMapper == null) {
selectionMapper = new DefaultSelectionMapper(filters, getSelectionModel());
}
return selectionMapper;
}
//----------------------------- filters
/** Returns the FilterPipeline for the table. */
public FilterPipeline getFilters() {
// PENDING: this is guaranteed to be != null because
// init calls setFilters(null) which enforces an empty
// pipeline
return filters;
}
/**
* setModel() and setFilters() may be called in either order.
*
* @param pipeline
*/
private void use(FilterPipeline pipeline) {
if (pipeline != null) {
// check JW: adding listener multiple times (after setModel)?
if (initialUse(pipeline)) {
pipeline.addPipelineListener(getFilterPipelineListener());
pipeline.assign(getComponentAdapter());
} else {
pipeline.flush();
}
}
}
/**
* @return true is not yet used in this JXTable, false otherwise
*/
private boolean initialUse(FilterPipeline pipeline) {
if (pipelineListener == null) return true;
PipelineListener[] l = pipeline.getPipelineListeners();
for (int i = 0; i < l.length; i++) {
if (pipelineListener.equals(l[i]))
return false;
}
return true;
}
/**
* Sets the FilterPipeline for filtering table rows, maybe null
* to remove all previously applied filters.
*
* Note: the current "interactive" sortState is preserved (by
* internally copying the old sortKeys to the new pipeline, if any).
*
* @param pipeline the FilterPipeline
to use, null removes
* all filters.
*/
public void setFilters(FilterPipeline pipeline) {
FilterPipeline old = getFilters();
List extends SortKey> sortKeys = null;
if (old != null) {
old.removePipelineListener(pipelineListener);
sortKeys = old.getSortController().getSortKeys();
}
if (pipeline == null) {
pipeline = new FilterPipeline();
}
filters = pipeline;
filters.getSortController().setSortKeys(sortKeys);
// JW: first assign to prevent (short?) illegal internal state
// #173-swingx
use(filters);
getRowModelMapper().setFilters(filters);
getSelectionMapper().setFilters(filters);
repaint();
}
/** returns the listener for changes in filters. */
protected PipelineListener getFilterPipelineListener() {
if (pipelineListener == null) {
pipelineListener = createPipelineListener();
}
return pipelineListener;
}
/** creates the listener for changes in filters. */
protected PipelineListener createPipelineListener() {
return new PipelineListener() {
public void contentsChanged(PipelineEvent e) {
updateOnFilterContentChanged();
}
};
}
/**
* method called on change notification from filterpipeline.
*/
protected void updateOnFilterContentChanged() {
revalidate();
repaint();
// this is a quick fix for #445-swingx: sort icon not updated on
// programatic sorts
if (getTableHeader() != null) {
getTableHeader().repaint();
}
}
//-------------------------------- sorting
/**
* 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
.
*
* @see TableColumnExt#isSortable()
* @param sortable
* boolean indicating whether or not this table supports sortable
* columns
*/
public void setSortable(boolean sortable) {
if (sortable == isSortable())
return;
this.sortable = sortable;
if (!isSortable()) resetSortOrder();
firePropertyChange("sortable", !sortable, sortable);
}
/**
* Returns the table's sortable property.
*
* @return true if the table is sortable.
*/
public boolean isSortable() {
return sortable;
}
/**
* Resets sorting of all columns.
*
*/
public void resetSortOrder() {
// JW PENDING: think about notification instead of manual repaint.
SortController controller = getSortController();
if (controller != null) {
controller.setSortKeys(null);
}
if (getTableHeader() != null) {
getTableHeader().repaint();
}
}
/**
*
* Toggles the sort order of the column at columnIndex.
*
* 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.
*
* Respects the tableColumnExt's sortable and comparator
* properties: routes the column's comparator to the SortController
* and does nothing if !isSortable(column).
*
*
* PRE: 0 <= columnIndex < getColumnCount()
*
* @param columnIndex the columnIndex in view coordinates.
*
*/
public void toggleSortOrder(int columnIndex) {
if (!isSortable(columnIndex))
return;
SortController controller = getSortController();
if (controller != null) {
TableColumnExt columnExt = getColumnExt(columnIndex);
controller.toggleSortOrder(convertColumnIndexToModel(columnIndex),
columnExt != null ? columnExt.getComparator() : null);
}
}
/**
* Decides if the column at columnIndex can be interactively sorted.
*
* Here: true if both this table and the column sortable property is
* enabled, false otherwise.
*
* @param columnIndex column in view coordinates
* @return boolean indicating whether or not the column is sortable
* in this table.
*/
protected boolean isSortable(int columnIndex) {
boolean sortable = isSortable();
TableColumnExt tableColumnExt = getColumnExt(columnIndex);
if (tableColumnExt != null) {
sortable = sortable && tableColumnExt.isSortable();
}
return sortable;
}
/**
* Sorts the table by the given column using SortOrder.
*
*
* Respects the tableColumnExt's sortable and comparator
* properties: routes the column's comparator to the SortController
* and does nothing if !isSortable(column).
*
*
* PRE: 0 <= columnIndex < getColumnCount()
*
*
*
* @param columnIndex the column index in view coordinates.
* @param sortOrder the sort order to use. If null or SortOrder.UNSORTED,
* this method has the same effect as resetSortOrder();
*
*/
public void setSortOrder(int columnIndex, SortOrder sortOrder) {
if ((sortOrder == null) || !sortOrder.isSorted()) {
resetSortOrder();
return;
}
if (!isSortable(columnIndex)) return;
SortController sortController = getSortController();
if (sortController != null) {
TableColumnExt columnExt = getColumnExt(columnIndex);
SortKey sortKey = new SortKey(sortOrder,
convertColumnIndexToModel(columnIndex),
columnExt != null ? columnExt.getComparator() : null);
sortController.setSortKeys(Collections.singletonList(sortKey));
}
}
/**
* Returns the SortOrder of the given column.
*
* @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) {
SortController sortController = getSortController();
if (sortController == null) return SortOrder.UNSORTED;
SortKey sortKey = SortKey.getFirstSortKeyForColumn(sortController.getSortKeys(),
convertColumnIndexToModel(columnIndex));
return sortKey != null ? sortKey.getSortOrder() : SortOrder.UNSORTED;
}
/**
*
* Toggles the sort order of the column with identifier.
*
* 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.
*
* Respects the tableColumnExt's sortable and comparator
* properties: routes the column's comparator to the SortController
* and does nothing if !isSortable(column).
*
*
* 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 (!isSortable(identifier))
return;
SortController controller = getSortController();
if (controller != null) {
TableColumnExt columnExt = getColumnExt(identifier);
if (columnExt == null) return;
controller.toggleSortOrder(columnExt.getModelIndex(),
columnExt.getComparator());
}
}
/**
* Sorts the table by the given column using the SortOrder.
*
*
* Respects the tableColumnExt's sortable and comparator
* properties: routes the column's comparator to the SortController
* and does nothing if !isSortable(column).
*
*
* 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 ((sortOrder == null) || !sortOrder.isSorted()) {
resetSortOrder();
return;
}
if (!isSortable(identifier)) return;
SortController sortController = getSortController();
if (sortController != null) {
TableColumnExt columnExt = getColumnExt(identifier);
if (columnExt == null) return;
SortKey sortKey = new SortKey(sortOrder,
columnExt.getModelIndex(),
columnExt.getComparator());
sortController.setSortKeys(Collections.singletonList(sortKey));
}
}
/**
* Returns the SortOrder of the given column.
*
* 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) {
SortController sortController = getSortController();
if (sortController == null) return SortOrder.UNSORTED;
TableColumnExt columnExt = getColumnExt(identifier);
if (columnExt == null) return SortOrder.UNSORTED;
int modelIndex = columnExt.getModelIndex();
SortKey sortKey = SortKey.getFirstSortKeyForColumn(sortController.getSortKeys(),
modelIndex);
return sortKey != null ? sortKey.getSortOrder() : SortOrder.UNSORTED;
}
/**
* Decides if the column with identifier can be interactively sorted.
*
* Here: true if both this table and the column sortable property is
* enabled, false otherwise.
*
* @param identifier the column's identifier
* @return boolean indicating whether or not the column is sortable
* in this table.
*/
protected boolean isSortable(Object identifier) {
boolean sortable = isSortable();
TableColumnExt tableColumnExt = getColumnExt(identifier);
if (tableColumnExt != null) {
sortable = sortable && tableColumnExt.isSortable();
}
return sortable;
}
/**
* returns the currently active SortController. Can be null
* on the very first call after instantiation.
* @return the currently active SortController
may be null
*/
protected SortController getSortController() {
// // this check is for the sake of the very first call after instantiation
if (filters == null) return null;
return getFilters().getSortController();
}
/**
*
* @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
SortController controller = getSortController();
if (controller != null) {
SortKey sortKey = SortKey.getFirstSortingKey(controller.getSortKeys());
if (sortKey != null) {
int sorterColumn = sortKey.getColumn();
List columns = getColumns(true);
for (Iterator iter = columns.iterator(); iter.hasNext();) {
TableColumn column = iter.next();
if (column.getModelIndex() == sorterColumn) {
return column;
}
}
}
}
return null;
}
/**
* overridden to remove the interactive sorter if the
* sorted column is no longer contained in the ColumnModel.
*/
@Override
public void columnRemoved(TableColumnModelEvent e) {
// JW - old problem: need access to removed column
// to get hold of removed modelIndex
// to remove interactive sorter if any
// no way
// int modelIndex = convertColumnIndexToModel(e.getFromIndex());
updateSorterAfterColumnRemoved();
super.columnRemoved(e);
}
/**
* guarantee that the interactive sorter is removed if its column
* is removed.
*
*/
private void updateSorterAfterColumnRemoved() {
TableColumn sortedColumn = getSortedColumn();
if (sortedColumn == null) {
resetSortOrder();
}
}
// ----------------- 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
* (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