Show one Address component per address so the pane stays in step with the person's list.
A second component of the same class is a new injected instance, leaving the cached singleton in place, and selection stays an attribute of the person. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -11,8 +11,8 @@ import java.util.Map;
|
||||
/**
|
||||
* The list of binding elements declared inside {@code fx:define}.
|
||||
* <p>
|
||||
* The list is not a scene-graph child. FXML adds each {@link DirectBinding} or
|
||||
* {@link ComputedBinding} child to {@link #getElements()}.
|
||||
* The list is not a scene-graph child. FXML adds each {@link DirectBinding},
|
||||
* {@link ComputedBinding}, or {@link Repeat} child to {@link #getElements()}.
|
||||
*/
|
||||
@DefaultProperty("elements")
|
||||
public class BindingList {
|
||||
@@ -29,8 +29,8 @@ public class BindingList {
|
||||
/**
|
||||
* Returns the binding elements in this list.
|
||||
* <p>
|
||||
* FXML appends child elements to the returned list. A child is a {@link DirectBinding} or a
|
||||
* {@link ComputedBinding}.
|
||||
* FXML appends child elements to the returned list. A child is a {@link DirectBinding}, a
|
||||
* {@link ComputedBinding}, or a {@link Repeat}.
|
||||
*
|
||||
* @return the mutable list of binding elements
|
||||
*/
|
||||
@@ -54,6 +54,8 @@ public class BindingList {
|
||||
DirectBindingApplier.apply(namespace, viewModel, directBinding, context);
|
||||
} else if (element instanceof ComputedBinding computedBinding) {
|
||||
ComputedBindingApplier.apply(namespace, viewModel, controller, root, computedBinding);
|
||||
} else if (element instanceof Repeat repeat) {
|
||||
RepeatApplier.apply(namespace, viewModel, repeat, context);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Not a binding element: " + element);
|
||||
}
|
||||
|
||||
@@ -31,7 +31,9 @@ final class Components {
|
||||
* <p>
|
||||
* An unknown type, such as a JavaFX control, makes {@link ApplicationContext#getComponent}
|
||||
* throw {@link IllegalArgumentException}. This method returns null so FXML constructs that
|
||||
* type itself. A failure while creating a known Component is not caught.
|
||||
* type itself. A failure while creating a known Component is not caught. The first Component
|
||||
* of a class in one load is the cached singleton. A second Component of that class is a new
|
||||
* injected instance, so the document can show two of them.
|
||||
*
|
||||
* @param type the type FXML is about to construct
|
||||
* @param context the container that creates Components
|
||||
@@ -44,7 +46,38 @@ final class Components {
|
||||
} catch (IllegalArgumentException notAComponent) {
|
||||
return null;
|
||||
}
|
||||
return () -> load(component, context);
|
||||
if (!LoadSession.claimFirst(type)) {
|
||||
component = context.createComponent(type);
|
||||
}
|
||||
Object created = component;
|
||||
return () -> load(created, context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new Component and loads its document.
|
||||
* <p>
|
||||
* The instance is not the cached singleton. Its dependencies still come from
|
||||
* {@link ApplicationContext#getComponent}. Bindings wait until {@link #setModel} runs.
|
||||
*
|
||||
* @param type the Component class
|
||||
* @param context the container that creates the instance and its dependencies
|
||||
* @return the loaded Component
|
||||
*/
|
||||
static Object fresh(Class<?> type, ApplicationContext context) {
|
||||
Object component = context.createComponent(type.asSubclass(Object.class));
|
||||
return load(component, context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the Component's model property.
|
||||
* <p>
|
||||
* A non-null value selects the ViewModel and then runs the Component's bindings.
|
||||
*
|
||||
* @param component the Component
|
||||
* @param model the Model instance
|
||||
*/
|
||||
static void setModel(Object component, Object model) {
|
||||
modelProperty(component).setValue(model);
|
||||
}
|
||||
|
||||
private static Object load(Object component, ApplicationContext context) {
|
||||
|
||||
@@ -3,7 +3,9 @@ package de.neitzel.fx.view;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Deque;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Holds the binding lists instantiated while one View is loading.
|
||||
@@ -18,6 +20,7 @@ final class LoadSession {
|
||||
private static final ThreadLocal<Deque<LoadSession>> STACK = new ThreadLocal<>();
|
||||
|
||||
private final List<BindingList> bindingLists = new ArrayList<>();
|
||||
private final Set<Class<?>> built = new HashSet<>();
|
||||
|
||||
private LoadSession() {
|
||||
}
|
||||
@@ -39,20 +42,33 @@ final class LoadSession {
|
||||
* Opens a nested session for a Component document.
|
||||
* <p>
|
||||
* Binding lists created until {@link #unnest()} register here, not on the parent session.
|
||||
* A Component created after the parent load has finished, such as a repeated child added
|
||||
* later, still gets a session so its own binding list can be captured.
|
||||
*
|
||||
* @return the nested session
|
||||
*/
|
||||
static LoadSession nest() {
|
||||
Deque<LoadSession> stack = STACK.get();
|
||||
if (stack == null) {
|
||||
stack = new ArrayDeque<>();
|
||||
STACK.set(stack);
|
||||
}
|
||||
LoadSession session = new LoadSession();
|
||||
STACK.get().push(session);
|
||||
stack.push(session);
|
||||
return session;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores the session that was current before {@link #nest()}.
|
||||
* <p>
|
||||
* When the nested session was the only one, the thread-local stack is dropped.
|
||||
*/
|
||||
static void unnest() {
|
||||
STACK.get().pop();
|
||||
Deque<LoadSession> stack = STACK.get();
|
||||
stack.pop();
|
||||
if (stack.isEmpty()) {
|
||||
STACK.remove();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -75,6 +91,23 @@ final class LoadSession {
|
||||
stack.peek().bindingLists.add(bindingList);
|
||||
}
|
||||
|
||||
/**
|
||||
* Records the first Component of {@code type} built during this load.
|
||||
* <p>
|
||||
* Nested documents share the outermost session, so a second Component of the same class
|
||||
* anywhere in the load is not the first.
|
||||
*
|
||||
* @param type the Component class
|
||||
* @return {@code true} when this is the first time that class is built during the load
|
||||
*/
|
||||
static boolean claimFirst(Class<?> type) {
|
||||
Deque<LoadSession> stack = STACK.get();
|
||||
if (stack == null || stack.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
return stack.peekLast().built.add(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the lists registered during this session, in document order.
|
||||
*
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
package de.neitzel.fx.view;
|
||||
|
||||
/**
|
||||
* A repeat element declared in a {@link BindingList}.
|
||||
* <p>
|
||||
* The element names the pane by {@code fx:id}, the Component class, and the collection
|
||||
* Attribute. The library keeps one Component child in that pane for each Model instance, in
|
||||
* list order.
|
||||
*/
|
||||
public class Repeat {
|
||||
|
||||
private String pane;
|
||||
private String component;
|
||||
private String collection;
|
||||
|
||||
/**
|
||||
* Sets the {@code fx:id} of the pane that holds the Component children.
|
||||
*
|
||||
* @param pane the pane id
|
||||
*/
|
||||
public void setPane(String pane) {
|
||||
this.pane = pane;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the Component class name. Each element gets a new instance of that class.
|
||||
*
|
||||
* @param component the class name
|
||||
*/
|
||||
public void setComponent(String component) {
|
||||
this.component = component;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the collection Attribute name. The list holds the Model instances.
|
||||
*
|
||||
* @param collection the attribute name
|
||||
*/
|
||||
public void setCollection(String collection) {
|
||||
this.collection = collection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@code fx:id} of the pane.
|
||||
*
|
||||
* @return the pane id
|
||||
*/
|
||||
public String getPane() {
|
||||
return pane;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Component class name.
|
||||
*
|
||||
* @return the class name
|
||||
*/
|
||||
public String getComponent() {
|
||||
return component;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the collection Attribute name.
|
||||
*
|
||||
* @return the attribute name
|
||||
*/
|
||||
public String getCollection() {
|
||||
return collection;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package de.neitzel.fx.view;
|
||||
|
||||
import de.neitzel.injection.ApplicationContext;
|
||||
import javafx.beans.property.Property;
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.layout.Pane;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Keeps one Component child in a pane for each Model instance in a collection.
|
||||
*/
|
||||
final class RepeatApplier {
|
||||
|
||||
private RepeatApplier() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Fills the named pane with one new Component for each element, in list order.
|
||||
* <p>
|
||||
* Each Component is a new injected instance. The cached singleton of that class is left
|
||||
* in place. The child's model property is the element. Adding or removing an element
|
||||
* adds or removes the matching child. The list is the one the entity holds, so a change
|
||||
* on either side is visible in the pane.
|
||||
*
|
||||
* @param namespace objects addressed by {@code fx:id}
|
||||
* @param viewModel the ViewModel for this View
|
||||
* @param repeat the declaration
|
||||
* @param context the container that creates each Component
|
||||
*/
|
||||
static void apply(Map<String, Object> namespace, ViewModel viewModel, Repeat repeat,
|
||||
ApplicationContext context) {
|
||||
Object named = namespace.get(repeat.getPane());
|
||||
if (!(named instanceof Pane pane)) {
|
||||
throw new IllegalArgumentException("No pane \"" + repeat.getPane() + "\"");
|
||||
}
|
||||
Property<?> property = viewModel.property(repeat.getCollection());
|
||||
if (property == null || !(property.getValue() instanceof ObservableList<?> values)) {
|
||||
throw new IllegalArgumentException("No collection \"" + repeat.getCollection() + "\"");
|
||||
}
|
||||
Class<?> type = componentType(repeat.getComponent());
|
||||
for (Object element : values) {
|
||||
pane.getChildren().add(child(type, context, element));
|
||||
}
|
||||
listen(pane, type, context, values);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static void listen(Pane pane, Class<?> type, ApplicationContext context, ObservableList<?> values) {
|
||||
ObservableList<Object> elements = (ObservableList<Object>) values;
|
||||
elements.addListener((ListChangeListener<Object>) change -> {
|
||||
while (change.next()) {
|
||||
if (change.wasPermutated() || change.wasUpdated()) {
|
||||
continue;
|
||||
}
|
||||
if (change.getRemovedSize() > 0) {
|
||||
pane.getChildren().remove(change.getFrom(), change.getFrom() + change.getRemovedSize());
|
||||
}
|
||||
for (int index = 0; index < change.getAddedSize(); index++) {
|
||||
pane.getChildren().add(
|
||||
change.getFrom() + index,
|
||||
child(type, context, change.getAddedSubList().get(index)));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static Node child(Class<?> type, ApplicationContext context, Object element) {
|
||||
Object component = Components.fresh(type, context);
|
||||
Components.setModel(component, element);
|
||||
return (Node) component;
|
||||
}
|
||||
|
||||
private static Class<?> componentType(String className) {
|
||||
try {
|
||||
return Class.forName(className);
|
||||
} catch (ClassNotFoundException failure) {
|
||||
throw new IllegalArgumentException("No component " + className, failure);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -37,11 +37,21 @@ public final class ViewLoader {
|
||||
* Attribute, with that Attribute's name as the source. A parent carries a copy with the source
|
||||
* extended. It does not fail the load.
|
||||
* A parent document names a Component class as an element. The context creates that Component
|
||||
* and injects its services. The Component document {@code ClassName.fxml}, in the same package,
|
||||
* is loaded with the Component as {@code fx:root} and as the Controller. That document's
|
||||
* bindings are not applied during this load. They run when the Component's model property
|
||||
* becomes a non-null Model. A Component that implements {@link HasViewModel} receives that
|
||||
* ViewModel after the property is set and before those bindings run.
|
||||
* and injects its services. A second element of the same class is another injected instance,
|
||||
* and the cached singleton stays the first one. The Component document {@code ClassName.fxml},
|
||||
* in the same package, is loaded with the Component as {@code fx:root} and as the Controller.
|
||||
* That document's bindings are not applied during this load. They run when the Component's
|
||||
* model property becomes a non-null Model. A Component that implements {@link HasViewModel}
|
||||
* receives that ViewModel after the property is set and before those bindings run.
|
||||
* A {@link Repeat} in the same list names a pane, a Component class, and a collection
|
||||
* Attribute. One new Component child is placed in that pane for each Model instance, in list
|
||||
* order. Adding or removing an element updates that pane. Those instances are created with
|
||||
* {@link de.neitzel.injection.ApplicationContext#createComponent} so the cached singleton of
|
||||
* that class stays in place. A collection of Model instances is an
|
||||
* {@link javafx.collections.ObservableList} of those instances, shared with the entity.
|
||||
* Selection is a separate Attribute of the parent Model. Null means nothing is selected.
|
||||
* A Direct binding can tie a control property to that Attribute. The repeated Component does
|
||||
* not store the selection.
|
||||
* Loading does not open a popup or fill a status bar. A Direct binding whose path is
|
||||
* {@code latestMessage} shows the latest Message text. {@link MessagePopup#showNewMessages}
|
||||
* shows each newly added Message in a dialog.
|
||||
|
||||
@@ -54,11 +54,12 @@ import java.util.function.Consumer;
|
||||
* Attribute, stores the ViewModel for the nested Model instance, or null when the Attribute
|
||||
* is null. Strings, numbers, booleans, enums, {@link Date}, {@link Calendar}, and {@code java.time}
|
||||
* values stay simple even when that type has setters. A collection of Model instances is not a
|
||||
* Complex attribute. A list, set, or collection of simple values is an object property whose
|
||||
* value is an {@link ObservableList} of those values. When that attribute's current value is a
|
||||
* list, the observable list is backed by it and stored on the entity when the setter accepts it,
|
||||
* so both sides hold one list. A cycle, or another Attribute that reaches the same instance,
|
||||
* reuses that ViewModel.
|
||||
* Complex attribute. It is an object property whose value is an {@link ObservableList} of those
|
||||
* Model instances, not of ViewModels. A list, set, or collection of simple values is an object
|
||||
* property whose value is an {@link ObservableList} of those values. When either attribute's
|
||||
* current value is a list, the observable list is backed by it and stored on the entity when the
|
||||
* setter accepts it, so both sides hold one list. A cycle, or another Attribute that reaches the
|
||||
* same instance, reuses that ViewModel.
|
||||
* The property is set from the Attribute when the ViewModel is created. A null number or
|
||||
* boolean leaves the primitive property at its default. After that, a change of the property
|
||||
* writes the Attribute. A Complex attribute writes the Model instance behind the ViewModel.
|
||||
@@ -113,7 +114,7 @@ public class ViewModel {
|
||||
properties.put(name, property);
|
||||
complexAttributes.add(name);
|
||||
} else {
|
||||
if (isSimpleValueCollection(getter)) {
|
||||
if (isSimpleValueCollection(getter) || isModelCollection(getter)) {
|
||||
current = shareSimpleList(setter, name, current);
|
||||
}
|
||||
Property<?> property = createProperty(type, current);
|
||||
@@ -545,6 +546,14 @@ public class ViewModel {
|
||||
return element != null && isSimpleValue(element);
|
||||
}
|
||||
|
||||
private static boolean isModelCollection(Method getter) {
|
||||
if (!Collection.class.isAssignableFrom(getter.getReturnType())) {
|
||||
return false;
|
||||
}
|
||||
Class<?> element = elementType(getter.getGenericReturnType());
|
||||
return element != null && isComplex(element);
|
||||
}
|
||||
|
||||
private static Class<?> elementType(Type type) {
|
||||
if (!(type instanceof ParameterizedType parameterized) || parameterized.getActualTypeArguments().length != 1) {
|
||||
return null;
|
||||
|
||||
Reference in New Issue
Block a user