001    /*
002     * $Id: ColumnFactory.java 2739 2008-02-20 20:06:39Z 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     */
021    package org.jdesktop.swingx.table;
022    
023    import java.awt.Component;
024    
025    import javax.swing.table.JTableHeader;
026    import javax.swing.table.TableCellRenderer;
027    import javax.swing.table.TableModel;
028    
029    import 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     */
084    public 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            TableCellRenderer renderer = getCellRenderer(table, columnExt);
393            for (int r = 0; r < getRowCount(table); r++) {
394                Component comp = renderer.getTableCellRendererComponent(table, table
395                        .getValueAt(r, column), false, false, r, column);
396                width = Math.max(width, comp.getPreferredSize().width);
397            }
398            if (margin < 0) {
399                margin = getDefaultPackMargin();
400            }
401            width += 2 * margin;
402    
403            /* Check if the width exceeds the max */
404            if (max != -1 && width > max)
405                width = max;
406    
407            columnExt.setPreferredWidth(width);
408    
409        }
410    
411        /**
412         * Returns the number of table view rows accessible during row-related
413         * config. All row-related access is bounded by the value returned from this
414         * method.
415         * 
416         * Here: delegates to table.getRowCount().
417         * <p>
418         * 
419         * Subclasses can override to reduce the number (for performance) or support
420         * restrictions due to lazy loading, f.i. Implementors must guarantee that
421         * view row access with <code>0 <= row < getRowCount(JXTable)</code>
422         * succeeds.
423         * 
424         * @param table the table to access
425         * @return valid rowCount
426         */
427        protected int getRowCount(JXTable table) {
428            return table.getRowCount();
429        }
430        
431    // ------------------------ default state
432        
433        /**
434         * Returns the default pack margin.
435         * 
436         * @return the default pack margin to use in packColumn.
437         * 
438         * @see #setDefaultPackMargin(int)
439         */
440        public int getDefaultPackMargin() {
441            return packMargin;
442        }
443        
444        /**
445         * Sets the default pack margin. <p>
446         * 
447         * Note: this is <b>not</b> really a margin in the sense of symmetrically 
448         * adding white space to the left/right of a cell's content. It's simply an 
449         * amount of space which is added twice to the measured widths in packColumn.
450         * 
451         * @param margin the default marging to use in packColumn.
452         * 
453         * @see #getDefaultPackMargin()
454         * @see #packColumn(JXTable, TableColumnExt, int, int)
455         */
456        public void setDefaultPackMargin(int margin) {
457            this.packMargin = margin;
458        }
459    
460        
461    }