Discover Converter capabilities and validate shared binding drafts
This commit is contained in:
@@ -115,6 +115,10 @@ public final class DesignerPane extends BorderPane implements AutoCloseable {
|
||||
private DesignerWorkflow bindingWorkflow;
|
||||
private BindingBrowser bindingBrowser;
|
||||
private DirectBindingEditor bindingEditor;
|
||||
private String validatedSource, validatingSource;
|
||||
private boolean validationPreview;
|
||||
private java.util.List<de.neitzel.fxdesigner.protocol.BindingMetadata.Problem> bindingProblems =
|
||||
java.util.List.of();
|
||||
private Tab directBindingTab;
|
||||
private long bindingRequest;
|
||||
private boolean interactiveRequested, interactiveRestart;
|
||||
@@ -182,7 +186,8 @@ public final class DesignerPane extends BorderPane implements AutoCloseable {
|
||||
if (kit.editor().getFxomDocument() == null) return;
|
||||
var target = new FXOMFxIdIndex(kit.editor().getFxomDocument()).lookup(id);
|
||||
if (target != null) kit.editor().getSelection().select(target);
|
||||
});
|
||||
},
|
||||
values -> validateBindings(values, false));
|
||||
bindingBrowser =
|
||||
new BindingBrowser(
|
||||
this::discoverBindings,
|
||||
@@ -639,6 +644,9 @@ public final class DesignerPane extends BorderPane implements AutoCloseable {
|
||||
|
||||
private void invalidateBindings(String message) {
|
||||
bindingRequest++;
|
||||
validatedSource = null;
|
||||
validatingSource = null;
|
||||
validationPreview = false;
|
||||
bindingWorkflow.cancel();
|
||||
bindingBrowser.invalidate(message);
|
||||
}
|
||||
@@ -682,10 +690,66 @@ public final class DesignerPane extends BorderPane implements AutoCloseable {
|
||||
return false;
|
||||
}
|
||||
|
||||
private void validateBindings(DirectBindingDocument.Values values, boolean preview) {
|
||||
withDraft(
|
||||
() -> {
|
||||
if (preview && draft.text().equals(validatingSource)) {
|
||||
validationPreview = true;
|
||||
return;
|
||||
}
|
||||
long ticket = ++bindingRequest;
|
||||
var captured = draft;
|
||||
validatingSource = captured.text();
|
||||
validationPreview |= preview;
|
||||
status.setText("Validating binding types in target runtime…");
|
||||
bindingWorkflow
|
||||
.bindingDetails(
|
||||
last,
|
||||
captured,
|
||||
values == null ? "" : values.path(),
|
||||
values == null ? "both" : values.direction(),
|
||||
values == null ? "" : values.control(),
|
||||
values == null ? "" : values.property())
|
||||
.whenComplete(
|
||||
(metadata, error) ->
|
||||
fx(
|
||||
() -> {
|
||||
if (closed
|
||||
|| ticket != bindingRequest
|
||||
|| draft.revision() != captured.revision()) return;
|
||||
validatingSource = null;
|
||||
boolean resumePreview = validationPreview;
|
||||
validationPreview = false;
|
||||
if (error != null) {
|
||||
status.setText(
|
||||
"Binding validation unavailable: " + error.getMessage());
|
||||
return;
|
||||
}
|
||||
validatedSource = captured.text();
|
||||
bindingProblems = metadata.problems();
|
||||
bindingEditor.metadata(
|
||||
metadata,
|
||||
values,
|
||||
(last
|
||||
? "Explicit retained build (outdated)"
|
||||
: "Current successful build")
|
||||
+ " · draft revision "
|
||||
+ captured.revision());
|
||||
status.setText(
|
||||
"Binding validation complete: "
|
||||
+ metadata.problems().size()
|
||||
+ " diagnostics");
|
||||
if (resumePreview) render();
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
private void discoverBindings(String path, String direction) {
|
||||
withDraft(
|
||||
() -> {
|
||||
long ticket = ++bindingRequest;
|
||||
validatingSource = null;
|
||||
validationPreview = false;
|
||||
bindingBrowser.invalidate("Discovering declared types in target runtime…");
|
||||
var captured = draft;
|
||||
bindingWorkflow
|
||||
@@ -803,6 +867,19 @@ public final class DesignerPane extends BorderPane implements AutoCloseable {
|
||||
withDraft(
|
||||
() -> {
|
||||
if (!bindingPreviewAllowed()) return;
|
||||
if (!draft.text().equals(validatedSource)) {
|
||||
validateBindings(null, true);
|
||||
return;
|
||||
}
|
||||
if (bindingProblems.stream().anyMatch(p -> p.severity().equals("ERROR"))) {
|
||||
status.setText(
|
||||
"Preview blocked: "
|
||||
+ bindingProblems.stream()
|
||||
.filter(p -> p.severity().equals("ERROR"))
|
||||
.map(p -> p.message())
|
||||
.collect(java.util.stream.Collectors.joining("; ")));
|
||||
return;
|
||||
}
|
||||
var selected = examples.getValue();
|
||||
if (selected == null || projection == null) return;
|
||||
long ticket = renderSequence.incrementAndGet();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package de.neitzel.fxdesigner.kit;
|
||||
|
||||
import de.neitzel.fxdesigner.protocol.BindingMetadata;
|
||||
import java.util.function.*;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.layout.*;
|
||||
@@ -18,6 +19,8 @@ final class DirectBindingEditor extends VBox {
|
||||
property = field("property"),
|
||||
path = field("path"),
|
||||
converter = field("converter");
|
||||
private final ComboBox<BindingMetadata.Converter> converters = new ComboBox<>();
|
||||
private final Button inspect = new Button("Find Converters / Validate");
|
||||
private final ComboBox<String> direction = new ComboBox<>();
|
||||
private final CheckBox replace = new CheckBox("Replace conflicting bindings");
|
||||
private final Label status = new Label("Open a valid FXML document to edit bindings.");
|
||||
@@ -29,9 +32,23 @@ final class DirectBindingEditor extends VBox {
|
||||
private boolean loading, writable, active, followingCanvas;
|
||||
private final Consumer<String> navigate;
|
||||
|
||||
DirectBindingEditor(Consumer<Intent> edit, Consumer<String> navigate) {
|
||||
DirectBindingEditor(
|
||||
Consumer<Intent> edit,
|
||||
Consumer<String> navigate,
|
||||
Consumer<DirectBindingDocument.Values> discover) {
|
||||
this.navigate = navigate;
|
||||
setSpacing(5);
|
||||
converters.setId("direct-converters");
|
||||
converters.setPrefWidth(680);
|
||||
inspect.setOnAction(e -> discover.accept(values()));
|
||||
converters.setOnAction(
|
||||
e -> {
|
||||
var c = converters.getValue();
|
||||
if (c != null) converter.setText(c.name());
|
||||
});
|
||||
for (var field : java.util.List.of(control, property, path))
|
||||
field.textProperty().addListener((o, a, b) -> converters.getItems().clear());
|
||||
direction.valueProperty().addListener((o, a, b) -> converters.getItems().clear());
|
||||
bindings.setId("direct-bindings");
|
||||
bindings.setPrefHeight(100);
|
||||
direction.setId("direct-direction");
|
||||
@@ -73,7 +90,13 @@ final class DirectBindingEditor extends VBox {
|
||||
new Label("Direction"),
|
||||
direction);
|
||||
grid.addRow(1, new Label("Model path"), path, new Label("Converter (optional)"), converter);
|
||||
getChildren().addAll(bindings, grid, new HBox(8, fresh, apply, remove, replace), status);
|
||||
getChildren()
|
||||
.addAll(
|
||||
bindings,
|
||||
grid,
|
||||
new HBox(8, inspect, converters),
|
||||
new HBox(8, fresh, apply, remove, replace),
|
||||
status);
|
||||
buttons();
|
||||
}
|
||||
|
||||
@@ -120,6 +143,7 @@ final class DirectBindingEditor extends VBox {
|
||||
|
||||
void load(String text, boolean editable) {
|
||||
if (source.equals(text) && writable == editable) return;
|
||||
converters.getItems().clear();
|
||||
source = text;
|
||||
writable = editable;
|
||||
loading = true;
|
||||
@@ -131,8 +155,8 @@ final class DirectBindingEditor extends VBox {
|
||||
bindings.getSelectionModel().clearSelection();
|
||||
status.setText(
|
||||
doc.errors().isEmpty()
|
||||
? "Select a DirectBinding or choose New binding. Converter types are not yet"
|
||||
+ " verified."
|
||||
? "Select a DirectBinding or choose New binding. Use Find Converters / Validate to"
|
||||
+ " check this draft."
|
||||
: String.join("\n", doc.errors()));
|
||||
} catch (Exception error) {
|
||||
bindings.getItems().clear();
|
||||
@@ -162,6 +186,35 @@ final class DirectBindingEditor extends VBox {
|
||||
});
|
||||
}
|
||||
|
||||
void metadata(
|
||||
BindingMetadata metadata, DirectBindingDocument.Values requested, String provenance) {
|
||||
status.setText(
|
||||
provenance
|
||||
+ "\n"
|
||||
+ (metadata.problems().isEmpty()
|
||||
? "No detected binding errors."
|
||||
: metadata.problems().stream()
|
||||
.map(
|
||||
p ->
|
||||
p.severity()
|
||||
+ " "
|
||||
+ p.control()
|
||||
+ "."
|
||||
+ p.property()
|
||||
+ ": "
|
||||
+ p.message())
|
||||
.collect(java.util.stream.Collectors.joining("\n"))));
|
||||
if (requested == null || !values().equals(requested)) return;
|
||||
converters.getItems().setAll(metadata.converters());
|
||||
var matches =
|
||||
metadata.converters().stream().filter(c -> c.status().equals("COMPATIBLE")).toList();
|
||||
if (converter.getText().isBlank() && matches.size() == 1) {
|
||||
converters.setValue(matches.getFirst());
|
||||
status.setText(
|
||||
status.getText() + "\nUnique compatible Converter proposed; review and Apply binding.");
|
||||
}
|
||||
}
|
||||
|
||||
void error(String message) {
|
||||
status.setText(message);
|
||||
}
|
||||
|
||||
@@ -267,6 +267,88 @@ public final class ProjectionHostChecks {
|
||||
return doc.snapshot().toCompletableFuture().get().text();
|
||||
}
|
||||
|
||||
private static boolean bindingStatus(DesignerPane pane, String text) throws Exception {
|
||||
return fx(() -> ((Label) pane.lookup("#direct-status")).getText().contains(text));
|
||||
}
|
||||
|
||||
private static void converterEdits(DesignerPane pane, DocumentSession document, String full)
|
||||
throws Exception {
|
||||
bindingTab(pane, "Direct bindings");
|
||||
selectDirect(pane, "rankInput");
|
||||
click(pane, "Find Converters / Validate");
|
||||
await(() -> fx(() -> !((ComboBox<?>) pane.lookup("#direct-converters")).getItems().isEmpty()));
|
||||
check(
|
||||
fx(
|
||||
() ->
|
||||
((TextField) pane.lookup("#direct-converter"))
|
||||
.getText()
|
||||
.equals("fixture.components.RankConverter")),
|
||||
"Existing Converter changed");
|
||||
check(documentText(document).equals(full), "Validation altered source");
|
||||
directField(pane, "converter", "");
|
||||
click(pane, "Apply binding");
|
||||
await(() -> !documentText(document).equals(full));
|
||||
String invalid = documentText(document);
|
||||
await(() -> fx(() -> source(pane).getText().equals(invalid)));
|
||||
click(pane, "Render drafts");
|
||||
await(() -> bindingStatus(pane, "Incompatible value types"));
|
||||
await(
|
||||
() ->
|
||||
fx(
|
||||
() ->
|
||||
pane.lookupAll(".label").stream()
|
||||
.anyMatch(
|
||||
n ->
|
||||
n instanceof Label l
|
||||
&& l.getText() != null
|
||||
&& l.getText().startsWith("Preview blocked:"))));
|
||||
selectDirect(pane, "rankInput");
|
||||
click(pane, "Find Converters / Validate");
|
||||
await(
|
||||
() ->
|
||||
fx(
|
||||
() ->
|
||||
((TextField) pane.lookup("#direct-converter"))
|
||||
.getText()
|
||||
.equals("fixture.components.RankConverter")));
|
||||
check(documentText(document).equals(invalid), "Proposal applied without review");
|
||||
click(pane, "Apply binding");
|
||||
await(() -> !documentText(document).equals(invalid));
|
||||
String repaired = documentText(document);
|
||||
click(pane, "Undo");
|
||||
await(() -> documentText(document).equals(invalid));
|
||||
click(pane, "Redo");
|
||||
await(() -> documentText(document).equals(repaired));
|
||||
await(() -> fx(() -> source(pane).getText().equals(repaired)));
|
||||
selectDirect(pane, "rankInput");
|
||||
directField(pane, "converter", "fixture.components.LegacyRankConverter");
|
||||
click(pane, "Apply binding");
|
||||
await(() -> documentText(document).contains("LegacyRankConverter"));
|
||||
await(() -> fx(() -> source(pane).getText().contains("LegacyRankConverter")));
|
||||
selectDirect(pane, "rankInput");
|
||||
click(pane, "Find Converters / Validate");
|
||||
await(() -> bindingStatus(pane, "WARNING rankInput.text"));
|
||||
check(
|
||||
fx(
|
||||
() ->
|
||||
((TextField) pane.lookup("#direct-converter"))
|
||||
.getText()
|
||||
.equals("fixture.components.LegacyRankConverter")),
|
||||
"Unverified explicit Converter replaced");
|
||||
set(document, full);
|
||||
await(() -> fx(() -> source(pane).getText().equals(full)));
|
||||
click(pane, "New binding");
|
||||
directField(pane, "control", "action");
|
||||
directField(pane, "property", "text");
|
||||
directField(pane, "path", "name");
|
||||
click(pane, "Find Converters / Validate");
|
||||
await(() -> fx(() -> ((ComboBox<?>) pane.lookup("#direct-converters")).getItems().size() > 1));
|
||||
check(
|
||||
fx(() -> ((TextField) pane.lookup("#direct-converter")).getText().isEmpty()),
|
||||
"Ambiguous candidates silently selected");
|
||||
check(documentText(document).equals(full), "Converter browsing altered source");
|
||||
}
|
||||
|
||||
private static void directBindingEdits(DesignerPane pane, DocumentSession document, String full)
|
||||
throws Exception {
|
||||
bindingTab(pane, "Direct bindings");
|
||||
@@ -697,6 +779,7 @@ public final class ProjectionHostChecks {
|
||||
&& ((TextField) pane.lookup("#direct-path")).getText().equals("rank")),
|
||||
"Discovery did not populate binding form");
|
||||
check(documentText(document).equals(full), "Discovery handoff changed source before Apply");
|
||||
converterEdits(pane, document, full);
|
||||
directBindingEdits(pane, document, full);
|
||||
document
|
||||
.save(document.snapshot().toCompletableFuture().get().revision())
|
||||
|
||||
Reference in New Issue
Block a user