/* * $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.hdesktop.swingx.table; import java.awt.Component; import javax.swing.table.JTableHeader; import javax.swing.table.TableCellRenderer; import javax.swing.table.TableModel; import org.hdesktop.swingx.JXTable; /** * Creates and configures <code>TableColumnExt</code>s. * <p> * TODO JW: explain types of configuration - initial from tableModel, initial * from table context, user triggered at runtime. * <p> * * <code>JXTable</code> delegates all <code>TableColumn</code> creation and * configuration to a <code>ColumnFactory</code>. Enhanced column * configuration should be implemented in a custom factory subclass. The example * beautifies the column titles to always start with a capital letter: * * <pre> * <code> * 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); * } * }; * </code> * </pre> * * 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. * * <pre><code> * ColumnFactory.setInstance(new MyColumnFactory()); * </code></pre> * * Alternatively, any instance of <code>JXTable</code> can be configured * individually with its own <code>ColumnFactory</code>. * * <pre> * <code> * JXTable table = new JXTable(); * table.setColumnFactory(new MyColumnFactory()); * table.setModel(myTableModel); * </code> * </pre> * * <p> * * @see org.hdesktop.swingx.JXTable#setColumnFactory(ColumnFactory) * * @author Jeanette Winzenburg * @author M.Hillary (the pack code) */ 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 <code>ColumnFactory</code> * @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 <code>JXTable</code> if none has been set individually. * * @param factory the default column factory. * @see #getInstance() * @see org.hdesktop.swingx.JXTable#getColumnFactory() */ public static synchronized void setInstance(ColumnFactory factory) { columnFactory = factory; } /** * Creates and configures a TableColumnExt. <code>JXTable</code> calls * this method for each column in the <code>TableModel</code>. * * @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 NPE if model == null * @throws IllegalStateException if the modelIndex is invalid * (in coordinate space of the tablemodel) * * @see #createTableColumn(int) * @see #configureTableColumn(TableModel, TableColumnExt) * @see org.hdesktop.swingx.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. * <p> * 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 <code>modelIndex</code> * * @see #createAndConfigureTableColumn(TableModel, int) * */ public TableColumnExt createTableColumn(int modelIndex) { return new TableColumnExt(modelIndex); } /** * Configure column properties from TableModel. This implementation * sets the column's <code>headerValue</code> property from the * model's <code>columnName</code>. * <p> * * The factory's initial column configuration is passed through this method, so * subclasses can override to customize. * <p> * * @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 <code>JXTable</code>. * This implementation sets the column's * <code>preferredWidth</code> with the strategy: * <ol> if the column has a prototype, measure the rendering * component with the prototype as value and use that as * pref width * <ol> if the column has no prototype, use the standard magic * pref width (= 75) * <ol> try to measure the column's header and use it's preferred * width if it exceeds the former. * </ol> * * TODO JW - rename method to better convey what's happening, maybe * initializeColumnWidths like the old method in JXTable. <p> * * 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 org.hdesktop.swingx.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.<p> * * 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 w = 0; int count; if (table.getVisibleColumnCount() < 0) { count = table.getColumnCount(); } else { count = Math.min(table.getColumnCount(), table.getVisibleColumnCount()); } 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. <p> * * @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.<p> * * @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 <code>preferredWidth</code> 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. * <p> * * 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. * <p> * * Note: though 2 * margin is added as spacing, this does <b>not</b> imply * a left/right symmetry - it's up to the table to place the renderer and/or * the renderer/highlighter to configure a border.<p> * * 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 org.hdesktop.swingx.JXTable#packTable(int) * @see org.hdesktop.swingx.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(). * <p> * * 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)</code> * 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. <p> * * Note: this is <b>not</b> 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; } }