001    /*
002     * $Id: JXList.java,v 1.34 2006/05/14 08:12:16 dmouse Exp $
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;
023    
024    import java.awt.Component;
025    import java.awt.Cursor;
026    import java.awt.Point;
027    import java.awt.Rectangle;
028    import java.awt.event.ActionEvent;
029    import java.beans.PropertyChangeEvent;
030    import java.beans.PropertyChangeListener;
031    import java.util.List;
032    import java.util.Vector;
033    import java.util.regex.Matcher;
034    import java.util.regex.Pattern;
035    
036    import javax.swing.AbstractAction;
037    import javax.swing.AbstractButton;
038    import javax.swing.AbstractListModel;
039    import javax.swing.Action;
040    import javax.swing.DefaultListCellRenderer;
041    import javax.swing.JComponent;
042    import javax.swing.JList;
043    import javax.swing.JTable;
044    import javax.swing.KeyStroke;
045    import javax.swing.ListCellRenderer;
046    import javax.swing.ListModel;
047    import javax.swing.ListSelectionModel;
048    import javax.swing.event.ChangeEvent;
049    import javax.swing.event.ChangeListener;
050    import javax.swing.event.ListDataEvent;
051    import javax.swing.event.ListDataListener;
052    
053    import org.jdesktop.swingx.action.LinkAction;
054    import org.jdesktop.swingx.decorator.ComponentAdapter;
055    import org.jdesktop.swingx.decorator.FilterPipeline;
056    import org.jdesktop.swingx.decorator.HighlighterPipeline;
057    import org.jdesktop.swingx.decorator.PipelineEvent;
058    import org.jdesktop.swingx.decorator.PipelineListener;
059    import org.jdesktop.swingx.decorator.SelectionMapper;
060    import org.jdesktop.swingx.decorator.SortKey;
061    
062    /**
063     * JXList
064     * 
065     * Enabled Rollover/LinkModel handling. Enabled Highlighter support.
066     * 
067     * Added experimental support for filtering/sorting. This feature is disabled by
068     * default because it has side-effects which might break "normal" expectations
069     * when using a JList: if enabled all row coordinates (including those returned
070     * by the selection) are in view coordinates. Furthermore, the model returned
071     * from getModel() is a wrapper around the actual data.
072     * 
073     * 
074     * 
075     * @author Ramesh Gupta
076     * @author Jeanette Winzenburg
077     */
078    public class JXList extends JList {
079        public static final String EXECUTE_BUTTON_ACTIONCOMMAND = "executeButtonAction";
080    
081        /** The pipeline holding the filters. */
082        protected FilterPipeline filters;
083    
084        /**
085         * The pipeline holding the highlighters.
086         */
087        protected HighlighterPipeline highlighters;
088    
089        /** listening to changeEvents from highlighterPipeline. */
090        private ChangeListener highlighterChangeListener;
091    
092        /** The ComponentAdapter for model data access. */
093        protected ComponentAdapter dataAdapter;
094    
095        /**
096         * Mouse/Motion/Listener keeping track of mouse moved in cell coordinates.
097         */
098        private RolloverProducer rolloverProducer;
099    
100        /**
101         * RolloverController: listens to cell over events and repaints
102         * entered/exited rows.
103         */
104        private ListRolloverController linkController;
105    
106        /** A wrapper around the default renderer enabling decoration. */
107        private DelegatingRenderer delegatingRenderer;
108    
109        private WrappingListModel wrappingModel;
110    
111        private PipelineListener pipelineListener;
112    
113        private boolean filterEnabled;
114    
115        private SelectionMapper selectionMapper;
116    
117        private Searchable searchable;
118    
119        public JXList() {
120            init();
121        }
122    
123        public JXList(ListModel dataModel) {
124            super(dataModel);
125            init();
126        }
127    
128        public JXList(Object[] listData) {
129            super(listData);
130            init();
131        }
132    
133        public JXList(Vector listData) {
134            super(listData);
135            init();
136        }
137    
138        private void init() {
139            Action findAction = createFindAction();
140            getActionMap().put("find", findAction);
141            // JW: this should be handled by the LF!
142            KeyStroke findStroke = KeyStroke.getKeyStroke("control F");
143            getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(findStroke, "find");
144            
145        }
146    
147        private Action createFindAction() {
148            Action findAction = new UIAction("find") {
149    
150                public void actionPerformed(ActionEvent e) {
151                    doFind();
152                    
153                }
154                
155            };
156            return findAction;
157        }
158    
159        protected void doFind() {
160            SearchFactory.getInstance().showFindInput(this, getSearchable());
161            
162        }
163    
164        /**
165         * 
166         * @return a not-null Searchable for this editor.
167         */
168        public Searchable getSearchable() {
169            if (searchable == null) {
170                searchable = new ListSearchable();
171            }
172            return searchable;
173        }
174    
175        /**
176         * sets the Searchable for this editor. If null, a default 
177         * searchable will be used.
178         * 
179         * @param searchable
180         */
181        public void setSearchable(Searchable searchable) {
182            this.searchable = searchable;
183        }
184        
185    
186        public class ListSearchable extends AbstractSearchable {
187    
188            @Override
189            protected void findMatchAndUpdateState(Pattern pattern, int startRow, boolean backwards) {
190                SearchResult searchResult = null;
191                if (backwards) {
192                    for (int index = startRow; index >= 0 && searchResult == null; index--) {
193                        searchResult = findMatchAt(pattern, index);
194                    }
195                } else {
196                    for (int index = startRow; index < getSize() && searchResult == null; index++) {
197                        searchResult = findMatchAt(pattern, index);
198                    }
199                }
200                updateState(searchResult);
201            }
202    
203            @Override
204            protected SearchResult findExtendedMatch(Pattern pattern, int row) {
205                
206                return findMatchAt(pattern, row);
207            }
208            /**
209             * Matches the cell content at row/col against the given Pattern.
210             * Returns an appropriate SearchResult if matching or null if no
211             * matching
212             * 
213             * @param pattern 
214             * @param row a valid row index in view coordinates
215             * @return <code>SearchResult</code> if matched otherwise null
216             */
217            protected SearchResult findMatchAt(Pattern pattern, int row) {
218                Object value = getElementAt(row);
219                if (value != null) {
220                    Matcher matcher = pattern.matcher(value.toString());
221                    if (matcher.find()) {
222                        return createSearchResult(matcher, row, -1);
223                    }
224                }
225                return null;
226            }
227            
228            @Override
229            protected int getSize() {
230                return getElementCount();
231            }
232    
233            @Override
234            protected void moveMatchMarker() {
235              setSelectedIndex(lastSearchResult.foundRow);
236              if (lastSearchResult.foundRow >= 0) {
237                  ensureIndexIsVisible(lastSearchResult.foundRow);
238              }
239                
240            }
241    
242        }
243        /**
244         * Property to enable/disable rollover support. This can be enabled to show
245         * "live" rollover behaviour, f.i. the cursor over LinkModel cells. Default
246         * is disabled.
247         * 
248         * @param rolloverEnabled
249         */
250        public void setRolloverEnabled(boolean rolloverEnabled) {
251            boolean old = isRolloverEnabled();
252            if (rolloverEnabled == old)
253                return;
254            if (rolloverEnabled) {
255                rolloverProducer = createRolloverProducer();
256                addMouseListener(rolloverProducer);
257                addMouseMotionListener(rolloverProducer);
258                getLinkController().install(this);
259            } else {
260                removeMouseListener(rolloverProducer);
261                removeMouseMotionListener(rolloverProducer);
262                rolloverProducer = null;
263                getLinkController().release();
264            }
265            firePropertyChange("rolloverEnabled", old, isRolloverEnabled());
266        }
267    
268        
269        protected ListRolloverController getLinkController() {
270            if (linkController == null) {
271                linkController = createLinkController();
272            }
273            return linkController;
274        }
275    
276        protected ListRolloverController createLinkController() {
277            return new ListRolloverController();
278        }
279    
280    
281        /**
282         * creates and returns the RolloverProducer to use with this tree.
283         * 
284         * @return <code>RolloverProducer</code> to use with this tree
285         */
286        protected RolloverProducer createRolloverProducer() {
287            RolloverProducer r = new RolloverProducer() {
288                protected void updateRolloverPoint(JComponent component,
289                        Point mousePoint) {
290                    JList list = (JList) component;
291                    int row = list.locationToIndex(mousePoint);
292                    if (row >= 0) {
293                        Rectangle cellBounds = list.getCellBounds(row, row);
294                        if (!cellBounds.contains(mousePoint)) {
295                            row = -1;
296                        }
297                    }
298                    int col = row < 0 ? -1 : 0;
299                    rollover.x = col;
300                    rollover.y = row;
301                }
302    
303            };
304            return r;
305        }
306        /**
307         * returns the rolloverEnabled property.
308         *
309         * TODO: why doesn't this just return rolloverEnabled???
310         *
311         * @return true if rollover is enabled
312         */
313        public boolean isRolloverEnabled() {
314            return rolloverProducer != null;
315        }
316    
317        /**
318         * listens to rollover properties. Repaints effected component regions.
319         * Updates link cursor.
320         * 
321         * @author Jeanette Winzenburg
322         */
323        public static class ListRolloverController<T extends JList> extends
324                RolloverController<T> {
325    
326            private Cursor oldCursor;
327    
328            // --------------------------------- JList rollover
329    
330            protected void rollover(Point oldLocation, Point newLocation) {
331                setRolloverCursor(newLocation);
332                // JW: partial repaints incomplete
333                component.repaint();
334            }
335    
336            /**
337             * something weird: cursor in JList behaves different from JTable?
338             * 
339             * @param location
340             */
341            private void setRolloverCursor(Point location) {
342                if (hasRollover(location)) {
343                    oldCursor = component.getCursor();
344                    component.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
345                } else {
346                    component.setCursor(oldCursor);
347                    oldCursor = null;
348                }
349    
350            }
351    
352            protected RolloverRenderer getRolloverRenderer(Point location,
353                    boolean prepare) {
354                ListCellRenderer renderer = component.getCellRenderer();
355                RolloverRenderer rollover = renderer instanceof RolloverRenderer 
356                    ? (RolloverRenderer) renderer : null;
357                if ((rollover != null) && !rollover.isEnabled()) {
358                    rollover = null;
359                }
360                if ((rollover != null) && prepare) {
361                    Object element = component.getModel().getElementAt(location.y);
362                    renderer.getListCellRendererComponent(component, element,
363                            location.y, false, true);
364                }
365                return rollover;
366            }
367    
368            @Override
369            protected Point getFocusedCell() {
370                int leadRow = component.getLeadSelectionIndex();
371                if (leadRow < 0)
372                    return null;
373                return new Point(0, leadRow);
374            }
375    
376        }
377    
378       
379        // ---------------------------- filters
380    
381        /**
382         * returns the element at the given index. The index is in view coordinates
383         * which might differ from model coordinates if filtering is enabled and
384         * filters/sorters are active.
385         * 
386         * @param viewIndex the index in view coordinates
387         * @return the element at the index
388         * @throws IndexOutOfBoundsException if viewIndex < 0 or viewIndex >=
389         *         getElementCount()
390         */
391        public Object getElementAt(int viewIndex) {
392            return getModel().getElementAt(viewIndex);
393        }
394    
395        /**
396         * Returns the number of elements in this list in view 
397         * coordinates. If filters are active this number might be
398         * less than the number of elements in the underlying model.
399         * 
400         * @return number of elements in this list in view coordinates
401         */
402        public int getElementCount() {
403            return getModel().getSize();
404        }
405    
406        /**
407         * Convert row index from view coordinates to model coordinates accounting
408         * for the presence of sorters and filters.
409         * 
410         * @param viewIndex index in view coordinates
411         * @return index in model coordinates
412         * @throws IndexOutOfBoundsException if viewIndex < 0 or viewIndex >= getElementCount() 
413         */
414        public int convertIndexToModel(int viewIndex) {
415            return isFilterEnabled() ? getFilters().convertRowIndexToModel(
416                    viewIndex) : viewIndex;
417        }
418    
419        /**
420         * Convert index from model coordinates to view coordinates accounting
421         * for the presence of sorters and filters.
422         * 
423         * PENDING Filter guards against out of range - should not? 
424         * 
425         * @param modelIndex index in model coordinates
426         * @return index in view coordinates if the model index maps to a view coordinate
427         *          or -1 if not contained in the view.
428         * 
429         */
430        public int convertIndexToView(int modelIndex) {
431            return isFilterEnabled() ? getFilters().convertRowIndexToView(
432                    modelIndex) : modelIndex;
433        }
434    
435        /**
436         * returns the underlying model. If !isFilterEnabled this will be the same
437         * as getModel().
438         * 
439         * @return the underlying model
440         */
441        public ListModel getWrappedModel() {
442            return isFilterEnabled() ? wrappingModel.getModel() : getModel();
443        }
444    
445        /**
446         * Enables/disables filtering support. If enabled all row indices -
447         * including the selection - are in view coordinates and getModel returns a
448         * wrapper around the underlying model.
449         * 
450         * Note: as an implementation side-effect calling this method clears the
451         * selection (done in super.setModel).
452         * 
453         * PENDING: cleanup state transitions!! - currently this can be safely
454         * applied once only to enable. Internal state is inconsistent if trying to
455         * disable again.
456         * 
457         * see Issue #2-swinglabs.
458         * 
459         * @param enabled
460         */
461        public void setFilterEnabled(boolean enabled) {
462            boolean old = isFilterEnabled();
463            if (old == enabled)
464                return;
465            filterEnabled = enabled;
466            if (!old) {
467                wrappingModel = new WrappingListModel(getModel());
468                super.setModel(wrappingModel);
469            } else {
470                ListModel model = wrappingModel.getModel();
471                wrappingModel = null;
472                super.setModel(model);
473            }
474    
475        }
476    
477        public boolean isFilterEnabled() {
478            return filterEnabled;
479        }
480    
481        /**
482         * Overridden to update selectionMapper
483         */
484        @Override 
485        public void setSelectionModel(ListSelectionModel newModel) {
486            super.setSelectionModel(newModel);
487            getSelectionMapper().setViewSelectionModel(getSelectionModel());
488        }
489    
490        /**
491         * set's the underlying data model. Note that if isFilterEnabled you must
492         * call getWrappedModel to access the model given here. In this case
493         * getModel returns a wrapper around the data!
494         * 
495         * 
496         * 
497         */
498        @Override
499        public void setModel(ListModel model) {
500            if (isFilterEnabled()) {
501                wrappingModel.setModel(model);
502            } else {
503                super.setModel(model);
504            }
505        }
506    
507        /**
508         * widened access for testing...
509         * @return the selection mapper
510         */
511        protected SelectionMapper getSelectionMapper() {
512            if (selectionMapper == null) {
513                selectionMapper = new SelectionMapper(filters, getSelectionModel());
514            }
515            return selectionMapper;
516        }
517    
518        public FilterPipeline getFilters() {
519            if ((filters == null) && isFilterEnabled()) {
520                setFilters(null);
521            }
522            return filters;
523        }
524    
525        /** Sets the FilterPipeline for filtering table rows. 
526         *  PRE: isFilterEnabled()
527         * 
528         * @param pipeline the filterPipeline to use.
529         * @throws IllegalStateException if !isFilterEnabled()
530         */
531        public void setFilters(FilterPipeline pipeline) {
532            if (!isFilterEnabled()) throw
533                new IllegalStateException("filters not enabled - not allowed to set filters");
534    
535            FilterPipeline old = filters;
536            List<? extends SortKey> sortKeys = null;
537            if (old != null) {
538                old.removePipelineListener(pipelineListener);
539                sortKeys = old.getSortController().getSortKeys();
540            }
541            if (pipeline == null) {
542                pipeline = new FilterPipeline();
543            }
544            filters = pipeline;
545            filters.getSortController().setSortKeys(sortKeys);
546            // JW: first assign to prevent (short?) illegal internal state
547            // #173-swingx
548            use(filters);
549            getSelectionMapper().setFilters(filters);
550    
551        }
552    
553        /**
554         * setModel() and setFilters() may be called in either order.
555         * 
556         * @param pipeline
557         */
558        private void use(FilterPipeline pipeline) {
559            if (pipeline != null) {
560                // check JW: adding listener multiple times (after setModel)?
561                if (initialUse(pipeline)) {
562                    pipeline.addPipelineListener(getFilterPipelineListener());
563                    pipeline.assign(getComponentAdapter());
564                } else {
565                    pipeline.flush();
566                }
567            }
568        }
569    
570        /**
571         * @return true is not yet used in this JXTable, false otherwise
572         */
573        private boolean initialUse(FilterPipeline pipeline) {
574            if (pipelineListener == null)
575                return true;
576            PipelineListener[] l = pipeline.getPipelineListeners();
577            for (int i = 0; i < l.length; i++) {
578                if (pipelineListener.equals(l[i]))
579                    return false;
580            }
581            return true;
582        }
583    
584        /** returns the listener for changes in filters. */
585        protected PipelineListener getFilterPipelineListener() {
586            if (pipelineListener == null) {
587                pipelineListener = createPipelineListener();
588            }
589            return pipelineListener;
590        }
591    
592        /** creates the listener for changes in filters. */
593        protected PipelineListener createPipelineListener() {
594            PipelineListener l = new PipelineListener() {
595                public void contentsChanged(PipelineEvent e) {
596                    updateOnFilterContentChanged();
597                }
598            };
599            return l;
600        }
601    
602        /**
603         * method called on change notification from filterpipeline.
604         */
605        protected void updateOnFilterContentChanged() {
606            // make the wrapper listen to the pipeline?
607            if (wrappingModel != null) {
608                wrappingModel.updateOnFilterContentChanged();
609            }
610            revalidate();
611            repaint();
612        }
613    
614        private class WrappingListModel extends AbstractListModel {
615    
616            private ListModel delegate;
617    
618            private ListDataListener listDataListener;
619    
620            public WrappingListModel(ListModel model) {
621                setModel(model);
622            }
623    
624            public void updateOnFilterContentChanged() {
625                fireContentsChanged(this, -1, -1);
626    
627            }
628    
629            public void setModel(ListModel model) {
630                ListModel old = this.getModel();
631                if (old != null) {
632                    old.removeListDataListener(listDataListener);
633                }
634                this.delegate = model;
635                delegate.addListDataListener(getListDataListener());
636                fireContentsChanged(this, -1, -1);
637            }
638    
639            private ListDataListener getListDataListener() {
640                if (listDataListener == null) {
641                    listDataListener = createListDataListener();
642                }
643                return listDataListener;
644            }
645    
646            private ListDataListener createListDataListener() {
647                ListDataListener l = new ListDataListener() {
648    
649                    public void intervalAdded(ListDataEvent e) {
650                        contentsChanged(e);
651    
652                    }
653    
654                    public void intervalRemoved(ListDataEvent e) {
655                        contentsChanged(e);
656    
657                    }
658    
659                    public void contentsChanged(ListDataEvent e) {
660                        getSelectionMapper().lock();
661                        fireContentsChanged(this, -1, -1);
662                        updateSelection(e);
663                        getFilters().flush();
664    
665                    }
666    
667                };
668                return l;
669            }
670    
671            protected void updateSelection(ListDataEvent e) {
672                if (e.getType() == ListDataEvent.INTERVAL_REMOVED) {
673                    getSelectionMapper()
674                            .removeIndexInterval(e.getIndex0(), e.getIndex1());
675                } else if (e.getType() == ListDataEvent.INTERVAL_ADDED) {
676    
677                    int minIndex = Math.min(e.getIndex0(), e.getIndex1());
678                    int maxIndex = Math.max(e.getIndex0(), e.getIndex1());
679                    int length = maxIndex - minIndex + 1;
680                    getSelectionMapper().insertIndexInterval(minIndex, length, true);
681                } else {
682                    getSelectionMapper().clearModelSelection();
683                }
684    
685            }
686    
687            public ListModel getModel() {
688                return delegate;
689            }
690    
691            public int getSize() {
692                return getFilters().getOutputSize();
693            }
694    
695            public Object getElementAt(int index) {
696                return getFilters().getValueAt(index, 0);
697            }
698    
699        }
700    
701        // ---------------------------- uniform data model
702    
703        protected ComponentAdapter getComponentAdapter() {
704            if (dataAdapter == null) {
705                dataAdapter = new ListAdapter(this);
706            }
707            return dataAdapter;
708        }
709    
710        protected static class ListAdapter extends ComponentAdapter {
711            private final JXList list;
712    
713            /**
714             * Constructs a <code>ListDataAdapter</code> for the specified target
715             * component.
716             * 
717             * @param component
718             *            the target component
719             */
720            public ListAdapter(JXList component) {
721                super(component);
722                list = component;
723            }
724    
725            /**
726             * Typesafe accessor for the target component.
727             * 
728             * @return the target component as a {@link org.jdesktop.swingx.JXList}
729             */
730            public JXList getList() {
731                return list;
732            }
733    
734            /**
735             * {@inheritDoc}
736             */
737            public boolean hasFocus() {
738                /** TODO: Think through printing implications */
739                return list.isFocusOwner() && (row == list.getLeadSelectionIndex());
740            }
741    
742            @Override
743            public int getRowCount() {
744                return list.getWrappedModel().getSize();
745            }
746    
747            /**
748             * {@inheritDoc}
749             */
750            public Object getValueAt(int row, int column) {
751                return list.getWrappedModel().getElementAt(row);
752            }
753    
754            public Object getFilteredValueAt(int row, int column) {
755                return list.getElementAt(row);
756            }
757    
758            public void setValueAt(Object aValue, int row, int column) {
759                throw new UnsupportedOperationException(
760                        "Method getFilteredValueAt() not yet implemented.");
761            }
762    
763            public boolean isCellEditable(int row, int column) {
764                return false;
765            }
766    
767            /**
768             * {@inheritDoc}
769             */
770            public boolean isSelected() {
771                /** TODO: Think through printing implications */
772                return list.isSelectedIndex(row);
773            }
774    
775            public String getColumnName(int columnIndex) {
776                return "Column_" + columnIndex;
777            }
778    
779            public String getColumnIdentifier(int columnIndex) {
780                return null;
781            }
782    
783        }
784    
785        // ------------------------------ renderers
786    
787        public HighlighterPipeline getHighlighters() {
788            return highlighters;
789        }
790    
791        /** Assigns a HighlighterPipeline to the table. */
792        public void setHighlighters(HighlighterPipeline pipeline) {
793            HighlighterPipeline old = getHighlighters();
794            if (old != null) {
795                old.removeChangeListener(getHighlighterChangeListener());
796            }
797            highlighters = pipeline;
798            if (highlighters != null) {
799                highlighters.addChangeListener(getHighlighterChangeListener());
800            }
801            firePropertyChange("highlighters", old, getHighlighters());
802        }
803    
804        private ChangeListener getHighlighterChangeListener() {
805            if (highlighterChangeListener == null) {
806                highlighterChangeListener = new ChangeListener() {
807    
808                    public void stateChanged(ChangeEvent e) {
809                        repaint();
810                    }
811    
812                };
813            }
814            return highlighterChangeListener;
815        }
816    
817        private DelegatingRenderer getDelegatingRenderer() {
818            if (delegatingRenderer == null) {
819                // only called once... to get hold of the default?
820                delegatingRenderer = new DelegatingRenderer(super.getCellRenderer());
821            }
822            return delegatingRenderer;
823        }
824    
825        @Override
826        public ListCellRenderer getCellRenderer() {
827            return getDelegatingRenderer();
828        }
829    
830        @Override
831        public void setCellRenderer(ListCellRenderer renderer) {
832            // JW: Pending - probably fires propertyChangeEvent with wrong newValue?
833            // how about fixedCellWidths?
834            // need to test!!
835            getDelegatingRenderer().setDelegateRenderer(renderer);
836            super.setCellRenderer(delegatingRenderer);
837        }
838    
839        private class DelegatingRenderer implements ListCellRenderer, RolloverRenderer {
840    
841            private ListCellRenderer delegateRenderer;
842    
843            public DelegatingRenderer(ListCellRenderer delegate) {
844                setDelegateRenderer(delegate);
845            }
846    
847            public void setDelegateRenderer(ListCellRenderer delegate) {
848                if (delegate == null) {
849                    delegate = new DefaultListCellRenderer();
850                }
851                delegateRenderer = delegate;
852            }
853    
854            public boolean isEnabled() {
855                return (delegateRenderer instanceof RolloverRenderer) && 
856                   ((RolloverRenderer) delegateRenderer).isEnabled();
857            }
858            
859            public void doClick() {
860                if (isEnabled()) {
861                    ((RolloverRenderer) delegateRenderer).doClick();
862                }
863            }
864            public Component getListCellRendererComponent(JList list, Object value,
865                    int index, boolean isSelected, boolean cellHasFocus) {
866                Component comp = null;
867    
868                comp = delegateRenderer.getListCellRendererComponent(list,
869                            value, index, isSelected, cellHasFocus);
870                if (highlighters != null) {
871                    ComponentAdapter adapter = getComponentAdapter();
872                    adapter.column = 0;
873                    adapter.row = index;
874                    comp = highlighters.apply(comp, adapter);
875                }
876                return comp;
877            }
878    
879    
880            public void updateUI() {
881                updateRendererUI(delegateRenderer);
882            }
883    
884            private void updateRendererUI(ListCellRenderer renderer) {
885                if (renderer instanceof JComponent) {
886                    ((JComponent) renderer).updateUI();
887                } else if (renderer != null) {
888                    Component comp = renderer.getListCellRendererComponent(
889                            JXList.this, null, -1, false, false);
890                    if (comp instanceof JComponent) {
891                        ((JComponent) comp).updateUI();
892                    }
893                }
894    
895            }
896    
897        }
898    
899        // --------------------------- updateUI
900    
901        @Override
902        public void updateUI() {
903            super.updateUI();
904            updateRendererUI();
905        }
906    
907        private void updateRendererUI() {
908            if (delegatingRenderer != null) {
909                delegatingRenderer.updateUI();
910            } else {
911                ListCellRenderer renderer = getCellRenderer();
912                if (renderer instanceof JComponent) {
913                    ((JComponent) renderer).updateUI();
914                }
915            }
916        }
917    
918    }