diff --git a/src/contextadapterfactory.cpp b/src/contextadapterfactory.cpp index ea02651..0cc9d7e 100644 --- a/src/contextadapterfactory.cpp +++ b/src/contextadapterfactory.cpp @@ -1,709 +1,724 @@ /*************************************************************************** * Copyright (C) 2018 by Emmanuel Lepage Vallee * * Author : Emmanuel Lepage Vallee * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 3 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * **************************************************************************/ #include "contextadapterfactory.h" // Qt #include #include #include #include #include #include // KQuickItemViews #include "adapters/abstractitemadapter.h" #include "viewbase.h" #include "viewport.h" #include "private/viewport_p.h" #include "private/indexmetadata_p.h" #include "extensions/contextextension.h" #include "adapters/modeladapter.h" #include "private/statetracker/viewitem_p.h" #include "adapters/contextadapter.h" using FactoryFunctor = std::function; /** * Add some metadata to be able to use QAbstractItemModel roles as QObject * properties. */ struct MetaProperty { // Bitmask to hold (future) metadata regarding how the property is used // by QML. enum Flags : int { UNUSED = 0x0 << 0, /*!< This property was never used */ READ = 0x1 << 0, /*!< If data() was ever called */ HAS_DATA = 0x1 << 1, /*!< When the QVariant is valid */ TRIED_WRITE = 0x1 << 2, /*!< When setData returns false */ HAS_WRITTEN = 0x1 << 3, /*!< When setData returns true */ HAS_CHANGED = 0x1 << 4, /*!< When the value was queried many times */ HAS_SUBSET = 0x1 << 5, /*!< When dataChanged has a role list */ HAS_GLOBAL = 0x1 << 6, /*!< When dataChanged has no role */ IS_ROLE = 0x1 << 7, /*!< When the MetaProperty map to a model role */ }; int flags {Flags::UNUSED}; /// Q_PROPERTY internal id int propId; /// The role ID from QAbstractItemModel::roleNames int roleId {-1}; /** * The name ID from QAbstractItemModel::roleNames * * (the pointer *is* on purpose reduce cache faults in this hot code path) */ QByteArray* name {nullptr}; uint signalId; }; struct GroupMetaData { ContextExtension* ptr; uint offset; }; /** * Keep the offset and id of the extension for validation and change notification. */ class ContextExtensionPrivate { public: uint m_Id {0}; uint m_Offset {0}; ContextAdapterFactoryPrivate *d_ptr {nullptr}; }; /** * This struct is the internal representation normally built by the Qt MOC * generator. * * It holds a "fake" type of QObject designed to reflect the model roles as * QObject properties. It also tracks the property being *used* by QML to * prevent too many events being pushed into the QML context. */ struct DynamicMetaType final { explicit DynamicMetaType(const QHash& roles); ~DynamicMetaType() { delete[] roles;//FIXME leak [roleCount*sizeof(MetaProperty))]; } const size_t roleCount { 0 }; size_t propertyCount { 0 }; MetaProperty* roles {nullptr}; - QSet used { }; + QSet m_lUsed { }; QMetaObject *m_pMetaObject {nullptr}; bool m_GroupInit { false }; QHash m_hRoleIds { }; uint8_t *m_pCacheMap {nullptr}; /** * Assuming the number of role is never *that* high, keep a jump map to * prevent doing dispatch vTable when checking the properties source. * * In theory it can be changed at runtime if the need arise, but for now * its static. Better harden the binaries a bit, having call maps on the * heap isn't the most secure scheme in the universe. */ GroupMetaData* m_lGroupMapping {nullptr}; }; class DynamicContext final : public QObject { public: explicit DynamicContext(ContextAdapterFactory* mt); virtual ~DynamicContext(); // Some "secret" QObject methods. virtual int qt_metacall(QMetaObject::Call call, int id, void **argv) override; virtual void* qt_metacast(const char *name) override; virtual const QMetaObject *metaObject() const override; // Use a C array to prevent the array bound checks QVariant **m_lVariants {nullptr}; DynamicMetaType * m_pMetaType {nullptr}; bool m_Cache { true }; QQmlContext * m_pCtx {nullptr}; QPersistentModelIndex m_Index { }; QQmlContext * m_pParentCtx{nullptr}; QMetaObject::Connection m_Conn; - QMutex m_Mutex; ContextAdapterFactoryPrivate* d_ptr {nullptr}; ContextAdapter* m_pBuilder; }; class ContextAdapterFactoryPrivate { public: // Attributes QList m_lGroups { }; mutable DynamicMetaType *m_pMetaType {nullptr}; QAbstractItemModel *m_pModel {nullptr}; FactoryFunctor m_fFactory; // Helper void initGroup(const QHash& rls); void finish(); ContextAdapterFactory* q_ptr; }; /** * Create a group of virtual Q_PROPERTY to match the model role names. */ class RoleGroup final : public ContextExtension { public: explicit RoleGroup(ContextAdapterFactoryPrivate* d) : d_ptr(d) {} // The implementation isn't necessary in this case given it uses a second // layer of vTable instead of a static list. It goes against the // documentation, but that's on purpose. virtual QVariant getProperty(AbstractItemAdapter* item, uint id, const QModelIndex& index) const override; virtual uint size() const override; virtual QByteArray getPropertyName(uint id) const override; + virtual bool setProperty(AbstractItemAdapter* item, uint id, const QVariant& value, const QModelIndex& index) const; + ContextAdapterFactoryPrivate* d_ptr; }; ContextAdapterFactory::ContextAdapterFactory(FactoryFunctor f) : d_ptr(new ContextAdapterFactoryPrivate()) { d_ptr->q_ptr = this; d_ptr->m_fFactory = f; addContextExtension(new RoleGroup(d_ptr)); } ContextAdapterFactory::~ContextAdapterFactory() { delete d_ptr; } uint ContextExtension::size() const { return propertyNames().size(); } bool ContextExtension::supportCaching(uint id) const { Q_UNUSED(id) return true; } QVector& ContextExtension::propertyNames() const { static QVector r; return r; } QByteArray ContextExtension::getPropertyName(uint id) const { return propertyNames()[id]; } -void ContextExtension::setProperty(AbstractItemAdapter* item, uint id, const QVariant& value) const +bool ContextExtension::setProperty(AbstractItemAdapter* item, uint id, const QVariant& value, const QModelIndex& index) const { Q_UNUSED(item) Q_UNUSED(id) Q_UNUSED(value) + Q_UNUSED(index) + return false; } void ContextExtension::changeProperty(AbstractItemAdapter* item, uint id) { Q_UNUSED(item) Q_UNUSED(id) Q_ASSERT(false); /* const auto metaRole = &d_ptr->d_ptr->m_pMetaType->roles[id]; Q_ASSERT(metaRole); auto mo = d_ptr->d_ptr->m_pMetaType->m_pMetaObject; index()*/ } void ContextAdapter::flushCache() { for (uint i = 0; i < d_ptr->m_pMetaType->propertyCount; i++) { if (d_ptr->m_lVariants[i]) delete d_ptr->m_lVariants[i]; d_ptr->m_lVariants[i] = nullptr; } } void AbstractItemAdapter::dismissCacheEntry(ContextExtension* e, int id) { auto dx = s_ptr->m_pMetadata->contextAdapter()->d_ptr; if (!dx) return; Q_ASSERT(e); Q_ASSERT(e->d_ptr->d_ptr->q_ptr == viewport()->modelAdapter()->contextAdapterFactory()); Q_ASSERT(e->d_ptr->d_ptr->m_lGroups[e->d_ptr->m_Id] == e); - Q_ASSERT(id >= 0 && id < e->size()); + Q_ASSERT(id >= 0 && id < (int) e->size()); dx->m_lVariants[e->d_ptr->m_Offset + id] = nullptr; } QVariant RoleGroup::getProperty(AbstractItemAdapter* item, uint id, const QModelIndex& index) const { Q_UNUSED(item) const auto metaRole = &d_ptr->m_pMetaType->roles[id]; // Keep track of the accessed roles - if (!(metaRole->flags & MetaProperty::Flags::READ)) { - d_ptr->m_pMetaType->used << metaRole; - //qDebug() << "\n NEW ROLE!" << (*metaRole->name) << d_ptr->m_pMetaType->used.size(); - } + if (!(metaRole->flags & MetaProperty::Flags::READ)) + d_ptr->m_pMetaType->m_lUsed << metaRole; metaRole->flags |= MetaProperty::Flags::READ; return index.data(metaRole->roleId); } uint RoleGroup::size() const { return d_ptr->m_pMetaType->roleCount; } QByteArray RoleGroup::getPropertyName(uint id) const { return *d_ptr->m_pMetaType->roles[id].name; } +bool RoleGroup::setProperty(AbstractItemAdapter* item, uint id, const QVariant& value, const QModelIndex& index) const +{ + // Avoid "useless" setData. With binding loops this can get very nasty. + // Yes: You could to do this on purpose in the past, but it's no longer + // safe. + if (getProperty(item, id, index) == value) + return false; + + const auto metaRole = &d_ptr->m_pMetaType->roles[id]; + + // Keep track of the accessed roles + if (!(metaRole->flags & MetaProperty::Flags::TRIED_WRITE)) + d_ptr->m_pMetaType->m_lUsed << metaRole; + + metaRole->flags |= MetaProperty::Flags::TRIED_WRITE; + + if (d_ptr->m_pModel->setData(index, value, metaRole->roleId)) { + metaRole->flags |= MetaProperty::Flags::HAS_WRITTEN; + return true; + } + + return false; +} + const QMetaObject *DynamicContext::metaObject() const { Q_ASSERT(m_pMetaType); return m_pMetaType->m_pMetaObject; } int DynamicContext::qt_metacall(QMetaObject::Call call, int id, void **argv) { - if (!m_Mutex.try_lock()) - return -1; - - const int realId = id - m_pMetaType->m_pMetaObject->propertyOffset(); //qDebug() << "META" << id << realId << call << QMetaObject::ReadProperty; - if (realId < 0) { - m_Mutex.unlock(); + if (realId < 0) return QObject::qt_metacall(call, id, argv); - } + + const auto group = &m_pMetaType->m_lGroupMapping[realId]; + Q_ASSERT(group->ptr); if (call == QMetaObject::ReadProperty) { if (Q_UNLIKELY(((size_t)realId) >= m_pMetaType->propertyCount)) { Q_ASSERT(false); - m_Mutex.unlock(); return -1; } - const auto group = &m_pMetaType->m_lGroupMapping[realId]; - Q_ASSERT(group->ptr); - const QModelIndex idx = m_pBuilder->item() ? m_pBuilder->item()->index() : m_Index; const bool supportsCache = m_Cache && (m_pMetaType->m_pCacheMap[realId/8] & (1 << (realId % 8))); // Use a special function for the role case. It's only known at runtime. QVariant *value = m_lVariants[realId] && supportsCache ? m_lVariants[realId] : new QVariant( group->ptr->getProperty(m_pBuilder->item(), realId - group->offset, idx)); if (supportsCache && !m_lVariants[realId]) m_lVariants[realId] = value; QMetaType::construct(QMetaType::QVariant, argv[0], value->data()); // if (!supportsCache) // delete value; } else if (call == QMetaObject::WriteProperty) { - //qDebug() << "SET" << argv[0]; - - //FIXME enable setData - //const QModelIndex idx = m_pBuilder->item() ? m_pBuilder->item()->index() : m_Index; - //Q_ASSERT(false); //TODO call setData - //const int roleId = m_hIdMapper.value(realId); - //m_Index.model()->setData( - // m_Index, roleId, QVariant(property.typeId, argv[0]) - //); - //m_lUsedProperties << roleId; - - *reinterpret_cast(argv[2]) = 1; // setProperty return value - QMetaObject::activate(this, m_pMetaType->m_pMetaObject, realId, nullptr); + const QVariant value = QVariant(QMetaType::QVariant, argv[0]).value(); + const QModelIndex idx = m_pBuilder->item() ? m_pBuilder->item()->index() : m_Index; + + const bool ret = group->ptr->setProperty( + m_pBuilder->item(), realId - group->offset, value, idx + ); + + // Register if setting the property worked + *reinterpret_cast(argv[2]) = ret ? 1 : 0; + + QMetaObject::activate(this, m_pMetaType->m_pMetaObject, realId, argv); + } else if (call == QMetaObject::InvokeMetaMethod) { int sigId = id - m_pMetaType->m_pMetaObject->methodOffset(); qDebug() << "LA LA INVOKE" << sigId << id; QMetaObject::activate(this, m_pMetaType->m_pMetaObject, id, nullptr); return -1; } - m_Mutex.unlock(); - return -1; } void* DynamicContext::qt_metacast(const char *name) { if (!strcmp(name, m_pMetaType->m_pMetaObject->className())) return this; return QObject::qt_metacast(name); } DynamicMetaType::DynamicMetaType(const QHash& rls) : roleCount(rls.size()) {} /// Populate a vTable with the propertyId -> group object void ContextAdapterFactoryPrivate::initGroup(const QHash& rls) { Q_ASSERT(!m_pMetaType->m_GroupInit); for (auto group : qAsConst(m_lGroups)) m_pMetaType->propertyCount += group->size(); m_pMetaType->m_lGroupMapping = (GroupMetaData*) malloc( sizeof(GroupMetaData) * m_pMetaType->propertyCount ); uint offset(0), realId(0), groupId(0); for (auto group : qAsConst(m_lGroups)) { Q_ASSERT(!group->d_ptr->d_ptr); group->d_ptr->d_ptr = this; group->d_ptr->m_Offset = offset; group->d_ptr->m_Id = groupId++; const uint gs = group->size(); for (uint i = 0; i < gs; i++) m_pMetaType->m_lGroupMapping[offset+i] = {group, offset}; offset += gs; } Q_ASSERT(offset == m_pMetaType->propertyCount); // Add a bitfield to store the properties that need to skip the cache const int fieldSize = m_pMetaType->propertyCount / 8 + (m_pMetaType->propertyCount%8?1:0); m_pMetaType->m_pCacheMap = (uint8_t*) malloc(fieldSize); for (int i = 0; i < fieldSize; i++) m_pMetaType->m_pCacheMap[i] = 0; m_pMetaType->m_GroupInit = true; // Create the metaobject QMetaObjectBuilder builder; builder.setClassName("DynamicContext"); builder.setSuperClass(&QObject::staticMetaObject); // Use a C array like the moc would do because this is called **A LOT** m_pMetaType->roles = new MetaProperty[m_pMetaType->propertyCount]; // Setup the role metadata for (auto i = rls.constBegin(); i != rls.constEnd(); i++) { uint id = realId++; MetaProperty* r = &m_pMetaType->roles[id]; r->roleId = i.key(); r->name = new QByteArray(i.value()); r->flags |= MetaProperty::Flags::IS_ROLE; m_pMetaType->m_hRoleIds[i.key()] = r; } realId = 0; // Add all object virtual properties for (const auto g : qAsConst(m_lGroups)) { for (uint j = 0; j < g->size(); j++) { uint id = realId++; Q_ASSERT(id < m_pMetaType->propertyCount); MetaProperty* r = &m_pMetaType->roles[id]; r->propId = id; const auto name = g->getPropertyName(j); auto property = builder.addProperty(name, "QVariant"); property.setWritable(true); auto signal = builder.addSignal(name + "Changed()"); r->signalId = signal.index(); property.setNotifySignal(signal); // Set the cache bit m_pMetaType->m_pCacheMap[id/8] |= (g->supportCaching(j)?1:0) << (id % 8); } } m_pMetaType->m_pMetaObject = builder.toMetaObject(); } DynamicContext::DynamicContext(ContextAdapterFactory* cm) : m_pMetaType(cm->d_ptr->m_pMetaType) { Q_ASSERT(m_pMetaType); Q_ASSERT(m_pMetaType->roleCount <= m_pMetaType->propertyCount); m_lVariants = (QVariant**) malloc(sizeof(QVariant*)*m_pMetaType->propertyCount); //TODO SIMD this for (uint i = 0; i < m_pMetaType->propertyCount; i++) m_lVariants[i] = nullptr; } DynamicContext::~DynamicContext() {} //FIXME delete the metatype now that it's invalid. // if (m_pMetaType) { // qDeleteAll(m_hContextMapper); // m_hContextMapper.clear(); // // delete m_pMetaType; // m_pMetaType = nullptr; // } bool ContextAdapter::updateRoles(const QVector &modified) const { if (!d_ptr->d_ptr->m_pMetaType) return false; bool ret = false; if (!modified.isEmpty()) { for (auto r : qAsConst(modified)) { if (auto mr = d_ptr->d_ptr->m_pMetaType->m_hRoleIds.value(r)) { // This works because the role offset is always 0 if (d_ptr->m_lVariants[mr->propId]) { delete d_ptr->m_lVariants[mr->propId]; d_ptr->m_lVariants[mr->propId] = nullptr; QMetaMethod m = d_ptr->metaObject()->method(mr->signalId); m.invoke(d_ptr); ret |= ret; } } } } else { // Only update the roles known to have an impact - for (auto mr : qAsConst(d_ptr->d_ptr->m_pMetaType->used)) { + for (auto mr : qAsConst(d_ptr->d_ptr->m_pMetaType->m_lUsed)) { // Use `READ` instead of checking the cache because it could have // been dismissed for many reasons. if ((!d_ptr->m_Cache) || mr->flags & MetaProperty::Flags::READ) { if (auto v = d_ptr->m_lVariants[mr->propId]) delete v; d_ptr->m_lVariants[mr->propId] = nullptr; //FIXME this should work, but it doesn't auto mo = d_ptr->d_ptr->m_pMetaType->m_pMetaObject; const int methodId = mo->methodOffset() + mr->signalId; QMetaMethod m = mo->method(methodId); //m.invoke(d_ptr); Q_ASSERT(m.name() == (*mr->name)+"Changed"); //FIXME this should also work, but also doesn't //QMetaObject::activate(d_ptr, mo, mr->signalId, nullptr); //FIXME Use this for now, but it prevent setData from being implemented - d_ptr->setProperty(*mr->name, 0x1337); + d_ptr->setProperty(*mr->name, d_ptr->property(*mr->name)); QMetaObject::activate(d_ptr, mo, mr->signalId, nullptr); ret = true; } } } return ret; } QAbstractItemModel *ContextAdapterFactory::model() const { return d_ptr->m_pModel; } void ContextAdapterFactory::setModel(QAbstractItemModel *m) { d_ptr->m_pModel = m; } void ContextAdapterFactoryPrivate::finish() { Q_ASSERT(m_pModel); if (m_pMetaType) return; const auto roles = m_pModel->roleNames(); m_pMetaType = new DynamicMetaType(roles); initGroup(roles); } void ContextAdapterFactory::addContextExtension(ContextExtension* pg) { Q_ASSERT(!d_ptr->m_pMetaType); if (d_ptr->m_pMetaType) { qWarning() << "It is not possible to add property group after creating a builder"; return; } d_ptr->m_lGroups << pg; } QSet ContextAdapterFactory::usedRoles() const { if (!d_ptr->m_pMetaType) return {}; QSet ret; - for (const auto mr : qAsConst(d_ptr->m_pMetaType->used)) { + for (const auto mr : qAsConst(d_ptr->m_pMetaType->m_lUsed)) { if (mr->roleId != -1) ret << *mr->name; } return ret; } ContextAdapter* ContextAdapterFactory::createAdapter(FactoryFunctor f, QQmlContext *parentContext) const { ContextAdapter* ret = f(parentContext); Q_ASSERT(!ret->d_ptr); d_ptr->finish(); ret->d_ptr = new DynamicContext(const_cast(this)); ret->d_ptr->d_ptr = d_ptr; ret->d_ptr->setParent(parentContext); ret->d_ptr->m_pBuilder = ret; ret->d_ptr->m_pParentCtx = parentContext; //HACK QtQuick ignores ret->d_ptr->m_Conn = QObject::connect(ret->d_ptr, &QObject::destroyed, ret->d_ptr, [ret, this, parentContext]() { qWarning() << "Rebuilding the cache because QtQuick bugs trashed it"; ret->d_ptr = new DynamicContext(const_cast(this)); ret->d_ptr->d_ptr = d_ptr; ret->d_ptr->setParent(parentContext); ret->d_ptr->m_pBuilder = ret; ret->d_ptr->m_pParentCtx = parentContext; }); return ret; } ContextAdapter* ContextAdapterFactory::createAdapter(QQmlContext *parentContext) const { return createAdapter(d_ptr->m_fFactory, parentContext); } ContextAdapter::ContextAdapter(QQmlContext *parentContext) { Q_UNUSED(parentContext) } ContextAdapter::~ContextAdapter() { if (d_ptr->m_pCtx) d_ptr->m_pCtx->setContextObject(nullptr); QObject::disconnect(d_ptr->m_Conn); d_ptr->m_pBuilder = nullptr; delete d_ptr; } bool ContextAdapter::isCacheEnabled() const { return d_ptr->m_Cache; } void ContextAdapter::setCacheEnabled(bool v) { d_ptr->m_Cache = v; } QModelIndex ContextAdapter::index() const { return d_ptr->m_Index; } void ContextAdapter::setModelIndex(const QModelIndex& index) { const bool hasIndex = d_ptr->m_Index.isValid(); if (d_ptr->m_Cache) flushCache(); d_ptr->m_Index = index; if (hasIndex) updateRoles({}); } QQmlContext* ContextAdapter::context() const { Q_ASSERT(d_ptr); if (!d_ptr->m_pCtx) { d_ptr->m_pCtx = new QQmlContext(d_ptr->m_pParentCtx, d_ptr->parent()); d_ptr->m_pCtx->setContextObject(d_ptr); d_ptr->m_pCtx->engine()->setObjectOwnership( d_ptr, QQmlEngine::CppOwnership ); d_ptr->m_pCtx->engine()->setObjectOwnership( d_ptr->m_pCtx, QQmlEngine::CppOwnership ); } return d_ptr->m_pCtx; } QObject *ContextAdapter::contextObject() const { return d_ptr; } bool ContextAdapter::isActive() const { return d_ptr->m_pCtx; } AbstractItemAdapter* ContextAdapter::item() const { return nullptr; } ContextExtension::ContextExtension() : d_ptr(new ContextExtensionPrivate()) {} diff --git a/src/extensions/contextextension.h b/src/extensions/contextextension.h index c522394..d5c87a4 100644 --- a/src/extensions/contextextension.h +++ b/src/extensions/contextextension.h @@ -1,105 +1,105 @@ /*************************************************************************** * Copyright (C) 2018 by Emmanuel Lepage Vallee * * Author : Emmanuel Lepage Vallee * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 3 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * **************************************************************************/ #ifndef KQUICKITEMVIEWS_CONTEXTEXTENSION_H #define KQUICKITEMVIEWS_CONTEXTEXTENSION_H // Qt #include #include #include class ContextExtensionPrivate; /** * Add more properties to the QML context. * * This allows to add a predefined set of extra properties to the QML * context. It does *not* support adding more properties at runtime. * * The memory ownership is not transferred, do not re-use the instance * across many views (it will crash). */ class ContextExtension { public: explicit ContextExtension(); virtual ~ContextExtension() {} /** * Return a list of properties. * * This **MUST NEVER CHANGE** and needs to always return the same * vector. Implementations should use a `static` QVector. * * The property index is what's passed to getProperty, setProperty * and is the argument of changeProperty. */ virtual QVector& propertyNames() const; /** * The default implementation returns propertyNames().size() * * Implementing this rather than using `propertyNames` makes sense * when the properties contained in an existing data structure or * associated with extra metadata. */ virtual uint size() const; /** * The default implementation returns propertyNames()[id]; * * Implementing this rather than using `propertyNames` makes sense * when the properties contained in an existing data structure or * associated with extra metadata. */ virtual QByteArray getPropertyName(uint id) const; /** * Some variants, like QModelIndex ones, cannot be cached and will * most likely point to invalid memory. * * If the group has such properties, implement this function. Note * that the return value cannot change. * * The default implementation always returns true. */ virtual bool supportCaching(uint id) const; /** * The id comes from propertyNames. * * It is recommended to use a switch statement or if/else_if for the * implementation and avoid QHash (or worst). */ virtual QVariant getProperty(AbstractItemAdapter* item, uint id, const QModelIndex& index) const = 0; /** * Optionally make the property read/write. */ - virtual void setProperty(AbstractItemAdapter* item, uint id, const QVariant& value) const; + virtual bool setProperty(AbstractItemAdapter* item, uint id, const QVariant& value, const QModelIndex& index) const; /** * Notify that content of this property has changed. */ void changeProperty(AbstractItemAdapter* item, uint id); ContextExtensionPrivate *d_ptr; }; #endif diff --git a/src/qmodelindexbinder.cpp b/src/qmodelindexbinder.cpp index 1b8b27d..8e418be 100644 --- a/src/qmodelindexbinder.cpp +++ b/src/qmodelindexbinder.cpp @@ -1,255 +1,270 @@ /*************************************************************************** * Copyright (C) 2018 by Emmanuel Lepage Vallee * * Author : Emmanuel Lepage Vallee * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 3 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * **************************************************************************/ #include "qmodelindexbinder.h" // Qt #include #include #include // KQuickItemViews #include "qmodelindexwatcher.h" #include class QModelIndexBinderPrivate : public QObject { Q_OBJECT public: enum class Mode { CONTAINER, ATTACHED, }; QByteArray m_Role { }; QByteArray m_Prop { }; QObject *m_pContent {nullptr}; int m_Delay { 0 }; QTimer *m_pTimer {nullptr}; bool m_AutoSave { true }; QQmlContext *m_pCTX {nullptr}; ContextAdapter *m_pAdapter {nullptr}; QModelIndexWatcher *m_pWatcher {nullptr}; bool m_isBinded { false }; Mode m_Mode; // Helper void bind(); QModelIndexBinder *q_ptr; public Q_SLOTS: void loadWatcher(); void slotModelPropChanged(); void slotObjectPropChanged(); }; QModelIndexBinder::QModelIndexBinder(QQuickItem *parent) : QQuickItem(parent), d_ptr(new QModelIndexBinderPrivate()) { d_ptr->q_ptr = this; d_ptr->m_Mode = QModelIndexBinderPrivate::Mode::CONTAINER; //TODO actually track the context, it could be reparented QTimer::singleShot(0, d_ptr, SLOT(loadWatcher())); } QModelIndexBinder::QModelIndexBinder(QObject *parent) : QQuickItem(nullptr), d_ptr(new QModelIndexBinderPrivate()) { QObject::setParent(parent); d_ptr->q_ptr = this; d_ptr->m_Mode = QModelIndexBinderPrivate::Mode::ATTACHED; //TODO actually track the context, it could be reparented QTimer::singleShot(0, d_ptr, SLOT(loadWatcher())); _setObject(parent); } QModelIndexBinder::~QModelIndexBinder() { delete d_ptr; } QString QModelIndexBinder::modelRole() const { return d_ptr->m_Role; } void QModelIndexBinder::setModelRole(const QString& role) { d_ptr->m_Role = role.toLatin1(); emit changed(); d_ptr->bind(); } QString QModelIndexBinder::objectProperty() const { return d_ptr->m_Prop; } void QModelIndexBinder::setObjectProperty(const QString& op) { d_ptr->m_Prop = op.toLatin1(); } QObject *QModelIndexBinder::_object() const { return d_ptr->m_pContent; } void QModelIndexBinder::_setObject(QObject *o) { // If the object is a QQuickItem, add it to the view if (d_ptr->m_Mode == QModelIndexBinderPrivate::Mode::CONTAINER) { if (auto w = qobject_cast(o)) { w->setParentItem(this); } } d_ptr->m_pContent = o; emit changed(); d_ptr->bind(); } int QModelIndexBinder::delay() const { return d_ptr->m_Delay; } void QModelIndexBinder::setDelay(int d) { d_ptr->m_Delay = d; emit changed(); } bool QModelIndexBinder::autoSave() const { return d_ptr->m_AutoSave; } void QModelIndexBinder::setAutoSave(bool v) { d_ptr->m_AutoSave = v; emit changed(); } bool QModelIndexBinder::isSynchronized() const { //TODO return true; } void QModelIndexBinder::reset() const { //TODO } bool QModelIndexBinder::applyNow() const { //TODO return true; } void QModelIndexBinderPrivate::loadWatcher() { if (m_pWatcher) return; // Container mode m_pCTX = QQmlEngine::contextForObject(q_ptr); // Attached mode if ((!m_pCTX) || m_pContent) m_pCTX = QQmlEngine::contextForObject(m_pContent); Q_ASSERT(m_pCTX); if (!m_pCTX) return; auto v = m_pCTX->contextProperty("_modelIndexWatcher"); auto c = m_pCTX->contextProperty("_contextAdapter"); m_pAdapter = qvariant_cast(c); m_pWatcher = qobject_cast( qvariant_cast(v) ); Q_ASSERT(m_pAdapter); Q_ASSERT(m_pWatcher); bind(); } void QModelIndexBinderPrivate::bind() { if (m_isBinded || m_Role.isEmpty() || m_Prop.isEmpty() || (!m_pContent) || (!m_pWatcher) || (!m_pAdapter)) return; const auto co = m_pAdapter->contextObject(); // Find the properties on both side const int objPropId = m_pContent->metaObject()->indexOfProperty(m_Prop); const int rolePropId = co->metaObject()->indexOfProperty(m_Role); if (rolePropId == -1) qWarning() << "Role" << m_Role << "not found"; if (objPropId == -1) qWarning() << "Property" << m_Prop << "not found"; Q_ASSERT(objPropId != -1 && rolePropId != -1); auto metaProp = m_pContent->metaObject()->property(objPropId); auto metaRole = co->metaObject()->property(rolePropId); // Connect to the metaSlots auto metaSlotProp = metaObject()->method(metaObject()->indexOfMethod("slotObjectPropChanged()")); auto metaSlotRole = metaObject()->method(metaObject()->indexOfMethod("slotModelPropChanged()")); + // Set the initial value before connecting + slotModelPropChanged(); + connect(m_pContent, metaProp.notifySignal(), this, metaSlotProp); connect(co , metaRole.notifySignal(), this, metaSlotRole); - - // Set the initial value - slotModelPropChanged(); } void QModelIndexBinderPrivate::slotModelPropChanged() { const auto role = m_pAdapter->contextObject()->property(m_Role); + + //HACK this is a bug in ContextAdapterFactory + if (!role.isValid()) + return; + const auto prop = m_pContent->property(m_Prop); - //qDebug() << "ROLE CHANGED" << role << prop; - m_pContent->setProperty(m_Prop, role); + + // Some widgets may not try to detect if the value **really** changes and + // emit signals anyway. + if (role != m_pContent->property(m_Prop)) + m_pContent->setProperty(m_Prop, role); } void QModelIndexBinderPrivate::slotObjectPropChanged() { const auto role = m_pAdapter->contextObject()->property(m_Role); + + Q_ASSERT(role.isValid()); + //HACK this is a bug in ContextAdapterFactory + if (!role.isValid()) + return; + const auto prop = m_pContent->property(m_Prop); - //qDebug() << "PROP CHANGED" << role << prop; - //m_pAdapter->contextObject()->setProperty(m_Role, prop); //FIXME fix the model::setData support + + if (role != prop) + m_pAdapter->contextObject()->setProperty(m_Role, prop); //FIXME fix the model::setData support } QModelIndexBinder *QModelIndexBinder::qmlAttachedProperties(QObject *object) { return new QModelIndexBinder(object); } #include diff --git a/src/views/treeview.cpp b/src/views/treeview.cpp index 729462b..3ed5f0a 100644 --- a/src/views/treeview.cpp +++ b/src/views/treeview.cpp @@ -1,190 +1,191 @@ /*************************************************************************** * Copyright (C) 2017 by Emmanuel Lepage Vallee * * Author : Emmanuel Lepage Vallee * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 3 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * **************************************************************************/ #include "treeview.h" // KQuickItemViews #include "adapters/abstractitemadapter.h" #include "extensions/contextextension.h" #include "adapters/modeladapter.h" #include "contextadapterfactory.h" #include "adapters/contextadapter.h" // Qt #include /** * Polymorphic tree item for the SingleModelViewBase. * * Classes implementing SingleModelViewBase need to provide an implementation of the pure * virtual functions. It is useful, for example, to manage both a raster and * QQuickItem based version of a view. * * The state is managed by the SingleModelViewBase and it's own protected virtual methods. */ class TreeViewItem final : public AbstractItemAdapter { public: explicit TreeViewItem(Viewport* r); virtual ~TreeViewItem(); // Actions virtual bool move () override; virtual bool remove () override; private: bool m_IsHead { false }; }; /// Add the same property as the QtQuick.ListView class TreeContextProperties final : public ContextExtension { public: virtual ~TreeContextProperties() {} virtual QVector& propertyNames() const override; virtual QVariant getProperty(AbstractItemAdapter* item, uint id, const QModelIndex& index) const override; - virtual void setProperty(AbstractItemAdapter* item, uint id, const QVariant& value) const override; + virtual bool setProperty(AbstractItemAdapter* item, uint id, const QVariant& value, const QModelIndex& index) const override; }; class TreeViewPrivate { public: }; TreeView::TreeView(QQuickItem* parent) : SingleModelViewBase(new ItemFactory(), parent), d_ptr(new TreeViewPrivate) { modelAdapters().first()->contextAdapterFactory()->addContextExtension( new TreeContextProperties() ); } TreeView::~TreeView() { delete d_ptr; } TreeViewItem::TreeViewItem(Viewport* r) : AbstractItemAdapter(r) { } TreeViewItem::~TreeViewItem() { delete item(); } bool TreeViewItem::move() { // Will happen when trying to move a FAILED, but buffered item if (!item()) { qDebug() << "NO ITEM" << index().data(); return false; } item()->setWidth(view()->contentItem()->width()); auto nextElem = static_cast(next(Qt::BottomEdge)); auto prevElem = static_cast(next(Qt::TopEdge)); // The root has been moved in the middle of the tree, find the new root //TODO maybe add a deterministic API instead of O(N) lookup if (prevElem && m_IsHead) { m_IsHead = false; auto root = prevElem; while (auto prev = root->next(Qt::TopEdge)) root = static_cast(prev); root->move(); Q_ASSERT(root->m_IsHead); } // So other items can be GCed without always resetting to 0x0, note that it // might be a good idea to extend Flickable to support a virtual // origin point. if ((!prevElem) || (nextElem && nextElem->m_IsHead)) { auto anchors = qvariant_cast(item()->property("anchors")); anchors->setProperty("top", {}); item()->setY(0); m_IsHead = true; } else if (prevElem) { Q_ASSERT(!m_IsHead); item()->setProperty("y", {}); auto anchors = qvariant_cast(item()->property("anchors")); anchors->setProperty("top", prevElem->item()->property("bottom")); } // Now, update the next anchors if (nextElem) { nextElem->m_IsHead = false; nextElem->item()->setProperty("y", {}); auto anchors = qvariant_cast(nextElem->item()->property("anchors")); anchors->setProperty("top", item()->property("bottom")); } updateGeometry(); return true; } bool TreeViewItem::remove() { if (item()) { item()->setParent(nullptr); item()->setParentItem(nullptr); item()->setVisible(false); } auto nextElem = static_cast(next(Qt::BottomEdge)); auto prevElem = static_cast(next(Qt::TopEdge)); if (nextElem) { if (m_IsHead) { auto anchors = qvariant_cast(nextElem->item()->property("anchors")); anchors->setProperty("top", {}); item()->setY(0); nextElem->m_IsHead = true; } else { //TODO maybe eventually use a state machine for this auto anchors = qvariant_cast(nextElem->item()->property("anchors")); anchors->setProperty("top", prevElem->item()->property("bottom")); } } return true; } QVector& TreeContextProperties::propertyNames() const { static QVector ret { "expanded" }; return ret; } QVariant TreeContextProperties::getProperty(AbstractItemAdapter* item, uint id, const QModelIndex& index) const { Q_UNUSED(index); Q_ASSERT(id == 0 && item); return !item->isCollapsed(); } -void TreeContextProperties::setProperty(AbstractItemAdapter* item, uint id, const QVariant& value) const +bool TreeContextProperties::setProperty(AbstractItemAdapter* item, uint id, const QVariant& value, const QModelIndex& index) const { Q_ASSERT(id == 0 && item && value.canConvert()); item->setCollapsed(!value.toBool()); + return true; }