Question

I am trying to add a custom toolbar to a generic TitledPane. My code seems to work (minimal example follows), but I have problems with layout. Specifically: I am adding my toolbar using setGraphic(), but that seems to have a fixed width, while I would like it to expand so I can have the buttons flushed right while keeping title on the left side.

(I cannot post images, so I will revert to ASCII art) This is the actual result of code below:

+------------------------------------+
| > Node 1 [a][b][c][d]              |
+------------------------------------+
| > Node                             |
+------------------------------------+

while I would like to get something like:

+------------------------------------+
| > Node 1              [a][b][c][d] |
+------------------------------------+
| > Node                             |
+------------------------------------+

I can fake the result setting explicitly the BorderPane width, but it will not follow resizing!


here starts the code:

import java.net.URL;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.Accordion;
import javafx.scene.control.Button;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.TitledPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.text.Text;
import javafx.stage.Stage;

public class Test extends Application {

private Node loadMinitool(String title) {
    try {
        URL u = getClass().getResource("Minitool.fxml");
        FXMLLoader l = new FXMLLoader(u);
        Node n = (Node) l.load();
        Minitool mtc = (Minitool) l.getController();
        mtc.setTitle(title);
        return n;
    } catch (Exception e) {
        System.err.println("Unable to load 'Minitool.fxml': "+e.getMessage());
    }
    return null;
}

public Parent createContent() {

    TitledPane t1 = new TitledPane(null, new Button("Button"));
    t1.setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
    t1.setGraphic(loadMinitool("Node 1"));
    TitledPane t2 = new TitledPane("Node 2", new Text("String"));
    TitledPane t3 = new TitledPane("Node 3", new Rectangle(120, 50,
            Color.RED));

    Accordion accordion = new Accordion();

    accordion.getPanes().add(t1);
    accordion.getPanes().add(t2);
    accordion.getPanes().add(t3);

    accordion.setMinSize(100, 100);
    accordion.setPrefSize(200, 400);

    return accordion;

}

@Override
public void start(Stage primaryStage) {
    primaryStage.setScene(new Scene(createContent()));
    primaryStage.show();

}

public static void main(String[] args) {
    launch(args);
}
}

the associated FXML is:

<?xml version="1.0" encoding="UTF-8"?>
<?import java.lang.*?>
<?import java.net.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.text.*?>

<BorderPane maxWidth="1.7976931348623157E308" prefWidth="-1.0" xmlns:fx="http://javafx.com/fxml" fx:controller="com.voith.hyconmde.ui.util.minitool.Minitool">
  <left>
    <Label fx:id="title" maxWidth="1.7976931348623157E308" text="Title goes here" BorderPane.alignment="CENTER_LEFT" />
  </left>
  <center>
    <Pane fx:id="filler" maxWidth="1.7976931348623157E308" minWidth="0.0" prefHeight="16.0" prefWidth="200.0" />
  </center>
  <right>
    <HBox>
      <Button fx:id="add" graphicTextGap="0.0" onAction="#addAction" styleClass="btnToolbar" />
      <Button fx:id="del" graphicTextGap="0.0" onAction="#delAction" styleClass="btnToolbar" />
      <Button fx:id="sub" graphicTextGap="0.0" onAction="#subAction" styleClass="btnToolbar" />
      <Button fx:id="dup" graphicTextGap="0.0" onAction="#dupAction" styleClass="btnToolbar" />
    </HBox>
  </right>
  <stylesheets>
    <URL value="@Minitool.css" />
  </stylesheets>
</BorderPane>

and the controller is:

import java.net.URL;
import java.util.ResourceBundle;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.Button;
import javafx.scene.control.Label;

public class Minitool {

    @FXML private ResourceBundle resources;
    @FXML private URL location;
    @FXML private Button add;
    @FXML private Button del;
    @FXML private Button dup;
    @FXML private Button sub;    
    @FXML private Label title;

    @FXML void addAction(ActionEvent event) {
    } 
    @FXML void delAction(ActionEvent event) {
    }
    @FXML void dupAction(ActionEvent event) {
    }
    @FXML void subAction(ActionEvent event) {
    }
    @FXML void initialize() {
        assert add != null : "fx:id=\"add\" was not injected: check your FXML file 'Minitool.fxml'.";
        assert del != null : "fx:id=\"del\" was not injected: check your FXML file 'Minitool.fxml'.";
        assert dup != null : "fx:id=\"dup\" was not injected: check your FXML file 'Minitool.fxml'.";
        assert sub != null : "fx:id=\"sub\" was not injected: check your FXML file 'Minitool.fxml'.";
        assert title != null : "fx:id=\"title\" was not injected: check your FXML file 'Minitool.fxml'.";
    }

    public void setTitle(String title) {
        this.title.setText(title);
    }

    public void setWidth() {
        if (parent == null) {
            Parent p = minitool.getParent();
                if (p != null)
            p = p.getParent();
            if (p instanceof TitledPane) {
                parent = (TitledPane) p;
                parent.widthProperty().addListener(new ChangeListener<Object>() {
                    @Override
                    public void changed(ObservableValue<?> observable, Object oldValue, Object newValue) {
                        if (newValue instanceof Number) {
                            Number n = (Number) newValue;
                            double d = n.doubleValue();
                            setFiller(d);
                        }
                    }
                });
                setFiller(parent.getWidth());
            }
        }
    }

    protected void setFiller(double d) {
        double w = d - title.getWidth() - 180; // XXX: this value has been hand-trimmed!
        if (w < 0)
            w = 0;
        filler.setPrefWidth(w);
    }
}
Was it helpful?

Solution

You need to do some basic math. Follow modifications to your code below:
1) In fxml file add properties to HBox:

<HBox alignment="CENTER_RIGHT" HBox.hgrow="ALWAYS">

The result is, when there is an extra space in title width (by resizing), this HBox will have a priority to expand and its children will always be aligned to the right (center).

2) Now we need to calculate the remaining title width this way

REMAINING_WIDTH = TITLE_TOTAL_WIDTH - TITLE_TEXT_WIDTH - ARROW_BUTTON_WIDTH - RIGHT_LEFT_PADDINGS

The calculated width will be our tool buttons pane's (i.e. HBox's) preferred width.

public Parent createContent() {

        String titleText = "Node 1";

        TitledPane t1 = new TitledPane(null, new Button("Button"));
        t1.setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
        t1.setGraphic(loadMinitool(titleText));

        double titleTextWidth = computeTextWidth(t1.getFont(), titleText, 0);
        double arrowButtonWidth = 14;  // I have given a static value here otherwise
            // it must be calculated by
            // t1.lookup(".arrow-button").getLayoutBounds().getWidth()
            // after the primary stage has been shown. Namely after primaryStage.show(); line.
        double paddings = 20; // right (10) and left (10) paddings defined in 
            // caspian.css for title of the pane. These values can also be obtained 
            // by lookup.
        double total = titleTextWidth + arrowButtonWidth + paddings;

        HBox toolButtons = (HBox) ((BorderPane) t1.getGraphic()).getRight();
        toolButtons.prefWidthProperty().bind(t1.widthProperty().subtract(total));

        TitledPane t2 = new TitledPane("Node 2", new Text("String"));
        TitledPane t3 = new TitledPane("Node 3", new Rectangle(120, 50,
                Color.RED));

        Accordion accordion = new Accordion();
        accordion.getPanes().add(t1);
        accordion.getPanes().add(t2);
        accordion.getPanes().add(t3);
        accordion.setMinSize(100, 100);
        accordion.setPrefSize(200, 400);

        return accordion;
    }

The computeTextWidth() code is taken from com.sun.javafx.scene.control.skin.Utils, and for reference only:

private double computeTextWidth(Font font, String text, double wrappingWidth) {
    Text helper = new Text();
    helper.setText(text);
    helper.setFont(font);
    // Note that the wrapping width needs to be set to zero before
    // getting the text's real preferred width.
    helper.setWrappingWidth(0);
    double w = Math.min(helper.prefWidth(-1), wrappingWidth);
    helper.setWrappingWidth((int) Math.ceil(w));
    return Math.ceil(helper.getLayoutBounds().getWidth());
}

The other codes of yours remain the same.

OTHER TIPS

I've come up with that solution:

import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TitledPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

public class TestApp extends Application {

    public static void main(String[] args) {
        launch();
    }

    @Override
    public void start(Stage primaryStage) {

        TitledPane title = new TitledPane();
        //title.setContent(new Label("Content"));

        Node graphic = new Button("Click");
        Label label = new Label("Titled Pane Header");
        // Allow the label to grow to max width
        label.setMaxWidth(Double.MAX_VALUE);
        // Let it actually grow
        HBox.setHgrow(label, Priority.ALWAYS);
        HBox hBox = new HBox(label, graphic);
        hBox.setAlignment(Pos.CENTER_LEFT);
        hBox.setPadding(new Insets(0, 3, 0, 0));

        // Bind the HBox's min width to the TitledPane's width and subtract arrow (15) and left padding (10)
        hBox.minWidthProperty().bind(title.widthProperty().subtract(25));

        title.setGraphic(hBox);

        Scene scene = new Scene(new VBox(title), 300, 250);

        primaryStage.setScene(scene);
        primaryStage.show();
    }
}
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top