Add reviewed FXML View generation in both hosts

This commit is contained in:
2026-11-13 21:39:06 +01:00
parent 1856f69637
commit 9ab1a4f530
18 changed files with 1463 additions and 9 deletions
@@ -118,6 +118,7 @@ public final class DesignerPane extends BorderPane implements AutoCloseable {
private DesignerWorkflow workflow;
private DesignerWorkflow interactiveWorkflow;
private DesignerWorkflow bindingWorkflow;
private ViewGenerationPane viewGeneration;
private BindingBrowser bindingBrowser;
private BindingEditor bindingEditor, computedEditor, repeatEditor;
private TableEditor tableEditor;
@@ -386,6 +387,9 @@ public final class DesignerPane extends BorderPane implements AutoCloseable {
problemsTab.setClosable(false);
var structureTab = new Tab("Structure", referenceEditor);
structureTab.setClosable(false);
viewGeneration = new ViewGenerationPane(host, launch, file);
var generationTab = new Tab("Generate View", new ScrollPane(viewGeneration));
generationTab.setClosable(false);
var bindings =
new TitledPane(
"Bindings",
@@ -396,7 +400,8 @@ public final class DesignerPane extends BorderPane implements AutoCloseable {
tableTab,
structureTab,
problemsTab,
discoveryTab));
discoveryTab,
generationTab));
bindings.setExpanded(false);
center.setBottom(new VBox(properties, bindings));
output.setEditable(false);
@@ -1430,6 +1435,7 @@ public final class DesignerPane extends BorderPane implements AutoCloseable {
workflow.close();
interactiveWorkflow.close();
bindingWorkflow.close();
viewGeneration.close();
bindingRequest++;
if (document != null) document.close();
}
@@ -0,0 +1,575 @@
package de.neitzel.fxdesigner.kit;
import de.neitzel.fxdesigner.contracts.DocumentSnapshot;
import de.neitzel.fxdesigner.generation.ViewGeneration;
import de.neitzel.fxdesigner.generation.ViewGeneration.*;
import de.neitzel.fxdesigner.generation.ViewGeneration.Control;
import de.neitzel.fxdesigner.host.*;
import de.neitzel.fxdesigner.protocol.BindingMetadata;
import de.neitzel.fxdesigner.workflow.*;
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.*;
import javafx.application.Platform;
import javafx.scene.control.*;
import javafx.scene.layout.*;
/** One review workflow shared by both hosts; application types stay in target workers. */
public final class ViewGenerationPane extends VBox implements AutoCloseable {
private final HostServices host;
private final Path origin;
private final DesignerWorkflow workflow;
private final TextField model = text("generate-model", "Fully qualified Model"),
controller = text("generate-controller", "Optional existing Controller"),
destination = text("generate-file", "New sibling filename.fxml"),
label = text("generate-label", "Label");
private final ListView<Field> fields = new ListView<>();
private final CheckBox include = new CheckBox("Include Attribute");
private final ComboBox<Control> control = new ComboBox<>();
private final ComboBox<String> direction = new ComboBox<>(),
component = new ComboBox<>(),
converter = new ComboBox<>();
private final TextArea columns = new TextArea(),
preview = new TextArea(),
choices = new TextArea();
private final GridPane layoutPreview = new GridPane();
private final Label status =
new Label("Load a Model from the current successful build, choose fields, then review.");
private final Button create = new Button("Create reviewed View");
private final AutoCloseable changes;
private boolean loading, closed;
private long version;
private Review reviewed;
private record Review(String text, String filename, String build, long revision, long version) {}
private record CheckedReview(Review review, BindingMetadata metadata) {}
private static TextField text(String id, String prompt) {
var field = new TextField();
field.setId(id);
field.setPromptText(prompt);
return field;
}
private static Button button(String name, Runnable action) {
var b = new Button(name);
b.setOnAction(e -> action.run());
return b;
}
private void fx(Runnable task) {
Platform.runLater(
() -> {
if (!closed) task.run();
});
}
private void fail(Throwable error) {
while (error.getCause() != null) error = error.getCause();
status.setText("Generation: " + error.getMessage());
}
private void invalidate() {
if (loading) return;
version++;
reviewed = null;
create.setDisable(true);
status.setText("Choices changed; review again before creating a file.");
}
public ViewGenerationPane(HostServices host, HostLaunch launch, Path origin) throws Exception {
super(6);
this.host = host;
this.origin = origin;
workflow =
new DesignerWorkflow(
host,
launch.configuration(),
launch.context(),
launch.maven(),
launch.tools(),
s -> {});
setId("view-generation");
fields.setId("generate-fields");
fields.setPrefHeight(130);
include.setId("generate-include");
control.setId("generate-control");
direction.setId("generate-direction");
component.setId("generate-component");
converter.setId("generate-converter");
columns.setId("generate-columns");
columns.setPromptText("Table Columns: attribute | header | optional width (one per line)");
columns.setPrefRowCount(2);
control.getItems().setAll(Control.values());
direction.getItems().setAll("both", "view", "model");
component.setEditable(true);
converter.setEditable(true);
preview.setId("generate-preview");
preview.setEditable(false);
preview.setPrefRowCount(5);
choices.setId("generate-choices");
choices.setEditable(false);
choices.setPrefRowCount(3);
status.setId("generate-status");
status.setWrapText(true);
layoutPreview.setId("generate-layout");
layoutPreview.setHgap(12);
layoutPreview.setVgap(10);
create.setId("generate-create");
create.setDisable(true);
create.setOnAction(e -> create());
destination.setText("NewView.fxml");
getChildren()
.addAll(
new Label("Generate a new editable View (existing files are never replaced)"),
new HBox(6, model, controller, button("Load Model fields", this::discover)),
fields,
new HBox(6, include, label, control, direction),
new HBox(
6,
new Label("Component / Row"),
component,
new Label("Converter"),
converter,
button("Discover field choices", this::choices),
button("Open selected Component / Row", this::openComponent)),
columns,
new HBox(
6,
button("Move field up", () -> move(-1)),
button("Move field down", () -> move(1))),
choices,
destination,
button("Review generated View", this::review),
new Label("Layout preview (structure only; Components run in target previews)"),
layoutPreview,
preview,
status,
create);
fields.getSelectionModel().selectedItemProperty().addListener((o, a, b) -> show(b));
for (var t : List.of(model, controller, destination))
t.textProperty().addListener((o, a, b) -> invalidate());
label.textProperty().addListener((o, a, b) -> edit());
columns.textProperty().addListener((o, a, b) -> edit());
include.selectedProperty().addListener((o, a, b) -> edit());
control
.valueProperty()
.addListener(
(o, a, b) -> {
if (!loading && b == Control.Label) direction.setValue("view");
configureEditors();
edit();
});
direction.valueProperty().addListener((o, a, b) -> edit());
for (var box : List.of(component, converter)) {
box.valueProperty()
.addListener(
(o, a, b) -> {
if (!loading && b != null) box.getEditor().setText(b);
edit();
});
box.getEditor().textProperty().addListener((o, a, b) -> edit());
}
Path reactor = launch.configuration().getParent().toRealPath();
changes =
host.documents()
.onChange(
uri -> {
if (Path.of(uri).startsWith(reactor)) fx(this::invalidate);
});
snapshot()
.thenAccept(
d ->
fx(
() -> {
if (!model.getText().isBlank()) return;
var m =
java.util.regex.Pattern.compile("<\\?neitzel-model\\s+([^?\\s]+)\\s*\\?>")
.matcher(d.text());
if (m.find()) model.setText(m.group(1));
}));
}
private CompletionStage<DocumentSnapshot> snapshot() {
return host.documents()
.open(origin.toUri())
.thenCompose(d -> d.snapshot().whenComplete((s, e) -> d.close()));
}
private static DocumentSnapshot proposed(DocumentSnapshot base, String xml) {
return new DocumentSnapshot(base.document(), base.revision(), xml, true, true);
}
private void configureEditors() {
var c = control.getValue();
boolean collection = c == Control.Repeat || c == Control.Table;
direction.setDisable(collection);
converter.setDisable(collection || c == Control.Component);
component.setDisable(!collection && c != Control.Component);
columns.setDisable(c != Control.Table);
}
private void openComponent() {
workflow
.componentSource(component.getEditor().getText().trim())
.thenCompose(uri -> host.navigation().open(new HostServices.Location(uri, 0)))
.whenComplete(
(v, e) -> {
if (e != null) fx(() -> fail(e));
});
}
private void show(Field f) {
if (loading || f == null) return;
loading = true;
try {
include.setSelected(f.included());
label.setText(f.label());
control.setValue(f.control());
direction.setValue(f.direction());
component.getEditor().setText(f.component());
converter.getEditor().setText(f.converter());
columns.setText(
String.join(
"\n",
f.columns().stream()
.map(c -> c.attribute() + " | " + c.header() + " | " + c.width())
.toList()));
} finally {
loading = false;
configureEditors();
}
}
private List<Column> columns() {
return columns
.getText()
.lines()
.filter(s -> !s.isBlank())
.map(
line -> {
var p = line.split("\\|", 3);
return new Column(
p[0].trim(),
p.length > 1 ? p[1].trim() : p[0].trim(),
p.length > 2 ? p[2].trim() : "");
})
.toList();
}
private void edit() {
if (loading) return;
int index = fields.getSelectionModel().getSelectedIndex();
if (index < 0 || control.getValue() == null || direction.getValue() == null) return;
var old = fields.getItems().get(index);
var next =
new Field(
old.path(),
label.getText(),
control.getValue(),
direction.getValue(),
converter.getEditor().getText().trim(),
component.getEditor().getText().trim(),
columns(),
include.isSelected());
loading = true;
try {
fields.getItems().set(index, next);
fields.getSelectionModel().select(index);
} finally {
loading = false;
}
invalidate();
}
private void move(int delta) {
int from = fields.getSelectionModel().getSelectedIndex(), to = from + delta;
if (from < 0 || to < 0 || to >= fields.getItems().size()) return;
Collections.swap(fields.getItems(), from, to);
fields.getSelectionModel().select(to);
invalidate();
}
private void discover() {
try {
String seed = ViewGeneration.seed(model.getText().trim(), controller.getText().trim());
long ticket = ++version;
reviewed = null;
create.setDisable(true);
status.setText("Discovering Model fields…");
snapshot()
.thenCompose(d -> workflow.bindings(false, proposed(d, seed), "", "both"))
.whenComplete(
(m, e) ->
fx(
() -> {
if (ticket != version) return;
if (e != null) {
fail(e);
return;
}
fields.getItems().setAll(ViewGeneration.propose(m));
if (!fields.getItems().isEmpty()) fields.getSelectionModel().selectFirst();
status.setText(
"Discovered "
+ fields.getItems().size()
+ " fields. Nested Models need a Component; numeric text needs a"
+ " Converter.\n"
+ String.join("\n", m.diagnostics()));
}));
} catch (Exception e) {
fail(e);
}
}
private void choices() {
int index = fields.getSelectionModel().getSelectedIndex();
if (index < 0) return;
var field = fields.getItems().get(index);
String xml;
try {
var only = new ArrayList<Field>();
for (int i = 0; i < fields.getItems().size(); i++) {
var f = fields.getItems().get(i);
only.add(
new Field(
f.path(),
f.label(),
f.control(),
f.direction(),
f.converter(),
f.component(),
f.columns(),
i == index));
}
xml = ViewGeneration.generate(model.getText().trim(), controller.getText().trim(), only);
} catch (Exception ignored) {
try {
xml = ViewGeneration.seed(model.getText().trim(), controller.getText().trim());
} catch (Exception error) {
fail(error);
return;
}
}
String proposal = xml;
long ticket = version;
snapshot()
.thenCompose(
d ->
workflow.bindingDetails(
false,
proposed(d, proposal),
field.path(),
field.direction(),
ViewGeneration.id(index),
field.control().property()))
.whenComplete(
(m, e) ->
fx(
() -> {
if (ticket != version) return;
if (e != null) {
fail(e);
return;
}
loading = true;
try {
String selected = component.getEditor().getText(),
converted = converter.getEditor().getText();
var candidates =
field.control() == Control.Table ? m.tableRows() : m.components();
component
.getItems()
.setAll(
candidates.stream()
.map(BindingMetadata.RepeatOption::name)
.toList());
converter
.getItems()
.setAll(
m.converters().stream()
.map(BindingMetadata.Converter::name)
.toList());
component.getEditor().setText(selected);
converter.getEditor().setText(converted);
choices.setText(
String.join("\n", candidates.stream().map(Object::toString).toList())
+ "\n"
+ String.join(
"\n", m.converters().stream().map(Object::toString).toList())
+ "\nColumns: "
+ m.tableColumns());
} finally {
loading = false;
}
}));
}
private String filename() {
String name = destination.getText().trim();
if (!name.matches("[A-Za-z_][A-Za-z0-9_.-]*\\.fxml"))
throw new IllegalArgumentException("Choose a new sibling .fxml filename (no directories)");
return name;
}
private void review() {
reviewed = null;
create.setDisable(true);
try {
String name = filename();
var selected = List.copyOf(fields.getItems());
if (selected.stream().noneMatch(Field::included))
throw new IllegalArgumentException("Include at least one Attribute");
String xml =
ViewGeneration.generate(model.getText().trim(), controller.getText().trim(), selected);
preview.setText(xml);
layoutPreview.getChildren().clear();
int row = 0;
for (var f : selected)
if (f.included()) {
boolean section =
Set.of(Control.Component, Control.Repeat, Control.Table, Control.ListView)
.contains(f.control());
layoutPreview.add(new Label(f.label()), 0, row, section ? 2 : 1, 1);
javafx.scene.Node sample =
switch (f.control()) {
case TextField -> {
var value = new TextField();
value.setPromptText(f.path());
value.setEditable(false);
yield value;
}
case CheckBox -> {
var value = new CheckBox();
value.setMouseTransparent(true);
yield value;
}
default ->
new Label(
"["
+ f.control()
+ (f.component().isBlank() ? "" : " · " + f.component())
+ "]");
};
layoutPreview.add(sample, section ? 0 : 1, section ? ++row : row, section ? 2 : 1, 1);
row++;
}
long ticket = ++version;
status.setText("Validating proposed FXML in target runtime…");
snapshot()
.thenCompose(
d ->
workflow
.currentBuildId(d.document())
.thenCompose(
build ->
workflow
.bindings(false, proposed(d, xml), "", "both")
.thenCompose(
m ->
workflow
.currentBuildId(d.document())
.thenApply(
now -> {
if (!build.equals(now))
throw new CancellationException(
"Build changed during review");
return new CheckedReview(
new Review(
xml, name, build, d.revision(), ticket),
m);
}))))
.whenComplete(
(result, e) ->
fx(
() -> {
if (ticket != version) return;
if (e != null) {
fail(e);
return;
}
var m = result.metadata();
boolean invalid =
m.problems().stream().anyMatch(p -> p.severity().equals("ERROR"));
status.setText(
(invalid ? "Review blocked" : "Review ready")
+ "\n"
+ String.join(
"\n",
m.problems().stream()
.map(p -> p.severity() + ": " + p.message())
.toList()));
if (!invalid) {
reviewed = result.review();
create.setDisable(false);
}
}));
} catch (Exception e) {
fail(e);
}
}
private void create() {
var accepted = reviewed;
if (accepted == null || accepted.version() != version) return;
create.setDisable(true);
snapshot()
.thenCompose(
d -> {
if (d.revision() != accepted.revision())
return CompletableFuture.<String>failedFuture(
new CancellationException("Source changed; review again"));
return workflow.currentBuildId(d.document());
})
.whenComplete(
(build, e) ->
fx(
() -> {
if (accepted.version() != version) return;
if (e != null) {
fail(e);
return;
}
if (!accepted.build().equals(build)) {
invalidate();
status.setText("Build changed; review again");
return;
}
var uri = origin.resolveSibling(accepted.filename()).toUri();
setDisable(true);
host.documents()
.create(uri, accepted.text())
.thenCompose(
d -> {
d.close();
return host.navigation().open(new HostServices.Location(uri, 0));
})
.whenComplete(
(v, error) ->
fx(
() -> {
setDisable(false);
reviewed = null;
version++;
create.setDisable(true);
if (error != null) fail(error);
else
status.setText(
"Created and opened "
+ accepted.filename()
+ ". Further changes use the normal editor; Build"
+ " and Refresh before target preview.");
}));
}));
}
public void close() {
closed = true;
workflow.close();
try {
changes.close();
} catch (Exception ignored) {
}
}
}
@@ -38,11 +38,7 @@ public final class ProjectionHostChecks {
}
private static TextArea source(DesignerPane pane) {
return (TextArea)
pane.lookupAll(".text-area").stream()
.filter(n -> n instanceof TextArea t && t.isEditable())
.findFirst()
.orElseThrow();
return (TextArea) pane.lookup("#fxml-source");
}
private static FXOMInstance object(DesignerPane pane, String id) {
@@ -1668,6 +1664,323 @@ public final class ProjectionHostChecks {
}
}
@SuppressWarnings("unchecked")
private static void generationSelect(DesignerPane pane, String path) throws Exception {
fx(
() -> {
var list =
(ListView<de.neitzel.fxdesigner.generation.ViewGeneration.Field>)
pane.lookup("#generate-fields");
list.getSelectionModel()
.select(
list.getItems().stream()
.filter(f -> f.path().equals(path))
.findFirst()
.orElseThrow());
return null;
});
}
private static void generationText(DesignerPane pane, String id, String text) throws Exception {
fx(
() -> {
var node = pane.lookup("#generate-" + id);
if (node instanceof ComboBox<?> box) box.getEditor().setText(text);
else ((TextInputControl) node).setText(text);
return null;
});
}
private static void generationReady(DesignerPane pane) throws Exception {
click(pane, "Review generated View");
await(
() ->
fx(
() ->
((Label) pane.lookup("#generate-status"))
.getText()
.startsWith("Review ready")));
check(fx(() -> !pane.lookup("#generate-create").isDisabled()), "Reviewed creation unavailable");
}
public static void generation(DesignerPane pane, DocumentSession document) throws Exception {
String original = documentText(document);
bindingTab(pane, "Generate View");
generationText(pane, "model", "fixture.model.Person");
generationText(pane, "controller", "");
click(pane, "Load Model fields");
await(() -> fx(() -> ((ListView<?>) pane.lookup("#generate-fields")).getItems().size() == 4));
check(
fx(
() ->
((Label) pane.lookup("#generate-status"))
.getText()
.contains("Generated Model metadata")),
"Generator did not use shared generated metadata");
click(pane, "Review generated View");
check(
fx(() -> pane.lookup("#generate-create").isDisabled()),
"Unconfigured nested field allowed creation");
generationSelect(pane, "home");
click(pane, "Discover field choices");
await(
() ->
fx(
() ->
((TextArea) pane.lookup("#generate-choices"))
.getText()
.contains(
"fixture.components.AddressForm : fixture.model.Address —"
+ " COMPATIBLE")));
generationText(pane, "component", "fixture.components.AddressForm");
generationSelect(pane, "visits");
generationText(pane, "component", "fixture.components.AddressForm");
generationSelect(pane, "name");
generationText(pane, "label", "Name & <display>");
click(pane, "Move field up");
generationSelect(pane, "rank");
click(pane, "Discover field choices");
await(
() ->
fx(
() ->
((TextArea) pane.lookup("#generate-choices"))
.getText()
.contains("fixture.components.RankConverter")));
click(pane, "Review generated View");
await(
() ->
fx(
() ->
((Label) pane.lookup("#generate-status"))
.getText()
.startsWith("Review blocked")));
check(
fx(() -> pane.lookup("#generate-create").isDisabled()),
"Numeric text without a Converter accepted");
generationText(pane, "converter", "fixture.components.LegacyRankConverter");
generationReady(pane);
check(
fx(() -> ((Label) pane.lookup("#generate-status")).getText().contains("WARNING")),
"Unverified Converter warning hidden");
generationText(pane, "converter", "fixture.components.RankConverter");
generationReady(pane);
String repeat = fx(() -> ((TextArea) pane.lookup("#generate-preview")).getText());
check(
repeat.contains("<Repeat") && repeat.contains("Name &amp; &lt;display&gt;"),
"Repeat/label generation lost choices");
check(documentText(document).equals(original), "Review modified the parent View");
generationText(pane, "label", "Rank reviewed");
check(
fx(() -> pane.lookup("#generate-create").isDisabled()),
"Edited proposal retained creation approval");
generationText(pane, "converter", "missing.Converter");
fx(
() -> {
((CheckBox) pane.lookup("#generate-include")).setSelected(false);
return null;
});
generationReady(pane);
check(
!fx(() -> ((TextArea) pane.lookup("#generate-preview")).getText())
.contains("path=\"rank\""),
"Omitted invalid field emitted");
generationText(pane, "converter", "fixture.components.RankConverter");
fx(
() -> {
((CheckBox) pane.lookup("#generate-include")).setSelected(true);
return null;
});
generationSelect(pane, "visits");
fx(
() -> {
@SuppressWarnings("unchecked")
var control =
(ComboBox<de.neitzel.fxdesigner.generation.ViewGeneration.Control>)
pane.lookup("#generate-control");
control.setValue(de.neitzel.fxdesigner.generation.ViewGeneration.Control.Table);
return null;
});
generationText(pane, "component", "fixture.components.VisitRow");
generationText(pane, "columns", "street | Street & road | 160");
click(pane, "Discover field choices");
await(
() ->
fx(
() ->
((TextArea) pane.lookup("#generate-choices"))
.getText()
.contains("fixture.components.VisitRow")));
generationReady(pane);
String table = fx(() -> ((TextArea) pane.lookup("#generate-preview")).getText());
check(
table.contains("<Table") && table.contains("attribute=\"street\""),
"Table generation lost Columns");
// Load both reviewed variants through the actual target-runtime sample provider.
for (String xml : List.of(repeat, table)) {
set(document, xml);
await(() -> fx(() -> source(pane).getText().equals(xml)));
fx(
() -> {
for (var n : pane.lookupAll(".combo-box"))
if (n instanceof ComboBox<?> box) {
for (Object value : box.getItems())
if (value
instanceof de.neitzel.fxdesigner.workflow.DesignerWorkflow.Example example
&& example.id().equals("standard")) {
@SuppressWarnings("unchecked")
var examples = (ComboBox<Object>) box;
examples.setValue(example);
break;
}
}
return null;
});
click(pane, "Interactive preview");
await(
() ->
fx(
() ->
pane.lookupAll(".label").stream()
.anyMatch(
n ->
n instanceof Label l
&& "Interactive preview running from captured drafts"
.equals(l.getText()))));
check(pane.interactivePid() > 0, "Generated View did not load in target worker");
click(pane, "Stop preview");
await(() -> pane.interactivePid() < 0);
}
set(document, original);
await(() -> fx(() -> source(pane).getText().equals(original)));
document
.save(document.snapshot().toCompletableFuture().get().revision())
.toCompletableFuture()
.get();
bindingTab(pane, "Generate View");
generationText(pane, "file", "../escape.fxml");
click(pane, "Review generated View");
check(fx(() -> pane.lookup("#generate-create").isDisabled()), "Traversal filename accepted");
generationText(pane, "file", "ReviewedView.fxml");
generationReady(pane);
set(document, original + "<!-- review invalidation -->");
await(() -> fx(() -> pane.lookup("#generate-create").isDisabled()));
set(document, original);
await(() -> fx(() -> source(pane).getText().equals(original)));
document
.save(document.snapshot().toCompletableFuture().get().revision())
.toCompletableFuture()
.get();
generationReady(pane);
String reviewed = fx(() -> ((TextArea) pane.lookup("#generate-preview")).getText());
click(pane, "Create reviewed View");
await(
() ->
fx(
() ->
((Label) pane.lookup("#generate-status"))
.getText()
.startsWith("Created and opened ReviewedView.fxml")));
var path =
java.nio.file.Path.of(document.snapshot().toCompletableFuture().get().document())
.resolveSibling("ReviewedView.fxml");
check(
java.nio.file.Files.readString(path).equals(reviewed),
"Created bytes differ from reviewed FXML");
check(documentText(document).equals(original), "Creation changed parent");
try (var created =
pane.kit().host().documents().open(path.toUri()).toCompletableFuture().get()) {
var before = created.snapshot().toCompletableFuture().get();
String edited = before.text() + "<!-- normal generated-View edit -->";
set(created, edited);
check(
created.undo().toCompletableFuture().get().text().equals(before.text()),
"Generated View Undo lost source");
check(
created.redo().toCompletableFuture().get().text().equals(edited),
"Generated View Redo lost source");
created
.save(created.snapshot().toCompletableFuture().get().revision())
.toCompletableFuture()
.get();
try {
pane.kit().host().documents().create(path.toUri(), "overwrite").toCompletableFuture().get();
throw new AssertionError("Generated View overwritten");
} catch (ExecutionException expected) {
check(
expected.getCause() instanceof java.nio.file.FileAlreadyExistsException,
"Wrong collision failure");
}
check(java.nio.file.Files.readString(path).equals(edited), "Collision changed existing file");
}
// Add an ordinary sample descriptor in this disposable fixture, so the newly
// created resource itself (not only an overlay at the parent URI) is loaded.
var launch = (de.neitzel.fxdesigner.workflow.HostLaunch) field(pane, "launch");
var root = launch.configuration().getParent();
var catalog =
root.resolve("app/src/test/resources/META-INF/neitzel-fx-designer/examples.properties");
if (!java.nio.file.Files.exists(catalog))
catalog =
root.resolve(
"preview/src/main/resources/META-INF/neitzel-fx-designer/examples.properties");
String catalogText = java.nio.file.Files.readString(catalog);
var properties = new java.util.Properties();
properties.load(new java.io.StringReader(catalogText));
int count = Integer.parseInt(properties.getProperty("count"));
String key = "example." + count + ".";
catalogText =
catalogText.replaceFirst("(?m)^count=\\d+", "count=" + (count + 1))
+ "\n"
+ key
+ "id=reviewed\n"
+ key
+ "label=Reviewed View\n"
+ key
+ "modelType=fixture.model.Person\n"
+ key
+ "providerClass=fixture.preview.Examples\n"
+ key
+ "viewResource=fixture/components/ReviewedView.fxml\n"
+ key
+ "componentPackage=fixture.components\n";
java.nio.file.Files.writeString(catalog, catalogText);
click(pane, "Build and Refresh");
var build = (de.neitzel.fxdesigner.build.BuildRefresh.Run) fx(() -> field(pane, "buildRun"));
check(
build.completion().toCompletableFuture().get(90, TimeUnit.SECONDS).status()
== de.neitzel.fxdesigner.build.BuildRefresh.Status.READY,
"Build after new View failed");
await(
() ->
fx(
() ->
object(pane, "card").getSceneGraphObject() instanceof SnapshotRegion r
&& r.image() != null
&& r.statusText().equals("Target runtime")));
try (var generatedWorkflow =
new de.neitzel.fxdesigner.workflow.DesignerWorkflow(
pane.kit().host(),
launch.configuration(),
launch.context(),
launch.maven(),
launch.tools(),
s -> {});
var created =
pane.kit().host().documents().open(path.toUri()).toCompletableFuture().get()) {
generatedWorkflow
.interactive(false, "reviewed", List.of(created.snapshot().toCompletableFuture().get()))
.toCompletableFuture()
.get(30, TimeUnit.SECONDS);
check(
generatedWorkflow.renderingPid() > 0,
"New generated resource did not load through NeitzelLib");
}
System.out.println(
"PASS reviewed View generation: metadata, choices, invalid/omitted fields, Repeat/Table"
+ " target loading, immutable review, create/open, collision protection, normal"
+ " Undo/Redo and rebuild");
}
public static void run(DesignerPane pane, DocumentSession document) throws Exception {
renderRevisionRace(pane, document);
String original = document.snapshot().toCompletableFuture().get().text();
@@ -2046,6 +2359,7 @@ public final class ProjectionHostChecks {
object(pane, "card").getSceneGraphObject() instanceof SnapshotRegion r
&& r.image() != null
&& r.statusText().equals("Target runtime")));
generation(pane, document);
System.out.println(
"PASS shared projection: exact source, picking/resize/relocation, nested/FQ/anonymous"
+ " Components, declared properties, selection, undo/redo, modes, target load,"