diff --git a/deployment/src/test/java/io/quarkiverse/fx/deployment/fxviews/FxViewTest.java b/deployment/src/test/java/io/quarkiverse/fx/deployment/fxviews/FxViewTest.java index ac026b7..5693022 100644 --- a/deployment/src/test/java/io/quarkiverse/fx/deployment/fxviews/FxViewTest.java +++ b/deployment/src/test/java/io/quarkiverse/fx/deployment/fxviews/FxViewTest.java @@ -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; @@ -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; diff --git a/deployment/src/test/resources/SampleDialog.css b/deployment/src/test/resources/views/SampleDialog.css similarity index 100% rename from deployment/src/test/resources/SampleDialog.css rename to deployment/src/test/resources/views/SampleDialog.css diff --git a/deployment/src/test/resources/SampleDialog.fxml b/deployment/src/test/resources/views/SampleDialog.fxml similarity index 100% rename from deployment/src/test/resources/SampleDialog.fxml rename to deployment/src/test/resources/views/SampleDialog.fxml diff --git a/deployment/src/test/resources/SampleScene.css b/deployment/src/test/resources/views/SampleScene.css similarity index 100% rename from deployment/src/test/resources/SampleScene.css rename to deployment/src/test/resources/views/SampleScene.css diff --git a/deployment/src/test/resources/SampleScene.fxml b/deployment/src/test/resources/views/SampleScene.fxml similarity index 100% rename from deployment/src/test/resources/SampleScene.fxml rename to deployment/src/test/resources/views/SampleScene.fxml diff --git a/deployment/src/test/resources/SampleStage.css b/deployment/src/test/resources/views/SampleStage.css similarity index 100% rename from deployment/src/test/resources/SampleStage.css rename to deployment/src/test/resources/views/SampleStage.css diff --git a/deployment/src/test/resources/SampleStage.fxml b/deployment/src/test/resources/views/SampleStage.fxml similarity index 100% rename from deployment/src/test/resources/SampleStage.fxml rename to deployment/src/test/resources/views/SampleStage.fxml diff --git a/deployment/src/test/resources/SampleTest.css b/deployment/src/test/resources/views/SampleTest/SampleTest.css similarity index 100% rename from deployment/src/test/resources/SampleTest.css rename to deployment/src/test/resources/views/SampleTest/SampleTest.css diff --git a/deployment/src/test/resources/SampleTest.fxml b/deployment/src/test/resources/views/SampleTest/SampleTest.fxml similarity index 100% rename from deployment/src/test/resources/SampleTest.fxml rename to deployment/src/test/resources/views/SampleTest/SampleTest.fxml diff --git a/deployment/src/test/resources/SampleTest.properties b/deployment/src/test/resources/views/SampleTest/SampleTest.properties similarity index 100% rename from deployment/src/test/resources/SampleTest.properties rename to deployment/src/test/resources/views/SampleTest/SampleTest.properties diff --git a/deployment/src/test/resources/SubSampleTest.fxml b/deployment/src/test/resources/views/SubSampleTest.fxml similarity index 100% rename from deployment/src/test/resources/SubSampleTest.fxml rename to deployment/src/test/resources/views/SubSampleTest.fxml diff --git a/docs/modules/ROOT/pages/fx-views.adoc b/docs/modules/ROOT/pages/fx-views.adoc index 4503b26..dacf496 100644 --- a/docs/modules/ROOT/pages/fx-views.adoc +++ b/docs/modules/ROOT/pages/fx-views.adoc @@ -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 : @@ -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 diff --git a/docs/modules/ROOT/pages/index.adoc b/docs/modules/ROOT/pages/index.adoc index 2c68257..2d5c51f 100644 --- a/docs/modules/ROOT/pages/index.adoc +++ b/docs/modules/ROOT/pages/index.adoc @@ -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. @@ -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` diff --git a/runtime/src/main/java/io/quarkiverse/fx/FxApplication.java b/runtime/src/main/java/io/quarkiverse/fx/FxApplication.java index 5cd2a51..e6b07e3 100644 --- a/runtime/src/main/java/io/quarkiverse/fx/FxApplication.java +++ b/runtime/src/main/java/io/quarkiverse/fx/FxApplication.java @@ -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 diff --git a/runtime/src/main/java/io/quarkiverse/fx/FxViewLoadEvent.java b/runtime/src/main/java/io/quarkiverse/fx/FxViewLoadEvent.java index 0b80da5..d823af7 100644 --- a/runtime/src/main/java/io/quarkiverse/fx/FxViewLoadEvent.java +++ b/runtime/src/main/java/io/quarkiverse/fx/FxViewLoadEvent.java @@ -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; } } diff --git a/runtime/src/main/java/io/quarkiverse/fx/views/FxViewConfig.java b/runtime/src/main/java/io/quarkiverse/fx/views/FxViewConfig.java index f2c6a93..da8978a 100644 --- a/runtime/src/main/java/io/quarkiverse/fx/views/FxViewConfig.java +++ b/runtime/src/main/java/io/quarkiverse/fx/views/FxViewConfig.java @@ -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 diff --git a/runtime/src/main/java/io/quarkiverse/fx/views/FxViewRepository.java b/runtime/src/main/java/io/quarkiverse/fx/views/FxViewRepository.java index ea073e4..ac4175f 100644 --- a/runtime/src/main/java/io/quarkiverse/fx/views/FxViewRepository.java +++ b/runtime/src/main/java/io/quarkiverse/fx/views/FxViewRepository.java @@ -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; @@ -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 { @@ -55,10 +57,12 @@ public void setViewNames(final List 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; @@ -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 @@ -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); @@ -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) { diff --git a/samples/fxviews/src/main/resources/application.properties b/samples/fxviews/src/main/resources/application.properties index f0a2d0f..700fb52 100644 --- a/samples/fxviews/src/main/resources/application.properties +++ b/samples/fxviews/src/main/resources/application.properties @@ -1,3 +1 @@ -quarkus.fx.fxml-root=/fxml/ -quarkus.fx.style-root=/style/ -quarkus.fx.bundle-root=/i18n/ \ No newline at end of file +quarkus.fx.views-root=fxviews \ No newline at end of file diff --git a/samples/fxviews/src/main/resources/fxml/Sample.fxml b/samples/fxviews/src/main/resources/fxviews/Sample.fxml similarity index 100% rename from samples/fxviews/src/main/resources/fxml/Sample.fxml rename to samples/fxviews/src/main/resources/fxviews/Sample.fxml diff --git a/samples/fxviews/src/main/resources/style/custom-sample.css b/samples/fxviews/src/main/resources/fxviews/custom-sample.css similarity index 100% rename from samples/fxviews/src/main/resources/style/custom-sample.css rename to samples/fxviews/src/main/resources/fxviews/custom-sample.css diff --git a/samples/fxviews/src/main/resources/fxml/custom-sample.fxml b/samples/fxviews/src/main/resources/fxviews/custom-sample.fxml similarity index 100% rename from samples/fxviews/src/main/resources/fxml/custom-sample.fxml rename to samples/fxviews/src/main/resources/fxviews/custom-sample.fxml diff --git a/samples/fxviews/src/main/resources/i18n/custom-sample.properties b/samples/fxviews/src/main/resources/fxviews/custom-sample.properties similarity index 100% rename from samples/fxviews/src/main/resources/i18n/custom-sample.properties rename to samples/fxviews/src/main/resources/fxviews/custom-sample.properties