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