Commit both corner-resize dimensions in one host transaction

This commit is contained in:
2026-11-12 12:27:04 +01:00
parent dfa2dd1508
commit b9d59e162f
5 changed files with 220 additions and 35 deletions
@@ -109,7 +109,8 @@ public final class DesignerPane extends BorderPane implements AutoCloseable {
private DocumentSnapshot draft;
private FxmlProjection projection;
private DesignerWorkflow workflow;
private boolean loading, closed, pending, last;
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;
@@ -205,6 +206,7 @@ public final class DesignerPane extends BorderPane implements AutoCloseable {
.addListener(
(o, a, b) -> {
if (!loading) {
flushCanvas();
if (pending && submittedSource == null) submittedSource = pendingBase;
commitSource();
workflow.cancel();
@@ -338,6 +340,8 @@ public final class DesignerPane extends BorderPane implements AutoCloseable {
private void load(DocumentSnapshot value) {
if (closed) return;
loading = true;
canvasCommitQueued = false;
canvasGeneration++;
renderSequence.incrementAndGet();
workflow.cancel();
try {
@@ -441,6 +445,28 @@ public final class DesignerPane extends BorderPane implements AutoCloseable {
}
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(
@@ -454,6 +480,7 @@ public final class DesignerPane extends BorderPane implements AutoCloseable {
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");
@@ -689,6 +716,7 @@ public final class DesignerPane extends BorderPane implements AutoCloseable {
}
private void flushInto(CompletableFuture<Void> result, long deadline) {
flushCanvas();
if (System.nanoTime() > deadline) {
result.completeExceptionally(
new IllegalStateException("Source edit could not be synchronized"));
@@ -118,6 +118,95 @@ public final class ProjectionHostChecks {
new javafx.scene.input.PickResult(node, p.getX() + dx, p.getY() + dy)));
}
public static void resizeCorner(DesignerPane pane, DocumentSession document) throws Exception {
String before = document.snapshot().toCompletableFuture().get().text();
fx(
() -> {
pane.kit().editor().getSelection().select(object(pane, "second"));
pane.applyCss();
pane.layout();
return null;
});
Thread.sleep(150);
double initialWidth =
fx(() -> ((SnapshotRegion) object(pane, "second").getSceneGraphObject()).getWidth());
double initialHeight =
fx(() -> ((SnapshotRegion) object(pane, "second").getSceneGraphObject()).getHeight());
fx(
() -> {
var handles = pane.content().lookupHandles(object(pane, "second"));
check(
handles
instanceof
com.oracle.javafx.scenebuilder.kit.editor.panel.content.driver.handles
.AbstractGenericHandles<
?>,
"Native resize handles missing");
var handle =
((com.oracle.javafx.scenebuilder.kit.editor.panel.content.driver.handles
.AbstractGenericHandles<
?>)
handles)
.getHandleNode(
com.oracle.javafx.scenebuilder.kit.editor.panel.content.util.CardinalPoint
.SE);
mouse(handle, javafx.scene.input.MouseEvent.MOUSE_PRESSED, 0, 0, true);
mouse(handle, javafx.scene.input.MouseEvent.DRAG_DETECTED, 20, 15, true);
mouse(handle, javafx.scene.input.MouseEvent.MOUSE_DRAGGED, 20, 15, true);
mouse(handle, javafx.scene.input.MouseEvent.MOUSE_RELEASED, 20, 15, false);
return null;
});
await(() -> !document.snapshot().toCompletableFuture().get().text().equals(before));
String resized = document.snapshot().toCompletableFuture().get().text();
check(
resized.contains("prefHeight=") && resized.contains("Card"),
"Resize must preserve real Component declaration");
var parser = javax.xml.parsers.DocumentBuilderFactory.newInstance();
parser.setNamespaceAware(true);
parser.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
var resizedXml =
parser
.newDocumentBuilder()
.parse(new org.xml.sax.InputSource(new java.io.StringReader(resized)));
var resizedNodes = resizedXml.getElementsByTagName("*");
org.w3c.dom.Element resizedCard = null;
for (int i = 0; i < resizedNodes.getLength(); i++) {
var candidate = (org.w3c.dom.Element) resizedNodes.item(i);
if (candidate.getAttributeNS("http://javafx.com/fxml/1", "id").equals("second"))
resizedCard = candidate;
}
String storedHeight = resizedCard.getAttribute("prefHeight");
check(
!storedHeight.isBlank() && Double.parseDouble(storedHeight) > initialHeight,
"Corner resize lost height: before="
+ initialHeight
+ " stored="
+ storedHeight
+ " source="
+ resized);
String storedWidth = resizedCard.getAttribute("prefWidth");
check(
!storedWidth.isBlank() && Double.parseDouble(storedWidth) > initialWidth,
"Corner resize lost width: before=" + initialWidth + " stored=" + storedWidth);
await(
() ->
fx(
() ->
source(pane).getText().equals(resized)
&& ((SnapshotRegion) object(pane, "second").getSceneGraphObject())
.getHeight()
> initialHeight));
click(pane, "Undo");
await(() -> document.snapshot().toCompletableFuture().get().text().equals(before));
await(() -> fx(() -> source(pane).getText().equals(before)));
click(pane, "Redo");
await(() -> document.snapshot().toCompletableFuture().get().text().equals(resized));
await(() -> fx(() -> source(pane).getText().equals(resized)));
click(pane, "Undo");
await(() -> document.snapshot().toCompletableFuture().get().text().equals(before));
await(() -> fx(() -> source(pane).getText().equals(before)));
}
public static void run(DesignerPane pane, DocumentSession document) throws Exception {
String original = document.snapshot().toCompletableFuture().get().text();
// The same real application, now with nested, fully-qualified Component declarations.
@@ -185,39 +274,7 @@ public final class ProjectionHostChecks {
pane.layout();
return null;
});
Thread.sleep(150);
fx(
() -> {
var handles = pane.content().lookupHandles(object(pane, "second"));
check(
handles
instanceof
com.oracle.javafx.scenebuilder.kit.editor.panel.content.driver.handles
.AbstractGenericHandles<
?>,
"Native resize handles missing");
var handle =
((com.oracle.javafx.scenebuilder.kit.editor.panel.content.driver.handles
.AbstractGenericHandles<
?>)
handles)
.getHandleNode(
com.oracle.javafx.scenebuilder.kit.editor.panel.content.util.CardinalPoint
.SE);
mouse(handle, javafx.scene.input.MouseEvent.MOUSE_PRESSED, 0, 0, true);
mouse(handle, javafx.scene.input.MouseEvent.DRAG_DETECTED, 20, 15, true);
mouse(handle, javafx.scene.input.MouseEvent.MOUSE_DRAGGED, 20, 15, true);
mouse(handle, javafx.scene.input.MouseEvent.MOUSE_RELEASED, 20, 15, false);
return null;
});
await(() -> !document.snapshot().toCompletableFuture().get().text().equals(changed));
String resized = document.snapshot().toCompletableFuture().get().text();
check(
resized.contains("prefHeight=") && resized.contains("fixture.components.Card"),
"Resize must preserve real Component declaration");
click(pane, "Undo");
await(() -> document.snapshot().toCompletableFuture().get().text().equals(changed));
await(() -> fx(() -> source(pane).getText().equals(changed)));
resizeCorner(pane, document);
// Exercise the exact Kit relocation command used after a canvas move. Native OS
// dragboard delivery is a separate manual/platform qualification, not synthesized here.
fx(
@@ -243,7 +300,11 @@ public final class ProjectionHostChecks {
document.save(snapshot.revision()).toCompletableFuture().get();
check(
java.nio.file.Files.readString(java.nio.file.Path.of(snapshot.document())).equals(changed),
"Save reformatted source");
"Save reformatted source: expected=["
+ changed
+ "] actual=["
+ java.nio.file.Files.readString(java.nio.file.Path.of(snapshot.document()))
+ "]");
try (var reopened =
pane.kit().host().documents().open(snapshot.document()).toCompletableFuture().get()) {
check(