Skip to content

Commit 542e7d5

Browse files
committed
Smooth Scrolling: fixes too slow repeating block (page) scrolling (e.g. hold down PageUp key) for Tree, TextArea, TextPane and EditorPane
1 parent 3628a03 commit 542e7d5

6 files changed

Lines changed: 172 additions & 27 deletions

File tree

flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatEditorPaneUI.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import javax.swing.plaf.ComponentUI;
3232
import javax.swing.plaf.basic.BasicEditorPaneUI;
3333
import javax.swing.text.Caret;
34+
import javax.swing.text.DefaultEditorKit;
3435
import javax.swing.text.JTextComponent;
3536
import com.formdev.flatlaf.FlatClientProperties;
3637
import com.formdev.flatlaf.ui.FlatStylingSupport.Styleable;
@@ -145,6 +146,21 @@ protected void uninstallListeners() {
145146
focusListener = null;
146147
}
147148

149+
@Override
150+
protected void installKeyboardActions() {
151+
super.installKeyboardActions();
152+
installKeyboardActions( getComponent() );
153+
}
154+
155+
static void installKeyboardActions( JTextComponent c ) {
156+
FlatScrollPaneUI.installSmoothScrollingDelegateActions( c, false,
157+
/* page-down */ DefaultEditorKit.pageDownAction, // PAGE_DOWN
158+
/* page-up */ DefaultEditorKit.pageUpAction, // PAGE_UP
159+
/* DefaultEditorKit.selectionPageDownAction */ "selection-page-down", // shift PAGE_DOWN
160+
/* DefaultEditorKit.selectionPageUpAction */ "selection-page-up" // shift PAGE_UP
161+
);
162+
}
163+
148164
@Override
149165
protected Caret createCaret() {
150166
return new FlatCaret( null, false );
@@ -159,6 +175,11 @@ protected void propertyChange( PropertyChangeEvent e ) {
159175

160176
super.propertyChange( e );
161177
propertyChange( getComponent(), e, this::installStyle );
178+
179+
// BasicEditorPaneUI.propertyChange() re-applied actions from editor kit,
180+
// which removed our delegate actions
181+
if( "editorKit".equals( propertyName ) )
182+
installKeyboardActions( getComponent() );
162183
}
163184

164185
static void propertyChange( JTextComponent c, PropertyChangeEvent e, Runnable installStyle ) {

flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatScrollBarUI.java

Lines changed: 6 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@
3434
import javax.swing.JComponent;
3535
import javax.swing.JScrollBar;
3636
import javax.swing.JScrollPane;
37-
import javax.swing.JViewport;
3837
import javax.swing.SwingUtilities;
3938
import javax.swing.UIManager;
4039
import javax.swing.plaf.ComponentUI;
@@ -481,7 +480,8 @@ public void runAndSetValueAnimated( Runnable r ) {
481480
// remember current scrollbar value so that we can start scroll animation from there
482481
int oldValue = scrollbar.getValue();
483482

484-
runWithoutBlitting( scrollbar.getParent(), () ->{
483+
// run given runnable, which computes and sets the new scrollbar value
484+
FlatScrollPaneUI.runWithoutBlitting( scrollbar.getParent(), () ->{
485485
// if invoked while animation is running, calculation of new value
486486
// should start at the previous target value
487487
if( targetValue != Integer.MIN_VALUE )
@@ -510,32 +510,16 @@ public void runAndSetValueAnimated( Runnable r ) {
510510
inRunAndSetValueAnimated = false;
511511
}
512512

513-
private void runWithoutBlitting( Container scrollPane, Runnable r ) {
514-
// prevent the viewport to immediately repaint using blitting
515-
JViewport viewport = null;
516-
int oldScrollMode = 0;
517-
if( scrollPane instanceof JScrollPane ) {
518-
viewport = ((JScrollPane) scrollPane).getViewport();
519-
if( viewport != null ) {
520-
oldScrollMode = viewport.getScrollMode();
521-
viewport.setScrollMode( JViewport.BACKINGSTORE_SCROLL_MODE );
522-
}
523-
}
524-
525-
try {
526-
r.run();
527-
} finally {
528-
if( viewport != null )
529-
viewport.setScrollMode( oldScrollMode );
530-
}
531-
}
532-
533513
private boolean inRunAndSetValueAnimated;
534514
private Animator animator;
535515
private int startValue = Integer.MIN_VALUE;
536516
private int targetValue = Integer.MIN_VALUE;
537517
private boolean useValueIsAdjusting = true;
538518

519+
int getTargetValue() {
520+
return targetValue;
521+
}
522+
539523
public void setValueAnimated( int initialValue, int value ) {
540524
// do some check if animation already running
541525
if( animator != null && animator.isRunning() && targetValue != Integer.MIN_VALUE ) {

flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatScrollPaneUI.java

Lines changed: 112 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,12 @@
1717
package com.formdev.flatlaf.ui;
1818

1919
import java.awt.Component;
20+
import java.awt.Container;
2021
import java.awt.Graphics;
2122
import java.awt.Insets;
2223
import java.awt.KeyboardFocusManager;
2324
import java.awt.Rectangle;
25+
import java.awt.event.ActionEvent;
2426
import java.awt.event.ContainerEvent;
2527
import java.awt.event.ContainerListener;
2628
import java.awt.event.FocusEvent;
@@ -31,6 +33,8 @@
3133
import java.beans.PropertyChangeListener;
3234
import java.util.Map;
3335
import java.util.concurrent.atomic.AtomicBoolean;
36+
import javax.swing.Action;
37+
import javax.swing.ActionMap;
3438
import javax.swing.BorderFactory;
3539
import javax.swing.JButton;
3640
import javax.swing.JComponent;
@@ -458,17 +462,39 @@ protected void syncScrollPaneWithViewport() {
458462
// if the viewport has been scrolled by using JComponent.scrollRectToVisible()
459463
// (e.g. by moving selection), then it is necessary to update the scroll bar values
460464
if( isSmoothScrollingEnabled() ) {
461-
runAndSyncScrollBarValueAnimated( scrollpane.getVerticalScrollBar(), 0, () -> {
462-
runAndSyncScrollBarValueAnimated( scrollpane.getHorizontalScrollBar(), 1, () -> {
465+
runAndSyncScrollBarValueAnimated( scrollpane.getVerticalScrollBar(), 0, false, () -> {
466+
runAndSyncScrollBarValueAnimated( scrollpane.getHorizontalScrollBar(), 1, false, () -> {
463467
super.syncScrollPaneWithViewport();
464468
} );
465469
} );
466470
} else
467471
super.syncScrollPaneWithViewport();
468472
}
469473

470-
private void runAndSyncScrollBarValueAnimated( JScrollBar sb, int i, Runnable r ) {
471-
if( inRunAndSyncValueAnimated[i] || sb == null ) {
474+
/**
475+
* Runs the given runnable, if smooth scrolling is enabled, with disabled
476+
* viewport blitting mode and with scroll bar value set to "target" value.
477+
* This is necessary when calculating new view position during animation.
478+
* Otherwise calculation would use wrong view position and (repeating) scrolling
479+
* would be much slower than without smooth scrolling.
480+
*/
481+
private void runWithScrollBarsTargetValues( boolean blittingOnly, Runnable r ) {
482+
if( isSmoothScrollingEnabled() ) {
483+
runWithoutBlitting( scrollpane, () -> {
484+
if( blittingOnly )
485+
r.run();
486+
else {
487+
runAndSyncScrollBarValueAnimated( scrollpane.getVerticalScrollBar(), 0, true, () -> {
488+
runAndSyncScrollBarValueAnimated( scrollpane.getHorizontalScrollBar(), 1, true, r );
489+
} );
490+
}
491+
} );
492+
} else
493+
r.run();
494+
}
495+
496+
private void runAndSyncScrollBarValueAnimated( JScrollBar sb, int i, boolean useTargetValue, Runnable r ) {
497+
if( inRunAndSyncValueAnimated[i] || sb == null || !(sb.getUI() instanceof FlatScrollBarUI) ) {
472498
r.run();
473499
return;
474500
}
@@ -480,6 +506,10 @@ private void runAndSyncScrollBarValueAnimated( JScrollBar sb, int i, Runnable r
480506
int oldMinimum = sb.getMinimum();
481507
int oldMaximum = sb.getMaximum();
482508

509+
FlatScrollBarUI ui = (FlatScrollBarUI) sb.getUI();
510+
if( useTargetValue && ui.getTargetValue() != Integer.MIN_VALUE )
511+
sb.setValue( ui.getTargetValue() );
512+
483513
r.run();
484514

485515
int newValue = sb.getValue();
@@ -490,14 +520,61 @@ private void runAndSyncScrollBarValueAnimated( JScrollBar sb, int i, Runnable r
490520
sb.getMaximum() == oldMaximum &&
491521
sb.getUI() instanceof FlatScrollBarUI )
492522
{
493-
((FlatScrollBarUI)sb.getUI()).setValueAnimated( oldValue, newValue );
523+
ui.setValueAnimated( oldValue, newValue );
494524
}
495525

496526
inRunAndSyncValueAnimated[i] = false;
497527
}
498528

499529
private final boolean[] inRunAndSyncValueAnimated = new boolean[2];
500530

531+
/**
532+
* Runs the given runnable with disabled viewport blitting mode.
533+
* If blitting mode is enabled, the viewport immediately repaints parts of the
534+
* view if the view position is changed via JViewport.setViewPosition().
535+
* This causes scrolling artifacts if smooth scrolling is enabled and the view position
536+
* is "temporary" changed to its new target position, changed back to its old position
537+
* and again moved animated to the target position.
538+
*/
539+
static void runWithoutBlitting( Container scrollPane, Runnable r ) {
540+
// prevent the viewport to immediately repaint using blitting
541+
JViewport viewport = null;
542+
int oldScrollMode = 0;
543+
if( scrollPane instanceof JScrollPane ) {
544+
viewport = ((JScrollPane) scrollPane).getViewport();
545+
if( viewport != null ) {
546+
oldScrollMode = viewport.getScrollMode();
547+
viewport.setScrollMode( JViewport.BACKINGSTORE_SCROLL_MODE );
548+
}
549+
}
550+
551+
try {
552+
r.run();
553+
} finally {
554+
if( viewport != null )
555+
viewport.setScrollMode( oldScrollMode );
556+
}
557+
}
558+
559+
public static void installSmoothScrollingDelegateActions( JComponent c, boolean blittingOnly, String... actionKeys ) {
560+
// get shared action map, used for all components of same type
561+
ActionMap map = SwingUtilities.getUIActionMap( c );
562+
if( map == null )
563+
return;
564+
565+
// install actions, but only if not already installed
566+
for( String actionKey : actionKeys )
567+
installSmoothScrollingDelegateAction( map, blittingOnly, actionKey );
568+
}
569+
570+
private static void installSmoothScrollingDelegateAction( ActionMap map, boolean blittingOnly, String actionKey ) {
571+
Action oldAction = map.get( actionKey );
572+
if( oldAction == null || oldAction instanceof SmoothScrollingDelegateAction )
573+
return; // not found or already installed
574+
575+
map.put( actionKey, new SmoothScrollingDelegateAction( oldAction, blittingOnly ) );
576+
}
577+
501578
//---- class Handler ------------------------------------------------------
502579

503580
/**
@@ -529,4 +606,34 @@ public void focusLost( FocusEvent e ) {
529606
scrollpane.repaint();
530607
}
531608
}
609+
610+
//---- class SmoothScrollingDelegateAction --------------------------------
611+
612+
/**
613+
* Used to run component actions with disabled blitting mode and
614+
* with scroll bar target values.
615+
*/
616+
private static class SmoothScrollingDelegateAction
617+
extends FlatUIAction
618+
{
619+
private final boolean blittingOnly;
620+
621+
private SmoothScrollingDelegateAction( Action delegate, boolean blittingOnly ) {
622+
super( delegate );
623+
this.blittingOnly = blittingOnly;
624+
}
625+
626+
@Override
627+
public void actionPerformed( ActionEvent e ) {
628+
Object source = e.getSource();
629+
JScrollPane scrollPane = (source instanceof Component)
630+
? (JScrollPane) SwingUtilities.getAncestorOfClass( JScrollPane.class, (Component) source )
631+
: null;
632+
if( scrollPane != null && scrollPane.getUI() instanceof FlatScrollPaneUI ) {
633+
((FlatScrollPaneUI)scrollPane.getUI()).runWithScrollBarsTargetValues( blittingOnly,
634+
() -> delegate.actionPerformed( e ) );
635+
} else
636+
delegate.actionPerformed( e );
637+
}
638+
}
532639
}

flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatTextAreaUI.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,12 @@ protected void uninstallListeners() {
141141
focusListener = null;
142142
}
143143

144+
@Override
145+
protected void installKeyboardActions() {
146+
super.installKeyboardActions();
147+
FlatEditorPaneUI.installKeyboardActions( getComponent() );
148+
}
149+
144150
@Override
145151
protected Caret createCaret() {
146152
return new FlatCaret( null, false );

flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatTextPaneUI.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,12 @@ protected void uninstallListeners() {
142142
focusListener = null;
143143
}
144144

145+
@Override
146+
protected void installKeyboardActions() {
147+
super.installKeyboardActions();
148+
FlatEditorPaneUI.installKeyboardActions( getComponent() );
149+
}
150+
145151
@Override
146152
protected Caret createCaret() {
147153
return new FlatCaret( null, false );
@@ -156,6 +162,11 @@ protected void propertyChange( PropertyChangeEvent e ) {
156162

157163
super.propertyChange( e );
158164
FlatEditorPaneUI.propertyChange( getComponent(), e, this::installStyle );
165+
166+
// BasicEditorPaneUI.propertyChange() re-applied actions from editor kit,
167+
// which removed our delegate actions
168+
if( "editorKit".equals( propertyName ) )
169+
FlatEditorPaneUI.installKeyboardActions( getComponent() );
159170
}
160171

161172
/** @since 2 */

flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatTreeUI.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,22 @@ protected void uninstallDefaults() {
238238
oldStyleValues = null;
239239
}
240240

241+
@Override
242+
protected void installKeyboardActions() {
243+
super.installKeyboardActions();
244+
245+
FlatScrollPaneUI.installSmoothScrollingDelegateActions( tree, false,
246+
"scrollDownChangeSelection", // PAGE_DOWN
247+
"scrollUpChangeSelection", // PAGE_UP
248+
"scrollDownChangeLead", // ctrl PAGE_DOWN
249+
"scrollUpChangeLead", // ctrl PAGE_UP
250+
"scrollDownExtendSelection", // shift PAGE_DOWN, shift ctrl PAGE_DOWN
251+
"scrollUpExtendSelection", // shift PAGE_UP, shift ctrl PAGE_UP
252+
"selectNextChangeLead", // ctrl DOWN
253+
"selectPreviousChangeLead" // ctrl UP
254+
);
255+
}
256+
241257
@Override
242258
protected void updateRenderer() {
243259
super.updateRenderer();

0 commit comments

Comments
 (0)