Search code examples
javaswingjlist

JList selection randomly jumps to previous index


I have a JList where I want to be able to navigate to different cells, type text, then press "enter" to commit the change. The problem is when I change a few cells and then navigate via the up and down keys and try typing in the currently selected cell, the selection somehow jumps to a previously filled in cell. I've pared down my code to what I think is the minimum to replicate the problem:

package listtest;

import java.awt.Dimension;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import javax.swing.DefaultListModel;
import javax.swing.JDialog;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.JTextField;
import javax.swing.ListSelectionModel;
import javax.swing.UIManager;
import javax.swing.border.EmptyBorder;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;

public class JListSelection{

    static int selectedIndex;
    static JList<String> serials;
    static DefaultListModel<String> model;
    static private JPopupMenu editPopup;
    static private JTextField editTextField;

    public static void main(String[] args) {
        JPanel pan = new JPanel(null);
        selectedIndex = 0;
        serials = new JList<String>();

        model = new DefaultListModel<String>();
        serials = new JList<String>(model);
        for(int i = 0; i < 19; i++) {
            model.addElement(" ");
        }
        serials.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
        serials.addListSelectionListener(new ListSelectionListener() {
            @Override
            public void valueChanged(ListSelectionEvent e) {
                selectedIndex = serials.getSelectedIndex();
                System.out.println("in listener: " + serials.getSelectedIndex());
            }
        });
        serials.addKeyListener(new KeyAdapter() {
            public void keyPressed(KeyEvent e) {
                System.out.println("In keypressed: " + e.getKeyCode() + " " + serials.getSelectedIndex());
            }
            public void keyReleased(KeyEvent e) {
                int code = e.getKeyCode();
                switch( code ){ 
                    case KeyEvent.VK_UP:
                        System.out.println("UP " + serials.getSelectedIndex());
                        break;
                    case KeyEvent.VK_DOWN:
                        System.out.println("DOWN " + serials.getSelectedIndex());
                        break;
                }
                if(e.getKeyCode() >= KeyEvent.VK_A && e.getKeyCode() <= KeyEvent.VK_Z
        || e.getKeyCode() >= KeyEvent.VK_0 && e.getKeyCode() <= KeyEvent.VK_9) {

                    System.out.println(selectedIndex + " " + serials.getSelectedIndex());
                    Rectangle r = serials.getCellBounds(selectedIndex, selectedIndex);

                    if (editPopup == null) {
                        createEditPopup();
                    }

                    editPopup.setPreferredSize(new Dimension(r.width, r.height));
                    editPopup.show(serials, r.x, r.y);

                    editTextField.setText(
                        serials.getSelectedValue().toString().equals(" ") ? 
                        e.getKeyChar()+"" : serials.getSelectedValue().toString());
                        editTextField.requestFocusInWindow(); 
                }
            }
        });

        serials.setBounds(0, 0, 200, 800);
        pan.add(serials);
        JDialog di = new JDialog();
        di.setContentPane(pan);
        di.pack();
        di.setLocationRelativeTo(null);
        di.setSize(300, 400);
        di.setVisible(true);
    }

    private static void createEditPopup(){
        editTextField = new JTextField();

        editTextField.setBorder(
            UIManager.getBorder("List.focusCellHighlightBorder"));
        editTextField.addActionListener(new ActionListener(){
            public void actionPerformed(ActionEvent e){
                DefaultListModel<String> model = (DefaultListModel<String>) 
                    serials.getModel();
                model.set(selectedIndex, editTextField.getText());
                editPopup.setVisible(false);
            }
        });

        editPopup = new JPopupMenu();
        editPopup.setBorder(new EmptyBorder(0, 0, 0, 0));
        editPopup.add(editTextField);
    }
}

If you run the code and start by selecting a cell and typing something then pressing enter it works how it should. If you then use the down arrow keys to type a few other cells at some point the selection will jump to a previously selected cell and I can't figure out any way to see what is causing this jump, let alone prevent it.


Solution

  • You have encountered a feature of JList. What is happening is as you type each character, the JList is attempting to scroll to the entry that begins with the letter you typed. So, if you entered "Joe" and "Dave" and then tried to type "Jerry" the JList will select the "Joe" row.

    See: Deactivate selection by letter in JList

    Following the technique from that question:

    // Add these lines just before your first "addKeyListener"
    for (KeyListener lsnr : serials.getKeyListeners()) {
      if(lsnr.getClass().getSimpleName().equals("Handler")){
        serials.removeKeyListener(lsnr);
      }
    }
    

    These lines go before line 47 in your code example. It's a bit of a rough way to kill the autoselect nature of JList.

    The listener we are removing is added to the JList by BasicListUI in the installListeners() method, as list.addKeyListener(getHandler()). Consult the source for BasicListUI. The class returned by getHandler() is a catch-all listener that implements several different listener interfaces, including KeyListener, and this is where the autoselect behavior is implemented.

    The odd usage of getSimpleName() to determine class name is necessary because Handler is a private class in BasicListUI, so we can't use instanceof. Needless to say, these kinds of shenanigans make for somewhat brittle code. If you wish to use this approach, make sure you document it well and prepare to fix it when migrating to future Java versions.

    If you find yourself fighting the design of a component like this, you might be using the wrong component. Perhaps you would be better off using a single-column JTable.