javajavafxprocessjavafx-8daemon

Stream Process Output to Java FX Text Area with Auto Scroll


I'm building and app that creates a process, and streams its stdout into a text area. Just like terminal emulators, I want the output to automatically scroll to the end of the console if the process shows too many lines.

The problem is the text area does not scroll to the end of the text, but rather stay at the top. Another weird behavior is when I try to scroll the text area using mouse wheel / keyboard / scroll bar, the text area scrolls to the very top.

I already tried the answers here, here, and here. It seems that I have something wrong somewhere else.

Here is the content of method void streamToTextArea(java.lang.Process process) - The method that does the heavy lifting in executing the process and streaming it to the consoleTextArea.

Task bgTask = new Task<Void>() {
    @Override
    protected void call() throws Exception {
        InputStream inputStream = process.getInputStream();
        StringBuilder consoleContent = new StringBuilder();

       try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
           String line;
           while((line = reader.readLine()) != null) {
               consoleContent
                   .append(line)
                   .append("\n");
               updateMessage(consoleContent.toString());
               consoleTextArea.appendText(""); // trigger ChangeListener
           }
       } catch (IOException e) { doSomething(e); }
       return null;
    }
};

StringProperty textProperty = consoleTextArea.textProperty();
textProperty.addListener((observable, oldValue, newValue) -> {
    // currently:
    consoleTextArea.selectPositionCaret(consoleTextArea.getLength());
    consoleTextArea.deselect();

    // also tried:
    // consoleTextArea.setScrollTop(Double.MAX);
});
textProperty.bind(bgTask.messageProperty());

// start bgTask as daemon thread
// add event handling when bgTask ended

Solution

  • First of all Task.call() method is not executed in JavaFX Application Thread, so changing any state of current view in this method is inappropriate. You are adding "" to consoleTextArea in wrong thread. You should do it like this:

    Platform.runLater(() -> {
        consoleTextArea.appendText("");
    });
    

    Second issue is that invoking consoleTextArea.appendText(""); will not trigger your ChangeListener(In fact, it will do nothing), because you bind consoleTextArea text property to Task message property textProperty.bind(bgTask.messageProperty());. In that case text area will only listen to text change in Task message property. Add your listener to message property:

    bgTask.messageProperty().addListener((observable, oldValue, newValue) -> {
        // currently:
        consoleTextArea.selectPositionCaret(consoleTextArea.getLength());
        consoleTextArea.deselect();
    
        // also tried:
        // consoleTextArea.setScrollTop(Double.MAX);
    });