Add a person-desk example and a guide for the fx library.
The example replaces the call sites of the removed JavaFX units so the module can build again. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -43,11 +43,11 @@ Some encryption classes that use format-preserving-encryption.
|
|||||||
|
|
||||||
### fx
|
### fx
|
||||||
|
|
||||||
Some JavaFX additions.
|
JavaFX library that binds an FXML View to an ordinary Java Model. See [fx/README.md](fx/README.md).
|
||||||
|
|
||||||
### fx-example
|
### fx-example
|
||||||
|
|
||||||
Some example programs that use the fx component
|
Person desk that shows how to use `fx`. Run `mvn -pl fx-example javafx:run`.
|
||||||
|
|
||||||
### gson
|
### gson
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# fx-example
|
||||||
|
|
||||||
|
A person desk that uses the [`fx`](../fx/README.md) library: a Direct binding, a Converter, a Computed binding, a nested Component, a Repeat, a list of simple values, and Messages.
|
||||||
|
|
||||||
|
From the NeitzelLib root, with Java 21:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mvn -pl fx-example javafx:run
|
||||||
|
```
|
||||||
|
|
||||||
|
The launch class is `de.neitzel.fx.example.Main`. The Model is an ordinary `Person`. The View is `Desk.fxml`.
|
||||||
+22
-3
@@ -16,27 +16,46 @@
|
|||||||
<link.name>${project.artifactId}</link.name>
|
<link.name>${project.artifactId}</link.name>
|
||||||
<launcher>${project.artifactId}</launcher>
|
<launcher>${project.artifactId}</launcher>
|
||||||
<appName>${project.artifactId}</appName>
|
<appName>${project.artifactId}</appName>
|
||||||
<main.class>de.neitzel.fx.injectfx.example.Main</main.class>
|
<main.class>de.neitzel.fx.example.Main</main.class>
|
||||||
<jar.filename>${project.artifactId}-${project.version}</jar.filename>
|
<jar.filename>${project.artifactId}-${project.version}</jar.filename>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<!-- FX dependency -->
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>de.neitzel.lib</groupId>
|
<groupId>de.neitzel.lib</groupId>
|
||||||
<artifactId>fx</artifactId>
|
<artifactId>fx</artifactId>
|
||||||
<version>${project.version}</version>
|
<version>${project.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.slf4j</groupId>
|
||||||
|
<artifactId>slf4j-simple</artifactId>
|
||||||
|
<scope>runtime</scope>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
<finalName>${jar.filename}</finalName>
|
<finalName>${jar.filename}</finalName>
|
||||||
<plugins>
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
|
<configuration>
|
||||||
|
<!-- This module has no Lombok types. Skip the processor so a newer JDK can compile it. -->
|
||||||
|
<proc>none</proc>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.openjfx</groupId>
|
||||||
|
<artifactId>javafx-maven-plugin</artifactId>
|
||||||
|
<version>${javafx.maven.plugin}</version>
|
||||||
|
<configuration>
|
||||||
|
<mainClass>${main.class}</mainClass>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
<plugin>
|
<plugin>
|
||||||
<groupId>com.github.spotbugs</groupId>
|
<groupId>com.github.spotbugs</groupId>
|
||||||
<artifactId>spotbugs-maven-plugin</artifactId>
|
<artifactId>spotbugs-maven-plugin</artifactId>
|
||||||
</plugin>
|
</plugin>
|
||||||
|
|
||||||
<plugin>
|
<plugin>
|
||||||
<groupId>org.apache.maven.plugins</groupId>
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
<artifactId>maven-pmd-plugin</artifactId>
|
<artifactId>maven-pmd-plugin</artifactId>
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
package de.neitzel.fx.component.example;
|
|
||||||
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.Setter;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Simple address model used in the FX example component.
|
|
||||||
*/
|
|
||||||
@Getter
|
|
||||||
@Setter
|
|
||||||
public class Address {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Street name and number.
|
|
||||||
*/
|
|
||||||
private String street;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* City name.
|
|
||||||
*/
|
|
||||||
private String city;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Default constructor only
|
|
||||||
*/
|
|
||||||
public Address() {
|
|
||||||
// default constructor only
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
package de.neitzel.fx.component.example;
|
|
||||||
|
|
||||||
import de.neitzel.fx.component.ComponentLoader;
|
|
||||||
import javafx.application.Application;
|
|
||||||
import javafx.scene.Parent;
|
|
||||||
import javafx.scene.Scene;
|
|
||||||
import javafx.stage.Stage;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Example JavaFX application demonstrating the {@link de.neitzel.fx.component.ComponentLoader} usage.
|
|
||||||
*/
|
|
||||||
public class ExampleApp extends Application {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Default constructor only
|
|
||||||
*/
|
|
||||||
public ExampleApp() {
|
|
||||||
// default constructor only
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Launches the JavaFX application.
|
|
||||||
*
|
|
||||||
* @param args command line arguments
|
|
||||||
*/
|
|
||||||
public static void main(String[] args) {
|
|
||||||
launch(args);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes sample model data, loads the FX component and shows the primary stage.
|
|
||||||
*
|
|
||||||
* @param primaryStage the primary stage for this application
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void start(Stage primaryStage) throws Exception {
|
|
||||||
// Beispielmodel initialisieren
|
|
||||||
Address address = new Address();
|
|
||||||
address.setStreet("Sample Street 1");
|
|
||||||
address.setCity("Sample City");
|
|
||||||
|
|
||||||
Person person = new Person();
|
|
||||||
person.setName("Max Mustermann");
|
|
||||||
person.setAddress(address);
|
|
||||||
|
|
||||||
// ComponentLoader verwenden
|
|
||||||
ComponentLoader loader = new ComponentLoader();
|
|
||||||
Parent root = loader.load(person, "/person.fxml");
|
|
||||||
|
|
||||||
// Scene erstellen und anzeigen
|
|
||||||
Scene scene = new Scene(root);
|
|
||||||
primaryStage.setTitle("ComponentLoader Example");
|
|
||||||
primaryStage.setScene(scene);
|
|
||||||
primaryStage.show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
package de.neitzel.fx.component.example;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Another Main class as workaround when the JavaFX Application ist started without
|
|
||||||
* taking care os Classloader Requirements of JavaFX. (Important when starting from inside NetBeans!)
|
|
||||||
*/
|
|
||||||
public class Main {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Default constructor only
|
|
||||||
*/
|
|
||||||
public Main() {
|
|
||||||
// default constructor only
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Additional main methode to start Application.
|
|
||||||
*
|
|
||||||
* @param args Commandline Arguments.
|
|
||||||
*/
|
|
||||||
public static void main(String[] args) {
|
|
||||||
ExampleApp.main(args);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
package de.neitzel.fx.component.example;
|
|
||||||
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.Setter;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Simple person model used in the FX example component.
|
|
||||||
*/
|
|
||||||
@Getter
|
|
||||||
@Setter
|
|
||||||
public class Person {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The person's full name.
|
|
||||||
*/
|
|
||||||
private String name;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The address of the person.
|
|
||||||
*/
|
|
||||||
private Address address;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Default constructor only
|
|
||||||
*/
|
|
||||||
public Person() {
|
|
||||||
// default constructor only
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
package de.neitzel.fx.example;
|
||||||
|
|
||||||
|
import de.neitzel.fx.example.model.Address;
|
||||||
|
import de.neitzel.fx.example.model.Person;
|
||||||
|
import de.neitzel.fx.example.view.DeskController;
|
||||||
|
import de.neitzel.fx.view.LoadedView;
|
||||||
|
import de.neitzel.fx.view.MessagePopup;
|
||||||
|
import de.neitzel.fx.view.ViewLoader;
|
||||||
|
import de.neitzel.injection.ApplicationContext;
|
||||||
|
import javafx.application.Application;
|
||||||
|
import javafx.scene.Scene;
|
||||||
|
import javafx.stage.Stage;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens the person desk for one ordinary {@link Person}.
|
||||||
|
*/
|
||||||
|
public class DeskApp extends Application {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the JavaFX application.
|
||||||
|
*/
|
||||||
|
public DeskApp() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds the person the desk edits.
|
||||||
|
*
|
||||||
|
* @return a person with a home address, one visit, and two tags
|
||||||
|
*/
|
||||||
|
public static Person sample() {
|
||||||
|
Address home = new Address("Sample Street 1", "Sample City");
|
||||||
|
Address visit = new Address("Work Road 2", "Office City");
|
||||||
|
Person person = new Person();
|
||||||
|
person.setName("Ada Lovelace");
|
||||||
|
person.setAge(36);
|
||||||
|
person.setHome(home);
|
||||||
|
person.setVisits(new ArrayList<>(List.of(visit)));
|
||||||
|
person.setSelectedVisit(visit);
|
||||||
|
person.setTags(new ArrayList<>(List.of("dns", "math")));
|
||||||
|
return person;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Launches the desk.
|
||||||
|
*
|
||||||
|
* @param args unused
|
||||||
|
*/
|
||||||
|
public static void main(String[] args) {
|
||||||
|
launch(args);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads the desk for {@link #sample()} and shows it.
|
||||||
|
*
|
||||||
|
* @param stage the primary stage
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void start(Stage stage) {
|
||||||
|
ApplicationContext context = new ApplicationContext(DeskApp.class);
|
||||||
|
Person person = sample();
|
||||||
|
LoadedView loaded = ViewLoader.load(DeskController.class.getResource("Desk.fxml"), person, context);
|
||||||
|
MessagePopup.showNewMessages(loaded.getViewModel());
|
||||||
|
stage.setTitle("fx example");
|
||||||
|
stage.setScene(new Scene(loaded.getRoot()));
|
||||||
|
stage.show();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package de.neitzel.fx.example;
|
||||||
|
|
||||||
|
import javafx.application.Application;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Launches the example without extending {@link Application}.
|
||||||
|
* <p>
|
||||||
|
* JavaFX on the classpath is started from a class that is not itself an {@link Application}.
|
||||||
|
*/
|
||||||
|
public final class Main {
|
||||||
|
|
||||||
|
private Main() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts the person desk.
|
||||||
|
*
|
||||||
|
* @param args unused
|
||||||
|
*/
|
||||||
|
public static void main(String[] args) {
|
||||||
|
Application.launch(DeskApp.class, args);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
package de.neitzel.fx.example.model;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An ordinary address the desk edits.
|
||||||
|
*/
|
||||||
|
public class Address {
|
||||||
|
|
||||||
|
private String street = "";
|
||||||
|
private String city = "";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an empty address.
|
||||||
|
*/
|
||||||
|
public Address() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an address with the given street and city.
|
||||||
|
*
|
||||||
|
* @param street the street
|
||||||
|
* @param city the city
|
||||||
|
*/
|
||||||
|
public Address(String street, String city) {
|
||||||
|
this.street = street;
|
||||||
|
this.city = city;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the street.
|
||||||
|
*
|
||||||
|
* @return the street
|
||||||
|
*/
|
||||||
|
public String getStreet() {
|
||||||
|
return street;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the street.
|
||||||
|
*
|
||||||
|
* @param street the street
|
||||||
|
*/
|
||||||
|
public void setStreet(String street) {
|
||||||
|
this.street = street;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the city.
|
||||||
|
*
|
||||||
|
* @return the city
|
||||||
|
*/
|
||||||
|
public String getCity() {
|
||||||
|
return city;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the city.
|
||||||
|
*
|
||||||
|
* @param city the city
|
||||||
|
*/
|
||||||
|
public void setCity(String city) {
|
||||||
|
this.city = city;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the street and city, for a combo box row.
|
||||||
|
*
|
||||||
|
* @return the street and city
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return street + ", " + city;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
package de.neitzel.fx.example.model;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An ordinary person the desk edits.
|
||||||
|
* <p>
|
||||||
|
* Read-write attributes are what the View binds to. The class carries no JavaFX types.
|
||||||
|
*/
|
||||||
|
public class Person {
|
||||||
|
|
||||||
|
private String name = "";
|
||||||
|
private Integer age;
|
||||||
|
private Address home;
|
||||||
|
private List<Address> visits = new ArrayList<>();
|
||||||
|
private Address selectedVisit;
|
||||||
|
private List<String> tags = new ArrayList<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an empty person.
|
||||||
|
*/
|
||||||
|
public Person() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the name.
|
||||||
|
*
|
||||||
|
* @return the name
|
||||||
|
*/
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the name.
|
||||||
|
*
|
||||||
|
* @param name the name
|
||||||
|
*/
|
||||||
|
public void setName(String name) {
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the age in years.
|
||||||
|
*
|
||||||
|
* @return the age, or {@code null} when none is set
|
||||||
|
*/
|
||||||
|
public Integer getAge() {
|
||||||
|
return age;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the age in years.
|
||||||
|
*
|
||||||
|
* @param age the age
|
||||||
|
*/
|
||||||
|
public void setAge(Integer age) {
|
||||||
|
this.age = age;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the home address.
|
||||||
|
*
|
||||||
|
* @return the home address, or {@code null} when none is set
|
||||||
|
*/
|
||||||
|
public Address getHome() {
|
||||||
|
return home;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the home address.
|
||||||
|
*
|
||||||
|
* @param home the home address
|
||||||
|
*/
|
||||||
|
public void setHome(Address home) {
|
||||||
|
this.home = home;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the visits.
|
||||||
|
* <p>
|
||||||
|
* After the View has loaded, this is the list the View is watching.
|
||||||
|
*
|
||||||
|
* @return the visits
|
||||||
|
*/
|
||||||
|
public List<Address> getVisits() {
|
||||||
|
return visits;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the visits.
|
||||||
|
*
|
||||||
|
* @param visits the visits
|
||||||
|
*/
|
||||||
|
public void setVisits(List<Address> visits) {
|
||||||
|
this.visits = visits;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the visit chosen in the combo box.
|
||||||
|
*
|
||||||
|
* @return the selected visit, or {@code null} when none is selected
|
||||||
|
*/
|
||||||
|
public Address getSelectedVisit() {
|
||||||
|
return selectedVisit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the visit chosen in the combo box.
|
||||||
|
*
|
||||||
|
* @param selectedVisit the selected visit
|
||||||
|
*/
|
||||||
|
public void setSelectedVisit(Address selectedVisit) {
|
||||||
|
this.selectedVisit = selectedVisit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the tags.
|
||||||
|
* <p>
|
||||||
|
* After the View has loaded, this is the list the View is watching.
|
||||||
|
*
|
||||||
|
* @return the tags
|
||||||
|
*/
|
||||||
|
public List<String> getTags() {
|
||||||
|
return tags;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the tags.
|
||||||
|
*
|
||||||
|
* @param tags the tags
|
||||||
|
*/
|
||||||
|
public void setTags(List<String> tags) {
|
||||||
|
this.tags = tags;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package de.neitzel.fx.example.view;
|
||||||
|
|
||||||
|
import jakarta.inject.Singleton;
|
||||||
|
import javafx.beans.property.ObjectProperty;
|
||||||
|
import javafx.beans.property.SimpleObjectProperty;
|
||||||
|
import javafx.scene.layout.VBox;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component that edits one address.
|
||||||
|
* <p>
|
||||||
|
* The class is the root and the Controller of {@code AddressForm.fxml}. A parent View sets
|
||||||
|
* {@link #modelProperty()} from a Direct binding. A repeated row gets its own instance.
|
||||||
|
*/
|
||||||
|
@Singleton
|
||||||
|
public class AddressForm extends VBox {
|
||||||
|
|
||||||
|
private final ObjectProperty<Object> model = new SimpleObjectProperty<>(this, "model");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an empty form. The container calls this constructor.
|
||||||
|
*/
|
||||||
|
public AddressForm() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the model property a parent View sets.
|
||||||
|
*
|
||||||
|
* @return the model property
|
||||||
|
*/
|
||||||
|
public ObjectProperty<Object> modelProperty() {
|
||||||
|
return model;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the address this form edits.
|
||||||
|
*
|
||||||
|
* @return the address, or {@code null} when none is set
|
||||||
|
*/
|
||||||
|
public Object getModel() {
|
||||||
|
return model.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the address this form edits.
|
||||||
|
*
|
||||||
|
* @param model the address
|
||||||
|
*/
|
||||||
|
public void setModel(Object model) {
|
||||||
|
this.model.set(model);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
package de.neitzel.fx.example.view;
|
||||||
|
|
||||||
|
import de.neitzel.fx.view.Converter;
|
||||||
|
import jakarta.inject.Inject;
|
||||||
|
import jakarta.inject.Singleton;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Translates an age between the Model and the text field.
|
||||||
|
*/
|
||||||
|
@Singleton
|
||||||
|
public class AgeConverter implements Converter {
|
||||||
|
|
||||||
|
private final AgeSuffix suffix;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a converter that reads its suffix from the injected service.
|
||||||
|
*
|
||||||
|
* @param suffix the suffix written in front of the age
|
||||||
|
*/
|
||||||
|
@Inject
|
||||||
|
public AgeConverter(AgeSuffix suffix) {
|
||||||
|
this.suffix = suffix;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes the suffix in front of the age.
|
||||||
|
*
|
||||||
|
* @param attributeValue the age
|
||||||
|
* @return the text shown in the field
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public Object toControlValue(Object attributeValue) {
|
||||||
|
if (attributeValue == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return suffix.prefix() + attributeValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads an age that starts with the suffix.
|
||||||
|
*
|
||||||
|
* @param controlValue the field text
|
||||||
|
* @return the age
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public Object toAttributeValue(Object controlValue) {
|
||||||
|
String text = controlValue == null ? "" : controlValue.toString();
|
||||||
|
String prefix = suffix.prefix();
|
||||||
|
if (!text.startsWith(prefix)) {
|
||||||
|
throw new IllegalArgumentException("Age must start with \"" + prefix.trim() + "\": " + text);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return Integer.valueOf(text.substring(prefix.length()).trim());
|
||||||
|
} catch (NumberFormatException failure) {
|
||||||
|
throw new IllegalArgumentException("Age must be a whole number: " + text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package de.neitzel.fx.example.view;
|
||||||
|
|
||||||
|
import jakarta.inject.Singleton;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The suffix an {@link AgeConverter} writes in front of an age.
|
||||||
|
*/
|
||||||
|
@Singleton
|
||||||
|
public class AgeSuffix {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the suffix used by the desk.
|
||||||
|
*/
|
||||||
|
public AgeSuffix() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the text written in front of the age.
|
||||||
|
*
|
||||||
|
* @return the suffix, including its trailing space
|
||||||
|
*/
|
||||||
|
public String prefix() {
|
||||||
|
return "years ";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
package de.neitzel.fx.example.view;
|
||||||
|
|
||||||
|
import de.neitzel.fx.example.model.Address;
|
||||||
|
import de.neitzel.fx.view.HasViewModel;
|
||||||
|
import de.neitzel.fx.view.Message;
|
||||||
|
import de.neitzel.fx.view.ViewModel;
|
||||||
|
import jakarta.inject.Singleton;
|
||||||
|
import javafx.fxml.FXML;
|
||||||
|
import javafx.scene.control.TextField;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controller for the person desk.
|
||||||
|
* <p>
|
||||||
|
* The container creates this class and injects nothing else. {@link #setViewModel} receives the
|
||||||
|
* ViewModel before the binding elements run, so {@link #greeting()} can read the name field on
|
||||||
|
* its first call.
|
||||||
|
*/
|
||||||
|
@Singleton
|
||||||
|
public class DeskController implements HasViewModel {
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private TextField nameField;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private TextField tagField;
|
||||||
|
|
||||||
|
private ViewModel viewModel;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the controller. The container calls this constructor.
|
||||||
|
*/
|
||||||
|
public DeskController() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Receives the ViewModel for the person on the desk.
|
||||||
|
*
|
||||||
|
* @param viewModel the ViewModel
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void setViewModel(ViewModel viewModel) {
|
||||||
|
this.viewModel = viewModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the greeting written to the label.
|
||||||
|
* <p>
|
||||||
|
* The method reads the name field. It does not write the Model.
|
||||||
|
*
|
||||||
|
* @return the greeting
|
||||||
|
*/
|
||||||
|
public String greeting() {
|
||||||
|
String name = nameField.getText();
|
||||||
|
if (name == null || name.isBlank()) {
|
||||||
|
return "Hello";
|
||||||
|
}
|
||||||
|
return "Hello, " + name.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a visit and leaves the previous selection in place.
|
||||||
|
*/
|
||||||
|
@FXML
|
||||||
|
public void addVisit() {
|
||||||
|
visits().add(new Address("New street", "New city"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds the tag field's text to the tag list and clears the field.
|
||||||
|
*/
|
||||||
|
@FXML
|
||||||
|
public void addTag() {
|
||||||
|
String tag = tagField.getText();
|
||||||
|
if (tag == null || tag.isBlank()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
tags().add(tag.trim());
|
||||||
|
tagField.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds an info Message that quotes the name already stored on the Model.
|
||||||
|
*/
|
||||||
|
@FXML
|
||||||
|
public void leaveNote() {
|
||||||
|
Object name = viewModel.property("name").getValue();
|
||||||
|
viewModel.addMessage(new Message(
|
||||||
|
Message.Severity.INFO,
|
||||||
|
"The model already has the name " + name + ".",
|
||||||
|
"desk"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private List<Address> visits() {
|
||||||
|
return (List<Address>) viewModel.property("visits").getValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private List<String> tags() {
|
||||||
|
return (List<String>) viewModel.property("tags").getValue();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
package de.neitzel.fx.injectfx.example;
|
|
||||||
|
|
||||||
import javafx.application.Application;
|
|
||||||
import javafx.fxml.FXMLLoader;
|
|
||||||
import javafx.scene.Parent;
|
|
||||||
import javafx.scene.Scene;
|
|
||||||
import javafx.stage.Stage;
|
|
||||||
|
|
||||||
import java.util.logging.Level;
|
|
||||||
import java.util.logging.Logger;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Minimal JavaFX application launcher used by the examples.
|
|
||||||
*
|
|
||||||
* <p>This class initializes the JavaFX runtime, loads {@code MainWindow.fxml} and shows
|
|
||||||
* the primary stage. It is intended as a small bootstrap used by IDEs or test runs
|
|
||||||
* to start the JavaFX UI for demonstration purposes.
|
|
||||||
*/
|
|
||||||
public class JavaFXApp extends Application {
|
|
||||||
|
|
||||||
private static final Logger LOGGER = Logger.getLogger(JavaFXApp.class.getName());
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Default constructor only.
|
|
||||||
*
|
|
||||||
* <p>The constructor does not perform UI initialization. UI setup happens in {@link #start(Stage)}
|
|
||||||
* after JavaFX has set up the application thread.
|
|
||||||
*/
|
|
||||||
public JavaFXApp() {
|
|
||||||
// default constructor only
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void main(String[] args) {
|
|
||||||
launch(args);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void start(Stage primaryStage) {
|
|
||||||
try {
|
|
||||||
primaryStage.setTitle("Hello World!");
|
|
||||||
FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("MainWindow.fxml"));
|
|
||||||
Parent root = fxmlLoader.load();
|
|
||||||
primaryStage.setScene(new Scene(root));
|
|
||||||
primaryStage.show();
|
|
||||||
} catch (Exception ex) {
|
|
||||||
LOGGER.log(Level.SEVERE, "Exception while starting JavaFXApp", ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
package de.neitzel.fx.injectfx.example;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Another Main class as workaround when the JavaFX Application ist started without
|
|
||||||
* taking care os Classloader Requirements of JavaFX. (Important when starting from inside NetBeans!)
|
|
||||||
*/
|
|
||||||
public class Main {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a new helper instance of {@code Main}.
|
|
||||||
*
|
|
||||||
* <p>This constructor is intentionally empty. The {@code Main} class serves as an alternative
|
|
||||||
* entry point to launch the JavaFX application via {@link JavaFXApp#main(String[])} in
|
|
||||||
* environments (IDEs, build tools) where directly starting a class that extends
|
|
||||||
* {@code javafx.application.Application} may fail due to classloader or JVM startup constraints.
|
|
||||||
*
|
|
||||||
* <p>Do not perform application initialization here; use the static {@code main} method to
|
|
||||||
* start the JavaFX runtime. This constructor exists to allow tooling and frameworks to
|
|
||||||
* instantiate the class reflectively if required.
|
|
||||||
*/
|
|
||||||
public Main() {
|
|
||||||
// default constructor only
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Additional main methode to start Application.
|
|
||||||
*
|
|
||||||
* @param args Commandline Arguments.
|
|
||||||
*/
|
|
||||||
public static void main(String[] args) {
|
|
||||||
JavaFXApp.main(args);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
package de.neitzel.fx.injectfx.example;
|
|
||||||
|
|
||||||
import javafx.event.ActionEvent;
|
|
||||||
import javafx.fxml.FXML;
|
|
||||||
import javafx.fxml.Initializable;
|
|
||||||
import javafx.scene.control.TextField;
|
|
||||||
|
|
||||||
import java.net.URL;
|
|
||||||
import java.util.ResourceBundle;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Controller for the MainWindow FXML view.
|
|
||||||
*
|
|
||||||
* <p>This class acts as the backing controller for the {@code MainWindow.fxml} UI. It demonstrates a
|
|
||||||
* minimal example of a JavaFX controller that:
|
|
||||||
* <ul>
|
|
||||||
* <li>receives UI element injection via {@link FXML} (the {@link #textField} field),</li>
|
|
||||||
* <li>initializes the view in {@link #initialize(URL, ResourceBundle)}, and</li>
|
|
||||||
* <li>handles user actions via {@link #onButtonClick(ActionEvent)}.</li>
|
|
||||||
* </ul>
|
|
||||||
*
|
|
||||||
* <p>Usage notes:
|
|
||||||
* <ul>
|
|
||||||
* <li>Fields annotated with {@link FXML} are injected by the {@code FXMLLoader} before
|
|
||||||
* {@link #initialize(URL, ResourceBundle)} is called — do not expect them to be non-null
|
|
||||||
* in the constructor.</li>
|
|
||||||
* <li>All UI updates must run on the JavaFX Application Thread. Methods called by FXML
|
|
||||||
* (such as {@link #onButtonClick(ActionEvent)}) are executed on that thread by the
|
|
||||||
* JavaFX runtime.</li>
|
|
||||||
* <li>The controller keeps a simple click counter and updates {@link #textField} to reflect
|
|
||||||
* the current count using {@link #displayCounter()}.</li>
|
|
||||||
* </ul>
|
|
||||||
*/
|
|
||||||
public class MainWindow implements Initializable {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Counter for button clicks shown in the UI.
|
|
||||||
*/
|
|
||||||
private int counter = 0;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Text field UI control showing the click counter.
|
|
||||||
*/
|
|
||||||
@FXML
|
|
||||||
private TextField textField;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Default constructor only
|
|
||||||
*/
|
|
||||||
public MainWindow() {
|
|
||||||
// default constructor only
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void initialize(URL location, ResourceBundle resources) {
|
|
||||||
displayCounter();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void displayCounter() {
|
|
||||||
textField.setText("Click counter: " + counter);
|
|
||||||
}
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private void onButtonClick(ActionEvent actionEvent) {
|
|
||||||
counter++;
|
|
||||||
displayCounter();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
|
|
||||||
<?import javafx.scene.control.TextField?>
|
|
||||||
<?import javafx.scene.layout.AnchorPane?>
|
|
||||||
<AnchorPane xmlns:fx="http://javafx.com/fxml"
|
|
||||||
xmlns:nfx="http://example.com/nfx"
|
|
||||||
xmlns="http://javafx.com/javafx"
|
|
||||||
fx:controller="de.neitzel.fx.component.ComponentController"
|
|
||||||
prefWidth="300" prefHeight="100">
|
|
||||||
<children>
|
|
||||||
<TextField layoutX="10" layoutY="10" prefWidth="280"
|
|
||||||
nfx:target="street" nfx:direction="bidirectional"/>
|
|
||||||
<TextField layoutX="10" layoutY="50" prefWidth="280"
|
|
||||||
nfx:target="city" nfx:direction="bidirectional"/>
|
|
||||||
</children>
|
|
||||||
</AnchorPane>
|
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<?import javafx.geometry.Insets?>
|
||||||
|
<?import javafx.scene.control.Label?>
|
||||||
|
<?import javafx.scene.control.TextField?>
|
||||||
|
<?import javafx.scene.layout.VBox?>
|
||||||
|
<?import de.neitzel.fx.view.BindingList?>
|
||||||
|
<?import de.neitzel.fx.view.DirectBinding?>
|
||||||
|
|
||||||
|
<fx:root type="javafx.scene.layout.VBox" xmlns="http://javafx.com/javafx"
|
||||||
|
xmlns:fx="http://javafx.com/fxml" spacing="6">
|
||||||
|
<padding>
|
||||||
|
<Insets top="4" right="4" bottom="4" left="4"/>
|
||||||
|
</padding>
|
||||||
|
<fx:define>
|
||||||
|
<BindingList>
|
||||||
|
<DirectBinding control="streetField" property="text" path="street" direction="both"/>
|
||||||
|
<DirectBinding control="cityField" property="text" path="city" direction="both"/>
|
||||||
|
</BindingList>
|
||||||
|
</fx:define>
|
||||||
|
<Label text="Street"/>
|
||||||
|
<TextField fx:id="streetField" id="streetField"/>
|
||||||
|
<Label text="City"/>
|
||||||
|
<TextField fx:id="cityField" id="cityField"/>
|
||||||
|
</fx:root>
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<?import javafx.geometry.Insets?>
|
||||||
|
<?import javafx.scene.control.Button?>
|
||||||
|
<?import javafx.scene.control.ComboBox?>
|
||||||
|
<?import javafx.scene.control.Label?>
|
||||||
|
<?import javafx.scene.control.ListView?>
|
||||||
|
<?import javafx.scene.control.TextField?>
|
||||||
|
<?import javafx.scene.layout.ColumnConstraints?>
|
||||||
|
<?import javafx.scene.layout.GridPane?>
|
||||||
|
<?import javafx.scene.layout.HBox?>
|
||||||
|
<?import javafx.scene.layout.VBox?>
|
||||||
|
<?import de.neitzel.fx.example.view.AddressForm?>
|
||||||
|
<?import de.neitzel.fx.view.BindingList?>
|
||||||
|
<?import de.neitzel.fx.view.ComputedBinding?>
|
||||||
|
<?import de.neitzel.fx.view.DirectBinding?>
|
||||||
|
<?import de.neitzel.fx.view.Repeat?>
|
||||||
|
|
||||||
|
<VBox xmlns="http://javafx.com/javafx" xmlns:fx="http://javafx.com/fxml"
|
||||||
|
fx:controller="de.neitzel.fx.example.view.DeskController"
|
||||||
|
spacing="8" prefWidth="520">
|
||||||
|
<padding>
|
||||||
|
<Insets top="12" right="12" bottom="12" left="12"/>
|
||||||
|
</padding>
|
||||||
|
<fx:define>
|
||||||
|
<BindingList>
|
||||||
|
<DirectBinding control="nameField" property="text" path="name" direction="both"/>
|
||||||
|
<DirectBinding control="ageField" property="text" path="age" direction="both"
|
||||||
|
converter="de.neitzel.fx.example.view.AgeConverter"/>
|
||||||
|
<ComputedBinding control="greetingLabel" property="text" method="#greeting"/>
|
||||||
|
<DirectBinding control="homeForm" property="model" path="home" direction="both"/>
|
||||||
|
<DirectBinding control="homeStreetLabel" property="text" path="home.street" direction="view"/>
|
||||||
|
<Repeat pane="visitPane" component="de.neitzel.fx.example.view.AddressForm" collection="visits"/>
|
||||||
|
<DirectBinding control="visitChoice" property="items" path="visits" direction="view"/>
|
||||||
|
<DirectBinding control="visitChoice" property="value" path="selectedVisit" direction="both"/>
|
||||||
|
<DirectBinding control="tagList" property="items" path="tags" direction="view"/>
|
||||||
|
<DirectBinding control="noticeLabel" property="text" path="latestMessage" direction="view"/>
|
||||||
|
</BindingList>
|
||||||
|
</fx:define>
|
||||||
|
|
||||||
|
<Label text="Person" style="-fx-font-size: 16px;"/>
|
||||||
|
<GridPane hgap="8" vgap="6">
|
||||||
|
<columnConstraints>
|
||||||
|
<ColumnConstraints minWidth="110"/>
|
||||||
|
<ColumnConstraints hgrow="ALWAYS" minWidth="280"/>
|
||||||
|
</columnConstraints>
|
||||||
|
<Label text="Name" GridPane.rowIndex="0" GridPane.columnIndex="0"/>
|
||||||
|
<TextField fx:id="nameField" id="nameField" GridPane.rowIndex="0" GridPane.columnIndex="1"/>
|
||||||
|
<Label text="Age" GridPane.rowIndex="1" GridPane.columnIndex="0"/>
|
||||||
|
<TextField fx:id="ageField" id="ageField" promptText="years 36"
|
||||||
|
GridPane.rowIndex="1" GridPane.columnIndex="1"/>
|
||||||
|
<Label text="Greeting" GridPane.rowIndex="2" GridPane.columnIndex="0"/>
|
||||||
|
<Label fx:id="greetingLabel" id="greetingLabel" GridPane.rowIndex="2" GridPane.columnIndex="1"/>
|
||||||
|
<Label text="Home street" GridPane.rowIndex="3" GridPane.columnIndex="0"/>
|
||||||
|
<Label fx:id="homeStreetLabel" id="homeStreetLabel" GridPane.rowIndex="3" GridPane.columnIndex="1"/>
|
||||||
|
</GridPane>
|
||||||
|
|
||||||
|
<Label text="Home"/>
|
||||||
|
<AddressForm fx:id="homeForm"/>
|
||||||
|
|
||||||
|
<Label text="Visits"/>
|
||||||
|
<HBox spacing="8">
|
||||||
|
<ComboBox fx:id="visitChoice" id="visitChoice" HBox.hgrow="ALWAYS" maxWidth="Infinity"/>
|
||||||
|
<Button text="Add visit" onAction="#addVisit"/>
|
||||||
|
</HBox>
|
||||||
|
<VBox fx:id="visitPane" id="visitPane" spacing="4"/>
|
||||||
|
|
||||||
|
<Label text="Tags"/>
|
||||||
|
<ListView fx:id="tagList" id="tagList" prefHeight="80"/>
|
||||||
|
<HBox spacing="8">
|
||||||
|
<TextField fx:id="tagField" id="tagField" promptText="Tag" HBox.hgrow="ALWAYS"/>
|
||||||
|
<Button text="Add tag" onAction="#addTag"/>
|
||||||
|
</HBox>
|
||||||
|
|
||||||
|
<Label text="Notice"/>
|
||||||
|
<Label fx:id="noticeLabel" id="noticeLabel" wrapText="true"/>
|
||||||
|
<Button text="Leave a note" onAction="#leaveNote"/>
|
||||||
|
</VBox>
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
|
|
||||||
<?import javafx.scene.control.Button?>
|
|
||||||
<?import javafx.scene.control.TextField?>
|
|
||||||
<?import javafx.scene.layout.AnchorPane?>
|
|
||||||
<AnchorPane xmlns:fx="http://javafx.com/fxml/1" prefHeight="127.0" prefWidth="209.0"
|
|
||||||
xmlns="http://javafx.com/javafx/17.0.2-ea" fx:controller="de.neitzel.fx.injectfx.example.MainWindow">
|
|
||||||
<children>
|
|
||||||
<Button fx:id="button" layoutX="44.0" layoutY="70.0" mnemonicParsing="false" onAction="#onButtonClick"
|
|
||||||
text="Click Me"/>
|
|
||||||
<TextField fx:id="textField" layoutX="14.0" layoutY="24.0"/>
|
|
||||||
</children>
|
|
||||||
</AnchorPane>
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
|
|
||||||
<?import javafx.scene.control.TextField?>
|
|
||||||
<?import javafx.scene.layout.AnchorPane?>
|
|
||||||
<?import javafx.scene.layout.Pane?>
|
|
||||||
<AnchorPane xmlns:fx="http://javafx.com/fxml"
|
|
||||||
xmlns:nfx="http://example.com/nfx"
|
|
||||||
xmlns="http://javafx.com/javafx"
|
|
||||||
fx:controller="de.neitzel.fx.component.ComponentController"
|
|
||||||
prefWidth="300" prefHeight="180">
|
|
||||||
<children>
|
|
||||||
<TextField layoutX="10" layoutY="10" prefWidth="280"
|
|
||||||
nfx:target="name" nfx:direction="bidirectional"/>
|
|
||||||
<Pane layoutX="10" layoutY="50" prefWidth="280" prefHeight="110"
|
|
||||||
nfx:target="address" nfx:direction="bidirectional"
|
|
||||||
nfx:source="/address.fxml"/>
|
|
||||||
</children>
|
|
||||||
</AnchorPane>
|
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
package de.neitzel.fx.example;
|
||||||
|
|
||||||
|
import de.neitzel.fx.example.model.Person;
|
||||||
|
import de.neitzel.fx.example.view.AddressForm;
|
||||||
|
import de.neitzel.fx.example.view.DeskController;
|
||||||
|
import de.neitzel.fx.view.LoadedView;
|
||||||
|
import de.neitzel.fx.view.ViewLoader;
|
||||||
|
import de.neitzel.injection.ApplicationContext;
|
||||||
|
import javafx.application.Platform;
|
||||||
|
import javafx.scene.Node;
|
||||||
|
import javafx.scene.Parent;
|
||||||
|
import javafx.scene.control.Label;
|
||||||
|
import javafx.scene.control.ListView;
|
||||||
|
import javafx.scene.control.TextField;
|
||||||
|
import javafx.scene.layout.VBox;
|
||||||
|
import org.junit.jupiter.api.BeforeAll;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.fail;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads the person desk and checks the bindings an application developer can see.
|
||||||
|
*/
|
||||||
|
class DeskLoadsTest {
|
||||||
|
|
||||||
|
@BeforeAll
|
||||||
|
static void startJavaFx() throws InterruptedException {
|
||||||
|
CountDownLatch started = new CountDownLatch(1);
|
||||||
|
try {
|
||||||
|
Platform.startup(started::countDown);
|
||||||
|
} catch (IllegalStateException alreadyStarted) {
|
||||||
|
started.countDown();
|
||||||
|
}
|
||||||
|
if (!started.await(10, TimeUnit.SECONDS)) {
|
||||||
|
fail("JavaFX toolkit did not start");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void deskEditsThePersonAndKeepsTheGreetingCurrent() throws Exception {
|
||||||
|
onFx(() -> {
|
||||||
|
Person person = DeskApp.sample();
|
||||||
|
ApplicationContext context = new ApplicationContext(DeskApp.class);
|
||||||
|
LoadedView loaded = ViewLoader.load(DeskController.class.getResource("Desk.fxml"), person, context);
|
||||||
|
Parent root = loaded.getRoot();
|
||||||
|
|
||||||
|
TextField nameField = (TextField) root.lookup("#nameField");
|
||||||
|
TextField ageField = (TextField) root.lookup("#ageField");
|
||||||
|
Label greeting = (Label) root.lookup("#greetingLabel");
|
||||||
|
Label homeStreet = (Label) root.lookup("#homeStreetLabel");
|
||||||
|
TextField street = (TextField) homeForm(root).lookup("#streetField");
|
||||||
|
VBox visits = (VBox) root.lookup("#visitPane");
|
||||||
|
ListView<?> tags = (ListView<?>) root.lookup("#tagList");
|
||||||
|
|
||||||
|
assertEquals("Ada Lovelace", nameField.getText());
|
||||||
|
assertEquals("Hello, Ada Lovelace", greeting.getText());
|
||||||
|
assertEquals("years 36", ageField.getText());
|
||||||
|
assertEquals("Sample Street 1", street.getText());
|
||||||
|
assertEquals("Sample Street 1", homeStreet.getText());
|
||||||
|
assertEquals(1, visits.getChildren().size());
|
||||||
|
assertEquals(2, tags.getItems().size());
|
||||||
|
|
||||||
|
nameField.setText("Grace");
|
||||||
|
assertEquals("Grace", person.getName());
|
||||||
|
assertEquals("Hello, Grace", greeting.getText());
|
||||||
|
|
||||||
|
ageField.setText("years 41");
|
||||||
|
assertEquals(Integer.valueOf(41), person.getAge());
|
||||||
|
ageField.setText("nope");
|
||||||
|
assertEquals(Integer.valueOf(41), person.getAge());
|
||||||
|
assertNotNull(loaded.getViewModel().getLatestMessage());
|
||||||
|
assertEquals("age", loaded.getViewModel().getLatestMessage().getSource());
|
||||||
|
|
||||||
|
street.setText("Other Street");
|
||||||
|
assertEquals("Other Street", person.getHome().getStreet());
|
||||||
|
assertEquals("Other Street", homeStreet.getText());
|
||||||
|
|
||||||
|
person.getTags().add("fx");
|
||||||
|
assertEquals(3, tags.getItems().size());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static AddressForm homeForm(Parent root) {
|
||||||
|
for (Node child : root.getChildrenUnmodifiable()) {
|
||||||
|
if (child instanceof AddressForm form) {
|
||||||
|
return form;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fail("home form was not loaded");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void onFx(Runnable check) throws InterruptedException {
|
||||||
|
CountDownLatch done = new CountDownLatch(1);
|
||||||
|
AtomicReference<Throwable> failure = new AtomicReference<>();
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
try {
|
||||||
|
check.run();
|
||||||
|
} catch (Throwable thrown) {
|
||||||
|
failure.set(thrown);
|
||||||
|
} finally {
|
||||||
|
done.countDown();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!done.await(10, TimeUnit.SECONDS)) {
|
||||||
|
fail("JavaFX check did not finish");
|
||||||
|
}
|
||||||
|
if (failure.get() != null) {
|
||||||
|
fail("desk check failed", failure.get());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+246
@@ -0,0 +1,246 @@
|
|||||||
|
# Using fx
|
||||||
|
|
||||||
|
`fx` binds an FXML View to an ordinary Java Model. The Model stays a JavaBean. The library builds one ViewModel for that instance, loads the View, and applies the binding elements declared in the FXML.
|
||||||
|
|
||||||
|
The worked example is the `fx-example` module, package `de.neitzel.fx.example`. From the NeitzelLib root:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mvn -pl fx-example javafx:run
|
||||||
|
```
|
||||||
|
|
||||||
|
The desk edits one person: name, age, greeting, home address, visits, and tags.
|
||||||
|
|
||||||
|
## What you write
|
||||||
|
|
||||||
|
| Piece | Role |
|
||||||
|
| --- | --- |
|
||||||
|
| Model | A JavaBean. Read-write getters and setters are the attributes the View can bind. |
|
||||||
|
| FXML | The View. Binding elements live in an `fx:define` list, so they are not scene-graph children. |
|
||||||
|
| Controller | Optional. A `@Singleton` the container creates. Computed bindings call its methods. |
|
||||||
|
| Component | A reusable fragment: one class, one `ClassName.fxml` in the same package, one model property. |
|
||||||
|
| Converter | Optional. Translates one Direct binding between the attribute value and the control value. |
|
||||||
|
| `ApplicationContext` | The existing injection container. It creates the Controller, each Component, and each Converter. |
|
||||||
|
|
||||||
|
The library creates the ViewModel. The container does not.
|
||||||
|
|
||||||
|
## A Model
|
||||||
|
|
||||||
|
A read-write attribute is a getter and a setter, and it is not declared on `Object`. Strings, numbers, booleans, enums, and date/time values stay simple values. A type that itself has a read-write attribute is a complex attribute: the ViewModel holds the ViewModel of that nested instance. A `List` of either kind is a list, not a nested ViewModel.
|
||||||
|
|
||||||
|
`Person` and `Address` in the example are Models. They mention no JavaFX type.
|
||||||
|
|
||||||
|
Start lists as a mutable `ArrayList`. After the View loads, the Model's list attribute is the `ObservableList` the View is watching. Add and remove on that list. A later call to the setter leaves the View on the list it already holds.
|
||||||
|
|
||||||
|
## Load a View
|
||||||
|
|
||||||
|
```java
|
||||||
|
ApplicationContext context = new ApplicationContext(DeskApp.class);
|
||||||
|
Person person = DeskApp.sample();
|
||||||
|
LoadedView loaded = ViewLoader.load(
|
||||||
|
DeskController.class.getResource("Desk.fxml"), person, context);
|
||||||
|
Parent root = loaded.getRoot();
|
||||||
|
ViewModel viewModel = loaded.getViewModel();
|
||||||
|
```
|
||||||
|
|
||||||
|
`ApplicationContext` scans the package of the class you pass, and its subpackages, for `@Singleton` and `@Named`. `ViewLoader.load` reuses the ViewModel already built for that instance, or builds one, then loads the FXML and applies the binding elements.
|
||||||
|
|
||||||
|
One Model instance has one ViewModel. A second View of the same instance reuses it, so the two Views stay aligned. The first View fixes the ViewModel class. A later View that names a different class fails to load.
|
||||||
|
|
||||||
|
Put the JavaFX launch in a class that does not extend `Application`, and call `Application.launch` from there. `Main` in the example does that. The scene is built on the JavaFX thread, which is where `start` runs.
|
||||||
|
|
||||||
|
## Direct binding
|
||||||
|
|
||||||
|
A Direct binding names a control by `fx:id`, a JavaFX property of that control, a Path, and a direction.
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<fx:define>
|
||||||
|
<BindingList>
|
||||||
|
<DirectBinding control="nameField" property="text" path="name" direction="both"/>
|
||||||
|
</BindingList>
|
||||||
|
</fx:define>
|
||||||
|
<TextField fx:id="nameField"/>
|
||||||
|
```
|
||||||
|
|
||||||
|
The Path `name` is the attribute of this View's ViewModel. `home.street` steps through the home ViewModel, and the direction applies to `street`. When a complex attribute along the Path is null, that binding is left unapplied and both sides keep their current values.
|
||||||
|
|
||||||
|
| Direction | What it does |
|
||||||
|
| --- | --- |
|
||||||
|
| `both` | Shows the attribute and writes it when the control changes. |
|
||||||
|
| `view` | Shows the attribute and follows later changes of the JavaFX property. Edits to the control stay on the control. |
|
||||||
|
| `model` | Shows the current value, writes the attribute when the control changes, and does not push later property changes back to the control. |
|
||||||
|
|
||||||
|
The write happens when the JavaFX property changes, including each change of a text property. There is no separate save for that write. After the ViewModel exists, it is the writer: a later call to the entity setter leaves the control on the value the ViewModel holds.
|
||||||
|
|
||||||
|
A Path is resolved when the binding is applied. A binding whose last segment is a complex attribute follows later changes of that attribute and passes the Model instance to the control. A longer Path stays with the child ViewModel that was in place when the binding was applied. The desk's "Home street" label is `home.street` toward the View. The home address itself is not swapped out from under that label.
|
||||||
|
|
||||||
|
A list is not indexed in a Path. Repeat a Component, or hand the list to a control.
|
||||||
|
|
||||||
|
## Converter
|
||||||
|
|
||||||
|
A text field edits an `int` through a Converter. The container creates the class and injects its services. `toControlValue` runs toward the View. `toAttributeValue` runs only when the direction is `model` or `both`. A toward-view binding does not need the reverse method.
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<DirectBinding control="ageField" property="text" path="age" direction="both"
|
||||||
|
converter="de.neitzel.fx.example.view.AgeConverter"/>
|
||||||
|
```
|
||||||
|
|
||||||
|
When a conversion throws, the other side keeps its previous value. The ViewModel records one error Message whose source is the attribute name (`age` on the desk). Further failures of that binding update the Message in place. A later success removes it.
|
||||||
|
|
||||||
|
## Computed binding
|
||||||
|
|
||||||
|
A Computed binding names a public no-argument method on the Controller. The method returns the value written to the control. It may read the Controller's fields. It does not write the Model.
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<ComputedBinding control="greetingLabel" property="text" method="#greeting"/>
|
||||||
|
```
|
||||||
|
|
||||||
|
```java
|
||||||
|
public String greeting() {
|
||||||
|
String name = nameField.getText();
|
||||||
|
if (name == null || name.isBlank()) {
|
||||||
|
return "Hello";
|
||||||
|
}
|
||||||
|
return "Hello, " + name.trim();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The library calls the method when the binding is applied, and again when a JavaFX property changes on a control in this View or on this View's ViewModel, other than the target. Writing the greeting label does not call `greeting` again.
|
||||||
|
|
||||||
|
## Controller
|
||||||
|
|
||||||
|
Name the class on the root with `fx:controller`. Leave it off and the library supplies a Controller, which is enough for a View that has only Direct bindings.
|
||||||
|
|
||||||
|
Implement `HasViewModel` when your code needs the ViewModel. The library calls `setViewModel` after construction and before the bindings run. The desk uses that hand-off for the tag list, the visit list, and Messages.
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Singleton
|
||||||
|
public class DeskController implements HasViewModel {
|
||||||
|
@Override
|
||||||
|
public void setViewModel(ViewModel viewModel) {
|
||||||
|
this.viewModel = viewModel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`@FXML` fields are filled during load. A method used as `onAction` is ordinary FXML and is separate from a Computed binding.
|
||||||
|
|
||||||
|
## Component
|
||||||
|
|
||||||
|
A Component is a class you reuse for one Model instance. It extends a pane, loads `ClassName.fxml` from its own package through `fx:root`, and is the Controller of that file. It exposes one JavaFX property named `model`.
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Singleton
|
||||||
|
public class AddressForm extends VBox {
|
||||||
|
private final ObjectProperty<Object> model = new SimpleObjectProperty<>(this, "model");
|
||||||
|
|
||||||
|
public ObjectProperty<Object> modelProperty() {
|
||||||
|
return model;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The component document binds its own controls and has no `fx:controller`:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<fx:root type="javafx.scene.layout.VBox" xmlns="http://javafx.com/javafx"
|
||||||
|
xmlns:fx="http://javafx.com/fxml">
|
||||||
|
<fx:define>
|
||||||
|
<BindingList>
|
||||||
|
<DirectBinding control="streetField" property="text" path="street" direction="both"/>
|
||||||
|
</BindingList>
|
||||||
|
</fx:define>
|
||||||
|
<TextField fx:id="streetField"/>
|
||||||
|
</fx:root>
|
||||||
|
```
|
||||||
|
|
||||||
|
A parent document names the class as an element and sets `model` with a Direct binding. When the Path's last segment is complex, the value assigned is the Model instance behind that ViewModel.
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<DirectBinding control="homeForm" property="model" path="home" direction="both"/>
|
||||||
|
<AddressForm fx:id="homeForm"/>
|
||||||
|
```
|
||||||
|
|
||||||
|
The container creates the Component and injects its services. The component's own bindings run once, when `model` first becomes non-null, and they see that ViewModel. Set the model once. To show another instance, load another Component.
|
||||||
|
|
||||||
|
A nested Component is the same mechanism: the child document names the next class as an element.
|
||||||
|
|
||||||
|
## Repeat
|
||||||
|
|
||||||
|
A `Repeat` element in the same list names a pane, a Component class, and a collection attribute. The library places one new Component in that pane for each Model instance, in list order. Adding or removing an element updates the pane. Those instances are new injected objects, so the cached singleton of the class stays the one used by a single element in the parent document.
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<Repeat pane="visitPane" component="de.neitzel.fx.example.view.AddressForm" collection="visits"/>
|
||||||
|
<VBox fx:id="visitPane"/>
|
||||||
|
```
|
||||||
|
|
||||||
|
Selection is a separate attribute of the parent Model. Null means nothing is selected. A Direct binding can tie a control's selection to it. The repeated Component does not store the selection.
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<DirectBinding control="visitChoice" property="items" path="visits" direction="view"/>
|
||||||
|
<DirectBinding control="visitChoice" property="value" path="selectedVisit" direction="both"/>
|
||||||
|
<ComboBox fx:id="visitChoice"/>
|
||||||
|
```
|
||||||
|
|
||||||
|
The desk's "Add visit" button appends to the list on the ViewModel. That is the list stored back onto the person.
|
||||||
|
|
||||||
|
```java
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private List<Address> visits() {
|
||||||
|
return (List<Address>) viewModel.property("visits").getValue();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## A list of simple values
|
||||||
|
|
||||||
|
A list of strings, numbers, or other simple values is one shared `ObservableList`. A Direct binding sets the control property to that list, so an add or remove is visible on both sides.
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<DirectBinding control="tagList" property="items" path="tags" direction="view"/>
|
||||||
|
<ListView fx:id="tagList"/>
|
||||||
|
```
|
||||||
|
|
||||||
|
The desk's "Add tag" button appends to `tags` the same way "Add visit" appends to `visits`.
|
||||||
|
|
||||||
|
## Messages
|
||||||
|
|
||||||
|
Every ViewModel has a list of Messages and a latest Message. A Message has a severity (`INFO`, `WARN`, `ERROR`, `FATAL`), a text, and a source. Add one of any severity yourself:
|
||||||
|
|
||||||
|
```java
|
||||||
|
viewModel.addMessage(new Message(
|
||||||
|
Message.Severity.INFO,
|
||||||
|
"The model already has the name " + name + ".",
|
||||||
|
"desk"));
|
||||||
|
```
|
||||||
|
|
||||||
|
Error and fatal Messages are written to the log. Info and warn stay on the ViewModel. A child Message is carried onto the parent with the source extended, so `street` on an address becomes `home.street` on the person. When the child drops the Message, the parent drops that copy.
|
||||||
|
|
||||||
|
The library does not open a popup or fill a status bar. Bind `latestMessage` when you want the text on a control:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<DirectBinding control="noticeLabel" property="text" path="latestMessage" direction="view"/>
|
||||||
|
```
|
||||||
|
|
||||||
|
`latestMessage` is the text of the latest Message. An in-place update of a binding failure refreshes that text and does not count as a new Message.
|
||||||
|
|
||||||
|
Call `MessagePopup.showNewMessages(viewModel)` when a new Message should also appear in a dialog. The example does that after load. An in-place update does not open another dialog.
|
||||||
|
|
||||||
|
`LoadedView.property(String)` returns the JavaFX property for an attribute name, for `latestMessage`, or for a JavaFX property declared on a ViewModel subclass.
|
||||||
|
|
||||||
|
## Choosing the ViewModel class
|
||||||
|
|
||||||
|
With no annotation, the library builds the generic ViewModel. Put `@ViewModelClass` on the Controller or the Component to name a subclass, or another object that exposes JavaFX properties by name. The class needs a constructor that takes the Model instance. The library calls that constructor. The container still does not create the ViewModel.
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Singleton
|
||||||
|
@ViewModelClass(TitledViewModel.class)
|
||||||
|
public class DeskController implements HasViewModel {
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The first View of an instance fixes that class. A later View that names a different class fails to load, and the earlier View stays as it was. A later View with no annotation reuses the ViewModel.
|
||||||
|
|
||||||
|
A subclass may add JavaFX properties under new names. Binding elements address those names the same way they address an attribute. `property` on the subclass is a method such as `titleProperty`.
|
||||||
|
|
||||||
|
## Packages
|
||||||
|
|
||||||
|
Every package in the `fx` module is public. Depend on `de.neitzel.lib:fx`. The module depends on `injection`. Application types that the container constructs (`Controller`, `Component`, `Converter`) use `jakarta.inject.Singleton` or `jakarta.inject.Named`, and constructor injection when they need a service.
|
||||||
@@ -15,10 +15,10 @@
|
|||||||
<module>encryption</module>
|
<module>encryption</module>
|
||||||
<module>injection</module>
|
<module>injection</module>
|
||||||
<module>fx</module>
|
<module>fx</module>
|
||||||
|
<module>fx-example</module>
|
||||||
<module>gson</module>
|
<module>gson</module>
|
||||||
<module>net</module>
|
<module>net</module>
|
||||||
<module>log4j</module>
|
<module>log4j</module>
|
||||||
<!-- fx-example still calls the removed JavaFX units and is not part of this reactor until it is migrated. -->
|
|
||||||
<module>quarkus</module>
|
<module>quarkus</module>
|
||||||
</modules>
|
</modules>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user