Implement retained target sessions, runtime sizing and Component navigation
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user