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?
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:
- The TextProperty is updated
- Your listener is called, and the scroll is set
- doSet is called
- textUpdated is called
- The scroll is set back to the top-left
When you then append "",
- The TextProperty is updated
- 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.
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.
Platform.runLater(() -> this.setScrollTop(Double.MAX_VALUE));
or setScrollTop(Double.MAX_VALUE)
. In addition my Textarea seems to be blurry. –
Archean 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;
}
}
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:
- The TextProperty is updated
- Your listener is called, and the scroll is set
- doSet is called
- textUpdated is called
- The scroll is set back to the top-left
When you then append "",
- The TextProperty is updated
- 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.
Alternative to that strange setText bug without using appendText
textArea.selectPositionCaret(textArea.getLength());
textArea.deselect(); //removes the highlighting
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.
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);
};
© 2022 - 2024 — McMap. All rights reserved.
setScrollTop(Double.MIN_VALUE);
scrolls to the top, whileMAX_VALUE
scrolls to the bottom. – Colbert