views:

58

answers:

2

I've got a JComboBox that potentially can have thousands of items. They're sorted, and there's find-as-you-type, so in principle it's not completely unusable.

In practice, it's pretty unusable with just a couple of hundred items. I managed to improve the initial display performance using setPrototypeDisplayValue(), but BasicListUI still insists on configuring the list cell renderer for every item in the box (see BasicListUI.updateLayoutState()).

This, or something like it, is apparently a known issue to Sun; it has been for going on eight years now, so I'm not holding my breath.

Short of implementing my own UI, has anyone got a workaround?

+2  A: 

JList might be a better choice, as it uses a fly-weight approach to rendering and appears to support find-as-you-type.

If you use JComboBox, add entries to the model before the component itself starts listening. This SortedComboBoxModel uses a simple insertion sort that is acceptable for a few thousand entries:

class SortedComboBoxModel extends DefaultComboBoxModel {

    /** Add elements by inserting in lexical order. */
    @Override
    public void addElement(Object element) {
        this.insertElementAt(element, 0);
    }

    /** Insert in lexical order by name; ignore index. */
    @Override
    public void insertElementAt(Object element, int index) {
        String name = element.toString();
        for (index = 0; index < this.getSize(); index++) {
            String s = getElementAt(index).toString();
            if (s.compareTo(name) > 0) {
                break;
            }
        }
        super.insertElementAt(element, index);
    }
}
trashgod
That's not really the issue -- the model's fully constructed before the combo box exists, and changes to it are all-or-nothing.
David Moles
A: 

Here's the hack that I came up with. The drawbacks are:

  • if you want to maintain the look and feel, you have to separately subclass each BasicComboBoxUI extension you care about
  • you have to use reflection to load your UI classes, since (for instance) a subclass of WindowsComboBoxUI won't load on Linux
  • it won't work with L&Fs (e.g. MacOS?) that don't extend BasicComboBoxUI
  • it makes assumptions about the ListCellRenderer that may not always be warranted

I'm still open to cleaner solutions.

class FastBasicComboBoxUI extends BasicComboBoxUI {
  @Override
  public void installUI(JComponent c) {
    super.installUI(c);

    Object prototypeValue = this.comboBox.getPrototypeDisplayValue();
    if (prototypeValue != null) {
      ListCellRenderer renderer = comboBox.getRenderer();
      Component rendererComponent = renderer
          .getListCellRendererComponent(this.listBox, 
              prototypeValue, 0, false, false);
      if (rendererComponent instanceof JLabel) {
        // Preferred size of the renderer itself is (-1,-1) at this point, 
        // so we need this hack
        Dimension prototypeSize = new JLabel(((JLabel) rendererComponent)
            .getText()).getPreferredSize();
        this.listBox.setFixedCellHeight(prototypeSize.height);
        this.listBox.setFixedCellWidth(prototypeSize.width);
      }
    }
  }
}

I'm still open to cleaner solutions.

Later

Turns out this only solved some of the problems. Initial display of a combo box with a large number of items could still be really slow. I had to make sure the popup list box immediately gets a fixed cell size, by moving the code into the ComboPopup itself, as follows. Note that, as above, this depends on the prototype value.

@Override
protected ComboPopup createPopup() {
  return new BasicComboPopup(comboBox) {
    @Override
    protected JList createList() {
      JList list = super.createList();
      Object prototypeValue = comboBox.getPrototypeDisplayValue();
      if (prototypeValue != null) {
        ListCellRenderer renderer = comboBox.getRenderer();
        Component rendererComponent = renderer
            .getListCellRendererComponent(list, prototypeValue, 0, false, false);
        if (rendererComponent instanceof JLabel) {
          // Preferred size of the renderer itself is (-1,-1) at this point, 
          // so we need this hack
          Dimension prototypeSize = new JLabel(((JLabel) rendererComponent)
              .getText()).getPreferredSize();
          list.setFixedCellHeight(prototypeSize.height);
          list.setFixedCellWidth(prototypeSize.width);
        }
      }
      return list;
    }
  };
}
David Moles