Updated to version 1.5.0, with futures for navigation, expanded testing, and the navigateBackAndClear function.

This commit is contained in:
Andrew Lalis 2023-12-26 12:22:12 -05:00
parent 48e150c3c3
commit ac011b7382
9 changed files with 218 additions and 14 deletions

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.4.0</version> <version>1.5.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

@ -105,6 +105,16 @@ public class RouteHistory {
currentItemIndex = -1; 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 * Gets a list of "breadcrumbs", or a representation of the current history
* and indication of where we are in that history. * and indication of where we are in that history.
@ -140,4 +150,20 @@ public class RouteHistory {
public int getCurrentItemIndex() { public int getCurrentItemIndex() {
return currentItemIndex; 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();
}
} }

View File

@ -1,5 +1,15 @@
package com.andrewlalis.javafx_scene_router; 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 { 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); void onRouteSelected(Object context);
} }

View File

@ -12,7 +12,9 @@ import java.io.IOException;
import java.io.UncheckedIOException; import java.io.UncheckedIOException;
import java.net.URL; import java.net.URL;
import java.util.*; import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.function.Supplier;
/** /**
* A router that shows different content in a pane depending on which route is * 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 { public class SceneRouter {
private final RouterView view; private final RouterView view;
private final Map<String, Parent> routeMap = new HashMap<>(); private final Map<String, Supplier<Parent>> routeMap = new HashMap<>();
private final RouteHistory history = new RouteHistory(); private final RouteHistory history = new RouteHistory();
private final ObservableList<BreadCrumb> breadCrumbs = FXCollections.observableArrayList(); private final ObservableList<BreadCrumb> breadCrumbs = FXCollections.observableArrayList();
private final StringProperty currentRouteProperty = new SimpleStringProperty(null); private final StringProperty currentRouteProperty = new SimpleStringProperty(null);
@ -73,7 +75,7 @@ public class SceneRouter {
* @return This router. * @return This router.
*/ */
public SceneRouter map(String route, Parent node) { public SceneRouter map(String route, Parent node) {
routeMap.put(route, node); routeMap.put(route, () -> node);
return this; return this;
} }
@ -104,40 +106,85 @@ public class SceneRouter {
* Navigates to a given route, with a given context object. * Navigates to a given route, with a given context object.
* @param route The route to navigate to. * @param route The route to navigate to.
* @param context The context that should be available at that route. * @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<Void> navigate(String route, Object context) {
String oldRoute = currentRouteProperty.get(); String oldRoute = currentRouteProperty.get();
Object oldContext = history.getCurrentContext(); Object oldContext = history.getCurrentContext();
CompletableFuture<Void> cf = new CompletableFuture<>();
Platform.runLater(() -> { Platform.runLater(() -> {
history.push(route, context); history.push(route, context);
setCurrentNode(route, oldRoute, oldContext); setCurrentNode(route, oldRoute, oldContext);
cf.complete(null);
}); });
return cf;
} }
/** /**
* Navigates to a given route, without any context. * Navigates to a given route, without any context.
* @param route The route to navigate to. * @param route The route to navigate to.
* @return A completable future that completes once navigation is done.
*/ */
public void navigate(String route) { public CompletableFuture<Void> navigate(String route) {
navigate(route, null); 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<Boolean> navigateBack() {
String oldRoute = currentRouteProperty.get(); String oldRoute = currentRouteProperty.get();
Object oldContext = history.getCurrentContext(); Object oldContext = history.getCurrentContext();
Platform.runLater(() -> history.back().ifPresent(prev -> setCurrentNode(prev.route(), oldRoute, oldContext))); if (!history.canGoBack()) return CompletableFuture.completedFuture(false);
CompletableFuture<Boolean> 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.
* <p>
* For example, suppose the history looks like this:<br>
* "A" -> "B" -> "C"<br>
* 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.
* </p>
* @return True if the router will navigate back.
*/
public CompletableFuture<Boolean> navigateBackAndClear() {
return navigateBack()
.thenCompose(success -> {
if (!success) return CompletableFuture.completedFuture(false);
CompletableFuture<Boolean> cf = new CompletableFuture<>();
Platform.runLater(() -> {
history.clearForward();
cf.complete(true);
});
return cf;
});
} }
/** /**
* Attempts to navigate forward. * Attempts to navigate forward.
* @return A future that resolves to true if forward navigation was successful.
*/ */
public void navigateForward() { public CompletableFuture<Boolean> navigateForward() {
String oldRoute = currentRouteProperty.get(); String oldRoute = currentRouteProperty.get();
Object oldContext = history.getCurrentContext(); Object oldContext = history.getCurrentContext();
Platform.runLater(() -> history.forward().ifPresent(next -> setCurrentNode(next.route(), oldRoute, oldContext))); if (!history.canGoForward()) return CompletableFuture.completedFuture(false);
CompletableFuture<Boolean> 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) { 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."); if (node == null) throw new IllegalArgumentException("Route " + route + " is not mapped to any node.");
return node; return node;
} }

View File

@ -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.");
}
}

View File

@ -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 org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assertions.*;
@ -39,7 +42,7 @@ public class RouteHistoryTest {
assertTrue(history.canGoBack()); assertTrue(history.canGoBack());
var prev = history.back(); var prev = history.back();
assertTrue(prev.isPresent()); assertTrue(prev.isPresent());
assertEquals(new RouteHistoryItem("a", "a"), prev.get()); Assertions.assertEquals(new RouteHistoryItem("a", "a"), prev.get());
assertFalse(history.canGoBack()); assertFalse(history.canGoBack());
assertTrue(history.back().isEmpty()); assertTrue(history.back().isEmpty());
} }
@ -73,6 +76,22 @@ public class RouteHistoryTest {
assertEquals(-1, history.getCurrentItemIndex()); 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 @Test
public void testGetBreadCrumbs() { public void testGetBreadCrumbs() {
var h1 = new RouteHistory(); var h1 = new RouteHistory();

View File

@ -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<SceneRouter> 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();
}
}

View File

@ -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;
}

View File

@ -0,0 +1,9 @@
<?import javafx.scene.control.Label?>
<?import javafx.scene.layout.VBox?>
<VBox
xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
fx:controller="com.andrewlalis.javafx_scene_router.test.RouteAController"
>
<Label text="Hello from route A"/>
</VBox>