001/*
002 * $Id: ColumnFactory.java 3554 2009-11-06 09:07:55Z kleopatra $
003 *
004 * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle,
005 * Santa Clara, California 95054, U.S.A. All rights reserved.
006 *
007 * This library is free software; you can redistribute it and/or
008 * modify it under the terms of the GNU Lesser General Public
009 * License as published by the Free Software Foundation; either
010 * version 2.1 of the License, or (at your option) any later version.
011 * 
012 * This library is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
015 * Lesser General Public License for more details.
016 * 
017 * You should have received a copy of the GNU Lesser General Public
018 * License along with this library; if not, write to the Free Software
019 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
020 */
021package org.jdesktop.swingx.table;
022
023import java.awt.Component;
024
025import javax.swing.table.JTableHeader;
026import javax.swing.table.TableCellRenderer;
027import javax.swing.table.TableModel;
028
029import org.jdesktop.swingx.JXTable;
030
031/**
032 * Creates and configures <code>TableColumnExt</code>s.
033 * <p>
034 * TODO JW: explain types of configuration - initial from tableModel, initial
035 * from table context, user triggered at runtime.
036 * <p>
037 * 
038 * <code>JXTable</code> delegates all <code>TableColumn</code> creation and
039 * configuration to a <code>ColumnFactory</code>. Enhanced column
040 * configuration should be implemented in a custom factory subclass. The example
041 * beautifies the column titles to always start with a capital letter:
042 * 
043 * <pre>
044 * <code>
045 *    MyColumnFactory extends ColumnFactory {
046 *        //@Override
047 *        public void configureTableColumn(TableModel model, 
048 *            TableColumnExt columnExt) {
049 *            super.configureTableColumn(model, columnExt);
050 *            String title = columnExt.getTitle();
051 *            title = title.substring(0,1).toUpperCase() + title.substring(1).toLowerCase();
052 *            columnExt.setTitle(title);
053 *        }
054 *    };
055 * </code>
056 * </pre>
057 * 
058 * By default a single instance is shared across all tables of an application.
059 * This instance can be replaced by a custom implementation, preferably "early"
060 * in the application's lifetime.
061 * 
062 * <pre><code>
063 * ColumnFactory.setInstance(new MyColumnFactory());
064 * </code></pre> 
065 * 
066 * Alternatively, any instance of <code>JXTable</code> can be configured
067 * individually with its own <code>ColumnFactory</code>.
068 * 
069 * <pre>
070 *  <code>
071 * JXTable table = new JXTable();
072 * table.setColumnFactory(new MyColumnFactory());
073 * table.setModel(myTableModel);
074 * </code>
075 *  </pre>
076 * 
077 * <p>
078 * 
079 * @see org.jdesktop.swingx.JXTable#setColumnFactory(ColumnFactory)
080 * 
081 * @author Jeanette Winzenburg
082 * @author M.Hillary (the pack code)
083 */
084public class ColumnFactory {
085    
086    /** the shared instance. */
087    private static ColumnFactory columnFactory;
088    /** the default margin to use in pack. */
089    private int packMargin = 4;
090    
091    /**
092     * Returns the shared default factory. 
093     * 
094     * @return the shared instance of <code>ColumnFactory</code>
095     * @see #setInstance(ColumnFactory)
096     */
097    public static synchronized ColumnFactory getInstance() {
098        if (columnFactory == null) {
099            columnFactory = new ColumnFactory();
100        }
101        return columnFactory;
102    }
103
104    /**
105     * Sets the shared default factory. The shared instance is used
106     * by <code>JXTable</code> if none has been set individually.
107     * 
108     * @param factory the default column factory.
109     * @see #getInstance()
110     * @see org.jdesktop.swingx.JXTable#getColumnFactory()
111     */
112    public static synchronized void  setInstance(ColumnFactory factory) {
113        columnFactory = factory;
114    }
115
116    /**
117     * Creates and configures a TableColumnExt. <code>JXTable</code> calls
118     * this method for each column in the <code>TableModel</code>.
119     * 
120     * @param model the TableModel to read configuration properties from
121     * @param modelIndex column index in model coordinates
122     * @return a TableColumnExt to use for the modelIndex
123     * @throws NPE if model == null
124     * @throws IllegalStateException if the modelIndex is invalid
125     *   (in coordinate space of the tablemodel)
126     *  
127     * @see #createTableColumn(int)
128     * @see #configureTableColumn(TableModel, TableColumnExt)
129     * @see org.jdesktop.swingx.JXTable#createDefaultColumnsFromModel() 
130     */
131    public TableColumnExt createAndConfigureTableColumn(TableModel model, int modelIndex) {
132        TableColumnExt column = createTableColumn(modelIndex);
133        if (column != null) {
134            configureTableColumn(model, column);
135        }
136        return column;
137    }
138    
139    /**
140     * Creates a table column with modelIndex.
141     * <p>
142     * The factory's column creation is passed through this method, so 
143     * subclasses can override to return custom column types.
144     * 
145     * @param modelIndex column index in model coordinates
146     * @return a TableColumnExt with <code>modelIndex</code>
147     * 
148     * @see #createAndConfigureTableColumn(TableModel, int)
149     * 
150     */
151    public TableColumnExt createTableColumn(int modelIndex) {
152        return new TableColumnExt(modelIndex);
153    }
154    
155    /**
156     * Configure column properties from TableModel. This implementation
157     * sets the column's <code>headerValue</code> property from the 
158     * model's <code>columnName</code>.
159     * <p>
160     * 
161     * The factory's initial column configuration is passed through this method, so 
162     * subclasses can override to customize.
163     * <p>
164     * 
165     * @param model the TableModel to read configuration properties from
166     * @param columnExt the TableColumnExt to configure.
167     * @throws NullPointerException if model or column == null
168     * @throws IllegalStateException if column does not have valid modelIndex
169     *   (in coordinate space of the tablemodel)
170     *   
171     * @see #createAndConfigureTableColumn(TableModel, int)  
172     */
173    public void configureTableColumn(TableModel model, TableColumnExt columnExt) {
174        if ((columnExt.getModelIndex() < 0) 
175                || (columnExt.getModelIndex() >= model.getColumnCount())) 
176            throw new IllegalStateException("column must have valid modelIndex");
177        columnExt.setHeaderValue(model.getColumnName(columnExt.getModelIndex()));
178    }
179    
180
181    /**
182     * Configures column initial widths properties from <code>JXTable</code>.
183     * This implementation sets the column's
184     * <code>preferredWidth</code> with the strategy:
185     * <ol> if the column has a prototype, measure the rendering
186     *    component with the prototype as value and use that as
187     *    pref width
188     * <ol> if the column has no prototype, use the standard magic
189     *   pref width (= 75) 
190     * <ol> try to measure the column's header and use it's preferred
191     *   width if it exceeds the former.    
192     * </ol>
193     * 
194     * TODO JW - rename method to better convey what's happening, maybe
195     * initializeColumnWidths like the old method in JXTable. <p>
196     * 
197     * TODO JW - how to handle default settings which are different from
198     *   standard 75?
199     * 
200     * @param table the context the column will live in.
201     * @param columnExt the Tablecolumn to configure.
202     * 
203     * @see org.jdesktop.swingx.JXTable#getPreferredScrollableViewportSize()
204     */
205    public void configureColumnWidths(JXTable table, TableColumnExt columnExt) {
206        /*
207         * PENDING JW: really only called once in a table's lifetime?
208         * unfortunately: yes - should be called always after structureChanged.
209         * 
210         */
211        // magic value: default in TableColumn
212        int prefWidth = 75 - table.getColumnMargin();
213        int prototypeWidth = calcPrototypeWidth(table, columnExt);
214        if (prototypeWidth > 0) {
215            prefWidth = prototypeWidth;
216        }
217        int headerWidth = calcHeaderWidth(table, columnExt);
218        prefWidth = Math.max(prefWidth, headerWidth);
219        prefWidth += table.getColumnModel().getColumnMargin();
220        columnExt.setPreferredWidth(prefWidth);
221    }
222
223    /**
224     * Calculates and returns the preferred scrollable viewport 
225     * width of the given table. Subclasses are free to override
226     * and implement a custom strategy.<p>
227     * 
228     * This implementation sums the pref widths of the first
229     * visibleColumnCount contained visible tableColumns. If
230     * the table contains less columns, the standard preferred
231     * width per column is added to the result. 
232     * 
233     * @param table the table containing the columns
234     */
235    public int getPreferredScrollableViewportWidth(JXTable table) {
236        int w = 0;
237        int count;
238        if (table.getVisibleColumnCount() < 0) {
239            count = table.getColumnCount();
240        } else {
241            count = Math.min(table.getColumnCount(), table.getVisibleColumnCount());
242        }
243        for (int i = 0; i < count; i++) {
244            // sum up column's pref size, until maximal the
245            // visibleColumnCount
246            w += table.getColumn(i).getPreferredWidth();
247        }
248        if (count < table.getVisibleColumnCount()) {
249            w += (table.getVisibleColumnCount() - count) * 75;
250        }
251        return w;
252        
253    }
254    /**
255     * Measures and returns the preferred width of the header. Returns -1 if not 
256     * applicable.
257     *  
258     * @param table the component the renderer lives in
259     * @param columnExt the TableColumn to configure
260     * @return the preferred width of the header or -1 if none.
261     */
262    protected int calcHeaderWidth(JXTable table, TableColumnExt columnExt) {
263        int prototypeWidth = -1;
264        // now calculate how much room the column header wants
265        TableCellRenderer renderer = getHeaderRenderer(table, columnExt);
266        if (renderer != null) {
267            Component comp = renderer.getTableCellRendererComponent(table,
268                    columnExt.getHeaderValue(), false, false, -1, -1);
269
270            prototypeWidth = comp.getPreferredSize().width;
271        }
272        return prototypeWidth;
273    }
274
275    /**
276     * Measures and returns the preferred width of the rendering component
277     * configured with the prototype value, if any. Returns -1 if not 
278     * applicable.
279     *  
280     * @param table the component the renderer lives in
281     * @param columnExt the TableColumn to configure
282     * @return the preferred width of the prototype or -1 if none.
283     */
284    protected int calcPrototypeWidth(JXTable table, TableColumnExt columnExt) {
285        int prototypeWidth = -1;
286        Object prototypeValue = columnExt.getPrototypeValue();
287        if (prototypeValue != null) {
288            // calculate how much room the prototypeValue requires
289            TableCellRenderer cellRenderer = getCellRenderer(table, columnExt);
290            Component comp = cellRenderer.getTableCellRendererComponent(table,
291                    prototypeValue, false, false, 0, -1);
292            prototypeWidth = comp.getPreferredSize().width;
293        }
294        return prototypeWidth;
295    }
296
297    /**
298     * Returns the cell renderer to use for measuring. Delegates to 
299     * JXTable for visible columns, duplicates table logic for hidden
300     * columns. <p>
301     * 
302     * @param table the table which provides the renderer
303     * @param columnExt the TableColumn to configure
304     * 
305     * @return returns a cell renderer for measuring.
306     */
307    protected TableCellRenderer getCellRenderer(JXTable table, TableColumnExt columnExt) {
308        int viewIndex = table.convertColumnIndexToView(columnExt
309                .getModelIndex());
310        if (viewIndex >= 0) {
311            // JW: ok to not guard against rowCount < 0?
312            // technically, the index should be a valid coordinate
313            return table.getCellRenderer(0, viewIndex);
314        }
315        // hidden column - need api on JXTable to access renderer for hidden?
316        // here we duplicate JXTable api ... maybe by-passing the strategy
317        // implemented there
318        TableCellRenderer renderer = columnExt.getCellRenderer();
319        if (renderer == null) {
320            renderer = table.getDefaultRenderer(table.getModel().getColumnClass(columnExt.getModelIndex()));
321        }
322        return renderer;
323    }
324
325    /**
326     * Looks up and returns the renderer used for the column's header.<p>
327     * 
328     * @param table the table which contains the column
329     * @param columnExt the column to lookup the header renderer for
330     * @return the renderer for the columns header, may be null.
331     */
332    protected TableCellRenderer getHeaderRenderer(JXTable table, TableColumnExt columnExt) {
333        TableCellRenderer renderer = columnExt.getHeaderRenderer();
334        if (renderer == null) {
335            JTableHeader header = table.getTableHeader();
336            if (header != null) {
337                renderer = header.getDefaultRenderer();
338            }
339        }
340        // JW: default to something if null? 
341        // if so, could be table's default object/string header?
342        return renderer;
343    }
344
345
346    /**
347     * Configures the column's <code>preferredWidth</code> to fit the content.
348     * It respects the table context, a margin to add and a maximum width. This
349     * is typically called in response to a user gesture to adjust the column's
350     * width to the "widest" cell content of a column.
351     * <p>
352     * 
353     * This implementation loops through all rows of the given column and
354     * measures the renderers pref width (it's a potential performance sink).
355     * Subclasses can override to implement a different strategy.
356     * <p>
357     * 
358     * Note: though 2 * margin is added as spacing, this does <b>not</b> imply
359     * a left/right symmetry - it's up to the table to place the renderer and/or
360     * the renderer/highlighter to configure a border.<p>
361     * 
362     * PENDING: support pack for hidden column? 
363     *      This implementation can't handle it! For now, added doc and 
364     *      fail-fast.
365     * 
366     * @param table the context the column will live in.
367     * @param columnExt the column to configure.
368     * @param margin the extra spacing to add twice, if -1 uses this factories
369     *        default
370     * @param max an upper limit to preferredWidth, -1 is interpreted as no
371     *        limit
372     * @throws IllegalStateException if column is not visible
373     * 
374     * @see #setDefaultPackMargin(int)
375     * @see org.jdesktop.swingx.JXTable#packTable(int)
376     * @see org.jdesktop.swingx.JXTable#packColumn(int, int)
377     * 
378     */
379    public void packColumn(JXTable table, TableColumnExt columnExt, int margin,
380            int max) {
381        if (!columnExt.isVisible()) 
382            throw new IllegalStateException("column must be visible to pack");
383        
384        int column = table.convertColumnIndexToView(columnExt.getModelIndex());
385        int width = 0;
386        TableCellRenderer headerRenderer = getHeaderRenderer(table, columnExt);
387        if (headerRenderer != null) {
388            Component comp = headerRenderer.getTableCellRendererComponent(table,
389                    columnExt.getHeaderValue(), false, false, 0, column);
390            width = comp.getPreferredSize().width;
391        }   
392        // PENDING JW: slightly inconsistent - the getCellRenderer here
393        // returns a (guessed) renderer for invisible columns which must not
394        // be used in the loop. For now that's okay, as we back out early anyway
395        TableCellRenderer renderer = getCellRenderer(table, columnExt);
396        for (int r = 0; r < getRowCount(table); r++) {
397            // JW: fix for #1215-swing as suggested by the reporter adrienclerc
398            Component comp = table.prepareRenderer(renderer, r, column);
399//            Component comp = renderer.getTableCellRendererComponent(table, table
400//                    .getValueAt(r, column), false, false, r, column);
401            width = Math.max(width, comp.getPreferredSize().width);
402        }
403        if (margin < 0) {
404            margin = getDefaultPackMargin();
405        }
406        width += 2 * margin;
407
408        /* Check if the width exceeds the max */
409        if (max != -1 && width > max)
410            width = max;
411
412        columnExt.setPreferredWidth(width);
413
414    }
415
416    /**
417     * Returns the number of table view rows accessible during row-related
418     * config. All row-related access is bounded by the value returned from this
419     * method.
420     * 
421     * Here: delegates to table.getRowCount().
422     * <p>
423     * 
424     * Subclasses can override to reduce the number (for performance) or support
425     * restrictions due to lazy loading, f.i. Implementors must guarantee that
426     * view row access with <code>0 <= row < getRowCount(JXTable)</code>
427     * succeeds.
428     * 
429     * @param table the table to access
430     * @return valid rowCount
431     */
432    protected int getRowCount(JXTable table) {
433        return table.getRowCount();
434    }
435    
436// ------------------------ default state
437    
438    /**
439     * Returns the default pack margin.
440     * 
441     * @return the default pack margin to use in packColumn.
442     * 
443     * @see #setDefaultPackMargin(int)
444     */
445    public int getDefaultPackMargin() {
446        return packMargin;
447    }
448    
449    /**
450     * Sets the default pack margin. <p>
451     * 
452     * Note: this is <b>not</b> really a margin in the sense of symmetrically 
453     * adding white space to the left/right of a cell's content. It's simply an 
454     * amount of space which is added twice to the measured widths in packColumn.
455     * 
456     * @param margin the default marging to use in packColumn.
457     * 
458     * @see #getDefaultPackMargin()
459     * @see #packColumn(JXTable, TableColumnExt, int, int)
460     */
461    public void setDefaultPackMargin(int margin) {
462        this.packMargin = margin;
463    }
464
465    
466}