diff --git a/Mainpage.dox b/Mainpage.dox
--- a/Mainpage.dox
+++ b/Mainpage.dox
@@ -23,6 +23,8 @@
- \link org::kde::kirigami::Page Page \endlink
- \link org::kde::kirigami::ScrollablePage ScrollablePage \endlink
- \link org::kde::kirigami::OverlaySheet OverlaySheet \endlink
+- \link PageRouter PageRouter \endlink
+- \link PageRoute PageRoute \endlink
- \link org::kde::kirigami::Theme Theme \endlink
- \link org::kde::kirigami::Units Units \endlink
- \link org::kde::kirigami::Icon Icon \endlink
diff --git a/autotests/CMakeLists.txt b/autotests/CMakeLists.txt
--- a/autotests/CMakeLists.txt
+++ b/autotests/CMakeLists.txt
@@ -28,5 +28,6 @@
tst_pagerow.qml
tst_icon.qml
tst_actiontoolbar.qml
+ tst_pagerouter.qml
)
diff --git a/autotests/tst_pagerouter.qml b/autotests/tst_pagerouter.qml
new file mode 100644
--- /dev/null
+++ b/autotests/tst_pagerouter.qml
@@ -0,0 +1,103 @@
+import QtQuick 2.12
+import QtQuick.Controls 2.12 as QQC2
+import org.kde.kirigami 2.12 as Kirigami
+import QtTest 1.0
+
+Kirigami.PageRow {
+ id: root
+ TestCase {
+ name: "PageRouterGeneralTests"
+ function test_10_init() {
+ compare(router.currentRoutes().length, 1)
+ }
+ function test_20_navigate() {
+ router.navigateToRoute(["home", "login"])
+ compare(router.currentRoutes().length, 2)
+ }
+ function test_30_data() {
+ router.navigateToRoute(["home", {"route": "login", "data": "red"}])
+ compare(router.routeActive(["home", {"route": "login", "data": "red"}]), true)
+ compare(router.routeActive(["home", {"route": "login", "data": "blue"}]), false)
+ }
+ function test_40_cache_works() {
+ router.navigateToRoute(["home", {"route": "login", "data": "red"}, {"route": "login", "data": "blue"}])
+ compare(router.currentRoutes().length, 3)
+ }
+ function test_50_push() {
+ router.pushRoute("home")
+ compare(router.currentRoutes().length, 4)
+ }
+ function test_60_pop() {
+ router.popRoute()
+ compare(router.currentRoutes().length, 3)
+ }
+ function test_70_bring_to_view() {
+ router.bringToView("home")
+ compare(root.columnView.currentIndex, 0)
+ router.bringToView({"route": "login", "data": "red"})
+ compare(root.columnView.currentIndex, 1)
+ router.bringToView({"route": "login", "data": "blue"})
+ compare(root.columnView.currentIndex, 2)
+ }
+ function test_80_routeactive() {
+ compare(router.routeActive(["home"]), true)
+ compare(router.routeActive(["home", "login"]), true)
+ compare(router.routeActive(["home", {"route": "login", "data": "red"}]), true)
+ compare(router.routeActive(["home", {"route": "login", "data": "blue"}]), false)
+ }
+ function test_90_initial_route() {
+ router.initialRoute = "login"
+ compare(router.routeActive(["login"]), false)
+ compare(router.currentRoutes().length, 3)
+ }
+ }
+ Kirigami.PageRouter {
+ id: router
+ initialRoute: "home"
+ pageStack: root.columnView
+
+ Kirigami.PageRoute {
+ name: "home"
+ cache: false
+ Component {
+ Kirigami.Page {
+ Column {
+ Kirigami.Heading {
+ text: "Welcome"
+ }
+ QQC2.Button {
+ text: "Red Login"
+ onClicked: Kirigami.PageRouter.navigateToRoute(["home", {"route": "login", "data": "red"}])
+ }
+ QQC2.Button {
+ text: "Blue Login"
+ onClicked: Kirigami.PageRouter.navigateToRoute(["home", {"route": "login", "data": "blue"}])
+ }
+ }
+ }
+ }
+ }
+ Kirigami.PageRoute {
+ name: "login"
+ cache: true
+ Component {
+ Kirigami.Page {
+ Column {
+ Kirigami.Heading {
+ text: "Login"
+ }
+ Rectangle {
+ height: 50
+ width: 50
+ color: Kirigami.PageRouter.data
+ }
+ QQC2.Button {
+ text: "Back to Home"
+ onClicked: Kirigami.PageRouter.navigateToRoute("home")
+ }
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/docs/pics/PageRouterModel.svg b/docs/pics/PageRouterModel.svg
new file mode 100644
--- /dev/null
+++ b/docs/pics/PageRouterModel.svg
@@ -0,0 +1,19 @@
+
diff --git a/docs/pics/PageRouterNavigate.svg b/docs/pics/PageRouterNavigate.svg
new file mode 100644
--- /dev/null
+++ b/docs/pics/PageRouterNavigate.svg
@@ -0,0 +1,16 @@
+
diff --git a/docs/pics/PageRouterPop.svg b/docs/pics/PageRouterPop.svg
new file mode 100644
--- /dev/null
+++ b/docs/pics/PageRouterPop.svg
@@ -0,0 +1,14 @@
+
diff --git a/docs/pics/PageRouterPush.svg b/docs/pics/PageRouterPush.svg
new file mode 100644
--- /dev/null
+++ b/docs/pics/PageRouterPush.svg
@@ -0,0 +1,14 @@
+
diff --git a/examples/PageRoute.qml b/examples/PageRoute.qml
new file mode 100644
--- /dev/null
+++ b/examples/PageRoute.qml
@@ -0,0 +1,8 @@
+Kirigami.PageRoute {
+ name: "routeOne"
+ Component {
+ Kirigami.Page {
+ // Page contents...
+ }
+ }
+}
\ No newline at end of file
diff --git a/examples/PageRouter.qml b/examples/PageRouter.qml
new file mode 100644
--- /dev/null
+++ b/examples/PageRouter.qml
@@ -0,0 +1,55 @@
+import QtQuick 2.12
+import QtQuick.Controls 2.12 as QQC2
+import org.kde.kirigami 2.12 as Kirigami
+
+Kirigami.ApplicationWindow {
+ id: applicaionWindow
+ Kirigami.PageRouter {
+ initialRoute: "home"
+ pageStack: applicaionWindow.pageStack.columnView
+
+ Kirigami.PageRoute {
+ name: "home"
+ cache: false
+ Component {
+ Kirigami.Page {
+ Column {
+ Kirigami.Heading {
+ text: "Welcome"
+ }
+ QQC2.Button {
+ text: "Red Login"
+ onClicked: Kirigami.PageRouter.navigateToRoute(["home", {"route": "login", "data": "red"}])
+ }
+ QQC2.Button {
+ text: "Blue Login"
+ onClicked: Kirigami.PageRouter.navigateToRoute(["home", {"route": "login", "data": "blue"}])
+ }
+ }
+ }
+ }
+ }
+ Kirigami.PageRoute {
+ name: "login"
+ cache: true
+ Component {
+ Kirigami.Page {
+ Column {
+ Kirigami.Heading {
+ text: "Login"
+ }
+ Rectangle {
+ height: 50
+ width: 50
+ color: Kirigami.PageRouter.data
+ }
+ QQC2.Button {
+ text: "Back to Home"
+ onClicked: Kirigami.PageRouter.navigateToRoute("home")
+ }
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/examples/PageRouterCachePagesDo.qml b/examples/PageRouterCachePagesDo.qml
new file mode 100644
--- /dev/null
+++ b/examples/PageRouterCachePagesDo.qml
@@ -0,0 +1,23 @@
+import QtQuick 2.12
+import org.kde.kirigami 2.12 as Kirigami
+
+Kirigami.ApplicationWindow {
+ id: applicaionWindow
+ Kirigami.PageRouter {
+ initialRoute: "home"
+ pageStack: applicaionWindow.pageStack.columnView
+
+ // home can be pushed onto the route twice
+ Component.onCompleted: navigateToRoute("home", "home")
+
+ Kirigami.PageRoute {
+ name: "home"
+ cache: false
+ Component {
+ Kirigami.Page {
+ // Page contents...
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/examples/PageRouterCachePagesDont.qml b/examples/PageRouterCachePagesDont.qml
new file mode 100644
--- /dev/null
+++ b/examples/PageRouterCachePagesDont.qml
@@ -0,0 +1,23 @@
+import QtQuick 2.12
+import org.kde.kirigami 2.12 as Kirigami
+
+Kirigami.ApplicationWindow {
+ id: applicaionWindow
+ Kirigami.PageRouter {
+ initialRoute: "home"
+ pageStack: applicaionWindow.pageStack.columnView
+
+ // home can't be pushed onto the route twice
+ Component.onCompleted: navigateToRoute("home", "home")
+
+ Kirigami.PageRoute {
+ name: "home"
+ cache: true
+ Component {
+ Kirigami.Page {
+ // Page contents...
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/examples/PageRouterColumnView.qml b/examples/PageRouterColumnView.qml
new file mode 100644
--- /dev/null
+++ b/examples/PageRouterColumnView.qml
@@ -0,0 +1,20 @@
+import QtQuick 2.12
+import org.kde.kirigami 2.12 as Kirigami
+
+Kirigami.ApplicationWindow {
+ id: applicaionWindow
+ Kirigami.PageRouter {
+ pageStack: applicaionWindow.pageStack.columnView
+
+ Kirigami.PageRoute {
+ name: "home"
+ Component {
+ Kirigami.Page {
+ // Page contents...
+ }
+ }
+ }
+ initialRoute: "home"
+ Component.onCompleted: navigateToRoute("home", "home")
+ }
+}
\ No newline at end of file
diff --git a/examples/PageRouterInitialRoute.qml b/examples/PageRouterInitialRoute.qml
new file mode 100644
--- /dev/null
+++ b/examples/PageRouterInitialRoute.qml
@@ -0,0 +1,27 @@
+import QtQuick 2.12
+import org.kde.kirigami 2.12 as Kirigami
+
+Kirigami.ApplicationWindow {
+ id: applicaionWindow
+ Kirigami.PageRouter {
+ initialRoute: "home"
+ pageStack: applicaionWindow.pageStack.columnView
+
+ Kirigami.PageRoute {
+ name: "home"
+ Component {
+ Kirigami.Page {
+ // This page will show up when starting the application
+ }
+ }
+ }
+ Kirigami.PageRoute {
+ name: "login"
+ Component {
+ Kirigami.Page {
+ // Page contents...
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/examples/PageRouterRoutes.qml b/examples/PageRouterRoutes.qml
new file mode 100644
--- /dev/null
+++ b/examples/PageRouterRoutes.qml
@@ -0,0 +1,34 @@
+import QtQuick 2.12
+import org.kde.kirigami 2.12 as Kirigami
+
+Kirigami.ApplicationWindow {
+ id: applicaionWindow
+ Kirigami.PageRouter {
+ pageStack: applicaionWindow.pageStack.columnView
+
+ Kirigami.PageRoute {
+ name: "routeOne"
+ Component {
+ Kirigami.Page {
+ // Page contents...
+ }
+ }
+ }
+ Kirigami.PageRoute {
+ name: "routeTwo"
+ Component {
+ Kirigami.Page {
+ // Page contents...
+ }
+ }
+ }
+ Kirigami.PageRoute {
+ name: "routeThree"
+ Component {
+ Kirigami.Page {
+ // Page contents...
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -27,6 +27,7 @@
shadowedrectangle.cpp
shadowedtexture.cpp
colorutils.cpp
+ pagerouter.cpp
scenegraph/shadowedrectanglenode.cpp
scenegraph/shadowedrectanglematerial.cpp
scenegraph/shadowedborderrectanglematerial.cpp
diff --git a/src/controls/PageRow.qml b/src/controls/PageRow.qml
--- a/src/controls/PageRow.qml
+++ b/src/controls/PageRow.qml
@@ -56,6 +56,17 @@
*/
contentItem: columnView
+ /**
+ * columnView: Kirigami::ColumnView
+ *
+ * The ColumnView that this PageRow owns.
+ * Generally, you shouldn't need to change
+ * the value of this.
+ *
+ * @since 2.12
+ */
+ property alias columnView: columnView
+
/**
* items: list
* All the items that are present in the PageRow
diff --git a/src/kirigamiplugin.cpp b/src/kirigamiplugin.cpp
--- a/src/kirigamiplugin.cpp
+++ b/src/kirigamiplugin.cpp
@@ -20,6 +20,7 @@
#include "shadowedrectangle.h"
#include "shadowedtexture.h"
#include "colorutils.h"
+#include "pagerouter.h"
#include
#include
@@ -248,6 +249,9 @@
qmlRegisterSingletonType(uri, 2, 12, "ColorUtils", [] (QQmlEngine*, QJSEngine*) -> QObject* { return new ColorUtils; });
qmlRegisterUncreatableType(uri, 2, 12, "CornersGroup", QStringLiteral("Used as grouped property"));
+ qmlRegisterType(uri, 2, 12, "PageRouter");
+ qmlRegisterType(uri, 2, 12, "PageRoute");
+ qmlRegisterUncreatableType(uri, 2, 12, "PageRouterAttached", QStringLiteral("PageRouterAttached cannot be created"));
qmlProtectModule(uri, 2);
}
diff --git a/src/pagerouter.h b/src/pagerouter.h
new file mode 100644
--- /dev/null
+++ b/src/pagerouter.h
@@ -0,0 +1,461 @@
+/*
+ * SPDX-FileCopyrightText: 2020 Carson Black
+ *
+ * SPDX-License-Identifier: LGPL-2.0-or-later
+ */
+
+#pragma once
+
+#include
+#include "columnview.h"
+
+struct ParsedRoute {
+ QString name;
+ QVariant data;
+ bool cache;
+ QObject* item;
+ bool operator==(const ParsedRoute& rhs)
+ {
+ return name == rhs.name && data == rhs.data && item == rhs.item && cache == rhs.cache;
+ }
+};
+
+/**
+ * Item representing a route the PageRouter can navigate to.
+ *
+ * @include PageRoute.qml
+ *
+ * @see PageRouter
+ */
+class PageRoute : public QObject
+{
+ Q_OBJECT
+
+ /**
+ * @brief The name of this route.
+ *
+ * This name should be unique per PageRoute in a PageRouter.
+ * When two PageRoutes have the same name, the one listed first
+ * in the PageRouter will be used.
+ */
+ Q_PROPERTY(QString name MEMBER m_name READ name)
+
+ /**
+ * @brief The page component of this route.
+ *
+ * This should be an instance of Component with a Kirigami::Page inside
+ * of it.
+ */
+ Q_PROPERTY(QQmlComponent* component MEMBER m_component READ component)
+
+ /**
+ * @brief Whether pages generated by this route should be cached or not.
+ *
+ * This should be an instance of Component with a Kirigami::Page inside
+ * of it.
+ *
+ * This will not work:
+ *
+ * @include PageRouterCachePagesDont.qml
+ *
+ * This will work:
+ *
+ * @include PageRouterCachePagesDo.qml
+ *
+ */
+ Q_PROPERTY(bool cache MEMBER m_cache READ cache)
+
+ Q_CLASSINFO("DefaultProperty", "component")
+
+private:
+ QString m_name;
+ QQmlComponent* m_component;
+ bool m_cache = false;
+
+public:
+ QQmlComponent* component() { return m_component; };
+ QString name() { return m_name; };
+ bool cache() { return m_cache; };
+};
+
+class PageRouterAttached;
+
+/**
+ * An item managing pages and data of a ColumnView using named routes.
+ *
+ *
+ *
+ * ## Using a PageRouter
+ *
+ * Applications typically manage their contents via elements called "pages" or "screens."
+ * In Kirigami, these are called @link org::kde::kirigami::Page Pages @endlink and are
+ * arranged in @link PageRoute routes @endlink using a PageRouter to manage them. The PageRouter
+ * manages a stack of @link org::kde::kirigami::Page Pages @endlink created from a pool of potential
+ * @link PageRoute PageRoutes @endlink.
+ *
+ * Unlike most traditional stacks, a PageRouter provides functions for random access to its pages
+ * with navigateToRoute and routeActive.
+ *
+ * When your user interface fits the stack paradigm and is likely to use random access navigation,
+ * using the PageRouter is appropriate. For simpler navigation, it is more appropriate to avoid
+ * the overhead of a PageRouter by using a @link org::kde::kirigami::PageRow PageRow @endlink
+ * instead.
+ *
+ *
+ *
+ * ## Navigation Model
+ *
+ * A PageRouter draws from a pool of @link PageRoute PageRoutes @endlink in order to construct
+ * its stack.
+ *
+ * @image html PageRouterModel.svg width=50%
+ *
+ *
+ *
+ * You can push pages onto this stack...
+ *
+ * @image html PageRouterPush.svg width=50%
+ *
+ * ...or pop them off...
+ *
+ * @image html PageRouterPop.svg width=50%
+ *
+ * ...or navigate to an arbitrary collection of pages.
+ *
+ * @image html PageRouterNavigate.svg width=50%
+ *
+ *
+ *
+ * Components are able to query the PageRouter about the currently active routes
+ * on the stack. This is useful for e.g. a card indicating that the page it takes
+ * the user to is currently active.
+ *
+ *
+ *
+ * ## Example
+ *
+ * @include PageRouter.qml
+ *
+ * @see PageRouterAttached
+ * @see PageRoute
+ */
+class PageRouter : public QObject, public QQmlParserStatus
+{
+ Q_OBJECT
+ Q_INTERFACES(QQmlParserStatus)
+
+ /**
+ * @brief The named routes a PageRouter can navigate to.
+ *
+ * @include PageRouterRoutes.qml
+ */
+ Q_PROPERTY(QQmlListProperty routes READ routes)
+
+ Q_CLASSINFO("DefaultProperty", "routes")
+
+ /**
+ * @brief The initial route.
+ *
+ * `initialRoute` is the page that the PageRouter will push upon
+ * creation. Changing it after creation will cause the PageRouter to reset
+ * its state. Not providing an `initialRoute` will result in undefined
+ * behavior.
+ *
+ * @include PageRouterInitialRoute.qml
+ */
+ Q_PROPERTY(QJSValue initialRoute READ initialRoute WRITE setInitialRoute NOTIFY initialRouteChanged)
+
+ /**
+ * @brief The ColumnView being puppeted by the PageRouter.
+ *
+ * All PageRouters should be created with a ColumnView, and creating one without
+ * a ColumnView is undefined behaviour.
+ *
+ * @warning You should **not** directly interact with a ColumnView being puppeted
+ * by a PageRouter. Instead, use a PageRouter's functions to manipulate the
+ * ColumnView.
+ *
+ * @include PageRouterColumnView.qml
+ */
+ Q_PROPERTY(ColumnView* pageStack MEMBER m_pageStack NOTIFY pageStackChanged)
+
+private:
+ /**
+ * @brief The routes the PageRouter is aware of.
+ *
+ * Generally, this should not be mutated from C++, only read.
+ */
+ QList m_routes;
+
+ /**
+ * @brief The PageRouter being puppeted.
+ *
+ * m_pageRow is the ColumnView this PageRouter puppets.
+ */
+ ColumnView* m_pageStack = nullptr;
+
+ /**
+ * @brief The route that the PageRouter will load on completion.
+ *
+ * m_initialRoute is the raw QJSValue from QML that will be
+ * parsed into a ParsedRoute struct on construction.
+ * Generally, this should not be mutated from C++, only read.
+ */
+ QJSValue m_initialRoute;
+
+ /**
+ * @brief The current routes pushed on the PageRow.
+ *
+ * Generally, the state of m_pageRow and m_currentRoutes
+ * should be kept in sync. Undesirable behaviour will result
+ * from desynchronisation of the two.
+ */
+ QList m_currentRoutes;
+
+ /**
+ * @brief Cached routes.
+ *
+ * A list of ParsedRoutes with instantiated items.
+ */
+ QList m_cachedRoutes;
+
+ /**
+ * @brief Helper function to push a route.
+ *
+ * This function has the shared logic between
+ * navigateToRoute and pushRoute.
+ */
+ void push(ParsedRoute route);
+
+ /**
+ * @brief Helper function to access whether m_routes has a key.
+ *
+ * This function abstracts the QJSValue.
+ */
+ bool routesContainsKey(const QString &key);
+
+ /**
+ * @brief Helper function to access the component of a key for m_routes.
+ *
+ * The return value will be a nullptr if @p key does not exist in
+ * m_routes.
+ */
+ QQmlComponent *routesValueForKey(const QString &key);
+
+ /**
+ * @brief Helper function to access the cache status of a key for m_routes.
+ *
+ * The return value will be false if @p key does not exist in
+ * m_routes.
+ */
+ bool routesCacheForKey(const QString &key);
+
+ static void appendRoute(QQmlListProperty* list, PageRoute*);
+ static int routeCount(QQmlListProperty* list);
+ static PageRoute* route(QQmlListProperty* list, int);
+ static void clearRoutes(QQmlListProperty* list);
+
+ QVariant dataFor(QObject* object);
+ bool isActive(QObject* object);
+
+ friend class PageRouterAttached;
+
+protected:
+ void classBegin() override;
+ void componentComplete() override;
+
+public:
+ PageRouter(QQuickItem *parent = nullptr);
+ ~PageRouter();
+
+ QQmlListProperty routes();
+
+ QJSValue initialRoute() const;
+ void setInitialRoute(QJSValue initialRoute);
+
+ /**
+ * @brief Navigate to the given route.
+ *
+ * Calling `navigateToRoute` causes the PageRouter to replace currently
+ * active pages with the new route.
+ *
+ * @param route The given route for the PageRouter to navigate to.
+ * A route is an array of variants or a single item. A string item will be interpreted
+ * as a page without associated data. An object item will be interpreted
+ * as follows:
+ * @code{.js}
+ * {
+ * "route": "/home" // The named page of the route.
+ * "data": QtObject {} // The data to pass to the page.
+ * }
+ * @endcode
+ * Navigating to a route not defined in a PageRouter's routes is undefined
+ * behavior.
+ *
+ * @code{.qml}
+ * Button {
+ * text: "Login"
+ * onClicked: {
+ * Kirigami.PageRouter.navigateToRoute(["/home", "/login"])
+ * }
+ * }
+ * @endcode
+ */
+ Q_INVOKABLE void navigateToRoute(QJSValue route);
+
+ /**
+ * @brief Check whether the current route is on the stack.
+ *
+ * `routeActive` will return true if the given route
+ * is on the stack.
+ *
+ * @param route The given route to check for.
+ *
+ * `routeActive` returns true for partial routes like
+ * the following:
+ *
+ * @code{.js}
+ * Kirigami.PageRouter.navigateToRoute(["/home", "/login", "/google"])
+ * Kirigami.PageRouter.routeActive(["/home", "/login"]) // returns true
+ * @endcode
+ *
+ * This only works from the root page, e.g. the following will return false:
+ * @code{.js}
+ * Kirigami.PageRouter.navigateToRoute(["/home", "/login", "/google"])
+ * Kirigami.PageRouter.routeActive(["/login", "/google"]) // returns false
+ * @endcode
+ */
+ Q_INVOKABLE bool routeActive(QJSValue route);
+
+ /**
+ * @brief Appends a route to the currently navigated route.
+ *
+ * Calling `pushRoute` will append the given @p route to the currently navigated
+ * routes. See navigateToRoute() if you want to replace the items currently on
+ * the PageRouter.
+ *
+ * @param route The given route to push.
+ *
+ * @code{.js}
+ * Kirigami.PageRouter.navigateToRoute(["/home", "/login"])
+ * // The PageRouter is navigated to /home/login
+ * Kirigami.PageRouter.pushRoute("/google")
+ * // The PageRouter is navigated to /home/login/google
+ * @endcode
+ */
+ Q_INVOKABLE void pushRoute(QJSValue route);
+
+ /**
+ * @brief Pops the last page on the router.
+ *
+ * Calling `popRoute` will result in the last page on the router getting popped.
+ * You should not call this function when there is only one page on the router.
+ *
+ * @code{.js}
+ * Kirigami.PageRouter.navigateToRoute(["/home", "/login"])
+ * // The PageRouter is navigated to /home/login
+ * Kirigami.PageRouter.popRoute()
+ * // The PageRouter is navigated to /home
+ * @endcode
+ */
+ Q_INVOKABLE void popRoute();
+
+ /**
+ * @brief Shifts keyboard focus and view to a given index on the PageRouter's stack.
+ *
+ * @param view The view to bring to focus. If this is an integer index, the PageRouter will
+ * navigate to the given index. If it's a route specifier, the PageRouter will navigate
+ * to the first route matching it.
+ *
+ * Navigating to route by index:
+ * @code{.js}
+ * Kirigami.PageRouter.navigateToRoute(["/home", "/browse", "/apps", "/login"])
+ * Kirigami.PageRouter.bringToView(1)
+ * @endcode
+ *
+ * Navigating to route by name:
+ * @code{.js}
+ * Kirigami.PageRouter.navigateToRoute(["/home", "/browse", "/apps", "/login"])
+ * Kirigami.PageRouter.bringToView("/browse")
+ * @endcode
+ *
+ * Navigating to route by data:
+ * @code{.js}
+ * Kirigami.PageRouter.navigateToRoute([{"route": "/page", "data": "red"},
+ * {"route": "/page", "data": "blue"},
+ * {"route": "/page", "data": "green"},
+ * {"route": "/page", "data": "yellow"}])
+ * Kirigami.PageRouter.bringToView({"route": "/page", "data": "blue"})
+ * @endcode
+ */
+ Q_INVOKABLE void bringToView(QJSValue route);
+
+ /**
+ * @brief Returns a QJSValue corresponding to the current pages on the stack.
+ *
+ * The returned value is in the same form as the input to navigateToRoute.
+ */
+ Q_INVOKABLE QJSValue currentRoutes() const;
+
+ static PageRouterAttached *qmlAttachedProperties(QObject *object);
+
+Q_SIGNALS:
+ void routesChanged();
+ void initialRouteChanged();
+ void pageStackChanged();
+ void currentIndexChanged();
+};
+
+/**
+ * Attached object allowing children of a PageRouter to access its functions
+ * without requiring the children to have the parent PageRouter's id.
+ *
+ * @see PageRouter
+ */
+class PageRouterAttached : public QObject
+{
+ Q_OBJECT
+
+ Q_PROPERTY(PageRouter *router READ router NOTIFY routerChanged)
+ /**
+ * The data for the page this item belongs to. Accessing this property
+ * outside of a PageRouter will result in undefined behavior.
+ */
+ Q_PROPERTY(QVariant data READ data MEMBER m_data NOTIFY dataChanged)
+
+ /**
+ * Whether the page this item belongs to is the current index of the ColumnView.
+ * Accessing this property outside of a PageRouter will result in undefined behaviour.
+ */
+ Q_PROPERTY(bool isCurrent READ isCurrent NOTIFY isCurrentChanged)
+
+private:
+ explicit PageRouterAttached(QObject *parent = nullptr);
+
+ QPointer m_router;
+ QVariant m_data;
+
+ friend class PageRouter;
+
+public:
+ PageRouter* router() const { return m_router; };
+ QVariant data() const;
+ bool isCurrent() const;
+ /// @see PageRouter::navigateToRoute()
+ Q_INVOKABLE void navigateToRoute(QJSValue route) { m_router->navigateToRoute(route); };
+ /// @see PageRouter::routeActive()
+ Q_INVOKABLE bool routeActive(QJSValue route) { return m_router->routeActive(route); };
+ /// @see PageRouter::pushRoute()
+ Q_INVOKABLE void pushRoute(QJSValue route) { m_router->pushRoute(route); };
+ /// @see PageRouter::popRoute()
+ Q_INVOKABLE void popRoute() { m_router->popRoute(); };
+ // @see PageRouter::bringToView()
+ Q_INVOKABLE void bringToView(QJSValue route) { m_router->bringToView(route); };
+
+Q_SIGNALS:
+ void routerChanged();
+ void dataChanged();
+ void isCurrentChanged();
+};
+
+QML_DECLARE_TYPEINFO(PageRouter, QML_HAS_ATTACHED_PROPERTIES)
diff --git a/src/pagerouter.cpp b/src/pagerouter.cpp
new file mode 100644
--- /dev/null
+++ b/src/pagerouter.cpp
@@ -0,0 +1,382 @@
+/*
+ * SPDX-FileCopyrightText: 2020 Carson Black
+ *
+ * SPDX-License-Identifier: LGPL-2.0-or-later
+ */
+
+#include
+#include
+#include
+#include
+#include
+#include "pagerouter.h"
+
+ParsedRoute parseRoute(QJSValue value)
+{
+ if (value.isUndefined()) {
+ return ParsedRoute{QString(), QVariant(), false, nullptr};
+ } else if (value.isString()) {
+ return ParsedRoute{
+ value.toString(),
+ QVariant(),
+ false,
+ nullptr
+ };
+ } else {
+ return ParsedRoute{
+ value.property(QStringLiteral("route")).toString(),
+ value.property(QStringLiteral("data")).toVariant(),
+ false,
+ nullptr
+ };
+ }
+}
+
+QList parseRoutes(QJSValue values)
+{
+ QList ret;
+ if (values.isArray()) {
+ for (auto route : values.toVariant().toList()) {
+ if (route.toString() != QString()) {
+ ret << ParsedRoute{
+ route.toString(),
+ QVariant(),
+ false,
+ nullptr
+ };
+ } else if (route.canConvert()) {
+ auto map = route.value();
+ ret << ParsedRoute{
+ map.value(QStringLiteral("route")).toString(),
+ map.value(QStringLiteral("data")),
+ false,
+ nullptr
+ };
+ }
+ }
+ } else {
+ ret << parseRoute(values);
+ }
+ return ret;
+}
+
+PageRouter::PageRouter(QQuickItem *parent) : QObject(parent)
+{
+ connect(this, &PageRouter::pageStackChanged, [=]() {
+ connect(m_pageStack, &ColumnView::itemRemoved, [=](QQuickItem *item) {
+ QList toRemove;
+ for (auto route : m_currentRoutes) {
+ if (route.item == qobject_cast(item)) {
+ if (!route.cache) {
+ route.item->deleteLater();
+ }
+ }
+ }
+ for (auto route : toRemove) {
+ m_currentRoutes.removeAll(route);
+ }
+ });
+ connect(m_pageStack, &ColumnView::currentIndexChanged, this, &PageRouter::currentIndexChanged);
+ });
+}
+
+QQmlListProperty PageRouter::routes()
+{
+ return QQmlListProperty(this, nullptr, appendRoute, routeCount, route, clearRoutes);
+}
+
+void PageRouter::appendRoute(QQmlListProperty* prop, PageRoute* route)
+{
+ auto router = qobject_cast(prop->object);
+ router->m_routes.append(route);
+}
+
+int PageRouter::routeCount(QQmlListProperty* prop)
+{
+ auto router = qobject_cast(prop->object);
+ return router->m_routes.length();
+}
+
+PageRoute* PageRouter::route(QQmlListProperty* prop, int index)
+{
+ auto router = qobject_cast(prop->object);
+ return router->m_routes[index];
+}
+
+void PageRouter::clearRoutes(QQmlListProperty* prop)
+{
+ auto router = qobject_cast(prop->object);
+ router->m_routes.clear();
+}
+
+PageRouter::~PageRouter() {}
+
+void PageRouter::classBegin()
+{
+
+}
+
+void PageRouter::componentComplete()
+{
+ if (m_pageStack == nullptr) {
+ qCritical() << "PageRouter should be created with a ColumnView. Not doing so is undefined behaviour, and is likely to result in a crash upon further interaction.";
+ } else {
+ Q_EMIT pageStackChanged();
+ m_currentRoutes.clear();
+ push(parseRoute(initialRoute()));
+ }
+}
+
+bool PageRouter::routesContainsKey(const QString &key)
+{
+ for (auto route : m_routes) {
+ if (route->name() == key) return true;
+ }
+ return false;
+}
+
+QQmlComponent* PageRouter::routesValueForKey(const QString &key)
+{
+ for (auto route : m_routes) {
+ if (route->name() == key) return route->component();
+ }
+ return nullptr;
+}
+
+bool PageRouter::routesCacheForKey(const QString &key)
+{
+ for (auto route : m_routes) {
+ if (route->name() == key) return route->cache();
+ }
+ return false;
+}
+
+void PageRouter::push(ParsedRoute route)
+{
+ if (!routesContainsKey(route.name)) {
+ qCritical() << "Route" << route.name << "not defined";
+ return;
+ }
+ if (routesCacheForKey(route.name)) {
+ for (auto cachedRoute : m_cachedRoutes) {
+ if (cachedRoute.name == route.name && cachedRoute.data == route.data) {
+ m_currentRoutes << cachedRoute;
+ m_pageStack->addItem(qobject_cast(cachedRoute.item));
+ return;
+ }
+ }
+ }
+ auto context = qmlContext(this);
+ auto component = routesValueForKey(route.name);
+ auto createAndPush = [component, context, route, this]() {
+ // We use beginCreate and completeCreate to allow
+ // for a PageRouterAttached to find its parent
+ // on construction time.
+ auto item = component->beginCreate(context);
+ item->setParent(this);
+ auto clone = route;
+ clone.item = item;
+ clone.cache = routesCacheForKey(route.name);
+ m_currentRoutes << clone;
+ if (routesCacheForKey(route.name)) {
+ m_cachedRoutes << clone;
+ }
+ component->completeCreate();
+ m_pageStack->addItem(qobject_cast(item));
+ m_pageStack->setCurrentIndex(m_currentRoutes.length()-1);
+ };
+
+ if (component->status() == QQmlComponent::Ready) {
+ createAndPush();
+ } else if (component->status() == QQmlComponent::Loading) {
+ connect(component, &QQmlComponent::statusChanged, [=](QQmlComponent::Status status) {
+ // Loading can only go to Ready or Error.
+ if (status != QQmlComponent::Ready) {
+ qCritical() << "Failed to push route:" << component->errors();
+ }
+ createAndPush();
+ });
+ } else {
+ qCritical() << "Failed to push route:" << component->errors();
+ }
+}
+
+QJSValue PageRouter::initialRoute() const
+{
+ return m_initialRoute;
+}
+
+void PageRouter::setInitialRoute(QJSValue value)
+{
+ m_initialRoute = value;
+}
+
+void PageRouter::navigateToRoute(QJSValue route)
+{
+ auto defaultRoute = ParsedRoute{};
+ auto incoming = parseRoutes(route);
+
+ int i = 0;
+ for (; i < m_currentRoutes.length(); i++) {
+ auto currentRoute = m_currentRoutes.at(i);
+ auto incomingRoute = incoming.value(i);
+ if (!(incomingRoute == defaultRoute)) {
+ if (currentRoute.name != incomingRoute.name || currentRoute.data != incomingRoute.data) {
+ break;
+ }
+ }
+ }
+
+ int trim = i;
+ for (; trim < m_currentRoutes.length(); trim++) {
+ auto route = m_currentRoutes.value(i);
+ m_pageStack->removeItem(QVariant::fromValue(route.item));
+ if (!route.cache) {
+ route.item->deleteLater();
+ }
+ }
+
+ m_currentRoutes.erase(m_currentRoutes.begin()+i, m_currentRoutes.end());
+ for (; i < incoming.length(); i++) {
+ auto incomingRoute = incoming.at(i);
+ push(incomingRoute);
+ }
+}
+
+void PageRouter::bringToView(QJSValue route)
+{
+ if (route.isNumber()) {
+ auto index = route.toNumber();
+ m_pageStack->setCurrentIndex(index);
+ } else {
+ auto parsed = parseRoute(route);
+ auto index = 0;
+ for (auto currentRoute : m_currentRoutes) {
+ if (currentRoute.name == parsed.name && currentRoute.data == parsed.data) {
+ m_pageStack->setCurrentIndex(index);
+ return;
+ }
+ index++;
+ }
+ qWarning() << "Route" << parsed.name << "with data" << parsed.data << "is not on the current stack of routes.";
+ }
+}
+
+bool PageRouter::routeActive(QJSValue route)
+{
+ auto parsed = parseRoutes(route);
+ if (parsed.length() > m_currentRoutes.length()) {
+ return false;
+ }
+ for (int i = 0; i < parsed.length(); i++) {
+ if (parsed[i].name != m_currentRoutes[i].name) {
+ return false;
+ }
+ if (parsed[i].data.isValid()) {
+ if (parsed[i].data != m_currentRoutes[i].data) {
+ return false;
+ }
+ }
+ }
+ return true;
+}
+
+void PageRouter::pushRoute(QJSValue route)
+{
+ push(parseRoute(route));
+}
+
+void PageRouter::popRoute()
+{
+ m_pageStack->pop(qobject_cast(m_currentRoutes.last().item));
+ if (!m_currentRoutes.last().cache) {
+ m_currentRoutes.last().item->deleteLater();
+ }
+ m_currentRoutes.removeLast();
+}
+
+QVariant PageRouter::dataFor(QObject *object)
+{
+ auto pointer = object;
+ while (pointer != nullptr) {
+ for (auto route : m_currentRoutes) {
+ if (route.item == pointer) {
+ return route.data;
+ }
+ }
+ pointer = pointer->parent();
+ }
+ return QVariant();
+}
+
+bool PageRouter::isActive(QObject *object)
+{
+ auto pointer = object;
+ while (pointer != nullptr) {
+ auto index = 0;
+ for (auto route : m_currentRoutes) {
+ if (route.item == pointer) {
+ return m_pageStack->currentIndex() == index;
+ }
+ index++;
+ }
+ pointer = pointer->parent();
+ }
+ qWarning() << "Object" << object << "not in current routes";
+ return false;
+}
+
+PageRouterAttached* PageRouter::qmlAttachedProperties(QObject *object)
+{
+ auto attached = new PageRouterAttached(object);
+ auto pointer = object;
+ // Climb the parent tree to find our parent PageRouter
+ while (pointer != nullptr) {
+ auto casted = qobject_cast(pointer);
+ if (casted != nullptr) {
+ attached->m_router = casted;
+ connect(casted, &PageRouter::currentIndexChanged, attached, &PageRouterAttached::isCurrentChanged);
+ break;
+ }
+ pointer = pointer->parent();
+ }
+ if (attached->m_router.isNull()) {
+ qCritical() << "PageRouterAttached could not find a parent PageRouter";
+ }
+ return attached;
+}
+
+QVariant PageRouterAttached::data() const
+{
+ if (m_router) {
+ return m_router->dataFor(parent());
+ } else {
+ qCritical() << "PageRouterAttached does not have a parent PageRouter";
+ return QVariant();
+ }
+}
+
+bool PageRouterAttached::isCurrent() const
+{
+ if (m_router) {
+ return m_router->isActive(parent());
+ } else {
+ qCritical() << "PageRouterAttached does not have a parent PageRouter";
+ return false;
+ }
+}
+
+QJSValue PageRouter::currentRoutes() const
+{
+ auto engine = qjsEngine(this);
+ auto ret = engine->newArray(m_currentRoutes.length());
+ for (int i = 0; i < m_currentRoutes.length(); ++i) {
+ auto object = engine->newObject();
+ object.setProperty(QStringLiteral("route"), m_currentRoutes[i].name);
+ object.setProperty(QStringLiteral("data"), engine->toScriptValue(m_currentRoutes[i].data));
+ ret.setProperty(i, object);
+ }
+ return ret;
+}
+
+PageRouterAttached::PageRouterAttached(QObject *parent) : QObject(parent) {}
\ No newline at end of file