Add shared source-preserving Repeat binding inspector
This commit is contained in:
@@ -6,7 +6,7 @@ Shared Scene Builder Kit designer for NeitzelLib FX, with minimal standalone and
|
|||||||
|
|
||||||
The first workflow is runnable in both hosts: Build and Refresh publishes immutable snapshots; authenticated target workers discover examples and render parent/Component FXML drafts into the shared Kit canvas. Source and supported canvas edits use host-owned documents and undo/redo. See [the connected workflow](docs/first-connected-workflow.md) for architecture, launch commands, evidence and limits.
|
The first workflow is runnable in both hosts: Build and Refresh publishes immutable snapshots; authenticated target workers discover examples and render parent/Component FXML drafts into the shared Kit canvas. Source and supported canvas edits use host-owned documents and undo/redo. See [the connected workflow](docs/first-connected-workflow.md) for architecture, launch commands, evidence and limits.
|
||||||
|
|
||||||
The repository builds six shared Maven modules, a Maven capture plugin and a separate IntelliJ adapter plugin. Application code runs only in target JVMs. The shared UI now applies [source-preserving edits](docs/source-preserving-projection.md), supports nested/anonymous Components and fx:root bodies, and provides Source/Design/Split modes. Retained [target sessions, target sizing, interactive preview and Component/Row navigation](docs/target-runtime-sessions.md) now extend that workflow. The shared [binding discovery browser](docs/binding-discovery.md) now lists declared Model paths and direction-aware target compatibility. The [DirectBinding inspector](docs/direct-binding-inspector.md) creates, edits and removes declarations through source-preserving host Undo/Redo. Shared [Converter discovery and binding validation](docs/converter-validation.md) now adds capability-based proposals, document diagnostics and preview gating. [Custom ViewModel discovery and Computed-binding validation](docs/viewmodel-computed-validation.md) now follow the explicitly selected Controller or Component in both hosts. The [Computed-binding inspector](docs/computed-binding-inspector.md) now creates, changes and removes Computed declarations with method suggestions and shared source-preserving Undo/Redo. Broader binding authoring, generation, structural editing and product packaging remain later work.
|
The repository builds six shared Maven modules, a Maven capture plugin and a separate IntelliJ adapter plugin. Application code runs only in target JVMs. The shared UI now applies [source-preserving edits](docs/source-preserving-projection.md), supports nested/anonymous Components and fx:root bodies, and provides Source/Design/Split modes. Retained [target sessions, target sizing, interactive preview and Component/Row navigation](docs/target-runtime-sessions.md) now extend that workflow. The shared [binding discovery browser](docs/binding-discovery.md) now lists declared Model paths and direction-aware target compatibility. The [DirectBinding inspector](docs/direct-binding-inspector.md) creates, edits and removes declarations through source-preserving host Undo/Redo. Shared [Converter discovery and binding validation](docs/converter-validation.md) now adds capability-based proposals, document diagnostics and preview gating. [Custom ViewModel discovery and Computed-binding validation](docs/viewmodel-computed-validation.md) now follow the explicitly selected Controller or Component in both hosts. The [Computed-binding inspector](docs/computed-binding-inspector.md) now creates, changes and removes Computed declarations with method suggestions and shared source-preserving Undo/Redo. The [Repeat inspector](docs/repeat-binding-inspector.md) adds source-preserving collection declarations with host Undo/Redo. Broader binding authoring, generation, structural editing and product packaging remain later work.
|
||||||
|
|
||||||
Configuration 1.0 schemas, context selection, local JDK mappings and draft protocol validation are now executable. See [configuration and protocol](docs/configuration-and-protocol.md) for formats and validation rules.
|
Configuration 1.0 schemas, context selection, local JDK mappings and draft protocol validation are now executable. See [configuration and protocol](docs/configuration-and-protocol.md) for formats and validation rules.
|
||||||
|
|
||||||
|
|||||||
@@ -10,10 +10,15 @@ final class BindingDocument {
|
|||||||
|
|
||||||
enum Kind {
|
enum Kind {
|
||||||
DIRECT,
|
DIRECT,
|
||||||
COMPUTED;
|
COMPUTED,
|
||||||
|
REPEAT;
|
||||||
|
|
||||||
String element() {
|
String element() {
|
||||||
return this == DIRECT ? "DirectBinding" : "ComputedBinding";
|
return switch (this) {
|
||||||
|
case DIRECT -> "DirectBinding";
|
||||||
|
case COMPUTED -> "ComputedBinding";
|
||||||
|
case REPEAT -> "Repeat";
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -23,18 +28,31 @@ final class BindingDocument {
|
|||||||
String path,
|
String path,
|
||||||
String direction,
|
String direction,
|
||||||
String converter,
|
String converter,
|
||||||
String method) {
|
String method,
|
||||||
|
String pane,
|
||||||
|
String component,
|
||||||
|
String collection) {
|
||||||
Values(String control, String property, String path, String direction, String converter) {
|
Values(String control, String property, String path, String direction, String converter) {
|
||||||
this(control, property, path, direction, converter, "");
|
this(control, property, path, direction, converter, "", "", "", "");
|
||||||
|
}
|
||||||
|
|
||||||
|
static Values repeat(String pane, String component, String collection) {
|
||||||
|
return new Values("", "", "", "view", "", "", pane, component, collection);
|
||||||
|
}
|
||||||
|
|
||||||
|
String target() {
|
||||||
|
return pane.isEmpty() ? control : pane;
|
||||||
}
|
}
|
||||||
|
|
||||||
static Values computed(String control, String property, String method) {
|
static Values computed(String control, String property, String method) {
|
||||||
return new Values(control, property, "", "view", "", method);
|
return new Values(control, property, "", "view", "", method, "", "", "");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
record Entry(int index, Values values, boolean editable) {
|
record Entry(int index, Values values, boolean editable) {
|
||||||
public String toString() {
|
public String toString() {
|
||||||
|
if (!values.pane().isEmpty())
|
||||||
|
return values.pane() + " ← " + values.collection() + " (" + values.component() + ")";
|
||||||
return values.control()
|
return values.control()
|
||||||
+ "."
|
+ "."
|
||||||
+ values.property()
|
+ values.property()
|
||||||
@@ -115,6 +133,9 @@ final class BindingDocument {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Values values(Element e) {
|
private Values values(Element e) {
|
||||||
|
if (kind == Kind.REPEAT)
|
||||||
|
return Values.repeat(
|
||||||
|
e.getAttribute("pane"), e.getAttribute("component"), e.getAttribute("collection"));
|
||||||
if (kind == Kind.COMPUTED)
|
if (kind == Kind.COMPUTED)
|
||||||
return Values.computed(
|
return Values.computed(
|
||||||
e.getAttribute("control"), e.getAttribute("property"), e.getAttribute("method"));
|
e.getAttribute("control"), e.getAttribute("property"), e.getAttribute("method"));
|
||||||
@@ -170,9 +191,34 @@ final class BindingDocument {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
var panes = new HashSet<String>();
|
||||||
|
for (var e : elements)
|
||||||
|
if (binding(e, "Repeat")) {
|
||||||
|
if (hasElementChildren(e)) continue;
|
||||||
|
String pane = e.getAttribute("pane");
|
||||||
|
if (!panes.add(pane)) errors.add("Duplicate Repeat pane: " + pane);
|
||||||
|
if (ids.getOrDefault(pane, 0) != 1) errors.add("Missing or ambiguous Repeat pane: " + pane);
|
||||||
|
if (!qualifiedName(e.getAttribute("collection")))
|
||||||
|
errors.add("Invalid or missing Repeat collection: " + pane);
|
||||||
|
if (!qualifiedName(e.getAttribute("component")))
|
||||||
|
errors.add("Invalid or missing Repeat Component class: " + pane);
|
||||||
|
}
|
||||||
return List.copyOf(errors);
|
return List.copyOf(errors);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static boolean qualifiedName(String value) {
|
||||||
|
return value.matches(
|
||||||
|
"[\\p{javaJavaIdentifierStart}][\\p{javaJavaIdentifierPart}]*(\\.[\\p{javaJavaIdentifierStart}][\\p{javaJavaIdentifierPart}]*)*");
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean conflicts(Element e, Values value) {
|
||||||
|
if (kind == Kind.REPEAT)
|
||||||
|
return binding(e, "Repeat") && e.getAttribute("pane").equals(value.pane());
|
||||||
|
return (binding(e, "DirectBinding") || binding(e, "ComputedBinding"))
|
||||||
|
&& e.getAttribute("control").equals(value.control())
|
||||||
|
&& e.getAttribute("property").equals(value.property());
|
||||||
|
}
|
||||||
|
|
||||||
List<Replacement> remove(int index) {
|
List<Replacement> remove(int index) {
|
||||||
Element e = entry(index);
|
Element e = entry(index);
|
||||||
var t = source.tags.get(indexes.get(e));
|
var t = source.tags.get(indexes.get(e));
|
||||||
@@ -180,23 +226,20 @@ final class BindingDocument {
|
|||||||
}
|
}
|
||||||
|
|
||||||
List<Replacement> write(Integer index, Values value, boolean replaceConflicts) {
|
List<Replacement> write(Integer index, Values value, boolean replaceConflicts) {
|
||||||
if (value.control().isBlank() || value.property().isBlank())
|
if (kind == Kind.REPEAT && value.pane().isBlank())
|
||||||
|
throw new IllegalArgumentException("Choose a pane");
|
||||||
|
if (kind != Kind.REPEAT && (value.control().isBlank() || value.property().isBlank()))
|
||||||
throw new IllegalArgumentException("Choose a control and property");
|
throw new IllegalArgumentException("Choose a control and property");
|
||||||
Element original = index == null ? null : entry(index);
|
Element original = index == null ? null : entry(index);
|
||||||
if (original != null && hasElementChildren(original))
|
if (original != null && hasElementChildren(original))
|
||||||
throw new IllegalArgumentException(
|
throw new IllegalArgumentException(
|
||||||
"This declaration uses nested property elements; edit it in Source.");
|
"This declaration uses nested property elements; edit it in Source.");
|
||||||
var conflicts =
|
var conflicts = elements.stream().filter(e -> e != original && conflicts(e, value)).toList();
|
||||||
elements.stream()
|
|
||||||
.filter(
|
|
||||||
e ->
|
|
||||||
e != original
|
|
||||||
&& (binding(e, "DirectBinding") || binding(e, "ComputedBinding"))
|
|
||||||
&& e.getAttribute("control").equals(value.control())
|
|
||||||
&& e.getAttribute("property").equals(value.property()))
|
|
||||||
.toList();
|
|
||||||
if (!conflicts.isEmpty() && !replaceConflicts)
|
if (!conflicts.isEmpty() && !replaceConflicts)
|
||||||
throw new Conflict(value.control() + "." + value.property());
|
throw new Conflict(
|
||||||
|
kind == Kind.REPEAT
|
||||||
|
? "Repeat pane " + value.pane()
|
||||||
|
: value.control() + "." + value.property());
|
||||||
var edits = new ArrayList<Replacement>();
|
var edits = new ArrayList<Replacement>();
|
||||||
for (var e : conflicts) {
|
for (var e : conflicts) {
|
||||||
var t = source.tags.get(indexes.get(e));
|
var t = source.tags.get(indexes.get(e));
|
||||||
@@ -205,7 +248,15 @@ final class BindingDocument {
|
|||||||
if (original != null) {
|
if (original != null) {
|
||||||
var changed = new LinkedHashMap<String, String>();
|
var changed = new LinkedHashMap<String, String>();
|
||||||
Map<String, String> fields =
|
Map<String, String> fields =
|
||||||
kind == Kind.COMPUTED
|
kind == Kind.REPEAT
|
||||||
|
? Map.of(
|
||||||
|
"pane",
|
||||||
|
value.pane(),
|
||||||
|
"component",
|
||||||
|
value.component(),
|
||||||
|
"collection",
|
||||||
|
value.collection())
|
||||||
|
: kind == Kind.COMPUTED
|
||||||
? Map.of(
|
? Map.of(
|
||||||
"control",
|
"control",
|
||||||
value.control(),
|
value.control(),
|
||||||
@@ -240,7 +291,19 @@ final class BindingDocument {
|
|||||||
+ "\" property=\""
|
+ "\" property=\""
|
||||||
+ XmlSource.escape(value.property(), '"')
|
+ XmlSource.escape(value.property(), '"')
|
||||||
+ "\"";
|
+ "\"";
|
||||||
if (kind == Kind.COMPUTED)
|
if (kind == Kind.REPEAT)
|
||||||
|
node =
|
||||||
|
"<"
|
||||||
|
+ PACKAGE
|
||||||
|
+ kind.element()
|
||||||
|
+ " pane=\""
|
||||||
|
+ XmlSource.escape(value.pane(), '"')
|
||||||
|
+ "\" component=\""
|
||||||
|
+ XmlSource.escape(value.component(), '"')
|
||||||
|
+ "\" collection=\""
|
||||||
|
+ XmlSource.escape(value.collection(), '"')
|
||||||
|
+ "\"";
|
||||||
|
else if (kind == Kind.COMPUTED)
|
||||||
node += " method=\"" + XmlSource.escape(value.method(), '"') + "\"";
|
node += " method=\"" + XmlSource.escape(value.method(), '"') + "\"";
|
||||||
else
|
else
|
||||||
node +=
|
node +=
|
||||||
|
|||||||
@@ -20,7 +20,10 @@ final class BindingEditor extends VBox {
|
|||||||
property = field("property"),
|
property = field("property"),
|
||||||
path = field("path"),
|
path = field("path"),
|
||||||
converter = field("converter"),
|
converter = field("converter"),
|
||||||
method = field("method");
|
method = field("method"),
|
||||||
|
pane = field("pane"),
|
||||||
|
component = field("component"),
|
||||||
|
collection = field("collection");
|
||||||
private final ComboBox<BindingMetadata.Computed> methods = new ComboBox<>();
|
private final ComboBox<BindingMetadata.Computed> methods = new ComboBox<>();
|
||||||
private final BindingDocument.Kind kind;
|
private final BindingDocument.Kind kind;
|
||||||
private final ComboBox<BindingMetadata.Converter> converters = new ComboBox<>();
|
private final ComboBox<BindingMetadata.Converter> converters = new ComboBox<>();
|
||||||
@@ -43,9 +46,11 @@ final class BindingEditor extends VBox {
|
|||||||
Consumer<BindingDocument.Values> discover) {
|
Consumer<BindingDocument.Values> discover) {
|
||||||
this.kind = kind;
|
this.kind = kind;
|
||||||
boolean computed = kind == BindingDocument.Kind.COMPUTED;
|
boolean computed = kind == BindingDocument.Kind.COMPUTED;
|
||||||
String prefix = computed ? "computed-" : "direct-";
|
boolean repeat = kind == BindingDocument.Kind.REPEAT;
|
||||||
|
String prefix = repeat ? "repeat-" : computed ? "computed-" : "direct-";
|
||||||
this.navigate = navigate;
|
this.navigate = navigate;
|
||||||
for (var field : java.util.List.of(control, property, path, converter, method))
|
for (var field :
|
||||||
|
java.util.List.of(control, property, path, converter, method, pane, component, collection))
|
||||||
field.setId(prefix + field.getId());
|
field.setId(prefix + field.getId());
|
||||||
methods.setId("computed-sources");
|
methods.setId("computed-sources");
|
||||||
methods.setPrefWidth(680);
|
methods.setPrefWidth(680);
|
||||||
@@ -60,6 +65,12 @@ final class BindingEditor extends VBox {
|
|||||||
apply.setText("Apply Computed binding");
|
apply.setText("Apply Computed binding");
|
||||||
remove.setText("Remove Computed binding");
|
remove.setText("Remove Computed binding");
|
||||||
}
|
}
|
||||||
|
if (repeat) {
|
||||||
|
inspect.setText("Validate Repeat bindings");
|
||||||
|
fresh.setText("New Repeat binding");
|
||||||
|
apply.setText("Apply Repeat binding");
|
||||||
|
remove.setText("Remove Repeat binding");
|
||||||
|
}
|
||||||
setSpacing(5);
|
setSpacing(5);
|
||||||
converters.setId("direct-converters");
|
converters.setId("direct-converters");
|
||||||
converters.setPrefWidth(680);
|
converters.setPrefWidth(680);
|
||||||
@@ -83,7 +94,9 @@ final class BindingEditor extends VBox {
|
|||||||
fresh.setOnAction(
|
fresh.setOnAction(
|
||||||
e ->
|
e ->
|
||||||
newBinding(
|
newBinding(
|
||||||
computed
|
repeat
|
||||||
|
? BindingDocument.Values.repeat("", "", "")
|
||||||
|
: computed
|
||||||
? BindingDocument.Values.computed("", "", "")
|
? BindingDocument.Values.computed("", "", "")
|
||||||
: new BindingDocument.Values("", "", "", "both", "")));
|
: new BindingDocument.Values("", "", "", "both", "")));
|
||||||
apply.setOnAction(
|
apply.setOnAction(
|
||||||
@@ -101,7 +114,7 @@ final class BindingEditor extends VBox {
|
|||||||
fill(b.values());
|
fill(b.values());
|
||||||
replace.setSelected(false);
|
replace.setSelected(false);
|
||||||
buttons();
|
buttons();
|
||||||
if (!followingCanvas) navigate.accept(b.values().control());
|
if (!followingCanvas) navigate.accept(b.values().target());
|
||||||
if (!b.editable())
|
if (!b.editable())
|
||||||
status.setText(
|
status.setText(
|
||||||
"Nested property elements are preserved. Edit this declaration in Source, or"
|
"Nested property elements are preserved. Edit this declaration in Source, or"
|
||||||
@@ -110,7 +123,7 @@ final class BindingEditor extends VBox {
|
|||||||
var grid = new GridPane();
|
var grid = new GridPane();
|
||||||
grid.setHgap(8);
|
grid.setHgap(8);
|
||||||
grid.setVgap(4);
|
grid.setVgap(4);
|
||||||
if (!computed)
|
if (!computed && !repeat)
|
||||||
grid.addRow(
|
grid.addRow(
|
||||||
0,
|
0,
|
||||||
new Label("Control (fx:id)"),
|
new Label("Control (fx:id)"),
|
||||||
@@ -119,19 +132,23 @@ final class BindingEditor extends VBox {
|
|||||||
property,
|
property,
|
||||||
new Label("Direction"),
|
new Label("Direction"),
|
||||||
direction);
|
direction);
|
||||||
if (!computed)
|
if (!computed && !repeat)
|
||||||
grid.addRow(1, new Label("Model path"), path, new Label("Converter (optional)"), converter);
|
grid.addRow(1, new Label("Model path"), path, new Label("Converter (optional)"), converter);
|
||||||
else {
|
else if (computed) {
|
||||||
grid.addRow(0, new Label("Control (fx:id)"), control, new Label("Property"), property);
|
grid.addRow(0, new Label("Control (fx:id)"), control, new Label("Property"), property);
|
||||||
grid.addRow(1, new Label("Controller method"), method);
|
grid.addRow(1, new Label("Controller method"), method);
|
||||||
for (var field : java.util.List.of(control, property))
|
for (var field : java.util.List.of(control, property))
|
||||||
field.textProperty().addListener((o, a, b) -> methods.getItems().clear());
|
field.textProperty().addListener((o, a, b) -> methods.getItems().clear());
|
||||||
}
|
}
|
||||||
|
if (repeat) {
|
||||||
|
grid.addRow(0, new Label("Pane (fx:id)"), pane, new Label("Collection path"), collection);
|
||||||
|
grid.addRow(1, new Label("Component class"), component);
|
||||||
|
}
|
||||||
getChildren()
|
getChildren()
|
||||||
.addAll(
|
.addAll(
|
||||||
bindings,
|
bindings,
|
||||||
grid,
|
grid,
|
||||||
new HBox(8, inspect, computed ? methods : converters),
|
repeat ? new HBox(8, inspect) : new HBox(8, inspect, computed ? methods : converters),
|
||||||
new HBox(8, fresh, apply, remove, replace),
|
new HBox(8, fresh, apply, remove, replace),
|
||||||
status);
|
status);
|
||||||
buttons();
|
buttons();
|
||||||
@@ -145,6 +162,9 @@ final class BindingEditor extends VBox {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private BindingDocument.Values values() {
|
private BindingDocument.Values values() {
|
||||||
|
if (kind == BindingDocument.Kind.REPEAT)
|
||||||
|
return BindingDocument.Values.repeat(
|
||||||
|
pane.getText().trim(), component.getText().trim(), collection.getText().trim());
|
||||||
if (kind == BindingDocument.Kind.COMPUTED)
|
if (kind == BindingDocument.Kind.COMPUTED)
|
||||||
return BindingDocument.Values.computed(
|
return BindingDocument.Values.computed(
|
||||||
control.getText().trim(), property.getText().trim(), method.getText().trim());
|
control.getText().trim(), property.getText().trim(), method.getText().trim());
|
||||||
@@ -163,6 +183,9 @@ final class BindingEditor extends VBox {
|
|||||||
direction.setValue(v.direction());
|
direction.setValue(v.direction());
|
||||||
converter.setText(v.converter());
|
converter.setText(v.converter());
|
||||||
method.setText(v.method());
|
method.setText(v.method());
|
||||||
|
pane.setText(v.pane());
|
||||||
|
component.setText(v.component());
|
||||||
|
collection.setText(v.collection());
|
||||||
}
|
}
|
||||||
|
|
||||||
void newBinding(BindingDocument.Values v) {
|
void newBinding(BindingDocument.Values v) {
|
||||||
@@ -214,9 +237,9 @@ final class BindingEditor extends VBox {
|
|||||||
void selectControl(String id) {
|
void selectControl(String id) {
|
||||||
if (loading || id == null) return;
|
if (loading || id == null) return;
|
||||||
var selected = bindings.getSelectionModel().getSelectedItem();
|
var selected = bindings.getSelectionModel().getSelectedItem();
|
||||||
if (selected != null && selected.values().control().equals(id)) return;
|
if (selected != null && selected.values().target().equals(id)) return;
|
||||||
bindings.getItems().stream()
|
bindings.getItems().stream()
|
||||||
.filter(e -> e.values().control().equals(id))
|
.filter(e -> e.values().target().equals(id))
|
||||||
.findFirst()
|
.findFirst()
|
||||||
.ifPresent(
|
.ifPresent(
|
||||||
e -> {
|
e -> {
|
||||||
@@ -250,6 +273,21 @@ final class BindingEditor extends VBox {
|
|||||||
+ ": "
|
+ ": "
|
||||||
+ p.message())
|
+ p.message())
|
||||||
.collect(java.util.stream.Collectors.joining("\n"))));
|
.collect(java.util.stream.Collectors.joining("\n"))));
|
||||||
|
if (kind == BindingDocument.Kind.REPEAT) {
|
||||||
|
try {
|
||||||
|
var errors = new BindingDocument(source, kind).errors();
|
||||||
|
status.setText(
|
||||||
|
provenance
|
||||||
|
+ "\n"
|
||||||
|
+ (errors.isEmpty() ? "No structural binding errors." : String.join("\n", errors))
|
||||||
|
+ "\n"
|
||||||
|
+ "Collection and Component compatibility have not been checked. Render drafts to"
|
||||||
|
+ " test them.");
|
||||||
|
} catch (Exception error) {
|
||||||
|
status.setText("Validation unavailable: " + error.getMessage());
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (requested == null || !values().equals(requested)) return;
|
if (requested == null || !values().equals(requested)) return;
|
||||||
if (kind == BindingDocument.Kind.COMPUTED) {
|
if (kind == BindingDocument.Kind.COMPUTED) {
|
||||||
methods.getItems().setAll(metadata.computed());
|
methods.getItems().setAll(metadata.computed());
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ public final class DesignerPane extends BorderPane implements AutoCloseable {
|
|||||||
String id = hit.getFxId();
|
String id = hit.getFxId();
|
||||||
if (bindingEditor != null) bindingEditor.selectControl(id);
|
if (bindingEditor != null) bindingEditor.selectControl(id);
|
||||||
if (computedEditor != null) computedEditor.selectControl(id);
|
if (computedEditor != null) computedEditor.selectControl(id);
|
||||||
|
if (repeatEditor != null) repeatEditor.selectControl(id);
|
||||||
if (id == null || !projection.components().containsKey(id)) return;
|
if (id == null || !projection.components().containsKey(id)) return;
|
||||||
componentInspector.getChildren().add(new Label(projection.components().get(id) + " · " + id));
|
componentInspector.getChildren().add(new Label(projection.components().get(id) + " · " + id));
|
||||||
componentInspector
|
componentInspector
|
||||||
@@ -115,7 +116,7 @@ public final class DesignerPane extends BorderPane implements AutoCloseable {
|
|||||||
private DesignerWorkflow interactiveWorkflow;
|
private DesignerWorkflow interactiveWorkflow;
|
||||||
private DesignerWorkflow bindingWorkflow;
|
private DesignerWorkflow bindingWorkflow;
|
||||||
private BindingBrowser bindingBrowser;
|
private BindingBrowser bindingBrowser;
|
||||||
private BindingEditor bindingEditor, computedEditor;
|
private BindingEditor bindingEditor, computedEditor, repeatEditor;
|
||||||
private String validatedSource, validatingSource;
|
private String validatedSource, validatingSource;
|
||||||
private boolean validationPreview;
|
private boolean validationPreview;
|
||||||
private java.util.List<de.neitzel.fxdesigner.protocol.BindingMetadata.Problem> bindingProblems =
|
private java.util.List<de.neitzel.fxdesigner.protocol.BindingMetadata.Problem> bindingProblems =
|
||||||
@@ -200,6 +201,16 @@ public final class DesignerPane extends BorderPane implements AutoCloseable {
|
|||||||
if (target != null) kit.editor().getSelection().select(target);
|
if (target != null) kit.editor().getSelection().select(target);
|
||||||
},
|
},
|
||||||
values -> validateBindings(values, false));
|
values -> validateBindings(values, false));
|
||||||
|
repeatEditor =
|
||||||
|
new BindingEditor(
|
||||||
|
BindingDocument.Kind.REPEAT,
|
||||||
|
this::editBinding,
|
||||||
|
id -> {
|
||||||
|
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 =
|
bindingBrowser =
|
||||||
new BindingBrowser(
|
new BindingBrowser(
|
||||||
this::discoverBindings,
|
this::discoverBindings,
|
||||||
@@ -348,8 +359,11 @@ public final class DesignerPane extends BorderPane implements AutoCloseable {
|
|||||||
directBindingTab.setClosable(false);
|
directBindingTab.setClosable(false);
|
||||||
var computedTab = new Tab("Computed bindings", computedEditor);
|
var computedTab = new Tab("Computed bindings", computedEditor);
|
||||||
computedTab.setClosable(false);
|
computedTab.setClosable(false);
|
||||||
|
var repeatTab = new Tab("Repeat bindings", repeatEditor);
|
||||||
|
repeatTab.setClosable(false);
|
||||||
var bindings =
|
var bindings =
|
||||||
new TitledPane("Bindings", new TabPane(directBindingTab, computedTab, discoveryTab));
|
new TitledPane(
|
||||||
|
"Bindings", new TabPane(directBindingTab, computedTab, repeatTab, discoveryTab));
|
||||||
bindings.setExpanded(false);
|
bindings.setExpanded(false);
|
||||||
center.setBottom(new VBox(properties, bindings));
|
center.setBottom(new VBox(properties, bindings));
|
||||||
output.setEditable(false);
|
output.setEditable(false);
|
||||||
@@ -491,6 +505,7 @@ public final class DesignerPane extends BorderPane implements AutoCloseable {
|
|||||||
draft = value;
|
draft = value;
|
||||||
bindingEditor.load(value.text(), value.writable());
|
bindingEditor.load(value.text(), value.writable());
|
||||||
computedEditor.load(value.text(), value.writable());
|
computedEditor.load(value.text(), value.writable());
|
||||||
|
repeatEditor.load(value.text(), value.writable());
|
||||||
submittedSource = null;
|
submittedSource = null;
|
||||||
commitSource();
|
commitSource();
|
||||||
return;
|
return;
|
||||||
@@ -514,6 +529,7 @@ public final class DesignerPane extends BorderPane implements AutoCloseable {
|
|||||||
draft = value;
|
draft = value;
|
||||||
bindingEditor.load(value.text(), value.writable());
|
bindingEditor.load(value.text(), value.writable());
|
||||||
computedEditor.load(value.text(), value.writable());
|
computedEditor.load(value.text(), value.writable());
|
||||||
|
repeatEditor.load(value.text(), value.writable());
|
||||||
if (!source.getText().equals(value.text())) {
|
if (!source.getText().equals(value.text())) {
|
||||||
int anchor = source.getAnchor(), caret = source.getCaretPosition();
|
int anchor = source.getAnchor(), caret = source.getCaretPosition();
|
||||||
var edits =
|
var edits =
|
||||||
@@ -686,7 +702,11 @@ public final class DesignerPane extends BorderPane implements AutoCloseable {
|
|||||||
? "Remove " + intent.kind().element()
|
? "Remove " + intent.kind().element()
|
||||||
: (intent.index() == null ? "Create " : "Edit ") + intent.kind().element());
|
: (intent.index() == null ? "Create " : "Edit ") + intent.kind().element());
|
||||||
} catch (Exception error) {
|
} catch (Exception error) {
|
||||||
(intent.kind() == BindingDocument.Kind.COMPUTED ? computedEditor : bindingEditor)
|
(intent.kind() == BindingDocument.Kind.REPEAT
|
||||||
|
? repeatEditor
|
||||||
|
: intent.kind() == BindingDocument.Kind.COMPUTED
|
||||||
|
? computedEditor
|
||||||
|
: bindingEditor)
|
||||||
.error(error.getMessage());
|
.error(error.getMessage());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -761,6 +781,14 @@ public final class DesignerPane extends BorderPane implements AutoCloseable {
|
|||||||
: "Current successful build")
|
: "Current successful build")
|
||||||
+ " · draft revision "
|
+ " · draft revision "
|
||||||
+ captured.revision());
|
+ captured.revision());
|
||||||
|
repeatEditor.metadata(
|
||||||
|
metadata,
|
||||||
|
values,
|
||||||
|
(last
|
||||||
|
? "Explicit retained build (outdated)"
|
||||||
|
: "Current successful build")
|
||||||
|
+ " · draft revision "
|
||||||
|
+ captured.revision());
|
||||||
status.setText(
|
status.setText(
|
||||||
"Binding validation complete: "
|
"Binding validation complete: "
|
||||||
+ metadata.problems().size()
|
+ metadata.problems().size()
|
||||||
|
|||||||
@@ -237,4 +237,108 @@ class BindingDocumentTest {
|
|||||||
.entries
|
.entries
|
||||||
.isEmpty());
|
.isEmpty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private BindingDocument repeats(String text) throws Exception {
|
||||||
|
return new BindingDocument(text, BindingDocument.Kind.REPEAT);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void repeatCreatesContainersAndEscapesFields() throws Exception {
|
||||||
|
for (String root :
|
||||||
|
List.of(
|
||||||
|
"<VBox/>",
|
||||||
|
"<VBox xmlns:f='http://javafx.com/fxml/1'><f:define/></VBox>",
|
||||||
|
"<VBox xmlns:f='http://javafx.com/fxml/1'><f:define><"
|
||||||
|
+ P
|
||||||
|
+ "BindingList><elements/></"
|
||||||
|
+ P
|
||||||
|
+ "BindingList></f:define></VBox>")) {
|
||||||
|
String text = "<?neitzel-model app.Person?>\r\n" + root;
|
||||||
|
var value = BindingDocument.Values.repeat("pane", "app.Child", "a'&<");
|
||||||
|
String result = apply(text, repeats(text).write(null, value, false));
|
||||||
|
assertEquals(value, repeats(result).entries.getFirst().values());
|
||||||
|
assertTrue(result.startsWith("<?neitzel-model app.Person?>\r\n"));
|
||||||
|
assertFalse(result.contains(" control="));
|
||||||
|
assertTrue(repeats(result).errors().stream().anyMatch(e -> e.contains("Repeat collection")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String repeatSource() {
|
||||||
|
return existing()
|
||||||
|
.replace(
|
||||||
|
"<app.Unknown",
|
||||||
|
"<Repeat pane = 'rows' component='app.Child' collection = 'visits' custom='keep'><!--"
|
||||||
|
+ " inner --></Repeat><app.Unknown")
|
||||||
|
.replace("</VBox>", "<VBox fx:id='rows'/><VBox fx:id='other'/></VBox>");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void repeatChangesAllFieldsWithoutRewritingOtherBindings() throws Exception {
|
||||||
|
String text = repeatSource();
|
||||||
|
var doc = repeats(text);
|
||||||
|
int index = doc.entries.getFirst().index();
|
||||||
|
var value = BindingDocument.Values.repeat("other", "app.OtherChild", "home.visits");
|
||||||
|
String changed = apply(text, doc.write(index, value, false));
|
||||||
|
assertEquals(
|
||||||
|
text.replace("pane = 'rows'", "pane = 'other'")
|
||||||
|
.replace("component='app.Child'", "component='app.OtherChild'")
|
||||||
|
.replace("collection = 'visits'", "collection = 'home.visits'"),
|
||||||
|
changed);
|
||||||
|
assertTrue(repeats(changed).write(index, value, false).isEmpty());
|
||||||
|
String removed = apply(changed, repeats(changed).remove(index));
|
||||||
|
assertTrue(repeats(removed).entries.isEmpty());
|
||||||
|
assertEquals(1, new BindingDocument(removed).entries.size());
|
||||||
|
assertTrue(removed.contains("<!-- preserve -->"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void repeatConflictsAreScopedToThePaneAndReplacedAtomically() throws Exception {
|
||||||
|
String text =
|
||||||
|
repeatSource()
|
||||||
|
.replace(
|
||||||
|
"<app.Unknown",
|
||||||
|
"<Repeat pane='rows' component='app.Child' collection='visits'/><app.Unknown");
|
||||||
|
var doc = repeats(text);
|
||||||
|
var value = BindingDocument.Values.repeat("rows", "app.NewChild", "visits");
|
||||||
|
assertTrue(doc.errors().stream().anyMatch(e -> e.contains("Duplicate Repeat pane")));
|
||||||
|
assertThrows(BindingDocument.Conflict.class, () -> doc.write(null, value, false));
|
||||||
|
String result = apply(text, doc.write(null, value, true));
|
||||||
|
assertEquals(1, repeats(result).entries.size());
|
||||||
|
assertEquals(1, new BindingDocument(result).entries.size());
|
||||||
|
assertTrue(repeats(result).errors().isEmpty());
|
||||||
|
// A DirectBinding to a property of the same pane remains independent.
|
||||||
|
var direct = new BindingDocument.Values("rows", "visible", "name", "view", "");
|
||||||
|
String withDirect = apply(result, new BindingDocument(result).write(null, direct, false));
|
||||||
|
assertEquals(1, repeats(withDirect).entries.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void repeatNestedSyntaxAndImportIdentityArePreserved() throws Exception {
|
||||||
|
String text =
|
||||||
|
repeatSource()
|
||||||
|
.replace(
|
||||||
|
"collection = 'visits' custom='keep'><!-- inner -->",
|
||||||
|
"custom='keep'><collection>visits</collection>");
|
||||||
|
var doc = repeats(text);
|
||||||
|
var entry = doc.entries.getFirst();
|
||||||
|
assertFalse(entry.editable());
|
||||||
|
assertThrows(
|
||||||
|
IllegalArgumentException.class,
|
||||||
|
() ->
|
||||||
|
doc.write(
|
||||||
|
entry.index(),
|
||||||
|
BindingDocument.Values.repeat("rows", "app.Child", "visits"),
|
||||||
|
false));
|
||||||
|
assertTrue(repeats(apply(text, doc.remove(entry.index()))).entries.isEmpty());
|
||||||
|
assertTrue(
|
||||||
|
repeats(text.replace("<?import de.neitzel.fx.view.*?>", "<?import app.Repeat?>"))
|
||||||
|
.entries
|
||||||
|
.isEmpty());
|
||||||
|
assertTrue(
|
||||||
|
repeats(repeatSource().replace("pane = 'rows'", "pane = 'missing'")).errors().stream()
|
||||||
|
.anyMatch(e -> e.contains("Missing or ambiguous Repeat pane")));
|
||||||
|
assertTrue(
|
||||||
|
repeats(repeatSource().replace("component='app.Child'", "component='' ")).errors().stream()
|
||||||
|
.anyMatch(e -> e.contains("Repeat Component")));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -644,6 +644,164 @@ public final class ProjectionHostChecks {
|
|||||||
await(() -> fx(() -> source(pane).getText().equals(full)));
|
await(() -> fx(() -> source(pane).getText().equals(full)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void repeatField(DesignerPane pane, String name, String value) throws Exception {
|
||||||
|
fx(
|
||||||
|
() -> {
|
||||||
|
((TextField) pane.lookup("#repeat-" + name)).setText(value);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void selectRepeat(DesignerPane pane, String name) throws Exception {
|
||||||
|
fx(
|
||||||
|
() -> {
|
||||||
|
var list = (ListView<Object>) pane.lookup("#repeat-bindings");
|
||||||
|
list.getSelectionModel()
|
||||||
|
.select(
|
||||||
|
list.getItems().stream()
|
||||||
|
.filter(e -> e.toString().startsWith(name + " ←"))
|
||||||
|
.findFirst()
|
||||||
|
.orElseThrow());
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void repeatHistory(
|
||||||
|
DesignerPane pane, DocumentSession document, String before, String after) throws Exception {
|
||||||
|
click(pane, "Undo");
|
||||||
|
await(() -> documentText(document).equals(before));
|
||||||
|
click(pane, "Redo");
|
||||||
|
await(() -> documentText(document).equals(after));
|
||||||
|
await(() -> fx(() -> source(pane).getText().equals(after)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void repeatBindingEdits(DesignerPane pane, DocumentSession document, String full)
|
||||||
|
throws Exception {
|
||||||
|
String base =
|
||||||
|
full.replace(
|
||||||
|
"<VBox fx:id=\"visits\"/>",
|
||||||
|
"<VBox fx:id=\"visits\"/><VBox fx:id=\"otherVisits\"/><!-- Repeat edit sentinel -->");
|
||||||
|
check(!base.equals(full), "Repeat fixture pane missing");
|
||||||
|
set(document, base);
|
||||||
|
await(() -> fx(() -> source(pane).getText().equals(base)));
|
||||||
|
bindingTab(pane, "Repeat bindings");
|
||||||
|
selectRepeat(pane, "visits");
|
||||||
|
click(pane, "Remove Repeat binding");
|
||||||
|
String removed =
|
||||||
|
base.replace(
|
||||||
|
"<Repeat pane=\"visits\" component=\"fixture.components.AddressForm\""
|
||||||
|
+ " collection=\"visits\"/>",
|
||||||
|
"");
|
||||||
|
await(() -> documentText(document).equals(removed));
|
||||||
|
repeatHistory(pane, document, base, removed);
|
||||||
|
click(pane, "New Repeat binding");
|
||||||
|
repeatField(pane, "pane", "visits");
|
||||||
|
repeatField(pane, "component", "fixture.components.AddressForm");
|
||||||
|
repeatField(pane, "collection", "visits");
|
||||||
|
check(documentText(document).equals(removed), "Repeat form changed source before Apply");
|
||||||
|
click(pane, "Apply Repeat binding");
|
||||||
|
await(() -> !documentText(document).equals(removed));
|
||||||
|
String created = documentText(document);
|
||||||
|
check(
|
||||||
|
created.contains(
|
||||||
|
"de.neitzel.fx.view.Repeat pane=\"visits\" component=\"fixture.components.AddressForm\""
|
||||||
|
+ " collection=\"visits\""),
|
||||||
|
"Repeat creation failed");
|
||||||
|
repeatHistory(pane, document, removed, created);
|
||||||
|
selectRepeat(pane, "visits");
|
||||||
|
repeatField(pane, "pane", "otherVisits");
|
||||||
|
repeatField(pane, "component", "fixture.components.VisitRow");
|
||||||
|
click(pane, "Apply Repeat binding");
|
||||||
|
String changed =
|
||||||
|
created.replace(
|
||||||
|
"Repeat pane=\"visits\" component=\"fixture.components.AddressForm\"",
|
||||||
|
"Repeat pane=\"otherVisits\" component=\"fixture.components.VisitRow\"");
|
||||||
|
await(() -> documentText(document).equals(changed));
|
||||||
|
repeatHistory(pane, document, created, changed);
|
||||||
|
document
|
||||||
|
.save(document.snapshot().toCompletableFuture().get().revision())
|
||||||
|
.toCompletableFuture()
|
||||||
|
.get();
|
||||||
|
check(
|
||||||
|
java.nio.file.Files.readString(
|
||||||
|
java.nio.file.Path.of(document.snapshot().toCompletableFuture().get().document()))
|
||||||
|
.equals(changed),
|
||||||
|
"Repeat save changed source");
|
||||||
|
click(pane, "Validate Repeat bindings");
|
||||||
|
await(
|
||||||
|
() ->
|
||||||
|
fx(
|
||||||
|
() ->
|
||||||
|
((Label) pane.lookup("#repeat-status"))
|
||||||
|
.getText()
|
||||||
|
.contains("Collection and Component compatibility")));
|
||||||
|
click(pane, "Render drafts");
|
||||||
|
await(
|
||||||
|
() ->
|
||||||
|
fx(
|
||||||
|
() ->
|
||||||
|
object(pane, "home").getSceneGraphObject() instanceof SnapshotRegion r
|
||||||
|
&& r.image() != null
|
||||||
|
&& r.statusText().equals("Target runtime")));
|
||||||
|
click(pane, "New Repeat binding");
|
||||||
|
repeatField(pane, "pane", "otherVisits");
|
||||||
|
repeatField(pane, "component", "fixture.components.AddressForm");
|
||||||
|
repeatField(pane, "collection", "visits");
|
||||||
|
click(pane, "Apply Repeat binding");
|
||||||
|
await(
|
||||||
|
() ->
|
||||||
|
fx(
|
||||||
|
() ->
|
||||||
|
((Label) pane.lookup("#repeat-status"))
|
||||||
|
.getText()
|
||||||
|
.contains("Replace conflicting bindings")));
|
||||||
|
check(documentText(document).equals(changed), "Repeat conflict silently replaced");
|
||||||
|
fx(
|
||||||
|
() -> {
|
||||||
|
((CheckBox) pane.lookup("#repeat-replace")).setSelected(true);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
click(pane, "Apply Repeat binding");
|
||||||
|
await(() -> !documentText(document).equals(changed));
|
||||||
|
String replaced = documentText(document);
|
||||||
|
check(
|
||||||
|
!replaced.contains("component=\"fixture.components.VisitRow\""),
|
||||||
|
"Repeat replacement retained old declaration");
|
||||||
|
repeatHistory(pane, document, changed, replaced);
|
||||||
|
selectRepeat(pane, "otherVisits");
|
||||||
|
repeatField(pane, "collection", "invalid path");
|
||||||
|
click(pane, "Apply Repeat binding");
|
||||||
|
String invalid = replaced.replace("collection=\"visits\"/>", "collection=\"invalid path\"/>");
|
||||||
|
await(() -> documentText(document).equals(invalid));
|
||||||
|
document
|
||||||
|
.save(document.snapshot().toCompletableFuture().get().revision())
|
||||||
|
.toCompletableFuture()
|
||||||
|
.get();
|
||||||
|
check(
|
||||||
|
java.nio.file.Files.readString(
|
||||||
|
java.nio.file.Path.of(document.snapshot().toCompletableFuture().get().document()))
|
||||||
|
.equals(invalid),
|
||||||
|
"Invalid Repeat was not saved");
|
||||||
|
click(pane, "Render drafts");
|
||||||
|
await(
|
||||||
|
() ->
|
||||||
|
fx(
|
||||||
|
() ->
|
||||||
|
pane.lookupAll(".label").stream()
|
||||||
|
.anyMatch(
|
||||||
|
n ->
|
||||||
|
n instanceof Label l
|
||||||
|
&& l.getText() != null
|
||||||
|
&& l.getText().startsWith("Preview blocked:")
|
||||||
|
&& l.getText().contains("Repeat collection"))));
|
||||||
|
repeatHistory(pane, document, replaced, invalid);
|
||||||
|
set(document, full);
|
||||||
|
await(() -> fx(() -> source(pane).getText().equals(full)));
|
||||||
|
System.out.println(
|
||||||
|
"PASS Repeat inspector: create, change, remove, conflict, exact undo/redo, save, target"
|
||||||
|
+ " load and invalid draft");
|
||||||
|
}
|
||||||
|
|
||||||
private static void viewModelBindings(
|
private static void viewModelBindings(
|
||||||
DesignerPane pane, DocumentSession document, String original) throws Exception {
|
DesignerPane pane, DocumentSession document, String original) throws Exception {
|
||||||
bindingTab(pane, "Discover targets");
|
bindingTab(pane, "Discover targets");
|
||||||
@@ -1022,6 +1180,7 @@ public final class ProjectionHostChecks {
|
|||||||
converterEdits(pane, document, full);
|
converterEdits(pane, document, full);
|
||||||
directBindingEdits(pane, document, full);
|
directBindingEdits(pane, document, full);
|
||||||
computedBindingEdits(pane, document, full);
|
computedBindingEdits(pane, document, full);
|
||||||
|
repeatBindingEdits(pane, document, full);
|
||||||
document
|
document
|
||||||
.save(document.snapshot().toCompletableFuture().get().revision())
|
.save(document.snapshot().toCompletableFuture().get().revision())
|
||||||
.toCompletableFuture()
|
.toCompletableFuture()
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ A target already used by a Direct or Computed declaration requires **Replace con
|
|||||||
|
|
||||||
Structural diagnostics include missing/ambiguous targets, missing properties and malformed method names. [Target-side validation](viewmodel-computed-validation.md) checks Controller method access, arity, return types and target compatibility without executing application methods. Errors block new previews while invalid drafts remain saveable and undoable. Build/draft freshness and explicit retained-build provenance continue to apply.
|
Structural diagnostics include missing/ambiguous targets, missing properties and malformed method names. [Target-side validation](viewmodel-computed-validation.md) checks Controller method access, arity, return types and target compatibility without executing application methods. Errors block new previews while invalid drafts remain saveable and undoable. Build/draft freshness and explicit retained-build provenance continue to apply.
|
||||||
|
|
||||||
The inspector does not generate Controller methods or evaluate their bodies. Generated metadata, full collection authoring, structural reference refactoring and complete IDE Problems integration remain later work. Existing skipped human checks remain skipped.
|
The inspector does not generate Controller methods or evaluate their bodies. The [Repeat inspector](repeat-binding-inspector.md) now supports collection declarations. Generated metadata, remaining collection authoring/compatibility, structural reference refactoring and complete IDE Problems integration remain later work. Existing skipped human checks remain skipped.
|
||||||
|
|
||||||
## Automated qualification
|
## Automated qualification
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ A target occupied by another DirectBinding or ComputedBinding requires **Replace
|
|||||||
|
|
||||||
Structural diagnostics cover duplicate targets, missing/ambiguous fx:id, empty property, malformed/missing Model path and invalid direction. [Converter discovery and binding validation](converter-validation.md) now add target-side path/property/type and capability checks, a shared chooser, and automatic validation before new previews. Saving and source repair remain available on errors.
|
Structural diagnostics cover duplicate targets, missing/ambiguous fx:id, empty property, malformed/missing Model path and invalid direction. [Converter discovery and binding validation](converter-validation.md) now add target-side path/property/type and capability checks, a shared chooser, and automatic validation before new previews. Saving and source repair remain available on errors.
|
||||||
|
|
||||||
[Custom ViewModel discovery and Computed validation](viewmodel-computed-validation.md) are implemented. Generated metadata, Repeat/Table authoring, complete semantic diagnostics and identifier rename/copy/subtree-delete reference repair remain later stage-4 work.
|
[Custom ViewModel discovery and Computed validation](viewmodel-computed-validation.md) are implemented. [Repeat authoring](repeat-binding-inspector.md) is implemented. Generated metadata, Table authoring, complete semantic diagnostics and identifier rename/copy/subtree-delete reference repair remain later stage-4 work.
|
||||||
|
|
||||||
## Automated qualification
|
## Automated qualification
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# Shared Repeat-binding inspector
|
||||||
|
|
||||||
|
Both hosts provide **Bindings → Repeat bindings**. Select a declaration or choose **New Repeat binding**, enter **Pane (fx:id)**, **Component class** and **Collection path**, then **Apply Repeat binding**. **Remove Repeat binding** deletes the selected declaration. Selecting an entry selects its pane on the canvas; canvas selection also locates the corresponding declaration.
|
||||||
|
|
||||||
|
The fields map directly to NeitzelLib `Repeat.pane`, `Repeat.component` and `Repeat.collection`. The runtime creates one fresh Component per collection item and assigns the item as its Model. Use a dedicated pane for these children. This inspector does not change NeitzelLib's list-change behavior or generate Component classes.
|
||||||
|
|
||||||
|
## Source and history
|
||||||
|
|
||||||
|
The inspector shares the lexical document editor and form lifecycle with Direct and Computed bindings. Creation adds a fully qualified Repeat declaration, plus required BindingList/FXML containers when absent. Editing changes only the three requested attributes. Existing quoting, imports, Model declarations, comments, unknown attributes and unrelated bindings remain intact. Alternate FXML prefixes, self-closing containers and explicit BindingList `elements` wrappers are supported.
|
||||||
|
|
||||||
|
Each create, change, remove or explicit replacement is one revision-checked host document command. Normal Undo/Redo restores the exact previous/new text. The active form is invalidated when source changes, and read-only documents cannot be edited. Nested-property declarations remain visible and removable but require Source for changes; unfamiliar syntax is not flattened.
|
||||||
|
|
||||||
|
A second Repeat targeting the same pane requires **Replace conflicting bindings**. Replacement removes the conflicting Repeat declarations atomically with the requested edit. A Direct/Computed binding on a property of that pane remains independent and is preserved.
|
||||||
|
|
||||||
|
## Validation boundary
|
||||||
|
|
||||||
|
Structural diagnostics cover missing/ambiguous pane identifiers, duplicate Repeat panes, and missing/malformed Component class or collection names. Known structural errors block new previews; invalid drafts remain saveable and undoable. **Validate Repeat bindings** reports these checks and existing target-side binding diagnostics for the current source. Validation does not apply pending form values.
|
||||||
|
|
||||||
|
This slice accepts typed field values. A collection/Component chooser and complete preflight compatibility discovery are later work. The pane's actual Java type, collection existence and ObservableList value, element/Component Model compatibility, Component loading and injection still require target-runtime loading. The inspector explicitly distinguishes structural validation from those remaining runtime checks. Generated metadata, Table/Column authoring, structural reference refactoring and complete IDE Problems integration also remain open.
|
||||||
|
|
||||||
|
## Automated qualification
|
||||||
|
|
||||||
|
Lexical tests cover container creation, escaping, all three field changes, exact source preservation, paired-element removal, duplicate-pane replacement, independence from Direct bindings, nested syntax and import identity. The same live shared-UI scenario runs in native standalone and registered IntelliJ against classpath and module-path fixtures. It removes an existing declaration, creates one, changes its pane and Component, saves and renders it, replaces a conflict explicitly, saves a malformed collection path while preview is blocked, and checks exact single-command Undo/Redo throughout. Existing Direct/Computed and lifecycle checks remain enabled.
|
||||||
|
|
||||||
|
Run `scripts/check-bootstrap.py` and `scripts/check-ide-host.py` with the pinned SDKs described in the [README](../README.md). No manual testing is required; previously skipped human checks remain skipped.
|
||||||
|
|
||||||
|
Final qualification on 2026-11-13 passed on the pinned macOS baseline: **99 unit/adapter tests** (92 Maven, 7 IntelliJ), **36 target processes / 484 fixture assertions**, **48 worker lifecycle cycles**, both native standalone modes and both registered IntelliJ modes. The registered IDE checks also verified exclusive JavaFX 26 natives and worker shutdown. No manual testing was used.
|
||||||
|
|
||||||
|
The initial full run timed out once in the existing module-path `viewmodels` fixture at its 30-second process limit, before the new inspector checks. Three fresh-process retries passed unchanged, followed by a successful full bootstrap rerun. The timeout's cause was not established; no timeout was relaxed and no runtime change was made to hide it.
|
||||||
|
|
||||||
|
Local evidence: `/tmp/repeat-bootstrap.log`, `/tmp/repeat-registered-ide.log`, `/tmp/repeat-bootstrap-initial-timeout.log`, `/tmp/repeat-fixture-retry-{0,1,2}.log`, `target/connected-workflow/` and `target/ide-workflow/{classpath,modulepath}.json`. Final live logs contain the Repeat inspector pass markers and no uncaught application exceptions; native-crash and shutdown checks passed. Malformed-XML diagnostics are intentional recovery-test inputs. These results qualify the pinned baseline, not additional platforms or IDE versions.
|
||||||
@@ -117,3 +117,7 @@ The [shared signature-based slice](viewmodel-computed-validation.md) implements
|
|||||||
## Shared Computed-binding inspector
|
## Shared Computed-binding inspector
|
||||||
|
|
||||||
The [Computed authoring slice](computed-binding-inspector.md) adds create/change/remove and explicit conflict replacement in both hosts through the same source-preserving editor as Direct bindings. Target method choices prepare the form without changing source; normal host Undo/Redo owns each operation. Full collection authoring, generated metadata, reference refactoring and Problems integration remain open.
|
The [Computed authoring slice](computed-binding-inspector.md) adds create/change/remove and explicit conflict replacement in both hosts through the same source-preserving editor as Direct bindings. Target method choices prepare the form without changing source; normal host Undo/Redo owns each operation. Full collection authoring, generated metadata, reference refactoring and Problems integration remain open.
|
||||||
|
|
||||||
|
## Shared Repeat-binding inspector
|
||||||
|
|
||||||
|
The [Repeat authoring slice](repeat-binding-inspector.md) adds create/change/remove, explicit duplicate-pane replacement, structural diagnostics and normal host Undo/Redo in both hosts. Collection/Component compatibility discovery and Table/Column authoring remain subsequent work, alongside generated metadata, reference refactoring and Problems integration.
|
||||||
|
|||||||
Reference in New Issue
Block a user