Updated to version 1.5.0, with futures for navigation, expanded testing, and the navigateBackAndClear function.
This commit is contained in:
parent
48e150c3c3
commit
ac011b7382
2
pom.xml
2
pom.xml
|
@ -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>
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.");
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -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>
|
Loading…
Reference in New Issue