| 1 | /* |
| 2 | * SPDX-FileCopyrightText: 2019 Marco Martin <mart@kde.org> |
| 3 | * |
| 4 | * SPDX-License-Identifier: LGPL-2.0-or-later |
| 5 | */ |
| 6 | |
| 7 | #include "pagepool.h" |
| 8 | |
| 9 | #include <QDebug> |
| 10 | #include <QQmlComponent> |
| 11 | #include <QQmlContext> |
| 12 | #include <QQmlEngine> |
| 13 | #include <QQmlProperty> |
| 14 | |
| 15 | #include "loggingcategory.h" |
| 16 | |
| 17 | PagePool::PagePool(QObject *parent) |
| 18 | : QObject(parent) |
| 19 | { |
| 20 | } |
| 21 | |
| 22 | PagePool::~PagePool() |
| 23 | { |
| 24 | } |
| 25 | |
| 26 | QUrl PagePool::lastLoadedUrl() const |
| 27 | { |
| 28 | return m_lastLoadedUrl; |
| 29 | } |
| 30 | |
| 31 | QQuickItem *PagePool::lastLoadedItem() const |
| 32 | { |
| 33 | return m_lastLoadedItem; |
| 34 | } |
| 35 | |
| 36 | QList<QQuickItem *> PagePool::items() const |
| 37 | { |
| 38 | return m_itemForUrl.values(); |
| 39 | } |
| 40 | |
| 41 | QList<QUrl> PagePool::urls() const |
| 42 | { |
| 43 | return m_urlForItem.values(); |
| 44 | } |
| 45 | |
| 46 | void PagePool::setCachePages(bool cache) |
| 47 | { |
| 48 | if (cache == m_cachePages) { |
| 49 | return; |
| 50 | } |
| 51 | |
| 52 | if (cache) { |
| 53 | clear(); |
| 54 | } |
| 55 | |
| 56 | m_cachePages = cache; |
| 57 | Q_EMIT cachePagesChanged(); |
| 58 | } |
| 59 | |
| 60 | bool PagePool::cachePages() const |
| 61 | { |
| 62 | return m_cachePages; |
| 63 | } |
| 64 | |
| 65 | QQuickItem *PagePool::loadPage(const QString &url, QJSValue callback) |
| 66 | { |
| 67 | return loadPageWithProperties(url, properties: QVariantMap(), callback); |
| 68 | } |
| 69 | |
| 70 | QQuickItem *PagePool::loadPageWithProperties(const QString &url, const QVariantMap &properties, QJSValue callback) |
| 71 | { |
| 72 | const auto engine = qmlEngine(this); |
| 73 | Q_ASSERT(engine); |
| 74 | |
| 75 | const QUrl actualUrl = resolvedUrl(file: url); |
| 76 | |
| 77 | auto found = m_itemForUrl.find(key: actualUrl); |
| 78 | if (found != m_itemForUrl.end()) { |
| 79 | m_lastLoadedUrl = found.key(); |
| 80 | m_lastLoadedItem = found.value(); |
| 81 | |
| 82 | if (callback.isCallable()) { |
| 83 | QJSValueList args = {engine->newQObject(object: found.value())}; |
| 84 | callback.call(args); |
| 85 | Q_EMIT lastLoadedUrlChanged(); |
| 86 | Q_EMIT lastLoadedItemChanged(); |
| 87 | // We could return the item, but for api coherence return null |
| 88 | return nullptr; |
| 89 | |
| 90 | } else { |
| 91 | Q_EMIT lastLoadedUrlChanged(); |
| 92 | Q_EMIT lastLoadedItemChanged(); |
| 93 | return found.value(); |
| 94 | } |
| 95 | } |
| 96 | |
| 97 | QQmlComponent *component = m_componentForUrl.value(key: actualUrl); |
| 98 | |
| 99 | if (!component) { |
| 100 | component = new QQmlComponent(engine, actualUrl, QQmlComponent::PreferSynchronous); |
| 101 | } |
| 102 | |
| 103 | if (component->status() == QQmlComponent::Loading) { |
| 104 | if (!callback.isCallable()) { |
| 105 | component->deleteLater(); |
| 106 | m_componentForUrl.remove(key: actualUrl); |
| 107 | return nullptr; |
| 108 | } |
| 109 | |
| 110 | connect(sender: component, signal: &QQmlComponent::statusChanged, context: this, slot: [this, engine, component, callback, properties](QQmlComponent::Status status) mutable { |
| 111 | if (status != QQmlComponent::Ready) { |
| 112 | qCWarning(KirigamiLog) << component->errors(); |
| 113 | m_componentForUrl.remove(key: component->url()); |
| 114 | component->deleteLater(); |
| 115 | return; |
| 116 | } |
| 117 | QQuickItem *item = createFromComponent(component, properties); |
| 118 | if (item) { |
| 119 | QJSValueList args = {engine->newQObject(object: item)}; |
| 120 | callback.call(args); |
| 121 | } |
| 122 | |
| 123 | if (m_cachePages) { |
| 124 | component->deleteLater(); |
| 125 | } else { |
| 126 | m_componentForUrl[component->url()] = component; |
| 127 | } |
| 128 | }); |
| 129 | |
| 130 | return nullptr; |
| 131 | |
| 132 | } else if (component->status() != QQmlComponent::Ready) { |
| 133 | qCWarning(KirigamiLog) << component->errors(); |
| 134 | return nullptr; |
| 135 | } |
| 136 | |
| 137 | QQuickItem *item = createFromComponent(component, properties); |
| 138 | if (!item) { |
| 139 | return nullptr; |
| 140 | } |
| 141 | |
| 142 | if (m_cachePages) { |
| 143 | component->deleteLater(); |
| 144 | QQmlEngine::setObjectOwnership(item, QQmlEngine::CppOwnership); |
| 145 | m_itemForUrl[component->url()] = item; |
| 146 | m_urlForItem[item] = component->url(); |
| 147 | Q_EMIT itemsChanged(); |
| 148 | Q_EMIT urlsChanged(); |
| 149 | |
| 150 | } else { |
| 151 | m_componentForUrl[component->url()] = component; |
| 152 | QQmlEngine::setObjectOwnership(item, QQmlEngine::JavaScriptOwnership); |
| 153 | } |
| 154 | |
| 155 | m_lastLoadedUrl = actualUrl; |
| 156 | m_lastLoadedItem = item; |
| 157 | Q_EMIT lastLoadedUrlChanged(); |
| 158 | Q_EMIT lastLoadedItemChanged(); |
| 159 | |
| 160 | if (callback.isCallable()) { |
| 161 | QJSValueList args = {engine->newQObject(object: item)}; |
| 162 | callback.call(args); |
| 163 | // We could return the item, but for api coherence return null |
| 164 | return nullptr; |
| 165 | } |
| 166 | return item; |
| 167 | } |
| 168 | |
| 169 | QQuickItem *PagePool::createFromComponent(QQmlComponent *component, const QVariantMap &properties) |
| 170 | { |
| 171 | const auto ctx = qmlContext(this); |
| 172 | Q_ASSERT(ctx); |
| 173 | |
| 174 | QObject *obj = component->createWithInitialProperties(initialProperties: properties, context: ctx); |
| 175 | |
| 176 | if (!obj || component->isError()) { |
| 177 | qCWarning(KirigamiLog) << component->errors(); |
| 178 | if (obj) { |
| 179 | obj->deleteLater(); |
| 180 | } |
| 181 | return nullptr; |
| 182 | } |
| 183 | |
| 184 | QQuickItem *item = qobject_cast<QQuickItem *>(o: obj); |
| 185 | if (!item) { |
| 186 | qCWarning(KirigamiLog) << "Storing Non-QQuickItem in PagePool not supported" ; |
| 187 | obj->deleteLater(); |
| 188 | return nullptr; |
| 189 | } |
| 190 | |
| 191 | return item; |
| 192 | } |
| 193 | |
| 194 | QUrl PagePool::resolvedUrl(const QString &stringUrl) const |
| 195 | { |
| 196 | const auto ctx = qmlContext(this); |
| 197 | Q_ASSERT(ctx); |
| 198 | |
| 199 | QUrl actualUrl(stringUrl); |
| 200 | if (actualUrl.scheme().isEmpty()) { |
| 201 | actualUrl = ctx->resolvedUrl(actualUrl); |
| 202 | } |
| 203 | return actualUrl; |
| 204 | } |
| 205 | |
| 206 | bool PagePool::isLocalUrl(const QUrl &url) |
| 207 | { |
| 208 | return url.isLocalFile() || url.scheme().isEmpty() || url.scheme() == QStringLiteral("qrc" ); |
| 209 | } |
| 210 | |
| 211 | QUrl PagePool::urlForPage(QQuickItem *item) const |
| 212 | { |
| 213 | return m_urlForItem.value(key: item); |
| 214 | } |
| 215 | |
| 216 | QQuickItem *PagePool::pageForUrl(const QUrl &url) const |
| 217 | { |
| 218 | return m_itemForUrl.value(key: resolvedUrl(stringUrl: url.toString()), defaultValue: nullptr); |
| 219 | } |
| 220 | |
| 221 | bool PagePool::contains(const QVariant &page) const |
| 222 | { |
| 223 | if (page.canConvert<QQuickItem *>()) { |
| 224 | return m_urlForItem.contains(key: page.value<QQuickItem *>()); |
| 225 | |
| 226 | } else if (page.canConvert<QString>()) { |
| 227 | const QUrl actualUrl = resolvedUrl(stringUrl: page.value<QString>()); |
| 228 | return m_itemForUrl.contains(key: actualUrl); |
| 229 | |
| 230 | } else { |
| 231 | return false; |
| 232 | } |
| 233 | } |
| 234 | |
| 235 | void PagePool::deletePage(const QVariant &page) |
| 236 | { |
| 237 | if (!contains(page)) { |
| 238 | return; |
| 239 | } |
| 240 | |
| 241 | QQuickItem *item; |
| 242 | if (page.canConvert<QQuickItem *>()) { |
| 243 | item = page.value<QQuickItem *>(); |
| 244 | } else if (page.canConvert<QString>()) { |
| 245 | QString url = page.value<QString>(); |
| 246 | if (url.isEmpty()) { |
| 247 | return; |
| 248 | } |
| 249 | const QUrl actualUrl = resolvedUrl(stringUrl: page.value<QString>()); |
| 250 | |
| 251 | item = m_itemForUrl.value(key: actualUrl); |
| 252 | } else { |
| 253 | return; |
| 254 | } |
| 255 | |
| 256 | if (!item) { |
| 257 | return; |
| 258 | } |
| 259 | |
| 260 | const QUrl url = m_urlForItem.value(key: item); |
| 261 | |
| 262 | if (url.isEmpty()) { |
| 263 | return; |
| 264 | } |
| 265 | |
| 266 | m_itemForUrl.remove(key: url); |
| 267 | m_urlForItem.remove(key: item); |
| 268 | item->deleteLater(); |
| 269 | |
| 270 | Q_EMIT itemsChanged(); |
| 271 | Q_EMIT urlsChanged(); |
| 272 | } |
| 273 | |
| 274 | void PagePool::clear() |
| 275 | { |
| 276 | for (const auto &component : std::as_const(t&: m_componentForUrl)) { |
| 277 | component->deleteLater(); |
| 278 | } |
| 279 | m_componentForUrl.clear(); |
| 280 | |
| 281 | for (const auto &item : std::as_const(t&: m_itemForUrl)) { |
| 282 | // items that had been deparented are safe to delete |
| 283 | if (!item->parentItem()) { |
| 284 | item->deleteLater(); |
| 285 | } |
| 286 | QQmlEngine::setObjectOwnership(item, QQmlEngine::JavaScriptOwnership); |
| 287 | } |
| 288 | m_itemForUrl.clear(); |
| 289 | m_urlForItem.clear(); |
| 290 | m_lastLoadedUrl = QUrl(); |
| 291 | m_lastLoadedItem = nullptr; |
| 292 | |
| 293 | Q_EMIT lastLoadedUrlChanged(); |
| 294 | Q_EMIT lastLoadedItemChanged(); |
| 295 | Q_EMIT itemsChanged(); |
| 296 | Q_EMIT urlsChanged(); |
| 297 | } |
| 298 | |
| 299 | #include "moc_pagepool.cpp" |
| 300 | |