Add shared source-preserving Computed binding inspector
This commit is contained in:
@@ -15,8 +15,7 @@ final class BindingBrowser extends VBox {
|
||||
final Label status = new Label("Build and Refresh, then discover binding targets.");
|
||||
private boolean updating;
|
||||
|
||||
BindingBrowser(
|
||||
BiConsumer<String, String> discover, Consumer<DirectBindingDocument.Values> create) {
|
||||
BindingBrowser(BiConsumer<String, String> discover, Consumer<BindingDocument.Values> create) {
|
||||
setSpacing(5);
|
||||
paths.setId("binding-path");
|
||||
directions.setId("binding-direction");
|
||||
@@ -40,7 +39,7 @@ final class BindingBrowser extends VBox {
|
||||
var target = targets.getSelectionModel().getSelectedItem();
|
||||
if (target != null)
|
||||
create.accept(
|
||||
new DirectBindingDocument.Values(
|
||||
new BindingDocument.Values(
|
||||
target.control(), target.property(), path(), directions.getValue(), ""));
|
||||
});
|
||||
paths.setOnAction(
|
||||
|
||||
+82
-28
@@ -4,15 +4,42 @@ import de.neitzel.fxdesigner.contracts.EditBatch.Replacement;
|
||||
import java.util.*;
|
||||
import org.w3c.dom.*;
|
||||
|
||||
/** Direct binding edit intents translated into lexical ranges in one immutable source revision. */
|
||||
final class DirectBindingDocument {
|
||||
/** Binding edit intents translated into lexical ranges in one immutable source revision. */
|
||||
final class BindingDocument {
|
||||
static final String FX = "http://javafx.com/fxml/1", PACKAGE = "de.neitzel.fx.view.";
|
||||
|
||||
record Values(String control, String property, String path, String direction, String converter) {}
|
||||
enum Kind {
|
||||
DIRECT,
|
||||
COMPUTED;
|
||||
|
||||
String element() {
|
||||
return this == DIRECT ? "DirectBinding" : "ComputedBinding";
|
||||
}
|
||||
}
|
||||
|
||||
record Values(
|
||||
String control,
|
||||
String property,
|
||||
String path,
|
||||
String direction,
|
||||
String converter,
|
||||
String method) {
|
||||
Values(String control, String property, String path, String direction, String converter) {
|
||||
this(control, property, path, direction, converter, "");
|
||||
}
|
||||
|
||||
static Values computed(String control, String property, String method) {
|
||||
return new Values(control, property, "", "view", "", method);
|
||||
}
|
||||
}
|
||||
|
||||
record Entry(int index, Values values, boolean editable) {
|
||||
public String toString() {
|
||||
return values.control() + "." + values.property() + " ← " + values.path();
|
||||
return values.control()
|
||||
+ "."
|
||||
+ values.property()
|
||||
+ " ← "
|
||||
+ (values.method().isEmpty() ? values.path() : values.method());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +52,7 @@ final class DirectBindingDocument {
|
||||
}
|
||||
}
|
||||
|
||||
final Kind kind;
|
||||
final String text;
|
||||
final Document document;
|
||||
final XmlSource source;
|
||||
@@ -33,7 +61,12 @@ final class DirectBindingDocument {
|
||||
private final List<String> imports = new ArrayList<>();
|
||||
private final Map<Element, Integer> indexes = new IdentityHashMap<>();
|
||||
|
||||
DirectBindingDocument(String text) throws Exception {
|
||||
BindingDocument(String text) throws Exception {
|
||||
this(text, Kind.DIRECT);
|
||||
}
|
||||
|
||||
BindingDocument(String text, Kind kind) throws Exception {
|
||||
this.kind = kind;
|
||||
this.text = text;
|
||||
document = FxmlProjection.parse(text);
|
||||
source = new XmlSource(text);
|
||||
@@ -49,7 +82,7 @@ final class DirectBindingDocument {
|
||||
if (elements.size() != source.tags.size())
|
||||
throw new IllegalArgumentException("XML source ranges do not match parsed elements");
|
||||
for (var e : elements)
|
||||
if (binding(e, "DirectBinding"))
|
||||
if (binding(e, kind.element()))
|
||||
entries.add(new Entry(indexes.get(e), values(e), !hasElementChildren(e)));
|
||||
}
|
||||
|
||||
@@ -81,7 +114,10 @@ final class DirectBindingDocument {
|
||||
return named(e, name) && bindingList(e) != null;
|
||||
}
|
||||
|
||||
private static Values values(Element e) {
|
||||
private Values values(Element e) {
|
||||
if (kind == Kind.COMPUTED)
|
||||
return Values.computed(
|
||||
e.getAttribute("control"), e.getAttribute("property"), e.getAttribute("method"));
|
||||
return new Values(
|
||||
e.getAttribute("control"),
|
||||
e.getAttribute("property"),
|
||||
@@ -120,6 +156,10 @@ final class DirectBindingDocument {
|
||||
if (ids.getOrDefault(e.getAttribute("control"), 0) != 1)
|
||||
errors.add("Missing or ambiguous control: " + e.getAttribute("control"));
|
||||
if (e.getAttribute("property").isBlank()) errors.add("Missing property: " + target);
|
||||
if (binding(e, "ComputedBinding")
|
||||
&& !e.getAttribute("method")
|
||||
.matches("#?[\\p{javaJavaIdentifierStart}][\\p{javaJavaIdentifierPart}]*"))
|
||||
errors.add("Invalid or missing Controller method: " + target);
|
||||
if (binding(e, "DirectBinding")) {
|
||||
if (!e.getAttribute("path")
|
||||
.matches(
|
||||
@@ -165,17 +205,25 @@ final class DirectBindingDocument {
|
||||
if (original != null) {
|
||||
var changed = new LinkedHashMap<String, String>();
|
||||
Map<String, String> fields =
|
||||
Map.of(
|
||||
"control",
|
||||
value.control(),
|
||||
"property",
|
||||
value.property(),
|
||||
"path",
|
||||
value.path(),
|
||||
"direction",
|
||||
value.direction(),
|
||||
"converter",
|
||||
value.converter());
|
||||
kind == Kind.COMPUTED
|
||||
? Map.of(
|
||||
"control",
|
||||
value.control(),
|
||||
"property",
|
||||
value.property(),
|
||||
"method",
|
||||
value.method())
|
||||
: Map.of(
|
||||
"control",
|
||||
value.control(),
|
||||
"property",
|
||||
value.property(),
|
||||
"path",
|
||||
value.path(),
|
||||
"direction",
|
||||
value.direction(),
|
||||
"converter",
|
||||
value.converter());
|
||||
for (var f : fields.entrySet())
|
||||
if (!original.getAttribute(f.getKey()).equals(f.getValue()))
|
||||
changed.put(
|
||||
@@ -186,19 +234,25 @@ final class DirectBindingDocument {
|
||||
String node =
|
||||
"<"
|
||||
+ PACKAGE
|
||||
+ "DirectBinding control=\""
|
||||
+ kind.element()
|
||||
+ " control=\""
|
||||
+ XmlSource.escape(value.control(), '"')
|
||||
+ "\" property=\""
|
||||
+ XmlSource.escape(value.property(), '"')
|
||||
+ "\" path=\""
|
||||
+ XmlSource.escape(value.path(), '"')
|
||||
+ "\" direction=\""
|
||||
+ XmlSource.escape(value.direction(), '"')
|
||||
+ "\""
|
||||
+ (value.converter().isEmpty()
|
||||
? ""
|
||||
: " converter=\"" + XmlSource.escape(value.converter(), '"') + "\"")
|
||||
+ "/>";
|
||||
+ "\"";
|
||||
if (kind == Kind.COMPUTED)
|
||||
node += " method=\"" + XmlSource.escape(value.method(), '"') + "\"";
|
||||
else
|
||||
node +=
|
||||
" path=\""
|
||||
+ XmlSource.escape(value.path(), '"')
|
||||
+ "\" direction=\""
|
||||
+ XmlSource.escape(value.direction(), '"')
|
||||
+ "\""
|
||||
+ (value.converter().isEmpty()
|
||||
? ""
|
||||
: " converter=\"" + XmlSource.escape(value.converter(), '"') + "\"");
|
||||
node += "/>";
|
||||
var lists = elements.stream().filter(this::list).toList();
|
||||
if (!lists.isEmpty()) {
|
||||
Element container = lists.getFirst();
|
||||
+78
-32
@@ -6,19 +6,23 @@ import javafx.scene.control.*;
|
||||
import javafx.scene.layout.*;
|
||||
|
||||
/** View-wide declarations and an explicit, revision-bound edit form shared by both hosts. */
|
||||
final class DirectBindingEditor extends VBox {
|
||||
final class BindingEditor extends VBox {
|
||||
record Intent(
|
||||
BindingDocument.Kind kind,
|
||||
String source,
|
||||
Integer index,
|
||||
DirectBindingDocument.Values values,
|
||||
BindingDocument.Values values,
|
||||
boolean remove,
|
||||
boolean replace) {}
|
||||
|
||||
private final ListView<DirectBindingDocument.Entry> bindings = new ListView<>();
|
||||
private final ListView<BindingDocument.Entry> bindings = new ListView<>();
|
||||
private final TextField control = field("control"),
|
||||
property = field("property"),
|
||||
path = field("path"),
|
||||
converter = field("converter");
|
||||
converter = field("converter"),
|
||||
method = field("method");
|
||||
private final ComboBox<BindingMetadata.Computed> methods = new ComboBox<>();
|
||||
private final BindingDocument.Kind kind;
|
||||
private final ComboBox<BindingMetadata.Converter> converters = new ComboBox<>();
|
||||
private final Button inspect = new Button("Find Converters / Validate");
|
||||
private final ComboBox<String> direction = new ComboBox<>();
|
||||
@@ -32,11 +36,30 @@ final class DirectBindingEditor extends VBox {
|
||||
private boolean loading, writable, active, followingCanvas;
|
||||
private final Consumer<String> navigate;
|
||||
|
||||
DirectBindingEditor(
|
||||
BindingEditor(
|
||||
BindingDocument.Kind kind,
|
||||
Consumer<Intent> edit,
|
||||
Consumer<String> navigate,
|
||||
Consumer<DirectBindingDocument.Values> discover) {
|
||||
Consumer<BindingDocument.Values> discover) {
|
||||
this.kind = kind;
|
||||
boolean computed = kind == BindingDocument.Kind.COMPUTED;
|
||||
String prefix = computed ? "computed-" : "direct-";
|
||||
this.navigate = navigate;
|
||||
for (var field : java.util.List.of(control, property, path, converter, method))
|
||||
field.setId(prefix + field.getId());
|
||||
methods.setId("computed-sources");
|
||||
methods.setPrefWidth(680);
|
||||
methods.setOnAction(
|
||||
e -> {
|
||||
var m = methods.getValue();
|
||||
if (m != null) method.setText(m.method());
|
||||
});
|
||||
if (computed) {
|
||||
inspect.setText("Find methods / Validate");
|
||||
fresh.setText("New Computed binding");
|
||||
apply.setText("Apply Computed binding");
|
||||
remove.setText("Remove Computed binding");
|
||||
}
|
||||
setSpacing(5);
|
||||
converters.setId("direct-converters");
|
||||
converters.setPrefWidth(680);
|
||||
@@ -49,18 +72,24 @@ final class DirectBindingEditor extends VBox {
|
||||
for (var field : java.util.List.of(control, property, path))
|
||||
field.textProperty().addListener((o, a, b) -> converters.getItems().clear());
|
||||
direction.valueProperty().addListener((o, a, b) -> converters.getItems().clear());
|
||||
bindings.setId("direct-bindings");
|
||||
bindings.setId(prefix + "bindings");
|
||||
bindings.setPrefHeight(100);
|
||||
direction.setId("direct-direction");
|
||||
direction.getItems().setAll("view", "model", "both");
|
||||
direction.setValue("both");
|
||||
replace.setId("direct-replace");
|
||||
status.setId("direct-status");
|
||||
replace.setId(prefix + "replace");
|
||||
status.setId(prefix + "status");
|
||||
status.setWrapText(true);
|
||||
fresh.setOnAction(e -> newBinding(new DirectBindingDocument.Values("", "", "", "both", "")));
|
||||
fresh.setOnAction(
|
||||
e ->
|
||||
newBinding(
|
||||
computed
|
||||
? BindingDocument.Values.computed("", "", "")
|
||||
: new BindingDocument.Values("", "", "", "both", "")));
|
||||
apply.setOnAction(
|
||||
e -> edit.accept(new Intent(source, selected, values(), false, replace.isSelected())));
|
||||
remove.setOnAction(e -> edit.accept(new Intent(source, selected, values(), true, false)));
|
||||
e ->
|
||||
edit.accept(new Intent(kind, source, selected, values(), false, replace.isSelected())));
|
||||
remove.setOnAction(e -> edit.accept(new Intent(kind, source, selected, values(), true, false)));
|
||||
bindings
|
||||
.getSelectionModel()
|
||||
.selectedItemProperty()
|
||||
@@ -81,20 +110,28 @@ final class DirectBindingEditor extends VBox {
|
||||
var grid = new GridPane();
|
||||
grid.setHgap(8);
|
||||
grid.setVgap(4);
|
||||
grid.addRow(
|
||||
0,
|
||||
new Label("Control (fx:id)"),
|
||||
control,
|
||||
new Label("Property"),
|
||||
property,
|
||||
new Label("Direction"),
|
||||
direction);
|
||||
grid.addRow(1, new Label("Model path"), path, new Label("Converter (optional)"), converter);
|
||||
if (!computed)
|
||||
grid.addRow(
|
||||
0,
|
||||
new Label("Control (fx:id)"),
|
||||
control,
|
||||
new Label("Property"),
|
||||
property,
|
||||
new Label("Direction"),
|
||||
direction);
|
||||
if (!computed)
|
||||
grid.addRow(1, new Label("Model path"), path, new Label("Converter (optional)"), converter);
|
||||
else {
|
||||
grid.addRow(0, new Label("Control (fx:id)"), control, new Label("Property"), property);
|
||||
grid.addRow(1, new Label("Controller method"), method);
|
||||
for (var field : java.util.List.of(control, property))
|
||||
field.textProperty().addListener((o, a, b) -> methods.getItems().clear());
|
||||
}
|
||||
getChildren()
|
||||
.addAll(
|
||||
bindings,
|
||||
grid,
|
||||
new HBox(8, inspect, converters),
|
||||
new HBox(8, inspect, computed ? methods : converters),
|
||||
new HBox(8, fresh, apply, remove, replace),
|
||||
status);
|
||||
buttons();
|
||||
@@ -102,13 +139,16 @@ final class DirectBindingEditor extends VBox {
|
||||
|
||||
private static TextField field(String name) {
|
||||
var f = new TextField();
|
||||
f.setId("direct-" + name);
|
||||
f.setId(name);
|
||||
f.setPrefColumnCount(14);
|
||||
return f;
|
||||
}
|
||||
|
||||
private DirectBindingDocument.Values values() {
|
||||
return new DirectBindingDocument.Values(
|
||||
private BindingDocument.Values values() {
|
||||
if (kind == BindingDocument.Kind.COMPUTED)
|
||||
return BindingDocument.Values.computed(
|
||||
control.getText().trim(), property.getText().trim(), method.getText().trim());
|
||||
return new BindingDocument.Values(
|
||||
control.getText().trim(),
|
||||
property.getText().trim(),
|
||||
path.getText().trim(),
|
||||
@@ -116,15 +156,16 @@ final class DirectBindingEditor extends VBox {
|
||||
converter.getText().trim());
|
||||
}
|
||||
|
||||
private void fill(DirectBindingDocument.Values v) {
|
||||
private void fill(BindingDocument.Values v) {
|
||||
control.setText(v.control());
|
||||
property.setText(v.property());
|
||||
path.setText(v.path());
|
||||
direction.setValue(v.direction());
|
||||
converter.setText(v.converter());
|
||||
method.setText(v.method());
|
||||
}
|
||||
|
||||
void newBinding(DirectBindingDocument.Values v) {
|
||||
void newBinding(BindingDocument.Values v) {
|
||||
loading = true;
|
||||
bindings.getSelectionModel().clearSelection();
|
||||
loading = false;
|
||||
@@ -144,19 +185,21 @@ final class DirectBindingEditor extends VBox {
|
||||
void load(String text, boolean editable) {
|
||||
if (source.equals(text) && writable == editable) return;
|
||||
converters.getItems().clear();
|
||||
methods.getItems().clear();
|
||||
source = text;
|
||||
writable = editable;
|
||||
loading = true;
|
||||
selected = null;
|
||||
active = false;
|
||||
try {
|
||||
var doc = new DirectBindingDocument(text);
|
||||
var doc = new BindingDocument(text, kind);
|
||||
bindings.getItems().setAll(doc.entries);
|
||||
bindings.getSelectionModel().clearSelection();
|
||||
status.setText(
|
||||
doc.errors().isEmpty()
|
||||
? "Select a DirectBinding or choose New binding. Use Find Converters / Validate to"
|
||||
+ " check this draft."
|
||||
? "Select a "
|
||||
+ kind.element()
|
||||
+ " or create a new binding. Validate to check this draft."
|
||||
: String.join("\n", doc.errors()));
|
||||
} catch (Exception error) {
|
||||
bindings.getItems().clear();
|
||||
@@ -186,8 +229,7 @@ final class DirectBindingEditor extends VBox {
|
||||
});
|
||||
}
|
||||
|
||||
void metadata(
|
||||
BindingMetadata metadata, DirectBindingDocument.Values requested, String provenance) {
|
||||
void metadata(BindingMetadata metadata, BindingDocument.Values requested, String provenance) {
|
||||
status.setText(
|
||||
provenance
|
||||
+ " · ViewModel: "
|
||||
@@ -209,6 +251,10 @@ final class DirectBindingEditor extends VBox {
|
||||
+ p.message())
|
||||
.collect(java.util.stream.Collectors.joining("\n"))));
|
||||
if (requested == null || !values().equals(requested)) return;
|
||||
if (kind == BindingDocument.Kind.COMPUTED) {
|
||||
methods.getItems().setAll(metadata.computed());
|
||||
return;
|
||||
}
|
||||
converters.getItems().setAll(metadata.converters());
|
||||
var matches =
|
||||
metadata.converters().stream().filter(c -> c.status().equals("COMPATIBLE")).toList();
|
||||
@@ -43,6 +43,7 @@ public final class DesignerPane extends BorderPane implements AutoCloseable {
|
||||
if (hit == null) return;
|
||||
String id = hit.getFxId();
|
||||
if (bindingEditor != null) bindingEditor.selectControl(id);
|
||||
if (computedEditor != null) computedEditor.selectControl(id);
|
||||
if (id == null || !projection.components().containsKey(id)) return;
|
||||
componentInspector.getChildren().add(new Label(projection.components().get(id) + " · " + id));
|
||||
componentInspector
|
||||
@@ -114,7 +115,7 @@ public final class DesignerPane extends BorderPane implements AutoCloseable {
|
||||
private DesignerWorkflow interactiveWorkflow;
|
||||
private DesignerWorkflow bindingWorkflow;
|
||||
private BindingBrowser bindingBrowser;
|
||||
private DirectBindingEditor bindingEditor;
|
||||
private BindingEditor bindingEditor, computedEditor;
|
||||
private String validatedSource, validatingSource;
|
||||
private boolean validationPreview;
|
||||
private java.util.List<de.neitzel.fxdesigner.protocol.BindingMetadata.Problem> bindingProblems =
|
||||
@@ -180,7 +181,18 @@ public final class DesignerPane extends BorderPane implements AutoCloseable {
|
||||
launch.tools(),
|
||||
this::log);
|
||||
bindingEditor =
|
||||
new DirectBindingEditor(
|
||||
new BindingEditor(
|
||||
BindingDocument.Kind.DIRECT,
|
||||
this::editBinding,
|
||||
id -> {
|
||||
if (kit.editor().getFxomDocument() == null) return;
|
||||
var target = new FXOMFxIdIndex(kit.editor().getFxomDocument()).lookup(id);
|
||||
if (target != null) kit.editor().getSelection().select(target);
|
||||
},
|
||||
values -> validateBindings(values, false));
|
||||
computedEditor =
|
||||
new BindingEditor(
|
||||
BindingDocument.Kind.COMPUTED,
|
||||
this::editBinding,
|
||||
id -> {
|
||||
if (kit.editor().getFxomDocument() == null) return;
|
||||
@@ -334,7 +346,10 @@ public final class DesignerPane extends BorderPane implements AutoCloseable {
|
||||
discoveryTab.setClosable(false);
|
||||
directBindingTab = new Tab("Direct bindings", bindingEditor);
|
||||
directBindingTab.setClosable(false);
|
||||
var bindings = new TitledPane("Bindings", new TabPane(directBindingTab, discoveryTab));
|
||||
var computedTab = new Tab("Computed bindings", computedEditor);
|
||||
computedTab.setClosable(false);
|
||||
var bindings =
|
||||
new TitledPane("Bindings", new TabPane(directBindingTab, computedTab, discoveryTab));
|
||||
bindings.setExpanded(false);
|
||||
center.setBottom(new VBox(properties, bindings));
|
||||
output.setEditable(false);
|
||||
@@ -475,6 +490,7 @@ public final class DesignerPane extends BorderPane implements AutoCloseable {
|
||||
}
|
||||
draft = value;
|
||||
bindingEditor.load(value.text(), value.writable());
|
||||
computedEditor.load(value.text(), value.writable());
|
||||
submittedSource = null;
|
||||
commitSource();
|
||||
return;
|
||||
@@ -497,6 +513,7 @@ public final class DesignerPane extends BorderPane implements AutoCloseable {
|
||||
String selectedId = hit == null ? null : hit.getFxId();
|
||||
draft = value;
|
||||
bindingEditor.load(value.text(), value.writable());
|
||||
computedEditor.load(value.text(), value.writable());
|
||||
if (!source.getText().equals(value.text())) {
|
||||
int anchor = source.getAnchor(), caret = source.getCaretPosition();
|
||||
var edits =
|
||||
@@ -651,14 +668,14 @@ public final class DesignerPane extends BorderPane implements AutoCloseable {
|
||||
bindingBrowser.invalidate(message);
|
||||
}
|
||||
|
||||
private void editBinding(DirectBindingEditor.Intent intent) {
|
||||
private void editBinding(BindingEditor.Intent intent) {
|
||||
withDraft(
|
||||
() -> {
|
||||
try {
|
||||
if (!draft.writable()) throw new IllegalStateException("Document is read-only");
|
||||
if (!draft.text().equals(intent.source()))
|
||||
throw new IllegalStateException("Document changed; select the binding again");
|
||||
var bindings = new DirectBindingDocument(draft.text());
|
||||
var bindings = new BindingDocument(draft.text(), intent.kind());
|
||||
var edits =
|
||||
intent.remove()
|
||||
? bindings.remove(java.util.Objects.requireNonNull(intent.index()))
|
||||
@@ -666,17 +683,18 @@ public final class DesignerPane extends BorderPane implements AutoCloseable {
|
||||
submit(
|
||||
edits,
|
||||
intent.remove()
|
||||
? "Remove DirectBinding"
|
||||
: intent.index() == null ? "Create DirectBinding" : "Edit DirectBinding");
|
||||
? "Remove " + intent.kind().element()
|
||||
: (intent.index() == null ? "Create " : "Edit ") + intent.kind().element());
|
||||
} catch (Exception error) {
|
||||
bindingEditor.error(error.getMessage());
|
||||
(intent.kind() == BindingDocument.Kind.COMPUTED ? computedEditor : bindingEditor)
|
||||
.error(error.getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private boolean bindingPreviewAllowed() {
|
||||
try {
|
||||
var errors = new DirectBindingDocument(draft.text()).errors();
|
||||
var errors = new BindingDocument(draft.text()).errors();
|
||||
if (errors.isEmpty()) return true;
|
||||
status.setText("Preview blocked: " + String.join("; ", errors));
|
||||
if (projection != null)
|
||||
@@ -690,7 +708,7 @@ public final class DesignerPane extends BorderPane implements AutoCloseable {
|
||||
return false;
|
||||
}
|
||||
|
||||
private void validateBindings(DirectBindingDocument.Values values, boolean preview) {
|
||||
private void validateBindings(BindingDocument.Values values, boolean preview) {
|
||||
withDraft(
|
||||
() -> {
|
||||
if (preview && draft.text().equals(validatingSource)) {
|
||||
@@ -735,6 +753,14 @@ public final class DesignerPane extends BorderPane implements AutoCloseable {
|
||||
: "Current successful build")
|
||||
+ " · draft revision "
|
||||
+ captured.revision());
|
||||
computedEditor.metadata(
|
||||
metadata,
|
||||
values,
|
||||
(last
|
||||
? "Explicit retained build (outdated)"
|
||||
: "Current successful build")
|
||||
+ " · draft revision "
|
||||
+ captured.revision());
|
||||
status.setText(
|
||||
"Binding validation complete: "
|
||||
+ metadata.problems().size()
|
||||
|
||||
@@ -0,0 +1,240 @@
|
||||
package de.neitzel.fxdesigner.kit;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import de.neitzel.fxdesigner.contracts.*;
|
||||
import java.net.URI;
|
||||
import java.util.*;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class BindingDocumentTest {
|
||||
private static final String P = "de.neitzel.fx.view.";
|
||||
private static final BindingDocument.Values VALUE =
|
||||
new BindingDocument.Values("label", "text", "name", "view", "");
|
||||
|
||||
private String apply(String text, List<EditBatch.Replacement> edits) throws Exception {
|
||||
String result =
|
||||
new EditBatch(7, "binding", edits)
|
||||
.applyTo(new DocumentSnapshot(URI.create("file:///test.fxml"), 7, text, true, false));
|
||||
new BindingDocument(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Test
|
||||
void createsRequiredContainersWithoutSerializingTheDocument() throws Exception {
|
||||
for (String root :
|
||||
List.of(
|
||||
"<VBox/>",
|
||||
"<VBox xmlns:fx='urn:other'/>",
|
||||
"<VBox><!-- untouched -->\r\n</VBox>",
|
||||
"<VBox xmlns:f='http://javafx.com/fxml/1'><f:define/></VBox>",
|
||||
"<VBox xmlns:f='http://javafx.com/fxml/1'><f:define><"
|
||||
+ P
|
||||
+ "BindingList/></f:define></VBox>")) {
|
||||
String text = "<?neitzel-model app.Person?>\n" + root;
|
||||
var doc = new BindingDocument(text);
|
||||
String result = apply(text, doc.write(null, VALUE, false));
|
||||
var created = new BindingDocument(result);
|
||||
assertEquals(1, created.entries.size());
|
||||
assertEquals(VALUE, created.entries.getFirst().values());
|
||||
assertTrue(result.startsWith("<?neitzel-model app.Person?>\n"));
|
||||
assertFalse(result.contains("<?import"));
|
||||
if (text.contains("untouched")) assertTrue(result.contains("<!-- untouched -->\r\n"));
|
||||
if (text.contains("urn:other"))
|
||||
assertTrue(result.contains("xmlns:fx1=\"http://javafx.com/fxml/1\""));
|
||||
}
|
||||
}
|
||||
|
||||
private String existing() {
|
||||
return "<?neitzel-model app.Person?>\n"
|
||||
+ "<?import de.neitzel.fx.view.*?>\n"
|
||||
+ "<VBox xmlns:fx='http://javafx.com/fxml/1'><!-- before"
|
||||
+ " --><fx:define><BindingList><!-- preserve --><DirectBinding control = 'label'"
|
||||
+ " property='text' path = 'name' direction='view' converter='app.Old'"
|
||||
+ " custom='untouched'/><!-- after --><app.Unknown"
|
||||
+ " value='keep'/></BindingList></fx:define><Label fx:id='label'/></VBox>";
|
||||
}
|
||||
|
||||
@Test
|
||||
void changesOnlyRequestedAttributeRangesAndRetainsUnknownContent() throws Exception {
|
||||
String text = existing();
|
||||
var doc = new BindingDocument(text);
|
||||
int index = doc.entries.getFirst().index();
|
||||
var value = new BindingDocument.Values("label", "text", "home.street", "both", "app.New");
|
||||
String updated = apply(text, doc.write(index, value, false));
|
||||
assertEquals(
|
||||
text.replace("path = 'name'", "path = 'home.street'")
|
||||
.replace("direction='view'", "direction='both'")
|
||||
.replace("converter='app.Old'", "converter='app.New'"),
|
||||
updated);
|
||||
assertTrue(new BindingDocument(updated).write(index, value, false).isEmpty());
|
||||
String escaped =
|
||||
apply(
|
||||
text,
|
||||
doc.write(
|
||||
index, new BindingDocument.Values("label", "text", "a'&<", "view", ""), false));
|
||||
assertEquals("a'&<", new BindingDocument(escaped).entries.getFirst().values().path());
|
||||
assertFalse(escaped.contains("converter='app.Old'"));
|
||||
assertFalse(new BindingDocument(escaped).errors().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void removesOnlyTheSelectedElementIncludingPairedTags() throws Exception {
|
||||
String text =
|
||||
existing()
|
||||
.replace("custom='untouched'/>", "custom='untouched'><!-- inner --></DirectBinding>");
|
||||
var doc = new BindingDocument(text);
|
||||
var tag = doc.source.tags.get(doc.entries.getFirst().index());
|
||||
String result = apply(text, doc.remove(doc.entries.getFirst().index()));
|
||||
assertEquals(text.substring(0, tag.start()) + text.substring(tag.end()), result);
|
||||
assertTrue(result.contains("<!-- preserve --><!-- after -->"));
|
||||
assertTrue(result.contains("app.Unknown"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void replacesAllConflictingDirectAndComputedBindingsAtomically() throws Exception {
|
||||
String text =
|
||||
existing()
|
||||
.replace(
|
||||
"<!-- after -->",
|
||||
"<ComputedBinding control='label' property='text' method='#caption'/><!-- after"
|
||||
+ " -->");
|
||||
var doc = new BindingDocument(text);
|
||||
assertTrue(doc.errors().stream().anyMatch(s -> s.contains("Duplicate")));
|
||||
assertThrows(BindingDocument.Conflict.class, () -> doc.write(null, VALUE, false));
|
||||
String replaced = apply(text, doc.write(null, VALUE, true));
|
||||
var parsed = new BindingDocument(replaced);
|
||||
assertEquals(1, parsed.entries.size());
|
||||
assertFalse(replaced.contains("<ComputedBinding"));
|
||||
assertTrue(parsed.errors().isEmpty());
|
||||
assertTrue(replaced.contains("app.Unknown"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void supportsExplicitElementsWrapperAndRefusesNestedPropertyEdits() throws Exception {
|
||||
String text =
|
||||
"<?import de.neitzel.fx.view.*?><VBox"
|
||||
+ " xmlns:fx='http://javafx.com/fxml/1'><fx:define><BindingList><elements><DirectBinding"
|
||||
+ " control='label'><property>text</property></DirectBinding></elements></BindingList></fx:define></VBox>";
|
||||
var doc = new BindingDocument(text);
|
||||
assertFalse(doc.entries.getFirst().editable());
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> doc.write(doc.entries.getFirst().index(), VALUE, false));
|
||||
String result =
|
||||
apply(
|
||||
text,
|
||||
doc.write(
|
||||
null, new BindingDocument.Values("other", "text", "name", "view", ""), false));
|
||||
assertEquals(2, new BindingDocument(result).entries.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void usesOnlyTheLibraryDeclarationsAndRejectsMalformedXml() throws Exception {
|
||||
String text =
|
||||
existing().replace("<?import de.neitzel.fx.view.*?>", "<?import app.DirectBinding?>");
|
||||
assertTrue(new BindingDocument(text).entries.isEmpty());
|
||||
assertThrows(Exception.class, () -> new BindingDocument("<VBox>"));
|
||||
assertThrows(
|
||||
Exception.class, () -> new BindingDocument("<!DOCTYPE x SYSTEM 'file:///etc/passwd'><x/>"));
|
||||
}
|
||||
|
||||
private BindingDocument computed(String text) throws Exception {
|
||||
return new BindingDocument(text, BindingDocument.Kind.COMPUTED);
|
||||
}
|
||||
|
||||
@Test
|
||||
void computedCreatesContainersAndRoundTripsEscapedMethods() throws Exception {
|
||||
for (String root :
|
||||
List.of(
|
||||
"<VBox/>",
|
||||
"<VBox xmlns:f='http://javafx.com/fxml/1'><f:define/></VBox>",
|
||||
"<VBox xmlns:f='http://javafx.com/fxml/1'><f:define><"
|
||||
+ P
|
||||
+ "BindingList><elements/></"
|
||||
+ P
|
||||
+ "BindingList></f:define></VBox>")) {
|
||||
String text = "<?neitzel-model app.Person?>\r\n" + root;
|
||||
var value = BindingDocument.Values.computed("label", "text", "#a'&<");
|
||||
String result = apply(text, computed(text).write(null, value, false));
|
||||
assertEquals(value, computed(result).entries.getFirst().values());
|
||||
assertTrue(result.startsWith("<?neitzel-model app.Person?>\r\n"));
|
||||
assertFalse(result.contains(" direction="));
|
||||
assertFalse(result.contains(" path="));
|
||||
assertTrue(computed(result).errors().stream().anyMatch(e -> e.contains("Controller method")));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void computedChangesOnlyItsFieldsAndRemovesPairedDeclaration() throws Exception {
|
||||
String text =
|
||||
existing()
|
||||
.replace(
|
||||
"<app.Unknown",
|
||||
"<ComputedBinding control = 'other' property='text' method = '#old'"
|
||||
+ " custom='keep'><!-- inner --></ComputedBinding><Label"
|
||||
+ " fx:id='other'/><app.Unknown");
|
||||
var doc = computed(text);
|
||||
int index = doc.entries.getFirst().index();
|
||||
var value = BindingDocument.Values.computed("label", "accessibleText", "greeting");
|
||||
String changed = apply(text, doc.write(index, value, false));
|
||||
assertEquals(
|
||||
text.replace("control = 'other'", "control = 'label'")
|
||||
.replace("property='text' method", "property='accessibleText' method")
|
||||
.replace("method = '#old'", "method = 'greeting'"),
|
||||
changed);
|
||||
assertTrue(computed(changed).write(index, value, false).isEmpty());
|
||||
String removed = apply(changed, computed(changed).remove(index));
|
||||
assertTrue(computed(removed).entries.isEmpty());
|
||||
assertEquals(1, new BindingDocument(removed).entries.size());
|
||||
assertTrue(removed.contains("<!-- preserve -->"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void computedReplacementRemovesDirectAndComputedConflictsTogether() throws Exception {
|
||||
String text =
|
||||
existing()
|
||||
.replace(
|
||||
"<!-- after -->",
|
||||
"<ComputedBinding control='label' property='text' method='#caption'/><!-- after"
|
||||
+ " -->");
|
||||
var doc = computed(text);
|
||||
var value = BindingDocument.Values.computed("label", "text", "#greeting");
|
||||
assertThrows(BindingDocument.Conflict.class, () -> doc.write(null, value, false));
|
||||
String result = apply(text, doc.write(null, value, true));
|
||||
assertTrue(new BindingDocument(result).entries.isEmpty());
|
||||
assertEquals(1, computed(result).entries.size());
|
||||
assertTrue(computed(result).errors().isEmpty());
|
||||
assertTrue(result.contains("<!-- preserve -->"));
|
||||
// Reverse replacement uses the same conflict policy.
|
||||
assertThrows(
|
||||
BindingDocument.Conflict.class,
|
||||
() -> new BindingDocument(result).write(null, VALUE, false));
|
||||
String direct = apply(result, new BindingDocument(result).write(null, VALUE, true));
|
||||
assertTrue(computed(direct).entries.isEmpty());
|
||||
assertEquals(1, new BindingDocument(direct).entries.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void computedNestedSyntaxIsVisibleRemovableButNotFlattened() throws Exception {
|
||||
String text =
|
||||
existing()
|
||||
.replace(
|
||||
"<app.Unknown",
|
||||
"<ComputedBinding control='other'"
|
||||
+ " property='text'><method>#caption</method></ComputedBinding><app.Unknown");
|
||||
var doc = computed(text);
|
||||
var entry = doc.entries.getFirst();
|
||||
assertFalse(entry.editable());
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() ->
|
||||
doc.write(
|
||||
entry.index(), BindingDocument.Values.computed("other", "text", "#new"), false));
|
||||
assertTrue(computed(apply(text, doc.remove(entry.index()))).entries.isEmpty());
|
||||
assertTrue(
|
||||
computed(text.replace("<?import de.neitzel.fx.view.*?>", "<?import app.ComputedBinding?>"))
|
||||
.entries
|
||||
.isEmpty());
|
||||
}
|
||||
}
|
||||
-146
@@ -1,146 +0,0 @@
|
||||
package de.neitzel.fxdesigner.kit;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import de.neitzel.fxdesigner.contracts.*;
|
||||
import java.net.URI;
|
||||
import java.util.*;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class DirectBindingDocumentTest {
|
||||
private static final String P = "de.neitzel.fx.view.";
|
||||
private static final DirectBindingDocument.Values VALUE =
|
||||
new DirectBindingDocument.Values("label", "text", "name", "view", "");
|
||||
|
||||
private String apply(String text, List<EditBatch.Replacement> edits) throws Exception {
|
||||
String result =
|
||||
new EditBatch(7, "binding", edits)
|
||||
.applyTo(new DocumentSnapshot(URI.create("file:///test.fxml"), 7, text, true, false));
|
||||
new DirectBindingDocument(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Test
|
||||
void createsRequiredContainersWithoutSerializingTheDocument() throws Exception {
|
||||
for (String root :
|
||||
List.of(
|
||||
"<VBox/>",
|
||||
"<VBox xmlns:fx='urn:other'/>",
|
||||
"<VBox><!-- untouched -->\r\n</VBox>",
|
||||
"<VBox xmlns:f='http://javafx.com/fxml/1'><f:define/></VBox>",
|
||||
"<VBox xmlns:f='http://javafx.com/fxml/1'><f:define><"
|
||||
+ P
|
||||
+ "BindingList/></f:define></VBox>")) {
|
||||
String text = "<?neitzel-model app.Person?>\n" + root;
|
||||
var doc = new DirectBindingDocument(text);
|
||||
String result = apply(text, doc.write(null, VALUE, false));
|
||||
var created = new DirectBindingDocument(result);
|
||||
assertEquals(1, created.entries.size());
|
||||
assertEquals(VALUE, created.entries.getFirst().values());
|
||||
assertTrue(result.startsWith("<?neitzel-model app.Person?>\n"));
|
||||
assertFalse(result.contains("<?import"));
|
||||
if (text.contains("untouched")) assertTrue(result.contains("<!-- untouched -->\r\n"));
|
||||
if (text.contains("urn:other"))
|
||||
assertTrue(result.contains("xmlns:fx1=\"http://javafx.com/fxml/1\""));
|
||||
}
|
||||
}
|
||||
|
||||
private String existing() {
|
||||
return "<?neitzel-model app.Person?>\n"
|
||||
+ "<?import de.neitzel.fx.view.*?>\n"
|
||||
+ "<VBox xmlns:fx='http://javafx.com/fxml/1'><!-- before"
|
||||
+ " --><fx:define><BindingList><!-- preserve --><DirectBinding control = 'label'"
|
||||
+ " property='text' path = 'name' direction='view' converter='app.Old'"
|
||||
+ " custom='untouched'/><!-- after --><app.Unknown"
|
||||
+ " value='keep'/></BindingList></fx:define><Label fx:id='label'/></VBox>";
|
||||
}
|
||||
|
||||
@Test
|
||||
void changesOnlyRequestedAttributeRangesAndRetainsUnknownContent() throws Exception {
|
||||
String text = existing();
|
||||
var doc = new DirectBindingDocument(text);
|
||||
int index = doc.entries.getFirst().index();
|
||||
var value = new DirectBindingDocument.Values("label", "text", "home.street", "both", "app.New");
|
||||
String updated = apply(text, doc.write(index, value, false));
|
||||
assertEquals(
|
||||
text.replace("path = 'name'", "path = 'home.street'")
|
||||
.replace("direction='view'", "direction='both'")
|
||||
.replace("converter='app.Old'", "converter='app.New'"),
|
||||
updated);
|
||||
assertTrue(new DirectBindingDocument(updated).write(index, value, false).isEmpty());
|
||||
String escaped =
|
||||
apply(
|
||||
text,
|
||||
doc.write(
|
||||
index,
|
||||
new DirectBindingDocument.Values("label", "text", "a'&<", "view", ""),
|
||||
false));
|
||||
assertEquals("a'&<", new DirectBindingDocument(escaped).entries.getFirst().values().path());
|
||||
assertFalse(escaped.contains("converter='app.Old'"));
|
||||
assertFalse(new DirectBindingDocument(escaped).errors().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void removesOnlyTheSelectedElementIncludingPairedTags() throws Exception {
|
||||
String text =
|
||||
existing()
|
||||
.replace("custom='untouched'/>", "custom='untouched'><!-- inner --></DirectBinding>");
|
||||
var doc = new DirectBindingDocument(text);
|
||||
var tag = doc.source.tags.get(doc.entries.getFirst().index());
|
||||
String result = apply(text, doc.remove(doc.entries.getFirst().index()));
|
||||
assertEquals(text.substring(0, tag.start()) + text.substring(tag.end()), result);
|
||||
assertTrue(result.contains("<!-- preserve --><!-- after -->"));
|
||||
assertTrue(result.contains("app.Unknown"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void replacesAllConflictingDirectAndComputedBindingsAtomically() throws Exception {
|
||||
String text =
|
||||
existing()
|
||||
.replace(
|
||||
"<!-- after -->",
|
||||
"<ComputedBinding control='label' property='text' method='#caption'/><!-- after"
|
||||
+ " -->");
|
||||
var doc = new DirectBindingDocument(text);
|
||||
assertTrue(doc.errors().stream().anyMatch(s -> s.contains("Duplicate")));
|
||||
assertThrows(DirectBindingDocument.Conflict.class, () -> doc.write(null, VALUE, false));
|
||||
String replaced = apply(text, doc.write(null, VALUE, true));
|
||||
var parsed = new DirectBindingDocument(replaced);
|
||||
assertEquals(1, parsed.entries.size());
|
||||
assertFalse(replaced.contains("<ComputedBinding"));
|
||||
assertTrue(parsed.errors().isEmpty());
|
||||
assertTrue(replaced.contains("app.Unknown"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void supportsExplicitElementsWrapperAndRefusesNestedPropertyEdits() throws Exception {
|
||||
String text =
|
||||
"<?import de.neitzel.fx.view.*?><VBox"
|
||||
+ " xmlns:fx='http://javafx.com/fxml/1'><fx:define><BindingList><elements><DirectBinding"
|
||||
+ " control='label'><property>text</property></DirectBinding></elements></BindingList></fx:define></VBox>";
|
||||
var doc = new DirectBindingDocument(text);
|
||||
assertFalse(doc.entries.getFirst().editable());
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> doc.write(doc.entries.getFirst().index(), VALUE, false));
|
||||
String result =
|
||||
apply(
|
||||
text,
|
||||
doc.write(
|
||||
null,
|
||||
new DirectBindingDocument.Values("other", "text", "name", "view", ""),
|
||||
false));
|
||||
assertEquals(2, new DirectBindingDocument(result).entries.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void usesOnlyTheLibraryDeclarationsAndRejectsMalformedXml() throws Exception {
|
||||
String text =
|
||||
existing().replace("<?import de.neitzel.fx.view.*?>", "<?import app.DirectBinding?>");
|
||||
assertTrue(new DirectBindingDocument(text).entries.isEmpty());
|
||||
assertThrows(Exception.class, () -> new DirectBindingDocument("<VBox>"));
|
||||
assertThrows(
|
||||
Exception.class,
|
||||
() -> new DirectBindingDocument("<!DOCTYPE x SYSTEM 'file:///etc/passwd'><x/>"));
|
||||
}
|
||||
}
|
||||
@@ -482,6 +482,168 @@ public final class ProjectionHostChecks {
|
||||
await(() -> fx(() -> source(pane).getText().equals(full)));
|
||||
}
|
||||
|
||||
private static void computedField(DesignerPane pane, String field, String value)
|
||||
throws Exception {
|
||||
fx(
|
||||
() -> {
|
||||
((TextField) pane.lookup("#computed-" + field)).setText(value);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
private static void selectComputed(DesignerPane pane, String control) throws Exception {
|
||||
fx(
|
||||
() -> {
|
||||
var list = (ListView<Object>) pane.lookup("#computed-bindings");
|
||||
list.getSelectionModel()
|
||||
.select(
|
||||
list.getItems().stream()
|
||||
.filter(e -> e.toString().startsWith(control + "."))
|
||||
.findFirst()
|
||||
.orElseThrow());
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
private static void computedBindingEdits(DesignerPane pane, DocumentSession document, String full)
|
||||
throws Exception {
|
||||
bindingTab(pane, "Computed bindings");
|
||||
click(pane, "New Computed binding");
|
||||
computedField(pane, "control", "action");
|
||||
computedField(pane, "property", "text");
|
||||
click(pane, "Find methods / Validate");
|
||||
await(() -> fx(() -> !((ComboBox<?>) pane.lookup("#computed-sources")).getItems().isEmpty()));
|
||||
fx(
|
||||
() -> {
|
||||
var methods =
|
||||
(ComboBox<de.neitzel.fxdesigner.protocol.BindingMetadata.Computed>)
|
||||
pane.lookup("#computed-sources");
|
||||
var greeting =
|
||||
methods.getItems().stream()
|
||||
.filter(m -> m.method().equals("#greeting"))
|
||||
.findFirst()
|
||||
.orElseThrow();
|
||||
check(greeting.status().equals("COMPATIBLE"), "Computed method type comparison failed");
|
||||
methods.setValue(greeting);
|
||||
return null;
|
||||
});
|
||||
check(documentText(document).equals(full), "Method choice changed source before Apply");
|
||||
click(pane, "Apply Computed binding");
|
||||
await(() -> !documentText(document).equals(full));
|
||||
String created = documentText(document);
|
||||
check(
|
||||
created.contains(
|
||||
"ComputedBinding control=\"action\" property=\"text\" method=\"#greeting\""),
|
||||
"Computed creation failed");
|
||||
click(pane, "Undo");
|
||||
await(() -> documentText(document).equals(full));
|
||||
click(pane, "Redo");
|
||||
await(() -> documentText(document).equals(created));
|
||||
await(() -> fx(() -> source(pane).getText().equals(created)));
|
||||
selectComputed(pane, "action");
|
||||
computedField(pane, "property", "accessibleText");
|
||||
computedField(pane, "method", "greeting");
|
||||
click(pane, "Apply Computed binding");
|
||||
String changed =
|
||||
created.replace(
|
||||
"property=\"text\" method=\"#greeting\"",
|
||||
"property=\"accessibleText\" method=\"greeting\"");
|
||||
await(() -> documentText(document).equals(changed));
|
||||
click(pane, "Undo");
|
||||
await(() -> documentText(document).equals(created));
|
||||
click(pane, "Redo");
|
||||
await(() -> documentText(document).equals(changed));
|
||||
await(() -> fx(() -> source(pane).getText().equals(changed)));
|
||||
document
|
||||
.save(document.snapshot().toCompletableFuture().get().revision())
|
||||
.toCompletableFuture()
|
||||
.get();
|
||||
check(
|
||||
java.nio.file.Files.readString(
|
||||
java.nio.file.Path.of(document.snapshot().toCompletableFuture().get().document()))
|
||||
.equals(changed),
|
||||
"Computed save changed source");
|
||||
click(pane, "Render drafts");
|
||||
await(
|
||||
() ->
|
||||
fx(
|
||||
() ->
|
||||
object(pane, "home").getSceneGraphObject() instanceof SnapshotRegion r
|
||||
&& r.image() != null
|
||||
&& r.statusText().equals("Target runtime")));
|
||||
selectComputed(pane, "action");
|
||||
click(pane, "Remove Computed binding");
|
||||
await(() -> !documentText(document).equals(changed));
|
||||
String removed = documentText(document);
|
||||
check(!removed.contains("ComputedBinding"), "Computed removal failed");
|
||||
click(pane, "Undo");
|
||||
await(() -> documentText(document).equals(changed));
|
||||
click(pane, "Redo");
|
||||
await(() -> documentText(document).equals(removed));
|
||||
await(() -> fx(() -> source(pane).getText().equals(removed)));
|
||||
click(pane, "New Computed binding");
|
||||
computedField(pane, "control", "rankInput");
|
||||
computedField(pane, "property", "text");
|
||||
computedField(pane, "method", "#greeting");
|
||||
click(pane, "Apply Computed binding");
|
||||
await(
|
||||
() ->
|
||||
fx(
|
||||
() ->
|
||||
((Label) pane.lookup("#computed-status"))
|
||||
.getText()
|
||||
.contains("Replace conflicting bindings")));
|
||||
check(documentText(document).equals(removed), "Computed silently replaced Direct binding");
|
||||
fx(
|
||||
() -> {
|
||||
((CheckBox) pane.lookup("#computed-replace")).setSelected(true);
|
||||
return null;
|
||||
});
|
||||
click(pane, "Apply Computed binding");
|
||||
await(() -> !documentText(document).equals(removed));
|
||||
String replaced = documentText(document);
|
||||
check(
|
||||
directDeclarations(replaced).stream()
|
||||
.noneMatch(e -> e.getAttribute("control").equals("rankInput")),
|
||||
"Computed replacement retained Direct conflict");
|
||||
click(pane, "Undo");
|
||||
await(() -> documentText(document).equals(removed));
|
||||
click(pane, "Redo");
|
||||
await(() -> documentText(document).equals(replaced));
|
||||
await(() -> fx(() -> source(pane).getText().equals(replaced)));
|
||||
selectComputed(pane, "rankInput");
|
||||
computedField(pane, "method", "#invalidComputed");
|
||||
click(pane, "Apply Computed binding");
|
||||
await(() -> documentText(document).contains("method=\"#invalidComputed\""));
|
||||
String invalid = documentText(document);
|
||||
document
|
||||
.save(document.snapshot().toCompletableFuture().get().revision())
|
||||
.toCompletableFuture()
|
||||
.get();
|
||||
click(pane, "Render drafts");
|
||||
await(
|
||||
() ->
|
||||
fx(
|
||||
() ->
|
||||
pane.lookupAll(".label").stream()
|
||||
.anyMatch(
|
||||
n ->
|
||||
n instanceof Label l
|
||||
&& l.getText() != null
|
||||
&& l.getText().startsWith("Preview blocked:"))));
|
||||
check(
|
||||
java.nio.file.Files.readString(
|
||||
java.nio.file.Path.of(document.snapshot().toCompletableFuture().get().document()))
|
||||
.equals(invalid),
|
||||
"Invalid Computed draft not saved");
|
||||
click(pane, "Undo");
|
||||
await(() -> documentText(document).equals(replaced));
|
||||
click(pane, "Redo");
|
||||
await(() -> documentText(document).equals(invalid));
|
||||
set(document, full);
|
||||
await(() -> fx(() -> source(pane).getText().equals(full)));
|
||||
}
|
||||
|
||||
private static void viewModelBindings(
|
||||
DesignerPane pane, DocumentSession document, String original) throws Exception {
|
||||
bindingTab(pane, "Discover targets");
|
||||
@@ -859,6 +1021,7 @@ public final class ProjectionHostChecks {
|
||||
check(documentText(document).equals(full), "Discovery handoff changed source before Apply");
|
||||
converterEdits(pane, document, full);
|
||||
directBindingEdits(pane, document, full);
|
||||
computedBindingEdits(pane, document, full);
|
||||
document
|
||||
.save(document.snapshot().toCompletableFuture().get().revision())
|
||||
.toCompletableFuture()
|
||||
|
||||
Reference in New Issue
Block a user