From 9dd08ed8a590d3c4ae58a841dbae021af255a93a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20de=20Tastes?= Date: Fri, 20 Sep 2024 13:20:54 +0200 Subject: [PATCH] CSS live reload --- .../ROOT/pages/includes/quarkus-fx.adoc | 34 +++++++++ .../fx/style/StylesheetWatchService.java | 71 +++++++++++++++++++ .../io/quarkiverse/fx/views/FxViewConfig.java | 12 ++++ .../fx/views/FxViewRepository.java | 34 +++++++-- .../main/resources/style/custom-sample.css | 2 + 5 files changed, 148 insertions(+), 5 deletions(-) create mode 100644 runtime/src/main/java/io/quarkiverse/fx/style/StylesheetWatchService.java diff --git a/docs/modules/ROOT/pages/includes/quarkus-fx.adoc b/docs/modules/ROOT/pages/includes/quarkus-fx.adoc index bc48dac..ff2f72c 100644 --- a/docs/modules/ROOT/pages/includes/quarkus-fx.adoc +++ b/docs/modules/ROOT/pages/includes/quarkus-fx.adoc @@ -60,4 +60,38 @@ endif::add-copy-button-to-env-var[] --|string |`/` + +a|icon:lock[title=Fixed at build time] [[quarkus-fx_quarkus-fx-main-resources]]`link:#quarkus-fx_quarkus-fx-main-resources[quarkus.fx.main-resources]` + + +[.description] +-- +Location for main resources (allowing stylesheet live reload) + +ifdef::add-copy-button-to-env-var[] +Environment variable: env_var_with_copy_button:+++QUARKUS_FX_MAIN_RESOURCES+++[] +endif::add-copy-button-to-env-var[] +ifndef::add-copy-button-to-env-var[] +Environment variable: `+++QUARKUS_FX_MAIN_RESOURCES+++` +endif::add-copy-button-to-env-var[] +--|string +|`src/main/resources/` + + +a|icon:lock[title=Fixed at build time] [[quarkus-fx_quarkus-fx-test-resources]]`link:#quarkus-fx_quarkus-fx-test-resources[quarkus.fx.test-resources]` + + +[.description] +-- +Location for test resources (allowing stylesheet live reload) + +ifdef::add-copy-button-to-env-var[] +Environment variable: env_var_with_copy_button:+++QUARKUS_FX_TEST_RESOURCES+++[] +endif::add-copy-button-to-env-var[] +ifndef::add-copy-button-to-env-var[] +Environment variable: `+++QUARKUS_FX_TEST_RESOURCES+++` +endif::add-copy-button-to-env-var[] +--|string +|`src/test/resources/` + |=== \ No newline at end of file diff --git a/runtime/src/main/java/io/quarkiverse/fx/style/StylesheetWatchService.java b/runtime/src/main/java/io/quarkiverse/fx/style/StylesheetWatchService.java new file mode 100644 index 0000000..2265fc2 --- /dev/null +++ b/runtime/src/main/java/io/quarkiverse/fx/style/StylesheetWatchService.java @@ -0,0 +1,71 @@ +package io.quarkiverse.fx.style; + +import java.io.IOException; +import java.net.URL; +import java.nio.file.FileSystems; +import java.nio.file.Path; +import java.nio.file.StandardWatchEventKinds; +import java.nio.file.WatchEvent; +import java.nio.file.WatchKey; +import java.nio.file.WatchService; +import java.util.concurrent.CompletableFuture; +import java.util.function.Supplier; + +import javafx.application.Platform; +import javafx.collections.ObservableList; + +/** + * This utility class allows live CSS reload by watching filesystem changes + * and re-setting the stylesheet upon change. + * It is automatically used in dev mode for all {@link io.quarkiverse.fx.views.FxView} + */ +public final class StylesheetWatchService { + + private StylesheetWatchService() { + // Utility class + } + + public static void setStyleAndStartWatchingTask( + final Supplier> stylesheetsSupplier, + final String stylesheet) throws IOException { + + // CSS live change monitoring + // Get stylesheet URL from disk (project root) + Path path = Path.of(stylesheet); + URL url = path.toUri().toURL(); + WatchService watchService = FileSystems.getDefault().newWatchService(); + path.getParent().register(watchService, StandardWatchEventKinds.ENTRY_MODIFY); + + ObservableList stylesheets = stylesheetsSupplier.get(); + String stylesheetExternalForm = url.toExternalForm(); + updateWithStylesheet(stylesheetExternalForm, stylesheets); + + CompletableFuture.runAsync(() -> { + try { + performBlockingWatch(watchService, stylesheets, stylesheetExternalForm); + } catch (InterruptedException e) { + e.printStackTrace(); + Thread.currentThread().interrupt(); + } + }); + } + + private static void performBlockingWatch( + final WatchService watchService, + final ObservableList stylesheets, + final String stylesheet) throws InterruptedException { + + WatchKey key; + while ((key = watchService.take()) != null) { + for (WatchEvent event : key.pollEvents()) { + // Reload CSS in FX thread + updateWithStylesheet(stylesheet, stylesheets); + } + key.reset(); + } + } + + private static void updateWithStylesheet(final String stylesheet, final ObservableList stylesheets) { + Platform.runLater(() -> stylesheets.setAll(stylesheet)); + } +} 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 5941827..cb5bb13 100644 --- a/runtime/src/main/java/io/quarkiverse/fx/views/FxViewConfig.java +++ b/runtime/src/main/java/io/quarkiverse/fx/views/FxViewConfig.java @@ -26,4 +26,16 @@ public interface FxViewConfig { */ @WithDefault("/") String bundleRoot(); + + /** + * Location for main resources (allowing stylesheet live reload) + */ + @WithDefault("src/main/resources/") + String mainResources(); + + /** + * Location for test resources (allowing stylesheet live reload) + */ + @WithDefault("src/test/resources/") + String testResources(); } 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 ce1d6a5..e82a38d 100644 --- a/runtime/src/main/java/io/quarkiverse/fx/views/FxViewRepository.java +++ b/runtime/src/main/java/io/quarkiverse/fx/views/FxViewRepository.java @@ -3,6 +3,8 @@ import java.io.IOException; import java.io.InputStream; import java.net.URL; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.HashMap; import java.util.List; import java.util.Locale; @@ -19,6 +21,8 @@ import org.jboss.logging.Logger; import io.quarkiverse.fx.FxViewLoadEvent; +import io.quarkiverse.fx.style.StylesheetWatchService; +import io.quarkus.runtime.LaunchMode; import javafx.fxml.FXMLLoader; import javafx.scene.Parent; @@ -49,6 +53,9 @@ public void setViewNames(final List views) { */ void setupViews(@Observes final FxViewLoadEvent event) { + LaunchMode launchMode = LaunchMode.current(); + boolean devOrTest = launchMode.isDevOrTest(); + ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); for (String name : this.viewNames) { @@ -72,10 +79,21 @@ void setupViews(@Observes final FxViewLoadEvent event) { } // Style + String style = null; LOGGER.debugf("Attempting to load css %s", css); - URL styleResource = classLoader.getResource(css); - if (styleResource != null) { - LOGGER.debugf("Found css %s", css); + if (devOrTest) { + String directory = launchMode == LaunchMode.DEVELOPMENT ? this.config.mainResources() + : this.config.testResources(); + Path devPath = Paths.get(directory + css); + if (devPath.toFile().exists()) { + style = devPath.toString(); + } + } else { + URL styleResource = classLoader.getResource(css); + if (styleResource != null) { + LOGGER.debugf("Found css %s", css); + style = styleResource.toExternalForm(); + } } // FXML @@ -95,8 +113,14 @@ void setupViews(@Observes final FxViewLoadEvent event) { loader.setLocation(url); Parent rootNode = loader.load(stream); - if (styleResource != null) { - rootNode.getStylesheets().add(styleResource.toExternalForm()); + if (style != null) { + if (devOrTest) { + // Stylesheet live reload in dev mode + StylesheetWatchService.setStyleAndStartWatchingTask(rootNode::getStylesheets, style); + } else { + // Regular setting (no live reload) + rootNode.getStylesheets().add(style); + } } Object controller = loader.getController(); diff --git a/samples/fxviews/src/main/resources/style/custom-sample.css b/samples/fxviews/src/main/resources/style/custom-sample.css index 75ea497..b2e5988 100644 --- a/samples/fxviews/src/main/resources/style/custom-sample.css +++ b/samples/fxviews/src/main/resources/style/custom-sample.css @@ -1,3 +1,5 @@ Label { -fx-font-size: 2em; + -fx-font-weight: bold; + -fx-text-fill: blue; } \ No newline at end of file