org.jdesktop.swingx.table.ColumnFactory Maven / Gradle / Ivy
Show all versions of swingx-all Show documentation
/*
* $Id: ColumnFactory.java 3554 2009-11-06 09:07:55Z 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.table;
import org.jdesktop.swingx.JXTable;
import javax.swing.table.JTableHeader;
import javax.swing.table.TableCellRenderer;
import javax.swing.table.TableModel;
import java.awt.Component;
/**
* Creates and configures TableColumnExt
s.
*
* TODO JW: explain types of configuration - initial from tableModel, initial
* from table context, user triggered at runtime.
*
*
* JXTable
delegates all TableColumn
creation and
* configuration to a ColumnFactory
. Enhanced column
* configuration should be implemented in a custom factory subclass. The example
* beautifies the column titles to always start with a capital letter:
*
*
*
* MyColumnFactory extends ColumnFactory {
* //@Override
* public void configureTableColumn(TableModel model,
* TableColumnExt columnExt) {
* super.configureTableColumn(model, columnExt);
* String title = columnExt.getTitle();
* title = title.substring(0,1).toUpperCase() + title.substring(1).toLowerCase();
* columnExt.setTitle(title);
* }
* };
*
*
*
* By default a single instance is shared across all tables of an application.
* This instance can be replaced by a custom implementation, preferably "early"
* in the application's lifetime.
*
*
* ColumnFactory.setInstance(new MyColumnFactory());
*
*
* Alternatively, any instance of JXTable
can be configured
* individually with its own ColumnFactory
.
*
*
*
* JXTable table = new JXTable();
* table.setColumnFactory(new MyColumnFactory());
* table.setModel(myTableModel);
*
*
*
*
*
* @author Jeanette Winzenburg
* @author M.Hillary (the pack code)
* @see JXTable#setColumnFactory(ColumnFactory)
*/
public class ColumnFactory {
/**
* the shared instance.
*/
private static ColumnFactory columnFactory;
/**
* the default margin to use in pack.
*/
private int packMargin = 4;
/**
* Returns the shared default factory.
*
* @return the shared instance of ColumnFactory
* @see #setInstance(ColumnFactory)
*/
public static synchronized ColumnFactory getInstance() {
if (columnFactory == null) {
columnFactory = new ColumnFactory();
}
return columnFactory;
}
/**
* Sets the shared default factory. The shared instance is used
* by JXTable
if none has been set individually.
*
* @param factory the default column factory.
* @see #getInstance()
* @see JXTable#getColumnFactory()
*/
public static synchronized void setInstance(ColumnFactory factory) {
columnFactory = factory;
}
/**
* Creates and configures a TableColumnExt. JXTable
calls
* this method for each column in the TableModel
.
*
* @param model the TableModel to read configuration properties from
* @param modelIndex column index in model coordinates
* @return a TableColumnExt to use for the modelIndex
* @throws NullPointerException if model == null
* @throws IllegalStateException if the modelIndex is invalid
* (in coordinate space of the tablemodel)
* @see #createTableColumn(int)
* @see #configureTableColumn(TableModel, TableColumnExt)
* @see JXTable#createDefaultColumnsFromModel()
*/
public TableColumnExt createAndConfigureTableColumn(TableModel model, int modelIndex) {
TableColumnExt column = createTableColumn(modelIndex);
if (column != null) {
configureTableColumn(model, column);
}
return column;
}
/**
* Creates a table column with modelIndex.
*
* The factory's column creation is passed through this method, so
* subclasses can override to return custom column types.
*
* @param modelIndex column index in model coordinates
* @return a TableColumnExt with modelIndex
* @see #createAndConfigureTableColumn(TableModel, int)
*/
public TableColumnExt createTableColumn(int modelIndex) {
return new TableColumnExt(modelIndex);
}
/**
* Configure column properties from TableModel. This implementation
* sets the column's headerValue
property from the
* model's columnName
.
*
*
* The factory's initial column configuration is passed through this method, so
* subclasses can override to customize.
*
*
* @param model the TableModel to read configuration properties from
* @param columnExt the TableColumnExt to configure.
* @throws NullPointerException if model or column == null
* @throws IllegalStateException if column does not have valid modelIndex
* (in coordinate space of the tablemodel)
* @see #createAndConfigureTableColumn(TableModel, int)
*/
public void configureTableColumn(TableModel model, TableColumnExt columnExt) {
if (columnExt.getModelIndex() < 0 || columnExt.getModelIndex() >= model.getColumnCount())
throw new IllegalStateException("column must have valid modelIndex");
columnExt.setHeaderValue(model.getColumnName(columnExt.getModelIndex()));
}
/**
* Configures column initial widths properties from JXTable
.
* This implementation sets the column's
* preferredWidth
with the strategy:
*
* -
* if the column has a prototype, measure the rendering
* component with the prototype as value and use that as
* pref width
*
* -
* if the column has no prototype, use the standard magic pref width (= 75)
*
* -
* try to measure the column's header and use it's preferred width if it exceeds the former.
*
*
*
* TODO JW - rename method to better convey what's happening, maybe
* initializeColumnWidths like the old method in JXTable.
*
* TODO JW - how to handle default settings which are different from
* standard 75?
*
* @param table the context the column will live in.
* @param columnExt the Tablecolumn to configure.
* @see JXTable#getPreferredScrollableViewportSize()
*/
public void configureColumnWidths(JXTable table, TableColumnExt columnExt) {
/*
* PENDING JW: really only called once in a table's lifetime?
* unfortunately: yes - should be called always after structureChanged.
*
*/
// magic value: default in TableColumn
int prefWidth = 75 - table.getColumnMargin();
int prototypeWidth = calcPrototypeWidth(table, columnExt);
if (prototypeWidth > 0) {
prefWidth = prototypeWidth;
}
int headerWidth = calcHeaderWidth(table, columnExt);
prefWidth = Math.max(prefWidth, headerWidth);
prefWidth += table.getColumnModel().getColumnMargin();
columnExt.setPreferredWidth(prefWidth);
}
/**
* Calculates and returns the preferred scrollable viewport
* width of the given table. Subclasses are free to override
* and implement a custom strategy.
*
* This implementation sums the pref widths of the first
* visibleColumnCount contained visible tableColumns. If
* the table contains less columns, the standard preferred
* width per column is added to the result.
*
* @param table the table containing the columns
*/
public int getPreferredScrollableViewportWidth(JXTable table) {
int count;
if (table.getVisibleColumnCount() < 0) {
count = table.getColumnCount();
} else {
count = Math.min(table.getColumnCount(), table.getVisibleColumnCount());
}
int w = 0;
for (int i = 0; i < count; i++) {
// sum up column's pref size, until maximal the
// visibleColumnCount
w += table.getColumn(i).getPreferredWidth();
}
if (count < table.getVisibleColumnCount()) {
w += (table.getVisibleColumnCount() - count) * 75;
}
return w;
}
/**
* Measures and returns the preferred width of the header. Returns -1 if not
* applicable.
*
* @param table the component the renderer lives in
* @param columnExt the TableColumn to configure
* @return the preferred width of the header or -1 if none.
*/
protected int calcHeaderWidth(JXTable table, TableColumnExt columnExt) {
int prototypeWidth = -1;
// now calculate how much room the column header wants
TableCellRenderer renderer = getHeaderRenderer(table, columnExt);
if (renderer != null) {
Component comp = renderer.getTableCellRendererComponent(table, columnExt.getHeaderValue(), false, false, -1, -1);
prototypeWidth = comp.getPreferredSize().width;
}
return prototypeWidth;
}
/**
* Measures and returns the preferred width of the rendering component
* configured with the prototype value, if any. Returns -1 if not
* applicable.
*
* @param table the component the renderer lives in
* @param columnExt the TableColumn to configure
* @return the preferred width of the prototype or -1 if none.
*/
protected int calcPrototypeWidth(JXTable table, TableColumnExt columnExt) {
int prototypeWidth = -1;
Object prototypeValue = columnExt.getPrototypeValue();
if (prototypeValue != null) {
// calculate how much room the prototypeValue requires
TableCellRenderer cellRenderer = getCellRenderer(table, columnExt);
Component comp = cellRenderer.getTableCellRendererComponent(table, prototypeValue, false, false, 0, -1);
prototypeWidth = comp.getPreferredSize().width;
}
return prototypeWidth;
}
/**
* Returns the cell renderer to use for measuring. Delegates to
* JXTable for visible columns, duplicates table logic for hidden
* columns.
*
* @param table the table which provides the renderer
* @param columnExt the TableColumn to configure
* @return returns a cell renderer for measuring.
*/
protected TableCellRenderer getCellRenderer(JXTable table, TableColumnExt columnExt) {
int viewIndex = table.convertColumnIndexToView(columnExt.getModelIndex());
if (viewIndex >= 0) {
// JW: ok to not guard against rowCount < 0?
// technically, the index should be a valid coordinate
return table.getCellRenderer(0, viewIndex);
}
// hidden column - need api on JXTable to access renderer for hidden?
// here we duplicate JXTable api ... maybe by-passing the strategy
// implemented there
TableCellRenderer renderer = columnExt.getCellRenderer();
if (renderer == null) {
renderer = table.getDefaultRenderer(table.getModel().getColumnClass(columnExt.getModelIndex()));
}
return renderer;
}
/**
* Looks up and returns the renderer used for the column's header.
*
* @param table the table which contains the column
* @param columnExt the column to lookup the header renderer for
* @return the renderer for the columns header, may be null.
*/
protected TableCellRenderer getHeaderRenderer(JXTable table, TableColumnExt columnExt) {
TableCellRenderer renderer = columnExt.getHeaderRenderer();
if (renderer == null) {
JTableHeader header = table.getTableHeader();
if (header != null) {
renderer = header.getDefaultRenderer();
}
}
// JW: default to something if null?
// if so, could be table's default object/string header?
return renderer;
}
/**
* Configures the column's preferredWidth
to fit the content.
* It respects the table context, a margin to add and a maximum width. This
* is typically called in response to a user gesture to adjust the column's
* width to the "widest" cell content of a column.
*
*
* This implementation loops through all rows of the given column and
* measures the renderers pref width (it's a potential performance sink).
* Subclasses can override to implement a different strategy.
*
*
* Note: though 2 * margin is added as spacing, this does not imply
* a left/right symmetry - it's up to the table to place the renderer and/or
* the renderer/highlighter to configure a border.
*
* PENDING: support pack for hidden column?
* This implementation can't handle it! For now, added doc and
* fail-fast.
*
* @param table the context the column will live in.
* @param columnExt the column to configure.
* @param margin the extra spacing to add twice, if -1 uses this factories
* default
* @param max an upper limit to preferredWidth, -1 is interpreted as no
* limit
* @throws IllegalStateException if column is not visible
* @see #setDefaultPackMargin(int)
* @see JXTable#packTable(int)
* @see JXTable#packColumn(int, int)
*/
public void packColumn(JXTable table, TableColumnExt columnExt, int margin, int max) {
if (!columnExt.isVisible())
throw new IllegalStateException("column must be visible to pack");
int column = table.convertColumnIndexToView(columnExt.getModelIndex());
int width = 0;
TableCellRenderer headerRenderer = getHeaderRenderer(table, columnExt);
if (headerRenderer != null) {
Component comp = headerRenderer.getTableCellRendererComponent(table, columnExt.getHeaderValue(), false, false, 0, column);
width = comp.getPreferredSize().width;
}
// PENDING JW: slightly inconsistent - the getCellRenderer here
// returns a (guessed) renderer for invisible columns which must not
// be used in the loop. For now that's okay, as we back out early anyway
TableCellRenderer renderer = getCellRenderer(table, columnExt);
for (int r = 0; r < getRowCount(table); r++) {
// JW: fix for #1215-swing as suggested by the reporter adrienclerc
Component comp = table.prepareRenderer(renderer, r, column);
// Component comp = renderer.getTableCellRendererComponent(table, table
// .getValueAt(r, column), false, false, r, column);
width = Math.max(width, comp.getPreferredSize().width);
}
if (margin < 0) {
margin = getDefaultPackMargin();
}
width += 2 * margin;
/* Check if the width exceeds the max */
if (max != -1 && width > max)
width = max;
columnExt.setPreferredWidth(width);
}
/**
* Returns the number of table view rows accessible during row-related
* config. All row-related access is bounded by the value returned from this
* method.
*
* Here: delegates to table.getRowCount().
*
*
* Subclasses can override to reduce the number (for performance) or support
* restrictions due to lazy loading, f.i. Implementors must guarantee that
* view row access with {@code 0 <= row < getRowCount(JXTable)}
* succeeds.
*
* @param table the table to access
* @return valid rowCount
*/
protected int getRowCount(JXTable table) {
return table.getRowCount();
}
// ------------------------ default state
/**
* Returns the default pack margin.
*
* @return the default pack margin to use in packColumn.
* @see #setDefaultPackMargin(int)
*/
public int getDefaultPackMargin() {
return packMargin;
}
/**
* Sets the default pack margin.
*
* Note: this is not really a margin in the sense of symmetrically
* adding white space to the left/right of a cell's content. It's simply an
* amount of space which is added twice to the measured widths in packColumn.
*
* @param margin the default marging to use in packColumn.
* @see #getDefaultPackMargin()
* @see #packColumn(JXTable, TableColumnExt, int, int)
*/
public void setDefaultPackMargin(int margin) {
this.packMargin = margin;
}
}