javaswingjpopupmenu

JPopupMenu jumps few rows only when I press Menu key on keyboard


This is strange. The JPopMenu works as expected when I use mouse right click. But when I try to show it using Menu key on keyboard the highlight jumps a few rows before or after.

I've used PopupMenuListener so that I can highlight a row directly when using right click:

popupMenu.addPopupMenuListener(new PopupMenuListener() {
            @Override
            public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
                SwingUtilities.invokeLater(new Runnable() {
                    @Override
                    public void run() {
                        Point point = SwingUtilities.convertPoint(popupMenu, new Point(0, 0), table);
                        int currentRow = table.rowAtPoint(point);
                        int currentColumn = table.columnAtPoint(point);
                        table.changeSelection(currentRow, currentColumn, false, false);
                    }
                });
            }

            @Override
            public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
                // TODO Auto-generated method stub
            }

            @Override
            public void popupMenuCanceled(PopupMenuEvent e) {
                // TODO Auto-generated method stub
            }
        });

Here is the full program:

import java.awt.Point;

import javax.swing.JFrame;
import javax.swing.JMenuItem;
import javax.swing.JPopupMenu;
import javax.swing.JScrollPane;

import javax.swing.JTable;
import javax.swing.ListSelectionModel;
import javax.swing.SwingUtilities;
import javax.swing.event.PopupMenuEvent;
import javax.swing.event.PopupMenuListener;

public class Test {

    private JFrame frame;
    private JTable table;
    private JScrollPane scrollpane;

    public Test() {
        initialize();
    }

    private void initialize() {
        frame = new JFrame();
        frame.setBounds(100, 100, 450, 300);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        
        String[] head = {"ID","NAME","DOB"};
        
        String[][] data = {
                {"1", "Peter", "2001/03/24"},
                {"2", "Carlos", "1996/09/02"},
                {"3", "Ahmed", "1999/07/07"},
                {"4", "John", "1993/10/15"},
                {"5", "Kumar", "1991/11/08"}
        };
        
        table = new JTable(data, head);
        scrollpane = new JScrollPane(table);
        table.setDefaultEditor(Object.class, null);
        table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
        
        JPopupMenu popupMenu = new JPopupMenu();
        JMenuItem edit = new JMenuItem("Edit");
        JMenuItem delete = new JMenuItem("Delete");
        popupMenu.add(edit);
        popupMenu.add(delete);
        table.setComponentPopupMenu(popupMenu);
        
        popupMenu.addPopupMenuListener(new PopupMenuListener() {
            @Override
            public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
                SwingUtilities.invokeLater(new Runnable() {
                    @Override
                    public void run() {
                        Point point = SwingUtilities.convertPoint(popupMenu, new Point(0, 0), table);
                        int currentRow = table.rowAtPoint(point);
                        int currentColumn = table.columnAtPoint(point);
                        table.changeSelection(currentRow, currentColumn, false, false);
                    }
                });
            }

            @Override
            public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
                // TODO Auto-generated method stub
            }

            @Override
            public void popupMenuCanceled(PopupMenuEvent e) {
                // TODO Auto-generated method stub
            }
        });
        
        frame.getContentPane().add(scrollpane);
    }
    
    public void setVisible(boolean state) {
        frame.setVisible(state);
    }

}

Solution

  • the selection jumps few records after or before the selected row

    I went one step further and added 20 rows to the table and then shrunk the frame so the scrollbars are displayed.

    The popup is always displayed in the middle of the viewport.

    This makes sense as default behaviour since a popup can be displayed on any component. The popup doesn't know that there is a component with selection logic in the viewport.

    The following code attempts to customize the popup/selection behaviour based on whether the popup is displayed by using the mouse or keyboard:

    import java.awt.*;
    import java.awt.event.*;
    
    import javax.swing.JFrame;
    import javax.swing.JMenuItem;
    import javax.swing.JPopupMenu;
    import javax.swing.JScrollPane;
    
    import javax.swing.JTable;
    import javax.swing.ListSelectionModel;
    import javax.swing.SwingUtilities;
    import javax.swing.event.PopupMenuEvent;
    import javax.swing.event.PopupMenuListener;
    
    public class Test {
    
        private JFrame frame;
        private JTable table;
        private JScrollPane scrollpane;
    
        public Test() {
            initialize();
        }
    
        private void initialize() {
            frame = new JFrame();
            frame.setBounds(100, 100, 450, 300);
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    
            String[] head = {"ID","NAME","DOB"};
    
            String[][] data = {
                    {"1", "Peter", "2001/03/24"},
                    {"2", "Carlos", "1996/09/02"},
                    {"3", "Ahmed", "1999/07/07"},
                    {"3", "Ahmed", "1999/07/07"},
                    {"3", "Ahmed", "1999/07/07"},
                    {"3", "Ahmed", "1999/07/07"},
                    {"3", "Ahmed", "1999/07/07"},
                    {"3", "Ahmed", "1999/07/07"},
                    {"3", "Ahmed", "1999/07/07"},
                    {"3", "Ahmed", "1999/07/07"},
                    {"3", "Ahmed", "1999/07/07"},
                    {"3", "Ahmed", "1999/07/07"},
                    {"3", "Ahmed", "1999/07/07"},
                    {"3", "Ahmed", "1999/07/07"},
                    {"3", "Ahmed", "1999/07/07"},
                    {"3", "Ahmed", "1999/07/07"},
                    {"3", "Ahmed", "1999/07/07"},
                    {"4", "John", "1993/10/15"},
                    {"5", "Kumar", "1991/11/08"}
            };
    
            table = new JTable(data, head);
            scrollpane = new JScrollPane(table);
            table.setDefaultEditor(Object.class, null);
            table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
    
            JPopupMenu popupMenu = new JPopupMenu();
            JMenuItem edit = new JMenuItem("Edit");
            JMenuItem delete = new JMenuItem("Delete");
            popupMenu.add(edit);
            popupMenu.add(delete);
            table.setComponentPopupMenu(popupMenu);
    
            popupMenu.addPopupMenuListener(new PopupMenuListener()
            {
                private boolean isMouse;
    
                @Override
                public void popupMenuWillBecomeVisible(PopupMenuEvent e)
                {
                    isMouse = EventQueue.getCurrentEvent().getID() == MouseEvent.MOUSE_RELEASED;
    
                    SwingUtilities.invokeLater(new Runnable() {
                        @Override
                        public void run()
                        {
                            if (isMouse) // change row/cell selection
                            {
                                Point point = SwingUtilities.convertPoint(popupMenu, new Point(0, 0), table);
                                int currentRow = table.rowAtPoint(point);
                                int currentColumn = table.columnAtPoint(point);
                                table.changeSelection(currentRow, currentColumn, false, false);
                            }
                            else  // use current selection
                            {
                                int selectedRow = table.getSelectedRow();
                                int selectedColumn = table.getSelectedColumn();
    
                                if (selectedRow == -1) // no row selected
                                {
                                    popupMenu.setVisible( false );
                                }
                                else  // reset popup location to selected row
                                {
                                    Rectangle bounds = table.getCellRect(selectedRow, selectedColumn, false);
                                    popupMenu.show(table, bounds.x, bounds.y + bounds.height);
                                }
                            }
                        }
                    });
                }
    
                @Override
                public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
                    // TODO Auto-generated method stub
                }
    
                @Override
                public void popupMenuCanceled(PopupMenuEvent e) {
                    // TODO Auto-generated method stub
                }
            });
    
            frame.getContentPane().add(scrollpane);
        }
    
        public void setVisible(boolean state) {
            frame.setVisible(state);
        }
    
        public static void main(String[] args) throws Exception
        {
            java.awt.EventQueue.invokeLater( () -> new Test().setVisible(true) );
        }
    }
    

    When the mouse is used the selection will be changed and the popup displayed at the mouse location.

    When the keyboard is used the popup location is based on the currently selected cell.