Issue
I want to display some formatted text using TextFlow. Previousely, I used a simple Label (with wrapText set to true) to display that text (unformatted), but want to make use of a Library that provides a List of Texts that I would like to display using a TextFlow.
My problem is that the text I want to display is larger than the available Area. Labels cut off the text when running out of space. This works great. Unfortunately TextFlow does not. When the text gets too long, it overflows the Region the TextFlow is in. Neighboring TextFlows then overlap each other. How can I mimic the behavior of the Label?
An MWE can be found here and below. I use a GridPane with two columns. Three TextFlows on the left, three Labels at the right. The displayed text is the same for all six elements. It produces this window:
As you can see, the text on the left (in the TextFlows) overlaps.
I tried, without success:
- Setting the maxWidth and maxHeight of the TextFlow to the available Area
- Creating a rectangle of appropriate size and setting it as a clip
JAVA:
package sample;
import javafx.application.Application;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.text.Text;
import javafx.scene.text.TextFlow;
import javafx.stage.Stage;
public class Main extends Application {
@FXML
private TextFlow textFlow0;
@FXML
private TextFlow textFlow1;
@FXML
private TextFlow textFlow2;
@FXML
private Label label0;
@FXML
private Label label1;
@FXML
private Label label2;
private String longText = "This is some really long text that should overflow the available Area. " +
"For TextFields, this is handeled by cropping the text to appropriate length and adding \"...\" at the end. " +
"No such option exists for TextFlows";
@Override
public void start(Stage primaryStage) throws Exception{
Parent root = FXMLLoader.load(getClass().getResource("sample.fxml"));
primaryStage.setTitle("Text Overflow");
primaryStage.setScene(new Scene(root, 300, 275));
primaryStage.show();
}
@FXML
private void initialize() {
textFlow0.getChildren().add(new Text(longText));
textFlow1.getChildren().add(new Text(longText));
textFlow2.getChildren().add(new Text(longText));
label0.setText(longText);
label1.setText(longText);
label2.setText(longText);
}
public static void main(String[] args) {
launch(args);
}
}
FXML:
<?import javafx.scene.control.Label?>
<GridPane fx:controller="sample.Main"
xmlns:fx="http://javafx.com/fxml" alignment="center" hgap="10" vgap="10">
<TextFlow fx:id="textFlow0" GridPane.rowIndex = "0" GridPane.columnIndex="0" />
<Label fx:id="label0" GridPane.rowIndex = "0" wrapText="true" GridPane.columnIndex="1"/>
<TextFlow fx:id="textFlow1" GridPane.rowIndex = "1" GridPane.columnIndex="0" />
<Label fx:id="label1" GridPane.rowIndex = "1" wrapText="true" GridPane.columnIndex="1"/>
<TextFlow fx:id="textFlow2" GridPane.rowIndex = "2" GridPane.columnIndex="0" />
<Label fx:id="label2" GridPane.rowIndex = "2" wrapText="true" GridPane.columnIndex="1"/>
</GridPane>
Failed: attempt to use clip
package sample;
import javafx.application.Application;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.layout.FlowPane;
import javafx.scene.shape.Rectangle;
import javafx.scene.text.Text;
import javafx.scene.text.TextFlow;
import javafx.stage.Stage;
public class Main extends Application {
@FXML
private FlowPane flowPane;
@FXML
private TextFlow textFlow0;
@FXML
private TextFlow textFlow1;
@FXML
private TextFlow textFlow2;
@FXML
private Label label0;
@FXML
private Label label1;
@FXML
private Label label2;
private String longText = "This is some really long text that should overflow the available Area. " +
"For TextFields, this is handeled by cropping the text to appropriate length and adding \"...\" at the end. " +
"No such option exists for TextFlows";
@Override
public void start(Stage primaryStage) throws Exception{
Parent root = FXMLLoader.load(getClass().getResource("sample.fxml"));
primaryStage.setTitle("Text Overflow");
primaryStage.setScene(new Scene(root, 300, 275));
primaryStage.show();
}
@FXML
private void initialize() {
flowPane.setPrefWrapLength(Double.MAX_VALUE);
Rectangle rect = new Rectangle();
rect.widthProperty().bind(flowPane.widthProperty());
rect.heightProperty().bind(flowPane.heightProperty());
flowPane.setClip(rect);
textFlow0.getChildren().add(new Text(longText));
textFlow1.getChildren().add(new Text(longText));
textFlow2.getChildren().add(new Text(longText));
label0.setText(longText);
label1.setText(longText);
label2.setText(longText);
}
public static void main(String[] args) {
launch(args);
}
}
FXML file for clip attempt
<?import javafx.scene.layout.GridPane?>
<?import javafx.scene.text.TextFlow?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.layout.FlowPane?>
<GridPane fx:controller="sample.Main"
xmlns:fx="http://javafx.com/fxml" alignment="center" hgap="10" vgap="10">
<FlowPane fx:id="flowPane" GridPane.rowIndex = "0" GridPane.columnIndex="0">
<TextFlow fx:id="textFlow0" />
</FlowPane>
<Label fx:id="label0" GridPane.rowIndex = "0" wrapText="true" GridPane.columnIndex="1"/>
<TextFlow fx:id="textFlow1" GridPane.rowIndex = "1" GridPane.columnIndex="0" />
<Label fx:id="label1" GridPane.rowIndex = "1" wrapText="true" GridPane.columnIndex="1"/>
<TextFlow fx:id="textFlow2" GridPane.rowIndex = "2" GridPane.columnIndex="0" />
<Label fx:id="label2" GridPane.rowIndex = "2" wrapText="true" GridPane.columnIndex="1"/>
</GridPane>
Solution
As there seems to be no built-in way to do this, I implemented my own. It's probably not the most efficient way to tackle this problem, but satisfies my use-case pretty well. If better solutions pop up in the next days, I will accept one of them. If not, I will select this answer as accepted.
There still is one problem: I need to click the window once for the text to show up in the beginning. Also, there is one major problem: What to do if a child node is not a Text object?
package sample;
import javafx.beans.DefaultProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ChangeListener;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.scene.Node;
import javafx.scene.text.Text;
import javafx.scene.text.TextFlow;
@DefaultProperty("children")
public class EllipsingTextFlow extends TextFlow {
private final static String DEFAULT_ELLIPSIS_STRING = "...";
private StringProperty ellipsisString;
//private ListProperty<Node> allChildren = new SimpleListProperty<Node>(new SimpleObs<Node>());
private ObservableList<Node> allChildren = FXCollections.observableArrayList();
private ChangeListener sizeChangeListener = (observableValue, number, t1) -> adjustText();
public EllipsingTextFlow() {
allChildren.addListener((ListChangeListener<Node>) this::adjustChildren);
widthProperty().addListener(sizeChangeListener);
heightProperty().addListener(sizeChangeListener);
adjustText();
}
@Override
public ObservableList<Node> getChildren() {
return allChildren;
}
private void adjustChildren(ListChangeListener.Change<? extends Node> change) {
while (change.next()) {
if (change.wasRemoved()) {
super.getChildren().remove(change.getFrom(), change.getTo());
} else if (change.wasAdded()) {
super.getChildren().addAll(change.getFrom(), change.getAddedSubList());
}
}
adjustText();
}
private void adjustText() {
// remove listeners
widthProperty().removeListener(sizeChangeListener);
heightProperty().removeListener(sizeChangeListener);
while (getHeight() > getMaxHeight() || getWidth() > getMaxWidth()) {
if (super.getChildren().isEmpty()) {
// nothing fits
widthProperty().addListener(sizeChangeListener);
heightProperty().addListener(sizeChangeListener);
return;
}
super.getChildren().remove(super.getChildren().size()-1);
super.autosize();
}
while (getHeight() <= getMaxHeight() && getWidth() <= getMaxWidth()) {
if (super.getChildren().size() == allChildren.size()) {
if (allChildren.size() > 0) {
// all Texts are displayed, let's make sure all chars are as well
Node lastChildAsShown = super.getChildren().get(super.getChildren().size() - 1);
Node lastChild = allChildren.get(allChildren.size() - 1);
if (lastChildAsShown instanceof Text && ((Text) lastChildAsShown).getText().length() < ((Text) lastChild).getText().length()) {
((Text) lastChildAsShown).setText(((Text) lastChild).getText());
} else {
// nothing to fill the space with
widthProperty().addListener(sizeChangeListener);
heightProperty().addListener(sizeChangeListener);
return;
}
}
} else {
super.getChildren().add(allChildren.get(super.getChildren().size()));
}
super.autosize();
}
// ellipse the last text as much as necessary
while (getHeight() > getMaxHeight() || getWidth() > getMaxWidth()) {
Node lastChildAsShown = super.getChildren().remove(super.getChildren().size()-1);
while (getEllipsisString().equals(((Text) lastChildAsShown).getText())) {
if (super.getChildren().size() == 0) {
widthProperty().addListener(sizeChangeListener);
heightProperty().addListener(sizeChangeListener);
return;
}
lastChildAsShown = super.getChildren().remove(super.getChildren().size() -1);
}
if (lastChildAsShown instanceof Text && ((Text) lastChildAsShown).getText().length() > 0) {
// Text shortenedChild = new Text(((Text) lastChildAsShown).getText().substring(0, ((Text) lastChildAsShown).getText().length()-1));
Text shortenedChild = new Text(ellipseString(((Text) lastChildAsShown).getText()));
super.getChildren().add(shortenedChild);
} else {
// don't know what to do with anything else. Leave without adding listeners
return;
}
super.autosize();
}
widthProperty().addListener(sizeChangeListener);
heightProperty().addListener(sizeChangeListener);
}
private String ellipseString(String s) {
int spacePos = s.lastIndexOf(' ');
if (spacePos < 0) {
return getEllipsisString();
}
return s.substring(0, spacePos) + getEllipsisString();
}
public final void setEllipsisString(String value) {
ellipsisString.set((value == null) ? "" : value);
}
public String getEllipsisString() {
return ellipsisString == null ? DEFAULT_ELLIPSIS_STRING : ellipsisString.get();
}
public final StringProperty ellipsisStringProperty(){
return ellipsisString;
}
}
Answered By - BenT