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:
@@ -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. Full binding/generation editing, 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. Binding edits, 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.
|
||||||
|
|
||||||
@@ -52,7 +52,7 @@ These graphical checks require a macOS desktop session; other platforms are not
|
|||||||
## Implementation references
|
## Implementation references
|
||||||
|
|
||||||
- [Initial project contract](docs/initial-project-contract.md): module responsibilities, configuration, snapshots, runtimes, preview sessions and communication.
|
- [Initial project contract](docs/initial-project-contract.md): module responsibilities, configuration, snapshots, runtimes, preview sessions and communication.
|
||||||
- [Repository bootstrap plan](docs/repository-bootstrap-plan.md): ordered work packages and acceptance gates. Bootstrap work packages 1–7 are implemented; source-preserving projection is implemented and its [manual drag/resize gate](docs/manual-input-validation.md) passed in both hosts on the recorded baseline. Stage 3 now implements [target-runtime sessions, sizing and navigation](docs/target-runtime-sessions.md), with its new human navigation/interactive review still open.
|
- [Repository bootstrap plan](docs/repository-bootstrap-plan.md): ordered work packages and acceptance gates. Bootstrap work packages 1–7 are implemented; source-preserving projection is implemented and its [manual drag/resize gate](docs/manual-input-validation.md) passed in both hosts on the recorded baseline. Stage 3 now implements [target-runtime sessions, sizing and navigation](docs/target-runtime-sessions.md), with [manual passes and explicitly skipped remaining checks](docs/manual-runtime-navigation.md) recorded.
|
||||||
- [Decision provenance](docs/provenance.md): workspace planning and captured prototype evidence.
|
- [Decision provenance](docs/provenance.md): workspace planning and captured prototype evidence.
|
||||||
- [Third-party notices](THIRD-PARTY-NOTICES.md): Maven wrapper and patched Kit source provenance.
|
- [Third-party notices](THIRD-PARTY-NOTICES.md): Maven wrapper and patched Kit source provenance.
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
package de.neitzel.fxdesigner.protocol;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
/** Non-constructing target metadata; compatibility is computed by the target type system. */
|
||||||
|
public record BindingMetadata(
|
||||||
|
String model,
|
||||||
|
String path,
|
||||||
|
String direction,
|
||||||
|
List<Attribute> attributes,
|
||||||
|
List<Target> targets,
|
||||||
|
List<String> diagnostics) {
|
||||||
|
public record Attribute(String path, String type, String kind, String elementType) {
|
||||||
|
public String toString() {
|
||||||
|
return path + " : " + type + " (" + kind + ")";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public record Target(
|
||||||
|
String control, String property, String type, String status, String reason) {}
|
||||||
|
|
||||||
|
public BindingMetadata {
|
||||||
|
attributes = List.copyOf(attributes);
|
||||||
|
targets = List.copyOf(targets);
|
||||||
|
diagnostics = List.copyOf(diagnostics);
|
||||||
|
if (!Set.of("view", "model", "both").contains(direction))
|
||||||
|
throw new IllegalArgumentException("Invalid direction");
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, String> encode() {
|
||||||
|
var m = new TreeMap<String, String>();
|
||||||
|
m.put("format", "1");
|
||||||
|
m.put("model", model);
|
||||||
|
m.put("path", path);
|
||||||
|
m.put("direction", direction);
|
||||||
|
m.put("attributes", "" + attributes.size());
|
||||||
|
m.put("targets", "" + targets.size());
|
||||||
|
m.put("diagnostics", "" + diagnostics.size());
|
||||||
|
for (int i = 0; i < attributes.size(); i++) {
|
||||||
|
var a = attributes.get(i);
|
||||||
|
String p = "a." + i + ".";
|
||||||
|
m.put(p + "path", a.path);
|
||||||
|
m.put(p + "type", a.type);
|
||||||
|
m.put(p + "kind", a.kind);
|
||||||
|
m.put(p + "element", a.elementType);
|
||||||
|
}
|
||||||
|
for (int i = 0; i < targets.size(); i++) {
|
||||||
|
var t = targets.get(i);
|
||||||
|
String p = "t." + i + ".";
|
||||||
|
m.put(p + "control", t.control);
|
||||||
|
m.put(p + "property", t.property);
|
||||||
|
m.put(p + "type", t.type);
|
||||||
|
m.put(p + "status", t.status);
|
||||||
|
m.put(p + "reason", t.reason);
|
||||||
|
}
|
||||||
|
for (int i = 0; i < diagnostics.size(); i++) m.put("d." + i, diagnostics.get(i));
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String field(Map<String, String> m, String key) {
|
||||||
|
return Objects.requireNonNull(m.get(key), "Missing " + key);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int count(Map<String, String> m, String key) {
|
||||||
|
int n = Integer.parseInt(field(m, key));
|
||||||
|
if (n < 0 || n > 2048) throw new IllegalArgumentException("Invalid metadata count");
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static BindingMetadata decode(Map<String, String> m) {
|
||||||
|
if (!"1".equals(field(m, "format")))
|
||||||
|
throw new IllegalArgumentException("Unsupported binding metadata");
|
||||||
|
var a = new ArrayList<Attribute>();
|
||||||
|
var t = new ArrayList<Target>();
|
||||||
|
var d = new ArrayList<String>();
|
||||||
|
for (int i = 0; i < count(m, "attributes"); i++) {
|
||||||
|
String p = "a." + i + ".";
|
||||||
|
a.add(
|
||||||
|
new Attribute(
|
||||||
|
field(m, p + "path"),
|
||||||
|
field(m, p + "type"),
|
||||||
|
field(m, p + "kind"),
|
||||||
|
field(m, p + "element")));
|
||||||
|
}
|
||||||
|
for (int i = 0; i < count(m, "targets"); i++) {
|
||||||
|
String p = "t." + i + ".";
|
||||||
|
t.add(
|
||||||
|
new Target(
|
||||||
|
field(m, p + "control"),
|
||||||
|
field(m, p + "property"),
|
||||||
|
field(m, p + "type"),
|
||||||
|
field(m, p + "status"),
|
||||||
|
field(m, p + "reason")));
|
||||||
|
}
|
||||||
|
for (int i = 0; i < count(m, "diagnostics"); i++) d.add(field(m, "d." + i));
|
||||||
|
return new BindingMetadata(field(m, "model"), field(m, "path"), field(m, "direction"), a, t, d);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -81,6 +81,9 @@ public final class ProtocolSession implements AutoCloseable {
|
|||||||
if (hello.preview().major() == 1
|
if (hello.preview().major() == 1
|
||||||
&& supported.contains("interactive-v1")
|
&& supported.contains("interactive-v1")
|
||||||
&& hello.capabilities().contains("interactive-v1")) usable.add(Operation.INTERACTIVE);
|
&& hello.capabilities().contains("interactive-v1")) usable.add(Operation.INTERACTIVE);
|
||||||
|
if (hello.metadata().major() == 1
|
||||||
|
&& supported.contains("bindings-v1")
|
||||||
|
&& hello.capabilities().contains("bindings-v1")) usable.add(Operation.BINDINGS);
|
||||||
var requiredByEither = new HashSet<>(required);
|
var requiredByEither = new HashSet<>(required);
|
||||||
requiredByEither.addAll(hello.required());
|
requiredByEither.addAll(hello.required());
|
||||||
if ((requiredByEither.contains("discover-v1") && !usable.contains(Operation.DISCOVER))
|
if ((requiredByEither.contains("discover-v1") && !usable.contains(Operation.DISCOVER))
|
||||||
@@ -89,6 +92,8 @@ public final class ProtocolSession implements AutoCloseable {
|
|||||||
if ((requiredByEither.contains("layout-v1") && !usable.contains(Operation.MEASURE))
|
if ((requiredByEither.contains("layout-v1") && !usable.contains(Operation.MEASURE))
|
||||||
|| (requiredByEither.contains("interactive-v1") && !usable.contains(Operation.INTERACTIVE)))
|
|| (requiredByEither.contains("interactive-v1") && !usable.contains(Operation.INTERACTIVE)))
|
||||||
throw new IllegalArgumentException("Required capability version incompatible");
|
throw new IllegalArgumentException("Required capability version incompatible");
|
||||||
|
if (requiredByEither.contains("bindings-v1") && !usable.contains(Operation.BINDINGS))
|
||||||
|
throw new IllegalArgumentException("Required capability version incompatible");
|
||||||
operations = Set.copyOf(usable);
|
operations = Set.copyOf(usable);
|
||||||
state = State.READY;
|
state = State.READY;
|
||||||
return operations;
|
return operations;
|
||||||
|
|||||||
@@ -76,7 +76,8 @@ public final class WireProtocol {
|
|||||||
RENDER,
|
RENDER,
|
||||||
MEASURE,
|
MEASURE,
|
||||||
OCCURRENCES,
|
OCCURRENCES,
|
||||||
INTERACTIVE
|
INTERACTIVE,
|
||||||
|
BINDINGS
|
||||||
}
|
}
|
||||||
|
|
||||||
public record Identity(
|
public record Identity(
|
||||||
@@ -114,11 +115,15 @@ public final class WireProtocol {
|
|||||||
Objects.requireNonNull(operation);
|
Objects.requireNonNull(operation);
|
||||||
Objects.requireNonNull(identity);
|
Objects.requireNonNull(identity);
|
||||||
Objects.requireNonNull(payload);
|
Objects.requireNonNull(payload);
|
||||||
boolean render = operation != Operation.DISCOVER;
|
boolean render = operation != Operation.DISCOVER && operation != Operation.BINDINGS;
|
||||||
if (render
|
if (render
|
||||||
&& (identity.width() == 0 || identity.height() == 0 || identity.revisions().isEmpty()))
|
&& (identity.width() == 0 || identity.height() == 0 || identity.revisions().isEmpty()))
|
||||||
throw new IllegalArgumentException("Rendering needs dimensions and document revisions");
|
throw new IllegalArgumentException("Rendering needs dimensions and document revisions");
|
||||||
if (!render
|
if (operation == Operation.BINDINGS
|
||||||
|
&& (identity.width() != 0 || identity.height() != 0 || identity.revisions().isEmpty()))
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"Binding discovery needs document revisions and no dimensions");
|
||||||
|
if (operation == Operation.DISCOVER
|
||||||
&& (identity.width() != 0 || identity.height() != 0 || !identity.revisions().isEmpty()))
|
&& (identity.width() != 0 || identity.height() != 0 || !identity.revisions().isEmpty()))
|
||||||
throw new IllegalArgumentException("Discovery does not carry render identity");
|
throw new IllegalArgumentException("Discovery does not carry render identity");
|
||||||
boolean image = operation == Operation.RENDER && kind == Kind.RESULT;
|
boolean image = operation == Operation.RENDER && kind == Kind.RESULT;
|
||||||
|
|||||||
+76
@@ -0,0 +1,76 @@
|
|||||||
|
package de.neitzel.fxdesigner.protocol;
|
||||||
|
|
||||||
|
import static de.neitzel.fxdesigner.protocol.WireProtocol.*;
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
import java.net.*;
|
||||||
|
import java.util.*;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
class BindingMetadataTest {
|
||||||
|
@Test
|
||||||
|
void metadataRoundTripAndIdentityAreBounded() throws Exception {
|
||||||
|
var m =
|
||||||
|
new BindingMetadata(
|
||||||
|
"Person",
|
||||||
|
"name",
|
||||||
|
"both",
|
||||||
|
List.of(new BindingMetadata.Attribute("name", "java.lang.String", "VALUE", "")),
|
||||||
|
List.of(
|
||||||
|
new BindingMetadata.Target(
|
||||||
|
"input", "text", "java.lang.String", "COMPATIBLE", "Types match")),
|
||||||
|
List.of());
|
||||||
|
assertEquals(
|
||||||
|
m, BindingMetadata.decode(PreviewMessages.decode(PreviewMessages.encode(m.encode()))));
|
||||||
|
var id = new Identity("s", "r", "c", "b", Map.of("file", 1L), 0, 0);
|
||||||
|
assertDoesNotThrow(
|
||||||
|
() -> new Packet(Kind.RESULT, Operation.BINDINGS, id, PreviewMessages.encode(m.encode())));
|
||||||
|
assertThrows(
|
||||||
|
IllegalArgumentException.class,
|
||||||
|
() ->
|
||||||
|
new Packet(
|
||||||
|
Kind.REQUEST,
|
||||||
|
Operation.BINDINGS,
|
||||||
|
new Identity("s", "r", "c", "b", Map.of(), 0, 0),
|
||||||
|
new byte[0]));
|
||||||
|
var invalid = new HashMap<>(m.encode());
|
||||||
|
invalid.put("targets", "99999");
|
||||||
|
assertThrows(IllegalArgumentException.class, () -> BindingMetadata.decode(invalid));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void negotiationRequiresMetadataVersion() throws Exception {
|
||||||
|
String token = ProtocolSession.newToken();
|
||||||
|
var session = new ProtocolSession("s", 1, token, Set.of("bindings-v1"), Set.of("bindings-v1"));
|
||||||
|
assertTrue(
|
||||||
|
session
|
||||||
|
.authenticate(
|
||||||
|
InetAddress.getLoopbackAddress(),
|
||||||
|
new Hello(
|
||||||
|
"s",
|
||||||
|
1,
|
||||||
|
token,
|
||||||
|
new Version(1, 0),
|
||||||
|
new Version(1, 0),
|
||||||
|
new Version(1, 0),
|
||||||
|
Set.of("bindings-v1"),
|
||||||
|
Set.of()))
|
||||||
|
.contains(Operation.BINDINGS));
|
||||||
|
var incompatible =
|
||||||
|
new ProtocolSession("s", 1, token, Set.of("bindings-v1"), Set.of("bindings-v1"));
|
||||||
|
assertThrows(
|
||||||
|
IllegalArgumentException.class,
|
||||||
|
() ->
|
||||||
|
incompatible.authenticate(
|
||||||
|
InetAddress.getLoopbackAddress(),
|
||||||
|
new Hello(
|
||||||
|
"s",
|
||||||
|
1,
|
||||||
|
token,
|
||||||
|
new Version(1, 0),
|
||||||
|
new Version(2, 0),
|
||||||
|
new Version(1, 0),
|
||||||
|
Set.of("bindings-v1"),
|
||||||
|
Set.of())));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -198,6 +198,30 @@ public final class DesignerWorkflow implements AutoCloseable {
|
|||||||
return build.start(request, approved, line -> log.accept(line.line()));
|
return build.start(request, approved, line -> log.accept(line.line()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public CompletionStage<BindingMetadata> bindings(
|
||||||
|
boolean last, DocumentSnapshot document, String path, String direction) {
|
||||||
|
return execute(
|
||||||
|
last,
|
||||||
|
List.of(document),
|
||||||
|
worker -> {
|
||||||
|
var result =
|
||||||
|
worker.request(
|
||||||
|
Operation.BINDINGS,
|
||||||
|
Map.of(document.document().toString(), document.revision()),
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
PreviewMessages.encode(
|
||||||
|
Map.of("fxml", document.text(), "path", path, "direction", direction)),
|
||||||
|
Duration.ofSeconds(15));
|
||||||
|
try (var current =
|
||||||
|
host.documents().open(document.document()).toCompletableFuture().get()) {
|
||||||
|
if (current.snapshot().toCompletableFuture().get().revision() != document.revision())
|
||||||
|
throw new CancellationException("Draft changed during discovery");
|
||||||
|
}
|
||||||
|
return BindingMetadata.decode(PreviewMessages.decode(result.payload()));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public CompletionStage<List<Example>> discover(boolean lastSuccessful) {
|
public CompletionStage<List<Example>> discover(boolean lastSuccessful) {
|
||||||
return discover(lastSuccessful, false, List.of());
|
return discover(lastSuccessful, false, List.of());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ public final class PreviewWorker implements AutoCloseable {
|
|||||||
id,
|
id,
|
||||||
process.pid(),
|
process.pid(),
|
||||||
token,
|
token,
|
||||||
Set.of("discover-v1", "render-rgba-v1", "layout-v1", "interactive-v1"),
|
Set.of("discover-v1", "render-rgba-v1", "layout-v1", "interactive-v1", "bindings-v1"),
|
||||||
config.requiredCapabilities());
|
config.requiredCapabilities());
|
||||||
}
|
}
|
||||||
drain(process.getInputStream(), log);
|
drain(process.getInputStream(), log);
|
||||||
|
|||||||
+22
@@ -42,6 +42,28 @@ public final class ConnectedWorkflowContract {
|
|||||||
try (var doc = await(host.documents().open(file.toUri()));
|
try (var doc = await(host.documents().open(file.toUri()));
|
||||||
var component = await(host.documents().open(child.toUri()))) {
|
var component = await(host.documents().open(child.toUri()))) {
|
||||||
var initial = await(doc.snapshot());
|
var initial = await(doc.snapshot());
|
||||||
|
var bindings = await(workflow.bindings(false, initial, "name", "both"));
|
||||||
|
check(
|
||||||
|
bindings.attributes().stream().anyMatch(a -> a.path().equals("home.street")),
|
||||||
|
"Nested Model path discovery");
|
||||||
|
check(
|
||||||
|
bindings.targets().stream()
|
||||||
|
.anyMatch(
|
||||||
|
t ->
|
||||||
|
t.control().equals("name")
|
||||||
|
&& t.property().equals("text")
|
||||||
|
&& t.status().equals("COMPATIBLE")),
|
||||||
|
"Compatible binding target discovery");
|
||||||
|
var complex = await(workflow.bindings(false, initial, "home", "both"));
|
||||||
|
check(
|
||||||
|
complex.targets().stream()
|
||||||
|
.anyMatch(
|
||||||
|
t ->
|
||||||
|
t.control().equals("card")
|
||||||
|
&& t.property().equals("model")
|
||||||
|
&& t.type().equals("fixture.model.Address")
|
||||||
|
&& t.status().equals("COMPATIBLE")),
|
||||||
|
"Component Model metadata compatibility");
|
||||||
var first = await(workflow.render(false, "standard", "card", 310, 220, List.of(initial)));
|
var first = await(workflow.render(false, "standard", "card", 310, 220, List.of(initial)));
|
||||||
check(first.rgba().length == 310 * 220 * 4, "Image frame");
|
check(first.rgba().length == 310 * 220 * 4, "Image frame");
|
||||||
long firstPid = workflow.renderingPid();
|
long firstPid = workflow.renderingPid();
|
||||||
|
|||||||
+1
-1
@@ -46,7 +46,7 @@ public final class ConnectedWorkflowTest extends BasePlatformTestCase {
|
|||||||
UIUtil.dispatchAllInvocationEvents();
|
UIUtil.dispatchAllInvocationEvents();
|
||||||
Thread.sleep(10);
|
Thread.sleep(10);
|
||||||
}
|
}
|
||||||
assertEquals(50, (int) future.get(1, TimeUnit.SECONDS));
|
assertEquals(56, (int) future.get(1, TimeUnit.SECONDS));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 final Path reactorRoot;
|
||||||
private DesignerWorkflow workflow;
|
private DesignerWorkflow workflow;
|
||||||
private DesignerWorkflow interactiveWorkflow;
|
private DesignerWorkflow interactiveWorkflow;
|
||||||
|
private DesignerWorkflow bindingWorkflow;
|
||||||
|
private BindingBrowser bindingBrowser;
|
||||||
|
private long bindingRequest;
|
||||||
private boolean interactiveRequested, interactiveRestart;
|
private boolean interactiveRequested, interactiveRestart;
|
||||||
private long interactiveRequest;
|
private long interactiveRequest;
|
||||||
private final java.util.concurrent.atomic.AtomicLong draftChanges =
|
private final java.util.concurrent.atomic.AtomicLong draftChanges =
|
||||||
@@ -161,6 +164,15 @@ public final class DesignerPane extends BorderPane implements AutoCloseable {
|
|||||||
launch.maven(),
|
launch.maven(),
|
||||||
launch.tools(),
|
launch.tools(),
|
||||||
this::log);
|
this::log);
|
||||||
|
bindingWorkflow =
|
||||||
|
new DesignerWorkflow(
|
||||||
|
host,
|
||||||
|
launch.configuration(),
|
||||||
|
launch.context(),
|
||||||
|
launch.maven(),
|
||||||
|
launch.tools(),
|
||||||
|
this::log);
|
||||||
|
bindingBrowser = new BindingBrowser(this::discoverBindings);
|
||||||
interactiveWorkflow.onInteractiveClosed(
|
interactiveWorkflow.onInteractiveClosed(
|
||||||
() ->
|
() ->
|
||||||
fx(
|
fx(
|
||||||
@@ -181,6 +193,7 @@ public final class DesignerPane extends BorderPane implements AutoCloseable {
|
|||||||
workflow.cancel();
|
workflow.cancel();
|
||||||
fx(
|
fx(
|
||||||
() -> {
|
() -> {
|
||||||
|
invalidateBindings("Document changed; discover bindings again.");
|
||||||
if (interactiveRequested)
|
if (interactiveRequested)
|
||||||
interactiveStatus.setText(
|
interactiveStatus.setText(
|
||||||
"Interactive preview uses earlier drafts; rerun or save to"
|
"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);
|
var properties = new ScrollPane(componentInspector);
|
||||||
properties.setFitToWidth(true);
|
properties.setFitToWidth(true);
|
||||||
properties.setPrefViewportHeight(100);
|
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.setEditable(false);
|
||||||
output.setPrefRowCount(5);
|
output.setPrefRowCount(5);
|
||||||
Path origin = file.resolveSibling(file.getFileName() + ".origin");
|
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));
|
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) {
|
private void build(Map<java.net.URI, Long> approvals) {
|
||||||
|
invalidateBindings("Build changed; discover bindings after Build and Refresh.");
|
||||||
withDraft(
|
withDraft(
|
||||||
() -> {
|
() -> {
|
||||||
renderSequence.incrementAndGet();
|
renderSequence.incrementAndGet();
|
||||||
@@ -960,6 +1013,8 @@ public final class DesignerPane extends BorderPane implements AutoCloseable {
|
|||||||
resize.stop();
|
resize.stop();
|
||||||
workflow.close();
|
workflow.close();
|
||||||
interactiveWorkflow.close();
|
interactiveWorkflow.close();
|
||||||
|
bindingWorkflow.close();
|
||||||
|
bindingRequest++;
|
||||||
if (document != null) document.close();
|
if (document != null) document.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -426,6 +426,61 @@ public final class ProjectionHostChecks {
|
|||||||
check(
|
check(
|
||||||
document.snapshot().toCompletableFuture().get().text().equals(full),
|
document.snapshot().toCompletableFuture().get().text().equals(full),
|
||||||
"Runtime sizing changed source");
|
"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
|
document
|
||||||
.save(document.snapshot().toCompletableFuture().get().revision())
|
.save(document.snapshot().toCompletableFuture().get().revision())
|
||||||
.toCompletableFuture()
|
.toCompletableFuture()
|
||||||
|
|||||||
+334
@@ -0,0 +1,334 @@
|
|||||||
|
package de.neitzel.fxdesigner.target;
|
||||||
|
|
||||||
|
import de.neitzel.fxdesigner.protocol.BindingMetadata;
|
||||||
|
import de.neitzel.fxdesigner.protocol.BindingMetadata.*;
|
||||||
|
import java.beans.*;
|
||||||
|
import java.io.*;
|
||||||
|
import java.lang.reflect.*;
|
||||||
|
import java.util.*;
|
||||||
|
import javafx.beans.property.Property;
|
||||||
|
import javafx.beans.value.ObservableValue;
|
||||||
|
import org.w3c.dom.*;
|
||||||
|
|
||||||
|
/** Signature-only fallback. Never invokes a getter, property accessor, provider or constructor. */
|
||||||
|
public final class BindingDiscovery {
|
||||||
|
private static final String FX = "http://javafx.com/fxml/1";
|
||||||
|
private final ClassLoader loader;
|
||||||
|
private final List<String> diagnostics = new ArrayList<>();
|
||||||
|
private final Map<String, Class<?>> paths = new TreeMap<>();
|
||||||
|
private final List<Attribute> attributes = new ArrayList<>();
|
||||||
|
|
||||||
|
private BindingDiscovery(ClassLoader loader) {
|
||||||
|
this.loader = loader;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static BindingMetadata inspect(
|
||||||
|
String fxml, String path, String direction, ClassLoader loader) throws Exception {
|
||||||
|
if (!Set.of("view", "model", "both").contains(direction))
|
||||||
|
throw new IllegalArgumentException("Invalid direction");
|
||||||
|
return new BindingDiscovery(loader).inspect(fxml, path, direction);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Document parse(String xml) throws Exception {
|
||||||
|
if (xml.length() > 512 * 1024) throw new IOException("FXML metadata input exceeds limit");
|
||||||
|
var f = javax.xml.parsers.DocumentBuilderFactory.newInstance();
|
||||||
|
f.setNamespaceAware(true);
|
||||||
|
f.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
|
||||||
|
f.setFeature("http://xml.org/sax/features/external-general-entities", false);
|
||||||
|
f.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
|
||||||
|
f.setXIncludeAware(false);
|
||||||
|
f.setExpandEntityReferences(false);
|
||||||
|
return f.newDocumentBuilder().parse(new org.xml.sax.InputSource(new StringReader(xml)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String model(Document doc) {
|
||||||
|
String model = "";
|
||||||
|
for (Node n = doc.getFirstChild(); n != null; n = n.getNextSibling())
|
||||||
|
if (n instanceof ProcessingInstruction pi && pi.getTarget().equals("neitzel-model")) {
|
||||||
|
if (!model.isEmpty()) throw new IllegalArgumentException("Duplicate Model declaration");
|
||||||
|
model = pi.getData().trim();
|
||||||
|
if (!model.matches("[\\w$]+(\\.[\\w$]+)+"))
|
||||||
|
throw new IllegalArgumentException("Invalid Model declaration");
|
||||||
|
}
|
||||||
|
return model;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Class<?> load(String name) throws ClassNotFoundException {
|
||||||
|
return Class.forName(name, false, loader);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<PropertyDescriptor> beans(Class<?> type) throws Exception {
|
||||||
|
if (type == Object.class || type.isInterface()) return List.of();
|
||||||
|
// Explicit BeanInfo can execute application code; it is not a safe fallback source.
|
||||||
|
for (Class<?> c = type; c != null && c != Object.class; c = c.getSuperclass()) {
|
||||||
|
try {
|
||||||
|
load(c.getName() + "BeanInfo");
|
||||||
|
diagnostics.add("Custom BeanInfo requires generated metadata: " + c.getName());
|
||||||
|
return List.of();
|
||||||
|
} catch (ClassNotFoundException absent) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Arrays.stream(
|
||||||
|
Introspector.getBeanInfo(type, Object.class, Introspector.IGNORE_ALL_BEANINFO)
|
||||||
|
.getPropertyDescriptors())
|
||||||
|
.sorted(Comparator.comparing(PropertyDescriptor::getName))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean simple(Class<?> c) {
|
||||||
|
return c.isPrimitive()
|
||||||
|
|| c.isEnum()
|
||||||
|
|| c.isArray()
|
||||||
|
|| c == String.class
|
||||||
|
|| c == Boolean.class
|
||||||
|
|| Number.class.isAssignableFrom(c)
|
||||||
|
|| Collection.class.isAssignableFrom(c)
|
||||||
|
|| Map.class.isAssignableFrom(c)
|
||||||
|
|| Date.class.isAssignableFrom(c)
|
||||||
|
|| Calendar.class.isAssignableFrom(c)
|
||||||
|
|| c.getPackageName().startsWith("java.time");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String element(Type t) {
|
||||||
|
if (t instanceof ParameterizedType p && p.getActualTypeArguments().length == 1) {
|
||||||
|
Type a = p.getActualTypeArguments()[0];
|
||||||
|
if (a instanceof Class<?> c) return c.getName();
|
||||||
|
if (a instanceof WildcardType w
|
||||||
|
&& w.getLowerBounds().length == 0
|
||||||
|
&& w.getUpperBounds().length == 1
|
||||||
|
&& w.getUpperBounds()[0] instanceof Class<?> c) return c.getName();
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
private void attributes(Class<?> type, String prefix, Set<Class<?>> ancestors, int depth)
|
||||||
|
throws Exception {
|
||||||
|
if (depth > 4 || ancestors.contains(type)) {
|
||||||
|
diagnostics.add("Nested path expansion stopped at " + prefix);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var next = new HashSet<>(ancestors);
|
||||||
|
next.add(type);
|
||||||
|
for (var p : beans(type)) {
|
||||||
|
if (attributes.size() >= 256) throw new IOException("Too many Model paths");
|
||||||
|
if (p.getReadMethod() == null || p.getWriteMethod() == null || p.getPropertyType() == null)
|
||||||
|
continue;
|
||||||
|
Class<?> c = p.getPropertyType();
|
||||||
|
String path = prefix + p.getName();
|
||||||
|
String kind =
|
||||||
|
Collection.class.isAssignableFrom(c)
|
||||||
|
? "COLLECTION"
|
||||||
|
: c == Object.class
|
||||||
|
? "UNRESOLVED"
|
||||||
|
: !simple(c)
|
||||||
|
&& beans(c).stream()
|
||||||
|
.anyMatch(
|
||||||
|
b -> b.getReadMethod() != null && b.getWriteMethod() != null)
|
||||||
|
? "COMPLEX"
|
||||||
|
: "VALUE";
|
||||||
|
paths.put(path, c);
|
||||||
|
attributes.add(
|
||||||
|
new Attribute(
|
||||||
|
path,
|
||||||
|
c.getName(),
|
||||||
|
kind,
|
||||||
|
kind.equals("COLLECTION") ? element(p.getReadMethod().getGenericReturnType()) : ""));
|
||||||
|
if (kind.equals("COMPLEX")) attributes(c, path + ".", next, depth + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private BindingMetadata inspect(String xml, String selected, String direction) throws Exception {
|
||||||
|
Document doc = parse(xml);
|
||||||
|
String model = model(doc);
|
||||||
|
if (model.isEmpty())
|
||||||
|
return new BindingMetadata(
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
direction,
|
||||||
|
List.of(),
|
||||||
|
List.of(),
|
||||||
|
List.of(
|
||||||
|
"Declare this View's Model with <?neitzel-model package.Model?> before discovering"
|
||||||
|
+ " bindings."));
|
||||||
|
attributes(load(model), "", Set.of(), 0);
|
||||||
|
if (selected.isEmpty())
|
||||||
|
selected =
|
||||||
|
paths.containsKey("name") ? "name" : paths.keySet().stream().findFirst().orElse("");
|
||||||
|
Class<?> source = paths.get(selected);
|
||||||
|
if (source == null && !selected.isEmpty()) diagnostics.add("Unknown Model path: " + selected);
|
||||||
|
var imports = new ArrayList<String>();
|
||||||
|
for (Node n = doc.getFirstChild(); n != null; n = n.getNextSibling())
|
||||||
|
if (n instanceof ProcessingInstruction pi && pi.getTarget().equals("import"))
|
||||||
|
imports.add(pi.getData().trim());
|
||||||
|
var targets = new ArrayList<Target>();
|
||||||
|
var ids = new HashSet<String>();
|
||||||
|
var nodes = doc.getElementsByTagName("*");
|
||||||
|
if (nodes.getLength() > 4096) throw new IOException("Too many FXML elements");
|
||||||
|
for (int i = 0; i < nodes.getLength(); i++) {
|
||||||
|
var e = (Element) nodes.item(i);
|
||||||
|
String id = e.getAttributeNS(FX, "id");
|
||||||
|
if (id.isEmpty() || defined(e)) continue;
|
||||||
|
if (!ids.add(id)) throw new IllegalArgumentException("Duplicate fx:id: " + id);
|
||||||
|
try {
|
||||||
|
Class<?> control = resolve(e, imports);
|
||||||
|
String componentModel = "";
|
||||||
|
var resource = control.getResource(control.getSimpleName() + ".fxml");
|
||||||
|
if (resource != null)
|
||||||
|
try (var in = resource.openStream()) {
|
||||||
|
byte[] b = in.readNBytes(512 * 1024 + 1);
|
||||||
|
if (b.length > 512 * 1024) throw new IOException("Component FXML exceeds limit");
|
||||||
|
componentModel = model(parse(new String(b, java.nio.charset.StandardCharsets.UTF_8)));
|
||||||
|
}
|
||||||
|
for (Method m : control.getMethods()) {
|
||||||
|
if (Modifier.isStatic(m.getModifiers())
|
||||||
|
|| m.getParameterCount() != 0
|
||||||
|
|| !m.getName().endsWith("Property")
|
||||||
|
|| !ObservableValue.class.isAssignableFrom(m.getReturnType())) continue;
|
||||||
|
String property = m.getName().substring(0, m.getName().length() - 8);
|
||||||
|
if (property.isEmpty()) continue;
|
||||||
|
Class<?> getter = valueType(control, property);
|
||||||
|
Class<?> value = propertyType(m.getGenericReturnType(), Map.of());
|
||||||
|
if (value == Number.class && getter != null && Number.class.isAssignableFrom(box(getter)))
|
||||||
|
value = getter;
|
||||||
|
else if (value != null && getter != null && box(value) != box(getter)) value = null;
|
||||||
|
boolean constrained = false;
|
||||||
|
if (property.equals("model") && !componentModel.isEmpty()) {
|
||||||
|
value = load(componentModel);
|
||||||
|
constrained = true;
|
||||||
|
}
|
||||||
|
String status, reason;
|
||||||
|
if (!Property.class.isAssignableFrom(m.getReturnType())) {
|
||||||
|
status = "INCOMPATIBLE";
|
||||||
|
reason = "Runtime bindings require a writable JavaFX Property accessor.";
|
||||||
|
} else if (source == null
|
||||||
|
|| source == Object.class
|
||||||
|
|| value == null
|
||||||
|
|| value == Object.class) {
|
||||||
|
status = "UNRESOLVED";
|
||||||
|
reason =
|
||||||
|
"Declared types are insufficient; rebuild with specific signatures/Model metadata.";
|
||||||
|
} else if (Collection.class.isAssignableFrom(source)
|
||||||
|
|| Collection.class.isAssignableFrom(value)
|
||||||
|
|| Map.class.isAssignableFrom(source)
|
||||||
|
|| Map.class.isAssignableFrom(value)) {
|
||||||
|
status = "UNRESOLVED";
|
||||||
|
reason =
|
||||||
|
"Collection variance requires further metadata; use Repeat/Table for Model"
|
||||||
|
+ " collections.";
|
||||||
|
} else {
|
||||||
|
Class<?> a = box(source), b = box(value);
|
||||||
|
boolean view = b.isAssignableFrom(a), back = a.isAssignableFrom(b);
|
||||||
|
boolean ok =
|
||||||
|
direction.equals("view") ? view : direction.equals("model") ? back : view && back;
|
||||||
|
status = ok ? "COMPATIBLE" : "CONVERTER_REQUIRED";
|
||||||
|
reason =
|
||||||
|
ok
|
||||||
|
? (constrained
|
||||||
|
? "Compatible with the Component's declared Model."
|
||||||
|
: "Declared types support " + direction + " binding.")
|
||||||
|
: "Declared types do not support "
|
||||||
|
+ direction
|
||||||
|
+ " binding; a compatible Converter is required.";
|
||||||
|
}
|
||||||
|
targets.add(
|
||||||
|
new Target(id, property, value == null ? "?" : value.getName(), status, reason));
|
||||||
|
if (targets.size() > 2048) throw new IOException("Too many binding targets");
|
||||||
|
}
|
||||||
|
} catch (ClassNotFoundException | LinkageError ex) {
|
||||||
|
diagnostics.add(id + ": unavailable target type: " + ex.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
targets.sort(
|
||||||
|
Comparator.comparingInt(
|
||||||
|
(Target t) ->
|
||||||
|
List.of("COMPATIBLE", "UNRESOLVED", "CONVERTER_REQUIRED", "INCOMPATIBLE")
|
||||||
|
.indexOf(t.status()))
|
||||||
|
.thenComparing(Target::control)
|
||||||
|
.thenComparing(Target::property));
|
||||||
|
return new BindingMetadata(model, selected, direction, attributes, targets, diagnostics);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean defined(Element e) {
|
||||||
|
for (Node n = e; n instanceof Element a; n = n.getParentNode())
|
||||||
|
if (FX.equals(a.getNamespaceURI()) && a.getLocalName().equals("define")) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Class<?> resolve(Element e, List<String> imports) throws Exception {
|
||||||
|
String tag = e.getTagName();
|
||||||
|
if (FX.equals(e.getNamespaceURI()) && e.getLocalName().equals("root"))
|
||||||
|
tag = e.getAttribute("type");
|
||||||
|
if (tag.contains(".")) return load(tag);
|
||||||
|
var candidates = new HashSet<Class<?>>();
|
||||||
|
for (String imp : imports) {
|
||||||
|
try {
|
||||||
|
if (imp.endsWith(".*")) candidates.add(load(imp.substring(0, imp.length() - 1) + tag));
|
||||||
|
else if (imp.endsWith("." + tag)) candidates.add(load(imp));
|
||||||
|
} catch (ClassNotFoundException ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (candidates.size() != 1) throw new ClassNotFoundException("No unique import for " + tag);
|
||||||
|
return candidates.iterator().next();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Class<?> propertyType(Type type, Map<TypeVariable<?>, Type> inherited) {
|
||||||
|
Class<?> raw;
|
||||||
|
var bindings = new HashMap<>(inherited);
|
||||||
|
if (type instanceof ParameterizedType p && p.getRawType() instanceof Class<?> c) {
|
||||||
|
raw = c;
|
||||||
|
for (int i = 0; i < c.getTypeParameters().length; i++) {
|
||||||
|
Type argument = p.getActualTypeArguments()[i];
|
||||||
|
if (argument instanceof TypeVariable<?> v) argument = bindings.getOrDefault(v, v);
|
||||||
|
bindings.put(c.getTypeParameters()[i], argument);
|
||||||
|
}
|
||||||
|
} else if (type instanceof Class<?> c) raw = c;
|
||||||
|
else return null;
|
||||||
|
if (raw == ObservableValue.class) {
|
||||||
|
Type value = bindings.get(raw.getTypeParameters()[0]);
|
||||||
|
return value instanceof Class<?> c
|
||||||
|
? c
|
||||||
|
: value instanceof ParameterizedType p && p.getRawType() instanceof Class<?> c ? c : null;
|
||||||
|
}
|
||||||
|
for (Type parent : raw.getGenericInterfaces()) {
|
||||||
|
Class<?> value = propertyType(parent, bindings);
|
||||||
|
if (value != null) return value;
|
||||||
|
}
|
||||||
|
return raw.getGenericSuperclass() == null
|
||||||
|
? null
|
||||||
|
: propertyType(raw.getGenericSuperclass(), bindings);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Class<?> valueType(Class<?> control, String name) {
|
||||||
|
String suffix = Character.toUpperCase(name.charAt(0)) + name.substring(1);
|
||||||
|
for (String prefix : List.of("get", "is"))
|
||||||
|
try {
|
||||||
|
Method m = control.getMethod(prefix + suffix);
|
||||||
|
if (!Modifier.isStatic(m.getModifiers()) && m.getReturnType() != void.class)
|
||||||
|
return m.getReturnType();
|
||||||
|
} catch (NoSuchMethodException ignored) {
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Class<?> box(Class<?> c) {
|
||||||
|
if (!c.isPrimitive()) return c;
|
||||||
|
return Map.<Class<?>, Class<?>>of(
|
||||||
|
int.class,
|
||||||
|
Integer.class,
|
||||||
|
long.class,
|
||||||
|
Long.class,
|
||||||
|
double.class,
|
||||||
|
Double.class,
|
||||||
|
float.class,
|
||||||
|
Float.class,
|
||||||
|
boolean.class,
|
||||||
|
Boolean.class,
|
||||||
|
byte.class,
|
||||||
|
Byte.class,
|
||||||
|
short.class,
|
||||||
|
Short.class,
|
||||||
|
char.class,
|
||||||
|
Character.class)
|
||||||
|
.getOrDefault(c, c);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -92,7 +92,7 @@ public final class WorkerMain {
|
|||||||
new Version(1, 0),
|
new Version(1, 0),
|
||||||
new Version(1, 0),
|
new Version(1, 0),
|
||||||
new Version(1, 0),
|
new Version(1, 0),
|
||||||
Set.of("discover-v1", "render-rgba-v1", "layout-v1", "interactive-v1"),
|
Set.of("discover-v1", "render-rgba-v1", "layout-v1", "interactive-v1", "bindings-v1"),
|
||||||
Set.of()));
|
Set.of()));
|
||||||
var welcome = FrameCodec.read(socket.getInputStream());
|
var welcome = FrameCodec.read(socket.getInputStream());
|
||||||
if (!(welcome instanceof Welcome w) || !session.equals(w.session()) || w.wire().major() != 1)
|
if (!(welcome instanceof Welcome w) || !session.equals(w.session()) || w.wire().major() != 1)
|
||||||
@@ -113,7 +113,21 @@ public final class WorkerMain {
|
|||||||
throw new IOException("Unnegotiated operation");
|
throw new IOException("Unnegotiated operation");
|
||||||
try {
|
try {
|
||||||
var loader = WorkerMain.class.getClassLoader();
|
var loader = WorkerMain.class.getClassLoader();
|
||||||
if (p.operation() == Operation.DISCOVER) {
|
if (p.operation() == Operation.BINDINGS) {
|
||||||
|
var input = PreviewMessages.decode(p.payload());
|
||||||
|
if (!input.keySet().equals(Set.of("fxml", "path", "direction")))
|
||||||
|
throw new IOException("Invalid binding discovery input");
|
||||||
|
var metadata =
|
||||||
|
BindingDiscovery.inspect(
|
||||||
|
input.get("fxml"), input.get("path"), input.get("direction"), loader);
|
||||||
|
send(
|
||||||
|
socket,
|
||||||
|
new Packet(
|
||||||
|
Kind.RESULT,
|
||||||
|
p.operation(),
|
||||||
|
p.identity(),
|
||||||
|
PreviewMessages.encode(metadata.encode())));
|
||||||
|
} else if (p.operation() == Operation.DISCOVER) {
|
||||||
if (p.payload().length != 0) throw new IOException("Discovery payload must be empty");
|
if (p.payload().length != 0) throw new IOException("Discovery payload must be empty");
|
||||||
var entries = ExampleCatalog.discover(loader);
|
var entries = ExampleCatalog.discover(loader);
|
||||||
var data = new TreeMap<String, String>();
|
var data = new TreeMap<String, String>();
|
||||||
|
|||||||
+141
@@ -0,0 +1,141 @@
|
|||||||
|
package de.neitzel.fxdesigner.target;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
class BindingDiscoveryTest {
|
||||||
|
public static class Address {
|
||||||
|
public String getStreet() {
|
||||||
|
throw new AssertionError("Getter executed");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setStreet(String s) {
|
||||||
|
throw new AssertionError("Setter executed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Model {
|
||||||
|
static {
|
||||||
|
System.setProperty("discovery.model.initialized", "yes");
|
||||||
|
}
|
||||||
|
|
||||||
|
public Model() {
|
||||||
|
throw new AssertionError("Model constructed");
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
throw new AssertionError("Getter executed");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setName(String s) {}
|
||||||
|
|
||||||
|
public int getRank() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRank(int n) {}
|
||||||
|
|
||||||
|
public Address getHome() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setHome(Address a) {}
|
||||||
|
|
||||||
|
public List<Address> getVisits() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setVisits(List<Address> a) {}
|
||||||
|
|
||||||
|
public String getReadOnly() {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String fxml() {
|
||||||
|
return "<?neitzel-model "
|
||||||
|
+ Model.class.getName()
|
||||||
|
+ "?><?import javafx.scene.control.TextField?><TextField"
|
||||||
|
+ " xmlns:fx=\"http://javafx.com/fxml/1\" fx:id=\"input\"/>";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void discoversNestedAndCollectionSignaturesWithoutApplicationOrToolkitInitialization()
|
||||||
|
throws Exception {
|
||||||
|
var metadata = BindingDiscovery.inspect(fxml(), "name", "both", getClass().getClassLoader());
|
||||||
|
assertEquals(
|
||||||
|
Set.of("home", "home.street", "name", "rank", "visits"),
|
||||||
|
metadata.attributes().stream()
|
||||||
|
.map(a -> a.path())
|
||||||
|
.collect(java.util.stream.Collectors.toSet()));
|
||||||
|
assertEquals(
|
||||||
|
Address.class.getName(),
|
||||||
|
metadata.attributes().stream()
|
||||||
|
.filter(a -> a.path().equals("visits"))
|
||||||
|
.findFirst()
|
||||||
|
.orElseThrow()
|
||||||
|
.elementType());
|
||||||
|
assertTrue(
|
||||||
|
metadata.targets().stream()
|
||||||
|
.anyMatch(t -> t.property().equals("text") && t.status().equals("COMPATIBLE")));
|
||||||
|
assertTrue(
|
||||||
|
metadata.targets().stream()
|
||||||
|
.anyMatch(t -> t.property().equals("width") && t.status().equals("INCOMPATIBLE")));
|
||||||
|
assertNull(System.getProperty("discovery.model.initialized"));
|
||||||
|
assertThrows(IllegalStateException.class, () -> javafx.application.Platform.runLater(() -> {}));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void explainsMismatchesAndRequiresExplicitModel() throws Exception {
|
||||||
|
var result = BindingDiscovery.inspect(fxml(), "rank", "view", getClass().getClassLoader());
|
||||||
|
assertTrue(
|
||||||
|
result.targets().stream()
|
||||||
|
.anyMatch(t -> t.property().equals("text") && t.status().equals("CONVERTER_REQUIRED")));
|
||||||
|
assertTrue(
|
||||||
|
BindingDiscovery.inspect("<VBox/>", "", "both", getClass().getClassLoader())
|
||||||
|
.diagnostics()
|
||||||
|
.getFirst()
|
||||||
|
.contains("neitzel-model"));
|
||||||
|
assertThrows(
|
||||||
|
Exception.class,
|
||||||
|
() ->
|
||||||
|
BindingDiscovery.inspect(
|
||||||
|
"<!DOCTYPE x SYSTEM 'file:///etc/passwd'><x/>",
|
||||||
|
"",
|
||||||
|
"both",
|
||||||
|
getClass().getClassLoader()));
|
||||||
|
assertThrows(
|
||||||
|
IllegalArgumentException.class,
|
||||||
|
() -> BindingDiscovery.inspect(fxml(), "", "wrong", getClass().getClassLoader()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class NumberModel {
|
||||||
|
public Number getAmount() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAmount(Number n) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void compatibilityDependsOnDirection() throws Exception {
|
||||||
|
String xml =
|
||||||
|
"<?neitzel-model "
|
||||||
|
+ NumberModel.class.getName()
|
||||||
|
+ "?><?import javafx.scene.control.ProgressBar?><ProgressBar"
|
||||||
|
+ " xmlns:fx=\"http://javafx.com/fxml/1\" fx:id=\"progress\"/>";
|
||||||
|
for (String direction : List.of("view", "model", "both")) {
|
||||||
|
var metadata =
|
||||||
|
BindingDiscovery.inspect(xml, "amount", direction, getClass().getClassLoader());
|
||||||
|
var target =
|
||||||
|
metadata.targets().stream()
|
||||||
|
.filter(t -> t.property().equals("progress"))
|
||||||
|
.findFirst()
|
||||||
|
.orElseThrow();
|
||||||
|
assertEquals(
|
||||||
|
direction.equals("model") ? "COMPATIBLE" : "CONVERTER_REQUIRED", target.status());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# Shared Model and binding-target discovery
|
||||||
|
|
||||||
|
This is the first stage-4 implementation slice: a read-only binding browser shared by standalone and IntelliJ. Binding declaration edits, Converter selection, generated metadata and the complete inspector remain subsequent work.
|
||||||
|
|
||||||
|
## Use
|
||||||
|
|
||||||
|
Build and Refresh a fixture, expand **Bindings — discovery** below the designer, and select **Discover bindings**. Select a Model path and direction (`view`, `model`, `both`); compatible targets appear first, followed by unresolved targets and mismatches with explanations. Discovery leaves the authoritative FXML and undo history untouched. Errors leave source editing available.
|
||||||
|
|
||||||
|
The document explicitly declares its Model in `<?neitzel-model fixture.model.Person?>`. A missing declaration prompts for it; an example Model is not silently substituted. Fixtures now declare Person for Person/Full Views and Address for their Component resources.
|
||||||
|
|
||||||
|
For Person the browser exposes `name`, `rank`, `home`, `home.street`, and `visits`, including Address as the collection element type. The current document's controls are indexed by fx:id. Nested Component internals belong to their own documents. The browser does not offer fx:define objects as scene controls.
|
||||||
|
|
||||||
|
## Metadata boundary
|
||||||
|
|
||||||
|
The fresh target worker loads classes without initialization and inspects signatures. It never constructs Models, Controllers, providers, controls or Converters, never invokes getters/property accessors, and never starts JavaFX. Application types stay outside both hosts. The independently negotiated `bindings-v1` capability adds the BINDINGS operation; it uses bounded UTF-8 metadata, build/context identity and document revisions, with zero image dimensions. Existing wire operation ordinals remain unchanged.
|
||||||
|
|
||||||
|
Model fallback follows NeitzelLib's public read/write JavaBean Attribute rules and simple/complex/collection classification. Nested paths expand to depth four with cycle diagnostics; collection paths expose declared element types without pretending a collection is a single nested Model. Explicit custom BeanInfo is not executed: types requiring it report a generated-metadata diagnostic. This slice does not yet expose custom/replacement ViewModel members, generated metadata, Computed methods or library-provided message properties. Its compatibility results describe the Model-Attribute binding case, not the complete custom ViewModel API.
|
||||||
|
|
||||||
|
Targets come from public instance JavaFX observable-property accessors. The runtime requires writable `Property` accessors, so read-only targets remain visible as incompatible. Generic observable signatures and getter types identify the value type; incomplete or contradictory signatures remain unresolved. Component `model` targets use the Component resource's explicit Model declaration instead of treating Object as a specific type. Component resource metadata comes from the selected build; current-document metadata comes from the captured draft.
|
||||||
|
|
||||||
|
Compatibility uses boxed declared types and target-runtime assignability: `view` tests Model-to-control, `model` the reverse, and `both` requires both. No implicit numeric/string conversion is assumed. Type mismatches require a compatible Converter; Converter availability/signatures are not yet discovered. Unknown Object types and collection variance remain unresolved, rather than being advertised as verified bindings. Results are potential type matches, not a check that a target is currently unbound or that all runtime access/injection conditions hold.
|
||||||
|
|
||||||
|
## Freshness and limits
|
||||||
|
|
||||||
|
Discovery owns a separate workflow so it does not replace the canvas or interactive worker. It uses a leased successful build and a captured current-document overlay. Generation and document-revision checks reject obsolete replies. Document changes clear the target list and require rediscovery; Build and Refresh invalidates the panel. Explicit retained-build selection is identified in the displayed provenance. Closed editors cancel their metadata worker.
|
||||||
|
|
||||||
|
FXML parsing disables external entities and DOCTYPE. Inputs, scene-node counts, path expansion, target counts and protocol payloads are bounded. Ambiguous imports, duplicate IDs, missing classes, invalid XML and absent declarations produce diagnostics. Custom ViewModel selection, attached-property binding discovery, open child-draft metadata overlays, generated metadata precedence and complete collection compatibility remain later extensions, not silently inferred support.
|
||||||
|
|
||||||
|
## Qualification
|
||||||
|
|
||||||
|
Tests cover non-initializing signature discovery, nested and collection metadata, direction-dependent assignability, read-only targets, incompatible targets, secure parsing, metadata codec bounds and capability negotiation. Real classpath/module-path Person fixtures check Component Model compatibility and absence of Model, Controller and provider construction/initialization. Connected contracts test the worker protocol in both document adapters; live shared Kit checks use the same browser in standalone and the registered IntelliJ editor and verify source preservation.
|
||||||
|
|
||||||
|
Qualification on 2026-11-13 passed: 69 Maven tests and 7 IntelliJ adapter tests; 34 target processes with 424 assertions; connected discovery checks in both document adapters (112 total workflow assertions); live shared-browser checks in standalone and registered IntelliJ for classpath and module-path applications. The existing 48 worker lifecycle cycles, owner-death checks, JavaFX 26 native isolation, package/bytecode boundaries and documentation checks also passed. No new native crash report appeared. Local run logs are `/tmp/binding-bootstrap.log` and `/tmp/binding-registered-ide.log`. The remaining stage-3 human tests are [explicitly skipped by user decision](manual-runtime-navigation.md); automated evidence and reported manual passes remain distinct.
|
||||||
@@ -104,3 +104,7 @@ Scoped Maven build planning and immutable successful-build snapshots are now [im
|
|||||||
## Stage-3 additive preview capabilities
|
## Stage-3 additive preview capabilities
|
||||||
|
|
||||||
The [runtime session implementation](target-runtime-sessions.md) adds negotiated `layout-v1` (MEASURE/OCCURRENCES) and `interactive-v1` operations without changing existing operation ordinals. The optional Preview configuration fields `requestTimeoutSeconds` and `readinessTimeoutSeconds` are schema-validated and default to 30 and 20 seconds. Readiness cannot exceed the request deadline. Measurement metadata and images retain full session/build/document/size correlation.
|
The [runtime session implementation](target-runtime-sessions.md) adds negotiated `layout-v1` (MEASURE/OCCURRENCES) and `interactive-v1` operations without changing existing operation ordinals. The optional Preview configuration fields `requestTimeoutSeconds` and `readinessTimeoutSeconds` are schema-validated and default to 30 and 20 seconds. Readiness cannot exceed the request deadline. Measurement metadata and images retain full session/build/document/size correlation.
|
||||||
|
|
||||||
|
## Binding discovery capability
|
||||||
|
|
||||||
|
The optional `bindings-v1` metadata capability negotiates the appended BINDINGS operation. Requests carry context/build identity and non-empty document revisions, zero image dimensions, and bounded metadata fields `fxml`, `path`, and `direction`. Results use binding metadata format 1 (Model, Attributes, targets, compatibility and diagnostics). Unsupported metadata majors reject required capability negotiation. See the [shared discovery slice](binding-discovery.md).
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Manual runtime navigation and interactive preview review
|
# Manual runtime navigation and interactive preview review
|
||||||
|
|
||||||
Status: the standalone classpath sequence below passed through human review in the development conversation. The corresponding IntelliJ review is next; the overall stage-3 human gate remains open.
|
Status: review ended at the user’s request; accepted to proceed on 2026-11-13. Standalone checks passed. The first eight IntelliJ checks passed; the remaining checks are explicitly skipped, not assumed to have passed. No additional manual tests are scheduled for this gate.
|
||||||
|
|
||||||
## Baseline
|
## Baseline
|
||||||
|
|
||||||
@@ -10,28 +10,28 @@ NeitzelFXDesigner `7b1d754`, including the native worker shutdown correction; ma
|
|||||||
|
|
||||||
| Scenario | Standalone | IntelliJ |
|
| Scenario | Standalone | IntelliJ |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| Full → home opens AddressForm separately and preserves parent | Passed | Pending |
|
| Full → home opens AddressForm separately and preserves parent | Passed | Passed |
|
||||||
| Opening home again reuses the existing editor | Passed | Pending |
|
| Opening home again reuses the existing editor | Passed | Passed |
|
||||||
| AddressForm → nested opens NoteForm with both ancestors open | Passed | Pending |
|
| AddressForm → nested opens NoteForm with both ancestors open | Passed | Passed |
|
||||||
| Repeat visits opens/reuses AddressForm without parent layout changes | Passed | Pending |
|
| Repeat visits opens/reuses AddressForm without parent layout changes | Passed | Passed |
|
||||||
| Explicit Open Row Component opens VisitRow without parent layout changes | Passed | Pending |
|
| Explicit Open Row Component opens VisitRow without parent layout changes | Passed | Passed |
|
||||||
| Independent VisitRow example renders and accepts street edits | Passed | Pending |
|
| Independent VisitRow example renders and accepts street edits | Passed | Passed |
|
||||||
| Closing preview and rerunning creates one fresh sample window | Passed | Pending |
|
| Closing preview and rerunning creates one fresh sample window | Passed | Passed |
|
||||||
| Stop preview and rerun preserve editor responsiveness | Passed | Pending |
|
| Stop preview and rerun preserve editor responsiveness | Passed | Passed |
|
||||||
| Unsaved source comment marks preview outdated and preserves edited sample state | Passed | Pending |
|
| Unsaved source comment marks preview outdated and preserves edited sample state | Passed | Skipped (user decision) |
|
||||||
| Save restarts exactly one preview with fresh data and clears outdated indication | Passed | Pending |
|
| Save restarts exactly one preview with fresh data and clears outdated indication | Passed | Skipped (user decision) |
|
||||||
| Closing VisitRow editor closes its preview; reopening succeeds | Passed | Pending |
|
| Closing VisitRow editor closes its preview; reopening succeeds | Passed | Skipped (user decision) |
|
||||||
| Full Table-row street edits immediately update home/repeated labels | Passed | Pending |
|
| Full Table-row street edits immediately update home/repeated labels | Passed | Skipped (user decision) |
|
||||||
| Invalid numeric input followed by 42 recovers; shared street editing remains responsive | Passed | Pending |
|
| Invalid numeric input followed by 42 recovers; shared street editing remains responsive | Passed | Skipped (user decision) |
|
||||||
| Parent draft and one-step Undo/Redo survive navigation to home and back | Passed | Pending |
|
| Parent draft and one-step Undo/Redo survive navigation to home and back | Passed | Skipped (user decision) |
|
||||||
| Explicit rerun uses current unsaved draft, resets sample state and clears outdated indication | Passed | Pending |
|
| Explicit rerun uses current unsaved draft, resets sample state and clears outdated indication | Passed | Skipped (user decision) |
|
||||||
| Closing AddressForm editor closes only its own preview; Full remains interactive | Passed | Pending |
|
| Closing AddressForm editor closes only its own preview; Full remains interactive | Passed | Skipped (user decision) |
|
||||||
| Reopened AddressForm builds/renders/previews while Full retains its edited sample data | Passed | Pending |
|
| Reopened AddressForm builds/renders/previews while Full retains its edited sample data | Passed | Skipped (user decision) |
|
||||||
|
|
||||||
The numeric recovery check demonstrates observed UI recovery; automated target checks establish Model conversion semantics. Controller actions and source-origin cases retain their recorded automated evidence; this review does not claim additional human observation of those cases.
|
The numeric recovery check demonstrates observed UI recovery; automated target checks establish Model conversion semantics. Controller actions and source-origin cases retain their recorded automated evidence; this review does not claim additional human observation of those cases.
|
||||||
|
|
||||||
## Next check
|
## Acceptance boundary
|
||||||
|
|
||||||
Launch the isolated IntelliJ classpath fixture on this baseline, open Full.fxml in Neitzel Designer, then Build and Refresh. First verify Open Component → home opens AddressForm in another editor tab while Full remains open. Continue the matching sequence above, adapting window ownership to IDE tabs. Preserve the completed standalone observations instead of restarting them.
|
The user stopped before the IntelliJ unsaved-draft test and authorized continuing with shared discovery. Remaining IntelliJ checks and any separate human module-path repetition are skipped. Automated qualification remains recorded separately; waived human checks are not evidence of observed behavior.
|
||||||
|
|
||||||
See [runtime implementation](target-runtime-sessions.md) and [native lifecycle correction](native-worker-lifecycle.md).
|
See [runtime implementation](target-runtime-sessions.md) and [native lifecycle correction](native-worker-lifecycle.md).
|
||||||
|
|||||||
@@ -98,4 +98,10 @@ Roadmap stage 2 now has source-range transactions, broader Component projection
|
|||||||
|
|
||||||
## Target-runtime sessions, sizing and navigation
|
## Target-runtime sessions, sizing and navigation
|
||||||
|
|
||||||
Stage 3 now has retained workers, scoped occurrences and target measurements, shared sizing/refresh, independent interactive preview, and Component/Row source navigation. See [implementation and qualification](target-runtime-sessions.md). Keep the new human navigation/interactive review open until its workflows are observed; the completed stage-2 drag/resize review does not automatically qualify these actions.
|
Stage 3 now has retained workers, scoped occurrences and target measurements, shared sizing/refresh, independent interactive preview, and Component/Row source navigation. See [implementation and qualification](target-runtime-sessions.md). The human navigation review records standalone and initial IntelliJ passes; on 2026-11-13 the user explicitly skipped the remainder and accepted moving to shared discovery. Skipped checks are not reported as passed.
|
||||||
|
|
||||||
|
Manual review disposition (2026-11-13): the user accepted proceeding after the recorded standalone and initial IntelliJ passes. Remaining human checks are [skipped explicitly](manual-runtime-navigation.md), not passed. Continue with shared discovery.
|
||||||
|
|
||||||
|
## Shared discovery: first stage-4 slice
|
||||||
|
|
||||||
|
Implemented the [read-only binding browser](binding-discovery.md) in the shared Kit UI: target-side Person Attribute/path and collection-element discovery, Component Model declarations and direction-aware binding-target compatibility. Real classpath and module-path fixtures qualify both hosts. Binding creation/editing and the remaining metadata categories retain their subsequent work packages.
|
||||||
|
|||||||
@@ -44,9 +44,9 @@ Reactor sources are editable. IntelliJ can also find source/resource pairs in op
|
|||||||
|
|
||||||
The qualification runs passed 64 Maven tests and 7 IntelliJ adapter tests; 32 target processes executed 402 fixture checks. Connected workflow checks cover both hosts with classpath and module-path applications (100 assertions), including native shared Kit rendering and interactive draft/save lifecycle. Registered IntelliJ checks additionally verify the actual designer extension and exclusive JavaFX 26 native libraries.
|
The qualification runs passed 64 Maven tests and 7 IntelliJ adapter tests; 32 target processes executed 402 fixture checks. Connected workflow checks cover both hosts with classpath and module-path applications (100 assertions), including native shared Kit rendering and interactive draft/save lifecycle. Registered IntelliJ checks additionally verify the actual designer extension and exclusive JavaFX 26 native libraries.
|
||||||
|
|
||||||
Run the bootstrap and registered IDE commands in [README](../README.md). A successful automated run does not close the human navigation/interactive review: verify parent → Component → nested Component, repeated open/reuse, independent examples, interactive editing, save-triggered restart, and closing/reopening preview in both hosts. The completed stage-2 mouse review remains valid for its artifact; input changes should receive focused retesting.
|
Run the bootstrap and registered IDE commands in [README](../README.md). The original human-review sequence was: verify parent → Component → nested Component, repeated open/reuse, independent examples, interactive editing, save-triggered restart, and closing/reopening preview in both hosts. The completed stage-2 mouse review remains valid for its artifact; input changes should receive focused retesting.
|
||||||
|
|
||||||
## Next human review (both hosts)
|
## Human review sequence and disposition
|
||||||
|
|
||||||
1. Open `Full.fxml`, Build and Refresh, and select its `full` example. Select AddressForm and use Open Component. Confirm a separate editor opens and repeated opening reuses it, with the parent draft intact.
|
1. Open `Full.fxml`, Build and Refresh, and select its `full` example. Select AddressForm and use Open Component. Confirm a separate editor opens and repeated opening reuses it, with the parent draft intact.
|
||||||
2. Build and Refresh the child; verify its independent example, then open nested NoteForm. Return to the parent and check selection/editability and Undo/Redo.
|
2. Build and Refresh the child; verify its independent example, then open nested NoteForm. Return to the parent and check selection/editability and Undo/Redo.
|
||||||
@@ -54,12 +54,14 @@ Run the bootstrap and registered IDE commands in [README](../README.md). A succe
|
|||||||
4. Start Interactive preview. Edit the street and exercise the action button and Table. Make an unsaved designer edit: the running sample state should remain and show outdated status. Save: preview should restart with fresh sample data and the saved change.
|
4. Start Interactive preview. Edit the street and exercise the action button and Table. Make an unsaved designer edit: the running sample state should remain and show outdated status. Save: preview should restart with fresh sample data and the saved change.
|
||||||
5. Close the interactive window, rerun it, stop it, and close the owning editor. Check that windows close and can be opened again. Repeat with the module-path fixture and IntelliJ host.
|
5. Close the interactive window, rerun it, stop it, and close the owning editor. Check that windows close and can be opened again. Repeat with the module-path fixture and IntelliJ host.
|
||||||
|
|
||||||
The standalone classpath sequence has passed the [recorded human review](manual-runtime-navigation.md). IntelliJ review is next; the overall human gate remains open. Automated window/control checks do not count as human acceptance.
|
The standalone classpath sequence has passed the [recorded human review](manual-runtime-navigation.md). The first eight IntelliJ checks also passed; the user skipped the remaining checks and accepted proceeding on 2026-11-13. Automated window/control checks do not count as human acceptance.
|
||||||
|
|
||||||
## Native-runtime lifecycle correction
|
## Native-runtime lifecycle correction
|
||||||
|
|
||||||
The native JavaFX 21 target-worker crash observed during stage-3 qualification has been reproduced and corrected. See the [diagnosis, shutdown contract and lifecycle regression](native-worker-lifecycle.md). The runners now reject native crash reports even when UI assertions pass. Human navigation and interactive-preview review remains open.
|
The native JavaFX 21 target-worker crash observed during stage-3 qualification has been reproduced and corrected. See the [diagnosis, shutdown contract and lifecycle regression](native-worker-lifecycle.md). The runners now reject native crash reports even when UI assertions pass. The remaining human review was explicitly skipped by the user; see its recorded disposition.
|
||||||
|
|
||||||
## Boundaries
|
## Boundaries
|
||||||
|
|
||||||
Qualification remains the recorded macOS arm64 / OpenJDK 25 / JavaFX and Kit 26 / IntelliJ 263.7307.200 host baseline and Java 21 / JavaFX 21.0.7 target fixtures. New protocol fields and preview behavior are development contracts, not a frozen public release. Structural authoring, binding/generation panels, production host handoff, broader platform coverage and packaging retain their later gates. VoiceOver remains outside scope.
|
Qualification remains the recorded macOS arm64 / OpenJDK 25 / JavaFX and Kit 26 / IntelliJ 263.7307.200 host baseline and Java 21 / JavaFX 21.0.7 target fixtures. New protocol fields and preview behavior are development contracts, not a frozen public release. Structural authoring, binding/generation panels, production host handoff, broader platform coverage and packaging retain their later gates. VoiceOver remains outside scope.
|
||||||
|
|
||||||
|
Manual review disposition (2026-11-13): the user accepted proceeding after the recorded standalone and initial IntelliJ passes. Remaining human checks are [skipped explicitly](manual-runtime-navigation.md), not passed. Continue with shared discovery.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Target-runtime sessions, sizing and Component navigation
|
# Target-runtime sessions, sizing and Component navigation
|
||||||
|
|
||||||
Status: stage-3 implementation and automated qualification are described in the [runtime implementation report](target-runtime-sessions.md). The new navigation/interactive human review remains a separate gate. This document retains the implementation sequence and acceptance criteria. Stage 2's [manual input gate](manual-input-validation.md) is closed on its recorded baseline. This work implements the accepted preview and host contracts; it does not reopen them as Wayfinder decisions.
|
Status: stage-3 implementation and automated qualification are described in the [runtime implementation report](target-runtime-sessions.md). The [human review](manual-runtime-navigation.md) records observed passes and the user’s decision to skip the remaining checks and proceed. This document retains the implementation sequence and acceptance criteria. Stage 2's [manual input gate](manual-input-validation.md) is closed on its recorded baseline. This work implements the accepted preview and host contracts; it does not reopen them as Wayfinder decisions.
|
||||||
|
|
||||||
## Starting implementation and gaps (before this stage)
|
## Starting implementation and gaps (before this stage)
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
package fixture.model;
|
package fixture.model;
|
||||||
|
|
||||||
public class Person {
|
public class Person {
|
||||||
|
static {
|
||||||
|
System.setProperty("fixture.person.initialized", "yes");
|
||||||
|
}
|
||||||
|
|
||||||
private java.util.List<Address> visits = new java.util.ArrayList<>();
|
private java.util.List<Address> visits = new java.util.ArrayList<>();
|
||||||
|
|
||||||
public java.util.List<Address> getVisits() {
|
public java.util.List<Address> getVisits() {
|
||||||
|
|||||||
+1
@@ -1,2 +1,3 @@
|
|||||||
|
<?neitzel-model fixture.model.Address?>
|
||||||
<?import javafx.scene.layout.VBox?><?import javafx.scene.control.Label?><?import fixture.components.NoteForm?>
|
<?import javafx.scene.layout.VBox?><?import javafx.scene.control.Label?><?import fixture.components.NoteForm?>
|
||||||
<fx:root type="javafx.scene.layout.VBox" xmlns:fx="http://javafx.com/fxml/1"><Label text="A long address explanation that should wrap across several lines when the allocated width is narrow" wrapText="true"/><NoteForm fx:id="nested"/></fx:root>
|
<fx:root type="javafx.scene.layout.VBox" xmlns:fx="http://javafx.com/fxml/1"><Label text="A long address explanation that should wrap across several lines when the allocated width is narrow" wrapText="true"/><NoteForm fx:id="nested"/></fx:root>
|
||||||
|
|||||||
+1
@@ -1,2 +1,3 @@
|
|||||||
|
<?neitzel-model fixture.model.Address?>
|
||||||
<?import javafx.scene.layout.VBox?><?import javafx.scene.control.Label?><?import javafx.scene.control.TextField?><?import de.neitzel.fx.view.BindingList?><?import de.neitzel.fx.view.DirectBinding?>
|
<?import javafx.scene.layout.VBox?><?import javafx.scene.control.Label?><?import javafx.scene.control.TextField?><?import de.neitzel.fx.view.BindingList?><?import de.neitzel.fx.view.DirectBinding?>
|
||||||
<fx:root type="javafx.scene.layout.VBox" xmlns:fx="http://javafx.com/fxml/1"><fx:define><BindingList><DirectBinding control="street" property="text" path="street" direction="both"/></BindingList></fx:define><Label fx:id="captionLabel"/><TextField fx:id="street"/></fx:root>
|
<fx:root type="javafx.scene.layout.VBox" xmlns:fx="http://javafx.com/fxml/1"><fx:define><BindingList><DirectBinding control="street" property="text" path="street" direction="both"/></BindingList></fx:define><Label fx:id="captionLabel"/><TextField fx:id="street"/></fx:root>
|
||||||
+1
@@ -1,3 +1,4 @@
|
|||||||
|
<?neitzel-model fixture.model.Person?>
|
||||||
<?import javafx.scene.layout.VBox?><?import javafx.scene.control.TextField?><?import javafx.scene.control.Button?><?import fixture.components.AddressForm?><?import de.neitzel.fx.view.BindingList?><?import de.neitzel.fx.view.DirectBinding?><?import de.neitzel.fx.view.Repeat?><?import de.neitzel.fx.view.Table?><?import de.neitzel.fx.view.Column?>
|
<?import javafx.scene.layout.VBox?><?import javafx.scene.control.TextField?><?import javafx.scene.control.Button?><?import fixture.components.AddressForm?><?import de.neitzel.fx.view.BindingList?><?import de.neitzel.fx.view.DirectBinding?><?import de.neitzel.fx.view.Repeat?><?import de.neitzel.fx.view.Table?><?import de.neitzel.fx.view.Column?>
|
||||||
<VBox xmlns:fx="http://javafx.com/fxml/1" fx:controller="fixture.components.PersonController" prefWidth="640" prefHeight="700">
|
<VBox xmlns:fx="http://javafx.com/fxml/1" fx:controller="fixture.components.PersonController" prefWidth="640" prefHeight="700">
|
||||||
<fx:define><BindingList><DirectBinding control="rankInput" property="text" path="rank" direction="both" converter="fixture.components.RankConverter"/><DirectBinding control="home" property="model" path="home" direction="both"/><Repeat pane="visits" component="fixture.components.AddressForm" collection="visits"/></BindingList></fx:define>
|
<fx:define><BindingList><DirectBinding control="rankInput" property="text" path="rank" direction="both" converter="fixture.components.RankConverter"/><DirectBinding control="home" property="model" path="home" direction="both"/><Repeat pane="visits" component="fixture.components.AddressForm" collection="visits"/></BindingList></fx:define>
|
||||||
|
|||||||
+1
@@ -1,2 +1,3 @@
|
|||||||
|
<?neitzel-model fixture.model.Address?>
|
||||||
<?import javafx.scene.layout.VBox?><?import javafx.scene.control.Label?><?import de.neitzel.fx.view.BindingList?><?import de.neitzel.fx.view.DirectBinding?>
|
<?import javafx.scene.layout.VBox?><?import javafx.scene.control.Label?><?import de.neitzel.fx.view.BindingList?><?import de.neitzel.fx.view.DirectBinding?>
|
||||||
<fx:root type="javafx.scene.layout.VBox" xmlns:fx="http://javafx.com/fxml/1"><fx:define><BindingList><DirectBinding control="note" property="text" path="street" direction="both"/></BindingList></fx:define><Label fx:id="note" wrapText="true" text="Note"/></fx:root>
|
<fx:root type="javafx.scene.layout.VBox" xmlns:fx="http://javafx.com/fxml/1"><fx:define><BindingList><DirectBinding control="note" property="text" path="street" direction="both"/></BindingList></fx:define><Label fx:id="note" wrapText="true" text="Note"/></fx:root>
|
||||||
|
|||||||
+1
@@ -1,2 +1,3 @@
|
|||||||
|
<?neitzel-model fixture.model.Person?>
|
||||||
<?import javafx.scene.layout.VBox?><?import javafx.scene.control.Label?><?import javafx.scene.control.TextField?><?import fixture.components.Card?><?import de.neitzel.fx.view.BindingList?><?import de.neitzel.fx.view.DirectBinding?><?import de.neitzel.fx.view.ComputedBinding?>
|
<?import javafx.scene.layout.VBox?><?import javafx.scene.control.Label?><?import javafx.scene.control.TextField?><?import fixture.components.Card?><?import de.neitzel.fx.view.BindingList?><?import de.neitzel.fx.view.DirectBinding?><?import de.neitzel.fx.view.ComputedBinding?>
|
||||||
<VBox xmlns:fx="http://javafx.com/fxml/1" fx:controller="fixture.components.PersonController" prefWidth="420" prefHeight="240"><fx:define><BindingList><DirectBinding control="name" property="text" path="name" direction="both"/><DirectBinding control="card" property="model" path="home" direction="both"/><DirectBinding control="second" property="model" path="home" direction="both"/><ComputedBinding control="greeting" property="text" method="#greeting"/></BindingList></fx:define><TextField fx:id="name"/><Label fx:id="greeting"/><Card fx:id="card" caption="FXML caption" prefWidth="310" alignment="CENTER_LEFT"/><Card fx:id="second" caption="Second"/></VBox>
|
<VBox xmlns:fx="http://javafx.com/fxml/1" fx:controller="fixture.components.PersonController" prefWidth="420" prefHeight="240"><fx:define><BindingList><DirectBinding control="name" property="text" path="name" direction="both"/><DirectBinding control="card" property="model" path="home" direction="both"/><DirectBinding control="second" property="model" path="home" direction="both"/><ComputedBinding control="greeting" property="text" method="#greeting"/></BindingList></fx:define><TextField fx:id="name"/><Label fx:id="greeting"/><Card fx:id="card" caption="FXML caption" prefWidth="310" alignment="CENTER_LEFT"/><Card fx:id="second" caption="Second"/></VBox>
|
||||||
+1
@@ -1,2 +1,3 @@
|
|||||||
|
<?neitzel-model fixture.model.Address?>
|
||||||
<?import javafx.scene.layout.HBox?><?import javafx.scene.control.TextField?><?import de.neitzel.fx.view.BindingList?><?import de.neitzel.fx.view.DirectBinding?>
|
<?import javafx.scene.layout.HBox?><?import javafx.scene.control.TextField?><?import de.neitzel.fx.view.BindingList?><?import de.neitzel.fx.view.DirectBinding?>
|
||||||
<fx:root type="javafx.scene.layout.HBox" xmlns:fx="http://javafx.com/fxml/1"><fx:define><BindingList><DirectBinding control="street" property="text" path="street" direction="both"/></BindingList></fx:define><TextField fx:id="street"/></fx:root>
|
<fx:root type="javafx.scene.layout.HBox" xmlns:fx="http://javafx.com/fxml/1"><fx:define><BindingList><DirectBinding control="street" property="text" path="street" direction="both"/></BindingList></fx:define><TextField fx:id="street"/></fx:root>
|
||||||
|
|||||||
+33
@@ -60,6 +60,39 @@ public final class PreviewChecks {
|
|||||||
System.out.println("PASS discovery " + checks);
|
System.out.println("PASS discovery " + checks);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (mode.equals("bindings")) {
|
||||||
|
String xml;
|
||||||
|
try (var in = loader.getResourceAsStream("fixture/components/Person.fxml")) {
|
||||||
|
xml = new String(in.readAllBytes(), java.nio.charset.StandardCharsets.UTF_8);
|
||||||
|
}
|
||||||
|
var metadata = BindingDiscovery.inspect(xml, "home", "both", loader);
|
||||||
|
check(
|
||||||
|
metadata.attributes().stream().anyMatch(a -> a.path().equals("home.street")),
|
||||||
|
"Nested metadata");
|
||||||
|
check(
|
||||||
|
metadata.attributes().stream()
|
||||||
|
.anyMatch(
|
||||||
|
a ->
|
||||||
|
a.path().equals("visits") && a.elementType().equals("fixture.model.Address")),
|
||||||
|
"Collection element metadata");
|
||||||
|
check(
|
||||||
|
metadata.targets().stream()
|
||||||
|
.anyMatch(
|
||||||
|
t ->
|
||||||
|
t.control().equals("card")
|
||||||
|
&& t.property().equals("model")
|
||||||
|
&& t.status().equals("COMPATIBLE")),
|
||||||
|
"Component Model target metadata");
|
||||||
|
check(
|
||||||
|
System.getProperty("fixture.person.initialized") == null, "Discovery initialized Person");
|
||||||
|
check(System.getProperty("fixture.models") == null, "Discovery constructed Model");
|
||||||
|
check(System.getProperty("fixture.controllers") == null, "Discovery constructed Controller");
|
||||||
|
check(
|
||||||
|
System.getProperty("fixture.provider.initialized") == null,
|
||||||
|
"Discovery initialized provider");
|
||||||
|
System.out.println("PASS bindings " + checks);
|
||||||
|
return;
|
||||||
|
}
|
||||||
var started = new CountDownLatch(1);
|
var started = new CountDownLatch(1);
|
||||||
Platform.startup(
|
Platform.startup(
|
||||||
() -> {
|
() -> {
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
package fixture.model;
|
package fixture.model;
|
||||||
|
|
||||||
public class Person {
|
public class Person {
|
||||||
|
static {
|
||||||
|
System.setProperty("fixture.person.initialized", "yes");
|
||||||
|
}
|
||||||
|
|
||||||
private java.util.List<Address> visits = new java.util.ArrayList<>();
|
private java.util.List<Address> visits = new java.util.ArrayList<>();
|
||||||
|
|
||||||
public java.util.List<Address> getVisits() {
|
public java.util.List<Address> getVisits() {
|
||||||
|
|||||||
+1
@@ -1,2 +1,3 @@
|
|||||||
|
<?neitzel-model fixture.model.Address?>
|
||||||
<?import javafx.scene.layout.VBox?><?import javafx.scene.control.Label?><?import fixture.components.NoteForm?>
|
<?import javafx.scene.layout.VBox?><?import javafx.scene.control.Label?><?import fixture.components.NoteForm?>
|
||||||
<fx:root type="javafx.scene.layout.VBox" xmlns:fx="http://javafx.com/fxml/1"><Label text="A long address explanation that should wrap across several lines when the allocated width is narrow" wrapText="true"/><NoteForm fx:id="nested"/></fx:root>
|
<fx:root type="javafx.scene.layout.VBox" xmlns:fx="http://javafx.com/fxml/1"><Label text="A long address explanation that should wrap across several lines when the allocated width is narrow" wrapText="true"/><NoteForm fx:id="nested"/></fx:root>
|
||||||
|
|||||||
+1
@@ -1,2 +1,3 @@
|
|||||||
|
<?neitzel-model fixture.model.Address?>
|
||||||
<?import javafx.scene.layout.VBox?><?import javafx.scene.control.Label?><?import javafx.scene.control.TextField?><?import de.neitzel.fx.view.BindingList?><?import de.neitzel.fx.view.DirectBinding?>
|
<?import javafx.scene.layout.VBox?><?import javafx.scene.control.Label?><?import javafx.scene.control.TextField?><?import de.neitzel.fx.view.BindingList?><?import de.neitzel.fx.view.DirectBinding?>
|
||||||
<fx:root type="javafx.scene.layout.VBox" xmlns:fx="http://javafx.com/fxml/1"><fx:define><BindingList><DirectBinding control="street" property="text" path="street" direction="both"/></BindingList></fx:define><Label fx:id="captionLabel"/><TextField fx:id="street"/></fx:root>
|
<fx:root type="javafx.scene.layout.VBox" xmlns:fx="http://javafx.com/fxml/1"><fx:define><BindingList><DirectBinding control="street" property="text" path="street" direction="both"/></BindingList></fx:define><Label fx:id="captionLabel"/><TextField fx:id="street"/></fx:root>
|
||||||
+1
@@ -1,3 +1,4 @@
|
|||||||
|
<?neitzel-model fixture.model.Person?>
|
||||||
<?import javafx.scene.layout.VBox?><?import javafx.scene.control.TextField?><?import javafx.scene.control.Button?><?import fixture.components.AddressForm?><?import de.neitzel.fx.view.BindingList?><?import de.neitzel.fx.view.DirectBinding?><?import de.neitzel.fx.view.Repeat?><?import de.neitzel.fx.view.Table?><?import de.neitzel.fx.view.Column?>
|
<?import javafx.scene.layout.VBox?><?import javafx.scene.control.TextField?><?import javafx.scene.control.Button?><?import fixture.components.AddressForm?><?import de.neitzel.fx.view.BindingList?><?import de.neitzel.fx.view.DirectBinding?><?import de.neitzel.fx.view.Repeat?><?import de.neitzel.fx.view.Table?><?import de.neitzel.fx.view.Column?>
|
||||||
<VBox xmlns:fx="http://javafx.com/fxml/1" fx:controller="fixture.components.PersonController" prefWidth="640" prefHeight="700">
|
<VBox xmlns:fx="http://javafx.com/fxml/1" fx:controller="fixture.components.PersonController" prefWidth="640" prefHeight="700">
|
||||||
<fx:define><BindingList><DirectBinding control="rankInput" property="text" path="rank" direction="both" converter="fixture.components.RankConverter"/><DirectBinding control="home" property="model" path="home" direction="both"/><Repeat pane="visits" component="fixture.components.AddressForm" collection="visits"/></BindingList></fx:define>
|
<fx:define><BindingList><DirectBinding control="rankInput" property="text" path="rank" direction="both" converter="fixture.components.RankConverter"/><DirectBinding control="home" property="model" path="home" direction="both"/><Repeat pane="visits" component="fixture.components.AddressForm" collection="visits"/></BindingList></fx:define>
|
||||||
|
|||||||
+1
@@ -1,2 +1,3 @@
|
|||||||
|
<?neitzel-model fixture.model.Address?>
|
||||||
<?import javafx.scene.layout.VBox?><?import javafx.scene.control.Label?><?import de.neitzel.fx.view.BindingList?><?import de.neitzel.fx.view.DirectBinding?>
|
<?import javafx.scene.layout.VBox?><?import javafx.scene.control.Label?><?import de.neitzel.fx.view.BindingList?><?import de.neitzel.fx.view.DirectBinding?>
|
||||||
<fx:root type="javafx.scene.layout.VBox" xmlns:fx="http://javafx.com/fxml/1"><fx:define><BindingList><DirectBinding control="note" property="text" path="street" direction="both"/></BindingList></fx:define><Label fx:id="note" wrapText="true" text="Note"/></fx:root>
|
<fx:root type="javafx.scene.layout.VBox" xmlns:fx="http://javafx.com/fxml/1"><fx:define><BindingList><DirectBinding control="note" property="text" path="street" direction="both"/></BindingList></fx:define><Label fx:id="note" wrapText="true" text="Note"/></fx:root>
|
||||||
|
|||||||
+1
@@ -1,2 +1,3 @@
|
|||||||
|
<?neitzel-model fixture.model.Person?>
|
||||||
<?import javafx.scene.layout.VBox?><?import javafx.scene.control.Label?><?import javafx.scene.control.TextField?><?import fixture.components.Card?><?import de.neitzel.fx.view.BindingList?><?import de.neitzel.fx.view.DirectBinding?><?import de.neitzel.fx.view.ComputedBinding?>
|
<?import javafx.scene.layout.VBox?><?import javafx.scene.control.Label?><?import javafx.scene.control.TextField?><?import fixture.components.Card?><?import de.neitzel.fx.view.BindingList?><?import de.neitzel.fx.view.DirectBinding?><?import de.neitzel.fx.view.ComputedBinding?>
|
||||||
<VBox xmlns:fx="http://javafx.com/fxml/1" fx:controller="fixture.components.PersonController" prefWidth="420" prefHeight="240"><fx:define><BindingList><DirectBinding control="name" property="text" path="name" direction="both"/><DirectBinding control="card" property="model" path="home" direction="both"/><DirectBinding control="second" property="model" path="home" direction="both"/><ComputedBinding control="greeting" property="text" method="#greeting"/></BindingList></fx:define><TextField fx:id="name"/><Label fx:id="greeting"/><Card fx:id="card" caption="FXML caption" prefWidth="310" alignment="CENTER_LEFT"/><Card fx:id="second" caption="Second"/></VBox>
|
<VBox xmlns:fx="http://javafx.com/fxml/1" fx:controller="fixture.components.PersonController" prefWidth="420" prefHeight="240"><fx:define><BindingList><DirectBinding control="name" property="text" path="name" direction="both"/><DirectBinding control="card" property="model" path="home" direction="both"/><DirectBinding control="second" property="model" path="home" direction="both"/><ComputedBinding control="greeting" property="text" method="#greeting"/></BindingList></fx:define><TextField fx:id="name"/><Label fx:id="greeting"/><Card fx:id="card" caption="FXML caption" prefWidth="310" alignment="CENTER_LEFT"/><Card fx:id="second" caption="Second"/></VBox>
|
||||||
+1
@@ -1,2 +1,3 @@
|
|||||||
|
<?neitzel-model fixture.model.Address?>
|
||||||
<?import javafx.scene.layout.HBox?><?import javafx.scene.control.TextField?><?import de.neitzel.fx.view.BindingList?><?import de.neitzel.fx.view.DirectBinding?>
|
<?import javafx.scene.layout.HBox?><?import javafx.scene.control.TextField?><?import de.neitzel.fx.view.BindingList?><?import de.neitzel.fx.view.DirectBinding?>
|
||||||
<fx:root type="javafx.scene.layout.HBox" xmlns:fx="http://javafx.com/fxml/1"><fx:define><BindingList><DirectBinding control="street" property="text" path="street" direction="both"/></BindingList></fx:define><TextField fx:id="street"/></fx:root>
|
<fx:root type="javafx.scene.layout.HBox" xmlns:fx="http://javafx.com/fxml/1"><fx:define><BindingList><DirectBinding control="street" property="text" path="street" direction="both"/></BindingList></fx:define><TextField fx:id="street"/></fx:root>
|
||||||
|
|||||||
+33
@@ -60,6 +60,39 @@ public final class PreviewChecks {
|
|||||||
System.out.println("PASS discovery " + checks);
|
System.out.println("PASS discovery " + checks);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (mode.equals("bindings")) {
|
||||||
|
String xml;
|
||||||
|
try (var in = loader.getResourceAsStream("fixture/components/Person.fxml")) {
|
||||||
|
xml = new String(in.readAllBytes(), java.nio.charset.StandardCharsets.UTF_8);
|
||||||
|
}
|
||||||
|
var metadata = BindingDiscovery.inspect(xml, "home", "both", loader);
|
||||||
|
check(
|
||||||
|
metadata.attributes().stream().anyMatch(a -> a.path().equals("home.street")),
|
||||||
|
"Nested metadata");
|
||||||
|
check(
|
||||||
|
metadata.attributes().stream()
|
||||||
|
.anyMatch(
|
||||||
|
a ->
|
||||||
|
a.path().equals("visits") && a.elementType().equals("fixture.model.Address")),
|
||||||
|
"Collection element metadata");
|
||||||
|
check(
|
||||||
|
metadata.targets().stream()
|
||||||
|
.anyMatch(
|
||||||
|
t ->
|
||||||
|
t.control().equals("card")
|
||||||
|
&& t.property().equals("model")
|
||||||
|
&& t.status().equals("COMPATIBLE")),
|
||||||
|
"Component Model target metadata");
|
||||||
|
check(
|
||||||
|
System.getProperty("fixture.person.initialized") == null, "Discovery initialized Person");
|
||||||
|
check(System.getProperty("fixture.models") == null, "Discovery constructed Model");
|
||||||
|
check(System.getProperty("fixture.controllers") == null, "Discovery constructed Controller");
|
||||||
|
check(
|
||||||
|
System.getProperty("fixture.provider.initialized") == null,
|
||||||
|
"Discovery initialized provider");
|
||||||
|
System.out.println("PASS bindings " + checks);
|
||||||
|
return;
|
||||||
|
}
|
||||||
var started = new CountDownLatch(1);
|
var started = new CountDownLatch(1);
|
||||||
Platform.startup(
|
Platform.startup(
|
||||||
() -> {
|
() -> {
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ with tempfile.TemporaryDirectory(prefix="designer-preview-") as temp:
|
|||||||
ignore=shutil.ignore_patterns("target"))
|
ignore=shutil.ignore_patterns("target"))
|
||||||
build(path, arrangement + "-build")
|
build(path, arrangement + "-build")
|
||||||
first = None
|
first = None
|
||||||
for mode in ("discovery", "standard", "async", "timeout", "cancel",
|
for mode in ("discovery", "bindings", "standard", "async", "timeout", "cancel",
|
||||||
"readiness-failure", "wrong-model", "invalid-attribute",
|
"readiness-failure", "wrong-model", "invalid-attribute",
|
||||||
"cleanup-failure", "factory-failure", "full", "full-empty", "card", "unstable"):
|
"cleanup-failure", "factory-failure", "full", "full-empty", "card", "unstable"):
|
||||||
log = launch(path, arrangement, mode, arrangement + "-" + mode)
|
log = launch(path, arrangement, mode, arrangement + "-" + mode)
|
||||||
|
|||||||
Reference in New Issue
Block a user