diff --git a/README.md b/README.md index ad53de0..83f5dc5 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ for that purpose, I've created javafx-scene-router. It allows you to initialize a router that controls the content of a Pane or similar, and depending on what route is selected, different content will be shown in that pane. -# Usage +## Usage Add the following dependency to your `pom.xml`: ```xml @@ -83,3 +83,22 @@ public class MainController { } } ``` + +## Reactivity + +The SceneRouter has been designed to be used in reactive JavaFX projects, and +includes a few ways of doing this: + +- The `currentRouteProperty` can be bound to, or have a listener attached, to +update each time the current route changes. +- You can `getBreadCrumbs()` to get an observable list of breadcrumbs that +changes each time the route's history changes. +- You can use `addRouteChangeListener` to add a listener that's notified each +time the route has changed, with additional context and the previous route. +- You can use `addRouteSelectionListener` to add a listener for a specific +route, that will be notified only when the router selects that route. +- You can make any of your controllers for route nodes implement `RouteSelectionListener`, +in which case they'll automatically be registered using `addRouteSelectionListener`. +Note that this **does not** apply to routes mapped using a pre-loaded node, as +in the `map(String route, Parent node)` method. Only methods which take a `URL` +to a resource work with this. diff --git a/pom.xml b/pom.xml index b1d4388..c2b16d9 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.andrewlalis javafx-scene-router - 1.2.0 + 1.3.0 JavaFX Scene Router A library that provides a router implementation for JavaFX, for browser-like navigation between pages. https://github.com/andrewlalis/javafx-scene-router diff --git a/src/main/java/com/andrewlalis/javafx_scene_router/RouteSelectionListener.java b/src/main/java/com/andrewlalis/javafx_scene_router/RouteSelectionListener.java new file mode 100644 index 0000000..34d2a37 --- /dev/null +++ b/src/main/java/com/andrewlalis/javafx_scene_router/RouteSelectionListener.java @@ -0,0 +1,5 @@ +package com.andrewlalis.javafx_scene_router; + +public interface RouteSelectionListener { + void onRouteSelected(Object context); +} diff --git a/src/main/java/com/andrewlalis/javafx_scene_router/SceneRouter.java b/src/main/java/com/andrewlalis/javafx_scene_router/SceneRouter.java index fd4a2d8..ed64432 100644 --- a/src/main/java/com/andrewlalis/javafx_scene_router/SceneRouter.java +++ b/src/main/java/com/andrewlalis/javafx_scene_router/SceneRouter.java @@ -1,8 +1,9 @@ package com.andrewlalis.javafx_scene_router; import javafx.application.Platform; -import javafx.beans.property.ListProperty; -import javafx.beans.property.SimpleListProperty; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; +import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.fxml.FXMLLoader; import javafx.scene.Parent; @@ -11,8 +12,7 @@ import javafx.scene.layout.Pane; import java.io.IOException; import java.io.UncheckedIOException; import java.net.URL; -import java.util.HashMap; -import java.util.Map; +import java.util.*; import java.util.function.Consumer; /** @@ -34,10 +34,18 @@ import java.util.function.Consumer; *

*/ public class SceneRouter { + public interface RouteChangeListener { + void routeChanged(String route, Object context, String oldRoute, Object oldContext); + } + private final Pane viewPane = new Pane(); private final Map routeMap = new HashMap<>(); private final RouteHistory history = new RouteHistory(); - private final ListProperty breadCrumbs = new SimpleListProperty<>(); + private final ObservableList breadCrumbs = FXCollections.observableArrayList(); + private final StringProperty currentRouteProperty = new SimpleStringProperty(null); + + private final List routeChangeListeners = new ArrayList<>(); + private final Map> routeSelectionListeners = new HashMap<>(); /** * Constructs the router. @@ -47,6 +55,14 @@ public class SceneRouter { /** * Maps the given route to a node, so that when the route is selected, the * given node is shown. + *

+ * Note that by supplying a pre-loaded JavaFX node, the SceneRouter is + * no longer able to check if the node's controller implements + * {@link RouteSelectionListener}, and so you'll need to register the + * controller manually with {@link #addRouteSelectionListener(String, RouteSelectionListener)} + * in order to have the controller be notified when its contents are + * shown. + *

* @param route The route. * @param node The node to show. * @return This router. @@ -66,7 +82,7 @@ public class SceneRouter { * @return This router. */ public SceneRouter map(String route, URL fxml, Consumer controllerCustomizer) { - return map(route, loadNode(fxml, controllerCustomizer)); + return map(route, loadNode(route, fxml, controllerCustomizer)); } /** @@ -85,9 +101,11 @@ public class SceneRouter { * @param context The context that should be available at that route. */ public void navigate(String route, Object context) { + String oldRoute = currentRouteProperty.get(); + Object oldContext = history.getCurrentContext(); Platform.runLater(() -> { history.push(route, context); - setCurrentNode(getMappedNode(route)); + setCurrentNode(route, oldRoute, oldContext); }); } @@ -103,18 +121,18 @@ public class SceneRouter { * Attempts to navigate back. */ public void navigateBack() { - Platform.runLater(() -> history.back() - .ifPresent(prev -> setCurrentNode(getMappedNode(prev.route()))) - ); + String oldRoute = currentRouteProperty.get(); + Object oldContext = history.getCurrentContext(); + Platform.runLater(() -> history.back().ifPresent(prev -> setCurrentNode(prev.route(), oldRoute, oldContext))); } /** * Attempts to navigate forward. */ public void navigateForward() { - Platform.runLater(() -> history.forward() - .ifPresent(next -> setCurrentNode(getMappedNode(next.route()))) - ); + String oldRoute = currentRouteProperty.get(); + Object oldContext = history.getCurrentContext(); + Platform.runLater(() -> history.forward().ifPresent(next -> setCurrentNode(next.route(), oldRoute, oldContext))); } /** @@ -143,6 +161,14 @@ public class SceneRouter { return viewPane; } + /** + * Gets a property that refers to the router's current route. + * @return The route property. + */ + public StringProperty currentRouteProperty() { + return currentRouteProperty; + } + /** * Gets an observable list of {@link BreadCrumb} that is updated each time * the router's navigation history is updated. @@ -152,6 +178,25 @@ public class SceneRouter { return breadCrumbs; } + /** + * Adds a listener that will be notified each time the current route changes. + * @param listener The listener that will be notified. + */ + public void addRouteChangeListener(RouteChangeListener listener) { + routeChangeListeners.add(listener); + } + + /** + * Adds a listener that will be notified when the route changes to a + * specified route. + * @param route The route to listen for. + * @param listener The listener to use. + */ + public void addRouteSelectionListener(String route, RouteSelectionListener listener) { + List listenerList = routeSelectionListeners.computeIfAbsent(route, s -> new ArrayList<>()); + listenerList.add(listener); + } + private Parent getMappedNode(String route) { Parent node = routeMap.get(route); if (node == null) throw new IllegalArgumentException("Route " + route + " is not mapped to any node."); @@ -161,19 +206,31 @@ public class SceneRouter { /** * Internal method to actually set this router's view pane to a particular * node. This is called any time a route changes. - * @param node The node to set. + * @param route The route to go to. + * @param oldRoute The previous route that the router was at. + * @param oldContext The context of the previous route. */ - private void setCurrentNode(Parent node) { - viewPane.getChildren().setAll(node); + private void setCurrentNode(String route, String oldRoute, Object oldContext) { + viewPane.getChildren().setAll(getMappedNode(route)); breadCrumbs.setAll(history.getBreadCrumbs()); + currentRouteProperty.set(route); + for (var listener : routeChangeListeners) { + listener.routeChanged(route, getContext(), oldRoute, oldContext); + } + for (var listener : routeSelectionListeners.getOrDefault(route, Collections.emptyList())) { + listener.onRouteSelected(getContext()); + } } - private Parent loadNode(URL resource, Consumer controllerCustomizer) { + private Parent loadNode(String route, URL resource, Consumer controllerCustomizer) { FXMLLoader loader = new FXMLLoader(resource); try { Parent p = loader.load(); + T controller = loader.getController(); + if (controller instanceof RouteSelectionListener rsl) { + addRouteSelectionListener(route, rsl); + } if (controllerCustomizer != null) { - T controller = loader.getController(); if (controller == null) throw new IllegalStateException("No controller found when loading " + resource.toString()); controllerCustomizer.accept(controller); }