Implement retained target sessions, runtime sizing and Component navigation

This commit is contained in:
2026-11-12 14:20:26 +01:00
parent e41b276b13
commit 6e25cf3ac6
71 changed files with 2538 additions and 120 deletions
@@ -108,7 +108,21 @@ public final class DesignerPane extends BorderPane implements AutoCloseable {
private DocumentSession document;
private DocumentSnapshot draft;
private FxmlProjection projection;
private final Path reactorRoot;
private DesignerWorkflow workflow;
private DesignerWorkflow interactiveWorkflow;
private boolean interactiveRequested, interactiveRestart;
private long interactiveRequest;
private final java.util.concurrent.atomic.AtomicLong draftChanges =
new java.util.concurrent.atomic.AtomicLong();
private final Label interactiveStatus = new Label();
private AutoCloseable saves;
public long interactivePid() {
return interactiveWorkflow.renderingPid();
}
private int layoutPasses;
private boolean loading, closed, pending, last, canvasCommitQueued;
private long canvasGeneration;
private final java.util.concurrent.atomic.AtomicLong renderSequence =
@@ -139,16 +153,61 @@ public final class DesignerPane extends BorderPane implements AutoCloseable {
launch.maven(),
launch.tools(),
this::log);
interactiveWorkflow =
new DesignerWorkflow(
host,
launch.configuration(),
launch.context(),
launch.maven(),
launch.tools(),
this::log);
interactiveWorkflow.onInteractiveClosed(
() ->
fx(
() -> {
interactiveRequested = false;
interactiveRequest++;
interactiveStatus.setText("Interactive preview closed");
}));
Path reactor = launch.configuration().getParent().toRealPath();
reactorRoot = reactor;
changes =
host.documents()
.onChange(
uri -> {
if (Path.of(uri).startsWith(reactor)) {
draftChanges.incrementAndGet();
renderSequence.incrementAndGet();
workflow.cancel();
fx(
() -> {
if (interactiveRequested)
interactiveStatus.setText(
"Interactive preview uses earlier drafts; rerun or save to"
+ " restart");
});
fx(
() -> {
layoutPasses = 0;
resize.playFromStart();
});
}
});
saves =
host.documents()
.onSave(
uri -> {
if (Path.of(uri).startsWith(reactorRoot))
fx(
() -> {
if (interactiveRequested) {
interactiveRestart = true;
interactiveRequest++;
interactiveWorkflow.cancel();
}
resize.playFromStart();
});
});
var bar =
new ToolBar(
button("Build and Refresh", () -> build(Map.of())),
@@ -160,14 +219,55 @@ public final class DesignerPane extends BorderPane implements AutoCloseable {
renderSequence.incrementAndGet();
}),
examples,
button("Render drafts", this::render),
button(
"Render drafts",
() -> {
layoutPasses = 0;
render();
}),
button(
"Interactive preview",
() -> {
interactiveRequested = true;
interactiveRequest++;
interactiveRestart = true;
interactiveWorkflow.cancel();
render();
}),
button(
"Stop preview",
() -> {
interactiveRequested = false;
interactiveRequest++;
interactiveRestart = false;
interactiveStatus.setText("");
interactiveWorkflow.cancel();
}),
button("Preview last successful", () -> discover(true)),
button(
"Save",
() -> withDraft(() -> document.save(draft.revision()).whenComplete(this::updated))),
button("Undo", () -> history(false)),
button("Redo", () -> history(true)),
button("Open Component", this::openComponent));
button("Open Component", this::openComponent),
button("Open Row Component", this::openRowComponent),
button(
"Open Java",
() -> {
var selected = kit.editor().getSelection().getHitItem();
if (selected != null
&& projection != null
&& projection.navigationTypes().containsKey(selected.getFxId()))
workflow
.javaSource(projection.navigationTypes().get(selected.getFxId()))
.thenCompose(
uri -> host.navigation().open(new HostServices.Location(uri, 0)))
.exceptionally(
error -> {
fx(() -> report(error));
return null;
});
}));
examples.setConverter(
new javafx.util.StringConverter<>() {
public String toString(DesignerWorkflow.Example value) {
@@ -198,9 +298,14 @@ public final class DesignerPane extends BorderPane implements AutoCloseable {
center.setBottom(properties);
output.setEditable(false);
output.setPrefRowCount(5);
setTop(bar);
Path origin = file.resolveSibling(file.getFileName() + ".origin");
setTop(
java.nio.file.Files.isRegularFile(origin)
? new VBox(
new Label("Read-only origin: " + java.nio.file.Files.readString(origin)), bar)
: bar);
setCenter(center);
setBottom(new VBox(status, output));
setBottom(new VBox(status, interactiveStatus, output));
source
.textProperty()
.addListener(
@@ -340,6 +445,7 @@ public final class DesignerPane extends BorderPane implements AutoCloseable {
private void load(DocumentSnapshot value) {
if (closed) return;
loading = true;
layoutPasses = 0;
canvasCommitQueued = false;
canvasGeneration++;
renderSequence.incrementAndGet();
@@ -543,7 +649,7 @@ public final class DesignerPane extends BorderPane implements AutoCloseable {
status.setText(
useLast ? "Discovering explicitly retained build…" : "Discovering successful build…");
workflow
.discover(useLast)
.examplesFor(useLast, file.toUri())
.whenComplete(
(entries, error) ->
fx(
@@ -601,8 +707,13 @@ public final class DesignerPane extends BorderPane implements AutoCloseable {
.filter(
d ->
d.document().toString().endsWith(".fxml")
&& Path.of(d.document())
.startsWith(launch.configuration().getParent()))
&& !Path.of(d.document())
.startsWith(
launch
.configuration()
.getParent()
.resolve(".neitzel-fx-designer"))
&& Path.of(d.document()).startsWith(reactorRoot))
.forEach(d -> drafts.put(d.document(), d));
drafts.put(
draft.document(),
@@ -640,7 +751,33 @@ public final class DesignerPane extends BorderPane implements AutoCloseable {
nodes.put(id, new DesignerWorkflow.Size(width, height));
region.updating();
}
if (nodes.isEmpty()) return;
if (interactiveRequested && interactiveRestart) {
interactiveRestart = false;
long request = ++interactiveRequest, changes = draftChanges.get();
interactiveStatus.setText("Starting interactive preview of current drafts…");
interactiveWorkflow
.interactive(last, example, drafts)
.whenComplete(
(value, error) ->
fx(
() -> {
if (!interactiveRequested || request != interactiveRequest) return;
if (error != null) {
interactiveStatus.setText(
"Interactive preview failed; use Interactive preview to retry");
report(error);
} else
interactiveStatus.setText(
changes == draftChanges.get()
? "Interactive preview running from captured drafts"
: "Interactive preview uses earlier drafts; rerun or save to"
+ " restart");
}));
}
if (nodes.isEmpty()) {
status.setText("No embedded Components; use Interactive preview for this View");
return;
}
workflow
.renderImages(last, example, nodes, drafts)
.whenComplete(
@@ -649,9 +786,18 @@ public final class DesignerPane extends BorderPane implements AutoCloseable {
() -> {
if (ticket != renderSequence.get()) return;
if (error != null) {
projection
.components()
.keySet()
.forEach(
id -> {
var region = proxy(id);
if (region != null) region.failed();
});
report(error);
return;
}
boolean measurementsChanged = false;
for (var frame : frames) {
int width = frame.width(), height = frame.height();
var image = new WritableImage(width, height);
@@ -676,8 +822,25 @@ public final class DesignerPane extends BorderPane implements AutoCloseable {
width);
images.put(frame.node(), image);
var region = proxy(frame.node());
if (region != null)
if (region != null) {
if (layoutPasses < 8)
measurementsChanged |= region.measurements(frame.metrics());
region.show(image, last ? "Explicit retained build" : "Target runtime");
}
}
layoutPasses = measurementsChanged ? layoutPasses + 1 : 0;
if (layoutPasses >= 8) {
resize.stop();
projection
.components()
.keySet()
.forEach(
id -> {
var region = proxy(id);
if (region != null) region.failed();
});
status.setText("Target layout did not stabilize; retaining last layout");
return;
}
status.setText(
"Rendered snapshot "
@@ -697,18 +860,62 @@ public final class DesignerPane extends BorderPane implements AutoCloseable {
.showAndWait()
.ifPresent(
id -> {
String type = projection.components().get(id);
workflow
.componentSource(type)
.thenCompose(child -> host.navigation().open(new HostServices.Location(child, 0)))
.exceptionally(
error -> {
fx(() -> report(error));
return null;
});
String type = projection.navigationTypes().get(id);
navigateComponent(type);
});
}
private void openRowComponent() {
if (projection == null || projection.rowComponents().isEmpty()) {
status.setText("No declared Row Component in this View");
return;
}
var rows = projection.rowComponents();
var choice = new ChoiceDialog<>(rows.iterator().next(), rows);
choice.setHeaderText("Open Row Component without changing its layout");
choice.showAndWait().ifPresent(this::navigateComponent);
}
private void navigateComponent(String type) {
host.navigation()
.componentSources(type)
.thenCompose(
attached -> {
if (attached.isEmpty()) return workflow.componentSources(type);
return java.util.concurrent.CompletableFuture.completedFuture(
attached.stream()
.map(
location ->
new DesignerWorkflow.ComponentSource(
location.document(), location.document().toString(), true))
.toList());
})
.whenComplete(
(sources, error) ->
fx(
() -> {
if (error != null) {
report(error);
return;
}
DesignerWorkflow.ComponentSource selected;
if (sources.size() == 1) selected = sources.getFirst();
else {
var choice = new ChoiceDialog<>(sources.getFirst(), sources);
choice.setHeaderText("Choose Component source origin");
selected = choice.showAndWait().orElse(null);
}
if (selected != null)
host.navigation()
.open(new HostServices.Location(selected.document(), 0))
.exceptionally(
failure -> {
fx(() -> report(failure));
return null;
});
}));
}
public CompletionStage<Void> flush() {
var result = new CompletableFuture<Void>();
flushInto(result, System.nanoTime() + TimeUnit.SECONDS.toNanos(5));
@@ -745,12 +952,14 @@ public final class DesignerPane extends BorderPane implements AutoCloseable {
closed = true;
try {
changes.close();
saves.close();
} catch (Exception ignored) {
}
poll.stop();
typing.stop();
resize.stop();
workflow.close();
interactiveWorkflow.close();
if (document != null) document.close();
}
}
@@ -61,6 +61,14 @@ public final class FxmlProjection {
private final Map<String, Element> sourceElements = new LinkedHashMap<>();
private final Set<String> synthetic = new HashSet<>();
private Document baseline;
private final Map<String, String> repeated = new LinkedHashMap<>();
public Map<String, String> navigationTypes() {
var result = new LinkedHashMap<>(components);
result.putAll(repeated);
return Map.copyOf(result);
}
private final Map<String, String> components = new LinkedHashMap<>();
public FxmlProjection(String text) throws Exception {
@@ -88,8 +96,28 @@ public final class FxmlProjection {
return Map.copyOf(components);
}
public Set<String> rowComponents() {
var result = new LinkedHashSet<String>();
var nodes = original.getElementsByTagName("*");
for (int i = 0; i < nodes.getLength(); i++) {
var node = (Element) nodes.item(i);
if (node.hasAttribute("row")) result.add(node.getAttribute("row"));
}
return Set.copyOf(result);
}
public String project() throws Exception {
components.clear();
repeated.clear();
var declarations = original.getElementsByTagName("*");
for (int i = 0; i < declarations.getLength(); i++) {
var element = (Element) declarations.item(i);
if ((element.getTagName().equals("Repeat")
|| element.getTagName().equals("de.neitzel.fx.view.Repeat"))
&& element.hasAttribute("pane")
&& element.hasAttribute("component"))
repeated.put(element.getAttribute("pane"), element.getAttribute("component"));
}
sourceNodes.clear();
sourceElements.clear();
synthetic.clear();
@@ -161,6 +189,8 @@ public final class FxmlProjection {
&& tag.contains(".")
&& Character.isUpperCase(tag.charAt(tag.lastIndexOf('.') + 1))
&& !tag.startsWith("javafx.")) componentType = tag;
if (componentType == null && repeated.containsKey(e.getAttributeNS(FX, "id")))
componentType = tag.contains(".") ? tag : "javafx.scene.layout." + tag;
if (componentType != null) {
String id = e.getAttributeNS(FX, "id");
components.put(id, componentType);
@@ -338,9 +368,12 @@ public final class FxmlProjection {
public String previewText() {
var edits = new ArrayList<de.neitzel.fxdesigner.contracts.EditBatch.Replacement>();
for (String id : synthetic)
if (components.containsKey(id))
edits.addAll(source.attributes(sourceNodes.get(id), Map.of("fx:id", id)));
for (String id : components.keySet()) {
var attributes = new LinkedHashMap<String, String>();
attributes.put("de.neitzel.fxdesigner.target.Occurrences.key", id);
if (synthetic.contains(id)) attributes.put("fx:id", id);
edits.addAll(source.attributes(sourceNodes.get(id), attributes));
}
if (!edits.isEmpty() && !original.getDocumentElement().hasAttribute("xmlns:fx"))
edits.addAll(source.attributes(0, Map.of("xmlns:fx", FX)));
return source.apply(edits);
@@ -23,6 +23,56 @@ public final class SnapshotRegion extends Region {
return alignment;
}
private de.neitzel.fxdesigner.protocol.LayoutMetrics metrics;
public boolean measurements(de.neitzel.fxdesigner.protocol.LayoutMetrics value) {
boolean changed =
metrics == null
|| Math.abs(metrics.prefWidth() - value.prefWidth()) > .5
|| Math.abs(metrics.prefHeight() - value.prefHeight()) > .5
|| Math.abs(metrics.minWidth() - value.minWidth()) > .5
|| Math.abs(metrics.minHeight() - value.minHeight()) > .5
|| Math.abs(metrics.maxWidth() - value.maxWidth()) > .5
|| Math.abs(metrics.maxHeight() - value.maxHeight()) > .5
|| Math.abs(metrics.baseline() - value.baseline()) > .5
|| !metrics.bias().equals(value.bias());
metrics = value;
if (changed) requestLayout();
return changed;
}
@Override
public javafx.geometry.Orientation getContentBias() {
return metrics == null || metrics.bias().equals("NONE")
? null
: javafx.geometry.Orientation.valueOf(metrics.bias());
}
@Override
public double getBaselineOffset() {
return metrics == null ? super.getBaselineOffset() : metrics.baseline();
}
protected double computeMinWidth(double height) {
return metrics == null ? 0 : metrics.minWidth();
}
protected double computeMinHeight(double width) {
return metrics == null ? 0 : metrics.minHeight();
}
protected double computeMaxWidth(double height) {
return metrics == null ? Double.MAX_VALUE : metrics.maxWidth();
}
protected double computeMaxHeight(double width) {
return metrics == null ? Double.MAX_VALUE : metrics.maxHeight();
}
public void failed() {
status.setText("Preview failed — retained image is stale; Retry with Render drafts");
}
private final ImageView pixels = new ImageView();
private final Label status = new Label("Render preview");
@@ -70,10 +120,10 @@ public final class SnapshotRegion extends Region {
}
protected double computePrefWidth(double height) {
return 300;
return metrics == null ? 300 : metrics.prefWidth();
}
protected double computePrefHeight(double width) {
return 220;
return metrics == null ? 220 : metrics.prefHeight();
}
}