From fd9dbbd7e6485be1440894f1f985dd63ae521259 Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Thu, 9 Jan 2020 23:43:58 +0100 Subject: [PATCH] support smooth scrolling with touchpads and high precision mouse wheels (issue #27) --- CHANGELOG.md | 2 + .../formdev/flatlaf/ui/FlatScrollPaneUI.java | 139 ++++++++++++++++++ .../com/formdev/flatlaf/FlatLaf.properties | 1 + 3 files changed, 142 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c8801d51..793df496 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,8 @@ FlatLaf Change Log ## Unreleased +- Support smooth scrolling with touchpads and high precision mouse wheels. + (issue #27) - Changed `.properties` file loading order: Now all core `.properties` files are loaded before loading addon `.properties` files. This makes it easier to overwrite core values in addons. Also, addon loading order can be specified. 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 31481542..1a118dbb 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 @@ -14,15 +14,24 @@ * limitations under the License. */ +/* + * Smooth scrolling code partly based on code from IntelliJ IDEA Community Edition, + * which is licensed under the Apache 2.0 license. Copyright 2000-2016 JetBrains s.r.o. + * See: https://github.com/JetBrains/intellij-community/blob/31e1b5a8e43219b9571951bab6457cfb3012e3ef/platform/platform-api/src/com/intellij/ui/components/SmoothScrollPane.java#L141-L185 + * + */ package com.formdev.flatlaf.ui; import java.awt.Component; import java.awt.Graphics; import java.awt.Insets; +import java.awt.Rectangle; import java.awt.event.ContainerEvent; import java.awt.event.ContainerListener; import java.awt.event.FocusEvent; import java.awt.event.FocusListener; +import java.awt.event.MouseWheelEvent; +import java.awt.event.MouseWheelListener; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import javax.swing.BorderFactory; @@ -34,6 +43,8 @@ import javax.swing.JTable; import javax.swing.JViewport; import javax.swing.LookAndFeel; import javax.swing.ScrollPaneConstants; +import javax.swing.Scrollable; +import javax.swing.SwingConstants; import javax.swing.UIManager; import javax.swing.plaf.ComponentUI; import javax.swing.plaf.basic.BasicScrollPaneUI; @@ -50,6 +61,10 @@ import com.formdev.flatlaf.FlatClientProperties; * @uiDefault ScrollPane.border Border * @uiDefault ScrollPane.viewportBorder Border * + * + * + * @uiDefault ScrollPane.smoothScrolling boolean + * * @author Karl Tauber */ public class FlatScrollPaneUI @@ -94,6 +109,130 @@ public class FlatScrollPaneUI handler = null; } + @Override + protected MouseWheelListener createMouseWheelListener() { + return new BasicScrollPaneUI.MouseWheelHandler() { + @Override + public void mouseWheelMoved( MouseWheelEvent e ) { + // 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). + if( UIManager.getBoolean( "ScrollPane.smoothScrolling" ) && + scrollpane.isWheelScrollingEnabled() && + e.getScrollType() == MouseWheelEvent.WHEEL_UNIT_SCROLL && + e.getPreciseWheelRotation() != 0 && + e.getPreciseWheelRotation() != e.getWheelRotation() ) + { + mouseWheelMovedSmooth( e ); + } else + super.mouseWheelMoved( e ); + } + }; + } + + private static final double EPSILON = 1e-5d; + + private void mouseWheelMovedSmooth( 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; + } + + // consume event + e.consume(); + + // get precise wheel rotation + double rotation = e.getPreciseWheelRotation(); + + // get unit and block increment + int unitIncrement; + int blockIncrement; + int orientation = scrollbar.getOrientation(); + Component view = viewport.getView(); + if( view instanceof Scrollable ) { + Scrollable scrollable = (Scrollable) view; + + // Use (0, 0) view position to obtain constant unit increment of first item + // (which might otherwise be variable on smaller-than-unit scrolling). + Rectangle visibleRect = new Rectangle( viewport.getViewSize() ); + unitIncrement = scrollable.getScrollableUnitIncrement( visibleRect, orientation, 1 ); + blockIncrement = scrollable.getScrollableBlockIncrement( visibleRect, orientation, 1 ); + + if( unitIncrement > 0 ) { + // For the case that the first item (e.g. in a list) is larger + // than the other items, get the unit increment of the second item + // and use the smaller one. + if( orientation == SwingConstants.VERTICAL ) { + visibleRect.y += unitIncrement; + visibleRect.height -= unitIncrement; + } else { + visibleRect.x += unitIncrement; + visibleRect.width -= unitIncrement; + } + int unitIncrement2 = scrollable.getScrollableUnitIncrement( visibleRect, orientation, 1 ); + if( unitIncrement2 > 0 ) + unitIncrement = Math.min( unitIncrement, unitIncrement2 ); + } + } else { + int direction = rotation < 0 ? -1 : 1; + unitIncrement = scrollbar.getUnitIncrement( direction ); + blockIncrement = scrollbar.getBlockIncrement( direction ); + } + + // limit scroll amount (number of units to scroll) for small viewports + // (e.g. vertical scrolling in file chooser) + int scrollAmount = e.getScrollAmount(); + int viewportWH = (orientation == SwingConstants.VERTICAL) + ? viewport.getHeight() + : viewport.getWidth(); + if( unitIncrement * scrollAmount > viewportWH ) + scrollAmount = Math.max( viewportWH / unitIncrement, 1 ); + + // compute relative delta + double delta = rotation * scrollAmount * unitIncrement; + boolean adjustDelta = Math.abs( rotation ) < (1.0 + EPSILON); + double adjustedDelta = adjustDelta + ? Math.max( -blockIncrement, Math.min( delta, blockIncrement ) ) + : delta; + + // compute new value + int value = scrollbar.getValue(); + double minDelta = scrollbar.getMinimum() - value; + double maxDelta = scrollbar.getMaximum() - scrollbar.getModel().getExtent() - value; + double boundedDelta = Math.max( minDelta, Math.min( adjustedDelta, maxDelta ) ); + int newValue = value + (int) Math.round( boundedDelta ); + + // set new value + if( newValue != value ) + scrollbar.setValue( newValue ); + +/*debug + System.out.println( String.format( "%4d %9f / %4d %4d / %12f %5s %12f / %4d %4d %4d / %12f %12f %12f / %4d", + e.getWheelRotation(), + e.getPreciseWheelRotation(), + unitIncrement, + blockIncrement, + delta, + adjustDelta, + adjustedDelta, + value, + scrollbar.getMinimum(), + scrollbar.getMaximum(), + minDelta, + maxDelta, + boundedDelta, + newValue ) ); +*/ + } + @Override protected PropertyChangeListener createPropertyChangeListener() { return new BasicScrollPaneUI.PropertyChangeHandler() { diff --git a/flatlaf-core/src/main/resources/com/formdev/flatlaf/FlatLaf.properties b/flatlaf-core/src/main/resources/com/formdev/flatlaf/FlatLaf.properties index 111ed511..065d54ea 100644 --- a/flatlaf-core/src/main/resources/com/formdev/flatlaf/FlatLaf.properties +++ b/flatlaf-core/src/main/resources/com/formdev/flatlaf/FlatLaf.properties @@ -311,6 +311,7 @@ ScrollBar.buttonDisabledArrowColor=$ComboBox.buttonDisabledArrowColor ScrollPane.border=com.formdev.flatlaf.ui.FlatBorder ScrollPane.background=$ScrollBar.track ScrollPane.fillUpperCorner=true +ScrollPane.smoothScrolling=true #---- Separator ----