All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.dua3.fx.controls.PinBoard Maven / Gradle / Ivy

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();
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy