001 /* 002 * $Id: TableColumnExt.java 3379 2009-07-08 11:09:05Z 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 022 package org.jdesktop.swingx.table; 023 import java.awt.Component; 024 import java.beans.PropertyChangeEvent; 025 import java.beans.PropertyChangeListener; 026 import java.util.Comparator; 027 import java.util.Hashtable; 028 029 import javax.swing.DefaultCellEditor; 030 import javax.swing.JComponent; 031 import javax.swing.SwingUtilities; 032 import javax.swing.event.ChangeEvent; 033 import javax.swing.event.ChangeListener; 034 import javax.swing.table.TableCellEditor; 035 import javax.swing.table.TableCellRenderer; 036 import javax.swing.table.TableColumn; 037 038 import org.jdesktop.swingx.decorator.CompoundHighlighter; 039 import org.jdesktop.swingx.decorator.Highlighter; 040 import org.jdesktop.swingx.decorator.UIDependent; 041 import org.jdesktop.swingx.renderer.AbstractRenderer; 042 043 /** 044 * <code>TableColumn</code> extension for enhanced view column configuration. 045 * The general drift is to strengthen the TableColumn abstraction as <b>the</b> 046 * place to configure and dynamically update view column properties, covering a 047 * broad range of customization requirements. Using collaborators are expected 048 * to listen to property changes and update themselves accordingly. 049 * <p> 050 * 051 * A functionality enhancement is the notion of column visibility: 052 * <code>TableColumnModelExt</code> manages sets of visible/hidden 053 * <code>TableColumnExt</code>s controlled by the columns' 054 * <code>visible</code> property. Typically, users can toggle column 055 * visibility at runtime, f.i. through a dedicated control in the upper trailing 056 * corner of a <code>JScrollPane</code>. 057 * <p> 058 * 059 * A prominent group of properties allows fine-grained, per-column control of 060 * corresponding Table/-Header features. 061 * 062 * <ul> 063 * <li><b>Sorting</b>: <code>sortable</code> controls whether this column 064 * should be sortable by user's sort gestures; <code>Comparator</code> can 065 * hold a column specific type. 066 * 067 * <li><b>Editing</b>: <code>editable</code> controls whether cells of this 068 * column should be accessible to in-table editing. 069 * 070 * <li><b>Tooltip</b>: <code>toolTipText</code> holds the column tooltip 071 * which is shown when hovering over the column's header. 072 * 073 * <li><b>Highlighter</b>: <code>highlighters</code> holds the column 074 * highlighters; these are applied to the renderer after the table highlighters. 075 * Any modification of the list of contained <code>Highlighter</code>s 076 * (setting them, adding one or removing one) will result in a 077 * {@code PropertyChangeEvent} being fired for "highlighters". State changes on 078 * contained <code>Highlighter</code>s will result in a PropertyChangeEvent 079 * for "highlighterStateChanged". 080 * </ul> 081 * 082 * 083 * Analogous to <code>JComponent</code>, this class supports per-instance 084 * "client" properties. They are meant as a small-scale extension mechanism. 085 * They are similar to regular bean properties in that registered 086 * <code>PropertyChangeListener</code>s are notified about changes. TODO: 087 * example? 088 * <p> 089 * 090 * A <code>TableColumnExt</code> implements UIDependent, that is it takes over 091 * responsibility to update LAF dependent properties of contained elements when 092 * messaged with updateUI. This implementation updates its <code>Highlighter</code>s, 093 * Cell-/HeaderRenderer and CellEditor. <p> 094 * 095 * TODO: explain prototype (sizing, collaborator-used-by ColumnFactory (?)) 096 * <p> 097 * 098 * @author Ramesh Gupta 099 * @author Amy Fowler 100 * @author Jeanette Winzenburg 101 * @author Karl Schaefer 102 * 103 * @see TableColumnModelExt 104 * @see ColumnFactory 105 * @see org.jdesktop.swingx.decorator.UIDependent 106 * @see javax.swing.JComponent#putClientProperty 107 */ 108 public class TableColumnExt extends TableColumn implements UIDependent { 109 110 /** visible property. Initialized to <code>true</code>.*/ 111 protected boolean visible = true; 112 113 /** prototype property. */ 114 protected Object prototypeValue; 115 116 117 /** per-column comparator */ 118 protected Comparator comparator; 119 /** per-column sortable property. Initialized to <code>true</code>. */ 120 protected boolean sortable = true; 121 /** per-column editable property. Initialized to <code>true</code>.*/ 122 protected boolean editable = true; 123 /** per-column tool tip text. */ 124 private String toolTipText; 125 126 /** storage for client properties. */ 127 protected Hashtable<Object, Object> clientProperties; 128 129 /** 130 * The compound highlighter for the column. 131 */ 132 protected CompoundHighlighter compoundHighlighter; 133 134 private ChangeListener highlighterChangeListener; 135 136 private boolean ignoreHighlighterStateChange; 137 138 /** 139 * Creates new table view column with a model index = 0. 140 */ 141 public TableColumnExt() { 142 this(0); 143 } 144 145 /** 146 * Creates new table view column with the specified model index. 147 * @param modelIndex index of table model column to which this view column 148 * is bound. 149 */ 150 public TableColumnExt(int modelIndex) { 151 this(modelIndex, 75); // default width taken from javax.swing.table.TableColumn 152 } 153 154 /** 155 * Creates new table view column with the specified model index and column width. 156 * @param modelIndex index of table model column to which this view column 157 * is bound. 158 * @param width pixel width of view column 159 */ 160 public TableColumnExt(int modelIndex, int width) { 161 this(modelIndex, width, null, null); 162 } 163 164 /** 165 * Creates new table view column with the specified model index, column 166 * width, cell renderer and cell editor. 167 * @param modelIndex index of table model column to which this view column 168 * is bound. 169 * @param width pixel width of view column 170 * @param cellRenderer the cell renderer which will render all cells in this 171 * view column 172 * @param cellEditor the cell editor which will edit cells in this view column 173 */ 174 public TableColumnExt(int modelIndex, int width, 175 TableCellRenderer cellRenderer, TableCellEditor cellEditor) { 176 super(modelIndex, width, cellRenderer, cellEditor); 177 } 178 179 /** 180 * Instantiates a new table view column with all properties copied from the 181 * given original. 182 * 183 * @param columnExt the column to copy properties from 184 * @see #copyFrom(TableColumnExt) 185 */ 186 public TableColumnExt(TableColumnExt columnExt) { 187 this(columnExt.getModelIndex(), columnExt.getWidth(), columnExt 188 .getCellRenderer(), columnExt.getCellEditor()); 189 copyFrom(columnExt); 190 } 191 192 193 /** 194 * Sets the <code>Highlighter</code>s to the table, replacing any old settings. 195 * None of the given Highlighters must be null.<p> 196 * 197 * This is a bound property. <p> 198 * 199 * Note: as of version #1.257 the null constraint is enforced strictly. To remove 200 * all highlighters use this method without param. 201 * 202 * @param highlighters zero or more not null highlighters to use for renderer decoration. 203 * @throws NullPointerException if array is null or array contains null values. 204 * 205 * @see #getHighlighters() 206 * @see #addHighlighter(Highlighter) 207 * @see #removeHighlighter(Highlighter) 208 * 209 */ 210 public void setHighlighters(Highlighter... highlighters) { 211 ignoreHighlighterStateChange = true; 212 Highlighter[] old = getHighlighters(); 213 getCompoundHighlighter().setHighlighters(highlighters); 214 firePropertyChange("highlighters", old, getHighlighters()); 215 ignoreHighlighterStateChange = false; 216 } 217 218 /** 219 * Returns the <code>Highlighter</code>s used by this table. 220 * Maybe empty, but guarantees to be never null. 221 * 222 * @return the Highlighters used by this table, guaranteed to never null. 223 * @see #setHighlighters(Highlighter[]) 224 */ 225 public Highlighter[] getHighlighters() { 226 return getCompoundHighlighter().getHighlighters(); 227 } 228 /** 229 * Appends a <code>Highlighter</code> to the end of the list of used 230 * <code>Highlighter</code>s. The argument must not be null. 231 * <p> 232 * 233 * @param highlighter the <code>Highlighter</code> to add, must not be null. 234 * @throws NullPointerException if <code>Highlighter</code> is null. 235 * 236 * @see #removeHighlighter(Highlighter) 237 * @see #setHighlighters(Highlighter[]) 238 */ 239 public void addHighlighter(Highlighter highlighter) { 240 ignoreHighlighterStateChange = true; 241 Highlighter[] old = getHighlighters(); 242 getCompoundHighlighter().addHighlighter(highlighter); 243 firePropertyChange("highlighters", old, getHighlighters()); 244 ignoreHighlighterStateChange = false; 245 } 246 247 /** 248 * Removes the given Highlighter. <p> 249 * 250 * Does nothing if the Highlighter is not contained. 251 * 252 * @param highlighter the Highlighter to remove. 253 * @see #addHighlighter(Highlighter) 254 * @see #setHighlighters(Highlighter...) 255 */ 256 public void removeHighlighter(Highlighter highlighter) { 257 ignoreHighlighterStateChange = true; 258 Highlighter[] old = getHighlighters(); 259 getCompoundHighlighter().removeHighlighter(highlighter); 260 firePropertyChange("highlighters", old, getHighlighters()); 261 ignoreHighlighterStateChange = false; 262 } 263 264 /** 265 * Returns the CompoundHighlighter assigned to the table, null if none. 266 * PENDING: open up for subclasses again?. 267 * 268 * @return the CompoundHighlighter assigned to the table. 269 */ 270 protected CompoundHighlighter getCompoundHighlighter() { 271 if (compoundHighlighter == null) { 272 compoundHighlighter = new CompoundHighlighter(); 273 compoundHighlighter.addChangeListener(getHighlighterChangeListener()); 274 } 275 return compoundHighlighter; 276 } 277 278 /** 279 * Returns the <code>ChangeListener</code> to use with highlighters. Lazily 280 * creates the listener. 281 * 282 * @return the ChangeListener for observing changes of highlighters, 283 * guaranteed to be <code>not-null</code> 284 */ 285 protected ChangeListener getHighlighterChangeListener() { 286 if (highlighterChangeListener == null) { 287 highlighterChangeListener = createHighlighterChangeListener(); 288 } 289 return highlighterChangeListener; 290 } 291 292 /** 293 * Creates and returns the ChangeListener observing Highlighters. 294 * <p> 295 * Here: repaints the table on receiving a stateChanged. 296 * 297 * @return the ChangeListener defining the reaction to changes of 298 * highlighters. 299 */ 300 protected ChangeListener createHighlighterChangeListener() { 301 return new ChangeListener() { 302 public void stateChanged(ChangeEvent e) { 303 if (ignoreHighlighterStateChange) return; 304 firePropertyChange("highlighterStateChanged", false, true); 305 } 306 }; 307 } 308 309 /** 310 * Returns true if the user <i>can</i> resize the TableColumn's width, 311 * false otherwise. This is a usability override: it takes into account 312 * the case where it's principally <i>allowed</i> to resize the column 313 * but not possible because the column has fixed size. 314 * 315 * @return a boolean indicating whether the user can resize this column. 316 */ 317 @Override 318 public boolean getResizable() { 319 // TODO JW: resizable is a bound property, so to be strict 320 // we'll need to override setMin/MaxWidth to fire resizable 321 // property change. 322 return super.getResizable() && (getMinWidth() < getMaxWidth()); 323 } 324 325 /** 326 * Sets the editable property. This property allows to mark all cells in a 327 * column as read-only, independent of the per-cell editability as returned 328 * by the <code>TableModel.isCellEditable</code>. If the cell is 329 * read-only in the model layer, this property will have no effect. 330 * 331 * @param editable boolean indicating whether or not the user may edit cell 332 * values in this view column 333 * @see #isEditable 334 * @see org.jdesktop.swingx.JXTable#isCellEditable(int, int) 335 * @see javax.swing.table.TableModel#isCellEditable 336 */ 337 public void setEditable(boolean editable) { 338 boolean oldEditable = this.editable; 339 this.editable = editable; 340 firePropertyChange("editable", 341 Boolean.valueOf(oldEditable), 342 Boolean.valueOf(editable)); 343 } 344 345 /** 346 * Returns the per-column editable property. 347 * The default is <code>true</code>. 348 * 349 * @return boolean indicating whether or not the user may edit cell 350 * values in this view column 351 * @see #setEditable 352 */ 353 public boolean isEditable() { 354 return editable; 355 } 356 357 /** 358 * Sets the prototypeValue property. The value should be of a type 359 * which corresponds to the column's class as defined by the table model. 360 * If non-null, the JXTable instance will use this property to calculate 361 * and set the initial preferredWidth of the column. Note that this 362 * initial preferredWidth will be overridden if the user resizes columns 363 * directly. 364 * 365 * @param value Object containing the value of the prototype to be used 366 * to calculate the initial preferred width of the column 367 * @see #getPrototypeValue 368 * @see org.jdesktop.swingx.JXTable#getPreferredScrollableViewportSize 369 */ 370 public void setPrototypeValue(Object value) { 371 Object oldPrototypeValue = this.prototypeValue; 372 this.prototypeValue = value; 373 firePropertyChange("prototypeValue", 374 oldPrototypeValue, 375 value); 376 377 } 378 379 /** 380 * Returns the prototypeValue property. 381 * The default is <code>null</code>. 382 * 383 * @return Object containing the value of the prototype to be used 384 * to calculate the initial preferred width of the column 385 * @see #setPrototypeValue 386 */ 387 public Object getPrototypeValue() { 388 return prototypeValue; 389 } 390 391 392 /** 393 * Sets the comparator to use for this column. 394 * <code>JXTable</code> sorting api respects this property by passing it on 395 * to the <code>SortController</code>. 396 * 397 * @param comparator a custom comparator to use in interactive 398 * sorting. 399 * @see #getComparator 400 * @see org.jdesktop.swingx.sort.SortController 401 * @see org.jdesktop.swingx.decorator.SortKey 402 */ 403 public void setComparator(Comparator comparator) { 404 Comparator old = getComparator(); 405 this.comparator = comparator; 406 firePropertyChange("comparator", old, getComparator()); 407 } 408 409 /** 410 * Returns the comparator to use for the column. 411 * The default is <code>null</code>. 412 * 413 * @return <code>Comparator</code> to use for this column 414 * @see #setComparator 415 */ 416 public Comparator getComparator() { 417 return comparator; 418 } 419 420 /** 421 * Sets the sortable property. <code>JXTable</code> sorting api respects this 422 * property by disabling interactive sorting on this column if false. 423 * 424 * @param sortable boolean indicating whether or not this column can 425 * be sorted in the table 426 * @see #isSortable 427 */ 428 public void setSortable(boolean sortable) { 429 boolean old = isSortable(); 430 this.sortable = sortable; 431 firePropertyChange("sortable", old, isSortable()); 432 } 433 434 /** 435 * Returns the sortable property. 436 * The default value is <code>true</code>. 437 * 438 * @return boolean indicating whether this view column is sortable 439 * @see #setSortable 440 */ 441 public boolean isSortable() { 442 return sortable; 443 } 444 445 /** 446 * Registers the text to display in the column's tool tip. 447 * Typically, this is used by <code>JXTableHeader</code> to 448 * display when the mouse cursor lingers over the column's 449 * header cell. 450 * 451 * @param toolTipText text to show. 452 * @see #setToolTipText(String) 453 */ 454 public void setToolTipText(String toolTipText) { 455 String old = getToolTipText(); 456 this.toolTipText = toolTipText; 457 firePropertyChange("toolTipText", old, getToolTipText()); 458 } 459 460 /** 461 * Returns the text of to display in the column's tool tip. 462 * The default is <code>null</code>. 463 * 464 * @return the text of the column ToolTip. 465 * @see #setToolTipText(String) 466 */ 467 public String getToolTipText() { 468 return toolTipText; 469 } 470 471 472 /** 473 * Sets the title of this view column. This is a convenience 474 * wrapper for <code>setHeaderValue</code>. 475 * @param title String containing the title of this view column 476 */ 477 public void setTitle(String title) { 478 setHeaderValue(title); // simple wrapper 479 } 480 481 /** 482 * Convenience method which returns the headerValue property after 483 * converting it to a string. 484 * @return String containing the title of this view column or null if 485 * no headerValue is set. 486 */ 487 public String getTitle() { 488 Object header = getHeaderValue(); 489 return header != null ? header.toString() : null; // simple wrapper 490 } 491 492 /** 493 * Sets the visible property. This property controls whether or not 494 * this view column is currently visible in the table. 495 * 496 * @param visible boolean indicating whether or not this view column is 497 * visible in the table 498 * @see #setVisible 499 */ 500 public void setVisible(boolean visible) { 501 boolean oldVisible = this.visible; 502 this.visible = visible; 503 firePropertyChange("visible", 504 Boolean.valueOf(oldVisible), 505 Boolean.valueOf(visible)); 506 } 507 508 /** 509 * Returns the visible property. 510 * The default is <code>true</code>. 511 * 512 * @return boolean indicating whether or not this view column is 513 * visible in the table 514 * @see #setVisible 515 */ 516 public boolean isVisible() { 517 return visible; 518 } 519 520 /** 521 * Sets the client property "key" to <code>value</code>. 522 * If <code>value</code> is <code>null</code> this method will remove the property. 523 * Changes to 524 * client properties are reported with <code>PropertyChange</code> events. 525 * The name of the property (for the sake of PropertyChange events) is 526 * <code>key.toString()</code>. 527 * <p> 528 * The <code>get/putClientProperty</code> methods provide access to a 529 * per-instance hashtable, which is intended for small scale extensions of 530 * TableColumn. 531 * <p> 532 * 533 * @param key Object which is used as key to retrieve value 534 * @param value Object containing value of client property 535 * @throws IllegalArgumentException if key is <code>null</code> 536 * @see #getClientProperty 537 * @see javax.swing.JComponent#putClientProperty 538 */ 539 public void putClientProperty(Object key, Object value) { 540 if (key == null) 541 throw new IllegalArgumentException("null key"); 542 543 if ((value == null) && (getClientProperty(key) == null)) { 544 return; 545 } 546 547 Object old = getClientProperty(key); 548 if (value == null) { 549 getClientProperties().remove(key); 550 } 551 else { 552 getClientProperties().put(key, value); 553 } 554 555 firePropertyChange(key.toString(), old, value); 556 /* Make all fireXXX methods in TableColumn protected instead of private */ 557 } 558 559 /** 560 * Returns the value of the property with the specified key. Only properties 561 * added with <code>putClientProperty</code> will return a non-<code>null</code> 562 * value. 563 * 564 * @param key Object which is used as key to retrieve value 565 * @return Object containing value of client property or <code>null</code> 566 * 567 * @see #putClientProperty 568 */ 569 public Object getClientProperty(Object key) { 570 return ((key == null) || (clientProperties == null)) ? 571 null : clientProperties.get(key); 572 } 573 574 private Hashtable<Object, Object> getClientProperties() { 575 if (clientProperties == null) { 576 clientProperties = new Hashtable<Object, Object>(); 577 } 578 return clientProperties; 579 } 580 581 582 /** 583 * Copies properties from original. Handles all properties except 584 * modelIndex, width, cellRenderer, cellEditor. Called from copy 585 * constructor. 586 * 587 * @param original the tableColumn to copy from 588 * 589 * @see #TableColumnExt(TableColumnExt) 590 */ 591 protected void copyFrom(TableColumnExt original) { 592 setEditable(original.isEditable()); 593 setHeaderValue(original.getHeaderValue()); // no need to copy setTitle(); 594 setToolTipText(original.getToolTipText()); 595 setIdentifier(original.getIdentifier()); 596 setMaxWidth(original.getMaxWidth()); 597 setMinWidth(original.getMinWidth()); 598 setPreferredWidth(original.getPreferredWidth()); 599 setPrototypeValue(original.getPrototypeValue()); 600 // JW: isResizable is overridden to return a calculated property! 601 setResizable(original.isResizable); 602 setVisible(original.isVisible()); 603 setSortable(original.isSortable()); 604 setComparator(original.getComparator()); 605 copyClientPropertiesFrom(original); 606 607 if (original.compoundHighlighter != null) { 608 setHighlighters(original.getHighlighters()); 609 } 610 611 } 612 613 /** 614 * Copies all clientProperties of this <code>TableColumnExt</code> 615 * to the target column. 616 * 617 * @param original the target column. 618 */ 619 protected void copyClientPropertiesFrom(TableColumnExt original) { 620 if (original.clientProperties == null) return; 621 for(Object key: original.clientProperties.keySet()) { 622 putClientProperty(key, original.getClientProperty(key)); 623 } 624 } 625 626 627 /** 628 * Notifies registered <code>PropertyChangeListener</code>s 629 * about property changes. This method must be invoked internally 630 * whe any of the enhanced properties changed. 631 * <p> 632 * Implementation note: needed to replicate super 633 * functionality because super's field <code>propertyChangeSupport</code> 634 * and method <code>fireXX</code> are both private. 635 * 636 * @param propertyName name of changed property 637 * @param oldValue old value of changed property 638 * @param newValue new value of changed property 639 */ 640 protected void firePropertyChange(String propertyName, Object oldValue, Object newValue) { 641 if ((oldValue != null && !oldValue.equals(newValue)) || 642 oldValue == null && newValue != null) { 643 PropertyChangeListener pcl[] = getPropertyChangeListeners(); 644 if (pcl != null && pcl.length != 0) { 645 PropertyChangeEvent pce = new PropertyChangeEvent(this, 646 propertyName, 647 oldValue, newValue); 648 649 for (int i = 0; i < pcl.length; i++) { 650 pcl[i].propertyChange(pce); 651 } 652 } 653 } 654 } 655 656 //---------------- implement UIDependent 657 658 /** 659 * Update ui of owned ui-dependent parts. This implementation 660 * updates the contained highlighters. 661 * 662 */ 663 public void updateUI() { 664 updateHighlighterUI(); 665 updateRendererUI(getCellRenderer()); 666 updateRendererUI(getHeaderRenderer()); 667 updateEditorUI(getCellEditor()); 668 } 669 670 /** 671 * @param editor 672 * 673 */ 674 private void updateEditorUI(TableCellEditor editor) { 675 if (editor == null) return; 676 // internal knowledge of core table - already updated 677 if ((editor instanceof JComponent) 678 || (editor instanceof DefaultCellEditor)) 679 return; 680 try { 681 Component comp = editor 682 .getTableCellEditorComponent(null, null, false, -1, -1); 683 if (comp != null) { 684 SwingUtilities.updateComponentTreeUI(comp); 685 } 686 } catch (Exception e) { 687 // can't do anything - renderer can't cope with off-range cells 688 } 689 } 690 691 /** 692 * @param tableCellRenderer 693 * 694 */ 695 private void updateRendererUI(TableCellRenderer renderer) { 696 if (renderer == null) return; 697 // internal knowledge of core table - already updated 698 if (renderer instanceof JComponent) { 699 return; 700 } 701 Component comp = null; 702 if (renderer instanceof AbstractRenderer) { 703 comp = ((AbstractRenderer) renderer).getComponentProvider().getRendererComponent(null); 704 } else { 705 try { 706 comp = renderer 707 .getTableCellRendererComponent(null, null, false, false, 708 -1, -1); 709 710 } catch (Exception e) { 711 // can't do anything - renderer can't cope with off-range cells 712 } 713 } 714 if (comp != null) { 715 SwingUtilities.updateComponentTreeUI(comp); 716 } 717 } 718 719 /** 720 * 721 */ 722 private void updateHighlighterUI() { 723 if (compoundHighlighter == null) return; 724 compoundHighlighter.updateUI(); 725 } 726 }