diff --git a/autotests/tst_pagerouter.qml b/autotests/tst_pagerouter.qml index 288dd757..bc77ef8b 100644 --- a/autotests/tst_pagerouter.qml +++ b/autotests/tst_pagerouter.qml @@ -1,110 +1,110 @@ 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() { + function test_a_init() { compare(router.currentRoutes().length, 1) } - function test_20_navigate() { + function test_b_navigate() { router.navigateToRoute(["home", "login"]) compare(router.currentRoutes().length, 2) } - function test_30_data() { + function test_c_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() { + function test_d_cache_works() { router.navigateToRoute(["home", {"route": "login", "data": "red"}, {"route": "login", "data": "blue"}]) compare(router.currentRoutes().length, 3) } - function test_50_push() { + function test_e_push() { router.pushRoute("home") compare(router.currentRoutes().length, 4) } - function test_60_pop() { + function test_f_pop() { router.popRoute() compare(router.currentRoutes().length, 3) } - function test_70_bring_to_view() { + function test_g_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() { + function test_h_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() { + function test_i_initial_route() { router.initialRoute = "login" compare(router.routeActive(["login"]), false) compare(router.currentRoutes().length, 3) } - function test_100_navigation_two() { + function test_j_navigation_two() { router.navigateToRoute(["home", {"route": "login", "data": "red"}, {"route": "login", "data": "blue"}]) compare(router.currentRoutes().length, 3) router.navigateToRoute(["home"]) compare(router.currentRoutes().length, 1) compare(router.pageStack.count, 1) } } 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/src/pagerouter.cpp b/src/pagerouter.cpp index e69972e7..e3a87f88 100644 --- a/src/pagerouter.cpp +++ b/src/pagerouter.cpp @@ -1,387 +1,387 @@ /* * 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 incomingRoutes = parseRoutes(route); QList resolvedRoutes; if (incomingRoutes.length() <= m_currentRoutes.length()) { resolvedRoutes = m_currentRoutes.mid(0, incomingRoutes.length()); } else { resolvedRoutes = m_currentRoutes; resolvedRoutes.reserve(incomingRoutes.length()-m_currentRoutes.length()); } for (int i = 0; i < incomingRoutes.length(); i++) { auto current = resolvedRoutes.value(i); auto incoming = incomingRoutes.at(i); if (i >= resolvedRoutes.length()) { resolvedRoutes.append(incoming); - } else if (current != incoming) { + } else if (current.name != incoming.name || current.data != incoming.data) { resolvedRoutes.replace(i, incoming); } } for (auto route : m_currentRoutes) { if (!resolvedRoutes.contains(route)) { if (!route.cache) { route.item->deleteLater(); } } } m_pageStack->clear(); m_currentRoutes.clear(); for (auto toPush : resolvedRoutes) { push(toPush); } } 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