/*
* $Id: JXTable.java,v 1.271 2009/05/07 09:21:22 kleopatra Exp $
*
* Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle,
* Santa Clara, California 95054, U.S.A. All rights reserved.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
package org.jdesktop.swingx;
import java.applet.Applet;
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.Field;
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 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.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.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.ComponentAdapter;
import org.jdesktop.swingx.decorator.CompoundHighlighter;
import org.jdesktop.swingx.decorator.DefaultSelectionMapper;
import org.jdesktop.swingx.decorator.FilterPipeline;
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.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.decorator.UIDependent;
import org.jdesktop.swingx.event.TableColumnModelExtListener;
import org.jdesktop.swingx.plaf.LookAndFeelAddons;
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.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.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;
/**
* 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 ...
*
* <h2>Sorting and Filtering</h2>
*
* JXTable supports sorting and filtering of rows.
*
* Sorting support is single column only. It provides api to apply
* a specific sort order or to toggle the sort order of columns identified
* by view index or column identifier or reset all sorts.
*
* <pre><code>
* table.setSortOrder("PERSON_ID", SortOrder.DESCENDING);
* table.toggleSortOder(4);
* table.resetSortOrder();
* </code></pre>
*
* Sorting sequence can be configured per column by setting the TableColumnExt's
* "comparator" property. Sorting can be disabled per column or per table by
* {@link #setSortable(boolean)}.
*
* <p>
* 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. The exact mapping of a user gesture to
* a sort effect is configurable by installing a custom SortGestureRecognizer on the
* JXTableHeader.
*
* <p>
* 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 nor
* 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.
*
* <b>NOTE:</b> SwingX sorting/filtering is incompatible with core sorting/filtering in
* JDK 6+. Will be replaced by core functionality after switching the target jdk
* version from 5 to 6.
*
* <h2>Rendering and Highlighting</h2>
*
* 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.
*
* <p>
* An example multiple highlighting (default striping as appropriate for the
* current LookAndFeel, cell foreground on matching pattern, and shading a
* column):
*
* <pre><code>
*
* 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);
* </code></pre>
*
* <p>
* 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:
*
* <pre><code>
* 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));
* </code></pre>
*
* <p>
* <b>Note</b>: 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.
*
*
* <h2>Rollover</h2>
*
* 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.
*
* <pre><code>
* JXTable table = new JXTable();
* table.addHighlighter(new ColorHighlighter(HighlightPredicate.ROLLOVER_ROW,
* null, Color.RED);
* </code></pre>
*
* <h2>Search</h2>
*
* 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.
* <p>
*
* 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.
*
* <h2>Column Configuration</h2>
*
* 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.
*
* <p>
* 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.
*
* <p>
* 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.
*
* <p>
* 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.
*
* <p>
*
*
* <p>
* Keys/Actions registered with this component:
*
* <ul>
* <li>"find" - open an appropriate search widget for searching cell content.
* The default action registeres itself with the SearchFactory as search target.
* <li>"print" - print the table
* <li> {@link JXTable#HORIZONTALSCROLL_ACTION_COMMAND} - toggle the horizontal
* scrollbar
* <li> {@link JXTable#PACKSELECTED_ACTION_COMMAND} - resize the selected column
* to fit the widest cell content
* <li> {@link JXTable#PACKALL_ACTION_COMMAND} - resize all columns to fit the
* widest cell content in each column
*
* </ul>
*
* <p>
* Key bindings.
*
* <ul>
* <li>"control F" - bound to actionKey "find".
* </ul>
*
* <p>
* Client Properties.
*
* <ul>
* <li> {@link JXTable#MATCH_HIGHLIGHTER} - set to Boolean.TRUE to use a
* SearchHighlighter to mark a cell as matching.
* </ul>
*
* @author Ramesh Gupta
* @author Amy Fowler
* @author Mark Davidson
* @author Jeanette Winzenburg
*
* @see JXTableHeader.SortGestureRecognizer
*/
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
* <code>ActionMap</code>.
*
*/
public static final String HORIZONTALSCROLL_ACTION_COMMAND = ColumnControlButton.COLUMN_CONTROL_MARKER
+ "horizontalScroll";
/**
* Identifier of pack table action, used in JXTable's <code>ActionMap</code>
* .
*/
public static final String PACKALL_ACTION_COMMAND = ColumnControlButton.COLUMN_CONTROL_MARKER
+ "packAll";
/**
* Identifier of pack selected column action, used in JXTable's
* <code>ActionMap</code>.
*/
public static final String PACKSELECTED_ACTION_COMMAND = ColumnControlButton.COLUMN_CONTROL_MARKER
+ "packSelected";
/**
* The prefix marker to find table related properties in the
* <code>ResourceBundle</code>.
*/
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
*/
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;
/**
* The handler for mapping view/model coordinates of row selection. Widened
* access to allow lazy instantiation in subclasses (#746-swingx).
*/
protected 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 <code>JScrollPane</code>.
*/
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<JXTable> 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 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;
/** 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 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());
}
//--------------- 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. <p>
*
* This can be enabled to show "live" rollover behaviour, f.i. the cursor over a cell
* rendered by a JXHyperlink.<p>
*
* 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();
addMouseListener(rolloverProducer);
addMouseMotionListener(rolloverProducer);
getLinkController().install(this);
} else {
removeMouseListener(rolloverProducer);
removeMouseMotionListener(rolloverProducer);
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. <p>
*
* 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<JXTable> 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<JXTable> createLinkController() {
return new TableRolloverController<JXTable>();
}
/**
* Creates and returns the RolloverProducer to use with this component.
* <p>
*
* @return <code>RolloverProducer</code> to use with this component
*
* @see #setRolloverEnabled(boolean)
*/
protected RolloverProducer createRolloverProducer() {
return new TableRolloverProducer();
}
/**
* Returns the column control visible property.
* <p>
*
* @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
* <code>JXTable</code> is contained in a <code>JScrollPane</code>, the
* table adds the column control to the trailing corner of the scroll pane.
* <p>
*
* Note: if the table is not inside a <code>JScrollPane</code> 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.
* <p>
*
* The default value is <code>false</code>.
*
* @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 <code>null</code>.
*
* @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
* <code>JScrollPane</code> if appropriate. Passing a <code>null</code>
* parameter restores the column control to the default.
* <p>
* The component is automatically visible only if the
* <code>columnControlVisible</code> property is <code>true</code> and the
* table is contained in a <code>JScrollPane</code>.
*
* <p>
* NOTE: from the table's perspective, the column control is simply a
* <code>JComponent</code> 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.
* <p>
*
* @param columnControl the <code>JComponent</code> 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 <code>ColumnControlButton</code> configured with
* default <code>ColumnControlIcon</code>.
*
* @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);
}
/**
* Sets the language-sensitive orientation that is to be used to order the
* elements or text within this component.
* <p>
*
* Overridden to work around a core bug: <code>JScrollPane</code> can't cope
* with corners when changing component orientation at runtime. This method
* explicitly re-configures the column control.
* <p>
*
* @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 <code>JScrollPane</code>.
* <p>
*
* Overridden to addionally configure the upper trailing corner with the
* column control.
*
* @see #configureColumnControl()
*
*/
@Override
protected void configureEnclosingScrollPane() {
super.configureEnclosingScrollPane();
configureColumnControl();
}
/**
* Unconfigures the enclosing <code>JScrollPane</code>.
* <p>
*
* 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
* <code>JScrollPane</code>.
*
* Here: removes the upper trailing corner and resets.
*
* @see #setColumnControlVisible(boolean)
* @see #setColumnControl(JComponent)
*/
protected void unconfigureColumnControl() {
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 (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);
verticalScrollPolicy = 0;
}
if (isColumnControlVisible()) {
scrollPane.setCorner(JScrollPane.UPPER_TRAILING_CORNER,
null);
}
}
}
}
/**
* Configures the upper trailing corner of an enclosing
* <code>JScrollPane</code>.
*
* Adds the <code>ColumnControl</code> if the
* <code>columnControlVisible</code> property is true.
* <p>
*
* @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()) {
if (verticalScrollPolicy == 0) {
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<KeyStroke>());
setFocusTraversalKeys(KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS,
new TreeSet<KeyStroke>());
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.
* <p>
* TODO (?): Is there a way that we can make this static?
* <p>
*
* 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())) {
doFind();
}
}
}
/**
* Registers additional, per-instance <code>Action</code>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();
}
@Override
public boolean isEnabled() {
return isEditing();
}
};
return action;
}
/**
* Creates and returns the default <code>Action</code> 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 <code>UIPREFIX</code> before doing the
* lookup. The lookup respects this table's current <code>locale</code>
* 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 <code>UIPREFIX</code>
* 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 <code>Action</code> 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 <b>Action </b> for packing all columns.
*/
private Action createPackAllAction() {
BoundAction action = new BoundAction(null, PACKALL_ACTION_COMMAND);
action.registerCallback(this, "packAll");
return action;
}
/**
* {@inheritDoc}
* <p>
* 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 <code>Locale</code>.
*
* Here: updates registered column actions' locale-dependent state.
* <p>
*
* 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
* <code>ActionMap</code>. Does nothing if no action with key is found.
* <p>
*
* Here: updates the <code>Action</code>'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.
* <p>
*
* By default this method is bound to the pack all columns
* <code>Action</code> and registered in the table's <code>ActionMap</code>.
*
*/
public void packAll() {
packTable(-1);
}
/**
* Resizes the lead column to fit its content.
* <p>
*
* By default this method is bound to the pack selected column
* <code>Action</code> and registered in the table's <code>ActionMap</code>.
*/
public void packSelected() {
int selected = getColumnModel().getSelectionModel()
.getLeadSelectionIndex();
if (selected >= 0) {
packColumn(selected, -1);
}
}
/**
* {@inheritDoc}
* <p>
*
* Overridden to update the enabled state of the pack selected column
* <code>Action</code>.
*/
@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 <code>JViewport</code>
* horizontally and shows the horizontal scrollbar if necessary.
* <p>
*
* The default value is <code>false</code>.
* <p>
*
* Note: this is <strong>not</strong> 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.
* <p>
* 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}
* <p>
*
* Overridden for internal bookkeeping related to the enhanced auto-resize
* behaviour.
* <p>
*
* Note: to enable/disable the enhanced auto-resize mode use exclusively
* <code>setHorizontalScrollEnabled</code>, 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 <code>Action</code> 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}
* <p>
*
* 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
* <code>autoResizeMode</code> property.
* <p>
* 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}
* <p>
*
* 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.
* <p>
*
* The default value is <code>true</code>.
* <p>
*
* Note: this a backport from Mustang's <code>JTable</code>.
*
* @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}
* <p>
*
* 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
* <code>!isCellEditable(row, column)</code>.
*
* {@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 <code>row</code> and <code>column</code> is
* editable. Otherwise, invoking <code>setValueAt</code> on the cell will
* have no effect.
* <p>
* Overridden to account for row index mapping and to support a layered
* editability control:
* <ul>
* <li>per-table: <code>JXTable.isEditable()</code>
* <li>per-column: <code>TableColumnExt.isEditable()</code>
* <li>per-cell: controlled by the model
* <code>TableModel.isCellEditable()</code>
* </ul>
* 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);
}
}
/**
* {@inheritDoc}
* <p>
*
* 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".
* <p>
*
* 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 org.jdesktop.swingx.table.ColumnFactory
*
*/
@Override
public boolean getAutoCreateColumnsFromModel() {
return super.getAutoCreateColumnsFromModel();
}
/**
* 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 {
SizeSequence rowModel = nullSuperRowModel(e);
super.tableChanged(e);
if (rowModel != null) {
retoreSuperRowModel(rowModel);
}
updateSelectionAndRowModel(e);
} finally {
getSelectionMapper().setEnabled(wasEnabled);
}
if (shouldSortOnChange(e)) {
use(filters);
}
if (isStructureChanged(e) && getAutoCreateColumnsFromModel()) {
initializeColumnWidths();
resetCalculatedScrollableSize(true);
}
}
/**
* Sets super's rowModel to the given SizeSequence if not null, does nothing
* if null.
*
* Hack around #924-swingx.
*
* @param rowModel the SizeSequence to set super's rowModel to.
*/
private void retoreSuperRowModel(SizeSequence rowModel) {
if (rowModel == null)
return;
setSuperRowModel(rowModel);
}
/**
* Nulls super's rowModel and returns the old one if rowHeightEnabled and
* the event is either a remove or a insert. Does nothing and returns null
* otherwise.
*
* Hack around #924-swingx.
*
* @param e the TableModelEvent used to decide whether a nulling is needed.
* @return super's old SizeSequence or null
*/
private SizeSequence nullSuperRowModel(TableModelEvent e) {
if (!isRowHeightEnabled())
return null;
if (!isInsertRemove(e))
return null;
SizeSequence result = getSuperRowModel();
setSuperRowModel(null);
return result;
}
/**
* @param e
* @return
*/
private boolean isInsertRemove(TableModelEvent e) {
if (isStructureChanged(e))
return false;
if ((e.getType() == TableModelEvent.INSERT)
|| (e.getType() == TableModelEvent.DELETE))
return true;
return false;
}
/**
* Returns a boolean to indicate whether the table should be resorted after
* receiving the given event. This implementation returns true always.
* <p>
*
* 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 = 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 <code>SelectionMapper</code>
*/
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.
*
* @return the FilterPipeline used 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 <code>FilterPipeline</code> 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 <code>sortable</code> is <code>true</code>
* then sorting will be enabled on all columns whose <code>sortable</code>
* property is <code>true</code>. If <code>sortable</code> is
* <code>false</code> then sorting will be disabled for all columns,
* regardless of each column's individual <code>sorting</code> property. The
* default is <code>true</code>.
*
* @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.
* <p>
* 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.
* <p>
* Respects the tableColumnExt's sortable and comparator properties: routes
* the column's comparator to the SortController and does nothing if
* !isSortable(column).
* <p>
*
* 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.
* <p>
* 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).
* <p>
*
* PRE: 0 <= columnIndex < getColumnCount()
* <p>
*
*
* @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.
* <p>
* 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.
* <p>
* Respects the tableColumnExt's sortable and comparator properties: routes
* the column's comparator to the SortController and does nothing if
* !isSortable(column).
* <p>
*
* 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).
* <p>
*
* 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.
* <p>
* 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 <code>SortController</code> 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<TableColumn> columns = getColumns(true);
for (Iterator<TableColumn> 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 <code>TableColumn</code> at view position
* <code>columnIndex</code>. The return value is not <code>null</code>.
*
* <p>
* NOTE: This delegate method is added to protect developer's from
* unexpected exceptions in jdk1.5+. Super does not expose the
* <code>TableColumn</code> access by index which may lead to unexpected
* <code>IllegalArgumentException</code>: 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 <code>TableColumn</code> 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 <code>List</code> of visible <code>TableColumn</code>s.
*
* @return a <code>List</code> of visible columns.
* @see #getColumns(boolean)
*/
public List<TableColumn> getColumns() {
return Collections.list(getColumnModel().getColumns());
}
/**
* Returns the margin between columns.
* <p>
*
* Convenience to expose column model properties through
* <code>JXTable</code> 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
* <code>JXTable</code> 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 <code>includeHidden</code> is
* true or false, respectively. If false, this method returns the same count
* as <code>getColumnCount()</code>. If the columnModel is not of type
* <code>TableColumnModelExt</code>, 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 <code>List</code> of contained <code>TableColumn</code>s.
* Includes or excludes invisible columns, depending on whether the
* <code>includeHidden</code> is true or false, respectively. If false, an
* <code>Iterator</code> over the List is equivalent to the
* <code>Enumeration</code> returned by <code>getColumns()</code>. If the
* columnModel is not of type <code>TableColumnModelExt</code>, the
* parameter value has no effect.
* <p>
*
* 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 <code>List</code> of contained columns.
*
* @see #getColumns()
* @see TableColumnModelExt#getColumns(boolean)
*/
public List<TableColumn> getColumns(boolean includeHidden) {
if (getColumnModel() instanceof TableColumnModelExt) {
return ((TableColumnModelExt) getColumnModel())
.getColumns(includeHidden);
}
return getColumns();
}
/**
* Returns the first <code>TableColumnExt</code> with the given
* <code>identifier</code>. The return value is null if there is no
* contained column with <b>identifier</b> or if the column with
* <code>identifier</code> is not of type <code>TableColumnExt</code>. The
* returned column may be visible or hidden.
*
* @param identifier the object used as column identifier
* @return first <code>TableColumnExt</code> 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 <code>TableColumnExt</code> at view position
* <code>columnIndex</code>. The return value is null, if the column at
* position <code>columnIndex</code> is not of type
* <code>TableColumnExt</code>. The returned column is visible.
*
* @param viewColumnIndex the index of the column desired
* @return the <code>TableColumnExt</code> object that matches the column
* index
* @throws ArrayIndexOutOfBoundsException if columnIndex out of allowed
* range, that is if
* <code> (columnIndex < 0) || (columnIndex >= getColumnCount())</code>
* .
*
* @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<TableColumn> columns = getColumns(true);
Map<Object, TableColumn> map = new HashMap<Object, TableColumn>();
for (Iterator<TableColumn> iter = columns.iterator(); iter.hasNext();) {
// PENDING: handle duplicate identifiers ...
TableColumn column = iter.next();
map.put(column.getIdentifier(), column);
getColumnModel().removeColumn(column);
}
for (int i = 0; i < identifiers.length; i++) {
TableColumn column = map.get(identifiers[i]);
if (column != null) {
getColumnModel().addColumn(column);
columns.remove(column);
}
}
for (Iterator<TableColumn> iter = columns.iterator(); iter.hasNext();) {
TableColumn column = (TableColumn) iter.next();
getColumnModel().addColumn(column);
}
}
// --------------- implement TableColumnModelExtListener
/**
* {@inheritDoc}
*
* Listens to column property changes.
*
*/
public void columnPropertyChange(PropertyChangeEvent event) {
if (event.getPropertyName().equals("editable")) {
updateEditingAfterColumnChanged((TableColumn) event.getSource(),
(Boolean) event.getNewValue());
} else if (event.getPropertyName().equals("sortable")) {
updateSortingAfterColumnChanged((TableColumn) event.getSource(),
(Boolean) event.getNewValue());
} else if (event.getPropertyName().startsWith("highlighter")) {
if (event.getSource() instanceof TableColumnExt
&& getRowCount() > 0) {
TableColumnExt column = (TableColumnExt) event.getSource();
Rectangle r = getCellRect(0, convertColumnIndexToView(column
.getModelIndex()), true);
r.height = getHeight();
repaint(r);
} else {
repaint();
}
}
}
/**
* Adjusts editing state after column's property change. Cancels ongoing
* editing if the sending column is the editingColumn and the column's
* editable changed to <code>false</code>, otherwise does nothing.
*
* @param column the <code>TableColumn</code> which sent the change
* notifcation
* @param editable the new value of the column's editable property
*/
private void updateEditingAfterColumnChanged(TableColumn column,
boolean editable) {
if (!isEditing())
return;
int viewIndex = convertColumnIndexToView(column.getModelIndex());
if ((viewIndex < 0) || (viewIndex != getEditingColumn()))
return;
getCellEditor().cancelCellEditing();
}
/**
* @param column the <code>TableColumn</code> which sent the change
* notifcation
* @param sortable the new value of the column's sortable property
*/
private void updateSortingAfterColumnChanged(TableColumn column,
boolean sortable) {
TableColumn sortedColumn = getSortedColumn();
if ((sortedColumn == null) || (sortedColumn != column))
return;
// here we assume that there's only one sorted column
// nothing is done to enforce at-least-one sorted column
// todo: search forum for when that was problem
resetSortOrder();
}
// -------------------------- ColumnFactory
/**
* Creates, configures and adds default <code>TableColumn</code>s for
* columns in this table's <code>TableModel</code>. Removes all currently
* contained <code>TableColumn</code>s. The exact type and configuration of
* the columns is controlled completely by the <code>ColumnFactory</code>.
* Client code can use {@link #setColumnFactory(ColumnFactory)} to plug-in a
* custom ColumnFactory implementing their own default column creation and
* behaviour.
* <p>
*
* <b>Note</b>: this method will probably become final (Issue #961-SwingX)
* so it's strongly recommended to not override now (and replace existing
* overrides by a custom ColumnFactory)!
*
* @see #setColumnFactory(ColumnFactory)
* @see org.jdesktop.swingx.table.ColumnFactory
*
*/
@Override
public void createDefaultColumnsFromModel() {
// JW: when could this happen?
if (getModel() == null)
return;
// Remove any current columns
removeColumns();
createAndAddColumns();
}
/**
* Creates and adds <code>TableColumn</code>s for each column of the table
* model.
* <p>
*
*
*/
private void createAndAddColumns() {
/*
* PENDING: go the whole distance and let the factory decide which model
* columns to map to view columns? That would introduce an collection
* managing operation into the factory, sprawling? Can't (and probably
* don't want to) move all collection related operations over - the
* ColumnFactory relies on TableColumnExt type columns, while the
* JXTable has to cope with all the base types.
*/
for (int i = 0; i < getModel().getColumnCount(); i++) {
// add directly to columnModel - don't go through this.addColumn
// to guarantee full control of ColumnFactory
// addColumn has the side-effect to set the header!
TableColumnExt tableColumn = getColumnFactory()
.createAndConfigureTableColumn(getModel(), i);
if (tableColumn != null) {
getColumnModel().addColumn(tableColumn);
}
}
}
/**
* Remove all columns, make sure to include hidden.
* <p>
*/
private void removeColumns() {
/*
* TODO: promote this method to superclass, and change
* createDefaultColumnsFromModel() to call this method
*/
List<TableColumn> columns = getColumns(true);
for (Iterator<TableColumn> iter = columns.iterator(); iter.hasNext();) {
getColumnModel().removeColumn(iter.next());
}
}
/**
* Returns the ColumnFactory.
* <p>
*
* @return the columnFactory to use for column creation and configuration,
* guaranteed to not be null.
*
* @see #setColumnFactory(ColumnFactory)
* @see org.jdesktop.swingx.table.ColumnFactory
*/
public ColumnFactory getColumnFactory() {
/*
* TODO JW: think about implications of not/ copying the reference to
* the shared instance into the table's field? Better access the
* getInstance() on each call? We are on single thread anyway...
* Furthermore, we don't expect the instance to change often, typically
* it is configured on startup. So we don't really have to worry about
* changes which would destabilize column state?
*/
if (columnFactory == null) {
return ColumnFactory.getInstance();
// columnFactory = ColumnFactory.getInstance();
}
return columnFactory;
}
/**
* Sets the <code>ColumnFactory</code> to use for column creation and
* configuration. The default value is the shared application ColumnFactory.
* <p>
*
* Note: this method has no side-effect, that is existing columns are
* <b>not</b> re-created automatically, client code must trigger it
* manually.
*
* @param columnFactory the factory to use, <code>null</code> indicates to
* use the shared application factory.
*
* @see #getColumnFactory()
* @see org.jdesktop.swingx.table.ColumnFactory
*/
public void setColumnFactory(ColumnFactory columnFactory) {
/*
*
* TODO auto-configure columns on set? or add public table api to do so?
* Mostly, this is meant to be done once in the lifetime of the table,
* preferably before a model is set ... overshoot?
*/
ColumnFactory old = getColumnFactory();
this.columnFactory = columnFactory;
firePropertyChange("columnFactory", old, getColumnFactory());
}
// -------------------------------- enhanced sizing support
/**
* Packs all the columns to their optimal size. Works best with auto
* resizing turned off.
*
* @param margin the margin to apply to each column.
*
* @see #packColumn(int, int)
* @see #packColumn(int, int, int)
*/
public void packTable(int margin) {
for (int c = 0; c < getColumnCount(); c++)
packColumn(c, margin, -1);
}
/**
* Packs an indivudal column in the table.
*
* @param column The Column index to pack in View Coordinates
* @param margin The Margin to apply to the column width.
*
* @see #packColumn(int, int, int)
* @see #packTable(int)
*/
public void packColumn(int column, int margin) {
packColumn(column, margin, -1);
}
/**
* Packs an indivual column in the table to less than or equal to the
* maximum witdth. If maximum is -1 then the column is made as wide as it
* needs.
*
* @param column the column index to pack in view coordinates
* @param margin the margin to apply to the column
* @param max the maximum width the column can be resized to, -1 means no
* limit
*
* @see #packColumn(int, int)
* @see #packTable(int)
* @see ColumnFactory#packColumn(JXTable, TableColumnExt, int, int)
*/
public void packColumn(int column, int margin, int max) {
getColumnFactory().packColumn(this, getColumnExt(column), margin, max);
}
/**
* Returns the preferred number of rows to show in a
* <code>JScrollPane</code>.
*
* @return the number of rows to show in a <code>JScrollPane</code>
* @see #setVisibleRowCount(int)
*/
public int getVisibleRowCount() {
return visibleRowCount;
}
/**
* Sets the preferred number of rows to show in a <code>JScrollPane</code>.
* <p>
*
* This is a bound property. The default value is 20.
* <p>
*
* PENDING: allow negative for use-all? Analogous to visColumnCount.
*
* @param visibleRowCount number of rows to show in a
* <code>JScrollPane</code>
* @throws IllegalArgumentException if given count is negative.
*
* @see #getVisibleRowCount()
*/
public void setVisibleRowCount(int visibleRowCount) {
if (visibleRowCount < 0)
throw new IllegalArgumentException(
"visible row count must not be negative " + visibleRowCount);
if (getVisibleRowCount() == visibleRowCount)
return;
int old = getVisibleRowCount();
this.visibleRowCount = visibleRowCount;
resetCalculatedScrollableSize(false);
firePropertyChange("visibleRowCount", old, getVisibleRowCount());
}
/**
* Returns the preferred number of columns to show in the
* <code>JScrollPane</code>.
*
* @return the number of columns to show in the scroll pane.
*
* @see #setVisibleColumnCount
*/
public int getVisibleColumnCount() {
return visibleColumnCount;
}
/**
* Sets the preferred number of Columns to show in a
* <code>JScrollPane</code>. A negative number is interpreted as use-all
* available visible columns.
* <p>
*
* This is a bound property. The default value is -1 (effectively the same
* as before the introduction of this property).
*
* @param visibleColumnCount number of rows to show in a
* <code>JScrollPane</code>
* @see #getVisibleColumnCount()
*/
public void setVisibleColumnCount(int visibleColumnCount) {
if (getVisibleColumnCount() == visibleColumnCount)
return;
int old = getVisibleColumnCount();
this.visibleColumnCount = visibleColumnCount;
resetCalculatedScrollableSize(true);
firePropertyChange("visibleColumnCount", old, getVisibleColumnCount());
}
/**
* Resets the calculated scrollable size in one dimension, if appropriate.
*
* @param isColumn flag to denote which dimension to reset, true for width,
* false for height
*
*/
private void resetCalculatedScrollableSize(boolean isColumn) {
if (calculatedPrefScrollableViewportSize != null) {
if (isColumn) {
calculatedPrefScrollableViewportSize.width = -1;
} else {
calculatedPrefScrollableViewportSize.height = -1;
}
}
}
/**
* {@inheritDoc}
* <p>
*
* If the given dimension is null, the auto-calculation of the pref
* scrollable size is enabled, otherwise the behaviour is the same as super.
*
* The default is auto-calc enabled on.
*
* @see #getPreferredScrollableViewportSize()
*/
@Override
public void setPreferredScrollableViewportSize(Dimension size) {
// TODO: figure out why firing the event screws the
// JXTableUnitTest.testPrefScrollableUpdatedOnStructureChanged
// Dimension old = getPreferredScrollableViewportSize();
super.setPreferredScrollableViewportSize(size);
// firePropertyChange("preferredScrollableViewportSize", old,
// getPreferredScrollableViewportSize());
}
/**
* {@inheritDoc}
* <p>
* Overridden to support auto-calculation of pref scrollable size, dependent
* on the visible row/column count properties. The auto-calc is on if
* there's no explicit pref scrollable size set. Otherwise the fixed size is
* returned
* <p>
*
* The calculation of the preferred scrollable width is delegated to the
* ColumnFactory to allow configuration with custom strategies implemented
* in custom factories.
*
* @see #setPreferredScrollableViewportSize(Dimension)
* @see org.jdesktop.swingx.table.ColumnFactory#getPreferredScrollableViewportWidth(JXTable)
*/
@Override
public Dimension getPreferredScrollableViewportSize() {
// client code has set this - takes precedence.
Dimension prefSize = super.getPreferredScrollableViewportSize();
if (prefSize != null) {
return new Dimension(prefSize);
}
if (calculatedPrefScrollableViewportSize == null) {
calculatedPrefScrollableViewportSize = new Dimension();
// JW: hmm... fishy ... shouldn't be necessary here?
// maybe its the "early init" in super's tableChanged();
// moved to init which looks okay so far
// initializeColumnPreferredWidths();
}
// the width is reset to -1 in setVisibleColumnCount
if (calculatedPrefScrollableViewportSize.width <= 0) {
calculatedPrefScrollableViewportSize.width = getColumnFactory()
.getPreferredScrollableViewportWidth(this);
}
// the heigth is reset in setVisualRowCount
if (calculatedPrefScrollableViewportSize.height <= 0) {
calculatedPrefScrollableViewportSize.height = getVisibleRowCount()
* getRowHeight();
}
return new Dimension(calculatedPrefScrollableViewportSize);
}
/**
* Initialize the width related properties of all contained TableColumns,
* both visible and hidden.
* <p>
* <ul>
* <li>PENDING: move into ColumnFactory?
* <li>PENDING: what to do if autoCreateColumn off?
* <li>PENDING: public? to allow manual setting of column properties which
* might effect their default sizing. Needed in testing - but real-world?
* the factory is meant to do the property setting, based on tableModel and
* meta-data (from where?). But leads to funny call sequence for per-table
* factory (new JXTable(), table.setColumnFactory(..), table.setModel(...))
* </ul>
*
* @see #initializeColumnPreferredWidth(TableColumn)
*/
protected void initializeColumnWidths() {
for (TableColumn column : getColumns(true)) {
initializeColumnPreferredWidth(column);
}
}
/**
* Initialize the width related properties of the specified column. The
* details are specified by the current <code>ColumnFactory</code> if the
* column is of type <code>TableColumnExt</code>. Otherwise nothing is
* changed.
* <p>
*
* TODO JW - need to cleanup getScrollablePreferred (refactor and inline)
*
* @param column TableColumn object representing view column
* @see org.jdesktop.swingx.table.ColumnFactory#configureColumnWidths
*/
protected void initializeColumnPreferredWidth(TableColumn column) {
if (column instanceof TableColumnExt) {
getColumnFactory().configureColumnWidths(this,
(TableColumnExt) column);
}
}
// ----------------- scrolling support
/**
* Scrolls vertically to make the given row visible. This might not have any
* effect if the table isn't contained in a <code>JViewport</code>.
* <p>
*
* Note: this method has no precondition as it internally uses
* <code>getCellRect</code> which is lenient to off-range coordinates.
*
* @param row the view row index of the cell
*
* @see #scrollColumnToVisible(int)
* @see #scrollCellToVisible(int, int)
* @see #scrollRectToVisible(Rectangle)
*/
public void scrollRowToVisible(int row) {
Rectangle cellRect = getCellRect(row, 0, false);
Rectangle visibleRect = getVisibleRect();
cellRect.x = visibleRect.x;
cellRect.width = visibleRect.width;
scrollRectToVisible(cellRect);
}
/**
* Scrolls horizontally to make the given column visible. This might not
* have any effect if the table isn't contained in a <code>JViewport</code>.
* <p>
*
* Note: this method has no precondition as it internally uses
* <code>getCellRect</code> which is lenient to off-range coordinates.
*
* @param column the view column index of the cell
*
* @see #scrollRowToVisible(int)
* @see #scrollCellToVisible(int, int)
* @see #scrollRectToVisible(Rectangle)
*/
public void scrollColumnToVisible(int column) {
Rectangle cellRect = getCellRect(0, column, false);
Rectangle visibleRect = getVisibleRect();
cellRect.y = visibleRect.y;
cellRect.height = visibleRect.height;
scrollRectToVisible(cellRect);
}
/**
* Scrolls to make the cell at row and column visible. This might not have
* any effect if the table isn't contained in a <code>JViewport</code>.
* <p>
*
* Note: this method has no precondition as it internally uses
* <code>getCellRect</code> which is lenient to off-range coordinates.
*
* @param row the view row index of the cell
* @param column the view column index of the cell
*
* @see #scrollColumnToVisible(int)
* @see #scrollRowToVisible(int)
* @see #scrollRectToVisible(Rectangle)
*/
public void scrollCellToVisible(int row, int column) {
Rectangle cellRect = getCellRect(row, column, false);
scrollRectToVisible(cellRect);
}
// ----------------------- delegating methods?? from super
/**
* Returns the selection mode used by this table's selection model.
* <p>
* PENDING JW - setter?
*
* @return the selection mode used by this table's selection model
* @see ListSelectionModel#getSelectionMode()
*/
public int getSelectionMode() {
return getSelectionModel().getSelectionMode();
}
// ----------------------- Search support
/**
* Starts a search on this List's visible items. This implementation asks the
* SearchFactory to open a find widget on itself.
*/
protected void doFind() {
SearchFactory.getInstance().showFindInput(this, getSearchable());
}
/**
* Returns a Searchable for this component, guaranteed to be not null. This
* implementation lazily creates a TableSearchable if necessary.
*
*
* @return a not-null Searchable for this component.
*
* @see #setSearchable(Searchable)
* @see org.jdesktop.swingx.search.TableSearchable
*/
public Searchable getSearchable() {
if (searchable == null) {
searchable = new TableSearchable(this);
}
return searchable;
}
/**
* Sets the Searchable for this table. If null, a default
* searchable will be used.
*
* @param searchable the Searchable to use for this table, may be null to indicate
* using the table's default searchable.
*/
public void setSearchable(Searchable searchable) {
this.searchable = searchable;
}
// ----------------------------------- uniform data model access
/**
* @return the unconfigured ComponentAdapter.
*/
protected ComponentAdapter getComponentAdapter() {
if (dataAdapter == null) {
dataAdapter = new TableAdapter(this);
}
return dataAdapter;
}
/**
* Convenience to access a configured ComponentAdapter.
*
* @param row the row index in view coordinates.
* @param column the column index in view coordinates.
* @return the configured ComponentAdapter.
*/
protected ComponentAdapter getComponentAdapter(int row, int column) {
ComponentAdapter adapter = getComponentAdapter();
adapter.row = row;
adapter.column = column;
return adapter;
}
protected static class TableAdapter extends ComponentAdapter {
private final JXTable table;
/**
* Constructs a <code>TableDataAdapter</code> for the specified target
* component.
*
* @param component the target component
*/
public TableAdapter(JXTable component) {
super(component);
table = component;
}
/**
* Typesafe accessor for the target component.
*
* @return the target component as a {@link javax.swing.JTable}
*/
public JXTable getTable() {
return table;
}
/**
* {@inheritDoc}
*/
@Override
public String getColumnName(int columnIndex) {
TableColumn column = getColumnByModelIndex(columnIndex);
return column == null ? "" : column.getHeaderValue().toString();
}
protected TableColumn getColumnByModelIndex(int modelColumn) {
// throwing here makes a filter test fail .. it's probably an issue
// but don't want to touch (swingx filters will be gone soon)
// if ((modelColumn < 0) || (modelColumn >= getColumnCount())) {
// throw new IllegalArgumentException("invalid column index: " +
// modelColumn);
// }
List<TableColumn> columns = table.getColumns(true);
for (Iterator<TableColumn> iter = columns.iterator(); iter
.hasNext();) {
TableColumn column = iter.next();
if (column.getModelIndex() == modelColumn) {
return column;
}
}
return null;
}
// @Override
// public String getColumnIdentifier(int columnIndex) {
// // TableColumn column = getColumnByModelIndex(columnIndex);
// // Object identifier = column != null ? column.getIdentifier() :
// null;
// Object identifier = getColumnIdentifierAt(columnIndex);
// return identifier != null ? identifier.toString() : null;
// }
/**
* {@inheritDoc}
*/
@Override
public Object getColumnIdentifierAt(int columnIndex) {
if ((columnIndex < 0) || (columnIndex >= getColumnCount())) {
throw new ArrayIndexOutOfBoundsException(
"invalid column index: " + columnIndex);
}
TableColumn column = getColumnByModelIndex(columnIndex);
Object identifier = column != null ? column.getIdentifier() : null;
return identifier;
}
/**
* {@inheritDoc}
*/
@Override
public int getColumnIndex(Object identifier) {
TableColumn column = table.getColumnExt(identifier);
return column != null ? column.getModelIndex() : -1;
}
/**
* {@inheritDoc}
*/
@Override
public int getColumnCount() {
return table.getModel().getColumnCount();
}
/**
* {@inheritDoc}
*/
@Override
public int getRowCount() {
return table.getModel().getRowCount();
}
/**
* {@inheritDoc}
*/
@Override
public Object getValueAt(int row, int column) {
return table.getModel().getValueAt(row, column);
}
/**
* {@inheritDoc}
*/
@Override
public void setValueAt(Object aValue, int row, int column) {
table.getModel().setValueAt(aValue, row, column);
}
/**
* {@inheritDoc}
*/
@Override
public boolean isCellEditable(int row, int column) {
return table.getModel().isCellEditable(row, column);
}
/**
* {@inheritDoc}
*/
@Override
public boolean isTestable(int column) {
return getColumnByModelIndex(column) != null;
}
// -------------------------- accessing view state/values
/**
* {@inheritDoc}
*/
@Override
public Object getFilteredValueAt(int row, int column) {
return getValueAt(table.convertRowIndexToModel(row), column);
}
/**
* {@inheritDoc}
*/
@Override
public Object getValue() {
return table.getValueAt(row, column);
}
/**
* {@inheritDoc}
* <p>
*
* PENDING JW: this is implemented to duplicate this table's lookup code
* if the column is not visible. That's not good enough if subclasses
* implemented a different strategy! We do it anyway for now, mostly we
* will be lucky and improve the situation against using toString
* always.
*
*/
@Override
public String getFilteredStringAt(int row, int column) {
int viewColumn = modelToView(column);
if (viewColumn >= 0) {
return table.getStringAt(row, viewColumn);
}
// PENDING JW: how to get a String rep for invisible cells?
// rows may be filtered, columns hidden.
TableCellRenderer renderer = getRendererByModelColumn(column);
if (renderer instanceof StringValue) {
return ((StringValue) renderer).getString(getFilteredValueAt(
row, column));
}
return super.getFilteredStringAt(row, column);
}
/**
* {@inheritDoc}
*/
@Override
public String getString() {
return table.getStringAt(row, column);
}
/**
* {@inheritDoc}
*
* PENDING JW: this is implemented to duplicate this table's lookup code
* if either the row or the column is not visible. That's not good
* enough if subclasses implemented a different strategy! We do it
* anyway for now, mostly we will be lucky and improve the situation
* against using toString always.
*
*/
@Override
public String getStringAt(int row, int column) {
int viewRow = table.convertRowIndexToView(row);
int viewColumn = table.convertColumnIndexToView(column);
if ((viewRow >= 0) && (viewColumn >= 0)) {
return table.getStringAt(viewRow, viewColumn);
}
TableCellRenderer renderer = getRendererByModelColumn(column);
if (renderer instanceof StringValue) {
return ((StringValue) renderer).getString(getValueAt(row,
column));
}
// no luck - return default
return super.getStringAt(row, column);
}
/**
* Returns a suitable renderer for the column index in model
* coordinates.
*
* PENDING JW: this duplicates this table's lookup code if column is not
* visible. That's not good enough if subclasses implemented a different
* strategy! We do it anyway for now, mostly we will be lucky and
* improve the situation against using toString always.
*
* @param column the columnIndex in model coordinates
* @return a renderer suitable for rendering cells in the given column
*/
private TableCellRenderer getRendererByModelColumn(int column) {
// PENDING JW: here we are tricksing - duplicating JXTable renderer
// lookup strategy
// that's inherently unsafe, as subclasses may decide to do it
// differently
TableColumn tableColumn = getColumnByModelIndex(column);
TableCellRenderer renderer = tableColumn.getCellRenderer();
if (renderer == null) {
renderer = table.getDefaultRenderer(table.getModel()
.getColumnClass(column));
}
if (renderer == null) {
renderer = table.getDefaultRenderer(Object.class);
}
return renderer;
}
/**
* {@inheritDoc}
*/
@Override
public boolean isEditable() {
return table.isCellEditable(row, column);
}
/**
* {@inheritDoc}
*/
@Override
public boolean isSelected() {
return table.isCellSelected(row, column);
}
/**
* {@inheritDoc}
*/
@Override
public boolean hasFocus() {
boolean rowIsLead = (table.getSelectionModel()
.getLeadSelectionIndex() == row);
boolean colIsLead = (table.getColumnModel().getSelectionModel()
.getLeadSelectionIndex() == column);
return table.isFocusOwner() && (rowIsLead && colIsLead);
}
/**
* {@inheritDoc}
*/
@Override
public int modelToView(int columnIndex) {
return table.convertColumnIndexToView(columnIndex);
}
/**
* {@inheritDoc}
*/
@Override
public int viewToModel(int columnIndex) {
return table.convertColumnIndexToModel(columnIndex);
}
}
// --------------------- managing renderers/editors
/**
* Sets the <code>Highlighter</code>s to the table, replacing any old
* settings. None of the given Highlighters must be null.
* <p>
*
* This is a bound property.
* <p>
*
* Note: as of version #1.257 the null constraint is enforced strictly. To
* remove all highlighters use this method without param.
*
* @param highlighters zero or more not null highlighters to use for
* renderer decoration.
* @throws NullPointerException if array is null or array contains null
* values.
*
* @see #getHighlighters()
* @see #addHighlighter(Highlighter)
* @see #removeHighlighter(Highlighter)
*
*/
public void setHighlighters(Highlighter... highlighters) {
Highlighter[] old = getHighlighters();
getCompoundHighlighter().setHighlighters(highlighters);
firePropertyChange("highlighters", old, getHighlighters());
}
/**
* Returns the <code>Highlighter</code>s used by this table. Maybe empty,
* but guarantees to be never null.
*
* @return the Highlighters used by this table, guaranteed to never null.
* @see #setHighlighters(Highlighter[])
*/
public Highlighter[] getHighlighters() {
return getCompoundHighlighter().getHighlighters();
}
/**
* Appends a <code>Highlighter</code> to the end of the list of used
* <code>Highlighter</code>s. The argument must not be null.
* <p>
*
* @param highlighter the <code>Highlighter</code> to add, must not be null.
* @throws NullPointerException if <code>Highlighter</code> is null.
*
* @see #removeHighlighter(Highlighter)
* @see #setHighlighters(Highlighter[])
*/
public void addHighlighter(Highlighter highlighter) {
Highlighter[] old = getHighlighters();
getCompoundHighlighter().addHighlighter(highlighter);
firePropertyChange("highlighters", old, getHighlighters());
}
/**
* Removes the given Highlighter.
* <p>
*
* Does nothing if the Highlighter is not contained.
*
* @param highlighter the Highlighter to remove.
* @see #addHighlighter(Highlighter)
* @see #setHighlighters(Highlighter...)
*/
public void removeHighlighter(Highlighter highlighter) {
Highlighter[] old = getHighlighters();
getCompoundHighlighter().removeHighlighter(highlighter);
firePropertyChange("highlighters", old, getHighlighters());
}
/**
* Returns the CompoundHighlighter assigned to the table, null if none.
* PENDING: open up for subclasses again?.
*
* @return the CompoundHighlighter assigned to the table.
*/
protected CompoundHighlighter getCompoundHighlighter() {
if (compoundHighlighter == null) {
compoundHighlighter = new CompoundHighlighter();
compoundHighlighter
.addChangeListener(getHighlighterChangeListener());
}
return compoundHighlighter;
}
/**
* Returns the <code>ChangeListener</code> to use with highlighters. Lazily
* creates the listener.
*
* @return the ChangeListener for observing changes of highlighters,
* guaranteed to be <code>not-null</code>
*/
protected ChangeListener getHighlighterChangeListener() {
if (highlighterChangeListener == null) {
highlighterChangeListener = createHighlighterChangeListener();
}
return highlighterChangeListener;
}
/**
* Creates and returns the ChangeListener observing Highlighters.
* <p>
* Here: repaints the table on receiving a stateChanged.
*
* @return the ChangeListener defining the reaction to changes of
* highlighters.
*/
protected ChangeListener createHighlighterChangeListener() {
return new ChangeListener() {
public void stateChanged(ChangeEvent e) {
repaint();
}
};
}
/**
* Returns the string representation of the cell value at the given
* position.
*
* @param row the row index of the cell in view coordinates
* @param column the column index of the cell in view coordinates.
* @return the string representation of the cell value as it will appear in
* the table.
*/
public String getStringAt(int row, int column) {
TableCellRenderer renderer = getCellRenderer(row, column);
if (renderer instanceof StringValue) {
return ((StringValue) renderer).getString(getValueAt(row, column));
}
return StringValues.TO_STRING.getString(getValueAt(row, column));
}
/**
* {@inheritDoc}
* <p>
*
* Overridden to fix core bug #4614616 (NPE if <code>TableModel</code>'s
* <code>Class</code> for the column is an interface). This method
* guarantees to always return a <code>not null</code> value. Returns the
* default renderer for <code>Object</code> if super returns
* <code>null</code>.
*
*
*/
@Override
public TableCellRenderer getCellRenderer(int row, int column) {
TableCellRenderer renderer = super.getCellRenderer(row, column);
if (renderer == null) {
renderer = getDefaultRenderer(Object.class);
}
return renderer;
}
/**
* Returns the decorated <code>Component</code> used as a stamp to render
* the specified cell. Overrides superclass version to provide support for
* cell decorators.
* <p>
*
* Adjusts component orientation (guaranteed to happen before applying
* Highlighters).
* <p>
*
* Per-column highlighters contained in
* {@link TableColumnExt#getHighlighters()} are applied to the renderer
* <i>after</i> the table highlighters.
* <p>
*
* TODO kgs: interaction of search highlighter and column highlighters
* <p>
*
* 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 in
* <code>resetDefaultTableCellRendererColors</code> which is called after
* super.prepareRenderer and before applying the Highlighters. The method is
* called always and for all renderers.
*
* @param renderer the <code>TableCellRenderer</code> to prepare
* @param row the row of the cell to render, where 0 is the first row
* @param column the column of the cell to render, where 0 is the first
* column
* @return the decorated <code>Component</code> used as a stamp to render
* the specified cell
* @see #resetDefaultTableCellRendererColors(Component, int, int)
* @see org.jdesktop.swingx.decorator.Highlighter
*/
@Override
public Component prepareRenderer(TableCellRenderer renderer, int row,
int column) {
Component stamp = super.prepareRenderer(renderer, row, column);
// #145-swingx: default renderers don't respect componentOrientation.
adjustComponentOrientation(stamp);
// #258-swingx: hacking around DefaultTableCellRenderer color memory.
resetDefaultTableCellRendererColors(stamp, row, column);
ComponentAdapter adapter = getComponentAdapter(row, column);
// a very slight optimization: if this instance never had a highlighter
// added then don't create a compound here.
if (compoundHighlighter != null) {
stamp = compoundHighlighter.highlight(stamp, adapter);
}
TableColumnExt columnExt = getColumnExt(column);
if (columnExt != null) {
// JW: fix for #838 - artificial compound installs listener
// PENDING JW: instead of doing the looping ourselves, how
// about adding a method prepareRenderer to the TableColumnExt
for (Highlighter highlighter : columnExt.getHighlighters()) {
stamp = highlighter.highlight(stamp, adapter);
}
// CompoundHighlighter columnHighlighters
// = new CompoundHighlighter(columnExt.getHighlighters());
}
return stamp;
}
/**
*
* Method to apply a hack around DefaultTableCellRenderer "color memory"
* (Issue #258-swingx). Applies the hack if the client property
* <code>USE_DTCR_COLORMEMORY_HACK</code> having the value of
* <code>Boolean.TRUE</code>, does nothing otherwise. The property is true
* by default.
* <p>
*
* The hack consists of applying a specialized <code>Highlighter</code> to
* force reset the color "memory" of <code>DefaultTableCellRenderer</code>.
* Note that the hack is applied always, that is even if there are no custom
* Highlighters.
* <p>
*
* Client code which solves the problem at the core (that is in a
* well-behaved <code>DefaultTableCellRenderer</code>) can disable the hack
* by removing the client property or by subclassing and override this to do
* nothing.
*
* @param renderer the <code>TableCellRenderer</code> to hack
* @param row the row of the cell to render
* @param column the column index of the cell to render
*
* @see #prepareRenderer(TableCellRenderer, int, int)
* @see #USE_DTCR_COLORMEMORY_HACK
* @see org.jdesktop.swingx.decorator.ResetDTCRColorHighlighter
*/
protected void resetDefaultTableCellRendererColors(Component renderer,
int row, int column) {
if (!Boolean.TRUE.equals(getClientProperty(USE_DTCR_COLORMEMORY_HACK)))
return;
ComponentAdapter adapter = getComponentAdapter(row, column);
if (resetDefaultTableCellRendererHighlighter == null) {
resetDefaultTableCellRendererHighlighter = new ResetDTCRColorHighlighter();
}
// hacking around DefaultTableCellRenderer color memory.
resetDefaultTableCellRendererHighlighter.highlight(renderer, adapter);
}
/**
* {@inheritDoc}
* <p>
*
* Overridden to adjust the editor's component orientation.
*/
@Override
public Component prepareEditor(TableCellEditor editor, int row, int column) {
Component comp = super.prepareEditor(editor, row, column);
// JW: might be null if generic editor barks about constructor
// super silently backs out - we do the same here
if (comp != null) {
adjustComponentOrientation(comp);
}
return comp;
}
/**
* Adjusts the <code>Component</code>'s orientation to this
* <code>JXTable</code>'s CO if appropriate. The parameter must not be
* <code>null</code>.
* <p>
*
* This implementation synchs the CO always.
*
* @param stamp the <code>Component</code> who's CO may need to be synched,
* must not be <code>null</code>.
*/
protected void adjustComponentOrientation(Component stamp) {
if (stamp.getComponentOrientation().equals(getComponentOrientation()))
return;
stamp.applyComponentOrientation(getComponentOrientation());
}
/**
* Returns a new instance of the default renderer for the specified class.
* This differs from <code>getDefaultRenderer()</code> in that it returns a
* <b>new </b> instance each time so that the renderer may be set and
* customized on a particular column.
* <p>
*
* NOTE: this doesn't work with swingx renderers! Do we really need it? It
* had been used in JNTable which is practically obsolete. If needed, we
* could make all renderer support classes clonable.
*
* @param columnClass Class of value being rendered
* @return TableCellRenderer instance which renders values of the specified
* type
* @see #getDefaultRenderer(Class)
*/
public TableCellRenderer getNewDefaultRenderer(Class<?> columnClass) {
TableCellRenderer renderer = getDefaultRenderer(columnClass);
if (renderer != null) {
try {
return renderer.getClass().newInstance();
} catch (Exception e) {
LOG.fine("could not create renderer for " + columnClass);
}
}
// JW PENDING: must not return null!
return null;
}
/**
* Creates default cell renderers for <code>Object</code>s,
* <code>Number</code>s, <code>Date</code>s, <code>Boolean</code>s, and
* <code>Icon/Image/</code>s.
* <p>
* Overridden to install SwingX renderers plus hacking around huge memory
* consumption of UIDefaults (see #6345050 in core Bug parade)
* <p>
* {@inheritDoc}
*
* @see org.jdesktop.swingx.renderer.DefaultTableRenderer
* @see org.jdesktop.swingx.renderer.ComponentProvider
*/
@Override
protected void createDefaultRenderers() {
// super.createDefaultRenderers();
// This duplicates JTable's functionality in order to make the renderers
// available in getNewDefaultRenderer(); If JTable's renderers either
// were public, or it provided a factory for *new* renderers, this would
// not be needed
// hack around #6345050 - new UIDefaults()
// is created with a huge initialCapacity
// giving a dummy key/value array as parameter reduces that capacity
// to length/2.
Object[] dummies = new Object[] { 1, 0, 2, 0, 3, 0, 4, 0, 5, 0, 6, 0,
7, 0, 8, 0, 9, 0, 10, 0, };
defaultRenderersByColumnClass = new UIDefaults(dummies);
defaultRenderersByColumnClass.clear();
// configured default table renderer (internally LabelProvider)
setDefaultRenderer(Object.class, new DefaultTableRenderer());
setDefaultRenderer(Number.class, new DefaultTableRenderer(
StringValues.NUMBER_TO_STRING, JLabel.RIGHT));
setDefaultRenderer(Date.class, new DefaultTableRenderer(
StringValues.DATE_TO_STRING));
// use the same center aligned default for Image/Icon
TableCellRenderer renderer = new DefaultTableRenderer(new MappedValue(
StringValues.EMPTY, IconValues.ICON), JLabel.CENTER);
setDefaultRenderer(Icon.class, renderer);
setDefaultRenderer(ImageIcon.class, renderer);
// use a ButtonProvider for booleans
setDefaultRenderer(Boolean.class, new DefaultTableRenderer(
new CheckBoxProvider()));
// // standard renderers
// // Objects
// setLazyRenderer(Object.class,
// "javax.swing.table.DefaultTableCellRenderer");
//
// // Numbers
// setLazyRenderer(Number.class,
// "org.jdesktop.swingx.JXTable$NumberRenderer");
//
// // Doubles and Floats
// setLazyRenderer(Float.class,
// "org.jdesktop.swingx.JXTable$DoubleRenderer");
// setLazyRenderer(Double.class,
// "org.jdesktop.swingx.JXTable$DoubleRenderer");
//
// // Dates
// setLazyRenderer(Date.class,
// "org.jdesktop.swingx.JXTable$DateRenderer");
//
// // Icons and ImageIcons
// setLazyRenderer(Icon.class,
// "org.jdesktop.swingx.JXTable$IconRenderer");
// setLazyRenderer(ImageIcon.class,
// "org.jdesktop.swingx.JXTable$IconRenderer");
//
// // Booleans
// setLazyRenderer(Boolean.class,
// "org.jdesktop.swingx.JXTable$BooleanRenderer");
}
/** c&p'ed from super */
@SuppressWarnings("unchecked")
private void setLazyValue(Hashtable h, Class c, String s) {
h.put(c, new UIDefaults.ProxyLazyValue(s));
}
/** c&p'ed from super */
private void setLazyEditor(Class<?> c, String s) {
setLazyValue(defaultEditorsByColumnClass, c, s);
}
/**
* Creates default cell editors for objects, numbers, and boolean values.
* <p>
* Overridden to hook enhanced editors (f.i. <code>NumberEditorExt</code>
* )plus hacking around huge memory consumption of UIDefaults (see #6345050
* in core Bug parade)
*
* @see DefaultCellEditor
*/
@Override
protected void createDefaultEditors() {
Object[] dummies = new Object[] { 1, 0, 2, 0, 3, 0, 4, 0, 5, 0, 6, 0,
7, 0, 8, 0, 9, 0, 10, 0,
};
defaultEditorsByColumnClass = new UIDefaults(dummies);
defaultEditorsByColumnClass.clear();
// defaultEditorsByColumnClass = new UIDefaults();
// Objects
setLazyEditor(Object.class, "org.jdesktop.swingx.JXTable$GenericEditor");
// Numbers
// setLazyEditor(Number.class,
// "org.jdesktop.swingx.JXTable$NumberEditor");
setLazyEditor(Number.class, "org.jdesktop.swingx.table.NumberEditorExt");
// Booleans
setLazyEditor(Boolean.class,
"org.jdesktop.swingx.JXTable$BooleanEditor");
}
/**
* Default editor registered for <code>Object</code>. The editor tries to
* create a new instance of the column's class by reflection. It assumes
* that the class has a constructor taking a single <code>String</code>
* parameter.
* <p>
*
* The editor can be configured with a custom <code>JTextField</code>.
*
*/
public static class GenericEditor extends DefaultCellEditor {
Class<?>[] argTypes = new Class<?>[] { String.class };
java.lang.reflect.Constructor<?> constructor;
Object value;
public GenericEditor() {
this(new JTextField());
}
public GenericEditor(JTextField textField) {
super(textField);
getComponent().setName("Table.editor");
}
@Override
public boolean stopCellEditing() {
String s = (String) super.getCellEditorValue();
// Here we are dealing with the case where a user
// has deleted the string value in a cell, possibly
// after a failed validation. Return null, so that
// they have the option to replace the value with
// null or use escape to restore the original.
// For Strings, return "" for backward compatibility.
if ("".equals(s)) {
if (constructor.getDeclaringClass() == String.class) {
value = s;
}
super.stopCellEditing();
}
try {
value = constructor.newInstance(new Object[] { s });
} catch (Exception e) {
((JComponent) getComponent()).setBorder(new LineBorder(
Color.red));
return false;
}
return super.stopCellEditing();
}
@Override
public Component getTableCellEditorComponent(JTable table,
Object value, boolean isSelected, int row, int column) {
this.value = null;
((JComponent) getComponent())
.setBorder(new LineBorder(Color.black));
try {
Class<?> type = table.getColumnClass(column);
// Since our obligation is to produce a value which is
// assignable for the required type it is OK to use the
// String constructor for columns which are declared
// to contain Objects. A String is an Object.
if (type == Object.class) {
type = String.class;
}
constructor = type.getConstructor(argTypes);
} catch (Exception e) {
return null;
}
return super.getTableCellEditorComponent(table, value, isSelected,
row, column);
}
@Override
public Object getCellEditorValue() {
return value;
}
}
/**
*
* Editor for <code>Number</code>s.
* <p>
* Note: this is no longer registered by default. The current default is
* <code>NumberEditorExt</code> which differs from this in being
* locale-aware.
*
*/
public static class NumberEditor extends GenericEditor {
public NumberEditor() {
((JTextField) getComponent())
.setHorizontalAlignment(JTextField.RIGHT);
}
}
/**
* The default editor for <code>Boolean</code> types.
*/
public static class BooleanEditor extends DefaultCellEditor {
public BooleanEditor() {
super(new JCheckBox());
JCheckBox checkBox = (JCheckBox) getComponent();
checkBox.setHorizontalAlignment(JCheckBox.CENTER);
}
}
// ----------------------------- enhanced editing support
/**
* Returns the editable property of the <code>JXTable</code> as a whole.
*
* @return boolean to indicate if the table is editable.
* @see #setEditable
*/
public boolean isEditable() {
return editable;
}
/**
* Sets the editable property. This property allows to mark all cells in a
* table as read-only, independent of their per-column editability as
* returned by <code>TableColumnExt.isEditable</code> and their per-cell
* editability as returned by the <code>TableModel.isCellEditable</code>. If
* a cell is read-only in its column or model layer, this property has no
* effect.
* <p>
*
* The default value is <code>true</code>.
*
* @param editable the flag to indicate if the table is editable.
* @see #isEditable
* @see #isCellEditable(int, int)
*/
public void setEditable(boolean editable) {
boolean old = isEditable();
this.editable = editable;
firePropertyChange("editable", old, isEditable());
}
/**
* Returns the property which determines the edit termination behaviour on
* focus lost.
*
* @return boolean to indicate whether an ongoing edit should be terminated
* if the focus is moved to somewhere outside of the table.
* @see #setTerminateEditOnFocusLost(boolean)
*/
public boolean isTerminateEditOnFocusLost() {
return Boolean.TRUE
.equals(getClientProperty("terminateEditOnFocusLost"));
}
/**
* Sets the property to determine whether an ongoing edit should be
* terminated if the focus is moved to somewhere outside of the table. If
* true, terminates the edit, does nothing otherwise. The exact behaviour is
* implemented in <code>JTable.CellEditorRemover</code>: "outside" is
* interpreted to be on a component which is not under the table hierarchy
* but inside the same toplevel window, "terminate" does so in any case,
* first tries to stop the edit, if that's unsuccessful it cancels the edit.
* <p>
* The default value is <code>true</code>.
*
* @param terminate the flag to determine whether or not to terminate the
* edit
* @see #isTerminateEditOnFocusLost()
*/
public void setTerminateEditOnFocusLost(boolean terminate) {
// JW: we can leave the propertyChange notification to the
// putClientProperty - the key and method name are the same
putClientProperty("terminateEditOnFocusLost", terminate);
}
/**
* Returns the autoStartsEdit property.
*
* @return boolean to indicate whether a keyStroke should try to start
* editing.
* @see #setAutoStartEditOnKeyStroke(boolean)
*/
public boolean isAutoStartEditOnKeyStroke() {
return !Boolean.FALSE
.equals(getClientProperty("JTable.autoStartsEdit"));
}
/**
* Sets the autoStartsEdit property. If true, keystrokes are passed-on to
* the cellEditor of the lead cell to let it decide whether to start an
* edit.
* <p>
* The default value is <code>true</code>.
* <p>
*
* @param autoStart boolean to determine whether a keyStroke should try to
* start editing.
* @see #isAutoStartEditOnKeyStroke()
*/
public void setAutoStartEditOnKeyStroke(boolean autoStart) {
boolean old = isAutoStartEditOnKeyStroke();
// JW: we have to take over propertyChange notification
// because the key and method name are different.
// As a consequence, there are two events fired: one for
// the client prop and one for this method.
putClientProperty("JTable.autoStartsEdit", autoStart);
firePropertyChange("autoStartEditOnKeyStroke", old,
isAutoStartEditOnKeyStroke());
}
/**
* {@inheritDoc}
* <p>
*
* overridden to install a custom editor remover.
*/
@Override
public boolean editCellAt(int row, int column, EventObject e) {
boolean started = super.editCellAt(row, column, e);
if (started) {
hackEditorRemover();
}
return started;
}
/**
* Overridden with backport from Mustang fix for #4684090, #4887999.
*/
@Override
public void removeEditor() {
// if (editorRemover != null) {
// editorRemover.uninstall();
// editorRemover = null;
// }
boolean isFocusOwnerInTheTable = isFocusOwnerDescending();
// let super do its stuff
super.removeEditor();
if (isFocusOwnerInTheTable) {
requestFocusInWindow();
}
}
/**
* Returns a boolean to indicate if the current focus owner is descending
* from this table. Returns false if not editing, otherwise walks the
* focusOwner hierarchy, taking popups into account.
*
* @return a boolean to indicate if the current focus owner is contained.
*/
private boolean isFocusOwnerDescending() {
if (!isEditing())
return false;
Component focusOwner = KeyboardFocusManager
.getCurrentKeyboardFocusManager().getFocusOwner();
// PENDING JW: special casing to not fall through ... really wanted?
if (focusOwner == null)
return false;
if (SwingXUtilities.isDescendingFrom(focusOwner, this))
return true;
// same with permanent focus owner
Component permanent = KeyboardFocusManager
.getCurrentKeyboardFocusManager().getPermanentFocusOwner();
return SwingXUtilities.isDescendingFrom(permanent, this);
}
// /**
// * @param focusOwner
// * @return
// */
// private boolean isDescending(Component focusOwner) {
// while (focusOwner != null) {
// if (focusOwner instanceof JPopupMenu) {
// focusOwner = ((JPopupMenu) focusOwner).getInvoker();
// if (focusOwner == null) {
// return false;
// }
// }
// if (focusOwner == this) {
// return true;
// }
// focusOwner = focusOwner.getParent();
// }
// return false;
// }
protected transient CellEditorRemover editorRemover;
/**
* removes the standard editor remover and adds the custom remover.
*
*/
private void hackEditorRemover() {
KeyboardFocusManager manager = KeyboardFocusManager
.getCurrentKeyboardFocusManager();
PropertyChangeListener[] listeners = manager
.getPropertyChangeListeners("permanentFocusOwner");
for (int i = listeners.length - 1; i >= 0; i--) {
if (listeners[i].getClass().getName().startsWith(
"javax.swing.JTable")) {
manager.removePropertyChangeListener("permanentFocusOwner",
listeners[i]);
break;
}
}
if (editorRemover == null) {
editorRemover = new CellEditorRemover();
}
}
/**
* {@inheritDoc}
* <p>
*
* Overridden to uninstall the custom editor remover.
*/
@Override
public void removeNotify() {
if (editorRemover != null) {
editorRemover.uninstall();
editorRemover = null;
}
super.removeNotify();
}
/**
* {@inheritDoc}
* <p>
*
* Overridden to prevent spurious focus loss to outside of table while
* removing the editor. This is essentially a hack around core bug #6210779.
*
* PENDING: add link to wiki!
*/
@Override
public boolean isFocusCycleRoot() {
if (isEditingFocusCycleRoot()) {
return true;
}
return super.isFocusCycleRoot();
}
/**
* {@inheritDoc}
* <p>
* Overridden to try to stop the edit, if appropriate. Calls super if
* succeeded, does not yield otherwise.
*
*/
@Override
public void transferFocus() {
if (isEditingFocusCycleRoot() && !getCellEditor().stopCellEditing())
return;
super.transferFocus();
}
/**
* {@inheritDoc}
* <p>
* Overridden to try to stop the edit, if appropiate. Calls super if
* succeeded, does not yield otherwise.
*
*/
@Override
public void transferFocusBackward() {
if (isEditingFocusCycleRoot() && !getCellEditor().stopCellEditing())
return;
super.transferFocusBackward();
}
/**
*
* @return a boolean to indicate whether the table needs to fake being focus
* cycle root.
*/
private boolean isEditingFocusCycleRoot() {
return isEditing() && isTerminateEditOnFocusLost();
}
/**
* This class tracks changes in the keyboard focus state. It is used when
* the JTable is editing to determine when to cancel the edit. If focus
* switches to a component outside of the jtable, but in the same window,
* this will cancel editing.
*/
class CellEditorRemover implements PropertyChangeListener {
KeyboardFocusManager focusManager;
public CellEditorRemover() {
install();
}
private void install() {
focusManager = KeyboardFocusManager
.getCurrentKeyboardFocusManager();
focusManager.addPropertyChangeListener("permanentFocusOwner", this);
focusManager.addPropertyChangeListener("managingFocus", this);
}
/**
* remove all listener registrations.
*
*/
public void uninstall() {
focusManager.removePropertyChangeListener("permanentFocusOwner",
this);
focusManager.removePropertyChangeListener("managingFocus", this);
focusManager = null;
}
public void propertyChange(PropertyChangeEvent ev) {
if (ev == null)
return;
if ("permanentFocusOwner".equals(ev.getPropertyName())) {
permanentFocusOwnerChange();
} else if ("managingFocus".equals(ev.getPropertyName())) {
// TODO uninstall/install after manager changed.
}
}
/**
*
*/
private void permanentFocusOwnerChange() {
if (!isEditing() || !isTerminateEditOnFocusLost()) {
return;
}
Component c = focusManager.getPermanentFocusOwner();
while (c != null) {
// PENDING JW: logic untested!
if (c instanceof JPopupMenu) {
c = ((JPopupMenu) c).getInvoker();
} else {
if (c == JXTable.this) {
// focus remains inside the table
return;
} else if (c instanceof JPopupMenu) {
// PENDING JW: left-over? we should never reach this ...
// need to switch the hierarchy to a popups invoker
} else if ((c instanceof Window)
|| (c instanceof Applet && c.getParent() == null)) {
if (c == SwingUtilities.getRoot(JXTable.this)) {
if (!getCellEditor().stopCellEditing()) {
getCellEditor().cancelCellEditing();
}
}
break;
}
c = c.getParent();
}
}
}
}
// ---------------------------- updateUI support
/**
* {@inheritDoc}
* <p>
* Additionally updates auto-adjusted row height and highlighters.
* <p>
* Another of the override motivation is to fix core issue (?? ID): super
* fails to update <b>all</b> renderers/editors.
*/
@Override
public void updateUI() {
super.updateUI();
updateColumnControlUI();
for (Enumeration<?> defaultEditors = defaultEditorsByColumnClass
.elements(); defaultEditors.hasMoreElements();) {
updateEditorUI(defaultEditors.nextElement());
}
for (Enumeration<?> defaultRenderers = defaultRenderersByColumnClass
.elements(); defaultRenderers.hasMoreElements();) {
updateRendererUI(defaultRenderers.nextElement());
}
for (TableColumn column : getColumns(true)) {
updateColumnUI(column);
}
updateRowHeightUI(true);
updateHighlighterUI();
}
/**
* Updates the ui of the columnControl if appropriate.
*/
protected void updateColumnControlUI() {
if ((columnControlButton != null)
&& (columnControlButton.getParent() == null)) {
SwingUtilities.updateComponentTreeUI(columnControlButton);
}
}
/**
* Tries its best to <code>updateUI</code> of the potential
* <code>TableCellEditor</code>.
*
* @param maybeEditor the potential editor.
*/
private void updateEditorUI(Object maybeEditor) {
// maybe null or proxyValue
if (!(maybeEditor instanceof TableCellEditor))
return;
// super handled this
if ((maybeEditor instanceof JComponent)
|| (maybeEditor instanceof DefaultCellEditor))
return;
// custom editors might balk about fake rows/columns
try {
Component comp = ((TableCellEditor) maybeEditor)
.getTableCellEditorComponent(this, null, false, -1, -1);
if (comp != null) {
SwingUtilities.updateComponentTreeUI(comp);
}
} catch (Exception e) {
// ignore - can't do anything
}
}
/**
* Tries its best to <code>updateUI</code> of the potential
* <code>TableCellRenderer</code>.
*
* @param maybeRenderer the potential renderer.
*/
private void updateRendererUI(Object maybeRenderer) {
// maybe null or proxyValue
if (!(maybeRenderer instanceof TableCellRenderer))
return;
// super handled this
if (maybeRenderer instanceof JComponent)
return;
Component comp = null;
if (maybeRenderer instanceof AbstractRenderer) {
comp = ((AbstractRenderer) maybeRenderer).getComponentProvider()
.getRendererComponent(null);
} else {
try {
// custom editors might balk about fake rows/columns
comp = ((TableCellRenderer) maybeRenderer)
.getTableCellRendererComponent(this, null, false,
false, -1, -1);
} catch (Exception e) {
// can't do anything - renderer can't cope with off-range cells
}
}
if (comp != null) {
SwingUtilities.updateComponentTreeUI(comp);
}
}
/**
* Updates TableColumn after updateUI changes. This implementation delegates
* to the column if it is of type UIDependent, takes over to try an update
* of the column's cellEditor, Cell-/HeaderRenderer otherwise.
*
* @param column the tableColumn to update.
*/
protected void updateColumnUI(TableColumn column) {
if (column instanceof UIDependent) {
((UIDependent) column).updateUI();
} else {
updateEditorUI(column.getCellEditor());
updateRendererUI(column.getCellRenderer());
updateRendererUI(column.getHeaderRenderer());
}
}
/**
* Updates highlighter after <code>updateUI</code> changes.
*
* @see org.jdesktop.swingx.decorator.UIDependent
*/
protected void updateHighlighterUI() {
if (compoundHighlighter == null)
return;
compoundHighlighter.updateUI();
}
/**
* Auto-adjusts rowHeight to something more pleasing then the default. This
* method is called after instantiation and after updating the UI. Does
* nothing if the given parameter is <code>true</code> and the rowHeight had
* been already set by client code. The underlying problem is that raw types
* can't implement UIResource.
* <p>
* This implementation asks the UIManager for a default value (stored with
* key "JXTable.rowHeight"). If none is available, calculates a "reasonable"
* height from the table's fontMetrics, assuming that most renderers/editors
* will have a border with top/bottom of 1.
* <p>
*
* @param respectRowSetFlag a boolean to indicate whether client-code flag
* should be respected.
* @see #isXTableRowHeightSet
*/
protected void updateRowHeightUI(boolean respectRowSetFlag) {
if (respectRowSetFlag && isXTableRowHeightSet)
return;
int uiHeight = UIManager.getInt(UIPREFIX + "rowHeight");
if (uiHeight > 0) {
setRowHeight(uiHeight);
} else {
int fontBasedHeight = getFontMetrics(getFont()).getHeight() + 2;
int magicMinimum = 18;
setRowHeight(Math.max(fontBasedHeight, magicMinimum));
}
isXTableRowHeightSet = false;
}
/**
* Convenience to set both grid line visibility and default margin for
* horizontal/vertical lines. The margin defaults to 1 or 0 if the grid
* lines are drawn or not drawn.
* <p>
*
* @param showHorizontalLines boolean to decide whether to draw horizontal
* grid lines.
* @param showVerticalLines boolean to decide whether to draw vertical grid
* lines.
* @see javax.swing.JTable#setShowGrid(boolean)
* @see javax.swing.JTable#setIntercellSpacing(Dimension)
*/
public void setShowGrid(boolean showHorizontalLines,
boolean showVerticalLines) {
int defaultRowMargin = showHorizontalLines ? 1 : 0;
setRowMargin(defaultRowMargin);
setShowHorizontalLines(showHorizontalLines);
int defaultColumnMargin = showVerticalLines ? 1 : 0;
setColumnMargin(defaultColumnMargin);
setShowVerticalLines(showVerticalLines);
}
/**
* {@inheritDoc}
* <p>
* Behaves exactly like super.
* <p>
* It's overridden to warn against a frequent programming error: this method
* toggles only the <b>visibility</b> of the grid lines, it <b>does not</b>
* update the row/column margins - which may lead to visual artefacts, as
* f.i. not showing the lines at all or showing normal table background in
* selected state where the lines should have been.
*
* @see #setShowGrid(boolean, boolean)
*/
@Override
public void setShowGrid(boolean showGrid) {
super.setShowGrid(showGrid);
}
/**
* {@inheritDoc}
* <p>
* Overriden to keep view/model coordinates of SizeSequence in synch. Marks
* the request as client-code induced.
*
* @see #isXTableRowHeightSet
*/
@Override
public void setRowHeight(int rowHeight) {
super.setRowHeight(rowHeight);
if (rowHeight > 0) {
isXTableRowHeightSet = true;
}
updateViewSizeSequence();
}
/**
* {@inheritDoc}
* <p>
* Does nothing if support of individual rowHeights is not enabled.
* Overriden to keep view/model coordinates of SizeSequence in synch.
*
* @see #isRowHeightEnabled()
*/
@Override
public void setRowHeight(int row, int rowHeight) {
if (!isRowHeightEnabled())
return;
super.setRowHeight(row, rowHeight);
updateViewSizeSequence();
resizeAndRepaint();
}
/**
* Sets enablement of individual rowHeight support. Enabling the support
* involves reflective access to super's private field rowModel which may
* fail due to security issues. If failing the support is not enabled.
* <p>
* The default value is <code>false</code>.
*
* @param enabled a boolean to indicate whether per-row heights should be
* enabled.
* @see #isRowHeightEnabled()
* @see #setRowHeight(int, int)
*/
public void setRowHeightEnabled(boolean enabled) {
// PENDING: should we throw an Exception if the enabled fails?
// Or silently fail - depends on runtime context,
// can't do anything about it.
boolean old = isRowHeightEnabled();
if (old == enabled)
return;
if (enabled && !canEnableRowHeight())
return;
rowHeightEnabled = enabled;
if (!enabled) {
adminSetRowHeight(getRowHeight());
}
firePropertyChange("rowHeightEnabled", old, rowHeightEnabled);
}
/**
* Returns a boolean to indicate whether individual row height is enabled.
*
* @return a boolean to indicate whether individual row height support is
* enabled.
* @see #setRowHeightEnabled(boolean)
* @see #setRowHeight(int, int)
*/
public boolean isRowHeightEnabled() {
return rowHeightEnabled;
}
/**
* Returns if it's possible to enable individual row height support.
*
* @return a boolean to indicate whether access of super's private
* <code>rowModel</code> is allowed.
*/
private boolean canEnableRowHeight() {
return getRowModelField() != null;
}
/**
* Returns super's private <code>rowModel</code> which holds the individual
* rowHeights. This method will return <code>null</code> if the access
* failed, f.i. in sandbox restricted applications.
*
* @return super's rowModel field or null if the access was not successful.
*/
private SizeSequence getSuperRowModel() {
try {
Field field = getRowModelField();
if (field != null) {
return (SizeSequence) field.get(this);
}
} catch (SecurityException e) {
LOG.fine("cannot use reflection "
+ " - expected behaviour in sandbox");
} catch (IllegalArgumentException e) {
LOG
.fine("problem while accessing super's private field - private api changed?");
} catch (IllegalAccessException e) {
LOG
.fine("cannot access private field "
+ " - expected behaviour in sandbox. "
+ "Could be program logic running wild in unrestricted contexts");
}
return null;
}
/**
* Sets super's private <code>rowModel</code> which holds the individual
* rowHeights. This method will do nothing if the access failed, f.i. in
* sandbox restricted applications.
*
* @param rowModel the SizeSequence to set super's rowModel to.
*/
private void setSuperRowModel(SizeSequence rowModel) {
try {
Field field = getRowModelField();
if (field != null) {
field.set(this, rowModel);
}
} catch (SecurityException e) {
LOG.fine("cannot use reflection "
+ " - expected behaviour in sandbox");
} catch (IllegalArgumentException e) {
LOG
.fine("problem while accessing super's private field - private api changed?");
} catch (IllegalAccessException e) {
LOG
.fine("cannot access private field "
+ " - expected behaviour in sandbox. "
+ "Could be program logic running wild in unrestricted contexts");
}
}
/**
* Returns super's private field which holds the individual rowHeights. This
* method will return <code>null</code> if the access failed, f.i. in
* sandbox restricted applications.
*
* @return the super's field with access allowed or null if an Exception
* caught while trying to access.
*/
private Field getRowModelField() {
if (rowModelField == null) {
try {
rowModelField = JTable.class.getDeclaredField("rowModel");
rowModelField.setAccessible(true);
} catch (SecurityException e) {
rowModelField = null;
LOG.fine("cannot access JTable private field rowModel "
+ "- expected behaviour in sandbox");
} catch (NoSuchFieldException e) {
LOG.fine("problem while accessing super's private field"
+ " - private api changed?");
}
}
return rowModelField;
}
/**
* Returns the mapper used synch individual rowHeights in view/model
* coordinates.
*
* @return the <code>SizeSequenceMapper</code> used to synch view/model
* coordinates for individual row heights
* @see org.jdesktop.swingx.decorator.SizeSequenceMapper
*/
protected SizeSequenceMapper getRowModelMapper() {
if (rowModelMapper == null) {
rowModelMapper = new SizeSequenceMapper(filters);
}
return rowModelMapper;
}
/**
* Sets the rowHeight for all rows to the given value. Keeps the flag
* <code>isXTableRowHeight</code> unchanged. This enables the distinction
* between setting the height for internal reasons from doing so by client
* code.
*
* @param rowHeight new height in pixel.
* @see #setRowHeight(int)
* @see #isXTableRowHeightSet
*/
protected void adminSetRowHeight(int rowHeight) {
boolean heightSet = isXTableRowHeightSet;
setRowHeight(rowHeight);
isXTableRowHeightSet = heightSet;
}
// ---------------------------- overriding super factory methods and buggy
/**
* {@inheritDoc}
* <p>
* Overridden to work around core Bug (ID #6291631): negative y is mapped to
* row 0).
*
*/
@Override
public int rowAtPoint(Point point) {
if (point.y < 0)
return -1;
return super.rowAtPoint(point);
}
/**
*
* {@inheritDoc}
* <p>
*
* Overridden to return a <code>JXTableHeader</code>.
*
* @see JXTableHeader
*/
@Override
protected JTableHeader createDefaultTableHeader() {
return new JXTableHeader(columnModel);
}
/**
*
* {@inheritDoc}
* <p>
*
* Overridden to return a <code>DefaultTableColumnModelExt</code>.
*
* @see org.jdesktop.swingx.table.DefaultTableColumnModelExt
*/
@Override
protected TableColumnModel createDefaultColumnModel() {
return new DefaultTableColumnModelExt();
}
/**
* {@inheritDoc}
* <p>
* Overridden because super throws NPE on null param.
*/
@Override
public void setSelectionBackground(Color selectionBackground) {
Color old = getSelectionBackground();
this.selectionBackground = selectionBackground;
firePropertyChange("selectionBackground", old, getSelectionBackground());
repaint();
// super.setSelectionBackground(selectionBackground);
}
/**
* {@inheritDoc}
* <p>
* Overridden because super throws NPE on null param.
*/
@Override
public void setSelectionForeground(Color selectionForeground) {
Color old = getSelectionForeground();
this.selectionForeground = selectionForeground;
firePropertyChange("selectionForeground", old, getSelectionForeground());
repaint();
}
/**
* {@inheritDoc}
* <p>
* Overridden because super throws NPE on null param.
*/
@Override
public void setGridColor(Color gridColor) {
Color old = getGridColor();
this.gridColor = gridColor;
firePropertyChange("gridColor", old, getGridColor());
repaint();
}
}