Search code examples
javafxlinepanedrag

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:


Solution

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