Search code examples
javafxoverflowoverlaptextflow

Textflow overflowing and overlapping from GridPane cell


I've been having issues with some Textflows overflowing from their defined cell in a GridPane. My guess would be that no prefHeight is defined but I'm not managing to find how to calculate this height in order to set or bind it.

My intent is to have the TextFlow takes as much size as it needs to display the text fully, as the whole GridPane is in a ScrollPane.

Or is it another issue?

Here's a screenshot (added borders for debugging): Screenshot of the overflow and overlap

Here's the fxml definition:

<?xml version="1.0" encoding="UTF-8"?>

<?import java.lang.String?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.ScrollPane?>
<?import javafx.scene.control.TableColumn?>
<?import javafx.scene.control.TableView?>
<?import javafx.scene.layout.AnchorPane?>
<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.layout.ColumnConstraints?>
<?import javafx.scene.layout.GridPane?>
<?import javafx.scene.layout.RowConstraints?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.text.Font?>
<?import javafx.scene.text.Text?>
<?import javafx.scene.text.TextFlow?>

<fx:root stylesheets="@../styles/main.css" type="BorderPane" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0" xmlns="http://javafx.com/javafx/21" xmlns:fx="http://javafx.com/fxml/1">
    <styleClass>
      <String fx:value="binocles-pane" />
      <String fx:value="book-pane" />
  </styleClass>
   <center>
        <VBox fx:id="bookZoneVBox" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
         <children>
              <Text fx:id="bookTitle" strokeType="OUTSIDE" strokeWidth="0.0" styleClass="main-title" text="Book title">
               <font>
                  <Font size="14.0" />
               </font>
            </Text>
              <ScrollPane fx:id="bookZoneScroll" fitToHeight="true" fitToWidth="true" focusTraversable="false" hbarPolicy="NEVER" minHeight="224.0" minWidth="300.0" prefHeight="233.0" prefWidth="310.0" styleClass="edge-to-edge" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
               <content>
                  <GridPane fx:id="bookGrid" minHeight="150.0" minWidth="300.0" prefHeight="215.0" prefWidth="300.0" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
                    <columnConstraints>
                      <ColumnConstraints hgrow="SOMETIMES" minWidth="100.0" prefWidth="100.0" />
                      <ColumnConstraints hgrow="ALWAYS" minWidth="200.0" />
                    </columnConstraints>
                    <rowConstraints>
                      <RowConstraints minHeight="10.0" prefHeight="22.0" vgrow="ALWAYS" />
                      <RowConstraints minHeight="10.0" prefHeight="21.0" vgrow="ALWAYS" />
                      <RowConstraints minHeight="10.0" prefHeight="24.0" vgrow="NEVER" />
                      <RowConstraints minHeight="10.0" prefHeight="138.0" vgrow="SOMETIMES" />
                    </rowConstraints>
                     <children>
                        <Label text="Synopsis" GridPane.valignment="TOP" />
                        <Label text="Description" GridPane.rowIndex="1" GridPane.valignment="TOP" />
                        <Label text="Metadata" GridPane.rowIndex="2" GridPane.valignment="BOTTOM" />
                        <TableView fx:id="bookMetadataTable" focusTraversable="false" maxHeight="150.0" maxWidth="300.0" prefHeight="150.0" prefWidth="300.0" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0" GridPane.columnSpan="2147483647" GridPane.hgrow="ALWAYS" GridPane.rowIndex="3">
                          <columns>
                            <TableColumn editable="false" prefWidth="75.0" sortable="false" text="Field" />
                            <TableColumn editable="false" prefWidth="215.0" sortable="false" text="Value" />
                          </columns>
                           <padding>
                              <Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
                           </padding>
                        </TableView>
                        <TextFlow fx:id="bookSynopsisField" minWidth="200.0" textAlignment="JUSTIFY" GridPane.columnIndex="1" GridPane.hgrow="ALWAYS" />
                        <TextFlow fx:id="bookDescriptionField" minWidth="200.0" textAlignment="JUSTIFY" GridPane.columnIndex="1" GridPane.hgrow="ALWAYS" GridPane.rowIndex="1" />
                     </children>
                  </GridPane>
               </content>
               <padding>
                  <Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
               </padding>
              </ScrollPane>
         </children>
      </VBox>
   </center>
</fx:root>

Here's the java code:

package com.github.sylordis.binocles.ui.views;

import java.io.IOException;
import java.net.URL;
import java.util.Map;
import java.util.ResourceBundle;

import com.github.sylordis.binocles.model.text.Book;
import com.github.sylordis.binocles.ui.javafxutils.FXFormatUtils;

import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.fxml.Initializable;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.TableView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.VBox;
import javafx.scene.text.Text;
import javafx.scene.text.TextFlow;

/**
 * Pane component for a book.
 */
public class BookView extends BorderPane implements Initializable, BinoclesTabPane {

    private Book book;

    @FXML
    private Text bookTitle;
    @FXML
    private VBox bookZoneVBox;
    @FXML
    private TextFlow bookSynopsisField;
    @FXML
    private TextFlow bookDescriptionField;
    @FXML
    private GridPane bookGrid;
    @FXML
    private ScrollPane bookZoneScroll;
    @FXML
    private TableView<Map.Entry<String, String>> bookMetadataTable;

    private ObservableList<Map.Entry<String, String>> bookMetadataTableData;

    public BookView(Book book) {
        super();
        this.book = book;
        FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("book_view.fxml"));
        fxmlLoader.setRoot(this);
        fxmlLoader.setController(this);
        try {
            fxmlLoader.load();
        } catch (IOException exception) {
            throw new RuntimeException(exception);
        }

    }

    @Override
    public void initialize(URL location, ResourceBundle resources) {
        // Set content
        bookTitle.setText(book.getTitle());
        bookSynopsisField.getChildren().add(new Text(book.getSynopsis()));
        bookDescriptionField.getChildren().add(new Text(book.getDescription()));
        bookMetadataTable.setPlaceholder(new Label("No metadata provided"));
        bookMetadataTable.setItems(FXCollections.observableArrayList(book.getMetadata().entrySet()));
        bookMetadataTable.refresh();
        // Format
        FXFormatUtils.bindToParent(bookGrid);
        bookSynopsisField.setMaxWidth(Double.MAX_VALUE);
        bookSynopsisField.setMaxHeight(Double.MAX_VALUE);
        bookDescriptionField.setMaxWidth(Double.MAX_VALUE);
        bookDescriptionField.setMaxHeight(Double.MAX_VALUE);
        bookGrid.maxWidthProperty().bind(bookZoneScroll.widthProperty().subtract(5));
        bookGrid.prefWidthProperty().bind(bookZoneScroll.widthProperty().subtract(5));
        bookGrid.getColumnConstraints().forEach(c -> c.setMaxWidth(Double.MAX_VALUE));
        // Debug
        bookSynopsisField.setStyle("-fx-border-color: #FF0000");
        bookDescriptionField.setStyle("-fx-border-color: #00FF00");
        
    }

    @Override
    public Object getItem() {
        return book;
    }

}

I'm using JavaFX 21 and Java 21.

Cheers


Solution

  • Some issues with your approach:

    1. You have a TableView (which is scrollable) inside a ScrollPane (which is scrollable). That usually isn't a good design. It results in one scrollable thing inside another, making for a difficult-to-use interface for your users.

    2. You are setting many more sizing constraints than needed, both in the FXML and code. Any constraints you add make the layout less flexible. So it is best to keep the number of constraints set to a minimum.

    3. AnchorPane is often not a good choice for a resizable layout, it is primarily designed to facilitate absolute layout positioning.

    4. Binding is a layout mechanism of last resort.

      • It is usually better to choose appropriate layout panes and constraints and let the layout panes manage the layout, instead of using binding to control the layout.

      • If no appropriate layout pane (or combination of panes) exists then you can create a custom layout pane design, but that is usually quite difficult and advanced, and, thankfully, usually not necessary.

    Example Code

    Here is an example that addresses some of the issues with your implementation.

    1. The outer ScrollPane is removed

      • The user can scroll the table if they want to see additional info in the table, but other elements are not scrollable.
    2. Minimal constraint information is provided in the FXML and code.

    3. Unnecessary container elements such as AnchorPanes are removed from the implementation.

    4. No binding is used to control layout.

    books

    book.fxml

    <?xml version="1.0" encoding="UTF-8"?>
    
    <?import javafx.geometry.Insets?>
    <?import javafx.scene.control.Label?>
    <?import javafx.scene.control.TableColumn?>
    <?import javafx.scene.control.TableView?>
    <?import javafx.scene.layout.ColumnConstraints?>
    <?import javafx.scene.layout.GridPane?>
    <?import javafx.scene.layout.RowConstraints?>
    <?import javafx.scene.layout.VBox?>
    <?import javafx.scene.text.TextFlow?>
    
    <VBox xmlns="http://javafx.com/javafx/19" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.test.demo.books.BookController">
        <children>
            <Label fx:id="bookTitle" text="Book title" />
            <GridPane hgap="5.0" vgap="3.0">
                <children>
                    <Label text="Synopsis" GridPane.valignment="TOP" />
                    <Label minHeight="-Infinity" text="Description" GridPane.rowIndex="1" GridPane.valignment="TOP" />
                    <TextFlow fx:id="bookSynopsisField" textAlignment="JUSTIFY" GridPane.columnIndex="1" GridPane.hgrow="ALWAYS" />
                    <TextFlow fx:id="bookDescriptionField" textAlignment="JUSTIFY" GridPane.columnIndex="1" GridPane.hgrow="ALWAYS" GridPane.rowIndex="1" />
                </children>
                <padding>
                    <Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
                </padding>
             <columnConstraints>
                <ColumnConstraints minWidth="-Infinity" />
                <ColumnConstraints />
             </columnConstraints>
             <rowConstraints>
                <RowConstraints />
                <RowConstraints />
             </rowConstraints>
            </GridPane>
          <VBox VBox.vgrow="ALWAYS">
             <children>
                    <Label text="Metadata" />
                  <TableView fx:id="bookMetadataTable" prefHeight="150.0" VBox.vgrow="ALWAYS">
                      <columns>
                          <TableColumn fx:id="fieldColumn" editable="false" prefWidth="75.0" sortable="false" text="Field" />
                          <TableColumn fx:id="valueColumn" editable="false" prefWidth="215.0" sortable="false" text="Value" />
                      </columns>
                  </TableView>
             </children>
             <padding>
                <Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
             </padding>
          </VBox>
        </children>
    </VBox>
    

    BookApp.java

    package com.test.demo.books;
    
    import javafx.application.Application;
    import javafx.fxml.FXMLLoader;
    import javafx.scene.Scene;
    import javafx.scene.layout.Pane;
    import javafx.stage.Stage;
    
    import java.io.IOException;
    import java.util.Objects;
    
    public class BookApp extends Application {
        @Override
        public void start(Stage stage) throws IOException {
            FXMLLoader loader = new FXMLLoader(
                    Objects.requireNonNull(
                            BookApp.class.getResource(
                                    "book.fxml"
                            )
                    )
            );
    
            Pane root = loader.load();
            root.setPrefWidth(500);
    
            stage.setScene(new Scene(root));
            stage.show();
        }
    }
    

    BookController.java

    package com.test.demo.books;
    
    import javafx.beans.property.ReadOnlyStringWrapper;
    import javafx.fxml.FXML;
    import javafx.scene.control.TableColumn;
    import javafx.scene.control.TableView;
    import javafx.scene.control.Label;
    import javafx.scene.text.Text;
    import javafx.scene.text.TextFlow;
    
    import java.util.Arrays;
    
    public class BookController {
    
        public static final String SYNOPSIS = "I've been having issues with some Textflows overflowing from their defined cell in a GridPane. My guess would be that no prefHeight is defined but I'm not managing to find how to calculate this height in order to set or bind it.";
    
        private record Metadata(String field, String value) {}
    
        @FXML
        private TextFlow bookDescriptionField;
    
        @FXML
        private TableView<Metadata> bookMetadataTable;
    
        @FXML
        private TextFlow bookSynopsisField;
    
        @FXML
        private Label bookTitle;
    
        @FXML
        private TableColumn<Metadata, String> fieldColumn;
    
        @FXML
        private TableColumn<Metadata, String> valueColumn;
    
        @FXML
        private void initialize() {
            bookTitle.setText("Textflow overflowing and overlapping from GridPane cell");
    
            bookDescriptionField.getChildren().add(new Text("My intent is to have the TextFlow takes as much size as it needs to display the text fully, as the whole GridPane is in a ScrollPane."));
            bookSynopsisField.getChildren().add(new Text(SYNOPSIS));
    
            fieldColumn.setCellValueFactory(p -> new ReadOnlyStringWrapper(p.getValue().field));
            valueColumn.setCellValueFactory(p -> new ReadOnlyStringWrapper(p.getValue().value));
    
            bookMetadataTable.getItems().setAll(
                    Arrays.stream(SYNOPSIS.split(" ")).map(s -> new Metadata(s, s)).toList()
            );
        }
    }
    

    Alternative Implementation: ScrollPane with Grid

    If you wanted to keep the outer ScrollPane, then I would recommend designing a new component based on GridPane for displaying your book metadata in a tabular format, rather than using a TableView. That is outside the scope of what I would be prepared to do here in this answer, but if you have questions about that, then ask about it specifically in a new question. If you do so, for context, you can put a link to this question in your new question.