966 lines
37 KiB
Java
966 lines
37 KiB
Java
package de.neitzel.fxdesigner.kit;
|
|
|
|
import com.oracle.javafx.scenebuilder.kit.editor.panel.content.ContentPanelController;
|
|
import com.oracle.javafx.scenebuilder.kit.editor.panel.hierarchy.HierarchyPanelController;
|
|
import com.oracle.javafx.scenebuilder.kit.editor.panel.inspector.InspectorPanelController;
|
|
import com.oracle.javafx.scenebuilder.kit.fxom.*;
|
|
import de.neitzel.fxdesigner.build.BuildRefresh;
|
|
import de.neitzel.fxdesigner.contracts.*;
|
|
import de.neitzel.fxdesigner.host.*;
|
|
import de.neitzel.fxdesigner.workflow.*;
|
|
import java.nio.file.*;
|
|
import java.util.*;
|
|
import java.util.concurrent.*;
|
|
import javafx.animation.*;
|
|
import javafx.application.Platform;
|
|
import javafx.scene.control.*;
|
|
import javafx.scene.image.*;
|
|
import javafx.scene.layout.*;
|
|
import javafx.util.Duration;
|
|
|
|
/** One shared Kit UI. Both hosts own the authoritative DocumentSession and native history. */
|
|
public final class DesignerPane extends BorderPane implements AutoCloseable {
|
|
private final HostServices host;
|
|
private final HostLaunch launch;
|
|
private final Path file;
|
|
private final KitView kit;
|
|
private final Label status = new Label("Opening document");
|
|
private final TextArea source = new TextArea(), output = new TextArea();
|
|
private final ComboBox<DesignerWorkflow.Example> examples = new ComboBox<>();
|
|
private final SplitPane design;
|
|
private final ContentPanelController content;
|
|
|
|
public ContentPanelController content() {
|
|
return content;
|
|
}
|
|
|
|
private final VBox componentInspector = new VBox(4);
|
|
|
|
private void componentSelection() {
|
|
componentInspector.getChildren().clear();
|
|
if (projection == null) return;
|
|
var hit = kit.editor().getSelection().getHitItem();
|
|
if (hit == null) return;
|
|
String id = hit.getFxId();
|
|
if (id == null || !projection.components().containsKey(id)) return;
|
|
componentInspector.getChildren().add(new Label(projection.components().get(id) + " · " + id));
|
|
componentInspector
|
|
.getChildren()
|
|
.add(new Label("Declared FXML properties (validated by target runtime)"));
|
|
projection
|
|
.componentAttributes(id)
|
|
.forEach(
|
|
(name, value) -> {
|
|
var field = new TextField(value);
|
|
field.setId("component-property-" + name);
|
|
field.setOnAction(
|
|
e -> {
|
|
try {
|
|
submit(
|
|
projection.componentAttribute(id, name, field.getText()),
|
|
"Edit Component " + name);
|
|
} catch (Exception error) {
|
|
report(error);
|
|
}
|
|
});
|
|
componentInspector.getChildren().add(new HBox(4, new Label(name), field));
|
|
});
|
|
}
|
|
|
|
private final Timeline poll;
|
|
private final PauseTransition typing = new PauseTransition(Duration.millis(250)),
|
|
resize = new PauseTransition(Duration.millis(350));
|
|
private String submittedSource;
|
|
private List<EditBatch.Replacement> pendingVisual;
|
|
private String pendingBase;
|
|
|
|
private static String apply(String text, List<EditBatch.Replacement> edits) {
|
|
return new EditBatch(0, "projection", edits)
|
|
.applyTo(
|
|
new DocumentSnapshot(java.net.URI.create("file:/projection"), 0, text, true, false));
|
|
}
|
|
|
|
private static List<EditBatch.Replacement> difference(String before, String after) {
|
|
int start = 0, end = before.length(), last = after.length();
|
|
while (start < end && start < last && before.charAt(start) == after.charAt(start)) start++;
|
|
while (end > start && last > start && before.charAt(end - 1) == after.charAt(last - 1)) {
|
|
end--;
|
|
last--;
|
|
}
|
|
return start == end && start == last
|
|
? List.of()
|
|
: List.of(new EditBatch.Replacement(start, end, after.substring(start, last)));
|
|
}
|
|
|
|
private static int mapped(int offset, List<EditBatch.Replacement> edits) {
|
|
int delta = 0;
|
|
var sorted = new ArrayList<>(edits);
|
|
sorted.sort(Comparator.comparingInt(EditBatch.Replacement::start));
|
|
for (var edit : sorted) {
|
|
if (offset < edit.start()) break;
|
|
if (offset <= edit.end())
|
|
return edit.start() + delta + Math.min(offset - edit.start(), edit.text().length());
|
|
delta += edit.text().length() - (edit.end() - edit.start());
|
|
}
|
|
return offset + delta;
|
|
}
|
|
|
|
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 =
|
|
new java.util.concurrent.atomic.AtomicLong();
|
|
private AutoCloseable changes;
|
|
private final Map<String, Image> images = new HashMap<>();
|
|
private BuildRefresh.Run buildRun;
|
|
|
|
public DesignerPane(HostServices host, HostLaunch launch, Path file) throws Exception {
|
|
this.host = host;
|
|
this.launch = launch;
|
|
this.file = file;
|
|
Thread.currentThread().setContextClassLoader(getClass().getClassLoader());
|
|
kit = new KitView(host);
|
|
content = new ContentPanelController(kit.editor());
|
|
design =
|
|
new SplitPane(
|
|
new HierarchyPanelController(kit.editor()).getPanelRoot(),
|
|
content.getPanelRoot(),
|
|
new InspectorPanelController(kit.editor()).getPanelRoot());
|
|
design.setDividerPositions(.18, .72);
|
|
kit.editor().getSelection().revisionProperty().addListener((o, a, b) -> componentSelection());
|
|
workflow =
|
|
new DesignerWorkflow(
|
|
host,
|
|
launch.configuration(),
|
|
launch.context(),
|
|
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())),
|
|
button(
|
|
"Cancel",
|
|
() -> {
|
|
if (buildRun != null) buildRun.cancel();
|
|
workflow.cancel();
|
|
renderSequence.incrementAndGet();
|
|
}),
|
|
examples,
|
|
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 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) {
|
|
return value == null ? "Select example" : value.label();
|
|
}
|
|
|
|
public DesignerWorkflow.Example fromString(String value) {
|
|
return null;
|
|
}
|
|
});
|
|
var split = new SplitPane(source, design);
|
|
split.setDividerPositions(.3);
|
|
var modes =
|
|
new ToolBar(
|
|
button("Source", () -> split.getItems().setAll(source)),
|
|
button("Design", () -> split.getItems().setAll(design)),
|
|
button(
|
|
"Split",
|
|
() -> {
|
|
split.getItems().setAll(source, design);
|
|
split.setDividerPositions(.3);
|
|
}));
|
|
var center = new BorderPane(split);
|
|
center.setTop(modes);
|
|
var properties = new ScrollPane(componentInspector);
|
|
properties.setFitToWidth(true);
|
|
properties.setPrefViewportHeight(100);
|
|
center.setBottom(properties);
|
|
output.setEditable(false);
|
|
output.setPrefRowCount(5);
|
|
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, interactiveStatus, output));
|
|
source
|
|
.textProperty()
|
|
.addListener(
|
|
(o, a, b) -> {
|
|
if (!loading) {
|
|
flushCanvas();
|
|
if (pending && submittedSource == null) submittedSource = pendingBase;
|
|
commitSource();
|
|
workflow.cancel();
|
|
renderSequence.incrementAndGet();
|
|
}
|
|
});
|
|
typing.setOnFinished(e -> commitSource());
|
|
resize.setOnFinished(e -> render());
|
|
poll =
|
|
new Timeline(
|
|
new KeyFrame(
|
|
Duration.millis(400),
|
|
e -> {
|
|
if (document != null
|
|
&& !pending
|
|
&& !typing.getStatus().equals(Animation.Status.RUNNING))
|
|
document
|
|
.snapshot()
|
|
.whenComplete(
|
|
(d, error) ->
|
|
fx(
|
|
() -> {
|
|
if (error != null) report(error);
|
|
else if (draft == null || d.revision() != draft.revision())
|
|
load(d);
|
|
}));
|
|
}));
|
|
poll.setCycleCount(Animation.INDEFINITE);
|
|
host.documents()
|
|
.open(file.toUri())
|
|
.whenComplete(
|
|
(d, error) ->
|
|
fx(
|
|
() -> {
|
|
if (error != null) {
|
|
report(error);
|
|
return;
|
|
}
|
|
if (closed) {
|
|
d.close();
|
|
return;
|
|
}
|
|
document = d;
|
|
d.snapshot().whenComplete(this::updated);
|
|
poll.play();
|
|
}));
|
|
}
|
|
|
|
private Button button(String text, Runnable action) {
|
|
var b = new Button(text);
|
|
b.setOnAction(
|
|
e -> {
|
|
try {
|
|
action.run();
|
|
} catch (Throwable error) {
|
|
report(error);
|
|
}
|
|
});
|
|
return b;
|
|
}
|
|
|
|
private void fx(Runnable action) {
|
|
Platform.runLater(
|
|
() -> {
|
|
if (!closed) action.run();
|
|
});
|
|
}
|
|
|
|
private void log(String text) {
|
|
fx(
|
|
() -> {
|
|
output.appendText(text + "\n");
|
|
if (output.getLength() > 60000) output.deleteText(0, output.getLength() - 50000);
|
|
});
|
|
}
|
|
|
|
private void report(Throwable error) {
|
|
host.diagnostics()
|
|
.report(
|
|
new HostServices.Diagnostic(
|
|
"Designer", HostServices.Severity.ERROR, String.valueOf(error), Optional.empty()));
|
|
status.setText("Designer: " + error.getMessage());
|
|
log(error.toString());
|
|
}
|
|
|
|
private void updated(DocumentSnapshot value, Throwable error) {
|
|
fx(
|
|
() -> {
|
|
pending = false;
|
|
if (error != null) {
|
|
submittedSource = null;
|
|
report(error);
|
|
return;
|
|
}
|
|
if (submittedSource != null && !source.getText().equals(submittedSource)) {
|
|
// Source typing can arrive before an asynchronous visual command completes.
|
|
// Rebase disjoint visual ranges; an overlapping later source edit wins explicitly
|
|
// in the next host command, whose Undo still restores the visual command.
|
|
if (submittedSource.equals(pendingBase) && pendingVisual != null) {
|
|
var typed = difference(pendingBase, source.getText());
|
|
var rebased = new ArrayList<EditBatch.Replacement>();
|
|
for (var visual : pendingVisual) {
|
|
if (typed.stream()
|
|
.noneMatch(t -> t.start() <= visual.end() && visual.start() <= t.end()))
|
|
rebased.add(
|
|
new EditBatch.Replacement(
|
|
mapped(visual.start(), typed),
|
|
mapped(visual.end(), typed),
|
|
visual.text()));
|
|
}
|
|
int anchor = source.getAnchor(), caret = source.getCaretPosition();
|
|
loading = true;
|
|
try {
|
|
rebased.sort(Comparator.comparingInt(EditBatch.Replacement::start).reversed());
|
|
for (var edit : rebased) source.replaceText(edit.start(), edit.end(), edit.text());
|
|
source.selectRange(mapped(anchor, rebased), mapped(caret, rebased));
|
|
} finally {
|
|
loading = false;
|
|
}
|
|
}
|
|
draft = value;
|
|
submittedSource = null;
|
|
commitSource();
|
|
return;
|
|
}
|
|
submittedSource = null;
|
|
load(value);
|
|
});
|
|
}
|
|
|
|
private void load(DocumentSnapshot value) {
|
|
if (closed) return;
|
|
loading = true;
|
|
layoutPasses = 0;
|
|
canvasCommitQueued = false;
|
|
canvasGeneration++;
|
|
renderSequence.incrementAndGet();
|
|
workflow.cancel();
|
|
try {
|
|
var hit = kit.editor().getSelection().getHitItem();
|
|
String selectedId = hit == null ? null : hit.getFxId();
|
|
draft = value;
|
|
if (!source.getText().equals(value.text())) {
|
|
int anchor = source.getAnchor(), caret = source.getCaretPosition();
|
|
var edits =
|
|
pendingVisual != null
|
|
&& source.getText().equals(pendingBase)
|
|
&& apply(pendingBase, pendingVisual).equals(value.text())
|
|
? pendingVisual
|
|
: difference(source.getText(), value.text());
|
|
var descending = new ArrayList<>(edits);
|
|
descending.sort(Comparator.comparingInt(EditBatch.Replacement::start).reversed());
|
|
for (var edit : descending) source.replaceText(edit.start(), edit.end(), edit.text());
|
|
source.selectRange(mapped(anchor, edits), mapped(caret, edits));
|
|
}
|
|
pendingVisual = null;
|
|
source.setEditable(value.writable());
|
|
// End an inspector editing session before replacing FXOM (also after native undo).
|
|
if (getScene() != null
|
|
&& getScene().getFocusOwner() instanceof TextInputControl text
|
|
&& text != source) text.getParent().requestFocus();
|
|
projection = new FxmlProjection(value.text());
|
|
String projected = projection.project();
|
|
kit.editor().setFxmlTextAndLocation(projected, file.toUri().toURL());
|
|
design.setDisable(!value.writable());
|
|
componentInspector.setDisable(!value.writable());
|
|
kit.editor()
|
|
.getFxomDocument()
|
|
.sceneGraphRevisionProperty()
|
|
.addListener((o, a, b) -> canvasChanged());
|
|
for (String id : projection.components().keySet()) {
|
|
var region = proxy(id);
|
|
if (region != null) {
|
|
region.show(
|
|
images.get(id), images.containsKey(id) ? "Previous preview" : "Render preview");
|
|
region
|
|
.widthProperty()
|
|
.addListener(
|
|
(o, a, b) -> {
|
|
if (!loading) resize.playFromStart();
|
|
});
|
|
region
|
|
.heightProperty()
|
|
.addListener(
|
|
(o, a, b) -> {
|
|
if (!loading) resize.playFromStart();
|
|
});
|
|
}
|
|
}
|
|
if (selectedId != null) {
|
|
var selected = new FXOMFxIdIndex(kit.editor().getFxomDocument()).lookup(selectedId);
|
|
if (selected != null) kit.editor().getSelection().select(selected);
|
|
}
|
|
componentSelection();
|
|
status.setText(
|
|
"Context "
|
|
+ launch.context()
|
|
+ " · document revision "
|
|
+ value.revision()
|
|
+ (value.dirty() ? " · unsaved" : ""));
|
|
} catch (Throwable error) {
|
|
design.setDisable(true);
|
|
componentInspector.setDisable(true);
|
|
projection = null;
|
|
report(error);
|
|
} finally {
|
|
loading = false;
|
|
}
|
|
}
|
|
|
|
private SnapshotRegion proxy(String id) {
|
|
var object = new FXOMFxIdIndex(kit.editor().getFxomDocument()).lookup(id);
|
|
return object instanceof FXOMInstance instance
|
|
&& instance.getSceneGraphObject() instanceof SnapshotRegion region
|
|
? region
|
|
: null;
|
|
}
|
|
|
|
private void replace(String text, String name) {
|
|
if (draft == null || pending) return;
|
|
submit(difference(draft.text(), text), name);
|
|
}
|
|
|
|
private void submit(List<EditBatch.Replacement> edits, String name) {
|
|
if (draft == null || pending || edits.isEmpty()) return;
|
|
pending = true;
|
|
pendingVisual = edits;
|
|
pendingBase = draft.text();
|
|
document.apply(new EditBatch(draft.revision(), name, edits)).whenComplete(this::updated);
|
|
}
|
|
|
|
private void commitSource() {
|
|
if (draft != null && !pending && !source.getText().equals(draft.text())) {
|
|
submittedSource = source.getText();
|
|
replace(submittedSource, "Edit FXML source");
|
|
}
|
|
}
|
|
|
|
private void canvasChanged() {
|
|
if (loading || pending || closed || canvasCommitQueued) return;
|
|
// Kit's corner-resize gesture pushes one job per dimension on the same FX event turn.
|
|
// Capture the completed gesture, not the intermediate width-only document.
|
|
canvasCommitQueued = true;
|
|
long generation = ++canvasGeneration;
|
|
var projectedDocument = kit.editor().getFxomDocument();
|
|
var expectedDraft = draft;
|
|
Platform.runLater(
|
|
() -> {
|
|
if (generation != canvasGeneration || closed) return;
|
|
if (projectedDocument != kit.editor().getFxomDocument() || draft != expectedDraft) {
|
|
canvasCommitQueued = false;
|
|
return;
|
|
}
|
|
flushCanvas();
|
|
});
|
|
}
|
|
|
|
private void flushCanvas() {
|
|
if (!canvasCommitQueued) return;
|
|
canvasCommitQueued = false;
|
|
canvasGeneration++;
|
|
if (loading || pending || closed) return;
|
|
try {
|
|
submit(
|
|
projection.edits(kit.editor().getFxomDocument().getFxmlText(false)),
|
|
"Component layout / property edit");
|
|
} catch (Throwable error) {
|
|
report(error);
|
|
load(draft);
|
|
}
|
|
}
|
|
|
|
private void withDraft(Runnable action) {
|
|
if (document == null) return;
|
|
flushCanvas();
|
|
if (pending || !source.getText().equals(draft.text())) {
|
|
commitSource();
|
|
status.setText("Finishing source edit; repeat the action when synchronized");
|
|
return;
|
|
}
|
|
action.run();
|
|
}
|
|
|
|
private void history(boolean redo) {
|
|
withDraft(() -> (redo ? document.redo() : document.undo()).whenComplete(this::updated));
|
|
}
|
|
|
|
private void build(Map<java.net.URI, Long> approvals) {
|
|
withDraft(
|
|
() -> {
|
|
renderSequence.incrementAndGet();
|
|
status.setText("Building selected context…");
|
|
buildRun = workflow.build(approvals);
|
|
buildRun
|
|
.completion()
|
|
.whenComplete(
|
|
(result, error) ->
|
|
fx(
|
|
() -> {
|
|
if (error != null) {
|
|
report(error);
|
|
return;
|
|
}
|
|
status.setText(result.message());
|
|
if (result.status() == BuildRefresh.Status.NEEDS_SAVE) {
|
|
var alert =
|
|
new Alert(
|
|
Alert.AlertType.CONFIRMATION,
|
|
"Save these build inputs?\n"
|
|
+ String.join(
|
|
"\n",
|
|
result.unsaved().stream()
|
|
.map(d -> d.document().toString())
|
|
.toList()),
|
|
ButtonType.OK,
|
|
ButtonType.CANCEL);
|
|
alert.setHeaderText("Save affected files and build");
|
|
if (alert.showAndWait().orElse(ButtonType.CANCEL) == ButtonType.OK) {
|
|
var approved = new HashMap<java.net.URI, Long>();
|
|
result
|
|
.unsaved()
|
|
.forEach(d -> approved.put(d.document(), d.revision()));
|
|
build(approved);
|
|
}
|
|
} else if (result.status() == BuildRefresh.Status.READY)
|
|
discover(false);
|
|
}));
|
|
});
|
|
}
|
|
|
|
public void discover(boolean useLast) {
|
|
withDraft(
|
|
() -> {
|
|
last = useLast;
|
|
status.setText(
|
|
useLast ? "Discovering explicitly retained build…" : "Discovering successful build…");
|
|
workflow
|
|
.examplesFor(useLast, file.toUri())
|
|
.whenComplete(
|
|
(entries, error) ->
|
|
fx(
|
|
() -> {
|
|
if (error != null) {
|
|
report(error);
|
|
return;
|
|
}
|
|
examples.getItems().setAll(entries);
|
|
if (!entries.isEmpty()) {
|
|
String preferred =
|
|
de.neitzel.fxdesigner.config.ConfigurationFiles.load(
|
|
launch.configuration())
|
|
.contexts()
|
|
.get(launch.context())
|
|
.preview()
|
|
.defaultExample();
|
|
examples
|
|
.getSelectionModel()
|
|
.select(
|
|
entries.stream()
|
|
.filter(e -> e.id().equals(preferred))
|
|
.findFirst()
|
|
.orElse(entries.getFirst()));
|
|
}
|
|
status.setText(
|
|
(last ? "Outdated build explicitly selected · " : "")
|
|
+ entries.size()
|
|
+ " examples");
|
|
render();
|
|
}));
|
|
});
|
|
}
|
|
|
|
public void render() {
|
|
withDraft(
|
|
() -> {
|
|
var selected = examples.getValue();
|
|
if (selected == null || projection == null) return;
|
|
long ticket = renderSequence.incrementAndGet();
|
|
host.documents()
|
|
.snapshots()
|
|
.whenComplete(
|
|
(dirty, error) ->
|
|
fx(
|
|
() -> {
|
|
if (error != null) {
|
|
report(error);
|
|
return;
|
|
}
|
|
if (ticket != renderSequence.get()) return;
|
|
var drafts = new LinkedHashMap<java.net.URI, DocumentSnapshot>();
|
|
drafts.put(draft.document(), draft);
|
|
dirty.stream()
|
|
.filter(
|
|
d ->
|
|
d.document().toString().endsWith(".fxml")
|
|
&& !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(),
|
|
new DocumentSnapshot(
|
|
draft.document(),
|
|
draft.revision(),
|
|
projection.previewText(),
|
|
draft.writable(),
|
|
draft.dirty()));
|
|
renderComponents(List.copyOf(drafts.values()), selected.id(), ticket);
|
|
}));
|
|
});
|
|
}
|
|
|
|
private void renderComponents(List<DocumentSnapshot> drafts, String example, long ticket) {
|
|
if (ticket != renderSequence.get() || closed) return;
|
|
var nodes = new LinkedHashMap<String, DesignerWorkflow.Size>();
|
|
for (String id : projection.components().keySet()) {
|
|
var region = proxy(id);
|
|
if (region == null) continue;
|
|
int
|
|
width =
|
|
(int)
|
|
Math.max(
|
|
1,
|
|
Math.min(
|
|
1600, region.getWidth() > 0 ? region.getWidth() : region.prefWidth(-1))),
|
|
height =
|
|
(int)
|
|
Math.max(
|
|
1,
|
|
Math.min(
|
|
1200,
|
|
region.getHeight() > 0 ? region.getHeight() : region.prefHeight(-1)));
|
|
nodes.put(id, new DesignerWorkflow.Size(width, height));
|
|
region.updating();
|
|
}
|
|
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(
|
|
(frames, error) ->
|
|
fx(
|
|
() -> {
|
|
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);
|
|
byte[] rgba = frame.rgba();
|
|
int[] argb = new int[width * height];
|
|
for (int i = 0; i < argb.length; i++)
|
|
argb[i] =
|
|
((rgba[i * 4 + 3] & 255) << 24)
|
|
| ((rgba[i * 4] & 255) << 16)
|
|
| ((rgba[i * 4 + 1] & 255) << 8)
|
|
| (rgba[i * 4 + 2] & 255);
|
|
image
|
|
.getPixelWriter()
|
|
.setPixels(
|
|
0,
|
|
0,
|
|
width,
|
|
height,
|
|
PixelFormat.getIntArgbInstance(),
|
|
argb,
|
|
0,
|
|
width);
|
|
images.put(frame.node(), image);
|
|
var region = proxy(frame.node());
|
|
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 "
|
|
+ frames.getFirst().snapshot()
|
|
+ " · revision "
|
|
+ draft.revision());
|
|
}));
|
|
}
|
|
|
|
private void openComponent() {
|
|
if (projection == null || projection.components().isEmpty()) return;
|
|
var choice =
|
|
new ChoiceDialog<>(
|
|
projection.components().keySet().iterator().next(), projection.components().keySet());
|
|
choice.setHeaderText("Open Component in a separate editor");
|
|
choice
|
|
.showAndWait()
|
|
.ifPresent(
|
|
id -> {
|
|
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));
|
|
return result;
|
|
}
|
|
|
|
private void flushInto(CompletableFuture<Void> result, long deadline) {
|
|
flushCanvas();
|
|
if (System.nanoTime() > deadline) {
|
|
result.completeExceptionally(
|
|
new IllegalStateException("Source edit could not be synchronized"));
|
|
return;
|
|
}
|
|
if (closed) {
|
|
result.completeExceptionally(new IllegalStateException("Editor closed"));
|
|
return;
|
|
}
|
|
if (!pending && draft != null && source.getText().equals(draft.text())) {
|
|
result.complete(null);
|
|
return;
|
|
}
|
|
commitSource();
|
|
var wait = new PauseTransition(Duration.millis(50));
|
|
wait.setOnFinished(e -> flushInto(result, deadline));
|
|
wait.play();
|
|
}
|
|
|
|
public KitView kit() {
|
|
return kit;
|
|
}
|
|
|
|
public void close() {
|
|
if (closed) return;
|
|
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();
|
|
}
|
|
}
|