1717package com .formdev .flatlaf .ui ;
1818
1919import java .awt .Component ;
20+ import java .awt .Container ;
2021import java .awt .Graphics ;
2122import java .awt .Insets ;
2223import java .awt .KeyboardFocusManager ;
2324import java .awt .Rectangle ;
25+ import java .awt .event .ActionEvent ;
2426import java .awt .event .ContainerEvent ;
2527import java .awt .event .ContainerListener ;
2628import java .awt .event .FocusEvent ;
3133import java .beans .PropertyChangeListener ;
3234import java .util .Map ;
3335import java .util .concurrent .atomic .AtomicBoolean ;
36+ import javax .swing .Action ;
37+ import javax .swing .ActionMap ;
3438import javax .swing .BorderFactory ;
3539import javax .swing .JButton ;
3640import 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}
0 commit comments