Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FX views location configuration #88

Merged
merged 6 commits into from
Oct 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,20 @@
package io.quarkiverse.fx.deployment.fxviews;

import static org.awaitility.Awaitility.await;

import java.net.URI;
import java.nio.file.Path;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;

import jakarta.enterprise.event.Observes;
import jakarta.inject.Inject;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkiverse.fx.FxPostStartupEvent;
import io.quarkiverse.fx.FxStartupLatch;
import io.quarkiverse.fx.QuarkusFxApplication;
Expand All @@ -8,41 +23,24 @@
import io.quarkiverse.fx.views.FxViewRepository;
import io.quarkus.runtime.Quarkus;
import io.quarkus.test.QuarkusUnitTest;
import jakarta.enterprise.event.Observes;
import jakarta.inject.Inject;
import javafx.collections.ObservableList;
import javafx.scene.Parent;
import javafx.stage.Stage;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import java.net.URI;
import java.nio.file.Path;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;

import static org.awaitility.Awaitility.await;

class FxViewTest {

@RegisterExtension
static final QuarkusUnitTest unitTest = new QuarkusUnitTest()
.withApplicationRoot((jar) -> {
jar.addClasses(SampleTestController.class, SubSampleTestController.class);
jar.addClasses(SampleStageController.class, SampleDialogController.class, SampleSceneController.class);
jar.addAsResource("SampleTest.fxml");
jar.addAsResource("SampleTest.properties");
jar.addAsResource("SampleTest.css");
jar.addAsResource("SubSampleTest.fxml");
jar.addAsResource("SampleStage.css");
jar.addAsResource("SampleStage.fxml");
jar.addAsResource("SampleDialog.css");
jar.addAsResource("SampleDialog.fxml");
jar.addAsResource("SampleScene.css");
jar.addAsResource("SampleScene.fxml");
});
jar.addClasses(
SampleTestController.class,
SubSampleTestController.class,
SampleStageController.class,
SampleDialogController.class,
SampleSceneController.class);
jar.addAsResource("views");
})
.overrideConfigKey("quarkus.fx.views-root", "views");

@Inject
FxViewRepository viewRepository;
Expand Down
52 changes: 39 additions & 13 deletions docs/modules/ROOT/pages/fx-views.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Marking an FX controller with such annotation allows for automatic load of :

* the corresponding `.fxml`
* the `.css` stylesheet (if any)
* the resource bundle (if any)
* the `.properties` resource bundle (if any)

By default, the filename is deduced from the controller class name.
For a controller class `MySampleController`, the extension will attempt to load :
Expand All @@ -21,29 +21,55 @@ For a controller class `MySampleController`, the extension will attempt to load

The `Controller` suffix is removed when deducing the filename. From a controller class named `MySample`, same filenames will be retrieved, though it is recommended as a convention to keep the `Controller` suffix.

Also, it is possible to use a custom filename : `@FxView("my-custom-name")` will attempt to load files `my-custom-name.fxml`, `my-custom-name.css`, ...
Also, it is possible to use a custom filename : `@FxView("my-custom-name")` will attempt to load files `my-custom-name.fxml`, `my-custom-name.css` and resource bundle with name `my-custom-name`.

== Directory lookup

By default, files are retrieved from the classpath root.
Therefore, files directly located under `src/main/resources` will be retrieved.

It is possible to customize the root directory for `.fxml`, `.css` and `.properties` files in the `application.properties` file.
It can be customised by specifying the property `fx-views-root` (`/` by default).

.application.properties
If we define this property with value `views` :

[source,properties]
----
fx-views-root=views
----
#FXML
quarkus.fx.fxml-root=/fxml/ <1>

# Style
quarkus.fx.style-root=/style/ <2>
Our project should look like this :

# I18n
quarkus.fx.bundle-root=/i18n/ <3>
[.literal]
----
src/
└── main/
└── resources/
└── views/
├── Home.fxml
├── Home.css
├── Home.properties
├── About.fxml
├── About.css
└── About.properties
----

If the view is not found in the root directory, it will look under the subdirectory corresponding to the view name, allowing to have a better organized application (one directory per view).

[.literal]
----
src/
└── main/
└── resources/
└── views/
├── Home/
│ ├── Home.fxml
│ ├── Home.css
│ └── Home.properties
└── About/
├── About.fxml
├── About.css
└── About.properties
----
<1> FXML lookup directory (default = "/")
<2> Style / CSS lookup directory (default = "/")
<3> Bundle / internationalization lookup directory (default = "/")

== Samples

Expand Down
4 changes: 2 additions & 2 deletions docs/modules/ROOT/pages/index.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ include::./includes/attributes.adoc[]
:diataxis-type: reference
:categories: miscellaneous

:extension-status: experimental
:extension-status: preview

This extension allows you to use https://openjfx.io/[Java FX] in your Quarkus application.

Expand Down Expand Up @@ -32,7 +32,7 @@ dependencies {

== Usage

The extension allows using cdi features in JavaFX controller classes. +
The extension allows using CDI features in JavaFX controller classes. +
The `FXMLLoader` is made a CDI bean and can be injected in your application.

.Loading FXML with injected `FXMLLoader`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public void start(final Stage primaryStage) {
beanManager.getEvent().fire(new FxApplicationStartupEvent(this));

// FXML conventional views loading
beanManager.getEvent().fire(new FxViewLoadEvent());
beanManager.getEvent().fire(new FxViewLoadEvent(primaryStage));

// Fire event that marks that the application has finished starting
// and that Stage instance is available for use
Expand Down
11 changes: 10 additions & 1 deletion runtime/src/main/java/io/quarkiverse/fx/FxViewLoadEvent.java
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
package io.quarkiverse.fx;

import javafx.stage.Stage;

public final class FxViewLoadEvent {

FxViewLoadEvent() {
private final Stage primaryStage;

FxViewLoadEvent(final Stage primaryStage) {
// Package-private constructor
this.primaryStage = primaryStage;
}

public Stage getPrimaryStage() {
return this.primaryStage;
}
}
17 changes: 3 additions & 14 deletions runtime/src/main/java/io/quarkiverse/fx/views/FxViewConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,11 @@
public interface FxViewConfig {

/**
* Root location for fx resources (.fxml)
* Root location for fx views.
* The extension will look for fx views from this root directory.
*/
@WithDefault("/")
String fxmlRoot();

/**
* Root location for style resources (.css)
*/
@WithDefault("/")
String styleRoot();

/**
* Root location for bundle resources (.properties)
*/
@WithDefault("/")
String bundleRoot();
String viewsRoot();

/**
* Enable (or disable) stylesheet live reload in dev mode
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,5 @@
package io.quarkiverse.fx.views;

import io.quarkiverse.fx.FxPostStartupEvent;
import io.quarkiverse.fx.FxViewLoadEvent;
import io.quarkiverse.fx.style.StylesheetWatchService;
import io.quarkus.runtime.LaunchMode;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.event.Observes;
import jakarta.enterprise.inject.Instance;
import jakarta.inject.Inject;
import javafx.collections.ObservableList;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.Dialog;
import javafx.stage.Stage;
import javafx.stage.Window;
import org.jboss.logging.Logger;

import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
Expand All @@ -30,6 +13,25 @@
import java.util.Objects;
import java.util.ResourceBundle;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.event.Observes;
import jakarta.enterprise.inject.Instance;
import jakarta.inject.Inject;

import org.jboss.logging.Logger;

import io.quarkiverse.fx.FxPostStartupEvent;
import io.quarkiverse.fx.FxViewLoadEvent;
import io.quarkiverse.fx.style.StylesheetWatchService;
import io.quarkus.runtime.LaunchMode;
import javafx.collections.ObservableList;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.Dialog;
import javafx.stage.Stage;
import javafx.stage.Window;

@ApplicationScoped
public class FxViewRepository {

Expand All @@ -55,10 +57,12 @@ public void setViewNames(final List<String> views) {
}

/**
* Observe the pre-startup event in order to initialize and set up views
* Observe the view load event in order to initialize and set up views
*/
void setupViews(@Observes final FxViewLoadEvent event) {

this.primaryStage = event.getPrimaryStage();

// Skip processing if no FX view is set
if (this.viewNames.isEmpty()) {
return;
Expand All @@ -73,19 +77,30 @@ void setupViews(@Observes final FxViewLoadEvent event) {
FXMLLoader loader = this.fxmlLoader.get();

// Append path and extensions
String fxml = this.config.fxmlRoot() + name + FXML_EXT;
String css = this.config.styleRoot() + name + STYLE_EXT;
String resources = this.config.bundleRoot() + name;
String viewsRoot = this.config.viewsRoot() + "/";
String fxml = viewsRoot + name + FXML_EXT;
String css = viewsRoot + name + STYLE_EXT;
String resources = viewsRoot + name;

// Resources
ResourceBundle bundle = null;
ResourceBundle bundle;
try {
LOGGER.debugf("Attempting to load resource bundle %s", resources);
bundle = ResourceBundle.getBundle(resources, Locale.getDefault(), classLoader);
LOGGER.debugf("Found resource bundle %s", bundle);
} catch (MissingResourceException e) {
// No bundle
LOGGER.debugf("No resource bundle found for %s", bundle);
// Look for alternate (in directory named after view name)
String alternateResources = resources + "." + name;

try {
LOGGER.debugf("Attempting to load resource bundle %s", resources);
bundle = ResourceBundle.getBundle(alternateResources, Locale.getDefault(), classLoader);
LOGGER.debugf("Found resource bundle %s", bundle);
} catch (MissingResourceException ee) {
// No bundle
bundle = null;
LOGGER.debugf("No resource bundle found for %s", name);
}
}

// Style
Expand All @@ -95,28 +110,48 @@ void setupViews(@Observes final FxViewLoadEvent event) {
Path devPath = Paths.get(this.config.mainResources() + css);
if (devPath.toFile().exists()) {
style = devPath.toString();
} else {
String alternateCss = viewsRoot + name + "/" + name + STYLE_EXT;
Path alternateDevPath = Paths.get(this.config.mainResources() + alternateCss);
if (alternateDevPath.toFile().exists()) {
style = alternateDevPath.toString();
}
}
} else {
URL styleResource = classLoader.getResource(css);
if (styleResource != null) {
LOGGER.debugf("Found css %s", css);
style = styleResource.toExternalForm();
} else {
// Look for alternate (in directory named after view name)
String alternateCss = viewsRoot + name + "/" + name + STYLE_EXT;
URL alternateStyleResource = classLoader.getResource(alternateCss);
if (alternateStyleResource != null) {
LOGGER.debugf("Found css %s", css);
style = alternateStyleResource.toExternalForm();
}
}
}

// FXML
LOGGER.debugf("Loading FXML %s", fxml);
InputStream stream = lookupResourceAsStream(classLoader, fxml);
Objects.requireNonNull(stream, "FXML " + fxml + " not found in classpath.");
if (stream == null) {
// Look for alternate (in directory named after view name)
String alternateFxml = viewsRoot + name + "/" + name + FXML_EXT;
stream = lookupResourceAsStream(classLoader, alternateFxml);
Objects.requireNonNull(stream, "FXML " + fxml + " not found in classpath.");
}

try {
if (bundle != null) {
loader.setResources(bundle);
}

// Set-up loader location (allows use of relative image path for instance)
URL url = lookupResource(classLoader, this.config.fxmlRoot());
URL url = lookupResource(classLoader, viewsRoot);
if (url == null) {
throw new IllegalStateException("Failed to find FXML root location : " + this.config.fxmlRoot());
throw new IllegalStateException("Failed to find FXML viewsRoot location : " + viewsRoot);
}
loader.setLocation(url);

Expand Down Expand Up @@ -144,13 +179,6 @@ void setupViews(@Observes final FxViewLoadEvent event) {
}
}

/**
* Listens for startup event then store primary Stage instance
*/
void setupPrimaryStage(@Observes final FxPostStartupEvent event) {
this.primaryStage = event.getPrimaryStage();
}

private static URL lookupResource(final ClassLoader classLoader, final String name) {
URL url = classLoader.getResource(name);
if (url == null) {
Expand Down
4 changes: 1 addition & 3 deletions samples/fxviews/src/main/resources/application.properties
Original file line number Diff line number Diff line change
@@ -1,3 +1 @@
quarkus.fx.fxml-root=/fxml/
quarkus.fx.style-root=/style/
quarkus.fx.bundle-root=/i18n/
quarkus.fx.views-root=fxviews