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 items = new ArrayList<>(); + for( int i = 0; i < 10; i++ ) { + for( int j = 0; j < 10; j++ ) { + char[] chars = new char[i*10 + j + 1]; + Arrays.fill( chars, ' ' ); + chars[chars.length - 1] = (char) ('0' + j); + if( i >= 5 ) + chars[50 - 1 - ((i-5)*10) - j] = (char) ('0' + j); + items.add( new String( chars ) ); + } + } + + // list model + list.setModel( new AbstractListModel() { + @Override + public int getSize() { + return items.size(); + } + @Override + public String getElementAt( int index ) { + return items.get( index ); + } + } ); + + // tree model + DefaultMutableTreeNode root = new DefaultMutableTreeNode( items.get( 0 ) ); + DefaultMutableTreeNode last = null; + for( int i = 1; i < items.size(); i++ ) { + DefaultMutableTreeNode node = new DefaultMutableTreeNode( items.get( i ) ); + if( i % 5 == 1 ) { + root.add( node ); + last = node; + } else + last.add( node ); + } + tree.setModel( new DefaultTreeModel( root ) ); + for( int row = tree.getRowCount() - 1; row >= 0; row-- ) + tree.expandRow( row ); + + // table model + table.setModel( new AbstractTableModel() { + @Override + public int getRowCount() { + return items.size(); + } + @Override + public int getColumnCount() { + return (table.getAutoResizeMode() == JTable.AUTO_RESIZE_OFF) ? 100 : 2; + } + @Override + public Object getValueAt( int rowIndex, int columnIndex ) { + return items.get( rowIndex ); + } + } ); + + // select some rows to better see smooth scrolling issues + for( int i = 5; i < items.size(); i += 10 ) { + list.addSelectionInterval( i, i ); + tree.addSelectionInterval( i, i ); + table.addRowSelectionInterval( i, i ); + } + + // text components + String longText = ""; + for( int i = 0; i < 100; i++ ) + longText += String.format( "%-5d ", i ); + longText += "100"; + String text = items.stream().collect( Collectors.joining( "\n" ) ) + '\n'; + textArea.setText( longText + '\n' + text ); + textPane.setText( text ); + editorPane.setText( text ); + + // move selection to beginning in text components + textArea.select( 0, 0 ); + textPane.select( 0, 0 ); + editorPane.select( 0, 0 ); + + // custom scrollable + StringBuilder buf = new StringBuilder() + .append( "" ) + .append( customButton.getText() ); + for( String item : items ) { + buf.append( "
" ); + 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 list; + private FlatSmoothScrollingTest.DebugScrollPane treeScrollPane; + private JTree tree; + private FlatSmoothScrollingTest.DebugScrollPane tableScrollPane; + private JTable table; + private JSlider scrollToSlider; + private JPanel panel2; + private JLabel textAreaLabel; + private JLabel textPaneLabel; + private JLabel editorPaneLabel; + private JLabel customLabel; + private FlatSmoothScrollingTest.DebugScrollPane textAreaScrollPane; + private JTextArea textArea; + private FlatSmoothScrollingTest.DebugScrollPane textPaneScrollPane; + private JTextPane textPane; + private FlatSmoothScrollingTest.DebugScrollPane editorPaneScrollPane; + private JEditorPane editorPane; + private FlatSmoothScrollingTest.DebugScrollPane customScrollPane; + private JButton customButton; + private LineChartPanel lineChartPanel; + // JFormDesigner - End of variables declaration //GEN-END:variables + + //---- class ScrollBarChangeHandler --------------------------------------- + + private class ScrollBarChangeHandler + implements ChangeListener + { + private final String name; + private final Color chartColor; // for smooth scrolling + private final Color chartColor2; // for non-smooth scrolling + private int count; + private int lastValue; + private long lastTime; + + ScrollBarChangeHandler( DebugScrollPane scrollPane, boolean vertical, String name, Color chartColor ) { + this.name = name; + this.chartColor = chartColor; + this.chartColor2 = ColorFunctions.lighten( chartColor, 0.1f ); + + // add change listener to viewport that is invoked from JViewport.setViewPosition() + scrollPane.getViewport().addChangeListener( e -> { + JViewport viewport = scrollPane.getViewport(); + Point viewPosition = viewport.getViewPosition(); + + if( (vertical && viewPosition.y != scrollPane.previousViewPosition.y) || + (!vertical && viewPosition.x != scrollPane.previousViewPosition.x) ) + { + // calculate value from view position because scrollbar value is not yet up-to-date + Dimension viewSize = viewport.getViewSize(); + double value = vertical + ? ((double) viewPosition.y) / (viewSize.height - viewport.getHeight()) + : ((double) viewPosition.x) / (viewSize.width - viewport.getWidth()); + int ivalue = vertical ? viewPosition.y : viewPosition.x; + + // add dot to chart if blit scroll mode is disabled + boolean dot = (scrollPane.getViewport().getScrollMode() != JViewport.BLIT_SCROLL_MODE); + + Color color = smoothScrollingCheckBox.isSelected() ? this.chartColor : chartColor2; + if( dot ) + lineChartPanel.addValueWithDot( color, value, ivalue, null, name ); + else + lineChartPanel.addValue( color, value, ivalue, name ); + } + } ); + } + + @Override + public void stateChanged( ChangeEvent e ) { + DefaultBoundedRangeModel m = (DefaultBoundedRangeModel) e.getSource(); + int value = m.getValue(); +/* + double chartValue = (double) (value - m.getMinimum()) / (double) (m.getMaximum() - m.getExtent()); + lineChartPanel.addValue( chartValue, value, false, false, + smoothScrollingCheckBox.isSelected() ? chartColor : chartColor2, name ); +*/ + long t = System.nanoTime() / 1000000; + + System.out.printf( "%s (%d): %4d --> %4d %3d ms %-5b %s%n", + name, ++count, + lastValue, + value, + t - lastTime, + m.getValueIsAdjusting(), + value > lastValue ? "down" : value < lastValue ? "up" : "" ); + + lastValue = value; + lastTime = t; + } + } + + //---- class DebugViewport ------------------------------------------------ + + private static class DebugScrollPane + extends JScrollPane + { + Point previousViewPosition = new Point(); + + @Override + protected JViewport createViewport() { + return new JViewport() { + @Override + public Point getViewPosition() { + Point viewPosition = super.getViewPosition(); +// System.out.println( " viewPosition " + viewPosition.x + "," + viewPosition.y ); + return viewPosition; + } + + @Override + public void setViewPosition( Point p ) { + // remember previous view position + previousViewPosition = getViewPosition(); + + super.setViewPosition( p ); + } + }; + } + } + + //---- class ColorIcon ---------------------------------------------------- + + private static class ColorIcon + implements Icon + { + private final Color color; + + ColorIcon( Color color ) { + this.color = color; + } + + @Override + public void paintIcon( Component c, Graphics g, int x, int y ) { + int width = getIconWidth(); + int height = getIconHeight(); + + g.setColor( color ); + g.fillRect( x, y, width, height ); + } + + @Override + public int getIconWidth() { + return UIScale.scale( 24 ); + } + + @Override + public int getIconHeight() { + return UIScale.scale( 12 ); + } + } +} diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.jfd b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.jfd new file mode 100644 index 000000000..1e582acb9 --- /dev/null +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.jfd @@ -0,0 +1,269 @@ +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( "com.formdev.flatlaf.testing.FlatTestPanel", new FormLayoutManager( class net.miginfocom.swing.MigLayout ) { + "$layoutConstraints": "ltr,insets dialog,hidemode 3" + "$columnConstraints": "[200,grow,fill]" + "$rowConstraints": "[][grow,fill]" + } ) { + name: "this" + add( new FormComponent( "javax.swing.JCheckBox" ) { + name: "smoothScrollingCheckBox" + "text": "Smooth scrolling" + "selected": true + "mnemonic": 83 + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "smoothScrollingChanged", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 0,alignx left,growx 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 0,growx" + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "durationLabel" + "text": "Duration:" + auxiliary() { + "JavaCodeGenerator.variableLocal": true + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 0" + } ) + add( new FormComponent( "javax.swing.JSlider" ) { + name: "durationSlider" + "maximum": 5000 + "value": 200 + "snapToTicks": true + "minimum": 100 + "minorTickSpacing": 50 + addEvent( new FormEvent( "javax.swing.event.ChangeListener", "stateChanged", "durationOrResolutionChanged", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 0" + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "durationValueLabel" + "text": "0000 ms" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 0,width 50" + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "resolutionLabel" + "text": "Resolution:" + auxiliary() { + "JavaCodeGenerator.variableLocal": true + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 0" + } ) + add( new FormComponent( "javax.swing.JSlider" ) { + name: "resolutionSlider" + "maximum": 1000 + "minimum": 10 + "value": 10 + "minorTickSpacing": 10 + "snapToTicks": true + addEvent( new FormEvent( "javax.swing.event.ChangeListener", "stateChanged", "durationOrResolutionChanged", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 0" + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "resolutionValueLabel" + "text": "0000 ms" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 0,width 50" + } ) + add( new FormContainer( "javax.swing.JSplitPane", new FormLayoutManager( class javax.swing.JSplitPane ) ) { + name: "splitPane1" + "orientation": 0 + "resizeWeight": 1.0 + add( new FormContainer( "javax.swing.JSplitPane", new FormLayoutManager( class javax.swing.JSplitPane ) ) { + name: "splitPane2" + "orientation": 0 + "resizeWeight": 0.5 + add( new FormContainer( "javax.swing.JPanel", new FormLayoutManager( class net.miginfocom.swing.MigLayout ) { + "$columnConstraints": "[200,grow,fill][200,grow,fill][200,grow,fill][200,grow,fill][fill]" + "$rowConstraints": "[]0[200,grow,fill]" + "$layoutConstraints": "ltr,insets 3,hidemode 3" + } ) { + name: "panel1" + add( new FormComponent( "javax.swing.JLabel" ) { + name: "listLabel" + "text": "JList:" + "horizontalTextPosition": 10 + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 0,aligny top,growy 0" + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "treeLabel" + "text": "JTree:" + "horizontalTextPosition": 10 + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 0" + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "tableLabel" + "text": "JTable:" + "horizontalTextPosition": 10 + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 2 0 2 1" + } ) + add( new FormComponent( "javax.swing.JCheckBox" ) { + name: "rowHeaderCheckBox" + "text": "Row header" + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "rowHeaderChanged", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 2 0 2 1,alignx right,growx 0" + } ) + add( new FormComponent( "javax.swing.JCheckBox" ) { + name: "showTableGridCheckBox" + "text": "Show table grid" + "mnemonic": 71 + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "showTableGridChanged", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 2 0 2 1,alignx right,growx 0" + } ) + add( new FormComponent( "javax.swing.JCheckBox" ) { + name: "autoResizeModeCheckBox" + "text": "Auto-resize mode" + "selected": true + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "autoResizeModeChanged", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 2 0 2 1,alignx right,growx 0" + } ) + add( new FormContainer( "com.formdev.flatlaf.testing.FlatSmoothScrollingTest$DebugScrollPane", new FormLayoutManager( class javax.swing.JScrollPane ) ) { + name: "listScrollPane" + add( new FormComponent( "javax.swing.JList" ) { + name: "list" + auxiliary() { + "JavaCodeGenerator.typeParameters": "String" + "JavaCodeGenerator.variableLocal": false + } + } ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 1,growx" + } ) + add( new FormContainer( "com.formdev.flatlaf.testing.FlatSmoothScrollingTest$DebugScrollPane", new FormLayoutManager( class javax.swing.JScrollPane ) ) { + name: "treeScrollPane" + add( new FormComponent( "javax.swing.JTree" ) { + name: "tree" + } ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 1" + } ) + add( new FormContainer( "com.formdev.flatlaf.testing.FlatSmoothScrollingTest$DebugScrollPane", new FormLayoutManager( class javax.swing.JScrollPane ) ) { + name: "tableScrollPane" + add( new FormComponent( "javax.swing.JTable" ) { + name: "table" + } ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 2 1 2 1,width 100,height 100" + } ) + add( new FormComponent( "javax.swing.JSlider" ) { + name: "scrollToSlider" + "orientation": 1 + "value": 0 + "inverted": true + addEvent( new FormEvent( "javax.swing.event.ChangeListener", "stateChanged", "scrollToChanged", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 4 1" + } ) + }, new FormLayoutConstraints( class java.lang.String ) { + "value": "left" + } ) + add( new FormContainer( "javax.swing.JPanel", new FormLayoutManager( class net.miginfocom.swing.MigLayout ) { + "$columnConstraints": "[200,grow,fill][200,grow,fill][200,grow,fill][200,grow,fill]" + "$rowConstraints": "[]0[200,grow,fill]" + "$layoutConstraints": "ltr,insets 3,hidemode 3" + } ) { + name: "panel2" + add( new FormComponent( "javax.swing.JLabel" ) { + name: "textAreaLabel" + "text": "JTextArea:" + "horizontalTextPosition": 10 + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 0" + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "textPaneLabel" + "text": "JTextPane:" + "horizontalTextPosition": 10 + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 0" + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "editorPaneLabel" + "text": "JEditorPane:" + "horizontalTextPosition": 10 + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 2 0" + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "customLabel" + "text": "Custom:" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 3 0" + } ) + add( new FormContainer( "com.formdev.flatlaf.testing.FlatSmoothScrollingTest$DebugScrollPane", new FormLayoutManager( class javax.swing.JScrollPane ) ) { + name: "textAreaScrollPane" + add( new FormComponent( "javax.swing.JTextArea" ) { + name: "textArea" + } ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 1" + } ) + add( new FormContainer( "com.formdev.flatlaf.testing.FlatSmoothScrollingTest$DebugScrollPane", new FormLayoutManager( class javax.swing.JScrollPane ) ) { + name: "textPaneScrollPane" + add( new FormComponent( "javax.swing.JTextPane" ) { + name: "textPane" + } ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 1" + } ) + add( new FormContainer( "com.formdev.flatlaf.testing.FlatSmoothScrollingTest$DebugScrollPane", new FormLayoutManager( class javax.swing.JScrollPane ) ) { + name: "editorPaneScrollPane" + add( new FormComponent( "javax.swing.JEditorPane" ) { + name: "editorPane" + } ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 2 1" + } ) + add( new FormContainer( "com.formdev.flatlaf.testing.FlatSmoothScrollingTest$DebugScrollPane", new FormLayoutManager( class javax.swing.JScrollPane ) ) { + name: "customScrollPane" + add( new FormComponent( "javax.swing.JButton" ) { + name: "customButton" + "text": "I'm a large button, but do not implement Scrollable interface" + "horizontalAlignment": 10 + "verticalAlignment": 1 + } ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 3 1" + } ) + }, new FormLayoutConstraints( class java.lang.String ) { + "value": "right" + } ) + }, new FormLayoutConstraints( class java.lang.String ) { + "value": "left" + } ) + add( new FormComponent( "com.formdev.flatlaf.testing.LineChartPanel" ) { + name: "lineChartPanel" + "legend1Text": "Rectangles: scrollbar values (mouse hover shows stack)" + "legend2Text": "Dots: disabled blitting mode in JViewport" + "legendYValueText": "scroll bar value" + }, new FormLayoutConstraints( class java.lang.String ) { + "value": "right" + } ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 1" + } ) + }, new FormLayoutConstraints( null ) { + "location": new java.awt.Point( 0, 0 ) + "size": new java.awt.Dimension( 875, 715 ) + } ) + } +} diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/LineChartPanel.java b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/LineChartPanel.java new file mode 100644 index 000000000..827b4e674 --- /dev/null +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/LineChartPanel.java @@ -0,0 +1,708 @@ +/* +/* + * 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 + * + * 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.Dimension; +import java.awt.EventQueue; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.Point; +import java.awt.Rectangle; +import java.awt.event.HierarchyEvent; +import java.awt.event.MouseEvent; +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import javax.swing.*; +import com.formdev.flatlaf.FlatClientProperties; +import com.formdev.flatlaf.FlatLaf; +import com.formdev.flatlaf.ui.FlatUIUtils; +import com.formdev.flatlaf.util.ColorFunctions; +import com.formdev.flatlaf.util.HSLColor; +import com.formdev.flatlaf.util.HiDPIUtils; +import com.formdev.flatlaf.util.UIScale; +import net.miginfocom.swing.*; + +/** + * @author Karl Tauber + */ +class LineChartPanel + extends JPanel +{ + LineChartPanel() { + initComponents(); + + lineChartScrollPane.putClientProperty( FlatClientProperties.SCROLL_PANE_SMOOTH_SCROLLING, false ); + + oneSecondWidthChanged(); + updateChartDelayedChanged(); + + // clear chart on startup + addHierarchyListener( e -> { + if( (e.getChangeFlags() & HierarchyEvent.SHOWING_CHANGED) != 0 && isShowing() ) + EventQueue.invokeLater( this::clearChart ); + } ); + + // show chart tooltips immediately and forever + ToolTipManager.sharedInstance().setInitialDelay( 0 ); + ToolTipManager.sharedInstance().setDismissDelay( Integer.MAX_VALUE ); + } + + @Override + public void addNotify() { + super.addNotify(); + + // allow clearing chart with Alt+C without moving focus to button + getRootPane().registerKeyboardAction( + e -> clearChart(), + KeyStroke.getKeyStroke( "alt " + (char) clearChartButton.getMnemonic() ), + JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT ); + } + + public String getLegendYValueText() { + return yValueLabel.getText(); + } + + public void setLegendYValueText( String s ) { + yValueLabel.setText( s ); + } + + public String getLegend1Text() { + return legend1Label.getText(); + } + + public void setLegend1Text( String s ) { + legend1Label.setText( s ); + } + + public String getLegend2Text() { + return legend2Label.getText(); + } + + public void setLegend2Text( String s ) { + legend2Label.setText( s ); + } + + public boolean isUpdateChartDelayed() { + return updateChartDelayedCheckBox.isSelected(); + } + + public void setUpdateChartDelayed( boolean updateChartDelayed ) { + updateChartDelayedCheckBox.setSelected( updateChartDelayed ); + } + + void addValue( Color chartColor, double value, int ivalue, String name ) { + lineChart.addValue( chartColor, value, ivalue, null, false, name ); + } + + void addValueWithDot( Color chartColor, double value, int ivalue, Color dotColor, String name ) { + if( dotColor == null ) + dotColor = chartColor; + lineChart.addValue( chartColor, value, ivalue, dotColor, false, name ); + } + + void addDot( Color chartColor, double value, int ivalue, Color dotColor, String name ) { + if( dotColor == null ) + dotColor = chartColor; + lineChart.addValue( chartColor, value, ivalue, dotColor, true, name ); + } + + void addMethodHighlight( String classAndMethod, String highlightColor ) { + lineChart.methodHighlightMap.put( classAndMethod, highlightColor ); + } + + private void oneSecondWidthChanged() { + int oneSecondWidth = oneSecondWidthSlider.getValue(); + int msPerLineX = + oneSecondWidth <= 2000 ? 100 : + oneSecondWidth <= 4000 ? 50 : + oneSecondWidth <= 8000 ? 25 : + 10; + + lineChart.setOneSecondWidth( oneSecondWidth ); + lineChart.setMsPerLineX( msPerLineX ); + lineChart.revalidate(); + lineChart.repaint(); + + if( xLabelText == null ) + xLabelText = xLabel.getText(); + xLabel.setText( MessageFormat.format( xLabelText, msPerLineX ) ); + } + private String xLabelText; + + private void updateChartDelayedChanged() { + lineChart.setUpdateDelayed( updateChartDelayedCheckBox.isSelected() ); + } + + private void clearChart() { + lineChart.clear(); + } + + private void initComponents() { + // JFormDesigner - Component initialization - DO NOT MODIFY //GEN-BEGIN:initComponents @formatter:off + lineChartScrollPane = new JScrollPane(); + lineChart = new LineChartPanel.LineChart(); + JPanel legendPanel = new JPanel(); + xLabel = new JLabel(); + legend1Label = new JLabel(); + JLabel yLabel = new JLabel(); + yValueLabel = new JLabel(); + JLabel yLabel2 = new JLabel(); + JPanel hSpacer1 = new JPanel(null); + legend2Label = new JLabel(); + JLabel oneSecondWidthLabel = new JLabel(); + oneSecondWidthSlider = new JSlider(); + updateChartDelayedCheckBox = new JCheckBox(); + clearChartButton = new JButton(); + + //======== this ======== + setLayout(new MigLayout( + "hidemode 3", + // columns + "[grow,fill]", + // rows + "[100:300,grow,fill]" + + "[]")); + + //======== lineChartScrollPane ======== + { + lineChartScrollPane.setViewportView(lineChart); + } + add(lineChartScrollPane, "cell 0 0"); + + //======== legendPanel ======== + { + legendPanel.setLayout(new MigLayout( + "insets 0,hidemode 3,gapy 0", + // columns + "[fill]para" + + "[fill]", + // rows + "[]" + + "[]")); + + //---- xLabel ---- + xLabel.setText("X: time ({0}ms per line)"); + legendPanel.add(xLabel, "cell 0 0"); + legendPanel.add(legend1Label, "cell 1 0"); + + //---- yLabel ---- + yLabel.setText("Y: "); + legendPanel.add(yLabel, "cell 0 1,gapx 0 0"); + + //---- yValueLabel ---- + yValueLabel.setText("value"); + legendPanel.add(yValueLabel, "cell 0 1,gapx 0 0"); + + //---- yLabel2 ---- + yLabel2.setText(" (10% per line)"); + legendPanel.add(yLabel2, "cell 0 1,gapx 0 0"); + legendPanel.add(hSpacer1, "cell 0 1,growx"); + legendPanel.add(legend2Label, "cell 1 1"); + } + add(legendPanel, "cell 0 1"); + + //---- oneSecondWidthLabel ---- + oneSecondWidthLabel.setText("Scale X:"); + oneSecondWidthLabel.setDisplayedMnemonic('A'); + oneSecondWidthLabel.setLabelFor(oneSecondWidthSlider); + add(oneSecondWidthLabel, "cell 0 1,alignx right,growx 0"); + + //---- oneSecondWidthSlider ---- + oneSecondWidthSlider.setMinimum(1000); + oneSecondWidthSlider.setMaximum(10000); + oneSecondWidthSlider.addChangeListener(e -> oneSecondWidthChanged()); + add(oneSecondWidthSlider, "cell 0 1,alignx right,growx 0,wmax 100"); + + //---- updateChartDelayedCheckBox ---- + updateChartDelayedCheckBox.setText("Update chart delayed"); + updateChartDelayedCheckBox.setMnemonic('P'); + updateChartDelayedCheckBox.setSelected(true); + updateChartDelayedCheckBox.addActionListener(e -> updateChartDelayedChanged()); + add(updateChartDelayedCheckBox, "cell 0 1,alignx right,growx 0"); + + //---- clearChartButton ---- + clearChartButton.setText("Clear Chart"); + clearChartButton.setMnemonic('C'); + clearChartButton.addActionListener(e -> clearChart()); + add(clearChartButton, "cell 0 1,alignx right,growx 0"); + // JFormDesigner - End of component initialization //GEN-END:initComponents @formatter:on + } + + // JFormDesigner - Variables declaration - DO NOT MODIFY //GEN-BEGIN:variables @formatter:off + private JScrollPane lineChartScrollPane; + private LineChartPanel.LineChart lineChart; + private JLabel xLabel; + private JLabel legend1Label; + private JLabel yValueLabel; + private JLabel legend2Label; + private JSlider oneSecondWidthSlider; + private JCheckBox updateChartDelayedCheckBox; + private JButton clearChartButton; + // JFormDesigner - End of variables declaration //GEN-END:variables @formatter:on + + + //---- class LineChart ---------------------------------------------------- + + private static class LineChart + extends JComponent + implements Scrollable + { + private static final int UPDATE_DELAY_MS = 20; + + private static final int NEW_SEQUENCE_TIME_LAG = 500; + private static final int NEW_SEQUENCE_GAP = 100; + private static final int HIT_OFFSET = 4; + + private int oneSecondWidth = 1000; + private int msPerLineX = 200; + private final HashMap methodHighlightMap = new HashMap<>(); + + private static class Data { + final double value; + final int ivalue; + final Color dotColor; + final boolean dotOnly; + final long time; // in milliseconds + final String name; + final Exception stack; + + Data( double value, int ivalue, Color dotColor, boolean dotOnly, long time, String name, Exception stack ) { + this.value = value; + this.ivalue = ivalue; + this.dotColor = dotColor; + this.dotOnly = dotOnly; + this.time = time; + this.name = name; + this.stack = stack; + } + + @Override + public String toString() { + // for debugging + return "value=" + value + ", ivalue=" + ivalue + ", dotColor=" + dotColor + + ", dotOnly=" + dotOnly + ", time=" + time + ", name=" + name; + } + } + + private final Map> color2dataMap = new HashMap<>(); + private final Timer repaintTime; + private Color lastUsedChartColor; + private boolean updateDelayed; + + private final List lastPoints = new ArrayList<>(); + private final List lastDatas = new ArrayList<>(); + private double lastSystemScaleFactor = 1; + private String lastToolTipPrinted; + + LineChart() { + repaintTime = new Timer( UPDATE_DELAY_MS, e -> repaintAndRevalidate() ); + repaintTime.setRepeats( false ); + + ToolTipManager.sharedInstance().registerComponent( this ); + } + + void addValue( Color chartColor, double value, int ivalue, Color dotColor, boolean dotOnly, String name ) { + List chartData = color2dataMap.computeIfAbsent( chartColor, k -> new ArrayList<>() ); + chartData.add( new Data( value, ivalue, dotColor, dotOnly, System.nanoTime() / 1000000, name, new Exception() ) ); + + lastUsedChartColor = chartColor; + + if( updateDelayed ) { + repaintTime.stop(); + repaintTime.start(); + } else + repaintAndRevalidate(); + } + + void clear() { + color2dataMap.clear(); + lastUsedChartColor = null; + + repaint(); + revalidate(); + } + + void setUpdateDelayed( boolean updateDelayed ) { + this.updateDelayed = updateDelayed; + } + + void setOneSecondWidth( int oneSecondWidth ) { + this.oneSecondWidth = oneSecondWidth; + } + + void setMsPerLineX( int msPerLineX ) { + this.msPerLineX = msPerLineX; + } + + private void repaintAndRevalidate() { + repaint(); + revalidate(); + + // scroll horizontally + if( lastUsedChartColor != null ) { + // compute chart width of last used color and start of last sequence + int[] lastSeqX = new int[1]; + int cw = chartWidth( color2dataMap.get( lastUsedChartColor ), lastSeqX ); + + // scroll to end of last sequence (of last used color) + int lastSeqWidth = cw - lastSeqX[0]; + int width = Math.min( lastSeqWidth, getParent().getWidth() ); + int x = cw - width; + scrollRectToVisible( new Rectangle( x, 0, width, getHeight() ) ); + } + } + + @Override + protected void paintComponent( Graphics g ) { + Graphics g2 = g.create(); + try { + HiDPIUtils.paintAtScale1x( (Graphics2D) g2, this, this::paintImpl ); + } finally { + g2.dispose(); + } + } + + private void paintImpl( Graphics2D g, int x, int y, int width, int height, double scaleFactor ) { + FlatUIUtils.setRenderingHints( g ); + + int oneSecondWidth = (int) (UIScale.scale( this.oneSecondWidth ) * scaleFactor); + int seqGapWidth = (int) (NEW_SEQUENCE_GAP * scaleFactor); + int hitOffset = (int) Math.round( UIScale.scale( HIT_OFFSET ) * scaleFactor ); + + Color lineColor = FlatUIUtils.getUIColor( "Component.borderColor", Color.lightGray ); + Color lineColor2 = FlatLaf.isLafDark() + ? new HSLColor( lineColor ).adjustTone( 30 ) + : new HSLColor( lineColor ).adjustShade( 30 ); + + g.translate( x, y ); + + // fill background + g.setColor( UIManager.getColor( "Table.background" ) ); + g.fillRect( x, y, width, height ); + + // paint horizontal lines + for( int i = 1; i < 10; i++ ) { + int hy = (height * i) / 10; + g.setColor( (i != 5) ? lineColor : lineColor2 ); + g.drawLine( 0, hy, width, hy ); + } + + // paint vertical lines + int perLineXWidth = Math.round( (oneSecondWidth / 1000f) * msPerLineX ); + for( int i = 1, xv = perLineXWidth; xv < width; xv += perLineXWidth, i++ ) { + g.setColor( (i % 5 != 0) ? lineColor : lineColor2 ); + g.drawLine( xv, 0, xv, height ); + } + + lastPoints.clear(); + lastDatas.clear(); + lastSystemScaleFactor = scaleFactor; + + // paint lines + for( Map.Entry> e : color2dataMap.entrySet() ) { + List chartData = e.getValue(); + Color chartColor = e.getKey(); + if( FlatLaf.isLafDark() ) + chartColor = new HSLColor( chartColor ).adjustTone( 50 ); + Color temporaryValueColor = ColorFunctions.fade( chartColor, FlatLaf.isLafDark() ? 0.7f : 0.3f ); + Color dataPointColor = ColorFunctions.fade( chartColor, FlatLaf.isLafDark() ? 0.6f : 0.2f ); + + // sequence start time and x coordinate + long seqTime = 0; + int seqX = 0; + + // "previous" data point time, x/y coordinates and count + long ptime = 0; + int px = 0; + int py = 0; + int pcount = 0; + + boolean first = true; + boolean isTemporaryValue = false; + int lastTemporaryValueIndex = -1; + + int size = chartData.size(); + for( int i = 0; i < size; i++ ) { + Data data = chartData.get( i ); + + boolean newSeq = (data.time > ptime + NEW_SEQUENCE_TIME_LAG); + ptime = data.time; + + if( newSeq ) { + // paint short horizontal line for previous sequence that has only one data point + if( !first && pcount == 0 ) { + g.setColor( chartColor ); + g.drawLine( px, py, px + (int) Math.round( UIScale.scale( 8 ) * scaleFactor ), py ); + } + + // start new sequence + seqTime = data.time; + seqX = !first ? px + seqGapWidth : 0; + px = seqX; + pcount = 0; + first = false; + isTemporaryValue = false; + } + + // x/y coordinates of current data point + int dy = (int) ((height - 1) * data.value); + int dx = (int) (seqX + (((data.time - seqTime) / 1000.) * oneSecondWidth)); + + // paint rectangle to indicate data point + g.setColor( dataPointColor ); + g.drawRect( dx - hitOffset, dy - hitOffset, hitOffset * 2, hitOffset * 2 ); + + // remember data point for tooltip + lastPoints.add( new Point( dx, dy ) ); + lastDatas.add( data ); + + if( data.dotColor != null ) { + int s1 = (int) Math.round( UIScale.scale( 1 ) * scaleFactor ); + int s3 = (int) Math.round( UIScale.scale( 3 ) * scaleFactor ); + g.setColor( data.dotColor ); + g.fillRect( dx - s1, dy - s1, s3, s3 ); + if( data.dotOnly ) + continue; + } + + if( !newSeq ) { + if( isTemporaryValue && i > lastTemporaryValueIndex ) + isTemporaryValue = false; + + g.setColor( isTemporaryValue ? temporaryValueColor : chartColor ); + + // line in sequence + g.drawLine( px, py, dx, dy ); + + px = dx; + pcount++; + + // check next data points for "temporary" value(s) + if( !isTemporaryValue ) { + // one or two values between two equal values are considered "temporary", + // which means that they are the target value for the following scroll animation + int stage = 0; + for( int j = i + 1; j < size && stage <= 2 && !isTemporaryValue; j++ ) { + Data nextData = chartData.get( j ); + if( nextData.dotOnly ) + continue; // ignore dots + + // check whether next data point is within 10 milliseconds + if( nextData.time > data.time + 10 ) + break; + + if( stage >= 1 && stage <= 2 && nextData.value == data.value ) { + isTemporaryValue = true; + lastTemporaryValueIndex = j; + } + stage++; + } + } + } + + py = dy; + } + } + } + + private int chartWidth() { + int width = 0; + for( List chartData : color2dataMap.values() ) + width = Math.max( width, chartWidth( chartData, null ) ); + return width; + } + + private int chartWidth( List chartData, int[] lastSeqX ) { + long seqTime = 0; + int seqX = 0; + long ptime = 0; + int px = 0; + + int size = chartData.size(); + for( int i = 0; i < size; i++ ) { + Data data = chartData.get( i ); + + if( data.time > ptime + NEW_SEQUENCE_TIME_LAG ) { + // start new sequence + seqTime = data.time; + seqX = (i > 0) ? px + NEW_SEQUENCE_GAP : 0; + px = seqX; + } else { + // line in sequence + int dx = (int) (seqX + (((data.time - seqTime) / 1000.) * UIScale.scale( oneSecondWidth ))); + px = dx; + } + + ptime = data.time; + } + + if( lastSeqX != null ) + lastSeqX[0] = seqX; + + return px; + } + + @Override + public Dimension getPreferredSize() { + return new Dimension( chartWidth(), 200 ); + } + + @Override + public Dimension getPreferredScrollableViewportSize() { + return new Dimension( chartWidth(), 200 ); + } + + @Override + public int getScrollableUnitIncrement( Rectangle visibleRect, int orientation, int direction ) { + return UIScale.scale( oneSecondWidth ); + } + + @Override + public int getScrollableBlockIncrement( Rectangle visibleRect, int orientation, int direction ) { + JViewport viewport = (JViewport) SwingUtilities.getAncestorOfClass( JViewport.class, this ); + return (viewport != null) ? viewport.getWidth() : 200; + } + + @Override + public boolean getScrollableTracksViewportWidth() { + JViewport viewport = (JViewport) SwingUtilities.getAncestorOfClass( JViewport.class, this ); + return (viewport != null) ? viewport.getWidth() > chartWidth() : true; + } + + @Override + public boolean getScrollableTracksViewportHeight() { + return true; + } + + @Override + public String getToolTipText( MouseEvent e ) { + int x = (int) Math.round( e.getX() * lastSystemScaleFactor ); + int y = (int) Math.round( e.getY() * lastSystemScaleFactor ); + int hitOffset = (int) Math.round( UIScale.scale( HIT_OFFSET ) * lastSystemScaleFactor ); + StringBuilder buf = null; + + int pointsCount = lastPoints.size(); + for( int i = 0; i < pointsCount; i++ ) { + Point pt = lastPoints.get( i ); + + // check X/Y coordinates + if( x < pt.x - hitOffset || x > pt.x + hitOffset || + y < pt.y - hitOffset || y > pt.y + hitOffset ) + continue; + + if( buf == null ) { + buf = new StringBuilder( 5000 ); + buf.append( "" ); + } + + Data data = lastDatas.get( i ); + buf.append( "

" ); + 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 getColumnClass(int columnIndex) { + return columnIndex % 3 == 2 ? Boolean.class : String.class; + } + }; + return tableModel; + } + } + + private static class ScrollableCustomPane extends JPanel implements Scrollable { + public ScrollableCustomPane() { + super(new GridLayout(100, 0)); + for(int i=0; i<10000; i++) { + add(new JButton("Button " + (i + 1))); + } + } + @Override + public Dimension getPreferredScrollableViewportSize() { + return getPreferredSize(); + } + @Override + public boolean getScrollableTracksViewportWidth() { + return false; + } + @Override + public boolean getScrollableTracksViewportHeight() { + return false; + } + @Override + public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction) { + Dimension referenceSize = getComponent(0).getSize(); + switch(orientation) { + case SwingConstants.VERTICAL: return referenceSize.height; + case SwingConstants.HORIZONTAL: return referenceSize.width; + } + return 20; + } + @Override + public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation, int direction) { + Dimension referenceSize = getComponent(0).getSize(); + switch(orientation) { + case SwingConstants.VERTICAL: return referenceSize.height * 10; + case SwingConstants.HORIZONTAL: return referenceSize.width * 5; + } + return 100; + } + } + + public static void main(String[] args) throws Exception { + UIManager.setLookAndFeel(new FlatLightLaf()); +// UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); + UIManager.getDefaults().put("ScrollBar.showButtons", true); + ToolTipManager.sharedInstance().setDismissDelay(Integer.MAX_VALUE); + JFrame frame = new JFrame(); + frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + JPanel contentPane = new JPanel(new BorderLayout()); + contentPane.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); + JCheckBox smoothCheckBox = new JCheckBox("Smooth", true); + JComboBox scrollModeComboBox = new JComboBox<>(new Integer[] {JViewport.BLIT_SCROLL_MODE, JViewport.BACKINGSTORE_SCROLL_MODE, JViewport.SIMPLE_SCROLL_MODE}); + JCheckBox blitBlockCheckBox = new JCheckBox("Prevent Blit"); + @SuppressWarnings( "rawtypes" ) + ListCellRenderer defaultComboRenderer = scrollModeComboBox.getRenderer(); + scrollModeComboBox.setRenderer(new ListCellRenderer() { + @SuppressWarnings( "unchecked" ) + @Override + public Component getListCellRendererComponent(JList list, Integer value, int index, boolean isSelected, boolean cellHasFocus) { + String sValue = null; + switch(value) { + case JViewport.BLIT_SCROLL_MODE: sValue = "Blit"; break; + case JViewport.BACKINGSTORE_SCROLL_MODE: sValue = "Backing Store"; break; + case JViewport.SIMPLE_SCROLL_MODE: sValue = "Simple"; break; + } + return defaultComboRenderer.getListCellRendererComponent(list, sValue, index, isSelected, cellHasFocus); + } + }); + JPanel northBar = new JPanel(); + JButton lightPopupButton = new JButton("Light Popup"); + lightPopupButton.addActionListener(e -> { + JPopupMenu popupMenu = new JPopupMenu(); + popupMenu.setLightWeightPopupEnabled(true); + popupMenu.putClientProperty(FlatClientProperties.POPUP_BORDER_CORNER_RADIUS, 0); + JTable table = new CustomTable(); + table.setColumnSelectionInterval(1, 1); + table.setRowSelectionInterval(28, 28); + JScrollPane tableScrollPane = new JScrollPane(table); + tableScrollPane.setPreferredSize(new Dimension(400, 600)); + tableScrollPane.putClientProperty(FlatClientProperties.SCROLL_PANE_SMOOTH_SCROLLING, !smoothCheckBox.isSelected()? Boolean.FALSE: null); + tableScrollPane.getViewport().setScrollMode((Integer)scrollModeComboBox.getSelectedItem()); + JRootPane popupRootPane = new JRootPane(); + popupRootPane.getContentPane().add(tableScrollPane); + popupMenu.add(popupRootPane); + popupMenu.show(lightPopupButton, 0, lightPopupButton.getHeight()); + }); + northBar.add(lightPopupButton); + JButton heavyPopupButton = new JButton("Heavy Popup"); + heavyPopupButton.addActionListener(e -> { + JPopupMenu popupMenu = new JPopupMenu(); + popupMenu.setLightWeightPopupEnabled(false); + JTable table = new CustomTable(); + table.setColumnSelectionInterval(1, 1); + table.setRowSelectionInterval(28, 28); + JScrollPane tableScrollPane = new JScrollPane(table); + tableScrollPane.setPreferredSize(new Dimension(400, 600)); + tableScrollPane.putClientProperty(FlatClientProperties.SCROLL_PANE_SMOOTH_SCROLLING, !smoothCheckBox.isSelected()? Boolean.FALSE: null); + tableScrollPane.getViewport().setScrollMode((Integer)scrollModeComboBox.getSelectedItem()); + JRootPane popupRootPane = new JRootPane(); + popupRootPane.getContentPane().add(tableScrollPane); + popupMenu.add(popupRootPane); + popupMenu.show(heavyPopupButton, 0, heavyPopupButton.getHeight()); + }); + northBar.add(heavyPopupButton); + contentPane.add(northBar, BorderLayout.NORTH); + JPanel centerPane = new JPanel(new GridLayout(1, 0)); + JTree tree = new CustomTree(); + JScrollPane treeScrollPane = new JScrollPane(tree); + centerPane.add(treeScrollPane); + for(int i=tree.getRowCount()-1; i>=0; i--) { + tree.expandRow(i); + } + tree.setSelectionRow(28); + JTable table = new CustomTable(); + table.setColumnSelectionInterval(1, 1); + table.setRowSelectionInterval(28, 28); + JScrollPane tableScrollPane = new JScrollPane(table); + centerPane.add(tableScrollPane); + ScrollableCustomPane scrollableCustomPane = new ScrollableCustomPane(); + JScrollPane customPaneScrollPane = new JScrollPane(scrollableCustomPane); + centerPane.add(customPaneScrollPane); + contentPane.add(centerPane, BorderLayout.CENTER); + JPanel southBar = new JPanel(new FlowLayout()); + smoothCheckBox.addItemListener(e -> { + boolean isSmooth = e.getStateChange() == ItemEvent.SELECTED; + treeScrollPane.putClientProperty(FlatClientProperties.SCROLL_PANE_SMOOTH_SCROLLING, !isSmooth? Boolean.FALSE: null); + tableScrollPane.putClientProperty(FlatClientProperties.SCROLL_PANE_SMOOTH_SCROLLING, !isSmooth? Boolean.FALSE: null); + customPaneScrollPane.putClientProperty(FlatClientProperties.SCROLL_PANE_SMOOTH_SCROLLING, !isSmooth? Boolean.FALSE: null); + }); + southBar.add(smoothCheckBox); + southBar.add(Box.createHorizontalStrut(30)); + JButton scrollButton = new JButton("Scroll rect"); + scrollButton.addActionListener(e -> { + treeScrollPane.getViewport().setViewPosition(new Point(9, Integer.MAX_VALUE / 2)); + tableScrollPane.getViewport().setViewPosition(new Point(9, Integer.MAX_VALUE / 2)); + customPaneScrollPane.getViewport().setViewPosition(new Point(9, Integer.MAX_VALUE / 2)); + }); + southBar.add(scrollButton); + southBar.add(Box.createHorizontalStrut(30)); + scrollModeComboBox.addItemListener(e -> { + SwingUtilities.invokeLater(() -> { + int scrollMode = (Integer)scrollModeComboBox.getSelectedItem(); + treeScrollPane.getViewport().setScrollMode(scrollMode); + tableScrollPane.getViewport().setScrollMode(scrollMode); + customPaneScrollPane.getViewport().setScrollMode(scrollMode); + }); + }); + southBar.add(scrollModeComboBox); + southBar.add(Box.createHorizontalStrut(30)); + JButton xButton1 = new JButton("Blit Blocker"); + xButton1.setBounds(20, 400, xButton1.getPreferredSize().width, xButton1.getPreferredSize().height); + JButton xButton2 = new JButton("Blit Blocker"); + xButton2.setBounds(600, 400, xButton2.getPreferredSize().width, xButton2.getPreferredSize().height); + JButton xButton3 = new JButton("Blit Blocker"); + xButton3.setBounds(800, 400, xButton3.getPreferredSize().width, xButton3.getPreferredSize().height); + blitBlockCheckBox.addItemListener(e -> { + boolean isBlockingBlit = e.getStateChange() == ItemEvent.SELECTED; + JLayeredPane layeredPane = frame.getLayeredPane(); + if(isBlockingBlit) { + layeredPane.add(xButton1); + layeredPane.add(xButton2); + layeredPane.add(xButton3); + } else { + layeredPane.remove(xButton1); + layeredPane.remove(xButton2); + layeredPane.remove(xButton3); + } + layeredPane.revalidate(); + layeredPane.repaint(); + }); + southBar.add(blitBlockCheckBox); + contentPane.add(southBar, BorderLayout.SOUTH); + frame.getContentPane().add(contentPane); + frame.setSize(1200, 800); + frame.setLocationByPlatform(true); + frame.setVisible(true); + } + +}