Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Auto-scroll with middle mouse button #890

Merged
merged 3 commits into from
Jan 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,11 +1,21 @@
package software.coley.recaf.ui.control;

import jakarta.annotation.Nonnull;
import javafx.animation.AnimationTimer;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.scene.Cursor;
import javafx.scene.control.ScrollBar;
import javafx.scene.input.MouseButton;
import javafx.scene.layout.Region;
import org.fxmisc.flowless.Virtualized;
import org.fxmisc.flowless.VirtualizedScrollPane;
import org.reactfx.value.Var;
import software.coley.collections.Unchecked;
import software.coley.recaf.util.NodeEvents;
import software.coley.recaf.util.ReflectUtil;

/**
* Wrapper for {@link VirtualizedScrollPane} to properly expose properties with JavaFX's property types instead of
Expand All @@ -17,36 +27,144 @@
* @author Matt Coley
*/
public class VirtualizedScrollPaneWrapper<V extends Region & Virtualized> extends VirtualizedScrollPane<V> {
private final SimpleDoubleProperty xScroll = new SimpleDoubleProperty(0);
private final SimpleDoubleProperty yScroll = new SimpleDoubleProperty(0);
private static final double AUTO_SCROLL_MULTIPLIER = 0.1;
private static final double AUTO_SCROLL_BUFFER_PX = 5;
private final DoubleProperty xScrollProperty = new SimpleDoubleProperty(0);
private final DoubleProperty yScrollProperty = new SimpleDoubleProperty(0);
private final BooleanProperty canAutoScroll = new SimpleBooleanProperty(true);
private final ScrollBar horizontalScrollbar;
private final ScrollBar verticalScrollbar;
private Cursor preAutoScrollCursor;
private boolean isAutoScrolling;
private double autoScrollStartY;
private double autoScrollCurrentY;
private final AnimationTimer autoScrollTimer = new AnimationTimer() {
@Override
public void handle(long now) {
updateAutoScroll();
}
};

/**
* @param content
* Virtualized content.
*/
public VirtualizedScrollPaneWrapper(V content) {
super(content);

horizontalScrollbar = Unchecked.get(() -> ReflectUtil.quietGet(this, VirtualizedScrollPane.class.getDeclaredField("hbar")));
verticalScrollbar = Unchecked.get(() -> ReflectUtil.quietGet(this, VirtualizedScrollPane.class.getDeclaredField("vbar")));

setup();
}

private void setup() {
xScroll.bind(estimatedScrollXProperty());
yScroll.bind(estimatedScrollYProperty());
xScrollProperty.bind(estimatedScrollXProperty());
yScrollProperty.bind(estimatedScrollYProperty());

// Handle middle mouse press to start auto-scrolling.
// - Press initiates the auto-scroll
// - Drag changes the auto-scroll speed
// - Release stops the auto-scroll
NodeEvents.addMousePressHandler(getContent(), e -> {
if (canAutoScroll.get() && e.getButton() == MouseButton.MIDDLE) {
preAutoScrollCursor = getContent().getCursor();
autoScrollStartY = e.getScreenY();
autoScrollCurrentY = autoScrollStartY;
}
});
NodeEvents.addMouseReleaseHandler(getContent(), e -> {
if (e.getButton() == MouseButton.MIDDLE && isAutoScrolling()) {
getContent().setCursor(preAutoScrollCursor);
autoScrollTimer.stop();
autoScrollCurrentY = -1;
isAutoScrolling = false;
}
});
NodeEvents.addMouseDraggedHandler(getContent(), e -> {
if (e.getButton() == MouseButton.MIDDLE) {
autoScrollCurrentY = e.getScreenY();

// Only begin the auto-scroll after the user has moved a couple of pixels away.
//
// We do this because the 'content' node may have middle click behavior similar
// to how a browser opens/closes tabs when using the middle mouse button on links.
// By not initiating until we're sure the user intends to move around via auto-scroll
// we don't mess with the UX of the existing behavior in the 'content' node.
if (!isAutoScrolling && Math.abs(autoScrollCurrentY - autoScrollStartY) > AUTO_SCROLL_BUFFER_PX) {
isAutoScrolling = true;
autoScrollTimer.start();
getContent().setCursor(Cursor.V_RESIZE);
}
}
});
}

private void updateAutoScroll() {
double deltaY = autoScrollCurrentY - autoScrollStartY;

// Get current scroll values
double value = verticalScrollbar.getValue();
double min = verticalScrollbar.getMin();
double max = verticalScrollbar.getMax();

// Calculate scroll amount based on viewport size
double viewportHeight = getHeight();
double scrollAmount = (deltaY * AUTO_SCROLL_MULTIPLIER);
if (Math.abs(scrollAmount) > 0.1) {
// Calculate new scroll position
double newValue = value + scrollAmount;
newValue = Math.max(min, Math.min(max, newValue));

// Update scroll position
verticalScrollbar.setValue(newValue);
}
}

/**
* @return {@code true} when this scroll pane is auto-scrolling.
*/
public boolean isAutoScrolling() {
return isAutoScrolling;
}

/**
* @return Horizontal scrollbar.
*/
@Nonnull
public ScrollBar getHorizontalScrollbar() {
return horizontalScrollbar;
}

/**
* @return Vertical scrollbar.
*/
@Nonnull
public ScrollBar getVerticalScrollbar() {
return verticalScrollbar;
}

/**
* @return Horizontal scroll property.
*/
@Nonnull
public SimpleDoubleProperty horizontalScrollProperty() {
return xScroll;
public DoubleProperty horizontalScrollProperty() {
return xScrollProperty;
}

/**
* @return Vertical scroll property.
*/
@Nonnull
public SimpleDoubleProperty verticalScrollProperty() {
return yScroll;
public DoubleProperty verticalScrollProperty() {
return yScrollProperty;
}

/**
* @return Can auto-scroll property.
*/
@Nonnull
public BooleanProperty canAutoScrollProperty() {
return canAutoScroll;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,13 @@
import javafx.scene.text.Text;
import org.fxmisc.flowless.Cell;
import org.fxmisc.flowless.VirtualFlow;
import org.fxmisc.flowless.VirtualizedScrollPane;
import org.fxmisc.richtext.CodeArea;
import org.fxmisc.richtext.GenericStyledArea;
import org.fxmisc.richtext.model.*;
import org.fxmisc.richtext.model.PlainTextChange;
import org.fxmisc.richtext.model.ReadOnlyStyledDocument;
import org.fxmisc.richtext.model.StyleSpans;
import org.fxmisc.richtext.model.StyledDocument;
import org.fxmisc.richtext.model.TwoDimensional;
import org.reactfx.Change;
import org.reactfx.EventStream;
import org.reactfx.EventStreams;
Expand Down Expand Up @@ -72,8 +75,7 @@ public class Editor extends BorderPane {
private static final StyleResult FALLBACK_STYLE_RESULT = new StyleResult(StyleSpans.singleton(Collections.emptyList(), 0), 0);
private final StackPane stackPane = new StackPane();
private final CodeArea codeArea = new SafeCodeArea();
private final ScrollBar horizontalScrollbar;
private final ScrollBar verticalScrollbar;
private final VirtualizedScrollPaneWrapper<CodeArea> codeScrollWrapper;
private final VirtualFlow<?, ?> virtualFlow;
private final MemoizationList<Cell<?, ?>> virtualCellList;
private final ExecutorService syntaxPool = ThreadPoolFactory.newSingleThreadExecutor("syntax-highlight");
Expand All @@ -92,17 +94,16 @@ public class Editor extends BorderPane {
public Editor() {
// Get the reflection hacks out of the way first.
// - Want to have access to scrollbars & the internal 'virtualFlow'
VirtualizedScrollPaneWrapper<CodeArea> scrollPane = new VirtualizedScrollPaneWrapper<>(codeArea);
horizontalScrollbar = Unchecked.get(() -> ReflectUtil.quietGet(scrollPane, VirtualizedScrollPane.class.getDeclaredField("hbar")));
verticalScrollbar = Unchecked.get(() -> ReflectUtil.quietGet(scrollPane, VirtualizedScrollPane.class.getDeclaredField("vbar")));
codeScrollWrapper = new VirtualizedScrollPaneWrapper<>(codeArea);

virtualFlow = Unchecked.get(() -> ReflectUtil.quietGet(codeArea, GenericStyledArea.class.getDeclaredField("virtualFlow")));
Object virtualCellManager = Unchecked.get(() -> ReflectUtil.quietGet(virtualFlow, VirtualFlow.class.getDeclaredField("cellListManager")));
virtualCellList = ReflectUtil.quietInvoke(virtualCellManager.getClass(), virtualCellManager, "getLazyCellList", new Class[0], new Object[0]);

// Initial layout / style.
getStylesheets().add("/style/code-editor.css");
setCenter(stackPane);
stackPane.getChildren().add(scrollPane);
stackPane.getChildren().add(codeScrollWrapper);

// Do not want text wrapping in a code editor.
codeArea.setWrapText(false);
Expand Down Expand Up @@ -643,15 +644,15 @@ public boolean isParagraphVisible(int line) {
*/
@Nonnull
public ScrollBar getHorizontalScrollbar() {
return horizontalScrollbar;
return codeScrollWrapper.getHorizontalScrollbar();
}

/**
* @return {@link #getCodeArea() Code area's} vertical scrollbar.
*/
@Nonnull
public ScrollBar getVerticalScrollbar() {
return verticalScrollbar;
return codeScrollWrapper.getVerticalScrollbar();
}

/**
Expand Down
11 changes: 11 additions & 0 deletions recaf-ui/src/main/java/software/coley/recaf/util/NodeEvents.java
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,17 @@ public static void addMouseMoveHandler(@Nonnull Node node, @Nonnull EventHandler
addHandler(node, handler, Unchecked.cast(original), Node::setOnMouseMoved);
}

/**
* @param node
* Node to add to.
* @param handler
* Handler to add.
*/
public static void addMouseDraggedHandler(@Nonnull Node node, @Nonnull EventHandler<MouseEvent> handler) {
Function<Node, EventHandler<? super MouseEvent>> original = Node::getOnMouseDragged;
addHandler(node, handler, Unchecked.cast(original), Node::setOnMouseDragged);
}

/**
* @param node
* Node to add to.
Expand Down
Loading