001 /*
002 * $Id: JXList.java 3401 2009-07-22 18:09:08Z kschaefe $
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.Point;
026 import java.awt.event.ActionEvent;
027 import java.util.ArrayList;
028 import java.util.Collections;
029 import java.util.Comparator;
030 import java.util.List;
031 import java.util.Vector;
032 import java.util.logging.Logger;
033
034 import javax.swing.AbstractListModel;
035 import javax.swing.Action;
036 import javax.swing.JComponent;
037 import javax.swing.JList;
038 import javax.swing.KeyStroke;
039 import javax.swing.ListCellRenderer;
040 import javax.swing.ListModel;
041 import javax.swing.ListSelectionModel;
042 import javax.swing.SortOrder;
043 import javax.swing.SwingUtilities;
044 import javax.swing.RowSorter.SortKey;
045 import javax.swing.event.ChangeEvent;
046 import javax.swing.event.ChangeListener;
047 import javax.swing.event.ListDataEvent;
048 import javax.swing.event.ListDataListener;
049 import javax.swing.plaf.ListUI;
050
051 import org.jdesktop.swingx.decorator.ComponentAdapter;
052 import org.jdesktop.swingx.decorator.CompoundHighlighter;
053 import org.jdesktop.swingx.decorator.Highlighter;
054 import org.jdesktop.swingx.plaf.LookAndFeelAddons;
055 import org.jdesktop.swingx.plaf.XListAddon;
056 import org.jdesktop.swingx.renderer.AbstractRenderer;
057 import org.jdesktop.swingx.renderer.DefaultListRenderer;
058 import org.jdesktop.swingx.renderer.StringValue;
059 import org.jdesktop.swingx.renderer.StringValues;
060 import org.jdesktop.swingx.rollover.ListRolloverController;
061 import org.jdesktop.swingx.rollover.ListRolloverProducer;
062 import org.jdesktop.swingx.rollover.RolloverProducer;
063 import org.jdesktop.swingx.rollover.RolloverRenderer;
064 import org.jdesktop.swingx.search.ListSearchable;
065 import org.jdesktop.swingx.search.SearchFactory;
066 import org.jdesktop.swingx.search.Searchable;
067 import org.jdesktop.swingx.sort.SortController;
068 import org.jdesktop.swingx.sort.SortUtils;
069
070 /**
071 * Enhanced List component with support for general SwingX sorting/filtering,
072 * rendering, highlighting, rollover and search functionality. List specific
073 * enhancements include ?? PENDING JW ...
074 *
075 * <h2>Sorting and Filtering</h2>
076 *
077 * <b>NOTE: sorting/filtering is currently disabled. All related methods are
078 * dis-functional, the overriden methods behave just the same as JList. </b>
079 * <p>
080 * JXList supports sorting and filtering.
081 *
082 * It provides api to apply a specific sort order, to toggle the sort order and to reset a sort.
083 * Sort sequence can be configured by setting a custom comparator.
084 *
085 * <pre><code>
086 * list.setFilterEnabled(true);
087 * list.setComparator(myComparator);
088 * list.setSortOrder(SortOrder.DESCENDING);
089 * list.toggleSortOder();
090 * list.resetSortOrder();
091 * </code></pre>
092 *
093 * <p>
094 * JXList provides api to access items of the underlying model in view coordinates
095 * and to convert from/to model coordinates.
096 *
097 * <b>Note</b>: List sorting/filtering is disabled by
098 * default because it has side-effects which might break "normal" expectations
099 * when using a JList: if enabled all row coordinates (including those returned
100 * by the selection) are in view coordinates. Furthermore, the model returned
101 * from getModel() is a wrapper around the actual data.
102 *
103 * <b>Note:</b> SwingX sorting/filtering is incompatible with core sorting/filtering in
104 * JDK 6+. Will be replaced by core functionality after switching the target jdk
105 * version from 5 to 6, we are on the move right now.
106 *
107 *
108 * <h2>Rendering and Highlighting</h2>
109 *
110 * As all SwingX collection views, a JXList is a HighlighterClient (PENDING JW:
111 * formally define and implement, like in AbstractTestHighlighter), that is it
112 * provides consistent api to add and remove Highlighters which can visually
113 * decorate the rendering component.
114 * <p>
115 *
116 * <pre><code>
117 *
118 * JXList list = new JXList(new Contributors());
119 * // implement a custom string representation, concated from first-, lastName
120 * StringValue sv = new StringValue() {
121 * public String getString(Object value) {
122 * if (value instanceof Contributor) {
123 * Contributor contributor = (Contributor) value;
124 * return contributor.lastName() + ", " + contributor.firstName();
125 * }
126 * return StringValues.TO_STRING(value);
127 * }
128 * };
129 * list.setCellRenderer(new DefaultListRenderer(sv);
130 * // highlight condition: gold merits
131 * HighlightPredicate predicate = new HighlightPredicate() {
132 * public boolean isHighlighted(Component renderer,
133 * ComponentAdapter adapter) {
134 * if (!(value instanceof Contributor)) return false;
135 * return ((Contributor) value).hasGold();
136 * }
137 * };
138 * // highlight with foreground color
139 * list.addHighlighter(new PainterHighlighter(predicate, goldStarPainter);
140 *
141 * </code></pre>
142 *
143 * <i>Note:</i> to support the highlighting this implementation wraps the
144 * ListCellRenderer set by client code with a DelegatingRenderer which applies
145 * the Highlighter after delegating the default configuration to the wrappee. As
146 * a side-effect, getCellRenderer does return the wrapper instead of the custom
147 * renderer. To access the latter, client code must call getWrappedCellRenderer.
148 * <p>
149 *
150 * <h2>Rollover</h2>
151 *
152 * As all SwingX collection views, a JXList supports per-cell rollover. If
153 * enabled, the component fires rollover events on enter/exit of a cell which by
154 * default is promoted to the renderer if it implements RolloverRenderer, that
155 * is simulates live behaviour. The rollover events can be used by client code
156 * as well, f.i. to decorate the rollover row using a Highlighter.
157 *
158 * <pre><code>
159 *
160 * JXList list = new JXList();
161 * list.setRolloverEnabled(true);
162 * list.setCellRenderer(new DefaultListRenderer());
163 * list.addHighlighter(new ColorHighlighter(HighlightPredicate.ROLLOVER_ROW,
164 * null, Color.RED);
165 *
166 * </code></pre>
167 *
168 *
169 * <h2>Search</h2>
170 *
171 * As all SwingX collection views, a JXList is searchable. A search action is
172 * registered in its ActionMap under the key "find". The default behaviour is to
173 * ask the SearchFactory to open a search component on this component. The
174 * default keybinding is retrieved from the SearchFactory, typically ctrl-f (or
175 * cmd-f for Mac). Client code can register custom actions and/or bindings as
176 * appropriate.
177 * <p>
178 *
179 * JXList provides api to vend a renderer-controlled String representation of
180 * cell content. This allows the Searchable and Highlighters to use WYSIWYM
181 * (What-You-See-Is-What-You-Match), that is pattern matching against the actual
182 * string as seen by the user.
183 *
184 *
185 * @author Ramesh Gupta
186 * @author Jeanette Winzenburg
187 */
188 public class JXList extends JList {
189 @SuppressWarnings("all")
190 private static final Logger LOG = Logger.getLogger(JXList.class.getName());
191
192 /**
193 * UI Class ID
194 */
195 public final static String uiClassID = "XListUI";
196
197 /**
198 * Registers a Addon for JXList.
199 */
200 // @KEEP JW- will be used if sortable/filterable again
201 static {
202 LookAndFeelAddons.contribute(new XListAddon());
203 }
204
205
206
207 public static final String EXECUTE_BUTTON_ACTIONCOMMAND = "executeButtonAction";
208
209 /**
210 * The pipeline holding the highlighters.
211 */
212 protected CompoundHighlighter compoundHighlighter;
213
214 /** listening to changeEvents from compoundHighlighter. */
215 private ChangeListener highlighterChangeListener;
216
217 /** The ComponentAdapter for model data access. */
218 protected ComponentAdapter dataAdapter;
219
220 /**
221 * Mouse/Motion/Listener keeping track of mouse moved in cell coordinates.
222 */
223 private RolloverProducer rolloverProducer;
224
225 /**
226 * RolloverController: listens to cell over events and repaints
227 * entered/exited rows.
228 */
229 private ListRolloverController<JXList> linkController;
230
231 /** A wrapper around the default renderer enabling decoration. */
232 private DelegatingRenderer delegatingRenderer;
233
234 private boolean filterEnabled;
235
236 private Searchable searchable;
237
238 private Comparator<?> comparator;
239
240 /**
241 * Constructs a <code>JXList</code> with an empty model and filters disabled.
242 *
243 */
244 public JXList() {
245 this(false);
246 }
247
248 /**
249 * Constructs a <code>JXList</code> that displays the elements in the
250 * specified, non-<code>null</code> model and filters disabled.
251 *
252 * @param dataModel the data model for this list
253 * @exception IllegalArgumentException if <code>dataModel</code>
254 * is <code>null</code>
255 */
256 public JXList(ListModel dataModel) {
257 this(dataModel, false);
258 }
259
260 /**
261 * Constructs a <code>JXList</code> that displays the elements in
262 * the specified array and filters disabled.
263 *
264 * @param listData the array of Objects to be loaded into the data model
265 * @throws IllegalArgumentException if <code>listData</code>
266 * is <code>null</code>
267 */
268 public JXList(Object[] listData) {
269 this(listData, false);
270 }
271
272 /**
273 * Constructs a <code>JXList</code> that displays the elements in
274 * the specified <code>Vector</code> and filtes disabled.
275 *
276 * @param listData the <code>Vector</code> to be loaded into the
277 * data model
278 * @throws IllegalArgumentException if <code>listData</code>
279 * is <code>null</code>
280 */
281 public JXList(Vector<?> listData) {
282 this(listData, false);
283 }
284
285
286 /**
287 * Constructs a <code>JXList</code> with an empty model and
288 * filterEnabled property.
289 *
290 * @param filterEnabled <code>boolean</code> to determine if
291 * filtering/sorting is enabled
292 */
293 public JXList(boolean filterEnabled) {
294 init(filterEnabled);
295 }
296
297 /**
298 * Constructs a <code>JXList</code> with the specified model and
299 * filterEnabled property.
300 *
301 * @param dataModel the data model for this list
302 * @param filterEnabled <code>boolean</code> to determine if
303 * filtering/sorting is enabled
304 * @throws IllegalArgumentException if <code>dataModel</code>
305 * is <code>null</code>
306 */
307 public JXList(ListModel dataModel, boolean filterEnabled) {
308 super(dataModel);
309 init(filterEnabled);
310 }
311
312 /**
313 * Constructs a <code>JXList</code> that displays the elements in
314 * the specified array and filterEnabled property.
315 *
316 * @param listData the array of Objects to be loaded into the data model
317 * @param filterEnabled <code>boolean</code> to determine if filtering/sorting
318 * is enabled
319 * @throws IllegalArgumentException if <code>listData</code>
320 * is <code>null</code>
321 */
322 public JXList(Object[] listData, boolean filterEnabled) {
323 super(listData);
324 if (listData == null)
325 throw new IllegalArgumentException("listData must not be null");
326 init(filterEnabled);
327 }
328
329 /**
330 * Constructs a <code>JXList</code> that displays the elements in
331 * the specified <code>Vector</code> and filtersEnabled property.
332 *
333 * @param listData the <code>Vector</code> to be loaded into the
334 * data model
335 * @param filterEnabled <code>boolean</code> to determine if filtering/sorting
336 * is enabled
337 * @throws IllegalArgumentException if <code>listData</code> is <code>null</code>
338 */
339 public JXList(Vector<?> listData, boolean filterEnabled) {
340 super(listData);
341 if (listData == null)
342 throw new IllegalArgumentException("listData must not be null");
343 init(filterEnabled);
344 }
345
346
347 private void init(boolean filterEnabled) {
348 setFilterEnabled(filterEnabled);
349
350 Action findAction = createFindAction();
351 getActionMap().put("find", findAction);
352
353 KeyStroke findStroke = SearchFactory.getInstance().getSearchAccelerator();
354 getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(findStroke, "find");
355
356 }
357
358 private Action createFindAction() {
359 return new UIAction("find") {
360 public void actionPerformed(ActionEvent e) {
361 doFind();
362 }
363 };
364 }
365
366 /**
367 * Starts a search on this List's visible items. This implementation asks the
368 * SearchFactory to open a find widget on itself.
369 */
370 protected void doFind() {
371 SearchFactory.getInstance().showFindInput(this, getSearchable());
372 }
373
374 /**
375 * Returns a Searchable for this component, guaranteed to be not null. This
376 * implementation lazily creates a ListSearchable if necessary.
377 *
378 * @return a not-null Searchable for this list.
379 *
380 * @see #setSearchable(Searchable)
381 * @see org.jdesktop.swingx.search.ListSearchable
382 */
383 public Searchable getSearchable() {
384 if (searchable == null) {
385 searchable = new ListSearchable(this);
386 }
387 return searchable;
388 }
389
390 /**
391 * Sets the Searchable for this component. If null, a default
392 * Searchable will be created and used.
393 *
394 * @param searchable the Searchable to use for this component, may be null to indicate
395 * using the list's default searchable.
396 * @see #getSearchable()
397 */
398 public void setSearchable(Searchable searchable) {
399 this.searchable = searchable;
400 }
401
402 //--------------------- Rollover support
403
404 /**
405 * Sets the property to enable/disable rollover support. If enabled, the list
406 * fires property changes on per-cell mouse rollover state, i.e.
407 * when the mouse enters/leaves a list cell. <p>
408 *
409 * This can be enabled to show "live" rollover behaviour, f.i. the cursor over a cell
410 * rendered by a JXHyperlink.<p>
411 *
412 * Default value is disabled.
413 *
414 * @param rolloverEnabled a boolean indicating whether or not the rollover
415 * functionality should be enabled.
416 *
417 * @see #isRolloverEnabled()
418 * @see #getLinkController()
419 * @see #createRolloverProducer()
420 * @see org.jdesktop.swingx.rollover.RolloverRenderer
421 *
422 */
423 public void setRolloverEnabled(boolean rolloverEnabled) {
424 boolean old = isRolloverEnabled();
425 if (rolloverEnabled == old)
426 return;
427 if (rolloverEnabled) {
428 rolloverProducer = createRolloverProducer();
429 addMouseListener(rolloverProducer);
430 addMouseMotionListener(rolloverProducer);
431 getLinkController().install(this);
432 } else {
433 removeMouseListener(rolloverProducer);
434 removeMouseMotionListener(rolloverProducer);
435 rolloverProducer = null;
436 getLinkController().release();
437 }
438 firePropertyChange("rolloverEnabled", old, isRolloverEnabled());
439 }
440
441 /**
442 * Returns a boolean indicating whether or not rollover support is enabled.
443 *
444 * @return a boolean indicating whether or not rollover support is enabled.
445 *
446 * @see #setRolloverEnabled(boolean)
447 */
448 public boolean isRolloverEnabled() {
449 return rolloverProducer != null;
450 }
451
452 /**
453 * Returns the RolloverController for this component. Lazyly creates the
454 * controller if necessary, that is the return value is guaranteed to be
455 * not null. <p>
456 *
457 * PENDING JW: rename to getRolloverController
458 *
459 * @return the RolloverController for this tree, guaranteed to be not null.
460 *
461 * @see #setRolloverEnabled(boolean)
462 * @see #createLinkController()
463 * @see org.jdesktop.swingx.rollover.RolloverController
464 */
465 protected ListRolloverController<JXList> getLinkController() {
466 if (linkController == null) {
467 linkController = createLinkController();
468 }
469 return linkController;
470 }
471
472 /**
473 * Creates and returns a RolloverController appropriate for this component.
474 *
475 * @return a RolloverController appropriate for this component.
476 *
477 * @see #getLinkController()
478 * @see org.jdesktop.swingx.rollover.RolloverController
479 */
480 protected ListRolloverController<JXList> createLinkController() {
481 return new ListRolloverController<JXList>();
482 }
483
484
485 /**
486 * Creates and returns the RolloverProducer to use with this tree.
487 * <p>
488 *
489 * @return <code>RolloverProducer</code> to use with this tree
490 *
491 * @see #setRolloverEnabled(boolean)
492 */
493 protected RolloverProducer createRolloverProducer() {
494 return new ListRolloverProducer();
495 }
496
497 //--------------------- public sort api
498 // /**
499 // * Returns the sortable property.
500 // * Here: same as filterEnabled.
501 // * @return true if the table is sortable.
502 // */
503 // public boolean isSortable() {
504 // return isFilterEnabled();
505 // }
506 /**
507 * Removes the interactive sorter.
508 *
509 */
510 public void resetSortOrder() {
511 SortController controller = getSortController();
512 if (controller != null) {
513 controller.resetSortOrders();
514 }
515 }
516
517 /**
518 *
519 * Toggles the sort order of the items.
520 * <p>
521 * The exact behaviour is defined by the SortController's
522 * toggleSortOrder implementation. Typically a unsorted
523 * column is sorted in ascending order, a sorted column's
524 * order is reversed.
525 * <p>
526 * PENDING: where to get the comparator from?
527 * <p>
528 *
529 *
530 */
531 public void toggleSortOrder() {
532 SortController controller = getSortController();
533 if (controller != null) {
534 controller.toggleSortOrder(0);
535 }
536 }
537
538 /**
539 * Sorts the list using SortOrder.
540 *
541 *
542 * Respects the JXList's sortable and comparator
543 * properties: routes the comparator to the SortController
544 * and does nothing if !isFilterEnabled().
545 * <p>
546 *
547 * @param sortOrder the sort order to use. If null or SortOrder.UNSORTED,
548 * this method has the same effect as resetSortOrder();
549 *
550 */
551 public void setSortOrder(SortOrder sortOrder) {
552 if (!SortUtils.isSorted(sortOrder)) {
553 resetSortOrder();
554 return;
555 }
556 SortController sortController = getSortController();
557 if (sortController != null) {
558 // sortController.setSortOrder(0, sortOrder);
559 }
560 }
561
562
563 /**
564 * Returns the SortOrder.
565 *
566 * @return the interactive sorter's SortOrder
567 * or SortOrder.UNSORTED
568 */
569 public SortOrder getSortOrder() {
570 SortController sortController = getSortController();
571 if (sortController == null) return SortOrder.UNSORTED;
572 return sortController.getSortOrder(0);
573 }
574
575 /**
576 *
577 * @return the comparator used.
578 * @see #setComparator(Comparator)
579 */
580 public Comparator<?> getComparator() {
581 return comparator;
582 }
583
584 /**
585 * Sets the comparator used. As a side-effect, the
586 * current sort might be updated. The exact behaviour
587 * is defined in #updateSortAfterComparatorChange.
588 *
589 * @param comparator the comparator to use.
590 */
591 public void setComparator(Comparator<?> comparator) {
592 Comparator<?> old = getComparator();
593 this.comparator = comparator;
594 updateSortAfterComparatorChange();
595 firePropertyChange("comparator", old, getComparator());
596 }
597
598 /**
599 * Updates sort after comparator has changed.
600 * Here: sets the current sortOrder with the new comparator.
601 *
602 */
603 protected void updateSortAfterComparatorChange() {
604 setSortOrder(getSortOrder());
605
606 }
607
608 /**
609 * returns the currently active SortController. Will be null if
610 * !isFilterEnabled().
611 * @return the currently active <code>SortController</code> may be null
612 */
613 protected SortController getSortController() {
614 return null;
615 }
616
617
618 // ---------------------------- filters
619
620 /**
621 * returns the element at the given index. The index is in view coordinates
622 * which might differ from model coordinates if filtering is enabled and
623 * filters/sorters are active.
624 *
625 * @param viewIndex the index in view coordinates
626 * @return the element at the index
627 * @throws IndexOutOfBoundsException if viewIndex < 0 or viewIndex >=
628 * getElementCount()
629 */
630 public Object getElementAt(int viewIndex) {
631 return getModel().getElementAt(viewIndex);
632 }
633
634 /**
635 * Returns the number of elements in this list in view
636 * coordinates. If filters are active this number might be
637 * less than the number of elements in the underlying model.
638 *
639 * @return number of elements in this list in view coordinates
640 */
641 public int getElementCount() {
642 return getModel().getSize();
643 }
644
645 /**
646 * Convert row index from view coordinates to model coordinates accounting
647 * for the presence of sorters and filters.
648 *
649 * @param viewIndex index in view coordinates
650 * @return index in model coordinates
651 * @throws IndexOutOfBoundsException if viewIndex < 0 or viewIndex >= getElementCount()
652 */
653 public int convertIndexToModel(int viewIndex) {
654 return viewIndex;
655 }
656
657 /**
658 * Convert index from model coordinates to view coordinates accounting
659 * for the presence of sorters and filters.
660 *
661 * PENDING Filter guards against out of range - should not?
662 *
663 * @param modelIndex index in model coordinates
664 * @return index in view coordinates if the model index maps to a view coordinate
665 * or -1 if not contained in the view.
666 *
667 */
668 public int convertIndexToView(int modelIndex) {
669 return modelIndex;
670 }
671
672 /**
673 * returns the underlying model. If !isFilterEnabled this will be the same
674 * as getModel().
675 *
676 * @return the underlying model
677 */
678 public ListModel getWrappedModel() {
679 return /* isFilterEnabled() ? wrappingModel.getModel() : */ getModel();
680 }
681
682 /**
683 * Enables/disables filtering support. If enabled all row indices -
684 * including the selection - are in view coordinates and getModel returns a
685 * wrapper around the underlying model.<p>
686 *
687 * <b>NOTE</b>: Currently, this method has no effect - filtering/sorting of
688 * a JXList is disabled until SwingX is fully moved to the core style
689 * filtering/sorting. <p>
690 *
691 * Note: as an implementation side-effect calling this method clears the
692 * selection (done in super.setModel).<p>
693 *
694 * PENDING: cleanup state transitions!! - currently this can be safely
695 * applied once only to enable. Internal state is inconsistent if trying to
696 * disable again. As a temporary emergency measure, this will throw a
697 * IllegalStateException.
698 *
699 * see Issue #2-swinglabs.
700 *
701 *
702 * @param enabled
703 * @throws IllegalStateException if trying to disable again.
704 */
705 public void setFilterEnabled(boolean enabled) {
706 return;
707 // boolean old = isFilterEnabled();
708 // if (old == enabled)
709 // return;
710 // if (old)
711 // throw new IllegalStateException("must not reset filterEnabled");
712 // // JW: filterEnabled must be set before calling super.setModel!
713 // filterEnabled = enabled;
714 // wrappingModel = new WrappingListModel(getModel());
715 // super.setModel(wrappingModel);
716 // firePropertyChange("filterEnabled", old, isFilterEnabled());
717 }
718
719 /**
720 *
721 * @return a <boolean> indicating if filtering is enabled.
722 * @see #setFilterEnabled(boolean)
723 */
724 public boolean isFilterEnabled() {
725 return filterEnabled;
726 }
727
728 /**
729 * {@inheritDoc} <p>
730 *
731 * Overridden to update selectionMapper
732 */
733 @Override
734 public void setSelectionModel(ListSelectionModel newModel) {
735 super.setSelectionModel(newModel);
736 // getSelectionMapper().setViewSelectionModel(getSelectionModel());
737 }
738
739 /**
740 * {@inheritDoc} <p>
741 *
742 * Sets the underlying data model. Note that if isFilterEnabled you must
743 * call getWrappedModel to access the model given here. In this case
744 * getModel returns a wrapper around the data!
745 *
746 * @param model the data model for this list.
747 *
748 */
749 @Override
750 public void setModel(ListModel model) {
751 super.setModel(model);
752 // if (isFilterEnabled()) {
753 // wrappingModel.setModel(model);
754 // } else {
755 // super.setModel(model);
756 // }
757 }
758
759
760 // ---------------------------- uniform data model
761
762 /**
763 * @return the unconfigured ComponentAdapter.
764 */
765 protected ComponentAdapter getComponentAdapter() {
766 if (dataAdapter == null) {
767 dataAdapter = new ListAdapter(this);
768 }
769 return dataAdapter;
770 }
771
772 /**
773 * Convenience to access a configured ComponentAdapter.
774 * Note: the column index of the configured adapter is always 0.
775 *
776 * @param index the row index in view coordinates, must be valid.
777 * @return the configured ComponentAdapter.
778 */
779 protected ComponentAdapter getComponentAdapter(int index) {
780 ComponentAdapter adapter = getComponentAdapter();
781 adapter.column = 0;
782 adapter.row = index;
783 return adapter;
784 }
785
786 /**
787 * A component adapter targeted at a JXList.
788 */
789 protected static class ListAdapter extends ComponentAdapter {
790 private final JXList list;
791
792 /**
793 * Constructs a <code>ListAdapter</code> for the specified target
794 * JXList.
795 *
796 * @param component the target list.
797 */
798 public ListAdapter(JXList component) {
799 super(component);
800 list = component;
801 }
802
803 /**
804 * Typesafe accessor for the target component.
805 *
806 * @return the target component as a {@link org.jdesktop.swingx.JXList}
807 */
808 public JXList getList() {
809 return list;
810 }
811
812 /**
813 * {@inheritDoc}
814 */
815 @Override
816 public boolean hasFocus() {
817 /** TODO: Think through printing implications */
818 return list.isFocusOwner() && (row == list.getLeadSelectionIndex());
819 }
820
821 /**
822 * {@inheritDoc}
823 */
824 @Override
825 public int getRowCount() {
826 return list.getWrappedModel().getSize();
827 }
828
829 /**
830 * {@inheritDoc} <p>
831 * Overridden to return value at implicit view coordinates.
832 */
833 @Override
834 public Object getValue() {
835 return list.getElementAt(row);
836 }
837
838 /**
839 * {@inheritDoc}
840 */
841 @Override
842 public Object getValueAt(int row, int column) {
843 return list.getWrappedModel().getElementAt(row);
844 }
845
846 /**
847 * {@inheritDoc}
848 */
849 @Override
850 public Object getFilteredValueAt(int row, int column) {
851 return list.getElementAt(row);
852 }
853
854
855 /**
856 * {@inheritDoc}
857 */
858 @Override
859 public String getFilteredStringAt(int row, int column) {
860 return list.getStringAt(row);
861 }
862
863 /**
864 * {@inheritDoc}
865 */
866 @Override
867 public String getString() {
868 return list.getStringAt(row);
869 }
870
871 /**
872 * {@inheritDoc}
873 */
874 @Override
875 public String getStringAt(int row, int column) {
876 // PENDING JW: here we are duplicating code from the list
877 // that's because list api is in view-coordinates
878 ListCellRenderer renderer = list.getDelegatingRenderer().getDelegateRenderer();
879 if (renderer instanceof StringValue) {
880 return ((StringValue) renderer).getString(getValueAt(row, column));
881 }
882 return StringValues.TO_STRING.getString(getValueAt(row, column));
883 }
884
885 /**
886 * {@inheritDoc}
887 */
888 @Override
889 public boolean isCellEditable(int row, int column) {
890 return false;
891 }
892
893 /**
894 * {@inheritDoc}
895 */
896 @Override
897 public boolean isEditable() {
898 return false;
899 }
900
901 /**
902 * {@inheritDoc}
903 */
904 @Override
905 public boolean isSelected() {
906 /** TODO: Think through printing implications */
907 return list.isSelectedIndex(row);
908 }
909
910 }
911
912 // ------------------------------ renderers
913
914
915
916 /**
917 * Sets the <code>Highlighter</code>s to the table, replacing any old settings.
918 * None of the given Highlighters must be null.<p>
919 *
920 * This is a bound property. <p>
921 *
922 * Note: as of version #1.257 the null constraint is enforced strictly. To remove
923 * all highlighters use this method without param.
924 *
925 * @param highlighters zero or more not null highlighters to use for renderer decoration.
926 * @throws NullPointerException if array is null or array contains null values.
927 *
928 * @see #getHighlighters()
929 * @see #addHighlighter(Highlighter)
930 * @see #removeHighlighter(Highlighter)
931 *
932 */
933 public void setHighlighters(Highlighter... highlighters) {
934 Highlighter[] old = getHighlighters();
935 getCompoundHighlighter().setHighlighters(highlighters);
936 firePropertyChange("highlighters", old, getHighlighters());
937 }
938
939 /**
940 * Returns the <code>Highlighter</code>s used by this table.
941 * Maybe empty, but guarantees to be never null.
942 *
943 * @return the Highlighters used by this table, guaranteed to never null.
944 * @see #setHighlighters(Highlighter[])
945 */
946 public Highlighter[] getHighlighters() {
947 return getCompoundHighlighter().getHighlighters();
948 }
949 /**
950 * Appends a <code>Highlighter</code> to the end of the list of used
951 * <code>Highlighter</code>s. The argument must not be null.
952 * <p>
953 *
954 * @param highlighter the <code>Highlighter</code> to add, must not be null.
955 * @throws NullPointerException if <code>Highlighter</code> is null.
956 *
957 * @see #removeHighlighter(Highlighter)
958 * @see #setHighlighters(Highlighter[])
959 */
960 public void addHighlighter(Highlighter highlighter) {
961 Highlighter[] old = getHighlighters();
962 getCompoundHighlighter().addHighlighter(highlighter);
963 firePropertyChange("highlighters", old, getHighlighters());
964 }
965
966 /**
967 * Removes the given Highlighter. <p>
968 *
969 * Does nothing if the Highlighter is not contained.
970 *
971 * @param highlighter the Highlighter to remove.
972 * @see #addHighlighter(Highlighter)
973 * @see #setHighlighters(Highlighter...)
974 */
975 public void removeHighlighter(Highlighter highlighter) {
976 Highlighter[] old = getHighlighters();
977 getCompoundHighlighter().removeHighlighter(highlighter);
978 firePropertyChange("highlighters", old, getHighlighters());
979 }
980
981 /**
982 * Returns the CompoundHighlighter assigned to the table, null if none.
983 * PENDING: open up for subclasses again?.
984 *
985 * @return the CompoundHighlighter assigned to the table.
986 */
987 protected CompoundHighlighter getCompoundHighlighter() {
988 if (compoundHighlighter == null) {
989 compoundHighlighter = new CompoundHighlighter();
990 compoundHighlighter.addChangeListener(getHighlighterChangeListener());
991 }
992 return compoundHighlighter;
993 }
994
995 /**
996 * Returns the <code>ChangeListener</code> to use with highlighters. Lazily
997 * creates the listener.
998 *
999 * @return the ChangeListener for observing changes of highlighters,
1000 * guaranteed to be <code>not-null</code>
1001 */
1002 protected ChangeListener getHighlighterChangeListener() {
1003 if (highlighterChangeListener == null) {
1004 highlighterChangeListener = createHighlighterChangeListener();
1005 }
1006 return highlighterChangeListener;
1007 }
1008
1009 /**
1010 * Creates and returns the ChangeListener observing Highlighters.
1011 * <p>
1012 * Here: repaints the table on receiving a stateChanged.
1013 *
1014 * @return the ChangeListener defining the reaction to changes of
1015 * highlighters.
1016 */
1017 protected ChangeListener createHighlighterChangeListener() {
1018 return new ChangeListener() {
1019 public void stateChanged(ChangeEvent e) {
1020 repaint();
1021 }
1022 };
1023 }
1024
1025
1026 /**
1027 * Returns the string representation of the cell value at the given position.
1028 *
1029 * @param row the row index of the cell in view coordinates
1030 * @return the string representation of the cell value as it will appear in the
1031 * table.
1032 */
1033 public String getStringAt(int row) {
1034 ListCellRenderer renderer = getDelegatingRenderer().getDelegateRenderer();
1035 if (renderer instanceof StringValue) {
1036 return ((StringValue) renderer).getString(getElementAt(row));
1037 }
1038 return StringValues.TO_STRING.getString(getElementAt(row));
1039 }
1040
1041 private DelegatingRenderer getDelegatingRenderer() {
1042 if (delegatingRenderer == null) {
1043 // only called once... to get hold of the default?
1044 delegatingRenderer = new DelegatingRenderer();
1045 }
1046 return delegatingRenderer;
1047 }
1048
1049 /**
1050 * Creates and returns the default cell renderer to use. Subclasses
1051 * may override to use a different type. Here: returns a <code>DefaultListRenderer</code>.
1052 *
1053 * @return the default cell renderer to use with this list.
1054 */
1055 protected ListCellRenderer createDefaultCellRenderer() {
1056 return new DefaultListRenderer();
1057 }
1058
1059 /**
1060 * {@inheritDoc} <p>
1061 *
1062 * Overridden to return the delegating renderer which is wrapped around the
1063 * original to support highlighting. The returned renderer is of type
1064 * DelegatingRenderer and guaranteed to not-null<p>
1065 *
1066 * @see #setCellRenderer(ListCellRenderer)
1067 * @see DelegatingRenderer
1068 */
1069 @Override
1070 public ListCellRenderer getCellRenderer() {
1071 return getDelegatingRenderer();
1072 }
1073
1074 /**
1075 * Returns the renderer installed by client code or the default if none has
1076 * been set.
1077 *
1078 * @return the wrapped renderer.
1079 * @see #setCellRenderer(ListCellRenderer)
1080 */
1081 public ListCellRenderer getWrappedCellRenderer() {
1082 return getDelegatingRenderer().getDelegateRenderer();
1083 }
1084
1085 /**
1086 * {@inheritDoc} <p>
1087 *
1088 * Overridden to wrap the given renderer in a DelegatingRenderer to support
1089 * highlighting. <p>
1090 *
1091 * Note: the wrapping implies that the renderer returned from the getCellRenderer
1092 * is <b>not</b> the renderer as given here, but the wrapper. To access the original,
1093 * use <code>getWrappedCellRenderer</code>.
1094 *
1095 * @see #getWrappedCellRenderer()
1096 * @see #getCellRenderer()
1097 *
1098 */
1099 @Override
1100 public void setCellRenderer(ListCellRenderer renderer) {
1101 // JW: Pending - probably fires propertyChangeEvent with wrong newValue?
1102 // how about fixedCellWidths?
1103 // need to test!!
1104 getDelegatingRenderer().setDelegateRenderer(renderer);
1105 super.setCellRenderer(delegatingRenderer);
1106 }
1107
1108 /**
1109 * A decorator for the original ListCellRenderer. Needed to hook highlighters
1110 * after messaging the delegate.<p>
1111 *
1112 * PENDING JW: formally implement UIDependent?
1113 */
1114 public class DelegatingRenderer implements ListCellRenderer, RolloverRenderer {
1115 /** the delegate. */
1116 private ListCellRenderer delegateRenderer;
1117
1118 /**
1119 * Instantiates a DelegatingRenderer with list's default renderer as delegate.
1120 */
1121 public DelegatingRenderer() {
1122 this(null);
1123 }
1124
1125 /**
1126 * Instantiates a DelegatingRenderer with the given delegate. If the
1127 * delegate is null, the default is created via the list's factory method.
1128 *
1129 * @param delegate the delegate to use, if null the list's default is
1130 * created and used.
1131 */
1132 public DelegatingRenderer(ListCellRenderer delegate) {
1133 setDelegateRenderer(delegate);
1134 }
1135
1136 /**
1137 * Sets the delegate. If the
1138 * delegate is null, the default is created via the list's factory method.
1139 *
1140 * @param delegate the delegate to use, if null the list's default is
1141 * created and used.
1142 */
1143 public void setDelegateRenderer(ListCellRenderer delegate) {
1144 if (delegate == null) {
1145 delegate = createDefaultCellRenderer();
1146 }
1147 delegateRenderer = delegate;
1148 }
1149
1150 /**
1151 * Returns the delegate.
1152 *
1153 * @return the delegate renderer used by this renderer, guaranteed to
1154 * not-null.
1155 */
1156 public ListCellRenderer getDelegateRenderer() {
1157 return delegateRenderer;
1158 }
1159
1160 /**
1161 * Updates the ui of the delegate.
1162 */
1163 public void updateUI() {
1164 updateRendererUI(delegateRenderer);
1165 }
1166
1167 /**
1168 *
1169 * @param renderer the renderer to update the ui of.
1170 */
1171 private void updateRendererUI(ListCellRenderer renderer) {
1172 if (renderer == null) return;
1173 Component comp = null;
1174 if (renderer instanceof AbstractRenderer) {
1175 comp = ((AbstractRenderer) renderer).getComponentProvider().getRendererComponent(null);
1176 } else if (renderer instanceof Component) {
1177 comp = (Component) renderer;
1178 } else {
1179 try {
1180 comp = renderer.getListCellRendererComponent(
1181 JXList.this, null, -1, false, false);
1182 } catch (Exception e) {
1183 // nothing to do - renderer barked on off-range row
1184 }
1185 }
1186 if (comp != null) {
1187 SwingUtilities.updateComponentTreeUI(comp);
1188 }
1189
1190 }
1191
1192 // --------- implement ListCellRenderer
1193 /**
1194 * {@inheritDoc} <p>
1195 *
1196 * Overridden to apply the highlighters, if any, after calling the delegate.
1197 * The decorators are not applied if the row is invalid.
1198 */
1199 public Component getListCellRendererComponent(JList list, Object value,
1200 int index, boolean isSelected, boolean cellHasFocus) {
1201 Component comp = delegateRenderer.getListCellRendererComponent(list, value, index,
1202 isSelected, cellHasFocus);
1203 if ((compoundHighlighter != null) && (index >= 0) && (index < getElementCount())) {
1204 comp = compoundHighlighter.highlight(comp, getComponentAdapter(index));
1205 }
1206 return comp;
1207 }
1208
1209
1210 // implement RolloverRenderer
1211
1212 /**
1213 * {@inheritDoc}
1214 *
1215 */
1216 public boolean isEnabled() {
1217 return (delegateRenderer instanceof RolloverRenderer) &&
1218 ((RolloverRenderer) delegateRenderer).isEnabled();
1219 }
1220
1221 /**
1222 * {@inheritDoc}
1223 */
1224 public void doClick() {
1225 if (isEnabled()) {
1226 ((RolloverRenderer) delegateRenderer).doClick();
1227 }
1228 }
1229
1230 }
1231
1232 // --------------------------- updateUI
1233
1234
1235 /**
1236 * {@inheritDoc} <p>
1237 *
1238 * Overridden to update renderer and Highlighters.
1239 */
1240 @Override
1241 public void updateUI() {
1242 // PENDING JW: temporary during dev to quickly switch between default and custom ui
1243 if (getUIClassID() == super.getUIClassID()) {
1244 super.updateUI();
1245 } else {
1246 setUI((ListUI) LookAndFeelAddons.getUI(this, ListUI.class));
1247 }
1248 updateRendererUI();
1249 updateHighlighterUI();
1250 }
1251
1252 @Override
1253 public String getUIClassID() {
1254 // PENDING JW: temporary during dev to quickly switch between default and custom ui
1255 return super.getUIClassID();
1256 // return uiClassID;
1257 }
1258
1259 private void updateRendererUI() {
1260 if (delegatingRenderer != null) {
1261 delegatingRenderer.updateUI();
1262 } else {
1263 ListCellRenderer renderer = getCellRenderer();
1264 if (renderer instanceof Component) {
1265 SwingUtilities.updateComponentTreeUI((Component) renderer);
1266 }
1267 }
1268 }
1269
1270 /**
1271 * Updates highlighter after <code>updateUI</code> changes.
1272 *
1273 * @see org.jdesktop.swingx.decorator.UIDependent
1274 */
1275 protected void updateHighlighterUI() {
1276 if (compoundHighlighter == null) return;
1277 compoundHighlighter.updateUI();
1278 }
1279
1280 }