3

I've got a JFrame that looks like this:

enter image description here

It's got two JTextFields on it, one JComboBox between them and a JPanel at the bottom (that you can't see).

One of the features of the JComboBox is that it can be given a custom editor. These implement the ComboBoxEditor interface. In each of the following three cases, the GUI looks exactly the same, and I would have expected them all to behave exactly the same:

  • I do not specify a custom editor, and use the default one.
  • I create a custom editor whose editor component is a JTextField.
  • I create a custom editor whose editor component is a JPanel with a JTextField on it (using a BorderLayout).

When the editor for the editable combo box is set to the default, pressing Tab moves the focus from the top JTextField into the editing area on the JComboBox and then into the other JTextField. If I create a custom editor whose editor component is a JTextField and otherwise does what you would expect, the same thing happens.

BUT, if I instead create a custom editor whose editor component is a JPanel with a JTextField added to it, the focus makes one additional stop. If the focus is on the top JTextField, then pressing Tab moves the focus to the little arrow at the right of the editable combo box before moving into the text area.

Why is this happening? The focus never moves on to the JPanel at the bottom of the frame, so why does the presence of a JPanel holding the JTextField affect the tab order on the combo box?

The following is an S(-ish)SCCE, which has one text field and all three types of combo box on it:

import javax.swing.*;
import java.awt.event.*;
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;

public class ComboBoxTest extends JFrame
{
  private JPanel layoutPanel;
  private JTextField meaninglessTextField;
  private JComboBox defaultEditorComboBox;
  private JComboBox textFieldEditorComboBox;
  private JComboBox panelEditorComboBox;

  public ComboBoxTest()
  {
    setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);

    layoutPanel = new JPanel();
    layoutPanel.setLayout(new BoxLayout(layoutPanel, BoxLayout.Y_AXIS));
    meaninglessTextField = new JTextField();

    defaultEditorComboBox = new JComboBox(); // Just a default JComboBox.
    defaultEditorComboBox.setEditable(true);

    textFieldEditorComboBox = new JComboBox();
    textFieldEditorComboBox.setEditable(true);
    textFieldEditorComboBox.setEditor(new TextFieldEditor());

    panelEditorComboBox = new JComboBox();
    panelEditorComboBox.setEditable(true);
    panelEditorComboBox.setEditor(new PanelEditor());

    layoutPanel.add(Box.createRigidArea(new Dimension(500,0)));
    layoutPanel.add(meaninglessTextField);
    layoutPanel.add(defaultEditorComboBox);
    layoutPanel.add(textFieldEditorComboBox);
    layoutPanel.add(panelEditorComboBox);

    Container contentPane = getContentPane();
    contentPane.add(layoutPanel, BorderLayout.CENTER);

    pack();
  }

  public static void main(String[] args)
  {
    java.awt.EventQueue.invokeLater(new Runnable() {
                    @Override
                    public void run()
                    {
                      new ComboBoxTest().setVisible(true);
                    }
    });
  }

  private class PanelEditor extends JPanel implements ComboBoxEditor
  {
    public JTextField inputTextField = new JTextField();

    public PanelEditor()
    {
      setLayout(new BorderLayout());
      add(inputTextField, BorderLayout.CENTER);
    }

    @Override
    public String getItem()
    {
      return inputTextField.getText();
    }

    @Override
    public void setItem(Object newText)
    {
      if (newText != null) {
        inputTextField.setText(newText.toString());
      }
      else {
        inputTextField.setText("");
      }
    }

    @Override
    public Component getEditorComponent()
    {
      return this;
    }

    @Override
    public void removeActionListener(ActionListener listener)
    {
      inputTextField.removeActionListener(listener);
    }

    @Override
    public void addActionListener(ActionListener listener)
    {
      inputTextField.addActionListener(listener);
    }

    @Override
    public void selectAll()
    {
      inputTextField.selectAll();
    }
  }

  private class TextFieldEditor extends PanelEditor implements ComboBoxEditor
  {
    // The same, except that the editor component is now just the JTextField
    // rather than the whole panel.
    public TextFieldEditor()
    {
    }

    @Override
    public JTextField getEditorComponent()
    {
      return inputTextField;
    }
  }
} 

Note: this behaviour becomes a problem if I want to add a JLabel to the editor. Then I have to put a JPanel there to hold both the label and the text field.

John Gowers
  • 2,646
  • 2
  • 24
  • 37
  • 2
    Yes, please show an [SSCCE](http://sscce.org). Just curious: why do you need the label inside the editor? And is the only reason for the panel to begin with? – splungebob Aug 28 '13 at 14:51
  • I've add the SSCCE now. I want the label inside the editor just because I think it looks better that way, and fits better with how the combo box looks. It's a dialog that allows you to select the concentration of four different kinds of ions in a solution: the label displays the ion you're specifying, and you can type the concentration into a text field. Using the drop-down list allows you to select a different ion, and I'd rather have the labels directly above the drop down list, rather than being to the side. – John Gowers Aug 28 '13 at 15:45
  • The best way to ensure a certain tab order is to use a `FocusTraversalPolicy` instead of relying on the implicit order of the layout manager. Using that you could ensure that the next component after your customer editor is indeed the desired `JTextField` –  Aug 28 '13 at 16:13

3 Answers3

4

The basic problem is that the combo's ui delegate can't handle compound editor components. There are several places where it assumes that the editor component is the target of whatever configuration it needs to do. The concrete mis-behaviour here is that it explicitly sets the editor's focusability to that of the combo itself

// in BasicComboBoxUI
protected void configureEditor() {
    ....
    editor.setFocusable(comboBox.isFocusable());
    ....
]

The implications

  • by default, the panel's focusable is true because the combo's is true
  • forcing the panel's focusable to false in its constructor has no effect (the ui resets it later on and whenever the LAF is switched)
  • disabling combo's focusable disables the panel's as well

To fix on the level of the editor, you can implement its isFocusable to return false unconditionally:

private class PanelEditor extends JPanel implements ComboBoxEditor

    public boolean isFocusable() {
        return false;
    }
    ...
}

An aside: for code hygiene, better not extend a view to implement its role as ComboBoxEditor (even though here you need a subclassed JPanel to avoid the problem, so it's arguably borderline :-) - instead implement the editor and let it use the tweaked panel.

Also beware that you might stumble into more problems with the compound editor (check the code of BasicComboUI for more places where it assumes a plain childless component), so you might consider not doing it at all but think of a different way to achieve your requirement.

kleopatra
  • 51,061
  • 28
  • 99
  • 211
0

Try this:

public PanelEditor()
{
  // other code...

  addFocusListener(new FocusAdapter()
  {
    @Override
    public void focusGained(FocusEvent e)
    {
      inputTextField.requestFocusInWindow();
    }
  });
}
splungebob
  • 5,357
  • 2
  • 22
  • 45
0

The focus isn't transferring to the JPanel; it's transferring to the JComboBox itself.

You can stop a component from receiving the focus by using its setFocusable method. If you add the line

setFocusable(false)

to the constructor of the PanelEditor in the example above, then the strange behaviour is still there, since the PanelEditor implements JPanel, so the setFocusable method of the JPanel overrides that of the JComboBox. Since the setFocusable method of a JPanel essentially does nothing, nothing changes.

If instead you add the line

panelEditorComboBox.setFocusable(false)

to the constructor of the JFrame itself then the JComboBox will not be able to receive the focus, but the JTextField inside the editor will. This isn't a perfect fix, since it would be better if the editor itself were responsible for turning off the focusability of the JComboBox, so you could always pass in the parent JComboBox as a parameter to the constructor of the editor, and have the focusability turned off there.

I don't know why the behaviour is different when you've got a JTextField as the editor. Some weird Swing thing.

John Gowers
  • 2,646
  • 2
  • 24
  • 37
  • did you tried code linked as comment to @splungebob answer here, interesting will be Look and feel, java version and native os, – mKorbel Aug 29 '13 at 09:49
  • note I see that 3rd. JComponent, JComboBoc required two tabs for leaving focus to next JComponent, JTextField – mKorbel Aug 29 '13 at 09:52