From 4063348faeee0faacb4556503ebfb10be6578150 Mon Sep 17 00:00:00 2001 From: lxpollitt <630494+lxpollitt@users.noreply.github.com> Date: Sat, 20 Jun 2026 13:32:32 +0100 Subject: [PATCH] Bring desktop dialogs to the foreground The desktop Swing dialogs (file open, settings, About, and the quit/exit confirmations) could open behind the application window on Windows. This was a behaviour regression introduced by the macOS dialog deadlock fix (#9) which moved the dialog calls onto the AWT event dispatch thread. On Windows a thread that does not own the foreground window cannot bring a new window to the front, so a dialog created on the EDT opens behind the application. Each dialog is now given a temporary, effectively invisible, owner window that is brought to the foreground and disposed as soon as the dialog closes, so the dialog appears in front. By default the code uses a gentle toFront()/requestFocus() to bring this window to the foreground. But a DIALOG_ALWAYS_ON_TOP static flag is included to switch to setting the owner window to always-on-top, which foregrounds more reliably but pins the dialog above all other windows while open, even when switching to ther applications. The correct choise for this static flag needs to be chosen after testing on Windows. The owner window is created on the EDT inside the existing dispatch, so the macOS deadlock fix is preserved. --- .../joric/lwjgl3/DesktopDialogHandler.java | 94 +++++++++++++++++-- 1 file changed, 84 insertions(+), 10 deletions(-) diff --git a/lwjgl3/src/main/java/emu/joric/lwjgl3/DesktopDialogHandler.java b/lwjgl3/src/main/java/emu/joric/lwjgl3/DesktopDialogHandler.java index 47d26e6..7f1cdf4 100644 --- a/lwjgl3/src/main/java/emu/joric/lwjgl3/DesktopDialogHandler.java +++ b/lwjgl3/src/main/java/emu/joric/lwjgl3/DesktopDialogHandler.java @@ -1,6 +1,7 @@ package emu.joric.lwjgl3; import java.awt.Image; +import java.awt.Window; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.image.BufferedImage; @@ -15,6 +16,7 @@ import javax.swing.JButton; import javax.swing.JDialog; import javax.swing.JFileChooser; +import javax.swing.JFrame; import javax.swing.JOptionPane; import javax.swing.SwingUtilities; import javax.swing.filechooser.FileNameExtensionFilter; @@ -32,7 +34,50 @@ public class DesktopDialogHandler implements DialogHandler { private boolean dialogOpen; - + + // The Swing dialogs are shown on the AWT event dispatch thread (see the + // comment in confirm() for why), which is not the libGDX/GLFW main thread + // that owns the application window. On Windows the OS will then open a + // dialog *behind* the application window, because a thread that does not + // own the foreground window is not allowed to bring a new window to the + // front. To work around this each dialog is given a temporary owner window + // (see createDialogOwner) that is brought to the foreground. + // + // When true, that owner is made always-on-top, which reliably foregrounds + // the dialog but also keeps it pinned above every other window for as long + // as it is open. When false (the default), a gentler toFront()/requestFocus() + // is used instead, which does not pin the dialog above other applications + // (nicer if e.g. you want to switch away to check a filename) but may be + // less reliable at foregrounding on some platforms. Flip this to true if + // dialogs still appear behind the application window. + private static final boolean DIALOG_ALWAYS_ON_TOP = false; + + /** + * Creates a temporary, effectively invisible owner window for a dialog and + * brings it to the foreground, so the dialog appears in front of the + * application rather than behind it (see DIALOG_ALWAYS_ON_TOP). The UTILITY + * window type keeps the owner out of the taskbar and the alt-tab list, and + * the 1x1 centred size keeps it out of sight while still centring the dialog + * on screen. The caller must dispose() the returned frame once the dialog + * has closed. Must be called on the AWT event dispatch thread. + * + * @return the temporary owner frame to pass as the dialog's parent. + */ + private JFrame createDialogOwner() { + JFrame owner = new JFrame(); + owner.setUndecorated(true); + owner.setType(Window.Type.UTILITY); + owner.setSize(1, 1); + owner.setLocationRelativeTo(null); + owner.setAlwaysOnTop(DIALOG_ALWAYS_ON_TOP); + owner.setVisible(true); + if (!DIALOG_ALWAYS_ON_TOP) { + owner.toFront(); + owner.requestFocus(); + } + return owner; + } + private Icon importIcon; private Icon exportIcon; private Icon clearIcon; @@ -117,7 +162,13 @@ public void confirm(final String message, final ConfirmResponseHandler responseH @Override public void run() { dialogOpen = true; - final int output = JOptionPane.showConfirmDialog(null, message, "Please confirm", JOptionPane.YES_NO_OPTION); + JFrame owner = createDialogOwner(); + final int output; + try { + output = JOptionPane.showConfirmDialog(owner, message, "Please confirm", JOptionPane.YES_NO_OPTION); + } finally { + owner.dispose(); + } dialogOpen = false; Gdx.app.postRunnable(new Runnable() { @Override @@ -154,7 +205,13 @@ public void run() { jfc.addChoosableFileFilter(filter); dialogOpen = true; - final int returnValue = jfc.showOpenDialog(null); + JFrame owner = createDialogOwner(); + final int returnValue; + try { + returnValue = jfc.showOpenDialog(owner); + } finally { + owner.dispose(); + } final String selectedPath = (returnValue == JFileChooser.APPROVE_OPTION) ? jfc.getSelectedFile().getPath() : null; dialogOpen = false; @@ -182,8 +239,14 @@ public void promptForTextInput(final String message, final String initialValue, @Override public void run() { dialogOpen = true; - final String text = (String) JOptionPane.showInputDialog(null, message, "Please enter value", - JOptionPane.INFORMATION_MESSAGE, null, null, initialValue != null ? initialValue : ""); + JFrame owner = createDialogOwner(); + final String text; + try { + text = (String) JOptionPane.showInputDialog(owner, message, "Please enter value", + JOptionPane.INFORMATION_MESSAGE, null, null, initialValue != null ? initialValue : ""); + } finally { + owner.dispose(); + } dialogOpen = false; Gdx.app.postRunnable(new Runnable() { @Override @@ -259,10 +322,15 @@ else if (button == clearButton) { okButton.addMouseListener(mouseListener); pane.setComponentOrientation(JOptionPane.getRootFrame().getComponentOrientation()); - JDialog dialog = pane.createDialog("About JOric"); + JFrame owner = createDialogOwner(); + JDialog dialog = pane.createDialog(owner, "About JOric"); dialog.setIconImage(loadImage("png/joric-32x32.png")); - dialog.show(); - dialog.dispose(); + try { + dialog.show(); + } finally { + dialog.dispose(); + owner.dispose(); + } final Object paneValue = pane.getValue(); dialogOpen = false; @@ -290,8 +358,14 @@ public void promptForOption(final String title, final String message, final Stri public void run() { dialogOpen = true; String dialogTitle = (title != null && !title.isEmpty()) ? title : "JOric"; - final Object result = JOptionPane.showInputDialog(null, message, dialogTitle, - JOptionPane.QUESTION_MESSAGE, null, options, currentSelection); + JFrame owner = createDialogOwner(); + final Object result; + try { + result = JOptionPane.showInputDialog(owner, message, dialogTitle, + JOptionPane.QUESTION_MESSAGE, null, options, currentSelection); + } finally { + owner.dispose(); + } dialogOpen = false; Gdx.app.postRunnable(new Runnable() { @Override