diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/FlatLaf.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/FlatLaf.java index 5d02f8090..410c243a5 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/FlatLaf.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/FlatLaf.java @@ -112,6 +112,7 @@ public abstract class FlatLaf private PopupFactory oldPopupFactory; private MnemonicHandler mnemonicHandler; private boolean subMenuUsabilityHelperInstalled; + private boolean smoothScrollingHelperInstalled; private Consumer postInitialization; private List> uiDefaultsGetters; @@ -271,6 +272,9 @@ public void initialize() { // install submenu usability helper subMenuUsabilityHelperInstalled = SubMenuUsabilityHelper.install(); + + // install smooth scrolling helper + smoothScrollingHelperInstalled = SmoothScrollingHelper.install(); // listen to desktop property changes to update UI if system font or scaling changes if( SystemInfo.isWindows ) { @@ -364,6 +368,12 @@ public void uninitialize() { subMenuUsabilityHelperInstalled = false; } + // uninstall smooth scrolling helper + if( smoothScrollingHelperInstalled ) { + SmoothScrollingHelper.uninstall(); + smoothScrollingHelperInstalled = false; + } + // restore default link color new HTMLEditorKit().getStyleSheet().addRule( "a, address { color: blue; }" ); postInitialization = null; diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/FlatSystemProperties.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/FlatSystemProperties.java index 5d4a76938..a9e4b8a6c 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/FlatSystemProperties.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/FlatSystemProperties.java @@ -132,6 +132,14 @@ public interface FlatSystemProperties */ String ANIMATION = "flatlaf.animation"; + /** + * Specifies whether smooth scrolling is enabled. + *

+ * Allowed Values {@code false} and {@code true}
+ * Default {@code true} + */ + String SMOOTH_SCROLLING = "flatlaf.smoothScrolling"; + /** * Specifies whether vertical text position is corrected when UI is scaled on HiDPI screens. *

diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/SmoothScrollingHelper.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/SmoothScrollingHelper.java new file mode 100644 index 000000000..09bd3f518 --- /dev/null +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/SmoothScrollingHelper.java @@ -0,0 +1,209 @@ +/* + * Christopher Deckers (chrriis@nextencia.net) + * http://www.nextencia.net + * + * See the file "readme.txt" for information on usage and redistribution of + * this file, and for a DISCLAIMER OF ALL WARRANTIES. + */ +package com.formdev.flatlaf; + +import java.awt.AWTEvent; +import java.awt.Component; +import java.awt.Container; +import java.awt.Rectangle; +import java.awt.Toolkit; +import java.awt.event.AWTEventListener; +import java.awt.event.ComponentEvent; +import java.awt.event.KeyEvent; +import java.awt.event.MouseEvent; +import java.util.Collections; +import java.util.IdentityHashMap; +import java.util.Set; +import javax.swing.JComponent; +import javax.swing.JScrollBar; +import javax.swing.JScrollPane; +import javax.swing.JViewport; +import javax.swing.RepaintManager; +import javax.swing.SwingUtilities; +import com.formdev.flatlaf.util.Animator; + +/** + * @author Christopher Deckers + */ +public class SmoothScrollingHelper implements AWTEventListener +{ + + private static SmoothScrollingHelper instance; + + static synchronized boolean install() { + if( instance != null ) + return false; + instance = new SmoothScrollingHelper(); + long eventMask = AWTEvent.MOUSE_EVENT_MASK | AWTEvent.MOUSE_MOTION_EVENT_MASK | AWTEvent.MOUSE_WHEEL_EVENT_MASK | AWTEvent.KEY_EVENT_MASK; + Toolkit.getDefaultToolkit().addAWTEventListener(instance, eventMask); + return true; + } + + static synchronized void uninstall() { + if( instance == null ) + return; + Toolkit.getDefaultToolkit().removeAWTEventListener(instance); + instance = null; + } + + @Override + public void eventDispatched( AWTEvent event ) { + if( event instanceof ComponentEvent && ((ComponentEvent)event).getComponent() instanceof JScrollBar ) { + // Do not disconnect blit scroll mode when e.g. dragging the scroll bar. + return; + } + if( Animator.useAnimation() && FlatSystemProperties.getBoolean( FlatSystemProperties.SMOOTH_SCROLLING, true ) ) { + boolean isHoldingScrollModeBlocked = false; + switch(event.getID()) { + case MouseEvent.MOUSE_MOVED: + case MouseEvent.MOUSE_ENTERED: + case MouseEvent.MOUSE_EXITED: + if(isBlitScrollModeBlocked) { + int modifiersEx = ((MouseEvent)event).getModifiersEx(); + if((modifiersEx & (MouseEvent.BUTTON1_DOWN_MASK | MouseEvent.BUTTON2_DOWN_MASK | MouseEvent.BUTTON3_DOWN_MASK)) == 0) { + // If the scroll mode was blocked for dragging, let's release if we receive a non-drag-related event (i.e. drag is done). + for( JViewport viewport: viewportSet ) { + setBlitScrollModeBlocked( viewport, false ); + setInSmoothScrolling( viewport, false ); + } + isBlitScrollModeBlocked = false; + } + } + break; + case MouseEvent.MOUSE_PRESSED: + case MouseEvent.MOUSE_DRAGGED: + isHoldingScrollModeBlocked = true; + // Fall through + case MouseEvent.MOUSE_RELEASED: + case MouseEvent.MOUSE_CLICKED: + case MouseEvent.MOUSE_WHEEL: + case KeyEvent.KEY_PRESSED: + case KeyEvent.KEY_RELEASED: + case KeyEvent.KEY_TYPED: + boolean isBlitScrollModeBlocked_ = isBlitScrollModeBlocked; + if(!isBlitScrollModeBlocked_) { + Component c = ((ComponentEvent)event).getComponent(); + for( JViewport viewport: viewportSet ) { + Container scrollPane = viewport.getParent(); + if(scrollPane == c || scrollPane.isAncestorOf( c )) { + setInSmoothScrolling( viewport, true ); + } + setBlitScrollModeBlocked( viewport, true ); + } + isBlitScrollModeBlocked = true; + } + if( !isHoldingScrollModeBlocked && ( !isBlitScrollModeBlocked_ || event.getID() == MouseEvent.MOUSE_RELEASED )) { + SwingUtilities.invokeLater( () -> { + for( JViewport viewport: viewportSet ) { + setBlitScrollModeBlocked( viewport, false ); + setInSmoothScrolling( viewport, false ); + } + isBlitScrollModeBlocked = false; + } ); + } + break; + } + } + } + + private boolean isBlitScrollModeBlocked; + + public static synchronized void setBlitScrollModeBlocked( JViewport viewport, boolean isBlocked ) { + if( instance == null ) + return; + String clientPropertyName = "_flatlaf.originalScrollMode"; + if( isBlocked ) { + int scrollMode = viewport.getScrollMode(); + if( scrollMode == JViewport.BLIT_SCROLL_MODE ) { + viewport.putClientProperty( clientPropertyName , scrollMode ); + viewport.setScrollMode( JViewport.SIMPLE_SCROLL_MODE ); + } + } else { + Integer scrollMode = (Integer)viewport.getClientProperty( clientPropertyName ); + if( scrollMode != null ) { + viewport.setScrollMode( scrollMode ); + viewport.putClientProperty( clientPropertyName , null ); + } + } + } + + public static void setScrollBarValueWithOptionalRepaint(JViewport viewport, JScrollBar scrollbar, int value) { + Container viewportParent = null; + Rectangle dirtyRegion = null; + viewportParent = viewport == null? null: viewport.getParent(); + dirtyRegion = viewportParent instanceof JComponent? RepaintManager.currentManager( viewport ).getDirtyRegion((JComponent)viewportParent ): null; + int scrollMode = viewport.getScrollMode(); + if(scrollMode == JViewport.BLIT_SCROLL_MODE) { + viewport.setScrollMode( JViewport.SIMPLE_SCROLL_MODE ); + } + scrollbar.setValue( value ); + if(scrollMode == JViewport.BLIT_SCROLL_MODE) { + viewport.setScrollMode( JViewport.BLIT_SCROLL_MODE ); + } + if(dirtyRegion != null && dirtyRegion.width == 0 && dirtyRegion.height == 0) { + // There was no dirty region. Let's restore that state for blit scroll mode to work. + RepaintManager.currentManager( viewport ).markCompletelyClean( (JComponent)viewportParent ); + } + } + + public static synchronized boolean isInBlockedBlitScrollMode( JViewport viewport ) { + if( instance == null ) + return false; + String clientPropertyName = "_flatlaf.originalScrollMode"; + return viewport.getClientProperty( clientPropertyName ) != null && viewport.getScrollMode() == JViewport.SIMPLE_SCROLL_MODE; + } + + public static synchronized void allowBlitScrollModeTemporarily( JViewport viewport, boolean isAllowed ) { + if( instance == null ) + return; + // When mouse is dragged, blit scroll mode is deactivated so that drag timers that provoke more scrolling do not repaint at wrong position. + // The FlatLaf animator re-activates the blit scroll mode just for its timed operation. + String clientPropertyName = "_flatlaf.originalScrollMode"; + if(viewport.getClientProperty( clientPropertyName ) != null) { + if(isAllowed) { + viewport.setScrollMode( JViewport.BLIT_SCROLL_MODE ); + } else { + viewport.setScrollMode( JViewport.SIMPLE_SCROLL_MODE ); + } + } + } + + public static synchronized boolean isInSmoothScrolling( JViewport viewport ) { + if( instance == null ) + return false; + String clientPropertyName = "_flatlaf.inSmoothScrolling"; + return viewport != null && viewport.getClientProperty( clientPropertyName ) != null; + } + + private static synchronized void setInSmoothScrolling( JViewport viewport, boolean isInSmoothScrolling ) { + if( instance == null ) + return; + String clientPropertyName = "_flatlaf.inSmoothScrolling"; + viewport.putClientProperty( clientPropertyName , isInSmoothScrolling? Boolean.TRUE: null); + } + + private static Set viewportSet = Collections.newSetFromMap(new IdentityHashMap()); + + public static synchronized void registerViewport( JViewport viewport ) { + if( instance == null ) + return; + if(viewport.getParent() instanceof JScrollPane) { + viewportSet.add( viewport ); + } + } + + public static synchronized void unregisterViewport( JViewport viewport ) { + if( instance == null ) + return; + if( instance.isBlitScrollModeBlocked ) { + instance.setBlitScrollModeBlocked( viewport, false ); + } + viewportSet.remove( viewport ); + } + +} 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 cb1b7c6c1..6c3f8bf52 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 @@ -22,6 +22,7 @@ import java.awt.Graphics; import java.awt.Insets; import java.awt.Rectangle; +import java.awt.event.ActionEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.beans.PropertyChangeListener; @@ -33,16 +34,21 @@ 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; import javax.swing.plaf.basic.BasicScrollBarUI; import com.formdev.flatlaf.FlatClientProperties; import com.formdev.flatlaf.FlatLaf; +import com.formdev.flatlaf.FlatSystemProperties; +import com.formdev.flatlaf.SmoothScrollingHelper; import com.formdev.flatlaf.ui.FlatStylingSupport.Styleable; import com.formdev.flatlaf.ui.FlatStylingSupport.StyleableField; import com.formdev.flatlaf.ui.FlatStylingSupport.StyleableLookupProvider; import com.formdev.flatlaf.ui.FlatStylingSupport.StyleableUI; +import com.formdev.flatlaf.util.Animator; +import com.formdev.flatlaf.util.CubicBezierEasing; import com.formdev.flatlaf.util.LoggingFacade; import com.formdev.flatlaf.util.SystemInfo; import com.formdev.flatlaf.util.UIScale; @@ -203,6 +209,16 @@ protected void uninstallDefaults() { oldStyleValues = null; } + @Override + protected TrackListener createTrackListener() { + return new FlatTrackListener(); + } + + @Override + protected ScrollListener createScrollListener() { + return new FlatScrollListener(); + } + @Override protected PropertyChangeListener createPropertyChangeListener() { PropertyChangeListener superListener = super.createPropertyChangeListener(); @@ -431,6 +447,226 @@ public boolean getSupportsAbsolutePositioning() { return allowsAbsolutePositioning; } + @Override + protected void scrollByBlock( int direction ) { + runAndSetValueAnimated( () -> { + super.scrollByBlock( direction ); + } ); + } + + @Override + protected void scrollByUnit( int direction ) { + runAndSetValueAnimated( () -> { + super.scrollByUnit( direction ); + } ); + } + + /** + * Runs the given runnable, which should modify the scroll bar value, + * and then animate scroll bar value from old value to new value. + */ + public void runAndSetValueAnimated( Runnable r ) { + if( inRunAndSetValueAnimated || !isSmoothScrollingEnabled() ) { + JViewport viewport = null; + Container parent = scrollbar.getParent(); + if ( parent instanceof JScrollPane ) { + JScrollPane scrollpane = (JScrollPane)parent; + viewport = scrollpane.getViewport(); + if( SmoothScrollingHelper.isInBlockedBlitScrollMode( viewport ) ) { + viewport = null; + } + } + try { + if( viewport != null ) + SmoothScrollingHelper.setBlitScrollModeBlocked( viewport, true ); + r.run(); + } finally { + if( viewport != null ) + SmoothScrollingHelper.setBlitScrollModeBlocked( viewport, false ); + } + return; + } + + inRunAndSetValueAnimated = true; + + if( animator != null ) + animator.cancel(); + + if( useValueIsAdjusting ) + scrollbar.setValueIsAdjusting( true ); + + // 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 ); + + r.run(); + + // do not use animation if started dragging thumb + if( isDragging ) { + // do not clear valueIsAdjusting here + inRunAndSetValueAnimated = false; + return; + } + + int newValue = scrollbar.getValue(); + if( newValue != oldValue ) { + // start scroll animation if value has changed + setValueAnimated( oldValue, newValue ); + } else { + // clear valueIsAdjusting if value has not changed + if( useValueIsAdjusting ) + scrollbar.setValueIsAdjusting( false ); + } + + inRunAndSetValueAnimated = false; + } + + private boolean inRunAndSetValueAnimated; + private Animator animator; + private int startValue = Integer.MIN_VALUE; + private int targetValue = Integer.MIN_VALUE; + private boolean useValueIsAdjusting = true; + + public void setValueAnimated( int initialValue, int value ) { + // do some check if animation already running + if( animator != null && animator.isRunning() && targetValue != Integer.MIN_VALUE ) { + // ignore requests if animation still running and scroll direction is the same + // and new value is within currently running animation + // (this may occur when repeat-scrolling via keyboard) + if( value == targetValue || + (value > startValue && value < targetValue) || // scroll down/right + (value < startValue && value > targetValue) ) // scroll up/left + return; + } + + if( useValueIsAdjusting ) + scrollbar.setValueIsAdjusting( true ); + + // set scrollbar value to initial value + scrollbar.setValue( initialValue ); + + startValue = initialValue; + targetValue = value; + + // create animator + if( animator == null ) { + int duration = FlatUIUtils.getUIInt( "ScrollPane.smoothScrolling.duration", 200 ); + int resolution = FlatUIUtils.getUIInt( "ScrollPane.smoothScrolling.resolution", 10 ); + Object interpolator = UIManager.get( "ScrollPane.smoothScrolling.interpolator" ); + + animator = new Animator( duration, fraction -> { + if( scrollbar == null || !scrollbar.isShowing() ) { + animator.stop(); + return; + } + + // re-enable valueIsAdjusting if disabled while animation is running + // (e.g. in mouse released listener) + if( useValueIsAdjusting && !scrollbar.getValueIsAdjusting() ) + scrollbar.setValueIsAdjusting( true ); + JViewport viewport = null; + Container parent = scrollbar.getParent(); + if ( parent instanceof JScrollPane ) { + JScrollPane scrollpane = (JScrollPane)parent; + viewport = scrollpane.getViewport(); + } + boolean isInBlockedBlitScrollMode = false; + try { + if( viewport != null ) { + isInBlockedBlitScrollMode = SmoothScrollingHelper.isInBlockedBlitScrollMode( viewport ); + if( isInBlockedBlitScrollMode ) { + SmoothScrollingHelper.allowBlitScrollModeTemporarily( viewport, true ); + } + } + scrollbar.setValue( startValue + Math.round( (targetValue - startValue) * fraction ) ); + } finally { + if( isInBlockedBlitScrollMode && viewport != null ) { + SmoothScrollingHelper.allowBlitScrollModeTemporarily( viewport, false ); + } + } + }, () -> { + startValue = targetValue = Integer.MIN_VALUE; + + if( useValueIsAdjusting && scrollbar != null ) + scrollbar.setValueIsAdjusting( false ); + }); + + animator.setResolution( resolution ); + animator.setInterpolator( (interpolator instanceof Animator.Interpolator) + ? (Animator.Interpolator) interpolator + : new CubicBezierEasing( 0.5f, 0.5f, 0.5f, 1 ) ); + } + + // restart animator + animator.cancel(); + animator.start(); + } + + int getTargetValue() { + return targetValue; + } + + protected boolean isSmoothScrollingEnabled() { + if( !Animator.useAnimation() || !FlatSystemProperties.getBoolean( FlatSystemProperties.SMOOTH_SCROLLING, true ) ) + return false; + + // if scroll bar is child of scroll pane, check only client property of scroll pane + Container parent = scrollbar.getParent(); + JComponent c = (parent instanceof JScrollPane) ? (JScrollPane) parent : scrollbar; + Object smoothScrolling = c.getClientProperty( FlatClientProperties.SCROLL_PANE_SMOOTH_SCROLLING ); + if( smoothScrolling instanceof Boolean ) + return (Boolean) smoothScrolling; + + // Note: Getting UI value "ScrollPane.smoothScrolling" here to allow + // applications to turn smooth scrolling on or off at any time + // (e.g. in application options dialog). + return UIManager.getBoolean( "ScrollPane.smoothScrolling" ); + } + + //---- class FlatTrackListener -------------------------------------------- + + protected class FlatTrackListener + extends TrackListener + { + @Override + public void mousePressed( MouseEvent e ) { + // Do not use valueIsAdjusting here (in runAndSetValueAnimated()) + // for smooth scrolling because super.mousePressed() enables this itself + // and super.mouseRelease() disables it later. + // If we would disable valueIsAdjusting here (in runAndSetValueAnimated()) + // and move the thumb with the mouse, then the thumb location is not updated + // if later scrolled with a key (e.g. HOME key). + useValueIsAdjusting = false; + + runAndSetValueAnimated( () -> { + super.mousePressed( e ); + } ); + } + + @Override + public void mouseReleased( MouseEvent e ) { + super.mouseReleased( e ); + useValueIsAdjusting = true; + } + } + + //---- class FlatScrollListener ------------------------------------------- + + protected class FlatScrollListener + extends ScrollListener + { + @Override + public void actionPerformed( ActionEvent e ) { + runAndSetValueAnimated( () -> { + super.actionPerformed( e ); + } ); + } + } + //---- class ScrollBarHoverListener --------------------------------------- // using static field to disabling hover for other scroll bars 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 a62e83698..1f480bdce 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 @@ -48,8 +48,11 @@ import javax.swing.plaf.ComponentUI; import javax.swing.plaf.basic.BasicScrollPaneUI; import com.formdev.flatlaf.FlatClientProperties; +import com.formdev.flatlaf.FlatSystemProperties; +import com.formdev.flatlaf.SmoothScrollingHelper; import com.formdev.flatlaf.ui.FlatStylingSupport.Styleable; import com.formdev.flatlaf.ui.FlatStylingSupport.StyleableUI; +import com.formdev.flatlaf.util.Animator; import com.formdev.flatlaf.util.LoggingFacade; /** @@ -135,18 +138,36 @@ protected MouseWheelListener createMouseWheelListener() { MouseWheelListener superListener = super.createMouseWheelListener(); return e -> { if( isSmoothScrollingEnabled() && - scrollpane.isWheelScrollingEnabled() && - e.getScrollType() == MouseWheelEvent.WHEEL_UNIT_SCROLL && - e.getPreciseWheelRotation() != 0 && - e.getPreciseWheelRotation() != e.getWheelRotation() ) + scrollpane.isWheelScrollingEnabled() ) { - mouseWheelMovedSmooth( e ); + if( e.getScrollType() == MouseWheelEvent.WHEEL_UNIT_SCROLL && + e.getPreciseWheelRotation() != 0 && + e.getPreciseWheelRotation() != e.getWheelRotation() ) + { + // precise scrolling + mouseWheelMovedPrecise( e ); + } else { + // smooth scrolling + JScrollBar scrollBar = findScrollBarToScroll( e ); + if( scrollBar != null && scrollBar.getUI() instanceof FlatScrollBarUI ) { + FlatScrollBarUI ui = (FlatScrollBarUI) scrollBar.getUI(); + ui.runAndSetValueAnimated( () -> { + superListener.mouseWheelMoved( e ); + } ); + } else + superListener.mouseWheelMoved( e ); + } } else superListener.mouseWheelMoved( e ); }; } protected boolean isSmoothScrollingEnabled() { + if( !Animator.useAnimation() || !FlatSystemProperties.getBoolean( FlatSystemProperties.SMOOTH_SCROLLING, true ) ) + return false; + // Only allow smooth scrolling if it originates from an event on the scrollpane. Programmatic calls do not trigger smooth scrolling. + if(!SmoothScrollingHelper.isInSmoothScrolling( scrollpane.getViewport() )) + return false; Object smoothScrolling = scrollpane.getClientProperty( FlatClientProperties.SCROLL_PANE_SMOOTH_SCROLLING ); if( smoothScrolling instanceof Boolean ) return (Boolean) smoothScrolling; @@ -157,19 +178,16 @@ protected boolean isSmoothScrollingEnabled() { return UIManager.getBoolean( "ScrollPane.smoothScrolling" ); } - private void mouseWheelMovedSmooth( MouseWheelEvent e ) { + private void mouseWheelMovedPrecise( MouseWheelEvent e ) { // return if there is no viewport JViewport viewport = scrollpane.getViewport(); if( viewport == null ) return; // find scrollbar to scroll - JScrollBar scrollbar = scrollpane.getVerticalScrollBar(); - if( scrollbar == null || !scrollbar.isVisible() || e.isShiftDown() ) { - scrollbar = scrollpane.getHorizontalScrollBar(); - if( scrollbar == null || !scrollbar.isVisible() ) - return; - } + JScrollBar scrollbar = findScrollBarToScroll( e ); + if( scrollbar == null ) + return; // consume event e.consume(); @@ -262,6 +280,16 @@ else if( rotation < 0 ) */ } + private JScrollBar findScrollBarToScroll( MouseWheelEvent e ) { + JScrollBar scrollBar = scrollpane.getVerticalScrollBar(); + if( scrollBar == null || !scrollBar.isVisible() || e.isShiftDown() ) { + scrollBar = scrollpane.getHorizontalScrollBar(); + if( scrollBar == null || !scrollBar.isVisible() ) + return null; + } + return scrollBar; + } + @Override protected PropertyChangeListener createPropertyChangeListener() { PropertyChangeListener superListener = super.createPropertyChangeListener(); @@ -428,6 +456,48 @@ public static boolean isPermanentFocusOwner( JScrollPane scrollPane ) { return false; } + @Override + protected void syncScrollPaneWithViewport() { + if( isSmoothScrollingEnabled() ) { + runAndSyncScrollBarValueAnimated( scrollpane.getVerticalScrollBar(), 0, () -> { + runAndSyncScrollBarValueAnimated( scrollpane.getHorizontalScrollBar(), 1, () -> { + super.syncScrollPaneWithViewport(); + } ); + } ); + } else + super.syncScrollPaneWithViewport(); + } + + private void runAndSyncScrollBarValueAnimated( JScrollBar sb, int i, Runnable r ) { + if( inRunAndSyncValueAnimated[i] || sb == null ) { + r.run(); + return; + } + + inRunAndSyncValueAnimated[i] = true; + + int oldValue = sb.getValue(); + int oldVisibleAmount = sb.getVisibleAmount(); + int oldMinimum = sb.getMinimum(); + int oldMaximum = sb.getMaximum(); + + r.run(); + + int newValue = sb.getValue(); + if( newValue != oldValue && + sb.getVisibleAmount() == oldVisibleAmount && + sb.getMinimum() == oldMinimum && + sb.getMaximum() == oldMaximum && + sb.getUI() instanceof FlatScrollBarUI ) + { + ((FlatScrollBarUI)sb.getUI()).setValueAnimated( oldValue, newValue ); + } + + inRunAndSyncValueAnimated[i] = false; + } + + private final boolean[] inRunAndSyncValueAnimated = new boolean[2]; + //---- class Handler ------------------------------------------------------ /** diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatTreeUI.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatTreeUI.java index 31d898e22..593c7a93e 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatTreeUI.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatTreeUI.java @@ -16,9 +16,14 @@ package com.formdev.flatlaf.ui; -import static com.formdev.flatlaf.FlatClientProperties.*; +import static com.formdev.flatlaf.FlatClientProperties.STYLE; +import static com.formdev.flatlaf.FlatClientProperties.STYLE_CLASS; +import static com.formdev.flatlaf.FlatClientProperties.TREE_PAINT_SELECTION; +import static com.formdev.flatlaf.FlatClientProperties.TREE_WIDE_SELECTION; +import static com.formdev.flatlaf.FlatClientProperties.clientPropertyBoolean; import java.awt.Color; import java.awt.Component; +import java.awt.Container; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Insets; @@ -35,16 +40,21 @@ import javax.swing.Icon; import javax.swing.JComponent; import javax.swing.JLabel; +import javax.swing.JScrollBar; +import javax.swing.JScrollPane; import javax.swing.JTree; +import javax.swing.JTree.DropLocation; +import javax.swing.JViewport; import javax.swing.LookAndFeel; import javax.swing.SwingUtilities; import javax.swing.UIManager; -import javax.swing.JTree.DropLocation; import javax.swing.event.TreeSelectionListener; import javax.swing.plaf.ComponentUI; +import javax.swing.plaf.ScrollBarUI; import javax.swing.plaf.basic.BasicTreeUI; import javax.swing.tree.DefaultTreeCellRenderer; import javax.swing.tree.TreePath; +import com.formdev.flatlaf.SmoothScrollingHelper; import com.formdev.flatlaf.ui.FlatStylingSupport.Styleable; import com.formdev.flatlaf.ui.FlatStylingSupport.StyleableUI; import com.formdev.flatlaf.util.LoggingFacade; @@ -347,6 +357,42 @@ protected PropertyChangeListener createPropertyChangeListener() { }; } + @Override + public int getRowCount( JTree tree ) { + // When certain events are received like page up/down while smooth scrolling is in progress, incorrect row is identified. + // For a page event, this would move at the top/bottom of the incorrect row. + // Any of these events would get the row count, so when row count is requested, we temporarily set the scrollbar target value. + int rowCount = super.getRowCount( tree ); + Container parent = tree.getParent(); + if ( parent instanceof JViewport ) { + JViewport viewport = (JViewport)parent; + // if an event is received that is not triggered by our smooth scrolling timers... + if( SmoothScrollingHelper.isInSmoothScrolling( viewport ) ) { + Container viewportParent = viewport.getParent(); + if( viewportParent instanceof JScrollPane ) { + JScrollBar verticalScrollBar = ((JScrollPane)viewportParent).getVerticalScrollBar(); + if( verticalScrollBar != null ) { + ScrollBarUI sbUi = verticalScrollBar.getUI(); + if( sbUi instanceof FlatScrollBarUI ) { + int targetValue = ((FlatScrollBarUI)sbUi).getTargetValue(); + // ... and if there was an active timer on the vertical scrollbar of that tree... + if( targetValue != Integer.MIN_VALUE ) { + int value = verticalScrollBar.getValue(); + // ... then we force the value temporarily to the target value + SmoothScrollingHelper.setScrollBarValueWithOptionalRepaint( viewport, verticalScrollBar, targetValue ); + SwingUtilities.invokeLater( () -> { + // ... and restore the smooth scrolling current value. + SmoothScrollingHelper.setScrollBarValueWithOptionalRepaint( viewport, verticalScrollBar, value ); + }); + } + } + } + } + } + } + return rowCount; + } + private void repaintWideDropLocation(JTree.DropLocation loc) { if( loc == null || isDropLine( loc ) ) return; diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatViewportUI.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatViewportUI.java index d844725e4..7514f9d49 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatViewportUI.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatViewportUI.java @@ -21,8 +21,11 @@ import java.lang.reflect.Method; import javax.swing.JComponent; import javax.swing.JViewport; +import javax.swing.event.AncestorEvent; +import javax.swing.event.AncestorListener; import javax.swing.plaf.ComponentUI; import javax.swing.plaf.basic.BasicViewportUI; +import com.formdev.flatlaf.SmoothScrollingHelper; /** * Provides the Flat LaF UI delegate for {@link javax.swing.JViewport}. @@ -36,7 +39,7 @@ * @author Karl Tauber */ public class FlatViewportUI - extends BasicViewportUI + extends BasicViewportUI implements AncestorListener { public static ComponentUI createUI( JComponent c ) { return FlatUIUtils.createSharedUI( FlatViewportUI.class, FlatViewportUI::new ); @@ -59,6 +62,38 @@ public void paint( Graphics g, JComponent c ) { } } + @Override + public void installUI( JComponent c ) { + super.installUI( c ); + c.addAncestorListener( this ); + } + + @Override + public void uninstallUI( JComponent c ) { + super.uninstallUI( c ); + c.removeAncestorListener( this ); + } + + @Override + public void ancestorAdded( AncestorEvent event ) { + SmoothScrollingHelper.registerViewport( (JViewport)event.getComponent() ); + } + + @Override + public void ancestorMoved( AncestorEvent event ) { + JViewport viewport = (JViewport)event.getComponent(); + if(viewport.isShowing()) { + SmoothScrollingHelper.registerViewport( viewport ); + } else { + SmoothScrollingHelper.unregisterViewport( viewport ); + } + } + + @Override + public void ancestorRemoved( AncestorEvent event ) { + SmoothScrollingHelper.unregisterViewport( (JViewport)event.getComponent() ); + } + //---- interface ViewportPainter ------------------------------------------ /**