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/ui/FlatEditorPaneUI.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatEditorPaneUI.java
index 2614ddee0..925d337b6 100644
--- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatEditorPaneUI.java
+++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatEditorPaneUI.java
@@ -31,6 +31,7 @@
import javax.swing.plaf.ComponentUI;
import javax.swing.plaf.basic.BasicEditorPaneUI;
import javax.swing.text.Caret;
+import javax.swing.text.DefaultEditorKit;
import javax.swing.text.JTextComponent;
import com.formdev.flatlaf.FlatClientProperties;
import com.formdev.flatlaf.ui.FlatStylingSupport.Styleable;
@@ -145,6 +146,21 @@ protected void uninstallListeners() {
focusListener = null;
}
+ @Override
+ protected void installKeyboardActions() {
+ super.installKeyboardActions();
+ installKeyboardActions( getComponent() );
+ }
+
+ static void installKeyboardActions( JTextComponent c ) {
+ FlatScrollPaneUI.installSmoothScrollingDelegateActions( c, false,
+ /* page-down */ DefaultEditorKit.pageDownAction, // PAGE_DOWN
+ /* page-up */ DefaultEditorKit.pageUpAction, // PAGE_UP
+ /* DefaultEditorKit.selectionPageDownAction */ "selection-page-down", // shift PAGE_DOWN
+ /* DefaultEditorKit.selectionPageUpAction */ "selection-page-up" // shift PAGE_UP
+ );
+ }
+
@Override
protected Caret createCaret() {
return new FlatCaret( null, false );
@@ -159,6 +175,11 @@ protected void propertyChange( PropertyChangeEvent e ) {
super.propertyChange( e );
propertyChange( getComponent(), e, this::installStyle );
+
+ // BasicEditorPaneUI.propertyChange() re-applied actions from editor kit,
+ // which removed our delegate actions
+ if( "editorKit".equals( propertyName ) )
+ installKeyboardActions( getComponent() );
}
static void propertyChange( JTextComponent c, PropertyChangeEvent e, Runnable installStyle ) {
diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatMenuBarUI.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatMenuBarUI.java
index 4cdb44fa3..7bf0901f6 100644
--- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatMenuBarUI.java
+++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatMenuBarUI.java
@@ -27,7 +27,6 @@
import java.beans.PropertyChangeListener;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
-import javax.swing.AbstractAction;
import javax.swing.ActionMap;
import javax.swing.BoxLayout;
import javax.swing.JComponent;
@@ -149,7 +148,7 @@ protected void installKeyboardActions() {
map = new ActionMapUIResource();
SwingUtilities.replaceUIActionMap( menuBar, map );
}
- map.put( "takeFocus", new TakeFocus() );
+ map.put( "takeFocus", new TakeFocus( "takeFocus" ) );
}
/** @since 2 */
@@ -373,8 +372,12 @@ public void layoutContainer( Container target ) {
* On other platforms, the popup of the first menu is shown.
*/
private static class TakeFocus
- extends AbstractAction
+ extends FlatUIAction
{
+ public TakeFocus( String name ) {
+ super( name );
+ }
+
@Override
public void actionPerformed( ActionEvent e ) {
JMenuBar menuBar = (JMenuBar) e.getSource();
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..0b61fe7bf 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;
@@ -39,10 +40,13 @@
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.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 +207,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 +445,197 @@ 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() ) {
+ r.run();
+ 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();
+
+ // run given runnable, which computes and sets the new scrollbar value
+ FlatScrollPaneUI.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();
+ } );
+
+ // 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;
+
+ int getTargetValue() {
+ return targetValue;
+ }
+
+ public void setValueAnimated( int initialValue, int value ) {
+ if( useValueIsAdjusting )
+ scrollbar.setValueIsAdjusting( true );
+
+ // (always) set scrollbar value to initial value
+ scrollbar.setValue( initialValue );
+
+ // 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.
+ // Without this check, repeating-scrolling via keyboard would become
+ // very slow when reaching the top/bottom/left/right of the viewport,
+ // because it would start a new 200ms animation to scroll a few pixels.
+ if( value == targetValue ||
+ (value > startValue && value < targetValue) || // scroll down/right
+ (value < startValue && value > targetValue) ) // scroll up/left
+ return;
+ }
+
+ 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 );
+
+ scrollbar.setValue( startValue + Math.round( (targetValue - startValue) * fraction ) );
+ }, () -> {
+ 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();
+ }
+
+ 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..2e5ee7282 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
@@ -17,10 +17,12 @@
package com.formdev.flatlaf.ui;
import java.awt.Component;
+import java.awt.Container;
import java.awt.Graphics;
import java.awt.Insets;
import java.awt.KeyboardFocusManager;
import java.awt.Rectangle;
+import java.awt.event.ActionEvent;
import java.awt.event.ContainerEvent;
import java.awt.event.ContainerListener;
import java.awt.event.FocusEvent;
@@ -31,6 +33,8 @@
import java.beans.PropertyChangeListener;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
+import javax.swing.Action;
+import javax.swing.ActionMap;
import javax.swing.BorderFactory;
import javax.swing.JButton;
import javax.swing.JComponent;
@@ -48,8 +52,10 @@
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.ui.FlatStylingSupport.Styleable;
import com.formdev.flatlaf.ui.FlatStylingSupport.StyleableUI;
+import com.formdev.flatlaf.util.Animator;
import com.formdev.flatlaf.util.LoggingFacade;
/**
@@ -135,18 +141,33 @@ 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 &&
+ isPreciseWheelEvent( e ) )
+ {
+ // 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;
+
Object smoothScrolling = scrollpane.getClientProperty( FlatClientProperties.SCROLL_PANE_SMOOTH_SCROLLING );
if( smoothScrolling instanceof Boolean )
return (Boolean) smoothScrolling;
@@ -157,19 +178,40 @@ protected boolean isSmoothScrollingEnabled() {
return UIManager.getBoolean( "ScrollPane.smoothScrolling" );
}
- private void mouseWheelMovedSmooth( MouseWheelEvent e ) {
+ private long lastPreciseWheelWhen;
+
+ private boolean isPreciseWheelEvent( MouseWheelEvent e ) {
+ double preciseWheelRotation = e.getPreciseWheelRotation();
+ if( preciseWheelRotation != 0 && preciseWheelRotation != e.getWheelRotation() ) {
+ // precise wheel event
+ lastPreciseWheelWhen = e.getWhen();
+ return true;
+ }
+
+ // If a non-precise wheel event occurs shortly after a precise wheel event,
+ // then it is probably still a precise wheel but the precise value
+ // is by chance an integer value (e.g. 1.0 or 2.0).
+ // Not handling this special case, would start an animation for smooth scrolling,
+ // which would be interrupted soon when the next precise wheel event occurs.
+ // This would result in jittery scrolling. E.g. on a MacBook using Trackpad or Magic Mouse.
+ if( e.getWhen() - lastPreciseWheelWhen < 1000 )
+ return true;
+
+ // non-precise wheel event
+ lastPreciseWheelWhen = 0;
+ return false;
+ }
+
+ 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 +304,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 +480,119 @@ public static boolean isPermanentFocusOwner( JScrollPane scrollPane ) {
return false;
}
+ @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, false, () -> {
+ runAndSyncScrollBarValueAnimated( scrollpane.getHorizontalScrollBar(), 1, false, () -> {
+ super.syncScrollPaneWithViewport();
+ } );
+ } );
+ } else
+ super.syncScrollPaneWithViewport();
+ }
+
+ /**
+ * Runs the given runnable, if smooth scrolling is enabled, with disabled
+ * viewport blitting mode and with scroll bar value set to "target" value.
+ * This is necessary when calculating new view position during animation.
+ * Otherwise calculation would use wrong view position and (repeating) scrolling
+ * would be much slower than without smooth scrolling.
+ */
+ private void runWithScrollBarsTargetValues( boolean blittingOnly, Runnable r ) {
+ if( isSmoothScrollingEnabled() ) {
+ runWithoutBlitting( scrollpane, () -> {
+ if( blittingOnly )
+ r.run();
+ else {
+ runAndSyncScrollBarValueAnimated( scrollpane.getVerticalScrollBar(), 0, true, () -> {
+ runAndSyncScrollBarValueAnimated( scrollpane.getHorizontalScrollBar(), 1, true, r );
+ } );
+ }
+ } );
+ } else
+ r.run();
+ }
+
+ private void runAndSyncScrollBarValueAnimated( JScrollBar sb, int i, boolean useTargetValue, Runnable r ) {
+ if( inRunAndSyncValueAnimated[i] || sb == null || !(sb.getUI() instanceof FlatScrollBarUI) ) {
+ r.run();
+ return;
+ }
+
+ inRunAndSyncValueAnimated[i] = true;
+
+ int oldValue = sb.getValue();
+ int oldVisibleAmount = sb.getVisibleAmount();
+ int oldMinimum = sb.getMinimum();
+ int oldMaximum = sb.getMaximum();
+
+ FlatScrollBarUI ui = (FlatScrollBarUI) sb.getUI();
+ if( useTargetValue && ui.getTargetValue() != Integer.MIN_VALUE )
+ sb.setValue( ui.getTargetValue() );
+
+ r.run();
+
+ int newValue = sb.getValue();
+
+ if( newValue != oldValue &&
+ sb.getVisibleAmount() == oldVisibleAmount &&
+ sb.getMinimum() == oldMinimum &&
+ sb.getMaximum() == oldMaximum &&
+ sb.getUI() instanceof FlatScrollBarUI )
+ {
+ ui.setValueAnimated( oldValue, newValue );
+ }
+
+ inRunAndSyncValueAnimated[i] = false;
+ }
+
+ private final boolean[] inRunAndSyncValueAnimated = new boolean[2];
+
+ /**
+ * Runs the given runnable with disabled viewport blitting mode.
+ * If blitting mode is enabled, the viewport immediately repaints parts of the
+ * view if the view position is changed via JViewport.setViewPosition().
+ * This causes scrolling artifacts if smooth scrolling is enabled and the view position
+ * is "temporary" changed to its new target position, changed back to its old position
+ * and again moved animated to the target position.
+ */
+ static void runWithoutBlitting( Container scrollPane, Runnable r ) {
+ // prevent the viewport to immediately repaint using blitting
+ JViewport viewport = (scrollPane instanceof JScrollPane) ? ((JScrollPane)scrollPane).getViewport() : null;
+ boolean isBlitScrollMode = (viewport != null) ? viewport.getScrollMode() == JViewport.BLIT_SCROLL_MODE : false;
+ if( isBlitScrollMode )
+ viewport.setScrollMode( JViewport.SIMPLE_SCROLL_MODE );
+
+ try {
+ r.run();
+ } finally {
+ if( isBlitScrollMode )
+ viewport.setScrollMode( JViewport.BLIT_SCROLL_MODE );
+ }
+ }
+
+ public static void installSmoothScrollingDelegateActions( JComponent c, boolean blittingOnly, String... actionKeys ) {
+ // get shared action map, used for all components of same type
+ ActionMap map = SwingUtilities.getUIActionMap( c );
+ if( map == null )
+ return;
+
+ // install actions, but only if not already installed
+ for( String actionKey : actionKeys )
+ installSmoothScrollingDelegateAction( map, blittingOnly, actionKey );
+ }
+
+ private static void installSmoothScrollingDelegateAction( ActionMap map, boolean blittingOnly, String actionKey ) {
+ Action oldAction = map.get( actionKey );
+ if( oldAction == null || oldAction instanceof SmoothScrollingDelegateAction )
+ return; // not found or already installed
+
+ map.put( actionKey, new SmoothScrollingDelegateAction( oldAction, blittingOnly ) );
+ }
+
//---- class Handler ------------------------------------------------------
/**
@@ -459,4 +624,34 @@ public void focusLost( FocusEvent e ) {
scrollpane.repaint();
}
}
+
+ //---- class SmoothScrollingDelegateAction --------------------------------
+
+ /**
+ * Used to run component actions with disabled blitting mode and
+ * with scroll bar target values.
+ */
+ private static class SmoothScrollingDelegateAction
+ extends FlatUIAction
+ {
+ private final boolean blittingOnly;
+
+ private SmoothScrollingDelegateAction( Action delegate, boolean blittingOnly ) {
+ super( delegate );
+ this.blittingOnly = blittingOnly;
+ }
+
+ @Override
+ public void actionPerformed( ActionEvent e ) {
+ Object source = e.getSource();
+ JScrollPane scrollPane = (source instanceof Component)
+ ? (JScrollPane) SwingUtilities.getAncestorOfClass( JScrollPane.class, (Component) source )
+ : null;
+ if( scrollPane != null && scrollPane.getUI() instanceof FlatScrollPaneUI ) {
+ ((FlatScrollPaneUI)scrollPane.getUI()).runWithScrollBarsTargetValues( blittingOnly,
+ () -> delegate.actionPerformed( e ) );
+ } else
+ delegate.actionPerformed( e );
+ }
+ }
}
diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatTabbedPaneUI.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatTabbedPaneUI.java
index 52bc470c8..c39510d95 100644
--- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatTabbedPaneUI.java
+++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatTabbedPaneUI.java
@@ -3520,10 +3520,8 @@ else if( direction == EAST || direction == SOUTH )
//---- class RunWithOriginalLayoutManagerDelegateAction -------------------
private static class RunWithOriginalLayoutManagerDelegateAction
- implements Action
+ extends FlatUIAction
{
- private final Action delegate;
-
static void install( ActionMap map, String key ) {
Action oldAction = map.get( key );
if( oldAction == null || oldAction instanceof RunWithOriginalLayoutManagerDelegateAction )
@@ -3533,24 +3531,9 @@ static void install( ActionMap map, String key ) {
}
private RunWithOriginalLayoutManagerDelegateAction( Action delegate ) {
- this.delegate = delegate;
- }
-
- @Override
- public Object getValue( String key ) {
- return delegate.getValue( key );
+ super( delegate );
}
- @Override
- public boolean isEnabled() {
- return delegate.isEnabled();
- }
-
- @Override public void putValue( String key, Object value ) {}
- @Override public void setEnabled( boolean b ) {}
- @Override public void addPropertyChangeListener( PropertyChangeListener listener ) {}
- @Override public void removePropertyChangeListener( PropertyChangeListener listener ) {}
-
@Override
public void actionPerformed( ActionEvent e ) {
JTabbedPane tabbedPane = (JTabbedPane) e.getSource();
diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatTextAreaUI.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatTextAreaUI.java
index 6b5cd3f8f..3c4d09586 100644
--- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatTextAreaUI.java
+++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatTextAreaUI.java
@@ -141,6 +141,12 @@ protected void uninstallListeners() {
focusListener = null;
}
+ @Override
+ protected void installKeyboardActions() {
+ super.installKeyboardActions();
+ FlatEditorPaneUI.installKeyboardActions( getComponent() );
+ }
+
@Override
protected Caret createCaret() {
return new FlatCaret( null, false );
diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatTextPaneUI.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatTextPaneUI.java
index 1452ebc77..b9986ed94 100644
--- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatTextPaneUI.java
+++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatTextPaneUI.java
@@ -142,6 +142,12 @@ protected void uninstallListeners() {
focusListener = null;
}
+ @Override
+ protected void installKeyboardActions() {
+ super.installKeyboardActions();
+ FlatEditorPaneUI.installKeyboardActions( getComponent() );
+ }
+
@Override
protected Caret createCaret() {
return new FlatCaret( null, false );
@@ -156,6 +162,11 @@ protected void propertyChange( PropertyChangeEvent e ) {
super.propertyChange( e );
FlatEditorPaneUI.propertyChange( getComponent(), e, this::installStyle );
+
+ // BasicEditorPaneUI.propertyChange() re-applied actions from editor kit,
+ // which removed our delegate actions
+ if( "editorKit".equals( propertyName ) )
+ FlatEditorPaneUI.installKeyboardActions( getComponent() );
}
/** @since 2 */
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..e3522d244 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
@@ -238,6 +238,22 @@ protected void uninstallDefaults() {
oldStyleValues = null;
}
+ @Override
+ protected void installKeyboardActions() {
+ super.installKeyboardActions();
+
+ FlatScrollPaneUI.installSmoothScrollingDelegateActions( tree, false,
+ "scrollDownChangeSelection", // PAGE_DOWN
+ "scrollUpChangeSelection", // PAGE_UP
+ "scrollDownChangeLead", // ctrl PAGE_DOWN
+ "scrollUpChangeLead", // ctrl PAGE_UP
+ "scrollDownExtendSelection", // shift PAGE_DOWN, shift ctrl PAGE_DOWN
+ "scrollUpExtendSelection", // shift PAGE_UP, shift ctrl PAGE_UP
+ "selectNextChangeLead", // ctrl DOWN
+ "selectPreviousChangeLead" // ctrl UP
+ );
+ }
+
@Override
protected void updateRenderer() {
super.updateRenderer();
diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatUIAction.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatUIAction.java
new file mode 100644
index 000000000..b509c471b
--- /dev/null
+++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatUIAction.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2023 FormDev Software GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.formdev.flatlaf.ui;
+
+import java.beans.PropertyChangeListener;
+import javax.swing.Action;
+
+/**
+ * Base class for UI actions used in ActionMap.
+ * (similar to class sun.swing.UIAction)
+ *
+ * @author Karl Tauber
+ * @since 3.3
+ */
+public abstract class FlatUIAction
+ implements Action
+{
+ protected final String name;
+ protected final Action delegate;
+
+ protected FlatUIAction( String name ) {
+ this.name = name;
+ this.delegate = null;
+ }
+
+ protected FlatUIAction( Action delegate ) {
+ this.name = null;
+ this.delegate = delegate;
+ }
+
+ @Override
+ public Object getValue( String key ) {
+ if( key == NAME && delegate == null )
+ return name;
+ return (delegate != null) ? delegate.getValue( key ) : null;
+ }
+
+ @Override
+ public boolean isEnabled() {
+ return (delegate != null) ? delegate.isEnabled() : true;
+ }
+
+ // do nothing in following methods because this class is immutable
+ @Override public void putValue( String key, Object value ) {}
+ @Override public void setEnabled( boolean b ) {}
+ @Override public void addPropertyChangeListener( PropertyChangeListener listener ) {}
+ @Override public void removePropertyChangeListener( PropertyChangeListener listener ) {}
+}
diff --git a/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/DemoFrame.java b/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/DemoFrame.java
index bfedf7a57..ca9c5795e 100644
--- a/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/DemoFrame.java
+++ b/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/DemoFrame.java
@@ -33,6 +33,7 @@
import com.formdev.flatlaf.FlatIntelliJLaf;
import com.formdev.flatlaf.FlatLaf;
import com.formdev.flatlaf.FlatLightLaf;
+import com.formdev.flatlaf.FlatSystemProperties;
import com.formdev.flatlaf.demo.HintManager.Hint;
import com.formdev.flatlaf.demo.extras.*;
import com.formdev.flatlaf.demo.intellijthemes.*;
@@ -266,6 +267,18 @@ private void alwaysShowMnemonics() {
repaint();
}
+ private void animationChanged() {
+ boolean enabled = animationMenuItem.isSelected();
+ System.setProperty( FlatSystemProperties.ANIMATION, Boolean.toString( enabled ) );
+
+ smoothScrollingMenuItem.setEnabled( enabled );
+ animatedLafChangeMenuItem.setEnabled( enabled );
+ }
+
+ private void smoothScrollingChanged() {
+ UIManager.put( "ScrollPane.smoothScrolling", smoothScrollingMenuItem.isSelected() );
+ }
+
private void animatedLafChangeChanged() {
System.setProperty( "flatlaf.animatedLafChange", String.valueOf( animatedLafChangeMenuItem.isSelected() ) );
}
@@ -505,6 +518,8 @@ private void initComponents() {
showTitleBarIconMenuItem = new JCheckBoxMenuItem();
underlineMenuSelectionMenuItem = new JCheckBoxMenuItem();
alwaysShowMnemonicsMenuItem = new JCheckBoxMenuItem();
+ animationMenuItem = new JCheckBoxMenuItem();
+ smoothScrollingMenuItem = new JCheckBoxMenuItem();
animatedLafChangeMenuItem = new JCheckBoxMenuItem();
JMenuItem showHintsMenuItem = new JMenuItem();
JMenuItem showUIDefaultsInspectorMenuItem = new JMenuItem();
@@ -792,6 +807,19 @@ private void initComponents() {
alwaysShowMnemonicsMenuItem.setText("Always show mnemonics");
alwaysShowMnemonicsMenuItem.addActionListener(e -> alwaysShowMnemonics());
optionsMenu.add(alwaysShowMnemonicsMenuItem);
+ optionsMenu.addSeparator();
+
+ //---- animationMenuItem ----
+ animationMenuItem.setText("Animation");
+ animationMenuItem.setSelected(true);
+ animationMenuItem.addActionListener(e -> animationChanged());
+ optionsMenu.add(animationMenuItem);
+
+ //---- smoothScrollingMenuItem ----
+ smoothScrollingMenuItem.setText("Smooth Scrolling");
+ smoothScrollingMenuItem.setSelected(true);
+ smoothScrollingMenuItem.addActionListener(e -> smoothScrollingChanged());
+ optionsMenu.add(smoothScrollingMenuItem);
//---- animatedLafChangeMenuItem ----
animatedLafChangeMenuItem.setText("Animated Laf Change");
@@ -981,6 +1009,8 @@ private void unsupported( JCheckBoxMenuItem menuItem ) {
private JCheckBoxMenuItem showTitleBarIconMenuItem;
private JCheckBoxMenuItem underlineMenuSelectionMenuItem;
private JCheckBoxMenuItem alwaysShowMnemonicsMenuItem;
+ private JCheckBoxMenuItem animationMenuItem;
+ private JCheckBoxMenuItem smoothScrollingMenuItem;
private JCheckBoxMenuItem animatedLafChangeMenuItem;
private JMenuItem aboutMenuItem;
private JToolBar toolBar;
diff --git a/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/DemoFrame.jfd b/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/DemoFrame.jfd
index ba3f2d4fb..1e5b104aa 100644
--- a/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/DemoFrame.jfd
+++ b/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/DemoFrame.jfd
@@ -418,6 +418,27 @@ new FormModel {
}
addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "alwaysShowMnemonics", false ) )
} )
+ add( new FormComponent( "javax.swing.JPopupMenu$Separator" ) {
+ name: "separator9"
+ } )
+ add( new FormComponent( "javax.swing.JCheckBoxMenuItem" ) {
+ name: "animationMenuItem"
+ "text": "Animation"
+ "selected": true
+ auxiliary() {
+ "JavaCodeGenerator.variableLocal": false
+ }
+ addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "animationChanged", false ) )
+ } )
+ add( new FormComponent( "javax.swing.JCheckBoxMenuItem" ) {
+ name: "smoothScrollingMenuItem"
+ "text": "Smooth Scrolling"
+ "selected": true
+ auxiliary() {
+ "JavaCodeGenerator.variableLocal": false
+ }
+ addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "smoothScrollingChanged", false ) )
+ } )
add( new FormComponent( "javax.swing.JCheckBoxMenuItem" ) {
name: "animatedLafChangeMenuItem"
"text": "Animated Laf Change"
diff --git a/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/intellijthemes/IJThemesPanel.java b/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/intellijthemes/IJThemesPanel.java
index 1af0f05c9..0c2561e2f 100644
--- a/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/intellijthemes/IJThemesPanel.java
+++ b/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/intellijthemes/IJThemesPanel.java
@@ -25,6 +25,7 @@
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.event.WindowListener;
+import java.beans.Beans;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.File;
@@ -156,6 +157,9 @@ private String buildToolTip( IJThemeInfo ti ) {
}
private void updateThemesList() {
+ if( Beans.isDesignTime() )
+ return; // disable if running in GUI builder
+
int filterLightDark = filterComboBox.getSelectedIndex();
boolean showLight = (filterLightDark != 2);
boolean showDark = (filterLightDark != 1);
diff --git a/flatlaf-extras/src/main/java/com/formdev/flatlaf/extras/FlatAnimatedLafChange.java b/flatlaf-extras/src/main/java/com/formdev/flatlaf/extras/FlatAnimatedLafChange.java
index b46d26f4e..46899a19c 100644
--- a/flatlaf-extras/src/main/java/com/formdev/flatlaf/extras/FlatAnimatedLafChange.java
+++ b/flatlaf-extras/src/main/java/com/formdev/flatlaf/extras/FlatAnimatedLafChange.java
@@ -67,7 +67,7 @@ public class FlatAnimatedLafChange
* Invoke before setting new look and feel.
*/
public static void showSnapshot() {
- if( !FlatSystemProperties.getBoolean( "flatlaf.animatedLafChange", true ) )
+ if( !Animator.useAnimation() || !FlatSystemProperties.getBoolean( "flatlaf.animatedLafChange", true ) )
return;
// stop already running animation
@@ -138,7 +138,7 @@ public void removeNotify() {
* Invoke after updating UI.
*/
public static void hideSnapshotWithAnimation() {
- if( !FlatSystemProperties.getBoolean( "flatlaf.animatedLafChange", true ) )
+ if( !Animator.useAnimation() || !FlatSystemProperties.getBoolean( "flatlaf.animatedLafChange", true ) )
return;
if( oldUIsnapshots.isEmpty() )
diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatAnimatorTest.java b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatAnimatorTest.java
index 831a132d7..7dbe44311 100644
--- a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatAnimatorTest.java
+++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatAnimatorTest.java
@@ -17,7 +17,11 @@
package com.formdev.flatlaf.testing;
import java.awt.*;
+import java.awt.event.MouseWheelEvent;
+import java.awt.event.MouseWheelListener;
import javax.swing.*;
+import javax.swing.border.*;
+import com.formdev.flatlaf.ui.FlatUIUtils;
import com.formdev.flatlaf.util.Animator;
import com.formdev.flatlaf.util.CubicBezierEasing;
import net.miginfocom.swing.*;
@@ -40,6 +44,8 @@ public static void main( String[] args ) {
FlatAnimatorTest() {
initComponents();
+
+ mouseWheelTestPanel.lineChartPanel = lineChartPanel;
}
private void start() {
@@ -74,11 +80,14 @@ private void startEaseInOut() {
private void initComponents() {
// JFormDesigner - Component initialization - DO NOT MODIFY //GEN-BEGIN:initComponents
- JLabel label1 = new JLabel();
+ JLabel linearLabel = new JLabel();
linearScrollBar = new JScrollBar();
- JLabel label2 = new JLabel();
+ JLabel easeInOutLabel = new JLabel();
easeInOutScrollBar = new JScrollBar();
startButton = new JButton();
+ JLabel mouseWheelTestLabel = new JLabel();
+ mouseWheelTestPanel = new FlatAnimatorTest.MouseWheelTestPanel();
+ lineChartPanel = new LineChartPanel();
//======== this ========
setLayout(new MigLayout(
@@ -89,20 +98,22 @@ private void initComponents() {
// rows
"[]" +
"[]" +
- "[]"));
+ "[]para" +
+ "[top]" +
+ "[400,grow,fill]"));
- //---- label1 ----
- label1.setText("Linear:");
- add(label1, "cell 0 0");
+ //---- linearLabel ----
+ linearLabel.setText("Linear:");
+ add(linearLabel, "cell 0 0");
//---- linearScrollBar ----
linearScrollBar.setOrientation(Adjustable.HORIZONTAL);
linearScrollBar.setBlockIncrement(1);
add(linearScrollBar, "cell 1 0");
- //---- label2 ----
- label2.setText("Ease in out:");
- add(label2, "cell 0 1");
+ //---- easeInOutLabel ----
+ easeInOutLabel.setText("Ease in out:");
+ add(easeInOutLabel, "cell 0 1");
//---- easeInOutScrollBar ----
easeInOutScrollBar.setOrientation(Adjustable.HORIZONTAL);
@@ -113,6 +124,18 @@ private void initComponents() {
startButton.setText("Start");
startButton.addActionListener(e -> start());
add(startButton, "cell 0 2");
+
+ //---- mouseWheelTestLabel ----
+ mouseWheelTestLabel.setText("Mouse wheel test:");
+ add(mouseWheelTestLabel, "cell 0 3");
+
+ //---- mouseWheelTestPanel ----
+ mouseWheelTestPanel.setBorder(new LineBorder(Color.red));
+ add(mouseWheelTestPanel, "cell 1 3,height 100");
+
+ //---- lineChartPanel ----
+ lineChartPanel.setUpdateChartDelayed(false);
+ add(lineChartPanel, "cell 0 4 2 1");
// JFormDesigner - End of component initialization //GEN-END:initComponents
}
@@ -120,5 +143,89 @@ private void initComponents() {
private JScrollBar linearScrollBar;
private JScrollBar easeInOutScrollBar;
private JButton startButton;
+ private FlatAnimatorTest.MouseWheelTestPanel mouseWheelTestPanel;
+ private LineChartPanel lineChartPanel;
// JFormDesigner - End of variables declaration //GEN-END:variables
+
+ //---- class MouseWheelTestPanel ------------------------------------------
+
+ static class MouseWheelTestPanel
+ extends JPanel
+ implements MouseWheelListener
+ {
+ private static final int MAX_VALUE = 1000;
+ private static final int STEP = 100;
+
+ private final JLabel valueLabel;
+ private final Animator animator;
+
+ LineChartPanel lineChartPanel;
+
+ private int value;
+ private int startValue;
+ private int targetValue = -1;
+
+ MouseWheelTestPanel() {
+ super( new BorderLayout() );
+ valueLabel = new JLabel( String.valueOf( value ), SwingConstants.CENTER );
+ valueLabel.setFont( valueLabel.getFont().deriveFont( (float) valueLabel.getFont().getSize() * 2 ) );
+ add( valueLabel, BorderLayout.CENTER );
+ add( new JLabel( " " ), BorderLayout.NORTH );
+ add( new JLabel( "(move mouse into rectangle and rotate mouse wheel)", SwingConstants.CENTER ), BorderLayout.SOUTH );
+
+ int duration = FlatUIUtils.getUIInt( "ScrollPane.smoothScrolling.duration", 200 );
+ int resolution = FlatUIUtils.getUIInt( "ScrollPane.smoothScrolling.resolution", 10 );
+
+ animator = new Animator( duration, fraction -> {
+ value = startValue + Math.round( (targetValue - startValue) * fraction );
+ valueLabel.setText( String.valueOf( value ) );
+
+ lineChartPanel.addValue( Color.red, value / (double) MAX_VALUE, value, null );
+ }, () -> {
+ targetValue = -1;
+ } );
+ animator.setResolution( resolution );
+ animator.setInterpolator( new CubicBezierEasing( 0.5f, 0.5f, 0.5f, 1 ) );
+
+ addMouseWheelListener( this );
+ }
+
+ @Override
+ public void mouseWheelMoved( MouseWheelEvent e ) {
+ double preciseWheelRotation = e.getPreciseWheelRotation();
+
+ // add a dot in the middle of the chart for the wheel rotation
+ // for unprecise wheels the rotation value is usually -1 or +1
+ // for precise wheels the rotation value is in range ca. -10 to +10,
+ // depending how fast the wheel is rotated
+ lineChartPanel.addDot( Color.red, 0.5 + (preciseWheelRotation / 20.), (int) (preciseWheelRotation * 1000), null, null );
+
+ // increase/decrease target value if animation is in progress
+ int newValue = (int) ((targetValue < 0 ? value : targetValue) + (STEP * preciseWheelRotation));
+ newValue = Math.min( Math.max( newValue, 0 ), MAX_VALUE );
+
+ if( preciseWheelRotation != 0 &&
+ preciseWheelRotation != e.getWheelRotation() )
+ {
+ // do not use animation for precise scrolling (e.g. with trackpad)
+
+ // stop running animation (if any)
+ animator.stop();
+
+ value = newValue;
+ valueLabel.setText( String.valueOf( value ) );
+
+ lineChartPanel.addValue( Color.red, value / (double) MAX_VALUE, value, null );
+ return;
+ }
+
+ // start next animation at the current value
+ startValue = value;
+ targetValue = newValue;
+
+ // restart animator
+ animator.cancel();
+ animator.start();
+ }
+ }
}
diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatAnimatorTest.jfd b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatAnimatorTest.jfd
index bb92a1299..ba9363e81 100644
--- a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatAnimatorTest.jfd
+++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatAnimatorTest.jfd
@@ -1,4 +1,4 @@
-JFDML JFormDesigner: "7.0.2.0.298" Java: "14.0.2" encoding: "UTF-8"
+JFDML JFormDesigner: "8.1.0.0.283" Java: "19.0.2" encoding: "UTF-8"
new FormModel {
contentType: "form/swing"
@@ -9,11 +9,11 @@ new FormModel {
add( new FormContainer( "com.formdev.flatlaf.testing.FlatTestPanel", new FormLayoutManager( class net.miginfocom.swing.MigLayout ) {
"$layoutConstraints": "ltr,insets dialog,hidemode 3"
"$columnConstraints": "[fill][grow,fill]"
- "$rowConstraints": "[][][]"
+ "$rowConstraints": "[][][]para[top][400,grow,fill]"
} ) {
name: "this"
add( new FormComponent( "javax.swing.JLabel" ) {
- name: "label1"
+ name: "linearLabel"
"text": "Linear:"
}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
"value": "cell 0 0"
@@ -29,7 +29,7 @@ new FormModel {
"value": "cell 1 0"
} )
add( new FormComponent( "javax.swing.JLabel" ) {
- name: "label2"
+ name: "easeInOutLabel"
"text": "Ease in out:"
}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
"value": "cell 0 1"
@@ -54,9 +54,33 @@ new FormModel {
}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
"value": "cell 0 2"
} )
+ add( new FormComponent( "javax.swing.JLabel" ) {
+ name: "mouseWheelTestLabel"
+ "text": "Mouse wheel test:"
+ }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
+ "value": "cell 0 3"
+ } )
+ add( new FormComponent( "com.formdev.flatlaf.testing.FlatAnimatorTest$MouseWheelTestPanel" ) {
+ name: "mouseWheelTestPanel"
+ "border": new javax.swing.border.LineBorder( sfield java.awt.Color red, 1, false )
+ auxiliary() {
+ "JavaCodeGenerator.variableLocal": false
+ }
+ }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
+ "value": "cell 1 3,height 100"
+ } )
+ add( new FormComponent( "com.formdev.flatlaf.testing.LineChartPanel" ) {
+ name: "lineChartPanel"
+ "updateChartDelayed": false
+ auxiliary() {
+ "JavaCodeGenerator.variableLocal": false
+ }
+ }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
+ "value": "cell 0 4 2 1"
+ } )
}, new FormLayoutConstraints( null ) {
"location": new java.awt.Point( 0, 0 )
- "size": new java.awt.Dimension( 415, 350 )
+ "size": new java.awt.Dimension( 625, 625 )
} )
}
}
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
new file mode 100644
index 000000000..22fb02a37
--- /dev/null
+++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.java
@@ -0,0 +1,726 @@
+/*
+ * Copyright 2020 FormDev Software GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.formdev.flatlaf.testing;
+
+import java.awt.Color;
+import java.awt.Component;
+import java.awt.Dimension;
+import java.awt.EventQueue;
+import java.awt.Graphics;
+import java.awt.Point;
+import java.awt.Rectangle;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.stream.Collectors;
+import javax.swing.*;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+import javax.swing.table.AbstractTableModel;
+import javax.swing.tree.*;
+import com.formdev.flatlaf.ui.FlatUIUtils;
+import com.formdev.flatlaf.util.Animator;
+import com.formdev.flatlaf.util.ColorFunctions;
+import com.formdev.flatlaf.util.UIScale;
+import net.miginfocom.swing.*;
+
+/**
+ * @author Karl Tauber
+ */
+public class FlatSmoothScrollingTest
+ extends FlatTestPanel
+{
+ public static void main( String[] args ) {
+ SwingUtilities.invokeLater( () -> {
+ FlatTestFrame frame = FlatTestFrame.create( args, "FlatSmoothScrollingTest" );
+ UIManager.put( "ScrollBar.showButtons", true );
+ frame.showFrame( FlatSmoothScrollingTest::new );
+ } );
+ }
+
+ FlatSmoothScrollingTest() {
+ initComponents();
+
+ initializeDurationAndResolution();
+
+ // allow enabling/disabling smooth scrolling with Alt+S without moving focus to checkbox
+ registerKeyboardAction(
+ e -> {
+ smoothScrollingCheckBox.setSelected( !smoothScrollingCheckBox.isSelected() );
+ smoothScrollingChanged();
+ },
+ KeyStroke.getKeyStroke( "alt " + (char) smoothScrollingCheckBox.getMnemonic() ),
+ JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT );
+
+ listLabel.setIcon( new ColorIcon( Color.red.darker() ) );
+ treeLabel.setIcon( new ColorIcon( Color.blue.darker() ) );
+ tableLabel.setIcon( new ColorIcon( Color.green.darker() ) );
+ textAreaLabel.setIcon( new ColorIcon( Color.magenta.darker() ) );
+ textPaneLabel.setIcon( new ColorIcon( Color.cyan.darker() ) );
+ editorPaneLabel.setIcon( new ColorIcon( Color.orange.darker() ) );
+ customLabel.setIcon( new ColorIcon( Color.pink ) );
+
+ 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( 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( 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( 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( 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( 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( customScrollPane, true, "custom vert", Color.pink ) );
+ customScrollPane.getHorizontalScrollBar().getModel().addChangeListener( new ScrollBarChangeHandler( customScrollPane, false, "custom horz", Color.pink.darker() ) );
+
+ ArrayList
" );
+ for( int i = 0; i < item.length(); i++ ) {
+ char ch = item.charAt( i );
+ if( ch == ' ' )
+ buf.append( " " );
+ else
+ buf.append( ch );
+ }
+ }
+ buf.append( "" );
+ customButton.setText( buf.toString() );
+
+ // line chart
+ lineChartPanel.addMethodHighlight( JViewport.class.getName() + ".setViewPosition", "#0000bb" );
+ lineChartPanel.addMethodHighlight( JViewport.class.getName() + ".scrollRectToVisible", "#00bbbb" );
+ lineChartPanel.addMethodHighlight( JScrollBar.class.getName() + ".setValue", "#00aa00" );
+ lineChartPanel.addMethodHighlight( Animator.class.getName() + ".timingEvent", "#bb0000" );
+ lineChartPanel.addMethodHighlight( JComponent.class.getName() + ".processKeyBinding", "#bb0000" );
+ lineChartPanel.addMethodHighlight( Component.class.getName() + ".processMouseEvent", "#bb0000" );
+ lineChartPanel.addMethodHighlight( Component.class.getName() + ".processMouseWheelEvent", "#bb0000" );
+ lineChartPanel.addMethodHighlight( Component.class.getName() + ".processMouseMotionEvent", "#bb0000" );
+ lineChartPanel.addMethodHighlight( "actionPerformed", "#bbbb00" );
+
+ // request focus for list
+ EventQueue.invokeLater( () -> {
+ EventQueue.invokeLater( () -> {
+ list.requestFocusInWindow();
+ } );
+ } );
+ }
+
+ private void smoothScrollingChanged() {
+ UIManager.put( "ScrollPane.smoothScrolling", smoothScrollingCheckBox.isSelected() );
+ }
+
+ private void initializeDurationAndResolution() {
+ int duration = FlatUIUtils.getUIInt( "ScrollPane.smoothScrolling.duration", 200 );
+ int resolution = FlatUIUtils.getUIInt( "ScrollPane.smoothScrolling.resolution", 10 );
+
+ durationSlider.setValue( duration );
+ resolutionSlider.setValue( resolution );
+
+ updateDurationAndResolutionLabels( duration, resolution );
+ }
+
+ private void durationOrResolutionChanged() {
+ int duration = durationSlider.getValue();
+ int resolution = resolutionSlider.getValue();
+
+ updateDurationAndResolutionLabels( duration, resolution );
+
+ UIManager.put( "ScrollPane.smoothScrolling.duration", duration );
+ UIManager.put( "ScrollPane.smoothScrolling.resolution", resolution );
+
+ // update UI of scroll bars to force re-creation of animator
+ JScrollPane[] scrollPanes = { listScrollPane, treeScrollPane, tableScrollPane,
+ textAreaScrollPane, textPaneScrollPane, editorPaneScrollPane, customScrollPane };
+ for( JScrollPane scrollPane : scrollPanes ) {
+ scrollPane.getVerticalScrollBar().updateUI();
+ scrollPane.getHorizontalScrollBar().updateUI();
+ }
+ }
+
+ private void updateDurationAndResolutionLabels( int duration, int resolution ) {
+ durationValueLabel.setText( duration + " ms" );
+ resolutionValueLabel.setText( resolution + " ms" );
+ }
+
+ private void scrollToChanged() {
+ if( scrollToSlider.getValueIsAdjusting() )
+ return;
+
+ int value = scrollToSlider.getValue();
+ JComponent[] comps = { list, tree, table, textArea, textPane, editorPane, customButton };
+ for( JComponent c : comps ) {
+ int x = (c.getWidth() * value) / 100;
+ int y = (c.getHeight() * value) / 100;
+ c.scrollRectToVisible( new Rectangle( x, y, 1, 1 ) );
+ }
+ }
+
+ private void rowHeaderChanged() {
+ JTable rowHeader = null;
+ if( rowHeaderCheckBox.isSelected() ) {
+ rowHeader = new JTable();
+ rowHeader.setPreferredScrollableViewportSize( new Dimension( UIScale.scale( 50 ), 100 ) );
+ rowHeader.setModel( new AbstractTableModel() {
+ @Override
+ public int getRowCount() {
+ return table.getRowCount();
+ }
+ @Override
+ public int getColumnCount() {
+ return 1;
+ }
+ @Override
+ public Object getValueAt( int rowIndex, int columnIndex ) {
+ char[] chars = new char[10];
+ Arrays.fill( chars, ' ' );
+ int i = rowIndex % 10;
+ if( (rowIndex / 10) % 2 == 0 )
+ chars[i] = (char) ('0' + i);
+ else
+ chars[9 - i] = (char) ('A' + i);
+ return new String( chars );
+ }
+ } );
+ }
+ tableScrollPane.setRowHeaderView( rowHeader );
+ }
+
+ private void showTableGridChanged() {
+ boolean showGrid = showTableGridCheckBox.isSelected();
+ table.setShowHorizontalLines( showGrid );
+ table.setShowVerticalLines( showGrid );
+ table.setIntercellSpacing( showGrid ? new Dimension( 1, 1 ) : new Dimension() );
+ table.setGridColor( Color.gray );
+ }
+
+ private void autoResizeModeChanged() {
+ table.setAutoResizeMode( autoResizeModeCheckBox.isSelected() ? JTable.AUTO_RESIZE_SUBSEQUENT_COLUMNS : JTable.AUTO_RESIZE_OFF );
+ ((AbstractTableModel)table.getModel()).fireTableStructureChanged();
+ }
+
+ @Override
+ public void updateUI() {
+ super.updateUI();
+
+ EventQueue.invokeLater( () -> {
+ showTableGridChanged();
+ } );
+ }
+
+ private void initComponents() {
+ // JFormDesigner - Component initialization - DO NOT MODIFY //GEN-BEGIN:initComponents
+ smoothScrollingCheckBox = new JCheckBox();
+ JPanel hSpacer1 = new JPanel(null);
+ JLabel durationLabel = new JLabel();
+ durationSlider = new JSlider();
+ durationValueLabel = new JLabel();
+ JLabel resolutionLabel = new JLabel();
+ resolutionSlider = new JSlider();
+ resolutionValueLabel = new JLabel();
+ splitPane1 = new JSplitPane();
+ splitPane2 = new JSplitPane();
+ panel1 = new JPanel();
+ listLabel = new JLabel();
+ treeLabel = new JLabel();
+ tableLabel = new JLabel();
+ rowHeaderCheckBox = new JCheckBox();
+ showTableGridCheckBox = new JCheckBox();
+ autoResizeModeCheckBox = new JCheckBox();
+ listScrollPane = new FlatSmoothScrollingTest.DebugScrollPane();
+ list = new JList<>();
+ treeScrollPane = new FlatSmoothScrollingTest.DebugScrollPane();
+ tree = new JTree();
+ tableScrollPane = new FlatSmoothScrollingTest.DebugScrollPane();
+ table = new JTable();
+ scrollToSlider = new JSlider();
+ panel2 = new JPanel();
+ textAreaLabel = new JLabel();
+ textPaneLabel = new JLabel();
+ editorPaneLabel = new JLabel();
+ customLabel = new JLabel();
+ textAreaScrollPane = new FlatSmoothScrollingTest.DebugScrollPane();
+ textArea = new JTextArea();
+ textPaneScrollPane = new FlatSmoothScrollingTest.DebugScrollPane();
+ textPane = new JTextPane();
+ editorPaneScrollPane = new FlatSmoothScrollingTest.DebugScrollPane();
+ editorPane = new JEditorPane();
+ customScrollPane = new FlatSmoothScrollingTest.DebugScrollPane();
+ customButton = new JButton();
+ lineChartPanel = new LineChartPanel();
+
+ //======== this ========
+ setLayout(new MigLayout(
+ "ltr,insets dialog,hidemode 3",
+ // columns
+ "[200,grow,fill]",
+ // rows
+ "[]" +
+ "[grow,fill]"));
+
+ //---- smoothScrollingCheckBox ----
+ smoothScrollingCheckBox.setText("Smooth scrolling");
+ smoothScrollingCheckBox.setSelected(true);
+ smoothScrollingCheckBox.setMnemonic('S');
+ smoothScrollingCheckBox.addActionListener(e -> smoothScrollingChanged());
+ add(smoothScrollingCheckBox, "cell 0 0,alignx left,growx 0");
+ add(hSpacer1, "cell 0 0,growx");
+
+ //---- durationLabel ----
+ durationLabel.setText("Duration:");
+ add(durationLabel, "cell 0 0");
+
+ //---- durationSlider ----
+ durationSlider.setMaximum(5000);
+ durationSlider.setValue(200);
+ durationSlider.setSnapToTicks(true);
+ durationSlider.setMinimum(100);
+ durationSlider.setMinorTickSpacing(50);
+ durationSlider.addChangeListener(e -> durationOrResolutionChanged());
+ add(durationSlider, "cell 0 0");
+
+ //---- durationValueLabel ----
+ durationValueLabel.setText("0000 ms");
+ add(durationValueLabel, "cell 0 0,width 50");
+
+ //---- resolutionLabel ----
+ resolutionLabel.setText("Resolution:");
+ add(resolutionLabel, "cell 0 0");
+
+ //---- resolutionSlider ----
+ resolutionSlider.setMaximum(1000);
+ resolutionSlider.setMinimum(10);
+ resolutionSlider.setValue(10);
+ resolutionSlider.setMinorTickSpacing(10);
+ resolutionSlider.setSnapToTicks(true);
+ resolutionSlider.addChangeListener(e -> durationOrResolutionChanged());
+ add(resolutionSlider, "cell 0 0");
+
+ //---- resolutionValueLabel ----
+ resolutionValueLabel.setText("0000 ms");
+ add(resolutionValueLabel, "cell 0 0,width 50");
+
+ //======== splitPane1 ========
+ {
+ splitPane1.setOrientation(JSplitPane.VERTICAL_SPLIT);
+ splitPane1.setResizeWeight(1.0);
+
+ //======== splitPane2 ========
+ {
+ splitPane2.setOrientation(JSplitPane.VERTICAL_SPLIT);
+ splitPane2.setResizeWeight(0.5);
+
+ //======== panel1 ========
+ {
+ panel1.setLayout(new MigLayout(
+ "ltr,insets 3,hidemode 3",
+ // columns
+ "[200,grow,fill]" +
+ "[200,grow,fill]" +
+ "[200,grow,fill]" +
+ "[200,grow,fill]" +
+ "[fill]",
+ // rows
+ "[]0" +
+ "[200,grow,fill]"));
+
+ //---- listLabel ----
+ listLabel.setText("JList:");
+ listLabel.setHorizontalTextPosition(SwingConstants.LEADING);
+ panel1.add(listLabel, "cell 0 0,aligny top,growy 0");
+
+ //---- treeLabel ----
+ treeLabel.setText("JTree:");
+ treeLabel.setHorizontalTextPosition(SwingConstants.LEADING);
+ panel1.add(treeLabel, "cell 1 0");
+
+ //---- tableLabel ----
+ tableLabel.setText("JTable:");
+ tableLabel.setHorizontalTextPosition(SwingConstants.LEADING);
+ panel1.add(tableLabel, "cell 2 0 2 1");
+
+ //---- rowHeaderCheckBox ----
+ rowHeaderCheckBox.setText("Row header");
+ rowHeaderCheckBox.addActionListener(e -> rowHeaderChanged());
+ panel1.add(rowHeaderCheckBox, "cell 2 0 2 1,alignx right,growx 0");
+
+ //---- showTableGridCheckBox ----
+ showTableGridCheckBox.setText("Show table grid");
+ showTableGridCheckBox.setMnemonic('G');
+ showTableGridCheckBox.addActionListener(e -> showTableGridChanged());
+ panel1.add(showTableGridCheckBox, "cell 2 0 2 1,alignx right,growx 0");
+
+ //---- autoResizeModeCheckBox ----
+ autoResizeModeCheckBox.setText("Auto-resize mode");
+ autoResizeModeCheckBox.setSelected(true);
+ autoResizeModeCheckBox.addActionListener(e -> autoResizeModeChanged());
+ panel1.add(autoResizeModeCheckBox, "cell 2 0 2 1,alignx right,growx 0");
+
+ //======== listScrollPane ========
+ {
+ listScrollPane.setViewportView(list);
+ }
+ panel1.add(listScrollPane, "cell 0 1,growx");
+
+ //======== treeScrollPane ========
+ {
+ treeScrollPane.setViewportView(tree);
+ }
+ panel1.add(treeScrollPane, "cell 1 1");
+
+ //======== tableScrollPane ========
+ {
+ tableScrollPane.setViewportView(table);
+ }
+ panel1.add(tableScrollPane, "cell 2 1 2 1,width 100,height 100");
+
+ //---- scrollToSlider ----
+ scrollToSlider.setOrientation(SwingConstants.VERTICAL);
+ scrollToSlider.setValue(0);
+ scrollToSlider.setInverted(true);
+ scrollToSlider.addChangeListener(e -> scrollToChanged());
+ panel1.add(scrollToSlider, "cell 4 1");
+ }
+ splitPane2.setTopComponent(panel1);
+
+ //======== panel2 ========
+ {
+ panel2.setLayout(new MigLayout(
+ "ltr,insets 3,hidemode 3",
+ // columns
+ "[200,grow,fill]" +
+ "[200,grow,fill]" +
+ "[200,grow,fill]" +
+ "[200,grow,fill]",
+ // rows
+ "[]0" +
+ "[200,grow,fill]"));
+
+ //---- textAreaLabel ----
+ textAreaLabel.setText("JTextArea:");
+ textAreaLabel.setHorizontalTextPosition(SwingConstants.LEADING);
+ panel2.add(textAreaLabel, "cell 0 0");
+
+ //---- textPaneLabel ----
+ textPaneLabel.setText("JTextPane:");
+ textPaneLabel.setHorizontalTextPosition(SwingConstants.LEADING);
+ panel2.add(textPaneLabel, "cell 1 0");
+
+ //---- editorPaneLabel ----
+ editorPaneLabel.setText("JEditorPane:");
+ editorPaneLabel.setHorizontalTextPosition(SwingConstants.LEADING);
+ panel2.add(editorPaneLabel, "cell 2 0");
+
+ //---- customLabel ----
+ customLabel.setText("Custom:");
+ panel2.add(customLabel, "cell 3 0");
+
+ //======== textAreaScrollPane ========
+ {
+ textAreaScrollPane.setViewportView(textArea);
+ }
+ panel2.add(textAreaScrollPane, "cell 0 1");
+
+ //======== textPaneScrollPane ========
+ {
+ textPaneScrollPane.setViewportView(textPane);
+ }
+ panel2.add(textPaneScrollPane, "cell 1 1");
+
+ //======== editorPaneScrollPane ========
+ {
+ editorPaneScrollPane.setViewportView(editorPane);
+ }
+ panel2.add(editorPaneScrollPane, "cell 2 1");
+
+ //======== customScrollPane ========
+ {
+
+ //---- customButton ----
+ customButton.setText("I'm a large button, but do not implement Scrollable interface");
+ customButton.setHorizontalAlignment(SwingConstants.LEADING);
+ customButton.setVerticalAlignment(SwingConstants.TOP);
+ customScrollPane.setViewportView(customButton);
+ }
+ panel2.add(customScrollPane, "cell 3 1");
+ }
+ splitPane2.setBottomComponent(panel2);
+ }
+ splitPane1.setTopComponent(splitPane2);
+
+ //---- lineChartPanel ----
+ lineChartPanel.setLegend1Text("Rectangles: scrollbar values (mouse hover shows stack)");
+ lineChartPanel.setLegend2Text("Dots: disabled blitting mode in JViewport");
+ lineChartPanel.setLegendYValueText("scroll bar value");
+ splitPane1.setBottomComponent(lineChartPanel);
+ }
+ add(splitPane1, "cell 0 1");
+ // JFormDesigner - End of component initialization //GEN-END:initComponents
+ }
+
+ // JFormDesigner - Variables declaration - DO NOT MODIFY //GEN-BEGIN:variables
+ private JCheckBox smoothScrollingCheckBox;
+ private JSlider durationSlider;
+ private JLabel durationValueLabel;
+ private JSlider resolutionSlider;
+ private JLabel resolutionValueLabel;
+ private JSplitPane splitPane1;
+ private JSplitPane splitPane2;
+ private JPanel panel1;
+ private JLabel listLabel;
+ private JLabel treeLabel;
+ private JLabel tableLabel;
+ private JCheckBox rowHeaderCheckBox;
+ private JCheckBox showTableGridCheckBox;
+ private JCheckBox autoResizeModeCheckBox;
+ private FlatSmoothScrollingTest.DebugScrollPane listScrollPane;
+ private JList" );
+ if( data.dotOnly )
+ buf.append( "DOT: " );
+ buf.append( data.name ).append( ' ' ).append( data.ivalue ).append( "
" );
+
+ StackTraceElement[] stackTrace = data.stack.getStackTrace();
+ for( int j = 0; j < stackTrace.length; j++ ) {
+ StackTraceElement stackElement = stackTrace[j];
+ String className = stackElement.getClassName();
+ String methodName = stackElement.getMethodName();
+ String classAndMethod = className + '.' + methodName;
+
+ // ignore methods from this class
+ if( className.startsWith( LineChartPanel.class.getName() ) )
+ continue;
+
+ int repeatCount = 0;
+ for( int k = j + 1; k < stackTrace.length; k++ ) {
+ if( !stackElement.equals( stackTrace[k] ) )
+ break;
+ repeatCount++;
+ }
+ j += repeatCount;
+
+ String highlight = methodHighlightMap.get( classAndMethod );
+ if( highlight == null )
+ highlight = methodHighlightMap.get( className );
+ if( highlight == null )
+ highlight = methodHighlightMap.get( methodName );
+ if( highlight != null )
+ buf.append( "" );
+
+ // append method
+ buf.append( className )
+ .append( "." )
+ .append( methodName )
+ .append( "" );
+ if( highlight != null )
+ buf.append( "" );
+
+ // append source
+ buf.append( " " );
+ if( stackElement.getFileName() != null ) {
+ buf.append( '(' );
+ buf.append( stackElement.getFileName() );
+ if( stackElement.getLineNumber() >= 0 )
+ buf.append( ':' ).append( stackElement.getLineNumber() );
+ buf.append( ')' );
+ } else
+ buf.append( "(Unknown Source)" );
+ buf.append( "" );
+
+ // append repeat count
+ if( repeatCount > 0 )
+ buf.append( " " ).append( repeatCount + 1 ).append( "x" );
+ buf.append( "
" );
+
+ // break at some methods to make stack smaller
+ if( classAndMethod.equals( "java.awt.event.InvocationEvent.dispatch" ) ||
+ classAndMethod.equals( "java.awt.Component.processMouseEvent" ) ||
+ classAndMethod.equals( "java.awt.Component.processMouseWheelEvent" ) ||
+ classAndMethod.equals( "java.awt.Component.processMouseMotionEvent" ) ||
+ classAndMethod.equals( "javax.swing.JComponent.processKeyBinding" ) ||
+ classAndMethod.equals( "com.formdev.flatlaf.util.Animator.timingEvent" ) )
+ break;
+ }
+ buf.append( "..." );
+ }
+
+ if( buf == null )
+ return null;
+
+ buf.append( "" );
+ String toolTip = buf.toString();
+
+ // print to console
+ if( !Objects.equals( toolTip, lastToolTipPrinted ) ) {
+ lastToolTipPrinted = toolTip;
+
+ System.out.println( toolTip
+ .replace( "
", "\n" )
+ .replace( "", "\n---- " )
+ .replace( "
", " ----\n" )
+ .replaceAll( "<[^>]+>", "" ) );
+ }
+
+ return buf.toString();
+ }
+ }
+}
diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/LineChartPanel.jfd b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/LineChartPanel.jfd
new file mode 100644
index 000000000..c81e0a659
--- /dev/null
+++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/LineChartPanel.jfd
@@ -0,0 +1,121 @@
+JFDML JFormDesigner: "8.1.0.0.283" Java: "19.0.2" encoding: "UTF-8"
+
+new FormModel {
+ contentType: "form/swing"
+ root: new FormRoot {
+ add( new FormContainer( "javax.swing.JPanel", new FormLayoutManager( class net.miginfocom.swing.MigLayout ) {
+ "$layoutConstraints": "hidemode 3"
+ "$columnConstraints": "[grow,fill]"
+ "$rowConstraints": "[100:300,grow,fill][]"
+ } ) {
+ name: "this"
+ add( new FormContainer( "javax.swing.JScrollPane", new FormLayoutManager( class javax.swing.JScrollPane ) ) {
+ name: "lineChartScrollPane"
+ add( new FormComponent( "com.formdev.flatlaf.testing.LineChartPanel$LineChart" ) {
+ name: "lineChart"
+ } )
+ }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
+ "value": "cell 0 0"
+ } )
+ add( new FormContainer( "javax.swing.JPanel", new FormLayoutManager( class net.miginfocom.swing.MigLayout ) {
+ "$layoutConstraints": "insets 0,hidemode 3,gapy 0"
+ "$columnConstraints": "[fill]para[fill]"
+ "$rowConstraints": "[][]"
+ } ) {
+ name: "legendPanel"
+ auxiliary() {
+ "JavaCodeGenerator.variableLocal": true
+ }
+ add( new FormComponent( "javax.swing.JLabel" ) {
+ name: "xLabel"
+ "text": "X: time ({0}ms per line)"
+ }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
+ "value": "cell 0 0"
+ } )
+ add( new FormComponent( "javax.swing.JLabel" ) {
+ name: "legend1Label"
+ }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
+ "value": "cell 1 0"
+ } )
+ add( new FormComponent( "javax.swing.JLabel" ) {
+ name: "yLabel"
+ "text": "Y: "
+ auxiliary() {
+ "JavaCodeGenerator.variableLocal": true
+ }
+ }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
+ "value": "cell 0 1,gapx 0 0"
+ } )
+ add( new FormComponent( "javax.swing.JLabel" ) {
+ name: "yValueLabel"
+ "text": "value"
+ }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
+ "value": "cell 0 1,gapx 0 0"
+ } )
+ add( new FormComponent( "javax.swing.JLabel" ) {
+ name: "yLabel2"
+ "text": " (10% per line)"
+ auxiliary() {
+ "JavaCodeGenerator.variableLocal": true
+ }
+ }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
+ "value": "cell 0 1,gapx 0 0"
+ } )
+ add( new FormComponent( "com.jformdesigner.designer.wrapper.HSpacer" ) {
+ name: "hSpacer1"
+ auxiliary() {
+ "JavaCodeGenerator.variableLocal": true
+ }
+ }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
+ "value": "cell 0 1,growx"
+ } )
+ add( new FormComponent( "javax.swing.JLabel" ) {
+ name: "legend2Label"
+ }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
+ "value": "cell 1 1"
+ } )
+ }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
+ "value": "cell 0 1"
+ } )
+ add( new FormComponent( "javax.swing.JLabel" ) {
+ name: "oneSecondWidthLabel"
+ "text": "Scale X:"
+ "displayedMnemonic": 65
+ "labelFor": new FormReference( "oneSecondWidthSlider" )
+ auxiliary() {
+ "JavaCodeGenerator.variableLocal": true
+ }
+ }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
+ "value": "cell 0 1,alignx right,growx 0"
+ } )
+ add( new FormComponent( "javax.swing.JSlider" ) {
+ name: "oneSecondWidthSlider"
+ "minimum": 1000
+ "maximum": 10000
+ addEvent( new FormEvent( "javax.swing.event.ChangeListener", "stateChanged", "oneSecondWidthChanged", false ) )
+ }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
+ "value": "cell 0 1,alignx right,growx 0,wmax 100"
+ } )
+ add( new FormComponent( "javax.swing.JCheckBox" ) {
+ name: "updateChartDelayedCheckBox"
+ "text": "Update chart delayed"
+ "mnemonic": 80
+ "selected": true
+ addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "updateChartDelayedChanged", false ) )
+ }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
+ "value": "cell 0 1,alignx right,growx 0"
+ } )
+ add( new FormComponent( "javax.swing.JButton" ) {
+ name: "clearChartButton"
+ "text": "Clear Chart"
+ "mnemonic": 67
+ addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "clearChart", false ) )
+ }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
+ "value": "cell 0 1,alignx right,growx 0"
+ } )
+ }, new FormLayoutConstraints( null ) {
+ "location": new java.awt.Point( 0, 0 )
+ "size": new java.awt.Dimension( 880, 300 )
+ } )
+ }
+}
diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/contrib/SmoothScrollingTest.java b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/contrib/SmoothScrollingTest.java
new file mode 100644
index 000000000..36abd8d39
--- /dev/null
+++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/contrib/SmoothScrollingTest.java
@@ -0,0 +1,305 @@
+package com.formdev.flatlaf.testing.contrib;
+
+import java.awt.BorderLayout;
+import java.awt.Component;
+import java.awt.Dimension;
+import java.awt.FlowLayout;
+import java.awt.GridLayout;
+import java.awt.Point;
+import java.awt.Rectangle;
+import java.awt.event.ItemEvent;
+
+import javax.swing.BorderFactory;
+import javax.swing.Box;
+import javax.swing.JButton;
+import javax.swing.JCheckBox;
+import javax.swing.JComboBox;
+import javax.swing.JFrame;
+import javax.swing.JLayeredPane;
+import javax.swing.JList;
+import javax.swing.JPanel;
+import javax.swing.JPopupMenu;
+import javax.swing.JRootPane;
+import javax.swing.JScrollPane;
+import javax.swing.JTable;
+import javax.swing.JTree;
+import javax.swing.JViewport;
+import javax.swing.ListCellRenderer;
+import javax.swing.Scrollable;
+import javax.swing.SwingConstants;
+import javax.swing.SwingUtilities;
+import javax.swing.ToolTipManager;
+import javax.swing.UIManager;
+import javax.swing.table.AbstractTableModel;
+import javax.swing.table.TableModel;
+import javax.swing.tree.DefaultMutableTreeNode;
+import javax.swing.tree.DefaultTreeModel;
+import javax.swing.tree.TreeModel;
+
+import com.formdev.flatlaf.FlatClientProperties;
+import com.formdev.flatlaf.FlatLightLaf;
+
+/**
+ * from https://github.com/JFormDesigner/FlatLaf/pull/683#issuecomment-1585667066
+ *
+ * @author Chrriis
+ */
+public class SmoothScrollingTest {
+
+ private static class CustomTree extends JTree {
+
+ public CustomTree() {
+ super(getDefaultTreeModel());
+ }
+
+ protected static TreeModel getDefaultTreeModel() {
+ DefaultMutableTreeNode root = new DefaultMutableTreeNode("JTree");
+ for(int i=0; i<1000; i++) {
+ DefaultMutableTreeNode parent;
+ parent = new DefaultMutableTreeNode("colors-" + i);
+ root.add(parent);
+ parent.add(new DefaultMutableTreeNode("blue"));
+ parent.add(new DefaultMutableTreeNode("violet"));
+ parent.add(new DefaultMutableTreeNode("red"));
+ parent.add(new DefaultMutableTreeNode("yellow"));
+ parent = new DefaultMutableTreeNode("sports-" + i);
+ root.add(parent);
+ parent.add(new DefaultMutableTreeNode("basketball"));
+ parent.add(new DefaultMutableTreeNode("soccer"));
+ parent.add(new DefaultMutableTreeNode("football"));
+ parent.add(new DefaultMutableTreeNode("hockey"));
+ parent = new DefaultMutableTreeNode("food-" + i);
+ root.add(parent);
+ parent.add(new DefaultMutableTreeNode("hot dogs"));
+ parent.add(new DefaultMutableTreeNode("pizza"));
+ parent.add(new DefaultMutableTreeNode("ravioli"));
+ parent.add(new DefaultMutableTreeNode("bananas"));
+ }
+ return new DefaultTreeModel(root);
+ }
+ }
+
+ private static class CustomTable extends JTable {
+ public CustomTable() {
+ super(createDefaultTableModel());
+ setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
+ setCellSelectionEnabled(true);
+ }
+ private static TableModel createDefaultTableModel() {
+ int columnChunkCount = 90;
+ Object[][] data = new Object[1000][3 * columnChunkCount];
+ String[] prefixes = {"", " ", " ", " "};
+ String[] titles = new String[columnChunkCount * 3];
+ for(int j=0; j