Question

I'd like to create a readonly combobox. The user should not be able to select another item from the popup list. That means that the popup list should not open or should be empty.

I see the following solutions:

  • Set a ComboBox model with only one item (the current selected item) so when the user clicks on the arrow button, an empty list is presented.

  • Add a PopupMenuListener and in the popupMenuWillBecomeVisible hide the menu. This is problematic: We have to call combo.hidePopup(); from within a SwingUtilities.invokeLater()

The empty model approach seems a little bit clunky. The second approach shows the popup list for a fraction of a second, short enough to be noticed. This is very ugly.

Is there a third solution?

EDIT: Implemented solution:

I implemented the suggested method from splungebob and here is my code for future reference:

private void makeComboReadonly() {
  Component editorComponent = box.getEditor().getEditorComponent();
  if (editorComponent instanceof JTextField) {
    ((JTextField) editorComponent).setEditable(false);
  }

  for (Component childComponent : box.getComponents()) {
    if (childComponent instanceof AbstractButton) {
      childComponent.setEnabled(false);
      final MouseListener[] listeners = childComponent.getListeners(MouseListener.class);
      for (MouseListener listener : listeners) {
        childComponent.removeMouseListener(listener);
      }
    }
  }

  final MouseListener[] mouseListeners = box.getListeners(MouseListener.class);
  for (MouseListener listener : mouseListeners) {
    box.removeMouseListener(listener);
  }

  final KeyListener[] keyListeners = box.getListeners(KeyListener.class);
  for (KeyListener keyListener : keyListeners) {
    box.removeKeyListener(keyListener);
  }

  box.setFocusable(false);

  //box.getActionMap().clear(); //no effect
  //box.getInputMap().clear();
}

The only problem is the Key-Event Alt-Down which oppens the popup menu even if I remove all the key listeners and clear the action map. I circumvent this problem by making the combo non focusable. Not ideal but good enough (-:

Was it helpful?

Solution

This is actually a good question about one of Swing's limitations (and has bugged me for a long time).

One would need a read-only combobox when... (wait for it)... the form is currently in read-only mode. Note that input from the user elswhere may flip the form into edit mode at a moment's notice, so switching JComponents (using a JLabel for instance) would not be visually desirable, IMO. Also note that a disabled combo does not convey the same information to a user as a read-only combo would:

setEnabled(false) -> entirely grayed out; the component cannot be interacted with; whatever data may be shown is not relevant and cannot be selected for Copy/Paste.

setReadOnly(true) -> text component of combo is not grayed out (but the arrow is); the component cannot be interacted with; whatever data may be shown is relevant and can be selected.

Justification for this is that Swing did implement this for JTextComponents in the form of setEditable(boolean). Thanks guys for that, but I also need it for JComboBox, JCheckbox, JRadioButton, etc. We had to roll our own versions for this missing API.

Another Swing gaffe (IHMO) is the inconsistent API. JTextComponent.setEditable(boolean) enforces a read-only behavior, whereas JComboBox.setEditable(boolean) does not.

Arrrgh!!!

So, to the problem. You gotta roll up your sleeves a bit. For an editable combo:

  • Get the combo's editor component via combo.getEditor().getEditorComponent(). It's a JTextField. Cast it, and call setEditable(false). This gives you both the functionality and appearance you want for the text portion of the combo.

  • Get the combo's arrow component by iterating over getComponents() of the combo. It's the only AbstractButton you'll find. Call setEnabled(false). This is for appearance only.

  • Find all of the default mouse listeners that came with the combo (which should be all of them if you didn't add any yourself) and remove them from both the combo and the arrow button.

  • Keep a reference to these listeners and the arrow button in case you want to switch it back to when read-only = false.

Or something like that. Your mileage may vary.

Cue kleopatra with an endorsement for SwingX, which probably has this functionality already built-in (I don't know that for sure, I'm just guessing).

Good luck.

OTHER TIPS

splungebob provides the perfect solution. Here is his comments turned into code so that you can grab it and go:

private void setJComboBoxReadOnly(JComboBox jcb)
{
   JTextField jtf = (JTextField)jcb.getEditor().getEditorComponent();
   jtf.setEditable(false);

   MouseListener[] mls = jcb.getMouseListeners();
   for (MouseListener listener : mls)
      jcb.removeMouseListener(listener);

   Component[] comps = jcb.getComponents();
   for (Component c : comps)
   {
      if (c instanceof AbstractButton)
      {
         AbstractButton ab = (AbstractButton)c;
         ab.setEnabled(false);

         MouseListener[] mls2 = ab.getMouseListeners();
         for (MouseListener listener : mls2)
            ab.removeMouseListener(listener);
      }
   }
}

Overriding:

@Override
public void showPopup()
{
    //do nothing
}

should do the trick.

Whats wrong with simply disabling the JComboBox?

setEnabled(false);

I had a similar requirement. Calling setEnabled(false) gives a horrible appearance and the user can't browse the drop down. Overriding showPopup() doesn't work. Trying to listen for the menu opening and then close it via invokeLater causes the menu to flash, and again the user can't browse the menu.

In the end I did this (not saying its perfect, but it does exactly what I wanted):

import javax.swing.JComboBox;

public class ReadOnlyComboBox<E> extends JComboBox<E>
{
    private static final long serialVersionUID = 5866761337995322114L;

    public ReadOnlyComboBox()
    {
        this.setModel(new ReadOnlyComboBoxModel<E>());
    }

    public void setReadOnly(boolean readOnly)
    {
       ((ReadOnlyComboBoxModel<E>)this.getModel()).setReadOnly(readOnly);
    }
}

import javax.swing.DefaultComboBoxModel;

public class ReadOnlyComboBoxModel<E> extends DefaultComboBoxModel<E>
{
    private static final long serialVersionUID = -1923833835224513983L;
    private boolean readOnly;

    @Override
    public void setSelectedItem(Object anItem)
    {
        if(!readOnly)
            super.setSelectedItem(anItem);
    }

    public void setReadOnly(boolean readOnly)
    {
        this.readOnly = readOnly;
    }
}

You need to call setReadOnly(false) on the ReadOnlyComboBox before setting the selected item programmatically if needed, then set it back to stop the user making selections.

Do note the unchecked cast, wasn't an issue for me in my small program, but should probably override the setModel method to chuck an exception if an attempt is made to use any other kind of model.

Edit: Also note that action listeners are still called (with the unchanged selection).

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top