Skip to content

Commit c791cd0

Browse files
committed
Smooth scrolling implementation
1 parent c953ff8 commit c791cd0

File tree

7 files changed

+629
-15
lines changed

7 files changed

+629
-15
lines changed

flatlaf-core/src/main/java/com/formdev/flatlaf/FlatLaf.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ public abstract class FlatLaf
112112
private PopupFactory oldPopupFactory;
113113
private MnemonicHandler mnemonicHandler;
114114
private boolean subMenuUsabilityHelperInstalled;
115+
private boolean smoothScrollingHelperInstalled;
115116

116117
private Consumer<UIDefaults> postInitialization;
117118
private List<Function<Object, Object>> uiDefaultsGetters;
@@ -271,6 +272,9 @@ public void initialize() {
271272

272273
// install submenu usability helper
273274
subMenuUsabilityHelperInstalled = SubMenuUsabilityHelper.install();
275+
276+
// install smooth scrolling helper
277+
smoothScrollingHelperInstalled = SmoothScrollingHelper.install();
274278

275279
// listen to desktop property changes to update UI if system font or scaling changes
276280
if( SystemInfo.isWindows ) {
@@ -364,6 +368,12 @@ public void uninitialize() {
364368
subMenuUsabilityHelperInstalled = false;
365369
}
366370

371+
// uninstall smooth scrolling helper
372+
if( smoothScrollingHelperInstalled ) {
373+
SmoothScrollingHelper.uninstall();
374+
smoothScrollingHelperInstalled = false;
375+
}
376+
367377
// restore default link color
368378
new HTMLEditorKit().getStyleSheet().addRule( "a, address { color: blue; }" );
369379
postInitialization = null;

flatlaf-core/src/main/java/com/formdev/flatlaf/FlatSystemProperties.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,14 @@ public interface FlatSystemProperties
132132
*/
133133
String ANIMATION = "flatlaf.animation";
134134

135+
/**
136+
* Specifies whether smooth scrolling is enabled.
137+
* <p>
138+
* <strong>Allowed Values</strong> {@code false} and {@code true}<br>
139+
* <strong>Default</strong> {@code true}
140+
*/
141+
String SMOOTH_SCROLLING = "flatlaf.smoothScrolling";
142+
135143
/**
136144
* Specifies whether vertical text position is corrected when UI is scaled on HiDPI screens.
137145
* <p>
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
/*
2+
* Christopher Deckers ([email protected])
3+
* http://www.nextencia.net
4+
*
5+
* See the file "readme.txt" for information on usage and redistribution of
6+
* this file, and for a DISCLAIMER OF ALL WARRANTIES.
7+
*/
8+
package com.formdev.flatlaf;
9+
10+
import java.awt.AWTEvent;
11+
import java.awt.Component;
12+
import java.awt.Container;
13+
import java.awt.Rectangle;
14+
import java.awt.Toolkit;
15+
import java.awt.event.AWTEventListener;
16+
import java.awt.event.ComponentEvent;
17+
import java.awt.event.KeyEvent;
18+
import java.awt.event.MouseEvent;
19+
import java.util.Collections;
20+
import java.util.IdentityHashMap;
21+
import java.util.Set;
22+
import javax.swing.JComponent;
23+
import javax.swing.JScrollBar;
24+
import javax.swing.JScrollPane;
25+
import javax.swing.JViewport;
26+
import javax.swing.RepaintManager;
27+
import javax.swing.SwingUtilities;
28+
import com.formdev.flatlaf.util.Animator;
29+
30+
/**
31+
* @author Christopher Deckers
32+
*/
33+
public class SmoothScrollingHelper implements AWTEventListener
34+
{
35+
36+
private static SmoothScrollingHelper instance;
37+
38+
static synchronized boolean install() {
39+
if( instance != null )
40+
return false;
41+
instance = new SmoothScrollingHelper();
42+
long eventMask = AWTEvent.MOUSE_EVENT_MASK | AWTEvent.MOUSE_MOTION_EVENT_MASK | AWTEvent.MOUSE_WHEEL_EVENT_MASK | AWTEvent.KEY_EVENT_MASK;
43+
Toolkit.getDefaultToolkit().addAWTEventListener(instance, eventMask);
44+
return true;
45+
}
46+
47+
static synchronized void uninstall() {
48+
if( instance == null )
49+
return;
50+
Toolkit.getDefaultToolkit().removeAWTEventListener(instance);
51+
instance = null;
52+
}
53+
54+
@Override
55+
public void eventDispatched( AWTEvent event ) {
56+
if( event instanceof ComponentEvent && ((ComponentEvent)event).getComponent() instanceof JScrollBar ) {
57+
// Do not disconnect blit scroll mode when e.g. dragging the scroll bar.
58+
return;
59+
}
60+
if( Animator.useAnimation() && FlatSystemProperties.getBoolean( FlatSystemProperties.SMOOTH_SCROLLING, true ) ) {
61+
boolean isHoldingScrollModeBlocked = false;
62+
switch(event.getID()) {
63+
case MouseEvent.MOUSE_MOVED:
64+
case MouseEvent.MOUSE_ENTERED:
65+
case MouseEvent.MOUSE_EXITED:
66+
if(isBlitScrollModeBlocked) {
67+
int modifiersEx = ((MouseEvent)event).getModifiersEx();
68+
if((modifiersEx & (MouseEvent.BUTTON1_DOWN_MASK | MouseEvent.BUTTON2_DOWN_MASK | MouseEvent.BUTTON3_DOWN_MASK)) == 0) {
69+
// If the scroll mode was blocked for dragging, let's release if we receive a non-drag-related event (i.e. drag is done).
70+
for( JViewport viewport: viewportSet ) {
71+
setBlitScrollModeBlocked( viewport, false );
72+
setInSmoothScrolling( viewport, false );
73+
}
74+
isBlitScrollModeBlocked = false;
75+
}
76+
}
77+
break;
78+
case MouseEvent.MOUSE_PRESSED:
79+
case MouseEvent.MOUSE_DRAGGED:
80+
isHoldingScrollModeBlocked = true;
81+
// Fall through
82+
case MouseEvent.MOUSE_RELEASED:
83+
case MouseEvent.MOUSE_CLICKED:
84+
case MouseEvent.MOUSE_WHEEL:
85+
case KeyEvent.KEY_PRESSED:
86+
case KeyEvent.KEY_RELEASED:
87+
case KeyEvent.KEY_TYPED:
88+
boolean isBlitScrollModeBlocked_ = isBlitScrollModeBlocked;
89+
if(!isBlitScrollModeBlocked_) {
90+
Component c = ((ComponentEvent)event).getComponent();
91+
for( JViewport viewport: viewportSet ) {
92+
Container scrollPane = viewport.getParent();
93+
if(scrollPane == c || scrollPane.isAncestorOf( c )) {
94+
setInSmoothScrolling( viewport, true );
95+
}
96+
setBlitScrollModeBlocked( viewport, true );
97+
}
98+
isBlitScrollModeBlocked = true;
99+
}
100+
if( !isHoldingScrollModeBlocked && ( !isBlitScrollModeBlocked_ || event.getID() == MouseEvent.MOUSE_RELEASED )) {
101+
SwingUtilities.invokeLater( () -> {
102+
for( JViewport viewport: viewportSet ) {
103+
setBlitScrollModeBlocked( viewport, false );
104+
setInSmoothScrolling( viewport, false );
105+
}
106+
isBlitScrollModeBlocked = false;
107+
} );
108+
}
109+
break;
110+
}
111+
}
112+
}
113+
114+
private boolean isBlitScrollModeBlocked;
115+
116+
public static synchronized void setBlitScrollModeBlocked( JViewport viewport, boolean isBlocked ) {
117+
if( instance == null )
118+
return;
119+
String clientPropertyName = "_flatlaf.originalScrollMode";
120+
if( isBlocked ) {
121+
int scrollMode = viewport.getScrollMode();
122+
if( scrollMode == JViewport.BLIT_SCROLL_MODE ) {
123+
viewport.putClientProperty( clientPropertyName , scrollMode );
124+
viewport.setScrollMode( JViewport.SIMPLE_SCROLL_MODE );
125+
}
126+
} else {
127+
Integer scrollMode = (Integer)viewport.getClientProperty( clientPropertyName );
128+
if( scrollMode != null ) {
129+
viewport.setScrollMode( scrollMode );
130+
viewport.putClientProperty( clientPropertyName , null );
131+
}
132+
}
133+
}
134+
135+
public static void setScrollBarValueWithOptionalRepaint(JViewport viewport, JScrollBar scrollbar, int value) {
136+
Container viewportParent = null;
137+
Rectangle dirtyRegion = null;
138+
viewportParent = viewport == null? null: viewport.getParent();
139+
dirtyRegion = viewportParent instanceof JComponent? RepaintManager.currentManager( viewport ).getDirtyRegion((JComponent)viewportParent ): null;
140+
int scrollMode = viewport.getScrollMode();
141+
if(scrollMode == JViewport.BLIT_SCROLL_MODE) {
142+
viewport.setScrollMode( JViewport.SIMPLE_SCROLL_MODE );
143+
}
144+
scrollbar.setValue( value );
145+
if(scrollMode == JViewport.BLIT_SCROLL_MODE) {
146+
viewport.setScrollMode( JViewport.BLIT_SCROLL_MODE );
147+
}
148+
if(dirtyRegion != null && dirtyRegion.width == 0 && dirtyRegion.height == 0) {
149+
// There was no dirty region. Let's restore that state for blit scroll mode to work.
150+
RepaintManager.currentManager( viewport ).markCompletelyClean( (JComponent)viewportParent );
151+
}
152+
}
153+
154+
public static synchronized boolean isInBlockedBlitScrollMode( JViewport viewport ) {
155+
if( instance == null )
156+
return false;
157+
String clientPropertyName = "_flatlaf.originalScrollMode";
158+
return viewport.getClientProperty( clientPropertyName ) != null && viewport.getScrollMode() == JViewport.SIMPLE_SCROLL_MODE;
159+
}
160+
161+
public static synchronized void allowBlitScrollModeTemporarily( JViewport viewport, boolean isAllowed ) {
162+
if( instance == null )
163+
return;
164+
// When mouse is dragged, blit scroll mode is deactivated so that drag timers that provoke more scrolling do not repaint at wrong position.
165+
// The FlatLaf animator re-activates the blit scroll mode just for its timed operation.
166+
String clientPropertyName = "_flatlaf.originalScrollMode";
167+
if(viewport.getClientProperty( clientPropertyName ) != null) {
168+
if(isAllowed) {
169+
viewport.setScrollMode( JViewport.BLIT_SCROLL_MODE );
170+
} else {
171+
viewport.setScrollMode( JViewport.SIMPLE_SCROLL_MODE );
172+
}
173+
}
174+
}
175+
176+
public static synchronized boolean isInSmoothScrolling( JViewport viewport ) {
177+
if( instance == null )
178+
return false;
179+
String clientPropertyName = "_flatlaf.inSmoothScrolling";
180+
return viewport != null && viewport.getClientProperty( clientPropertyName ) != null;
181+
}
182+
183+
private static synchronized void setInSmoothScrolling( JViewport viewport, boolean isInSmoothScrolling ) {
184+
if( instance == null )
185+
return;
186+
String clientPropertyName = "_flatlaf.inSmoothScrolling";
187+
viewport.putClientProperty( clientPropertyName , isInSmoothScrolling? Boolean.TRUE: null);
188+
}
189+
190+
private static Set<JViewport> viewportSet = Collections.newSetFromMap(new IdentityHashMap<JViewport, Boolean>());
191+
192+
public static synchronized void registerViewport( JViewport viewport ) {
193+
if( instance == null )
194+
return;
195+
if(viewport.getParent() instanceof JScrollPane) {
196+
viewportSet.add( viewport );
197+
}
198+
}
199+
200+
public static synchronized void unregisterViewport( JViewport viewport ) {
201+
if( instance == null )
202+
return;
203+
if( instance.isBlitScrollModeBlocked ) {
204+
instance.setBlitScrollModeBlocked( viewport, false );
205+
}
206+
viewportSet.remove( viewport );
207+
}
208+
209+
}

0 commit comments

Comments
 (0)