Preserve FXML source ranges and extend shared Component projection
This commit is contained in:
@@ -0,0 +1,367 @@
|
||||
package de.neitzel.fxdesigner.kit;
|
||||
|
||||
import com.oracle.javafx.scenebuilder.kit.fxom.*;
|
||||
import de.neitzel.fxdesigner.contracts.*;
|
||||
import de.neitzel.fxdesigner.host.DocumentSession;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.*;
|
||||
import javafx.application.Platform;
|
||||
import javafx.scene.control.*;
|
||||
|
||||
/**
|
||||
* Identical live UI/document scenarios used by native standalone and the isolated IDE companion.
|
||||
*/
|
||||
public final class ProjectionHostChecks {
|
||||
private static <T> T fx(Callable<T> task) throws Exception {
|
||||
var f = new CompletableFuture<T>();
|
||||
Platform.runLater(
|
||||
() -> {
|
||||
try {
|
||||
f.complete(task.call());
|
||||
} catch (Throwable e) {
|
||||
f.completeExceptionally(e);
|
||||
}
|
||||
});
|
||||
return f.get(15, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
private static void await(Callable<Boolean> test) throws Exception {
|
||||
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(45);
|
||||
while (!test.call()) {
|
||||
if (System.nanoTime() > deadline) throw new AssertionError("Projection UI deadline");
|
||||
Thread.sleep(50);
|
||||
}
|
||||
}
|
||||
|
||||
private static void check(boolean ok, String message) {
|
||||
if (!ok) throw new AssertionError(message);
|
||||
}
|
||||
|
||||
private static TextArea source(DesignerPane pane) {
|
||||
return (TextArea)
|
||||
pane.lookupAll(".text-area").stream()
|
||||
.filter(n -> n instanceof TextArea t && t.isEditable())
|
||||
.findFirst()
|
||||
.orElseThrow();
|
||||
}
|
||||
|
||||
private static FXOMInstance object(DesignerPane pane, String id) {
|
||||
return (FXOMInstance) new FXOMFxIdIndex(pane.kit().editor().getFxomDocument()).lookup(id);
|
||||
}
|
||||
|
||||
private static void click(DesignerPane pane, String text) throws Exception {
|
||||
fx(
|
||||
() -> {
|
||||
((Button)
|
||||
pane.lookupAll(".button").stream()
|
||||
.filter(n -> n instanceof Button b && b.getText().equals(text))
|
||||
.findFirst()
|
||||
.orElseThrow())
|
||||
.fire();
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
private static void set(DocumentSession document, String text) throws Exception {
|
||||
var current = document.snapshot().toCompletableFuture().get();
|
||||
document
|
||||
.apply(
|
||||
new EditBatch(
|
||||
current.revision(),
|
||||
"Fixture source edit",
|
||||
List.of(new EditBatch.Replacement(0, current.text().length(), text))))
|
||||
.toCompletableFuture()
|
||||
.get();
|
||||
}
|
||||
|
||||
private static void mouse(
|
||||
javafx.scene.Node node,
|
||||
javafx.event.EventType<javafx.scene.input.MouseEvent> type,
|
||||
double dx,
|
||||
double dy,
|
||||
boolean down) {
|
||||
mouse(node, type, dx, dy, down, node);
|
||||
}
|
||||
|
||||
private static void mouse(
|
||||
javafx.scene.Node node,
|
||||
javafx.event.EventType<javafx.scene.input.MouseEvent> type,
|
||||
double dx,
|
||||
double dy,
|
||||
boolean down,
|
||||
javafx.scene.Node receiver) {
|
||||
var p =
|
||||
node.localToScene(
|
||||
node.getBoundsInLocal().getCenterX(), node.getBoundsInLocal().getCenterY(), true);
|
||||
var screen =
|
||||
node.localToScreen(
|
||||
node.getBoundsInLocal().getCenterX(), node.getBoundsInLocal().getCenterY());
|
||||
receiver.fireEvent(
|
||||
new javafx.scene.input.MouseEvent(
|
||||
type,
|
||||
p.getX() + dx,
|
||||
p.getY() + dy,
|
||||
screen.getX() + dx,
|
||||
screen.getY() + dy,
|
||||
javafx.scene.input.MouseButton.PRIMARY,
|
||||
1,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
down,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
type != javafx.scene.input.MouseEvent.MOUSE_DRAGGED,
|
||||
new javafx.scene.input.PickResult(node, p.getX() + dx, p.getY() + dy)));
|
||||
}
|
||||
|
||||
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.
|
||||
String nested =
|
||||
"<?import javafx.scene.layout.AnchorPane?>\n"
|
||||
+ original
|
||||
.replace(
|
||||
"<Card fx:id=\"second\"",
|
||||
"<javafx.scene.layout.AnchorPane prefHeight='260'><fixture.components.Card"
|
||||
+ " fx:id=\"second\" AnchorPane.leftAnchor='12' AnchorPane.topAnchor='8'")
|
||||
.replace(
|
||||
"caption=\"Second\"/>",
|
||||
"caption=\"Second\"/></javafx.scene.layout.AnchorPane>");
|
||||
check(!nested.equals(original), "Fixture must add nested Component");
|
||||
set(document, nested);
|
||||
await(() -> fx(() -> source(pane).getText().equals(nested) && object(pane, "second") != null));
|
||||
fx(
|
||||
() -> {
|
||||
pane.kit().editor().getSelection().select(object(pane, "second"));
|
||||
return null;
|
||||
});
|
||||
await(() -> fx(() -> pane.lookup("#component-property-caption") != null));
|
||||
fx(
|
||||
() -> {
|
||||
var text = source(pane);
|
||||
int at = text.getText().indexOf("path=\"home\"");
|
||||
text.selectRange(at, at + 11);
|
||||
var field = (TextField) pane.lookup("#component-property-caption");
|
||||
field.setText("Nested & selected");
|
||||
field.fireEvent(new javafx.event.ActionEvent());
|
||||
return null;
|
||||
});
|
||||
String changed = nested.replace("caption=\"Second\"", "caption=\"Nested & selected\"");
|
||||
await(() -> document.snapshot().toCompletableFuture().get().text().equals(changed));
|
||||
await(() -> fx(() -> source(pane).getText().equals(changed)));
|
||||
check(
|
||||
fx(() -> source(pane).getSelectedText().equals("path=\"home\"")),
|
||||
"Unaffected source selection lost");
|
||||
click(pane, "Undo");
|
||||
await(() -> document.snapshot().toCompletableFuture().get().text().equals(nested));
|
||||
click(pane, "Redo");
|
||||
await(() -> document.snapshot().toCompletableFuture().get().text().equals(changed));
|
||||
await(() -> fx(() -> source(pane).getText().equals(changed)));
|
||||
click(pane, "Source");
|
||||
click(pane, "Design");
|
||||
click(pane, "Split");
|
||||
check(fx(() -> source(pane).getText().equals(changed)), "Mode switch changed source");
|
||||
click(pane, "Render drafts");
|
||||
await(
|
||||
() ->
|
||||
fx(
|
||||
() ->
|
||||
object(pane, "second").getSceneGraphObject() instanceof SnapshotRegion r
|
||||
&& r.image() != null
|
||||
&& r.statusText().equals("Target runtime")));
|
||||
fx(
|
||||
() -> {
|
||||
var item = object(pane, "second");
|
||||
var node = (javafx.scene.Node) item.getSceneGraphObject();
|
||||
var point = node.localToScene(node.getBoundsInLocal().getCenterX(), 5, true);
|
||||
check(
|
||||
pane.content().pick(point.getX(), point.getY()) == item, "Native Component picking");
|
||||
pane.kit().editor().getSelection().select(item);
|
||||
pane.applyCss();
|
||||
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)));
|
||||
// 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(
|
||||
() -> {
|
||||
var item = object(pane, "second");
|
||||
pane.kit().editor().getSelection().select(item);
|
||||
pane.kit()
|
||||
.editor()
|
||||
.getJobManager()
|
||||
.push(
|
||||
new com.oracle.javafx.scenebuilder.kit.editor.job.RelocateSelectionJob(
|
||||
Map.of(item, new javafx.geometry.Point2D(42, 18)), pane.kit().editor()));
|
||||
return null;
|
||||
});
|
||||
await(() -> !document.snapshot().toCompletableFuture().get().text().equals(changed));
|
||||
check(
|
||||
document.snapshot().toCompletableFuture().get().text().contains("AnchorPane.leftAnchor"),
|
||||
"Attached constraints lost during move");
|
||||
click(pane, "Undo");
|
||||
await(() -> document.snapshot().toCompletableFuture().get().text().equals(changed));
|
||||
await(() -> fx(() -> source(pane).getText().equals(changed)));
|
||||
var snapshot = document.snapshot().toCompletableFuture().get();
|
||||
document.save(snapshot.revision()).toCompletableFuture().get();
|
||||
check(
|
||||
java.nio.file.Files.readString(java.nio.file.Path.of(snapshot.document())).equals(changed),
|
||||
"Save reformatted source");
|
||||
try (var reopened =
|
||||
pane.kit().host().documents().open(snapshot.document()).toCompletableFuture().get()) {
|
||||
check(
|
||||
reopened.snapshot().toCompletableFuture().get().text().equals(changed),
|
||||
"Reopen lost source");
|
||||
}
|
||||
String anonymous =
|
||||
changed.replace(
|
||||
"</javafx.scene.layout.AnchorPane>",
|
||||
"<fixture.components.Card caption='Anonymous'"
|
||||
+ " layoutY='240'/></javafx.scene.layout.AnchorPane>");
|
||||
var anonymousProjection = new FxmlProjection(anonymous);
|
||||
anonymousProjection.project();
|
||||
String anonymousId =
|
||||
anonymousProjection.components().keySet().stream()
|
||||
.filter(id -> id.startsWith("__designer_"))
|
||||
.findFirst()
|
||||
.orElseThrow();
|
||||
set(document, anonymous);
|
||||
await(
|
||||
() ->
|
||||
fx(
|
||||
() ->
|
||||
source(pane).getText().equals(anonymous) && object(pane, anonymousId) != null));
|
||||
click(pane, "Render drafts");
|
||||
await(
|
||||
() ->
|
||||
fx(
|
||||
() ->
|
||||
object(pane, anonymousId).getSceneGraphObject() instanceof SnapshotRegion r
|
||||
&& r.image() != null
|
||||
&& r.statusText().equals("Target runtime")));
|
||||
check(
|
||||
!document.snapshot().toCompletableFuture().get().text().contains("__designer_"),
|
||||
"Preview identity leaked into source");
|
||||
set(document, changed);
|
||||
await(() -> fx(() -> source(pane).getText().equals(changed)));
|
||||
// Incomplete source retains the old canvas, but blocks visual editing until recovery.
|
||||
set(document, changed + "<");
|
||||
await(() -> fx(() -> source(pane).getText().equals(changed + "<")));
|
||||
check(
|
||||
fx(() -> ((javafx.scene.Node) object(pane, "second").getSceneGraphObject()).isDisabled()),
|
||||
"Invalid source leaves canvas editable");
|
||||
set(document, original);
|
||||
await(
|
||||
() ->
|
||||
fx(
|
||||
() ->
|
||||
source(pane).getText().equals(original)
|
||||
&& !((javafx.scene.Node) object(pane, "second").getSceneGraphObject())
|
||||
.isDisabled()));
|
||||
// fx:root Component editing needs no application class in this host.
|
||||
String root =
|
||||
"<?import javafx.scene.layout.VBox?><?import javafx.scene.control.Label?><fx:root"
|
||||
+ " type='javafx.scene.layout.VBox' xmlns:fx='http://javafx.com/fxml/1'><Label"
|
||||
+ " text='root text'/></fx:root>";
|
||||
set(document, root);
|
||||
await(
|
||||
() ->
|
||||
fx(
|
||||
() ->
|
||||
source(pane).getText().equals(root)
|
||||
&& pane.kit()
|
||||
.editor()
|
||||
.getFxomDocument()
|
||||
.getFxmlText(false)
|
||||
.contains("root text")));
|
||||
fx(
|
||||
() -> {
|
||||
var item = object(pane, "__designer_1");
|
||||
pane.kit().editor().getSelection().select(item);
|
||||
return null;
|
||||
});
|
||||
await(
|
||||
() ->
|
||||
fx(
|
||||
() ->
|
||||
pane.lookupAll(".text-field").stream()
|
||||
.anyMatch(
|
||||
n -> n instanceof TextField t && t.getText().equals("root text"))));
|
||||
fx(
|
||||
() -> {
|
||||
var field =
|
||||
(TextField)
|
||||
pane.lookupAll(".text-field").stream()
|
||||
.filter(n -> n instanceof TextField t && t.getText().equals("root text"))
|
||||
.findFirst()
|
||||
.orElseThrow();
|
||||
field.setText("root changed");
|
||||
field.fireEvent(new javafx.event.ActionEvent());
|
||||
source(pane).appendText("<!-- queued source edit -->");
|
||||
return null;
|
||||
});
|
||||
await(
|
||||
() ->
|
||||
document
|
||||
.snapshot()
|
||||
.toCompletableFuture()
|
||||
.get()
|
||||
.text()
|
||||
.equals(root.replace("root text", "root changed") + "<!-- queued source edit -->"));
|
||||
set(document, original);
|
||||
await(() -> fx(() -> source(pane).getText().equals(original) && object(pane, "card") != null));
|
||||
document
|
||||
.save(document.snapshot().toCompletableFuture().get().revision())
|
||||
.toCompletableFuture()
|
||||
.get();
|
||||
click(pane, "Render drafts");
|
||||
await(
|
||||
() ->
|
||||
fx(
|
||||
() ->
|
||||
object(pane, "card").getSceneGraphObject() instanceof SnapshotRegion r
|
||||
&& r.image() != null
|
||||
&& r.statusText().equals("Target runtime")));
|
||||
System.out.println(
|
||||
"PASS shared projection: exact source, picking/resize/relocation, nested/FQ/anonymous"
|
||||
+ " Components, declared properties, selection, undo/redo, modes, target load,"
|
||||
+ " save/reopen, invalid recovery, fx:root");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user