Skip to content

Commit 7adecd3

Browse files
committed
Have chats respond to PgUp and PgDown
Resolves #2087
1 parent b4bcb13 commit 7adecd3

File tree

2 files changed

+112
-0
lines changed

2 files changed

+112
-0
lines changed
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
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+
}

apps/desktop/desktop/src/main/java/bisq/desktop/main/content/chat/message_container/list/ChatMessagesListView.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import bisq.chat.ChatChannel;
2121
import bisq.chat.ChatMessage;
2222
import bisq.desktop.common.ManagedDuration;
23+
import bisq.desktop.common.utils.PageScrollHandler;
2324
import bisq.desktop.components.controls.Badge;
2425
import bisq.desktop.components.controls.BisqTooltip;
2526
import bisq.desktop.components.list_view.ListViewUtil;
@@ -74,6 +75,7 @@ protected void layoutChildren() {
7475
private final Label placeholderTitle = new Label("");
7576
private final Label placeholderDescription = new Label("");
7677
private final Pane scrollDownBackground;
78+
private final PageScrollHandler pageScrollHandler;
7779
private Optional<ScrollBar> scrollBar = Optional.empty();
7880
private Subscription hasUnreadMessagesPin, showScrolledDownButtonPin;
7981
private Timeline fadeInScrollDownBadgeTimeline;
@@ -93,6 +95,8 @@ public ChatMessagesListView(ChatMessagesListModel model, ChatMessagesListControl
9395
listView.setSelectionModel(new NoSelectionModel<>());
9496
VBox.setVgrow(listView, Priority.ALWAYS);
9597

98+
pageScrollHandler = new PageScrollHandler(listView);
99+
96100
scrollDownBackground = new Pane();
97101
scrollDownBackground.getStyleClass().add("scroll-down-bg");
98102

@@ -171,6 +175,7 @@ protected void onViewAttached() {
171175
model.getLayoutChildrenDone().bind(root.getLayoutChildrenDone());
172176

173177
scrollDownBadge.setOnMouseClicked(e -> controller.onScrollToBottom());
178+
pageScrollHandler.subscribe();
174179
}
175180

176181
@Override
@@ -185,6 +190,7 @@ protected void onViewDetached() {
185190
model.getLayoutChildrenDone().unbind();
186191
hasUnreadMessagesPin.unsubscribe();
187192
showScrolledDownButtonPin.unsubscribe();
193+
pageScrollHandler.unsubscribe();
188194

189195
Tooltip.uninstall(scrollDownBadge, scrollDownTooltip);
190196

0 commit comments

Comments
 (0)