Document FX and injection and integrate the Designer reference example
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
# Architecture and lifetime
|
||||
|
||||
## Model, View and ViewModel
|
||||
|
||||
The Model is an ordinary JavaBean representing application data. A View is an FXML document and its JavaFX controls. The ViewModel makes Model Attributes observable and writes changes back to the bean. This is the separation behind Model–View–ViewModel (MVVM): controls depend on observable presentation state rather than carrying persistence or business logic themselves.
|
||||
|
||||
NeitzelLib creates that presentation layer at runtime. You can subclass it for additional properties, choose a replacement object, or generate typed Java accessors. None of these choices generates a controller automatically. A controller remains an optional, application-written object for event handlers, Computed methods and screen behavior. A Component combines a reusable JavaFX root, its own FXML, and a Model property.
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
App[Application: Model + ApplicationContext] --> Loader[ViewLoader]
|
||||
FXML[FXML: controls and binding declarations] --> Loader
|
||||
Loader --> View[LoadedView: JavaFX root]
|
||||
Loader --> VM[Runtime ViewModel]
|
||||
Model[JavaBean Model] -->|Initial getter read / lazy child resolution| VM
|
||||
VM -->|Property change calls setter| Model
|
||||
View <-->|DirectBinding| VM
|
||||
Context[ApplicationContext] --> Controller[Controller / Computed methods]
|
||||
Context --> Converter[Converters]
|
||||
Context --> Component[Component root and controller]
|
||||
Controller --> View
|
||||
Converter --> View
|
||||
Component --> View
|
||||
Registry[Optional generated ViewModel registry] --> VM
|
||||
```
|
||||
|
||||
For the person desk, `Person` is the Model, `Desk.fxml` the View, `PersonViewModel` adds `title`, and `DeskController` handles buttons and calculates the greeting. `AddressForm` renders an Address, `VisitRow` renders a Table row, and converters adapt model values to editable text. The [example walkthrough](../../fx-example/README.md) connects these files to visible behavior.
|
||||
|
||||
The [dependency baseline](../README.md#dependencies) distinguishes application runtime dependencies from optional build and Designer tooling.
|
||||
|
||||
## Loading order
|
||||
|
||||
`ViewLoader.load` reads the controller declaration and its ViewModel selection, obtains or creates the Model's ViewModel, then loads FXML. The injection context supplies controllers, Components and Converters. JavaFX performs normal `@FXML` field injection and calls `initialize`. After loading, the library supplies the ViewModel to `HasViewModel` only when the selected object extends `ViewModel` (replacement objects do not receive this callback), applies BindingLists in document order, and opens custom Tables.
|
||||
|
||||
Consequently, `initialize` must not assume that `HasViewModel.setViewModel` has already run. A Component's own bindings wait for its first non-null Model. Binding order can matter when an earlier declaration causes a nested Model's ViewModel to be created before a later Component requests a more specific class.
|
||||
|
||||
`LoadedView` exposes the root, named properties and the runtime ViewModel. `getViewModel()` returns the generic `ViewModel` or its subclass and throws `IllegalStateException` for a replacement that does not extend it. `property(name)` also supports a replacement exposing named JavaFX properties and returns null for an unknown name. A plain `FXMLLoader` does not apply NeitzelLib's binding declarations or DI integration.
|
||||
|
||||
## Observability and identity
|
||||
|
||||
Attributes are JavaBeans properties with both a getter and setter. Runtime properties are initialized from the bean and changes to them call its setter. Direct calls to a bean setter do not send notifications back to the runtime property. Update the ViewModel on the JavaFX application thread when application logic should update an existing View.
|
||||
|
||||
The runtime holds a **global, strong, identity-based cache** of Model-to-ViewModel associations. The same Model object shares one ViewModel across Views and contexts; equal but distinct beans do not. Registration before traversing children supports cyclic graphs. There is no public eviction or release API. Treat this as an explicit limitation for applications with many short-lived Models.
|
||||
|
||||
Complex Attributes are mapped lazily to nested ViewModels. A null child stays null. A dotted binding resolves intermediate objects when the binding is applied; it does not dynamically reattach after replacing an intermediate child. Bind a Component's `model` to the complex Attribute when you want a subform, but note that the current Component implementation also applies its internal bindings only for its first non-null Model.
|
||||
|
||||
Lists are shared ObservableLists when possible. A `List` bean setter generally accepts the observable wrapper; a concrete collection setter may not. Non-List collections are copied. Initialize collections before loading, and mutate the observable list exposed by the loaded Model/ViewModel, not an old non-observable backing reference. Replacing a bean's collection with its setter does not update an already loaded ViewModel. Repeat and Table handle add/remove and `set(index, replacement)` notifications, but ignore permutations and extractor update notifications.
|
||||
|
||||
## Context and View lifetime
|
||||
|
||||
The [injection context](../../injection/README.md) caches one instance of every scanned class. This includes controllers and converters. Reusing a controller in multiple live Views overwrites its injected FXML fields. Components reuse the context instance for the first occurrence of a class in a load and create fresh instances for additional occurrences; Repeat and Table explicitly create fresh row roots. Nested Components still need care when the same context is reused across independent loads.
|
||||
|
||||
Use a separate context for independent windows unless shared controller/component lifetime is intentional. This does not isolate the global Model identity cache: use separate Model instances if you need independent drafts.
|
||||
|
||||
There is no general `LoadedView.close()`, ViewModel disposal, unbind-all operation, or container shutdown hook. Bindings, collection listeners and MessagePopup listeners can retain objects. Closing a Stage does not constitute a documented cleanup contract for these listeners. Install popup listeners once, keep application lifetimes bounded, and test long-running/repeated-window workloads before depending on this snapshot.
|
||||
|
||||
Run loading, scene changes, observable property updates and message operations on the JavaFX application thread. The context and ViewModel cache are not concurrent services. Perform slow I/O separately and deliver results with `Platform.runLater`; do not perform it inside Computed methods or property listeners.
|
||||
|
||||
## Modules and reflection
|
||||
|
||||
The library artifacts currently act as automatic modules named `fx` and `injection`. An application using JPMS generally needs `requires fx`, `requires injection`, `requires jakarta.inject`, `requires javafx.controls` and `requires javafx.fxml`. The precise requirements depend on its imports.
|
||||
|
||||
Open bean packages to `fx` for reflective Attribute access. FXML controller and Component packages need reflective access for JavaFX and the library/container; an unqualified `opens your.controls;` is a straightforward starting point while diagnosing a modular application. Injection may invoke non-public constructors and members. Generated ViewModel packages also require suitable exports/opens, and a named-module registry uses `provides de.neitzel.fx.view.ViewModelRegistry with ...` rather than only a classpath service file.
|
||||
|
||||
Package scanning depends on the runtime's class/module paths. Verify the real modular launch; success on the classpath does not prove a module configuration is correct. The Designer separately validates classpath and module-path target applications and keeps its Kit runtime distinct from the application's JavaFX runtime.
|
||||
|
||||
## Choosing extensions
|
||||
|
||||
Start with generic runtime properties. Add a ViewModel subclass for derived presentation state and a controller for event handling. Use [generated ViewModels](generated-viewmodels.md) for typed access and shared discovery metadata. Use [Components, Repeat and Table](../README.md#component) to reuse form structure. The Designer is an optional authoring tool: the application remains runnable without it.
|
||||
|
||||
The library is a development snapshot and a personal helper collection. In particular, it is not a transactional form engine, a virtualized table, a dependency-tracking expression language, or a full dependency injection framework. Keep business validation and persistence boundaries explicit in application code.
|
||||
@@ -6,4 +6,54 @@ Modules contribute public `ViewModelRegistry` marker services annotated with rep
|
||||
|
||||
`ViewModelDefaults.select` checks duplicate/conflicting defaults, finds the most-specific applicable Model declaration, and requires an explicit override to extend the corresponding generated base. No declaration preserves generic/default replacement behavior. A compatible explicit request takes precedence before creation. Existing instances are reused for a requested superclass/interface that they satisfy; incompatible requests fail. Nested lazy loading uses the same registry and identity table as direct loading. Failed subclass construction removes its partially registered instance.
|
||||
|
||||
Generated Java source and compiler integration live in NeitzelFXDesigner. This library API is a development snapshot, not a published release. Runtime regressions cover stored identity/write-back, cycles, null children, Messages, compatible/incompatible requests, most-specific defaults, provider non-execution, conflicting contributions and constructor failure cleanup. Existing ungenerated Model and Component tests remain required.
|
||||
Generated Java source and compiler integration live in NeitzelFXDesigner; its [generation guide](https://gitea.neitzel.de/konrad/NeitzelFXDesigner/src/branch/main/docs/user-guide/generation.md) explains processor setup, generated defaults and reviewed FXML creation. This library API is a development snapshot, not a published release. Runtime regressions cover stored identity/write-back, cycles, null children, Messages, compatible/incompatible requests, most-specific defaults, provider non-execution, conflicting contributions and constructor failure cleanup. Existing ungenerated Model and Component tests remain required.
|
||||
|
||||
## What a generated class represents
|
||||
|
||||
For a Model with a `String name` Attribute, a generated base can expose the existing property without creating a second storage location:
|
||||
|
||||
```java
|
||||
package example.generated;
|
||||
|
||||
import de.neitzel.fx.view.ViewModel;
|
||||
import javafx.beans.property.StringProperty;
|
||||
|
||||
public class PersonViewModel extends ViewModel {
|
||||
public PersonViewModel(example.Person model) { super(model); }
|
||||
public StringProperty nameProperty() {
|
||||
return (StringProperty) storedProperty("name");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The generator owns this base; put handwritten derived properties in a subclass. The generic runtime already supports the Model without generated code. Generation adds typed access and discoverable contracts, not a separate binding engine or an automatically generated event controller.
|
||||
|
||||
A registry contribution for the generated base itself as default looks like this:
|
||||
|
||||
```java
|
||||
package example.generated;
|
||||
|
||||
import de.neitzel.fx.view.ViewModelRegistry;
|
||||
import de.neitzel.fx.view.ViewModelRegistration;
|
||||
|
||||
@ViewModelRegistration(model = example.Person.class,
|
||||
generated = PersonViewModel.class,
|
||||
defaultType = PersonViewModel.class)
|
||||
public final class Registry implements ViewModelRegistry { }
|
||||
```
|
||||
|
||||
On the classpath, put this line in `src/main/resources/META-INF/services/de.neitzel.fx.view.ViewModelRegistry`:
|
||||
|
||||
```text
|
||||
example.generated.Registry
|
||||
```
|
||||
|
||||
For JPMS, declare the service in the application module instead:
|
||||
|
||||
```java
|
||||
provides de.neitzel.fx.view.ViewModelRegistry with example.generated.Registry;
|
||||
```
|
||||
|
||||
The selected default may be a handwritten subclass of the generated base. `@ViewModelRegistration` is repeatable; its container is `@ViewModelRegistrations`. Conflicting defaults, ambiguous equally applicable Model declarations, or a default/explicit override outside the required generated base fail rather than silently choosing one. `ViewModelDefaults.select(modelClass, explicitClass, classLoader)` is the public selection operation used by runtime integration; normal applications let `ViewLoader` perform it.
|
||||
|
||||
Keep the generated registry and classes on the same application runtime path as the Model. If discovery succeeds during compilation but the registry service is absent at runtime, the runtime cannot select that default. Do not change the selected class after an instance's ViewModel has already been created: only requests compatible with the existing instance can reuse it.
|
||||
|
||||
@@ -12,3 +12,15 @@ the editor. Increment/decrement use the normal JavaFX spinner behavior.
|
||||
|
||||
The factory rejects negative/inverted bounds and out-of-range initial values.
|
||||
Applications own their field labels, units, and domain-specific bounds.
|
||||
|
||||
|
||||
Create and add a spinner in a controller's `initialize` method or in `Application.start`:
|
||||
|
||||
```java
|
||||
Spinner<Integer> quantity = IntegerSpinners.nonNegative(0, 100, 1);
|
||||
quantity.valueProperty().addListener((observable, oldValue, newValue) ->
|
||||
System.out.println("Quantity: " + newValue));
|
||||
container.getChildren().add(quantity);
|
||||
```
|
||||
|
||||
Import `javafx.scene.control.Spinner` and `de.neitzel.fx.control.IntegerSpinners`; `container` is your Pane. Read `quantity.getValue()` to obtain the committed number. Set a programmatic value through `quantity.getValueFactory().setValue(...)`. Use the factory's bounds as UI constraints and keep independent business validation where the value is saved. The helper does not add an FXML binding declaration or create a Model Attribute automatically.
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
# Your first JavaFX application
|
||||
|
||||
This walkthrough creates a complete Maven project with one editable name and a label that follows it. It uses Java 21, JavaFX 21.0.7 and the current `1.0-SNAPSHOT` NeitzelLib sources. These are local development artifacts; the instructions do not assume that this snapshot is published in Maven Central.
|
||||
|
||||
## Prepare the library
|
||||
|
||||
Install a JDK 21. Use the library's Maven wrapper (3.9.11), or a separate Maven installation version 3.8.6 or newer. Check `java -version` and `./mvnw -version` before building; on macOS, select the JDK with `export JAVA_HOME=$(/usr/libexec/java_home -v 21)` if necessary.
|
||||
|
||||
```bash
|
||||
git clone https://gitea.neitzel.de/konrad/NeitzelLib.git
|
||||
cd NeitzelLib
|
||||
./mvnw -pl fx-example -am -Dmaven.test.failure.ignore=false install
|
||||
./mvnw -f fx-example/pom.xml javafx:run
|
||||
```
|
||||
|
||||
The `install` command installs the parent POM, `core`, `injection` and `fx` into your local Maven repository and builds the example. The repository hardcodes Surefire failure-ignore, so Maven success alone does not establish passing tests, even with the explicit flag. Inspect the XML reports as shown below. The `javafx:run` command opens the [person desk example](../../fx-example/README.md). Run `javafx:run` only on the example POM, not with `-am`: the reactor parent is not a JavaFX application.
|
||||
|
||||
Check test results after the build (requires Python 3):
|
||||
|
||||
```bash
|
||||
python3 - <<'PYTHON'
|
||||
from pathlib import Path
|
||||
import xml.etree.ElementTree as ET
|
||||
reports = list(Path('.').glob('*/target/surefire-reports/TEST-*.xml'))
|
||||
assert reports, 'No test reports found'
|
||||
failures = []
|
||||
for report in reports:
|
||||
suite = ET.parse(report).getroot()
|
||||
if int(suite.get('failures', 0)) or int(suite.get('errors', 0)):
|
||||
failures.append(str(report))
|
||||
assert not failures, 'Failed test reports: ' + ', '.join(failures)
|
||||
print(f'{len(reports)} test suites passed')
|
||||
PYTHON
|
||||
```
|
||||
|
||||
Use a clean build when qualifying a change so stale reports cannot stand in for tests that did not run.
|
||||
|
||||
## Create the application
|
||||
|
||||
Create a separate directory `hello-fx` with these files. This is a classpath application; it deliberately has no `module-info.java`.
|
||||
|
||||
```text
|
||||
hello-fx/
|
||||
pom.xml
|
||||
src/main/java/example/Main.java
|
||||
src/main/java/example/HelloApp.java
|
||||
src/main/java/example/Person.java
|
||||
src/main/resources/example/Hello.fxml
|
||||
```
|
||||
|
||||
`pom.xml`:
|
||||
|
||||
```xml
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>example</groupId>
|
||||
<artifactId>hello-fx</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
<properties>
|
||||
<maven.compiler.release>21</maven.compiler.release>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>de.neitzel.lib</groupId>
|
||||
<artifactId>fx</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-simple</artifactId>
|
||||
<version>2.0.17</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.14.1</version>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.openjfx</groupId>
|
||||
<artifactId>javafx-maven-plugin</artifactId>
|
||||
<version>0.0.8</version>
|
||||
<configuration><mainClass>example.Main</mainClass></configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
```
|
||||
|
||||
`src/main/java/example/Person.java`:
|
||||
|
||||
```java
|
||||
package example;
|
||||
|
||||
public final class Person {
|
||||
private String name = "Ada";
|
||||
|
||||
public String getName() { return name; }
|
||||
public void setName(String name) { this.name = name; }
|
||||
}
|
||||
```
|
||||
|
||||
`src/main/java/example/Main.java`:
|
||||
|
||||
```java
|
||||
package example;
|
||||
|
||||
import javafx.application.Application;
|
||||
|
||||
public final class Main {
|
||||
public static void main(String[] args) {
|
||||
Application.launch(HelloApp.class, args);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`src/main/java/example/HelloApp.java`:
|
||||
|
||||
```java
|
||||
package example;
|
||||
|
||||
import de.neitzel.fx.view.LoadedView;
|
||||
import de.neitzel.fx.view.ViewLoader;
|
||||
import de.neitzel.injection.ApplicationContext;
|
||||
import javafx.application.Application;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.stage.Stage;
|
||||
|
||||
public final class HelloApp extends Application {
|
||||
@Override
|
||||
public void start(Stage stage) throws Exception {
|
||||
Person person = new Person();
|
||||
ApplicationContext context = new ApplicationContext("example");
|
||||
LoadedView view = ViewLoader.load(
|
||||
HelloApp.class.getResource("Hello.fxml"), person, context);
|
||||
stage.setScene(new Scene(view.getRoot()));
|
||||
stage.setTitle("Hello NeitzelLib");
|
||||
stage.show();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`src/main/resources/example/Hello.fxml`:
|
||||
|
||||
```xml
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<?import javafx.scene.layout.VBox?>
|
||||
<?import javafx.scene.control.Label?>
|
||||
<?import javafx.scene.control.TextField?>
|
||||
<?import de.neitzel.fx.view.BindingList?>
|
||||
<?import de.neitzel.fx.view.DirectBinding?>
|
||||
<VBox xmlns="http://javafx.com/javafx/21" xmlns:fx="http://javafx.com/fxml/1"
|
||||
spacing="12" prefWidth="360" prefHeight="120">
|
||||
<fx:define>
|
||||
<BindingList>
|
||||
<DirectBinding control="name" property="text" path="name" direction="both"/>
|
||||
<DirectBinding control="echo" property="text" path="name" direction="view"/>
|
||||
</BindingList>
|
||||
</fx:define>
|
||||
<TextField fx:id="name"/>
|
||||
<Label fx:id="echo"/>
|
||||
</VBox>
|
||||
```
|
||||
|
||||
From `hello-fx`, run:
|
||||
|
||||
```bash
|
||||
mvn clean compile javafx:run
|
||||
```
|
||||
|
||||
Without a separate Maven installation, use the absolute path to the cloned library's `mvnw` with `-f /absolute/path/to/hello-fx/pom.xml clean compile javafx:run`.
|
||||
|
||||
Type another name: the label follows immediately, and the `Person` setter receives the new value. No controller is required for this form. `ViewLoader` creates a runtime ViewModel that holds the JavaFX properties. Updating `person.setName(...)` directly later does **not** notify the ViewModel; update `((javafx.beans.property.StringProperty) view.property("name")).set("Grace")` on the JavaFX application thread instead.
|
||||
|
||||
## Add behavior and reusable forms
|
||||
|
||||
Use an injected controller for event handlers or Computed bindings, and a Component for a reusable FXML form. The [person desk](../../fx-example/README.md) demonstrates both, converters, lists, tables and messages. Read the [architecture guide](architecture.md) before introducing nested Models or sharing windows: Model identity, controller lifetime and list notifications matter.
|
||||
|
||||
`fx` brings `core`, `injection` and JavaFX base, graphics, controls, FXML and web transitively. You do not need Lombok or a generated ViewModel to use this example. Java ViewModel generation and the Designer are optional tools, not prerequisites for loading an FXML view.
|
||||
|
||||
For a named-module application, see the [module and lifecycle notes](architecture.md#modules-and-reflection). Do not simply add `module-info.java` without opening the packages used through reflection.
|
||||
|
||||
## Troubleshooting the first launch
|
||||
|
||||
| Symptom | Check |
|
||||
| --- | --- |
|
||||
| Maven cannot resolve `de.neitzel.lib:fx:1.0-SNAPSHOT` or its parent | Install the library reactor first; use the same Maven local repository for both projects. These instructions use local snapshots. |
|
||||
| `No plugin found for prefix 'javafx'` | Invoke the example POM directly, or use the complete application POM above. Do not invoke the goal on the library reactor parent. |
|
||||
| JavaFX runtime/native library failure | Check the JDK version, architecture and JavaFX dependencies actually resolved; avoid mixing native artifacts from different platforms or JavaFX versions. |
|
||||
| `getResource(...)` returns null | The FXML belongs under `src/main/resources` in the matching package; verify it was copied to `target/classes`. Resource names are case-sensitive. |
|
||||
| `No component found for type` | Annotate the controller/converter/Component, include it in the context's scan root, and inspect `ComponentScanner.getErrors()` for unresolved dependencies. |
|
||||
| Binding cannot find control/property/path | Match `fx:id`, a public JavaFX `nameProperty()` target, and a readable/writable Model Attribute. Use `ViewLoader`, not plain `FXMLLoader`. |
|
||||
| Initial data shows but a bean setter does not update the UI | Update the observable ViewModel property; the bean is not itself observable. |
|
||||
| A second window fails with an already-parented node | Create a separate context for that window; avoid reusing cached Component roots and controllers. |
|
||||
| Reflective access fails only on module path | Add the appropriate `opens` declarations and verify registry service declarations, not only dependencies. |
|
||||
|
||||
Do not build a single ordinary JAR and expect it to include JavaFX's platform-specific runtime automatically. The Maven run goal handles the development launch; deployment requires the application's dependencies or a separately built runtime image. The person desk's JAR-plus-`libs` packaging is an example of the former, and the Designer has its own bundled runtime packaging.
|
||||
@@ -24,3 +24,29 @@ safe from any thread. This is also the public seam for lifecycle tests. The test
|
||||
exercise real JavaFX Stage close/open, cancelled/accepted quit, and unsupported
|
||||
fallback with deterministic native registrations. Native platform behavior must
|
||||
also be checked in the consuming application.
|
||||
|
||||
|
||||
## Application integration
|
||||
|
||||
Keep the handle as application state and give users a Quit button/menu even if the tray is unavailable:
|
||||
|
||||
```java
|
||||
// In Application.start(Stage stage); icon is a java.awt.Image.
|
||||
tray = TraySupport.attach(stage, icon, "My application",
|
||||
() -> confirmUnsavedChanges(), Platform::exit);
|
||||
quitButton.setOnAction(event -> tray.requestQuit());
|
||||
```
|
||||
|
||||
```java
|
||||
// Application field and shutdown hook:
|
||||
private TraySupport tray;
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
if (tray != null) tray.close();
|
||||
}
|
||||
```
|
||||
|
||||
Import `de.neitzel.fx.tray.TraySupport` and `javafx.application.Platform`. Supply your own `java.awt.Image`, confirmation function and Button. This is an integration fragment, not a complete application. `mayQuit` must return whether quitting may proceed; the library does not save documents itself. `isAvailable()` reads current availability, while the read-only `availableProperty()` can drive a status label. A pending installation is not availability.
|
||||
|
||||
Use one owner for the application's primary tray lifecycle: JavaFX implicit exit is global, so independent handles are not a coordinated multi-window policy. This API does not intercept every operating-system application Quit action. `close()` is idempotent and releases only its own registration and handlers; ordinary application resources still need their own cleanup.
|
||||
|
||||
Reference in New Issue
Block a user