FXMisc / RichTextFX

Rich-text area for JavaFX
BSD 2-Clause "Simplified" License
1.21k stars 237 forks source link

Question: How to show whole text when text area is added to scene #1243

Closed PavelTurk closed 2 months ago

PavelTurk commented 2 months ago

Let's assume there is an unknown text with some styles.

Now, I need

  1. to create text area (InlineCssTextArea) with fixed width (for example, 500 pixels)
  2. to add text to it with styles
  3. to find whole document height
  4. to make text area height equal to document height
  5. to add text area to scene to show whole document without scrollbars.

As it is seen the problem is that I need whole document height when I only add text area to scene. I tried this code:

var messageArea = new InlineCssTextArea();
var scrollPane = new VirtualizedScrollPane(messageArea);
messageArea.totalHeightEstimateProperty().addListener((ov, oldV, newV) -> scrollPane.setMinHeight((double) newV));

This code works, but the problem is that when messageArea becomes visible it has one height and later its height is changed and user sees flickering. I tried to add

messageArea.setAutoHeight(true);
messageArea.applyCss();
messageArea.requestLayout();

But it didn't help. Could anyone say how to do it if it is possible?

Jugen commented 2 months ago

Not sure if this will work but let's start with just this:

var messageArea = new InlineCssTextArea();
messageArea.setPrefWidth(500);
messageArea.setMinWidth(500);
messageArea.setMaxWidth(500);
messageArea.setAutoHeight(true);
// add text to it with styles

So no scrollpane and no height property listener. If this doesn't work as you would like then we'll build on it.

PavelTurk commented 2 months ago

@Jugen Thank you very much for your help. I have two problems with the code you provided. This is the first problem. Code:

public class JavaFxTest extends Application {

    @Override
    public void start(Stage primaryStage) {
        var text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed lacinia, elit in bibendum tempor, purus dolor venenatis arcu, vitae congue dolor dui non tellus. Sed iaculis et risus sed scelerisque. Pellentesque id nibh sit amet magna molestie mollis in pharetra nunc. Suspendisse vitae pretium lacus. Fusce consectetur quam ac ante gravida gravida. Aenean nunc justo, vestibulum quis libero eget, semper condimentum turpis. Phasellus suscipit aliquam libero sit amet viverra. Fusce semper tempus lorem, at facilisis ante lacinia non.";

        var messageArea = new InlineCssTextArea();
        messageArea.setWrapText(true);
        messageArea.setEditable(false);
        messageArea.setPrefWidth(500);
        messageArea.setMinWidth(500);
        messageArea.setMaxWidth(500);
        messageArea.setAutoHeight(true);
        messageArea.appendText(text);

        var root = new VBox(messageArea);
        root.setStyle("-fx-background-color: red");
        var scene = new Scene(root, 600, 200);
        primaryStage.setTitle("JavaFX Test");
        primaryStage.setScene(scene);
        primaryStage.show();
    }

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

If you run this code, and after that click on text area you will see that autoheight is not very accurate:

Peek 2024-07-19 17-33

Or I do something wrong?

PavelTurk commented 2 months ago

This is the second problem. It seems that when autoHeight is used then getHeight() returns wrong value in PostLayoutPulseListener. This is my code:

public class JavaFxTest extends Application {

    private boolean pulsePassed = false;

    @Override
    public void start(Stage primaryStage) {
        var text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed lacinia, elit in bibendum tempor, purus dolor venenatis arcu, vitae congue dolor dui non tellus. Sed iaculis et risus sed scelerisque. Pellentesque id nibh sit amet magna molestie mollis in pharetra nunc. Suspendisse vitae pretium lacus. Fusce consectetur quam ac ante gravida gravida. Aenean nunc justo, vestibulum quis libero eget, semper condimentum turpis. Phasellus suscipit aliquam libero sit amet viverra. Fusce semper tempus lorem, at facilisis ante lacinia non.";
        var root = new StackPane();
        root.setStyle("-fx-background-color: red");
        var button = new Button("Push Me");
        root.getChildren().add(button);
        var scene = new Scene(root, 600, 400);
        primaryStage.setTitle("JavaFX Test");
        primaryStage.setScene(scene);
        primaryStage.show();

        button.setOnAction(e -> {
            var messageArea = new InlineCssTextArea();
            messageArea.setWrapText(true);
            messageArea.setEditable(false);
            messageArea.setPrefWidth(500);
            messageArea.setMinWidth(500);
            messageArea.setMaxWidth(500);
            messageArea.setAutoHeight(true);
            messageArea.appendText(text);

            var label = new Label(text);
            label.setWrapText(true);
            label.setStyle("-fx-background-color: cyan");
            label.setPrefWidth(500);
            label.setMinWidth(500);
            label.setMaxWidth(500);

            var box = new VBox(messageArea, label);
            box.sceneProperty().addListener((ov, oldV, newV) -> {
                if (newV != null) {
                    newV.addPostLayoutPulseListener(() -> {
                        if (!pulsePassed) {
                            this.pulsePassed = true;
                            System.out.println("TextArea height:" + messageArea.getHeight());
                            System.out.println("Label height:" + label.getHeight());
                        }
                    });
                }
            });
            root.getChildren().add(new Pane(box));
        });
    }

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

This is output:

TextArea height:100.0
Label height:128.0

And these are real sizes:

Screenshot from 2024-07-19 19-42-52

As you see textArea height is about 120 pixels (but not 100). At the same time label height it correct - 128 pixels.

Jugen commented 2 months ago

With your first code provided above I see what you mean in video but can't replicate on my system ? With the 2nd code I get the same "result" as you. It seems that the TextArea height requires another pulse to calculate correctly. If you change pulsePassed to an int and increment it, printing to console when it reaches 1 or 2 then it'll be correct. This maybe explains the flicker ?

PavelTurk commented 2 months ago

@Jugen About the first problem - I will try to run it on windows and play with font-size. BTW - maybe you could install virtual box with linux? It is very easy ... when it works :). There are also building problems on linux that we discussed.

About the second problem, yes, it can explain the flicker. But is it possible to make TextArea calculate height in the first pulse? Or is it impossible? I am asking because Every time a pulse occurs, this listener will be called on the JavaFX Application Thread directly after the CSS and layout passes, but before any rendering is done for this frame. so, it is normally to expect that when pulse listener is called all sizes must be already calculated (as I understand this javadoc).

Jugen commented 2 months ago

It seems that the delay only happens the first time the TextArea is rendered. This is because for the height to be calculated the width needs to known, so if the width is zero then a layout request is issued first to determine the width and this then results in the height only being calculated on the 2nd pass.

PavelTurk commented 2 months ago

@Jugen But we do set width : messageArea.setPrefWidth(500); messageArea.setMinWidth(500); messageArea.setMaxWidth(500);. And yes, it seems to happen the first time the TextArea is rendered. However, for my application it is a problem as I plan to use TextArea in dialogs and popups.

Jugen commented 2 months ago

Hmm, the TextArea requires its actual width. Those methods just specify the width limits and preference, not it's final rendered width which will depend on other things as well like the parent containers it is in.

You can try the following hack to prepare the messageArea for dialogs and popups:

// Dummy scene to trigger initial layout to set width to 500
new Scene( messageArea, 500, 50 ).snapshot( null ); 
messageArea.appendText( text ); // Must be after
// Add messageArea to the Dialog/Popup container
PavelTurk commented 2 months ago

@Jugen Can we add something like messageArea.setAutoHeight(true, 500); where 500 is the supposed real width? If I provide correct width, then I get correct height, if wrong, that's my fault.

PavelTurk commented 2 months ago

@Jugen Or messageArea.setAutoHeight(true, usePrefWidthForCalculation);

Jugen commented 2 months ago

Have submitted a PR that calculates the height using the preferred width when appropriate as suggested. Also updated latest maven snapshot including this PR. Please assess and provide feedback, thanks.

PavelTurk commented 2 months ago

@Jugen I checked maven snapshot for the tests of both problems. Everything seems to work:

Screenshot from 2024-07-24 22-33-48

with output:

TextArea height:135.0
Label height:128.0

Thank you very much for your help.