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 2614ddee..925d337b 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.UIManager; 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 @@ public class FlatEditorPaneUI 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 @@ public class FlatEditorPaneUI 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/FlatScrollBarUI.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatScrollBarUI.java index f999e399..7b042e63 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatScrollBarUI.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatScrollBarUI.java @@ -34,7 +34,6 @@ import javax.swing.JButton; import javax.swing.JComponent; import javax.swing.JScrollBar; import javax.swing.JScrollPane; -import javax.swing.JViewport; import javax.swing.SwingUtilities; import javax.swing.UIManager; import javax.swing.plaf.ComponentUI; @@ -481,7 +480,8 @@ public class FlatScrollBarUI // remember current scrollbar value so that we can start scroll animation from there int oldValue = scrollbar.getValue(); - runWithoutBlitting( scrollbar.getParent(), () ->{ + // 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 ) @@ -510,32 +510,16 @@ public class FlatScrollBarUI inRunAndSetValueAnimated = false; } - private void runWithoutBlitting( Container scrollPane, Runnable r ) { - // prevent the viewport to immediately repaint using blitting - JViewport viewport = null; - int oldScrollMode = 0; - if( scrollPane instanceof JScrollPane ) { - viewport = ((JScrollPane) scrollPane).getViewport(); - if( viewport != null ) { - oldScrollMode = viewport.getScrollMode(); - viewport.setScrollMode( JViewport.BACKINGSTORE_SCROLL_MODE ); - } - } - - try { - r.run(); - } finally { - if( viewport != null ) - viewport.setScrollMode( oldScrollMode ); - } - } - private boolean inRunAndSetValueAnimated; private Animator animator; private int startValue = Integer.MIN_VALUE; private int targetValue = Integer.MIN_VALUE; private boolean useValueIsAdjusting = true; + int getTargetValue() { + return targetValue; + } + public void setValueAnimated( int initialValue, int value ) { // do some check if animation already running if( animator != null && animator.isRunning() && targetValue != Integer.MIN_VALUE ) { diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatScrollPaneUI.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatScrollPaneUI.java index c59e1ed2..d4b67c9b 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.PropertyChangeEvent; 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; @@ -458,8 +462,8 @@ public class FlatScrollPaneUI // if the viewport has been scrolled by using JComponent.scrollRectToVisible() // (e.g. by moving selection), then it is necessary to update the scroll bar values if( isSmoothScrollingEnabled() ) { - runAndSyncScrollBarValueAnimated( scrollpane.getVerticalScrollBar(), 0, () -> { - runAndSyncScrollBarValueAnimated( scrollpane.getHorizontalScrollBar(), 1, () -> { + runAndSyncScrollBarValueAnimated( scrollpane.getVerticalScrollBar(), 0, false, () -> { + runAndSyncScrollBarValueAnimated( scrollpane.getHorizontalScrollBar(), 1, false, () -> { super.syncScrollPaneWithViewport(); } ); } ); @@ -467,8 +471,30 @@ public class FlatScrollPaneUI super.syncScrollPaneWithViewport(); } - private void runAndSyncScrollBarValueAnimated( JScrollBar sb, int i, Runnable r ) { - if( inRunAndSyncValueAnimated[i] || sb == null ) { + /** + * 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; } @@ -480,6 +506,10 @@ public class FlatScrollPaneUI 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(); @@ -490,7 +520,7 @@ public class FlatScrollPaneUI sb.getMaximum() == oldMaximum && sb.getUI() instanceof FlatScrollBarUI ) { - ((FlatScrollBarUI)sb.getUI()).setValueAnimated( oldValue, newValue ); + ui.setValueAnimated( oldValue, newValue ); } inRunAndSyncValueAnimated[i] = false; @@ -498,6 +528,53 @@ public class FlatScrollPaneUI 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 = null; + int oldScrollMode = 0; + if( scrollPane instanceof JScrollPane ) { + viewport = ((JScrollPane) scrollPane).getViewport(); + if( viewport != null ) { + oldScrollMode = viewport.getScrollMode(); + viewport.setScrollMode( JViewport.BACKINGSTORE_SCROLL_MODE ); + } + } + + try { + r.run(); + } finally { + if( viewport != null ) + viewport.setScrollMode( oldScrollMode ); + } + } + + 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 ------------------------------------------------------ /** @@ -529,4 +606,34 @@ public class FlatScrollPaneUI 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/FlatTextAreaUI.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatTextAreaUI.java index 6b5cd3f8..3c4d0958 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 @@ public class FlatTextAreaUI 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 1452ebc7..b9986ed9 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 @@ public class FlatTextPaneUI focusListener = null; } + @Override + protected void installKeyboardActions() { + super.installKeyboardActions(); + FlatEditorPaneUI.installKeyboardActions( getComponent() ); + } + @Override protected Caret createCaret() { return new FlatCaret( null, false ); @@ -156,6 +162,11 @@ public class FlatTextPaneUI 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 31d898e2..e3522d24 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 @@ public class FlatTreeUI 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();