001/*
002 * $Id: JXComboBox.java 4158 2012-02-03 18:29:40Z kschaefe $
003 * 
004 * Copyright 2010 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 */
021package org.jdesktop.swingx;
022
023import java.awt.BorderLayout;
024import java.awt.Component;
025import java.awt.EventQueue;
026import java.awt.Rectangle;
027import java.awt.event.KeyEvent;
028import java.io.Serializable;
029import java.util.ArrayList;
030import java.util.List;
031import java.util.Vector;
032
033import javax.accessibility.Accessible;
034import javax.swing.ComboBoxModel;
035import javax.swing.DefaultComboBoxModel;
036import javax.swing.JComboBox;
037import javax.swing.JList;
038import javax.swing.JTable;
039import javax.swing.KeyStroke;
040import javax.swing.ListCellRenderer;
041import javax.swing.SwingUtilities;
042import javax.swing.UIManager;
043import javax.swing.event.ChangeEvent;
044import javax.swing.event.ChangeListener;
045import javax.swing.plaf.UIResource;
046import javax.swing.plaf.basic.ComboPopup;
047
048import org.jdesktop.swingx.decorator.ComponentAdapter;
049import org.jdesktop.swingx.decorator.CompoundHighlighter;
050import org.jdesktop.swingx.decorator.Highlighter;
051import org.jdesktop.swingx.plaf.UIDependent;
052import org.jdesktop.swingx.renderer.DefaultListRenderer;
053import org.jdesktop.swingx.renderer.JRendererPanel;
054import org.jdesktop.swingx.renderer.StringValue;
055import org.jdesktop.swingx.rollover.RolloverRenderer;
056import org.jdesktop.swingx.sort.StringValueRegistry;
057import org.jdesktop.swingx.util.Contract;
058
059/**
060 * An enhanced {@code JComboBox} that provides the following additional functionality:
061 * <p>
062 * Auto-starts edits correctly for AutoCompletion when inside a {@code JTable}. A normal {@code
063 * JComboBox} fails to recognize the first key stroke when it has been
064 * {@link org.jdesktop.swingx.autocomplete.AutoCompleteDecorator#decorate(JComboBox) decorated}.
065 * <p>
066 * Adds highlighting support.
067 * 
068 * @author Karl Schaefer
069 * @author Jeanette Winzenburg
070 */
071@SuppressWarnings({"nls", "serial"})
072public class JXComboBox extends JComboBox {
073    /**
074     * A decorator for the original ListCellRenderer. Needed to hook highlighters
075     * after messaging the delegate.<p>
076     */
077    public class DelegatingRenderer implements ListCellRenderer, RolloverRenderer, UIDependent {
078        /** the delegate. */
079        private ListCellRenderer delegateRenderer;
080        private JRendererPanel wrapper;
081
082        /**
083         * Instantiates a DelegatingRenderer with combo box's default renderer as delegate.
084         */
085        public DelegatingRenderer() {
086            this(null);
087        }
088        
089        /**
090         * Instantiates a DelegatingRenderer with the given delegate. If the
091         * delegate is {@code null}, the default is created via the combo box's factory method.
092         * 
093         * @param delegate the delegate to use, if {@code null} the combo box's default is
094         *   created and used.
095         */
096        public DelegatingRenderer(ListCellRenderer delegate) {
097            wrapper = new JRendererPanel(new BorderLayout());
098            setDelegateRenderer(delegate);
099        }
100
101        /**
102         * Sets the delegate. If the delegate is {@code null}, the default is created via the combo
103         * box's factory method.
104         * 
105         * @param delegate
106         *            the delegate to use, if null the list's default is created and used.
107         */
108        public void setDelegateRenderer(ListCellRenderer delegate) {
109            if (delegate == null) {
110                delegate = createDefaultCellRenderer();
111            }
112            delegateRenderer = delegate;
113        }
114
115        /**
116         * Returns the delegate.
117         * 
118         * @return the delegate renderer used by this renderer, guaranteed to
119         *   not-null.
120         */
121        public ListCellRenderer getDelegateRenderer() {
122            return delegateRenderer;
123        }
124
125        /**
126         * {@inheritDoc}
127         */
128        @Override
129        public void updateUI() {
130             wrapper.updateUI();
131             
132             if (delegateRenderer instanceof UIDependent) {
133                 ((UIDependent) delegateRenderer).updateUI();
134             } else if (delegateRenderer instanceof Component) {
135                 SwingUtilities.updateComponentTreeUI((Component) delegateRenderer);
136             } else if (delegateRenderer != null) {
137                 try {
138                     Component comp = delegateRenderer.getListCellRendererComponent(
139                             getPopupListFor(JXComboBox.this), null, -1, false, false);
140                     SwingUtilities.updateComponentTreeUI(comp);
141                 } catch (Exception e) {
142                     // nothing to do - renderer barked on off-range row
143                 }
144             }
145         }
146         
147         // --------- implement ListCellRenderer
148        /**
149         * {@inheritDoc} <p>
150         * 
151         * Overridden to apply the highlighters, if any, after calling the delegate.
152         * The decorators are not applied if the row is invalid.
153         */
154        @Override
155        public Component getListCellRendererComponent(JList list, Object value, int index,
156                boolean isSelected, boolean cellHasFocus) {
157            Component comp = null;
158
159            if (index == -1) {
160                comp = delegateRenderer.getListCellRendererComponent(list, value,
161                        getSelectedIndex(), isSelected, cellHasFocus);
162                
163                if (isUseHighlightersForCurrentValue() && compoundHighlighter != null && getSelectedIndex() != -1) {
164                    comp = compoundHighlighter.highlight(comp, getComponentAdapter(getSelectedIndex()));
165                    
166                    // this is done to "trick" BasicComboBoxUI.paintCurrentValue which resets all of
167                    // the painted information after asking the list to render the value. the panel
168                    // wrappers receives all of the post-rendering configuration, which is dutifully
169                    // ignored by the real rendering component
170                    wrapper.add(comp);
171                    comp = wrapper;
172                }
173            } else {
174                comp = delegateRenderer.getListCellRendererComponent(list, value, index,
175                        isSelected, cellHasFocus);
176                
177                if ((compoundHighlighter != null) && (index >= 0) && (index < getItemCount())) {
178                    comp = compoundHighlighter.highlight(comp, getComponentAdapter(index));
179                }
180            }
181
182            return comp;
183        }
184
185        // implement RolloverRenderer
186        
187        /**
188         * {@inheritDoc}
189         * 
190         */
191        @Override
192        public boolean isEnabled() {
193            return (delegateRenderer instanceof RolloverRenderer) && 
194               ((RolloverRenderer) delegateRenderer).isEnabled();
195        }
196        
197        /**
198         * {@inheritDoc}
199         */
200        @Override
201        public void doClick() {
202            if (isEnabled()) {
203                ((RolloverRenderer) delegateRenderer).doClick();
204            }
205        }
206    }
207    
208    @SuppressWarnings("hiding")
209    protected static class ComboBoxAdapter extends ComponentAdapter {
210        private final JXComboBox comboBox;
211
212        /**
213         * Constructs a <code>ListAdapter</code> for the specified target
214         * JXList.
215         * 
216         * @param component  the target list.
217         */
218        public ComboBoxAdapter(JXComboBox component) {
219            super(component);
220            comboBox = component;
221        }
222
223        /**
224         * Typesafe accessor for the target component.
225         * 
226         * @return the target component as a {@link org.jdesktop.swingx.JXComboBox}
227         */
228        public JXComboBox getComboBox() {
229            return comboBox;
230        }
231
232        /**
233         * A safe way to access the combo box's popup visibility.
234         * 
235         * @return {@code true} if the popup is visible; {@code false} otherwise
236         */
237        protected boolean isPopupVisible() {
238            if (comboBox.updatingUI) {
239                return false;
240            }
241            
242            return comboBox.isPopupVisible();
243        }
244        
245        /**
246         * {@inheritDoc}
247         */
248        @Override
249        public boolean hasFocus() {
250            if (isPopupVisible()) {
251                JList list = getPopupListFor(comboBox);
252                
253                return list != null && list.isFocusOwner() && (row == list.getLeadSelectionIndex());
254            }
255            
256            return comboBox.isFocusOwner();
257        }
258
259        /**
260         * {@inheritDoc}
261         */
262        @Override
263        public int getRowCount() {
264            return comboBox.getModel().getSize();
265        }
266
267        /**
268         * {@inheritDoc}
269         */
270        @Override
271        public Object getValueAt(int row, int column) {
272            return comboBox.getModel().getElementAt(row);
273        }
274
275        /**
276         * {@inheritDoc}
277         * This is implemented to query the table's StringValueRegistry for an appropriate
278         * StringValue and use that for getting the string representation.
279         */
280        @Override
281        public String getStringAt(int row, int column) {
282            StringValue sv = comboBox.getStringValueRegistry().getStringValue(row, column);
283            
284            return sv.getString(getValueAt(row, column));
285        }
286        
287        /**
288         * {@inheritDoc}
289         */
290        @Override
291        public Rectangle getCellBounds() {
292            JList list = getPopupListFor(comboBox);
293            
294            if (list == null) {
295                assert false;
296                return new Rectangle(comboBox.getSize());
297            }
298
299            return list.getCellBounds(row, row);
300        }
301        
302        /**
303         * {@inheritDoc}
304         */
305        @Override
306        public boolean isCellEditable(int row, int column) {
307            return row == -1 && comboBox.isEditable();
308        }
309
310        /**
311         * {@inheritDoc}
312         */
313        @Override
314        public boolean isEditable() {
315            return isCellEditable(row, column);
316        }
317        
318        /**
319         * {@inheritDoc}
320         */
321        @Override
322        public boolean isSelected() {
323            if (isPopupVisible()) {
324                JList list = getPopupListFor(comboBox);
325                
326                return list != null && row == list.getLeadSelectionIndex();
327            }
328            
329            return comboBox.isFocusOwner();
330        }
331    }
332    
333    class StringValueKeySelectionManager implements KeySelectionManager, Serializable, UIDependent {
334        private long timeFactor;
335        private long lastTime = 0L;
336        private String prefix = "";
337        private String typedString = "";
338        
339        public StringValueKeySelectionManager() {
340            updateUI();
341        }
342
343        @Override
344        public int selectionForKey(char aKey, ComboBoxModel aModel) {
345            if (lastTime == 0L) {
346                prefix = "";
347                typedString = "";
348            }
349            
350            int startIndex = getSelectedIndex();
351            
352            if (EventQueue.getMostRecentEventTime() - lastTime < timeFactor) {
353                typedString += aKey;
354                if ((prefix.length() == 1) && (aKey == prefix.charAt(0))) {
355                    // Subsequent same key presses move the keyboard focus to the next
356                    // object that starts with the same letter.
357                    startIndex++;
358                } else {
359                    prefix = typedString;
360                }
361            } else {
362                startIndex++;
363                typedString = "" + aKey;
364                prefix = typedString;
365            }
366            
367            lastTime = EventQueue.getMostRecentEventTime();
368
369            if (startIndex < 0 || startIndex >= aModel.getSize()) {
370                startIndex = 0;
371            }
372            
373            for (int i = startIndex, c = aModel.getSize(); i < c; i++) {
374                String v = getStringAt(i).toLowerCase();
375                
376                if (v.length() > 0 && v.charAt(0) == aKey) {
377                    return i;
378                }
379            }
380            
381            for (int i = startIndex, c = aModel.getSize(); i < c; i++) {
382                String v = getStringAt(i).toLowerCase();
383                
384                if (v.length() > 0 && v.charAt(0) == aKey) {
385                    return i;
386                }
387            }
388
389            for (int i = 0; i < startIndex; i++) {
390                String v = getStringAt(i).toLowerCase();
391                
392                if (v.length() > 0 && v.charAt(0) == aKey) {
393                    return i;
394                }
395            }
396            
397            return -1;
398        }
399
400        @Override
401        public void updateUI() {
402            Long l = (Long) UIManager.get("ComboBox.timeFactor");
403            timeFactor = l == null ? 1000L : l.longValue();
404        }
405    }
406
407    private ComboBoxAdapter dataAdapter;
408    
409    private DelegatingRenderer delegatingRenderer;
410    
411    private StringValueRegistry stringValueRegistry;
412
413    private boolean useHighlightersForCurrentValue = true;
414    
415    private CompoundHighlighter compoundHighlighter;
416
417    private ChangeListener highlighterChangeListener;
418
419    private List<KeyEvent> pendingEvents;
420
421    private boolean isDispatching;
422
423    private boolean updatingUI;
424
425    /**
426     * Creates a <code>JXComboBox</code> with a default data model. The default data model is an
427     * empty list of objects. Use <code>addItem</code> to add items. By default the first item in
428     * the data model becomes selected.
429     * 
430     * @see DefaultComboBoxModel
431     */
432    public JXComboBox() {
433        super();
434        init();
435    }
436
437    /**
438     * Creates a <code>JXComboBox</code> that takes its items from an existing
439     * <code>ComboBoxModel</code>. Since the <code>ComboBoxModel</code> is provided, a combo box
440     * created using this constructor does not create a default combo box model and may impact how
441     * the insert, remove and add methods behave.
442     * 
443     * @param model
444     *            the <code>ComboBoxModel</code> that provides the displayed list of items
445     * @see DefaultComboBoxModel
446     */
447    public JXComboBox(ComboBoxModel model) {
448        super(model);
449        init();
450    }
451
452    /**
453     * Creates a <code>JXComboBox</code> that contains the elements in the specified array. By
454     * default the first item in the array (and therefore the data model) becomes selected.
455     * 
456     * @param items
457     *            an array of objects to insert into the combo box
458     * @see DefaultComboBoxModel
459     */
460    public JXComboBox(Object[] items) {
461        super(items);
462        init();
463    }
464
465    /**
466     * Creates a <code>JXComboBox</code> that contains the elements in the specified Vector. By
467     * default the first item in the vector (and therefore the data model) becomes selected.
468     * 
469     * @param items
470     *            an array of vectors to insert into the combo box
471     * @see DefaultComboBoxModel
472     */
473    public JXComboBox(Vector<?> items) {
474        super(items);
475        init();
476    }
477
478    private void init() {
479        pendingEvents = new ArrayList<KeyEvent>();
480        
481        if (keySelectionManager == null || keySelectionManager instanceof UIResource) {
482            setKeySelectionManager(createDefaultKeySelectionManager());
483        }
484    }
485    
486    protected static JList getPopupListFor(JComboBox comboBox) {
487        int count = comboBox.getUI().getAccessibleChildrenCount(comboBox);
488
489        for (int i = 0; i < count; i++) {
490            Accessible a = comboBox.getUI().getAccessibleChild(comboBox, i);
491            
492            if (a instanceof ComboPopup) {
493                return ((ComboPopup) a).getList();
494            }
495        }
496
497        return null;
498    }
499
500    /**
501     * {@inheritDoc}
502     * <p>
503     * This implementation uses the {@code StringValue} representation of the elements to determine
504     * the selected item.
505     */
506    @Override
507    protected KeySelectionManager createDefaultKeySelectionManager() {
508        return new StringValueKeySelectionManager();
509    }
510    
511    /**
512     * {@inheritDoc}
513     */
514    @Override
515    protected boolean processKeyBinding(KeyStroke ks, final KeyEvent e, int condition,
516            boolean pressed) {
517        boolean retValue = super.processKeyBinding(ks, e, condition, pressed);
518
519        if (!retValue && editor != null) {
520            if (isStartingCellEdit(e)) {
521                pendingEvents.add(e);
522            } else if (pendingEvents.size() == 2) {
523                pendingEvents.add(e);
524                isDispatching = true;
525
526                SwingUtilities.invokeLater(new Runnable() {
527                    @Override
528                    public void run() {
529                        try {
530                            for (KeyEvent event : pendingEvents) {
531                                editor.getEditorComponent().dispatchEvent(event);
532                            }
533
534                            pendingEvents.clear();
535                        } finally {
536                            isDispatching = false;
537                        }
538                    }
539                });
540            }
541        }
542        return retValue;
543    }
544
545    private boolean isStartingCellEdit(KeyEvent e) {
546        if (isDispatching) {
547            return false;
548        }
549
550        JTable table = (JTable) SwingUtilities.getAncestorOfClass(JTable.class, this);
551        boolean isOwned = table != null
552                && !Boolean.FALSE.equals(table.getClientProperty("JTable.autoStartsEdit"));
553
554        return isOwned && e.getComponent() == table;
555    }
556
557    /**
558     * @return the unconfigured ComponentAdapter.
559     */
560    protected ComponentAdapter getComponentAdapter() {
561        if (dataAdapter == null) {
562            dataAdapter = new ComboBoxAdapter(this);
563        }
564        return dataAdapter;
565    }
566
567    /**
568     * Convenience to access a configured ComponentAdapter.
569     * Note: the column index of the configured adapter is always 0.
570     * 
571     * @param index the row index in view coordinates, must be valid.
572     * @return the configured ComponentAdapter.
573     */
574    protected ComponentAdapter getComponentAdapter(int index) {
575        ComponentAdapter adapter = getComponentAdapter();
576        adapter.column = 0;
577        adapter.row = index;
578        return adapter;
579    }
580    
581    /**
582     * Returns the StringValueRegistry which defines the string representation for
583     * each cells. This is strictly for internal use by the table, which has the 
584     * responsibility to keep in synch with registered renderers.<p>
585     * 
586     * Currently exposed for testing reasons, client code is recommended to not use nor override.
587     * 
588     * @return the current string value registry
589     */
590    protected StringValueRegistry getStringValueRegistry() {
591        if (stringValueRegistry == null) {
592            stringValueRegistry = createDefaultStringValueRegistry();
593        }
594        return stringValueRegistry;
595    }
596
597    /**
598     * Creates and returns the default registry for StringValues.<p>
599     * 
600     * @return the default registry for StringValues.
601     */
602    protected StringValueRegistry createDefaultStringValueRegistry() {
603        return new StringValueRegistry();
604    }
605    
606    /**
607     * Returns the string representation of the cell value at the given position. 
608     * 
609     * @param row the row index of the cell in view coordinates
610     * @return the string representation of the cell value as it will appear in the 
611     *   table. 
612     */
613    public String getStringAt(int row) {
614        // changed implementation to use StringValueRegistry
615        StringValue stringValue = getStringValueRegistry().getStringValue(row, 0);
616        
617        return stringValue.getString(getItemAt(row));
618    }
619
620    private DelegatingRenderer getDelegatingRenderer() {
621        if (delegatingRenderer == null) {
622            // only called once... to get hold of the default?
623            delegatingRenderer = new DelegatingRenderer();
624        }
625        return delegatingRenderer;
626    }
627
628    /**
629     * Creates and returns the default cell renderer to use. Subclasses
630     * may override to use a different type. Here: returns a <code>DefaultListRenderer</code>.
631     * 
632     * @return the default cell renderer to use with this list.
633     */
634    protected ListCellRenderer createDefaultCellRenderer() {
635        return new DefaultListRenderer();
636    }
637
638    /**
639     * {@inheritDoc} <p>
640     * 
641     * Overridden to return the delegating renderer which is wrapped around the
642     * original to support highlighting. The returned renderer is of type 
643     * DelegatingRenderer and guaranteed to not-null<p>
644     * 
645     * @see #setRenderer(ListCellRenderer)
646     * @see DelegatingRenderer
647     */
648    @Override
649    public ListCellRenderer getRenderer() {
650        // PENDING JW: something wrong here - why exactly can't we return super? 
651        // not even if we force the initial setting in init?
652//        return super.getCellRenderer();
653        return getDelegatingRenderer();
654    }
655
656    /**
657     * Returns the renderer installed by client code or the default if none has
658     * been set.
659     * 
660     * @return the wrapped renderer.
661     * @see #setRenderer(ListCellRenderer)
662     */
663    public ListCellRenderer getWrappedRenderer() {
664        return getDelegatingRenderer().getDelegateRenderer();
665    }
666
667    /**
668     * {@inheritDoc} <p>
669     * 
670     * Overridden to wrap the given renderer in a DelegatingRenderer to support
671     * highlighting. <p>
672     * 
673     * Note: the wrapping implies that the renderer returned from the getCellRenderer
674     * is <b>not</b> the renderer as given here, but the wrapper. To access the original,
675     * use <code>getWrappedCellRenderer</code>.
676     * 
677     * @see #getWrappedRenderer()
678     * @see #getRenderer()
679     */
680    @Override
681    public void setRenderer(ListCellRenderer renderer) {
682        // PENDING: do something against recursive setting
683        // == multiple delegation...
684        ListCellRenderer oldValue = super.getRenderer();
685        getDelegatingRenderer().setDelegateRenderer(renderer);
686        getStringValueRegistry().setStringValue(
687                renderer instanceof StringValue ? (StringValue) renderer : null, 0);
688        super.setRenderer(delegatingRenderer);
689        
690        if (oldValue == delegatingRenderer) {
691            firePropertyChange("renderer", null, delegatingRenderer);
692        }
693    }
694
695    /**
696     * PENDING JW to KS: review method naming - doesn't sound like valid English to me (no 
697     * native speaker of course :-). Options are to 
698     * change the property name to usingHighlightersForCurrentValue (as we did in JXMonthView
699     * after some debate) or stick to getXX. Thinking about it: maybe then the property should be
700     * usesHighlightersXX, that is third person singular instead of imperative, 
701     * like in tracksVerticalViewport of JTable?
702     * 
703     * @return {@code true} if the combo box decorates the current value with highlighters; {@code false} otherwise
704     */
705    public boolean isUseHighlightersForCurrentValue() {
706        return useHighlightersForCurrentValue;
707    }
708    
709    public void setUseHighlightersForCurrentValue(boolean useHighlightersForCurrentValue) {
710        boolean oldValue = isUseHighlightersForCurrentValue();
711        this.useHighlightersForCurrentValue = useHighlightersForCurrentValue;
712        repaint();
713        firePropertyChange("useHighlightersForCurrentValue", oldValue,
714                isUseHighlightersForCurrentValue());
715    }
716    
717    /**
718     * Returns the CompoundHighlighter assigned to the table, null if none. PENDING: open up for
719     * subclasses again?.
720     * 
721     * @return the CompoundHighlighter assigned to the table.
722     * @see #setCompoundHighlighter(CompoundHighlighter)
723     */
724    private CompoundHighlighter getCompoundHighlighter() {
725        return compoundHighlighter;
726    }
727
728    /**
729     * Assigns a CompoundHighlighter to the table, maybe null to remove all Highlighters.
730     * <p>
731     * 
732     * The default value is <code>null</code>.
733     * <p>
734     * 
735     * PENDING: open up for subclasses again?.
736     * 
737     * @param pipeline
738     *            the CompoundHighlighter to use for renderer decoration.
739     * @see #getCompoundHighlighter()
740     * @see #addHighlighter(Highlighter)
741     * @see #removeHighlighter(Highlighter)
742     * 
743     */
744    private void setCompoundHighlighter(CompoundHighlighter pipeline) {
745        CompoundHighlighter old = getCompoundHighlighter();
746        if (old != null) {
747            old.removeChangeListener(getHighlighterChangeListener());
748        }
749        compoundHighlighter = pipeline;
750        if (compoundHighlighter != null) {
751            compoundHighlighter.addChangeListener(getHighlighterChangeListener());
752        }
753        // PENDING: wrong event - the property is either "compoundHighlighter"
754        // or "highlighters" with the old/new array as value
755        firePropertyChange("highlighters", old, getCompoundHighlighter());
756    }
757
758    /**
759     * Sets the <code>Highlighter</code>s to the column, replacing any old settings. None of the
760     * given Highlighters must be null.
761     * <p>
762     * 
763     * @param highlighters
764     *            zero or more not null highlighters to use for renderer decoration.
765     * 
766     * @see #getHighlighters()
767     * @see #addHighlighter(Highlighter)
768     * @see #removeHighlighter(Highlighter)
769     * 
770     */
771    public void setHighlighters(Highlighter... highlighters) {
772        Contract.asNotNull(highlighters, "highlighters cannot be null or contain null");
773
774        CompoundHighlighter pipeline = null;
775        if (highlighters.length > 0) {
776            pipeline = new CompoundHighlighter(highlighters);
777        }
778
779        setCompoundHighlighter(pipeline);
780    }
781
782    /**
783     * Returns the <code>Highlighter</code>s used by this column. Maybe empty, but guarantees to be
784     * never null.
785     * 
786     * @return the Highlighters used by this column, guaranteed to never null.
787     * @see #setHighlighters(Highlighter[])
788     */
789    public Highlighter[] getHighlighters() {
790        return getCompoundHighlighter() != null ? getCompoundHighlighter().getHighlighters()
791                : CompoundHighlighter.EMPTY_HIGHLIGHTERS;
792    }
793
794    /**
795     * Adds a Highlighter. Appends to the end of the list of used Highlighters.
796     * <p>
797     * 
798     * @param highlighter
799     *            the <code>Highlighter</code> to add.
800     * @throws NullPointerException
801     *             if <code>Highlighter</code> is null.
802     * 
803     * @see #removeHighlighter(Highlighter)
804     * @see #setHighlighters(Highlighter[])
805     */
806    public void addHighlighter(Highlighter highlighter) {
807        CompoundHighlighter pipeline = getCompoundHighlighter();
808        if (pipeline == null) {
809            setCompoundHighlighter(new CompoundHighlighter(highlighter));
810        } else {
811            pipeline.addHighlighter(highlighter);
812        }
813    }
814
815    /**
816     * Removes the given Highlighter.
817     * <p>
818     * 
819     * Does nothing if the Highlighter is not contained.
820     * 
821     * @param highlighter
822     *            the Highlighter to remove.
823     * @see #addHighlighter(Highlighter)
824     * @see #setHighlighters(Highlighter...)
825     */
826    public void removeHighlighter(Highlighter highlighter) {
827        if ((getCompoundHighlighter() == null)) {
828            return;
829        }
830        getCompoundHighlighter().removeHighlighter(highlighter);
831    }
832
833    /**
834     * Returns the <code>ChangeListener</code> to use with highlighters. Lazily creates the
835     * listener.
836     * 
837     * @return the ChangeListener for observing changes of highlighters, guaranteed to be
838     *         <code>not-null</code>
839     */
840    protected ChangeListener getHighlighterChangeListener() {
841        if (highlighterChangeListener == null) {
842            highlighterChangeListener = createHighlighterChangeListener();
843        }
844        
845        return highlighterChangeListener;
846    }
847
848    /**
849     * Creates and returns the ChangeListener observing Highlighters.
850     * <p>
851     * A property change event is create for a state change.
852     * 
853     * @return the ChangeListener defining the reaction to changes of highlighters.
854     */
855    protected ChangeListener createHighlighterChangeListener() {
856        return new ChangeListener() {
857            @Override
858            public void stateChanged(ChangeEvent e) {
859                // need to fire change so JXComboBox can update
860                firePropertyChange("highlighters", null, getHighlighters());
861                repaint();
862            }
863        };
864    }
865    
866    /**
867     * {@inheritDoc}
868     * <p>
869     * Overridden to update renderer and highlighters.
870     */
871    @Override
872    public void updateUI() {
873        updatingUI = true;
874        
875        try {
876            super.updateUI();
877            
878            if (keySelectionManager instanceof UIDependent) {
879                ((UIDependent) keySelectionManager).updateUI();
880            }
881            
882            ListCellRenderer renderer = getRenderer();
883            
884            if (renderer instanceof UIDependent) {
885                ((UIDependent) renderer).updateUI();
886            } else if (renderer instanceof Component) {
887                SwingUtilities.updateComponentTreeUI((Component) renderer);
888            }
889            
890            if (compoundHighlighter != null) {
891                compoundHighlighter.updateUI();
892            }
893        } finally {
894            updatingUI = false;
895        }
896    }
897}