Complete JavaFX MVVM examples, bindings and library documentation
This commit is contained in:
@@ -17,6 +17,9 @@ import java.util.Map;
|
||||
@DefaultProperty("elements")
|
||||
public class BindingList {
|
||||
|
||||
/**
|
||||
* The binding elements, in document order.
|
||||
*/
|
||||
private final List<Object> elements = new ArrayList<>();
|
||||
|
||||
/**
|
||||
|
||||
@@ -17,11 +17,33 @@ import java.util.Set;
|
||||
*/
|
||||
final class ChildMessageMirror implements MessageListener {
|
||||
|
||||
/**
|
||||
* The parent that receives copies of the child's notices.
|
||||
*/
|
||||
private final ViewModel parent;
|
||||
|
||||
/**
|
||||
* The Attribute that leads to the child. It is prepended to each copied source.
|
||||
*/
|
||||
private final String attribute;
|
||||
|
||||
/**
|
||||
* The child whose notices are carried.
|
||||
*/
|
||||
private final ViewModel child;
|
||||
|
||||
/**
|
||||
* Copies already carried, keyed by the identity of the child notice.
|
||||
*/
|
||||
private final Map<Message, Message> copies = new IdentityHashMap<>();
|
||||
|
||||
/**
|
||||
* Creates a mirror. {@link #attach} registers it and copies notices already present.
|
||||
*
|
||||
* @param parent the parent ViewModel
|
||||
* @param attribute the Attribute that leads to the child
|
||||
* @param child the child ViewModel
|
||||
*/
|
||||
private ChildMessageMirror(ViewModel parent, String attribute, ViewModel child) {
|
||||
this.parent = parent;
|
||||
this.attribute = attribute;
|
||||
@@ -97,6 +119,11 @@ final class ChildMessageMirror implements MessageListener {
|
||||
parent.noteMessageText(copy);
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies {@code message} onto the parent unless that copy is already there or would cycle.
|
||||
*
|
||||
* @param message the child notice
|
||||
*/
|
||||
private void carry(Message message) {
|
||||
if (copies.containsKey(message) || repeats(message)) {
|
||||
return;
|
||||
|
||||
@@ -18,6 +18,11 @@ public class Column {
|
||||
*/
|
||||
private String attribute;
|
||||
|
||||
/**
|
||||
* The starting width, or {@code null} when this Column takes an equal share of the Table.
|
||||
*/
|
||||
private Double width;
|
||||
|
||||
/**
|
||||
* Sets the header text.
|
||||
*
|
||||
@@ -53,4 +58,34 @@ public class Column {
|
||||
public String getAttribute() {
|
||||
return attribute;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the starting width.
|
||||
* <p>
|
||||
* A Column with no starting width opens at an equal share of the Table. A drag can change the
|
||||
* width later. A width that is not positive fails the load.
|
||||
*
|
||||
* @param width the starting width
|
||||
*/
|
||||
public void setWidth(double width) {
|
||||
this.width = width;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the starting width.
|
||||
*
|
||||
* @return the starting width, or {@code 0} when this Column takes an equal share
|
||||
*/
|
||||
public double getWidth() {
|
||||
return width == null ? 0 : width;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether a starting width was set.
|
||||
*
|
||||
* @return {@code true} when {@link #setWidth(double)} has been called
|
||||
*/
|
||||
boolean hasWidth() {
|
||||
return width != null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,9 @@ import java.util.Map;
|
||||
*/
|
||||
final class Components {
|
||||
|
||||
/**
|
||||
* Prevents instantiation.
|
||||
*/
|
||||
private Components() {
|
||||
}
|
||||
|
||||
@@ -86,6 +89,13 @@ final class Components {
|
||||
modelProperty(component).setValue(model);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads {@code ClassName.fxml} with the Component as root and Controller.
|
||||
*
|
||||
* @param component the Component
|
||||
* @param context the container that creates nested Components
|
||||
* @return the loaded Component
|
||||
*/
|
||||
private static Object load(Object component, ApplicationContext context) {
|
||||
Class<?> type = component.getClass();
|
||||
String document = type.getSimpleName() + ".fxml";
|
||||
@@ -109,6 +119,14 @@ final class Components {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the Component's bindings once, when {@code model} first becomes non-null.
|
||||
*
|
||||
* @param component the Component
|
||||
* @param lists the binding lists captured from its document
|
||||
* @param namespace objects addressed by {@code fx:id}
|
||||
* @param context the container that creates a named Converter
|
||||
*/
|
||||
private static void watchModel(Object component, List<BindingList> lists, Map<String, Object> namespace,
|
||||
ApplicationContext context) {
|
||||
Property<Object> model = modelProperty(component);
|
||||
@@ -129,6 +147,12 @@ final class Components {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Component's {@code model} property.
|
||||
*
|
||||
* @param component the Component
|
||||
* @return the property
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private static Property<Object> modelProperty(Object component) {
|
||||
try {
|
||||
|
||||
@@ -10,8 +10,19 @@ package de.neitzel.fx.view;
|
||||
*/
|
||||
public class ComputedBinding {
|
||||
|
||||
/**
|
||||
* The {@code fx:id} of the control that receives the method result.
|
||||
*/
|
||||
private String control;
|
||||
|
||||
/**
|
||||
* The control property that receives the method result, such as {@code text}.
|
||||
*/
|
||||
private String property;
|
||||
|
||||
/**
|
||||
* The Controller method, written as {@code #method}.
|
||||
*/
|
||||
private String method;
|
||||
|
||||
/**
|
||||
|
||||
@@ -16,6 +16,9 @@ import java.util.Map;
|
||||
*/
|
||||
final class ComputedBindingApplier {
|
||||
|
||||
/**
|
||||
* Prevents instantiation.
|
||||
*/
|
||||
private ComputedBindingApplier() {
|
||||
}
|
||||
|
||||
@@ -52,6 +55,16 @@ final class ComputedBindingApplier {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a refresh that calls the Controller method and writes the result.
|
||||
* <p>
|
||||
* A refresh that is already running does not start again, so writing the target does not loop.
|
||||
*
|
||||
* @param controller the Controller
|
||||
* @param methodValue the method, written as {@code #method}
|
||||
* @param target the control property
|
||||
* @return the refresh
|
||||
*/
|
||||
private static Runnable refresh(Object controller, String methodValue, Property<Object> target) {
|
||||
boolean[] running = {false};
|
||||
return () -> {
|
||||
@@ -67,10 +80,24 @@ final class ComputedBindingApplier {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Watches JavaFX properties on controls under {@code root}, other than {@code target}.
|
||||
*
|
||||
* @param root the scene-graph root
|
||||
* @param target the property the method writes, which is not watched
|
||||
* @param refresh the refresh to run when a watched property changes
|
||||
*/
|
||||
private static void watchControls(Parent root, Property<?> target, Runnable refresh) {
|
||||
watchNode(root, target, refresh);
|
||||
}
|
||||
|
||||
/**
|
||||
* Watches one control and walks its children.
|
||||
*
|
||||
* @param node the node
|
||||
* @param target the property the method writes, which is not watched
|
||||
* @param refresh the refresh to run when a watched property changes
|
||||
*/
|
||||
private static void watchNode(Node node, Property<?> target, Runnable refresh) {
|
||||
if (node instanceof Control control) {
|
||||
watchProperties(control, target, refresh);
|
||||
@@ -82,6 +109,13 @@ final class ComputedBindingApplier {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Listens to every JavaFX property on {@code bean} except {@code target}.
|
||||
*
|
||||
* @param bean the control
|
||||
* @param target the property the method writes
|
||||
* @param refresh the refresh to run when a watched property changes
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private static void watchProperties(Object bean, Property<?> target, Runnable refresh) {
|
||||
ChangeListener<Object> listener = (observable, oldValue, newValue) -> refresh.run();
|
||||
@@ -99,6 +133,13 @@ final class ComputedBindingApplier {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes a no-argument property method.
|
||||
*
|
||||
* @param bean the object that declares the property
|
||||
* @param method the property method
|
||||
* @return the property
|
||||
*/
|
||||
private static Object readProperty(Object bean, Method method) {
|
||||
try {
|
||||
return method.invoke(bean);
|
||||
@@ -107,6 +148,13 @@ final class ComputedBindingApplier {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the writable JavaFX property named on the control.
|
||||
*
|
||||
* @param control the control
|
||||
* @param propertyName the property name, such as {@code text}
|
||||
* @return the property
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private static Property<Object> controlProperty(Object control, String propertyName) {
|
||||
String methodName = propertyName + "Property";
|
||||
@@ -125,6 +173,13 @@ final class ComputedBindingApplier {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls the Controller method named by {@code methodValue}.
|
||||
*
|
||||
* @param controller the Controller
|
||||
* @param methodValue the method, written as {@code #method}
|
||||
* @return the method result
|
||||
*/
|
||||
private static Object invoke(Object controller, String methodValue) {
|
||||
if (controller == null) {
|
||||
throw new IllegalArgumentException("No controller for " + methodValue);
|
||||
@@ -140,6 +195,12 @@ final class ComputedBindingApplier {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Drops a leading {@code #} from an FXML method value.
|
||||
*
|
||||
* @param methodValue the method value
|
||||
* @return the method name
|
||||
*/
|
||||
private static String methodName(String methodValue) {
|
||||
if (methodValue != null && methodValue.startsWith("#")) {
|
||||
return methodValue.substring(1);
|
||||
|
||||
@@ -12,10 +12,29 @@ package de.neitzel.fx.view;
|
||||
*/
|
||||
public class DirectBinding {
|
||||
|
||||
/**
|
||||
* The {@code fx:id} of the control.
|
||||
*/
|
||||
private String control;
|
||||
|
||||
/**
|
||||
* The control property, such as {@code text}.
|
||||
*/
|
||||
private String property;
|
||||
|
||||
/**
|
||||
* The Path. One name is an Attribute of this View's ViewModel.
|
||||
*/
|
||||
private String path;
|
||||
|
||||
/**
|
||||
* The direction: {@code view}, {@code model}, or {@code both}.
|
||||
*/
|
||||
private String direction;
|
||||
|
||||
/**
|
||||
* The Converter class name, or {@code null} when values pass through.
|
||||
*/
|
||||
private String converter;
|
||||
|
||||
/**
|
||||
|
||||
@@ -14,6 +14,9 @@ import java.util.Map;
|
||||
*/
|
||||
final class DirectBindingApplier {
|
||||
|
||||
/**
|
||||
* Prevents instantiation.
|
||||
*/
|
||||
private DirectBindingApplier() {
|
||||
}
|
||||
|
||||
@@ -68,6 +71,13 @@ final class DirectBindingApplier {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Aligns a plain Attribute in the named direction.
|
||||
*
|
||||
* @param controlProperty the control property
|
||||
* @param attribute the Attribute
|
||||
* @param direction {@code view}, {@code model}, or {@code both}
|
||||
*/
|
||||
private static void alignPlain(Property<?> controlProperty, Property<?> attribute, String direction) {
|
||||
switch (direction) {
|
||||
case "both" -> alignBoth(controlProperty, attribute);
|
||||
@@ -84,11 +94,35 @@ final class DirectBindingApplier {
|
||||
* name as the source, so a parent only carries the extended copy.
|
||||
*/
|
||||
private static final class Resolved {
|
||||
|
||||
/**
|
||||
* The Attribute the Path names.
|
||||
*/
|
||||
private final Property<?> attribute;
|
||||
|
||||
/**
|
||||
* Whether that Attribute is complex.
|
||||
*/
|
||||
private final boolean complex;
|
||||
|
||||
/**
|
||||
* The ViewModel that owns the Attribute. A conversion failure is recorded here.
|
||||
*/
|
||||
private final ViewModel owner;
|
||||
|
||||
/**
|
||||
* The last segment of the Path, used as the failure source.
|
||||
*/
|
||||
private final String name;
|
||||
|
||||
/**
|
||||
* Records one resolved Path.
|
||||
*
|
||||
* @param attribute the Attribute
|
||||
* @param complex whether the Attribute is complex
|
||||
* @param owner the ViewModel that owns it
|
||||
* @param name the last segment
|
||||
*/
|
||||
private Resolved(Property<?> attribute, boolean complex, ViewModel owner, String name) {
|
||||
this.attribute = attribute;
|
||||
this.complex = complex;
|
||||
@@ -165,6 +199,13 @@ final class DirectBindingApplier {
|
||||
ViewModels.of(instance, requested);
|
||||
}
|
||||
|
||||
/**
|
||||
* Follows a Path through the Model instances behind the ViewModels.
|
||||
*
|
||||
* @param viewModel the ViewModel for this View
|
||||
* @param path the Path
|
||||
* @return the Model instance at the end of the Path, or {@code null} when a step is null
|
||||
*/
|
||||
private static Object instanceAt(Object viewModel, String path) {
|
||||
if (!(viewModel instanceof ViewModel root)) {
|
||||
return null;
|
||||
@@ -179,6 +220,13 @@ final class DirectBindingApplier {
|
||||
return current;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads one JavaBean attribute through {@code get} or {@code is}.
|
||||
*
|
||||
* @param bean the Model instance
|
||||
* @param name the attribute name
|
||||
* @return the current value
|
||||
*/
|
||||
private static Object readAttribute(Object bean, String name) {
|
||||
String suffix = Character.toUpperCase(name.charAt(0)) + name.substring(1);
|
||||
for (String prefix : List.of("get", "is")) {
|
||||
@@ -194,6 +242,13 @@ final class DirectBindingApplier {
|
||||
throw new IllegalArgumentException("No read-write attribute for path \"" + name + "\"");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the JavaFX property named on a ViewModel or a replacement.
|
||||
*
|
||||
* @param target the ViewModel or replacement
|
||||
* @param name the property name
|
||||
* @return the property, or {@code null} when that name is not declared
|
||||
*/
|
||||
private static Property<?> propertyOf(Object target, String name) {
|
||||
if (target instanceof ViewModel viewModel) {
|
||||
return viewModel.property(name);
|
||||
@@ -201,10 +256,23 @@ final class DirectBindingApplier {
|
||||
return NamedProperties.get(target, name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports a Path that is not a read-write Attribute.
|
||||
*
|
||||
* @param path the Path
|
||||
* @return the failure
|
||||
*/
|
||||
private static IllegalArgumentException missing(String path) {
|
||||
return new IllegalArgumentException("No read-write attribute for path \"" + path + "\"");
|
||||
}
|
||||
|
||||
/**
|
||||
* Asks the container for the named Converter.
|
||||
*
|
||||
* @param context the container
|
||||
* @param className the Converter class name
|
||||
* @return the Converter
|
||||
*/
|
||||
private static Converter converter(ApplicationContext context, String className) {
|
||||
try {
|
||||
Class<?> type = Class.forName(className);
|
||||
@@ -245,6 +313,16 @@ final class DirectBindingApplier {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the converted Attribute onto the control. A failure keeps the previous control value.
|
||||
*
|
||||
* @param control the control property
|
||||
* @param attribute the Attribute
|
||||
* @param converter the Converter
|
||||
* @param owner the ViewModel that records a failure
|
||||
* @param source the Attribute name used as the failure source
|
||||
* @param link the guard against hearing this write
|
||||
*/
|
||||
private static void pushControl(Property<Object> control, Property<Object> attribute, Converter converter,
|
||||
ViewModel owner, String source, Link link) {
|
||||
try {
|
||||
@@ -261,6 +339,16 @@ final class DirectBindingApplier {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the converted control value onto the Attribute. A failure keeps the previous Attribute.
|
||||
*
|
||||
* @param control the control property
|
||||
* @param attribute the Attribute
|
||||
* @param converter the Converter
|
||||
* @param owner the ViewModel that records a failure
|
||||
* @param source the Attribute name used as the failure source
|
||||
* @param link the guard against hearing this write
|
||||
*/
|
||||
private static void pushAttribute(Property<Object> control, Property<Object> attribute, Converter converter,
|
||||
ViewModel owner, String source, Link link) {
|
||||
try {
|
||||
@@ -277,6 +365,12 @@ final class DirectBindingApplier {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the exception message, or a fallback when it is missing.
|
||||
*
|
||||
* @param failure the conversion failure
|
||||
* @return the text recorded on the ViewModel
|
||||
*/
|
||||
private static String failureText(RuntimeException failure) {
|
||||
String text = failure.getMessage();
|
||||
if (text == null || text.isEmpty()) {
|
||||
@@ -289,9 +383,20 @@ final class DirectBindingApplier {
|
||||
* Guards one converted binding against hearing its own writes.
|
||||
*/
|
||||
private static final class Link {
|
||||
|
||||
/**
|
||||
* Whether this binding is currently writing one side, so the other listener stays quiet.
|
||||
*/
|
||||
private boolean applying;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the writable JavaFX property named on the control.
|
||||
*
|
||||
* @param control the control
|
||||
* @param propertyName the property name, such as {@code text}
|
||||
* @return the property
|
||||
*/
|
||||
private static Property<?> controlProperty(Object control, String propertyName) {
|
||||
String methodName = propertyName + "Property";
|
||||
try {
|
||||
@@ -309,6 +414,15 @@ final class DirectBindingApplier {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Keeps a complex Attribute and the control aligned in both directions.
|
||||
* <p>
|
||||
* The control receives the Model instance. A later control value is stored as that instance's
|
||||
* ViewModel.
|
||||
*
|
||||
* @param controlProperty the control property
|
||||
* @param attribute the complex Attribute
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private static void alignBothComplex(Property<?> controlProperty, Property<?> attribute) {
|
||||
Property<Object> control = (Property<Object>) controlProperty;
|
||||
@@ -328,6 +442,12 @@ final class DirectBindingApplier {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a complex Attribute on the control and follows later changes. The control does not write back.
|
||||
*
|
||||
* @param controlProperty the control property
|
||||
* @param attribute the complex Attribute
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private static void alignTowardViewComplex(Property<?> controlProperty, Property<?> attribute) {
|
||||
Property<Object> control = (Property<Object>) controlProperty;
|
||||
@@ -336,6 +456,12 @@ final class DirectBindingApplier {
|
||||
source.addListener((observable, oldValue, newValue) -> control.setValue(modelBehind(newValue)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a complex Attribute once, then writes the Attribute when the control changes.
|
||||
*
|
||||
* @param controlProperty the control property
|
||||
* @param attribute the complex Attribute
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private static void alignTowardModelComplex(Property<?> controlProperty, Property<?> attribute) {
|
||||
Property<Object> control = (Property<Object>) controlProperty;
|
||||
@@ -349,6 +475,12 @@ final class DirectBindingApplier {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Model instance stored in a complex Attribute.
|
||||
*
|
||||
* @param value the Attribute value, a ViewModel or {@code null}
|
||||
* @return the Model instance, or {@code null}
|
||||
*/
|
||||
private static Object modelBehind(Object value) {
|
||||
if (value instanceof ViewModel viewModel) {
|
||||
return viewModel.model();
|
||||
@@ -356,16 +488,34 @@ final class DirectBindingApplier {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the control property to the shared list. Adds and removes are then visible on both sides.
|
||||
*
|
||||
* @param controlProperty the control property, such as {@code items}
|
||||
* @param values the list the ViewModel and the Model share
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private static void assignList(Property<?> controlProperty, ObservableList<?> values) {
|
||||
((Property<Object>) controlProperty).setValue(values);
|
||||
}
|
||||
|
||||
/**
|
||||
* Binds the control and the Attribute in both directions.
|
||||
*
|
||||
* @param controlProperty the control property
|
||||
* @param attribute the Attribute
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private static void alignBoth(Property<?> controlProperty, Property<?> attribute) {
|
||||
((Property<Object>) controlProperty).bindBidirectional((Property<Object>) attribute);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the Attribute and follows later changes. Edits to the control stay on the control.
|
||||
*
|
||||
* @param controlProperty the control property
|
||||
* @param attribute the Attribute
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private static void alignTowardView(Property<?> controlProperty, Property<?> attribute) {
|
||||
Property<Object> control = (Property<Object>) controlProperty;
|
||||
@@ -374,6 +524,12 @@ final class DirectBindingApplier {
|
||||
source.addListener((observable, oldValue, newValue) -> control.setValue(newValue));
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the current value, then writes the Attribute when the control changes.
|
||||
*
|
||||
* @param controlProperty the control property
|
||||
* @param attribute the Attribute
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private static void alignTowardModel(Property<?> controlProperty, Property<?> attribute) {
|
||||
Property<Object> control = (Property<Object>) controlProperty;
|
||||
|
||||
@@ -18,8 +18,14 @@ import java.net.URL;
|
||||
*/
|
||||
final class FxmlControllers {
|
||||
|
||||
/**
|
||||
* The FXML namespace used by {@code fx:controller}.
|
||||
*/
|
||||
private static final String FXML_NAMESPACE = "http://javafx.com/fxml";
|
||||
|
||||
/**
|
||||
* Prevents instantiation.
|
||||
*/
|
||||
private FxmlControllers() {
|
||||
}
|
||||
|
||||
@@ -42,6 +48,12 @@ final class FxmlControllers {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@code fx:controller} value on the document root.
|
||||
*
|
||||
* @param fxml the FXML document
|
||||
* @return the class name, or {@code null} when the document names none
|
||||
*/
|
||||
private static String controllerName(URL fxml) {
|
||||
try (InputStream input = fxml.openStream()) {
|
||||
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
|
||||
|
||||
@@ -17,11 +17,24 @@ import java.util.Set;
|
||||
*/
|
||||
final class LoadSession {
|
||||
|
||||
/**
|
||||
* The sessions open on this thread. The last one is the outermost load.
|
||||
*/
|
||||
private static final ThreadLocal<Deque<LoadSession>> STACK = new ThreadLocal<>();
|
||||
|
||||
/**
|
||||
* Binding lists created while this session is the current one, in document order.
|
||||
*/
|
||||
private final List<BindingList> bindingLists = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* Component classes already built during the outermost load.
|
||||
*/
|
||||
private final Set<Class<?>> built = new HashSet<>();
|
||||
|
||||
/**
|
||||
* Prevents instantiation from outside this class.
|
||||
*/
|
||||
private LoadSession() {
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,14 @@ import javafx.scene.Parent;
|
||||
*/
|
||||
public final class LoadedView {
|
||||
|
||||
/**
|
||||
* The scene-graph root of the loaded View.
|
||||
*/
|
||||
private final Parent root;
|
||||
|
||||
/**
|
||||
* The ViewModel the View is bound to, or a replacement that exposes JavaFX properties by name.
|
||||
*/
|
||||
private final Object viewModel;
|
||||
|
||||
/**
|
||||
|
||||
@@ -19,9 +19,24 @@ public final class Message {
|
||||
FATAL
|
||||
}
|
||||
|
||||
/**
|
||||
* How serious the notice is.
|
||||
*/
|
||||
private final Severity severity;
|
||||
|
||||
/**
|
||||
* The notice text. A binding failure updates this in place.
|
||||
*/
|
||||
private String text;
|
||||
|
||||
/**
|
||||
* Where the notice came from, such as a binding Path.
|
||||
*/
|
||||
private final String source;
|
||||
|
||||
/**
|
||||
* The child notice this one copies, or {@code null} when this notice was added directly.
|
||||
*/
|
||||
private Message origin;
|
||||
|
||||
/**
|
||||
|
||||
@@ -12,6 +12,9 @@ import javafx.scene.control.Label;
|
||||
*/
|
||||
public final class MessagePopup {
|
||||
|
||||
/**
|
||||
* Prevents instantiation.
|
||||
*/
|
||||
private MessagePopup() {
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,9 @@ import java.lang.reflect.Method;
|
||||
*/
|
||||
final class NamedProperties {
|
||||
|
||||
/**
|
||||
* Prevents instantiation.
|
||||
*/
|
||||
private NamedProperties() {
|
||||
}
|
||||
|
||||
|
||||
@@ -9,8 +9,19 @@ package de.neitzel.fx.view;
|
||||
*/
|
||||
public class Repeat {
|
||||
|
||||
/**
|
||||
* The {@code fx:id} of the pane that holds the Component children.
|
||||
*/
|
||||
private String pane;
|
||||
|
||||
/**
|
||||
* The Component class name. Each element gets a new instance of that class.
|
||||
*/
|
||||
private String component;
|
||||
|
||||
/**
|
||||
* The collection Attribute. The list holds the Model instances.
|
||||
*/
|
||||
private String collection;
|
||||
|
||||
/**
|
||||
|
||||
@@ -14,6 +14,9 @@ import java.util.Map;
|
||||
*/
|
||||
final class RepeatApplier {
|
||||
|
||||
/**
|
||||
* Prevents instantiation.
|
||||
*/
|
||||
private RepeatApplier() {
|
||||
}
|
||||
|
||||
@@ -47,6 +50,13 @@ final class RepeatApplier {
|
||||
listen(pane, type, context, values);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the collection Attribute named on the ViewModel.
|
||||
*
|
||||
* @param viewModel the ViewModel for this View
|
||||
* @param name the collection Attribute
|
||||
* @return the property, or {@code null} when the name is not an Attribute
|
||||
*/
|
||||
private static Property<?> collection(Object viewModel, String name) {
|
||||
if (viewModel instanceof ViewModel model) {
|
||||
return model.property(name);
|
||||
@@ -54,6 +64,17 @@ final class RepeatApplier {
|
||||
return NamedProperties.get(viewModel, name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds and removes Component children as the collection changes.
|
||||
* <p>
|
||||
* A permutation or an element update is left as it is. An add or remove keeps the children
|
||||
* in list order.
|
||||
*
|
||||
* @param pane the pane named by the repeat
|
||||
* @param type the Component class
|
||||
* @param context the container that creates each Component
|
||||
* @param values the collection
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private static void listen(Pane pane, Class<?> type, ApplicationContext context, ObservableList<?> values) {
|
||||
ObservableList<Object> elements = (ObservableList<Object>) values;
|
||||
@@ -74,12 +95,26 @@ final class RepeatApplier {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates one Component and sets its model to {@code element}.
|
||||
*
|
||||
* @param type the Component class
|
||||
* @param context the container
|
||||
* @param element the Model instance
|
||||
* @return the loaded Component
|
||||
*/
|
||||
private static Node child(Class<?> type, ApplicationContext context, Object element) {
|
||||
Object component = Components.fresh(type, context);
|
||||
Components.setModel(component, element);
|
||||
return (Node) component;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Component class named by the repeat.
|
||||
*
|
||||
* @param className the class name
|
||||
* @return the class
|
||||
*/
|
||||
private static Class<?> componentType(String className) {
|
||||
try {
|
||||
return Class.forName(className);
|
||||
|
||||
@@ -8,6 +8,9 @@ package de.neitzel.fx.view;
|
||||
*/
|
||||
final class SuppliedController implements HasViewModel {
|
||||
|
||||
/**
|
||||
* The ViewModel handed over before the bindings run.
|
||||
*/
|
||||
private ViewModel viewModel;
|
||||
|
||||
/**
|
||||
|
||||
@@ -47,8 +47,10 @@ import java.util.Set;
|
||||
* A collection shown with one Component for each element.
|
||||
* <p>
|
||||
* The developer places the Table in the View and names the collection Attribute, the Row
|
||||
* class, and each Column. Filter and Sort change which Rows are showing and in what order.
|
||||
* class, and each Column. A Column may name a starting width; otherwise the Columns open at
|
||||
* equal widths. Filter and Sort change which Rows are showing and in what order.
|
||||
* The collection keeps its own order. The body scrolls vertically and the header stays put.
|
||||
* Controls after the Column fields stay at their own width.
|
||||
*/
|
||||
@DefaultProperty("columns")
|
||||
public class Table extends VBox {
|
||||
@@ -202,6 +204,9 @@ public class Table extends VBox {
|
||||
* One Filter per Column. A null entry means that Column has no Filter yet.
|
||||
*/
|
||||
private final List<ValueFilter> valueFilters = new ArrayList<>();
|
||||
/**
|
||||
* Index of the Column whose popup is open, or {@code -1} when no popup is open.
|
||||
*/
|
||||
private int popupIndex = -1;
|
||||
|
||||
/**
|
||||
@@ -410,6 +415,9 @@ public class Table extends VBox {
|
||||
if (!seen.add(column.getAttribute())) {
|
||||
throw new IllegalArgumentException("Attribute \"" + column.getAttribute() + "\" is used twice");
|
||||
}
|
||||
if (column.hasWidth() && !(column.getWidth() > 0 && column.getWidth() < Double.POSITIVE_INFINITY)) {
|
||||
throw new IllegalArgumentException("Column \"" + column.getHeader() + "\" needs a positive width");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1344,40 +1352,98 @@ public class Table extends VBox {
|
||||
}
|
||||
|
||||
/**
|
||||
* Gives each Column its width, and the same width to each field in that order.
|
||||
* Gives each Column its width.
|
||||
* <p>
|
||||
* Columns with no starting width share the Table width equally. A starting width is kept, and
|
||||
* a later change of the Table's width does not redistribute the Columns. A starting width
|
||||
* narrower than the header text opens at the header text.
|
||||
*/
|
||||
private void ensureWidths() {
|
||||
if (columnWidths != null && columnWidths.length == columns.size()) {
|
||||
return;
|
||||
}
|
||||
columnWidths = new double[columns.size()];
|
||||
if (!anyStartingWidth()) {
|
||||
java.util.Arrays.fill(columnWidths, tableWidth() / columns.size());
|
||||
return;
|
||||
}
|
||||
double specified = 0;
|
||||
int open = 0;
|
||||
for (int index = 0; index < columns.size(); index++) {
|
||||
Column column = columns.get(index);
|
||||
if (column.hasWidth()) {
|
||||
double chosen = Math.max(column.getWidth(), minimumWidth(index));
|
||||
columnWidths[index] = chosen;
|
||||
specified += chosen;
|
||||
} else {
|
||||
open++;
|
||||
}
|
||||
}
|
||||
if (open == 0) {
|
||||
return;
|
||||
}
|
||||
double share = (tableWidth() - specified) / open;
|
||||
for (int index = 0; index < columns.size(); index++) {
|
||||
if (!columns.get(index).hasWidth()) {
|
||||
columnWidths[index] = Math.max(share, minimumWidth(index));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether any Column names a starting width.
|
||||
*
|
||||
* @return {@code true} when at least one Column has a starting width
|
||||
*/
|
||||
private boolean anyStartingWidth() {
|
||||
for (Column column : columns) {
|
||||
if (column.hasWidth()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the width the equal shares are taken from.
|
||||
*
|
||||
* @return the Table width, or a fallback when the Table has not been laid out
|
||||
*/
|
||||
private double tableWidth() {
|
||||
double width = getWidth() > 0 ? getWidth() : getPrefWidth();
|
||||
if (width <= 0) {
|
||||
width = columns.size() * 80.0;
|
||||
return columns.size() * 80.0;
|
||||
}
|
||||
columnWidths = new double[columns.size()];
|
||||
java.util.Arrays.fill(columnWidths, width / columns.size());
|
||||
return width;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the stored Column widths to the header and the fields.
|
||||
* <p>
|
||||
* The first fields, one per Column, take those widths. Controls after them keep their own
|
||||
* size. Spacing on an {@link HBox} row is repeated between the header cells.
|
||||
*/
|
||||
private void applyWidths() {
|
||||
if (columns.isEmpty() || columnWidths == null) {
|
||||
return;
|
||||
}
|
||||
double sum = 0;
|
||||
double columnSum = 0;
|
||||
for (double width : columnWidths) {
|
||||
sum += width;
|
||||
columnSum += width;
|
||||
}
|
||||
header.setPrefWidth(sum);
|
||||
header.setMinWidth(sum);
|
||||
double spacing = rowSpacing();
|
||||
header.setSpacing(spacing);
|
||||
double headerWidth = columnSum + spacing * Math.max(0, columns.size() - 1);
|
||||
header.setPrefWidth(headerWidth);
|
||||
header.setMinWidth(headerWidth);
|
||||
for (int index = 0; index < header.getChildren().size(); index++) {
|
||||
fixWidth((Region) header.getChildren().get(index), columnWidths[index]);
|
||||
}
|
||||
for (Node rowNode : body.getChildren()) {
|
||||
double rowWidth = rowNode instanceof HBox box ? widthOf(box, columnSum) : headerWidth;
|
||||
if (rowNode instanceof Region region) {
|
||||
region.setPrefWidth(sum);
|
||||
region.setMinWidth(sum);
|
||||
region.setPrefWidth(rowWidth);
|
||||
region.setMinWidth(rowWidth);
|
||||
}
|
||||
if (rowNode instanceof Parent parent) {
|
||||
int index = 0;
|
||||
@@ -1394,6 +1460,46 @@ public class Table extends VBox {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the spacing of a Row, so the header can use the same gaps.
|
||||
*
|
||||
* @return the spacing, or {@code 0} when no Row is an {@link HBox}
|
||||
*/
|
||||
private double rowSpacing() {
|
||||
for (Node row : body.getChildren()) {
|
||||
if (row instanceof HBox box) {
|
||||
return box.getSpacing();
|
||||
}
|
||||
}
|
||||
for (Node row : rows.values()) {
|
||||
if (row instanceof HBox box) {
|
||||
return box.getSpacing();
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the width that fits the Columns and any controls after them.
|
||||
*
|
||||
* @param box the Row
|
||||
* @param columnSum the sum of the Column widths
|
||||
* @return the Row width
|
||||
*/
|
||||
private double widthOf(HBox box, double columnSum) {
|
||||
double width = columnSum + box.getInsets().getLeft() + box.getInsets().getRight();
|
||||
if (box.getChildren().size() > 1) {
|
||||
width += box.getSpacing() * (box.getChildren().size() - 1);
|
||||
}
|
||||
for (int index = columnWidths.length; index < box.getChildren().size(); index++) {
|
||||
Node child = box.getChildren().get(index);
|
||||
if (child.isManaged() && child instanceof Region region) {
|
||||
width += region.prefWidth(-1);
|
||||
}
|
||||
}
|
||||
return width;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fixes one node's width.
|
||||
*
|
||||
|
||||
@@ -77,17 +77,59 @@ import java.util.function.Consumer;
|
||||
*/
|
||||
public class ViewModel {
|
||||
|
||||
/**
|
||||
* Receives error and fatal notices. Info and warn are not logged.
|
||||
*/
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(ViewModel.class);
|
||||
|
||||
/**
|
||||
* The Model instance this ViewModel was built for.
|
||||
*/
|
||||
private final Object model;
|
||||
|
||||
/**
|
||||
* JavaFX properties keyed by Attribute name.
|
||||
*/
|
||||
private final Map<String, Property<?>> properties = new HashMap<>();
|
||||
|
||||
/**
|
||||
* Attribute names whose value is a nested ViewModel.
|
||||
*/
|
||||
private final Set<String> complexAttributes = new HashSet<>();
|
||||
|
||||
/**
|
||||
* Notices on this ViewModel, oldest first.
|
||||
*/
|
||||
private final List<Message> messages = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* The current conversion failure for an Attribute name, so a later failure updates it in place.
|
||||
*/
|
||||
private final Map<String, Message> bindingFailures = new HashMap<>();
|
||||
|
||||
/**
|
||||
* Listeners told when a notice is added, removed, or updated.
|
||||
*/
|
||||
private final List<MessageListener> messageListeners = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* Mirrors that carry a child ViewModel's notices, keyed by the Attribute that leads to the child.
|
||||
*/
|
||||
private final Map<String, ChildMessageMirror> childMirrors = new HashMap<>();
|
||||
|
||||
/**
|
||||
* The text of {@link #latestMessage}, bound as {@code latestMessage}.
|
||||
*/
|
||||
private final StringProperty latestMessageText = new SimpleStringProperty();
|
||||
|
||||
/**
|
||||
* Callbacks registered by {@link MessagePopup#showNewMessages} and similar callers.
|
||||
*/
|
||||
private final List<Consumer<Message>> messageAdded = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* The notice most recently added, or {@code null} when there is none.
|
||||
*/
|
||||
private Message latestMessage;
|
||||
|
||||
/**
|
||||
@@ -191,6 +233,12 @@ public class ViewModel {
|
||||
return NamedProperties.get(this, name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the JavaBean attributes of {@code type}, excluding those declared on {@link Object}.
|
||||
*
|
||||
* @param type the Model class
|
||||
* @return the attributes
|
||||
*/
|
||||
private static List<PropertyDescriptor> descriptors(Class<?> type) {
|
||||
try {
|
||||
BeanInfo info = Introspector.getBeanInfo(type, Object.class);
|
||||
@@ -200,6 +248,12 @@ public class ViewModel {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether {@code type} itself has a read-write attribute and is not a simple value.
|
||||
*
|
||||
* @param type the declared attribute type
|
||||
* @return {@code true} when the attribute is complex
|
||||
*/
|
||||
private static boolean isComplex(Class<?> type) {
|
||||
if (simpleValue(type)) {
|
||||
return false;
|
||||
@@ -214,6 +268,15 @@ public class ViewModel {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether {@code type} stays a simple value even when it has setters.
|
||||
* <p>
|
||||
* Primitives, enums, arrays, strings, numbers, booleans, collections, maps, dates, and
|
||||
* {@code java.time} types are simple.
|
||||
*
|
||||
* @param type the declared attribute type
|
||||
* @return {@code true} when the attribute is not complex
|
||||
*/
|
||||
private static boolean simpleValue(Class<?> type) {
|
||||
if (type.isPrimitive() || type.isEnum() || type.isArray()
|
||||
|| type == String.class || type == Boolean.class) {
|
||||
@@ -232,6 +295,15 @@ public class ViewModel {
|
||||
return packaged != null && packaged.getName().startsWith("java.time");
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the JavaFX property for a declared attribute type.
|
||||
* <p>
|
||||
* A null number or boolean leaves the primitive property at its default.
|
||||
*
|
||||
* @param type the declared type
|
||||
* @param value the current value, or {@code null}
|
||||
* @return the property
|
||||
*/
|
||||
private static Property<?> createProperty(Class<?> type, Object value) {
|
||||
if (type == String.class) {
|
||||
return new SimpleStringProperty((String) value);
|
||||
@@ -254,6 +326,12 @@ public class ViewModel {
|
||||
return new SimpleObjectProperty<>(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an integer property. A null value leaves it at zero.
|
||||
*
|
||||
* @param value the current value, or {@code null}
|
||||
* @return the property
|
||||
*/
|
||||
private static IntegerProperty integerProperty(Object value) {
|
||||
SimpleIntegerProperty property = new SimpleIntegerProperty();
|
||||
if (value != null) {
|
||||
@@ -262,6 +340,12 @@ public class ViewModel {
|
||||
return property;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a long property. A null value leaves it at zero.
|
||||
*
|
||||
* @param value the current value, or {@code null}
|
||||
* @return the property
|
||||
*/
|
||||
private static LongProperty longProperty(Object value) {
|
||||
SimpleLongProperty property = new SimpleLongProperty();
|
||||
if (value != null) {
|
||||
@@ -270,6 +354,12 @@ public class ViewModel {
|
||||
return property;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a float property. A null value leaves it at zero.
|
||||
*
|
||||
* @param value the current value, or {@code null}
|
||||
* @return the property
|
||||
*/
|
||||
private static FloatProperty floatProperty(Object value) {
|
||||
SimpleFloatProperty property = new SimpleFloatProperty();
|
||||
if (value != null) {
|
||||
@@ -278,6 +368,12 @@ public class ViewModel {
|
||||
return property;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a double property. A null value leaves it at zero.
|
||||
*
|
||||
* @param value the current value, or {@code null}
|
||||
* @return the property
|
||||
*/
|
||||
private static DoubleProperty doubleProperty(Object value) {
|
||||
SimpleDoubleProperty property = new SimpleDoubleProperty();
|
||||
if (value != null) {
|
||||
@@ -286,6 +382,12 @@ public class ViewModel {
|
||||
return property;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a boolean property. A null value leaves it at {@code false}.
|
||||
*
|
||||
* @param value the current value, or {@code null}
|
||||
* @return the property
|
||||
*/
|
||||
private static BooleanProperty booleanProperty(Object value) {
|
||||
SimpleBooleanProperty property = new SimpleBooleanProperty();
|
||||
if (value != null) {
|
||||
@@ -294,6 +396,13 @@ public class ViewModel {
|
||||
return property;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the Attribute through its getter.
|
||||
*
|
||||
* @param getter the getter
|
||||
* @param name the Attribute name, used when the read fails
|
||||
* @return the current value
|
||||
*/
|
||||
private Object read(Method getter, String name) {
|
||||
try {
|
||||
return getter.invoke(model);
|
||||
@@ -302,6 +411,13 @@ public class ViewModel {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the Attribute when the property changes.
|
||||
*
|
||||
* @param property the JavaFX property
|
||||
* @param setter the setter
|
||||
* @param name the Attribute name, used when the write fails
|
||||
*/
|
||||
private void watch(Property<?> property, Method setter, String name) {
|
||||
property.addListener((observable, oldValue, newValue) -> write(setter, name, newValue));
|
||||
}
|
||||
@@ -314,14 +430,35 @@ public class ViewModel {
|
||||
*/
|
||||
private static final class LazyComplex extends SimpleObjectProperty<Object> {
|
||||
|
||||
/**
|
||||
* The Model instance, kept until the nested ViewModel is built.
|
||||
*/
|
||||
private final Object current;
|
||||
|
||||
/**
|
||||
* Whether {@link #get()} has already built the nested ViewModel.
|
||||
*/
|
||||
private boolean realized;
|
||||
|
||||
/**
|
||||
* Whether the change listener is still being attached. Reads during that time are ignored.
|
||||
*/
|
||||
private boolean attaching = true;
|
||||
|
||||
/**
|
||||
* Holds the Model instance until the nested ViewModel is built.
|
||||
*
|
||||
* @param current the Model instance, or {@code null}
|
||||
*/
|
||||
private LazyComplex(Object current) {
|
||||
this.current = current;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the nested ViewModel on the first read after the listener is attached.
|
||||
*
|
||||
* @return the nested ViewModel, or {@code null} when the Attribute is null
|
||||
*/
|
||||
@Override
|
||||
public Object get() {
|
||||
if (!attaching && !realized) {
|
||||
@@ -331,15 +468,31 @@ public class ViewModel {
|
||||
return super.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows the next {@link #get()} to build the nested ViewModel.
|
||||
*/
|
||||
private void attached() {
|
||||
attaching = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether this attribute still holds {@code instance} and has not built its ViewModel.
|
||||
*
|
||||
* @param instance the Model instance just selected elsewhere
|
||||
* @return {@code true} when a shared read should realize this attribute too
|
||||
*/
|
||||
private boolean pending(Object instance) {
|
||||
return !realized && current == instance;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the Model instance behind a complex Attribute and carries that child's notices.
|
||||
*
|
||||
* @param property the JavaFX property
|
||||
* @param setter the setter
|
||||
* @param name the Attribute name
|
||||
*/
|
||||
private void watchComplex(Property<?> property, Method setter, String name) {
|
||||
property.addListener((observable, oldValue, newValue) -> {
|
||||
if (oldValue != newValue) {
|
||||
@@ -360,6 +513,10 @@ public class ViewModel {
|
||||
* @param name the Attribute that leads to the child
|
||||
* @param child the child ViewModel
|
||||
*/
|
||||
private void attachChild(String name, ViewModel child) {
|
||||
childMirrors.put(name, ChildMessageMirror.attach(this, name, child));
|
||||
}
|
||||
|
||||
/**
|
||||
* Realizes every other Complex attribute that holds the same Model instance.
|
||||
* <p>
|
||||
@@ -379,10 +536,6 @@ public class ViewModel {
|
||||
}
|
||||
}
|
||||
|
||||
private void attachChild(String name, ViewModel child) {
|
||||
childMirrors.put(name, ChildMessageMirror.attach(this, name, child));
|
||||
}
|
||||
|
||||
/**
|
||||
* Drops the copies carried for an Attribute and stops following that child.
|
||||
*
|
||||
@@ -395,6 +548,13 @@ public class ViewModel {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the Attribute through its setter.
|
||||
*
|
||||
* @param setter the setter
|
||||
* @param name the Attribute name, used when the write fails
|
||||
* @param value the value
|
||||
*/
|
||||
private void write(Method setter, String name, Object value) {
|
||||
try {
|
||||
setter.invoke(model, value);
|
||||
@@ -607,6 +767,12 @@ public class ViewModel {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the getter's collection holds simple values.
|
||||
*
|
||||
* @param getter the collection getter
|
||||
* @return {@code true} when the element type is a simple value
|
||||
*/
|
||||
private static boolean isSimpleValueCollection(Method getter) {
|
||||
if (!Collection.class.isAssignableFrom(getter.getReturnType())) {
|
||||
return false;
|
||||
@@ -615,6 +781,12 @@ public class ViewModel {
|
||||
return element != null && isSimpleValue(element);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the getter's collection holds Model instances.
|
||||
*
|
||||
* @param getter the collection getter
|
||||
* @return {@code true} when the element type is complex
|
||||
*/
|
||||
private static boolean isModelCollection(Method getter) {
|
||||
if (!Collection.class.isAssignableFrom(getter.getReturnType())) {
|
||||
return false;
|
||||
@@ -623,6 +795,12 @@ public class ViewModel {
|
||||
return element != null && isComplex(element);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the single type argument of a parameterized type.
|
||||
*
|
||||
* @param type the generic return type
|
||||
* @return the element class, or {@code null} when it cannot be read
|
||||
*/
|
||||
private static Class<?> elementType(Type type) {
|
||||
if (!(type instanceof ParameterizedType parameterized) || parameterized.getActualTypeArguments().length != 1) {
|
||||
return null;
|
||||
@@ -630,6 +808,12 @@ public class ViewModel {
|
||||
return classOf(parameterized.getActualTypeArguments()[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the raw class of a type argument, including a wildcard's upper bound.
|
||||
*
|
||||
* @param type the type argument
|
||||
* @return the class, or {@code null} when it cannot be read
|
||||
*/
|
||||
private static Class<?> classOf(Type type) {
|
||||
if (type instanceof Class<?> raw) {
|
||||
return raw;
|
||||
@@ -643,6 +827,12 @@ public class ViewModel {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether a collection element stays a plain value rather than a ViewModel.
|
||||
*
|
||||
* @param type the element type
|
||||
* @return {@code true} when the element is a simple value
|
||||
*/
|
||||
private static boolean isSimpleValue(Class<?> type) {
|
||||
if (type == String.class || type == Boolean.class || type == boolean.class || type.isEnum()) {
|
||||
return true;
|
||||
@@ -659,6 +849,14 @@ public class ViewModel {
|
||||
return TemporalAccessor.class.isAssignableFrom(type) || TemporalAmount.class.isAssignableFrom(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps a collection in one {@link ObservableList} and stores it on the Model when the setter accepts it.
|
||||
*
|
||||
* @param setter the setter
|
||||
* @param name the Attribute name
|
||||
* @param current the current collection
|
||||
* @return the list the View watches
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private Object shareSimpleList(Method setter, String name, Object current) {
|
||||
if (!(current instanceof Collection<?> collection)) {
|
||||
|
||||
@@ -12,8 +12,14 @@ import java.util.Map;
|
||||
*/
|
||||
final class ViewModels {
|
||||
|
||||
/**
|
||||
* ViewModels already built, keyed by Model identity.
|
||||
*/
|
||||
private static final Map<Object, Object> BY_INSTANCE = new IdentityHashMap<>();
|
||||
|
||||
/**
|
||||
* Prevents instantiation.
|
||||
*/
|
||||
private ViewModels() {
|
||||
}
|
||||
|
||||
@@ -75,6 +81,16 @@ final class ViewModels {
|
||||
return annotation.value();
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a ViewModel or a replacement with the Model instance.
|
||||
* <p>
|
||||
* A result that is not a {@link ViewModel} is registered here. A {@link ViewModel} registers
|
||||
* itself from its constructor, before its Attributes are walked.
|
||||
*
|
||||
* @param model the Model instance
|
||||
* @param viewModelClass the class to construct
|
||||
* @return the constructed object
|
||||
*/
|
||||
private static Object construct(Object model, Class<?> viewModelClass) {
|
||||
Constructor<?> constructor = constructor(model, viewModelClass);
|
||||
constructor.setAccessible(true);
|
||||
@@ -94,6 +110,13 @@ final class ViewModels {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the constructor that takes the Model, preferring the most specific parameter type.
|
||||
*
|
||||
* @param model the Model instance
|
||||
* @param viewModelClass the class to construct
|
||||
* @return the constructor
|
||||
*/
|
||||
private static Constructor<?> constructor(Object model, Class<?> viewModelClass) {
|
||||
Constructor<?> chosen = null;
|
||||
for (Constructor<?> candidate : viewModelClass.getDeclaredConstructors()) {
|
||||
|
||||
@@ -24,8 +24,14 @@ import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
|
||||
/**
|
||||
* A child notice is carried onto its parent, with the attribute name prepended to the source.
|
||||
*/
|
||||
class CarryChildMessageTest {
|
||||
|
||||
/**
|
||||
* Starts the JavaFX toolkit once for this class.
|
||||
*/
|
||||
@BeforeAll
|
||||
static void startJavaFx() throws InterruptedException {
|
||||
CountDownLatch started = new CountDownLatch(1);
|
||||
@@ -39,6 +45,9 @@ class CarryChildMessageTest {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A street notice on the address appears on the person as address street.
|
||||
*/
|
||||
@Test
|
||||
void aStreetNoticeOnTheAddressAppearsOnThePersonAsAddressStreet() throws Exception {
|
||||
onFx(() -> {
|
||||
@@ -64,6 +73,9 @@ class CarryChildMessageTest {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* A nested street notice reads as person address street.
|
||||
*/
|
||||
@Test
|
||||
void aNestedStreetNoticeReadsAsPersonAddressStreet() throws Exception {
|
||||
onFx(() -> {
|
||||
@@ -90,6 +102,9 @@ class CarryChildMessageTest {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* A bad street is owned by the address and carried as address street.
|
||||
*/
|
||||
@Test
|
||||
void aBadStreetIsOwnedByTheAddressAndCarriedAsAddressStreet() throws Exception {
|
||||
onFx(() -> {
|
||||
@@ -120,6 +135,9 @@ class CarryChildMessageTest {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* A fixed street drops the notice from the address and the person.
|
||||
*/
|
||||
@Test
|
||||
void aFixedStreetDropsTheNoticeFromTheAddressAndThePerson() throws Exception {
|
||||
onFx(() -> {
|
||||
@@ -143,6 +161,9 @@ class CarryChildMessageTest {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Another bad street updates the carried notice in place.
|
||||
*/
|
||||
@Test
|
||||
void anotherBadStreetUpdatesTheCarriedNoticeInPlace() throws Exception {
|
||||
onFx(() -> {
|
||||
@@ -186,6 +207,9 @@ class CarryChildMessageTest {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Clearing the address drops the carried notice and a new address brings its own.
|
||||
*/
|
||||
@Test
|
||||
void clearingTheAddressDropsTheCarriedNoticeAndANewAddressBringsItsOwn() throws Exception {
|
||||
onFx(() -> {
|
||||
@@ -225,6 +249,9 @@ class CarryChildMessageTest {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fixing the street leaves the persons own notice.
|
||||
*/
|
||||
@Test
|
||||
void fixingTheStreetLeavesThePersonsOwnNotice() throws Exception {
|
||||
onFx(() -> {
|
||||
@@ -265,6 +292,9 @@ class CarryChildMessageTest {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* A fixed nested street drops the notice up the path.
|
||||
*/
|
||||
@Test
|
||||
void aFixedNestedStreetDropsTheNoticeUpThePath() throws Exception {
|
||||
onFx(() -> {
|
||||
@@ -293,6 +323,9 @@ class CarryChildMessageTest {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* A shared address carries the street notice on home and on work.
|
||||
*/
|
||||
@Test
|
||||
void aSharedAddressCarriesTheStreetNoticeOnHomeAndOnWork() throws Exception {
|
||||
onFx(() -> {
|
||||
@@ -318,6 +351,11 @@ class CarryChildMessageTest {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs one check on the JavaFX application thread.
|
||||
*
|
||||
* @param action the check
|
||||
*/
|
||||
private static void onFx(FxAction action) throws Exception {
|
||||
if (Platform.isFxApplicationThread()) {
|
||||
action.run();
|
||||
@@ -349,8 +387,16 @@ class CarryChildMessageTest {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* One check that runs on the JavaFX application thread.
|
||||
*/
|
||||
@FunctionalInterface
|
||||
private interface FxAction {
|
||||
/**
|
||||
* Runs the check.
|
||||
*
|
||||
* @throws Exception when the check fails
|
||||
*/
|
||||
void run() throws Exception;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,8 +21,14 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
|
||||
/**
|
||||
* The controller and the view-model class are chosen from the document and then reused.
|
||||
*/
|
||||
class ChooseControllerAndViewModelTest {
|
||||
|
||||
/**
|
||||
* Starts the JavaFX toolkit once for this class.
|
||||
*/
|
||||
@BeforeAll
|
||||
static void startJavaFx() throws InterruptedException {
|
||||
CountDownLatch started = new CountDownLatch(1);
|
||||
@@ -36,6 +42,9 @@ class ChooseControllerAndViewModelTest {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Label shows the name recorded when the view model was handed over.
|
||||
*/
|
||||
@Test
|
||||
void labelShowsTheNameRecordedWhenTheViewModelWasHandedOver() throws Exception {
|
||||
onFx(() -> {
|
||||
@@ -49,6 +58,9 @@ class ChooseControllerAndViewModelTest {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Label shows the greeting injected into the controller.
|
||||
*/
|
||||
@Test
|
||||
void labelShowsTheGreetingInjectedIntoTheController() throws Exception {
|
||||
onFx(() -> {
|
||||
@@ -62,6 +74,9 @@ class ChooseControllerAndViewModelTest {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Label shows the title from the named view model.
|
||||
*/
|
||||
@Test
|
||||
void labelShowsTheTitleFromTheNamedViewModel() throws Exception {
|
||||
onFx(() -> {
|
||||
@@ -81,6 +96,9 @@ class ChooseControllerAndViewModelTest {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Label shows the badge from a replacement view model.
|
||||
*/
|
||||
@Test
|
||||
void labelShowsTheBadgeFromAReplacementViewModel() throws Exception {
|
||||
onFx(() -> {
|
||||
@@ -100,6 +118,9 @@ class ChooseControllerAndViewModelTest {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Component label shows the caption from the named view model.
|
||||
*/
|
||||
@Test
|
||||
void componentLabelShowsTheCaptionFromTheNamedViewModel() throws Exception {
|
||||
onFx(() -> {
|
||||
@@ -114,6 +135,9 @@ class ChooseControllerAndViewModelTest {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* A later view that names a different class fails and leaves the first view.
|
||||
*/
|
||||
@Test
|
||||
void aLaterViewThatNamesADifferentClassFailsAndLeavesTheFirstView() throws Exception {
|
||||
onFx(() -> {
|
||||
@@ -134,6 +158,9 @@ class ChooseControllerAndViewModelTest {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* A later view with no annotation reuses the view model.
|
||||
*/
|
||||
@Test
|
||||
void aLaterViewWithNoAnnotationReusesTheViewModel() throws Exception {
|
||||
onFx(() -> {
|
||||
@@ -155,6 +182,11 @@ class ChooseControllerAndViewModelTest {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs one check on the JavaFX application thread.
|
||||
*
|
||||
* @param action the check
|
||||
*/
|
||||
private static void onFx(FxAction action) throws Exception {
|
||||
if (Platform.isFxApplicationThread()) {
|
||||
action.run();
|
||||
@@ -186,8 +218,16 @@ class ChooseControllerAndViewModelTest {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* One check that runs on the JavaFX application thread.
|
||||
*/
|
||||
@FunctionalInterface
|
||||
private interface FxAction {
|
||||
/**
|
||||
* Runs the check.
|
||||
*
|
||||
* @throws Exception when the check fails
|
||||
*/
|
||||
void run() throws Exception;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,8 +18,14 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
|
||||
/**
|
||||
* A computed binding writes a controller method result and refreshes it when another property changes.
|
||||
*/
|
||||
class ComputeAValueTest {
|
||||
|
||||
/**
|
||||
* Starts the JavaFX toolkit once for this class.
|
||||
*/
|
||||
@BeforeAll
|
||||
static void startJavaFx() throws InterruptedException {
|
||||
CountDownLatch started = new CountDownLatch(1);
|
||||
@@ -33,6 +39,9 @@ class ComputeAValueTest {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Computed binding shows the method result and leaves the model unchanged.
|
||||
*/
|
||||
@Test
|
||||
void computedBindingShowsTheMethodResultAndLeavesTheModelUnchanged() throws Exception {
|
||||
onFx(() -> {
|
||||
@@ -49,6 +58,9 @@ class ComputeAValueTest {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Typing in another text field updates the computed label.
|
||||
*/
|
||||
@Test
|
||||
void typingInAnotherTextFieldUpdatesTheComputedLabel() throws Exception {
|
||||
onFx(() -> {
|
||||
@@ -65,6 +77,9 @@ class ComputeAValueTest {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Setting a view model property updates the computed label.
|
||||
*/
|
||||
@Test
|
||||
void settingAViewModelPropertyUpdatesTheComputedLabel() throws Exception {
|
||||
onFx(() -> {
|
||||
@@ -82,6 +97,9 @@ class ComputeAValueTest {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Changing the computed label does not run the method again.
|
||||
*/
|
||||
@Test
|
||||
void changingTheComputedLabelDoesNotRunTheMethodAgain() throws Exception {
|
||||
onFx(() -> {
|
||||
@@ -97,6 +115,11 @@ class ComputeAValueTest {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs one check on the JavaFX application thread.
|
||||
*
|
||||
* @param action the check
|
||||
*/
|
||||
private static void onFx(FxAction action) throws Exception {
|
||||
if (Platform.isFxApplicationThread()) {
|
||||
action.run();
|
||||
@@ -128,8 +151,16 @@ class ComputeAValueTest {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* One check that runs on the JavaFX application thread.
|
||||
*/
|
||||
@FunctionalInterface
|
||||
private interface FxAction {
|
||||
/**
|
||||
* Runs the check.
|
||||
*
|
||||
* @throws Exception when the check fails
|
||||
*/
|
||||
void run() throws Exception;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,8 +25,14 @@ import static org.junit.jupiter.api.Assertions.assertSame;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
|
||||
/**
|
||||
* A converter failure keeps the previous value and records one error message.
|
||||
*/
|
||||
class ConvertAndRecordFailureTest {
|
||||
|
||||
/**
|
||||
* Starts the JavaFX toolkit once for this class.
|
||||
*/
|
||||
@BeforeAll
|
||||
static void startJavaFx() throws InterruptedException {
|
||||
CountDownLatch started = new CountDownLatch(1);
|
||||
@@ -40,6 +46,9 @@ class ConvertAndRecordFailureTest {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Injected converter prefixes the age text.
|
||||
*/
|
||||
@Test
|
||||
void injectedConverterPrefixesTheAgeText() throws Exception {
|
||||
onFx(() -> {
|
||||
@@ -54,6 +63,9 @@ class ConvertAndRecordFailureTest {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Typing a prefixed age updates the model.
|
||||
*/
|
||||
@Test
|
||||
void typingAPrefixedAgeUpdatesTheModel() throws Exception {
|
||||
onFx(() -> {
|
||||
@@ -69,6 +81,9 @@ class ConvertAndRecordFailureTest {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Toward view loads when the reverse method is absent.
|
||||
*/
|
||||
@Test
|
||||
void towardViewLoadsWhenTheReverseMethodIsAbsent() throws Exception {
|
||||
onFx(() -> {
|
||||
@@ -85,6 +100,9 @@ class ConvertAndRecordFailureTest {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Toward view converter follows a later age.
|
||||
*/
|
||||
@Test
|
||||
void towardViewConverterFollowsALaterAge() throws Exception {
|
||||
onFx(() -> {
|
||||
@@ -103,6 +121,9 @@ class ConvertAndRecordFailureTest {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Toward model converts typed text without following the attribute.
|
||||
*/
|
||||
@Test
|
||||
void towardModelConvertsTypedTextWithoutFollowingTheAttribute() throws Exception {
|
||||
onFx(() -> {
|
||||
@@ -123,6 +144,9 @@ class ConvertAndRecordFailureTest {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Bad age text keeps the previous int and one error message.
|
||||
*/
|
||||
@Test
|
||||
void badAgeTextKeepsThePreviousIntAndOneErrorMessage() throws Exception {
|
||||
onFx(() -> {
|
||||
@@ -146,6 +170,9 @@ class ConvertAndRecordFailureTest {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Another bad age updates the same message.
|
||||
*/
|
||||
@Test
|
||||
void anotherBadAgeUpdatesTheSameMessage() throws Exception {
|
||||
onFx(() -> {
|
||||
@@ -168,6 +195,9 @@ class ConvertAndRecordFailureTest {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* A valid age removes the failure message.
|
||||
*/
|
||||
@Test
|
||||
void aValidAgeRemovesTheFailureMessage() throws Exception {
|
||||
onFx(() -> {
|
||||
@@ -187,6 +217,9 @@ class ConvertAndRecordFailureTest {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Developer info stays latest and is not logged.
|
||||
*/
|
||||
@Test
|
||||
void developerInfoStaysLatestAndIsNotLogged() throws Exception {
|
||||
onFx(() -> {
|
||||
@@ -212,6 +245,9 @@ class ConvertAndRecordFailureTest {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Error and fatal messages are written to the log.
|
||||
*/
|
||||
@Test
|
||||
void errorAndFatalMessagesAreWrittenToTheLog() throws Exception {
|
||||
onFx(() -> {
|
||||
@@ -240,6 +276,11 @@ class ConvertAndRecordFailureTest {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs one check on the JavaFX application thread.
|
||||
*
|
||||
* @param action the check
|
||||
*/
|
||||
private static void onFx(FxAction action) throws Exception {
|
||||
if (Platform.isFxApplicationThread()) {
|
||||
action.run();
|
||||
@@ -271,6 +312,12 @@ class ConvertAndRecordFailureTest {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs one check and returns what it wrote to standard error.
|
||||
*
|
||||
* @param action the check
|
||||
* @return the text written to standard error
|
||||
*/
|
||||
private static String captureErr(FxAction action) throws Exception {
|
||||
PrintStream previous = System.err;
|
||||
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
|
||||
@@ -285,8 +332,16 @@ class ConvertAndRecordFailureTest {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* One check that runs on the JavaFX application thread.
|
||||
*/
|
||||
@FunctionalInterface
|
||||
private interface FxAction {
|
||||
/**
|
||||
* Runs the check.
|
||||
*
|
||||
* @throws Exception when the check fails
|
||||
*/
|
||||
void run() throws Exception;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,8 +23,14 @@ import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
|
||||
/**
|
||||
* A component edits one model instance, and a nested component edits the next.
|
||||
*/
|
||||
class EditAddressInAComponentTest {
|
||||
|
||||
/**
|
||||
* Starts the JavaFX toolkit once for this class.
|
||||
*/
|
||||
@BeforeAll
|
||||
static void startJavaFx() throws InterruptedException {
|
||||
CountDownLatch started = new CountDownLatch(1);
|
||||
@@ -38,6 +44,9 @@ class EditAddressInAComponentTest {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Editing the street updates that address.
|
||||
*/
|
||||
@Test
|
||||
void editingTheStreetUpdatesThatAddress() throws Exception {
|
||||
onFx(() -> {
|
||||
@@ -58,6 +67,9 @@ class EditAddressInAComponentTest {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Null address leaves the model unset and keeps the street text.
|
||||
*/
|
||||
@Test
|
||||
void nullAddressLeavesTheModelUnsetAndKeepsTheStreetText() throws Exception {
|
||||
onFx(() -> {
|
||||
@@ -75,6 +87,9 @@ class EditAddressInAComponentTest {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Street label shows the injected prefix.
|
||||
*/
|
||||
@Test
|
||||
void streetLabelShowsTheInjectedPrefix() throws Exception {
|
||||
onFx(() -> {
|
||||
@@ -90,6 +105,9 @@ class EditAddressInAComponentTest {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Component receives the address view model before its bindings run.
|
||||
*/
|
||||
@Test
|
||||
void componentReceivesTheAddressViewModelBeforeItsBindingsRun() throws Exception {
|
||||
onFx(() -> {
|
||||
@@ -108,6 +126,9 @@ class EditAddressInAComponentTest {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Nested component shows the inner street.
|
||||
*/
|
||||
@Test
|
||||
void nestedComponentShowsTheInnerStreet() throws Exception {
|
||||
onFx(() -> {
|
||||
@@ -124,6 +145,11 @@ class EditAddressInAComponentTest {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs one check on the JavaFX application thread.
|
||||
*
|
||||
* @param action the check
|
||||
*/
|
||||
private static void onFx(FxAction action) throws Exception {
|
||||
if (Platform.isFxApplicationThread()) {
|
||||
action.run();
|
||||
@@ -155,8 +181,16 @@ class EditAddressInAComponentTest {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* One check that runs on the JavaFX application thread.
|
||||
*/
|
||||
@FunctionalInterface
|
||||
private interface FxAction {
|
||||
/**
|
||||
* Runs the check.
|
||||
*
|
||||
* @throws Exception when the check fails
|
||||
*/
|
||||
void run() throws Exception;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
package de.neitzel.fx.view;
|
||||
|
||||
import de.neitzel.fx.view.component.ActionRow;
|
||||
import de.neitzel.fx.view.fixture.Host;
|
||||
import de.neitzel.fx.view.fixture.Visit;
|
||||
import de.neitzel.injection.ApplicationContext;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.net.URL;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
|
||||
/**
|
||||
* A control after the Column fields keeps its own width, and the header uses the Row's spacing.
|
||||
*/
|
||||
class KeepAControlAfterTheColumnsTest {
|
||||
|
||||
/**
|
||||
* Starts the JavaFX toolkit once for this class.
|
||||
*/
|
||||
@BeforeAll
|
||||
static void startJavaFx() throws InterruptedException {
|
||||
FxThread.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* The remove button stays its own size and the row is wide enough to show it.
|
||||
*/
|
||||
@Test
|
||||
void aControlAfterTheFieldsKeepsItsOwnWidth() throws Exception {
|
||||
FxThread.onFx(() -> {
|
||||
Table table = table();
|
||||
HBox header = header(table);
|
||||
assertEquals(2, header.getChildren().size());
|
||||
assertEquals(6, header.getSpacing());
|
||||
assertEquals(306, header.getPrefWidth());
|
||||
ActionRow row = row(table);
|
||||
Region street = (Region) row.getChildren().get(0);
|
||||
Region city = (Region) row.getChildren().get(1);
|
||||
Button remove = (Button) row.lookup("#removeButton");
|
||||
assertEquals(150, street.getPrefWidth());
|
||||
assertEquals(150, city.getPrefWidth());
|
||||
assertEquals(Region.USE_COMPUTED_SIZE, remove.getMinWidth());
|
||||
assertEquals(Region.USE_COMPUTED_SIZE, remove.getPrefWidth());
|
||||
assertEquals(Region.USE_COMPUTED_SIZE, remove.getMaxWidth());
|
||||
assertEquals("Remove", remove.getText());
|
||||
assertEquals(300 + 12 + remove.prefWidth(-1), row.getPrefWidth());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the visits table whose row has a button after the fields.
|
||||
*
|
||||
* @return the table
|
||||
*/
|
||||
private static Table table() {
|
||||
URL fxml = KeepAControlAfterTheColumnsTest.class.getResource("show-visits-action.fxml");
|
||||
assertNotNull(fxml);
|
||||
Host host = new Host();
|
||||
host.setVisits(new ArrayList<>(List.of(new Visit("Main", "Berlin"))));
|
||||
LoadedView loaded = ViewLoader.load(fxml, host, new ApplicationContext("de.neitzel.fx.view.component"));
|
||||
return (Table) loaded.getRoot().lookup("#visitsTable");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the header row.
|
||||
*
|
||||
* @param table the table
|
||||
* @return the header
|
||||
*/
|
||||
private static HBox header(Table table) {
|
||||
ScrollPane headerScroll = (ScrollPane) table.lookup(".table-header-scroll");
|
||||
return (HBox) headerScroll.getContent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the first row.
|
||||
*
|
||||
* @param table the table
|
||||
* @return the row
|
||||
*/
|
||||
private static ActionRow row(Table table) {
|
||||
ScrollPane scroll = (ScrollPane) table.lookup("." + Table.SCROLL_STYLE);
|
||||
return (ActionRow) ((VBox) scroll.getContent()).getChildren().get(0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
package de.neitzel.fx.view;
|
||||
|
||||
import de.neitzel.fx.view.fixture.Host;
|
||||
import de.neitzel.fx.view.fixture.Visit;
|
||||
import de.neitzel.injection.ApplicationContext;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.input.MouseButton;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.text.Text;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.net.URL;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
/**
|
||||
* A Column opens at its starting width. A Column without one takes an equal share.
|
||||
*/
|
||||
class OpenAtAGivenWidthTest {
|
||||
|
||||
/**
|
||||
* Starts the JavaFX toolkit once for this class.
|
||||
*/
|
||||
@BeforeAll
|
||||
static void startJavaFx() throws InterruptedException {
|
||||
FxThread.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Named widths are kept, including after a drag and a change of the Table's width.
|
||||
*/
|
||||
@Test
|
||||
void aColumnOpensAtItsWidth() throws Exception {
|
||||
FxThread.onFx(() -> {
|
||||
Table table = table("show-visits-widths.fxml");
|
||||
HBox header = header(table);
|
||||
Region street = (Region) header.getChildren().get(0);
|
||||
Region city = (Region) header.getChildren().get(1);
|
||||
assertEquals(220, street.getPrefWidth());
|
||||
assertEquals(80, city.getPrefWidth());
|
||||
assertEquals(220, field(table, 0).getPrefWidth());
|
||||
assertEquals(80, field(table, 1).getPrefWidth());
|
||||
drag(street.lookup("." + Table.RESIZE_STYLE), 0, 40);
|
||||
assertEquals(260, street.getPrefWidth());
|
||||
assertEquals(80, city.getPrefWidth());
|
||||
table.setPrefWidth(900);
|
||||
assertEquals(260, street.getPrefWidth());
|
||||
assertEquals(80, city.getPrefWidth());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* A Column with no width takes what the named widths leave of the Table.
|
||||
*/
|
||||
@Test
|
||||
void aColumnWithoutAWidthTakesTheRemainingShare() throws Exception {
|
||||
FxThread.onFx(() -> {
|
||||
HBox header = header(table("show-visits-one-width.fxml"));
|
||||
assertEquals(100, ((Region) header.getChildren().get(0)).getPrefWidth());
|
||||
assertEquals(200, ((Region) header.getChildren().get(1)).getPrefWidth());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* A starting width narrower than the header text opens wide enough to show that text.
|
||||
*/
|
||||
@Test
|
||||
void aNarrowWidthOpensAtTheHeaderText() throws Exception {
|
||||
FxThread.onFx(() -> {
|
||||
HBox header = header(table("show-visits-narrow.fxml"));
|
||||
assertEquals(280, ((Region) header.getChildren().get(0)).getPrefWidth());
|
||||
Text text = new Text("City");
|
||||
double headerText = Math.ceil(text.getLayoutBounds().getWidth()) + 8;
|
||||
assertEquals(headerText, ((Region) header.getChildren().get(1)).getPrefWidth());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* A width that is not positive fails the load.
|
||||
*/
|
||||
@Test
|
||||
void aWidthThatIsNotPositiveFailsToLoad() throws Exception {
|
||||
FxThread.onFx(() -> assertThrows(IllegalArgumentException.class, () -> table("show-visits-bad-width.fxml")));
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a visits table.
|
||||
*
|
||||
* @param name the document name
|
||||
* @return the table
|
||||
*/
|
||||
private static Table table(String name) {
|
||||
URL fxml = OpenAtAGivenWidthTest.class.getResource(name);
|
||||
assertNotNull(fxml);
|
||||
Host host = new Host();
|
||||
host.setVisits(new ArrayList<>(List.of(new Visit("Main", "Berlin"))));
|
||||
LoadedView loaded = ViewLoader.load(fxml, host, new ApplicationContext("de.neitzel.fx.view.component"));
|
||||
return (Table) loaded.getRoot().lookup("#visitsTable");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the header row.
|
||||
*
|
||||
* @param table the table
|
||||
* @return the header
|
||||
*/
|
||||
private static HBox header(Table table) {
|
||||
ScrollPane headerScroll = (ScrollPane) table.lookup(".table-header-scroll");
|
||||
return (HBox) headerScroll.getContent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns one field of the first row.
|
||||
*
|
||||
* @param table the table
|
||||
* @param index the field index
|
||||
* @return the field
|
||||
*/
|
||||
private static Region field(Table table, int index) {
|
||||
ScrollPane scroll = (ScrollPane) table.lookup("." + Table.SCROLL_STYLE);
|
||||
javafx.scene.layout.VBox body = (javafx.scene.layout.VBox) scroll.getContent();
|
||||
return (Region) ((javafx.scene.Parent) body.getChildren().get(0)).getChildrenUnmodifiable().get(index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Drags a resize handle from one screen x to another.
|
||||
*
|
||||
* @param handle the handle
|
||||
* @param from the start screen x
|
||||
* @param to the end screen x
|
||||
*/
|
||||
private static void drag(javafx.scene.Node handle, double from, double to) {
|
||||
handle.fireEvent(mouse(MouseEvent.MOUSE_PRESSED, from));
|
||||
handle.fireEvent(mouse(MouseEvent.MOUSE_DRAGGED, to));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a primary mouse event.
|
||||
*
|
||||
* @param type the event type
|
||||
* @param screen the screen x
|
||||
* @return the event
|
||||
*/
|
||||
private static MouseEvent mouse(javafx.event.EventType<MouseEvent> type, double screen) {
|
||||
return new MouseEvent(type, screen, 0, screen, 0, MouseButton.PRIMARY, 1,
|
||||
false, false, false, false, true, false, false, false, false, false, null);
|
||||
}
|
||||
}
|
||||
@@ -31,8 +31,14 @@ import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
|
||||
/**
|
||||
* A path steps through a nested view model, and a null step leaves the binding unapplied.
|
||||
*/
|
||||
class ReachNestedAttributeTest {
|
||||
|
||||
/**
|
||||
* Starts the JavaFX toolkit once for this class.
|
||||
*/
|
||||
@BeforeAll
|
||||
static void startJavaFx() throws InterruptedException {
|
||||
CountDownLatch started = new CountDownLatch(1);
|
||||
@@ -46,6 +52,9 @@ class ReachNestedAttributeTest {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Complex attribute holds the view model for the nested address.
|
||||
*/
|
||||
@Test
|
||||
void complexAttributeHoldsTheViewModelForTheNestedAddress() throws Exception {
|
||||
onFx(() -> {
|
||||
@@ -61,6 +70,9 @@ class ReachNestedAttributeTest {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Both ways text field reads and writes the nested street.
|
||||
*/
|
||||
@Test
|
||||
void bothWaysTextFieldReadsAndWritesTheNestedStreet() throws Exception {
|
||||
onFx(() -> {
|
||||
@@ -78,6 +90,9 @@ class ReachNestedAttributeTest {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Toward view label follows the nested street and does not write.
|
||||
*/
|
||||
@Test
|
||||
void towardViewLabelFollowsTheNestedStreetAndDoesNotWrite() throws Exception {
|
||||
onFx(() -> {
|
||||
@@ -99,6 +114,9 @@ class ReachNestedAttributeTest {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Both ways binding on a complex path yields the model instance.
|
||||
*/
|
||||
@Test
|
||||
void bothWaysBindingOnAComplexPathYieldsTheModelInstance() throws Exception {
|
||||
onFx(() -> {
|
||||
@@ -117,6 +135,9 @@ class ReachNestedAttributeTest {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Filling a missing address stores the view model and writes the model.
|
||||
*/
|
||||
@Test
|
||||
void fillingAMissingAddressStoresTheViewModelAndWritesTheModel() throws Exception {
|
||||
onFx(() -> {
|
||||
@@ -137,6 +158,9 @@ class ReachNestedAttributeTest {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Missing address leaves the control and the model unchanged.
|
||||
*/
|
||||
@Test
|
||||
void missingAddressLeavesTheControlAndTheModelUnchanged() throws Exception {
|
||||
onFx(() -> {
|
||||
@@ -151,6 +175,9 @@ class ReachNestedAttributeTest {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Null complex attribute holds null.
|
||||
*/
|
||||
@Test
|
||||
void nullComplexAttributeHoldsNull() throws Exception {
|
||||
onFx(() -> {
|
||||
@@ -163,6 +190,9 @@ class ReachNestedAttributeTest {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Attributes that reach the same address share its view model.
|
||||
*/
|
||||
@Test
|
||||
void attributesThatReachTheSameAddressShareItsViewModel() throws Exception {
|
||||
onFx(() -> {
|
||||
@@ -183,6 +213,9 @@ class ReachNestedAttributeTest {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Equal addresses keep separate view models.
|
||||
*/
|
||||
@Test
|
||||
void equalAddressesKeepSeparateViewModels() throws Exception {
|
||||
onFx(() -> {
|
||||
@@ -206,6 +239,9 @@ class ReachNestedAttributeTest {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* A person who is their own friend reuses the same view model.
|
||||
*/
|
||||
@Test
|
||||
void aPersonWhoIsTheirOwnFriendReusesTheSameViewModel() throws Exception {
|
||||
onFx(() -> {
|
||||
@@ -219,6 +255,9 @@ class ReachNestedAttributeTest {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Two people who point at each other reuse the view models already built.
|
||||
*/
|
||||
@Test
|
||||
void twoPeopleWhoPointAtEachOtherReuseTheViewModelsAlreadyBuilt() throws Exception {
|
||||
onFx(() -> {
|
||||
@@ -236,6 +275,9 @@ class ReachNestedAttributeTest {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Enums booleans and date values stay simple values.
|
||||
*/
|
||||
@Test
|
||||
void enumsBooleansAndDateValuesStaySimpleValues() throws Exception {
|
||||
onFx(() -> {
|
||||
@@ -260,6 +302,9 @@ class ReachNestedAttributeTest {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* A collection stays the collection rather than a view model.
|
||||
*/
|
||||
@Test
|
||||
void aCollectionStaysTheCollectionRatherThanAViewModel() throws Exception {
|
||||
onFx(() -> {
|
||||
@@ -281,6 +326,9 @@ class ReachNestedAttributeTest {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Two loads of the same person share the view model.
|
||||
*/
|
||||
@Test
|
||||
void twoLoadsOfTheSamePersonShareTheViewModel() throws Exception {
|
||||
onFx(() -> {
|
||||
@@ -299,6 +347,11 @@ class ReachNestedAttributeTest {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs one check on the JavaFX application thread.
|
||||
*
|
||||
* @param action the check
|
||||
*/
|
||||
private static void onFx(FxAction action) throws Exception {
|
||||
if (Platform.isFxApplicationThread()) {
|
||||
action.run();
|
||||
@@ -330,8 +383,16 @@ class ReachNestedAttributeTest {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* One check that runs on the JavaFX application thread.
|
||||
*/
|
||||
@FunctionalInterface
|
||||
private interface FxAction {
|
||||
/**
|
||||
* Runs the check.
|
||||
*
|
||||
* @throws Exception when the check fails
|
||||
*/
|
||||
void run() throws Exception;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,8 +28,14 @@ import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
|
||||
/**
|
||||
* A repeat places one component in a pane for each model instance.
|
||||
*/
|
||||
class RepeatAddressComponentsTest {
|
||||
|
||||
/**
|
||||
* Starts the JavaFX toolkit once for this class.
|
||||
*/
|
||||
@BeforeAll
|
||||
static void startJavaFx() throws InterruptedException {
|
||||
CountDownLatch started = new CountDownLatch(1);
|
||||
@@ -43,6 +49,9 @@ class RepeatAddressComponentsTest {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Addresses are the address instances on the shared list.
|
||||
*/
|
||||
@Test
|
||||
void addressesAreTheAddressInstancesOnTheSharedList() throws Exception {
|
||||
onFx(() -> {
|
||||
@@ -67,6 +76,9 @@ class RepeatAddressComponentsTest {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Each address appears as its own form in list order.
|
||||
*/
|
||||
@Test
|
||||
void eachAddressAppearsAsItsOwnFormInListOrder() throws Exception {
|
||||
onFx(() -> {
|
||||
@@ -96,6 +108,9 @@ class RepeatAddressComponentsTest {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Adding and removing an address updates the pane and the entity.
|
||||
*/
|
||||
@Test
|
||||
void addingAndRemovingAnAddressUpdatesThePaneAndTheEntity() throws Exception {
|
||||
onFx(() -> {
|
||||
@@ -131,6 +146,9 @@ class RepeatAddressComponentsTest {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Selection is a separate attribute and the forms do not store it.
|
||||
*/
|
||||
@Test
|
||||
void selectionIsASeparateAttributeAndTheFormsDoNotStoreIt() throws Exception {
|
||||
onFx(() -> {
|
||||
@@ -161,6 +179,9 @@ class RepeatAddressComponentsTest {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* A second address form in the same document is another component.
|
||||
*/
|
||||
@Test
|
||||
void aSecondAddressFormInTheSameDocumentIsAnotherComponent() throws Exception {
|
||||
onFx(() -> {
|
||||
@@ -189,10 +210,21 @@ class RepeatAddressComponentsTest {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the street of each address, in list order.
|
||||
*
|
||||
* @param addresses the addresses
|
||||
* @return the streets, in list order
|
||||
*/
|
||||
private static List<String> streets(List<Address> addresses) {
|
||||
return addresses.stream().map(Address::getStreet).toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs one check on the JavaFX application thread.
|
||||
*
|
||||
* @param action the check
|
||||
*/
|
||||
private static void onFx(FxAction action) throws Exception {
|
||||
if (Platform.isFxApplicationThread()) {
|
||||
action.run();
|
||||
@@ -224,8 +256,16 @@ class RepeatAddressComponentsTest {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* One check that runs on the JavaFX application thread.
|
||||
*/
|
||||
@FunctionalInterface
|
||||
private interface FxAction {
|
||||
/**
|
||||
* Runs the check.
|
||||
*
|
||||
* @throws Exception when the check fails
|
||||
*/
|
||||
void run() throws Exception;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,8 +20,14 @@ import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
|
||||
/**
|
||||
* A list of simple values is one shared list on the control and the model.
|
||||
*/
|
||||
class ShowAListOfSimpleValuesTest {
|
||||
|
||||
/**
|
||||
* Starts the JavaFX toolkit once for this class.
|
||||
*/
|
||||
@BeforeAll
|
||||
static void startJavaFx() throws InterruptedException {
|
||||
CountDownLatch started = new CountDownLatch(1);
|
||||
@@ -35,6 +41,9 @@ class ShowAListOfSimpleValuesTest {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* List view and person share the tag list.
|
||||
*/
|
||||
@Test
|
||||
void listViewAndPersonShareTheTagList() throws Exception {
|
||||
onFx(() -> {
|
||||
@@ -61,6 +70,11 @@ class ShowAListOfSimpleValuesTest {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs one check on the JavaFX application thread.
|
||||
*
|
||||
* @param action the check
|
||||
*/
|
||||
private static void onFx(FxAction action) throws Exception {
|
||||
if (Platform.isFxApplicationThread()) {
|
||||
action.run();
|
||||
@@ -92,8 +106,16 @@ class ShowAListOfSimpleValuesTest {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* One check that runs on the JavaFX application thread.
|
||||
*/
|
||||
@FunctionalInterface
|
||||
private interface FxAction {
|
||||
/**
|
||||
* Runs the check.
|
||||
*
|
||||
* @throws Exception when the check fails
|
||||
*/
|
||||
void run() throws Exception;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,8 +21,14 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
|
||||
/**
|
||||
* A direct binding shows one attribute and writes it when the direction says so.
|
||||
*/
|
||||
class ShowAndEditOneAttributeTest {
|
||||
|
||||
/**
|
||||
* Starts the JavaFX toolkit once for this class.
|
||||
*/
|
||||
@BeforeAll
|
||||
static void startJavaFx() throws InterruptedException {
|
||||
CountDownLatch started = new CountDownLatch(1);
|
||||
@@ -36,6 +42,9 @@ class ShowAndEditOneAttributeTest {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Both ways text field shows the name and typing updates the model.
|
||||
*/
|
||||
@Test
|
||||
void bothWaysTextFieldShowsTheNameAndTypingUpdatesTheModel() throws Exception {
|
||||
onFx(() -> {
|
||||
@@ -51,6 +60,9 @@ class ShowAndEditOneAttributeTest {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Toward view label shows the name follows the field and does not write the model.
|
||||
*/
|
||||
@Test
|
||||
void towardViewLabelShowsTheNameFollowsTheFieldAndDoesNotWriteTheModel() throws Exception {
|
||||
onFx(() -> {
|
||||
@@ -70,6 +82,9 @@ class ShowAndEditOneAttributeTest {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Entity setter after load leaves the text control unchanged.
|
||||
*/
|
||||
@Test
|
||||
void entitySetterAfterLoadLeavesTheTextControlUnchanged() throws Exception {
|
||||
onFx(() -> {
|
||||
@@ -84,6 +99,9 @@ class ShowAndEditOneAttributeTest {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Binding a read only getter fails the load.
|
||||
*/
|
||||
@Test
|
||||
void bindingAReadOnlyGetterFailsTheLoad() throws Exception {
|
||||
onFx(() -> {
|
||||
@@ -97,6 +115,9 @@ class ShowAndEditOneAttributeTest {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Binding a method declared on object fails the load.
|
||||
*/
|
||||
@Test
|
||||
void bindingAMethodDeclaredOnObjectFailsTheLoad() throws Exception {
|
||||
onFx(() -> {
|
||||
@@ -110,6 +131,9 @@ class ShowAndEditOneAttributeTest {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Null integer attribute uses an integer property.
|
||||
*/
|
||||
@Test
|
||||
void nullIntegerAttributeUsesAnIntegerProperty() throws Exception {
|
||||
onFx(() -> {
|
||||
@@ -122,6 +146,11 @@ class ShowAndEditOneAttributeTest {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs one check on the JavaFX application thread.
|
||||
*
|
||||
* @param action the check
|
||||
*/
|
||||
private static void onFx(FxAction action) throws Exception {
|
||||
if (Platform.isFxApplicationThread()) {
|
||||
action.run();
|
||||
@@ -153,8 +182,16 @@ class ShowAndEditOneAttributeTest {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* One check that runs on the JavaFX application thread.
|
||||
*/
|
||||
@FunctionalInterface
|
||||
private interface FxAction {
|
||||
/**
|
||||
* Runs the check.
|
||||
*
|
||||
* @throws Exception when the check fails
|
||||
*/
|
||||
void run() throws Exception;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,8 +29,14 @@ import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
|
||||
/**
|
||||
* A message stays on the view model until the developer asks for a dialog.
|
||||
*/
|
||||
class ShowMessagesWhenAskedTest {
|
||||
|
||||
/**
|
||||
* Starts the JavaFX toolkit once for this class.
|
||||
*/
|
||||
@BeforeAll
|
||||
static void startJavaFx() throws InterruptedException {
|
||||
Platform.setImplicitExit(false);
|
||||
@@ -45,6 +51,9 @@ class ShowMessagesWhenAskedTest {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adding a message does not open a popup or fill a status bar.
|
||||
*/
|
||||
@Test
|
||||
void addingAMessageDoesNotOpenAPopupOrFillAStatusBar() throws Exception {
|
||||
onFx(() -> {
|
||||
@@ -64,6 +73,9 @@ class ShowMessagesWhenAskedTest {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Direct binding shows the latest message text.
|
||||
*/
|
||||
@Test
|
||||
void directBindingShowsTheLatestMessageText() throws Exception {
|
||||
onFx(() -> {
|
||||
@@ -82,6 +94,9 @@ class ShowMessagesWhenAskedTest {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Opted in helper shows a new message in a popup.
|
||||
*/
|
||||
@Test
|
||||
void optedInHelperShowsANewMessageInAPopup() throws Exception {
|
||||
onFx(() -> {
|
||||
@@ -104,6 +119,9 @@ class ShowMessagesWhenAskedTest {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Updating a binding failure does not open another popup.
|
||||
*/
|
||||
@Test
|
||||
void updatingABindingFailureDoesNotOpenAnotherPopup() throws Exception {
|
||||
onFx(() -> {
|
||||
@@ -140,10 +158,22 @@ class ShowMessagesWhenAskedTest {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the windows open before a check.
|
||||
*
|
||||
* @return the windows that are open
|
||||
*/
|
||||
private static Set<Window> snapshotWindows() {
|
||||
return new HashSet<>(Window.getWindows());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the window opened during a check whose content is the given text.
|
||||
*
|
||||
* @param text the text
|
||||
* @param alreadyOpen the windows open before the check
|
||||
* @return the window, or {@code null} when none shows the text
|
||||
*/
|
||||
private static Window windowShowing(String text, Set<Window> alreadyOpen) {
|
||||
for (Window window : Window.getWindows()) {
|
||||
if (!alreadyOpen.contains(window) && windowShows(window, text)) {
|
||||
@@ -153,6 +183,13 @@ class ShowMessagesWhenAskedTest {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether a window's scene shows the given text.
|
||||
*
|
||||
* @param window the window
|
||||
* @param text the text
|
||||
* @return {@code true} when the window shows the text
|
||||
*/
|
||||
private static boolean windowShows(Window window, String text) {
|
||||
if (window.getScene() == null || !(window.getScene().getRoot() instanceof Parent root)) {
|
||||
return false;
|
||||
@@ -168,10 +205,23 @@ class ShowMessagesWhenAskedTest {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether a labeled node shows the given text.
|
||||
*
|
||||
* @param node the node
|
||||
* @param text the text
|
||||
* @return {@code true} when the node shows the text
|
||||
*/
|
||||
private static boolean nodeShows(Node node, String text) {
|
||||
return node instanceof Labeled labeled && text.equals(labeled.getText());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the windows that were not open in the earlier snapshot.
|
||||
*
|
||||
* @param alreadyOpen the windows open before the check
|
||||
* @return the windows opened since the snapshot
|
||||
*/
|
||||
private static List<Window> windowsOpenedSince(Set<Window> alreadyOpen) {
|
||||
List<Window> opened = new ArrayList<>();
|
||||
for (Window window : Window.getWindows()) {
|
||||
@@ -182,6 +232,11 @@ class ShowMessagesWhenAskedTest {
|
||||
return opened;
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the windows opened during a check.
|
||||
*
|
||||
* @param alreadyOpen the windows open before the check
|
||||
*/
|
||||
private static void closeWindowsOpenedSince(Set<Window> alreadyOpen) {
|
||||
List<Window> open = new ArrayList<>(Window.getWindows());
|
||||
for (Window window : open) {
|
||||
@@ -191,6 +246,12 @@ class ShowMessagesWhenAskedTest {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fails when the given text is visible in the scene or in a dialog.
|
||||
*
|
||||
* @param root the loaded scene
|
||||
* @param text the text
|
||||
*/
|
||||
private static void assertMessageIsNotShown(Parent root, String text) {
|
||||
for (Node node : root.lookupAll("*")) {
|
||||
if (node instanceof Labeled labeled) {
|
||||
@@ -202,6 +263,11 @@ class ShowMessagesWhenAskedTest {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs one check on the JavaFX application thread.
|
||||
*
|
||||
* @param action the check
|
||||
*/
|
||||
private static void onFx(FxAction action) throws Exception {
|
||||
if (Platform.isFxApplicationThread()) {
|
||||
action.run();
|
||||
@@ -233,8 +299,16 @@ class ShowMessagesWhenAskedTest {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* One check that runs on the JavaFX application thread.
|
||||
*/
|
||||
@FunctionalInterface
|
||||
private interface FxAction {
|
||||
/**
|
||||
* Runs the check.
|
||||
*
|
||||
* @throws Exception when the check fails
|
||||
*/
|
||||
void run() throws Exception;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,11 +5,14 @@ import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
|
||||
/**
|
||||
* Replacement for the library ViewModel. It exposes a badge property by name and does not
|
||||
* Replacement for the library viewmodel. it exposes a badge property by name and does not
|
||||
* extend {@link de.neitzel.fx.view.ViewModel}.
|
||||
*/
|
||||
public class BadgeViewModel {
|
||||
|
||||
/**
|
||||
* The badge text this view model adds beside the model attributes.
|
||||
*/
|
||||
private final StringProperty badge = new SimpleStringProperty();
|
||||
|
||||
/**
|
||||
@@ -23,7 +26,7 @@ public class BadgeViewModel {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the badge property a Binding element addresses by name.
|
||||
* Returns the badge property a binding element addresses by name.
|
||||
*
|
||||
* @return the badge property
|
||||
*/
|
||||
|
||||
@@ -13,10 +13,13 @@ import javafx.scene.layout.VBox;
|
||||
@ViewModelClass(StreetTitleViewModel.class)
|
||||
public class CaptionAddressForm extends VBox {
|
||||
|
||||
/**
|
||||
* The model this component edits. A parent view or a repeat sets it once.
|
||||
*/
|
||||
private final ObjectProperty<Object> model = new SimpleObjectProperty<>(this, "model");
|
||||
|
||||
/**
|
||||
* Returns the model property a parent View sets with a Direct binding.
|
||||
* Returns the model property a parent view sets with a direct binding.
|
||||
*
|
||||
* @return the model property
|
||||
*/
|
||||
|
||||
@@ -9,6 +9,9 @@ import jakarta.inject.Singleton;
|
||||
@Singleton
|
||||
public class DeskController {
|
||||
|
||||
/**
|
||||
* The service the greeting method reads.
|
||||
*/
|
||||
private final DeskGreeting greeting;
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,7 +3,7 @@ package de.neitzel.fx.view.chosen;
|
||||
import jakarta.inject.Singleton;
|
||||
|
||||
/**
|
||||
* Service whose text a Controller must show, so a test can see that injection ran.
|
||||
* Service whose text a controller must show, so a test can see that injection ran.
|
||||
*/
|
||||
@Singleton
|
||||
public class DeskGreeting {
|
||||
|
||||
@@ -10,12 +10,15 @@ import jakarta.inject.Singleton;
|
||||
@Singleton
|
||||
public class HandedNameController implements HasViewModel {
|
||||
|
||||
/**
|
||||
* The name handed to this controller before the bindings run.
|
||||
*/
|
||||
private String handed;
|
||||
|
||||
/**
|
||||
* Records a label value that is available only from the handed ViewModel.
|
||||
*
|
||||
* @param viewModel the ViewModel for this View
|
||||
* @param viewModel the view model
|
||||
*/
|
||||
@Override
|
||||
public void setViewModel(ViewModel viewModel) {
|
||||
|
||||
@@ -10,6 +10,9 @@ import javafx.beans.property.StringProperty;
|
||||
*/
|
||||
public class StreetTitleViewModel extends ViewModel {
|
||||
|
||||
/**
|
||||
* The caption this view model adds for the street.
|
||||
*/
|
||||
private final StringProperty caption = new SimpleStringProperty();
|
||||
|
||||
/**
|
||||
@@ -24,7 +27,7 @@ public class StreetTitleViewModel extends ViewModel {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the caption property a Binding element addresses by name.
|
||||
* Returns the caption property a binding element addresses by name.
|
||||
*
|
||||
* @return the caption property
|
||||
*/
|
||||
|
||||
@@ -4,7 +4,7 @@ import de.neitzel.fx.view.ViewModelClass;
|
||||
import jakarta.inject.Singleton;
|
||||
|
||||
/**
|
||||
* Controller that names {@link TitledViewModel} for its View.
|
||||
* Controller that names {@link titledviewmodel} for its view.
|
||||
*/
|
||||
@Singleton
|
||||
@ViewModelClass(TitledViewModel.class)
|
||||
|
||||
@@ -10,6 +10,9 @@ import javafx.beans.property.StringProperty;
|
||||
*/
|
||||
public class TitledViewModel extends ViewModel {
|
||||
|
||||
/**
|
||||
* The title this view model adds beside the model attributes.
|
||||
*/
|
||||
private final StringProperty title = new SimpleStringProperty();
|
||||
|
||||
/**
|
||||
@@ -24,7 +27,7 @@ public class TitledViewModel extends ViewModel {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the title property a Binding element addresses by name.
|
||||
* Returns the title property a binding element addresses by name.
|
||||
*
|
||||
* @return the title property
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
package de.neitzel.fx.view.component;
|
||||
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.inject.Singleton;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.scene.layout.HBox;
|
||||
|
||||
/**
|
||||
* A Row with street and city fields and a control after them.
|
||||
*/
|
||||
@Singleton
|
||||
public class ActionRow extends HBox {
|
||||
|
||||
/**
|
||||
* The visit this row shows.
|
||||
*/
|
||||
private final ObjectProperty<Object> model = new SimpleObjectProperty<>(this, "model");
|
||||
|
||||
/**
|
||||
* Creates a row.
|
||||
*/
|
||||
@Inject
|
||||
public ActionRow() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the model property.
|
||||
*
|
||||
* @return the model property
|
||||
*/
|
||||
public ObjectProperty<Object> modelProperty() {
|
||||
return model;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the visit this row shows.
|
||||
*
|
||||
* @return the visit, or {@code null}
|
||||
*/
|
||||
public Object getModel() {
|
||||
return model.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the visit this row shows.
|
||||
*
|
||||
* @param model the visit
|
||||
*/
|
||||
public void setModel(Object model) {
|
||||
this.model.set(model);
|
||||
}
|
||||
}
|
||||
@@ -14,21 +14,39 @@ 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.
|
||||
* The container injects the caption service. the street label shows that service's prefix.
|
||||
*/
|
||||
@Singleton
|
||||
public class AddressForm extends VBox implements HasViewModel {
|
||||
|
||||
/**
|
||||
* The service that supplies the street caption.
|
||||
*/
|
||||
private final StreetCaption captions;
|
||||
/**
|
||||
* The model this component edits. A parent view or a repeat sets it once.
|
||||
*/
|
||||
private final ObjectProperty<Object> model = new SimpleObjectProperty<>(this, "model");
|
||||
|
||||
/**
|
||||
* The label filled by the component's own binding.
|
||||
*/
|
||||
@FXML
|
||||
private Label streetLabel;
|
||||
|
||||
/**
|
||||
* The street field filled by the component's own binding.
|
||||
*/
|
||||
@FXML
|
||||
private TextField streetField;
|
||||
|
||||
/**
|
||||
* The view model handed over before the component's bindings run.
|
||||
*/
|
||||
private ViewModel receivedViewModel;
|
||||
/**
|
||||
* The street text visible when the view model is handed over.
|
||||
*/
|
||||
private String streetTextAtHandOff;
|
||||
|
||||
/**
|
||||
@@ -50,7 +68,7 @@ public class AddressForm extends VBox implements HasViewModel {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the model property a parent View sets with a Direct binding.
|
||||
* Returns the model property a parent view sets with a direct binding.
|
||||
*
|
||||
* @return the model property
|
||||
*/
|
||||
@@ -79,7 +97,7 @@ public class AddressForm extends VBox implements HasViewModel {
|
||||
/**
|
||||
* Records the ViewModel handed to this component, and the street text at that moment.
|
||||
*
|
||||
* @param viewModel the ViewModel selected for the address
|
||||
* @param viewModel the view model
|
||||
*/
|
||||
@Override
|
||||
public void setViewModel(ViewModel viewModel) {
|
||||
|
||||
@@ -7,6 +7,9 @@ import de.neitzel.fx.view.fixture.Address;
|
||||
*/
|
||||
public class Envelope {
|
||||
|
||||
/**
|
||||
* The address this model holds.
|
||||
*/
|
||||
private Address address;
|
||||
|
||||
/**
|
||||
|
||||
@@ -11,10 +11,13 @@ import javafx.scene.layout.VBox;
|
||||
@Singleton
|
||||
public class EnvelopeForm extends VBox {
|
||||
|
||||
/**
|
||||
* The model this component edits. A parent view or a repeat sets it once.
|
||||
*/
|
||||
private final ObjectProperty<Object> model = new SimpleObjectProperty<>(this, "model");
|
||||
|
||||
/**
|
||||
* Returns the model property a parent View sets with a Direct binding.
|
||||
* Returns the model property a parent view sets with a direct binding.
|
||||
*
|
||||
* @return the model property
|
||||
*/
|
||||
|
||||
@@ -5,6 +5,9 @@ package de.neitzel.fx.view.component;
|
||||
*/
|
||||
public class Letter {
|
||||
|
||||
/**
|
||||
* The envelope this letter holds.
|
||||
*/
|
||||
private Envelope envelope;
|
||||
|
||||
/**
|
||||
|
||||
@@ -5,11 +5,14 @@ import javafx.fxml.FXML;
|
||||
import javafx.scene.control.TextField;
|
||||
|
||||
/**
|
||||
* Controller that derives a label from the name field and does not write the Model.
|
||||
* Controller that derives a label from the name field and does not write the model.
|
||||
*/
|
||||
@Singleton
|
||||
public class WorkingAgeController {
|
||||
|
||||
/**
|
||||
* The name field a computed binding reads.
|
||||
*/
|
||||
@FXML
|
||||
private TextField nameField;
|
||||
|
||||
|
||||
@@ -10,6 +10,9 @@ import java.util.Objects;
|
||||
*/
|
||||
public class Address {
|
||||
|
||||
/**
|
||||
* The street.
|
||||
*/
|
||||
private String street;
|
||||
|
||||
/**
|
||||
@@ -39,6 +42,12 @@ public class Address {
|
||||
this.street = street;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares addresses by street.
|
||||
*
|
||||
* @param other the other address
|
||||
* @return {@code true} when the streets are equal
|
||||
*/
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
if (other instanceof Address address) {
|
||||
@@ -47,6 +56,11 @@ public class Address {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a hash code for the street.
|
||||
*
|
||||
* @return the hash code
|
||||
*/
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hashCode(street);
|
||||
|
||||
@@ -5,6 +5,9 @@ package de.neitzel.fx.view.fixture;
|
||||
*/
|
||||
public class Household {
|
||||
|
||||
/**
|
||||
* The person in this household.
|
||||
*/
|
||||
private Person person;
|
||||
|
||||
/**
|
||||
|
||||
@@ -9,6 +9,9 @@ import javafx.scene.layout.Pane;
|
||||
*/
|
||||
public class ObjectHolder extends Pane {
|
||||
|
||||
/**
|
||||
* The value a direct binding writes.
|
||||
*/
|
||||
private final ObjectProperty<Object> value = new SimpleObjectProperty<>(this, "value");
|
||||
|
||||
/**
|
||||
|
||||
@@ -10,19 +10,61 @@ import java.util.List;
|
||||
*/
|
||||
public class Person {
|
||||
|
||||
/**
|
||||
* The name.
|
||||
*/
|
||||
private String name;
|
||||
/**
|
||||
* The age in years, or {@code null} when it has not been set.
|
||||
*/
|
||||
private Integer age;
|
||||
/**
|
||||
* Whether the person is active.
|
||||
*/
|
||||
private boolean active;
|
||||
/**
|
||||
* The address this model holds.
|
||||
*/
|
||||
private Address address;
|
||||
/**
|
||||
* The home address.
|
||||
*/
|
||||
private Address home;
|
||||
/**
|
||||
* The work address.
|
||||
*/
|
||||
private Address work;
|
||||
/**
|
||||
* Another person, used to follow a cycle.
|
||||
*/
|
||||
private Person friend;
|
||||
/**
|
||||
* The person's role.
|
||||
*/
|
||||
private Role role;
|
||||
/**
|
||||
* The birth date.
|
||||
*/
|
||||
private LocalDate born;
|
||||
/**
|
||||
* The last update.
|
||||
*/
|
||||
private Date updated;
|
||||
/**
|
||||
* The hire date.
|
||||
*/
|
||||
private Calendar hired;
|
||||
/**
|
||||
* The tags. after a view loads, this is the list the control watches.
|
||||
*/
|
||||
private List<String> tags;
|
||||
/**
|
||||
* The addresses. after a view loads, this is the list a repeat watches.
|
||||
*/
|
||||
private List<Address> addresses;
|
||||
/**
|
||||
* The selected address, or {@code null} when none is selected.
|
||||
*/
|
||||
private Address selectedAddress;
|
||||
|
||||
/**
|
||||
@@ -62,7 +104,7 @@ public class Person {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the age. The value may be null.
|
||||
* Returns the age. the value may be null.
|
||||
*
|
||||
* @return the age
|
||||
*/
|
||||
@@ -242,7 +284,7 @@ public class Person {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the tags. The value may be null.
|
||||
* Returns the tags. the value may be null.
|
||||
*
|
||||
* @return the tags
|
||||
*/
|
||||
@@ -260,7 +302,7 @@ public class Person {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the addresses. The value may be null.
|
||||
* Returns the addresses. the value may be null.
|
||||
*
|
||||
* @return the addresses
|
||||
*/
|
||||
@@ -278,7 +320,7 @@ public class Person {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the selected address. Null means nothing is selected.
|
||||
* Returns the selected address. null means nothing is selected.
|
||||
*
|
||||
* @return the selected address, or {@code null}
|
||||
*/
|
||||
|
||||
@@ -10,6 +10,9 @@ import jakarta.inject.Singleton;
|
||||
@Singleton
|
||||
public class PrefixedAgeConverter implements Converter {
|
||||
|
||||
/**
|
||||
* The unit written in front of an age.
|
||||
*/
|
||||
private final AgeUnit ageUnit;
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,6 +4,13 @@ package de.neitzel.fx.view.fixture;
|
||||
* A simple enum Attribute used by view-loading tests.
|
||||
*/
|
||||
public enum Role {
|
||||
/**
|
||||
* An ordinary member.
|
||||
*/
|
||||
MEMBER,
|
||||
|
||||
/**
|
||||
* An administrator.
|
||||
*/
|
||||
ADMIN
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import javafx.scene.control.Button?>
|
||||
<?import javafx.scene.control.TextField?>
|
||||
<?import javafx.scene.layout.HBox?>
|
||||
<?import de.neitzel.fx.view.BindingList?>
|
||||
<?import de.neitzel.fx.view.DirectBinding?>
|
||||
|
||||
<fx:root type="javafx.scene.layout.HBox" xmlns="http://javafx.com/javafx"
|
||||
xmlns:fx="http://javafx.com/fxml" spacing="6">
|
||||
<fx:define>
|
||||
<BindingList>
|
||||
<DirectBinding control="streetField" property="text" path="street" direction="both"/>
|
||||
<DirectBinding control="cityField" property="text" path="city" direction="both"/>
|
||||
</BindingList>
|
||||
</fx:define>
|
||||
<TextField fx:id="streetField" id="streetField"/>
|
||||
<TextField fx:id="cityField" id="cityField"/>
|
||||
<Button fx:id="removeButton" id="removeButton" text="Remove"/>
|
||||
</fx:root>
|
||||
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import javafx.scene.layout.VBox?>
|
||||
<?import de.neitzel.fx.view.Column?>
|
||||
<?import de.neitzel.fx.view.Table?>
|
||||
|
||||
<VBox xmlns="http://javafx.com/javafx" xmlns:fx="http://javafx.com/fxml">
|
||||
<Table fx:id="visitsTable" id="visitsTable" collection="visits"
|
||||
row="de.neitzel.fx.view.component.ActionRow" prefWidth="300">
|
||||
<Column header="Street" attribute="street"/>
|
||||
<Column header="City" attribute="city"/>
|
||||
</Table>
|
||||
</VBox>
|
||||
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import javafx.scene.layout.VBox?>
|
||||
<?import de.neitzel.fx.view.Column?>
|
||||
<?import de.neitzel.fx.view.Table?>
|
||||
|
||||
<VBox xmlns="http://javafx.com/javafx" xmlns:fx="http://javafx.com/fxml">
|
||||
<Table fx:id="visitsTable" id="visitsTable" collection="visits"
|
||||
row="de.neitzel.fx.view.component.VisitRow" prefWidth="300">
|
||||
<Column header="Street" attribute="street" width="100"/>
|
||||
<Column header="City" attribute="city" width="0"/>
|
||||
</Table>
|
||||
</VBox>
|
||||
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import javafx.scene.layout.VBox?>
|
||||
<?import de.neitzel.fx.view.Column?>
|
||||
<?import de.neitzel.fx.view.Table?>
|
||||
|
||||
<VBox xmlns="http://javafx.com/javafx" xmlns:fx="http://javafx.com/fxml">
|
||||
<Table fx:id="visitsTable" id="visitsTable" collection="visits"
|
||||
row="de.neitzel.fx.view.component.VisitRow" prefWidth="300">
|
||||
<Column header="Street" attribute="street" width="280"/>
|
||||
<Column header="City" attribute="city" width="1"/>
|
||||
</Table>
|
||||
</VBox>
|
||||
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import javafx.scene.layout.VBox?>
|
||||
<?import de.neitzel.fx.view.Column?>
|
||||
<?import de.neitzel.fx.view.Table?>
|
||||
|
||||
<VBox xmlns="http://javafx.com/javafx" xmlns:fx="http://javafx.com/fxml">
|
||||
<Table fx:id="visitsTable" id="visitsTable" collection="visits"
|
||||
row="de.neitzel.fx.view.component.VisitRow" prefWidth="300">
|
||||
<Column header="Street" attribute="street" width="100"/>
|
||||
<Column header="City" attribute="city"/>
|
||||
</Table>
|
||||
</VBox>
|
||||
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import javafx.scene.layout.VBox?>
|
||||
<?import de.neitzel.fx.view.Column?>
|
||||
<?import de.neitzel.fx.view.Table?>
|
||||
|
||||
<VBox xmlns="http://javafx.com/javafx" xmlns:fx="http://javafx.com/fxml">
|
||||
<Table fx:id="visitsTable" id="visitsTable" collection="visits"
|
||||
row="de.neitzel.fx.view.component.VisitRow" prefWidth="300">
|
||||
<Column header="Street" attribute="street" width="220"/>
|
||||
<Column header="City" attribute="city" width="80"/>
|
||||
</Table>
|
||||
</VBox>
|
||||
Reference in New Issue
Block a user