Sarada
Sarada

Reputation: 33

How to make an pane stay over line connecting draggablenode in javafx

I am designing a UI of a graph structure with draggable nodes. In the graph I have a component called relation(it is a pane) which shows the link between two nodes. I want relation to stay and move along with line at mid of line.

Current UI design is as shown below

And the expected one is like:

Upvotes: 1

Views: 190

Answers (1)

fabian
fabian

Reputation: 82461

You need to refresh the position of the node when the line's end coordinates are modified. To avoid triggering the calculation multiple times per layout pass, I recommend doing this from the layoutChildren method of the parent, but you could also do this from a listener to the startX, endY, ... properties. This will lead to some unnecessary computations though.

As for calcualting the position of the node: The center of the node needs to align with the midpoint of the line, so you need to solve the following equation for markTopLeft:

markTopLeft + (markWidth, markHeight) / 2 = (lineStart + lineEnd) / 2

markTopLeft = (lineStart + lineEnd - (markWidth, markHeight)) / 2

Example

Pane allowing for custom layout calculations

public class PostProcessPane extends Pane {

    private final Set<Node> modifiedChildren = new HashSet<>();
    private final Set<Node> modifiedChildrenUnmodifiable = Collections.unmodifiableSet(modifiedChildren);
    private final List<Consumer<Set<Node>>> postProcessors = new ArrayList<>();

    public List<Consumer<Set<Node>>> getPostProcessors() {
        return postProcessors;
    }

    private final ChangeListener listener = (o, oldValue, newValue) -> modifiedChildren.add((Node) ((ReadOnlyProperty) o).getBean());

    private void initListener() {
        getChildren().addListener((ListChangeListener.Change<? extends Node> c) -> {
            while (c.next()) {
                if (c.wasRemoved()) {
                    for (Node n : c.getRemoved()) {
                        n.boundsInParentProperty().removeListener(listener);
                    }
                }
                if (c.wasAdded()) {
                    for (Node n : c.getAddedSubList()) {
                        n.boundsInParentProperty().addListener(listener);
                    }
                }
            }
        });
    }

    public PostProcessPane() {
        initListener();
    }

    public PostProcessPane(Node... children) {
        super(children);
        initListener();

        for (Node n : children) {
            n.boundsInParentProperty().addListener(listener);
        }
    }

    @Override
    protected void layoutChildren() {
        super.layoutChildren();

        if (!modifiedChildren.isEmpty()) {
            for (Consumer<Set<Node>> processor : postProcessors) {
                processor.accept(modifiedChildrenUnmodifiable);
            }
            modifiedChildren.clear();
        }

    }

}

Usage

@Override
public void start(Stage primaryStage) throws Exception {
    Rectangle r1 = new Rectangle(200, 50, Color.BLUE);
    Rectangle r2 = new Rectangle(200, 50, Color.RED);
    Rectangle mark = new Rectangle(200, 50, Color.YELLOW);
    Line line = new Line();

    r1.setX(20);
    r2.setX(380);
    r2.setY(450);

    PostProcessPane root = new PostProcessPane(line, r1, r2, mark);
    root.getPostProcessors().add(changedNodes -> {
        if (changedNodes.contains(r1) || changedNodes.contains(r2) || changedNodes.contains(mark)) {
            Bounds bounds1 = r1.getBoundsInParent();
            Bounds bounds2 = r2.getBoundsInParent();

            // refresh line ends
            line.setStartX(bounds1.getMinX() + bounds1.getWidth() / 2);
            line.setStartY(bounds1.getMaxY());
            line.setEndX(bounds2.getMinX() + bounds2.getWidth() / 2);
            line.setEndY(bounds2.getMinY());

            // recalculate mark position
            mark.setX((line.getStartX() + line.getEndX() - mark.getWidth()) / 2);
            mark.setY((line.getStartY() + line.getEndY() - mark.getHeight()) / 2);
        }
    });

    // add some movement for the nodes
    Timeline timeline = new Timeline(
            new KeyFrame(Duration.ZERO,
                    new KeyValue(r1.xProperty(), r1.getX()),
                    new KeyValue(r1.yProperty(), r1.getY()),
                    new KeyValue(r2.xProperty(), r2.getX())),
            new KeyFrame(Duration.seconds(1),
                    new KeyValue(r2.xProperty(), r1.getX())),
            new KeyFrame(Duration.seconds(2),
                    new KeyValue(r1.xProperty(), r2.getX()),
                    new KeyValue(r1.yProperty(), r2.getY() / 2))
    );
    timeline.setAutoReverse(true);
    timeline.setCycleCount(Animation.INDEFINITE);
    timeline.play();

    Scene scene = new Scene(root);

    primaryStage.setScene(scene);
    primaryStage.show();
}

Upvotes: 1

Related Questions