I am trying to raise a custom loading dialog in java and then execute some synchronous function which takes a few seconds.
I would like the dialog to be present as long as the function executes and once it finishes I would close the dialog.
My Dialog looks as follows:
public abstract class LoaderControl extends Control implements SimpleDialogInfo {
private static final StyleablePropertyFactory<LoaderControl> FACTORY = new StyleablePropertyFactory<>(Control.getClassCssMetaData());
private LoaderDialogResponse response;
private final DialogInfo dialogInfo;
private final SimpleStringProperty text = new SimpleStringProperty("");
private final SimpleBooleanProperty spinnerVisible = new SimpleBooleanProperty(true);
private UpdaterStates state;
private CloseDialogFunction onClose;
#Override
public void closeDialog(){
onClose.closeDialog();
}
#Override
public void setCloseDialog(CloseDialogFunction onClose){
this.onClose = onClose;
}
}
This is how I create it and show it:
public void createIndependentDialog(SimpleDialogInfo content, EventHandler<MouseEvent> onClose) {
Platform.runLater(() -> {
Stage stage = new Stage();
Parent p = new StackPane();
Scene s = new Scene(p);
stage.setScene(s);
MFXGenericDialog dialogContent = MFXGenericDialogBuilder.build()
.makeScrollable(true)
.setShowAlwaysOnTop(false)
.get();
MFXStageDialog dialog = MFXGenericDialogBuilder.build(dialogContent)
.toStageDialogBuilder()
.initModality(Modality.APPLICATION_MODAL)
.setDraggable(true)
.initOwner(stage)
.setTitle("Dialogs Preview")
.setOwnerNode(grid)
.setScrimPriority(ScrimPriority.WINDOW)
.setScrimOwner(true)
.get();
dialogContent.setMinSize(350, 200);
MFXFontIcon infoIcon = new MFXFontIcon(content.getDialogInfo().getIcon(), 18);
dialogContent.setHeaderIcon(infoIcon);
dialogContent.setHeaderText(content.getDialogInfo().getHeader());
dialogContent.setContent((Node) content);
MFXGenericDialog finalDialogContent = dialogContent;
MFXStageDialog finalDialog = dialog;
content.setCloseDialog(dialog::close);
convertDialogTo(String.format("mfx-%s-dialog", content.getDialogInfo().getDialogType()));
if(onClose != null)
dialogContent.setOnClose(onClose);
dialog.showAndWait();
});
}
This is how it looks like in the calling class:
DialogLoaderControlImpl preloader = new DialogLoaderControlImpl(new LoaderDialogInfo("Searching For New Versions"));
DialogsController.getInstance().createIndependentDialog(preloader);
someSynchronousMethod();
preloader.closeDialog();
The issue is that when I get to the "preloader.closeDialog()" line, the closeDialog function which should close the dialog is null (the onClose field is null).
In short:
The createIndependentDialog() method should raise a dialog and I would like to proceed to execute the method "someSynchronousMethod()" while the dialog is still shown and close it once the method finishes.
Please note that I use a Skin for the dialog which is not shown here but it works if I remove the Platform.runLater, but then it is stuck in the showAndWait() without advancing which is expected
Is there a way or a known design of some sort that will help to run tasks/methods with custom dialogs?
This can be done, but as pointed out in the comments, it is probably better to use some type of progress node. I used Alert in this example but Dialog should be very similar.
The key is closing the Alert/Dialog after the task is complete using the task's setOnSucceeded.
longRunningTask.setOnSucceeded((t) -> {
System.out.println("Task Done!");
alert.close();
});
Full Code
import javafx.application.Application;
import javafx.concurrent.Task;
import javafx.scene.Scene;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonType;
import javafx.scene.control.Label;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
public class App extends Application
{
public static void main(String[] args)
{
launch();
}
#Override
public void start(Stage stage)
{
Scene scene = new Scene(new StackPane(new Label("Hello World!")), 320, 240);
stage.setTitle("Hello!");
stage.setScene(scene);
stage.show();
Task<Integer> longRunningTask = new Task<Integer>() {
#Override protected Integer call() throws Exception {
int iterations;
for (iterations = 0; iterations < 100000; iterations++) {
if (isCancelled()) {
break;
}
System.out.println("Iteration " + iterations);
}
return iterations;
}
};
Alert alert = new Alert(Alert.AlertType.INFORMATION);
Button okButton = (Button)alert.getDialogPane().lookupButton(ButtonType.OK);
okButton.setDisable(true);
longRunningTask.setOnSucceeded((t) -> {
System.out.println("Task Done!");
alert.close();
});
new Thread(longRunningTask).start();
alert.setTitle("Hello World");
alert.setHeaderText("Hello");
alert.setContentText("I will close when the long running task ends!");
alert.showAndWait();
}
}
Altered code from https://docs.oracle.com/javase/8/javafx/interoperability-tutorial/concurrency.htm.
One pitfall I can see is someone closing the Alert/Dialog before the task finishes.
Related
I'm currently working on a password manager. Before making any changes to a certain service, the program will ask the user for a password for authorization and then proceed to show the appropriate dialog, if the password is correct.
The issue that I'm having is that if I go through the cycle of putting in my password to make the change, click "ok", and then proceeding to make changes on the shown dialog, on the next turn if instead of putting the password when prompted I close the prompt, then the program retrieves the password from the previous iteration although it has been explicitly cleared. Resulting in the concurrent dialog showing, which is only supposed to show if you put in the correct password.
private void handleEditButton(MouseEvent event) {
Optional<String> rslt = passwordConfirmDialog.showAndWait();
if (rslt.get().equals(""))
return; //Do not proceed
String userInput = rslt.get().trim();
// Complex expression, but use of && statement is necessary to avoid an
// unecessary call to db and have return statement on this IF
if (!(!userInput.isBlank() && isCorrectPassword(userInput))) {
// show dialog
AlertConfigs.invalidPasswordTransactionFailed.showAndWait();
return;
}
System.out.println("Edit Handler: Correct password. -> " + userInput);
//Proceed to show next dialog...
private void initializePasswordConfirmDialog() {
passwordConfirmDialog.setTitle("User Account Control");
passwordConfirmDialog.setHeaderText("Please enter your password to continue.");
// Set the button types.
ButtonType ok = new ButtonType("Ok", ButtonData.OK_DONE);
passwordConfirmDialog.getDialogPane().getButtonTypes().addAll(ok, ButtonType.CANCEL);
final PasswordField psField = new PasswordField();
GridPane grid = new GridPane();
grid.setHgap(10);
grid.setVgap(10);
grid.setPadding(new Insets(20, 150, 10, 10));
grid.add(new Label("Please Enter your password"), 0, 0);
grid.add(psField, 1, 0);
passwordConfirmDialog.getDialogPane().setContent(grid);
passwordConfirmDialog.setResultConverter(buttonType -> {
String rslt = "";
if (buttonType == ok) {
rslt = psField.getText();
}
psField.clear();
return rslt;
});
}
I've posted a video on YouTube to help visualize the problem. https://youtu.be/sgayh7Q7Ne8
The PasswordField in initializePasswordConfirmDialog() is cleared because whenever I run the the prompt the second time, the PasswordField is blank (visually). Nevertheless, for some reason it still grabs the result from the previous iteration.
The initializePasswordConfirmDialog() is called once inside the constructor and is responsible for set the passwordConfirmDialog variable with the adequate properties.
Some additional code:
HomeController.java
#FXML
private GridPane servicesGrid;
private Dialog<String> passwordConfirmDialog;
private Dialog<Service> editServiceDialog;
private final int NUM_COLUMNS = 7;
public HomeController() {
passwordConfirmDialog = new Dialog<>();
initializePasswordConfirmDialog();
editServiceDialog = new Dialog<>();
}
#Override
public void initialize(URL arg0, ResourceBundle arg1) {
loadServicesGridpane();
}
private void loadServicesGridpane() {
ArrayList<Service> currS = acct.getServices();
// int currentRow = 1;
for (Service s : currS)
addRowToServiceGrid(s);
}
private void addRowToServiceGrid(Service s) {
int rowIdx = servicesGrid.getChildren().size() / 4;
Button editButton = new Button("Edit");
editButton.setOnMouseClicked(event -> {
handleEditButton(event);
});
Button deleteButton = new Button("Delete");
deleteButton.setOnMouseClicked(event -> {
handleDeleteButton(event);
});
deleteButton.setId(s.getServiceName());
Label currServiceName = new Label(s.getServiceName());
currServiceName.setId(s.getServiceName());
Label currUsername = new Label(s.getServiceUsername());
Label currPassword = new Label(s.getServicePassword());
Label dateCreated = new Label(s.getDateCreated());
Label lastPssdChange = new Label(s.getLastPasswordChange());
servicesGrid.addRow(rowIdx, currServiceName, currUsername, currPassword, dateCreated, lastPssdChange,
deleteButton, editButton);
}
To study the problem in isolation, I refactored this example to permit reusing the dialog. As shown below, reusing the dialog requires clearing the password field. Replace the parameter dialog with an invocation of createDialog() to see that creating the dialog each time does not require clearing the password field. Comparing the profile of each approach may help you decide which approach is acceptable; in my experiments, reuse added negligible memory overhead (~250 KB), and it protracted garbage collection slightly(~50 ms).
#!/bin/sh
java … DialogTest -reuse &
pid1=$!
java … DialogTest -no-reuse &
pid2=$!
echo $pid1 $pid2
jconsole $pid1 $pid2
Unfortunately, creating the dialog each time may only appear to solve the problem; it may have exposed a latent synchronization problem. In particular, verify that your result converter's callback executes on the JavaFX Application Thread. To illustrate, I've added a call to Platform.isFxApplicationThread() in resultsNotPresent() below.
import javafx.application.Application;
import javafx.application.Platform;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonType;
import javafx.scene.control.Dialog;
import javafx.scene.control.Label;
import javafx.scene.control.PasswordField;
import javafx.scene.control.TextField;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
/**
* #see https://stackoverflow.com/q/73328282/230513
* #see https://stackoverflow.com/a/44172143/230513
*/
public class DialogTest extends Application {
private static boolean REUSE_DIALOG = true;
private record Results(String text, String pass) {
private static Results of(String text, String pass) {
return new Results(text, pass);
}
}
#Override
public void start(Stage stage) {
var label = new Label("Reuse: " + REUSE_DIALOG);
var button = new Button("Button");
if (REUSE_DIALOG) {
var dialog = createDialog();
button.setOnAction(e -> showDialog(dialog));
} else {
button.setOnAction(e -> showDialog(createDialog()));
}
stage.setScene(new Scene(new HBox(8, label, button)));
stage.show();
}
private Dialog<Results> createDialog() {
var dialog = new Dialog<Results>();
dialog.setTitle("Dialog Test");
dialog.setHeaderText("Please authenticate…");
var dialogPane = dialog.getDialogPane();
dialogPane.getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL);
var text = new TextField("Name");
var pass = new PasswordField();
dialogPane.setContent(new VBox(8, text, pass));
dialog.showingProperty().addListener((o, wasShowing, isShowing) -> {
if (isShowing) {
Platform.runLater(pass::requestFocus);
}
});
dialog.setResultConverter((ButtonType bt) -> {
if (ButtonType.OK == bt) {
var results = Results.of(text.getText(), pass.getText());
pass.clear();
return results;
}
pass.clear();
return null;
});
return dialog;
}
private void showDialog(Dialog<Results> dialog) {
var optionalResult = dialog.showAndWait();
optionalResult.ifPresentOrElse(
(var results) -> System.out.println(results),
(this::resultsNotPresent));
}
private void resultsNotPresent() {
System.out.println("Canceled on FX application thread: "
+ Platform.isFxApplicationThread());
}
public static void main(String[] args) {
if (args.length > 0) {
REUSE_DIALOG = args[0].startsWith("-r");
}
launch(args);
}
}
Here is a small sample from my custom dialog, which is meant to display the progress of a running javafx.concurrent.Task.
DialogPane pane = this.getDialogPane()
pane.getButtonTypes().addAll(ButtonType.CANCEL);
pane.headerTextProperty().bind(task.titleProperty());
pane.contentTextProperty().bind(task.messageProperty());
For some reason, the buttons in the button bar disappeared, but only after some text updated. After further investigation, I found that binding the header text of a dialog seems to remove all the buttons in the button bar. Why would this happen, and what would I do to stop the buttons from being hidden?
EDIT: Here's an MCVE demonstrating the problem.
import javafx.application.Application;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.concurrent.Task;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.layout.StackPane;
import javafx.stage.Modality;
import javafx.stage.Stage;
public class MCVE extends Application {
public static class CustomDialog extends Dialog<ButtonType> {
public CustomDialog(Task<?> task) {
this.initModality(Modality.APPLICATION_MODAL);
DialogPane pane = this.getDialogPane();
{
pane.getButtonTypes().addAll(ButtonType.CANCEL);
pane.headerTextProperty().bind(task.titleProperty());
}
setOnCloseRequest(event -> {
if (task.isRunning()) event.consume();
});
}
}
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage stage) {
StackPane root = new StackPane();
Button starter = new Button("Showcase");
starter.setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE);
root.getChildren().add(starter);
Scene scene = new Scene(root, 500, 500);
stage.setScene(scene);
stage.show();
starter.setOnAction(event -> {
Task<Void> task = new Task<>() {
#Override
protected Void call() throws Exception {
updateTitle("Before loop");
try {
Thread.sleep(1000);
} catch (InterruptedException ignored) {
if (isCancelled()) return null;
}
for (int i = 1; i <= 20; i++) {
updateTitle("loop " + i);
try {
Thread.sleep(1000);
} catch (InterruptedException ignored) {
if (isCancelled()) return null;
}
}
return null;
}
};
Thread worker = new Thread(task);
worker.start();
new CustomDialog(task).showAndWait();
});
}
}
It appears that the issue needs fixing on JavaFX's end. In fact, the button bar is simply pushed out of view. There is a relatively decent workaround though. Dialog internally uses a styled GridPane to display header text and graphics, so I simply replicated that using an external GridPane and instead bound to the textProperty() of a Label.
public static class CustomDialog extends Dialog<ButtonType> {
public CustomDialog(Task<?> task) {
DialogPane pane = this.getDialogPane(); {
pane.getButtonTypes().addAll(ButtonType.CANCEL);
//construct custom header
GridPane headerRoot = new GridPane(); {
Label headerText = new Label();
//headerText is the label containing the header text
headerText.textProperty().bind(task.titleProperty());
headerRoot.add(headerText, 0, 0);
}
headerRoot.getStyleClass().addAll("header-panel");
pane.setHeader(headerRoot);
pane.setContentText("Placeholder content");
pane.getScene().getWindow().setOnCloseRequest(event -> {
if (!task.isDone()) {
event.consume();
}
});
}
}
It looks exactly the same as a default dialog, without the issue mentioned in the question above.
The following are the changes I made in fxml
Changes in the java file , here my code :
private ProgressIndicator pi;
void handlebuildButtonAction(ActionEvent event) throws IOException, GeneralSecurityException {
if ((entServer.isSelected()==true || compasServer.isSelected()==true)) {
if(!fileList.isEmpty()){
ProgressIndicator pi = new ProgressIndicator();
pi.setProgress(10);
}
}
The progress indicator is not updated when I run the application. I'm not sure how to sync the changes to UI. Assist me on this. Thanks in advance.
output
For example: if you set 0.1 - progress will be 10%, 0.2 - 20% and so on, so when you set the progress => 1 you will always have "done".
Here, this an example with a button, when you click the button, your progress indicator will be updated(one click + 10%):
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ProgressIndicator;
import javafx.scene.layout.*;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.stage.Stage;
public class Test extends Application {
private ProgressIndicator pi;
private double counter = 0;
public void start(Stage stage)
{
ProgressIndicator pi = new ProgressIndicator();
Button button = new Button("Press");
TilePane root = new TilePane();
// action event
EventHandler<ActionEvent> event = new EventHandler<ActionEvent>() {
public void handle(ActionEvent e)
{
counter += 0.1;
pi.setProgress(counter);
}
};
button.setOnAction(event);
root.getChildren().add(button);
root.getChildren().add(pi);
// create a scene
Scene scene = new Scene(root, 200, 200);
// set the scene
stage.setScene(scene);
stage.show();
}
public static void main(String args[])
{
// launch the application
launch(args);
}
}
Just change this code for your case:
EventHandler<ActionEvent> event = new EventHandler<ActionEvent>() {
public void handle(ActionEvent e)
{
if ((entServer.isSelected()==true || compasServer.isSelected()==true)) {
if (!fileList.isEmpty()) {
counter += 0.1;
pi.setProgress(counter);
}
}
}
};
Hope that helps you!
I've been migrating a project of mine to JavaFX and started running into thread issues. I'll attach a short example. After much searching I managed to sort out the problem. I can't change the tableView data outside of the fx application thread. I switched my code over from using SwingWorker to a Task.
At first, that worked until I added a change listener to the table's observableList. I then received the error "Not on FX application thread;"
The error happened inside the onChanged method when I attempted to update a Label's value. I resolved this by wrapping it inside Platform.runLater().
I'm just confused as to why changing the label says it wasn't on the application thread. On what thread was this running? Also, am I adding rows to my table correctly by using a task? In my actual application, I could be adding 50k rows hence why the separate thread so as to not lock up the UI.
public class Temp extends Application{
private ObservableList<String> libraryList = FXCollections.observableArrayList();
public void start(Stage stage) {
Label statusLabel = new Label("stuff goes here");
TableView<String> table = new TableView<String>(libraryList);
table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
TableColumn<String, String> col = new TableColumn<String, String>("Stuff");
col.setCellValueFactory(cellData -> new ReadOnlyStringWrapper(cellData.getValue()));
table.getColumns().add(col);
libraryList.addListener(new ListChangeListener<String>() {
public void onChanged(Change change) {
// Problem was caused by setting the label's text (prior to adding the runLater)
Platform.runLater(()->{
statusLabel.setText(libraryList.size()+" entries");
});
}
});
// dummy stuff
libraryList.add("foo");
libraryList.add("bar");
Button b = new Button("Press Me");
b.setOnAction(new EventHandler<ActionEvent>() {
public void handle(ActionEvent e) {
FileTask task = new FileTask();
new Thread(task).start();
}
});
BorderPane mainBody = new BorderPane();
mainBody.setTop(statusLabel);
mainBody.setCenter(table);
mainBody.setBottom(b);
Scene scene = new Scene(mainBody);
stage.setScene(scene);
stage.show();
}
class FileTask extends Task<Boolean>{
public FileTask(){
}
protected Boolean call() throws Exception{
Random rand = new Random();
for(int i = 0; i < 5; i++) {
String s = ""+rand.nextInt(Integer.MAX_VALUE);
libraryList.add(s);
}
return true;
}
}
public static void main(String[] args) {
Application.launch(args);
}
}
It's working as expected, you have the application thread and the task thread, they kind of look like this:
App ------\ ----------------------
Task \-label.setText() Exception
You can't do any UI work on anything but the App thread, so adding your RunLater does this:
App ----\ -------------/ RunLater(label.setText()) ----------
Task \-add to list/
which works well. There are a few ways to manage this based on what you want to do:
If you want to update the Table list within the Task, you can move the RunLater call to inside the task, rather than inside the handler, this way it will still get you back to the App thread. This way if you're actually on the app thread, there is no need to call RunLater within the handler.
App ---\ -----------------------/ label.setText() ----------
Task \-RunLater(add to list)/
Another option is to just use a Task> which will run on the other thread, and return the full list of strings that are going to be added. This is more likely what you want if you're making network calls in the task, get a list of items, then add them once they are all downloaded to the table.
App -----\ ------------------------------/ label.setText() ---/ add to table list-------
Task \-build list, update progress /- return final list /
Hopefully the formatting stays.
Consider encapsulating the information needed by the view in a separate class (typically referred to as model).
The view should respond to changes in the model by means of listener or binding.
You can use a thread or threads, to update the model:
import java.util.Random;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.ReadOnlyStringWrapper;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
public class Temp extends Application{
#Override
public void start(Stage stage) {
Model model = new Model();
Label statusLabel = new Label("stuff goes here");
TableView<String> table = new TableView<>(model.getLibraryList());
table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
TableColumn<String, String> col = new TableColumn<>("Stuff");
col.setCellValueFactory(cellData -> new ReadOnlyStringWrapper(cellData.getValue()));
table.getColumns().add(col);
statusLabel.textProperty().bind(Bindings.concat(model.sizeProperty.asString(), " entries"));
// dummy stuff
model.add("foo"); model.add("bar");
Button b = new Button("Press Me");
b.setOnAction(e -> {
FileTask task = new FileTask(model);
new Thread(task).start();
});
BorderPane mainBody = new BorderPane();
mainBody.setTop(statusLabel);
mainBody.setCenter(table);
mainBody.setBottom(b);
Scene scene = new Scene(mainBody);
stage.setScene(scene);
stage.show();
}
class Model {
private final ObservableList<String> libraryList;
private final IntegerProperty sizeProperty;
Model(){
libraryList = FXCollections.observableArrayList();
sizeProperty = new SimpleIntegerProperty(0);
libraryList.addListener((ListChangeListener<String>) change -> {
Platform.runLater(()->sizeProperty.set(libraryList.size()));
});
}
//synchronize if you want to use multithread
void add(String string) {
Platform.runLater(()->sizeProperty.set(libraryList.add(string)));
}
ObservableList<String> getLibraryList() {
return libraryList;
}
IntegerProperty getSizeProperty() {
return sizeProperty;
}
}
class FileTask implements Runnable{
private final Model model;
public FileTask(Model model){
this.model = model;
}
#Override
public void run() {
Random rand = new Random();
for(int i = 0; i < 5; i++) {
String s = ""+rand.nextInt(Integer.MAX_VALUE);
model.add(s);
}
}
}
public static void main(String[] args) {
Application.launch(args);
}
}
Is there a way to get the current opened Stage in JavaFX, if there is one open?
Something like this:
Stage newStage = new Stage();
newStage.initOwner(JavaFx.getCurrentOpenedStage()); //Like this
Java 9 makes this possible by the addition of the javafx.stage.Window.getWindows() method. Therefore you can just get list of Windows and see which are showing
List<Window> open = Stage.getWindows().stream().filter(Window::isShowing);
If you need the current stage reference inside an event handler method, you can get it from the ActionEvent param. For example:
#FXML
public void OnButtonClick(ActionEvent event) {
Stage stage = (Stage)((Node) event.getSource()).getScene().getWindow();
(...)
}
You can also get it from any control declared in your controller:
#FXML
private Button buttonSave;
(...)
Stage stage = (Stage) buttonSave.getScene().getWindow();
There's no built-in functionality for this. In most use cases, you open a new Stage as a result of user action, so you can call getScene().getWindow() on the node on which the action occurred to get the "current" window.
In other use cases, you will have to write code to track current windows yourself. Of course, multiple windows might be open, so you need to track them in some kind of collection. I'd recommend creating a factory class to manage the stages and registering event handlers for the stages opening and closing, so you can update a property and/or list. You'd probably want this to be a singleton. Here's a sample implementation: here getOpenStages() gives an observable list of open stages - the last one is the most recently opened - and currentStageProperty() gives the focused stage (if any). Your exact implementation might be different, depending on your exact needs.
public enum StageFactory {
INSTANCE ;
private final ObservableList<Stage> openStages = FXCollections.observableArrayList();
public ObservableList<Stage> getOpenStages() {
return openStages ;
}
private final ObjectProperty<Stage> currentStage = new SimpleObjectProperty<>(null);
public final ObjectProperty<Stage> currentStageProperty() {
return this.currentStage;
}
public final javafx.stage.Stage getCurrentStage() {
return this.currentStageProperty().get();
}
public final void setCurrentStage(final javafx.stage.Stage currentStage) {
this.currentStageProperty().set(currentStage);
}
public void registerStage(Stage stage) {
stage.addEventHandler(WindowEvent.WINDOW_SHOWN, e ->
openStages.add(stage));
stage.addEventHandler(WindowEvent.WINDOW_HIDDEN, e ->
openStages.remove(stage));
stage.focusedProperty().addListener((obs, wasFocused, isNowFocused) -> {
if (isNowFocused) {
currentStage.set(stage);
} else {
currentStage.set(null);
}
});
}
public Stage createStage() {
Stage stage = new Stage();
registerStage(stage);
return stage ;
}
}
Note this only allows you to track stages obtained from StageFactory.INSTANCE.createStage() or created elsewhere and passed to the StageFactory.INSTANCE.registerStage(...) method, so your code has to collaborate with that requirement. On the other hand, it gives you the chance to centralize code that initializes your stages, which may be otherwise beneficial.
Here's a simple example using this:
import javafx.application.Application;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import javafx.stage.WindowEvent;
public class SceneTrackingExample extends Application {
int count = 0 ;
#Override
public void start(Stage primaryStage) {
StageFactory factory = StageFactory.INSTANCE ;
factory.registerStage(primaryStage);
configureStage(primaryStage);
primaryStage.show();
}
private void configureStage(Stage stage) {
StageFactory stageFactory = StageFactory.INSTANCE;
Stage owner = stageFactory.getCurrentStage() ;
Label ownerLabel = new Label();
if (owner == null) {
ownerLabel.setText("No owner");
} else {
ownerLabel.setText("Owner: "+owner.getTitle());
stage.initOwner(owner);
}
stage.setTitle("Stage "+(++count));
Button newStage = new Button("New Stage");
newStage.setOnAction(e -> {
Stage s = stageFactory.createStage();
Stage current = stageFactory.getCurrentStage() ;
if (current != null) {
s.setX(current.getX() + 20);
s.setY(current.getY() + 20);
}
configureStage(s);
s.show();
});
VBox root = new VBox(10, ownerLabel, newStage);
root.setAlignment(Pos.CENTER);
stage.setScene(new Scene(root, 360, 150));
}
public enum StageFactory {
INSTANCE ;
private final ObservableList<Stage> openStages = FXCollections.observableArrayList();
public ObservableList<Stage> getOpenStages() {
return openStages ;
}
private final ObjectProperty<Stage> currentStage = new SimpleObjectProperty<>(null);
public final ObjectProperty<Stage> currentStageProperty() {
return this.currentStage;
}
public final javafx.stage.Stage getCurrentStage() {
return this.currentStageProperty().get();
}
public final void setCurrentStage(final javafx.stage.Stage currentStage) {
this.currentStageProperty().set(currentStage);
}
public void registerStage(Stage stage) {
stage.addEventHandler(WindowEvent.WINDOW_SHOWN, e ->
openStages.add(stage));
stage.addEventHandler(WindowEvent.WINDOW_HIDDEN, e ->
openStages.remove(stage));
stage.focusedProperty().addListener((obs, wasFocused, isNowFocused) -> {
if (isNowFocused) {
currentStage.set(stage);
} else {
currentStage.set(null);
}
});
}
public Stage createStage() {
Stage stage = new Stage();
registerStage(stage);
return stage ;
}
}
public static void main(String[] args) {
launch(args);
}
}
You can create a label in your java fxml.
Then in your controller class refer your label like this :
#FXML
private Label label;
Then in any function of the controller class you can access the current stage by this block of code :
private void any_function(){
Stage stage;
stage=(Stage) label.getScene().getWindow();
}