Let a reused Component edit one nested Model after the container injects it.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,108 @@
|
|||||||
|
package de.neitzel.fx.view;
|
||||||
|
|
||||||
|
import de.neitzel.injection.ApplicationContext;
|
||||||
|
import javafx.beans.property.Property;
|
||||||
|
import javafx.fxml.FXMLLoader;
|
||||||
|
import javafx.scene.Parent;
|
||||||
|
import javafx.util.Builder;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.lang.reflect.InvocationTargetException;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads a Component from {@code ClassName.fxml} in the Component's package.
|
||||||
|
* <p>
|
||||||
|
* The container creates the instance. The document uses {@code fx:root} with that instance as
|
||||||
|
* the root and as the Controller. Bindings declared in the document wait until the model
|
||||||
|
* property holds a non-null Model. A Component that implements {@link HasViewModel} is handed
|
||||||
|
* that ViewModel after the property is set and before those bindings run.
|
||||||
|
*/
|
||||||
|
final class Components {
|
||||||
|
|
||||||
|
private Components() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a builder for a type the container knows.
|
||||||
|
* <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.
|
||||||
|
*
|
||||||
|
* @param type the type FXML is about to construct
|
||||||
|
* @param context the container that creates Components
|
||||||
|
* @return a builder for a Component, or {@code null} when the type is not a Component
|
||||||
|
*/
|
||||||
|
static Builder<?> builder(Class<?> type, ApplicationContext context) {
|
||||||
|
Object component;
|
||||||
|
try {
|
||||||
|
component = context.getComponent(type);
|
||||||
|
} catch (IllegalArgumentException notAComponent) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return () -> load(component, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Object load(Object component, ApplicationContext context) {
|
||||||
|
Class<?> type = component.getClass();
|
||||||
|
String document = type.getSimpleName() + ".fxml";
|
||||||
|
URL fxml = type.getResource(document);
|
||||||
|
if (fxml == null) {
|
||||||
|
throw new IllegalArgumentException("No component document " + document + " for " + type.getName());
|
||||||
|
}
|
||||||
|
LoadSession nested = LoadSession.nest();
|
||||||
|
try {
|
||||||
|
FXMLLoader loader = new FXMLLoader(fxml);
|
||||||
|
loader.setRoot(component);
|
||||||
|
loader.setController(component);
|
||||||
|
loader.setBuilderFactory(nestedType -> builder(nestedType, context));
|
||||||
|
loader.load();
|
||||||
|
watchModel(component, List.copyOf(nested.bindingLists()), loader.getNamespace(), context);
|
||||||
|
return component;
|
||||||
|
} catch (IOException failure) {
|
||||||
|
throw new IllegalStateException("Could not load component " + fxml, failure);
|
||||||
|
} finally {
|
||||||
|
LoadSession.unnest();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void watchModel(Object component, List<BindingList> lists, Map<String, Object> namespace,
|
||||||
|
ApplicationContext context) {
|
||||||
|
Property<Object> model = modelProperty(component);
|
||||||
|
boolean[] applied = {false};
|
||||||
|
model.addListener((observable, oldValue, newValue) -> {
|
||||||
|
if (newValue == null || applied[0]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
applied[0] = true;
|
||||||
|
ViewModel viewModel = ViewModels.of(newValue);
|
||||||
|
if (component instanceof HasViewModel hasViewModel) {
|
||||||
|
hasViewModel.setViewModel(viewModel);
|
||||||
|
}
|
||||||
|
Parent root = (Parent) component;
|
||||||
|
for (BindingList list : lists) {
|
||||||
|
list.apply(namespace, viewModel, component, root, context);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private static Property<Object> modelProperty(Object component) {
|
||||||
|
try {
|
||||||
|
Method method = component.getClass().getMethod("modelProperty");
|
||||||
|
Object value = method.invoke(component);
|
||||||
|
if (!(value instanceof Property<?> property)) {
|
||||||
|
throw new IllegalArgumentException("modelProperty did not return a property");
|
||||||
|
}
|
||||||
|
return (Property<Object>) property;
|
||||||
|
} catch (NoSuchMethodException failure) {
|
||||||
|
throw new IllegalArgumentException("No model property on " + component.getClass().getName(), failure);
|
||||||
|
} catch (IllegalAccessException | InvocationTargetException failure) {
|
||||||
|
throw new IllegalStateException("Could not read model property", failure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package de.neitzel.fx.view;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Receives the ViewModel selected for a Controller or a Component.
|
||||||
|
* <p>
|
||||||
|
* A Component is called after its model property is set to a non-null Model and before that
|
||||||
|
* Component's bindings run.
|
||||||
|
*/
|
||||||
|
public interface HasViewModel {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Receives the ViewModel for the Model this object is bound to.
|
||||||
|
*
|
||||||
|
* @param viewModel the selected ViewModel
|
||||||
|
*/
|
||||||
|
void setViewModel(ViewModel viewModel);
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
package de.neitzel.fx.view;
|
package de.neitzel.fx.view;
|
||||||
|
|
||||||
|
import java.util.ArrayDeque;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Deque;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -8,11 +10,12 @@ import java.util.List;
|
|||||||
* <p>
|
* <p>
|
||||||
* FXML objects declared in {@code fx:define} are not scene-graph children and have no
|
* FXML objects declared in {@code fx:define} are not scene-graph children and have no
|
||||||
* {@code fx:id} in the accepted document shape, so each {@link BindingList} registers
|
* {@code fx:id} in the accepted document shape, so each {@link BindingList} registers
|
||||||
* itself here from its constructor.
|
* itself here from its constructor. A Component document opens a nested session, so its
|
||||||
|
* lists stay off the parent session.
|
||||||
*/
|
*/
|
||||||
final class LoadSession {
|
final class LoadSession {
|
||||||
|
|
||||||
private static final ThreadLocal<LoadSession> CURRENT = new ThreadLocal<>();
|
private static final ThreadLocal<Deque<LoadSession>> STACK = new ThreadLocal<>();
|
||||||
|
|
||||||
private final List<BindingList> bindingLists = new ArrayList<>();
|
private final List<BindingList> bindingLists = new ArrayList<>();
|
||||||
|
|
||||||
@@ -25,16 +28,38 @@ final class LoadSession {
|
|||||||
* @return the open session
|
* @return the open session
|
||||||
*/
|
*/
|
||||||
static LoadSession open() {
|
static LoadSession open() {
|
||||||
|
Deque<LoadSession> stack = new ArrayDeque<>();
|
||||||
LoadSession session = new LoadSession();
|
LoadSession session = new LoadSession();
|
||||||
CURRENT.set(session);
|
stack.push(session);
|
||||||
|
STACK.set(stack);
|
||||||
return session;
|
return session;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens a nested session for a Component document.
|
||||||
|
* <p>
|
||||||
|
* Binding lists created until {@link #unnest()} register here, not on the parent session.
|
||||||
|
*
|
||||||
|
* @return the nested session
|
||||||
|
*/
|
||||||
|
static LoadSession nest() {
|
||||||
|
LoadSession session = new LoadSession();
|
||||||
|
STACK.get().push(session);
|
||||||
|
return session;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restores the session that was current before {@link #nest()}.
|
||||||
|
*/
|
||||||
|
static void unnest() {
|
||||||
|
STACK.get().pop();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Drops the session for this thread.
|
* Drops the session for this thread.
|
||||||
*/
|
*/
|
||||||
static void close() {
|
static void close() {
|
||||||
CURRENT.remove();
|
STACK.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -43,10 +68,11 @@ final class LoadSession {
|
|||||||
* @param bindingList the list FXML just constructed
|
* @param bindingList the list FXML just constructed
|
||||||
*/
|
*/
|
||||||
static void register(BindingList bindingList) {
|
static void register(BindingList bindingList) {
|
||||||
LoadSession session = CURRENT.get();
|
Deque<LoadSession> stack = STACK.get();
|
||||||
if (session != null) {
|
if (stack == null || stack.isEmpty()) {
|
||||||
session.bindingLists.add(bindingList);
|
return;
|
||||||
}
|
}
|
||||||
|
stack.peek().bindingLists.add(bindingList);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -34,6 +34,12 @@ public final class ViewLoader {
|
|||||||
* A path that is not a read-write Attribute fails the load. The context creates a Controller,
|
* A path that is not a read-write Attribute fails the load. The context creates a Controller,
|
||||||
* and a Converter when a Direct binding names one. A converter that cannot produce a value
|
* and a Converter when a Direct binding names one. A converter that cannot produce a value
|
||||||
* keeps the previous value and records one error Message. It does not fail the load.
|
* keeps the previous value and records one error Message. 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.
|
||||||
*
|
*
|
||||||
* @param fxml the FXML document
|
* @param fxml the FXML document
|
||||||
* @param model the Model instance
|
* @param model the Model instance
|
||||||
@@ -47,6 +53,7 @@ public final class ViewLoader {
|
|||||||
try {
|
try {
|
||||||
FXMLLoader loader = new FXMLLoader(fxml);
|
FXMLLoader loader = new FXMLLoader(fxml);
|
||||||
loader.setControllerFactory(type -> controller(context, type));
|
loader.setControllerFactory(type -> controller(context, type));
|
||||||
|
loader.setBuilderFactory(type -> Components.builder(type, context));
|
||||||
Object loaded = loader.load();
|
Object loaded = loader.load();
|
||||||
if (!(loaded instanceof Parent root)) {
|
if (!(loaded instanceof Parent root)) {
|
||||||
throw new IllegalArgumentException("View root must be a parent");
|
throw new IllegalArgumentException("View root must be a parent");
|
||||||
|
|||||||
@@ -0,0 +1,162 @@
|
|||||||
|
package de.neitzel.fx.view;
|
||||||
|
|
||||||
|
import de.neitzel.fx.view.component.AddressForm;
|
||||||
|
import de.neitzel.fx.view.component.Envelope;
|
||||||
|
import de.neitzel.fx.view.component.Letter;
|
||||||
|
import de.neitzel.fx.view.fixture.Address;
|
||||||
|
import de.neitzel.fx.view.fixture.Person;
|
||||||
|
import de.neitzel.injection.ApplicationContext;
|
||||||
|
import javafx.application.Platform;
|
||||||
|
import javafx.scene.control.Label;
|
||||||
|
import javafx.scene.control.TextField;
|
||||||
|
import org.junit.jupiter.api.BeforeAll;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.net.URL;
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||||
|
import static org.junit.jupiter.api.Assertions.fail;
|
||||||
|
|
||||||
|
class EditAddressInAComponentTest {
|
||||||
|
|
||||||
|
@BeforeAll
|
||||||
|
static void startJavaFx() throws InterruptedException {
|
||||||
|
CountDownLatch started = new CountDownLatch(1);
|
||||||
|
try {
|
||||||
|
Platform.startup(started::countDown);
|
||||||
|
} catch (IllegalStateException alreadyStarted) {
|
||||||
|
started.countDown();
|
||||||
|
}
|
||||||
|
if (!started.await(10, TimeUnit.SECONDS)) {
|
||||||
|
fail("JavaFX toolkit did not start");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void editingTheStreetUpdatesThatAddress() throws Exception {
|
||||||
|
onFx(() -> {
|
||||||
|
Address address = new Address("Main");
|
||||||
|
Person person = new Person("Ada");
|
||||||
|
person.setAddress(address);
|
||||||
|
URL fxml = EditAddressInAComponentTest.class.getResource("address-component.fxml");
|
||||||
|
assertNotNull(fxml);
|
||||||
|
ApplicationContext context = new ApplicationContext("de.neitzel.fx.view.component");
|
||||||
|
LoadedView loaded = ViewLoader.load(fxml, person, context);
|
||||||
|
AddressForm form = (AddressForm) loaded.getRoot().lookup("#addressForm");
|
||||||
|
assertNotNull(form);
|
||||||
|
assertSame(address, form.getModel());
|
||||||
|
TextField streetField = (TextField) form.lookup("#streetField");
|
||||||
|
assertEquals("Main", streetField.getText());
|
||||||
|
streetField.setText("Oak");
|
||||||
|
assertEquals("Oak", address.getStreet());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void nullAddressLeavesTheModelUnsetAndKeepsTheStreetText() throws Exception {
|
||||||
|
onFx(() -> {
|
||||||
|
Person person = new Person("Ada");
|
||||||
|
URL fxml = EditAddressInAComponentTest.class.getResource("address-component.fxml");
|
||||||
|
assertNotNull(fxml);
|
||||||
|
ApplicationContext context = new ApplicationContext("de.neitzel.fx.view.component");
|
||||||
|
LoadedView loaded = ViewLoader.load(fxml, person, context);
|
||||||
|
AddressForm form = (AddressForm) loaded.getRoot().lookup("#addressForm");
|
||||||
|
assertNotNull(form);
|
||||||
|
assertNull(form.getModel());
|
||||||
|
TextField streetField = (TextField) form.lookup("#streetField");
|
||||||
|
assertEquals("kept", streetField.getText());
|
||||||
|
assertNull(person.getAddress());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void streetLabelShowsTheInjectedPrefix() throws Exception {
|
||||||
|
onFx(() -> {
|
||||||
|
Person person = new Person("Ada");
|
||||||
|
person.setAddress(new Address("Main"));
|
||||||
|
URL fxml = EditAddressInAComponentTest.class.getResource("address-component.fxml");
|
||||||
|
assertNotNull(fxml);
|
||||||
|
ApplicationContext context = new ApplicationContext("de.neitzel.fx.view.component");
|
||||||
|
LoadedView loaded = ViewLoader.load(fxml, person, context);
|
||||||
|
AddressForm form = (AddressForm) loaded.getRoot().lookup("#addressForm");
|
||||||
|
Label streetLabel = (Label) form.lookup("#streetLabel");
|
||||||
|
assertEquals("Lane", streetLabel.getText());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void componentReceivesTheAddressViewModelBeforeItsBindingsRun() throws Exception {
|
||||||
|
onFx(() -> {
|
||||||
|
Address address = new Address("Main");
|
||||||
|
Person person = new Person("Ada");
|
||||||
|
person.setAddress(address);
|
||||||
|
URL fxml = EditAddressInAComponentTest.class.getResource("address-component.fxml");
|
||||||
|
assertNotNull(fxml);
|
||||||
|
ApplicationContext context = new ApplicationContext("de.neitzel.fx.view.component");
|
||||||
|
LoadedView loaded = ViewLoader.load(fxml, person, context);
|
||||||
|
AddressForm form = (AddressForm) loaded.getRoot().lookup("#addressForm");
|
||||||
|
assertSame(loaded.property("address").getValue(), form.receivedViewModel());
|
||||||
|
assertEquals("kept", form.streetTextAtHandOff());
|
||||||
|
TextField streetField = (TextField) form.lookup("#streetField");
|
||||||
|
assertEquals("Main", streetField.getText());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void nestedComponentShowsTheInnerStreet() throws Exception {
|
||||||
|
onFx(() -> {
|
||||||
|
Address address = new Address("Main");
|
||||||
|
Letter letter = new Letter(new Envelope(address));
|
||||||
|
URL fxml = EditAddressInAComponentTest.class.getResource("letter-envelope.fxml");
|
||||||
|
assertNotNull(fxml);
|
||||||
|
ApplicationContext context = new ApplicationContext("de.neitzel.fx.view.component");
|
||||||
|
LoadedView loaded = ViewLoader.load(fxml, letter, context);
|
||||||
|
TextField streetField = (TextField) loaded.getRoot().lookup("#streetField");
|
||||||
|
assertEquals("Main", streetField.getText());
|
||||||
|
streetField.setText("Oak");
|
||||||
|
assertEquals("Oak", address.getStreet());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void onFx(FxAction action) throws Exception {
|
||||||
|
if (Platform.isFxApplicationThread()) {
|
||||||
|
action.run();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
AtomicReference<Throwable> thrown = new AtomicReference<>();
|
||||||
|
CountDownLatch done = new CountDownLatch(1);
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
try {
|
||||||
|
action.run();
|
||||||
|
} catch (Throwable failure) {
|
||||||
|
thrown.set(failure);
|
||||||
|
} finally {
|
||||||
|
done.countDown();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!done.await(10, TimeUnit.SECONDS)) {
|
||||||
|
fail("JavaFX thread did not finish");
|
||||||
|
}
|
||||||
|
Throwable failure = thrown.get();
|
||||||
|
if (failure instanceof Exception exception) {
|
||||||
|
throw exception;
|
||||||
|
}
|
||||||
|
if (failure instanceof Error error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
if (failure != null) {
|
||||||
|
throw new AssertionError(failure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
private interface FxAction {
|
||||||
|
void run() throws Exception;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
package de.neitzel.fx.view.component;
|
||||||
|
|
||||||
|
import de.neitzel.fx.view.HasViewModel;
|
||||||
|
import de.neitzel.fx.view.ViewModel;
|
||||||
|
import jakarta.inject.Inject;
|
||||||
|
import jakarta.inject.Singleton;
|
||||||
|
import javafx.beans.property.ObjectProperty;
|
||||||
|
import javafx.beans.property.SimpleObjectProperty;
|
||||||
|
import javafx.fxml.FXML;
|
||||||
|
import javafx.scene.control.Label;
|
||||||
|
import javafx.scene.control.TextField;
|
||||||
|
import javafx.scene.layout.VBox;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component that shows one address.
|
||||||
|
* <p>
|
||||||
|
* The container injects the caption service. The street label shows that service's prefix.
|
||||||
|
*/
|
||||||
|
@Singleton
|
||||||
|
public class AddressForm extends VBox implements HasViewModel {
|
||||||
|
|
||||||
|
private final StreetCaption captions;
|
||||||
|
private final ObjectProperty<Object> model = new SimpleObjectProperty<>(this, "model");
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private Label streetLabel;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private TextField streetField;
|
||||||
|
|
||||||
|
private ViewModel receivedViewModel;
|
||||||
|
private String streetTextAtHandOff;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a form that reads its street label from the injected caption.
|
||||||
|
*
|
||||||
|
* @param captions the service that supplies the street prefix
|
||||||
|
*/
|
||||||
|
@Inject
|
||||||
|
public AddressForm(StreetCaption captions) {
|
||||||
|
this.captions = captions;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes the injected prefix onto the street label.
|
||||||
|
*/
|
||||||
|
@FXML
|
||||||
|
public void initialize() {
|
||||||
|
streetLabel.setText(captions.prefix());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the model property a parent View sets with a Direct binding.
|
||||||
|
*
|
||||||
|
* @return the model property
|
||||||
|
*/
|
||||||
|
public ObjectProperty<Object> modelProperty() {
|
||||||
|
return model;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the address this component shows.
|
||||||
|
*
|
||||||
|
* @return the address, or {@code null} when none is set
|
||||||
|
*/
|
||||||
|
public Object getModel() {
|
||||||
|
return model.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the address this component shows.
|
||||||
|
*
|
||||||
|
* @param model the address
|
||||||
|
*/
|
||||||
|
public void setModel(Object model) {
|
||||||
|
this.model.set(model);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Records the ViewModel handed to this component, and the street text at that moment.
|
||||||
|
*
|
||||||
|
* @param viewModel the ViewModel selected for the address
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void setViewModel(ViewModel viewModel) {
|
||||||
|
this.receivedViewModel = viewModel;
|
||||||
|
this.streetTextAtHandOff = streetField.getText();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the ViewModel this component was handed.
|
||||||
|
*
|
||||||
|
* @return the ViewModel, or {@code null} when none was handed over
|
||||||
|
*/
|
||||||
|
public ViewModel receivedViewModel() {
|
||||||
|
return receivedViewModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the street text observed when the ViewModel was handed over.
|
||||||
|
*
|
||||||
|
* @return the street text at hand-off
|
||||||
|
*/
|
||||||
|
public String streetTextAtHandOff() {
|
||||||
|
return streetTextAtHandOff;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package de.neitzel.fx.view.component;
|
||||||
|
|
||||||
|
import de.neitzel.fx.view.fixture.Address;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ordinary model that holds one address, used to nest an address component.
|
||||||
|
*/
|
||||||
|
public class Envelope {
|
||||||
|
|
||||||
|
private Address address;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an envelope for the given address.
|
||||||
|
*
|
||||||
|
* @param address the address inside the envelope
|
||||||
|
*/
|
||||||
|
public Envelope(Address address) {
|
||||||
|
this.address = address;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the address.
|
||||||
|
*
|
||||||
|
* @return the address
|
||||||
|
*/
|
||||||
|
public Address getAddress() {
|
||||||
|
return address;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the address.
|
||||||
|
*
|
||||||
|
* @param address the address
|
||||||
|
*/
|
||||||
|
public void setAddress(Address address) {
|
||||||
|
this.address = address;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package de.neitzel.fx.view.component;
|
||||||
|
|
||||||
|
import jakarta.inject.Singleton;
|
||||||
|
import javafx.beans.property.ObjectProperty;
|
||||||
|
import javafx.beans.property.SimpleObjectProperty;
|
||||||
|
import javafx.scene.layout.VBox;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component that shows one envelope and the address component inside it.
|
||||||
|
*/
|
||||||
|
@Singleton
|
||||||
|
public class EnvelopeForm extends VBox {
|
||||||
|
|
||||||
|
private final ObjectProperty<Object> model = new SimpleObjectProperty<>(this, "model");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the model property a parent View sets with a Direct binding.
|
||||||
|
*
|
||||||
|
* @return the model property
|
||||||
|
*/
|
||||||
|
public ObjectProperty<Object> modelProperty() {
|
||||||
|
return model;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the envelope this component shows.
|
||||||
|
*
|
||||||
|
* @return the envelope, or {@code null} when none is set
|
||||||
|
*/
|
||||||
|
public Object getModel() {
|
||||||
|
return model.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the envelope this component shows.
|
||||||
|
*
|
||||||
|
* @param model the envelope
|
||||||
|
*/
|
||||||
|
public void setModel(Object model) {
|
||||||
|
this.model.set(model);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package de.neitzel.fx.view.component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ordinary model that holds one envelope.
|
||||||
|
*/
|
||||||
|
public class Letter {
|
||||||
|
|
||||||
|
private Envelope envelope;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a letter for the given envelope.
|
||||||
|
*
|
||||||
|
* @param envelope the envelope
|
||||||
|
*/
|
||||||
|
public Letter(Envelope envelope) {
|
||||||
|
this.envelope = envelope;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the envelope.
|
||||||
|
*
|
||||||
|
* @return the envelope
|
||||||
|
*/
|
||||||
|
public Envelope getEnvelope() {
|
||||||
|
return envelope;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the envelope.
|
||||||
|
*
|
||||||
|
* @param envelope the envelope
|
||||||
|
*/
|
||||||
|
public void setEnvelope(Envelope envelope) {
|
||||||
|
this.envelope = envelope;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package de.neitzel.fx.view.component;
|
||||||
|
|
||||||
|
import jakarta.inject.Singleton;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service whose text an address component must show, so a test can see that injection ran.
|
||||||
|
*/
|
||||||
|
@Singleton
|
||||||
|
public class StreetCaption {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the prefix written on the street label.
|
||||||
|
*
|
||||||
|
* @return the prefix
|
||||||
|
*/
|
||||||
|
public String prefix() {
|
||||||
|
return "Lane";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<?import javafx.scene.layout.StackPane?>
|
||||||
|
<?import de.neitzel.fx.view.BindingList?>
|
||||||
|
<?import de.neitzel.fx.view.DirectBinding?>
|
||||||
|
<?import de.neitzel.fx.view.component.AddressForm?>
|
||||||
|
|
||||||
|
<StackPane xmlns="http://javafx.com/javafx" xmlns:fx="http://javafx.com/fxml">
|
||||||
|
<fx:define>
|
||||||
|
<BindingList>
|
||||||
|
<DirectBinding control="addressForm" property="model" path="address" direction="both"/>
|
||||||
|
</BindingList>
|
||||||
|
</fx:define>
|
||||||
|
<AddressForm fx:id="addressForm"/>
|
||||||
|
</StackPane>
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<?import javafx.scene.control.Label?>
|
||||||
|
<?import javafx.scene.control.TextField?>
|
||||||
|
<?import javafx.scene.layout.VBox?>
|
||||||
|
<?import de.neitzel.fx.view.BindingList?>
|
||||||
|
<?import de.neitzel.fx.view.DirectBinding?>
|
||||||
|
|
||||||
|
<fx:root type="javafx.scene.layout.VBox" xmlns="http://javafx.com/javafx"
|
||||||
|
xmlns:fx="http://javafx.com/fxml">
|
||||||
|
<fx:define>
|
||||||
|
<BindingList>
|
||||||
|
<DirectBinding control="streetField" property="text" path="street" direction="both"/>
|
||||||
|
</BindingList>
|
||||||
|
</fx:define>
|
||||||
|
<Label fx:id="streetLabel" id="streetLabel"/>
|
||||||
|
<TextField fx:id="streetField" id="streetField" text="kept"/>
|
||||||
|
</fx:root>
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<?import javafx.scene.layout.VBox?>
|
||||||
|
<?import de.neitzel.fx.view.BindingList?>
|
||||||
|
<?import de.neitzel.fx.view.DirectBinding?>
|
||||||
|
<?import de.neitzel.fx.view.component.AddressForm?>
|
||||||
|
|
||||||
|
<fx:root type="javafx.scene.layout.VBox" xmlns="http://javafx.com/javafx"
|
||||||
|
xmlns:fx="http://javafx.com/fxml">
|
||||||
|
<fx:define>
|
||||||
|
<BindingList>
|
||||||
|
<DirectBinding control="addressForm" property="model" path="address" direction="both"/>
|
||||||
|
</BindingList>
|
||||||
|
</fx:define>
|
||||||
|
<AddressForm fx:id="addressForm"/>
|
||||||
|
</fx:root>
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<?import javafx.scene.layout.StackPane?>
|
||||||
|
<?import de.neitzel.fx.view.BindingList?>
|
||||||
|
<?import de.neitzel.fx.view.DirectBinding?>
|
||||||
|
<?import de.neitzel.fx.view.component.EnvelopeForm?>
|
||||||
|
|
||||||
|
<StackPane xmlns="http://javafx.com/javafx" xmlns:fx="http://javafx.com/fxml">
|
||||||
|
<fx:define>
|
||||||
|
<BindingList>
|
||||||
|
<DirectBinding control="envelopeForm" property="model" path="envelope" direction="both"/>
|
||||||
|
</BindingList>
|
||||||
|
</fx:define>
|
||||||
|
<EnvelopeForm fx:id="envelopeForm"/>
|
||||||
|
</StackPane>
|
||||||
Reference in New Issue
Block a user