Discover Model paths and compatible binding targets in both hosts

Add non-constructing target metadata and a shared read-only browser for the Person fixtures. Negotiate binding discovery independently, preserve FXML, and explain direction/type mismatches. Record remaining manual navigation checks as explicitly skipped.
This commit is contained in:
2026-11-13 08:57:35 +01:00
parent 2156894fa8
commit 2fe408bb4d
38 changed files with 1087 additions and 37 deletions
@@ -0,0 +1,90 @@
package de.neitzel.fxdesigner.kit;
import de.neitzel.fxdesigner.protocol.BindingMetadata;
import java.util.function.*;
import javafx.beans.property.ReadOnlyStringWrapper;
import javafx.scene.control.*;
import javafx.scene.layout.*;
/** Shared read-only first slice of the binding inspector. */
final class BindingBrowser extends VBox {
final ComboBox<BindingMetadata.Attribute> paths = new ComboBox<>();
final ComboBox<String> directions = new ComboBox<>();
final TableView<BindingMetadata.Target> targets = new TableView<>();
final Label status = new Label("Build and Refresh, then discover binding targets.");
private boolean updating;
BindingBrowser(BiConsumer<String, String> discover) {
setSpacing(5);
paths.setId("binding-path");
directions.setId("binding-direction");
targets.setId("binding-targets");
status.setId("binding-status");
paths.setPrefWidth(320);
directions.getItems().setAll("view", "model", "both");
directions.setValue("both");
Button refresh = new Button("Discover bindings");
refresh.setOnAction(e -> discover.accept(path(), directions.getValue()));
paths.setOnAction(
e -> {
if (!updating) discover.accept(path(), directions.getValue());
});
directions.setOnAction(
e -> {
if (!updating && !paths.getItems().isEmpty())
discover.accept(path(), directions.getValue());
});
column("Control", BindingMetadata.Target::control, 110);
column("Property", BindingMetadata.Target::property, 120);
column("Type", BindingMetadata.Target::type, 180);
column("Compatibility", BindingMetadata.Target::status, 160);
column("Explanation", BindingMetadata.Target::reason, 420);
targets.setPrefHeight(160);
status.setWrapText(true);
getChildren()
.addAll(
new HBox(
8, refresh, new Label("Model path"), paths, new Label("Direction"), directions),
status,
targets);
}
private String path() {
return paths.getValue() == null ? "" : paths.getValue().path();
}
private void column(String name, Function<BindingMetadata.Target, String> value, int width) {
var c = new TableColumn<BindingMetadata.Target, String>(name);
c.setCellValueFactory(v -> new ReadOnlyStringWrapper(value.apply(v.getValue())));
c.setPrefWidth(width);
targets.getColumns().add(c);
}
void invalidate(String message) {
targets.getItems().clear();
status.setText(message);
}
void show(BindingMetadata metadata, String provenance) {
updating = true;
try {
paths.getItems().setAll(metadata.attributes());
paths.setValue(
metadata.attributes().stream()
.filter(a -> a.path().equals(metadata.path()))
.findFirst()
.orElse(null));
directions.setValue(metadata.direction());
targets.getItems().setAll(metadata.targets());
status.setText(
"Model: "
+ metadata.model()
+ " · "
+ provenance
+ "\n"
+ String.join("\n", metadata.diagnostics()));
} finally {
updating = false;
}
}
}
@@ -111,6 +111,9 @@ public final class DesignerPane extends BorderPane implements AutoCloseable {
private final Path reactorRoot;
private DesignerWorkflow workflow;
private DesignerWorkflow interactiveWorkflow;
private DesignerWorkflow bindingWorkflow;
private BindingBrowser bindingBrowser;
private long bindingRequest;
private boolean interactiveRequested, interactiveRestart;
private long interactiveRequest;
private final java.util.concurrent.atomic.AtomicLong draftChanges =
@@ -161,6 +164,15 @@ public final class DesignerPane extends BorderPane implements AutoCloseable {
launch.maven(),
launch.tools(),
this::log);
bindingWorkflow =
new DesignerWorkflow(
host,
launch.configuration(),
launch.context(),
launch.maven(),
launch.tools(),
this::log);
bindingBrowser = new BindingBrowser(this::discoverBindings);
interactiveWorkflow.onInteractiveClosed(
() ->
fx(
@@ -181,6 +193,7 @@ public final class DesignerPane extends BorderPane implements AutoCloseable {
workflow.cancel();
fx(
() -> {
invalidateBindings("Document changed; discover bindings again.");
if (interactiveRequested)
interactiveStatus.setText(
"Interactive preview uses earlier drafts; rerun or save to"
@@ -295,7 +308,9 @@ public final class DesignerPane extends BorderPane implements AutoCloseable {
var properties = new ScrollPane(componentInspector);
properties.setFitToWidth(true);
properties.setPrefViewportHeight(100);
center.setBottom(properties);
var bindings = new TitledPane("Bindings — discovery", bindingBrowser);
bindings.setExpanded(false);
center.setBottom(new VBox(properties, bindings));
output.setEditable(false);
output.setPrefRowCount(5);
Path origin = file.resolveSibling(file.getFileName() + ".origin");
@@ -599,7 +614,45 @@ public final class DesignerPane extends BorderPane implements AutoCloseable {
withDraft(() -> (redo ? document.redo() : document.undo()).whenComplete(this::updated));
}
private void invalidateBindings(String message) {
bindingRequest++;
bindingWorkflow.cancel();
bindingBrowser.invalidate(message);
}
private void discoverBindings(String path, String direction) {
withDraft(
() -> {
long ticket = ++bindingRequest;
bindingBrowser.invalidate("Discovering declared types in target runtime…");
var captured = draft;
bindingWorkflow
.bindings(last, captured, path, direction)
.whenComplete(
(metadata, error) ->
fx(
() -> {
if (closed || ticket != bindingRequest) return;
if (error != null) {
bindingBrowser.invalidate(
"Discovery unavailable: "
+ error.getMessage()
+ ". Build and Refresh, then retry.");
return;
}
bindingBrowser.show(
metadata,
(last
? "explicitly retained build (outdated)"
: "current successful build")
+ " · draft revision "
+ captured.revision());
}));
});
}
private void build(Map<java.net.URI, Long> approvals) {
invalidateBindings("Build changed; discover bindings after Build and Refresh.");
withDraft(
() -> {
renderSequence.incrementAndGet();
@@ -960,6 +1013,8 @@ public final class DesignerPane extends BorderPane implements AutoCloseable {
resize.stop();
workflow.close();
interactiveWorkflow.close();
bindingWorkflow.close();
bindingRequest++;
if (document != null) document.close();
}
}
@@ -426,6 +426,61 @@ public final class ProjectionHostChecks {
check(
document.snapshot().toCompletableFuture().get().text().equals(full),
"Runtime sizing changed source");
fx(
() -> {
pane.lookupAll(".titled-pane").stream()
.filter(n -> n instanceof TitledPane t && t.getText().equals("Bindings — discovery"))
.map(n -> (TitledPane) n)
.forEach(t -> t.setExpanded(true));
return null;
});
click(pane, "Discover bindings");
await(
() ->
fx(
() ->
((TableView<?>) pane.lookup("#binding-targets"))
.getItems().stream()
.anyMatch(
v ->
v
instanceof
de.neitzel.fxdesigner.protocol.BindingMetadata.Target t
&& t.control().equals("rankInput")
&& t.property().equals("text")
&& t.status().equals("COMPATIBLE"))));
fx(
() -> {
var paths =
(ComboBox<de.neitzel.fxdesigner.protocol.BindingMetadata.Attribute>)
pane.lookup("#binding-path");
check(
paths.getItems().stream().anyMatch(a -> a.path().equals("home.street")),
"Binding browser lacks nested path");
paths.setValue(
paths.getItems().stream()
.filter(a -> a.path().equals("rank"))
.findFirst()
.orElseThrow());
return null;
});
await(
() ->
fx(
() ->
((TableView<?>) pane.lookup("#binding-targets"))
.getItems().stream()
.anyMatch(
v ->
v
instanceof
de.neitzel.fxdesigner.protocol.BindingMetadata.Target t
&& t.control().equals("rankInput")
&& t.property().equals("text")
&& t.status().equals("CONVERTER_REQUIRED"))));
check(
document.snapshot().toCompletableFuture().get().text().equals(full),
"Binding discovery altered source");
document
.save(document.snapshot().toCompletableFuture().get().revision())
.toCompletableFuture()