Preserve FXML source ranges and extend shared Component projection
This commit is contained in:
@@ -6,7 +6,7 @@ Shared Scene Builder Kit designer for NeitzelLib FX, with minimal standalone and
|
|||||||
|
|
||||||
The first workflow is runnable in both hosts: Build and Refresh publishes immutable snapshots; authenticated target workers discover examples and render parent/Component FXML drafts into the shared Kit canvas. Source and supported canvas edits use host-owned documents and undo/redo. See [the connected workflow](docs/first-connected-workflow.md) for architecture, launch commands, evidence and limits.
|
The first workflow is runnable in both hosts: Build and Refresh publishes immutable snapshots; authenticated target workers discover examples and render parent/Component FXML drafts into the shared Kit canvas. Source and supported canvas edits use host-owned documents and undo/redo. See [the connected workflow](docs/first-connected-workflow.md) for architecture, launch commands, evidence and limits.
|
||||||
|
|
||||||
The repository builds six shared Maven modules, a Maven capture plugin and a separate IntelliJ adapter plugin. Application code runs only in target JVMs. The minimal UI supports scalar/layout projection; full binding/generation editing, structural editing and product packaging remain later work.
|
The repository builds six shared Maven modules, a Maven capture plugin and a separate IntelliJ adapter plugin. Application code runs only in target JVMs. The shared UI now applies [source-preserving edits](docs/source-preserving-projection.md), supports nested/anonymous Components and fx:root bodies, and provides Source/Design/Split modes. Full binding/generation editing, structural editing and product packaging remain later work.
|
||||||
|
|
||||||
Configuration 1.0 schemas, context selection, local JDK mappings and draft protocol validation are now executable. See [configuration and protocol](docs/configuration-and-protocol.md) for formats and validation rules.
|
Configuration 1.0 schemas, context selection, local JDK mappings and draft protocol validation are now executable. See [configuration and protocol](docs/configuration-and-protocol.md) for formats and validation rules.
|
||||||
|
|
||||||
@@ -50,7 +50,7 @@ These graphical checks require a macOS desktop session; other platforms are not
|
|||||||
## Implementation references
|
## Implementation references
|
||||||
|
|
||||||
- [Initial project contract](docs/initial-project-contract.md): module responsibilities, configuration, snapshots, runtimes, preview sessions and communication.
|
- [Initial project contract](docs/initial-project-contract.md): module responsibilities, configuration, snapshots, runtimes, preview sessions and communication.
|
||||||
- [Repository bootstrap plan](docs/repository-bootstrap-plan.md): ordered work packages and acceptance gates. Bootstrap work packages 1–7 are implemented; next is authoritative document editing and generalizing the Kit projection.
|
- [Repository bootstrap plan](docs/repository-bootstrap-plan.md): ordered work packages and acceptance gates. Bootstrap work packages 1–7 are implemented; source-preserving projection is implemented, with physical drag/drop still requiring manual qualification. Next implementation stage: target-runtime sessions, sizing and navigation.
|
||||||
- [Decision provenance](docs/provenance.md): workspace planning and captured prototype evidence.
|
- [Decision provenance](docs/provenance.md): workspace planning and captured prototype evidence.
|
||||||
- [Third-party notices](THIRD-PARTY-NOTICES.md): Maven wrapper and patched Kit source provenance.
|
- [Third-party notices](THIRD-PARTY-NOTICES.md): Maven wrapper and patched Kit source provenance.
|
||||||
|
|
||||||
|
|||||||
+21
-1
@@ -149,6 +149,21 @@ public final class IntellijDocuments implements HostServices.Documents, AutoClos
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void refreshClean(State s) throws java.io.IOException {
|
||||||
|
var manager = FileDocumentManager.getInstance();
|
||||||
|
if (manager.isDocumentUnsaved(s.document)) return;
|
||||||
|
String disk = Files.readString(Path.of(s.file.getPath()));
|
||||||
|
if (disk.equals(s.document.getText())) {
|
||||||
|
s.disk = disk; // Native autosave already persisted this revision; do not reload it.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!disk.equals(s.disk)) {
|
||||||
|
s.file.refresh(false, false);
|
||||||
|
manager.reloadFromDisk(s.document);
|
||||||
|
s.disk = disk;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private DocumentSnapshot snapshot(State s) {
|
private DocumentSnapshot snapshot(State s) {
|
||||||
return new DocumentSnapshot(
|
return new DocumentSnapshot(
|
||||||
canonicalUri(s.file),
|
canonicalUri(s.file),
|
||||||
@@ -176,12 +191,17 @@ public final class IntellijDocuments implements HostServices.Documents, AutoClos
|
|||||||
}
|
}
|
||||||
|
|
||||||
public CompletionStage<DocumentSnapshot> snapshot() {
|
public CompletionStage<DocumentSnapshot> snapshot() {
|
||||||
return run(() -> IntellijDocuments.this.snapshot(s));
|
return run(
|
||||||
|
() -> {
|
||||||
|
refreshClean(s);
|
||||||
|
return IntellijDocuments.this.snapshot(s);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public CompletionStage<DocumentSnapshot> apply(EditBatch edits) {
|
public CompletionStage<DocumentSnapshot> apply(EditBatch edits) {
|
||||||
return run(
|
return run(
|
||||||
() -> {
|
() -> {
|
||||||
|
refreshClean(s);
|
||||||
edits.applyTo(IntellijDocuments.this.snapshot(s));
|
edits.applyTo(IntellijDocuments.this.snapshot(s));
|
||||||
var ranges = new ArrayList<>(edits.replacements());
|
var ranges = new ArrayList<>(edits.replacements());
|
||||||
ranges.sort(Comparator.comparingInt(EditBatch.Replacement::start));
|
ranges.sort(Comparator.comparingInt(EditBatch.Replacement::start));
|
||||||
|
|||||||
+50
@@ -109,4 +109,54 @@ public final class IntellijDocumentsTest extends BasePlatformTestCase {
|
|||||||
Files.deleteIfExists(directory);
|
Files.deleteIfExists(directory);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void testCleanExternalReloadAndRangeCaretPreservation() throws Exception {
|
||||||
|
Path file = Files.createTempFile("designer-reload-", ".fxml");
|
||||||
|
Files.writeString(file, "prefix value suffix");
|
||||||
|
com.intellij.openapi.vfs.newvfs.impl.VfsRootAccess.allowRootAccess(
|
||||||
|
getTestRootDisposable(), file.toRealPath().toString());
|
||||||
|
try (var docs = new IntellijDocuments(getProject());
|
||||||
|
var handle = docs.open(file.toUri()).toCompletableFuture().get()) {
|
||||||
|
var initial = handle.snapshot().toCompletableFuture().get();
|
||||||
|
Files.writeString(file, "prefix external suffix");
|
||||||
|
var current = handle.snapshot().toCompletableFuture().get();
|
||||||
|
assertEquals("prefix external suffix", current.text());
|
||||||
|
assertFalse(current.dirty());
|
||||||
|
assertTrue(initial.revision() != current.revision());
|
||||||
|
var vf =
|
||||||
|
com.intellij.openapi.vfs.LocalFileSystem.getInstance().refreshAndFindFileByNioFile(file);
|
||||||
|
var document =
|
||||||
|
com.intellij.openapi.fileEditor.FileDocumentManager.getInstance().getDocument(vf);
|
||||||
|
var editor =
|
||||||
|
com.intellij.openapi.editor.EditorFactory.getInstance()
|
||||||
|
.createEditor(document, getProject());
|
||||||
|
try {
|
||||||
|
editor.getCaretModel().moveToOffset(current.text().length());
|
||||||
|
editor
|
||||||
|
.getSelectionModel()
|
||||||
|
.setSelection(current.text().indexOf("suffix"), current.text().length());
|
||||||
|
var updated =
|
||||||
|
handle
|
||||||
|
.apply(
|
||||||
|
new EditBatch(
|
||||||
|
current.revision(),
|
||||||
|
"Range edit",
|
||||||
|
List.of(new EditBatch.Replacement(7, 15, "X"))))
|
||||||
|
.toCompletableFuture()
|
||||||
|
.get();
|
||||||
|
assertEquals("prefix X suffix", updated.text());
|
||||||
|
assertEquals("suffix", editor.getSelectionModel().getSelectedText());
|
||||||
|
assertEquals(updated.text().length(), editor.getCaretModel().getOffset());
|
||||||
|
com.intellij.openapi.fileEditor.FileDocumentManager.getInstance().saveDocument(document);
|
||||||
|
var saved = handle.snapshot().toCompletableFuture().get();
|
||||||
|
assertEquals(updated.revision(), saved.revision());
|
||||||
|
assertFalse(saved.dirty());
|
||||||
|
handle.save(updated.revision()).toCompletableFuture().get();
|
||||||
|
} finally {
|
||||||
|
com.intellij.openapi.editor.EditorFactory.getInstance().releaseEditor(editor);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
Files.deleteIfExists(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,18 @@
|
|||||||
</dependencies>
|
</dependencies>
|
||||||
<build>
|
<build>
|
||||||
<plugins>
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-jar-plugin</artifactId>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<goals><goal>test-jar</goal></goals>
|
||||||
|
<configuration>
|
||||||
|
<includes><include>**/ProjectionHostChecks*.class</include></includes>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
<plugin>
|
<plugin>
|
||||||
<groupId>org.apache.maven.plugins</groupId>
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
<artifactId>maven-enforcer-plugin</artifactId>
|
<artifactId>maven-enforcer-plugin</artifactId>
|
||||||
|
|||||||
@@ -28,10 +28,83 @@ public final class DesignerPane extends BorderPane implements AutoCloseable {
|
|||||||
private final TextArea source = new TextArea(), output = new TextArea();
|
private final TextArea source = new TextArea(), output = new TextArea();
|
||||||
private final ComboBox<DesignerWorkflow.Example> examples = new ComboBox<>();
|
private final ComboBox<DesignerWorkflow.Example> examples = new ComboBox<>();
|
||||||
private final SplitPane design;
|
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 Timeline poll;
|
||||||
private final PauseTransition typing = new PauseTransition(Duration.millis(250)),
|
private final PauseTransition typing = new PauseTransition(Duration.millis(250)),
|
||||||
resize = new PauseTransition(Duration.millis(350));
|
resize = new PauseTransition(Duration.millis(350));
|
||||||
private String submittedSource;
|
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 DocumentSession document;
|
||||||
private DocumentSnapshot draft;
|
private DocumentSnapshot draft;
|
||||||
private FxmlProjection projection;
|
private FxmlProjection projection;
|
||||||
@@ -49,12 +122,14 @@ public final class DesignerPane extends BorderPane implements AutoCloseable {
|
|||||||
this.file = file;
|
this.file = file;
|
||||||
Thread.currentThread().setContextClassLoader(getClass().getClassLoader());
|
Thread.currentThread().setContextClassLoader(getClass().getClassLoader());
|
||||||
kit = new KitView(host);
|
kit = new KitView(host);
|
||||||
|
content = new ContentPanelController(kit.editor());
|
||||||
design =
|
design =
|
||||||
new SplitPane(
|
new SplitPane(
|
||||||
new HierarchyPanelController(kit.editor()).getPanelRoot(),
|
new HierarchyPanelController(kit.editor()).getPanelRoot(),
|
||||||
new ContentPanelController(kit.editor()).getPanelRoot(),
|
content.getPanelRoot(),
|
||||||
new InspectorPanelController(kit.editor()).getPanelRoot());
|
new InspectorPanelController(kit.editor()).getPanelRoot());
|
||||||
design.setDividerPositions(.18, .72);
|
design.setDividerPositions(.18, .72);
|
||||||
|
kit.editor().getSelection().revisionProperty().addListener((o, a, b) -> componentSelection());
|
||||||
workflow =
|
workflow =
|
||||||
new DesignerWorkflow(
|
new DesignerWorkflow(
|
||||||
host,
|
host,
|
||||||
@@ -102,21 +177,35 @@ public final class DesignerPane extends BorderPane implements AutoCloseable {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
var tabs = new TabPane();
|
|
||||||
var split = new SplitPane(source, design);
|
var split = new SplitPane(source, design);
|
||||||
split.setDividerPositions(.3);
|
split.setDividerPositions(.3);
|
||||||
tabs.getTabs().add(new Tab("Source / Design", split));
|
var modes =
|
||||||
tabs.getTabs().getFirst().setClosable(false);
|
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.setEditable(false);
|
||||||
output.setPrefRowCount(5);
|
output.setPrefRowCount(5);
|
||||||
setTop(bar);
|
setTop(bar);
|
||||||
setCenter(tabs);
|
setCenter(center);
|
||||||
setBottom(new VBox(status, output));
|
setBottom(new VBox(status, output));
|
||||||
source
|
source
|
||||||
.textProperty()
|
.textProperty()
|
||||||
.addListener(
|
.addListener(
|
||||||
(o, a, b) -> {
|
(o, a, b) -> {
|
||||||
if (!loading) {
|
if (!loading) {
|
||||||
|
if (pending && submittedSource == null) submittedSource = pendingBase;
|
||||||
commitSource();
|
commitSource();
|
||||||
workflow.cancel();
|
workflow.cancel();
|
||||||
renderSequence.incrementAndGet();
|
renderSequence.incrementAndGet();
|
||||||
@@ -211,6 +300,31 @@ public final class DesignerPane extends BorderPane implements AutoCloseable {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (submittedSource != null && !source.getText().equals(submittedSource)) {
|
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;
|
draft = value;
|
||||||
submittedSource = null;
|
submittedSource = null;
|
||||||
commitSource();
|
commitSource();
|
||||||
@@ -227,8 +341,23 @@ public final class DesignerPane extends BorderPane implements AutoCloseable {
|
|||||||
renderSequence.incrementAndGet();
|
renderSequence.incrementAndGet();
|
||||||
workflow.cancel();
|
workflow.cancel();
|
||||||
try {
|
try {
|
||||||
|
var hit = kit.editor().getSelection().getHitItem();
|
||||||
|
String selectedId = hit == null ? null : hit.getFxId();
|
||||||
draft = value;
|
draft = value;
|
||||||
if (!source.getText().equals(value.text())) source.setText(value.text());
|
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());
|
source.setEditable(value.writable());
|
||||||
// End an inspector editing session before replacing FXOM (also after native undo).
|
// End an inspector editing session before replacing FXOM (also after native undo).
|
||||||
if (getScene() != null
|
if (getScene() != null
|
||||||
@@ -238,6 +367,7 @@ public final class DesignerPane extends BorderPane implements AutoCloseable {
|
|||||||
String projected = projection.project();
|
String projected = projection.project();
|
||||||
kit.editor().setFxmlTextAndLocation(projected, file.toUri().toURL());
|
kit.editor().setFxmlTextAndLocation(projected, file.toUri().toURL());
|
||||||
design.setDisable(!value.writable());
|
design.setDisable(!value.writable());
|
||||||
|
componentInspector.setDisable(!value.writable());
|
||||||
kit.editor()
|
kit.editor()
|
||||||
.getFxomDocument()
|
.getFxomDocument()
|
||||||
.sceneGraphRevisionProperty()
|
.sceneGraphRevisionProperty()
|
||||||
@@ -261,6 +391,11 @@ public final class DesignerPane extends BorderPane implements AutoCloseable {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (selectedId != null) {
|
||||||
|
var selected = new FXOMFxIdIndex(kit.editor().getFxomDocument()).lookup(selectedId);
|
||||||
|
if (selected != null) kit.editor().getSelection().select(selected);
|
||||||
|
}
|
||||||
|
componentSelection();
|
||||||
status.setText(
|
status.setText(
|
||||||
"Context "
|
"Context "
|
||||||
+ launch.context()
|
+ launch.context()
|
||||||
@@ -269,6 +404,8 @@ public final class DesignerPane extends BorderPane implements AutoCloseable {
|
|||||||
+ (value.dirty() ? " · unsaved" : ""));
|
+ (value.dirty() ? " · unsaved" : ""));
|
||||||
} catch (Throwable error) {
|
} catch (Throwable error) {
|
||||||
design.setDisable(true);
|
design.setDisable(true);
|
||||||
|
componentInspector.setDisable(true);
|
||||||
|
projection = null;
|
||||||
report(error);
|
report(error);
|
||||||
} finally {
|
} finally {
|
||||||
loading = false;
|
loading = false;
|
||||||
@@ -285,14 +422,15 @@ public final class DesignerPane extends BorderPane implements AutoCloseable {
|
|||||||
|
|
||||||
private void replace(String text, String name) {
|
private void replace(String text, String name) {
|
||||||
if (draft == null || pending) return;
|
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;
|
pending = true;
|
||||||
document
|
pendingVisual = edits;
|
||||||
.apply(
|
pendingBase = draft.text();
|
||||||
new EditBatch(
|
document.apply(new EditBatch(draft.revision(), name, edits)).whenComplete(this::updated);
|
||||||
draft.revision(),
|
|
||||||
name,
|
|
||||||
List.of(new EditBatch.Replacement(0, draft.text().length(), text))))
|
|
||||||
.whenComplete(this::updated);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void commitSource() {
|
private void commitSource() {
|
||||||
@@ -305,8 +443,8 @@ public final class DesignerPane extends BorderPane implements AutoCloseable {
|
|||||||
private void canvasChanged() {
|
private void canvasChanged() {
|
||||||
if (loading || pending || closed) return;
|
if (loading || pending || closed) return;
|
||||||
try {
|
try {
|
||||||
replace(
|
submit(
|
||||||
projection.restore(kit.editor().getFxomDocument().getFxmlText(false)),
|
projection.edits(kit.editor().getFxomDocument().getFxmlText(false)),
|
||||||
"Component layout / property edit");
|
"Component layout / property edit");
|
||||||
} catch (Throwable error) {
|
} catch (Throwable error) {
|
||||||
report(error);
|
report(error);
|
||||||
@@ -439,6 +577,14 @@ public final class DesignerPane extends BorderPane implements AutoCloseable {
|
|||||||
&& Path.of(d.document())
|
&& Path.of(d.document())
|
||||||
.startsWith(launch.configuration().getParent()))
|
.startsWith(launch.configuration().getParent()))
|
||||||
.forEach(d -> drafts.put(d.document(), d));
|
.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);
|
renderComponents(List.copyOf(drafts.values()), selected.id(), ticket);
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -36,13 +36,36 @@ public final class FxmlProjection {
|
|||||||
"GridPane.columnIndex",
|
"GridPane.columnIndex",
|
||||||
"GridPane.rowIndex",
|
"GridPane.rowIndex",
|
||||||
"GridPane.hgrow",
|
"GridPane.hgrow",
|
||||||
"GridPane.vgrow");
|
"GridPane.vgrow",
|
||||||
|
"GridPane.columnSpan",
|
||||||
|
"GridPane.rowSpan",
|
||||||
|
"AnchorPane.topAnchor",
|
||||||
|
"AnchorPane.bottomAnchor",
|
||||||
|
"AnchorPane.leftAnchor",
|
||||||
|
"AnchorPane.rightAnchor",
|
||||||
|
"HBox.hgrow",
|
||||||
|
"VBox.vgrow",
|
||||||
|
"StackPane.alignment",
|
||||||
|
"HBox.alignment",
|
||||||
|
"VBox.alignment",
|
||||||
|
"translateX",
|
||||||
|
"translateY",
|
||||||
|
"rotate",
|
||||||
|
"scaleX",
|
||||||
|
"scaleY",
|
||||||
|
"style",
|
||||||
|
"wrapText");
|
||||||
private final Document original;
|
private final Document original;
|
||||||
|
private final XmlSource source;
|
||||||
|
private final Map<String, Integer> sourceNodes = new LinkedHashMap<>();
|
||||||
|
private final Map<String, Element> sourceElements = new LinkedHashMap<>();
|
||||||
|
private final Set<String> synthetic = new HashSet<>();
|
||||||
private Document baseline;
|
private Document baseline;
|
||||||
private final Map<String, String> components = new LinkedHashMap<>();
|
private final Map<String, String> components = new LinkedHashMap<>();
|
||||||
|
|
||||||
public FxmlProjection(String text) throws Exception {
|
public FxmlProjection(String text) throws Exception {
|
||||||
original = parse(text);
|
original = parse(text);
|
||||||
|
source = new XmlSource(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Document parse(String text) throws Exception {
|
static Document parse(String text) throws Exception {
|
||||||
@@ -66,7 +89,46 @@ public final class FxmlProjection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public String project() throws Exception {
|
public String project() throws Exception {
|
||||||
|
components.clear();
|
||||||
|
sourceNodes.clear();
|
||||||
|
sourceElements.clear();
|
||||||
|
synthetic.clear();
|
||||||
var d = (Document) original.cloneNode(true);
|
var d = (Document) original.cloneNode(true);
|
||||||
|
var all = d.getElementsByTagName("*");
|
||||||
|
var originals = original.getElementsByTagName("*");
|
||||||
|
var reserved = new HashSet<String>();
|
||||||
|
for (int i = 0; i < all.getLength(); i++) {
|
||||||
|
String id = ((Element) all.item(i)).getAttributeNS(FX, "id");
|
||||||
|
if (!id.isBlank() && !reserved.add(id)) throw new IOException("Duplicate fx:id: " + id);
|
||||||
|
}
|
||||||
|
for (int i = 0; i < all.getLength(); i++) {
|
||||||
|
var element = (Element) all.item(i);
|
||||||
|
if (!isObject(element)
|
||||||
|
&& !(i == 0
|
||||||
|
&& FX.equals(element.getNamespaceURI())
|
||||||
|
&& "root".equals(element.getLocalName()))) continue;
|
||||||
|
String id = element.getAttributeNS(FX, "id");
|
||||||
|
if (id.isBlank()) {
|
||||||
|
id = "__designer_" + i;
|
||||||
|
while (reserved.contains(id)) id += "_";
|
||||||
|
reserved.add(id);
|
||||||
|
synthetic.add(id);
|
||||||
|
element.setAttributeNS(FX, "fx:id", id);
|
||||||
|
}
|
||||||
|
element.removeAttributeNS(FX, "id");
|
||||||
|
element.setAttributeNS(FX, "fx:id", id);
|
||||||
|
sourceNodes.put(id, i);
|
||||||
|
sourceElements.put(id, (Element) originals.item(i));
|
||||||
|
}
|
||||||
|
if (!d.getDocumentElement().hasAttribute("xmlns:fx"))
|
||||||
|
d.getDocumentElement().setAttribute("xmlns:fx", FX);
|
||||||
|
var root = d.getDocumentElement();
|
||||||
|
if (FX.equals(root.getNamespaceURI()) && "root".equals(root.getLocalName())) {
|
||||||
|
String type = root.getAttribute("type");
|
||||||
|
if (type.isBlank()) throw new IOException("fx:root requires a type");
|
||||||
|
d.renameNode(root, null, type);
|
||||||
|
root.removeAttribute("type");
|
||||||
|
}
|
||||||
var imports = new HashMap<String, String>();
|
var imports = new HashMap<String, String>();
|
||||||
var remove = new ArrayList<Node>();
|
var remove = new ArrayList<Node>();
|
||||||
for (Node n = d.getFirstChild(); n != null; n = n.getNextSibling())
|
for (Node n = d.getFirstChild(); n != null; n = n.getNextSibling())
|
||||||
@@ -94,18 +156,27 @@ public final class FxmlProjection {
|
|||||||
String tag = e.getTagName();
|
String tag = e.getTagName();
|
||||||
if (FX.equals(e.getNamespaceURI()))
|
if (FX.equals(e.getNamespaceURI()))
|
||||||
throw new IOException("This minimal projection does not support " + tag);
|
throw new IOException("This minimal projection does not support " + tag);
|
||||||
if (imports.containsKey(tag)) {
|
String componentType = imports.get(tag);
|
||||||
|
if (componentType == null
|
||||||
|
&& tag.contains(".")
|
||||||
|
&& Character.isUpperCase(tag.charAt(tag.lastIndexOf('.') + 1))
|
||||||
|
&& !tag.startsWith("javafx.")) componentType = tag;
|
||||||
|
if (componentType != null) {
|
||||||
String id = e.getAttributeNS(FX, "id");
|
String id = e.getAttributeNS(FX, "id");
|
||||||
if (id.isBlank()) throw new IOException("Component projection requires fx:id");
|
components.put(id, componentType);
|
||||||
components.put(id, imports.get(tag));
|
// Component internals belong to the target runtime and their own source document.
|
||||||
if (e.hasChildNodes())
|
while (e.hasChildNodes()) e.removeChild(e.getFirstChild());
|
||||||
throw new IOException("Inline Component content is outside this projection");
|
|
||||||
d.renameNode(e, null, "SnapshotRegion");
|
d.renameNode(e, null, "SnapshotRegion");
|
||||||
var attrs = e.getAttributes();
|
var attrs = e.getAttributes();
|
||||||
for (int a = attrs.getLength() - 1; a >= 0; a--) {
|
for (int a = attrs.getLength() - 1; a >= 0; a--) {
|
||||||
var attr = (Attr) attrs.item(a);
|
var attr = (Attr) attrs.item(a);
|
||||||
if (!EDITABLE.contains(attr.getName()) && !attr.getName().equals("fx:id"))
|
if ((!EDITABLE.contains(attr.getName())
|
||||||
e.removeAttributeNode(attr);
|
|| attr.getName().equals("text")
|
||||||
|
|| attr.getName().equals("wrapText")
|
||||||
|
|| attr.getName().equals("spacing")
|
||||||
|
|| attr.getName().equals("hgap")
|
||||||
|
|| attr.getName().equals("vgap"))
|
||||||
|
&& !attr.getName().equals("fx:id")) e.removeAttributeNode(attr);
|
||||||
}
|
}
|
||||||
} else if (Character.isUpperCase(tag.charAt(0))
|
} else if (Character.isUpperCase(tag.charAt(0))
|
||||||
&& tag.contains(".")
|
&& tag.contains(".")
|
||||||
@@ -126,7 +197,7 @@ public final class FxmlProjection {
|
|||||||
|
|
||||||
private static Map<String, Element> indexed(Document d) throws IOException {
|
private static Map<String, Element> indexed(Document d) throws IOException {
|
||||||
var result = new LinkedHashMap<String, Element>();
|
var result = new LinkedHashMap<String, Element>();
|
||||||
result.put("@root", d.getDocumentElement());
|
|
||||||
var nodes = d.getElementsByTagName("*");
|
var nodes = d.getElementsByTagName("*");
|
||||||
for (int i = 0; i < nodes.getLength(); i++) {
|
for (int i = 0; i < nodes.getLength(); i++) {
|
||||||
var e = (Element) nodes.item(i);
|
var e = (Element) nodes.item(i);
|
||||||
@@ -144,9 +215,14 @@ public final class FxmlProjection {
|
|||||||
for (int i = 0; i < nodes.getLength(); i++) {
|
for (int i = 0; i < nodes.getLength(); i++) {
|
||||||
var e = (Element) nodes.item(i);
|
var e = (Element) nodes.item(i);
|
||||||
String tag = e.getTagName();
|
String tag = e.getTagName();
|
||||||
if (Character.isUpperCase(tag.charAt(0))) {
|
if (isObject(e)) {
|
||||||
Node parent = e.getParentNode();
|
Node parent = e.getParentNode();
|
||||||
while (parent != null && !objects.containsKey(parent)) parent = parent.getParentNode();
|
var propertyPath = new ArrayList<String>();
|
||||||
|
while (parent != null && !objects.containsKey(parent)) {
|
||||||
|
if (parent instanceof Element wrapper && !wrapper.getTagName().equals("children"))
|
||||||
|
propertyPath.add(wrapper.getTagName());
|
||||||
|
parent = parent.getParentNode();
|
||||||
|
}
|
||||||
var fixed = new TreeMap<String, String>();
|
var fixed = new TreeMap<String, String>();
|
||||||
if (e != document.getDocumentElement() && e.getAttributeNS(FX, "id").isBlank())
|
if (e != document.getDocumentElement() && e.getAttributeNS(FX, "id").isBlank())
|
||||||
for (int a = 0; a < e.getAttributes().getLength(); a++) {
|
for (int a = 0; a < e.getAttributes().getLength(); a++) {
|
||||||
@@ -155,25 +231,39 @@ public final class FxmlProjection {
|
|||||||
fixed.put(attr.getNodeName(), attr.getNodeValue());
|
fixed.put(attr.getNodeName(), attr.getNodeValue());
|
||||||
}
|
}
|
||||||
result.add(
|
result.add(
|
||||||
tag
|
tag.substring(tag.lastIndexOf('.') + 1)
|
||||||
+ ":"
|
+ ":"
|
||||||
+ e.getAttributeNS(FX, "id")
|
+ e.getAttributeNS(FX, "id")
|
||||||
+ ":"
|
+ ":"
|
||||||
+ objects.getOrDefault(parent, -1)
|
+ objects.getOrDefault(parent, -1)
|
||||||
+ ":"
|
+ ":"
|
||||||
+ fixed);
|
+ fixed
|
||||||
|
+ ":"
|
||||||
|
+ propertyPath);
|
||||||
objects.put(e, objects.size());
|
objects.put(e, objects.size());
|
||||||
|
} else if (!e.getTextContent().isBlank() && e.getElementsByTagName("*").getLength() == 0) {
|
||||||
|
result.add("property:" + tag + ":" + e.getTextContent());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static boolean isObject(Element e) {
|
||||||
|
String tag = e.getTagName();
|
||||||
|
return !FX.equals(e.getNamespaceURI())
|
||||||
|
&& Character.isUpperCase(tag.charAt(tag.lastIndexOf('.') + 1));
|
||||||
|
}
|
||||||
|
|
||||||
public String restore(String projected) throws Exception {
|
public String restore(String projected) throws Exception {
|
||||||
|
return source.apply(edits(projected));
|
||||||
|
}
|
||||||
|
|
||||||
|
public java.util.List<de.neitzel.fxdesigner.contracts.EditBatch.Replacement> edits(
|
||||||
|
String projected) throws Exception {
|
||||||
var changed = parse(projected);
|
var changed = parse(projected);
|
||||||
var before = indexed(baseline);
|
var before = indexed(baseline);
|
||||||
var after = indexed(changed);
|
var after = indexed(changed);
|
||||||
var output = (Document) original.cloneNode(true);
|
var result = new ArrayList<de.neitzel.fxdesigner.contracts.EditBatch.Replacement>();
|
||||||
var targets = indexed(output);
|
|
||||||
if (!before.keySet().equals(after.keySet()) || !structure(baseline).equals(structure(changed)))
|
if (!before.keySet().equals(after.keySet()) || !structure(baseline).equals(structure(changed)))
|
||||||
throw new IOException("Structural editing is outside this minimal projection");
|
throw new IOException("Structural editing is outside this minimal projection");
|
||||||
for (String id : before.keySet()) {
|
for (String id : before.keySet()) {
|
||||||
@@ -187,18 +277,72 @@ public final class FxmlProjection {
|
|||||||
&& !before.get(id).getAttribute(attr).equals(after.get(id).getAttribute(attr)))
|
&& !before.get(id).getAttribute(attr).equals(after.get(id).getAttribute(attr)))
|
||||||
throw new IOException("Edit " + attr + " in source; this projection does not support it");
|
throw new IOException("Edit " + attr + " in source; this projection does not support it");
|
||||||
}
|
}
|
||||||
for (String id : before.keySet())
|
var requiredImports = new TreeSet<String>();
|
||||||
|
for (String id : before.keySet()) {
|
||||||
|
var values = new HashMap<String, String>();
|
||||||
for (String attr : EDITABLE) {
|
for (String attr : EDITABLE) {
|
||||||
String old = before.get(id).getAttribute(attr), value = after.get(id).getAttribute(attr);
|
String old = before.get(id).getAttribute(attr), value = after.get(id).getAttribute(attr);
|
||||||
if (!old.equals(value)) {
|
if (!old.equals(value)
|
||||||
var target = targets.get(id);
|
|| before.get(id).hasAttribute(attr) != after.get(id).hasAttribute(attr)) {
|
||||||
|
var target = sourceElements.get(id);
|
||||||
String originalValue = target.getAttribute(attr);
|
String originalValue = target.getAttribute(attr);
|
||||||
if (originalValue.startsWith("$") || originalValue.startsWith("#"))
|
if (originalValue.startsWith("$") || originalValue.startsWith("#"))
|
||||||
throw new IOException("Bound attributes must be edited in source");
|
throw new IOException("Bound attributes must be edited in source");
|
||||||
if (value.isEmpty()) target.removeAttribute(attr);
|
values.put(attr, after.get(id).hasAttribute(attr) ? value : null);
|
||||||
else target.setAttribute(attr, value);
|
if (after.get(id).hasAttribute(attr) && attr.contains(".")) {
|
||||||
|
String owner = "javafx.scene.layout." + attr.substring(0, attr.indexOf('.'));
|
||||||
|
boolean imported = false;
|
||||||
|
for (Node n = original.getFirstChild(); n != null; n = n.getNextSibling())
|
||||||
|
if (n instanceof ProcessingInstruction pi
|
||||||
|
&& pi.getTarget().equals("import")
|
||||||
|
&& (pi.getData().trim().equals(owner)
|
||||||
|
|| pi.getData().trim().equals("javafx.scene.layout.*"))) imported = true;
|
||||||
|
if (!imported) requiredImports.add(owner);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return xml(output);
|
}
|
||||||
|
result.addAll(source.attributes(sourceNodes.get(id), values));
|
||||||
|
}
|
||||||
|
if (!requiredImports.isEmpty()) {
|
||||||
|
int at = source.text.matches("(?s)^<\\?xml\\s.*") ? source.text.indexOf("?>") + 2 : 0;
|
||||||
|
String newline = source.text.contains("\r\n") ? "\r\n" : "\n";
|
||||||
|
String declarations =
|
||||||
|
(at == 0 ? "" : newline)
|
||||||
|
+ requiredImports.stream()
|
||||||
|
.map(type -> "<?import " + type + "?>" + newline)
|
||||||
|
.collect(java.util.stream.Collectors.joining());
|
||||||
|
result.add(new de.neitzel.fxdesigner.contracts.EditBatch.Replacement(at, at, declarations));
|
||||||
|
}
|
||||||
|
return List.copyOf(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, String> componentAttributes(String id) {
|
||||||
|
var values = new TreeMap<String, String>();
|
||||||
|
if (!components.containsKey(id)) return values;
|
||||||
|
var attrs = sourceElements.get(id).getAttributes();
|
||||||
|
for (int i = 0; i < attrs.getLength(); i++) {
|
||||||
|
var attr = attrs.item(i);
|
||||||
|
if (!attr.getNodeName().contains(":") && !attr.getNodeName().startsWith("on"))
|
||||||
|
values.put(attr.getNodeName(), attr.getNodeValue());
|
||||||
|
}
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<de.neitzel.fxdesigner.contracts.EditBatch.Replacement> componentAttribute(
|
||||||
|
String id, String name, String value) throws IOException {
|
||||||
|
if (!componentAttributes(id).containsKey(name))
|
||||||
|
throw new IOException("Unknown declared Component property");
|
||||||
|
if (sourceElements.get(id).getAttribute(name).equals(value)) return List.of();
|
||||||
|
return source.attributes(sourceNodes.get(id), Collections.singletonMap(name, value));
|
||||||
|
}
|
||||||
|
|
||||||
|
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)));
|
||||||
|
if (!edits.isEmpty() && !original.getDocumentElement().hasAttribute("xmlns:fx"))
|
||||||
|
edits.addAll(source.attributes(0, Map.of("xmlns:fx", FX)));
|
||||||
|
return source.apply(edits);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,105 @@
|
|||||||
|
package de.neitzel.fxdesigner.kit;
|
||||||
|
|
||||||
|
import de.neitzel.fxdesigner.contracts.EditBatch.Replacement;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.regex.*;
|
||||||
|
|
||||||
|
/** Lexical ranges for an already securely parsed XML document; never serializes untouched XML. */
|
||||||
|
final class XmlSource {
|
||||||
|
record Attribute(int start, int end, int valueStart, int valueEnd, char quote) {}
|
||||||
|
|
||||||
|
record Tag(int insert, Map<String, Attribute> attributes) {}
|
||||||
|
|
||||||
|
private static final Pattern ATTRIBUTE =
|
||||||
|
Pattern.compile("([^\\s=<>/]+)\\s*=\\s*(['\"])(.*?)\\2", Pattern.DOTALL);
|
||||||
|
final String text;
|
||||||
|
final List<Tag> tags = new ArrayList<>();
|
||||||
|
|
||||||
|
XmlSource(String text) throws IOException {
|
||||||
|
this.text = text;
|
||||||
|
int i = 0;
|
||||||
|
while ((i = text.indexOf('<', i)) >= 0) {
|
||||||
|
String end =
|
||||||
|
text.startsWith("<!--", i)
|
||||||
|
? "-->"
|
||||||
|
: text.startsWith("<![CDATA[", i) ? "]]>" : text.startsWith("<?", i) ? "?>" : null;
|
||||||
|
if (end != null) {
|
||||||
|
int close = text.indexOf(end, i);
|
||||||
|
if (close < 0) throw new IOException("Incomplete XML");
|
||||||
|
i = close + end.length();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int start = i++, quote = 0;
|
||||||
|
while (i < text.length()) {
|
||||||
|
char c = text.charAt(i);
|
||||||
|
if (quote != 0) {
|
||||||
|
if (c == quote) quote = 0;
|
||||||
|
} else if (c == '\'' || c == '"') quote = c;
|
||||||
|
else if (c == '>') break;
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
if (i == text.length()) throw new IOException("Incomplete XML tag");
|
||||||
|
if (text.charAt(start + 1) != '/') {
|
||||||
|
var attrs = new LinkedHashMap<String, Attribute>();
|
||||||
|
var matcher = ATTRIBUTE.matcher(text);
|
||||||
|
matcher.region(start + 1, i);
|
||||||
|
while (matcher.find())
|
||||||
|
attrs.put(
|
||||||
|
matcher.group(1),
|
||||||
|
new Attribute(
|
||||||
|
matcher.start(),
|
||||||
|
matcher.end(),
|
||||||
|
matcher.start(3),
|
||||||
|
matcher.end(3),
|
||||||
|
matcher.group(2).charAt(0)));
|
||||||
|
tags.add(new Tag(text.charAt(i - 1) == '/' ? i - 1 : i, attrs));
|
||||||
|
}
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Replacement> attributes(int index, Map<String, String> values) {
|
||||||
|
var tag = tags.get(index);
|
||||||
|
var result = new ArrayList<Replacement>();
|
||||||
|
var added = new StringBuilder();
|
||||||
|
new TreeMap<>(values)
|
||||||
|
.forEach(
|
||||||
|
(name, value) -> {
|
||||||
|
var attr = tag.attributes().get(name);
|
||||||
|
if (attr == null) {
|
||||||
|
if (value != null)
|
||||||
|
added
|
||||||
|
.append(' ')
|
||||||
|
.append(name)
|
||||||
|
.append("=\"")
|
||||||
|
.append(escape(value, '"'))
|
||||||
|
.append('"');
|
||||||
|
} else if (value == null) result.add(new Replacement(attr.start(), attr.end(), ""));
|
||||||
|
else
|
||||||
|
result.add(
|
||||||
|
new Replacement(
|
||||||
|
attr.valueStart(), attr.valueEnd(), escape(value, attr.quote())));
|
||||||
|
});
|
||||||
|
if (!added.isEmpty()) result.add(new Replacement(tag.insert(), tag.insert(), added.toString()));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
static String escape(String value, char quote) {
|
||||||
|
return value
|
||||||
|
.replace("&", "&")
|
||||||
|
.replace("<", "<")
|
||||||
|
.replace(quote == '"' ? "\"" : "'", quote == '"' ? """ : "'")
|
||||||
|
.replace("\r", " ")
|
||||||
|
.replace("\n", " ")
|
||||||
|
.replace("\t", "	");
|
||||||
|
}
|
||||||
|
|
||||||
|
String apply(List<Replacement> edits) {
|
||||||
|
var sorted = new ArrayList<>(edits);
|
||||||
|
sorted.sort(Comparator.comparingInt(Replacement::start).reversed());
|
||||||
|
var out = new StringBuilder(text);
|
||||||
|
for (var edit : sorted) out.replace(edit.start(), edit.end(), edit.text());
|
||||||
|
return out.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -49,13 +49,114 @@ class FxmlProjectionTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void rejectsStructuralEditsAndUnnamedComponents() throws Exception {
|
void rejectsStructuralEditsAndSupportsUnnamedComponents() throws Exception {
|
||||||
var projection = new FxmlProjection(XML);
|
var projection = new FxmlProjection(XML);
|
||||||
String projected = projection.project();
|
String projected = projection.project();
|
||||||
assertThrows(
|
assertThrows(
|
||||||
Exception.class,
|
Exception.class,
|
||||||
() -> projection.restore(projected.replace("fx:id=\"card\"", "fx:id=\"different\"")));
|
() -> projection.restore(projected.replace("fx:id=\"card\"", "fx:id=\"different\"")));
|
||||||
|
var anonymous = new FxmlProjection(XML.replace("fx:id=\"card\"", ""));
|
||||||
|
String view = anonymous.project();
|
||||||
|
assertEquals(XML.replace("fx:id=\"card\"", ""), anonymous.restore(view));
|
||||||
|
assertTrue(anonymous.previewText().contains("fx:id=\"__designer_"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void preservesEveryUnchangedCharacterAndUsesAttributeRanges() throws Exception {
|
||||||
|
String text = XML.replace("caption=\"before\"", "caption = 'a & b'").replace("\n", "\r\n");
|
||||||
|
var p = new FxmlProjection(text);
|
||||||
|
String view = p.project();
|
||||||
|
assertEquals(text, p.restore(view));
|
||||||
|
assertTrue(p.componentAttribute("card", "caption", "a & b").isEmpty());
|
||||||
|
var edits = p.edits(view.replace("caption=\"a & b\"", "caption=\"after\""));
|
||||||
|
assertEquals(1, edits.size());
|
||||||
|
assertEquals("a & b", text.substring(edits.getFirst().start(), edits.getFirst().end()));
|
||||||
|
assertEquals(
|
||||||
|
text.replace("a & b", "after"),
|
||||||
|
p.restore(view.replace("caption=\"a & b\"", "caption=\"after\"")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void projectsFullyQualifiedNestedAndAnonymousComponents() throws Exception {
|
||||||
|
String text =
|
||||||
|
"<?import javafx.scene.layout.VBox?><VBox"
|
||||||
|
+ " xmlns:fx=\"http://javafx.com/fxml/1\"><VBox><app.Card caption='one'/><app.Card"
|
||||||
|
+ " caption='two'><opaque value='retained'/></app.Card></VBox></VBox>";
|
||||||
|
var p = new FxmlProjection(text);
|
||||||
|
String view = p.project();
|
||||||
|
assertEquals(2, p.components().size());
|
||||||
|
assertEquals(
|
||||||
|
text.replace("caption='two'", "caption='changed'"),
|
||||||
|
p.restore(view.replace("caption=\"two\"", "caption=\"changed\"")));
|
||||||
|
assertTrue(p.previewText().contains("<opaque value='retained'/ >".replace("/ >", "/>")));
|
||||||
|
assertEquals(text, p.restore(view));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void fxRootAndUnnamedStandardControlsKeepOriginalDeclarations() throws Exception {
|
||||||
|
String text =
|
||||||
|
"<?import javafx.scene.layout.VBox?><?import javafx.scene.control.Label?><fx:root"
|
||||||
|
+ " type='VBox' xmlns:fx='http://javafx.com/fxml/1'"
|
||||||
|
+ " xmlns='http://javafx.com/javafx/21'><Label text='before'/></fx:root>";
|
||||||
|
var p = new FxmlProjection(text);
|
||||||
|
assertEquals(
|
||||||
|
text.replace("text='before'", "text='after'"),
|
||||||
|
p.restore(p.project().replace("text=\"before\"", "text=\"after\"")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void sourcePropertyEditsEscapeQuotesWithoutTouchingUnknownContent() throws Exception {
|
||||||
|
var p = new FxmlProjection(XML);
|
||||||
|
p.project();
|
||||||
|
var changes = p.componentAttribute("card", "custom", "a\"<&\n");
|
||||||
|
assertEquals(1, changes.size());
|
||||||
|
assertEquals("a"<& ", changes.getFirst().text());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void preservesAlternatePrefixesCommentsCdataAndQuotedDelimiters() throws Exception {
|
||||||
|
String text =
|
||||||
|
"<?import javafx.scene.layout.VBox?><?import app.Card?><VBox"
|
||||||
|
+ " xmlns:f='http://javafx.com/fxml/1'><!-- <Card/> --><f:define><String"
|
||||||
|
+ " f:id='hidden'><![CDATA[<x/>]]></String></f:define><Card f:id='card' caption = 'a"
|
||||||
|
+ " > b' custom='"'/></VBox>";
|
||||||
|
var p = new FxmlProjection(text);
|
||||||
|
String projected = p.project();
|
||||||
|
assertEquals(
|
||||||
|
text.replace("a > b", "changed"),
|
||||||
|
p.restore(projected.replace("caption=\"a > b\"", "caption=\"changed\"")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void rejectsPropertyReparentingInsteadOfSilentlySavingIt() throws Exception {
|
||||||
|
var p =
|
||||||
|
new FxmlProjection(
|
||||||
|
"<?import javafx.scene.layout.BorderPane?><?import"
|
||||||
|
+ " javafx.scene.control.Label?><BorderPane"
|
||||||
|
+ " xmlns:fx='http://javafx.com/fxml/1'><left><Label"
|
||||||
|
+ " fx:id='label'/></left></BorderPane>");
|
||||||
|
String projected = p.project();
|
||||||
assertThrows(
|
assertThrows(
|
||||||
Exception.class, () -> new FxmlProjection(XML.replace("fx:id=\"card\"", "")).project());
|
Exception.class,
|
||||||
|
() -> p.restore(projected.replace("<left>", "<right>").replace("</left>", "</right>")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void addsAnAttachedConstraintImportWithoutReserializingTheDocument() throws Exception {
|
||||||
|
String text =
|
||||||
|
"<?xml version='1.0'?>\r\n"
|
||||||
|
+ "<javafx.scene.layout.AnchorPane xmlns:fx='http://javafx.com/fxml/1'><app.Card"
|
||||||
|
+ " fx:id='card' caption='same'/></javafx.scene.layout.AnchorPane>";
|
||||||
|
var p = new FxmlProjection(text);
|
||||||
|
String projected =
|
||||||
|
p.project().replace("caption=\"same\"", "caption=\"same\" AnchorPane.leftAnchor=\"12\"");
|
||||||
|
String restored = p.restore(projected);
|
||||||
|
assertTrue(
|
||||||
|
restored.startsWith(
|
||||||
|
"<?xml version='1.0'?>\r\n<?import javafx.scene.layout.AnchorPane?>\r\n"));
|
||||||
|
assertTrue(restored.contains("caption='same' AnchorPane.leftAnchor=\"12\""));
|
||||||
|
assertTrue(restored.endsWith("</javafx.scene.layout.AnchorPane>"));
|
||||||
|
var again = new FxmlProjection(restored);
|
||||||
|
assertEquals(restored, again.restore(again.project()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,6 +28,14 @@
|
|||||||
<artifactId>junit-jupiter</artifactId>
|
<artifactId>junit-jupiter</artifactId>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<!-- Keep the main UI before its test-support dependency so its patched Kit class wins. -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>de.neitzel.fxdesigner</groupId>
|
||||||
|
<artifactId>designer-kit-ui</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
<type>test-jar</type>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
<build>
|
<build>
|
||||||
<plugins>
|
<plugins>
|
||||||
|
|||||||
+25
-2
@@ -64,7 +64,11 @@ public final class FileDocuments implements HostServices.Documents, AutoCloseabl
|
|||||||
}
|
}
|
||||||
|
|
||||||
public CompletionStage<List<DocumentSnapshot>> snapshots() {
|
public CompletionStage<List<DocumentSnapshot>> snapshots() {
|
||||||
return submit(() -> documents.values().stream().map(FileDocuments::snapshot).toList());
|
return submit(
|
||||||
|
() -> {
|
||||||
|
for (State s : documents.values()) refresh(s);
|
||||||
|
return documents.values().stream().map(FileDocuments::snapshot).toList();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public CompletionStage<List<DocumentSnapshot>> unsaved() {
|
public CompletionStage<List<DocumentSnapshot>> unsaved() {
|
||||||
@@ -76,6 +80,18 @@ public final class FileDocuments implements HostServices.Documents, AutoCloseabl
|
|||||||
.toList());
|
.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void refresh(State s) throws java.io.IOException {
|
||||||
|
if (!s.text.equals(s.saved)) return; // Dirty draft belongs to the user; save detects conflict.
|
||||||
|
String disk = Files.readString(s.file);
|
||||||
|
if (!disk.equals(s.saved)) {
|
||||||
|
s.text = s.saved = disk;
|
||||||
|
s.undo.clear();
|
||||||
|
s.redo.clear();
|
||||||
|
s.revision++;
|
||||||
|
changes.forEach(listener -> listener.accept(s.file.toUri()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static DocumentSnapshot snapshot(State s) {
|
private static DocumentSnapshot snapshot(State s) {
|
||||||
return new DocumentSnapshot(
|
return new DocumentSnapshot(
|
||||||
s.file.toUri(), s.revision, s.text, Files.isWritable(s.file), !s.text.equals(s.saved));
|
s.file.toUri(), s.revision, s.text, Files.isWritable(s.file), !s.text.equals(s.saved));
|
||||||
@@ -98,12 +114,17 @@ public final class FileDocuments implements HostServices.Documents, AutoCloseabl
|
|||||||
}
|
}
|
||||||
|
|
||||||
public CompletionStage<DocumentSnapshot> snapshot() {
|
public CompletionStage<DocumentSnapshot> snapshot() {
|
||||||
return run(() -> FileDocuments.snapshot(s));
|
return run(
|
||||||
|
() -> {
|
||||||
|
refresh(s);
|
||||||
|
return FileDocuments.snapshot(s);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public CompletionStage<DocumentSnapshot> apply(EditBatch edits) {
|
public CompletionStage<DocumentSnapshot> apply(EditBatch edits) {
|
||||||
return run(
|
return run(
|
||||||
() -> {
|
() -> {
|
||||||
|
refresh(s);
|
||||||
String updated = edits.applyTo(FileDocuments.snapshot(s));
|
String updated = edits.applyTo(FileDocuments.snapshot(s));
|
||||||
if (!updated.equals(s.text)) {
|
if (!updated.equals(s.text)) {
|
||||||
s.undo.push(s.text);
|
s.undo.push(s.text);
|
||||||
@@ -154,6 +175,8 @@ public final class FileDocuments implements HostServices.Documents, AutoCloseabl
|
|||||||
!= null) {
|
!= null) {
|
||||||
Files.setPosixFilePermissions(temp, Files.getPosixFilePermissions(s.file));
|
Files.setPosixFilePermissions(temp, Files.getPosixFilePermissions(s.file));
|
||||||
}
|
}
|
||||||
|
if (!Files.readString(s.file).equals(s.saved))
|
||||||
|
throw new ConcurrentModificationException("External change; draft retained");
|
||||||
Files.move(
|
Files.move(
|
||||||
temp,
|
temp,
|
||||||
s.file,
|
s.file,
|
||||||
|
|||||||
+28
@@ -67,4 +67,32 @@ class FileDocumentsTest {
|
|||||||
assertEquals("draft", d.snapshot().toCompletableFuture().get().text());
|
assertEquals("draft", d.snapshot().toCompletableFuture().get().text());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void cleanExternalChangesReloadAndInvalidateOldTransactions() throws Exception {
|
||||||
|
Path file = folder.resolve("External.fxml");
|
||||||
|
Files.writeString(file, "old");
|
||||||
|
try (var docs = new FileDocuments()) {
|
||||||
|
var first = docs.open(file.toUri()).toCompletableFuture().get();
|
||||||
|
var second = docs.open(file.toUri()).toCompletableFuture().get();
|
||||||
|
var initial = first.snapshot().toCompletableFuture().get();
|
||||||
|
Files.writeString(file, "external");
|
||||||
|
var reloaded = second.snapshot().toCompletableFuture().get();
|
||||||
|
assertEquals("external", reloaded.text());
|
||||||
|
assertFalse(reloaded.dirty());
|
||||||
|
assertNotEquals(initial.revision(), reloaded.revision());
|
||||||
|
assertThrows(
|
||||||
|
ExecutionException.class,
|
||||||
|
() ->
|
||||||
|
first
|
||||||
|
.apply(
|
||||||
|
new EditBatch(
|
||||||
|
initial.revision(),
|
||||||
|
"old edit",
|
||||||
|
List.of(new EditBatch.Replacement(0, 1, "X"))))
|
||||||
|
.toCompletableFuture()
|
||||||
|
.get());
|
||||||
|
assertEquals("external", first.undo().toCompletableFuture().get().text());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+20
-1
@@ -127,15 +127,30 @@ public final class KitWorkflowSmoke {
|
|||||||
var field =
|
var field =
|
||||||
(TextField)
|
(TextField)
|
||||||
pane.lookupAll(".text-field").stream()
|
pane.lookupAll(".text-field").stream()
|
||||||
.filter(n -> n instanceof TextField t && t.getText().equals(caption))
|
.filter(
|
||||||
|
n ->
|
||||||
|
n instanceof TextField t
|
||||||
|
&& t.getText().equals(caption)
|
||||||
|
&& (t.getId() == null
|
||||||
|
|| !t.getId().startsWith("component-property-")))
|
||||||
.findFirst()
|
.findFirst()
|
||||||
.orElseThrow();
|
.orElseThrow();
|
||||||
field.setText("alpha bravo charlie");
|
field.setText("alpha bravo charlie");
|
||||||
pane.applyCss();
|
pane.applyCss();
|
||||||
pane.layout();
|
pane.layout();
|
||||||
var result = new StringBuilder();
|
var result = new StringBuilder();
|
||||||
|
try {
|
||||||
TextSelectionChecks.run(field, result);
|
TextSelectionChecks.run(field, result);
|
||||||
|
} finally {
|
||||||
System.out.print(result);
|
System.out.print(result);
|
||||||
|
System.out.println(
|
||||||
|
"Caption field: "
|
||||||
|
+ field.getId()
|
||||||
|
+ " width="
|
||||||
|
+ field.getWidth()
|
||||||
|
+ " text="
|
||||||
|
+ field.getText());
|
||||||
|
}
|
||||||
field.setText(caption);
|
field.setText(caption);
|
||||||
field.fireEvent(new javafx.event.ActionEvent());
|
field.fireEvent(new javafx.event.ActionEvent());
|
||||||
return null;
|
return null;
|
||||||
@@ -223,6 +238,10 @@ public final class KitWorkflowSmoke {
|
|||||||
if (!Files.readString(Path.of(args[1])).contains("Native canvas edit"))
|
if (!Files.readString(Path.of(args[1])).contains("Native canvas edit"))
|
||||||
throw new AssertionError("Host save missing");
|
throw new AssertionError("Host save missing");
|
||||||
}
|
}
|
||||||
|
try (var document =
|
||||||
|
host.services().documents().open(Path.of(args[1]).toUri()).toCompletableFuture().get()) {
|
||||||
|
ProjectionHostChecks.run(pane, document);
|
||||||
|
}
|
||||||
fx(
|
fx(
|
||||||
() -> {
|
() -> {
|
||||||
pane.close();
|
pane.close();
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
Bootstrap work package 7 connects the existing build/snapshot services to real target workers and the same Kit UI in standalone and IntelliJ. This is a runnable development milestone, with bounded scalar/layout projection, not the complete designer product.
|
Bootstrap work package 7 connects the existing build/snapshot services to real target workers and the same Kit UI in standalone and IntelliJ. This is a runnable development milestone, with bounded scalar/layout projection, not the complete designer product.
|
||||||
|
|
||||||
|
Later implementation: [source-preserving projection](source-preserving-projection.md) replaces the original serialization and extends anonymous/nested Component and fx:root editing. Historical qualification and initial limitations below describe work package 7.
|
||||||
|
|
||||||
## Flow and ownership
|
## Flow and ownership
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ A parent rendering snapshot shares its Model graph across embedded Components; s
|
|||||||
|
|
||||||
Track configuration schema, wire/metadata contracts and preview API compatibility independently from product releases. Major changes represent breaking contracts; additive features use declared optional capabilities. Equal product versions alone are not proof of compatibility. Verify discovery and rendering independently so one unavailable capability does not unnecessarily disable the other.
|
Track configuration schema, wire/metadata contracts and preview API compatibility independently from product releases. Major changes represent breaking contracts; additive features use declared optional capabilities. Equal product versions alone are not proof of compatibility. Verify discovery and rendering independently so one unavailable capability does not unnecessarily disable the other.
|
||||||
|
|
||||||
Use a dedicated loopback-only socket with a per-launch random token and version/capability handshake for worker messages. Standard output/error remain captured application logs and cannot carry protocol framing. Do not log tokens. Bind the session to the launched worker and reject messages before authentication/negotiation. Use explicit bounded framing, validate lengths and resource limits before allocation, correlate requests/replies and define cancellation/error outcomes. Encoding, limits and negotiation are now defined by the [executable draft wire contract](configuration-and-protocol.md#draft-wire-protocol-10); transport integration remains later work.
|
Use a dedicated loopback-only socket with a per-launch random token and version/capability handshake for worker messages. Standard output/error remain captured application logs and cannot carry protocol framing. Do not log tokens. Bind the session to the launched worker and reject messages before authentication/negotiation. Use explicit bounded framing, validate lengths and resource limits before allocation, correlate requests/replies and define cancellation/error outcomes. Encoding, limits and negotiation are now defined by the [executable draft wire contract](configuration-and-protocol.md#draft-wire-protocol-10); transport integration is implemented in the first connected workflow.
|
||||||
|
|
||||||
Every applicable request/result identifies its worker session, request, context and build snapshot; rendering also identifies document revisions and requested size. Obsolete results cannot mutate current document/UI state. Diagnostics identify operation and available source origin while preserving editing.
|
Every applicable request/result identifies its worker session, request, context and build snapshot; rendering also identifies document revisions and requested size. Obsolete results cannot mutate current document/UI state. Diagnostics identify operation and available source origin while preserving editing.
|
||||||
|
|
||||||
|
|||||||
@@ -91,3 +91,7 @@ Connect shared core/UI to the worker protocol and fixtures. Exercise Build and R
|
|||||||
## Delivery discipline
|
## Delivery discipline
|
||||||
|
|
||||||
Do not label these packages as additional unresolved Wayfinder decisions: the architecture is accepted. They are concrete implementation work with validation gates. Reopen a focused design question only if implementation evidence exposes a real incompatibility or missing seam. Bootstrap, library integration and designer implementation remain separately reviewable changes.
|
Do not label these packages as additional unresolved Wayfinder decisions: the architecture is accepted. They are concrete implementation work with validation gates. Reopen a focused design question only if implementation evidence exposes a real incompatibility or missing seam. Bootstrap, library integration and designer implementation remain separately reviewable changes.
|
||||||
|
|
||||||
|
## After bootstrap: source-preserving projection
|
||||||
|
|
||||||
|
Roadmap stage 2 now has source-range transactions, broader Component projection and shared live checks in both hosts. See [implementation and qualification boundaries](source-preserving-projection.md). Physical drag/drop delivery remains an explicit manual gate; the automated suite exercises Kit relocation commands and resize-handle gestures. The next implementation stage is target-runtime sessions, sizing and navigation.
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
# Authoritative documents and source-preserving projection
|
||||||
|
|
||||||
|
This stage replaces whole-document XML serialization with source-range edits and extends the shared Kit projection. Both hosts use the same implementation and live qualification scenarios. It establishes the document/projection foundation; structural authoring and complete target-type discovery are later work.
|
||||||
|
|
||||||
|
## Source ownership and edits
|
||||||
|
|
||||||
|
The host Document remains authoritative. A securely parsed DOM identifies elements and compares Kit's projected state, while a lexical index records the original start tags and attribute-value ranges. An accepted canvas edit produces an `EditBatch` containing only changed ranges, with the expected host revision and one named undo transaction. Kit's generated XML never replaces the source document.
|
||||||
|
|
||||||
|
Unchanged characters stay unchanged: import-only binding types, Model processing instructions, comments, unknown declarations, namespace versions, attribute order, quote style, whitespace, entities and line endings. Attribute values are escaped for their existing quote delimiter. Removing an attribute removes only that attribute; new attributes are inserted into its original start tag. A newly edited attached constraint adds its required layout-class import if absent, without moving existing processing instructions. No-op round trips return the original text exactly.
|
||||||
|
|
||||||
|
Source edits use a minimal changed range. Shared source selection is mapped through visual ranges, while native IntelliJ editors retain their Document range markers. A source edit queued during an asynchronous visual transaction retains disjoint visual changes; later overlapping source typing wins in its own undo command. Revisions belong to each host adapter and do not assume that IntelliJ modification stamps increase across undo.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
D[Host Document and revision] --> L[Secure DOM + lexical ranges]
|
||||||
|
L --> P[Kit projection with private identities]
|
||||||
|
P --> C[Canvas property or layout proposal]
|
||||||
|
C --> V[Validate identities and structure]
|
||||||
|
V --> E[Attribute-range EditBatch]
|
||||||
|
E --> D
|
||||||
|
D --> O[Private draft overlay]
|
||||||
|
O --> W[Target-runtime rendering]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Broader Component coverage
|
||||||
|
|
||||||
|
- Explicitly imported and fully qualified Component declarations, multiple instances, and Components nested in standard layout containers.
|
||||||
|
- Anonymous Components and standard nodes receive collision-checked projection identities. They are never saved into source. Anonymous Component render identities are added only to the private FXML overlay.
|
||||||
|
- `fx:root` bodies project through their declared type, allowing ordinary controls inside a Component's own FXML to be edited while retaining the original `fx:root` declaration.
|
||||||
|
- Component internals remain opaque to the parent canvas. Inline content and unknown attributes stay in source; target rendering sees the original declaration. Open Component continues to navigate to its separate source.
|
||||||
|
- The shared Component panel shows the real declared class and existing FXML properties. Enter applies an escaped source-range edit; target rendering validates application-specific values. It does not infer application property types from `SnapshotRegion`.
|
||||||
|
- Native Kit picking, resize handles and relocation commands operate on proxies. Attribute editing now includes AnchorPane constraints, GridPane spans/growth, HBox/VBox growth, alignment, transforms and existing scalar/layout properties.
|
||||||
|
|
||||||
|
Kit's built-in hierarchy/inspector still identifies the host placeholder as `SnapshotRegion`; the shared Component panel supplies the actual class/properties. There is no new upstream Kit patch in this stage. A full target-aware hierarchy/typed inspector remains a later integration seam.
|
||||||
|
|
||||||
|
## Failure and external-edit behavior
|
||||||
|
|
||||||
|
Source, Design and Split are explicit modes over the same draft. Invalid or unrepresentable source leaves source editing available and disables the retained canvas and Component property panel. Correcting the source restores the projection. Unsupported structural proposals, identifier changes and unsupported property changes are rejected without rewriting the source. Changes of property ownership, such as moving a node from BorderPane.left to right, cannot be silently ignored.
|
||||||
|
|
||||||
|
Standalone polls clean files through document snapshots, reloads changed contents, advances the revision and clears obsolete local history. IntelliJ refreshes/reloads clean Documents through its native file/document manager. If native autosave already made disk and Document identical, it only advances the disk baseline; it does not reload the same text or invalidate a render revision. Dirty drafts are preserved; saving checks the disk baseline and rejects a conflict. Standalone rechecks immediately before its atomic replacement. There is no cross-process filesystem lock or interactive three-way merge UI; source editing and the retained draft remain available for resolution.
|
||||||
|
|
||||||
|
The tests reuse independent handles for the same authoritative draft, including native IDE Documents opened through path aliases. Context-specific workflows do not create independent document histories.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
Run the full bootstrap and real-IDE checks from [README](../README.md). The same `ProjectionHostChecks` test artifact runs in native standalone windows and the disposable IntelliJ companion, for classpath and module-path applications. The artifact is test-only and excluded from the delivered plugin.
|
||||||
|
|
||||||
|
The live scenarios cover exact source preservation, nested/fully qualified/anonymous Components, declared-property editing, unaffected source selection, native undo/redo, mode transitions, target NeitzelLib loading, native picking, resize-handle gestures, Kit relocation commands and attached constraints, save/reopen, invalid-source recovery, `fx:root` editing and source typing queued during a visual edit. Existing text-selection and worker-lifetime regressions remain required. Unit/adapter tests cover alternate XML prefixes, CDATA/comments/quoted delimiters, safe rejection of property reparenting, clean external reload, dirty conflicts, stale revisions and native caret/selection preservation.
|
||||||
|
|
||||||
|
Verified on 2026-11-12: **65 tests** (58 Maven, seven IntelliJ), zero failures/errors/skips, plus the retained 346 target assertions in 24 JVMs, 14 Build and Refresh checks and 64 connected workflow assertions. Both native standalone layouts and both real registered IntelliJ layouts passed the shared projection scenarios. The final unchanged-property/entity-spelling and missing constraint-import regressions passed in the subsequent Maven/IDE builds. Plugin packaging, dependency/bytecode boundaries, test-artifact exclusion and documentation checks pass. Both isolated IDEs loaded exactly one Glass library with the recorded JavaFX 26 artifact hash. Evidence remains under `target/connected-workflow/` and `target/ide-workflow/`. The intentional SDK until-build warning is unchanged.
|
||||||
|
|
||||||
|
Physical OS drag-and-drop delivery is **not qualified by this automated result**. Synthetic mouse events cannot start JavaFX's native dragboard; a desktop Robot attempt did not result in a document move, including after bringing the test process to the foreground. The suite therefore tests the actual Kit relocation command separately from resize-handle events. A human drag/drop check in both hosts remains part of stage-2 platform qualification.
|
||||||
|
|
||||||
|
## Remaining boundaries
|
||||||
|
|
||||||
|
This stage preserves arbitrary source, but does not make arbitrary structure visually editable. Insert/delete/reparent, binding-aware renames and subtree copy/paste require later structural transactions. Application wildcard imports, `fx:include`, scripts and unrepresentable roots retain source fallback. Arbitrary target properties use declared FXML text editing until shared discovery supplies typed metadata. Anonymous identities are local to a projection revision, not the persistent occurrence identities needed for repeated/Table Components. Target node lookup still uses the initial CSS-ID route; separate CSS IDs and repeated/internal scopes need stage 3's occurrence identity work.
|
||||||
|
|
||||||
|
Qualification remains scoped to the recorded macOS arm64 / IntelliJ 263.7307.200 / JavaFX and Kit 26 host baseline and Java 21 / JavaFX 21.0.7 application fixtures. Product packaging, a broader platform matrix, lossless structural editing and physical drag/drop qualification are not implied. Next implementation stage: target-runtime sessions, sizing and navigation, while retaining the explicit manual stage-2 input gate.
|
||||||
@@ -235,13 +235,18 @@ public final class WorkflowProbe implements StartupActivity.DumbAware {
|
|||||||
output.resolve("intellij.png").toFile());
|
output.resolve("intellij.png").toFile());
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
try (var document =
|
||||||
|
pane.kit().host().documents().open(path.toUri()).toCompletableFuture().get()) {
|
||||||
|
de.neitzel.fxdesigner.kit.ProjectionHostChecks.run(pane, document);
|
||||||
|
}
|
||||||
String version = fx(() -> System.getProperty("javafx.runtime.version"));
|
String version = fx(() -> System.getProperty("javafx.runtime.version"));
|
||||||
if (!version.startsWith("26"))
|
if (!version.startsWith("26"))
|
||||||
throw new AssertionError("Wrong host JavaFX: " + version);
|
throw new AssertionError("Wrong host JavaFX: " + version);
|
||||||
Files.writeString(
|
Files.writeString(
|
||||||
output.resolve("SUCCESS.txt"),
|
output.resolve("SUCCESS.txt"),
|
||||||
"PASS registered IntelliJ editor, shared Kit, real Build and Refresh, target"
|
"PASS registered IntelliJ editor, shared Kit, real Build and Refresh, target"
|
||||||
+ " pixels, native document edits, undo/redo, draft render; JavaFX "
|
+ " pixels, native document edits, undo/redo, draft render,"
|
||||||
|
+ " source-preserving projection and extended Component scenarios; JavaFX "
|
||||||
+ version
|
+ version
|
||||||
+ "\n");
|
+ "\n");
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
"""Launch a disposable standalone or isolated IntelliJ fixture host (macOS development)."""
|
"""Launch a disposable standalone or isolated IntelliJ fixture host (macOS development)."""
|
||||||
import argparse
|
import argparse
|
||||||
import os
|
import os
|
||||||
|
import shutil
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
@@ -39,6 +40,9 @@ else:
|
|||||||
if args.check:
|
if args.check:
|
||||||
classes = base / "check-classes"
|
classes = base / "check-classes"
|
||||||
classes.mkdir()
|
classes.mkdir()
|
||||||
|
checks = base / "plugins/qualification/lib/projection-checks.jar"
|
||||||
|
checks.parent.mkdir(parents=True)
|
||||||
|
shutil.copyfile(ROOT / "designer-kit-ui/target/designer-kit-ui-0.1.0-SNAPSHOT-tests.jar", checks)
|
||||||
jars = [*(args.idea_home / "lib").glob("*.jar"), *(base / "plugins").rglob("*.jar")]
|
jars = [*(args.idea_home / "lib").glob("*.jar"), *(base / "plugins").rglob("*.jar")]
|
||||||
subprocess.run([str(args.java_home / "bin/javac"), "-cp", os.pathsep.join(map(str,jars)), "-d", str(classes),
|
subprocess.run([str(args.java_home / "bin/javac"), "-cp", os.pathsep.join(map(str,jars)), "-d", str(classes),
|
||||||
str(ROOT / "integration-tests/idea/WorkflowProbe.java")], check=True)
|
str(ROOT / "integration-tests/idea/WorkflowProbe.java")], check=True)
|
||||||
@@ -46,7 +50,7 @@ else:
|
|||||||
meta.mkdir()
|
meta.mkdir()
|
||||||
(meta / "plugin.xml").write_text('<idea-plugin><id>de.neitzel.fxdesigner.qualification</id><name>Designer workflow qualification</name><version>1</version><vendor>Local tests</vendor><idea-version since-build="263" until-build="263.*"/><depends>com.intellij.modules.platform</depends><depends>de.neitzel.fxdesigner</depends><extensions defaultExtensionNs="com.intellij"><postStartupActivity implementation="qualification.WorkflowProbe"/></extensions></idea-plugin>')
|
(meta / "plugin.xml").write_text('<idea-plugin><id>de.neitzel.fxdesigner.qualification</id><name>Designer workflow qualification</name><version>1</version><vendor>Local tests</vendor><idea-version since-build="263" until-build="263.*"/><depends>com.intellij.modules.platform</depends><depends>de.neitzel.fxdesigner</depends><extensions defaultExtensionNs="com.intellij"><postStartupActivity implementation="qualification.WorkflowProbe"/></extensions></idea-plugin>')
|
||||||
jar = base / "plugins/qualification/lib/qualification.jar"
|
jar = base / "plugins/qualification/lib/qualification.jar"
|
||||||
jar.parent.mkdir(parents=True)
|
jar.parent.mkdir(parents=True, exist_ok=True)
|
||||||
subprocess.run([str(args.java_home / "bin/jar"), "cf", str(jar), "-C", str(classes), "."], check=True)
|
subprocess.run([str(args.java_home / "bin/jar"), "cf", str(jar), "-C", str(classes), "."], check=True)
|
||||||
options = base / "config/options"
|
options = base / "config/options"
|
||||||
options.mkdir(parents=True)
|
options.mkdir(parents=True)
|
||||||
|
|||||||
Reference in New Issue
Block a user