Why JScrollPane does not react to mouse wheel events?

Solution 1:

Walter beat me to analysing the issue :-)

Adding a bit of detail:

It's correct that a JScrollPane supports mouseWheelHandling. According to the rules of mouseEvent dispatching, the top-most (in z-order) component gets the event, and that's the scrollPane around the textArea. So if wheeling the textarea is not required, a simple solution might be to disable the wheel-support in its scrollPane. And JScrollPane even has api for doing it:

scrollPane.setWheelScrollingEnabled(false); 

Unfortunately, that doesn't work. Reason it's not working is that this property has no effect in the event dispatch chain which ultimately calls into eventTypeEnabled:

case MouseEvent.MOUSE_WHEEL:
      if ((eventMask & AWTEvent.MOUSE_WHEEL_EVENT_MASK) != 0 ||
          mouseWheelListener != null) {
          return true;
      }

This returns true if a mouseWheelListener is installed - which is done unconditionally by BasicScrollPaneUI, and not removed when the wheelEnabled property is changed (the ui doesn't even listen to that property ...) Plus the listener simply does nothing if the property is false. At least one of those facts is a bug, the ui should

  • either remove/add the listener depending on wheelEnabled
  • or: implement the listener such that it dispatches the event up the chain (as Walter does in his example)

The first option can be handled by application code:

scrollPane = new JScrollPane();
scrollPane.removeMouseWheelListener(scrollPane.getMouseWheelListeners()[0]);

it's a bit of a hack (as bug-workarounds always are :-), production code would have to listen to the wheelEnable to re-install if needed plus listen to LAF changes to update/re-remove the listeners installed by the ui.

Implementing the second option in slight modification (as to Walter's dispatching) by subclassing the JScrollPane and dispatch the event to parent if the wheelEnabled is false:

scrollPane = new JScrollPane() {

    @Override
    protected void processMouseWheelEvent(MouseWheelEvent e) {
        if (!isWheelScrollingEnabled()) {
            if (getParent() != null) 
                getParent().dispatchEvent(
                        SwingUtilities.convertMouseEvent(this, e, getParent()));
            return;
        }
        super.processMouseWheelEvent(e);
    }

};
scrollPane.setWheelScrollingEnabled(false); 

Solution 2:

The mouse wheel event gets consumed by the scroll pane around the text area. You can try to manually pass the event to the parent scroll pane like this:

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;

public class TestScrollPane2 {
    public static void main(String[] args) {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                // might want to use a http://tips4java.wordpress.com/2009/12/20/scrollable-panel/
                JPanel panel = new JPanel(new GridLayout(0, 1));
                for (int i = 0; i < 10; i++) {
                    panel.add(new JScrollPane(new JTextArea(3, 40)) {
                         @Override
                        protected void processMouseWheelEvent(MouseWheelEvent e) {
                            Point oldPosition = getViewport().getViewPosition();
                            super.processMouseWheelEvent(e);

                            if(getViewport().getViewPosition().y == oldPosition.y) {
                                delegateToParent(e);
                            }
                        }

                        private void delegateToParent(MouseWheelEvent e) {
                            // even with scroll bar set to never the event doesn't reach the parent scroll frame
                            JScrollPane ancestor = (JScrollPane) SwingUtilities.getAncestorOfClass(
                                    JScrollPane.class, this);
                            if (ancestor != null) {
                                MouseWheelEvent converted = null;
                                for (MouseWheelListener listener : ancestor
                                        .getMouseWheelListeners()) {
                                    listener.mouseWheelMoved(converted != null ? converted
                                            : (converted = (MouseWheelEvent) SwingUtilities
                                                    .convertMouseEvent(this, e, ancestor)));
                                }
                            }
                        }
                    });
                }
                JFrame frame = new JFrame("Test");
                frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
                frame.getContentPane().add(new JScrollPane(panel));
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
            }
        });
    }
}