JavaFX TextArea and autoscroll
Asked Answered
F

6

23

I am trying to get a TextArea to autoscroll to the bottom with new text which is put in via an event handler. Each new entry is just one long string of text with each entry separated by a line break. I have tried a change handler which sets setscrolltop to Double.MIN_VALUE but to no avail. Any ideas of how this could be done?

Fencer answered 22/7, 2013 at 23:30 Comment(0)
D
10

I don't have enough reputation to comment, but wanted to give some insight for future readers as to why setText doesn't appear to trigger the listener, but appendText does, as in Math's answer.

I Just found this answer while encountering similar issues myself, and looked into the code. This is currently the top result for 'javafx textarea settext scroll' in a google search.

setText does indeed trigger the listener. According to the javadoc on the doSet method in TextInputControl (TextArea's superclass):

     * doSet is called whenever the setText() method was called directly
     * on the TextInputControl, or when the text property was bound,
     * unbound, or reacted to a binding invalidation. It is *not* called
     * when modifications to the content happened indirectly, such as
     * through the replaceText / replaceSelection methods.

Inside the doSet method, a call is made to updateText(), which TextArea overrides:

  @Override final void textUpdated() {
        setScrollTop(0);
        setScrollLeft(0);
    }  

So, when you set the scroll amount in the listener as in Math's answer, the following happens:

  1. The TextProperty is updated
  2. Your listener is called, and the scroll is set
  3. doSet is called
  4. textUpdated is called
  5. The scroll is set back to the top-left

When you then append "",

  1. The TextProperty is updated
  2. Your listener is called, and the scroll is set

The javadoc is above is clear why this is the case - doSet is only called when using setText. In fact, appendText calls insertText which calls replaceText - and the javadoc further states that replaceText does NOT trigger a call to doSet.

The behaviour is rather irritating, especially since these are all final methods, and not obvious at first glance - but is not a bug.

Dyscrasia answered 29/12, 2018 at 17:48 Comment(0)
H
34

You have to add a listener to the TextArea element to scroll to the bottom when it's value is changed:

@FXML private TextArea txa; 

...

txa.textProperty().addListener(new ChangeListener<Object>() {
    @Override
    public void changed(ObservableValue<?> observable, Object oldValue,
            Object newValue) {
        txa.setScrollTop(Double.MAX_VALUE); //this will scroll to the bottom
        //use Double.MIN_VALUE to scroll to the top
    }
});

But this listener is not triggered when you use the setText(text) method, so if you want to trigger it after a setText(text) use the appendText(text) right after it:

txa.setText("Text into the textArea"); //does not trigger the listener
txa.appendText("");  //this will trigger the listener and will scroll the
                     //TextArea to the bottom

This sounds more like a bug, once the setText() should trigger the changed listener, however it doesn't. This is the workaround I use myself and hope it helps you.

Horsefly answered 13/12, 2013 at 13:53 Comment(5)
setScrollTop(Double.MIN_VALUE); scrolls to the top, while MAX_VALUE scrolls to the bottom.Colbert
@AdamJensen I'll make a test here, because I answered this some time ago and I just can't remember by looking at it. Thanks for the report.Horsefly
@AdamJensen you were absolutely right. I just fixed that. Thanks!Horsefly
I'd recommend setting the scroll within Platform.runLater() -- my text area sometimes missed some scroll updates without it.Denude
I can't seem to get it to work, my TextArea just stays scrolled to the Top. Any Ideas? Both using Platform.runLater(() -> this.setScrollTop(Double.MAX_VALUE)); or setScrollTop(Double.MAX_VALUE). In addition my Textarea seems to be blurry.Archean
I
14

txa.appendText("") will scroll to the bottom without a listener. This becomes an issue if you want to scroll back and the text is being constantly updated. txa.setText("") puts the scroll bar back at the top and same issue applies.

My solution was to extend the TextArea class, ammend the FXML tag from textArea to LogTextArea. Where this works, it clearly causes problems in scene builder as it does not know what this component is

import javafx.scene.control.TextArea;
import javafx.scene.text.Font;

public class LogTextArea extends TextArea {

private boolean pausedScroll = false;
private double scrollPosition = 0;

public LogTextArea() {
    super();
}

public void setMessage(String data) {
    if (pausedScroll) {
        scrollPosition = this.getScrollTop();
        this.setText(data);
        this.setScrollTop(scrollPosition);
    } else {
        this.setText(data);
        this.setScrollTop(Double.MAX_VALUE);
    }
}

public void pauseScroll(Boolean pause) {
    pausedScroll = pause;
}

}
Inquisitive answered 2/1, 2015 at 18:46 Comment(0)
D
10

I don't have enough reputation to comment, but wanted to give some insight for future readers as to why setText doesn't appear to trigger the listener, but appendText does, as in Math's answer.

I Just found this answer while encountering similar issues myself, and looked into the code. This is currently the top result for 'javafx textarea settext scroll' in a google search.

setText does indeed trigger the listener. According to the javadoc on the doSet method in TextInputControl (TextArea's superclass):

     * doSet is called whenever the setText() method was called directly
     * on the TextInputControl, or when the text property was bound,
     * unbound, or reacted to a binding invalidation. It is *not* called
     * when modifications to the content happened indirectly, such as
     * through the replaceText / replaceSelection methods.

Inside the doSet method, a call is made to updateText(), which TextArea overrides:

  @Override final void textUpdated() {
        setScrollTop(0);
        setScrollLeft(0);
    }  

So, when you set the scroll amount in the listener as in Math's answer, the following happens:

  1. The TextProperty is updated
  2. Your listener is called, and the scroll is set
  3. doSet is called
  4. textUpdated is called
  5. The scroll is set back to the top-left

When you then append "",

  1. The TextProperty is updated
  2. Your listener is called, and the scroll is set

The javadoc is above is clear why this is the case - doSet is only called when using setText. In fact, appendText calls insertText which calls replaceText - and the javadoc further states that replaceText does NOT trigger a call to doSet.

The behaviour is rather irritating, especially since these are all final methods, and not obvious at first glance - but is not a bug.

Dyscrasia answered 29/12, 2018 at 17:48 Comment(0)
I
8

Alternative to that strange setText bug without using appendText

textArea.selectPositionCaret(textArea.getLength());
textArea.deselect(); //removes the highlighting
Interdenominational answered 15/5, 2015 at 16:26 Comment(0)
P
2

One addendum I would add to jamesarbrown's response would be to this would be to use a boolean property instead so you can access it from within FXML. Something like this.

import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.scene.control.TextArea;

public class LogTextArea extends TextArea {
    private final BooleanProperty pausedScrollProperty = new SimpleBooleanProperty(false);
    private double scrollPosition = 0;

    public LogTextArea() {
        super();
    }

    public void setMessage(String data) {
        if (isPausedScroll()) {
            scrollPosition = this.getScrollTop();
            this.setText(data);
            this.setScrollTop(scrollPosition);
        } else {
            this.setText(data);
            this.setScrollTop(Double.MAX_VALUE);
        }
    }

    public final BooleanProperty pausedScrollProperty() { return pausedScrollProperty; }
    public final boolean isPausedScroll() { return pausedScrollProperty.getValue(); }
    public final void setPausedScroll(boolean value) { pausedScrollProperty.setValue(value); }
}

However, the problem with this answer is that if you get flooded with an unreasonably large amount of input (as can happen when retrieving a log from an IO Stream) the javaFX thread will lock up because the TextArea gets too much data.

Pergola answered 23/2, 2016 at 19:26 Comment(0)
B
0

As Matthew has posted the setText call is the problem. A easy workaround is to call clear, appendText and then setScrollTop. The other suggestions above did not work well for me, with enough delay it worked but was unreliable behaviour.

  textAreaListener = (observable, oldValue, newValue) -> {
            textArea.clear();
            textArea.appendText(newValue);
            textArea.setScrollTop(Double.MAX_VALUE);
        };
Bristling answered 16/1, 2022 at 1:14 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.