Skip to content

Commit

Permalink
Merge branch 'pr/890'
Browse files Browse the repository at this point in the history
  • Loading branch information
Col-E committed Jan 3, 2025
2 parents 26b6ebc + 2c48937 commit f474505
Show file tree
Hide file tree
Showing 3 changed files with 148 additions and 18 deletions.
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 @@ -73,8 +76,7 @@ public class Editor extends BorderPane implements Closing {
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 @@ -93,17 +95,16 @@ public class Editor extends BorderPane implements Closing {
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 @@ -652,15 +653,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

0 comments on commit f474505

Please sign in to comment.