Package and qualify standalone macOS designer with bundled runtime

This commit is contained in:
2026-11-13 22:06:35 +01:00
parent 9ab1a4f530
commit e865d7b4df
10 changed files with 600 additions and 10 deletions
@@ -8,7 +8,7 @@ import javafx.application.Application;
import javafx.scene.Scene;
import javafx.stage.Stage;
/** Development standalone entry: launch settings file and initial FXML path. */
/** Standalone document windows and lifecycle, shared by development and packaged launch. */
public final class DesignerApplication extends Application {
private HostLaunch launch;
private StandaloneHost host;
@@ -17,12 +17,24 @@ public final class DesignerApplication extends Application {
@Override
public void start(Stage stage) throws Exception {
var args = getParameters().getRaw();
if (args.size() != 2)
throw new IllegalArgumentException("Arguments: local-launch.properties View.fxml");
var options = StandaloneOptions.parse(getParameters().getRaw());
Path file = options.document();
if (file == null) {
var chooser = new javafx.stage.FileChooser();
chooser.setTitle("Open FXML View");
chooser
.getExtensionFilters()
.add(new javafx.stage.FileChooser.ExtensionFilter("FXML Views", "*.fxml"));
var selected = chooser.showOpenDialog(stage);
if (selected == null) {
javafx.application.Platform.exit();
return;
}
file = selected.toPath();
}
try {
launch = HostLaunch.load(Path.of(args.get(0)));
open(stage, Path.of(args.get(1)));
launch = options.load();
open(stage, file);
} catch (Exception setup) {
if (host != null) host.close();
host =
@@ -32,7 +44,7 @@ public final class DesignerApplication extends Application {
diagnostic -> System.err.println(diagnostic.message()));
var source =
new de.neitzel.fxdesigner.kit.SourceOnlyPane(
host.services(), Path.of(args.get(1)), String.valueOf(setup.getMessage()));
host.services(), file, String.valueOf(setup.getMessage()));
stage.setTitle("Neitzel FX Designer — source");
stage.setScene(new Scene(source, 1000, 700));
stage.setOnCloseRequest(
@@ -119,7 +131,7 @@ public final class DesignerApplication extends Application {
},
d -> System.err.println(d.message()));
var pane = new DesignerPane(host.services(), launch, file);
stage.setTitle("Neitzel FX Designer — " + file.getFileName());
stage.setTitle("Neitzel FX Designer — " + file.getFileName() + " [" + launch.context() + "]");
stage.setScene(new Scene(pane, 1300, 850));
windows++;
editors.put(identity, stage);
@@ -1,8 +1,27 @@
package de.neitzel.fxdesigner.standalone;
/** Plain launcher supports the development classpath packaging of JavaFX. */
/** Plain entry point shared by the packaged launcher and development runs. */
public final class Launcher {
public static void main(String[] args) {
if (args.length == 1 && args[0].equals("--help")) {
System.out.println(StandaloneOptions.USAGE);
return;
}
if (args.length == 1 && args[0].equals("--version")) {
System.out.println(
"Neitzel FX Designer 0.1.0-SNAPSHOT; Java "
+ System.getProperty("java.version")
+ "; runtime "
+ System.getProperty("java.home"));
return;
}
try {
StandaloneOptions.parse(java.util.List.of(args));
} catch (IllegalArgumentException error) {
System.err.println(error.getMessage() + "\n" + StandaloneOptions.USAGE);
System.exit(2);
return;
}
javafx.application.Application.launch(DesignerApplication.class, args);
}
}
@@ -0,0 +1,80 @@
package de.neitzel.fxdesigner.standalone;
import de.neitzel.fxdesigner.config.*;
import de.neitzel.fxdesigner.workflow.HostLaunch;
import java.nio.file.*;
import java.util.*;
/** Standalone launch inputs; application runtimes remain explicitly configured. */
public record StandaloneOptions(
Path legacy, Path configuration, Path settings, String context, Path maven, Path document) {
public static final String USAGE =
"Neitzel FX Designer\n"
+ " --configuration project/neitzel-fx-designer.json --settings runtimes.json\n"
+ " [--context name] [--maven /path/to/mvnw] [View.fxml]\n"
+ " --launch host.properties [View.fxml]\n"
+ " --help | --version\n"
+ "Without View.fxml, select a document in the file chooser. Multiple contexts require"
+ " --context.";
public static StandaloneOptions parse(List<String> args) {
if (args.size() == 2 && !args.get(0).startsWith("--"))
return new StandaloneOptions(
Path.of(args.get(0)), null, null, null, null, Path.of(args.get(1)));
var values = new HashMap<String, String>();
Path document = null;
for (int i = 0; i < args.size(); i++) {
String arg = args.get(i);
if (arg.startsWith("--")) {
if (!Set.of("--launch", "--configuration", "--settings", "--context", "--maven")
.contains(arg)
|| i + 1 == args.size()
|| args.get(i + 1).startsWith("--")
|| values.containsKey(arg))
throw new IllegalArgumentException("Unknown, duplicate or incomplete option: " + arg);
values.put(arg, args.get(++i));
} else {
if (document != null)
throw new IllegalArgumentException("Specify only one initial FXML document");
document = Path.of(arg);
}
}
if (values.containsKey("--launch")) {
if (values.size() != 1)
throw new IllegalArgumentException("--launch cannot be combined with project options");
} else if (!values.containsKey("--configuration") || !values.containsKey("--settings"))
throw new IllegalArgumentException("Supply --configuration and --settings, or --launch");
return new StandaloneOptions(
path(values, "--launch"),
path(values, "--configuration"),
path(values, "--settings"),
values.get("--context"),
path(values, "--maven"),
document);
}
private static Path path(Map<String, String> values, String key) {
return values.containsKey(key) ? Path.of(values.get(key)).toAbsolutePath().normalize() : null;
}
public HostLaunch load() throws Exception {
if (legacy != null) return HostLaunch.load(legacy);
var contexts = ConfigurationFiles.load(configuration).contexts();
String selected = context;
if (selected == null && contexts.size() == 1) selected = contexts.keySet().iterator().next();
if (selected == null || !contexts.containsKey(selected))
throw new IllegalArgumentException(
"Select --context from: " + String.join(", ", contexts.keySet()));
Path app =
Path.of(StandaloneOptions.class.getProtectionDomain().getCodeSource().getLocation().toURI())
.getParent();
Path directory = app.resolve("tools");
List<Path> tools;
try (var entries = Files.list(directory)) {
tools = entries.filter(p -> p.toString().endsWith(".jar")).sorted().toList();
}
if (tools.size() != 3)
throw new IllegalArgumentException("Bundled target tools missing: " + directory);
return new HostLaunch(configuration, selected, maven, tools, LocalSettings.load(settings));
}
}
@@ -0,0 +1,37 @@
package de.neitzel.fxdesigner.standalone;
import static org.junit.jupiter.api.Assertions.*;
import java.util.List;
import org.junit.jupiter.api.Test;
class StandaloneOptionsTest {
@Test
void supportsProjectArgumentsAndOptionalDocument() {
var options =
StandaloneOptions.parse(
List.of("--configuration", "project/config.json", "--settings", "local.json"));
assertNull(options.document());
assertTrue(options.configuration().isAbsolute());
assertEquals(
"sample",
StandaloneOptions.parse(List.of("--launch", "host.properties", "sample"))
.document()
.toString());
assertNotNull(StandaloneOptions.parse(List.of("host.properties", "View.fxml")).legacy());
}
@Test
void rejectsAmbiguousAndIncompleteArguments() {
for (var args :
List.of(
List.<String>of(),
List.of("--bad"),
List.of("--launch"),
List.of("--launch", "a", "--context", "b"),
List.of("--launch", "a", "--launch", "b"),
List.of("--configuration", "a"),
List.of("--launch", "a", "one", "two")))
assertThrows(IllegalArgumentException.class, () -> StandaloneOptions.parse(args));
}
}