qskinny/examples/iot-dashboard/src/pagerouter.cpp

726 lines
21 KiB
C++

/*
* SPDX-FileCopyrightText: 2020 Carson Black <uhhadd@gmail.com>
*
* SPDX-License-Identifier: LGPL-2.0-or-later
*/
#include <QJsonValue>
#include <QJsonObject>
#include <QJSValue>
#include <QJSEngine>
#include <QQmlProperty>
#include <QQuickWindow>
#include "pagerouter.h"
ParsedRoute* parseRoute(QJSValue value)
{
if (value.isUndefined()) {
return new ParsedRoute{};
} else if (value.isString()) {
return new ParsedRoute{
value.toString(),
QVariant()
};
} else {
auto map = value.toVariant().value<QVariantMap>();
map.remove(QStringLiteral("route"));
map.remove(QStringLiteral("data"));
return new ParsedRoute{
value.property(QStringLiteral("route")).toString(),
value.property(QStringLiteral("data")).toVariant(),
map,
false
};
}
}
QList<ParsedRoute*> parseRoutes(QJSValue values)
{
QList<ParsedRoute*> ret;
if (values.isArray()) {
const auto valuesList = values.toVariant().toList();
for (const auto &route : valuesList) {
if (route.toString() != QString()) {
ret << new ParsedRoute{
route.toString(),
QVariant(),
QVariantMap(),
false,
nullptr
};
} else if (route.canConvert<QVariantMap>()) {
auto map = route.value<QVariantMap>();
auto copy = map;
copy.remove(QStringLiteral("route"));
copy.remove(QStringLiteral("data"));
ret << new ParsedRoute{
map.value(QStringLiteral("route")).toString(),
map.value(QStringLiteral("data")),
copy,
false,
nullptr
};
}
}
} else {
ret << parseRoute(values);
}
return ret;
}
PageRouter::PageRouter(QQuickItem *parent) : QObject(parent), m_cache(), m_preload()
{
connect(this, &PageRouter::pageStackChanged, [=]() {
connect(m_pageStack, &ColumnView::currentIndexChanged, this, &PageRouter::currentIndexChanged);
});
}
QQmlListProperty<PageRoute> PageRouter::routes()
{
return QQmlListProperty<PageRoute>(this, nullptr, appendRoute, routeCount, route, clearRoutes);
}
void PageRouter::appendRoute(QQmlListProperty<PageRoute>* prop, PageRoute* route)
{
auto router = qobject_cast<PageRouter*>(prop->object);
router->m_routes.append(route);
}
int PageRouter::routeCount(QQmlListProperty<PageRoute>* prop)
{
auto router = qobject_cast<PageRouter*>(prop->object);
return router->m_routes.length();
}
PageRoute* PageRouter::route(QQmlListProperty<PageRoute>* prop, int index)
{
auto router = qobject_cast<PageRouter*>(prop->object);
return router->m_routes[index];
}
void PageRouter::clearRoutes(QQmlListProperty<PageRoute>* prop)
{
auto router = qobject_cast<PageRouter*>(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) const
{
for (auto route : m_routes) {
if (route->name() == key) return true;
}
return false;
}
QQmlComponent* PageRouter::routesValueForKey(const QString &key) const
{
for (auto route : m_routes) {
if (route->name() == key) return route->component();
}
return nullptr;
}
bool PageRouter::routesCacheForKey(const QString &key) const
{
for (auto route : m_routes) {
if (route->name() == key) return route->cache();
}
return false;
}
int PageRouter::routesCostForKey(const QString &key) const
{
for (auto route : m_routes) {
if (route->name() == key) return route->cost();
}
return -1;
}
void PageRouter::push(ParsedRoute* route)
{
Q_ASSERT(route);
if (!routesContainsKey(route->name)) {
qCritical() << "Route" << route->name << "not defined";
return;
}
if (routesCacheForKey(route->name)) {
auto push = [route, this](ParsedRoute* item) {
m_currentRoutes << item;
for ( auto it = route->properties.begin(); it != route->properties.end(); it++ ) {
item->item->setProperty(qUtf8Printable(it.key()), it.value());
}
m_pageStack->addItem(item->item);
};
auto item = m_cache.take(qMakePair(route->name, route->hash()));
if (item && item->item) {
push(item);
return;
}
item = m_preload.take(qMakePair(route->name, route->hash()));
if (item && item->item) {
push(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 qqItem = qobject_cast<QQuickItem*>(item);
if (!qqItem) {
qCritical() << "Route" << route->name << "is not an item! This is undefined behaviour and will likely crash your application.";
}
for ( auto it = route->properties.begin(); it != route->properties.end(); it++ ) {
qqItem->setProperty(qUtf8Printable(it.key()), it.value());
}
route->setItem(qqItem);
route->cache = routesCacheForKey(route->name);
m_currentRoutes << route;
auto attached = qobject_cast<PageRouterAttached*>(qmlAttachedPropertiesObject<PageRouter>(item, true));
attached->m_router = this;
component->completeCreate();
m_pageStack->addItem(qqItem);
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<ParsedRoute*> 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 incoming = incomingRoutes.at(i);
Q_ASSERT(incoming);
if (i >= resolvedRoutes.length()) {
resolvedRoutes.append(incoming);
} else {
auto current = resolvedRoutes.value(i);
Q_ASSERT(current);
if (current->name != incoming->name || current->data != incoming->data) {
resolvedRoutes.replace(i, incoming);
}
}
}
for (const auto &route : qAsConst(m_currentRoutes)) {
if (!resolvedRoutes.contains(route)) {
placeInCache(route);
}
}
m_pageStack->clear();
m_currentRoutes.clear();
for (auto toPush : qAsConst(resolvedRoutes)) {
push(toPush);
}
Q_EMIT navigationChanged();
}
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 : qAsConst(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));
Q_EMIT navigationChanged();
}
void PageRouter::popRoute()
{
m_pageStack->pop(m_currentRoutes.last()->item);
placeInCache(m_currentRoutes.last());
m_currentRoutes.removeLast();
Q_EMIT navigationChanged();
}
QVariant PageRouter::dataFor(QObject *object)
{
auto pointer = object;
auto qqiPointer = qobject_cast<QQuickItem*>(object);
QHash<QQuickItem*,ParsedRoute*> routes;
for (auto route : qAsConst(m_cache.items)) {
routes[route->item] = route;
}
for (auto route : qAsConst(m_preload.items)) {
routes[route->item] = route;
}
for (auto route : qAsConst(m_currentRoutes)) {
routes[route->item] = route;
}
while (qqiPointer != nullptr) {
const auto keys = routes.keys();
for (auto item : keys) {
if (item == qqiPointer) {
return routes[item]->data;
}
}
qqiPointer = qqiPointer->parentItem();
}
while (pointer != nullptr) {
const auto keys = routes.keys();
for (auto item : keys) {
if (item == pointer) {
return routes[item]->data;
}
}
pointer = pointer->parent();
}
return QVariant();
}
bool PageRouter::isActive(QObject *object)
{
auto pointer = object;
while (pointer != nullptr) {
auto index = 0;
for (auto route : qAsConst(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);
return attached;
}
QSet<QObject*> flatParentTree(QObject* object)
{
// See below comment in Climber::climbObjectParents for why this is here.
static const QMetaObject* metaObject = QMetaType::metaObjectForType(QMetaType::type("QQuickItem*"));
QSet<QObject*> ret;
// Use an inline struct type so that climbItemParents and climbObjectParents
// can call eachother
struct Climber
{
void climbItemParents(QSet<QObject*> &out, QQuickItem *item) {
auto parent = item->parentItem();
while (parent != nullptr) {
out << parent;
climbObjectParents(out, parent);
parent = parent->parentItem();
}
}
void climbObjectParents(QSet<QObject*> &out, QObject *object) {
auto parent = object->parent();
while (parent != nullptr) {
out << parent;
// We manually call metaObject()->inherits() and
// use a reinterpret cast because qobject_cast seems
// to have stability issues here due to mutable
// pointer mechanics.
if (parent->metaObject()->inherits(metaObject)) {
climbItemParents(out, reinterpret_cast<QQuickItem*>(parent));
}
parent = parent->parent();
}
}
};
Climber climber;
if (qobject_cast<QQuickItem*>(object)) {
climber.climbItemParents(ret, qobject_cast<QQuickItem*>(object));
}
climber.climbObjectParents(ret, object);
return ret;
}
void PageRouter::preload(ParsedRoute* route)
{
for (auto preloaded : qAsConst(m_preload.items)) {
if (preloaded->equals(route)) {
delete route;
return;
}
}
if (!routesContainsKey(route->name)) {
qCritical() << "Route" << route->name << "not defined";
delete route;
return;
}
auto context = qmlContext(this);
auto component = routesValueForKey(route->name);
auto createAndCache = [component, context, route, this]() {
auto item = component->beginCreate(context);
item->setParent(this);
auto qqItem = qobject_cast<QQuickItem*>(item);
if (!qqItem) {
qCritical() << "Route" << route->name << "is not an item! This is undefined behaviour and will likely crash your application.";
}
for ( auto it = route->properties.begin(); it != route->properties.end(); it++ ) {
qqItem->setProperty(qUtf8Printable(it.key()), it.value());
}
route->setItem(qqItem);
route->cache = routesCacheForKey(route->name);
auto attached = qobject_cast<PageRouterAttached*>(qmlAttachedPropertiesObject<PageRouter>(item, true));
attached->m_router = this;
component->completeCreate();
if (!route->cache) {
qCritical() << "Route" << route->name << "is being preloaded despite it not having caching enabled.";
delete route;
return;
}
auto string = route->name;
auto hash = route->hash();
m_preload.insert(qMakePair(string, hash), route, routesCostForKey(route->name));
};
if (component->status() == QQmlComponent::Ready) {
createAndCache();
} 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();
}
createAndCache();
});
} else {
qCritical() << "Failed to push route:" << component->errors();
}
}
void PageRouter::unpreload(ParsedRoute* route)
{
ParsedRoute* toDelete = nullptr;
for (auto preloaded : qAsConst(m_preload.items)) {
if (preloaded->equals(route)) {
toDelete = preloaded;
}
}
if (toDelete != nullptr) {
m_preload.take(qMakePair(toDelete->name, toDelete->hash()));
delete toDelete;
}
delete route;
}
void PreloadRouteGroup::handleChange()
{
if (!(m_parent->m_router)) {
qCritical() << "PreloadRouteGroup does not have a parent PageRouter";
return;
}
auto r = m_parent->m_router;
auto parsed = parseRoute(m_route);
if (m_when) {
r->preload(parsed);
} else {
r->unpreload(parsed);
}
}
PreloadRouteGroup::~PreloadRouteGroup()
{
if (m_parent->m_router) {
m_parent->m_router->unpreload(parseRoute(m_route));
}
}
void PageRouterAttached::findParent()
{
QQuickItem *parent = qobject_cast<QQuickItem *>(this->parent());
while (parent != nullptr) {
auto attached = qobject_cast<PageRouterAttached*>(qmlAttachedPropertiesObject<PageRouter>(parent, false));
if (attached != nullptr && attached->m_router != nullptr) {
m_router = attached->m_router;
Q_EMIT routerChanged();
Q_EMIT dataChanged();
Q_EMIT isCurrentChanged();
Q_EMIT navigationChanged();
break;
}
parent = parent->parentItem();
}
}
void PageRouterAttached::navigateToRoute(QJSValue route)
{
if (m_router) {
m_router->navigateToRoute(route);
} else {
qCritical() << "PageRouterAttached does not have a parent PageRouter";
return;
}
}
bool PageRouterAttached::routeActive(QJSValue route)
{
if (m_router) {
return m_router->routeActive(route);
} else {
qCritical() << "PageRouterAttached does not have a parent PageRouter";
return false;
}
}
void PageRouterAttached::pushRoute(QJSValue route)
{
if (m_router) {
m_router->pushRoute(route);
} else {
qCritical() << "PageRouterAttached does not have a parent PageRouter";
return;
}
}
void PageRouterAttached::popRoute()
{
if (m_router) {
m_router->popRoute();
} else {
qCritical() << "PageRouterAttached does not have a parent PageRouter";
return;
}
}
void PageRouterAttached::bringToView(QJSValue route)
{
if (m_router) {
m_router->bringToView(route);
} else {
qCritical() << "PageRouterAttached does not have a parent PageRouter";
return;
}
}
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;
}
}
bool PageRouterAttached::watchedRouteActive()
{
if (m_router) {
return m_router->routeActive(m_watchedRoute);
} else {
qCritical() << "PageRouterAttached does not have a parent PageRouter";
return false;
}
}
void PageRouterAttached::setWatchedRoute(QJSValue route)
{
m_watchedRoute = route;
Q_EMIT watchedRouteChanged();
}
QJSValue PageRouterAttached::watchedRoute()
{
return m_watchedRoute;
}
void PageRouterAttached::pushFromHere(QJSValue route)
{
if (m_router) {
m_router->pushFromObject(parent(), route);
} else {
qCritical() << "PageRouterAttached does not have a parent PageRouter";
}
}
void PageRouterAttached::replaceFromHere(QJSValue route)
{
if (m_router) {
m_router->pushFromObject(parent(), route, true);
} else {
qCritical() << "PageRouterAttached does not have a parent PageRouter";
}
}
void PageRouterAttached::popFromHere()
{
if (m_router) {
m_router->pushFromObject(parent(), QJSValue());
} else {
qCritical() << "PageRouterAttached does not have a parent PageRouter";
}
}
void PageRouter::placeInCache(ParsedRoute *route)
{
Q_ASSERT(route);
if (!route->cache) {
delete route;
return;
}
auto string = route->name;
auto hash = route->hash();
m_cache.insert(qMakePair(string, hash), route, routesCostForKey(route->name));
}
void PageRouter::pushFromObject(QObject *object, QJSValue inputRoute, bool replace)
{
const auto parsed = parseRoutes(inputRoute);
const auto objects = flatParentTree(object);
for (const auto& obj : objects) {
bool popping = false;
for (auto route : qAsConst(m_currentRoutes)) {
if (popping) {
m_currentRoutes.removeAll(route);
placeInCache(route);
continue;
}
if (route->item == obj) {
m_pageStack->pop(route->item);
if (replace) {
m_currentRoutes.removeAll(route);
m_pageStack->removeItem(route->item);
}
popping = true;
}
}
if (popping) {
if (!inputRoute.isUndefined()) {
for (auto route : parsed) {
push(route);
}
}
Q_EMIT navigationChanged();
return;
}
}
qWarning() << "Object" << object << "not in current routes";
}
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), m_preload(new PreloadRouteGroup(this))
{
findParent();
auto item = qobject_cast<QQuickItem*>(parent);
if (item != nullptr) {
connect(item, &QQuickItem::windowChanged, this, [this]() {
findParent();
});
connect(item, &QQuickItem::parentChanged, this, [this]() {
findParent();
});
}
}