com.dua3.fx.controls.PinBoard Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of fx-controls Show documentation
Show all versions of fx-controls Show documentation
JavaFX utilities (controls)
The newest version!
/*
* Copyright (c) 2022. Axel Howind ([email protected])
* This package is distributed under the Artistic License 2.0.
*/
package com.dua3.fx.controls;
import com.dua3.fx.util.FxRefresh;
import com.dua3.fx.util.FxUtil;
import com.dua3.fx.util.PlatformHelper;
import com.dua3.utility.data.Pair;
import com.dua3.utility.lang.LangUtil;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.geometry.Bounds;
import javafx.geometry.Dimension2D;
import javafx.geometry.Rectangle2D;
import javafx.scene.Node;
import javafx.scene.control.Control;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.Skin;
import javafx.scene.control.SkinBase;
import javafx.scene.layout.AnchorPane;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.function.Supplier;
/**
* A JavaFX component where items can be pinned at a position.
*/
public class PinBoard extends Control {
final ObservableList- items = FXCollections.observableArrayList();
private final ObjectProperty
areaProperty = new SimpleObjectProperty<>(new Rectangle2D(0, 0, 0, 0));
public PinBoard() {
}
public void clear() {
PlatformHelper.checkApplicationThread();
items.clear();
areaProperty.set(new Rectangle2D(0, 0, 0, 0));
}
public void refresh() {
if (getSkin() instanceof PinBoardSkin skin) {
skin.refresh();
}
}
public void dispose() {
if (getSkin() instanceof PinBoardSkin skin) {
skin.dispose();
}
}
@Override
protected Skin createDefaultSkin() {
return new PinBoardSkin(this);
}
public ReadOnlyObjectProperty areaProperty() {
return areaProperty;
}
public ObservableList- getItems() {
return FXCollections.unmodifiableObservableList(items);
}
public Pair
getScrollPosition() {
if (getSkin() instanceof PinBoardSkin skin) {
return skin.getScrollPosition();
} else {
return Pair.of(0.0, 0.0);
}
}
public void setScrollPosition(Pair scrollPosition) {
setScrollPosition(scrollPosition.first(), scrollPosition.second());
}
public void setScrollPosition(double hValue, double vValue) {
if (getSkin() instanceof PinBoardSkin skin) {
skin.setScrollPosition(hValue, vValue);
}
}
/**
* Get Item at point.
*
* @param x x-coordinate (relative to viewport)
* @param y y-coordinate (relative to viewport)
* @return Optional containing the item at (x,y)
*/
public Optional- getItemAt(double x, double y) {
return getPositionInItem(x, y).map(PositionInItem::item);
}
/**
* Get Item at point and coordinates transformed to item coordinates.
*
* @param x x-coordinate (relative to viewport)
* @param y y-coordinate (relative to viewport)
* @return Optional containing the item and the transformed coordinates
*/
public Optional
getPositionInItem(double x, double y) {
if (getSkin() instanceof PinBoardSkin skin) {
return skin.getPositionInItem(x, y);
} else {
return Optional.empty();
}
}
/**
* Add item at the bottom, centered horizontally.
*
* @param name item name
* @param nodeSupplier supplier (factory) for item node
* @param dimension item dimension
*/
public void pinBottom(String name, Supplier nodeSupplier, Dimension2D dimension) {
Rectangle2D boardArea = getArea();
double xCenter = (boardArea.getMaxX() + boardArea.getMinX()) / 2.0;
double y = boardArea.getMaxY();
Rectangle2D area = new Rectangle2D(xCenter - dimension.getWidth() / 2, y, dimension.getWidth(), dimension.getHeight());
pin(new Item(name, area, nodeSupplier));
}
public Rectangle2D getArea() {
return areaProperty.get();
}
public void pin(Item item) {
pin(Collections.singleton(item));
}
public void pin(Collection- itemsToPin) {
PlatformHelper.checkApplicationThread();
if (itemsToPin.isEmpty()) {
return;
}
items.addAll(itemsToPin);
itemsToPin.stream()
.map(Item::area)
.reduce(FxUtil::union)
.map(r -> FxUtil.union(getArea(), r))
.ifPresent(r -> {
if (!r.equals(getArea())) {
areaProperty.set(r);
}
});
}
@Override
public String toString() {
return "PinBoard{" +
"area=" + areaProperty.get() +
", items=" + items +
'}';
}
public record Item(String name, Rectangle2D area, Supplier
nodeBuilder) {}
public record PositionInItem(Item item, double x, double y) {}
}
class PinBoardSkin extends SkinBase {
private static final Logger LOG = LogManager.getLogger(PinBoardSkin.class);
private final FxRefresh refresher;
private final AnchorPane pane = new AnchorPane();
private final ScrollPane scrollPane = new ScrollPane(pane);
PinBoardSkin(PinBoard pinBoard) {
super(pinBoard);
this.refresher = FxRefresh.create(
LangUtil.defaultToString(this),
() -> PlatformHelper.runLater(this::updateNodes),
pinBoard
);
scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED);
scrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED);
scrollPane.setPannable(true);
getChildren().setAll(scrollPane);
pinBoard.areaProperty().addListener((v, o, n) -> {
pane.setMinWidth(n.getWidth());
pane.setMinHeight(n.getHeight());
});
pinBoard.getItems().addListener((ListChangeListener.Change> c) -> refresh());
pane.layoutBoundsProperty().addListener((o) -> refresh());
scrollPane.hvalueProperty().addListener((h) -> refresh());
scrollPane.vvalueProperty().addListener((v) -> refresh());
scrollPane.widthProperty().addListener((e) -> refresh());
scrollPane.heightProperty().addListener((e) -> refresh());
scrollPane.viewportBoundsProperty().addListener((v, o, n) -> refresh());
// enable/disable refresher
refresher.setActive(true);
}
private void updateNodes() {
LOG.trace("updateNodes()");
PlatformHelper.checkApplicationThread();
PinBoard board = getSkinnable();
Rectangle2D viewPort = getViewPort();
Rectangle2D boardArea = board.getArea();
double dx = Math.max(0, viewPort.getWidth() - boardArea.getWidth()) / 2.0;
double dy = Math.max(0, viewPort.getHeight() - boardArea.getHeight()) / 2.0;
Rectangle2D viewportInLocal = new Rectangle2D(viewPort.getMinX() + boardArea.getMinX(), viewPort.getMinY() + boardArea.getMinY(), viewPort.getWidth(), viewPort.getHeight());
// populate pane with nodes of visible items
List nodes = new ArrayList<>(board.items) // copy list to avoid concurrent modification
.stream()
.filter(item -> item.area().intersects(viewportInLocal))
.map(item -> {
LOG.debug("item is visible: {}", item.name());
Rectangle2D itemArea = item.area();
Node node = item.nodeBuilder().get();
node.setTranslateX(dx);
node.setTranslateY(dy + itemArea.getMinY());
return node;
})
.toList();
pane.setMinWidth(boardArea.getWidth());
pane.setMinHeight(boardArea.getHeight());
pane.getChildren().setAll(nodes);
}
void refresh() {
refresher.refresh();
}
private Rectangle2D getViewPort() {
Bounds vpBounds = scrollPane.getViewportBounds();
return new Rectangle2D(-vpBounds.getMinX(), -vpBounds.getMinY(), vpBounds.getWidth(), vpBounds.getHeight());
}
@Override
public void dispose() {
refresher.stop();
super.dispose();
}
public Pair getScrollPosition() {
return Pair.of(scrollPane.getHvalue(), scrollPane.getVvalue());
}
public void setScrollPosition(double hValue, double vValue) {
scrollPane.setHvalue(hValue);
scrollPane.setVvalue(vValue);
}
/**
* Get Item at point and coordinates relative to item.
*
* @param xViewport x-coordinate (relative to board)
* @param yViewport y-coordinate (relative to board)
* @return Optional containing the item at (x,y) and the coordinates relative to the item area
*/
public Optional getPositionInItem(double xViewport, double yViewport) {
Rectangle2D vp = getViewPort();
double x = xViewport + vp.getMinX();
double y = yViewport + vp.getMinY();
Rectangle2D b = getSkinnable().getArea();
List items = new ArrayList<>(getSkinnable().getItems());
for (PinBoard.Item item : items) {
Rectangle2D a = item.area();
if (a.contains(x, y)) {
return Optional.of(new PinBoard.PositionInItem(item, x + b.getMinX() - a.getMinX(), y + b.getMinY() - a.getMinY()));
}
}
return Optional.empty();
}
}