org.fxmisc.flowless.VirtualizedScrollPane Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of richtextfx Show documentation
Show all versions of richtextfx Show documentation
FX-Text-Area for formatted text and other special effects.
package org.fxmisc.flowless;
import static javafx.scene.control.ScrollPane.ScrollBarPolicy.*;
import javafx.application.Platform;
import javafx.beans.NamedArg;
import javafx.beans.Observable;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.DoubleBinding;
import javafx.beans.value.ChangeListener;
import javafx.css.PseudoClass;
import javafx.geometry.Bounds;
import javafx.geometry.Orientation;
import javafx.scene.Node;
import javafx.scene.control.ScrollBar;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.Region;
import org.reactfx.value.Val;
import org.reactfx.value.Var;
public class VirtualizedScrollPane extends Region implements Virtualized {
private static final PseudoClass CONTENT_FOCUSED = PseudoClass.getPseudoClass("content-focused");
private final ScrollBar hbar;
private final ScrollBar vbar;
private final V content;
private final ChangeListener contentFocusedListener;
private Var hbarValue;
private Var vbarValue;
/** The Policy for the Horizontal ScrollBar */
private final Var hbarPolicy;
public final ScrollPane.ScrollBarPolicy getHbarPolicy() { return hbarPolicy.getValue(); }
public final void setHbarPolicy(ScrollPane.ScrollBarPolicy value) { hbarPolicy.setValue(value); }
public final Var hbarPolicyProperty() { return hbarPolicy; }
/** The Policy for the Vertical ScrollBar */
private final Var vbarPolicy;
public final ScrollPane.ScrollBarPolicy getVbarPolicy() { return vbarPolicy.getValue(); }
public final void setVbarPolicy(ScrollPane.ScrollBarPolicy value) { vbarPolicy.setValue(value); }
public final Var vbarPolicyProperty() { return vbarPolicy; }
/**
* Constructs a VirtualizedScrollPane with the given content and policies
*/
public VirtualizedScrollPane(
@NamedArg("content") V content,
@NamedArg("hPolicy") ScrollPane.ScrollBarPolicy hPolicy,
@NamedArg("vPolicy") ScrollPane.ScrollBarPolicy vPolicy
) {
this.getStyleClass().add("virtualized-scroll-pane");
this.content = content;
// create scrollbars
hbar = new ScrollBar();
vbar = new ScrollBar();
hbar.setOrientation(Orientation.HORIZONTAL);
vbar.setOrientation(Orientation.VERTICAL);
// scrollbar ranges
hbar.setMin(0);
vbar.setMin(0);
hbar.maxProperty().bind(content.totalWidthEstimateProperty());
vbar.maxProperty().bind(content.totalHeightEstimateProperty());
// scrollbar increments
setupUnitIncrement(hbar);
setupUnitIncrement(vbar);
hbar.blockIncrementProperty().bind(hbar.visibleAmountProperty());
vbar.blockIncrementProperty().bind(vbar.visibleAmountProperty());
// scrollbar positions
Var hPosEstimate = Val
.combine(
content.estimatedScrollXProperty(),
Val.map(content.layoutBoundsProperty(), Bounds::getWidth),
content.totalWidthEstimateProperty(),
VirtualizedScrollPane::offsetToScrollbarPosition)
.asVar(this::setHPosition);
Var vPosEstimate = Val
.combine(
content.estimatedScrollYProperty(),
Val.map(content.layoutBoundsProperty(), Bounds::getHeight),
content.totalHeightEstimateProperty(),
VirtualizedScrollPane::offsetToScrollbarPosition)
.orElseConst(0.0)
.asVar(this::setVPosition);
hbarValue = Var.doubleVar(hbar.valueProperty());
vbarValue = Var.doubleVar(vbar.valueProperty());
Bindings.bindBidirectional(hbarValue, hPosEstimate);
Bindings.bindBidirectional(vbarValue, vPosEstimate);
// scrollbar visibility
hbarPolicy = Var.newSimpleVar(hPolicy);
vbarPolicy = Var.newSimpleVar(vPolicy);
Val layoutWidth = Val.map(layoutBoundsProperty(), Bounds::getWidth);
Val layoutHeight = Val.map(layoutBoundsProperty(), Bounds::getHeight);
Val needsHBar0 = Val.combine(
content.totalWidthEstimateProperty(),
layoutWidth,
(cw, lw) -> cw > lw);
Val needsVBar0 = Val.combine(
content.totalHeightEstimateProperty(),
layoutHeight,
(ch, lh) -> ch > lh);
Val needsHBar = Val.combine(
needsHBar0,
needsVBar0,
content.totalWidthEstimateProperty(),
vbar.widthProperty(),
layoutWidth,
(needsH, needsV, cw, vbw, lw) -> needsH || needsV && cw + vbw.doubleValue() > lw);
Val needsVBar = Val.combine(
needsVBar0,
needsHBar0,
content.totalHeightEstimateProperty(),
hbar.heightProperty(),
layoutHeight,
(needsV, needsH, ch, hbh, lh) -> needsV || needsH && ch + hbh.doubleValue() > lh);
Val shouldDisplayHorizontal = Val.flatMap(hbarPolicy, policy -> {
switch (policy) {
case NEVER:
return Val.constant(false);
case ALWAYS:
return Val.constant(true);
default: // AS_NEEDED
return needsHBar;
}
});
Val shouldDisplayVertical = Val.flatMap(vbarPolicy, policy -> {
switch (policy) {
case NEVER:
return Val.constant(false);
case ALWAYS:
return Val.constant(true);
default: // AS_NEEDED
return needsVBar;
}
});
// request layout later, because if currently in layout, the request is ignored
shouldDisplayHorizontal.addListener(obs -> Platform.runLater(this::requestLayout));
shouldDisplayVertical.addListener(obs -> Platform.runLater(this::requestLayout));
hbar.visibleProperty().bind(shouldDisplayHorizontal);
vbar.visibleProperty().bind(shouldDisplayVertical);
contentFocusedListener = (obs, ov, nv) -> pseudoClassStateChanged(CONTENT_FOCUSED, nv);
content.focusedProperty().addListener(contentFocusedListener);
getChildren().addAll(content, hbar, vbar);
getChildren().addListener((Observable obs) -> dispose());
}
/**
* Constructs a VirtualizedScrollPane that only displays its horizontal and vertical scroll bars as needed
*/
public VirtualizedScrollPane(@NamedArg("content") V content) {
this(content, AS_NEEDED, AS_NEEDED);
}
/**
* Does not unbind scrolling from Content before returning Content.
* @return - the content
*/
public V getContent() {
return content;
}
/**
* Unbinds scrolling from Content before returning Content.
* @return - the content
*/
public V removeContent() {
getChildren().clear();
return content;
}
private void dispose() {
content.focusedProperty().removeListener(contentFocusedListener);
hbarValue.unbindBidirectional(content.estimatedScrollXProperty());
vbarValue.unbindBidirectional(content.estimatedScrollYProperty());
unbindScrollBar(hbar);
unbindScrollBar(vbar);
}
private void unbindScrollBar(ScrollBar bar) {
bar.maxProperty().unbind();
bar.unitIncrementProperty().unbind();
bar.blockIncrementProperty().unbind();
bar.visibleProperty().unbind();
}
@Override
public Val totalWidthEstimateProperty() {
return content.totalWidthEstimateProperty();
}
@Override
public Val totalHeightEstimateProperty() {
return content.totalHeightEstimateProperty();
}
@Override
public Var estimatedScrollXProperty() {
return content.estimatedScrollXProperty();
}
@Override
public Var estimatedScrollYProperty() {
return content.estimatedScrollYProperty();
}
@Override
public void scrollXBy(double deltaX) {
content.scrollXBy(deltaX);
}
@Override
public void scrollYBy(double deltaY) {
content.scrollYBy(deltaY);
}
@Override
public void scrollXToPixel(double pixel) {
content.scrollXToPixel(pixel);
}
@Override
public void scrollYToPixel(double pixel) {
content.scrollYToPixel(pixel);
}
@Override
protected double computePrefWidth(double height) {
return content.prefWidth(height);
}
@Override
protected double computePrefHeight(double width) {
return content.prefHeight(width);
}
@Override
protected double computeMinWidth(double height) {
return vbar.minWidth(-1);
}
@Override
protected double computeMinHeight(double width) {
return hbar.minHeight(-1);
}
@Override
protected double computeMaxWidth(double height) {
return content.maxWidth(height);
}
@Override
protected double computeMaxHeight(double width) {
return content.maxHeight(width);
}
@Override
protected void layoutChildren() {
double layoutWidth = getLayoutBounds().getWidth();
double layoutHeight = getLayoutBounds().getHeight();
boolean vbarVisible = vbar.isVisible();
boolean hbarVisible = hbar.isVisible();
double vbarWidth = vbarVisible ? vbar.prefWidth(-1) : 0;
double hbarHeight = hbarVisible ? hbar.prefHeight(-1) : 0;
double w = layoutWidth - vbarWidth;
double h = layoutHeight - hbarHeight;
content.resize(w, h);
hbar.setVisibleAmount(w);
vbar.setVisibleAmount(h);
if(vbarVisible) {
vbar.resizeRelocate(layoutWidth - vbarWidth, 0, vbarWidth, h);
}
if(hbarVisible) {
hbar.resizeRelocate(0, layoutHeight - hbarHeight, w, hbarHeight);
}
}
private void setHPosition(double pos) {
double offset = scrollbarPositionToOffset(
pos,
content.getLayoutBounds().getWidth(),
content.totalWidthEstimateProperty().getValue());
content.estimatedScrollXProperty().setValue(offset);
}
private void setVPosition(double pos) {
double offset = scrollbarPositionToOffset(
pos,
content.getLayoutBounds().getHeight(),
content.totalHeightEstimateProperty().getValue());
content.estimatedScrollYProperty().setValue(offset);
}
private static void setupUnitIncrement(ScrollBar bar) {
bar.unitIncrementProperty().bind(new DoubleBinding() {
{ bind(bar.maxProperty(), bar.visibleAmountProperty()); }
@Override
protected double computeValue() {
double max = bar.getMax();
double visible = bar.getVisibleAmount();
return max > visible
? 16 / (max - visible) * max
: 0;
}
});
}
private static double offsetToScrollbarPosition(
double contentOffset, double viewportSize, double contentSize) {
return contentSize > viewportSize
? contentOffset / (contentSize - viewportSize) * contentSize
: 0;
}
private static double scrollbarPositionToOffset(
double scrollbarPos, double viewportSize, double contentSize) {
return contentSize > viewportSize
? scrollbarPos / contentSize * (contentSize - viewportSize)
: 0;
}
}