44import java .awt .Graphics ;
55import java .awt .Graphics2D ;
66import java .awt .GraphicsEnvironment ;
7+ import java .awt .Rectangle ;
8+ import java .awt .event .MouseWheelEvent ;
9+ import java .awt .event .MouseWheelListener ;
710import java .awt .geom .AffineTransform ;
11+ import javax .swing .JScrollBar ;
812import javax .swing .JScrollPane ;
913import javax .swing .JViewport ;
14+ import javax .swing .Scrollable ;
15+ import javax .swing .SwingConstants ;
16+ import javax .swing .UIManager ;
1017
1118/**
1219 * The {@code JitterlessScrollPane} class is a {@code JScrollPane} that combats jittering of
@@ -21,15 +28,12 @@ public class JitterlessScrollPane extends JScrollPane {
2128
2229 public JitterlessScrollPane () {
2330 super ();
31+ addMouseWheelListener (new PreciseWheelListener ());
2432 }
2533
2634 public JitterlessScrollPane (Component view ) {
2735 super (view );
28- }
29-
30- @ Override
31- public String getUIClassID () {
32- return "JitterlessScrollPaneUI" ;
36+ addMouseWheelListener (new PreciseWheelListener ());
3337 }
3438
3539 public void setFracOffsetX (double fracOffsetX ) {
@@ -83,7 +87,7 @@ protected void paintChildren(Graphics graphics) {
8387
8488 Graphics2D g = (Graphics2D ) graphics .create ();
8589
86- // Get DPI scale from screen
90+ // Get graphics environment scale from screen
8791 AffineTransform scaleDPI = GraphicsEnvironment
8892 .getLocalGraphicsEnvironment ()
8993 .getDefaultScreenDevice ()
@@ -102,4 +106,91 @@ protected void paintChildren(Graphics graphics) {
102106 g .dispose ();
103107 }
104108 }
109+
110+ /**
111+ * The {@code PreciseWheelListener} class is used by the {@code JitterlessScrollPane} class
112+ * to intercept scrolling events from precise inputs like trackpads and uses them to give
113+ * the {@code JitterlessScrollPane} sub-pixel scrolling accuracy and prevent "jumping".
114+ */
115+ protected class PreciseWheelListener implements MouseWheelListener {
116+
117+ @ Override
118+ public void mouseWheelMoved (MouseWheelEvent e ) {
119+
120+ // Only intercept scroll wheel events from precise inputs and if enabled
121+ if (
122+ isWheelScrollingEnabled () &&
123+ UIManager .getBoolean ("ScrollPane.smoothScrolling" ) &&
124+ e .getPreciseWheelRotation () != e .getWheelRotation () &&
125+ viewport != null
126+ ) {
127+ // Modified code from com.formdev.flatlaf.ui.FlatScrollPaneUI#mouseWheelMovedSmooth
128+ JScrollBar scrollbar = verticalScrollBar ;
129+ if (scrollbar == null || !scrollbar .isVisible () || e .isShiftDown ()) {
130+ scrollbar = horizontalScrollBar ;
131+ if (scrollbar == null || !scrollbar .isVisible ()) {
132+ return ;
133+ }
134+ }
135+
136+ e .consume ();
137+
138+ double rotation = e .getPreciseWheelRotation ();
139+ int unitIncrement ;
140+ int orientation = scrollbar .getOrientation ();
141+ Component view = viewport .getView ();
142+
143+ if (view instanceof Scrollable scrollable ) {
144+
145+ Rectangle visibleRect = new Rectangle (viewport .getExtentSize ());
146+ unitIncrement = scrollable .getScrollableUnitIncrement (visibleRect , orientation , 1 );
147+
148+ if (unitIncrement > 0 ) {
149+
150+ if (orientation == SwingConstants .VERTICAL ) {
151+ visibleRect .y += unitIncrement ;
152+ visibleRect .height -= unitIncrement ;
153+ } else {
154+ visibleRect .x += unitIncrement ;
155+ visibleRect .width -= unitIncrement ;
156+ }
157+
158+ int unitIncrement2 = scrollable .getScrollableUnitIncrement (visibleRect , orientation , 1 );
159+ if (unitIncrement2 > 0 ) {
160+ unitIncrement = Math .min (unitIncrement , unitIncrement2 );
161+ }
162+ }
163+ } else {
164+
165+ int direction = rotation < 0 ? -1 : 1 ;
166+ unitIncrement = scrollbar .getUnitIncrement (direction );
167+ }
168+
169+ // Compute new position based on previous sub-pixel offset
170+ double delta = rotation * unitIncrement * e .getScrollAmount () +
171+ ((orientation == SwingConstants .VERTICAL ) ?
172+ getFracOffsetY () : getFracOffsetX ());
173+ int idelta = (int ) Math .round (delta );
174+
175+ int value = scrollbar .getValue ();
176+ int minValue = scrollbar .getMinimum ();
177+ int maxValue = scrollbar .getMaximum () - scrollbar .getModel ().getExtent ();
178+ int newValue = Math .max (minValue , Math .min (value + idelta , maxValue ));
179+
180+ if (newValue != value ) {
181+ scrollbar .setValue (newValue );
182+ }
183+
184+ // Set new sub-pixel offset
185+ double newFracOffset =
186+ (value + delta > minValue && value + delta < maxValue ) ?
187+ delta - idelta : 0.0 ;
188+ if (orientation == SwingConstants .VERTICAL ) {
189+ setFracOffsetY (newFracOffset );
190+ } else {
191+ setFracOffsetX (newFracOffset );
192+ }
193+ }
194+ }
195+ }
105196}
0 commit comments