Search code examples
javaswing

MouseEvents are not processed due to nested MouseWheelListener


I debugged one issue I have to solve and discovered what seems to be Swing's limitation

If your container receives a MouseEvent, it won't process it right away. Instead, it will try to delegate its processing to one of its children, recursively. The problem with that approach is a parent may delegate processing of clicks to a child that processes wheel motion only, and the click event will be ignored

Here are relevant snippets from the Swing library (the comments are mostly mine)

// java.awt.Container#getMouseEventTargetImpl

// comp is a child of "this", it happens inside a loop
                    if (comp instanceof Container) {
                        Container child = (Container) comp;
                        // recursive call on a container child
                        Component deeper = child.getMouseEventTarget(
                                x - child.x,
                                y - child.y,
                                includeSelf,
                                filter,
                                searchHeavyweightDescendants);
                        // if a child processing MouseEvents is found, it will be the event processor 
                        if (deeper != null) {
                            return deeper;
                        }
                    } else {
                        if (filter.accept(comp)) {
                            // there isn't a deeper target, but this component
                            // is a target
                            return comp;
                        }
                    }
// java.awt.Container.MouseEventTargetFilter

// this filter will return true when called on a MouseWheelListener
// even if the original event was about clicks

    static class MouseEventTargetFilter implements EventTargetFilter {
        static final EventTargetFilter FILTER = new MouseEventTargetFilter();

        private MouseEventTargetFilter() {}

        public boolean accept(final Component comp) {
            return (comp.eventMask & AWTEvent.MOUSE_MOTION_EVENT_MASK) != 0
                || (comp.eventMask & AWTEvent.MOUSE_EVENT_MASK) != 0
                || (comp.eventMask & AWTEvent.MOUSE_WHEEL_EVENT_MASK) != 0
                || comp.mouseListener != null
                || comp.mouseMotionListener != null
                || comp.mouseWheelListener != null;
        }
    }

I also wrote an MRE. No external dependencies are required

package demos.popup;

import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JMenuItem;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.JScrollPane;
import javax.swing.WindowConstants;
import java.awt.BorderLayout;
import java.awt.Container;
import java.awt.Dimension;

/**
 * Right click to trigger a popup.
 */
public class PopupDemo {
    public static void main(String[] args) {
        Container mainPanel = createMainPanel();
        JFrame frame = new JFrame("Popup Demo");
        frame.setContentPane(mainPanel);
        frame.setLocationRelativeTo(null);
        frame.pack();
        frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
        frame.setVisible(true);
    }

    private static JComponent createMainPanel() {
        JComponent mainComp = createPanelWithPopup();
        return mainComp;
    }

    private static JPanel createPanelWithPopup() {
        JPanel panel = new JPanel(new BorderLayout()) {
            @Override
            public String toString() {
                return "Popup Panel";
            }
        };
//         uncomment this to "break" the popup
//        panel.add(createScrollPane());
        panel.setPreferredSize(new Dimension(250,150));
        panel.setComponentPopupMenu(createPopupMenu());
        return panel;
    }

    private static JScrollPane createScrollPane() {
        JScrollPane scroller = new JScrollPane();
        return scroller;
    }

    private static JPopupMenu createPopupMenu() {
        JPopupMenu popupMenu = new JPopupMenu();
        popupMenu.add(createTestMenuItem());
        return popupMenu;
    }

    private static JMenuItem createTestMenuItem() {
        JMenuItem menuItem = new JMenuItem("Test menu item");
        menuItem.addActionListener(e -> System.out.println("Test menu item triggered..."));
        return menuItem;
    }
}

I need to attach a popup to a panel that contains many (dynamic) JScrollPanes. Right clicks are "processed" by a scroll pane, de-facto ignored

How can I work around this problem?


Solution

  • Calling scroller.setInheritsPopupMenu(true) should fix it:

        private static JScrollPane createScrollPane() {
            JScrollPane scroller = new JScrollPane();
            scroller.setInheritsPopupMenu(true);
            return scroller;
        }
    

    The JScrollPane will still consume the MouseEvent, but now it will look at its parents when anyone consults scroller.getComponentPopupMenu().