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 }