Search code examples
javaswingjtableindexoutofboundsexceptionjpopupmenu

Error when removing JTable column from component menu on same column


I've got a JTable with a component menu set for the table header. It contains entries for removing columns. My problem occurs when removing the same column that the component menu was triggered from and it appears to treat this action as a column being dragged, which leads to a ArrayIndexOutOfBoundsException with -1.

How can I safely remove the current column from the component menu without incurring this error?

Here is a minimal example of how to trigger this kind of behavior. Simply run it and remove the same column that you are on (note that it appears to work when you remove the last column, but that is hardly relevant for me):

import java.awt.BorderLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JFrame;
import javax.swing.JMenuItem;
import javax.swing.JPopupMenu;
import javax.swing.JTable;
import javax.swing.table.DefaultTableModel;
import javax.swing.table.TableColumn;

public class BasicExample extends JFrame {

    private JTable t;
    private DefaultTableModel dtm;

    public BasicExample() {
        init();
    }

    private void init() {
        dtm = new DefaultTableModel(new String[][]{{"a", "b"}, {"c", "d"}}, new String[]{"A", "B"});
        t = new JTable(dtm);
        t.getTableHeader().setComponentPopupMenu(new PopupMenu(t));
        this.setLayout(new BorderLayout());
        add(t.getTableHeader(), BorderLayout.NORTH);
        add(t, BorderLayout.CENTER);
        pack();
        setVisible(true);
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    }

    public class PopupMenu extends JPopupMenu {

        private JTable table;

        public PopupMenu(JTable table) {
            this.table = table;
            init();
        }

        private void init() {
            for (int i = 0; i < table.getModel().getColumnCount(); i++) {
                String columnName = table.getModel().getColumnName(i);

                JMenuItem item = new JMenuItem(columnName);
                item.addActionListener(new ActionListener() {
                    @Override
                    public void actionPerformed(ActionEvent e) {
                        TableColumn tc = table.getColumn(columnName);
                        table.getColumnModel().removeColumn(tc);
                    }
                });
                this.add(item);
            }
        }
    }

    public static void main(String[] args) {
        BasicExample be = new BasicExample();
    }
}

In the stack trace I see that it appears to treat the column as dragged, because it enters a conditional if (draggedColumn != null) { in BasicTableHeaderUI.java, while the action I'm performing isn't really a drag. The full stack trace is below:

Exception in thread "AWT-EventQueue-0" java.lang.ArrayIndexOutOfBoundsException: -1
    at java.util.Vector.elementData(Vector.java:734)
    at java.util.Vector.elementAt(Vector.java:477)
    at javax.swing.table.DefaultTableColumnModel.getColumn(DefaultTableColumnModel.java:294)
    at javax.swing.plaf.basic.BasicTableHeaderUI.getHeaderRenderer(BasicTableHeaderUI.java:693)
    at javax.swing.plaf.basic.BasicTableHeaderUI.paintCell(BasicTableHeaderUI.java:709)
    at javax.swing.plaf.basic.BasicTableHeaderUI.paint(BasicTableHeaderUI.java:685)
    at javax.swing.plaf.ComponentUI.update(ComponentUI.java:161)
    at javax.swing.JComponent.paintComponent(JComponent.java:780)
    at javax.swing.JComponent.paint(JComponent.java:1056)
    at javax.swing.JComponent.paintToOffscreen(JComponent.java:5210)
    at javax.swing.RepaintManager$PaintManager.paintDoubleBuffered(RepaintManager.java:1579)
    at javax.swing.RepaintManager$PaintManager.paint(RepaintManager.java:1502)
    at javax.swing.RepaintManager.paint(RepaintManager.java:1272)
    at javax.swing.JComponent._paintImmediately(JComponent.java:5158)
    at javax.swing.JComponent.paintImmediately(JComponent.java:4969)
    at javax.swing.RepaintManager$4.run(RepaintManager.java:831)
    at javax.swing.RepaintManager$4.run(RepaintManager.java:814)
    at java.security.AccessController.doPrivileged(Native Method)
    at java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(ProtectionDomain.java:76)
    at javax.swing.RepaintManager.paintDirtyRegions(RepaintManager.java:814)
    at javax.swing.RepaintManager.paintDirtyRegions(RepaintManager.java:789)
    at javax.swing.RepaintManager.prePaintDirtyRegions(RepaintManager.java:738)
    at javax.swing.RepaintManager.access$1200(RepaintManager.java:64)
    at javax.swing.RepaintManager$ProcessingRunnable.run(RepaintManager.java:1732)
    at java.awt.event.InvocationEvent.dispatch(InvocationEvent.java:311)
    at java.awt.EventQueue.dispatchEventImpl(EventQueue.java:756)
    at java.awt.EventQueue.access$500(EventQueue.java:97)
    at java.awt.EventQueue$3.run(EventQueue.java:709)
    at java.awt.EventQueue$3.run(EventQueue.java:703)
    at java.security.AccessController.doPrivileged(Native Method)
    at java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(ProtectionDomain.java:76)
    at java.awt.EventQueue.dispatchEvent(EventQueue.java:726)
    at java.awt.EventDispatchThread.pumpOneEventForFilters(EventDispatchThread.java:201)
    at java.awt.EventDispatchThread.pumpEventsForFilter(EventDispatchThread.java:116)
    at java.awt.EventDispatchThread.pumpEventsForHierarchy(EventDispatchThread.java:105)
    at java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:101)
    at java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:93)
    at java.awt.EventDispatchThread.run(EventDispatchThread.java:82)

If it is relevant this occurs in Java 8. Changing JRE/JDK isn't an option.


Solution

  • Accidentally we are having the same error in our environment, but really sporadically, so I could not reproduce it. But your example is perfect and I was able to reproduce my bug. See code below.

    I modified my answer during investigation. Old comments do not rteflect the latest code.

    Yours is a reported bug: https://bugs.java.com/bugdatabase/view_bug.do?bug_id=8068824

    And here is a solution based on bug workaround:

    import java.awt.BorderLayout;
    import java.awt.event.ActionEvent;
    import java.awt.event.ActionListener;
    
    import javax.swing.JFrame;
    import javax.swing.JMenuItem;
    import javax.swing.JPopupMenu;
    import javax.swing.JTable;
    import javax.swing.table.DefaultTableModel;
    import javax.swing.table.TableColumn;
    
    public class BasicExample extends JFrame {
      private JTable t;
      private DefaultTableModel dtm;
    
      public BasicExample() {
        init();
      }
    
      private void init() {
        dtm = new DefaultTableModel(new String[][] { { "a", "b" }, { "c", "d" } },
            new String[] { "A", "B" });
        t = new JTable(dtm);
    
        t.getTableHeader().setComponentPopupMenu(new PopupMenu(t));
        this.setLayout(new BorderLayout());
        add(t.getTableHeader(), BorderLayout.NORTH);
        add(t, BorderLayout.CENTER);
        pack();
        setVisible(true);
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
      }
    
      public class PopupMenu extends JPopupMenu {
    
        private JTable table;
    
        public PopupMenu(JTable table) {
          this.table = table;
          init();
        }
    
        private void init() {
          for (int i = 0; i < table.getModel().getColumnCount(); i++) {
            String columnName = table.getModel().getColumnName(i);
    
            JMenuItem item = new JMenuItem(columnName);
            item.addActionListener(new ActionListener() {
              @Override
              public void actionPerformed(ActionEvent e) {
                TableColumn tc = table.getColumn(columnName);
    
                t.getTableHeader().setDraggedColumn(null);
    
                table.getColumnModel().removeColumn(tc);
              }
            });
            this.add(item);
          }
        }
      }
    
      public static void main(String[] args) {
        BasicExample be = new BasicExample();
      }
    }
    

    Just set draggedColumn to null before removing.

    I kept digging because it did not explain why in our environment the bug was happening really infrequently and only in QA. :)

    Here is how code looks in our environment (thanks again for really good small example).

    import java.awt.BorderLayout;
    import java.awt.event.ActionEvent;
    import java.awt.event.ActionListener;
    import java.awt.event.MouseAdapter;
    import java.awt.event.MouseEvent;
    
    import javax.swing.JFrame;
    import javax.swing.JMenuItem;
    import javax.swing.JPopupMenu;
    import javax.swing.JTable;
    import javax.swing.table.DefaultTableModel;
    import javax.swing.table.TableColumn;
    
    public class BasicExample extends JFrame {
      private JTable t;
      private DefaultTableModel dtm;
    
      public BasicExample() {
        init();
      }
    
      private void init() {
        dtm = new DefaultTableModel(new String[][] { { "a", "b" }, { "c", "d" } },
            new String[] { "A", "B" });
        t = new JTable(dtm);
    
        PopupMenu lPopupMenu = new PopupMenu(t);
    
        //    t.getTableHeader().setComponentPopupMenu(new PopupMenu(t));
        t.getTableHeader().addMouseListener(new MouseAdapter() {
          @Override
          public void mousePressed(MouseEvent pE) {
            lPopupMenu.show(pE.getComponent(), pE.getX(), pE.getY());
          }
        });
    
        this.setLayout(new BorderLayout());
        add(t.getTableHeader(), BorderLayout.NORTH);
        add(t, BorderLayout.CENTER);
        pack();
        setVisible(true);
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
      }
    
      public class PopupMenu extends JPopupMenu {
    
        private JTable table;
    
        public PopupMenu(JTable table) {
          this.table = table;
          init();
        }
    
        private void init() {
          for (int i = 0; i < table.getModel().getColumnCount(); i++) {
            String columnName = table.getModel().getColumnName(i);
    
            JMenuItem item = new JMenuItem(columnName);
            item.addActionListener(new ActionListener() {
              @Override
              public void actionPerformed(ActionEvent e) {
                TableColumn tc = table.getColumn(columnName);
    
                //            t.getTableHeader().setDraggedColumn(null);
    
                table.getColumnModel().removeColumn(tc);
              }
            });
            this.add(item);
          }
        }
      }
    
      public static void main(String[] args) {
        BasicExample be = new BasicExample();
      }
    }
    

    The difference with your code is that we don't use setComponentPopupMenu(), but show PopupMenu manually. And your use case does not produce the exception with our code.
    Why?
    Good question, and I don't have an answer.
    Somehow with your code JPopupMenu consume mouse events. And with our code JPopupMenu propagate mouse events to underlying component. You can see it if you substitute UI for table header.
    And that's how I reproduced the exception with our code: press right mouse button on header, but do not release it and keep moving mouse over popup down away from header. Release right button when pointer away from header (so that header does not process MOUSE_RELEASED event). Get the exception.