0

I am developing an applications that can run on 2 different interfaces: one is JavaFX, the other is CLI. During the usage of the applications, i have to run a new stage that implements some sort of fake payment loading a new stage with a new FXML... in the JavaFX run everything runs just fine, but in the CLI i get this error: "This operation is permitted on the event thread only; currentThread = main" which i think is caused because my main doesn't extend "Applications" and cannot load a stage. This is the CLI main (pretty simple).

public static void main(String[] args){
        LoginControllerG2 loginControllerG2 = new LoginControllerG2();
        loginControllerG2.getRole();
} 

I need to load the stage with my CLI interface.

mplungjan
  • 169,008
  • 28
  • 173
  • 236
Matteo
  • 1
  • 1
  • Just to be clear: Even in your CLI interface you want to run *some JavaFX* parts. Is that accurate? – Joachim Sauer Jan 31 '23 at 13:03
  • @JoachimSauer yes, i need to run a class that loads a new Stage with an fxml in my cli – Matteo Jan 31 '23 at 13:05
  • [This question](https://stackoverflow.com/questions/59541555/how-to-achieve-javafx-and-non-javafx-interaction) and the `Platform.startup()` method and/or `JFXPanel` class mentioned in there seems relevant. Note that this is a fairly unusual thing to do, so expect some issues. – Joachim Sauer Jan 31 '23 at 13:10
  • If your "CLI" needs to open a JavaFX Stage, then it's not a CLI, it's a JavaFX application. Subclass `Application` as usual. If you don't need to show a window immediately, start a new Thread from `start()` and implement the CLI portion from there, then open a new stage on the FX Application Thread when you need. You can also use `Platform.startup()` etc. as described above, but note you can only call `startup()` once and must not do so from the FX Application Thread. You're basically reimplementing the lifecycle that `Application` implements for you at that stage. – James_D Jan 31 '23 at 13:32
  • @James_D ye i know, but as my project final i must only change graphic controllers and the implementation of controllers and model must remain the same. i implemented this method payment during javaFX and i did not think about this problem i am witnessing now. by the way, i think your solution will solve my issue. ill keep this post updates – Matteo Jan 31 '23 at 13:35
  • 1
    See the [wumpus app for some info](https://stackoverflow.com/questions/24320014/how-to-call-launch-more-than-once-in-java/24320562#24320562). – jewelsea Jan 31 '23 at 15:02

1 Answers1

3

If your "CLI" shows a JavaFX window, then it is a JavaFX application. Subclass Application in the usual way.

Your application does not need to show the primary stage if it's not ready to during the start method. You can launch your CLI and let the user interact with it.

Note that whatever approach you use here, you have to manage two threads: the JavaFX Application Thread and the thread on which the CLI runs. Since the CLI is going to block for user input, it must not run on the FX Application Thread.

If you launch the CLI from Application.start(), since Application.start() is executed on the FX Application Thread, you need to create a new thread for it.

Use all the usual multithreading precautions for sharing data between the two threads.

Here is a quick example. Here's a very basic CLI class with a method that runs a simple REPL. There are only two commands: "login" shows a login screen using JavaFX and retrieves some data from it. "exit" exits the application.

import javafx.application.Platform;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;

import java.io.IOException;
import java.util.Scanner;
import java.util.concurrent.FutureTask;

public class CommandLineInterpreter {

    public void runCli() {
        try (Scanner scanner = new Scanner(System.in)) {
            boolean done = false;
            while (! done) {
                System.out.println("Enter login to log in, or exit to quit:");
                String input = scanner.nextLine();
                if ("login".equalsIgnoreCase(input)) {
                    String user = getLoginData();
                    System.out.println("Welcome "+user);
                } else if (input.equalsIgnoreCase("exit")) {
                    Platform.exit();
                    done = true ;
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private String getLoginData() throws Exception{
        FutureTask<String> loginWithUITask = new FutureTask<>(this::getUserFromUI);
        Platform.runLater(loginWithUITask);
        return loginWithUITask.get();
    }

    private String getUserFromUI() throws IOException {
        FXMLLoader loader = new FXMLLoader(getClass().getResource("Login.fxml"));
        Parent root = loader.load();
        LoginController controller = loader.getController();
        Scene scene = new Scene(root);
        Stage stage = new Stage();
        stage.setScene(scene);
        stage.setOnShown(e -> stage.toFront());
        stage.showAndWait();
        return controller.getUser();
    }
}

Here's a basic application class that starts the CLI above in a background thread. Note that we have to call Platform.setImplicitExit(false) to make sure the FX platform doesn't close down when the last window is closed.

import javafx.application.Application;
import javafx.application.Platform;
import javafx.stage.Stage;

import java.io.IOException;

public class HelloApplication extends Application {
    @Override
    public void start(Stage stage) throws IOException {
        Platform.setImplicitExit(false);
        CommandLineInterpreter cli = new CommandLineInterpreter();
        Thread cliThread = new Thread(cli::runCli);
        cliThread.start();
    }

    @Override
    public void stop() {
        // Cleanup code...
    }

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

And for completeness the FXML and controller class, which are nothing special:

<?xml version="1.0" encoding="UTF-8"?>

<?import java.lang.*?>
<?import java.util.*?>
<?import javafx.scene.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>

<?import javafx.geometry.Insets?>
<GridPane xmlns="http://javafx.com/javafx"
          xmlns:fx="http://javafx.com/fxml"
          fx:controller="org.jamesd.examples.cli.LoginController"
          hgap="5" vgap="5">

    <columnConstraints>
        <ColumnConstraints halignment="RIGHT" hgrow="NEVER"/>
        <ColumnConstraints halignment="LEFT" hgrow="ALWAYS"/>
    </columnConstraints>

    <padding><Insets topRightBottomLeft="5"/></padding>

    <Label text="User Name:" GridPane.columnIndex="0" GridPane.rowIndex="0"/>
    <TextField fx:id="userField" GridPane.columnIndex="1" GridPane.rowIndex="0"/>
    <Label text="Password" GridPane.columnIndex="0" GridPane.rowIndex="1"/>
    <PasswordField GridPane.columnIndex="1" GridPane.rowIndex="1"/>
    <Button text="OK" onAction="#login"
            GridPane.columnIndex="0" GridPane.rowIndex="2"
            GridPane.columnSpan="2" GridPane.halignment="CENTER"/>
</GridPane>
import javafx.fxml.FXML;
import javafx.scene.control.TextField;

public class LoginController {
    @FXML
    private TextField userField ;

    @FXML
    private void login() {
        userField.getScene().getWindow().hide();
    }

    public String getUser() {
        return userField.getText();
    }
}

Other approaches are possible, e.g. you might use Platform.startup() in a non-JavaFX thread to "manually" start the FX platform, bypassing the usual application lifecycle structure. For example, you can replace the HelloApplication class above with this implementation:

import javafx.application.Platform;

public class HelloApplication {

    public static void main(String[] args) {
        Platform.startup(() -> Platform.setImplicitExit(false));
        CommandLineInterpreter cli = new CommandLineInterpreter();
        cli.runCli();
    }
}

Note that with this approach you don't have a stop() method to perform any cleanup code, as you do with the previous approach.

James_D
  • 201,275
  • 16
  • 291
  • 322