diff --git a/src/controls/PagePoolAction.qml b/src/controls/PagePoolAction.qml index fe4d5f9b..371796ee 100644 --- a/src/controls/PagePoolAction.qml +++ b/src/controls/PagePoolAction.qml @@ -1,84 +1,94 @@ /* * SPDX-FileCopyrightText: 2016 Marco Martin * * SPDX-License-Identifier: LGPL-2.0-or-later */ import QtQuick 2.7 import QtQuick.Controls 2.5 as Controls import org.kde.kirigami 2.11 as Kirigami /** * An action used to load Pages coming from a common PagePool * in a PageRow or QtQuickControls2 StackView * * @inherit Action */ Kirigami.Action { id: root /** * page: string * Url or filename of the page this action will load */ property string page /** * pagePool: Kirigami.PagePool * The PagePool used by this PagePoolAction. * PagePool will make sure only one instance of the page identified by the page url will be created and reused. *PagePool's lastLoaderUrl property will be used to control the mutual * exclusivity of the checked state of the PagePoolAction instances * sharing the same PagePool */ property Kirigami.PagePool pagePool /** * pageStack: Kirigami.PageRow or QtQuickControls2 StackView * The component that will instantiate the pages, which has to work with a stack logic. * Kirigami.PageRow is recommended, but will work with QtQuicControls2 StackView as well. * By default this property is binded to ApplicationWindow's global * pageStack, which is a PageRow by default. */ property Item pageStack: typeof applicationWindow != undefined ? applicationWindow().pageStack : null /** * basePage: Kirigami.Page * The page of pageStack new pages will be pushed after. * All pages present after the given basePage will be removed from the pageStack */ property Controls.Page basePage + /** + * initialProperties: JavaScript Object + * The properties object specifies a map of initial property values for the created page + * when it is pushed onto the Kirigami.PagePool. + */ + property var initialProperties + checked: pagePool && pagePool.resolvedUrl(page) == pagePool.lastLoadedUrl onTriggered: { if (page.length == 0 || !pagePool || !pageStack) { return; } if (pagePool.resolvedUrl(page) == pagePool.lastLoadedUrl) { return; } if (!pageStack.hasOwnProperty("pop") || typeof pageStack.pop !== "function" || !pageStack.hasOwnProperty("push") || typeof pageStack.push !== "function") { return; } if (pagePool.isLocalUrl(page)) { if (basePage) { pageStack.pop(basePage); } else { pageStack.clear(); } - pageStack.push(pagePool.loadPage(page)); + + pageStack.push(properties ? + pagePool.loadPageWithProperties(page, properties) : + pagePool.loadPage(page)); } else { pagePool.loadPage(page, function(item) { if (basePage) { pageStack.pop(basePage); } else { pageStack.clear(); } pageStack.push(item); }); } } } diff --git a/src/pagepool.cpp b/src/pagepool.cpp index 8340ffe1..dce8739f 100644 --- a/src/pagepool.cpp +++ b/src/pagepool.cpp @@ -1,253 +1,304 @@ /* * SPDX-FileCopyrightText: 2019 Marco Martin * * SPDX-License-Identifier: LGPL-2.0-or-later */ #include "pagepool.h" #include #include #include #include +#include +#include PagePool::PagePool(QObject *parent) : QObject(parent) { } PagePool::~PagePool() { } QUrl PagePool::lastLoadedUrl() const { return m_lastLoadedUrl; } void PagePool::setCachePages(bool cache) { if (cache == m_cachePages) { return; } if (cache) { for (auto *c : m_componentForUrl.values()) { c->deleteLater(); } m_componentForUrl.clear(); for (auto *i : m_itemForUrl.values()) { // items that had been deparented are safe to delete if (!i->parentItem()) { i->deleteLater(); } QQmlEngine::setObjectOwnership(i, QQmlEngine::JavaScriptOwnership); } m_itemForUrl.clear(); } m_cachePages = cache; emit cachePagesChanged(); } bool PagePool::cachePages() const { return m_cachePages; } QQuickItem *PagePool::loadPage(const QString &url, QJSValue callback) +{ + return loadPageWithProperties(url, QVariantMap(), callback); +} + +QQuickItem *PagePool::loadPageWithProperties( + const QString &url, const QVariantMap &properties, QJSValue callback) { Q_ASSERT(qmlEngine(this)); QQmlContext *ctx = QQmlEngine::contextForObject(this); Q_ASSERT(ctx); const QUrl actualUrl = resolvedUrl(url); QQuickItem *foundItem = nullptr; if (actualUrl == m_lastLoadedUrl && m_lastLoadedItem) { foundItem = m_lastLoadedItem; } else if (m_itemForUrl.contains(actualUrl)) { foundItem = m_itemForUrl[actualUrl]; } if (foundItem) { if (callback.isCallable()) { QJSValueList args = {qmlEngine(this)->newQObject(foundItem)}; callback.call(args); m_lastLoadedUrl = actualUrl; emit lastLoadedUrlChanged(); // We could return the item, but for api coherence return null return nullptr; } else { m_lastLoadedUrl = actualUrl; emit lastLoadedUrlChanged(); return foundItem; } } QQmlComponent *component = m_componentForUrl.value(actualUrl); if (!component) { component = new QQmlComponent(qmlEngine(this), actualUrl, QQmlComponent::PreferSynchronous); } if (component->status() == QQmlComponent::Loading) { if (!callback.isCallable()) { component->deleteLater(); m_componentForUrl.remove(actualUrl); return nullptr; } connect(component, &QQmlComponent::statusChanged, this, - [this, component, callback] (QQmlComponent::Status status) mutable { + [this, component, callback, properties] (QQmlComponent::Status status) mutable { if (status != QQmlComponent::Ready) { qWarning() << component->errors(); m_componentForUrl.remove(component->url()); component->deleteLater(); return; } - QQuickItem *item = createFromComponent(component); + QQuickItem *item = createFromComponent(component, properties); if (item) { QJSValueList args = {qmlEngine(this)->newQObject(item)}; callback.call(args); } if (m_cachePages) { component->deleteLater(); } else { m_componentForUrl[component->url()] = component; } }); return nullptr; } else if (component->status() != QQmlComponent::Ready) { qWarning() << component->errors(); return nullptr; } - QQuickItem *item = createFromComponent(component); + QQuickItem *item = createFromComponent(component, properties); if (m_cachePages) { component->deleteLater(); } else { m_componentForUrl[component->url()] = component; } if (callback.isCallable()) { QJSValueList args = {qmlEngine(this)->newQObject(item)}; callback.call(args); m_lastLoadedUrl = actualUrl; emit lastLoadedUrlChanged(); // We could return the item, but for api coherence return null return nullptr; } else { m_lastLoadedUrl = actualUrl; emit lastLoadedUrlChanged(); return item; } } -QQuickItem *PagePool::createFromComponent(QQmlComponent *component) +// As soon as we can depend on Qt 5.14, usage of this incubator should be +// replaced with a call to QQmlComponent::createWithInitialProperties +class PropertyInitializingIncubator : public QQmlIncubator +{ + const QVariantMap &_props; + QQmlContext *_ctx; + +public: + PropertyInitializingIncubator( + const QVariantMap &props, QQmlContext *ctx, + IncubationMode mode = Asynchronous) + : QQmlIncubator(mode), _props(props), _ctx(ctx) + {} + +protected: + void setInitialState(QObject *obj) override + { + QMapIterator i(_props); + while (i.hasNext()) { + i.next(); + + QQmlProperty p(obj, i.key(), _ctx); + if (!p.isValid()) { + qWarning() << "Invalid property " << i.key(); + continue; + } + if (!p.write(i.value())) { + qWarning() << "Could not set property " << i.key(); + continue; + } + } + } +}; + + +QQuickItem *PagePool::createFromComponent(QQmlComponent *component, const QVariantMap &properties) { QQmlContext *ctx = QQmlEngine::contextForObject(this); Q_ASSERT(ctx); - QObject *obj = component->create(ctx); + PropertyInitializingIncubator incubator(properties, ctx); + component->create(incubator, ctx); + + while (!incubator.isReady()) { + QCoreApplication::processEvents(QEventLoop::AllEvents, 50); + } + + QObject *obj = incubator.object(); + // Error? if (!obj) { return nullptr; } QQuickItem *item = qobject_cast(obj); if (!item) { obj->deleteLater(); return nullptr; } // Always cache just the last one m_lastLoadedItem = item; if (m_cachePages) { QQmlEngine::setObjectOwnership(item, QQmlEngine::CppOwnership); m_itemForUrl[component->url()] = item; } else { QQmlEngine::setObjectOwnership(item, QQmlEngine::JavaScriptOwnership); } return item; } QUrl PagePool::resolvedUrl(const QString &stringUrl) const { Q_ASSERT(qmlEngine(this)); QQmlContext *ctx = QQmlEngine::contextForObject(this); Q_ASSERT(ctx); QUrl actualUrl(stringUrl); if (actualUrl.scheme().isEmpty()) { actualUrl = ctx->resolvedUrl(actualUrl); } return actualUrl; } bool PagePool::isLocalUrl(const QUrl &url) { return url.isLocalFile() || url.scheme().isEmpty() || url.scheme() == QStringLiteral("qrc"); } QUrl PagePool::urlForPage(QQuickItem *item) const { return m_urlForItem.value(item); } bool PagePool::contains(const QVariant &page) const { if (page.canConvert()) { return m_urlForItem.contains(page.value()); } else if (page.canConvert()) { const QUrl actualUrl = resolvedUrl(page.value()); return m_itemForUrl.contains(actualUrl); } else { return false; } } void PagePool::deletePage(const QVariant &page) { if (!contains(page)) { return; } QQuickItem *item; if (page.canConvert()) { item = page.value(); } else if (page.canConvert()) { QString url = page.value(); if (url.isEmpty()) { return; } const QUrl actualUrl = resolvedUrl(page.value()); item = m_itemForUrl.value(actualUrl); } else { return; } if (!item) { return; } const QUrl url = m_urlForItem.value(item); if (url.isEmpty()) { return; } m_itemForUrl.remove(url); m_urlForItem.remove(item); item->deleteLater(); } #include "moc_pagepool.cpp" diff --git a/src/pagepool.h b/src/pagepool.h index 27f719a5..175e6acc 100644 --- a/src/pagepool.h +++ b/src/pagepool.h @@ -1,102 +1,105 @@ /* * SPDX-FileCopyrightText: 2019 Marco Martin * * SPDX-License-Identifier: LGPL-2.0-or-later */ #pragma once #include #include #include /** * A Pool of Page items, pages will be unique per url and the items * will be kept around unless explicitly deleted. * Instaces are C++ owned and can be deleted only manually using deletePage() * Instance are unique per url: if you need 2 different instance for a page * url, you should instantiate them in the traditional way * or use a different PagePool instance. */ class PagePool : public QObject { Q_OBJECT /** * The last url that was loaded with @loadPage. Useful if you need * to have a "checked" state to buttons or list items that * load the page when clicked. */ Q_PROPERTY(QUrl lastLoadedUrl READ lastLoadedUrl NOTIFY lastLoadedUrlChanged) /** * If true (default) the pages will be kept around, will have C++ ownership and only one instance per page will be created. * If false the pages will have Javascript ownership (thus deleted on pop by the page stacks) and each call to loadPage will create a new page instance. When cachePages is false, Components gets cached never the less */ Q_PROPERTY(bool cachePages READ cachePages WRITE setCachePages NOTIFY cachePagesChanged) public: PagePool(QObject *parent = nullptr); ~PagePool(); QUrl lastLoadedUrl() const; void setCachePages(bool cache); bool cachePages() const; /** * Returns the instance of the item defined in the QML file identified * by url, only one instance will be made per url if cachePAges is true. If the url is remote (i.e. http) don't rely on the return value but us the async callback instead * @param url full url of the item: it can be a well formed Url, * an absolute path * or a relative one to the path of the qml file the PagePool is instantiated from * @param callback If we are loading a remote url, we can't have the item immediately but will be passed as a parameter to the provided callback. * Normally, don't set a callback, use it only in case of remote urls. * @returns the page instance that will have been created if necessary. * If the url is remote it will return null, * as well will return null if the callback has been provided */ Q_INVOKABLE QQuickItem *loadPage(const QString &url, QJSValue callback = QJSValue()); + Q_INVOKABLE QQuickItem *loadPageWithProperties( + const QString &url, const QVariantMap &properties, QJSValue callback = QJSValue()); + /** * @returns The url of the page for the given instance, empty if there is no correspondence */ Q_INVOKABLE QUrl urlForPage(QQuickItem *item) const; /** * @returns true if the is managed by the PagePool * @param the page can be either a QQuickItem or an url */ Q_INVOKABLE bool contains(const QVariant &page) const; /** * Deletes the page (only if is managed by the pool. * @param page either the url or the instance of the page */ Q_INVOKABLE void deletePage(const QVariant &page); /** * @returns full url from an absolute or relative path */ Q_INVOKABLE QUrl resolvedUrl(const QString &file) const; /** * @returns true if the url identifies a local resource (local file or a file inside Qt's resource system). * False if the url points to a network location */ Q_INVOKABLE bool isLocalUrl(const QUrl &url); Q_SIGNALS: void lastLoadedUrlChanged(); void cachePagesChanged(); private: - QQuickItem *createFromComponent(QQmlComponent *component); + QQuickItem *createFromComponent(QQmlComponent *component, const QVariantMap &properties); QUrl m_lastLoadedUrl; QPointer m_lastLoadedItem; QHash m_itemForUrl; QHash m_componentForUrl; QHash m_urlForItem; bool m_cachePages = true; };