diff --git a/pom.xml b/pom.xml
index 544001f..392122b 100644
--- a/pom.xml
+++ b/pom.xml
@@ -6,7 +6,7 @@
com.andrewlalisjavafx-scene-router
- 1.4.0
+ 1.5.0JavaFX Scene RouterA 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