How can I make a TextArea stretch to fill the content, expanding the parent in the process?
Asked Answered
B

6

18

So I have a TextArea and as the user pastes paragraphs into it, or just writes in it, I want it to expand vertically to reveal all the available text. I.e. not to use a scrollbar in the text field itself... much like what happens on many web pages. Many users, myself included, don't like to be forced to edit in a small window. Exactly how Facebook status updates box works.

I've tried

myTextArea.autoSize()

wrapped in an

myTextArea.textProperty().addListener(new ChangeListener()....);

but that doesn't work. I think it's happy autosizing to its current size.

The left, right & top anchors are set to it's parent AnchorPane. I've tried it with the bottom attached and not attached. Ideally I'd like to grow the anchor pane as the textarea grows.

I don't mind reading the TextProperty and calculating a trigger size which I set myself... but this seems a hacky approach IF there is already a best practise. The number of properties and sub objects of javafx is sufficiently daunting that it seems like a good point to ask the question here, rather than trying to figure out how many pixels the font/paragraphs etc are taking up.

Update:

So I thought maybe I was overthinking it, and all I needed to do was to switch the scrollbars off and the rest would happen. Alas, looking for available fields and methods for "scroll", "vertical", "vbar" comes up with nothing I can use. ScrollTopProperty looks like it's for something else.

Blanche answered 3/9, 2013 at 9:32 Comment(1)
here is a sample hot it can be achived: #44141672Lichfield
I
15

The problem; the height of textArea is wanted to be grown or shrunk while its text is changing by either user's typing or copy-pasting. Here is another approach:

public class TextAreaDemo extends Application {

    private Text textHolder = new Text();
    private double oldHeight = 0;

    @Override
    public void start(Stage primaryStage) {
        final TextArea textArea = new TextArea();
        textArea.setPrefSize(200, 40);
        textArea.setWrapText(true);

        textHolder.textProperty().bind(textArea.textProperty());
        textHolder.layoutBoundsProperty().addListener(new ChangeListener<Bounds>() {
            @Override
            public void changed(ObservableValue<? extends Bounds> observable, Bounds oldValue, Bounds newValue) {
                if (oldHeight != newValue.getHeight()) {
                    System.out.println("newValue = " + newValue.getHeight());
                    oldHeight = newValue.getHeight();
                    textArea.setPrefHeight(textHolder.getLayoutBounds().getHeight() + 20); // +20 is for paddings
                }
            }
        });

        Group root = new Group(textArea);
        Scene scene = new Scene(root, 300, 250);
        primaryStage.setScene(scene);
        primaryStage.show();

        //  See the explanation below of the following line. 
        //  textHolder.setWrappingWidth(textArea.getWidth() - 10);  // -10 for left-right padding. Exact value can be obtained from caspian.css
    }

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

But it has a drawback; the textarea's height is changing only if there are line breaks (ie Enter keys) between multiple lines, if the user types long enough the text gets wrapped to multiple line but the height is not changing.

To workaround this drawback I added this line

textHolder.setWrappingWidth(textArea.getWidth() - 10);

after primaryStage.show();. It works well for long typings where user does not linebreaks. However this generates another problem. This problem occurs when the user is deleting the text by hitting "backspace". The problem occurs exactly when the textHolder height is changed and where the textArea's height is set to new value. IMO it maybe a bug, didn't observe deeper.

In both case the copy-pasting is handling properly.

Interment answered 4/10, 2013 at 20:26 Comment(4)
Like you said, it has a few problems. Though at this point it's the best option I've seen. :/Overrate
this really is the best solution i have seen so far. makes me really hate javafx that such an obvious problem only has hacky solutions.Margaretemargaretha
anyway, i would suggest textHolder.getLayoutBounds().getHeight()*1.05 as my TextArea became too small if there were too many lines added. the *1.05 seems to perfectly to the trick.Margaretemargaretha
also: if you can't call textHolder.setWrappingWidth(textArea.getWidth() - 10); after primaryStage.show(); you might as well just call textHolder.setWrappingWidth(textArea.getPrefWidth() - 10);. Just make sure to call it again if you change the prefWidth. I also changed the -10 to -20 as it didn't break early enough. or bind it to the widthProperty, that would probably work betterMargaretemargaretha
H
9

Awaiting a better, i use this hacky solution.

  • lookup the vertical scrollbar of the textarea.
  • make it transparent
  • listen to its visible property
  • when the scrollbar become visible i add a row to the textarea.

The code:

import javafx.application.Application;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.ObservableList;
import javafx.geometry.Orientation;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.ScrollBar;
import javafx.scene.control.TextArea;
import javafx.scene.layout.AnchorPane;
import javafx.stage.Stage;

public class GrowGrowTextArea extends Application {
    public static void main(String[] args) {
        launch(args);
    }

    @Override
    public void start(Stage primaryStage) throws Exception {
        AnchorPane root = new AnchorPane();
        root.setStyle("-fx-padding:20;-fx-background-color:dodgerblue;");
        final TextArea textArea = new TextArea();
        AnchorPane.setTopAnchor(textArea, 10.0);
        AnchorPane.setLeftAnchor(textArea, 10.0);
        AnchorPane.setRightAnchor(textArea, 10.0);
        root.getChildren().add(textArea);
        primaryStage.setScene(new Scene(root, 400, 300));
        primaryStage.show();
        ScrollBar scrollBar = lookupVerticalScrollBar(textArea);
        scrollBar.setOpacity(0.0);
        scrollBar.visibleProperty().addListener(new ChangeListener<Boolean>() {

            @Override
            public void changed(ObservableValue<? extends Boolean> source,
                                Boolean wasVisible,
                                Boolean isVisible) {
                if (isVisible) {
                    textArea.setPrefRowCount(textArea.getPrefRowCount() + 1);
                    textArea.requestLayout();
                }
            }
        });

    }

    private ScrollBar lookupVerticalScrollBar(Node node) {
        if (node instanceof ScrollBar && ((ScrollBar)node).getOrientation() == Orientation.VERTICAL) {
            return (ScrollBar) node;
        }
        if (node instanceof Parent) {
            ObservableList<Node> children = ((Parent) node).getChildrenUnmodifiable();
            for (Node child : children) {
                ScrollBar scrollBar = lookupVerticalScrollBar(child);
                if (scrollBar != null) {
                    return scrollBar;
                }
            }
        }
        return null;
    }
}
Harbison answered 30/9, 2013 at 7:58 Comment(2)
Thanx for posting! Will mark it correct when I have time to check... which isn't at the moment unfortunately.Blanche
Thanks gontard. Though as you said, it is a bit hacky...but cleaver none-the-less. I'm finding that it just doesn't quite behave right. It doesn't expand correctly if there are blank lines in the text area and also waits to expand until characters are entered on the last line.Overrate
K
2

I had a similar problem with creating expanding TextArea. I was creating TextArea that looks like TextField and expand vertically every time when there is no more space in line. I have tested all solutions that I could find on this topic on stack and other sources available. I found few good solutions but neither was good enough.

After many hours of fighting, I figured out this approach.

I extended TextArea class, override layoutChildren() method and add a listener on text height.

  @Override
  protected void layoutChildren() {
    super.layoutChildren();
    setWrapText(true);
    addListenerToTextHeight();
  }

  private void addListenerToTextHeight() {
    ScrollPane scrollPane = (ScrollPane) lookup(".scroll-pane");
    scrollPane.setHbarPolicy(ScrollBarPolicy.NEVER);
    scrollPane.setVbarPolicy(ScrollBarPolicy.NEVER);

    StackPane viewport = (StackPane) scrollPane.lookup(".viewport");

    Region content = (Region) viewport.lookup(".content");

    Text text = (Text) content.lookup(".text");
    text.textProperty().addListener(textHeightListener(text));
  }

  private InvalidationListener textHeightListener(Text text) {
    return (property) -> {
      // + 1 for little margin
      double textHeight = text.getBoundsInLocal().getHeight() + 1;

      //To prevent that our TextArena will be smaller than our TextField
      //I used DEFAULT_HEIGHT = 18.0
      if (textHeight < DEFAULT_HEIGHT) {
        textHeight = DEFAULT_HEIGHT;
      }

      setMinHeight(textHeight);
      setPrefHeight(textHeight);
      setMaxHeight(textHeight);
    };
  }
Kenner answered 5/10, 2017 at 16:26 Comment(0)
I
0

I used some of the code found in the previous answers. The growTextAreaIfNecessary method will increase the height of textArea until the scrollbar is not visible (limited to 20 lines in this example).

The problem with this approach is that the window needs to be redrawn several times until the perfect height is found.

private ScrollBar lookupVerticalScrollBar(Node node) {
    if (node instanceof ScrollBar && ((ScrollBar) node).getOrientation() == Orientation.VERTICAL) {
        return (ScrollBar) node;
    }
    if (node instanceof Parent) {
        ObservableList<Node> children = ((Parent) node).getChildrenUnmodifiable();
        for (Node child : children) {
            ScrollBar scrollBar = lookupVerticalScrollBar(child);
            if (scrollBar != null) {
                return scrollBar;
            }
        }
    }
    return null;
}

private void growTextAreaIfNecessary(TextArea textArea) {
    Platform.runLater(() -> {
        ScrollBar lookupVerticalScrollBar = lookupVerticalScrollBar(textArea);
        int prefRowCount = textArea.getPrefRowCount();

        if (lookupVerticalScrollBar.isVisible() && prefRowCount < 20) {
            textArea.setPrefRowCount(prefRowCount + 1);
            System.out.println("increasing height to: " + (prefRowCount + 1));
            growTextAreaIfNecessary(textArea);
        }
    });
}
Istle answered 12/5, 2020 at 17:17 Comment(0)
U
0

I have tried many hacks, most of them had jitters while typing, this to me was the perfect result:

textArea.textProperty().addListener((obs,old,niu)->{
            Text t = new Text(old+niu);
            t.setFont(textArea.getFont());
            StackPane pane = new StackPane(t);
            pane.layout();
            double height = t.getLayoutBounds().getHeight();
            double padding = 20 ;
            textArea.setMinHeight(height+padding);
        });
Upu answered 28/1, 2022 at 17:15 Comment(0)
V
0
    textArea.textProperty().addListener((observable, oldValue, newValue)->
    {
        Text t = new Text(oldValue + newValue);
        t.setFont(ta.getFont());
        t.setWrappingWidth(textArea.getWidth());
        textArea.setMinHeight(t.getLayoutBounds().getHeight());
    });   
Vaticinate answered 7/2 at 9:55 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.