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:
@@ -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>
|
||||
<launcher>${project.artifactId}</launcher>
|
||||
<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>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<!-- FX dependency -->
|
||||
<dependency>
|
||||
<groupId>de.neitzel.lib</groupId>
|
||||
<artifactId>fx</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-simple</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<finalName>${jar.filename}</finalName>
|
||||
<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>
|
||||
<groupId>com.github.spotbugs</groupId>
|
||||
<artifactId>spotbugs-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<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());
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user