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