Updated to 1.3.0, added more listener shenanigans.

This commit is contained in:
Andrew Lalis 2023-12-24 08:31:48 -05:00
parent 5bb0b5410e
commit 18ab61be28
4 changed files with 101 additions and 20 deletions

View File

@ -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 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. route is selected, different content will be shown in that pane.
# Usage ## Usage
Add the following dependency to your `pom.xml`: Add the following dependency to your `pom.xml`:
```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.

View File

@ -6,7 +6,7 @@
<groupId>com.andrewlalis</groupId> <groupId>com.andrewlalis</groupId>
<artifactId>javafx-scene-router</artifactId> <artifactId>javafx-scene-router</artifactId>
<version>1.2.0</version> <version>1.3.0</version>
<name>JavaFX Scene Router</name> <name>JavaFX Scene Router</name>
<description>A library that provides a router implementation for JavaFX, for browser-like navigation between pages.</description> <description>A library that provides a router implementation for JavaFX, for browser-like navigation between pages.</description>
<url>https://github.com/andrewlalis/javafx-scene-router</url> <url>https://github.com/andrewlalis/javafx-scene-router</url>

View File

@ -0,0 +1,5 @@
package com.andrewlalis.javafx_scene_router;
public interface RouteSelectionListener {
void onRouteSelected(Object context);
}

View File

@ -1,8 +1,9 @@
package com.andrewlalis.javafx_scene_router; package com.andrewlalis.javafx_scene_router;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.beans.property.ListProperty; import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.SimpleListProperty; import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList; import javafx.collections.ObservableList;
import javafx.fxml.FXMLLoader; import javafx.fxml.FXMLLoader;
import javafx.scene.Parent; import javafx.scene.Parent;
@ -11,8 +12,7 @@ import javafx.scene.layout.Pane;
import java.io.IOException; import java.io.IOException;
import java.io.UncheckedIOException; import java.io.UncheckedIOException;
import java.net.URL; import java.net.URL;
import java.util.HashMap; import java.util.*;
import java.util.Map;
import java.util.function.Consumer; import java.util.function.Consumer;
/** /**
@ -34,10 +34,18 @@ import java.util.function.Consumer;
* </p> * </p>
*/ */
public class SceneRouter { public class SceneRouter {
public interface RouteChangeListener {
void routeChanged(String route, Object context, String oldRoute, Object oldContext);
}
private final Pane viewPane = new Pane(); private final Pane viewPane = new Pane();
private final Map<String, Parent> routeMap = new HashMap<>(); private final Map<String, Parent> routeMap = new HashMap<>();
private final RouteHistory history = new RouteHistory(); private final RouteHistory history = new RouteHistory();
private final ListProperty<BreadCrumb> breadCrumbs = new SimpleListProperty<>(); private final ObservableList<BreadCrumb> breadCrumbs = FXCollections.observableArrayList();
private final StringProperty currentRouteProperty = new SimpleStringProperty(null);
private final List<RouteChangeListener> routeChangeListeners = new ArrayList<>();
private final Map<String, List<RouteSelectionListener>> routeSelectionListeners = new HashMap<>();
/** /**
* Constructs the router. * 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 * Maps the given route to a node, so that when the route is selected, the
* given node is shown. * given node is shown.
* <p>
* 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.
* </p>
* @param route The route. * @param route The route.
* @param node The node to show. * @param node The node to show.
* @return This router. * @return This router.
@ -66,7 +82,7 @@ public class SceneRouter {
* @return This router. * @return This router.
*/ */
public SceneRouter map(String route, URL fxml, Consumer<?> controllerCustomizer) { 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. * @param context The context that should be available at that route.
*/ */
public void navigate(String route, Object context) { public void navigate(String route, Object context) {
String oldRoute = currentRouteProperty.get();
Object oldContext = history.getCurrentContext();
Platform.runLater(() -> { Platform.runLater(() -> {
history.push(route, context); history.push(route, context);
setCurrentNode(getMappedNode(route)); setCurrentNode(route, oldRoute, oldContext);
}); });
} }
@ -103,18 +121,18 @@ public class SceneRouter {
* Attempts to navigate back. * Attempts to navigate back.
*/ */
public void navigateBack() { public void navigateBack() {
Platform.runLater(() -> history.back() String oldRoute = currentRouteProperty.get();
.ifPresent(prev -> setCurrentNode(getMappedNode(prev.route()))) Object oldContext = history.getCurrentContext();
); Platform.runLater(() -> history.back().ifPresent(prev -> setCurrentNode(prev.route(), oldRoute, oldContext)));
} }
/** /**
* Attempts to navigate forward. * Attempts to navigate forward.
*/ */
public void navigateForward() { public void navigateForward() {
Platform.runLater(() -> history.forward() String oldRoute = currentRouteProperty.get();
.ifPresent(next -> setCurrentNode(getMappedNode(next.route()))) Object oldContext = history.getCurrentContext();
); Platform.runLater(() -> history.forward().ifPresent(next -> setCurrentNode(next.route(), oldRoute, oldContext)));
} }
/** /**
@ -143,6 +161,14 @@ public class SceneRouter {
return viewPane; 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 * Gets an observable list of {@link BreadCrumb} that is updated each time
* the router's navigation history is updated. * the router's navigation history is updated.
@ -152,6 +178,25 @@ public class SceneRouter {
return breadCrumbs; 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<RouteSelectionListener> listenerList = routeSelectionListeners.computeIfAbsent(route, s -> new ArrayList<>());
listenerList.add(listener);
}
private Parent getMappedNode(String route) { private Parent getMappedNode(String route) {
Parent node = routeMap.get(route); Parent node = routeMap.get(route);
if (node == null) throw new IllegalArgumentException("Route " + route + " is not mapped to any node."); 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 * Internal method to actually set this router's view pane to a particular
* node. This is called any time a route changes. * 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) { private void setCurrentNode(String route, String oldRoute, Object oldContext) {
viewPane.getChildren().setAll(node); viewPane.getChildren().setAll(getMappedNode(route));
breadCrumbs.setAll(history.getBreadCrumbs()); 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 <T> Parent loadNode(URL resource, Consumer<T> controllerCustomizer) { private <T> Parent loadNode(String route, URL resource, Consumer<T> controllerCustomizer) {
FXMLLoader loader = new FXMLLoader(resource); FXMLLoader loader = new FXMLLoader(resource);
try { try {
Parent p = loader.load(); Parent p = loader.load();
T controller = loader.getController();
if (controller instanceof RouteSelectionListener rsl) {
addRouteSelectionListener(route, rsl);
}
if (controllerCustomizer != null) { if (controllerCustomizer != null) {
T controller = loader.getController();
if (controller == null) throw new IllegalStateException("No controller found when loading " + resource.toString()); if (controller == null) throw new IllegalStateException("No controller found when loading " + resource.toString());
controllerCustomizer.accept(controller); controllerCustomizer.accept(controller);
} }