JavaFX problems replacing an open modal dialog

1.3k Views Asked by At

I have an application which sometimes shows a modal dialog, however after receiving an external message I wish to remove that dialog and replace it with another one, as the situation has changed and the first dialog no longer applies.

However the second dialog does not repaint correctly and neither does the parent stage.

Background does not repaint when dialog moved

I've seen this issue on jdk-8u11-windows-x64 with controls fx controlsfx-8.0.6 and also controls-fx-8.20.8. I've managed to recreate this issue outside of my application

import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

import javafx.application.Application;
import javafx.application.Platform;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.stage.Stage;

import org.controlsfx.dialog.Dialog;

public class NestedEventLoop extends Application {

    private Dialog firstDialog;
    private Object stage;

    @Override
    public void start(Stage stage) throws Exception {
        Button button = new Button("press");
        button.setOnAction((e) -> {
            firstDialog = new Dialog(stage, "dialog", false);
            firstDialog.setContent("Content...");
            openAnotherDialogLater();
            firstDialog.show();

        });
        stage.setScene(new Scene(button));
        stage.show();
        this.stage = stage;
    }

    private void openAnotherDialogLater() {
        Runnable openDialog = () -> {
            firstDialog.hide();
            Dialog anotherDialog = new Dialog(stage, "anotherDialog", false);
            anotherDialog.show();
        };

        Executors.newScheduledThreadPool(1).schedule(() -> {
            Platform.runLater(openDialog);
        }, 2, TimeUnit.SECONDS);
    }

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

My analysis of what is happening.

  • When the first dialog is opened the JavaFX thread goes into a "nested event loop"
  • When the second dialog is required the call to hide() does not cause the first nested event loop to exit
  • A new nested event loop is created on top of the first - this seems to be the cause of the repaint issues.

My question

  • How can I close the first dialog and get out of the first nested loop before opening the second dialog? - without arbitrary sleep etc.

Stack trace from jconsole whilst second dialog open.

com.sun.glass.ui.win.WinApplication._enterNestedEventLoopImpl(Native Method)
com.sun.glass.ui.win.WinApplication._enterNestedEventLoop(WinApplication.java:142)
com.sun.glass.ui.Application.enterNestedEventLoop(Application.java:500)
com.sun.glass.ui.EventLoop.enter(EventLoop.java:107)
com.sun.javafx.tk.quantum.QuantumToolkit.enterNestedEventLoop(QuantumToolkit.java:542)
javafx.stage.Stage.showAndWait(Stage.java:455)
org.controlsfx.dialog.HeavyweightDialog$1.showAndWait(HeavyweightDialog.java:87)
org.controlsfx.dialog.HeavyweightDialog.show(HeavyweightDialog.java:284)
org.controlsfx.dialog.Dialog.show(Dialog.java:384)
NestedEventLoop.lambda$1(NestedEventLoop.java:37)
NestedEventLoop$$Lambda$7/7730735.run(Unknown Source)
com.sun.javafx.application.PlatformImpl$6$1.run(PlatformImpl.java:301)
com.sun.javafx.application.PlatformImpl$6$1.run(PlatformImpl.java:298)
java.security.AccessController.doPrivileged(Native Method)
com.sun.javafx.application.PlatformImpl$6.run(PlatformImpl.java:298)
com.sun.glass.ui.InvokeLaterDispatcher$Future.run(InvokeLaterDispatcher.java:95)
com.sun.glass.ui.win.WinApplication._enterNestedEventLoopImpl(Native Method)
com.sun.glass.ui.win.WinApplication._enterNestedEventLoop(WinApplication.java:142)
com.sun.glass.ui.Application.enterNestedEventLoop(Application.java:500)
com.sun.glass.ui.EventLoop.enter(EventLoop.java:107)
com.sun.javafx.tk.quantum.QuantumToolkit.enterNestedEventLoop(QuantumToolkit.java:542)
javafx.stage.Stage.showAndWait(Stage.java:455)
org.controlsfx.dialog.HeavyweightDialog$1.showAndWait(HeavyweightDialog.java:87)
org.controlsfx.dialog.HeavyweightDialog.show(HeavyweightDialog.java:284)
org.controlsfx.dialog.Dialog.show(Dialog.java:384)
NestedEventLoop.lambda$0(NestedEventLoop.java:24)
NestedEventLoop$$Lambda$1/12269754.handle(Unknown Source)
com.sun.javafx.event.CompositeEventHandler.dispatchBubblingEvent(CompositeEventHandler.java:86)
com.sun.javafx.event.EventHandlerManager.dispatchBubblingEvent(EventHandlerManager.java:238)
com.sun.javafx.event.EventHandlerManager.dispatchBubblingEvent(EventHandlerManager.java:191)
com.sun.javafx.event.CompositeEventDispatcher.dispatchBubblingEvent(CompositeEventDispatcher.java:59)
com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:58)
com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56)
com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56)
com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
com.sun.javafx.event.EventUtil.fireEventImpl(EventUtil.java:74)
com.sun.javafx.event.EventUtil.fireEvent(EventUtil.java:49)
javafx.event.Event.fireEvent(Event.java:204)
javafx.scene.Node.fireEvent(Node.java:8175)
javafx.scene.control.Button.fire(Button.java:185)
com.sun.javafx.scene.control.behavior.ButtonBehavior.mouseReleased(ButtonBehavior.java:182)
com.sun.javafx.scene.control.skin.BehaviorSkinBase$1.handle(BehaviorSkinBase.java:96)
com.sun.javafx.scene.control.skin.BehaviorSkinBase$1.handle(BehaviorSkinBase.java:89)
com.sun.javafx.event.CompositeEventHandler$NormalEventHandlerRecord.handleBubblingEvent(CompositeEventHandler.java:218)
com.sun.javafx.event.CompositeEventHandler.dispatchBubblingEvent(CompositeEventHandler.java:80)
com.sun.javafx.event.EventHandlerManager.dispatchBubblingEvent(EventHandlerManager.java:238)
com.sun.javafx.event.EventHandlerManager.dispatchBubblingEvent(EventHandlerManager.java:191)
com.sun.javafx.event.CompositeEventDispatcher.dispatchBubblingEvent(CompositeEventDispatcher.java:59)
com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:58)
com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56)
com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56)
com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
com.sun.javafx.event.EventUtil.fireEventImpl(EventUtil.java:74)
com.sun.javafx.event.EventUtil.fireEvent(EventUtil.java:54)
javafx.event.Event.fireEvent(Event.java:204)
javafx.scene.Scene$MouseHandler.process(Scene.java:3746)
javafx.scene.Scene$MouseHandler.access$1800(Scene.java:3471)
javafx.scene.Scene.impl_processMouseEvent(Scene.java:1695)
javafx.scene.Scene$ScenePeerListener.mouseEvent(Scene.java:2486)
com.sun.javafx.tk.quantum.GlassViewEventHandler$MouseEventNotification.run(GlassViewEventHandler.java:314)
com.sun.javafx.tk.quantum.GlassViewEventHandler$MouseEventNotification.run(GlassViewEventHandler.java:243)
java.security.AccessController.doPrivileged(Native Method)
com.sun.javafx.tk.quantum.GlassViewEventHandler.handleMouseEvent(GlassViewEventHandler.java:345)
com.sun.glass.ui.View.handleMouseEvent(View.java:526)
com.sun.glass.ui.View.notifyMouse(View.java:898)
com.sun.glass.ui.win.WinApplication._runLoop(Native Method)
com.sun.glass.ui.win.WinApplication.access$300(WinApplication.java:39)
com.sun.glass.ui.win.WinApplication$4$1.run(WinApplication.java:112)
java.lang.Thread.run(Thread.java:745)

Update: since writing up this question I've discovered a workaround by closing the first dialog using runLater() and then opening the new dialog with another runLater(). I'm still interested on other peoples ideas on this though.

private void openAnotherDialogLater() {
    Runnable closeDialog = () -> {
        firstDialog.hide();
    };
    Runnable openDialog = () -> {
        Dialog anotherDialog = new Dialog(stage, "anotherDialog", false);
        anotherDialog.show();
    };

    Executors.newScheduledThreadPool(1).schedule(() -> {
        Platform.runLater(closeDialog);
        Platform.runLater(openDialog);
    }, 2, TimeUnit.SECONDS);
}
1

There are 1 best solutions below

2
On BEST ANSWER

Regardless of my comment, I suggest using the new Dialogs API in JDK 8u40. You can represent the arrival of an external message in the result type of the first dialog, and if the message arrived, open the second dialog as part of handling the first dialog's result:

enum Result { MSG_ARRIVED, ... }

Dialog<Result> firstDialog = ...;

firstDialog.showAndWait().ifPresent(res -> {
    if(res == Result.MSG_ARRIVED) {
        anotherDialog = ...;
        anotherDialog.show();
    }
});

When the message arrives, just set the result of the first dialog to MSG_ARRIVED and close the dialog:

Platform.runLater(() -> {
    firstDialog.setResult(Result.MSG_ARRIVED);
    firstDialog.hide();
});