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    }