File indexing completed on 2024-04-28 11:42:22

0001 /*
0002  *  SPDX-FileCopyrightText: 2011 Marco Martin <mart@kde.org>
0003  *  SPDX-FileCopyrightText: 2014 Aleix Pol Gonzalez <aleixpol@blue-systems.com>
0004  *
0005  *  SPDX-License-Identifier: LGPL-2.0-or-later
0006  */
0007 
0008 #include "delegaterecycler.h"
0009 
0010 #include "loggingcategory.h"
0011 #include <QDebug>
0012 #include <QQmlComponent>
0013 #include <QQmlContext>
0014 #include <QQmlEngine>
0015 
0016 DelegateRecyclerAttached::DelegateRecyclerAttached(QObject *parent)
0017     : QObject(parent)
0018 {
0019 }
0020 
0021 DelegateRecyclerAttached::~DelegateRecyclerAttached()
0022 {
0023 }
0024 /*
0025 void setRecycler(DelegateRecycler *recycler)
0026 {
0027     m_recycler = recycler;
0028 }
0029 
0030 DelegateRecycler *recycler() const
0031 {
0032     return m_recycler;
0033 }
0034 */
0035 
0036 class DelegateCache
0037 {
0038 public:
0039     DelegateCache();
0040     ~DelegateCache();
0041 
0042     void ref(QQmlComponent *);
0043     void deref(QQmlComponent *);
0044 
0045     void insert(QQmlComponent *, QQuickItem *);
0046     QQuickItem *take(QQmlComponent *);
0047 
0048 private:
0049     static const int s_cacheSize = 40;
0050     QHash<QQmlComponent *, int> m_refs;
0051     QHash<QQmlComponent *, QList<QQuickItem *>> m_unusedItems;
0052 };
0053 
0054 Q_GLOBAL_STATIC(DelegateCache, s_delegateCache)
0055 
0056 DelegateCache::DelegateCache()
0057 {
0058 }
0059 
0060 DelegateCache::~DelegateCache()
0061 {
0062     for (auto &item : std::as_const(m_unusedItems)) {
0063         qDeleteAll(item);
0064     }
0065 }
0066 
0067 void DelegateCache::ref(QQmlComponent *component)
0068 {
0069     m_refs[component]++;
0070 }
0071 
0072 void DelegateCache::deref(QQmlComponent *component)
0073 {
0074     auto itRef = m_refs.find(component);
0075     if (itRef == m_refs.end()) {
0076         return;
0077     }
0078 
0079     (*itRef)--;
0080     if (*itRef <= 0) {
0081         m_refs.erase(itRef);
0082 
0083         qDeleteAll(m_unusedItems.take(component));
0084     }
0085 }
0086 
0087 void DelegateCache::insert(QQmlComponent *component, QQuickItem *item)
0088 {
0089     auto &items = m_unusedItems[component];
0090     if (items.length() >= s_cacheSize) {
0091         item->deleteLater();
0092         return;
0093     }
0094 
0095     DelegateRecyclerAttached *attached = qobject_cast<DelegateRecyclerAttached *>(qmlAttachedPropertiesObject<DelegateRecycler>(item, false));
0096     if (attached) {
0097         Q_EMIT attached->pooled();
0098     }
0099 
0100     item->setParentItem(nullptr);
0101     items.append(item);
0102 }
0103 
0104 QQuickItem *DelegateCache::take(QQmlComponent *component)
0105 {
0106     auto it = m_unusedItems.find(component);
0107     if (it != m_unusedItems.end() && !it->isEmpty()) {
0108         return it->takeFirst();
0109     }
0110     return nullptr;
0111 }
0112 
0113 DelegateRecycler::DelegateRecycler(QQuickItem *parent)
0114     : QQuickItem(parent)
0115 {
0116     setFlags(QQuickItem::ItemIsFocusScope);
0117 }
0118 
0119 DelegateRecycler::~DelegateRecycler()
0120 {
0121     if (m_sourceComponent) {
0122         s_delegateCache->insert(m_sourceComponent, m_item);
0123         s_delegateCache->deref(m_sourceComponent);
0124     }
0125 }
0126 
0127 void DelegateRecycler::syncIndex()
0128 {
0129     const QVariant newIndex = m_propertiesTracker->property("trackedIndex");
0130     if (!m_item || !newIndex.isValid()) {
0131         return;
0132     }
0133     QQmlContext *ctx = QQmlEngine::contextForObject(m_item)->parentContext();
0134     ctx->setContextProperty(QStringLiteral("index"), newIndex);
0135 }
0136 
0137 void DelegateRecycler::syncModel()
0138 {
0139     const QVariant newModel = m_propertiesTracker->property("trackedModel");
0140     if (!m_item || !newModel.isValid()) {
0141         return;
0142     }
0143     QQmlContext *ctx = QQmlEngine::contextForObject(m_item)->parentContext();
0144     ctx->setContextProperty(QStringLiteral("model"), newModel);
0145 
0146     // try to bind all properties
0147     QObject *modelObj = newModel.value<QObject *>();
0148     if (modelObj) {
0149         const QMetaObject *metaObj = modelObj->metaObject();
0150         for (int i = metaObj->propertyOffset(); i < metaObj->propertyCount(); ++i) {
0151             ctx->setContextProperty(QString::fromUtf8(metaObj->property(i).name()), metaObj->property(i).read(modelObj));
0152         }
0153     }
0154 }
0155 
0156 void DelegateRecycler::syncModelProperties()
0157 {
0158     const QVariant model = m_propertiesTracker->property("trackedModel");
0159     if (!m_item || !model.isValid()) {
0160         return;
0161     }
0162     QQmlContext *ctx = QQmlEngine::contextForObject(m_item)->parentContext();
0163 
0164     // try to bind all properties
0165     QObject *modelObj = model.value<QObject *>();
0166     if (modelObj) {
0167         const QMetaObject *metaObj = modelObj->metaObject();
0168         for (int i = metaObj->propertyOffset(); i < metaObj->propertyCount(); ++i) {
0169             ctx->setContextProperty(QString::fromUtf8(metaObj->property(i).name()), metaObj->property(i).read(modelObj));
0170         }
0171     }
0172 }
0173 
0174 void DelegateRecycler::syncModelData()
0175 {
0176     const QVariant newModelData = m_propertiesTracker->property("trackedModelData");
0177     if (!m_item || !newModelData.isValid()) {
0178         return;
0179     }
0180     QQmlContext *ctx = QQmlEngine::contextForObject(m_item)->parentContext();
0181     ctx->setContextProperty(QStringLiteral("modelData"), newModelData);
0182 }
0183 
0184 QQmlComponent *DelegateRecycler::sourceComponent() const
0185 {
0186     return m_sourceComponent;
0187 }
0188 
0189 void DelegateRecycler::setSourceComponent(QQmlComponent *component)
0190 {
0191     if (component && component->parent() == this) {
0192         qCWarning(KirigamiLog) << "Error: source components cannot be declared inside DelegateRecycler";
0193         return;
0194     }
0195     if (m_sourceComponent == component) {
0196         return;
0197     }
0198 
0199     if (!m_propertiesTracker) {
0200         static QHash<QQmlEngine *, QQmlComponent *> propertiesTrackerComponent;
0201         auto engine = qmlEngine(this);
0202         auto it = propertiesTrackerComponent.find(engine);
0203         if (it == propertiesTrackerComponent.end()) {
0204             connect(engine, &QObject::destroyed, engine, [engine] {
0205                 propertiesTrackerComponent.remove(engine);
0206             });
0207             it = propertiesTrackerComponent.insert(engine, new QQmlComponent(engine, engine));
0208 
0209             /* clang-format off */
0210             (*it)->setData(QByteArrayLiteral(R"(
0211 import QtQuick 2.3
0212 QtObject {
0213     property int trackedIndex: index
0214     property var trackedModel: typeof model != 'undefined' ? model : null
0215     property var trackedModelData: typeof modelData != 'undefined' ? modelData : null
0216 }
0217 )"), QUrl(QStringLiteral("delegaterecycler.cpp")));
0218         }
0219         /* clang-format on */
0220         m_propertiesTracker = (*it)->create(QQmlEngine::contextForObject(this));
0221 
0222         connect(m_propertiesTracker, SIGNAL(trackedIndexChanged()), this, SLOT(syncIndex()));
0223         connect(m_propertiesTracker, SIGNAL(trackedModelChanged()), this, SLOT(syncModel()));
0224         connect(m_propertiesTracker, SIGNAL(trackedModelDataChanged()), this, SLOT(syncModelData()));
0225     }
0226 
0227     if (m_sourceComponent) {
0228         if (m_item) {
0229             disconnect(m_item.data(), &QQuickItem::implicitWidthChanged, this, &DelegateRecycler::updateHints);
0230             disconnect(m_item.data(), &QQuickItem::implicitHeightChanged, this, &DelegateRecycler::updateHints);
0231             s_delegateCache->insert(component, m_item);
0232         }
0233         s_delegateCache->deref(component);
0234     }
0235 
0236     m_sourceComponent = component;
0237     s_delegateCache->ref(component);
0238 
0239     m_item = s_delegateCache->take(component);
0240 
0241     if (!m_item) {
0242         QQuickItem *candidate = parentItem();
0243         QQmlContext *ctx = nullptr;
0244         if (component->creationContext()) {
0245             ctx = new QQmlContext(component->creationContext());
0246         }
0247         while (!ctx && candidate) {
0248             QQmlContext *parentCtx = QQmlEngine::contextForObject(candidate);
0249             if (parentCtx) {
0250                 ctx = new QQmlContext(parentCtx, candidate);
0251                 break;
0252             } else {
0253                 candidate = candidate->parentItem();
0254             }
0255         }
0256 
0257         Q_ASSERT(ctx);
0258 
0259         QObject *contextObjectToSet = nullptr;
0260         {
0261             // Find the first parent that has a context object with a valid translationDomain property, i.e. is a KLocalizedContext
0262             QQmlContext *auxCtx = ctx;
0263             while (auxCtx != nullptr) {
0264                 QObject *auxCtxObj = auxCtx->contextObject();
0265                 if (auxCtxObj && auxCtxObj->property("translationDomain").isValid()) {
0266                     contextObjectToSet = auxCtxObj;
0267                     break;
0268                 }
0269                 auxCtx = auxCtx->parentContext();
0270             }
0271         }
0272         if (contextObjectToSet) {
0273             ctx->setContextObject(contextObjectToSet);
0274         }
0275 
0276         QObject *modelObj = m_propertiesTracker->property("trackedModel").value<QObject *>();
0277         if (modelObj) {
0278             const QMetaObject *metaObj = modelObj->metaObject();
0279             for (int i = metaObj->propertyOffset(); i < metaObj->propertyCount(); ++i) {
0280                 QMetaProperty prop = metaObj->property(i);
0281                 ctx->setContextProperty(QString::fromUtf8(prop.name()), prop.read(modelObj));
0282                 if (prop.hasNotifySignal()) {
0283                     QMetaMethod updateSlot = metaObject()->method(metaObject()->indexOfSlot("syncModelProperties()"));
0284                     connect(modelObj, prop.notifySignal(), this, updateSlot);
0285                 }
0286             }
0287         }
0288 
0289         ctx->setContextProperty(QStringLiteral("model"), m_propertiesTracker->property("trackedModel"));
0290         ctx->setContextProperty(QStringLiteral("modelData"), m_propertiesTracker->property("trackedModelData"));
0291         ctx->setContextProperty(QStringLiteral("index"), m_propertiesTracker->property("trackedIndex"));
0292         ctx->setContextProperty(QStringLiteral("delegateRecycler"), this);
0293 
0294         QObject *obj = component->create(ctx);
0295         m_item = qobject_cast<QQuickItem *>(obj);
0296         if (!m_item) {
0297             obj->deleteLater();
0298         } else {
0299             connect(m_item.data(), &QObject::destroyed, ctx, &QObject::deleteLater);
0300             // if the user binded an explicit width, consider it, otherwise base upon implicit
0301             m_widthFromItem = m_item->width() > 0 && m_item->width() != m_item->implicitWidth();
0302             m_heightFromItem = m_item->height() > 0 && m_item->height() != m_item->implicitHeight();
0303 
0304             if (m_widthFromItem && m_heightFromItem) {
0305                 connect(m_item.data(), &QQuickItem::heightChanged, this, [this]() {
0306                     updateSize(false);
0307                 });
0308             }
0309         }
0310     } else {
0311         syncModel();
0312 
0313         QQmlContext *ctx = QQmlEngine::contextForObject(m_item)->parentContext();
0314         ctx->setContextProperties({QQmlContext::PropertyPair{QStringLiteral("modelData"), m_propertiesTracker->property("trackedModelData")},
0315                                    QQmlContext::PropertyPair{QStringLiteral("index"), m_propertiesTracker->property("trackedIndex")},
0316                                    QQmlContext::PropertyPair{QStringLiteral("delegateRecycler"), QVariant::fromValue<QObject *>(this)}});
0317 
0318         DelegateRecyclerAttached *attached = qobject_cast<DelegateRecyclerAttached *>(qmlAttachedPropertiesObject<DelegateRecycler>(m_item, false));
0319         if (attached) {
0320             Q_EMIT attached->reused();
0321         }
0322     }
0323 
0324     if (m_item) {
0325         m_item->setParentItem(this);
0326         connect(m_item.data(), &QQuickItem::implicitWidthChanged, this, &DelegateRecycler::updateHints);
0327         connect(m_item.data(), &QQuickItem::implicitHeightChanged, this, &DelegateRecycler::updateHints);
0328 
0329         updateSize(true);
0330     }
0331 
0332     Q_EMIT sourceComponentChanged();
0333 }
0334 
0335 void DelegateRecycler::resetSourceComponent()
0336 {
0337     s_delegateCache->deref(m_sourceComponent);
0338     m_sourceComponent = nullptr;
0339 }
0340 
0341 DelegateRecyclerAttached *DelegateRecycler::qmlAttachedProperties(QObject *object)
0342 {
0343     return new DelegateRecyclerAttached(object);
0344 }
0345 
0346 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0347 void DelegateRecycler::geometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry)
0348 #else
0349 void DelegateRecycler::geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry)
0350 #endif
0351 {
0352     if (m_item && newGeometry.size() != oldGeometry.size()) {
0353         updateSize(true);
0354     }
0355 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0356     QQuickItem::geometryChanged(newGeometry, oldGeometry);
0357 #else
0358     QQuickItem::geometryChange(newGeometry, oldGeometry);
0359 #endif
0360 }
0361 
0362 void DelegateRecycler::focusInEvent(QFocusEvent *event)
0363 {
0364     QQuickItem::focusInEvent(event);
0365     if (!m_item) {
0366         return;
0367     }
0368 
0369     m_item->setFocus(event->reason());
0370 }
0371 
0372 void DelegateRecycler::updateHints()
0373 {
0374     updateSize(false);
0375 }
0376 
0377 void DelegateRecycler::updateSize(bool parentResized)
0378 {
0379     if (!m_item) {
0380         return;
0381     }
0382 
0383     const bool needToUpdateWidth = !m_widthFromItem && parentResized && widthValid();
0384     const bool needToUpdateHeight = !m_heightFromItem && parentResized && heightValid();
0385 
0386     if (parentResized) {
0387         m_item->setPosition(QPoint(0, 0));
0388     }
0389     if (needToUpdateWidth && needToUpdateHeight) {
0390         m_item->setSize(QSizeF(width(), height()));
0391     } else if (needToUpdateWidth) {
0392         m_item->setWidth(width());
0393     } else if (needToUpdateHeight) {
0394         m_item->setHeight(height());
0395     }
0396 
0397     if (m_updatingSize) {
0398         return;
0399     }
0400 
0401     m_updatingSize = true;
0402 
0403     if (m_heightFromItem) {
0404         setHeight(m_item->height());
0405     }
0406     if (m_widthFromItem) {
0407         setWidth(m_item->width());
0408     }
0409 
0410     setImplicitSize(m_item->implicitWidth() >= 0 ? m_item->implicitWidth() : m_item->width(),
0411                     m_item->implicitHeight() >= 0 ? m_item->implicitHeight() : m_item->height());
0412 
0413     m_updatingSize = false;
0414 }
0415 
0416 #include "moc_delegaterecycler.cpp"