Maran23
Maran23

Reputation: 256

JavaFX Border don't fit for node with custom shape

I'm trying to figure out, if it is possible to draw a border for a node with a custom shape. Currently the border doesn't fit the shape of the node.

This is what it currently looks like:
enter image description here

The shape is achieved by the following CSS:

.arrow-tail {
    -fx-shape: "M 0 0 L 10 0 L 10 10 L 0 10 L 10 5 Z";
}

.arrow-head {
    -fx-shape: "M 0 0 L 10 5 L 0 10 Z";
}

This is the important code of the arrow class where the CSS is used:

public class Arrow extends HBox {

    public void Arrow(Node graphic, String title) {
        getChildren().addAll(getArrowTail(), getArrowMiddlePart(graphic, title), getArrowHead());
    }

    private final Region getArrowTail() {
        final Region arrowTail = new Region();
        arrowTail.setMinWidth(10);
        arrowTail.getStyleClass().add("arrow-tail");
        return arrowTail;
     }

     private final Node getArrowMiddlePart(Node graphic, String text) {
        labelTitle = new Label(text);
        labelTitle.setGraphic(graphic);
        labelTitle.idProperty().bind(idProperty());

        final Tooltip tooltip = new Tooltip();
        tooltip.textProperty().bind(labelTitle.textProperty());
        Tooltip.install(labelTitle, tooltip);

        final HBox arrowMiddlePart = new HBox(labelTitle);
        arrowMiddlePart.minWidthProperty().bind(minWidthProperty());
        arrowMiddlePart.setAlignment(Pos.CENTER);
        return arrowMiddlePart;
   }

    private final Region getArrowHead() {
        final Region arrowHead = new Region();
        arrowHead.setMinWidth(10);
        arrowHead.getStyleClass().add("arrow-head");
        return arrowHead;
    }  

}

The Arrow class is a HBox, where I create a custom shaped region as arrow tail and arrow head and another HBox with an label in it as arrow middle part.

Upvotes: 3

Views: 215

Answers (1)

fabian
fabian

Reputation: 82461

Unfortunately there doesn't seem to be a way to set the borders for the different sides of a Region with a shape applied to it independently.

I recommend extending Region directly adding a Path as first child and overriding layoutChildren resize this path.

public class Arrow extends Region {

    private static final double ARROW_LENGTH = 10;
    private static final Insets MARGIN = new Insets(1, ARROW_LENGTH, 1, ARROW_LENGTH);

    private final HBox container;
    private final HLineTo hLineTop;
    private final LineTo tipTop;
    private final LineTo tipBottom;
    private final LineTo tailBottom;

    public Arrow(Node graphic, String title) {
        Path path = new Path(
                new MoveTo(),
                hLineTop = new HLineTo(),
                tipTop = new LineTo(ARROW_LENGTH, 0),
                tipBottom = new LineTo(-ARROW_LENGTH, 0),
                new HLineTo(),
                tailBottom = new LineTo(ARROW_LENGTH, 0),
                new ClosePath());

        tipTop.setAbsolute(false);
        tipBottom.setAbsolute(false);

        path.setManaged(false);
        path.setStrokeType(StrokeType.INSIDE);
        path.getStyleClass().add("arrow-shape");

        Label labelTitle = new Label(title, graphic);
        container = new HBox(labelTitle);

        getChildren().addAll(path, container);
        HBox.setHgrow(labelTitle, Priority.ALWAYS);
        labelTitle.setAlignment(Pos.CENTER);
        labelTitle.setMaxWidth(Double.POSITIVE_INFINITY);
    }

    @Override
    protected void layoutChildren() {
        // hbox layout
        Insets insets = getInsets();
        double left = insets.getLeft();
        double top = insets.getTop();
        double width = getWidth();
        double height = getHeight();
        layoutInArea(container,
                left, top,
                width - left - insets.getRight(), height - top - insets.getBottom(),
                0, MARGIN, true, true, HPos.LEFT, VPos.TOP);

        // adjust arrow shape
        double length = width - ARROW_LENGTH;
        double h2 = height / 2;

        hLineTop.setX(length);

        tipTop.setY(h2);
        tipBottom.setY(h2);
        tailBottom.setY(h2);
    }

    @Override
    protected double computeMinWidth(double height) {
        Insets insets = getInsets();
        return 2 * ARROW_LENGTH + insets.getLeft() + insets.getRight() + container.minWidth(height);
    }

    @Override
    protected double computeMinHeight(double width) {
        Insets insets = getInsets();
        return 2 + insets.getTop() + insets.getBottom() + container.minHeight(width);
    }

    @Override
    protected double computePrefWidth(double height) {
        Insets insets = getInsets();
        return 2 * ARROW_LENGTH + insets.getLeft() + insets.getRight() + container.prefWidth(height);
    }

    @Override
    protected double computePrefHeight(double width) {
        Insets insets = getInsets();
        return 2 + insets.getTop() + insets.getBottom() + container.prefHeight(width);
    }

    @Override
    protected double computeMaxWidth(double height) {
        Insets insets = getInsets();
        return 2 * ARROW_LENGTH + insets.getLeft() + insets.getRight() + container.maxWidth(height);
    }

    @Override
    protected double computeMaxHeight(double width) {
        Insets insets = getInsets();
        return 2 + insets.getTop() + insets.getBottom() + container.maxHeight(width);
    }

}

CSS

.arrow-shape {
    -fx-fill: dodgerblue;
    -fx-stroke: black;
}

Note that the code would be simpler, if you extend HBox, but this would allow other classes access to the child list which could result in removal of the Path; extending Region allows us to keep the method protected preventing this kind of access but requires us to implement the compute... methods and the layouting of the children.

Upvotes: 1

Related Questions