Smooth Scrolling: fixes too slow repeating block (page) scrolling (e.g. hold down PageUp key) for Tree, TextArea, TextPane and EditorPane

This commit is contained in:
Karl Tauber
2023-08-24 22:38:52 +02:00
parent 3628a03c9d
commit 542e7d5f60
6 changed files with 172 additions and 27 deletions

View File

@@ -31,6 +31,7 @@ import javax.swing.UIManager;
import javax.swing.plaf.ComponentUI; import javax.swing.plaf.ComponentUI;
import javax.swing.plaf.basic.BasicEditorPaneUI; import javax.swing.plaf.basic.BasicEditorPaneUI;
import javax.swing.text.Caret; import javax.swing.text.Caret;
import javax.swing.text.DefaultEditorKit;
import javax.swing.text.JTextComponent; import javax.swing.text.JTextComponent;
import com.formdev.flatlaf.FlatClientProperties; import com.formdev.flatlaf.FlatClientProperties;
import com.formdev.flatlaf.ui.FlatStylingSupport.Styleable; import com.formdev.flatlaf.ui.FlatStylingSupport.Styleable;
@@ -145,6 +146,21 @@ public class FlatEditorPaneUI
focusListener = null; 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 @Override
protected Caret createCaret() { protected Caret createCaret() {
return new FlatCaret( null, false ); return new FlatCaret( null, false );
@@ -159,6 +175,11 @@ public class FlatEditorPaneUI
super.propertyChange( e ); super.propertyChange( e );
propertyChange( getComponent(), e, this::installStyle ); 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 ) { static void propertyChange( JTextComponent c, PropertyChangeEvent e, Runnable installStyle ) {

View File

@@ -34,7 +34,6 @@ import javax.swing.JButton;
import javax.swing.JComponent; import javax.swing.JComponent;
import javax.swing.JScrollBar; import javax.swing.JScrollBar;
import javax.swing.JScrollPane; import javax.swing.JScrollPane;
import javax.swing.JViewport;
import javax.swing.SwingUtilities; import javax.swing.SwingUtilities;
import javax.swing.UIManager; import javax.swing.UIManager;
import javax.swing.plaf.ComponentUI; 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 // remember current scrollbar value so that we can start scroll animation from there
int oldValue = scrollbar.getValue(); 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 // if invoked while animation is running, calculation of new value
// should start at the previous target value // should start at the previous target value
if( targetValue != Integer.MIN_VALUE ) if( targetValue != Integer.MIN_VALUE )
@@ -510,32 +510,16 @@ public class FlatScrollBarUI
inRunAndSetValueAnimated = false; 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 boolean inRunAndSetValueAnimated;
private Animator animator; private Animator animator;
private int startValue = Integer.MIN_VALUE; private int startValue = Integer.MIN_VALUE;
private int targetValue = Integer.MIN_VALUE; private int targetValue = Integer.MIN_VALUE;
private boolean useValueIsAdjusting = true; private boolean useValueIsAdjusting = true;
int getTargetValue() {
return targetValue;
}
public void setValueAnimated( int initialValue, int value ) { public void setValueAnimated( int initialValue, int value ) {
// do some check if animation already running // do some check if animation already running
if( animator != null && animator.isRunning() && targetValue != Integer.MIN_VALUE ) { if( animator != null && animator.isRunning() && targetValue != Integer.MIN_VALUE ) {

View File

@@ -17,10 +17,12 @@
package com.formdev.flatlaf.ui; package com.formdev.flatlaf.ui;
import java.awt.Component; import java.awt.Component;
import java.awt.Container;
import java.awt.Graphics; import java.awt.Graphics;
import java.awt.Insets; import java.awt.Insets;
import java.awt.KeyboardFocusManager; import java.awt.KeyboardFocusManager;
import java.awt.Rectangle; import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ContainerEvent; import java.awt.event.ContainerEvent;
import java.awt.event.ContainerListener; import java.awt.event.ContainerListener;
import java.awt.event.FocusEvent; import java.awt.event.FocusEvent;
@@ -31,6 +33,8 @@ import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener; import java.beans.PropertyChangeListener;
import java.util.Map; import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import javax.swing.Action;
import javax.swing.ActionMap;
import javax.swing.BorderFactory; import javax.swing.BorderFactory;
import javax.swing.JButton; import javax.swing.JButton;
import javax.swing.JComponent; import javax.swing.JComponent;
@@ -458,8 +462,8 @@ public class FlatScrollPaneUI
// if the viewport has been scrolled by using JComponent.scrollRectToVisible() // 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 // (e.g. by moving selection), then it is necessary to update the scroll bar values
if( isSmoothScrollingEnabled() ) { if( isSmoothScrollingEnabled() ) {
runAndSyncScrollBarValueAnimated( scrollpane.getVerticalScrollBar(), 0, () -> { runAndSyncScrollBarValueAnimated( scrollpane.getVerticalScrollBar(), 0, false, () -> {
runAndSyncScrollBarValueAnimated( scrollpane.getHorizontalScrollBar(), 1, () -> { runAndSyncScrollBarValueAnimated( scrollpane.getHorizontalScrollBar(), 1, false, () -> {
super.syncScrollPaneWithViewport(); super.syncScrollPaneWithViewport();
} ); } );
} ); } );
@@ -467,8 +471,30 @@ public class FlatScrollPaneUI
super.syncScrollPaneWithViewport(); 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(); r.run();
return; return;
} }
@@ -480,6 +506,10 @@ public class FlatScrollPaneUI
int oldMinimum = sb.getMinimum(); int oldMinimum = sb.getMinimum();
int oldMaximum = sb.getMaximum(); int oldMaximum = sb.getMaximum();
FlatScrollBarUI ui = (FlatScrollBarUI) sb.getUI();
if( useTargetValue && ui.getTargetValue() != Integer.MIN_VALUE )
sb.setValue( ui.getTargetValue() );
r.run(); r.run();
int newValue = sb.getValue(); int newValue = sb.getValue();
@@ -490,7 +520,7 @@ public class FlatScrollPaneUI
sb.getMaximum() == oldMaximum && sb.getMaximum() == oldMaximum &&
sb.getUI() instanceof FlatScrollBarUI ) sb.getUI() instanceof FlatScrollBarUI )
{ {
((FlatScrollBarUI)sb.getUI()).setValueAnimated( oldValue, newValue ); ui.setValueAnimated( oldValue, newValue );
} }
inRunAndSyncValueAnimated[i] = false; inRunAndSyncValueAnimated[i] = false;
@@ -498,6 +528,53 @@ public class FlatScrollPaneUI
private final boolean[] inRunAndSyncValueAnimated = new boolean[2]; 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 ------------------------------------------------------ //---- class Handler ------------------------------------------------------
/** /**
@@ -529,4 +606,34 @@ public class FlatScrollPaneUI
scrollpane.repaint(); 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 );
}
}
} }

View File

@@ -141,6 +141,12 @@ public class FlatTextAreaUI
focusListener = null; focusListener = null;
} }
@Override
protected void installKeyboardActions() {
super.installKeyboardActions();
FlatEditorPaneUI.installKeyboardActions( getComponent() );
}
@Override @Override
protected Caret createCaret() { protected Caret createCaret() {
return new FlatCaret( null, false ); return new FlatCaret( null, false );

View File

@@ -142,6 +142,12 @@ public class FlatTextPaneUI
focusListener = null; focusListener = null;
} }
@Override
protected void installKeyboardActions() {
super.installKeyboardActions();
FlatEditorPaneUI.installKeyboardActions( getComponent() );
}
@Override @Override
protected Caret createCaret() { protected Caret createCaret() {
return new FlatCaret( null, false ); return new FlatCaret( null, false );
@@ -156,6 +162,11 @@ public class FlatTextPaneUI
super.propertyChange( e ); super.propertyChange( e );
FlatEditorPaneUI.propertyChange( getComponent(), e, this::installStyle ); 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 */ /** @since 2 */

View File

@@ -238,6 +238,22 @@ public class FlatTreeUI
oldStyleValues = null; 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 @Override
protected void updateRenderer() { protected void updateRenderer() {
super.updateRenderer(); super.updateRenderer();