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 }