Skip to content

Commit cf70cfb

Browse files
committed
ScrollBar: fixed temporary painting at wrong location during smooth scrolling when using mouse-wheel or scroll bar
(still occurs when scrolling by moving selection via keyboard) many thanks to @Chrriis for the idea to temporary disable blitting mode on viewport
1 parent 29f6c5f commit cf70cfb

File tree

4 files changed

+97
-35
lines changed

4 files changed

+97
-35
lines changed

flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatScrollBarUI.java

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import javax.swing.JComponent;
3535
import javax.swing.JScrollBar;
3636
import javax.swing.JScrollPane;
37+
import javax.swing.JViewport;
3738
import javax.swing.SwingUtilities;
3839
import javax.swing.UIManager;
3940
import javax.swing.plaf.ComponentUI;
@@ -480,12 +481,14 @@ public void runAndSetValueAnimated( Runnable r ) {
480481
// remember current scrollbar value so that we can start scroll animation from there
481482
int oldValue = scrollbar.getValue();
482483

483-
// if invoked while animation is running, calculation of new value
484-
// should start at the previous target value
485-
if( targetValue != Integer.MIN_VALUE )
486-
scrollbar.setValue( targetValue );
484+
runWithoutBlitting( scrollbar.getParent(), () ->{
485+
// if invoked while animation is running, calculation of new value
486+
// should start at the previous target value
487+
if( targetValue != Integer.MIN_VALUE )
488+
scrollbar.setValue( targetValue );
487489

488-
r.run();
490+
r.run();
491+
} );
489492

490493
// do not use animation if started dragging thumb
491494
if( isDragging ) {
@@ -507,6 +510,26 @@ public void runAndSetValueAnimated( Runnable r ) {
507510
inRunAndSetValueAnimated = false;
508511
}
509512

513+
private void runWithoutBlitting( Container scrollPane, Runnable r ) {
514+
// prevent the viewport to immediately repaint using blitting
515+
JViewport viewport = null;
516+
int oldScrollMode = 0;
517+
if( scrollPane instanceof JScrollPane ) {
518+
viewport = ((JScrollPane) scrollPane).getViewport();
519+
if( viewport != null ) {
520+
oldScrollMode = viewport.getScrollMode();
521+
viewport.setScrollMode( JViewport.BACKINGSTORE_SCROLL_MODE );
522+
}
523+
}
524+
525+
try {
526+
r.run();
527+
} finally {
528+
if( viewport != null )
529+
viewport.setScrollMode( oldScrollMode );
530+
}
531+
}
532+
510533
private boolean inRunAndSetValueAnimated;
511534
private Animator animator;
512535
private int startValue = Integer.MIN_VALUE;

flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatScrollPaneUI.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -455,6 +455,8 @@ public static boolean isPermanentFocusOwner( JScrollPane scrollPane ) {
455455

456456
@Override
457457
protected void syncScrollPaneWithViewport() {
458+
// if the viewport has been scrolled by using JComponent.scrollRectToVisible()
459+
// (e.g. by moving selection), then it is necessary to update the scroll bar values
458460
if( isSmoothScrollingEnabled() ) {
459461
runAndSyncScrollBarValueAnimated( scrollpane.getVerticalScrollBar(), 0, () -> {
460462
runAndSyncScrollBarValueAnimated( scrollpane.getHorizontalScrollBar(), 1, () -> {

flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.java

Lines changed: 64 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -78,26 +78,26 @@ public static void main( String[] args ) {
7878
editorPaneLabel.setIcon( new ColorIcon( Color.orange.darker() ) );
7979
customLabel.setIcon( new ColorIcon( Color.pink ) );
8080

81-
listScrollPane.getVerticalScrollBar().getModel().addChangeListener( new ScrollBarChangeHandler( "list vert", Color.red.darker() ) );
82-
listScrollPane.getHorizontalScrollBar().getModel().addChangeListener( new ScrollBarChangeHandler( "list horz", Color.red ) );
81+
listScrollPane.getVerticalScrollBar().getModel().addChangeListener( new ScrollBarChangeHandler( listScrollPane, true, "list vert", Color.red.darker() ) );
82+
listScrollPane.getHorizontalScrollBar().getModel().addChangeListener( new ScrollBarChangeHandler( listScrollPane, false, "list horz", Color.red ) );
8383

84-
treeScrollPane.getVerticalScrollBar().getModel().addChangeListener( new ScrollBarChangeHandler( "tree vert", Color.blue.darker() ) );
85-
treeScrollPane.getHorizontalScrollBar().getModel().addChangeListener( new ScrollBarChangeHandler( "tree horz", Color.blue ) );
84+
treeScrollPane.getVerticalScrollBar().getModel().addChangeListener( new ScrollBarChangeHandler( treeScrollPane, true, "tree vert", Color.blue.darker() ) );
85+
treeScrollPane.getHorizontalScrollBar().getModel().addChangeListener( new ScrollBarChangeHandler( treeScrollPane, false, "tree horz", Color.blue ) );
8686

87-
tableScrollPane.getVerticalScrollBar().getModel().addChangeListener( new ScrollBarChangeHandler( "table vert", Color.green.darker() ) );
88-
tableScrollPane.getHorizontalScrollBar().getModel().addChangeListener( new ScrollBarChangeHandler( "table horz", Color.green ) );
87+
tableScrollPane.getVerticalScrollBar().getModel().addChangeListener( new ScrollBarChangeHandler( tableScrollPane, true, "table vert", Color.green.darker() ) );
88+
tableScrollPane.getHorizontalScrollBar().getModel().addChangeListener( new ScrollBarChangeHandler( tableScrollPane, false, "table horz", Color.green ) );
8989

90-
textAreaScrollPane.getVerticalScrollBar().getModel().addChangeListener( new ScrollBarChangeHandler( "textArea vert", Color.magenta.darker() ) );
91-
textAreaScrollPane.getHorizontalScrollBar().getModel().addChangeListener( new ScrollBarChangeHandler( "textArea horz", Color.magenta ) );
90+
textAreaScrollPane.getVerticalScrollBar().getModel().addChangeListener( new ScrollBarChangeHandler( textAreaScrollPane, true, "textArea vert", Color.magenta.darker() ) );
91+
textAreaScrollPane.getHorizontalScrollBar().getModel().addChangeListener( new ScrollBarChangeHandler( textAreaScrollPane, false, "textArea horz", Color.magenta ) );
9292

93-
textPaneScrollPane.getVerticalScrollBar().getModel().addChangeListener( new ScrollBarChangeHandler( "textPane vert", Color.cyan.darker() ) );
94-
textPaneScrollPane.getHorizontalScrollBar().getModel().addChangeListener( new ScrollBarChangeHandler( "textPane horz", Color.cyan ) );
93+
textPaneScrollPane.getVerticalScrollBar().getModel().addChangeListener( new ScrollBarChangeHandler( textPaneScrollPane, true, "textPane vert", Color.cyan.darker() ) );
94+
textPaneScrollPane.getHorizontalScrollBar().getModel().addChangeListener( new ScrollBarChangeHandler( textPaneScrollPane, false, "textPane horz", Color.cyan ) );
9595

96-
editorPaneScrollPane.getVerticalScrollBar().getModel().addChangeListener( new ScrollBarChangeHandler( "editorPane vert", Color.orange.darker() ) );
97-
editorPaneScrollPane.getHorizontalScrollBar().getModel().addChangeListener( new ScrollBarChangeHandler( "editorPane horz", Color.orange ) );
96+
editorPaneScrollPane.getVerticalScrollBar().getModel().addChangeListener( new ScrollBarChangeHandler( editorPaneScrollPane, true, "editorPane vert", Color.orange.darker() ) );
97+
editorPaneScrollPane.getHorizontalScrollBar().getModel().addChangeListener( new ScrollBarChangeHandler( editorPaneScrollPane, false, "editorPane horz", Color.orange ) );
9898

99-
customScrollPane.getVerticalScrollBar().getModel().addChangeListener( new ScrollBarChangeHandler( "custom vert", Color.pink ) );
100-
customScrollPane.getHorizontalScrollBar().getModel().addChangeListener( new ScrollBarChangeHandler( "custom horz", Color.pink.darker() ) );
99+
customScrollPane.getVerticalScrollBar().getModel().addChangeListener( new ScrollBarChangeHandler( customScrollPane, true, "custom vert", Color.pink ) );
100+
customScrollPane.getHorizontalScrollBar().getModel().addChangeListener( new ScrollBarChangeHandler( customScrollPane, false, "custom horz", Color.pink.darker() ) );
101101

102102
ArrayList<String> items = new ArrayList<>();
103103
for( char ch = '0'; ch < 'z'; ch++ ) {
@@ -151,6 +151,13 @@ public Object getValueAt( int rowIndex, int columnIndex ) {
151151
}
152152
} );
153153

154+
// select some rows to better see smooth scrolling issues
155+
for( int i = 5; i < items.size(); i += 10 ) {
156+
list.addSelectionInterval( i, i );
157+
tree.addSelectionInterval( i, i );
158+
table.addRowSelectionInterval( i, i );
159+
}
160+
154161
// text components
155162
String text = items.stream().collect( Collectors.joining( "\n" ) );
156163
textArea.setText( text );
@@ -207,7 +214,7 @@ private void initComponents() {
207214
list = new JList<>();
208215
treeScrollPane = new FlatSmoothScrollingTest.DebugScrollPane();
209216
tree = new JTree();
210-
tableScrollPane = new JScrollPane();
217+
tableScrollPane = new FlatSmoothScrollingTest.DebugScrollPane();
211218
table = new JTable();
212219
textAreaLabel = new JLabel();
213220
textPaneLabel = new JLabel();
@@ -219,7 +226,7 @@ private void initComponents() {
219226
textPane = new JTextPane();
220227
editorPaneScrollPane = new FlatSmoothScrollingTest.DebugScrollPane();
221228
editorPane = new JEditorPane();
222-
customScrollPane = new JScrollPane();
229+
customScrollPane = new FlatSmoothScrollingTest.DebugScrollPane();
223230
button1 = new JButton();
224231
scrollPane1 = new JScrollPane();
225232
lineChartPanel = new FlatSmoothScrollingTest.LineChartPanel();
@@ -382,7 +389,7 @@ private void initComponents() {
382389
private JList<String> list;
383390
private FlatSmoothScrollingTest.DebugScrollPane treeScrollPane;
384391
private JTree tree;
385-
private JScrollPane tableScrollPane;
392+
private FlatSmoothScrollingTest.DebugScrollPane tableScrollPane;
386393
private JTable table;
387394
private JLabel textAreaLabel;
388395
private JLabel textPaneLabel;
@@ -394,7 +401,7 @@ private void initComponents() {
394401
private JTextPane textPane;
395402
private FlatSmoothScrollingTest.DebugScrollPane editorPaneScrollPane;
396403
private JEditorPane editorPane;
397-
private JScrollPane customScrollPane;
404+
private FlatSmoothScrollingTest.DebugScrollPane customScrollPane;
398405
private JButton button1;
399406
private JScrollPane scrollPane1;
400407
private FlatSmoothScrollingTest.LineChartPanel lineChartPanel;
@@ -413,39 +420,52 @@ private class ScrollBarChangeHandler
413420
private int count;
414421
private long lastTime;
415422

416-
ScrollBarChangeHandler( String name, Color chartColor ) {
423+
ScrollBarChangeHandler( DebugScrollPane scrollPane, boolean vertical, String name, Color chartColor ) {
417424
this.name = name;
418425
this.chartColor = chartColor;
426+
427+
// add change listener to viewport that is invoked from JViewport.setViewPosition()
428+
scrollPane.getViewport().addChangeListener( e -> {
429+
// add dot to chart if blit scroll mode is disabled
430+
if( vertical == scrollPane.lastScrollingWasVertical &&
431+
scrollPane.getViewport().getScrollMode() != JViewport.BLIT_SCROLL_MODE )
432+
{
433+
JScrollBar sb = vertical ? scrollPane.getVerticalScrollBar() : scrollPane.getHorizontalScrollBar();
434+
lineChartPanel.addValue( getChartValue( sb.getModel() ), true, chartColor );
435+
}
436+
} );
419437
}
420438

421439
@Override
422440
public void stateChanged( ChangeEvent e ) {
423441
DefaultBoundedRangeModel m = (DefaultBoundedRangeModel) e.getSource();
424-
int value = m.getValue();
425-
boolean valueIsAdjusting = m.getValueIsAdjusting();
426442

427-
if( chartColor != null ) {
428-
double chartValue = (double) (value - m.getMinimum()) / (double) (m.getMaximum() - m.getExtent());
429-
lineChartPanel.addValue( chartValue, chartColor );
430-
}
443+
lineChartPanel.addValue( getChartValue( m ), chartColor );
431444

432445
long t = System.nanoTime() / 1000000;
433446

434447
System.out.printf( "%s (%d): %4d %3d ms %b%n",
435448
name, ++count,
436-
value,
449+
m.getValue(),
437450
t - lastTime,
438-
valueIsAdjusting );
451+
m.getValueIsAdjusting() );
439452

440453
lastTime = t;
441454
}
455+
456+
private double getChartValue( BoundedRangeModel m ) {
457+
int value = m.getValue();
458+
return (double) (value - m.getMinimum()) / (double) (m.getMaximum() - m.getExtent());
459+
}
442460
}
443461

444462
//---- class DebugViewport ------------------------------------------------
445463

446464
private static class DebugScrollPane
447465
extends JScrollPane
448466
{
467+
boolean lastScrollingWasVertical;
468+
449469
@Override
450470
protected JViewport createViewport() {
451471
return new JViewport() {
@@ -455,6 +475,23 @@ public Point getViewPosition() {
455475
// System.out.println( " viewPosition " + viewPosition.x + "," + viewPosition.y );
456476
return viewPosition;
457477
}
478+
479+
@Override
480+
public void setViewPosition( Point p ) {
481+
// remember whether scrolling vertically or horizontally
482+
Component view = getView();
483+
if( view != null ) {
484+
int oldY = (view instanceof JComponent)
485+
? ((JComponent) view).getY()
486+
: view.getBounds().y;
487+
488+
int newY = -p.y;
489+
lastScrollingWasVertical = (oldY != newY);
490+
} else
491+
lastScrollingWasVertical = true;
492+
493+
super.setViewPosition( p );
494+
}
458495
};
459496
}
460497
}

flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.jfd

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
JFDML JFormDesigner: "7.0.2.0.298" Java: "14" encoding: "UTF-8"
1+
JFDML JFormDesigner: "8.1.0.0.283" Java: "19.0.2" encoding: "UTF-8"
22

33
new FormModel {
44
contentType: "form/swing"
@@ -75,7 +75,7 @@ new FormModel {
7575
}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
7676
"value": "cell 1 2"
7777
} )
78-
add( new FormContainer( "javax.swing.JScrollPane", new FormLayoutManager( class javax.swing.JScrollPane ) ) {
78+
add( new FormContainer( "com.formdev.flatlaf.testing.FlatSmoothScrollingTest$DebugScrollPane", new FormLayoutManager( class javax.swing.JScrollPane ) ) {
7979
name: "tableScrollPane"
8080
add( new FormComponent( "javax.swing.JTable" ) {
8181
name: "table"
@@ -134,7 +134,7 @@ new FormModel {
134134
}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
135135
"value": "cell 2 4"
136136
} )
137-
add( new FormContainer( "javax.swing.JScrollPane", new FormLayoutManager( class javax.swing.JScrollPane ) ) {
137+
add( new FormContainer( "com.formdev.flatlaf.testing.FlatSmoothScrollingTest$DebugScrollPane", new FormLayoutManager( class javax.swing.JScrollPane ) ) {
138138
name: "customScrollPane"
139139
add( new FormComponent( "javax.swing.JButton" ) {
140140
name: "button1"

0 commit comments

Comments
 (0)