Search code examples
javalanterna

Lanterna - How to make a TextBox scroll to the bottom upon adding more text


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?


Solution

  • 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 TextBoxes 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.