0

I'm trying to display text in TextArea with delay in between each sentence, like you're having a conversation. I tried using the sleep function but this doesn't work since the text only gets displayed when all methods stopped running. What would be an efficiënt way to do this:

(Pseudo code)

textArea.appendText("Goodday sir, how are you doing?");
(0.5 second delay);
textArea.appendText("I'm fine thanks");
(1 second delay);
textArea.appendText("What can I do for you?");
getPlayerInput();
textArea.appendText("Sure, I'll take care of it.");

To clarify what I'm trying to do: Display text in textArea with delays inbetween and be able to run functions in between.

Wouter A
  • 23
  • 6
  • First: The first 0,5sec should be a imput, too. Triggering inner authism - sry. – LenglBoy Apr 05 '18 at 12:27
  • You can use a `java.util-timer` which does represent the component `timeline`. [Here](https://stackoverflow.com/questions/9966136/javafx-periodic-background-task/9966213#9966213) you can find an example of it. you can trigger background tasks and it will work then. – LenglBoy Apr 05 '18 at 12:29
  • a) don't block the application thread b) `getPlayerInput` will probably not work, if you're expecting any user input, since JavaFX is event based and you're likely blocking the thread responsible for handling the events. c) take a look at multithreading+`Platform.runLater` or the `javafx.animation` package. – fabian Apr 05 '18 at 12:38

2 Answers2

1

you can use a Timeline's onFinished to make delayed actions in JavaFX

try the following code

package application;

import java.util.ArrayList;
import java.util.Iterator;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.TextArea;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
import javafx.util.Duration;

public class Main extends Application {
    Timeline delay = new Timeline();
    TextArea textArea = new TextArea();
    boolean waitForInput = false;
    Msg current;

    @Override
    public void start(Stage primaryStage) {

        StackPane root = new StackPane();

        root.getChildren().add(textArea);

        Scene scene = new Scene(root, 500, 500);

        ArrayList<Msg> msgs = new ArrayList<Msg>();
        msgs.add(new Msg("Goodday sir, how are you doing?\n", Duration.seconds(1), false));
        msgs.add(new Msg("i'm fine thanks!\n", Duration.seconds(2), false));
        msgs.add(new Msg("What can I do for you?\n", Duration.seconds(0.1), true));
        msgs.add(new Msg("Sure, I'll take care of it.\n", Duration.seconds(1), false));
        msgs.add(new Msg("....", Duration.seconds(0.5), false));
        msgs.add(new Msg("are you sure it's the only thing you need?\n", Duration.seconds(0.1), true));
        msgs.add(new Msg("alright bye", Duration.seconds(0), true));

        Iterator<Msg> it = msgs.iterator();
        delay.getKeyFrames().setAll(new KeyFrame(Duration.seconds(0)));
        delay.setOnFinished(e -> {
            if (it.hasNext()) {
                current = it.next();
                delay.getKeyFrames().setAll(new KeyFrame(current.getDuration()));
                delay.playFromStart();
                textArea.appendText(current.getContent());
                if (current.requiresInput()) {
                    waitForInput = true;
                    delay.pause();
                }
            }
        });
        delay.playFromStart();

        primaryStage.setScene(scene);
        primaryStage.show();

        scene.addEventFilter(KeyEvent.KEY_PRESSED, e ->
        {
            if (waitForInput && e.getCode().equals(KeyCode.ENTER)) {
                delay.play();
                waitForInput = false;
            }
        });
        scene.addEventFilter(KeyEvent.KEY_TYPED, e -> {
            if (!waitForInput) {
                e.consume();
            }
        });

    }

    public static void main(String[] args) {
        launch(args);
    }

    class Msg {
        private boolean requireInput;
        private String content;
        private Duration duration;

        public Msg(String c, Duration d, boolean b) {
            content = c;
            duration = d;
            requireInput = b;
        }

        public String getContent() {
            return content;
        }

        public Duration getDuration() {
            return duration;
        }

        public boolean requiresInput() {
            return requireInput;
        }
    }
}
SDIDSA
  • 894
  • 10
  • 19
  • A slight variation on this, which might be a little simpler, would be to have multiple key frames (say, at 1 second and 2 seconds), with event handlers for the key frames that appended the various strings. Then you wouldn't have the "nested timelines" where one timeline created another when it was finished. – James_D Apr 05 '18 at 14:29
  • indeed he will have to create a new frame, remove the old one and add the newly created instead of creating a new timeline! – SDIDSA Apr 05 '18 at 15:10
  • Why "remove the old one"? – James_D Apr 05 '18 at 15:11
  • if he changes duration let's say the first one is 2 seconds and the new one is 0.5 seconds the timeline will take 2 seconds to finish, – SDIDSA Apr 05 '18 at 15:12
  • Thanks, this works well, things get clunky when I want to add a lot of lines however. – Wouter A Apr 05 '18 at 15:58
  • sorry for the delay xD,now check the update i had to create the class Msg which will save the String and Duration of the message, and also a boolean if it will ask for input or not – SDIDSA Apr 05 '18 at 19:09
1

As a variation on the timeline in the other answer, you can create a different KeyFrame for every message you want to display. This avoids the scenario of having "nested timelines", which I think would become unmanageable if you had more than two or three messages to display one after the other.

Here's a SSCCE using this idea:

import javafx.animation.Animation;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.scene.Scene;
import javafx.scene.control.TextArea;
import javafx.scene.control.TextField;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
import javafx.util.Duration;

public class Conversation extends Application {

    private TextArea console ;
    private TextField input ;
    private BooleanProperty readyForInput ;

    private Timeline createTimeline(String[] messages) {
        Timeline timeline = new Timeline();
        Duration delayBetweenMessages = Duration.seconds(1);
        Duration frame = delayBetweenMessages ;
        for (String msg : messages) {
            timeline.getKeyFrames().add(new KeyFrame(frame, e -> console.appendText(msg+"\n")));
            frame = frame.add(delayBetweenMessages);
        }
        timeline.statusProperty().addListener((obs, oldStatus, newStatus) -> {
            readyForInput.set(newStatus != Animation.Status.RUNNING);
            if (newStatus != Animation.Status.RUNNING) {
                input.requestFocus();
            }
        });
        return timeline ;
    }

    @Override
    public void start(Stage primaryStage) {

        readyForInput = new SimpleBooleanProperty(false);

        console = new TextArea();
        console.setEditable(false);

        input = new TextField();
        input.disableProperty().bind(readyForInput.not());

        input.setOnAction(e -> {
            String inputText = input.getText();
            console.appendText("> "+inputText+"\n");
            input.clear();
            createTimeline(getMessages(inputText)).play();
        });

        BorderPane root = new BorderPane(console, input, null, null, null) ;
        Scene scene = new Scene(root);
        primaryStage.setScene(scene);
        primaryStage.show();

        createTimeline(getMessages(null)).play();
    }

    private String[] getMessages(String input) {
        if (input == null || input.isEmpty()) {
            return new String[] {
                    "Goodday sir, how are you doing?",
                    "I'm fine thanks",
                    "What can I do for you?"
            };
        } else {
            // AI logic here...
            return new String[] { "Sure, I'll take care of it." };
        }
    }

    public static void main(String[] args) {
        launch(args);
    }
}
James_D
  • 201,275
  • 16
  • 291
  • 322
  • you can use setAll instead of add so if he wants to use different delays each time – SDIDSA Apr 05 '18 at 15:51
  • @JavaNoob And you would do that looping through an array... how, exactly??? – James_D Apr 05 '18 at 15:53
  • This is what I needed, This makes it easy to quickly write a conversation. I guess I can also use a HashMap that uses the string as key and value as duration? – Wouter A Apr 05 '18 at 15:54
  • Just realized a HashMap has no specified order – Wouter A Apr 05 '18 at 15:59
  • @WouterA You could certainly use a `Map` (of any kind) to map messages to the ensuing pause. (I don't see that the order things are stored in the map would matter at all.) Or any other logic; just replace `delayBetweenMessages` with any `Duration` calculated or looked up from the content of the message. – James_D Apr 05 '18 at 16:04