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 }