Files
NeitzelFXDesigner/designer-kit-ui/src/main/java/de/neitzel/fxdesigner/kit/DesignerPane.java
T

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();
}
}