diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..acb7754 --- /dev/null +++ b/.gitignore @@ -0,0 +1,39 @@ +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr +.idea + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..7c01dbe --- /dev/null +++ b/pom.xml @@ -0,0 +1,151 @@ + + + 4.0.0 + + com.andrewlalis + javafx-scene-router + 0.0.1-SNAPSHOT + 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 + + + 21 + 21 + UTF-8 + 21.0.1 + + + + + org.openjfx + javafx-controls + ${javafx.version} + + + org.openjfx + javafx-fxml + ${javafx.version} + + + + + + + org.apache.maven.plugins + maven-source-plugin + 3.2.1 + + + attach-sources + + jar-no-fork + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.3.1 + + + attach-javadocs + + jar + + + + + + org.apache.maven.plugins + maven-gpg-plugin + 3.1.0 + + + sign-artifacts + verify + + sign + + + + + + net.ju-n.maven.plugins + checksum-maven-plugin + 1.4 + + + checksum-maven-plugin-files + verify + + files + + + + + + + ${project.build.directory} + + *.pom + *.jar + + + + + SHA-1 + MD5 + SHA-256 + SHA-512 + + + + + org.sonatype.plugins + nexus-staging-maven-plugin + 1.6.13 + true + + ossrh + https://s01.oss.sonatype.org + false + + + + + + + https://github.com/andrewlalis/javafx-scene-router + scm:git:git://github.com/andrewlalis/javafx-scene-router.git + scm:git:ssh://github.com/andrewlalis/javafx-scene-router.git + + + + + MIT License + https://www.opensource.org/licenses/mit-license.php + + + + + + Andrew Lalis + andrewlalisofficial@gmail.com + + + + + + ossrh + https://s01.oss.sonatype.org/content/repositories/snapshots + + + ossrh + https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/ + + + \ No newline at end of file diff --git a/src/main/java/com/andrewlalis/javafx_scene_router/BreadCrumb.java b/src/main/java/com/andrewlalis/javafx_scene_router/BreadCrumb.java new file mode 100644 index 0000000..1dc7b26 --- /dev/null +++ b/src/main/java/com/andrewlalis/javafx_scene_router/BreadCrumb.java @@ -0,0 +1,10 @@ +package com.andrewlalis.javafx_scene_router; + +/** + * A breadcrumb entry that represents one item in a route history. + * @param label The display label. + * @param route The route. + * @param context The context object for this route. + * @param current Whether the history this was generated from is at this route right now. + */ +public record BreadCrumb(String label, String route, Object context, boolean current) {} diff --git a/src/main/java/com/andrewlalis/javafx_scene_router/RouteHistory.java b/src/main/java/com/andrewlalis/javafx_scene_router/RouteHistory.java new file mode 100644 index 0000000..63bb612 --- /dev/null +++ b/src/main/java/com/andrewlalis/javafx_scene_router/RouteHistory.java @@ -0,0 +1,127 @@ +package com.andrewlalis.javafx_scene_router; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +/** + * A component that tracks navigation history through a series of routes, and + * provides facilities for navigating forward and backward through the history, + * as well as pushing new routes into the history. + *

+ * This history is designed to work much like a typical web browser, where + * a linear history is maintained, such that you can move backward and + * forward along the history, but once you navigate to a new page, any + * forward-history is cleared. + *

+ */ +public class RouteHistory { + private final List items = new ArrayList<>(); + private int currentItemIndex = -1; + + /** + * Constructs a new history instance. + */ + public RouteHistory() {} + + /** + * Pushes a new route after the current place in the history, and clears + * all forward-routes beyond that. + * @param route The route to push. + * @param context The context object associated with the route. + */ + public void push(String route, Object context) { + int nextIndex = currentItemIndex + 1; + items.subList(nextIndex, items.size()).clear(); + items.add(nextIndex, new RouteHistoryItem(route, context)); + currentItemIndex = nextIndex; + } + + /** + * Gets the current context object, or null if none is set. + * @return The context object associated with the current route. + * @param The type to implicitly cast to. Note that this may result in + * an unchecked exception if you attempt to coerce to an invalid + * type. + */ + @SuppressWarnings("unchecked") + public T getCurrentContext() { + if (currentItemIndex >= 0 && currentItemIndex < items.size()) { + return (T) items.get(currentItemIndex).context(); + } + return null; + } + + /** + * Checks if it's possible to navigate back in the history. + * @return True if it is possible to go back. + */ + public boolean canGoBack() { + return currentItemIndex > 0; + } + + /** + * Attempts to go back in the history. + * @return If successful, the previous history item that we went back to; + * empty otherwise. + */ + public Optional back() { + if (canGoBack()) { + RouteHistoryItem prev = items.get(currentItemIndex - 1); + currentItemIndex--; + return Optional.of(prev); + } + return Optional.empty(); + } + + /** + * Checks if it's possible to navigate forward in the history. + * @return True if it is possible to go forward. + */ + public boolean canGoForward() { + return currentItemIndex + 1 < items.size(); + } + + /** + * Attempts to go forward in the history. + * @return If successful, the next history item that we went forward to; + * empty otherwise. + */ + public Optional forward() { + if (canGoForward()) { + RouteHistoryItem next = items.get(currentItemIndex + 1); + currentItemIndex++; + return Optional.of(next); + } + return Optional.empty(); + } + + /** + * Clears the history completely. + */ + public void clear() { + items.clear(); + currentItemIndex = -1; + } + + /** + * Gets a list of "breadcrumbs", or a representation of the current history + * and indication of where we are in that history. + * @return The list of breadcrumbs. + */ + public List getBreadCrumbs() { + if (items.isEmpty()) return Collections.emptyList(); + List breadCrumbs = new ArrayList<>(items.size()); + for (int i = 0; i < items.size(); i++) { + var item = items.get(i); + breadCrumbs.add(new BreadCrumb( + item.route(), + item.route(), + item.context(), + i == currentItemIndex + )); + } + return breadCrumbs; + } +} diff --git a/src/main/java/com/andrewlalis/javafx_scene_router/RouteHistoryItem.java b/src/main/java/com/andrewlalis/javafx_scene_router/RouteHistoryItem.java new file mode 100644 index 0000000..6a3ce9b --- /dev/null +++ b/src/main/java/com/andrewlalis/javafx_scene_router/RouteHistoryItem.java @@ -0,0 +1,8 @@ +package com.andrewlalis.javafx_scene_router; + +/** + * An entry that stores information about a point in a user's route history. + * @param route The route. + * @param context The context object associated with the route. + */ +record RouteHistoryItem(String route, 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 new file mode 100644 index 0000000..5cb30c7 --- /dev/null +++ b/src/main/java/com/andrewlalis/javafx_scene_router/SceneRouter.java @@ -0,0 +1,163 @@ +package com.andrewlalis.javafx_scene_router; + +import javafx.application.Platform; +import javafx.fxml.FXMLLoader; +import javafx.scene.Parent; +import javafx.scene.layout.Pane; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Consumer; + +/** + * A router that shows different content in a pane depending on which route is + * selected. Each router must be initialized with a JavaFX pane, or a consumer + * function that's called to set the content each time a new route is selected. + *

+ * The router has a mapping of "routes" (think, Strings) to JavaFX Parent + * nodes. When a route is selected, the router will lookup the mapped node, + * and put that node into the pre-defined pane or consumer function. + *

+ *

+ * The router maintains a {@link RouteHistory} so that it's possible to + * navigate backward and forward, much like a web browser would. + *

+ */ +public class SceneRouter { + private final Consumer setter; + private final Map routeMap = new HashMap<>(); + private final RouteHistory history = new RouteHistory(); + + /** + * Constructs the router to show route content in the given pane. + * @param pane The pane to show route content in. + */ + public SceneRouter(Pane pane) { + this(p -> pane.getChildren().setAll(p)); + } + + /** + * Constructs the router to supply route content to the given consumer, so + * that it may place the content somewhere. For example, you might like to + * use this if you'd like to have a router place content in the center of a + * {@link javafx.scene.layout.BorderPane}, like so: + *

var router = new SceneRouter(myBorderPane::setCenter);

+ * @param setter The consumer that is supplied route content to show. + */ + public SceneRouter(Consumer setter) { + this.setter = setter; + } + + /** + * Maps the given route to a node, so that when the route is selected, the + * given node is shown. + * @param route The route. + * @param node The node to show. + * @return This router. + */ + public SceneRouter map(String route, Parent node) { + routeMap.put(route, node); + return this; + } + + /** + * Maps the given route to a node that is loaded from a given FXML resource. + * @param route The route. + * @param fxml The FXML classpath resource to load from. + * @param controllerCustomizer A function that takes controller instance + * from the loaded FXML and customizes it. This + * may be null. + * @return This router. + */ + public SceneRouter map(String route, String fxml, Consumer controllerCustomizer) { + return map(route, loadNode(fxml, controllerCustomizer)); + } + + /** + * Maps the given route to a node that is loaded from a given FXML resource. + * @param route The route. + * @param fxml The FXML classpath resource to load from. + * @return This router. + */ + public SceneRouter map(String route, String fxml) { + return map(route, fxml, null); + } + + /** + * 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. + */ + public void navigate(String route, Object context) { + Platform.runLater(() -> { + history.push(route, context); + setter.accept(getMappedNode(route)); + }); + } + + /** + * Navigates to a given route, without any context. + * @param route The route to navigate to. + */ + public void navigate(String route) { + navigate(route, null); + } + + /** + * Attempts to navigate back. + */ + public void navigateBack() { + Platform.runLater(() -> history.back() + .ifPresent(prev -> setter.accept(getMappedNode(prev.route()))) + ); + } + + /** + * Attempts to navigate forward. + */ + public void navigateForward() { + Platform.runLater(() -> history.forward() + .ifPresent(next -> setter.accept(getMappedNode(next.route()))) + ); + } + + /** + * Gets the context object for the current route. + * @return The context object, or null. + * @param The type of the object. + */ + public T getContext() { + return history.getCurrentContext(); + } + + /** + * Gets the internal history representation of this router. + * @return The history used by this router. + */ + public RouteHistory getHistory() { + return history; + } + + private Parent getMappedNode(String route) { + Parent node = routeMap.get(route); + if (node == null) throw new IllegalArgumentException("Route " + route + " is not mapped to any node."); + return node; + } + + private Parent loadNode(String fxml, Consumer controllerCustomizer) { + FXMLLoader loader = new FXMLLoader(SceneRouter.class.getResource(fxml)); + try { + Parent p = loader.load(); + if (controllerCustomizer != null) { + T controller = loader.getController(); + if (controller == null) throw new IllegalStateException("No controller found when loading " + fxml); + controllerCustomizer.accept(controller); + } + return p; + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } +} diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java new file mode 100644 index 0000000..acf59e9 --- /dev/null +++ b/src/main/java/module-info.java @@ -0,0 +1,10 @@ +/** + * The JavaFX-Scene-Router module. Require this to use the library. + */ +module com.andrewlalis.javafx_scene_router { + requires javafx.base; + requires javafx.controls; + requires javafx.fxml; + + exports com.andrewlalis.javafx_scene_router; +}