diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatScrollBarUI.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatScrollBarUI.java index f9aef8e6..f999e399 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatScrollBarUI.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatScrollBarUI.java @@ -34,6 +34,7 @@ import javax.swing.JButton; import javax.swing.JComponent; import javax.swing.JScrollBar; import javax.swing.JScrollPane; +import javax.swing.JViewport; import javax.swing.SwingUtilities; import javax.swing.UIManager; import javax.swing.plaf.ComponentUI; @@ -480,12 +481,14 @@ public class FlatScrollBarUI // remember current scrollbar value so that we can start scroll animation from there int oldValue = scrollbar.getValue(); - // if invoked while animation is running, calculation of new value - // should start at the previous target value - if( targetValue != Integer.MIN_VALUE ) - scrollbar.setValue( targetValue ); + runWithoutBlitting( scrollbar.getParent(), () ->{ + // if invoked while animation is running, calculation of new value + // should start at the previous target value + if( targetValue != Integer.MIN_VALUE ) + scrollbar.setValue( targetValue ); - r.run(); + r.run(); + } ); // do not use animation if started dragging thumb if( isDragging ) { @@ -507,6 +510,26 @@ public class FlatScrollBarUI inRunAndSetValueAnimated = false; } + private void runWithoutBlitting( Container scrollPane, Runnable r ) { + // prevent the viewport to immediately repaint using blitting + JViewport viewport = null; + int oldScrollMode = 0; + if( scrollPane instanceof JScrollPane ) { + viewport = ((JScrollPane) scrollPane).getViewport(); + if( viewport != null ) { + oldScrollMode = viewport.getScrollMode(); + viewport.setScrollMode( JViewport.BACKINGSTORE_SCROLL_MODE ); + } + } + + try { + r.run(); + } finally { + if( viewport != null ) + viewport.setScrollMode( oldScrollMode ); + } + } + private boolean inRunAndSetValueAnimated; private Animator animator; private int startValue = Integer.MIN_VALUE; diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatScrollPaneUI.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatScrollPaneUI.java index c1378162..c59e1ed2 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatScrollPaneUI.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatScrollPaneUI.java @@ -455,6 +455,8 @@ public class FlatScrollPaneUI @Override protected void syncScrollPaneWithViewport() { + // if the viewport has been scrolled by using JComponent.scrollRectToVisible() + // (e.g. by moving selection), then it is necessary to update the scroll bar values if( isSmoothScrollingEnabled() ) { runAndSyncScrollBarValueAnimated( scrollpane.getVerticalScrollBar(), 0, () -> { runAndSyncScrollBarValueAnimated( scrollpane.getHorizontalScrollBar(), 1, () -> { diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.java b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.java index 95e27d67..b606a21d 100644 --- a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.java +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.java @@ -78,26 +78,26 @@ public class FlatSmoothScrollingTest editorPaneLabel.setIcon( new ColorIcon( Color.orange.darker() ) ); customLabel.setIcon( new ColorIcon( Color.pink ) ); - listScrollPane.getVerticalScrollBar().getModel().addChangeListener( new ScrollBarChangeHandler( "list vert", Color.red.darker() ) ); - listScrollPane.getHorizontalScrollBar().getModel().addChangeListener( new ScrollBarChangeHandler( "list horz", Color.red ) ); + listScrollPane.getVerticalScrollBar().getModel().addChangeListener( new ScrollBarChangeHandler( listScrollPane, true, "list vert", Color.red.darker() ) ); + listScrollPane.getHorizontalScrollBar().getModel().addChangeListener( new ScrollBarChangeHandler( listScrollPane, false, "list horz", Color.red ) ); - treeScrollPane.getVerticalScrollBar().getModel().addChangeListener( new ScrollBarChangeHandler( "tree vert", Color.blue.darker() ) ); - treeScrollPane.getHorizontalScrollBar().getModel().addChangeListener( new ScrollBarChangeHandler( "tree horz", Color.blue ) ); + treeScrollPane.getVerticalScrollBar().getModel().addChangeListener( new ScrollBarChangeHandler( treeScrollPane, true, "tree vert", Color.blue.darker() ) ); + treeScrollPane.getHorizontalScrollBar().getModel().addChangeListener( new ScrollBarChangeHandler( treeScrollPane, false, "tree horz", Color.blue ) ); - tableScrollPane.getVerticalScrollBar().getModel().addChangeListener( new ScrollBarChangeHandler( "table vert", Color.green.darker() ) ); - tableScrollPane.getHorizontalScrollBar().getModel().addChangeListener( new ScrollBarChangeHandler( "table horz", Color.green ) ); + tableScrollPane.getVerticalScrollBar().getModel().addChangeListener( new ScrollBarChangeHandler( tableScrollPane, true, "table vert", Color.green.darker() ) ); + tableScrollPane.getHorizontalScrollBar().getModel().addChangeListener( new ScrollBarChangeHandler( tableScrollPane, false, "table horz", Color.green ) ); - textAreaScrollPane.getVerticalScrollBar().getModel().addChangeListener( new ScrollBarChangeHandler( "textArea vert", Color.magenta.darker() ) ); - textAreaScrollPane.getHorizontalScrollBar().getModel().addChangeListener( new ScrollBarChangeHandler( "textArea horz", Color.magenta ) ); + textAreaScrollPane.getVerticalScrollBar().getModel().addChangeListener( new ScrollBarChangeHandler( textAreaScrollPane, true, "textArea vert", Color.magenta.darker() ) ); + textAreaScrollPane.getHorizontalScrollBar().getModel().addChangeListener( new ScrollBarChangeHandler( textAreaScrollPane, false, "textArea horz", Color.magenta ) ); - textPaneScrollPane.getVerticalScrollBar().getModel().addChangeListener( new ScrollBarChangeHandler( "textPane vert", Color.cyan.darker() ) ); - textPaneScrollPane.getHorizontalScrollBar().getModel().addChangeListener( new ScrollBarChangeHandler( "textPane horz", Color.cyan ) ); + textPaneScrollPane.getVerticalScrollBar().getModel().addChangeListener( new ScrollBarChangeHandler( textPaneScrollPane, true, "textPane vert", Color.cyan.darker() ) ); + textPaneScrollPane.getHorizontalScrollBar().getModel().addChangeListener( new ScrollBarChangeHandler( textPaneScrollPane, false, "textPane horz", Color.cyan ) ); - editorPaneScrollPane.getVerticalScrollBar().getModel().addChangeListener( new ScrollBarChangeHandler( "editorPane vert", Color.orange.darker() ) ); - editorPaneScrollPane.getHorizontalScrollBar().getModel().addChangeListener( new ScrollBarChangeHandler( "editorPane horz", Color.orange ) ); + editorPaneScrollPane.getVerticalScrollBar().getModel().addChangeListener( new ScrollBarChangeHandler( editorPaneScrollPane, true, "editorPane vert", Color.orange.darker() ) ); + editorPaneScrollPane.getHorizontalScrollBar().getModel().addChangeListener( new ScrollBarChangeHandler( editorPaneScrollPane, false, "editorPane horz", Color.orange ) ); - customScrollPane.getVerticalScrollBar().getModel().addChangeListener( new ScrollBarChangeHandler( "custom vert", Color.pink ) ); - customScrollPane.getHorizontalScrollBar().getModel().addChangeListener( new ScrollBarChangeHandler( "custom horz", Color.pink.darker() ) ); + customScrollPane.getVerticalScrollBar().getModel().addChangeListener( new ScrollBarChangeHandler( customScrollPane, true, "custom vert", Color.pink ) ); + customScrollPane.getHorizontalScrollBar().getModel().addChangeListener( new ScrollBarChangeHandler( customScrollPane, false, "custom horz", Color.pink.darker() ) ); ArrayList items = new ArrayList<>(); for( char ch = '0'; ch < 'z'; ch++ ) { @@ -151,6 +151,13 @@ public class FlatSmoothScrollingTest } } ); + // select some rows to better see smooth scrolling issues + for( int i = 5; i < items.size(); i += 10 ) { + list.addSelectionInterval( i, i ); + tree.addSelectionInterval( i, i ); + table.addRowSelectionInterval( i, i ); + } + // text components String text = items.stream().collect( Collectors.joining( "\n" ) ); textArea.setText( text ); @@ -207,7 +214,7 @@ public class FlatSmoothScrollingTest list = new JList<>(); treeScrollPane = new FlatSmoothScrollingTest.DebugScrollPane(); tree = new JTree(); - tableScrollPane = new JScrollPane(); + tableScrollPane = new FlatSmoothScrollingTest.DebugScrollPane(); table = new JTable(); textAreaLabel = new JLabel(); textPaneLabel = new JLabel(); @@ -219,7 +226,7 @@ public class FlatSmoothScrollingTest textPane = new JTextPane(); editorPaneScrollPane = new FlatSmoothScrollingTest.DebugScrollPane(); editorPane = new JEditorPane(); - customScrollPane = new JScrollPane(); + customScrollPane = new FlatSmoothScrollingTest.DebugScrollPane(); button1 = new JButton(); scrollPane1 = new JScrollPane(); lineChartPanel = new FlatSmoothScrollingTest.LineChartPanel(); @@ -382,7 +389,7 @@ public class FlatSmoothScrollingTest private JList list; private FlatSmoothScrollingTest.DebugScrollPane treeScrollPane; private JTree tree; - private JScrollPane tableScrollPane; + private FlatSmoothScrollingTest.DebugScrollPane tableScrollPane; private JTable table; private JLabel textAreaLabel; private JLabel textPaneLabel; @@ -394,7 +401,7 @@ public class FlatSmoothScrollingTest private JTextPane textPane; private FlatSmoothScrollingTest.DebugScrollPane editorPaneScrollPane; private JEditorPane editorPane; - private JScrollPane customScrollPane; + private FlatSmoothScrollingTest.DebugScrollPane customScrollPane; private JButton button1; private JScrollPane scrollPane1; private FlatSmoothScrollingTest.LineChartPanel lineChartPanel; @@ -413,32 +420,43 @@ public class FlatSmoothScrollingTest private int count; private long lastTime; - ScrollBarChangeHandler( String name, Color chartColor ) { + ScrollBarChangeHandler( DebugScrollPane scrollPane, boolean vertical, String name, Color chartColor ) { this.name = name; this.chartColor = chartColor; + + // add change listener to viewport that is invoked from JViewport.setViewPosition() + scrollPane.getViewport().addChangeListener( e -> { + // add dot to chart if blit scroll mode is disabled + if( vertical == scrollPane.lastScrollingWasVertical && + scrollPane.getViewport().getScrollMode() != JViewport.BLIT_SCROLL_MODE ) + { + JScrollBar sb = vertical ? scrollPane.getVerticalScrollBar() : scrollPane.getHorizontalScrollBar(); + lineChartPanel.addValue( getChartValue( sb.getModel() ), true, chartColor ); + } + } ); } @Override public void stateChanged( ChangeEvent e ) { DefaultBoundedRangeModel m = (DefaultBoundedRangeModel) e.getSource(); - int value = m.getValue(); - boolean valueIsAdjusting = m.getValueIsAdjusting(); - if( chartColor != null ) { - double chartValue = (double) (value - m.getMinimum()) / (double) (m.getMaximum() - m.getExtent()); - lineChartPanel.addValue( chartValue, chartColor ); - } + lineChartPanel.addValue( getChartValue( m ), chartColor ); long t = System.nanoTime() / 1000000; System.out.printf( "%s (%d): %4d %3d ms %b%n", name, ++count, - value, + m.getValue(), t - lastTime, - valueIsAdjusting ); + m.getValueIsAdjusting() ); lastTime = t; } + + private double getChartValue( BoundedRangeModel m ) { + int value = m.getValue(); + return (double) (value - m.getMinimum()) / (double) (m.getMaximum() - m.getExtent()); + } } //---- class DebugViewport ------------------------------------------------ @@ -446,6 +464,8 @@ public class FlatSmoothScrollingTest private static class DebugScrollPane extends JScrollPane { + boolean lastScrollingWasVertical; + @Override protected JViewport createViewport() { return new JViewport() { @@ -455,6 +475,23 @@ public class FlatSmoothScrollingTest // System.out.println( " viewPosition " + viewPosition.x + "," + viewPosition.y ); return viewPosition; } + + @Override + public void setViewPosition( Point p ) { + // remember whether scrolling vertically or horizontally + Component view = getView(); + if( view != null ) { + int oldY = (view instanceof JComponent) + ? ((JComponent) view).getY() + : view.getBounds().y; + + int newY = -p.y; + lastScrollingWasVertical = (oldY != newY); + } else + lastScrollingWasVertical = true; + + super.setViewPosition( p ); + } }; } } diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.jfd b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.jfd index 1a2fbc3f..fa3a555e 100644 --- a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.jfd +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.jfd @@ -1,4 +1,4 @@ -JFDML JFormDesigner: "7.0.2.0.298" Java: "14" encoding: "UTF-8" +JFDML JFormDesigner: "8.1.0.0.283" Java: "19.0.2" encoding: "UTF-8" new FormModel { contentType: "form/swing" @@ -75,7 +75,7 @@ new FormModel { }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 1 2" } ) - add( new FormContainer( "javax.swing.JScrollPane", new FormLayoutManager( class javax.swing.JScrollPane ) ) { + add( new FormContainer( "com.formdev.flatlaf.testing.FlatSmoothScrollingTest$DebugScrollPane", new FormLayoutManager( class javax.swing.JScrollPane ) ) { name: "tableScrollPane" add( new FormComponent( "javax.swing.JTable" ) { name: "table" @@ -134,7 +134,7 @@ new FormModel { }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 2 4" } ) - add( new FormContainer( "javax.swing.JScrollPane", new FormLayoutManager( class javax.swing.JScrollPane ) ) { + add( new FormContainer( "com.formdev.flatlaf.testing.FlatSmoothScrollingTest$DebugScrollPane", new FormLayoutManager( class javax.swing.JScrollPane ) ) { name: "customScrollPane" add( new FormComponent( "javax.swing.JButton" ) { name: "button1"