I am creating a basic terminal chat application in Java using Lanterna. I have a TextBox
component that I call addLine()
on as new messages come in. The default behavior of a TextBox
appears to be to maintain its previous scroll position until the user focuses on it and scrolls manually. I would like for the TextBox
itself to scroll to the bottom automatically.
There is no obvious way to set the scroll position of a TextBox
programmatically, so my best idea was to extend TextBox
and make my own version of addLine()
:
import com.googlecode.lanterna.TerminalSize;
import com.googlecode.lanterna.gui2.TextBox;
public class ChatWindowTextBox extends TextBox{
public ChatWindowTextBox(TerminalSize preferredSize, Style style){
super(preferredSize, style);
}
public ChatWindowTextBox addLineAndScrollDown(String line){
addLine(line);
setReadOnly(false); // I make the chat window read only
// in my screen setup, so I undo that
takeFocus();
setCaretPosition(Integer.MAX_VALUE, Integer.MAX_VALUE);
setReadOnly(true);
return this;
}
}
Via debugging, I have verified that the arguments to setCaretPosition
get correctly clamped to the actual values of the last line and column, and the internal value of caretPosition
is updated to those values. However, this does not make the TextBox
scroll. Have I misunderstood how setCaretPosition()
behaves, and is there a viable way to programmatically make a TextBox
scroll to the bottom?
A solution I found is to force the program to pause before making the ChatWindowTextBox
read-only again:
public ChatWindowTextBox addLineAndScrollDown(String line){
super.addLine(line);
setReadOnly(false);
takeFocus();
setCaretPosition(Integer.MAX_VALUE, Integer.MAX_VALUE);
try {
Thread.sleep(15);
} catch (InterruptedException e) {
e.printStackTrace();
}
setReadOnly(true);
return this;
}
15 ms is about the smallest pause that allows the ChatWindowTextBox
to scroll as expected. You may want to go for 20 or 30 ms.
The primary problem with the original code has to do with making the ChatWindowTextBox
read-only. The solution proposed in the question actually works perfectly for TextBox
es that are not made read-only.
Digging into Lanterna's source code, the caret position of a TextBox
is not taken into account if it was set to read-only. However, the code in the question would appear to account for this by unsetting readOnly
, changing the caret position, then resetting readOnly
. So why is there unexpected behavior? Internally, readOnly()
also calls invalidate()
, which according to Lanterna, "Marks the component as invalid and requiring to be drawn at next opportunity." I assume that the ChatWindowTextBox
being invalidated and soon redrawn causes the change in caret position to not completely go through before it is made read-only again.