Stopping JPopupMenu stealing the focus
Asked Answered
D

3

11

I have a JTextField for which I'm hoping to suggest results to match the user's input. I'm displaying these suggestions in a JList contained within a JPopupMenu.

However, when opening the popup menu programmatically via show(Component invoker, int x, int y), the focus is getting taken from the JTextField.

Strangely enough, if I call setVisible(true) instead, the focus is not stolen; but then the JPopupMenu is not attached to any panel, and when minimizing the application whilst the box is open, it stays painted on the window.

I've also tried to reset the focus to the JTextField using requestFocus(), but then I have to restore the caret position using SwingUtilities.invokeLater(), and the invoke later side of things is giving the user a slight margin to mess around with the existing contents / overwrite it / or do other unpredictable things.

The code I've got is effectively:

JTextField field = new JTextField();
JPopupMenu menu = new JPopupMenu();

field.addKeyListener(new KeyAdapter() {
    public void keyTyped(KeyEvent e) {
        JList list = getAListOfResults();

        menu.add(list);
        menu.show(field, 0, field.getHeight());
    }
});

Can anyone suggest the best avenue to go down to show the JPopupMenu programmatically whilst preserving the focus on the JTextField?

Disclose answered 5/4, 2012 at 13:48 Comment(0)
S
14

The technical answer is to set the popup's focusable property to false:

popup.setFocusable(false);

The implication is that the textField has to take over all keyboard and mouse-triggered actions that are normally handled by the list itself, sosmething like:

final JList list = new JList(Locale.getAvailableLocales());
final JPopupMenu popup = new JPopupMenu();
popup.add(new JScrollPane(list));
popup.setFocusable(false);
final JTextField field = new JTextField(20);
Action down = new AbstractAction("nextElement") {

    @Override
    public void actionPerformed(ActionEvent e) {
       int next = Math.min(list.getSelectedIndex() + 1,
               list.getModel().getSize() - 1);
       list.setSelectedIndex(next);
       list.ensureIndexIsVisible(next);
    }
};
field.getActionMap().put("nextElement", down);
field.getInputMap().put(
        KeyStroke.getKeyStroke("DOWN"), "nextElement");

As your context is very similar to a JComboBox, you might consider having a look into the sources of BasicComboBoxUI and BasicComboPopup.

Edit

Just for fun, the following is not answering the focus question :-) Instead, it demonstrates how to use a sortable/filterable JXList to show only the options in the dropdown which correspond to the typed text (here with a starts-with rule)

// instantiate a sortable JXList
final JXList list = new JXList(Locale.getAvailableLocales(), true);
list.setSortOrder(SortOrder.ASCENDING);

final JPopupMenu popup = new JPopupMenu();
popup.add(new JScrollPane(list));
popup.setFocusable(false);
final JTextField field = new JTextField(20);

// instantiate a PatternModel to map text --> pattern 
final PatternModel model = new PatternModel();
model.setMatchRule(PatternModel.MATCH_RULE_STARTSWITH);
// listener which to update the list's RowFilter on changes to the model's pattern property  
PropertyChangeListener modelListener = new PropertyChangeListener() {

    @Override
    public void propertyChange(PropertyChangeEvent evt) {
        if ("pattern".equals(evt.getPropertyName())) {
            updateFilter((Pattern) evt.getNewValue());
        }
    }

    private void updateFilter(Pattern newValue) {
        RowFilter<Object, Integer> filter = null;
        if (newValue != null) {
            filter = RowFilters.regexFilter(newValue);
        }
        list.setRowFilter(filter);
    }
};
model.addPropertyChangeListener(modelListener);

// DocumentListener to update the model's rawtext property on changes to the field
DocumentListener documentListener = new DocumentListener() {

    @Override
    public void removeUpdate(DocumentEvent e) {
        updateAfterDocumentChange();
    }

    @Override
    public void insertUpdate(DocumentEvent e) {
        updateAfterDocumentChange();
    }

    private void updateAfterDocumentChange() {
        if (!popup.isVisible()) {
            popup.show(field, 0, field.getHeight());
        } 
        model.setRawText(field.getText());
    }

    @Override
    public void changedUpdate(DocumentEvent e) {
    }
};
field.getDocument().addDocumentListener(documentListener);
Sowers answered 6/4, 2012 at 11:38 Comment(1)
After a lot of googling, this answer helped me alotWillingham
Q
2

It looks straight forward to me. Add the following

field.requestFocus();

after

 menu.add(list);
 menu.show(field, 0, field.getHeight());

Of course, you will have to code for when to hide the popup etc based on what is going on with the JTextField.

i.e;

 menu.show(field, field.getX(), field.getY()+field.getHeight());
 menu.setVisible(true);
 field.requestFocus();
Quirinus answered 5/4, 2012 at 17:34 Comment(1)
Thanks for your answer; unfortunately I've already tried this approach. It gets more complicated as I then have to restore the caret position using SwingUtilities.invokeLater(), and the invoke later side of things gives the user a slight margin to mess around with the existing contents / overwrite it / or do other unpredictable things.Disclose
T
1

You may take a look to JXSearchField, which is part of xswingx

Tracey answered 10/8, 2012 at 12:39 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.