1+ package bisq .desktop .common .utils ;
2+
3+ import javafx .event .EventHandler ;
4+ import javafx .geometry .Orientation ;
5+ import javafx .scene .control .Control ;
6+ import javafx .scene .control .ScrollBar ;
7+ import javafx .scene .input .KeyCode ;
8+ import javafx .scene .input .KeyEvent ;
9+ import lombok .extern .slf4j .Slf4j ;
10+ import org .fxmisc .easybind .Subscription ;
11+
12+ import java .util .Objects ;
13+ import java .util .Optional ;
14+
15+ /**
16+ * PageScrollHandler
17+ * <p>
18+ * Utility that installs a key event handler on a JavaFX Control to perform page-wise
19+ * scrolling when the PAGE_UP and PAGE_DOWN keys are pressed. The handler locates the
20+ * control's vertical ScrollBar (if present) and adjusts its value by the ScrollBar's
21+ * block increment, mimicking a page click on the scrollbar trough. The key event is
22+ * consumed only when a scroll action is performed.
23+ * <p>
24+ * Typical usage:
25+ * - Instantiate with a JavaFX Control.
26+ * - Call {@link #subscribe()} to enable the handler.
27+ * - Call {@link #unsubscribe()} to remove the handler.
28+ * <p>
29+ * Note: methods that inspect or modify the scene graph (e.g. locating ScrollBars)
30+ * must be called on the JavaFX Application Thread.
31+ */
32+ @ Slf4j
33+ public class PageScrollHandler implements Subscription {
34+
35+ private final Control control ;
36+ private final EventHandler <KeyEvent > keyEventHandler ;
37+
38+ public PageScrollHandler (Control control ) {
39+ this .control = Objects .requireNonNull (control );
40+ this .keyEventHandler = createKeyEventHandler ();
41+ }
42+
43+ public static Optional <ScrollBar > findScrollBar (Control control , Orientation orientation ) {
44+ return findScrollBar (control , orientation , "VirtualScrollBar" )
45+ .or (() -> findScrollBar (control , orientation , ".scroll-bar" ));
46+ }
47+
48+ public static Optional <ScrollBar > findScrollBar (Control control , Orientation orientation , String selector ) {
49+ if (control .getSkin () == null ) {
50+ log .warn ("Control has no skin; cannot find ScrollBar" );
51+ return Optional .empty ();
52+ }
53+
54+ return control .lookupAll (selector ).stream ()
55+ .filter (node -> node instanceof ScrollBar sb && sb .getOrientation () == orientation )
56+ .map (ScrollBar .class ::cast )
57+ .findFirst ();
58+ }
59+
60+ private EventHandler <KeyEvent > createKeyEventHandler () {
61+ return event -> {
62+ if (event .getCode () == KeyCode .PAGE_UP && blockDecrement ()) {
63+ event .consume ();
64+ } else if (event .getCode () == KeyCode .PAGE_DOWN && blockIncrement ()) {
65+ event .consume ();
66+ }
67+ };
68+ }
69+
70+ public void subscribe () {
71+ unsubscribe ();
72+ control .addEventFilter (KeyEvent .KEY_PRESSED , keyEventHandler );
73+ }
74+
75+ public void unsubscribe () {
76+ control .removeEventFilter (KeyEvent .KEY_PRESSED , keyEventHandler );
77+ }
78+
79+ protected boolean blockDecrement () {
80+ return findScrollBar (control , Orientation .VERTICAL )
81+ .map (this ::adjustScrollBarDecrement )
82+ .orElse (false );
83+ }
84+
85+ protected boolean blockIncrement () {
86+ return findScrollBar (control , Orientation .VERTICAL )
87+ .map (this ::adjustScrollBarIncrement )
88+ .orElse (false );
89+ }
90+
91+ private boolean adjustScrollBarDecrement (ScrollBar vbar ) {
92+ return adjustScrollBar (vbar , -vbar .getBlockIncrement ());
93+ }
94+
95+ private boolean adjustScrollBarIncrement (ScrollBar vbar ) {
96+ return adjustScrollBar (vbar , vbar .getBlockIncrement ());
97+ }
98+
99+ private boolean adjustScrollBar (ScrollBar vbar , double increment ) {
100+ double oldValue = vbar .getValue ();
101+ vbar .adjustValue (oldValue + increment );
102+ double newValue = vbar .getValue ();
103+ log .debug ("ScrollBar adjusted: {}; {} -> {}" , vbar , oldValue , newValue );
104+ return oldValue != newValue ;
105+ }
106+ }
0 commit comments