Skip to content

Commit

Permalink
FX views location configuration (#88)
Browse files Browse the repository at this point in the history
* dev
  • Loading branch information
CodeSimcoe authored Oct 17, 2024
1 parent 785df9f commit 5c278be
Show file tree
Hide file tree
Showing 22 changed files with 142 additions and 94 deletions.
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
2 changes: 1 addition & 1 deletion runtime/src/main/java/io/quarkiverse/fx/FxApplication.java
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
96 changes: 62 additions & 34 deletions runtime/src/main/java/io/quarkiverse/fx/views/FxViewRepository.java
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

0 comments on commit 5c278be

Please sign in to comment.