All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.jdesktop.swingx.JXTable Maven / Gradle / Ivy

There is a newer version: 1.7.2
Show newest version
/*
 * $Id: JXTable.java 4266 2012-12-05 16:34:37Z kleopatra $
 *
 * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle,
 * Santa Clara, California 95054, U.S.A. All rights reserved.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 */

package org.jdesktop.swingx;

import org.jdesktop.beans.JavaBean;
import org.jdesktop.swingx.action.AbstractActionExt;
import org.jdesktop.swingx.action.BoundAction;
import org.jdesktop.swingx.decorator.ComponentAdapter;
import org.jdesktop.swingx.decorator.CompoundHighlighter;
import org.jdesktop.swingx.decorator.Highlighter;
import org.jdesktop.swingx.decorator.ResetDTCRColorHighlighter;
import org.jdesktop.swingx.event.TableColumnModelExtListener;
import org.jdesktop.swingx.hyperlink.HyperlinkAction;
import org.jdesktop.swingx.plaf.LookAndFeelAddons;
import org.jdesktop.swingx.plaf.TableAddon;
import org.jdesktop.swingx.plaf.UIAction;
import org.jdesktop.swingx.plaf.UIDependent;
import org.jdesktop.swingx.plaf.UIManagerExt;
import org.jdesktop.swingx.renderer.AbstractRenderer;
import org.jdesktop.swingx.renderer.CheckBoxProvider;
import org.jdesktop.swingx.renderer.DefaultTableRenderer;
import org.jdesktop.swingx.renderer.HyperlinkProvider;
import org.jdesktop.swingx.renderer.IconValues;
import org.jdesktop.swingx.renderer.MappedValue;
import org.jdesktop.swingx.renderer.StringValue;
import org.jdesktop.swingx.renderer.StringValues;
import org.jdesktop.swingx.rollover.RolloverProducer;
import org.jdesktop.swingx.rollover.TableRolloverController;
import org.jdesktop.swingx.rollover.TableRolloverProducer;
import org.jdesktop.swingx.search.AbstractSearchable;
import org.jdesktop.swingx.search.SearchFactory;
import org.jdesktop.swingx.search.Searchable;
import org.jdesktop.swingx.search.TableSearchable;
import org.jdesktop.swingx.sort.DefaultSortController;
import org.jdesktop.swingx.sort.SortController;
import org.jdesktop.swingx.sort.SortUtils;
import org.jdesktop.swingx.sort.StringValueRegistry;
import org.jdesktop.swingx.sort.TableSortController;
import org.jdesktop.swingx.table.ColumnControlButton;
import org.jdesktop.swingx.table.ColumnFactory;
import org.jdesktop.swingx.table.DefaultTableColumnModelExt;
import org.jdesktop.swingx.table.NumberEditorExt;
import org.jdesktop.swingx.table.TableColumnExt;
import org.jdesktop.swingx.table.TableColumnModelExt;

import javax.swing.*;
import javax.swing.RowSorter.SortKey;
import javax.swing.border.LineBorder;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.RowSorterEvent;
import javax.swing.event.TableColumnModelEvent;
import javax.swing.event.TableModelEvent;
import javax.swing.table.JTableHeader;
import javax.swing.table.TableCellEditor;
import javax.swing.table.TableCellRenderer;
import javax.swing.table.TableColumn;
import javax.swing.table.TableColumnModel;
import javax.swing.table.TableModel;
import java.awt.Color;
import java.awt.Component;
import java.awt.ComponentOrientation;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.KeyboardFocusManager;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Window;
import java.awt.event.ActionEvent;
import java.awt.print.PrinterException;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.lang.reflect.Constructor;
import java.net.URI;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.EventObject;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Vector;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Enhanced Table component with support for general SwingX sorting/filtering,
 * rendering, highlighting, rollover and search functionality. Table specific
 * enhancements include runtime configuration options like toggle column
 * visibility, column sizing, PENDING JW ...
 *
 * 

Sorting and Filtering

*

* JXTable supports sorting and filtering of rows (switched to core sorting). *

* Additionally, it provides api to apply * a specific sort order, to toggle the sort order of columns identified * by view index or column identifier and to reset all sorts. F.i: * *


 * table.setSortOrder("PERSON_ID", SortOrder.DESCENDING);
 * table.toggleSortOder(4);
 * table.resetSortOrder();
 * 
*

* Sorting sequence can be configured per column by setting the TableColumnExt's * comparator property. Sorting can be disabled per column - setting the TableColumnExt's * sortable or per table by {@link #setSortable(boolean)}. * The table takes responsibility to propagate these * properties to the current sorter, if available

*

* Note that the enhanced sorting controls are effective only if the RowSorter is * of type SortController, which it is by default. Different from core JTable, the * autoCreateRowSorter property is enabled by default. If on, the JXTable creates and * uses a default row sorter as returned by the createDefaultRowSorter method. * *

* Typically, a JXTable is sortable by left clicking on column headers. By default, each * subsequent click on a header reverses the order of the sort, and a sort arrow * icon is automatically drawn on the header. * *

* *

Rendering and Highlighting

*

* As all SwingX collection views, a JXTable is a HighlighterClient (PENDING JW: * formally define and implement, like in AbstractTestHighlighter), that is it * provides consistent api to add and remove Highlighters which can visually * decorate the rendering component. * *

* An example multiple highlighting (default striping as appropriate for the * current LookAndFeel, cell foreground on matching pattern, and shading a * column): * *


 *
 * Highlighter simpleStriping = HighlighterFactory.createSimpleStriping();
 * PatternPredicate patternPredicate = new PatternPredicate("ˆM", 1);
 * ColorHighlighter magenta = new ColorHighlighter(patternPredicate, null,
 *       Color.MAGENTA, null, Color.MAGENTA);
 * Highlighter shading = new ShadingColorHighlighter(
 *       new HighlightPredicate.ColumnHighlightPredicate(1));
 *
 * table.setHighlighters(simpleStriping,
 *        magenta,
 *        shading);
 * 
* *

* To fully support, JXTable registers SwingX default table renderers instead of * core defaults (see {@link DefaultTableRenderer}) The recommended approach for * customizing rendered content it to intall a DefaultTableRenderer configured * with a custom String- and/or IconValue. F.i. assuming the cell value is a * File and should be rendered by showing its name followed and date of last * change: * *


 * StringValue sv = new StringValue() {
 *      public String getString(Object value) {
 *        if (!(value instanceof File)) return StringValues.TO_STRING.getString(value);
 *        return StringValues.FILE_NAME.getString(value) + ", "
 *           + StringValues.DATE_TO_STRING.getString(((File) value).lastModified());
 * }};
 * table.setCellRenderer(File.class, new DefaultTableRenderer(sv));
 * 
*

* In addition to super default per-class registration, JXTable registers a default * renderer for URIs which opens the default application to view the related * document as supported by Desktop. Note: this action is triggered only if * rolloverEnabled is true (default value) and the cell is not editable. * *

* Note: DefaultTableCellRenderer and subclasses require a hack to play * nicely with Highlighters because it has an internal "color memory" in * setForeground/setBackground. The hack is applied by default which might lead * to unexpected side-effects in custom renderers subclassing DTCR. See * {@link #resetDefaultTableCellRendererHighlighter} for details. *

* * Note: by default JXTable disables the alternate row striping provided * by Nimbus, instead it does use the color provided by Nimbus to configure the * UIColorHighlighter. Like in any other LAF without striping support, * client code has to explicitly turn on striping by * setting a Highlighter like: * *


 * table.addHighlighter(HighlighterFactory.createSimpleStriping());
 * 
*

* Alternatively, if client code wants to rely on the LAF provided striping * support, it can set a property in the UIManager ("early" in the application * lifetime to prevent JXTable to disable Nimbus handling it. In this case it is * recommended to not any of the ui-dependent Highlighters provided by the * HighlighterFactory. * *


 * UIManager.put("Nimbus.keepAlternateRowColor", Boolean.TRUE);
 * 
* *

Rollover

*

* As all SwingX collection views, a JXTable supports per-cell rollover which is * enabled by default. If enabled, the component fires rollover events on * enter/exit of a cell which by default is promoted to the renderer if it * implements RolloverRenderer, that is simulates live behaviour. The rollover * events can be used by client code as well, f.i. to decorate the rollover row * using a Highlighter. * *


 * JXTable table = new JXTable();
 * table.addHighlighter(new ColorHighlighter(HighlightPredicate.ROLLOVER_ROW,
 *      null, Color.RED);
 * 
* *

Search

*

* As all SwingX collection views, a JXTable is searchable. A search action is * registered in its ActionMap under the key "find". The default behaviour is to * ask the SearchFactory to open a search component on this component. The * default keybinding is retrieved from the SearchFactory, typically ctrl-f (or * cmd-f for Mac). Client code can register custom actions and/or bindings as * appropriate. *

*

* JXTable provides api to vend a renderer-controlled String representation of * cell content. This allows the Searchable and Highlighters to use WYSIWYM * (What-You-See-Is-What-You-Match), that is pattern matching against the actual * string as seen by the user. * *

Column Configuration

*

* JXTable's default column model * is of type TableColumnModelExt which allows management of hidden columns. * Furthermore, it guarantees to delegate creation and configuration of table columns * to its ColumnFactory. The factory is meant as the central place to * customize column configuration. * *

* Columns can be hidden or shown by setting the visible property on the * TableColumnExt using {@link TableColumnExt#setVisible(boolean)}. Columns can * also be shown or hidden from the column control popup. * *

* The column control popup is triggered by an icon drawn to the far right of * the column headers, above the table's scrollbar (when installed in a * JScrollPane). The popup allows the user to select which columns should be * shown or hidden, as well as to pack columns and turn on horizontal scrolling. * To show or hide the column control, use the * {@link #setColumnControlVisible(boolean show)}method. * *

* You can resize all columns, selected columns, or a single column using the * methods like {@link #packAll()}. Packing combines several other aspects of a * JXTable. If horizontal scrolling is enabled using * {@link #setHorizontalScrollEnabled(boolean)}, then the scrollpane will allow * the table to scroll right-left, and columns will be sized to their preferred * size. To control the preferred sizing of a column, you can provide a * prototype value for the column in the TableColumnExt using * {@link TableColumnExt#setPrototypeValue(Object)}. The prototype is used as an * indicator of the preferred size of the column. This can be useful if some * data in a given column is very long, but where the resize algorithm would * normally not pick this up. * *

* * *

* Keys/Actions registered with this component: * *

    *
  • "find" - open an appropriate search widget for searching cell content. * The default action registeres itself with the SearchFactory as search target. *
  • "print" - print the table *
  • {@link JXTable#HORIZONTALSCROLL_ACTION_COMMAND} - toggle the horizontal * scrollbar *
  • {@link JXTable#PACKSELECTED_ACTION_COMMAND} - resize the selected column * to fit the widest cell content *
  • {@link JXTable#PACKALL_ACTION_COMMAND} - resize all columns to fit the * widest cell content in each column * *
* *

* Key bindings. * *

    *
  • "control F" - bound to actionKey "find". *
* *

* Client Properties. * *

    *
  • {@link JXTable#MATCH_HIGHLIGHTER} - set to Boolean.TRUE to use a * SearchHighlighter to mark a cell as matching. *
* * @author Ramesh Gupta * @author Amy Fowler * @author Mark Davidson * @author Jeanette Winzenburg */ @JavaBean public class JXTable extends JTable implements TableColumnModelExtListener { /** * */ public static final String FOCUS_PREVIOUS_COMPONENT = "focusPreviousComponent"; /** * */ public static final String FOCUS_NEXT_COMPONENT = "focusNextComponent"; private static final Logger LOG = Logger.getLogger(JXTable.class.getName()); /** * Identifier of show horizontal scroll action, used in JXTable's * ActionMap. */ public static final String HORIZONTALSCROLL_ACTION_COMMAND = ColumnControlButton.COLUMN_CONTROL_MARKER + "horizontalScroll"; /** * Identifier of pack table action, used in JXTable's ActionMap * . */ public static final String PACKALL_ACTION_COMMAND = ColumnControlButton.COLUMN_CONTROL_MARKER + "packAll"; /** * Identifier of pack selected column action, used in JXTable's * ActionMap. */ public static final String PACKSELECTED_ACTION_COMMAND = ColumnControlButton.COLUMN_CONTROL_MARKER + "packSelected"; /** * The prefix marker to find table related properties in the * ResourceBundle. */ public static final String UIPREFIX = "JXTable."; /** * key for client property to use SearchHighlighter as match marker. */ public static final String MATCH_HIGHLIGHTER = AbstractSearchable.MATCH_HIGHLIGHTER; static { // Hack: make sure the resource bundle is loaded LookAndFeelAddons.getAddon(); LookAndFeelAddons.contribute(new TableAddon()); } /** * The CompoundHighlighter for the table. */ protected CompoundHighlighter compoundHighlighter; /** * The key for the client property deciding about whether the color memory * hack for DefaultTableCellRenderer should be used. * * @see #resetDefaultTableCellRendererHighlighter */ public static final String USE_DTCR_COLORMEMORY_HACK = "useDTCRColorMemoryHack"; /** * The Highlighter used to hack around DefaultTableCellRenderer's color * memory. */ protected Highlighter resetDefaultTableCellRendererHighlighter; /** * The ComponentAdapter for model data access. */ protected ComponentAdapter dataAdapter; /** * Listens for changes from the highlighters. */ private ChangeListener highlighterChangeListener; /** * the factory to use for column creation and configuration. */ private ColumnFactory columnFactory; /** * The default number of visible rows (in a ScrollPane). */ private int visibleRowCount = 20; /** * The default number of visible columns (in a ScrollPane). */ private int visibleColumnCount = -1; /** * Flag to indicate if the column control is visible. */ private boolean columnControlVisible; /** * ScrollPane's original vertical scroll policy. If the column control is * visible the policy is set to ALWAYS. */ private int verticalScrollPolicy; /** * The component used a column control in the upper trailing corner of an * enclosing JScrollPane. */ private JComponent columnControlButton; /** * Mouse/Motion/Listener keeping track of mouse moved in cell coordinates. */ private transient RolloverProducer rolloverProducer; /** * RolloverController: listens to cell over events and repaints * entered/exited rows. */ private transient TableRolloverController linkController; /** * field to store the autoResizeMode while interactively setting horizontal * scrollbar to visible. */ private int oldAutoResizeMode; /** * flag to indicate enhanced auto-resize-off behaviour is on. This is * set/reset in setHorizontalScrollEnabled. */ private boolean intelliMode; /** * internal flag indicating that we are in super.doLayout(). (used in * columnMarginChanged to not update the resizingCol's prefWidth). */ private boolean inLayout; /** * Flag to distinguish internal settings of row height from client code * settings. The rowHeight will be internally adjusted to font size on * instantiation and in updateUI if the height has not been set explicitly * by the application. * * @see #adminSetRowHeight(int) * @see #setRowHeight(int) */ protected boolean isXTableRowHeightSet; /** * property to control search behaviour. */ protected Searchable searchable; /** * property to control table's editability as a whole. */ private boolean editable; private Dimension calculatedPrefScrollableViewportSize; /** * flag to indicate whether the rowSorter is auto-created. */ private boolean autoCreateRowSorter; /** * flag to indicate if table is interactively sortable. */ private boolean sortable; /** * flag to indicate whether model update events should trigger resorts. */ private boolean sortsOnUpdates; /** * flag to indicate that it's unsafe to update sortable-related sorter properties. */ private boolean ignoreAddColumn; /** * Registry of per-cell string representation. */ private transient StringValueRegistry stringValueRegistry; private SortOrder[] sortOrderCycle; /** * Instantiates a JXTable with a default table model, no data. */ public JXTable() { init(); } /** * Instantiates a JXTable with a specific table model. * * @param dm The model to use. */ public JXTable(TableModel dm) { super(dm); init(); } /** * Instantiates a JXTable with a specific table model. * * @param dm The model to use. */ public JXTable(TableModel dm, TableColumnModel cm) { super(dm, cm); init(); } /** * Instantiates a JXTable with a specific table model, column model, and * selection model. * * @param dm The table model to use. * @param cm The column model to use. * @param sm The list selection model to use. */ public JXTable(TableModel dm, TableColumnModel cm, ListSelectionModel sm) { super(dm, cm, sm); init(); } /** * Instantiates a JXTable for a given number of columns and rows. * * @param numRows Count of rows to accommodate. * @param numColumns Count of columns to accommodate. */ public JXTable(int numRows, int numColumns) { super(numRows, numColumns); init(); } /** * Instantiates a JXTable with data in a vector or rows and column names. * * @param rowData Row data, as a Vector of Objects. * @param columnNames Column names, as a Vector of Strings. */ public JXTable(Vector rowData, Vector columnNames) { super(rowData, columnNames); init(); } /** * Instantiates a JXTable with data in a array or rows and column names. * * @param rowData Row data, as a two-dimensional Array of Objects (by row, * for column). * @param columnNames Column names, as a Array of Strings. */ public JXTable(Object[][] rowData, Object[] columnNames) { super(rowData, columnNames); init(); } /** * Initializes the table for use. */ private void init() { putClientProperty(USE_DTCR_COLORMEMORY_HACK, Boolean.TRUE); initDefaultStringValues(); sortOrderCycle = DefaultSortController.getDefaultSortOrderCycle(); setSortsOnUpdates(true); setSortable(true); setAutoCreateRowSorter(true); setRolloverEnabled(true); setEditable(true); setTerminateEditOnFocusLost(true); initActionsAndBindings(); initFocusBindings(); // instantiate row height depending ui setting or font size. updateRowHeightUI(false); // set to null - don't want hard-coded pixel sizes. setPreferredScrollableViewportSize(null); // PENDING: need to duplicate here.. // why doesn't the call in tableChanged work? initializeColumnWidths(); setFillsViewportHeight(true); updateLocaleState(getLocale()); } //--------------- Rollover support /** * Sets the property to enable/disable rollover support. If enabled, this component * fires property changes on per-cell mouse rollover state, i.e. * when the mouse enters/leaves a list cell.

*

* This can be enabled to show "live" rollover behaviour, f.i. the cursor over a cell * rendered by a JXHyperlink.

*

* The default value is true. * * @param rolloverEnabled a boolean indicating whether or not the rollover * functionality should be enabled. * @see #isRolloverEnabled() * @see #getLinkController() * @see #createRolloverProducer() * @see org.jdesktop.swingx.rollover.RolloverRenderer */ public void setRolloverEnabled(boolean rolloverEnabled) { boolean old = isRolloverEnabled(); if (rolloverEnabled == old) return; if (rolloverEnabled) { rolloverProducer = createRolloverProducer(); rolloverProducer.install(this); getLinkController().install(this); } else { rolloverProducer.release(this); rolloverProducer = null; getLinkController().release(); } firePropertyChange("rolloverEnabled", old, isRolloverEnabled()); } /** * Returns a boolean indicating whether or not rollover support is enabled. * * @return a boolean indicating whether or not rollover support is enabled. * @see #setRolloverEnabled(boolean) */ public boolean isRolloverEnabled() { return rolloverProducer != null; } /** * Returns the RolloverController for this component. Lazyly creates the * controller if necessary, that is the return value is guaranteed to be * not null.

*

* PENDING JW: rename to getRolloverController * * @return the RolloverController for this tree, guaranteed to be not null. * @see #setRolloverEnabled(boolean) * @see #createLinkController() * @see org.jdesktop.swingx.rollover.RolloverController */ protected TableRolloverController getLinkController() { if (linkController == null) { linkController = createLinkController(); } return linkController; } /** * Creates and returns a RolloverController appropriate for this component. * * @return a RolloverController appropriate for this component. * @see #getLinkController() * @see org.jdesktop.swingx.rollover.RolloverController */ protected TableRolloverController createLinkController() { return new TableRolloverController<>(); } /** * Creates and returns the RolloverProducer to use with this component. *

* * @return RolloverProducer to use with this component * @see #setRolloverEnabled(boolean) */ protected RolloverProducer createRolloverProducer() { return new TableRolloverProducer(); } /** * Returns the column control visible property. *

* * @return boolean to indicate whether the column control is visible. * @see #setColumnControlVisible(boolean) * @see #setColumnControl(JComponent) */ public boolean isColumnControlVisible() { return columnControlVisible; } /** * Sets the column control visible property. If true and * JXTable is contained in a JScrollPane, the * table adds the column control to the trailing corner of the scroll pane. *

*

* Note: if the table is not inside a JScrollPane the column * control is not shown even if this returns true. In this case it's the * responsibility of the client code to actually show it. *

*

* The default value is false. * * @param visible boolean to indicate if the column control should be shown * @see #isColumnControlVisible() * @see #setColumnControl(JComponent) */ public void setColumnControlVisible(boolean visible) { if (isColumnControlVisible() == visible) return; boolean old = isColumnControlVisible(); if (old) { unconfigureColumnControl(); } this.columnControlVisible = visible; if (isColumnControlVisible()) { configureColumnControl(); } firePropertyChange("columnControlVisible", old, !old); } /** * Returns the component used as column control. Lazily creates the control * to the default if it is null. * * @return component for column control, guaranteed to be != null. * @see #setColumnControl(JComponent) * @see #createDefaultColumnControl() */ public JComponent getColumnControl() { if (columnControlButton == null) { columnControlButton = createDefaultColumnControl(); } return columnControlButton; } /** * Sets the component used as column control. Updates the enclosing * JScrollPane if appropriate. Passing a null * parameter restores the column control to the default. *

* The component is automatically visible only if the * columnControlVisible property is true and the * table is contained in a JScrollPane. * *

* NOTE: from the table's perspective, the column control is simply a * JComponent to add to and keep in the trailing corner of the * scrollpane. (if any). It's up the concrete control to configure itself * from and keep synchronized to the columns' states. *

* * @param columnControl the JComponent to use as columnControl. * @see #getColumnControl() * @see #createDefaultColumnControl() * @see #setColumnControlVisible(boolean) */ public void setColumnControl(JComponent columnControl) { // PENDING JW: release old column control? who's responsible? // Could implement CCB.autoRelease()? JComponent old = columnControlButton; this.columnControlButton = columnControl; configureColumnControl(); firePropertyChange("columnControl", old, getColumnControl()); } /** * Creates the default column control used by this table. This * implementation returns a ColumnControlButton configured with * default ColumnControlIcon. * * @return the default component used as column control. * @see #setColumnControl(JComponent) * @see ColumnControlButton * @see org.jdesktop.swingx.icon.ColumnControlIcon */ protected JComponent createDefaultColumnControl() { return new ColumnControlButton(this); } /** * Sets the language-sensitive orientation that is to be used to order the * elements or text within this component. *

*

* Overridden to work around a core bug: JScrollPane can't cope * with corners when changing component orientation at runtime. This method * explicitly re-configures the column control. *

* * @param o the ComponentOrientation for this table. * @see Component#setComponentOrientation(ComponentOrientation) */ @Override public void setComponentOrientation(ComponentOrientation o) { removeColumnControlFromCorners(); super.setComponentOrientation(o); configureColumnControl(); } /** * Sets upper corners in JScrollPane to null if same as getColumnControl(). * This is a hack around core not coping correctly with component orientation. * * @see #setComponentOrientation(ComponentOrientation) */ protected void removeColumnControlFromCorners() { JScrollPane scrollPane = getEnclosingScrollPane(); if (scrollPane == null || !isColumnControlVisible()) return; removeColumnControlFromCorners(scrollPane, JScrollPane.UPPER_LEFT_CORNER, JScrollPane.UPPER_RIGHT_CORNER); } private void removeColumnControlFromCorners(JScrollPane scrollPane, String... corners) { for (String corner : corners) { if (scrollPane.getCorner(corner) == getColumnControl()) { scrollPane.setCorner(corner, null); } } } /** * Configures the enclosing JScrollPane. *

*

* Overridden to addionally configure the upper trailing corner with the * column control. * * @see #configureColumnControl() */ @Override protected void configureEnclosingScrollPane() { super.configureEnclosingScrollPane(); configureColumnControl(); } /** * Unconfigures the enclosing JScrollPane. *

*

* Overridden to addionally unconfigure the upper trailing corner with the * column control. * * @see #unconfigureColumnControl() */ @Override protected void unconfigureEnclosingScrollPane() { unconfigureColumnControl(); super.unconfigureEnclosingScrollPane(); } /** * /** Unconfigures the upper trailing corner of an enclosing * JScrollPane. *

* Here: removes the upper trailing corner and resets. * * @see #setColumnControlVisible(boolean) * @see #setColumnControl(JComponent) */ protected void unconfigureColumnControl() { JScrollPane scrollPane = getEnclosingScrollPane(); if (scrollPane == null) return; if (verticalScrollPolicy != 0) { // Fix #155-swingx: reset only if we had forced always before // PENDING: JW - doesn't cope with dynamically changing the // policy // shouldn't be much of a problem because doesn't happen too // often?? scrollPane.setVerticalScrollBarPolicy(verticalScrollPolicy); verticalScrollPolicy = 0; } if (isColumnControlVisible()) { scrollPane.setCorner(JScrollPane.UPPER_TRAILING_CORNER, null); } } /** * Configures the upper trailing corner of an enclosing * JScrollPane. *

* Adds the ColumnControl if the * columnControlVisible property is true. *

* * @see #setColumnControlVisible(boolean) * @see #setColumnControl(JComponent) */ protected void configureColumnControl() { if (!isColumnControlVisible()) return; JScrollPane scrollPane = getEnclosingScrollPane(); if (scrollPane == null) return; if (verticalScrollPolicy == 0) { verticalScrollPolicy = scrollPane.getVerticalScrollBarPolicy(); } scrollPane.setCorner(JScrollPane.UPPER_TRAILING_CORNER, getColumnControl()); scrollPane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS); } /** * Returns the enclosing JScrollPane of this table, or null if not * contained in a JScrollPane or not the main view of the scrollPane. * * @return the enclosing JScrollPane if this table is the main view or * null if not. */ protected JScrollPane getEnclosingScrollPane() { Container p = getParent(); if (p instanceof JViewport) { Container gp = p.getParent(); if (gp instanceof JScrollPane) { JScrollPane scrollPane = (JScrollPane) gp; // Make certain we are the viewPort's view and not, for // example, the rowHeaderView of the scrollPane - // an implementor of fixed columns might do this. JViewport viewport = scrollPane.getViewport(); if (viewport == null || viewport.getView() != this) { return null; } return scrollPane; } } return null; } // --------------------- actions /** * Take over ctrl-tab. */ private void initFocusBindings() { setFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS, Collections.emptySet()); setFocusTraversalKeys(KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS, Collections.emptySet()); getInputMap(WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(KeyStroke.getKeyStroke("ctrl TAB"), FOCUS_NEXT_COMPONENT); getInputMap(WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(KeyStroke.getKeyStroke("shift ctrl TAB"), FOCUS_PREVIOUS_COMPONENT); getActionMap().put(FOCUS_NEXT_COMPONENT, createFocusTransferAction(true)); getActionMap().put(FOCUS_PREVIOUS_COMPONENT, createFocusTransferAction(false)); } /** * Creates and returns an action for forward/backward focus transfer, * depending on the given flag. * * @param forward a boolean indicating the direction of the required focus * transfer * @return the action bound to focusTraversal. */ private Action createFocusTransferAction(boolean forward) { BoundAction action = new BoundAction(null, forward ? FOCUS_NEXT_COMPONENT : FOCUS_PREVIOUS_COMPONENT); action.registerCallback(this, forward ? "transferFocus" : "transferFocusBackward"); return action; } /** * A small class which dispatches actions. *

* TODO (?): Is there a way that we can make this static? *

*

* PENDING JW: don't use UIAction ... we are in OO-land! */ private class Actions extends UIAction { Actions(String name) { super(name); } @Override public void actionPerformed(ActionEvent evt) { if ("print".equals(getName())) { try { print(); } catch (PrinterException ex) { // REMIND(aim): should invoke pluggable application error // handler LOG.log(Level.WARNING, "", ex); } } else if ("find".equals(getName())) { doFind(); } } } /** * Registers additional, per-instance Actions to the this * table's ActionMap. Binds the search accelerator (as returned by the * SearchFactory) to the find action. */ private void initActionsAndBindings() { // Register the actions that this class can handle. ActionMap map = getActionMap(); map.put("print", new Actions("print")); map.put("find", new Actions("find")); // hack around core bug: cancel editing doesn't fire // reported against SwingX as of #610-swingx map.put("cancel", createCancelAction()); map.put(PACKALL_ACTION_COMMAND, createPackAllAction()); map.put(PACKSELECTED_ACTION_COMMAND, createPackSelectedAction()); map.put(HORIZONTALSCROLL_ACTION_COMMAND, createHorizontalScrollAction()); KeyStroke findStroke = SearchFactory.getInstance().getSearchAccelerator(); getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(findStroke, "find"); } /** * Creates and returns an Action which cancels an ongoing edit correctly. * Note: the correct thing to do is to call the editor's cancelEditing, the * wrong thing to do is to call table removeEditor (as core JTable does...). * So this is a quick hack around a core bug, reported against SwingX in * #610-swingx. * * @return an Action which cancels an edit. */ private Action createCancelAction() { return new AbstractActionExt() { @Override public void actionPerformed(ActionEvent e) { if (!isEditing()) return; getCellEditor().cancelCellEditing(); } @Override public boolean isEnabled() { return isEditing(); } }; } /** * Creates and returns the default Action for toggling the * horizontal scrollBar. */ private Action createHorizontalScrollAction() { BoundAction action = new BoundAction(null, HORIZONTALSCROLL_ACTION_COMMAND); action.setStateAction(); action.registerCallback(this, "setHorizontalScrollEnabled"); action.setSelected(isHorizontalScrollEnabled()); return action; } /** * Returns a potentially localized value from the UIManager. The given key * is prefixed by this table's UIPREFIX before doing the * lookup. The lookup respects this table's current locale * property. Returns the key, if no value is found. * * @param key the bare key to look up in the UIManager. * @return the value mapped to UIPREFIX + key or key if no value is found. */ protected String getUIString(String key) { return getUIString(key, getLocale()); } /** * Returns a potentially localized value from the UIManager for the given * locale. The given key is prefixed by this table's UIPREFIX * before doing the lookup. Returns the key, if no value is found. * * @param key the bare key to look up in the UIManager. * @param locale the locale use for lookup * @return the value mapped to UIPREFIX + key in the given locale, or key if * no value is found. */ protected String getUIString(String key, Locale locale) { String text = UIManagerExt.getString(UIPREFIX + key, locale); return text != null ? text : key; } /** * Creates and returns the default Action for packing the * selected column. */ private Action createPackSelectedAction() { BoundAction action = new BoundAction(null, PACKSELECTED_ACTION_COMMAND); action.registerCallback(this, "packSelected"); action.setEnabled(getSelectedColumnCount() > 0); return action; } /** * Creates and returns the default Action for packing all columns. */ private Action createPackAllAction() { BoundAction action = new BoundAction(null, PACKALL_ACTION_COMMAND); action.registerCallback(this, "packAll"); return action; } /** * {@inheritDoc} *

* Overridden to update locale-dependent properties. * * @see #updateLocaleState(Locale) */ @Override public void setLocale(Locale locale) { updateLocaleState(locale); super.setLocale(locale); } /** * Updates locale-dependent state to the given Locale. *

* Here: updates registered column actions' locale-dependent state. *

*

* PENDING: Try better to find all column actions including custom * additions? Or move to columnControl? * * @param locale the Locale to use for value lookup * @see #setLocale(Locale) * @see #updateLocaleActionState(String, Locale) */ protected void updateLocaleState(Locale locale) { updateLocaleActionState(HORIZONTALSCROLL_ACTION_COMMAND, locale); updateLocaleActionState(PACKALL_ACTION_COMMAND, locale); updateLocaleActionState(PACKSELECTED_ACTION_COMMAND, locale); } /** * Updates locale-dependent state of action registered with key in * ActionMap. Does nothing if no action with key is found. *

*

* Here: updates the Action's name property. * * @param key the string for lookup in this table's ActionMap * @see #updateLocaleState(Locale) */ protected void updateLocaleActionState(String key, Locale locale) { Action action = getActionMap().get(key); if (action == null) return; action.putValue(Action.NAME, getUIString(key, locale)); } // ------------------ bound action callback methods /** * Resizes all columns to fit their content. *

*

* By default this method is bound to the pack all columns * Action and registered in the table's ActionMap. */ public void packAll() { packTable(-1); } /** * Resizes the lead column to fit its content. *

*

* By default this method is bound to the pack selected column * Action and registered in the table's ActionMap. */ public void packSelected() { int selected = getColumnModel().getSelectionModel().getLeadSelectionIndex(); if (selected >= 0) { packColumn(selected, -1); } } /** * {@inheritDoc} *

*

* Overridden to update the enabled state of the pack selected column * Action. */ @Override public void columnSelectionChanged(ListSelectionEvent e) { super.columnSelectionChanged(e); if (e.getValueIsAdjusting()) return; Action packSelected = getActionMap().get(PACKSELECTED_ACTION_COMMAND); if (packSelected != null) { packSelected.setEnabled(!((ListSelectionModel) e.getSource()).isSelectionEmpty()); } } // ----------------------- scrollable control /** * Sets the enablement of enhanced horizontal scrolling. If enabled, it * toggles an auto-resize mode which always fills the JViewport * horizontally and shows the horizontal scrollbar if necessary. *

*

* The default value is false. *

*

* Note: this is not a bound property, though it follows * bean naming conventions. *

* PENDING: Probably should be... If so, could be taken by a listening * Action as in the app-framework. *

* PENDING JW: the name is mis-leading? * * @param enabled a boolean indicating whether enhanced auto-resize mode is * enabled. * @see #isHorizontalScrollEnabled() */ public void setHorizontalScrollEnabled(boolean enabled) { /* * PENDING JW: add a "real" mode? Problematic because there are several * places in core which check for #AUTO_RESIZE_OFF, can't use different * value without unwanted side-effects. The current solution with * tagging the #AUTO_RESIZE_OFF by a boolean flag #intelliMode is * brittle - need to be very careful to turn off again ... Another * problem is to keep the horizontalScrollEnabled toggling action in * synch with this property. Yet another problem is the change * notification: currently this is _not_ a bound property. */ if (enabled == isHorizontalScrollEnabled()) { return; } boolean old = isHorizontalScrollEnabled(); if (enabled) { // remember the resizeOn mode if any if (getAutoResizeMode() != AUTO_RESIZE_OFF) { oldAutoResizeMode = getAutoResizeMode(); } setAutoResizeMode(AUTO_RESIZE_OFF); // setAutoResizeModel always disables the intelliMode // must set after calling and update the action again intelliMode = true; updateHorizontalAction(); } else { setAutoResizeMode(oldAutoResizeMode); } firePropertyChange("horizontalScrollEnabled", old, isHorizontalScrollEnabled()); } /** * Returns the current setting for horizontal scrolling. * * @return the enablement of enhanced horizontal scrolling. * @see #setHorizontalScrollEnabled(boolean) */ public boolean isHorizontalScrollEnabled() { return intelliMode && getAutoResizeMode() == AUTO_RESIZE_OFF; } /** * {@inheritDoc} *

*

* Overridden for internal bookkeeping related to the enhanced auto-resize * behaviour. *

*

* Note: to enable/disable the enhanced auto-resize mode use exclusively * setHorizontalScrollEnabled, this method can't cope with it. * * @see #setHorizontalScrollEnabled(boolean) */ @Override public void setAutoResizeMode(int mode) { if (mode != AUTO_RESIZE_OFF) { oldAutoResizeMode = mode; } intelliMode = false; super.setAutoResizeMode(mode); updateHorizontalAction(); } /** * Synchs selected state of horizontal scrolling Action to * enablement of enhanced auto-resize behaviour. */ protected void updateHorizontalAction() { Action showHorizontal = getActionMap().get(HORIZONTALSCROLL_ACTION_COMMAND); if (showHorizontal instanceof BoundAction) { ((BoundAction) showHorizontal).setSelected(isHorizontalScrollEnabled()); } } /** * {@inheritDoc} *

*

* Overridden to support enhanced auto-resize behaviour enabled and * necessary. * * @see #setHorizontalScrollEnabled(boolean) */ @Override public boolean getScrollableTracksViewportWidth() { boolean shouldTrack = super.getScrollableTracksViewportWidth(); if (isHorizontalScrollEnabled()) { return hasExcessWidth(); } return shouldTrack; } /** * Layouts column width. The exact behaviour depends on the * autoResizeMode property. *

* Overridden to support enhanced auto-resize behaviour enabled and * necessary. * * @see #setAutoResizeMode(int) * @see #setHorizontalScrollEnabled(boolean) */ @Override public void doLayout() { int resizeMode = getAutoResizeMode(); // fool super... if (isHorizontalScrollEnabled() && hasRealizedParent() && hasExcessWidth()) { autoResizeMode = oldAutoResizeMode; } inLayout = true; super.doLayout(); inLayout = false; autoResizeMode = resizeMode; } /** * @return boolean to indicate whether the table has a realized parent. */ private boolean hasRealizedParent() { return getWidth() > 0 && getParent() != null && getParent().getWidth() > 0; } /** * PRE: hasRealizedParent() * * @return boolean to indicate whether the table has widths excessing * parent's width */ private boolean hasExcessWidth() { return getPreferredSize().width < getParent().getWidth(); } /** * {@inheritDoc} *

*

* Overridden to support enhanced auto-resize behaviour enabled and * necessary. * * @see #setHorizontalScrollEnabled(boolean) */ @Override public void columnMarginChanged(ChangeEvent e) { if (isEditing()) { removeEditor(); } TableColumn resizingColumn = getResizingColumn(); // Need to do this here, before the parent's // layout manager calls getPreferredSize(). if (resizingColumn != null && autoResizeMode == AUTO_RESIZE_OFF && !inLayout) { resizingColumn.setPreferredWidth(resizingColumn.getWidth()); } resizeAndRepaint(); } /** * Returns the column which is interactively resized. The return value is * null if the header is null or has no resizing column. * * @return the resizing column. */ private TableColumn getResizingColumn() { return tableHeader == null ? null : tableHeader.getResizingColumn(); } /** * {@inheritDoc}

*

* Overridden for documentation reasons only: same behaviour but different default value. *

*

* The default value is true. *

*/ @Override public void setFillsViewportHeight(boolean fillsViewportHeight) { if (fillsViewportHeight == getFillsViewportHeight()) return; super.setFillsViewportHeight(fillsViewportHeight); } // ------------------------ override super because of filter-awareness /** * {@inheritDoc}

* Overridden to respect the cell's editability, that is it has no effect if * !isCellEditable(row, column). * * @see #isCellEditable(int, int) */ @Override public void setValueAt(Object aValue, int row, int column) { if (!isCellEditable(row, column)) return; super.setValueAt(aValue, row, column); } /** * Returns true if the cell at row and column is * editable. Otherwise, invoking setValueAt on the cell will * have no effect. *

* Overridden to account for row index mapping and to support a layered * editability control: *

    *
  • per-table: JXTable.isEditable() *
  • per-column: TableColumnExt.isEditable() *
  • per-cell: controlled by the model * TableModel.isCellEditable() *
* The view cell is considered editable only if all three layers are * enabled. * * @param row the row index in view coordinates * @param column the column index in view coordinates * @return true if the cell is editable * @see #setValueAt(Object, int, int) * @see #isEditable() * @see TableColumnExt#isEditable * @see TableModel#isCellEditable */ @Override public boolean isCellEditable(int row, int column) { if (!isEditable()) return false; boolean editable = super.isCellEditable(row, column); if (editable) { TableColumnExt tableColumn = getColumnExt(column); if (tableColumn != null) { editable = tableColumn.isEditable(); } } return editable; } /** * {@inheritDoc} *

*

* Overridden for documentation clarification. The property has the same * meaning as super, that is if true to re-create all table columns on * either setting a new TableModel or receiving a structureChanged from the * existing. The most obvious visual effect is that custom column properties * appear to be "lost". *

*

* JXTable does support additonal custom configuration (via a custom * ColumnFactory) which can (and incorrectly was) called independently from * the creation. Setting this property to false guarantees that no column * configuration is applied. * * @see #tableChanged(TableModelEvent) * @see ColumnFactory */ @Override public boolean getAutoCreateColumnsFromModel() { return super.getAutoCreateColumnsFromModel(); } /** * {@inheritDoc} *

*

* Overridden to update internal state related to enhanced functionality and * hack around core bugs. *

    *
  • re-calculate intialize column width and preferred * scrollable size after a structureChanged if autocreateColumnsFromModel is * true. *
  • update string representation control after structureChanged *
  • core bug #6791934 logic to force revalidate if appropriate *
*

*/ @Override public void tableChanged(TableModelEvent e) { preprocessModelChange(e); super.tableChanged(e); if (isStructureChanged(e) && getAutoCreateColumnsFromModel()) { initializeColumnWidths(); resetCalculatedScrollableSize(true); } if (isStructureChanged(e)) { updateStringValueRegistryColumnClasses(); } postprocessModelChange(e); } //----> start hack around core issue 6791934: // table not updated correctly after updating model // while having a sorter with filter. /** * Overridden to hack around core bug * http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6791934 */ @Override public void sorterChanged(RowSorterEvent e) { super.sorterChanged(e); postprocessSorterChanged(e); } /** * flag to indicate if forced revalidate is needed. */ protected boolean forceRevalidate; /** * flag to indicate if a sortOrderChanged has happened between pre- and postProcessModelChange. */ protected boolean filteredRowCountChanged; /** * Hack around core issue 6791934: sets flags to force revalidate if appropriate. * Called before processing the event. * * @param e the TableModelEvent received from the model */ protected void preprocessModelChange(TableModelEvent e) { forceRevalidate = getSortsOnUpdates() && getRowFilter() != null && isUpdate(e); } /** * Hack around core issue 6791934: forces a revalidate if appropriate and resets * internal flags. * Called after processing the event. * * @param e the TableModelEvent received from the model */ protected void postprocessModelChange(TableModelEvent e) { if (forceRevalidate && filteredRowCountChanged) { resizeAndRepaint(); } filteredRowCountChanged = false; forceRevalidate = false; } /** * Hack around core issue 6791934: sets the sorter changed flag if appropriate. * Called after processing the event. * * @param e the sorter event received from the sorter */ protected void postprocessSorterChanged(RowSorterEvent e) { filteredRowCountChanged = false; if (forceRevalidate && e.getType() == RowSorterEvent.Type.SORTED) { filteredRowCountChanged = e.getPreviousRowCount() != getRowCount(); } } //----> end hack around core issue 6791934: /** * {@inheritDoc}

*

* Overridden to prevent super from creating RowSorter. */ @Override public void setModel(TableModel dataModel) { boolean old = getAutoCreateRowSorter(); try { this.autoCreateRowSorter = false; this.ignoreAddColumn = true; super.setModel(dataModel); } finally { this.autoCreateRowSorter = old; this.ignoreAddColumn = false; } if (getAutoCreateRowSorter()) { setRowSorter(createDefaultRowSorter()); } } /** * {@inheritDoc}

*

* Overridden to synch sorter state from columns. */ @Override public void setColumnModel(TableColumnModel columnModel) { super.setColumnModel(columnModel); configureSorterProperties(); initPerColumnStringValues(); } /** * {@inheritDoc}

*

* Overridden to *

    *
  • fix core bug: replaces sorter even if flag doesn't change. *
  • use xflag (need because super's RowSorter creation is hard-coded. *
*/ @Override public void setAutoCreateRowSorter(boolean autoCreateRowSorter) { if (getAutoCreateRowSorter() == autoCreateRowSorter) return; boolean oldValue = getAutoCreateRowSorter(); this.autoCreateRowSorter = autoCreateRowSorter; if (autoCreateRowSorter) { setRowSorter(createDefaultRowSorter()); } firePropertyChange("autoCreateRowSorter", oldValue, getAutoCreateRowSorter()); } /** * {@inheritDoc}

*

* Overridden to return xflag */ @Override public boolean getAutoCreateRowSorter() { return autoCreateRowSorter; } /** * {@inheritDoc}

*

* Overridden propagate sort-related properties to the sorter after calling super, * if the given RowSorter is of type SortController. Does nothing additional otherwise. */ @Override public void setRowSorter(RowSorter sorter) { super.setRowSorter(sorter); configureSorterProperties(); } /** * Propagates sort-related properties from table/columns to the sorter if it * is of type SortController, does nothing otherwise. */ protected void configureSorterProperties() { // need to hack: if a structureChange is the result of a setModel // the rowsorter is not yet updated if (ignoreAddColumn || !getControlsSorterProperties()) return; getSortController().setStringValueProvider(getStringValueRegistry()); // configure from table properties getSortController().setSortable(sortable); getSortController().setSortsOnUpdates(sortsOnUpdates); getSortController().setSortOrderCycle(getSortOrderCycle()); // configure from column properties List columns = getColumns(true); for (TableColumn tableColumn : columns) { int modelIndex = tableColumn.getModelIndex(); getSortController().setSortable( modelIndex, tableColumn instanceof TableColumnExt ? ((TableColumnExt) tableColumn).isSortable() : true ); getSortController().setComparator( modelIndex, tableColumn instanceof TableColumnExt ? ((TableColumnExt) tableColumn).getComparator() : null ); } } /** * Creates and returns the default RowSorter. Note that this is already * configured to the current TableModel - no api in the base class to set * the model?

*

* PENDING JW: review method signature - better expose the need for the * model by adding a parameter? * * @return the default RowSorter. */ protected RowSorter createDefaultRowSorter() { return new TableSortController<>(getModel()); } /** * Convenience method to detect dataChanged table event type. * * @param e the event to examine. * @return true if the event is of type dataChanged, false else. */ protected boolean isDataChanged(TableModelEvent e) { if (e == null) return false; return e.getType() == TableModelEvent.UPDATE && e.getFirstRow() == 0 && e.getLastRow() == Integer.MAX_VALUE; } /** * Convenience method to detect update table event type. * * @param e the event to examine. * @return true if the event is of type update and not dataChanged, false * else. */ protected boolean isUpdate(TableModelEvent e) { if (isStructureChanged(e)) return false; return e.getType() == TableModelEvent.UPDATE && e.getLastRow() < Integer.MAX_VALUE; } /** * Convenience method to detect a structureChanged table event type. * * @param e the event to examine. * @return true if the event is of type structureChanged or null, false * else. */ protected boolean isStructureChanged(TableModelEvent e) { return e == null || e.getFirstRow() == TableModelEvent.HEADER_ROW; } // -------------------------------- sorting: configure sorter /** * Sets "sortable" property indicating whether or not this table * supports sortable columns. If sortable is true * then sorting will be enabled on all columns whose sortable * property is true. If sortable is * false then sorting will be disabled for all columns, * regardless of each column's individual sorting property. The * default is true.

* * Note: as of post-1.0 this property is propagated to the SortController * if controlsSorterProperties is true. * Whether or not a change triggers a re-sort is up to either the concrete controller * implementation (the default doesn't) or client code. This behaviour is * different from old SwingX style sorting. * * @param sortable boolean indicating whether or not this table supports * sortable columns * @see #getControlsSorterProperties() */ public void setSortable(boolean sortable) { boolean old = isSortable(); this.sortable = sortable; if (getControlsSorterProperties()) { getSortController().setSortable(sortable); } firePropertyChange("sortable", old, isSortable()); } /** * Returns the table's sortable property.

* * @return true if the table is sortable. * @see #setSortable(boolean) */ public boolean isSortable() { return sortable; } /** * If true, specifies that a sort should happen when the underlying * model is updated (rowsUpdated is invoked). For * example, if this is true and the user edits an entry the * location of that item in the view may change. * This property is propagated to the SortController * if controlsSorterProperties is true. *

*

* The default value is true.

* * @param sortsOnUpdates whether or not to sort on update events * @see #getSortsOnUpdates() * @see #getControlsSorterProperties() */ public void setSortsOnUpdates(boolean sortsOnUpdates) { boolean old = getSortsOnUpdates(); this.sortsOnUpdates = sortsOnUpdates; if (getControlsSorterProperties()) { getSortController().setSortsOnUpdates(sortsOnUpdates); } firePropertyChange("sortsOnUpdates", old, getSortsOnUpdates()); } /** * Returns true if a sort should happen when the underlying * model is updated; otherwise, returns false. * * @return whether or not to sort when the model is updated */ public boolean getSortsOnUpdates() { return sortsOnUpdates; } /** * Sets the sortorder cycle used when toggle sorting this table's columns. * This property is propagated to the SortController * if controlsSorterProperties is true. * * @param cycle the sequence of zero or more not-null SortOrders to cycle through. * @throws NullPointerException if the array or any of its elements are null */ public void setSortOrderCycle(SortOrder... cycle) { SortOrder[] old = getSortOrderCycle(); if (getControlsSorterProperties()) { getSortController().setSortOrderCycle(cycle); } this.sortOrderCycle = Arrays.copyOf(cycle, cycle.length); firePropertyChange("sortOrderCycle", old, getSortOrderCycle()); } /** * Returns the sortOrder cycle used when toggle sorting this table's columns, guaranteed * to be not null. * * @return the sort order cycle used in toggle sort, not null */ public SortOrder[] getSortOrderCycle() { return Arrays.copyOf(sortOrderCycle, sortOrderCycle.length); } //------------------------- sorting: sort/filter /** * Sets the filter to the sorter, if available and of type SortController. * Does nothing otherwise. *

* * @param filter the filter used to determine what entries should be * included */ @SuppressWarnings("unchecked") public void setRowFilter(RowFilter filter) { if (hasSortController()) { // all fine, because R is a TableModel (R extends TableModel) SortController controller = (SortController) getSortController(); controller.setRowFilter(filter); } } /** * Returns the filter of the sorter, if available and of type SortController. * Returns null otherwise.

*

* PENDING JW: generics? had to remove return type from getSortController to * make this compilable, so probably wrong. * * @return the filter used in the sorter. */ public RowFilter getRowFilter() { return hasSortController() ? getSortController().getRowFilter() : null; } /** * Resets sorting of all columns. * Delegates to the SortController if available, or does nothing if not.

*

* PENDING JW: method name - consistent in SortController and here. */ public void resetSortOrder() { if (!hasSortController()) return; getSortController().resetSortOrders(); // JW PENDING: think about notification instead of manual repaint. if (getTableHeader() != null) { getTableHeader().repaint(); } } /** * Toggles the sort order of the column at columnIndex. * Delegates to the SortController if available, or does nothing if not.

* *

* The exact behaviour is defined by the SortController's toggleSortOrder * implementation. Typically a unsorted column is sorted in ascending order, * a sorted column's order is reversed. *

*

* PRE: {@code 0 <= columnIndex < getColumnCount()} * * @param columnIndex the columnIndex in view coordinates. */ public void toggleSortOrder(int columnIndex) { if (hasSortController()) { getSortController().toggleSortOrder(convertColumnIndexToModel(columnIndex)); } } /** * Sorts the table by the given column using SortOrder. * Delegates to the SortController if available, or does nothing if not.

*

* PRE: {@code 0 <= columnIndex < getColumnCount()} *

* * @param columnIndex the column index in view coordinates. * @param sortOrder the sort order to use. */ public void setSortOrder(int columnIndex, SortOrder sortOrder) { if (hasSortController()) { getSortController().setSortOrder(convertColumnIndexToModel(columnIndex), sortOrder); } } /** * Returns the SortOrder of the given column. * Delegates to the SortController if available, or returns SortOrder.UNSORTED if not.

* * @param columnIndex the column index in view coordinates. * @return the interactive sorter's SortOrder if matches the column or * SortOrder.UNSORTED */ public SortOrder getSortOrder(int columnIndex) { if (hasSortController()) { return getSortController().getSortOrder(convertColumnIndexToModel(columnIndex)); } return SortOrder.UNSORTED; } /** * Toggles the sort order of the column with identifier. * Delegates to the SortController if available, or does nothing if not.

*

* The exact behaviour of a toggle is defined by the SortController's toggleSortOrder * implementation. Typically a unsorted column is sorted in ascending order, * a sorted column's order is reversed. *

*

* PENDING: JW - define the behaviour if the identifier is not found. This * can happen if either there's no column at all with the identifier or if * there's no column of type TableColumnExt. Currently does nothing, that is * does not change sort state. * * @param identifier the column identifier. */ public void toggleSortOrder(Object identifier) { if (!hasSortController()) return; TableColumn columnExt = getColumnByIdentifier(identifier); if (columnExt == null) return; getSortController().toggleSortOrder(columnExt.getModelIndex()); } /** * Sorts the table by the given column using the SortOrder. * Delegates to the SortController, if available or does nothing if not. *

*

* PENDING: JW - define the behaviour if the identifier is not found. This * can happen if either there's no column at all with the identifier or if * there's no column of type TableColumnExt. Currently does nothing, that is * does not change sort state. * * @param identifier the column's identifier. * @param sortOrder the sort order to use. If null or SortOrder.UNSORTED, * this method has the same effect as resetSortOrder(); */ public void setSortOrder(Object identifier, SortOrder sortOrder) { if (!hasSortController()) return; TableColumn columnExt = getColumnByIdentifier(identifier); if (columnExt == null) return; getSortController().setSortOrder(columnExt.getModelIndex(), sortOrder); } /** * Returns the SortOrder of the given column. * Delegates to the SortController if available, or returns SortOrder.UNSORTED if not.

*

* PENDING: JW - define the behaviour if the identifier is not found. This * can happen if either there's no column at all with the identifier or if * there's no column of type TableColumnExt. Currently returns * SortOrder.UNSORTED. * * @param identifier the column's identifier. * @return the interactive sorter's SortOrder if matches the column or * SortOrder.UNSORTED */ public SortOrder getSortOrder(Object identifier) { if (!hasSortController()) return SortOrder.UNSORTED; TableColumn columnExt = getColumnByIdentifier(identifier); if (columnExt == null) return SortOrder.UNSORTED; int modelIndex = columnExt.getModelIndex(); return getSortController().getSortOrder(modelIndex); } /** * Returns a contained TableColumn with the given identifier. *

* Note that this is a hack around weird columnModel.getColumn(Object) contract in * core TableColumnModel (throws exception if not found). * * @param identifier the column identifier * @return a TableColumn with the identifier if found, or null if not found. */ private TableColumn getColumnByIdentifier(Object identifier) { TableColumn columnExt; try { columnExt = getColumn(identifier); } catch (IllegalArgumentException e) { // hacking around weird getColumn(Object) behaviour - // PENDING JW: revisit and override columnExt = getColumnExt(identifier); } return columnExt; } /** * Returns the currently active SortController. May be null, if the current RowSorter * is not an instance of SortController.

*

* PENDING JW: generics - can't get the * RowFilter getter signature correct with having controller typed here.

*

* PENDING JW: swaying about hiding or not - currently the only way to * make the view not configure a RowSorter of type SortController is to * let this return null. * * @return the currently active SortController may be null */ @SuppressWarnings("unchecked") protected SortController getSortController() { if (hasSortController()) { // JW: the RowSorter is always of type // so the unchecked cast is safe return (SortController) getRowSorter(); } return null; } /** * Returns a boolean indicating whether the table has a SortController. * If true, the call to getSortController is guaranteed to return a not-null * value. * * @return a boolean indicating whether the table has a SortController. * @see #getSortController() */ protected boolean hasSortController() { return getRowSorter() instanceof SortController; } /** * Returns a boolean indicating whether the table configures the sorter's * properties. If true, guaranteed that table's and the columns' sort related * properties are propagated to the sorter. If false, guaranteed to not * touch the sorter's configuration.

*

* This implementation returns true if the sorter is of type SortController. *

* Note: the synchronization is unidirection from the table to the sorter. * Changing the sorter under the table's feet might lead to undefined * behaviour. * * @return a boolean indicating whether the table configurers the sorter's * properties. */ protected boolean getControlsSorterProperties() { return hasSortController() && getAutoCreateRowSorter(); } /** * Returns the primary sort column, or null if nothing sorted or no sortKey * corresponds to a TableColumn currently contained in the TableColumnModel. * * @return the currently interactively sorted TableColumn or null if there * is not sorter active or if the sorted column index does not * correspond to any column in the TableColumnModel. */ public TableColumn getSortedColumn() { // bloody hack: get primary SortKey and // check if there's a column with it available RowSorter controller = getRowSorter(); if (controller != null) { // PENDING JW: must use RowSorter? SortKey sortKey = SortUtils.getFirstSortingKey(controller.getSortKeys()); if (sortKey != null) { int sorterColumn = sortKey.getColumn(); List columns = getColumns(true); for (TableColumn column : columns) { if (column.getModelIndex() == sorterColumn) { return column; } } } } return null; } /** * Returns the view column index of the primary sort column. * * @return the view column index of the primary sort column or -1 if nothing * sorted or the primary sort column not visible. */ public int getSortedColumnIndex() { RowSorter controller = getRowSorter(); if (controller != null) { SortKey sortKey = SortUtils.getFirstSortingKey(controller.getSortKeys()); if (sortKey != null) { return convertColumnIndexToView(sortKey.getColumn()); } } return -1; } /** * {@inheritDoc}

*

* Overridden to propagate sort-related column properties to the SortController and * to update string representation of column.

*

* PENDING JW: check correct update on visibility change!

* PENDING JW: need cleanup of string rep after column removed (if it's a real remove) */ @Override public void columnAdded(TableColumnModelEvent e) { super.columnAdded(e); // PENDING JW: check for visibility event? TableColumn column = getColumn(e.getToIndex()); updateStringValueForColumn(column, column.getCellRenderer()); if (ignoreAddColumn) return; updateSortableAfterColumnChanged(column, column instanceof TableColumnExt ? ((TableColumnExt) column).isSortable() : true); updateComparatorAfterColumnChanged(column, column instanceof TableColumnExt ? ((TableColumnExt) column).getComparator() : null); } // ----------------- enhanced column support: delegation to TableColumnModel /** * Returns the TableColumn at view position * columnIndex. The return value is not null. * *

* NOTE: This delegate method is added to protect developer's from * unexpected exceptions in jdk1.5+. Super does not expose the * TableColumn access by index which may lead to unexpected * IllegalArgumentException: If client code assumes the * delegate method is available, autoboxing will convert the given int to an * Integer which will call the getColumn(Object) method. * * @param viewColumnIndex index of the column with the object in question * @return the TableColumn object that matches the column index * @throws ArrayIndexOutOfBoundsException if viewColumnIndex out of allowed * range. * @see #getColumn(Object) * @see #getColumnExt(int) * @see TableColumnModel#getColumn(int) */ public TableColumn getColumn(int viewColumnIndex) { return getColumnModel().getColumn(viewColumnIndex); } /** * Returns a List of visible TableColumns. * * @return a List of visible columns. * @see #getColumns(boolean) */ public List getColumns() { return Collections.list(getColumnModel().getColumns()); } /** * Returns the margin between columns. *

*

* Convenience to expose column model properties through * JXTable api. * * @return the margin between columns * @see #setColumnMargin(int) * @see TableColumnModel#getColumnMargin() */ public int getColumnMargin() { return getColumnModel().getColumnMargin(); } /** * Sets the margin between columns. *

* Convenience to expose column model properties through * JXTable api. * * @param value margin between columns; must be greater than or equal to * zero. * @see #getColumnMargin() * @see TableColumnModel#setColumnMargin(int) */ public void setColumnMargin(int value) { getColumnModel().setColumnMargin(value); } // ----------------- enhanced column support: delegation to // TableColumnModelExt /** * Returns the number of contained columns. The count includes or excludes * invisible columns, depending on whether the includeHidden is * true or false, respectively. If false, this method returns the same count * as getColumnCount(). If the columnModel is not of type * TableColumnModelExt, the parameter value has no effect. * * @param includeHidden a boolean to indicate whether invisible columns * should be included * @return the number of contained columns, including or excluding the * invisible as specified. * @see #getColumnCount() * @see TableColumnModelExt#getColumnCount(boolean) */ public int getColumnCount(boolean includeHidden) { if (getColumnModel() instanceof TableColumnModelExt) { return ((TableColumnModelExt) getColumnModel()).getColumnCount(includeHidden); } return getColumnCount(); } /** * Returns a List of contained TableColumns. * Includes or excludes invisible columns, depending on whether the * includeHidden is true or false, respectively. If false, an * Iterator over the List is equivalent to the * Enumeration returned by getColumns(). If the * columnModel is not of type TableColumnModelExt, the * parameter value has no effect. *

*

* NOTE: the order of columns in the List depends on whether or not the * invisible columns are included, in the former case it's the insertion * order in the latter it's the current order of the visible columns. * * @param includeHidden a boolean to indicate whether invisible columns * should be included * @return a List of contained columns. * @see #getColumns() * @see TableColumnModelExt#getColumns(boolean) */ public List getColumns(boolean includeHidden) { if (getColumnModel() instanceof TableColumnModelExt) { return ((TableColumnModelExt) getColumnModel()).getColumns(includeHidden); } return getColumns(); } /** * Returns the first TableColumnExt with the given * identifier. The return value is null if there is no * contained column with identifier or if the column with * identifier is not of type TableColumnExt. The * returned column may be visible or hidden. * * @param identifier the object used as column identifier * @return first TableColumnExt with the given identifier or * null if none is found * @see #getColumnExt(int) * @see #getColumn(Object) * @see TableColumnModelExt#getColumnExt(Object) */ public TableColumnExt getColumnExt(Object identifier) { if (getColumnModel() instanceof TableColumnModelExt) { return ((TableColumnModelExt) getColumnModel()).getColumnExt(identifier); } else { // PENDING: not tested! try { TableColumn column = getColumn(identifier); if (column instanceof TableColumnExt) { return (TableColumnExt) column; } } catch (Exception e) { // TODO: handle exception } } return null; } /** * Returns the TableColumnExt at view position * columnIndex. The return value is null, if the column at * position columnIndex is not of type * TableColumnExt. The returned column is visible. * * @param viewColumnIndex the index of the column desired * @return the TableColumnExt object that matches the column * index * @throws ArrayIndexOutOfBoundsException if columnIndex out of allowed * range, that is if * {@code (columnIndex < 0) || (columnIndex >= getColumnCount())} * @see #getColumnExt(Object) * @see #getColumn(int) * @see TableColumnModelExt#getColumnExt(int) */ public TableColumnExt getColumnExt(int viewColumnIndex) { TableColumn column = getColumn(viewColumnIndex); if (column instanceof TableColumnExt) { return (TableColumnExt) column; } return null; } // ---------------------- enhanced TableColumn/Model support: convenience /** * Reorders the columns in the sequence given array. Logical names that do * not correspond to any column in the model will be ignored. Columns with * logical names not contained are added at the end. *

* PENDING JW - do we want this? It's used by JNTable. * * @param identifiers array of logical column names * @see #getColumns(boolean) */ public void setColumnSequence(Object[] identifiers) { /* * JW: not properly tested (not in all in fact) ... */ List columns = getColumns(true); Map map = new HashMap<>(); for (TableColumn column : columns) { // PENDING: handle duplicate identifiers ... map.put(column.getIdentifier(), column); getColumnModel().removeColumn(column); } for (Object identifier : identifiers) { TableColumn column = map.get(identifier); if (column != null) { getColumnModel().addColumn(column); columns.remove(column); } } for (TableColumn column : columns) { getColumnModel().addColumn(column); } } // --------------- implement TableColumnModelExtListener /** * {@inheritDoc} *

* Listens to column property changes. */ @Override public void columnPropertyChange(PropertyChangeEvent event) { if ("editable".equals(event.getPropertyName())) { updateEditingAfterColumnChanged((TableColumn) event.getSource(), ((Boolean) event.getNewValue()).booleanValue()); } else if ("sortable".equals(event.getPropertyName())) { updateSortableAfterColumnChanged((TableColumn) event.getSource(), ((Boolean) event.getNewValue()).booleanValue()); } else if ("comparator".equals(event.getPropertyName())) { updateComparatorAfterColumnChanged((TableColumn) event.getSource(), (Comparator) event.getNewValue()); } else if ("cellRenderer".equals(event.getPropertyName())) { updateStringValueForColumn((TableColumn) event.getSource(), (TableCellRenderer) 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 false, otherwise does nothing. * * @param column the TableColumn 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(); } /** * Synch's the SortController column sortable property to the new value, if * controlsSorterProperties. Does nothing otherwise. This method is * called on sortable property change notification from the ext column model.

* * @param column the TableColumn which sent the change * notifcation * @param sortable the new value of the column's sortable property */ private void updateSortableAfterColumnChanged(TableColumn column, boolean sortable) { if (getControlsSorterProperties()) { getSortController().setSortable(column.getModelIndex(), sortable); } } /** * Synch's the SortController column comparator property to the new value, if * controlsSorterProperties. Does nothing otherwise. This method is * called on comparator property change notification from the ext column model.

* * @param column the TableColumn which sent the change * notifcation * @param comparator the new value of the column's sortable property */ private void updateComparatorAfterColumnChanged(TableColumn column, Comparator comparator) { if (getControlsSorterProperties()) { getSortController().setComparator(column.getModelIndex(), comparator); } } // -------------------------- ColumnFactory /** * Creates, configures and adds default TableColumns for * columns in this table's TableModel. Removes all currently * contained TableColumns. The exact type and configuration of * the columns is controlled completely by the ColumnFactory. * Client code can use {@link #setColumnFactory(ColumnFactory)} to plug-in a * custom ColumnFactory implementing their own default column creation and * behaviour. *

* * Note: 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 ColumnFactory */ @Override public final void createDefaultColumnsFromModel() { // JW: when could this happen? if (getModel() == null) return; // Remove any current columns removeColumns(); createAndAddColumns(); } /** * Creates and adds TableColumns for each column of the table * model. *

*/ 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. *

*/ private void removeColumns() { /* * TODO: promote this method to superclass, and change * createDefaultColumnsFromModel() to call this method */ List columns = getColumns(true); for (TableColumn column : columns) { getColumnModel().removeColumn(column); } } /** * Returns the ColumnFactory. *

* * @return the columnFactory to use for column creation and configuration, * guaranteed to not be null. * @see #setColumnFactory(ColumnFactory) * @see 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(); } return columnFactory; } /** * Sets the ColumnFactory to use for column creation and * configuration. The default value is the shared application ColumnFactory. *

*

* Note: this method has no side-effect, that is existing columns are * not re-created automatically, client code must trigger it * manually. * * @param columnFactory the factory to use, null indicates to * use the shared application factory. * @see #getColumnFactory() * @see 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 * JScrollPane. * * @return the number of rows to show in a JScrollPane * @see #setVisibleRowCount(int) */ public int getVisibleRowCount() { return visibleRowCount; } /** * Sets the preferred number of rows to show in a JScrollPane. *

*

* This is a bound property. The default value is 20. *

*

* PENDING: allow negative for use-all? Analogous to visColumnCount. * * @param visibleRowCount number of rows to show in a * JScrollPane * @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 * JScrollPane. * * @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 * JScrollPane. A negative number is interpreted as use-all * available visible columns. *

*

* 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 * JScrollPane * @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} *

*

* 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} *

* 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 *

*

* 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 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. *

*

    *
  • PENDING: move into ColumnFactory? *
  • PENDING: what to do if autoCreateColumn off? *
  • 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(...)) *
* * @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 ColumnFactory if the * column is of type TableColumnExt. Otherwise nothing is * changed. *

*

* TODO JW - need to cleanup getScrollablePreferred (refactor and inline) * * @param column TableColumn object representing view column * @see 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 JViewport. *

*

* Note: this method has no precondition as it internally uses * getCellRect 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 JViewport. *

*

* Note: this method has no precondition as it internally uses * getCellRect 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 JViewport. *

*

* Note: this method has no precondition as it internally uses * getCellRect 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. *

* 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 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 TableDataAdapter 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 JTable} */ public JXTable getTable() { return table; } //----------------- column meta data /** * {@inheritDoc} */ @Override public String getColumnName(int columnIndex) { TableColumn column = getColumnByModelIndex(columnIndex); return column == null ? "" : column.getHeaderValue().toString(); } /** * Returns the first contained TableColumn with the given model index, or * null if none is found. * * @param modelColumn the column index in model coordinates, must be valid * @return the first contained TableColumn with the given model index, or * null if none is found * @throws IllegalArgumentException if model index invalid */ protected TableColumn getColumnByModelIndex(int modelColumn) { if (modelColumn < 0 || modelColumn >= getColumnCount()) { throw new IllegalArgumentException( "invalid column index, must be positive and less than " + getColumnCount() + " was: " + modelColumn ); } List columns = table.getColumns(true); for (TableColumn column : columns) { if (column.getModelIndex() == modelColumn) { return column; } } return null; } /** * {@inheritDoc} */ @Override public Object getColumnIdentifierAt(int columnIndex) { if (columnIndex < 0 || columnIndex >= getColumnCount()) { throw new ArrayIndexOutOfBoundsException("invalid column index: " + columnIndex); } TableColumn column = getColumnByModelIndex(columnIndex); return column != null ? column.getIdentifier() : null; } /** * {@inheritDoc} */ @Override public int getColumnIndex(Object identifier) { TableColumn column = table.getColumnExt(identifier); return column != null ? column.getModelIndex() : -1; } /** * {@inheritDoc} */ @Override public Class getColumnClass(int column) { return table.getModel().getColumnClass(column); } //--------------------- model meta data /** * {@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 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} *

* This is implemented to query the table's StringValueRegistry for an appropriate * StringValue and use that for getting the string representation. */ @Override public String getStringAt(int row, int column) { StringValue sv = table.getStringValueRegistry().getStringValue(row, column); return sv.getString(getValueAt(row, column)); } /** * {@inheritDoc} */ @Override public Rectangle getCellBounds() { return table.getCellRect(row, column, false); } /** * {@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 convertColumnIndexToView(int columnIndex) { return table.convertColumnIndexToView(columnIndex); } /** * {@inheritDoc} */ @Override public int convertColumnIndexToModel(int columnIndex) { return table.convertColumnIndexToModel(columnIndex); } /** * {@inheritDoc} */ @Override public int convertRowIndexToView(int rowModelIndex) { return table.convertRowIndexToView(rowModelIndex); } /** * {@inheritDoc} */ @Override public int convertRowIndexToModel(int rowViewIndex) { return table.convertRowIndexToModel(rowViewIndex); } } // --------------------- managing renderers/editors /** * Sets the Highlighters to the table, replacing any old * settings. None of the given Highlighters must be null. *

*

* This is a bound property. *

*

* 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 Highlighters 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 Highlighter to the end of the list of used * Highlighters. The argument must not be null. *

* * @param highlighter the Highlighter to add, must not be null. * @throws NullPointerException if Highlighter 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. *

*

* 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 ChangeListener to use with highlighters. Lazily * creates the listener. * * @return the ChangeListener for observing changes of highlighters, * guaranteed to be not-null */ protected ChangeListener getHighlighterChangeListener() { if (highlighterChangeListener == null) { highlighterChangeListener = createHighlighterChangeListener(); } return highlighterChangeListener; } /** * Creates and returns the ChangeListener observing Highlighters. *

* Here: repaints the table on receiving a stateChanged. * * @return the ChangeListener defining the reaction to changes of * highlighters. */ protected ChangeListener createHighlighterChangeListener() { return e -> repaint(); } /** * Returns the StringValueRegistry which defines the string representation for * each cells. This is strictly for internal use by the table, which has the * responsibility to keep in synch with registered renderers.

*

* Currently exposed for testing reasons, client code is recommended to not use nor override. * * @return the current string value registry */ protected StringValueRegistry getStringValueRegistry() { if (stringValueRegistry == null) { stringValueRegistry = createDefaultStringValueRegistry(); } return stringValueRegistry; } /** * Creates and returns the default registry for StringValues.

* * @return the default registry for StringValues. */ protected StringValueRegistry createDefaultStringValueRegistry() { return new StringValueRegistry(); } /** * Updates per-column class in StringValueRegistry. This is called after * structureChanged. */ private void updateStringValueRegistryColumnClasses() { getStringValueRegistry().setColumnClasses(null); for (int i = 0; i < getModel().getColumnCount(); i++) { getStringValueRegistry().setColumnClass(getModel().getColumnClass(i), i); } } /** * Updates per-column StringValue in StringValueRegistry based on given tableColumn. * This is called after the column's renderer property changed or after the column * is added. * * @param tableColumn the column to update from * @param renderer the renderer potentially useful as StringValue. */ private void updateStringValueForColumn(TableColumn tableColumn, TableCellRenderer renderer) { getStringValueRegistry().setStringValue( renderer instanceof StringValue ? (StringValue) renderer : null, tableColumn.getModelIndex() ); } /** * Called in init to synch the StringValueProvider with default renderers per class */ private void initDefaultStringValues() { for (Object clazz : defaultRenderersByColumnClass.keySet()) { Object renderer = defaultRenderersByColumnClass.get(clazz); if (renderer instanceof StringValue) { getStringValueRegistry().setStringValue((StringValue) renderer, (Class) clazz); } } } /** * Inits per column string values from TableColumns */ private void initPerColumnStringValues() { getStringValueRegistry().clearColumnStringValues(); for (TableColumn tableColumn : getColumns(true)) { updateStringValueForColumn(tableColumn, tableColumn.getCellRenderer()); } } /** * {@inheritDoc}

*

* Overridden to synchronize the string representation. If the renderer is of type * StringValue a mapping it will be used as converter for the class type. If not, * the mapping is reset to default. */ @Override public void setDefaultRenderer(Class columnClass, TableCellRenderer renderer) { super.setDefaultRenderer(columnClass, renderer); getStringValueRegistry().setStringValue(renderer instanceof StringValue ? (StringValue) renderer : null, columnClass); } /** * 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) { // changed implementation to use StringValueRegistry StringValue stringValue = getStringValueRegistry().getStringValue(convertRowIndexToModel(row), convertColumnIndexToModel(column)); return stringValue.getString(getValueAt(row, column)); } /** * {@inheritDoc} *

*

* Overridden to fix core bug #4614616 (NPE if TableModel's * Class for the column is an interface). This method * guarantees to always return a not null value. Returns the * default renderer for Object if super returns * null.

* * Note: The lookup strategy for is unchanged compared to super. Subclasses * which override this with a different lookup strategy are strongly advised to * implement a custom StringValueRegistry with that lookup strategy to keep * the WYSIWYM in SwingX. */ @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 Component used as a stamp to render * the specified cell. Overrides superclass version to provide support for * cell decorators. *

*

* Adjusts component orientation (guaranteed to happen before applying * Highlighters). *

*

* Per-column highlighters contained in * {@link TableColumnExt#getHighlighters()} are applied to the renderer * after the table highlighters. *

*

* TODO kgs: interaction of search highlighter and column highlighters *

*

* 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 * resetDefaultTableCellRendererColors which is called after * super.prepareRenderer and before applying the Highlighters. The method is * called always and for all renderers. * * @param renderer the TableCellRenderer 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 Component used as a stamp to render * the specified cell * @see #resetDefaultTableCellRendererColors(Component, int, int) * @see 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); } } return stamp; } /** * Convenience method to get the rendering component for the given cell. * * @param row the row of the cell to render, where 0 is the first row * @param col the column of the cell to render, where 0 is the first * column * @return the decorated Component used as a stamp to render * the specified cell */ public Component prepareRenderer(int row, int col) { return prepareRenderer(getCellRenderer(row, col), row, col); } /** * Method to apply a hack around DefaultTableCellRenderer "color memory" * (Issue #258-swingx). Applies the hack if the client property * USE_DTCR_COLORMEMORY_HACK having the value of * Boolean.TRUE, does nothing otherwise. The property is true * by default. *

*

* The hack consists of applying a specialized Highlighter to * force reset the color "memory" of DefaultTableCellRenderer. * Note that the hack is applied always, that is even if there are no custom * Highlighters. *

*

* Client code which solves the problem at the core (that is in a * well-behaved DefaultTableCellRenderer) can disable the hack * by removing the client property or by subclassing and override this to do * nothing. * * @param renderer the TableCellRenderer 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 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} *

*

* 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 Component's orientation to this * JXTable's CO if appropriate. The parameter must not be * null. *

*

* This implementation synchs the CO always. * * @param stamp the Component who's CO may need to be synched, * must not be null. */ protected void adjustComponentOrientation(Component stamp) { if (stamp.getComponentOrientation().equals(getComponentOrientation())) return; stamp.applyComponentOrientation(getComponentOrientation()); } /** * Creates default cell renderers for Objects, * Numbers, Dates, Booleans, * Icon/Image/s and URIs. *

* Overridden to replace all super default renderers with SwingX variants and * additionally register a default for URI types. Note: the latter * registration will fail silently in headless environments or when the runtime * context doesn't support Desktop. * *

* {@inheritDoc} * * @see DefaultTableRenderer * @see org.jdesktop.swingx.renderer.ComponentProvider */ @Override protected void createDefaultRenderers() { defaultRenderersByColumnClass = new UIDefaults(8, 0.75f); // 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())); try { setDefaultRenderer(URI.class, new DefaultTableRenderer(new HyperlinkProvider(new HyperlinkAction()))); } catch (Exception e) { // nothing to do - either headless or Desktop not supported } } /** * Creates default cell editors for objects, numbers, and boolean values. *

* Overridden to hook enhanced editors (f.i. NumberEditorExt * * @see DefaultCellEditor */ @SuppressWarnings("unchecked") @Override protected void createDefaultEditors() { defaultEditorsByColumnClass = new UIDefaults(3, 0.75f); defaultEditorsByColumnClass.put(Object.class, new GenericEditor()); // Numbers // JW: fix for // Issue #1183-swingx: NumberEditorExt throws in getCellEditorValue if // Integer (short, byte..) below/above min/max. // Issue #1236-swingx: NumberEditorExt cannot be used in columns with Object type defaultEditorsByColumnClass.put(Number.class, new NumberEditorExt(true)); // Booleans defaultEditorsByColumnClass.put(Boolean.class, new BooleanEditor()); } /** * Default editor registered for Object. 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 String * parameter. *

*

* The editor can be configured with a custom JTextField. */ public static class GenericEditor extends DefaultCellEditor { private Constructor constructor; private 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(); // JW: changed logic to hack around (core!) Issue #1535 // don't special case empty, but string contructor: // if so, by-pass relection altogether if (constructor.getDeclaringClass() == String.class) { value = s; } else { // try instantiating a new Object with the string try { value = constructor.newInstance(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(String.class); } catch (Exception e) { return null; } return super.getTableCellEditorComponent(table, value, isSelected, row, column); } @Override public Object getCellEditorValue() { return value; } } /** * Editor for Numbers. *

* Note: this is no longer registered by default. The current default is * NumberEditorExt 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 Boolean types. */ public static class BooleanEditor extends DefaultCellEditor { public BooleanEditor() { super(new JCheckBox()); JCheckBox checkBox = (JCheckBox) getComponent(); checkBox.setHorizontalAlignment(JCheckBox.CENTER); } @Override public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) { // fix #1521-swingx: consistent selection behaviour return super.getTableCellEditorComponent(table, value, isSelected || shouldSelectCell(null), row, column); } } // ----------------------------- enhanced editing support /** * Returns the editable property of the JXTable 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 TableColumnExt.isEditable and their per-cell * editability as returned by the TableModel.isCellEditable. If * a cell is read-only in its column or model layer, this property has no * effect. *

*

* The default value is true. * * @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 JTable.CellEditorRemover: "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. *

* The default value is true. * * @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. *

* The default value is true. *

* * @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} *

*

* 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() { 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); } 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} *

*

* Overridden to uninstall the custom editor remover. */ @Override public void removeNotify() { if (editorRemover != null) { editorRemover.uninstall(); editorRemover = null; } super.removeNotify(); } /** * {@inheritDoc} *

*

* 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} *

* 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} *

* 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. */ private class CellEditorRemover implements PropertyChangeListener { private KeyboardFocusManager focusManager; 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; } @Override 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) { if (c == SwingUtilities.getRoot(JXTable.this)) { if (!getCellEditor().stopCellEditing()) { getCellEditor().cancelCellEditing(); } } break; } c = c.getParent(); } } } } // ---------------------------- updateUI support /** * {@inheritDoc} *

* Additionally updates auto-adjusted row height and highlighters. *

* Another of the override motivation is to fix core issue (?? ID): super * fails to update all renderers/editors. */ @Override public void updateUI() { super.updateUI(); updateColumnControlUI(); for (Object o : defaultEditorsByColumnClass.values()) { updateEditorUI(o); } for (Object o : defaultRenderersByColumnClass.values()) { updateRendererUI(o); } 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 updateUI of the potential * TableCellEditor. * * @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 updateUI of the potential * TableCellRenderer. * * @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 updateUI changes. * * @see 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 true and the rowHeight had * been already set by client code. The underlying problem is that raw types * can't implement UIResource. *

* 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. *

* * @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. *

* * @param showHorizontalLines boolean to decide whether to draw horizontal * grid lines. * @param showVerticalLines boolean to decide whether to draw vertical grid * lines. * @see JTable#setShowGrid(boolean) * @see 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} *

* Behaves exactly like super. *

* It's overridden to warn against a frequent programming error: this method * toggles only the visibility of the grid lines, it does not * 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} *

* Overriden to mark the request as client-code induced. * * @see #isXTableRowHeightSet */ @Override public void setRowHeight(int rowHeight) { super.setRowHeight(rowHeight); if (rowHeight > 0) { isXTableRowHeightSet = true; } } /** * Sets the rowHeight for all rows to the given value. Keeps the flag * isXTableRowHeight 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} *

* 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} *

*

* Overridden to return a JXTableHeader. * * @see JXTableHeader */ @Override protected JTableHeader createDefaultTableHeader() { return new JXTableHeader(columnModel); } /** * {@inheritDoc} *

*

* Overridden to return a DefaultTableColumnModelExt. * * @see DefaultTableColumnModelExt */ @Override protected TableColumnModel createDefaultColumnModel() { return new DefaultTableColumnModelExt(); } /** * {@inheritDoc} *

* 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(); } /** * {@inheritDoc} *

* 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} *

* 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(); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy