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;
}
}
}
java swing gui
add a comment |
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;
}
}
}
java swing gui
add a comment |
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;
}
}
}
java swing gui
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
java swing gui
edited 8 hours ago
asked 10 hours ago
Roland Illig
10.8k11844
10.8k11844
add a comment |
add a comment |
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
});
}
});
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
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
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.
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
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
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
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