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 }