diff --git a/pom.xml b/pom.xml index 544001f..392122b 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.andrewlalis javafx-scene-router - 1.4.0 + 1.5.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/RouteHistory.java b/src/main/java/com/andrewlalis/javafx_scene_router/RouteHistory.java index e9cc2b9..82cffbb 100644 --- a/src/main/java/com/andrewlalis/javafx_scene_router/RouteHistory.java +++ b/src/main/java/com/andrewlalis/javafx_scene_router/RouteHistory.java @@ -105,6 +105,16 @@ public class RouteHistory { currentItemIndex = -1; } + /** + * Clears any history ahead of the current route item, such that the user + * cannot navigate forward. + */ + public void clearForward() { + if (currentItemIndex + 1 < items.size()) { + items.subList(currentItemIndex + 1, items.size()).clear(); + } + } + /** * Gets a list of "breadcrumbs", or a representation of the current history * and indication of where we are in that history. @@ -140,4 +150,20 @@ public class RouteHistory { public int getCurrentItemIndex() { return currentItemIndex; } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder("RouteHistory:\n"); + for (int i = 0; i < items.size(); i++) { + var item = items.get(i); + sb.append(String.format("%4d route = \"%s\", context = %s", i, item.route(), item.context())); + if (i == currentItemIndex) { + sb.append(" <--- Current Item"); + } + if (i + 1 < items.size()) { + sb.append(String.format("%n")); + } + } + return sb.toString(); + } } diff --git a/src/main/java/com/andrewlalis/javafx_scene_router/RouteSelectionListener.java b/src/main/java/com/andrewlalis/javafx_scene_router/RouteSelectionListener.java index 34d2a37..6a33e6a 100644 --- a/src/main/java/com/andrewlalis/javafx_scene_router/RouteSelectionListener.java +++ b/src/main/java/com/andrewlalis/javafx_scene_router/RouteSelectionListener.java @@ -1,5 +1,15 @@ package com.andrewlalis.javafx_scene_router; +/** + * A listener that's notified when the router it's attached to navigates to + * a specific, pre-defined route. Usually used to do something once the user + * has navigated to a route. + */ public interface RouteSelectionListener { + /** + * Called when a specific, pre-defined route is selected. + * @param context The context that was provided when the user navigated to + * the route. This may be null. + */ 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 9b91006..421476d 100644 --- a/src/main/java/com/andrewlalis/javafx_scene_router/SceneRouter.java +++ b/src/main/java/com/andrewlalis/javafx_scene_router/SceneRouter.java @@ -12,7 +12,9 @@ import java.io.IOException; import java.io.UncheckedIOException; import java.net.URL; import java.util.*; +import java.util.concurrent.CompletableFuture; import java.util.function.Consumer; +import java.util.function.Supplier; /** * A router that shows different content in a pane depending on which route is @@ -34,7 +36,7 @@ import java.util.function.Consumer; */ public class SceneRouter { private final RouterView view; - private final Map routeMap = new HashMap<>(); + private final Map> routeMap = new HashMap<>(); private final RouteHistory history = new RouteHistory(); private final ObservableList breadCrumbs = FXCollections.observableArrayList(); private final StringProperty currentRouteProperty = new SimpleStringProperty(null); @@ -73,7 +75,7 @@ public class SceneRouter { * @return This router. */ public SceneRouter map(String route, Parent node) { - routeMap.put(route, node); + routeMap.put(route, () -> node); return this; } @@ -104,40 +106,85 @@ public class SceneRouter { * Navigates to a given route, with a given context object. * @param route The route to navigate to. * @param context The context that should be available at that route. + * @return A completable future that completes once navigation is done. */ - public void navigate(String route, Object context) { + public CompletableFuture navigate(String route, Object context) { String oldRoute = currentRouteProperty.get(); Object oldContext = history.getCurrentContext(); + CompletableFuture cf = new CompletableFuture<>(); Platform.runLater(() -> { history.push(route, context); setCurrentNode(route, oldRoute, oldContext); + cf.complete(null); }); + return cf; } /** * Navigates to a given route, without any context. * @param route The route to navigate to. + * @return A completable future that completes once navigation is done. */ - public void navigate(String route) { - navigate(route, null); + public CompletableFuture navigate(String route) { + return navigate(route, null); } /** - * Attempts to navigate back. + * Attempts to navigate back to the previous route. + * @return True if the router will navigate back. */ - public void navigateBack() { + public CompletableFuture navigateBack() { String oldRoute = currentRouteProperty.get(); Object oldContext = history.getCurrentContext(); - Platform.runLater(() -> history.back().ifPresent(prev -> setCurrentNode(prev.route(), oldRoute, oldContext))); + if (!history.canGoBack()) return CompletableFuture.completedFuture(false); + CompletableFuture cf = new CompletableFuture<>(); + Platform.runLater(() -> { + RouteHistoryItem prev = history.back().orElseThrow(); + setCurrentNode(prev.route(), oldRoute, oldContext); + cf.complete(true); + }); + return cf; + } + + /** + * Attempts to navigate back to the previous route, and then erase all + * forward route history. + *

+ * For example, suppose the history looks like this:
+ * "A" -> "B" -> "C"
+ * where the router is currently at C. Then, if this method is called, + * the router will go back to B, and remove C from the history. + *

+ * @return True if the router will navigate back. + */ + public CompletableFuture navigateBackAndClear() { + return navigateBack() + .thenCompose(success -> { + if (!success) return CompletableFuture.completedFuture(false); + CompletableFuture cf = new CompletableFuture<>(); + Platform.runLater(() -> { + history.clearForward(); + cf.complete(true); + }); + return cf; + }); } /** * Attempts to navigate forward. + * @return A future that resolves to true if forward navigation was successful. */ - public void navigateForward() { + public CompletableFuture navigateForward() { String oldRoute = currentRouteProperty.get(); Object oldContext = history.getCurrentContext(); - Platform.runLater(() -> history.forward().ifPresent(next -> setCurrentNode(next.route(), oldRoute, oldContext))); + if (!history.canGoForward()) return CompletableFuture.completedFuture(false); + CompletableFuture cf = new CompletableFuture<>(); + Platform.runLater(() -> { + RouteHistoryItem next = history.forward().orElseThrow(); + setCurrentNode(next.route(), oldRoute, oldContext); + cf.complete(true); + }); + return cf; } /** @@ -202,7 +249,7 @@ public class SceneRouter { } private Parent getMappedNode(String route) { - Parent node = routeMap.get(route); + Parent node = routeMap.get(route).get(); if (node == null) throw new IllegalArgumentException("Route " + route + " is not mapped to any node."); return node; } diff --git a/src/test/java/com/andrewlalis/javafx_scene_router/test/RouteAController.java b/src/test/java/com/andrewlalis/javafx_scene_router/test/RouteAController.java new file mode 100644 index 0000000..f70c08a --- /dev/null +++ b/src/test/java/com/andrewlalis/javafx_scene_router/test/RouteAController.java @@ -0,0 +1,13 @@ +package com.andrewlalis.javafx_scene_router.test; + +import com.andrewlalis.javafx_scene_router.RouteSelectionListener; + +public class RouteAController implements RouteSelectionListener { + public int routeSelectedCount = 0; + + @Override + public void onRouteSelected(Object context) { + routeSelectedCount++; + System.out.println("Route A selected."); + } +} diff --git a/src/test/java/com/andrewlalis/javafx_scene_router/RouteHistoryTest.java b/src/test/java/com/andrewlalis/javafx_scene_router/test/RouteHistoryTest.java similarity index 76% rename from src/test/java/com/andrewlalis/javafx_scene_router/RouteHistoryTest.java rename to src/test/java/com/andrewlalis/javafx_scene_router/test/RouteHistoryTest.java index 38d409d..642f51e 100644 --- a/src/test/java/com/andrewlalis/javafx_scene_router/RouteHistoryTest.java +++ b/src/test/java/com/andrewlalis/javafx_scene_router/test/RouteHistoryTest.java @@ -1,5 +1,8 @@ -package com.andrewlalis.javafx_scene_router; +package com.andrewlalis.javafx_scene_router.test; +import com.andrewlalis.javafx_scene_router.RouteHistory; +import com.andrewlalis.javafx_scene_router.RouteHistoryItem; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; @@ -39,7 +42,7 @@ public class RouteHistoryTest { assertTrue(history.canGoBack()); var prev = history.back(); assertTrue(prev.isPresent()); - assertEquals(new RouteHistoryItem("a", "a"), prev.get()); + Assertions.assertEquals(new RouteHistoryItem("a", "a"), prev.get()); assertFalse(history.canGoBack()); assertTrue(history.back().isEmpty()); } @@ -73,6 +76,22 @@ public class RouteHistoryTest { assertEquals(-1, history.getCurrentItemIndex()); } + @Test + public void testClearForward() { + var history = new RouteHistory(); + history.push("a", "a"); + history.push("b", "b"); + history.push("c", "c"); + assertEquals(3, history.getItems().size()); + assertEquals(2, history.getCurrentItemIndex()); + history.back(); + assertEquals(3, history.getItems().size()); + assertEquals(1, history.getCurrentItemIndex()); + history.clearForward(); + assertEquals(1, history.getCurrentItemIndex()); + assertEquals(2, history.getItems().size()); + } + @Test public void testGetBreadCrumbs() { var h1 = new RouteHistory(); diff --git a/src/test/java/com/andrewlalis/javafx_scene_router/test/SceneRouterTest.java b/src/test/java/com/andrewlalis/javafx_scene_router/test/SceneRouterTest.java new file mode 100644 index 0000000..8da27d8 --- /dev/null +++ b/src/test/java/com/andrewlalis/javafx_scene_router/test/SceneRouterTest.java @@ -0,0 +1,69 @@ +package com.andrewlalis.javafx_scene_router.test; + +import com.andrewlalis.javafx_scene_router.AnchorPaneRouterView; +import com.andrewlalis.javafx_scene_router.SceneRouter; +import javafx.application.Platform; +import javafx.scene.control.Label; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.HBox; +import org.junit.jupiter.api.Test; + +import java.util.concurrent.CompletableFuture; + +import static org.junit.jupiter.api.Assertions.*; + +public class SceneRouterTest { + @Test + public void testNavigate() { + var router = getSampleRouter(); + // Make some assertions prior to navigation. + assertTrue(router.getHistory().getItems().isEmpty()); + assertFalse(router.navigateBack().join()); + assertFalse(router.navigateForward().join()); + assertNull(router.currentRouteProperty().get()); + + // Test some basic navigation. + var contextA = "CONTEXT"; + router.navigate("A", contextA).join(); + assertEquals(1, router.getHistory().getItems().size()); + assertEquals("A", router.currentRouteProperty().get()); + assertEquals(contextA, router.getContext()); + assertEquals(1, router.getBreadCrumbs().size()); + assertTrue(router.getBreadCrumbs().getFirst().current()); + + router.navigate("B").join(); + assertEquals(2, router.getHistory().getItems().size()); + assertEquals("B", router.currentRouteProperty().get()); + assertNull(router.getContext()); + assertEquals(2, router.getBreadCrumbs().size()); + assertTrue(router.getBreadCrumbs().getLast().current()); + assertFalse(router.getBreadCrumbs().getFirst().current()); + + // Test that navigating back and forward works. + assertTrue(router.navigateBack().join()); + assertEquals("A", router.currentRouteProperty().get()); + assertEquals(contextA, router.getContext()); + + assertTrue(router.navigateForward().join()); + assertEquals("B", router.currentRouteProperty().get()); + assertNull(router.getContext()); + assertFalse(router.navigateForward().join()); + + // Test that navigateBackAndClear works. + assertTrue(router.navigateBackAndClear().join()); + assertEquals("A", router.currentRouteProperty().get()); + assertEquals(1, router.getHistory().getItems().size()); + } + + private SceneRouter getSampleRouter() { + CompletableFuture future = new CompletableFuture<>(); + Platform.startup(() -> { + SceneRouter router = new SceneRouter(new AnchorPaneRouterView()); + router.map("A", SceneRouterTest.class.getResource("/routeA.fxml")); + router.map("B", new BorderPane(new Label("Hello from route B"))); + router.map("C", new HBox(new Label("Hello from route C"))); + future.complete(router); + }); + return future.join(); + } +} diff --git a/src/test/java/module-info.java b/src/test/java/module-info.java new file mode 100644 index 0000000..ddb8ecc --- /dev/null +++ b/src/test/java/module-info.java @@ -0,0 +1,11 @@ +module com.andrewlalis.javafx_scene_router.test { + requires javafx.fxml; + requires javafx.graphics; + requires javafx.controls; + + requires com.andrewlalis.javafx_scene_router; + + requires org.junit.jupiter.api; + + exports com.andrewlalis.javafx_scene_router.test to javafx.fxml, org.junit.platform.commons; +} \ No newline at end of file diff --git a/src/test/resources/routeA.fxml b/src/test/resources/routeA.fxml new file mode 100644 index 0000000..bfa4cab --- /dev/null +++ b/src/test/resources/routeA.fxml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file