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:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user