Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -119,3 +119,5 @@ jdks/
mods/
code/manifest-tests
.DS_Store

.superset/
222 changes: 134 additions & 88 deletions AGENTS.md

Large diffs are not rendered by default.

15 changes: 5 additions & 10 deletions code/src/java/pcgen/gui2/PCGenActionMap.java
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
import pcgen.gui2.tools.Icons;
import pcgen.gui2.tools.PCGenAction;
import pcgen.gui3.GuiAssertions;
import pcgen.gui3.JFXPanelFromResource;
import pcgen.gui3.PanelFromResource;
import pcgen.gui3.dialog.CalculatorDialogController;
import pcgen.gui3.dialog.DebugDialog;
import pcgen.gui3.dialog.ExportDialogController;
Expand Down Expand Up @@ -258,8 +258,6 @@ public void actionPerformed(ActionEvent e)
private static final class CalculatorAction extends PCGenAction
{

private JFXPanelFromResource<CalculatorDialogController> dialog;

private CalculatorAction()
{
super("mnuToolsCalculator", CALCULATOR_COMMAND, "F11");
Expand All @@ -268,10 +266,7 @@ private CalculatorAction()
@Override
public void actionPerformed(ActionEvent e)
{
if (dialog == null)
{
dialog = new JFXPanelFromResource<>(CalculatorDialogController.class, "CalculatorDialog.fxml");
}
var dialog = new PanelFromResource<>(CalculatorDialogController.class, "CalculatorDialog.fxml");
dialog.showAsStage(LanguageBundle.getString("mnuToolsCalculator"));
}
}
Expand Down Expand Up @@ -656,9 +651,9 @@ private ExportAction()
public void actionPerformed(ActionEvent e)
{
GuiAssertions.assertIsNotJavaFXThread();
JFXPanelFromResource
jfxPanelFromResource = new JFXPanelFromResource(ExportDialogController.class, "ExportDialog.fxml");
jfxPanelFromResource.showAsStage("Export a PC or Party");
PanelFromResource<ExportDialogController> panel =
new PanelFromResource<>(ExportDialogController.class, "ExportDialog.fxml");
panel.showAsStage("Export a PC or Party");
}

}
Expand Down
4 changes: 2 additions & 2 deletions code/src/java/pcgen/gui2/prefs/PurchaseModeFrame.java
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@
import pcgen.core.utils.MessageType;
import pcgen.core.utils.ShowMessageDelegate;
import pcgen.gui3.GuiUtility;
import pcgen.gui3.JFXPanelFromResource;
import pcgen.gui3.PanelFromResource;
import pcgen.gui3.component.OKCloseButtonBar;
import pcgen.gui3.dialog.NewPurchaseMethodDialogController;
import pcgen.rules.context.AbstractReferenceContext;
Expand Down Expand Up @@ -102,7 +102,7 @@ public final class PurchaseModeFrame extends JDialog
//
private void addMethodButtonActionPerformed()
{
var npmd = new JFXPanelFromResource<>(
var npmd = new PanelFromResource<>(
NewPurchaseMethodDialogController.class,
"NewPurchaseMethodDialog.fxml"
);
Expand Down
39 changes: 9 additions & 30 deletions code/src/java/pcgen/gui3/JFXPanelFromResource.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
import java.io.IOException;
import java.net.URL;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;

import pcgen.system.LanguageBundle;
import pcgen.util.Logging;
Expand All @@ -30,10 +29,17 @@
import javafx.embed.swing.JFXPanel;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.stage.Stage;

/**
* Displays HTML content as a "panel".
* Embeds an FXML-defined JavaFX scene inside a Swing {@link JFXPanel}.
*
* <p>This class is only suitable for the JFX-in-Swing embedding use case (i.e.
* the panel will be added to a Swing container). To open the FXML resource
* as a top-level JavaFX {@link javafx.stage.Stage} dialog, use
* {@link PanelFromResource} instead — re-parenting an embedded scene onto a
* standalone {@code Stage} corrupts the scene's quantum peer and triggers an
* NPE in {@code com.sun.javafx.tk.quantum.GlassScene#updateSceneState} on
* macOS HiDPI displays.
*
* @param <T> The class of the controller
*/
Expand Down Expand Up @@ -84,31 +90,4 @@ public T getControllerFromJavaFXThread()
GuiAssertions.assertIsJavaFXThread();
return fxmlLoader.getController();
}

public void showAsStage(String title)
{
Platform.runLater(() -> {
Stage stage = new Stage();
stage.setTitle(title);
stage.setScene(getScene());
stage.sizeToScene();
stage.show();
});
}

public void showAndBlock(String title)
{
GuiAssertions.assertIsNotJavaFXThread();
CompletableFuture<Integer> lock = new CompletableFuture<>();
Platform.runLater(() -> {
Stage stage = new Stage();
stage.setTitle(title);
stage.setScene(getScene());
stage.sizeToScene();
stage.showAndWait();
lock.completeAsync(() -> 0);

});
lock.join();
}
}
150 changes: 141 additions & 9 deletions code/src/java/pcgen/gui3/PanelFromResource.java
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package pcgen.gui3;

import javafx.application.Platform;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.stage.Stage;
import pcgen.system.LanguageBundle;

import java.text.MessageFormat;
import java.util.concurrent.CompletableFuture;
import java.util.logging.Level;
import java.util.logging.Logger;

Expand All @@ -18,6 +20,13 @@
* This class provides functionality to load FXML files, assign their controllers,
* and display the loaded scenes in a new JavaFX stage.
*
* <p>Unlike {@link JFXPanelFromResource}, this class does <strong>not</strong> extend
* {@code JFXPanel}, so the loaded {@link Scene} is never wrapped in an embedded
* scene peer. This avoids the {@code EmbeddedScene.sceneState} NPE that occurs
* on macOS HiDPI displays when an embedded scene is later re-parented onto a
* top-level {@link Stage}. Use this class for any dialog that is shown via
* {@link #showAsStage(String)} or {@link #showAndBlock(String)}.
*
* @param <T> The type of the controller associated with the FXML resource.
*/
public class PanelFromResource<T> implements Controllable<T>
Expand Down Expand Up @@ -47,33 +56,52 @@ public PanelFromResource(Class<? extends T> klass, String resourceName)

/**
* Retrieves the controller associated with the loaded FXML resource.
* The controller is only available after one of the {@code show*} methods has
* been invoked (which is when the FXML file is actually loaded).
*
* <p>This method may be called from any thread; if invoked off the JavaFX
* application thread it will bounce to it and wait for the result.
*
* @return The controller instance.
* @throws IllegalStateException if this method is called outside the JavaFX application thread.
* @return The controller instance, or {@code null} if no FXML has been loaded yet.
*/
@Override
public T getController()
{
GuiAssertions.assertIsJavaFXThread();
return fxmlLoader.getController();
if (Platform.isFxApplicationThread())
{
return fxmlLoader.getController();
}
return GuiUtility.runOnJavaFXThreadNow(fxmlLoader::getController);
}

/**
* Displays the loaded FXML resource as a new JavaFX stage.
* Displays the loaded FXML resource as a new non-modal JavaFX stage.
*
* <p>This method may be called from any thread. If invoked off the JavaFX
* application thread, the stage creation and display are dispatched there
* via {@link Platform#runLater(Runnable)} and this call returns immediately.
*
* @param title The title of the stage to be displayed.
* @throws IllegalStateException if this method is called outside the JavaFX application thread.
*/
public void showAsStage(String title)
{
GuiAssertions.assertIsJavaFXThread();
if (Platform.isFxApplicationThread())
{
showAsStageOnFxThread(title);
}
else
{
ensureToolkitInitialized();
Platform.runLater(() -> showAsStageOnFxThread(title));
}
}

private void showAsStageOnFxThread(String title)
{
try
{
// Load the scene from the FXML resource.
Scene scene = fxmlLoader.load();

// Create and configure a new stage.
Stage stage = new Stage();
stage.setTitle(title);
stage.setScene(scene);
Expand All @@ -86,4 +114,108 @@ public void showAsStage(String title)
e);
}
}

/**
* Displays the loaded FXML resource as a JavaFX stage, blocking the
* calling thread until the user closes the dialog.
*
* <p>Must <strong>not</strong> be called from the JavaFX application thread;
* doing so would deadlock because {@code Stage.showAndWait()} requires the
* FX thread to remain available to pump events.
Comment thread
karianna marked this conversation as resolved.
*
* @param title The title of the stage to be displayed.
* @throws RuntimeException if this method is called on the JavaFX application thread.
*/
public void showAndBlock(String title)
{
GuiAssertions.assertIsNotJavaFXThread();
ensureToolkitInitialized();
CompletableFuture<Integer> lock = new CompletableFuture<>();
Platform.runLater(() -> {
try
{
Scene scene = fxmlLoader.load();

Stage stage = new Stage();
stage.setTitle(title);
stage.setScene(scene);
stage.sizeToScene();
stage.showAndWait();
} catch (IOException e)
{
LOG.log(Level.SEVERE,
MessageFormat.format("Failed to load stream fxml from location {0}", fxmlLoader.getLocation()),
e);
} finally
{
lock.complete(0);
}
});
lock.join();
}

/**
* Ensures the JavaFX toolkit has been initialized before this class
* schedules any work on the FX application thread.
*
* <p><strong>Why this exists.</strong> Historically the OptionsPathDialog
* (and a handful of other early dialogs) were opened via
* {@link JFXPanelFromResource}, which extends
* {@link javafx.embed.swing.JFXPanel}. Constructing a {@code JFXPanel} is
* one of the three documented ways to bootstrap the JavaFX runtime — it
* implicitly initialises the toolkit (and sets implicit-exit to
* {@code false}) as a side effect, so callers never had to think about
* lifecycle.
*
* <p>When {@code PanelFromResource} was introduced — to avoid the macOS
* HiDPI {@code com.sun.javafx.tk.quantum.GlassScene#updateSceneState} NPE
* caused by re-parenting a {@code JFXPanel}'s embedded {@link Scene} onto
* a top-level {@link Stage} — it intentionally stopped extending
* {@code JFXPanel}. That deliberate change removed the implicit toolkit
* bootstrap, which in turn exposed a latent ordering bug in
* {@link pcgen.system.Main#startupWithGUI()}: {@code loadProperties(true)}
* runs <em>before</em> {@code new JFXPanel()} on that path. On a fresh
* installation with no {@code settings.files.path} and no {@code -s} CLI
* argument, {@code loadProperties} needs to show the OptionsPathDialog,
* and the first call into {@link Platform#runLater(Runnable)} would throw
* {@code IllegalStateException: Toolkit not initialized}. The JVM would
* log SEVERE and exit before the dialog could be displayed, so the
* application could never start fresh.
*
* <p>This helper makes {@code PanelFromResource} self-sufficient: if the
* toolkit has not yet been started we start it; if it already has, the
* {@link IllegalStateException} from {@link Platform#startup(Runnable)} is
* swallowed — that exception is the documented signal that initialisation
* has already happened (e.g. from a prior {@code PanelFromResource} use,
* a {@code JFXPanel} construction, or an explicit {@code Platform.startup}
* elsewhere). {@link Platform#setImplicitExit(boolean)} is then forced to
* {@code false} so that closing this dialog does not tear down the toolkit
* before the rest of the GUI gets a chance to bring up its own Stages —
* matching the previous {@code JFXPanel} behaviour and preventing a
* regression where dismissing an early dialog would shut JavaFX down.
*
* <p>{@link Platform#startup(Runnable)} is internally guarded by an atomic
* flag in {@code com.sun.javafx.application.PlatformImpl}, so concurrent
* callers race safely: exactly one wins and starts the toolkit, the rest
* see {@code IllegalStateException} and fall through. No additional
* synchronisation is required here.
*/
private static void ensureToolkitInitialized()
{
try
{
Platform.startup(() -> {
// The toolkit is considered initialised the moment this Runnable
// is queued onto the FX application thread; we have no work to
// perform here ourselves.
});
}
catch (IllegalStateException alreadyStarted)
{
LOG.log(Level.FINEST,
"JavaFX toolkit was already initialised before PanelFromResource bootstrap; continuing.",
alreadyStarted);
}
Platform.setImplicitExit(false);
}
}
9 changes: 4 additions & 5 deletions code/src/java/pcgen/gui3/component/PCGenToolBar.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
import pcgen.gui2.PCGenUIManager;
import pcgen.gui2.dialog.PrintPreviewDialog;
import pcgen.gui2.tools.Icons;
import pcgen.gui3.JFXPanelFromResource;
import pcgen.gui3.PanelFromResource;
import pcgen.gui3.behavior.EnabledOnlyWithCharacter;
import pcgen.gui3.behavior.EnabledOnlyWithSources;
import pcgen.gui3.dialog.ExportDialogController;
Expand Down Expand Up @@ -136,10 +136,9 @@ private void onPrint(final ActionEvent actionEvent)

private void onExport(final ActionEvent actionEvent)
{
//GuiAssertions.assertIsNotJavaFXThread();
JFXPanelFromResource<ExportDialogController>
jfxPanelFromResource = new JFXPanelFromResource<>(ExportDialogController.class, "ExportDialog.fxml");
jfxPanelFromResource.showAsStage("Export a PC or Party");
PanelFromResource<ExportDialogController> panel =
new PanelFromResource<>(ExportDialogController.class, "ExportDialog.fxml");
panel.showAsStage("Export a PC or Party");
}

private void onPreferences(final ActionEvent actionEvent)
Expand Down
4 changes: 2 additions & 2 deletions code/src/java/pcgen/system/Main.java
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
import pcgen.gui2.PCGenUIManager;
import pcgen.gui2.UIPropertyContext;
import pcgen.gui2.converter.TokenConverter;
import pcgen.gui3.JFXPanelFromResource;
import pcgen.gui3.PanelFromResource;
import pcgen.gui3.namegen.RandomNameDialog;
import pcgen.gui3.dialog.OptionsPathDialogController;
import pcgen.gui3.preloader.PCGenPreloader;
Expand Down Expand Up @@ -258,7 +258,7 @@ public static void loadProperties(boolean useGui)
Logging.errorPrint("No settingsDir specified via -s in batch mode and no default exists.");
GracefulExit.exit(1);
}
var panel = new JFXPanelFromResource<>(
var panel = new PanelFromResource<>(
OptionsPathDialogController.class,
"OptionsPathDialog.fxml"
);
Expand Down
Loading