Add reviewed FXML View generation in both hosts
This commit is contained in:
@@ -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 & <display>"),
|
||||
"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,"
|
||||
|
||||
Reference in New Issue
Block a user