From 922ede8608c0c5c50a570c2ff160f12eb7e5c1be Mon Sep 17 00:00:00 2001 From: Konrad Neitzel Date: Fri, 13 Nov 2026 23:04:20 +0100 Subject: [PATCH] Document FX and injection and integrate the Designer reference example --- .gitignore | 3 + README.md | 10 +- fx-example/README.md | 84 +++++++- fx-example/pom.xml | 50 +++++ .../preview/AddressPreviewProvider.java | 31 +++ .../example/preview/DeskPreviewProvider.java | 21 ++ .../example/preview/NotePreviewProvider.java | 22 ++ .../example/preview/PreviewProvidersTest.java | 86 ++++++++ .../fx/example/preview/SampleSession.java | 11 + .../neitzel-fx-designer/examples.properties | 37 ++++ .../fx/example/view/DeskController.java | 12 ++ .../neitzel/fx/example/view/AddressForm.fxml | 3 +- .../de/neitzel/fx/example/view/Desk.fxml | 14 +- .../de/neitzel/fx/example/view/NoteForm.fxml | 3 +- .../de/neitzel/fx/example/view/VisitForm.fxml | 3 +- .../de/neitzel/fx/example/view/VisitRow.fxml | 3 +- fx/README.md | 90 ++++++-- fx/docs/architecture.md | 71 ++++++ fx/docs/generated-viewmodels.md | 52 ++++- fx/docs/integer-spinners.md | 12 ++ fx/docs/quick-start.md | 203 ++++++++++++++++++ fx/docs/tray-support.md | 26 +++ .../java/de/neitzel/fx/view/ViewModels.java | 10 +- injection/README.md | 52 ++++- neitzel-fx-designer.json | 17 ++ 25 files changed, 876 insertions(+), 50 deletions(-) create mode 100644 fx-example/src/designer/java/de/neitzel/fx/example/preview/AddressPreviewProvider.java create mode 100644 fx-example/src/designer/java/de/neitzel/fx/example/preview/DeskPreviewProvider.java create mode 100644 fx-example/src/designer/java/de/neitzel/fx/example/preview/NotePreviewProvider.java create mode 100644 fx-example/src/designer/java/de/neitzel/fx/example/preview/PreviewProvidersTest.java create mode 100644 fx-example/src/designer/java/de/neitzel/fx/example/preview/SampleSession.java create mode 100644 fx-example/src/designer/resources/META-INF/neitzel-fx-designer/examples.properties create mode 100644 fx/docs/architecture.md create mode 100644 fx/docs/quick-start.md create mode 100644 neitzel-fx-designer.json diff --git a/.gitignore b/.gitignore index b5a5fe3..1597ed3 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,6 @@ debug.out /nbactions.xml dependency-reduced-pom.xml .DS_Store + +# Machine-local designer paths, snapshots and drafts +.neitzel-fx-designer/ diff --git a/README.md b/README.md index 0d9cbc0..9f9cf1e 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ Package scanner and type-based container used by `fx`. See [injection/README.md] ### fx-example -Person desk that shows how to use `fx` and `injection`. Run `mvn -pl fx-example -am javafx:run`. See [fx-example/README.md](fx-example/README.md). +Person desk that shows how to use `fx` and `injection`. Build with `./mvnw -pl fx-example -am install`, then run `./mvnw -f fx-example/pom.xml javafx:run`. See [fx-example/README.md](fx-example/README.md). ### gson @@ -68,4 +68,10 @@ Some network stuff using javax.mail as dependency. ### quarkus -Some additions for quarkus that might be useful. \ No newline at end of file +Some additions for quarkus that might be useful. + +## Start a JavaFX project + +The [complete quick start](fx/docs/quick-start.md) includes local snapshot installation, a standalone Maven POM, Java classes, FXML and launch commands. Continue with [MVVM and runtime architecture](fx/docs/architecture.md), the [FXML reference](fx/README.md), and the [person desk walkthrough](fx-example/README.md). The [injection guide](injection/README.md) also covers use without JavaFX. + +Build with Java 21. Maven wrapper version is 3.9.11; the project requires Maven 3.8.6 or later. Library version `1.0-SNAPSHOT` denotes development artifacts, not a published stable release. The parent POM currently ignores Surefire failures: check test reports, as explained in the quick start, rather than relying only on Maven's exit code. diff --git a/fx-example/README.md b/fx-example/README.md index 2013b0e..7ae86bd 100644 --- a/fx-example/README.md +++ b/fx-example/README.md @@ -1,13 +1,85 @@ -# fx-example +# Person desk reference application -A person desk that uses [`fx`](../fx/README.md) and [`injection`](../injection/README.md). The guides use this module as the worked example. +This runnable application demonstrates [NeitzelLib FX](../fx/README.md) and [injection](../injection/README.md) together. It edits one Person with a home Address, nested Note, visit list, selected visit and tags. Use it as a reference for wiring a real screen; the [minimal quick start](../fx/docs/quick-start.md) is the smaller starting point for a new project. -The desk shows a direct binding in each direction, a converter, a computed binding, a view model subclass, a component with a nested note, a repeat, a table with a starting width and an open column, a date column, a list of simple values, and messages. The container side shows `@Config`, `@Singleton`, `@Named`, and injection by constructor, field, and setter. +## Build and run -From the NeitzelLib root, with Java 21: +From the NeitzelLib root with JDK 21: ```bash -mvn -pl fx-example -am javafx:run +./mvnw -pl fx-example -am install +./mvnw -f fx-example/pom.xml javafx:run ``` -The launch class is `de.neitzel.fx.example.Main`. The model is an ordinary `Person`. The view is `Desk.fxml`. `DeskLoadsTest` loads that same desk. +The first command installs local `1.0-SNAPSHOT` artifacts; the second runs only the application module. Do not combine `javafx:run` with `-am`, since the parent has no JavaFX launcher. The repository currently ignores Surefire failures; verify XML reports as shown in the [quick start](../fx/docs/quick-start.md), even when Maven reports success. + +The packaged alternative, after `package` or `install`, is: + +```bash +java -jar fx-example/target/fx-example-1.0-SNAPSHOT.jar +``` + +Keep the adjacent `target/libs` directory with the JAR: the manifest refers to those copied runtime dependencies. A desktop session is required. This example has no `module-info.java`; it is a classpath application. + +## Follow the code + +Paths below are relative to this module. Java sources share `src/main/java/de/neitzel/fx/example/`; FXML resources share `src/main/resources/de/neitzel/fx/example/view/`. + +| File/type | Responsibility | +| --- | --- | +| `Main` | Plain Java launcher calling `Application.launch` | +| `DeskApp` | Scan configuration, fresh sample Model, ViewLoader, Scene and message popup registration | +| `model/Person`, `Address`, `Note`, `Role` | JavaBean data and enum; no JavaFX properties required | +| `view/DeskController` and `Desk.fxml` | Screen handlers, computed greeting, controls and binding declarations | +| `view/PersonViewModel` | Handwritten ViewModel subclass with a derived title | +| `view/AddressForm` and `AddressForm.fxml` | Reusable home subform and injected heading, containing NoteForm | +| `view/NoteForm` and `NoteForm.fxml` | Nested Component bound to the note | +| `view/VisitForm` and `VisitForm.fxml` | Fresh form per repeated Address | +| `view/VisitRow` and `VisitRow.fxml` | Table row with editable fields and a trailing Remove button | +| `src/test/resources/de/neitzel/fx/example/NameLine.fxml` | Test-only second View of the same Person without reusable Component nodes | +| `view/AgeConverter`, `StreetConverter`, `VisitedOnConverter` | Text adaptation, validation failures and ISO dates | + +Use the actual source directories to locate helper services such as `AgeSuffix`, `StreetPolicy` and `FormHeading`. The injection guide explains constructor, field and setter injection with these types. No code generation is required to run this example; `PersonViewModel` is handwritten. + +## Try the behaviors + +1. Change Name. The Model and name echo follow; title and computed greeting refresh. The draft field uses `direction="model"`: its edits write to the Model but later changes from another field do not overwrite it. +2. Edit Age using its `years ` text format. An invalid value stays visible while the previous Model value is retained. A message explains the failure; a valid value clears that conversion failure. +3. Edit the home street. The path-bound label follows. Empty street input fails through `StreetConverter`; the parent receives the child message with the `home.street` source prefix. +4. Edit the nested note. AddressForm's inner NoteForm binds to its own Model. The `Opened as ...` label records initial setup rather than tracking every later city edit. +5. Add a visit. Both the repeated forms and Table gain a row. Edit a street in one representation and observe the same Address in the other. The Table's Remove button delegates to the controller. +6. Select a visit in the ComboBox or Table. Both use the `selectedVisit` Attribute. Resize, sort and filter Table columns; filtering and sorting affect displayed rows without reordering the Model's list. +7. Edit the visited date in ISO form (`yyyy-MM-dd`). Invalid text keeps the old date. Add a tag to see a shared ObservableList of simple values. +8. Try the information, warning and fatal message buttons. Severity is data and logging behavior; a fatal message does not itself exit the application. Close each non-blocking popup normally. + +Changing a JavaBean setter directly after loading does not notify controls. All edits above go through ViewModel properties or observable collections. The [architecture guide](../fx/docs/architecture.md) explains this distinction and why you should normally use a new context for a separate window. + +## Automated regression coverage + +`src/test/java/de/neitzel/fx/example/DeskLoadsTest.java` loads the real Desk FXML on JavaFX and checks initial values, all binding directions, derived/computed text, conversion failures and recovery, child messages, Table edits/removal, message severities and boolean/enum write-back. It also demonstrates that a direct bean setter leaves controls unchanged and that another simple View reuses the same Model's ViewModel. + +Run the reactor tests without installing artifacts with: + +```bash +./mvnw -pl fx-example -am clean verify +``` + +Inspect Surefire XML reports; a successful Maven exit alone is insufficient with the current parent configuration. JavaFX tests need a usable graphical environment. See the Designer integration section below for the optional preview setup; ordinary application tests do not require the Designer. + +## Open the example in NeitzelFXDesigner + +The repository root includes `neitzel-fx-designer.json` with the `person-desk` context. It selects `fx-example`, classpath launch, the `designer-preview` Maven profile, the local runtime key `java21`, and `de.neitzel.fx.example.preview.DeskPreviewProvider`. Configure your own JDK path for that key; machine paths are not part of the shared project configuration. + +The optional profile adds the Designer preview API only to the test source set, compiles `src/designer/java`, includes its resources, and produces Model metadata for Person, Address and Note. It does not add a runtime dependency to the ordinary application, replace its handwritten PersonViewModel, or generate a default ViewModel registry. + +After preparing the Designer's local preview API and metadata processor artifacts, build with: + +```bash +./mvnw -pl fx-example -am -Pdesigner-preview verify +``` + +Check XML test reports as for the normal build. Follow `docs/user-guide/quick-start.md` in the [NeitzelFXDesigner repository](https://gitea.neitzel.de/konrad/NeitzelFXDesigner) for the complete artifact preparation, library-example setup script, local runtime mapping, application launch and Build and Refresh workflow. That guide covers both the standalone host and IntelliJ integration; this profile alone is not a Designer installation. + +FXML documents carry `neitzel-model` processing instructions for discovery. The preview provider offers `desk`, `address`, `visit`, `row` and `note` samples for the corresponding documents. Every session receives fresh sample Models and an injection context; the preview does not install the application's message popups. Open the document, Build and Refresh, choose its matching sample, then start interactive preview. Editing the preview's data does not save those values into the application source. + +The role choices are populated in `DeskController.initialize()` rather than using an application-enum `fx:constant` in FXML, because the current Designer projection cannot resolve that application constant. This is a Designer limitation, not a restriction of NeitzelLib or ordinary FXMLLoader. The regular application still shows the same Role values. diff --git a/fx-example/pom.xml b/fx-example/pom.xml index aa380e5..84617fd 100644 --- a/fx-example/pom.xml +++ b/fx-example/pom.xml @@ -97,4 +97,54 @@ + + + designer-preview + + + de.neitzel.fxdesigner + designer-preview-api + 0.1.0-SNAPSHOT + test + + + + + + org.codehaus.mojo + build-helper-maven-plugin + 3.6.1 + + + designer-preview-sources + generate-test-sources + add-test-sourceadd-test-resource + + src/designer/java + src/designer/resources + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + full + + + de.neitzel.fxdesigner + designer-model-processor + 0.1.0-SNAPSHOT + + + + -Adesigner.models=de.neitzel.fx.example.model.Person,de.neitzel.fx.example.model.Address,de.neitzel.fx.example.model.Note + + + + + + + diff --git a/fx-example/src/designer/java/de/neitzel/fx/example/preview/AddressPreviewProvider.java b/fx-example/src/designer/java/de/neitzel/fx/example/preview/AddressPreviewProvider.java new file mode 100644 index 0000000..3e7170d --- /dev/null +++ b/fx-example/src/designer/java/de/neitzel/fx/example/preview/AddressPreviewProvider.java @@ -0,0 +1,31 @@ +package de.neitzel.fx.example.preview; + +import de.neitzel.fx.example.DeskApp; +import de.neitzel.fx.example.model.Address; +import de.neitzel.fx.example.view.DeskController; +import de.neitzel.fx.example.view.PersonViewModel; +import de.neitzel.fxdesigner.preview.PreviewProvider; +import de.neitzel.fxdesigner.preview.PreviewSession; +import de.neitzel.injection.ApplicationContext; + +/** Independent samples for the nested, repeated and table-row Address components. */ +public final class AddressPreviewProvider implements PreviewProvider { + @Override + public Class
modelType() { + return Address.class; + } + + @Override + public PreviewSession open(String id) { + if (!java.util.Set.of("address", "visit", "row").contains(id)) + throw new IllegalArgumentException("Unknown address example: " + id); + var person = DeskApp.sample(); + var context = new ApplicationContext(DeskApp.class); + // An independently previewed row still needs the parent action's state. + // No Desk.fxml is loaded and no popup listener is installed. + if ("row".equals(id)) + context.getComponent(DeskController.class).setViewModel(new PersonViewModel(person)); + return new SampleSession<>( + "address".equals(id) ? person.getHome() : person.getVisits().getFirst(), context); + } +} diff --git a/fx-example/src/designer/java/de/neitzel/fx/example/preview/DeskPreviewProvider.java b/fx-example/src/designer/java/de/neitzel/fx/example/preview/DeskPreviewProvider.java new file mode 100644 index 0000000..a3811fe --- /dev/null +++ b/fx-example/src/designer/java/de/neitzel/fx/example/preview/DeskPreviewProvider.java @@ -0,0 +1,21 @@ +package de.neitzel.fx.example.preview; + +import de.neitzel.fx.example.DeskApp; +import de.neitzel.fx.example.model.Person; +import de.neitzel.fxdesigner.preview.PreviewProvider; +import de.neitzel.fxdesigner.preview.PreviewSession; +import de.neitzel.injection.ApplicationContext; + +/** Optional test-source preview adapter; it never enters the application distribution. */ +public final class DeskPreviewProvider implements PreviewProvider { + @Override + public Class modelType() { + return Person.class; + } + + @Override + public PreviewSession open(String id) { + if (!"desk".equals(id)) throw new IllegalArgumentException("Unknown desk example: " + id); + return new SampleSession<>(DeskApp.sample(), new ApplicationContext(DeskApp.class)); + } +} diff --git a/fx-example/src/designer/java/de/neitzel/fx/example/preview/NotePreviewProvider.java b/fx-example/src/designer/java/de/neitzel/fx/example/preview/NotePreviewProvider.java new file mode 100644 index 0000000..23db4f4 --- /dev/null +++ b/fx-example/src/designer/java/de/neitzel/fx/example/preview/NotePreviewProvider.java @@ -0,0 +1,22 @@ +package de.neitzel.fx.example.preview; + +import de.neitzel.fx.example.DeskApp; +import de.neitzel.fx.example.model.Note; +import de.neitzel.fxdesigner.preview.PreviewProvider; +import de.neitzel.fxdesigner.preview.PreviewSession; +import de.neitzel.injection.ApplicationContext; + +/** Independent sample for the nested note editor. */ +public final class NotePreviewProvider implements PreviewProvider { + @Override + public Class modelType() { + return Note.class; + } + + @Override + public PreviewSession open(String id) { + if (!"note".equals(id)) throw new IllegalArgumentException("Unknown note example: " + id); + return new SampleSession<>( + DeskApp.sample().getHome().getNote(), new ApplicationContext(DeskApp.class)); + } +} diff --git a/fx-example/src/designer/java/de/neitzel/fx/example/preview/PreviewProvidersTest.java b/fx-example/src/designer/java/de/neitzel/fx/example/preview/PreviewProvidersTest.java new file mode 100644 index 0000000..2c3daa2 --- /dev/null +++ b/fx-example/src/designer/java/de/neitzel/fx/example/preview/PreviewProvidersTest.java @@ -0,0 +1,86 @@ +package de.neitzel.fx.example.preview; + +import static org.junit.jupiter.api.Assertions.*; + +import de.neitzel.fx.example.view.DeskController; +import de.neitzel.fx.view.ViewLoader; + +import javafx.application.Platform; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.util.concurrent.*; + +class PreviewProvidersTest { + @BeforeAll + static void startToolkit() throws Exception { + var ready = new CountDownLatch(1); + try { + Platform.startup(ready::countDown); + } catch (IllegalStateException alreadyRunning) { + ready.countDown(); + } + assertTrue(ready.await(10, TimeUnit.SECONDS)); + } + + private static void onFx(Runnable action) throws Exception { + var result = new CompletableFuture(); + Platform.runLater( + () -> { + try { + action.run(); + result.complete(null); + } catch (Throwable e) { + result.completeExceptionally(e); + } + }); + result.get(20, TimeUnit.SECONDS); + } + + @Test + void deskSamplesAreFreshAndLoadTheRealDesk() throws Exception { + onFx( + () -> { + var provider = new DeskPreviewProvider(); + var first = provider.open("desk"); + var second = provider.open("desk"); + assertNotSame(first.model(), second.model()); + assertNotSame(first.model().getHome(), second.model().getHome()); + assertNotSame(first.context(), second.context()); + var view = + ViewLoader.load( + DeskController.class.getResource("Desk.fxml"), + first.model(), + first.context()); + assertNotNull(view.getRoot().lookup("#nameField")); + assertEquals("Ada Lovelace", first.model().getName()); + assertTrue(first.ready().toCompletableFuture().isDone()); + assertThrows(IllegalArgumentException.class, () -> provider.open("unknown")); + }); + } + + @Test + void componentSamplesAndIndependentRowActionHaveRequiredState() throws Exception { + onFx( + () -> { + var addresses = new AddressPreviewProvider(); + var home = addresses.open("address"); + assertNotNull(home.model().getNote()); + var visit = addresses.open("visit"); + assertNotNull(visit.model().getVisitedOn()); + var row = addresses.open("row"); + assertDoesNotThrow( + () -> + row.context() + .getComponent(DeskController.class) + .removeVisit(row.model())); + var note = new NotePreviewProvider().open("note"); + assertEquals("Kept at home", note.model().getText()); + assertThrows(IllegalArgumentException.class, () -> addresses.open("desk")); + assertThrows( + IllegalArgumentException.class, + () -> new NotePreviewProvider().open("desk")); + }); + } +} diff --git a/fx-example/src/designer/java/de/neitzel/fx/example/preview/SampleSession.java b/fx-example/src/designer/java/de/neitzel/fx/example/preview/SampleSession.java new file mode 100644 index 0000000..8e29aa7 --- /dev/null +++ b/fx-example/src/designer/java/de/neitzel/fx/example/preview/SampleSession.java @@ -0,0 +1,11 @@ +package de.neitzel.fx.example.preview; + +import de.neitzel.fxdesigner.preview.PreviewSession; +import de.neitzel.injection.ApplicationContext; + +/** The sample owns no timers, network connections or background work to release. */ +record SampleSession(M model, ApplicationContext context) + implements PreviewSession { + @Override + public void close() {} +} diff --git a/fx-example/src/designer/resources/META-INF/neitzel-fx-designer/examples.properties b/fx-example/src/designer/resources/META-INF/neitzel-fx-designer/examples.properties new file mode 100644 index 0000000..a34ff5d --- /dev/null +++ b/fx-example/src/designer/resources/META-INF/neitzel-fx-designer/examples.properties @@ -0,0 +1,37 @@ +format=1 +count=5 +example.0.id=desk +example.0.label=Person desk +example.0.modelType=de.neitzel.fx.example.model.Person +example.0.providerClass=de.neitzel.fx.example.preview.DeskPreviewProvider +example.0.viewResource=de/neitzel/fx/example/view/Desk.fxml +example.0.componentPackage=de.neitzel.fx.example.view + +example.1.id=address +example.1.label=Home address +example.1.modelType=de.neitzel.fx.example.model.Address +example.1.providerClass=de.neitzel.fx.example.preview.AddressPreviewProvider +example.1.viewResource=de/neitzel/fx/example/view/AddressForm.fxml +example.1.componentPackage=de.neitzel.fx.example.view + +example.2.id=visit +example.2.label=Repeated visit +example.2.modelType=de.neitzel.fx.example.model.Address +example.2.providerClass=de.neitzel.fx.example.preview.AddressPreviewProvider +example.2.viewResource=de/neitzel/fx/example/view/VisitForm.fxml +example.2.componentPackage=de.neitzel.fx.example.view + +example.3.id=row +example.3.label=Table row +example.3.modelType=de.neitzel.fx.example.model.Address +example.3.providerClass=de.neitzel.fx.example.preview.AddressPreviewProvider +example.3.viewResource=de/neitzel/fx/example/view/VisitRow.fxml +example.3.componentPackage=de.neitzel.fx.example.view + +example.4.id=note +example.4.label=Address note +example.4.modelType=de.neitzel.fx.example.model.Note +example.4.providerClass=de.neitzel.fx.example.preview.NotePreviewProvider +example.4.viewResource=de/neitzel/fx/example/view/NoteForm.fxml +example.4.componentPackage=de.neitzel.fx.example.view + diff --git a/fx-example/src/main/java/de/neitzel/fx/example/view/DeskController.java b/fx-example/src/main/java/de/neitzel/fx/example/view/DeskController.java index faade7b..fc4b2a9 100644 --- a/fx-example/src/main/java/de/neitzel/fx/example/view/DeskController.java +++ b/fx-example/src/main/java/de/neitzel/fx/example/view/DeskController.java @@ -1,6 +1,7 @@ package de.neitzel.fx.example.view; import de.neitzel.fx.example.model.Address; +import de.neitzel.fx.example.model.Role; import de.neitzel.fx.view.HasViewModel; import de.neitzel.fx.view.Message; import de.neitzel.fx.view.ViewModel; @@ -8,6 +9,7 @@ import de.neitzel.fx.view.ViewModelClass; import jakarta.inject.Singleton; import javafx.fxml.FXML; import javafx.scene.control.TextField; +import javafx.scene.control.ChoiceBox; import java.time.LocalDate; import java.util.List; @@ -35,6 +37,16 @@ public class DeskController implements HasViewModel { @FXML private TextField tagField; + /** Role values are supplied in the application runtime, not instantiated by the designer host. */ + @FXML + private ChoiceBox roleChoice; + + /** Supplies enum choices before the declarative value binding is applied. */ + @FXML + public void initialize() { + roleChoice.getItems().setAll(Role.values()); + } + /** * The person view model, set before the bindings run. */ diff --git a/fx-example/src/main/resources/de/neitzel/fx/example/view/AddressForm.fxml b/fx-example/src/main/resources/de/neitzel/fx/example/view/AddressForm.fxml index 76862bf..7721f0f 100644 --- a/fx-example/src/main/resources/de/neitzel/fx/example/view/AddressForm.fxml +++ b/fx-example/src/main/resources/de/neitzel/fx/example/view/AddressForm.fxml @@ -1,4 +1,5 @@ + @@ -9,7 +10,7 @@ + xmlns:fx="http://javafx.com/fxml/1" spacing="6"> diff --git a/fx-example/src/main/resources/de/neitzel/fx/example/view/Desk.fxml b/fx-example/src/main/resources/de/neitzel/fx/example/view/Desk.fxml index 5f356c7..67caff1 100644 --- a/fx-example/src/main/resources/de/neitzel/fx/example/view/Desk.fxml +++ b/fx-example/src/main/resources/de/neitzel/fx/example/view/Desk.fxml @@ -1,6 +1,6 @@ + - @@ -13,7 +13,6 @@ - @@ -22,7 +21,7 @@ - @@ -70,14 +69,7 @@