Mouse wheel scrolling for nested scrollable Swing components











up vote
1
down vote

favorite












By default, mouse wheel scrolling in Java Swing behaves differently than in web browsers. In Swing, when you have an inner scrollable component and an outer scrollable component, the mouse wheel events are only ever dispatched to the inner component. This means that when you are in a text area inside a larger panel, you cannot use the mouse wheel to scroll the panel. You first need to move the mouse cursor out of the text area.



I tested Google Chrome, Firefox, Internet Explorer and Edge, and they almost behave the same. I prefer their behavior over the default Swing behavior, therefore I implemented it.




  • The basic idea is that every top-level window has its "active scrolling component". This component receives all scroll events until it gets inactive by timeout (1 second).

  • To find a new "active component", the one at the mouse cursor is taken. If that component is not scrollable into the direction given by the event, its parent is taken, and so on.

  • The Microsoft browsers behave as if the timeout were 0 seconds, Chrome and Firefox have a timeout of 1 second.

  • When the mouse is moved while scrolling (really an edge case I think), my code happens to do the same as the browsers: scrolling continues with the component below the mouse cursor. I didn't plan for that, but it seems to be sensible.


Here is the code:



package de.roland_illig.playground.scroll;

import java.awt.Component;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import javax.swing.JScrollBar;
import javax.swing.JScrollPane;
import javax.swing.SwingUtilities;

public final class WheelScrolling {

/**
* Passes mouse wheel events to the parent component if this component
* cannot scroll further in the given direction.
* <p>
* This is the behavior of most web browsers and similar programs
* that need to handle nested scrollable components.
*/
public static void install(JScrollPane pane) {
pane.addMouseWheelListener(new Listener(pane));
}

private static class Listener implements MouseWheelListener {

private final JScrollPane pane;
private boolean inHandler; // To avoid StackOverflowError in nested calls

Listener(JScrollPane pane) {
this.pane = pane;
pane.setWheelScrollingEnabled(false);
}

public void mouseWheelMoved(MouseWheelEvent e) {
if (!inHandler) {
inHandler = true;
try {
handleMoved(e);
} finally {
inHandler = false;
}
}
}

private void handleMoved(MouseWheelEvent e) {
JScrollPane curr = currentPane(e);
if (curr == null || curr == pane || e.isControlDown() || e.isAltDown()) {
dispatchDefault(pane, e);
} else {
dispatchDefault(curr, (MouseWheelEvent)
SwingUtilities.convertMouseEvent(pane, e, curr));
}
}

private static void dispatchDefault(JScrollPane comp, MouseWheelEvent e) {
if (comp.isWheelScrollingEnabled()) {
comp.dispatchEvent(e);
} else {
comp.setWheelScrollingEnabled(true);
comp.dispatchEvent(e);
comp.setWheelScrollingEnabled(false);
}
}

private JScrollPane currentPane(MouseWheelEvent e) {
Current current = current(pane);
if (current == null) {
return null;
}

long validUntil = current.validUntil;
current.validUntil = e.getWhen() + 1000;

if (e.getWhen() < validUntil) {
return current.pane;
}

for (Component comp = pane; comp != null; comp = comp.getParent()) {
if (comp instanceof JScrollPane) {
JScrollPane otherPane = (JScrollPane) comp;
if (canScrollFurther(otherPane, e)) {
current.pane = otherPane;
return current.pane;
}
}
}

current.pane = null;
return null;
}

private static boolean canScrollFurther(JScrollPane pane, MouseWheelEvent e) {

// See BasicScrollPaneUI
JScrollBar bar = pane.getVerticalScrollBar();
if (bar == null || !bar.isVisible() || e.isShiftDown()) {
bar = pane.getHorizontalScrollBar();
if (bar == null || !bar.isVisible()) {
return false;
}
}

if (e.getWheelRotation() < 0) {
return bar.getValue() != 0;
} else {
int limit = bar.getMaximum() - bar.getVisibleAmount();
return bar.getValue() != limit;
}
}

private static Current current(Component component) {
if (component.getParent() == null) {
return null;
}

Component top = component;
while (top.getParent() != null) {
top = top.getParent();
}

for (MouseWheelListener listener : top.getMouseWheelListeners()) {
if (listener instanceof Current) {
return (Current) listener;
}
}

Current current = new Current();
top.addMouseWheelListener(current);
return current;
}
}

/**
* The "currently active scroll pane" needs to remembered once
* per top-level window.
* <p>
* Since a Component does not provide a storage for arbitrary data,
* this data is stored in a no-op listener.
*/
private static class Current implements MouseWheelListener {
private JScrollPane pane;
private long validUntil;

@Override
public void mouseWheelMoved(MouseWheelEvent e) {
// Do nothing.
}
}
}


And here is a demo application using the above mouse wheel behavior:



package de.roland_illig.playground.scroll;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Rectangle;
import java.util.Random;
import javax.swing.BoxLayout;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.Scrollable;
import javax.swing.SwingConstants;
import javax.swing.WindowConstants;

public final class WheelScrollingDemo {

public static void main(String args) {
EventQueue.invokeLater(WheelScrollingDemo::main);
}

private static void main() {
Random rnd = new Random(0);

JPanel panel = new ScrollablePanel(new JTextArea());
panel.setLayout(new BoxLayout(panel, BoxLayout.PAGE_AXIS));
for (int i = 0; i < 10; i++) {
panel.add(newInnerPanel(rnd.nextInt(50)));
}

JScrollPane pane = new JScrollPane(panel);

JFrame frame = new JFrame("Mouse Wheel Scrolling Demo");
frame.setPreferredSize(new Dimension(500, 500)); // Just a bad guess.
frame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
frame.getContentPane().add(pane, BorderLayout.CENTER);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}

private static JPanel newInnerPanel(int rows) {
JLabel head = new JLabel("Heading");
head.setHorizontalTextPosition(SwingConstants.CENTER);

JTextArea body = new JTextArea();
body.setColumns(20);
body.setRows(rows);

JScrollPane pane = new JScrollPane(body);
WheelScrolling.install(pane);

JPanel panel = new ScrollablePanel(body);
panel.setLayout(new BorderLayout());
panel.setBackground(Color.LIGHT_GRAY);
panel.setPreferredSize(new Dimension(400, 200)); // Just a bad guess.
panel.add(head, BorderLayout.PAGE_START);
panel.add(pane, BorderLayout.CENTER);

return panel;
}

/**
* Without this class, the mouse wheel scrolling speed differs
* between the JTextArea and the JPanel.
*/
private static final class ScrollablePanel extends JPanel implements Scrollable {

private static final long serialVersionUID = 20181212;

private final Scrollable ref;

/**
* @param ref the component providing the block and unit increments
* for scrolling
*/
private ScrollablePanel(Scrollable ref) {
this.ref = ref;
}

@Override
public Dimension getPreferredScrollableViewportSize() {
return getPreferredSize();
}

@Override
public int getScrollableUnitIncrement(
Rectangle visibleRect, int orientation, int direction) {
return ref.getScrollableUnitIncrement(visibleRect, orientation, direction);
}

@Override
public int getScrollableBlockIncrement(
Rectangle visibleRect, int orientation, int direction) {
return ref.getScrollableBlockIncrement(visibleRect, orientation, direction);
}

@Override
public boolean getScrollableTracksViewportWidth() {
return false;
}

@Override
public boolean getScrollableTracksViewportHeight() {
return false;
}
}
}









share|improve this question




























    up vote
    1
    down vote

    favorite












    By default, mouse wheel scrolling in Java Swing behaves differently than in web browsers. In Swing, when you have an inner scrollable component and an outer scrollable component, the mouse wheel events are only ever dispatched to the inner component. This means that when you are in a text area inside a larger panel, you cannot use the mouse wheel to scroll the panel. You first need to move the mouse cursor out of the text area.



    I tested Google Chrome, Firefox, Internet Explorer and Edge, and they almost behave the same. I prefer their behavior over the default Swing behavior, therefore I implemented it.




    • The basic idea is that every top-level window has its "active scrolling component". This component receives all scroll events until it gets inactive by timeout (1 second).

    • To find a new "active component", the one at the mouse cursor is taken. If that component is not scrollable into the direction given by the event, its parent is taken, and so on.

    • The Microsoft browsers behave as if the timeout were 0 seconds, Chrome and Firefox have a timeout of 1 second.

    • When the mouse is moved while scrolling (really an edge case I think), my code happens to do the same as the browsers: scrolling continues with the component below the mouse cursor. I didn't plan for that, but it seems to be sensible.


    Here is the code:



    package de.roland_illig.playground.scroll;

    import java.awt.Component;
    import java.awt.event.MouseWheelEvent;
    import java.awt.event.MouseWheelListener;
    import javax.swing.JScrollBar;
    import javax.swing.JScrollPane;
    import javax.swing.SwingUtilities;

    public final class WheelScrolling {

    /**
    * Passes mouse wheel events to the parent component if this component
    * cannot scroll further in the given direction.
    * <p>
    * This is the behavior of most web browsers and similar programs
    * that need to handle nested scrollable components.
    */
    public static void install(JScrollPane pane) {
    pane.addMouseWheelListener(new Listener(pane));
    }

    private static class Listener implements MouseWheelListener {

    private final JScrollPane pane;
    private boolean inHandler; // To avoid StackOverflowError in nested calls

    Listener(JScrollPane pane) {
    this.pane = pane;
    pane.setWheelScrollingEnabled(false);
    }

    public void mouseWheelMoved(MouseWheelEvent e) {
    if (!inHandler) {
    inHandler = true;
    try {
    handleMoved(e);
    } finally {
    inHandler = false;
    }
    }
    }

    private void handleMoved(MouseWheelEvent e) {
    JScrollPane curr = currentPane(e);
    if (curr == null || curr == pane || e.isControlDown() || e.isAltDown()) {
    dispatchDefault(pane, e);
    } else {
    dispatchDefault(curr, (MouseWheelEvent)
    SwingUtilities.convertMouseEvent(pane, e, curr));
    }
    }

    private static void dispatchDefault(JScrollPane comp, MouseWheelEvent e) {
    if (comp.isWheelScrollingEnabled()) {
    comp.dispatchEvent(e);
    } else {
    comp.setWheelScrollingEnabled(true);
    comp.dispatchEvent(e);
    comp.setWheelScrollingEnabled(false);
    }
    }

    private JScrollPane currentPane(MouseWheelEvent e) {
    Current current = current(pane);
    if (current == null) {
    return null;
    }

    long validUntil = current.validUntil;
    current.validUntil = e.getWhen() + 1000;

    if (e.getWhen() < validUntil) {
    return current.pane;
    }

    for (Component comp = pane; comp != null; comp = comp.getParent()) {
    if (comp instanceof JScrollPane) {
    JScrollPane otherPane = (JScrollPane) comp;
    if (canScrollFurther(otherPane, e)) {
    current.pane = otherPane;
    return current.pane;
    }
    }
    }

    current.pane = null;
    return null;
    }

    private static boolean canScrollFurther(JScrollPane pane, MouseWheelEvent e) {

    // See BasicScrollPaneUI
    JScrollBar bar = pane.getVerticalScrollBar();
    if (bar == null || !bar.isVisible() || e.isShiftDown()) {
    bar = pane.getHorizontalScrollBar();
    if (bar == null || !bar.isVisible()) {
    return false;
    }
    }

    if (e.getWheelRotation() < 0) {
    return bar.getValue() != 0;
    } else {
    int limit = bar.getMaximum() - bar.getVisibleAmount();
    return bar.getValue() != limit;
    }
    }

    private static Current current(Component component) {
    if (component.getParent() == null) {
    return null;
    }

    Component top = component;
    while (top.getParent() != null) {
    top = top.getParent();
    }

    for (MouseWheelListener listener : top.getMouseWheelListeners()) {
    if (listener instanceof Current) {
    return (Current) listener;
    }
    }

    Current current = new Current();
    top.addMouseWheelListener(current);
    return current;
    }
    }

    /**
    * The "currently active scroll pane" needs to remembered once
    * per top-level window.
    * <p>
    * Since a Component does not provide a storage for arbitrary data,
    * this data is stored in a no-op listener.
    */
    private static class Current implements MouseWheelListener {
    private JScrollPane pane;
    private long validUntil;

    @Override
    public void mouseWheelMoved(MouseWheelEvent e) {
    // Do nothing.
    }
    }
    }


    And here is a demo application using the above mouse wheel behavior:



    package de.roland_illig.playground.scroll;

    import java.awt.BorderLayout;
    import java.awt.Color;
    import java.awt.Dimension;
    import java.awt.EventQueue;
    import java.awt.Rectangle;
    import java.util.Random;
    import javax.swing.BoxLayout;
    import javax.swing.JFrame;
    import javax.swing.JLabel;
    import javax.swing.JPanel;
    import javax.swing.JScrollPane;
    import javax.swing.JTextArea;
    import javax.swing.Scrollable;
    import javax.swing.SwingConstants;
    import javax.swing.WindowConstants;

    public final class WheelScrollingDemo {

    public static void main(String args) {
    EventQueue.invokeLater(WheelScrollingDemo::main);
    }

    private static void main() {
    Random rnd = new Random(0);

    JPanel panel = new ScrollablePanel(new JTextArea());
    panel.setLayout(new BoxLayout(panel, BoxLayout.PAGE_AXIS));
    for (int i = 0; i < 10; i++) {
    panel.add(newInnerPanel(rnd.nextInt(50)));
    }

    JScrollPane pane = new JScrollPane(panel);

    JFrame frame = new JFrame("Mouse Wheel Scrolling Demo");
    frame.setPreferredSize(new Dimension(500, 500)); // Just a bad guess.
    frame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
    frame.getContentPane().add(pane, BorderLayout.CENTER);
    frame.pack();
    frame.setLocationRelativeTo(null);
    frame.setVisible(true);
    }

    private static JPanel newInnerPanel(int rows) {
    JLabel head = new JLabel("Heading");
    head.setHorizontalTextPosition(SwingConstants.CENTER);

    JTextArea body = new JTextArea();
    body.setColumns(20);
    body.setRows(rows);

    JScrollPane pane = new JScrollPane(body);
    WheelScrolling.install(pane);

    JPanel panel = new ScrollablePanel(body);
    panel.setLayout(new BorderLayout());
    panel.setBackground(Color.LIGHT_GRAY);
    panel.setPreferredSize(new Dimension(400, 200)); // Just a bad guess.
    panel.add(head, BorderLayout.PAGE_START);
    panel.add(pane, BorderLayout.CENTER);

    return panel;
    }

    /**
    * Without this class, the mouse wheel scrolling speed differs
    * between the JTextArea and the JPanel.
    */
    private static final class ScrollablePanel extends JPanel implements Scrollable {

    private static final long serialVersionUID = 20181212;

    private final Scrollable ref;

    /**
    * @param ref the component providing the block and unit increments
    * for scrolling
    */
    private ScrollablePanel(Scrollable ref) {
    this.ref = ref;
    }

    @Override
    public Dimension getPreferredScrollableViewportSize() {
    return getPreferredSize();
    }

    @Override
    public int getScrollableUnitIncrement(
    Rectangle visibleRect, int orientation, int direction) {
    return ref.getScrollableUnitIncrement(visibleRect, orientation, direction);
    }

    @Override
    public int getScrollableBlockIncrement(
    Rectangle visibleRect, int orientation, int direction) {
    return ref.getScrollableBlockIncrement(visibleRect, orientation, direction);
    }

    @Override
    public boolean getScrollableTracksViewportWidth() {
    return false;
    }

    @Override
    public boolean getScrollableTracksViewportHeight() {
    return false;
    }
    }
    }









    share|improve this question


























      up vote
      1
      down vote

      favorite









      up vote
      1
      down vote

      favorite











      By default, mouse wheel scrolling in Java Swing behaves differently than in web browsers. In Swing, when you have an inner scrollable component and an outer scrollable component, the mouse wheel events are only ever dispatched to the inner component. This means that when you are in a text area inside a larger panel, you cannot use the mouse wheel to scroll the panel. You first need to move the mouse cursor out of the text area.



      I tested Google Chrome, Firefox, Internet Explorer and Edge, and they almost behave the same. I prefer their behavior over the default Swing behavior, therefore I implemented it.




      • The basic idea is that every top-level window has its "active scrolling component". This component receives all scroll events until it gets inactive by timeout (1 second).

      • To find a new "active component", the one at the mouse cursor is taken. If that component is not scrollable into the direction given by the event, its parent is taken, and so on.

      • The Microsoft browsers behave as if the timeout were 0 seconds, Chrome and Firefox have a timeout of 1 second.

      • When the mouse is moved while scrolling (really an edge case I think), my code happens to do the same as the browsers: scrolling continues with the component below the mouse cursor. I didn't plan for that, but it seems to be sensible.


      Here is the code:



      package de.roland_illig.playground.scroll;

      import java.awt.Component;
      import java.awt.event.MouseWheelEvent;
      import java.awt.event.MouseWheelListener;
      import javax.swing.JScrollBar;
      import javax.swing.JScrollPane;
      import javax.swing.SwingUtilities;

      public final class WheelScrolling {

      /**
      * Passes mouse wheel events to the parent component if this component
      * cannot scroll further in the given direction.
      * <p>
      * This is the behavior of most web browsers and similar programs
      * that need to handle nested scrollable components.
      */
      public static void install(JScrollPane pane) {
      pane.addMouseWheelListener(new Listener(pane));
      }

      private static class Listener implements MouseWheelListener {

      private final JScrollPane pane;
      private boolean inHandler; // To avoid StackOverflowError in nested calls

      Listener(JScrollPane pane) {
      this.pane = pane;
      pane.setWheelScrollingEnabled(false);
      }

      public void mouseWheelMoved(MouseWheelEvent e) {
      if (!inHandler) {
      inHandler = true;
      try {
      handleMoved(e);
      } finally {
      inHandler = false;
      }
      }
      }

      private void handleMoved(MouseWheelEvent e) {
      JScrollPane curr = currentPane(e);
      if (curr == null || curr == pane || e.isControlDown() || e.isAltDown()) {
      dispatchDefault(pane, e);
      } else {
      dispatchDefault(curr, (MouseWheelEvent)
      SwingUtilities.convertMouseEvent(pane, e, curr));
      }
      }

      private static void dispatchDefault(JScrollPane comp, MouseWheelEvent e) {
      if (comp.isWheelScrollingEnabled()) {
      comp.dispatchEvent(e);
      } else {
      comp.setWheelScrollingEnabled(true);
      comp.dispatchEvent(e);
      comp.setWheelScrollingEnabled(false);
      }
      }

      private JScrollPane currentPane(MouseWheelEvent e) {
      Current current = current(pane);
      if (current == null) {
      return null;
      }

      long validUntil = current.validUntil;
      current.validUntil = e.getWhen() + 1000;

      if (e.getWhen() < validUntil) {
      return current.pane;
      }

      for (Component comp = pane; comp != null; comp = comp.getParent()) {
      if (comp instanceof JScrollPane) {
      JScrollPane otherPane = (JScrollPane) comp;
      if (canScrollFurther(otherPane, e)) {
      current.pane = otherPane;
      return current.pane;
      }
      }
      }

      current.pane = null;
      return null;
      }

      private static boolean canScrollFurther(JScrollPane pane, MouseWheelEvent e) {

      // See BasicScrollPaneUI
      JScrollBar bar = pane.getVerticalScrollBar();
      if (bar == null || !bar.isVisible() || e.isShiftDown()) {
      bar = pane.getHorizontalScrollBar();
      if (bar == null || !bar.isVisible()) {
      return false;
      }
      }

      if (e.getWheelRotation() < 0) {
      return bar.getValue() != 0;
      } else {
      int limit = bar.getMaximum() - bar.getVisibleAmount();
      return bar.getValue() != limit;
      }
      }

      private static Current current(Component component) {
      if (component.getParent() == null) {
      return null;
      }

      Component top = component;
      while (top.getParent() != null) {
      top = top.getParent();
      }

      for (MouseWheelListener listener : top.getMouseWheelListeners()) {
      if (listener instanceof Current) {
      return (Current) listener;
      }
      }

      Current current = new Current();
      top.addMouseWheelListener(current);
      return current;
      }
      }

      /**
      * The "currently active scroll pane" needs to remembered once
      * per top-level window.
      * <p>
      * Since a Component does not provide a storage for arbitrary data,
      * this data is stored in a no-op listener.
      */
      private static class Current implements MouseWheelListener {
      private JScrollPane pane;
      private long validUntil;

      @Override
      public void mouseWheelMoved(MouseWheelEvent e) {
      // Do nothing.
      }
      }
      }


      And here is a demo application using the above mouse wheel behavior:



      package de.roland_illig.playground.scroll;

      import java.awt.BorderLayout;
      import java.awt.Color;
      import java.awt.Dimension;
      import java.awt.EventQueue;
      import java.awt.Rectangle;
      import java.util.Random;
      import javax.swing.BoxLayout;
      import javax.swing.JFrame;
      import javax.swing.JLabel;
      import javax.swing.JPanel;
      import javax.swing.JScrollPane;
      import javax.swing.JTextArea;
      import javax.swing.Scrollable;
      import javax.swing.SwingConstants;
      import javax.swing.WindowConstants;

      public final class WheelScrollingDemo {

      public static void main(String args) {
      EventQueue.invokeLater(WheelScrollingDemo::main);
      }

      private static void main() {
      Random rnd = new Random(0);

      JPanel panel = new ScrollablePanel(new JTextArea());
      panel.setLayout(new BoxLayout(panel, BoxLayout.PAGE_AXIS));
      for (int i = 0; i < 10; i++) {
      panel.add(newInnerPanel(rnd.nextInt(50)));
      }

      JScrollPane pane = new JScrollPane(panel);

      JFrame frame = new JFrame("Mouse Wheel Scrolling Demo");
      frame.setPreferredSize(new Dimension(500, 500)); // Just a bad guess.
      frame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
      frame.getContentPane().add(pane, BorderLayout.CENTER);
      frame.pack();
      frame.setLocationRelativeTo(null);
      frame.setVisible(true);
      }

      private static JPanel newInnerPanel(int rows) {
      JLabel head = new JLabel("Heading");
      head.setHorizontalTextPosition(SwingConstants.CENTER);

      JTextArea body = new JTextArea();
      body.setColumns(20);
      body.setRows(rows);

      JScrollPane pane = new JScrollPane(body);
      WheelScrolling.install(pane);

      JPanel panel = new ScrollablePanel(body);
      panel.setLayout(new BorderLayout());
      panel.setBackground(Color.LIGHT_GRAY);
      panel.setPreferredSize(new Dimension(400, 200)); // Just a bad guess.
      panel.add(head, BorderLayout.PAGE_START);
      panel.add(pane, BorderLayout.CENTER);

      return panel;
      }

      /**
      * Without this class, the mouse wheel scrolling speed differs
      * between the JTextArea and the JPanel.
      */
      private static final class ScrollablePanel extends JPanel implements Scrollable {

      private static final long serialVersionUID = 20181212;

      private final Scrollable ref;

      /**
      * @param ref the component providing the block and unit increments
      * for scrolling
      */
      private ScrollablePanel(Scrollable ref) {
      this.ref = ref;
      }

      @Override
      public Dimension getPreferredScrollableViewportSize() {
      return getPreferredSize();
      }

      @Override
      public int getScrollableUnitIncrement(
      Rectangle visibleRect, int orientation, int direction) {
      return ref.getScrollableUnitIncrement(visibleRect, orientation, direction);
      }

      @Override
      public int getScrollableBlockIncrement(
      Rectangle visibleRect, int orientation, int direction) {
      return ref.getScrollableBlockIncrement(visibleRect, orientation, direction);
      }

      @Override
      public boolean getScrollableTracksViewportWidth() {
      return false;
      }

      @Override
      public boolean getScrollableTracksViewportHeight() {
      return false;
      }
      }
      }









      share|improve this question















      By default, mouse wheel scrolling in Java Swing behaves differently than in web browsers. In Swing, when you have an inner scrollable component and an outer scrollable component, the mouse wheel events are only ever dispatched to the inner component. This means that when you are in a text area inside a larger panel, you cannot use the mouse wheel to scroll the panel. You first need to move the mouse cursor out of the text area.



      I tested Google Chrome, Firefox, Internet Explorer and Edge, and they almost behave the same. I prefer their behavior over the default Swing behavior, therefore I implemented it.




      • The basic idea is that every top-level window has its "active scrolling component". This component receives all scroll events until it gets inactive by timeout (1 second).

      • To find a new "active component", the one at the mouse cursor is taken. If that component is not scrollable into the direction given by the event, its parent is taken, and so on.

      • The Microsoft browsers behave as if the timeout were 0 seconds, Chrome and Firefox have a timeout of 1 second.

      • When the mouse is moved while scrolling (really an edge case I think), my code happens to do the same as the browsers: scrolling continues with the component below the mouse cursor. I didn't plan for that, but it seems to be sensible.


      Here is the code:



      package de.roland_illig.playground.scroll;

      import java.awt.Component;
      import java.awt.event.MouseWheelEvent;
      import java.awt.event.MouseWheelListener;
      import javax.swing.JScrollBar;
      import javax.swing.JScrollPane;
      import javax.swing.SwingUtilities;

      public final class WheelScrolling {

      /**
      * Passes mouse wheel events to the parent component if this component
      * cannot scroll further in the given direction.
      * <p>
      * This is the behavior of most web browsers and similar programs
      * that need to handle nested scrollable components.
      */
      public static void install(JScrollPane pane) {
      pane.addMouseWheelListener(new Listener(pane));
      }

      private static class Listener implements MouseWheelListener {

      private final JScrollPane pane;
      private boolean inHandler; // To avoid StackOverflowError in nested calls

      Listener(JScrollPane pane) {
      this.pane = pane;
      pane.setWheelScrollingEnabled(false);
      }

      public void mouseWheelMoved(MouseWheelEvent e) {
      if (!inHandler) {
      inHandler = true;
      try {
      handleMoved(e);
      } finally {
      inHandler = false;
      }
      }
      }

      private void handleMoved(MouseWheelEvent e) {
      JScrollPane curr = currentPane(e);
      if (curr == null || curr == pane || e.isControlDown() || e.isAltDown()) {
      dispatchDefault(pane, e);
      } else {
      dispatchDefault(curr, (MouseWheelEvent)
      SwingUtilities.convertMouseEvent(pane, e, curr));
      }
      }

      private static void dispatchDefault(JScrollPane comp, MouseWheelEvent e) {
      if (comp.isWheelScrollingEnabled()) {
      comp.dispatchEvent(e);
      } else {
      comp.setWheelScrollingEnabled(true);
      comp.dispatchEvent(e);
      comp.setWheelScrollingEnabled(false);
      }
      }

      private JScrollPane currentPane(MouseWheelEvent e) {
      Current current = current(pane);
      if (current == null) {
      return null;
      }

      long validUntil = current.validUntil;
      current.validUntil = e.getWhen() + 1000;

      if (e.getWhen() < validUntil) {
      return current.pane;
      }

      for (Component comp = pane; comp != null; comp = comp.getParent()) {
      if (comp instanceof JScrollPane) {
      JScrollPane otherPane = (JScrollPane) comp;
      if (canScrollFurther(otherPane, e)) {
      current.pane = otherPane;
      return current.pane;
      }
      }
      }

      current.pane = null;
      return null;
      }

      private static boolean canScrollFurther(JScrollPane pane, MouseWheelEvent e) {

      // See BasicScrollPaneUI
      JScrollBar bar = pane.getVerticalScrollBar();
      if (bar == null || !bar.isVisible() || e.isShiftDown()) {
      bar = pane.getHorizontalScrollBar();
      if (bar == null || !bar.isVisible()) {
      return false;
      }
      }

      if (e.getWheelRotation() < 0) {
      return bar.getValue() != 0;
      } else {
      int limit = bar.getMaximum() - bar.getVisibleAmount();
      return bar.getValue() != limit;
      }
      }

      private static Current current(Component component) {
      if (component.getParent() == null) {
      return null;
      }

      Component top = component;
      while (top.getParent() != null) {
      top = top.getParent();
      }

      for (MouseWheelListener listener : top.getMouseWheelListeners()) {
      if (listener instanceof Current) {
      return (Current) listener;
      }
      }

      Current current = new Current();
      top.addMouseWheelListener(current);
      return current;
      }
      }

      /**
      * The "currently active scroll pane" needs to remembered once
      * per top-level window.
      * <p>
      * Since a Component does not provide a storage for arbitrary data,
      * this data is stored in a no-op listener.
      */
      private static class Current implements MouseWheelListener {
      private JScrollPane pane;
      private long validUntil;

      @Override
      public void mouseWheelMoved(MouseWheelEvent e) {
      // Do nothing.
      }
      }
      }


      And here is a demo application using the above mouse wheel behavior:



      package de.roland_illig.playground.scroll;

      import java.awt.BorderLayout;
      import java.awt.Color;
      import java.awt.Dimension;
      import java.awt.EventQueue;
      import java.awt.Rectangle;
      import java.util.Random;
      import javax.swing.BoxLayout;
      import javax.swing.JFrame;
      import javax.swing.JLabel;
      import javax.swing.JPanel;
      import javax.swing.JScrollPane;
      import javax.swing.JTextArea;
      import javax.swing.Scrollable;
      import javax.swing.SwingConstants;
      import javax.swing.WindowConstants;

      public final class WheelScrollingDemo {

      public static void main(String args) {
      EventQueue.invokeLater(WheelScrollingDemo::main);
      }

      private static void main() {
      Random rnd = new Random(0);

      JPanel panel = new ScrollablePanel(new JTextArea());
      panel.setLayout(new BoxLayout(panel, BoxLayout.PAGE_AXIS));
      for (int i = 0; i < 10; i++) {
      panel.add(newInnerPanel(rnd.nextInt(50)));
      }

      JScrollPane pane = new JScrollPane(panel);

      JFrame frame = new JFrame("Mouse Wheel Scrolling Demo");
      frame.setPreferredSize(new Dimension(500, 500)); // Just a bad guess.
      frame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
      frame.getContentPane().add(pane, BorderLayout.CENTER);
      frame.pack();
      frame.setLocationRelativeTo(null);
      frame.setVisible(true);
      }

      private static JPanel newInnerPanel(int rows) {
      JLabel head = new JLabel("Heading");
      head.setHorizontalTextPosition(SwingConstants.CENTER);

      JTextArea body = new JTextArea();
      body.setColumns(20);
      body.setRows(rows);

      JScrollPane pane = new JScrollPane(body);
      WheelScrolling.install(pane);

      JPanel panel = new ScrollablePanel(body);
      panel.setLayout(new BorderLayout());
      panel.setBackground(Color.LIGHT_GRAY);
      panel.setPreferredSize(new Dimension(400, 200)); // Just a bad guess.
      panel.add(head, BorderLayout.PAGE_START);
      panel.add(pane, BorderLayout.CENTER);

      return panel;
      }

      /**
      * Without this class, the mouse wheel scrolling speed differs
      * between the JTextArea and the JPanel.
      */
      private static final class ScrollablePanel extends JPanel implements Scrollable {

      private static final long serialVersionUID = 20181212;

      private final Scrollable ref;

      /**
      * @param ref the component providing the block and unit increments
      * for scrolling
      */
      private ScrollablePanel(Scrollable ref) {
      this.ref = ref;
      }

      @Override
      public Dimension getPreferredScrollableViewportSize() {
      return getPreferredSize();
      }

      @Override
      public int getScrollableUnitIncrement(
      Rectangle visibleRect, int orientation, int direction) {
      return ref.getScrollableUnitIncrement(visibleRect, orientation, direction);
      }

      @Override
      public int getScrollableBlockIncrement(
      Rectangle visibleRect, int orientation, int direction) {
      return ref.getScrollableBlockIncrement(visibleRect, orientation, direction);
      }

      @Override
      public boolean getScrollableTracksViewportWidth() {
      return false;
      }

      @Override
      public boolean getScrollableTracksViewportHeight() {
      return false;
      }
      }
      }






      java swing gui






      share|improve this question















      share|improve this question













      share|improve this question




      share|improve this question








      edited 8 hours ago

























      asked 10 hours ago









      Roland Illig

      10.8k11844




      10.8k11844



























          active

          oldest

          votes











          Your Answer





          StackExchange.ifUsing("editor", function () {
          return StackExchange.using("mathjaxEditing", function () {
          StackExchange.MarkdownEditor.creationCallbacks.add(function (editor, postfix) {
          StackExchange.mathjaxEditing.prepareWmdForMathJax(editor, postfix, [["\$", "\$"]]);
          });
          });
          }, "mathjax-editing");

          StackExchange.ifUsing("editor", function () {
          StackExchange.using("externalEditor", function () {
          StackExchange.using("snippets", function () {
          StackExchange.snippets.init();
          });
          });
          }, "code-snippets");

          StackExchange.ready(function() {
          var channelOptions = {
          tags: "".split(" "),
          id: "196"
          };
          initTagRenderer("".split(" "), "".split(" "), channelOptions);

          StackExchange.using("externalEditor", function() {
          // Have to fire editor after snippets, if snippets enabled
          if (StackExchange.settings.snippets.snippetsEnabled) {
          StackExchange.using("snippets", function() {
          createEditor();
          });
          }
          else {
          createEditor();
          }
          });

          function createEditor() {
          StackExchange.prepareEditor({
          heartbeatType: 'answer',
          convertImagesToLinks: false,
          noModals: true,
          showLowRepImageUploadWarning: true,
          reputationToPostImages: null,
          bindNavPrevention: true,
          postfix: "",
          imageUploader: {
          brandingHtml: "Powered by u003ca class="icon-imgur-white" href="https://imgur.com/"u003eu003c/au003e",
          contentPolicyHtml: "User contributions licensed under u003ca href="https://creativecommons.org/licenses/by-sa/3.0/"u003ecc by-sa 3.0 with attribution requiredu003c/au003e u003ca href="https://stackoverflow.com/legal/content-policy"u003e(content policy)u003c/au003e",
          allowUrls: true
          },
          onDemand: true,
          discardSelector: ".discard-answer"
          ,immediatelyShowMarkdownHelp:true
          });


          }
          });














          draft saved

          draft discarded


















          StackExchange.ready(
          function () {
          StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f209546%2fmouse-wheel-scrolling-for-nested-scrollable-swing-components%23new-answer', 'question_page');
          }
          );

          Post as a guest















          Required, but never shown






























          active

          oldest

          votes













          active

          oldest

          votes









          active

          oldest

          votes






          active

          oldest

          votes
















          draft saved

          draft discarded




















































          Thanks for contributing an answer to Code Review Stack Exchange!


          • Please be sure to answer the question. Provide details and share your research!

          But avoid



          • Asking for help, clarification, or responding to other answers.

          • Making statements based on opinion; back them up with references or personal experience.


          Use MathJax to format equations. MathJax reference.


          To learn more, see our tips on writing great answers.





          Some of your past answers have not been well-received, and you're in danger of being blocked from answering.


          Please pay close attention to the following guidance:


          • Please be sure to answer the question. Provide details and share your research!

          But avoid



          • Asking for help, clarification, or responding to other answers.

          • Making statements based on opinion; back them up with references or personal experience.


          To learn more, see our tips on writing great answers.




          draft saved


          draft discarded














          StackExchange.ready(
          function () {
          StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f209546%2fmouse-wheel-scrolling-for-nested-scrollable-swing-components%23new-answer', 'question_page');
          }
          );

          Post as a guest















          Required, but never shown





















































          Required, but never shown














          Required, but never shown












          Required, but never shown







          Required, but never shown

































          Required, but never shown














          Required, but never shown












          Required, but never shown







          Required, but never shown







          Popular posts from this blog

          Morgemoulin

          Scott Moir

          Souastre