diff options
Diffstat (limited to 'src/qmlmodels')
39 files changed, 17366 insertions, 0 deletions
diff --git a/src/qmlmodels/configure.json b/src/qmlmodels/configure.json new file mode 100644 index 0000000000..2aa8a50e69 --- /dev/null +++ b/src/qmlmodels/configure.json @@ -0,0 +1,31 @@ +{ + "module": "qmlmodels", + "depends": [ + "core-private", + "qml-private" + ], + + "features": { + "qml-list-model": { + "label": "QML list model", + "purpose": "Provides the ListModel QML type.", + "section": "QML", + "output": [ "privateFeature" ] + }, + "qml-delegate-model": { + "label": "QML delegate model", + "purpose": "Provides the DelegateModel QML type.", + "section": "QML", + "output": [ "privateFeature" ] + } + }, + "summary": [ + { + "section": "Qt QML Models", + "entries": [ + "qml-list-model", + "qml-delegate-model" + ] + } + ] +} diff --git a/src/qmlmodels/qmlmodels.pro b/src/qmlmodels/qmlmodels.pro new file mode 100644 index 0000000000..84f87f8bb1 --- /dev/null +++ b/src/qmlmodels/qmlmodels.pro @@ -0,0 +1,57 @@ +TARGET = QtQmlModels +QT = core-private qml-private + +DEFINES += QT_NO_URL_CAST_FROM_STRING QT_NO_INTEGER_EVENT_COORDINATES QT_NO_FOREACH + +HEADERS += \ + $$PWD/qqmlchangeset_p.h \ + $$PWD/qqmlinstantiator_p.h \ + $$PWD/qqmlinstantiator_p_p.h \ + $$PWD/qqmllistaccessor_p.h \ + $$PWD/qqmllistcompositor_p.h \ + $$PWD/qqmlmodelsmodule_p.h \ + $$PWD/qqmlobjectmodel_p.h \ + $$PWD/qqmltableinstancemodel_p.h \ + $$PWD/qqmltablemodel_p.h \ + $$PWD/qqmltablemodelcolumn_p.h \ + $$PWD/qquickpackage_p.h \ + $$PWD/qtqmlmodelsglobal_p.h \ + $$PWD/qtqmlmodelsglobal.h \ + +SOURCES += \ + $$PWD/qqmlchangeset.cpp \ + $$PWD/qqmlinstantiator.cpp \ + $$PWD/qqmllistaccessor.cpp \ + $$PWD/qqmllistcompositor.cpp \ + $$PWD/qqmlmodelsmodule.cpp \ + $$PWD/qqmlobjectmodel.cpp \ + $$PWD/qqmltableinstancemodel.cpp \ + $$PWD/qqmltablemodel.cpp \ + $$PWD/qqmltablemodelcolumn.cpp \ + $$PWD/qquickpackage.cpp + +qtConfig(qml-list-model) { + SOURCES += \ + $$PWD/qqmllistmodel.cpp \ + $$PWD/qqmllistmodelworkeragent.cpp + + HEADERS += \ + $$PWD/qqmllistmodel_p.h \ + $$PWD/qqmllistmodel_p_p.h \ + $$PWD/qqmllistmodelworkeragent_p.h +} + +qtConfig(qml-delegate-model) { + SOURCES += \ + $$PWD/qqmladaptormodel.cpp \ + $$PWD/qqmldelegatemodel.cpp \ + $$PWD/qqmldelegatecomponent.cpp + + HEADERS += \ + $$PWD/qqmladaptormodel_p.h \ + $$PWD/qqmldelegatemodel_p.h \ + $$PWD/qqmldelegatemodel_p_p.h \ + $$PWD/qqmldelegatecomponent_p.h +} + +load(qt_module) diff --git a/src/qmlmodels/qqmladaptormodel.cpp b/src/qmlmodels/qqmladaptormodel.cpp new file mode 100644 index 0000000000..f991ae0a69 --- /dev/null +++ b/src/qmlmodels/qqmladaptormodel.cpp @@ -0,0 +1,1037 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtQml module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "qqmladaptormodel_p.h" + +#include <private/qqmldelegatemodel_p_p.h> +#include <private/qmetaobjectbuilder_p.h> +#include <private/qqmlproperty_p.h> + +#include <private/qv4value_p.h> +#include <private/qv4functionobject_p.h> + +QT_BEGIN_NAMESPACE + +class QQmlAdaptorModelEngineData : public QV8Engine::Deletable +{ +public: + QQmlAdaptorModelEngineData(QV4::ExecutionEngine *v4); + ~QQmlAdaptorModelEngineData(); + + QV4::ExecutionEngine *v4; + QV4::PersistentValue listItemProto; +}; + +V4_DEFINE_EXTENSION(QQmlAdaptorModelEngineData, engineData) + +static QV4::ReturnedValue get_index(const QV4::FunctionObject *f, const QV4::Value *thisObject, const QV4::Value *, int) +{ + QV4::Scope scope(f); + QV4::Scoped<QQmlDelegateModelItemObject> o(scope, thisObject->as<QQmlDelegateModelItemObject>()); + if (!o) + RETURN_RESULT(scope.engine->throwTypeError(QStringLiteral("Not a valid DelegateModel object"))); + + RETURN_RESULT(QV4::Encode(o->d()->item->index)); +} + +template <typename T, typename M> static void setModelDataType(QMetaObjectBuilder *builder, M *metaType) +{ + builder->setFlags(QMetaObjectBuilder::DynamicMetaObject); + builder->setClassName(T::staticMetaObject.className()); + builder->setSuperClass(&T::staticMetaObject); + metaType->propertyOffset = T::staticMetaObject.propertyCount(); + metaType->signalOffset = T::staticMetaObject.methodCount(); +} + +static void addProperty(QMetaObjectBuilder *builder, int propertyId, const QByteArray &propertyName, const QByteArray &propertyType) +{ + builder->addSignal("__" + QByteArray::number(propertyId) + "()"); + QMetaPropertyBuilder property = builder->addProperty( + propertyName, propertyType, propertyId); + property.setWritable(true); +} + +class VDMModelDelegateDataType; + +class QQmlDMCachedModelData : public QQmlDelegateModelItem +{ +public: + QQmlDMCachedModelData( + QQmlDelegateModelItemMetaType *metaType, + VDMModelDelegateDataType *dataType, + int index, int row, int column); + + int metaCall(QMetaObject::Call call, int id, void **arguments); + + virtual QVariant value(int role) const = 0; + virtual void setValue(int role, const QVariant &value) = 0; + + void setValue(const QString &role, const QVariant &value) override; + bool resolveIndex(const QQmlAdaptorModel &model, int idx) override; + + static QV4::ReturnedValue get_property(const QV4::FunctionObject *, const QV4::Value *thisObject, const QV4::Value *argv, int argc); + static QV4::ReturnedValue set_property(const QV4::FunctionObject *, const QV4::Value *thisObject, const QV4::Value *argv, int argc); + + VDMModelDelegateDataType *type; + QVector<QVariant> cachedData; +}; + +class VDMModelDelegateDataType + : public QQmlRefCount + , public QQmlAdaptorModel::Accessors + , public QAbstractDynamicMetaObject +{ +public: + VDMModelDelegateDataType(QQmlAdaptorModel *model) + : model(model) + , propertyOffset(0) + , signalOffset(0) + , hasModelData(false) + { + } + + bool notify( + const QQmlAdaptorModel &, + const QList<QQmlDelegateModelItem *> &items, + int index, + int count, + const QVector<int> &roles) const override + { + bool changed = roles.isEmpty() && !watchedRoles.isEmpty(); + if (!changed && !watchedRoles.isEmpty() && watchedRoleIds.isEmpty()) { + QList<int> roleIds; + for (const QByteArray &r : watchedRoles) { + QHash<QByteArray, int>::const_iterator it = roleNames.find(r); + if (it != roleNames.end()) + roleIds << it.value(); + } + const_cast<VDMModelDelegateDataType *>(this)->watchedRoleIds = roleIds; + } + + QVector<int> signalIndexes; + for (int i = 0; i < roles.count(); ++i) { + const int role = roles.at(i); + if (!changed && watchedRoleIds.contains(role)) + changed = true; + + int propertyId = propertyRoles.indexOf(role); + if (propertyId != -1) + signalIndexes.append(propertyId + signalOffset); + } + if (roles.isEmpty()) { + const int propertyRolesCount = propertyRoles.count(); + signalIndexes.reserve(propertyRolesCount); + for (int propertyId = 0; propertyId < propertyRolesCount; ++propertyId) + signalIndexes.append(propertyId + signalOffset); + } + + for (int i = 0, c = items.count(); i < c; ++i) { + QQmlDelegateModelItem *item = items.at(i); + const int idx = item->modelIndex(); + if (idx >= index && idx < index + count) { + for (int i = 0; i < signalIndexes.count(); ++i) + QMetaObject::activate(item, signalIndexes.at(i), nullptr); + } + } + return changed; + } + + void replaceWatchedRoles( + QQmlAdaptorModel &, + const QList<QByteArray> &oldRoles, + const QList<QByteArray> &newRoles) const override + { + VDMModelDelegateDataType *dataType = const_cast<VDMModelDelegateDataType *>(this); + + dataType->watchedRoleIds.clear(); + for (const QByteArray &oldRole : oldRoles) + dataType->watchedRoles.removeOne(oldRole); + dataType->watchedRoles += newRoles; + } + + static QV4::ReturnedValue get_hasModelChildren(const QV4::FunctionObject *b, const QV4::Value *thisObject, const QV4::Value *, int) + { + QV4::Scope scope(b); + QV4::Scoped<QQmlDelegateModelItemObject> o(scope, thisObject->as<QQmlDelegateModelItemObject>()); + if (!o) + RETURN_RESULT(scope.engine->throwTypeError(QStringLiteral("Not a valid DelegateModel object"))); + + const QQmlAdaptorModel *const model = static_cast<QQmlDMCachedModelData *>(o->d()->item)->type->model; + if (o->d()->item->index >= 0 && *model) { + const QAbstractItemModel * const aim = model->aim(); + RETURN_RESULT(QV4::Encode(aim->hasChildren(aim->index(o->d()->item->index, 0, model->rootIndex)))); + } else { + RETURN_RESULT(QV4::Encode(false)); + } + } + + + void initializeConstructor(QQmlAdaptorModelEngineData *const data) + { + QV4::ExecutionEngine *v4 = data->v4; + QV4::Scope scope(v4); + QV4::ScopedObject proto(scope, v4->newObject()); + proto->defineAccessorProperty(QStringLiteral("index"), get_index, nullptr); + proto->defineAccessorProperty(QStringLiteral("hasModelChildren"), get_hasModelChildren, nullptr); + QV4::ScopedProperty p(scope); + + typedef QHash<QByteArray, int>::const_iterator iterator; + for (iterator it = roleNames.constBegin(), end = roleNames.constEnd(); it != end; ++it) { + const int propertyId = propertyRoles.indexOf(it.value()); + const QByteArray &propertyName = it.key(); + + QV4::ScopedString name(scope, v4->newString(QString::fromUtf8(propertyName))); + QV4::ExecutionContext *global = v4->rootContext(); + QV4::ScopedFunctionObject g(scope, v4->memoryManager->allocate<QV4::IndexedBuiltinFunction>(global, propertyId, QQmlDMCachedModelData::get_property)); + QV4::ScopedFunctionObject s(scope, v4->memoryManager->allocate<QV4::IndexedBuiltinFunction>(global, propertyId, QQmlDMCachedModelData::set_property)); + p->setGetter(g); + p->setSetter(s); + proto->insertMember(name, p, QV4::Attr_Accessor|QV4::Attr_NotEnumerable|QV4::Attr_NotConfigurable); + } + prototype.set(v4, proto); + } + + // QAbstractDynamicMetaObject + + void objectDestroyed(QObject *) override + { + release(); + } + + int metaCall(QObject *object, QMetaObject::Call call, int id, void **arguments) override + { + return static_cast<QQmlDMCachedModelData *>(object)->metaCall(call, id, arguments); + } + + QV4::PersistentValue prototype; + QList<int> propertyRoles; + QList<int> watchedRoleIds; + QList<QByteArray> watchedRoles; + QHash<QByteArray, int> roleNames; + QQmlAdaptorModel *model; + int propertyOffset; + int signalOffset; + bool hasModelData; +}; + +QQmlDMCachedModelData::QQmlDMCachedModelData(QQmlDelegateModelItemMetaType *metaType, VDMModelDelegateDataType *dataType, int index, int row, int column) + : QQmlDelegateModelItem(metaType, dataType, index, row, column) + , type(dataType) +{ + if (index == -1) + cachedData.resize(type->hasModelData ? 1 : type->propertyRoles.count()); + + QObjectPrivate::get(this)->metaObject = type; + + type->addref(); +} + +int QQmlDMCachedModelData::metaCall(QMetaObject::Call call, int id, void **arguments) +{ + if (call == QMetaObject::ReadProperty && id >= type->propertyOffset) { + const int propertyIndex = id - type->propertyOffset; + if (index == -1) { + if (!cachedData.isEmpty()) { + *static_cast<QVariant *>(arguments[0]) = cachedData.at( + type->hasModelData ? 0 : propertyIndex); + } + } else if (*type->model) { + *static_cast<QVariant *>(arguments[0]) = value(type->propertyRoles.at(propertyIndex)); + } + return -1; + } else if (call == QMetaObject::WriteProperty && id >= type->propertyOffset) { + const int propertyIndex = id - type->propertyOffset; + if (index == -1) { + const QMetaObject *meta = metaObject(); + if (cachedData.count() > 1) { + cachedData[propertyIndex] = *static_cast<QVariant *>(arguments[0]); + QMetaObject::activate(this, meta, propertyIndex, nullptr); + } else if (cachedData.count() == 1) { + cachedData[0] = *static_cast<QVariant *>(arguments[0]); + QMetaObject::activate(this, meta, 0, nullptr); + QMetaObject::activate(this, meta, 1, nullptr); + } + } else if (*type->model) { + setValue(type->propertyRoles.at(propertyIndex), *static_cast<QVariant *>(arguments[0])); + } + return -1; + } else { + return qt_metacall(call, id, arguments); + } +} + +void QQmlDMCachedModelData::setValue(const QString &role, const QVariant &value) +{ + QHash<QByteArray, int>::iterator it = type->roleNames.find(role.toUtf8()); + if (it != type->roleNames.end()) { + for (int i = 0; i < type->propertyRoles.count(); ++i) { + if (type->propertyRoles.at(i) == *it) { + cachedData[i] = value; + return; + } + } + } +} + +bool QQmlDMCachedModelData::resolveIndex(const QQmlAdaptorModel &adaptorModel, int idx) +{ + if (index == -1) { + Q_ASSERT(idx >= 0); + cachedData.clear(); + setModelIndex(idx, adaptorModel.rowAt(idx), adaptorModel.columnAt(idx)); + const QMetaObject *meta = metaObject(); + const int propertyCount = type->propertyRoles.count(); + for (int i = 0; i < propertyCount; ++i) + QMetaObject::activate(this, meta, i, nullptr); + return true; + } else { + return false; + } +} + +QV4::ReturnedValue QQmlDMCachedModelData::get_property(const QV4::FunctionObject *b, const QV4::Value *thisObject, const QV4::Value *, int) +{ + QV4::Scope scope(b); + QV4::Scoped<QQmlDelegateModelItemObject> o(scope, thisObject->as<QQmlDelegateModelItemObject>()); + if (!o) + return scope.engine->throwTypeError(QStringLiteral("Not a valid DelegateModel object")); + + uint propertyId = static_cast<const QV4::IndexedBuiltinFunction *>(b)->d()->index; + + QQmlDMCachedModelData *modelData = static_cast<QQmlDMCachedModelData *>(o->d()->item); + if (o->d()->item->index == -1) { + if (!modelData->cachedData.isEmpty()) { + return scope.engine->fromVariant( + modelData->cachedData.at(modelData->type->hasModelData ? 0 : propertyId)); + } + } else if (*modelData->type->model) { + return scope.engine->fromVariant( + modelData->value(modelData->type->propertyRoles.at(propertyId))); + } + return QV4::Encode::undefined(); +} + +QV4::ReturnedValue QQmlDMCachedModelData::set_property(const QV4::FunctionObject *b, const QV4::Value *thisObject, const QV4::Value *argv, int argc) +{ + QV4::Scope scope(b); + QV4::Scoped<QQmlDelegateModelItemObject> o(scope, thisObject->as<QQmlDelegateModelItemObject>()); + if (!o) + return scope.engine->throwTypeError(QStringLiteral("Not a valid DelegateModel object")); + if (!argc) + return scope.engine->throwTypeError(); + + uint propertyId = static_cast<const QV4::IndexedBuiltinFunction *>(b)->d()->index; + + if (o->d()->item->index == -1) { + QQmlDMCachedModelData *modelData = static_cast<QQmlDMCachedModelData *>(o->d()->item); + if (!modelData->cachedData.isEmpty()) { + if (modelData->cachedData.count() > 1) { + modelData->cachedData[propertyId] = scope.engine->toVariant(argv[0], QVariant::Invalid); + QMetaObject::activate(o->d()->item, o->d()->item->metaObject(), propertyId, nullptr); + } else if (modelData->cachedData.count() == 1) { + modelData->cachedData[0] = scope.engine->toVariant(argv[0], QVariant::Invalid); + QMetaObject::activate(o->d()->item, o->d()->item->metaObject(), 0, nullptr); + QMetaObject::activate(o->d()->item, o->d()->item->metaObject(), 1, nullptr); + } + } + } + return QV4::Encode::undefined(); +} + +//----------------------------------------------------------------- +// QAbstractItemModel +//----------------------------------------------------------------- + +class QQmlDMAbstractItemModelData : public QQmlDMCachedModelData +{ + Q_OBJECT + Q_PROPERTY(bool hasModelChildren READ hasModelChildren CONSTANT) + +public: + QQmlDMAbstractItemModelData( + QQmlDelegateModelItemMetaType *metaType, + VDMModelDelegateDataType *dataType, + int index, int row, int column) + : QQmlDMCachedModelData(metaType, dataType, index, row, column) + { + } + + bool hasModelChildren() const + { + if (index >= 0 && *type->model) { + const QAbstractItemModel * const model = type->model->aim(); + return model->hasChildren(model->index(row, column, type->model->rootIndex)); + } else { + return false; + } + } + + QVariant value(int role) const override + { + return type->model->aim()->index(row, column, type->model->rootIndex).data(role); + } + + void setValue(int role, const QVariant &value) override + { + type->model->aim()->setData( + type->model->aim()->index(row, column, type->model->rootIndex), value, role); + } + + QV4::ReturnedValue get() override + { + if (type->prototype.isUndefined()) { + QQmlAdaptorModelEngineData * const data = engineData(v4); + type->initializeConstructor(data); + } + QV4::Scope scope(v4); + QV4::ScopedObject proto(scope, type->prototype.value()); + QV4::ScopedObject o(scope, proto->engine()->memoryManager->allocate<QQmlDelegateModelItemObject>(this)); + o->setPrototypeOf(proto); + ++scriptRef; + return o.asReturnedValue(); + } +}; + +class VDMAbstractItemModelDataType : public VDMModelDelegateDataType +{ +public: + VDMAbstractItemModelDataType(QQmlAdaptorModel *model) + : VDMModelDelegateDataType(model) + { + } + + int rowCount(const QQmlAdaptorModel &model) const override + { + return model.aim()->rowCount(model.rootIndex); + } + + int columnCount(const QQmlAdaptorModel &model) const override + { + return model.aim()->columnCount(model.rootIndex); + } + + void cleanup(QQmlAdaptorModel &) const override + { + const_cast<VDMAbstractItemModelDataType *>(this)->release(); + } + + QVariant value(const QQmlAdaptorModel &model, int index, const QString &role) const override + { + QHash<QByteArray, int>::const_iterator it = roleNames.find(role.toUtf8()); + if (it != roleNames.end()) { + return model.aim()->index(model.rowAt(index), model.columnAt(index), model.rootIndex).data(*it); + } else if (role == QLatin1String("hasModelChildren")) { + return QVariant(model.aim()->hasChildren(model.aim()->index(model.rowAt(index), model.columnAt(index), model.rootIndex))); + } else { + return QVariant(); + } + } + + QVariant parentModelIndex(const QQmlAdaptorModel &model) const override + { + return model + ? QVariant::fromValue(model.aim()->parent(model.rootIndex)) + : QVariant(); + } + + QVariant modelIndex(const QQmlAdaptorModel &model, int index) const override + { + return model + ? QVariant::fromValue(model.aim()->index(model.rowAt(index), model.columnAt(index), model.rootIndex)) + : QVariant(); + } + + bool canFetchMore(const QQmlAdaptorModel &model) const override + { + return model && model.aim()->canFetchMore(model.rootIndex); + } + + void fetchMore(QQmlAdaptorModel &model) const override + { + if (model) + model.aim()->fetchMore(model.rootIndex); + } + + QQmlDelegateModelItem *createItem( + QQmlAdaptorModel &model, + QQmlDelegateModelItemMetaType *metaType, + int index, int row, int column) const override + { + VDMAbstractItemModelDataType *dataType = const_cast<VDMAbstractItemModelDataType *>(this); + if (!metaObject) + dataType->initializeMetaType(model); + return new QQmlDMAbstractItemModelData(metaType, dataType, index, row, column); + } + + void initializeMetaType(QQmlAdaptorModel &model) + { + QMetaObjectBuilder builder; + setModelDataType<QQmlDMAbstractItemModelData>(&builder, this); + + const QByteArray propertyType = QByteArrayLiteral("QVariant"); + const QHash<int, QByteArray> names = model.aim()->roleNames(); + for (QHash<int, QByteArray>::const_iterator it = names.begin(), cend = names.end(); it != cend; ++it) { + const int propertyId = propertyRoles.count(); + propertyRoles.append(it.key()); + roleNames.insert(it.value(), it.key()); + addProperty(&builder, propertyId, it.value(), propertyType); + } + if (propertyRoles.count() == 1) { + hasModelData = true; + const int role = names.begin().key(); + const QByteArray propertyName = QByteArrayLiteral("modelData"); + + propertyRoles.append(role); + roleNames.insert(propertyName, role); + addProperty(&builder, 1, propertyName, propertyType); + } + + metaObject.reset(builder.toMetaObject()); + *static_cast<QMetaObject *>(this) = *metaObject; + propertyCache.adopt(new QQmlPropertyCache(metaObject.data(), model.modelItemRevision)); + } +}; + +//----------------------------------------------------------------- +// QQmlListAccessor +//----------------------------------------------------------------- + +class QQmlDMListAccessorData : public QQmlDelegateModelItem +{ + Q_OBJECT + Q_PROPERTY(QVariant modelData READ modelData WRITE setModelData NOTIFY modelDataChanged) +public: + QQmlDMListAccessorData(QQmlDelegateModelItemMetaType *metaType, + QQmlAdaptorModel::Accessors *accessor, + int index, int row, int column, const QVariant &value) + : QQmlDelegateModelItem(metaType, accessor, index, row, column) + , cachedData(value) + { + } + + QVariant modelData() const + { + return cachedData; + } + + void setModelData(const QVariant &data) + { + if (data == cachedData) + return; + + cachedData = data; + emit modelDataChanged(); + } + + static QV4::ReturnedValue get_modelData(const QV4::FunctionObject *b, const QV4::Value *thisObject, const QV4::Value *, int) + { + QV4::ExecutionEngine *v4 = b->engine(); + const QQmlDelegateModelItemObject *o = thisObject->as<QQmlDelegateModelItemObject>(); + if (!o) + return v4->throwTypeError(QStringLiteral("Not a valid DelegateModel object")); + + return v4->fromVariant(static_cast<QQmlDMListAccessorData *>(o->d()->item)->cachedData); + } + + static QV4::ReturnedValue set_modelData(const QV4::FunctionObject *b, const QV4::Value *thisObject, const QV4::Value *argv, int argc) + { + QV4::ExecutionEngine *v4 = b->engine(); + const QQmlDelegateModelItemObject *o = thisObject->as<QQmlDelegateModelItemObject>(); + if (!o) + return v4->throwTypeError(QStringLiteral("Not a valid DelegateModel object")); + if (!argc) + return v4->throwTypeError(); + + static_cast<QQmlDMListAccessorData *>(o->d()->item)->setModelData(v4->toVariant(argv[0], QVariant::Invalid)); + return QV4::Encode::undefined(); + } + + QV4::ReturnedValue get() override + { + QQmlAdaptorModelEngineData *data = engineData(v4); + QV4::Scope scope(v4); + QV4::ScopedObject o(scope, v4->memoryManager->allocate<QQmlDelegateModelItemObject>(this)); + QV4::ScopedObject p(scope, data->listItemProto.value()); + o->setPrototypeOf(p); + ++scriptRef; + return o.asReturnedValue(); + } + + void setValue(const QString &role, const QVariant &value) override + { + if (role == QLatin1String("modelData")) + cachedData = value; + } + + bool resolveIndex(const QQmlAdaptorModel &model, int idx) override + { + if (index == -1) { + index = idx; + cachedData = model.list.at(idx); + emit modelIndexChanged(); + emit modelDataChanged(); + return true; + } else { + return false; + } + } + + +Q_SIGNALS: + void modelDataChanged(); + +private: + QVariant cachedData; +}; + + +class VDMListDelegateDataType : public QQmlRefCount, public QQmlAdaptorModel::Accessors +{ +public: + VDMListDelegateDataType() + : QQmlRefCount() + , QQmlAdaptorModel::Accessors() + {} + + void cleanup(QQmlAdaptorModel &) const override + { + const_cast<VDMListDelegateDataType *>(this)->release(); + } + + int rowCount(const QQmlAdaptorModel &model) const override + { + return model.list.count(); + } + + int columnCount(const QQmlAdaptorModel &) const override + { + return 1; + } + + QVariant value(const QQmlAdaptorModel &model, int index, const QString &role) const override + { + return role == QLatin1String("modelData") + ? model.list.at(index) + : QVariant(); + } + + QQmlDelegateModelItem *createItem( + QQmlAdaptorModel &model, + QQmlDelegateModelItemMetaType *metaType, + int index, int row, int column) const override + { + VDMListDelegateDataType *dataType = const_cast<VDMListDelegateDataType *>(this); + if (!propertyCache) { + dataType->propertyCache.adopt(new QQmlPropertyCache( + &QQmlDMListAccessorData::staticMetaObject, model.modelItemRevision)); + } + + return new QQmlDMListAccessorData( + metaType, + dataType, + index, row, column, + index >= 0 && index < model.list.count() ? model.list.at(index) : QVariant()); + } + + bool notify(const QQmlAdaptorModel &model, const QList<QQmlDelegateModelItem *> &items, int index, int count, const QVector<int> &) const override + { + for (auto modelItem : items) { + const int modelItemIndex = modelItem->index; + if (modelItemIndex < index || modelItemIndex >= index + count) + continue; + + auto listModelItem = static_cast<QQmlDMListAccessorData *>(modelItem); + QVariant updatedModelData = model.list.at(listModelItem->index); + listModelItem->setModelData(updatedModelData); + } + return true; + } +}; + +//----------------------------------------------------------------- +// QObject +//----------------------------------------------------------------- + +class VDMObjectDelegateDataType; +class QQmlDMObjectData : public QQmlDelegateModelItem, public QQmlAdaptorModelProxyInterface +{ + Q_OBJECT + Q_PROPERTY(QObject *modelData READ modelData NOTIFY modelDataChanged) + Q_INTERFACES(QQmlAdaptorModelProxyInterface) +public: + QQmlDMObjectData( + QQmlDelegateModelItemMetaType *metaType, + VDMObjectDelegateDataType *dataType, + int index, int row, int column, + QObject *object); + + void setModelData(QObject *modelData) + { + if (modelData == object) + return; + + object = modelData; + emit modelDataChanged(); + } + + QObject *modelData() const { return object; } + QObject *proxiedObject() override { return object; } + + QPointer<QObject> object; + +Q_SIGNALS: + void modelDataChanged(); +}; + +class VDMObjectDelegateDataType : public QQmlRefCount, public QQmlAdaptorModel::Accessors +{ +public: + int propertyOffset; + int signalOffset; + bool shared; + QMetaObjectBuilder builder; + + VDMObjectDelegateDataType() + : propertyOffset(0) + , signalOffset(0) + , shared(true) + { + } + + VDMObjectDelegateDataType(const VDMObjectDelegateDataType &type) + : QQmlRefCount() + , QQmlAdaptorModel::Accessors() + , propertyOffset(type.propertyOffset) + , signalOffset(type.signalOffset) + , shared(false) + , builder(type.metaObject.data(), QMetaObjectBuilder::Properties + | QMetaObjectBuilder::Signals + | QMetaObjectBuilder::SuperClass + | QMetaObjectBuilder::ClassName) + { + builder.setFlags(QMetaObjectBuilder::DynamicMetaObject); + } + + int rowCount(const QQmlAdaptorModel &model) const override + { + return model.list.count(); + } + + int columnCount(const QQmlAdaptorModel &) const override + { + return 1; + } + + QVariant value(const QQmlAdaptorModel &model, int index, const QString &role) const override + { + if (QObject *object = model.list.at(index).value<QObject *>()) + return object->property(role.toUtf8()); + return QVariant(); + } + + QQmlDelegateModelItem *createItem( + QQmlAdaptorModel &model, + QQmlDelegateModelItemMetaType *metaType, + int index, int row, int column) const override + { + VDMObjectDelegateDataType *dataType = const_cast<VDMObjectDelegateDataType *>(this); + if (!metaObject) + dataType->initializeMetaType(model); + return index >= 0 && index < model.list.count() + ? new QQmlDMObjectData(metaType, dataType, index, row, column, qvariant_cast<QObject *>(model.list.at(index))) + : nullptr; + } + + void initializeMetaType(QQmlAdaptorModel &model) + { + Q_UNUSED(model); + setModelDataType<QQmlDMObjectData>(&builder, this); + + metaObject.reset(builder.toMetaObject()); + // Note: ATM we cannot create a shared property cache for this class, since each model + // object can have different properties. And to make those properties available to the + // delegate, QQmlDMObjectData makes use of a QAbstractDynamicMetaObject subclass + // (QQmlDMObjectDataMetaObject), which we cannot represent in a QQmlPropertyCache. + // By not having a shared property cache, revisioned properties in QQmlDelegateModelItem + // will always be available to the delegate, regardless of the import version. + } + + void cleanup(QQmlAdaptorModel &) const override + { + const_cast<VDMObjectDelegateDataType *>(this)->release(); + } + + bool notify(const QQmlAdaptorModel &model, const QList<QQmlDelegateModelItem *> &items, int index, int count, const QVector<int> &) const override + { + for (auto modelItem : items) { + const int modelItemIndex = modelItem->index; + if (modelItemIndex < index || modelItemIndex >= index + count) + continue; + + auto objectModelItem = static_cast<QQmlDMObjectData *>(modelItem); + QObject *updatedModelData = qvariant_cast<QObject *>(model.list.at(objectModelItem->index)); + objectModelItem->setModelData(updatedModelData); + } + return true; + } +}; + +class QQmlDMObjectDataMetaObject : public QAbstractDynamicMetaObject +{ +public: + QQmlDMObjectDataMetaObject(QQmlDMObjectData *data, VDMObjectDelegateDataType *type) + : m_data(data) + , m_type(type) + { + QObjectPrivate *op = QObjectPrivate::get(m_data); + *static_cast<QMetaObject *>(this) = *type->metaObject; + op->metaObject = this; + m_type->addref(); + } + + ~QQmlDMObjectDataMetaObject() + { + m_type->release(); + } + + int metaCall(QObject *o, QMetaObject::Call call, int id, void **arguments) override + { + Q_ASSERT(o == m_data); + Q_UNUSED(o); + + static const int objectPropertyOffset = QObject::staticMetaObject.propertyCount(); + if (id >= m_type->propertyOffset + && (call == QMetaObject::ReadProperty + || call == QMetaObject::WriteProperty + || call == QMetaObject::ResetProperty)) { + if (m_data->object) + QMetaObject::metacall(m_data->object, call, id - m_type->propertyOffset + objectPropertyOffset, arguments); + return -1; + } else if (id >= m_type->signalOffset && call == QMetaObject::InvokeMetaMethod) { + QMetaObject::activate(m_data, this, id - m_type->signalOffset, nullptr); + return -1; + } else { + return m_data->qt_metacall(call, id, arguments); + } + } + + int createProperty(const char *name, const char *) override + { + if (!m_data->object) + return -1; + const QMetaObject *metaObject = m_data->object->metaObject(); + static const int objectPropertyOffset = QObject::staticMetaObject.propertyCount(); + + const int previousPropertyCount = propertyCount() - propertyOffset(); + int propertyIndex = metaObject->indexOfProperty(name); + if (propertyIndex == -1) + return -1; + if (previousPropertyCount + objectPropertyOffset == metaObject->propertyCount()) + return propertyIndex + m_type->propertyOffset - objectPropertyOffset; + + if (m_type->shared) { + VDMObjectDelegateDataType *type = m_type; + m_type = new VDMObjectDelegateDataType(*m_type); + type->release(); + } + + const int previousMethodCount = methodCount(); + int notifierId = previousMethodCount - methodOffset(); + for (int propertyId = previousPropertyCount; propertyId < metaObject->propertyCount() - objectPropertyOffset; ++propertyId) { + QMetaProperty property = metaObject->property(propertyId + objectPropertyOffset); + QMetaPropertyBuilder propertyBuilder; + if (property.hasNotifySignal()) { + m_type->builder.addSignal("__" + QByteArray::number(propertyId) + "()"); + propertyBuilder = m_type->builder.addProperty(property.name(), property.typeName(), notifierId); + ++notifierId; + } else { + propertyBuilder = m_type->builder.addProperty(property.name(), property.typeName()); + } + propertyBuilder.setWritable(property.isWritable()); + propertyBuilder.setResettable(property.isResettable()); + propertyBuilder.setConstant(property.isConstant()); + } + + m_type->metaObject.reset(m_type->builder.toMetaObject()); + *static_cast<QMetaObject *>(this) = *m_type->metaObject; + + notifierId = previousMethodCount; + for (int i = previousPropertyCount; i < metaObject->propertyCount() - objectPropertyOffset; ++i) { + QMetaProperty property = metaObject->property(i + objectPropertyOffset); + if (property.hasNotifySignal()) { + QQmlPropertyPrivate::connect( + m_data->object, property.notifySignalIndex(), m_data, notifierId); + ++notifierId; + } + } + return propertyIndex + m_type->propertyOffset - objectPropertyOffset; + } + + QQmlDMObjectData *m_data; + VDMObjectDelegateDataType *m_type; +}; + +QQmlDMObjectData::QQmlDMObjectData(QQmlDelegateModelItemMetaType *metaType, + VDMObjectDelegateDataType *dataType, + int index, int row, int column, + QObject *object) + : QQmlDelegateModelItem(metaType, dataType, index, row, column) + , object(object) +{ + new QQmlDMObjectDataMetaObject(this, dataType); +} + +//----------------------------------------------------------------- +// QQmlAdaptorModel +//----------------------------------------------------------------- + +static const QQmlAdaptorModel::Accessors qt_vdm_null_accessors; + +QQmlAdaptorModel::Accessors::~Accessors() +{ +} + +QQmlAdaptorModel::QQmlAdaptorModel() + : accessors(&qt_vdm_null_accessors) +{ +} + +QQmlAdaptorModel::~QQmlAdaptorModel() +{ + accessors->cleanup(*this); +} + +void QQmlAdaptorModel::setModel(const QVariant &variant, QObject *parent, QQmlEngine *engine) +{ + accessors->cleanup(*this); + + list.setList(variant, engine); + + if (QObject *object = qvariant_cast<QObject *>(list.list())) { + setObject(object, parent); + if (qobject_cast<QAbstractItemModel *>(object)) + accessors = new VDMAbstractItemModelDataType(this); + else + accessors = new VDMObjectDelegateDataType; + } else if (list.type() == QQmlListAccessor::ListProperty) { + setObject(static_cast<const QQmlListReference *>(variant.constData())->object(), parent); + accessors = new VDMObjectDelegateDataType; + } else if (list.type() != QQmlListAccessor::Invalid + && list.type() != QQmlListAccessor::Instance) { // Null QObject + setObject(nullptr, parent); + accessors = new VDMListDelegateDataType; + } else { + setObject(nullptr, parent); + accessors = &qt_vdm_null_accessors; + } +} + +void QQmlAdaptorModel::invalidateModel() +{ + accessors->cleanup(*this); + accessors = &qt_vdm_null_accessors; + // Don't clear the model object as we still need the guard to clear the list variant if the + // object is destroyed. +} + +bool QQmlAdaptorModel::isValid() const +{ + return accessors != &qt_vdm_null_accessors; +} + +int QQmlAdaptorModel::count() const +{ + return rowCount() * columnCount(); +} + +int QQmlAdaptorModel::rowCount() const +{ + return qMax(0, accessors->rowCount(*this)); +} + +int QQmlAdaptorModel::columnCount() const +{ + return qMax(0, accessors->columnCount(*this)); +} + +int QQmlAdaptorModel::rowAt(int index) const +{ + int count = rowCount(); + return count <= 0 ? -1 : index % count; +} + +int QQmlAdaptorModel::columnAt(int index) const +{ + int count = rowCount(); + return count <= 0 ? -1 : index / count; +} + +int QQmlAdaptorModel::indexAt(int row, int column) const +{ + return column * rowCount() + row; +} + +void QQmlAdaptorModel::useImportVersion(int minorVersion) +{ + modelItemRevision = minorVersion; +} + +void QQmlAdaptorModel::objectDestroyed(QObject *) +{ + setModel(QVariant(), nullptr, nullptr); +} + +QQmlAdaptorModelEngineData::QQmlAdaptorModelEngineData(QV4::ExecutionEngine *v4) + : v4(v4) +{ + QV4::Scope scope(v4); + QV4::ScopedObject proto(scope, v4->newObject()); + proto->defineAccessorProperty(QStringLiteral("index"), get_index, nullptr); + proto->defineAccessorProperty(QStringLiteral("modelData"), + QQmlDMListAccessorData::get_modelData, QQmlDMListAccessorData::set_modelData); + listItemProto.set(v4, proto); +} + +QQmlAdaptorModelEngineData::~QQmlAdaptorModelEngineData() +{ +} + +QT_END_NAMESPACE + +#include <qqmladaptormodel.moc> diff --git a/src/qmlmodels/qqmladaptormodel_p.h b/src/qmlmodels/qqmladaptormodel_p.h new file mode 100644 index 0000000000..a4549127af --- /dev/null +++ b/src/qmlmodels/qqmladaptormodel_p.h @@ -0,0 +1,181 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtQml module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QQMLADAPTORMODEL_P_H +#define QQMLADAPTORMODEL_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <QtCore/qabstractitemmodel.h> + +#include <private/qqmlglobal_p.h> +#include <private/qqmllistaccessor_p.h> +#include <private/qtqmlmodelsglobal_p.h> +#include <private/qqmlguard_p.h> +#include <private/qqmlnullablevalue_p.h> +#include <private/qqmlpropertycache_p.h> + +QT_REQUIRE_CONFIG(qml_delegate_model); + +QT_BEGIN_NAMESPACE + +class QQmlEngine; + +class QQmlDelegateModel; +class QQmlDelegateModelItem; +class QQmlDelegateModelItemMetaType; + +class Q_QMLMODELS_PRIVATE_EXPORT QQmlAdaptorModel : public QQmlStrongJSQObjectReference<QObject> +{ +public: + class Accessors + { + public: + inline Accessors() {} + virtual ~Accessors(); + virtual int rowCount(const QQmlAdaptorModel &) const { return 0; } + virtual int columnCount(const QQmlAdaptorModel &) const { return 0; } + virtual void cleanup(QQmlAdaptorModel &) const {} + + virtual QVariant value(const QQmlAdaptorModel &, int, const QString &) const { + return QVariant(); } + + virtual QQmlDelegateModelItem *createItem( + QQmlAdaptorModel &, + QQmlDelegateModelItemMetaType *, + int, int, int) const { return nullptr; } + + virtual bool notify( + const QQmlAdaptorModel &, + const QList<QQmlDelegateModelItem *> &, + int, + int, + const QVector<int> &) const { return false; } + virtual void replaceWatchedRoles( + QQmlAdaptorModel &, + const QList<QByteArray> &, + const QList<QByteArray> &) const {} + virtual QVariant parentModelIndex(const QQmlAdaptorModel &) const { + return QVariant(); } + virtual QVariant modelIndex(const QQmlAdaptorModel &, int) const { + return QVariant(); } + virtual bool canFetchMore(const QQmlAdaptorModel &) const { return false; } + virtual void fetchMore(QQmlAdaptorModel &) const {} + + QScopedPointer<QMetaObject, QScopedPointerPodDeleter> metaObject; + QQmlRefPointer<QQmlPropertyCache> propertyCache; + }; + + const Accessors *accessors; + QPersistentModelIndex rootIndex; + QQmlListAccessor list; + + int modelItemRevision = 0; + + QQmlAdaptorModel(); + ~QQmlAdaptorModel(); + + inline QVariant model() const { return list.list(); } + void setModel(const QVariant &variant, QObject *parent, QQmlEngine *engine); + void invalidateModel(); + + bool isValid() const; + int count() const; + int rowCount() const; + int columnCount() const; + int rowAt(int index) const; + int columnAt(int index) const; + int indexAt(int row, int column) const; + + void useImportVersion(int minorVersion); + + inline bool adaptsAim() const { return qobject_cast<QAbstractItemModel *>(object()); } + inline QAbstractItemModel *aim() { return static_cast<QAbstractItemModel *>(object()); } + inline const QAbstractItemModel *aim() const { return static_cast<const QAbstractItemModel *>(object()); } + + inline QVariant value(int index, const QString &role) const { + return accessors->value(*this, index, role); } + inline QQmlDelegateModelItem *createItem(QQmlDelegateModelItemMetaType *metaType, int index) { + return accessors->createItem(*this, metaType, index, rowAt(index), columnAt(index)); } + inline bool hasProxyObject() const { + return list.type() == QQmlListAccessor::Instance || list.type() == QQmlListAccessor::ListProperty; } + + inline bool notify( + const QList<QQmlDelegateModelItem *> &items, + int index, + int count, + const QVector<int> &roles) const { + return accessors->notify(*this, items, index, count, roles); } + inline void replaceWatchedRoles( + const QList<QByteArray> &oldRoles, const QList<QByteArray> &newRoles) { + accessors->replaceWatchedRoles(*this, oldRoles, newRoles); } + + inline QVariant modelIndex(int index) const { return accessors->modelIndex(*this, index); } + inline QVariant parentModelIndex() const { return accessors->parentModelIndex(*this); } + inline bool canFetchMore() const { return accessors->canFetchMore(*this); } + inline void fetchMore() { return accessors->fetchMore(*this); } + +protected: + void objectDestroyed(QObject *) override; +}; + +class QQmlAdaptorModelProxyInterface +{ +public: + virtual ~QQmlAdaptorModelProxyInterface() {} + + virtual QObject *proxiedObject() = 0; +}; + +#define QQmlAdaptorModelProxyInterface_iid "org.qt-project.Qt.QQmlAdaptorModelProxyInterface" + +Q_DECLARE_INTERFACE(QQmlAdaptorModelProxyInterface, QQmlAdaptorModelProxyInterface_iid) + +QT_END_NAMESPACE + +#endif diff --git a/src/qmlmodels/qqmlchangeset.cpp b/src/qmlmodels/qqmlchangeset.cpp new file mode 100644 index 0000000000..ba876b42e2 --- /dev/null +++ b/src/qmlmodels/qqmlchangeset.cpp @@ -0,0 +1,583 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtQml module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "qqmlchangeset_p.h" + +QT_BEGIN_NAMESPACE + + +/*! + \class QQmlChangeSet + \brief The QQmlChangeSet class stores an ordered list of notifications about + changes to a linear data set. + \internal + + QQmlChangeSet can be used to record a series of notifications about items in an indexed list + being inserted, removed, moved, and changed. Notifications in the set are re-ordered so that + all notifications of a single type are grouped together and sorted in order of ascending index, + with remove notifications preceding all others, followed by insert notification, and then + change notifications. + + Moves in a change set are represented by a remove notification paired with an insert + notification by way of a shared unique moveId. Re-ordering may result in one or both of the + paired notifications being divided, when this happens the offset member of the notification + will indicate the relative offset of the divided notification from the beginning of the + original. +*/ + +/*! + Constructs an empty change set. +*/ + +QQmlChangeSet::QQmlChangeSet() + : m_difference(0) +{ +} + +/*! + Constructs a copy of a \a changeSet. +*/ + +QQmlChangeSet::QQmlChangeSet(const QQmlChangeSet &changeSet) + : m_removes(changeSet.m_removes) + , m_inserts(changeSet.m_inserts) + , m_changes(changeSet.m_changes) + , m_difference(changeSet.m_difference) +{ +} + +/*! + Destroys a change set. +*/ + +QQmlChangeSet::~QQmlChangeSet() +{ +} + +/*! + Assigns the value of a \a changeSet to another. +*/ + +QQmlChangeSet &QQmlChangeSet::operator =(const QQmlChangeSet &changeSet) +{ + m_removes = changeSet.m_removes; + m_inserts = changeSet.m_inserts; + m_changes = changeSet.m_changes; + m_difference = changeSet.m_difference; + return *this; +} + +/*! + Appends a notification that \a count items were inserted at \a index. +*/ + +void QQmlChangeSet::insert(int index, int count) +{ + insert(QVector<Change>() << Change(index, count)); +} + +/*! + Appends a notification that \a count items were removed at \a index. +*/ + +void QQmlChangeSet::remove(int index, int count) +{ + QVector<Change> removes; + removes.append(Change(index, count)); + remove(&removes, nullptr); +} + +/*! + Appends a notification that \a count items were moved \a from one index \a to another. + + The \a moveId must be unique across the lifetime of the change set and any related + change sets. +*/ + +void QQmlChangeSet::move(int from, int to, int count, int moveId) +{ + QVector<Change> removes; + removes.append(Change(from, count, moveId)); + QVector<Change> inserts; + inserts.append(Change(to, count, moveId)); + remove(&removes, &inserts); + insert(inserts); +} + +/*! + Appends a notification that \a count items were changed at \a index. +*/ + +void QQmlChangeSet::change(int index, int count) +{ + QVector<Change> changes; + changes.append(Change(index, count)); + change(changes); +} + +/*! + Applies the changes in a \a changeSet to another. +*/ + +void QQmlChangeSet::apply(const QQmlChangeSet &changeSet) +{ + QVector<Change> r = changeSet.m_removes; + QVector<Change> i = changeSet.m_inserts; + QVector<Change> c = changeSet.m_changes; + remove(&r, &i); + insert(i); + change(c); +} + +/*! + Applies a list of \a removes to a change set. + + If a remove contains a moveId then any intersecting insert in the set will replace the + corresponding intersection in the optional \a inserts list. +*/ + +void QQmlChangeSet::remove(const QVector<Change> &removes, QVector<Change> *inserts) +{ + QVector<Change> r = removes; + remove(&r, inserts); +} + +void QQmlChangeSet::remove(QVector<Change> *removes, QVector<Change> *inserts) +{ + int removeCount = 0; + int insertCount = 0; + QVector<Change>::iterator insert = m_inserts.begin(); + QVector<Change>::iterator change = m_changes.begin(); + QVector<Change>::iterator rit = removes->begin(); + for (; rit != removes->end(); ++rit) { + int index = rit->index + removeCount; + int count = rit->count; + + // Decrement the accumulated remove count from the indexes of any changes prior to the + // current remove. + for (; change != m_changes.end() && change->end() < rit->index; ++change) + change->index -= removeCount; + // Remove any portion of a change notification that intersects the current remove. + for (; change != m_changes.end() && change->index > rit->end(); ++change) { + change->count -= qMin(change->end(), rit->end()) - qMax(change->index, rit->index); + if (change->count == 0) { + change = m_changes.erase(change); + } else if (rit->index < change->index) { + change->index = rit->index; + } + } + + // Decrement the accumulated remove count from the indexes of any inserts prior to the + // current remove. + for (; insert != m_inserts.end() && insert->end() <= index; ++insert) { + insertCount += insert->count; + insert->index -= removeCount; + } + + rit->index -= insertCount; + + // Remove any portion of a insert notification that intersects the current remove. + while (insert != m_inserts.end() && insert->index < index + count) { + int offset = index - insert->index; + const int difference = qMin(insert->end(), index + count) - qMax(insert->index, index); + + // If part of the remove or insert that precedes the intersection has a moveId create + // a new delta for that portion and subtract the size of that delta from the current + // one. + if (offset < 0 && rit->moveId != -1) { + rit = removes->insert(rit, Change( + rit->index, -offset, rit->moveId, rit->offset)); + ++rit; + rit->count -= -offset; + rit->offset += -offset; + index += -offset; + count -= -offset; + removeCount += -offset; + offset = 0; + } else if (offset > 0 && insert->moveId != -1) { + insert = m_inserts.insert(insert, Change( + insert->index - removeCount, offset, insert->moveId, insert->offset)); + ++insert; + insert->index += offset; + insert->count -= offset; + insert->offset += offset; + rit->index -= offset; + insertCount += offset; + } + + // If the current remove has a move id, find any inserts with the same move id and + // replace the corresponding sections with the insert removed from the change set. + if (rit->moveId != -1 && difference > 0 && inserts) { + for (QVector<Change>::iterator iit = inserts->begin(); iit != inserts->end(); ++iit) { + if (iit->moveId != rit->moveId + || rit->offset > iit->offset + iit->count + || iit->offset > rit->offset + difference) { + continue; + } + // If the intersecting insert starts before the replacement one create + // a new insert for the portion prior to the replacement insert. + const int overlapOffset = rit->offset - iit->offset; + if (overlapOffset > 0) { + iit = inserts->insert(iit, Change( + iit->index, overlapOffset, iit->moveId, iit->offset)); + ++iit; + iit->index += overlapOffset; + iit->count -= overlapOffset; + iit->offset += overlapOffset; + } + if (iit->offset >= rit->offset + && iit->offset + iit->count <= rit->offset + difference) { + // If the replacement insert completely encapsulates the existing + // one just change the moveId. + iit->moveId = insert->moveId; + iit->offset = insert->offset + qMax(0, -overlapOffset); + } else { + // Create a new insertion before the intersecting one with the number of intersecting + // items and remove that number from that insert. + const int count + = qMin(iit->offset + iit->count, rit->offset + difference) + - qMax(iit->offset, rit->offset); + iit = inserts->insert(iit, Change( + iit->index, + count, + insert->moveId, + insert->offset + qMax(0, -overlapOffset))); + ++iit; + iit->index += count; + iit->count -= count; + iit->offset += count; + } + } + } + + // Subtract the number of intersecting items from the current remove and insert. + insert->count -= difference; + insert->offset += difference; + rit->count -= difference; + rit->offset += difference; + + index += difference; + count -= difference; + removeCount += difference; + + if (insert->count == 0) { + insert = m_inserts.erase(insert); + } else if (rit->count == -offset || rit->count == 0) { + insert->index += difference; + break; + } else { + insert->index -= removeCount - difference; + rit->index -= insert->count; + insertCount += insert->count; + ++insert; + } + } + removeCount += rit->count; + } + for (; insert != m_inserts.end(); ++insert) + insert->index -= removeCount; + + removeCount = 0; + QVector<Change>::iterator remove = m_removes.begin(); + for (rit = removes->begin(); rit != removes->end(); ++rit) { + if (rit->count == 0) + continue; + // Accumulate consecutive removes into a single delta before attempting to apply. + for (QVector<Change>::iterator next = rit + 1; next != removes->end() + && next->index == rit->index + && next->moveId == -1 + && rit->moveId == -1; ++next) { + next->count += rit->count; + rit = next; + } + int index = rit->index + removeCount; + // Decrement the accumulated remove count from the indexes of any inserts prior to the + // current remove. + for (; remove != m_removes.end() && index > remove->index; ++remove) + remove->index -= removeCount; + while (remove != m_removes.end() && index + rit->count >= remove->index) { + int count = 0; + const int offset = remove->index - index; + QVector<Change>::iterator rend = remove; + for (; rend != m_removes.end() + && rit->moveId == -1 + && rend->moveId == -1 + && index + rit->count >= rend->index; ++rend) { + count += rend->count; + } + if (remove != rend) { + // Accumulate all existing non-move removes that are encapsulated by or immediately + // follow the current remove into it. + int difference = 0; + if (rend == m_removes.end()) { + difference = rit->count; + } else if (rit->index + rit->count < rend->index - removeCount) { + difference = rit->count; + } else if (rend->moveId != -1) { + difference = rend->index - removeCount - rit->index; + index += difference; + } + count += difference; + + rit->count -= difference; + removeCount += difference; + remove->index = rit->index; + remove->count = count; + remove = m_removes.erase(++remove, rend); + } else { + // Insert a remove for the portion of the unmergable current remove prior to the + // point of intersection. + if (offset > 0) { + remove = m_removes.insert(remove, Change( + rit->index, offset, rit->moveId, rit->offset)); + ++remove; + rit->count -= offset; + rit->offset += offset; + removeCount += offset; + index += offset; + } + remove->index = rit->index; + + ++remove; + } + } + + if (rit->count > 0) { + remove = m_removes.insert(remove, *rit); + ++remove; + } + removeCount += rit->count; + } + for (; remove != m_removes.end(); ++remove) + remove->index -= removeCount; + m_difference -= removeCount; +} + +/*! + Applies a list of \a inserts to a change set. +*/ + +void QQmlChangeSet::insert(const QVector<Change> &inserts) +{ + int insertCount = 0; + QVector<Change>::iterator insert = m_inserts.begin(); + QVector<Change>::iterator change = m_changes.begin(); + for (QVector<Change>::const_iterator iit = inserts.begin(); iit != inserts.end(); ++iit) { + if (iit->count == 0) + continue; + int index = iit->index - insertCount; + + Change current = *iit; + // Accumulate consecutive inserts into a single delta before attempting to insert. + for (QVector<Change>::const_iterator next = iit + 1; next != inserts.end() + && next->index == iit->index + iit->count + && next->moveId == -1 + && iit->moveId == -1; ++next) { + current.count += next->count; + iit = next; + } + + // Increment the index of any changes before the current insert by the accumlated insert + // count. + for (; change != m_changes.end() && change->index >= index; ++change) + change->index += insertCount; + // If the current insert index is in the middle of a change split it in two at that + // point and increment the index of the latter half. + if (change != m_changes.end() && change->index < index + iit->count) { + int offset = index - change->index; + change = m_changes.insert(change, Change(change->index + insertCount, offset)); + ++change; + change->index += iit->count + offset; + change->count -= offset; + } + + // Increment the index of any inserts before the current insert by the accumlated insert + // count. + for (; insert != m_inserts.end() && index > insert->index + insert->count; ++insert) + insert->index += insertCount; + if (insert == m_inserts.end()) { + insert = m_inserts.insert(insert, current); + ++insert; + } else { + const int offset = index - insert->index; + + if (offset < 0) { + // If the current insert is before an existing insert and not adjacent just insert + // it into the list. + insert = m_inserts.insert(insert, current); + ++insert; + } else if (iit->moveId == -1 && insert->moveId == -1) { + // If neither the current nor existing insert has a moveId add the current insert + // to the existing one. + if (offset < insert->count) { + insert->index -= current.count; + insert->count += current.count; + } else { + insert->index += insertCount; + insert->count += current.count; + ++insert; + } + } else if (offset < insert->count) { + // If either insert has a moveId then split the existing insert and insert the + // current one in the middle. + if (offset > 0) { + insert = m_inserts.insert(insert, Change( + insert->index + insertCount, offset, insert->moveId, insert->offset)); + ++insert; + insert->index += offset; + insert->count -= offset; + insert->offset += offset; + } + insert = m_inserts.insert(insert, current); + ++insert; + } else { + insert->index += insertCount; + ++insert; + insert = m_inserts.insert(insert, current); + ++insert; + } + } + insertCount += current.count; + } + for (; insert != m_inserts.end(); ++insert) + insert->index += insertCount; + m_difference += insertCount; +} + +/*! + Applies a combined list of \a removes and \a inserts to a change set. This is equivalent + calling \l remove() followed by \l insert() with the same lists. +*/ + +void QQmlChangeSet::move(const QVector<Change> &removes, const QVector<Change> &inserts) +{ + QVector<Change> r = removes; + QVector<Change> i = inserts; + remove(&r, &i); + insert(i); +} + +/*! + Applies a list of \a changes to a change set. +*/ + +void QQmlChangeSet::change(const QVector<Change> &changes) +{ + QVector<Change> c = changes; + change(&c); +} + +void QQmlChangeSet::change(QVector<Change> *changes) +{ + QVector<Change>::iterator insert = m_inserts.begin(); + QVector<Change>::iterator change = m_changes.begin(); + for (QVector<Change>::iterator cit = changes->begin(); cit != changes->end(); ++cit) { + for (; insert != m_inserts.end() && insert->end() < cit->index; ++insert) {} + for (; insert != m_inserts.end() && insert->index < cit->end(); ++insert) { + const int offset = insert->index - cit->index; + const int count = cit->count + cit->index - insert->index - insert->count; + if (offset == 0) { + cit->index = insert->index + insert->count; + cit->count = count; + } else { + cit = changes->insert(++cit, Change(insert->index + insert->count, count)); + --cit; + cit->count = offset; + } + } + + for (; change != m_changes.end() && change->index + change->count < cit->index; ++change) {} + if (change == m_changes.end() || change->index > cit->index + cit->count) { + if (cit->count > 0) { + change = m_changes.insert(change, *cit); + ++change; + } + } else { + if (cit->index < change->index) { + change->count += change->index - cit->index; + change->index = cit->index; + } + + if (cit->index + cit->count > change->index + change->count) { + change->count = cit->index + cit->count - change->index; + QVector<Change>::iterator cbegin = change; + QVector<Change>::iterator cend = ++cbegin; + for (; cend != m_changes.end() && cend->index <= change->index + change->count; ++cend) { + if (cend->index + cend->count > change->index + change->count) + change->count = cend->index + cend->count - change->index; + } + if (cbegin != cend) { + change = m_changes.erase(cbegin, cend); + --change; + } + } + } + } +} + +/*! + Prints the contents of a change \a set to the \a debug stream. +*/ + +QDebug operator <<(QDebug debug, const QQmlChangeSet &set) +{ + debug.nospace() << "QQmlChangeSet("; + const QVector<QQmlChangeSet::Change> &removes = set.removes(); + for (const QQmlChangeSet::Change &remove : removes) + debug << remove; + const QVector<QQmlChangeSet::Change> &inserts = set.inserts(); + for (const QQmlChangeSet::Change &insert : inserts) + debug << insert; + const QVector<QQmlChangeSet::Change> &changes = set.changes(); + for (const QQmlChangeSet::Change &change : changes) + debug << change; + return debug.nospace() << ')'; +} + +/*! + Prints a \a change to the \a debug stream. +*/ + +QDebug operator <<(QDebug debug, const QQmlChangeSet::Change &change) +{ + return (debug.nospace() << "Change(" << change.index << ',' << change.count << ')').space(); +} + +QT_END_NAMESPACE + diff --git a/src/qmlmodels/qqmlchangeset_p.h b/src/qmlmodels/qqmlchangeset_p.h new file mode 100644 index 0000000000..5b44d2958c --- /dev/null +++ b/src/qmlmodels/qqmlchangeset_p.h @@ -0,0 +1,161 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtQml module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QQMLCHANGESET_P_H +#define QQMLCHANGESET_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <QtCore/qdebug.h> +#include <QtCore/qvector.h> +#include <QtQmlModels/private/qtqmlmodelsglobal_p.h> + +QT_BEGIN_NAMESPACE + +class Q_QMLMODELS_PRIVATE_EXPORT QQmlChangeSet +{ +public: + struct MoveKey + { + MoveKey() {} + MoveKey(int moveId, int offset) : moveId(moveId), offset(offset) {} + int moveId = -1; + int offset = 0; + }; + + // The storrage for Change (below). This struct is trivial, which it has to be in order to store + // it in a QV4::Heap::Base object. The Change struct doesn't add any storage fields, so it is + // safe to cast ChangeData to/from Change. + struct ChangeData + { + int index; + int count; + int moveId; + int offset; + }; + + struct Change: ChangeData + { + Change() { + index = 0; + count = 0; + moveId = -1; + offset = 0; + } + Change(int index, int count, int moveId = -1, int offset = 0) { + this->index = index; + this->count = count; + this->moveId = moveId; + this->offset = offset; + } + + bool isMove() const { return moveId >= 0; } + + MoveKey moveKey(int index) const { + return MoveKey(moveId, index - Change::index + offset); } + + int start() const { return index; } + int end() const { return index + count; } + }; + + QQmlChangeSet(); + QQmlChangeSet(const QQmlChangeSet &changeSet); + ~QQmlChangeSet(); + + QQmlChangeSet &operator =(const QQmlChangeSet &changeSet); + + const QVector<Change> &removes() const { return m_removes; } + const QVector<Change> &inserts() const { return m_inserts; } + const QVector<Change> &changes() const { return m_changes; } + + void insert(int index, int count); + void remove(int index, int count); + void move(int from, int to, int count, int moveId); + void change(int index, int count); + + void insert(const QVector<Change> &inserts); + void remove(const QVector<Change> &removes, QVector<Change> *inserts = nullptr); + void move(const QVector<Change> &removes, const QVector<Change> &inserts); + void change(const QVector<Change> &changes); + void apply(const QQmlChangeSet &changeSet); + + bool isEmpty() const { return m_removes.empty() && m_inserts.empty() && m_changes.isEmpty(); } + + void clear() + { + m_removes.clear(); + m_inserts.clear(); + m_changes.clear(); + m_difference = 0; + } + + int difference() const { return m_difference; } + +private: + void remove(QVector<Change> *removes, QVector<Change> *inserts); + void change(QVector<Change> *changes); + + QVector<Change> m_removes; + QVector<Change> m_inserts; + QVector<Change> m_changes; + int m_difference; +}; + +Q_DECLARE_TYPEINFO(QQmlChangeSet::Change, Q_PRIMITIVE_TYPE); +Q_DECLARE_TYPEINFO(QQmlChangeSet::MoveKey, Q_PRIMITIVE_TYPE); + +inline uint qHash(const QQmlChangeSet::MoveKey &key) { return qHash(qMakePair(key.moveId, key.offset)); } +inline bool operator ==(const QQmlChangeSet::MoveKey &l, const QQmlChangeSet::MoveKey &r) { + return l.moveId == r.moveId && l.offset == r.offset; } + +Q_QMLMODELS_PRIVATE_EXPORT QDebug operator <<(QDebug debug, const QQmlChangeSet::Change &change); +Q_QMLMODELS_PRIVATE_EXPORT QDebug operator <<(QDebug debug, const QQmlChangeSet &change); + +QT_END_NAMESPACE + +#endif diff --git a/src/qmlmodels/qqmldelegatecomponent.cpp b/src/qmlmodels/qqmldelegatecomponent.cpp new file mode 100644 index 0000000000..a7e9536917 --- /dev/null +++ b/src/qmlmodels/qqmldelegatecomponent.cpp @@ -0,0 +1,300 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtQml module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "qqmldelegatecomponent_p.h" +#include <QtQmlModels/private/qqmladaptormodel_p.h> + +QT_BEGIN_NAMESPACE + +QQmlAbstractDelegateComponent::QQmlAbstractDelegateComponent(QObject *parent) + : QQmlComponent(parent) +{ +} + +QQmlAbstractDelegateComponent::~QQmlAbstractDelegateComponent() +{ +} + +QVariant QQmlAbstractDelegateComponent::value(QQmlAdaptorModel *adaptorModel, int row, int column, const QString &role) const +{ + if (!adaptorModel) + return QVariant(); + return adaptorModel->value(adaptorModel->indexAt(row, column), role); +} + +/*! + \qmlmodule Qt.labs.qmlmodels 1.0 + \title Qt Labs QML Models - QML Types + \ingroup qmlmodules + \brief The Qt Labs QML Models module provides various model-related types for use with views. + + To use this module, import the module with the following line: + + \qml + import Qt.labs.qmlmodels 1.0 + \endqml +*/ + +/*! + \qmltype DelegateChoice + \instantiates QQmlDelegateChoice + \inqmlmodule Qt.labs.qmlmodels + \brief Encapsulates a delegate and when to use it. + + The DelegateChoice type wraps a delegate and defines the circumstances + in which it should be chosen. + + DelegateChoices can be nested inside a DelegateChooser. + + \sa DelegateChooser +*/ + +/*! + \qmlproperty string QtQml.Models::DelegateChoice::roleValue + This property holds the value used to match the role data for the role provided by \l DelegateChooser::role. +*/ +QVariant QQmlDelegateChoice::roleValue() const +{ + return m_value; +} + +void QQmlDelegateChoice::setRoleValue(const QVariant &value) +{ + if (m_value == value) + return; + m_value = value; + emit roleValueChanged(); + emit changed(); +} + +/*! + \qmlproperty index QtQml.Models::DelegateChoice::row + This property holds the value used to match the row value of model elements. + With models that have only the index property (and thus only one column), this property + should be intended as an index, and set to the desired index value. + + \note Setting both row and index has undefined behavior. The two are equivalent and only + one should be used. + + \sa index +*/ + +/*! + \qmlproperty index QtQml.Models::DelegateChoice::index + This property holds the value used to match the index value of model elements. + This is effectively an alias for \l row. + + \sa row +*/ +int QQmlDelegateChoice::row() const +{ + return m_row; +} + +void QQmlDelegateChoice::setRow(int r) +{ + if (m_row == r) + return; + m_row = r; + emit rowChanged(); + emit indexChanged(); + emit changed(); +} + +/*! + \qmlproperty index QtQml.Models::DelegateChoice::column + This property holds the value used to match the column value of model elements. +*/ +int QQmlDelegateChoice::column() const +{ + return m_column; +} + +void QQmlDelegateChoice::setColumn(int c) +{ + if (m_column == c) + return; + m_column = c; + emit columnChanged(); + emit changed(); +} + +QQmlComponent *QQmlDelegateChoice::delegate() const +{ + return m_delegate; +} + +/*! + \qmlproperty Component QtQml.Models::DelegateChoice::delegate + This property holds the delegate to use if this choice matches the model item. +*/ +void QQmlDelegateChoice::setDelegate(QQmlComponent *delegate) +{ + if (m_delegate == delegate) + return; + QQmlAbstractDelegateComponent *adc = static_cast<QQmlAbstractDelegateComponent *>(m_delegate); + if (adc) + disconnect(adc, &QQmlAbstractDelegateComponent::delegateChanged, this, &QQmlDelegateChoice::delegateChanged); + m_delegate = delegate; + adc = static_cast<QQmlAbstractDelegateComponent *>(delegate); + if (adc) + connect(adc, &QQmlAbstractDelegateComponent::delegateChanged, this, &QQmlDelegateChoice::delegateChanged); + emit delegateChanged(); + emit changed(); +} + +bool QQmlDelegateChoice::match(int row, int column, const QVariant &value) const +{ + if (!m_value.isValid() && m_row < 0 && m_column < 0) + return true; + + const bool roleMatched = (m_value.isValid()) ? value == m_value : true; + const bool rowMatched = (m_row < 0 ) ? true : m_row == row; + const bool columnMatched = (m_column < 0 ) ? true : m_column == column; + return roleMatched && rowMatched && columnMatched; +} + +/*! + \qmltype DelegateChooser + \instantiates QQmlDelegateChooser + \inqmlmodule Qt.labs.qmlmodels + \brief Allows a view to use different delegates for different types of items in the model. + + The DelegateChooser is a special \l Component type intended for those scenarios where a Component is required + by a view and used as a delegate. + DelegateChooser encapsulates a set of \l {DelegateChoice}s. + These choices are used determine the delegate that will be instantiated for each + item in the model. + The selection of the choice is performed based on the value that a model item has for \l role, + and also based on index. + + \note This type is intended to transparently work only with TableView and any DelegateModel-based view. + Views (including user-defined views) that aren't internally based on a DelegateModel need to explicitly support + this type of component to make it function as described. + + \sa DelegateChoice +*/ + +/*! + \qmlproperty string QtQml.Models::DelegateChooser::role + This property holds the role used to determine the delegate for a given model item. + + \sa DelegateChoice +*/ +void QQmlDelegateChooser::setRole(const QString &role) +{ + if (m_role == role) + return; + m_role = role; + emit roleChanged(); +} + +/*! + \qmlproperty list<DelegateChoice> QtQml.Models::DelegateChooser::choices + \default + + The list of DelegateChoices for the chooser. + + The list is treated as an ordered list, where the first DelegateChoice to match + will be used be a view. + + It should not generally be necessary to refer to the \c choices property, + as it is the default property for DelegateChooser and thus all child items are + automatically assigned to this property. +*/ + +QQmlListProperty<QQmlDelegateChoice> QQmlDelegateChooser::choices() +{ + return QQmlListProperty<QQmlDelegateChoice>(this, nullptr, + QQmlDelegateChooser::choices_append, + QQmlDelegateChooser::choices_count, + QQmlDelegateChooser::choices_at, + QQmlDelegateChooser::choices_clear); +} + +void QQmlDelegateChooser::choices_append(QQmlListProperty<QQmlDelegateChoice> *prop, QQmlDelegateChoice *choice) +{ + QQmlDelegateChooser *q = static_cast<QQmlDelegateChooser *>(prop->object); + q->m_choices.append(choice); + connect(choice, &QQmlDelegateChoice::changed, q, &QQmlAbstractDelegateComponent::delegateChanged); + q->delegateChanged(); +} + +int QQmlDelegateChooser::choices_count(QQmlListProperty<QQmlDelegateChoice> *prop) +{ + QQmlDelegateChooser *q = static_cast<QQmlDelegateChooser*>(prop->object); + return q->m_choices.count(); +} + +QQmlDelegateChoice *QQmlDelegateChooser::choices_at(QQmlListProperty<QQmlDelegateChoice> *prop, int index) +{ + QQmlDelegateChooser *q = static_cast<QQmlDelegateChooser*>(prop->object); + return q->m_choices.at(index); +} + +void QQmlDelegateChooser::choices_clear(QQmlListProperty<QQmlDelegateChoice> *prop) +{ + QQmlDelegateChooser *q = static_cast<QQmlDelegateChooser *>(prop->object); + for (QQmlDelegateChoice *choice : q->m_choices) + disconnect(choice, &QQmlDelegateChoice::changed, q, &QQmlAbstractDelegateComponent::delegateChanged); + q->m_choices.clear(); + q->delegateChanged(); +} + +QQmlComponent *QQmlDelegateChooser::delegate(QQmlAdaptorModel *adaptorModel, int row, int column) const +{ + QVariant v; + if (!m_role.isNull()) + v = value(adaptorModel, row, column, m_role); + if (!v.isValid()) { // check if the row only has modelData, for example if the row is a QVariantMap + v = value(adaptorModel, row, column, QStringLiteral("modelData")); + if (v.isValid()) + v = v.toMap().value(m_role); + } + // loop through choices, finding first one that fits + for (int i = 0; i < m_choices.count(); ++i) { + const QQmlDelegateChoice *choice = m_choices.at(i); + if (choice->match(row, column, v)) + return choice->delegate(); + } + + return nullptr; +} + +QT_END_NAMESPACE diff --git a/src/qmlmodels/qqmldelegatecomponent_p.h b/src/qmlmodels/qqmldelegatecomponent_p.h new file mode 100644 index 0000000000..1d20f0327b --- /dev/null +++ b/src/qmlmodels/qqmldelegatecomponent_p.h @@ -0,0 +1,155 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtQml module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QQMLDELEGATECOMPONENT_P_H +#define QQMLDELEGATECOMPONENT_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <private/qtqmlmodelsglobal_p.h> +#include <qqmlcomponent.h> + +QT_REQUIRE_CONFIG(qml_delegate_model); + +QT_BEGIN_NAMESPACE + +// TODO: consider making QQmlAbstractDelegateComponent public API +class QQmlAbstractDelegateComponentPrivate; +class QQmlAdaptorModel; +class Q_QMLMODELS_PRIVATE_EXPORT QQmlAbstractDelegateComponent : public QQmlComponent +{ + Q_OBJECT +public: + QQmlAbstractDelegateComponent(QObject *parent = nullptr); + ~QQmlAbstractDelegateComponent() override; + + virtual QQmlComponent *delegate(QQmlAdaptorModel *adaptorModel, int row, int column = 0) const = 0; + +signals: + void delegateChanged(); + +protected: + QVariant value(QQmlAdaptorModel *adaptorModel,int row, int column, const QString &role) const; + +private: + Q_DECLARE_PRIVATE(QQmlAbstractDelegateComponent) + Q_DISABLE_COPY(QQmlAbstractDelegateComponent) +}; + +class Q_QMLMODELS_PRIVATE_EXPORT QQmlDelegateChoice : public QObject +{ + Q_OBJECT + Q_PROPERTY(QVariant roleValue READ roleValue WRITE setRoleValue NOTIFY roleValueChanged) + Q_PROPERTY(int row READ row WRITE setRow NOTIFY rowChanged) + Q_PROPERTY(int index READ row WRITE setRow NOTIFY indexChanged) + Q_PROPERTY(int column READ column WRITE setColumn NOTIFY columnChanged) + Q_PROPERTY(QQmlComponent* delegate READ delegate WRITE setDelegate NOTIFY delegateChanged) + Q_CLASSINFO("DefaultProperty", "delegate") +public: + QVariant roleValue() const; + void setRoleValue(const QVariant &roleValue); + + int row() const; + void setRow(int r); + + int column() const; + void setColumn(int c); + + QQmlComponent *delegate() const; + void setDelegate(QQmlComponent *delegate); + + virtual bool match(int row, int column, const QVariant &value) const; + +signals: + void roleValueChanged(); + void rowChanged(); + void indexChanged(); + void columnChanged(); + void delegateChanged(); + void changed(); + +private: + QVariant m_value; + int m_row = -1; + int m_column = -1; + QQmlComponent *m_delegate = nullptr; +}; + +class Q_QMLMODELS_PRIVATE_EXPORT QQmlDelegateChooser : public QQmlAbstractDelegateComponent +{ + Q_OBJECT + Q_PROPERTY(QString role READ role WRITE setRole NOTIFY roleChanged) + Q_PROPERTY(QQmlListProperty<QQmlDelegateChoice> choices READ choices CONSTANT) + Q_CLASSINFO("DefaultProperty", "choices") + +public: + QString role() const { return m_role; } + void setRole(const QString &role); + + virtual QQmlListProperty<QQmlDelegateChoice> choices(); + static void choices_append(QQmlListProperty<QQmlDelegateChoice> *, QQmlDelegateChoice *); + static int choices_count(QQmlListProperty<QQmlDelegateChoice> *); + static QQmlDelegateChoice *choices_at(QQmlListProperty<QQmlDelegateChoice> *, int); + static void choices_clear(QQmlListProperty<QQmlDelegateChoice> *); + + QQmlComponent *delegate(QQmlAdaptorModel *adaptorModel, int row, int column = -1) const override; + +signals: + void roleChanged(); + +private: + QString m_role; + QList<QQmlDelegateChoice *> m_choices; +}; + +QT_END_NAMESPACE + +QML_DECLARE_TYPE(QQmlDelegateChoice) +QML_DECLARE_TYPE(QQmlDelegateChooser) + +#endif // QQMLDELEGATECOMPONENT_P_H diff --git a/src/qmlmodels/qqmldelegatemodel.cpp b/src/qmlmodels/qqmldelegatemodel.cpp new file mode 100644 index 0000000000..742c164508 --- /dev/null +++ b/src/qmlmodels/qqmldelegatemodel.cpp @@ -0,0 +1,3540 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtQml module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "qqmldelegatemodel_p_p.h" +#include "qqmldelegatecomponent_p.h" + +#include <QtQml/qqmlinfo.h> + +#include <private/qquickpackage_p.h> +#include <private/qmetaobjectbuilder_p.h> +#include <private/qqmladaptormodel_p.h> +#include <private/qqmlchangeset_p.h> +#include <private/qqmlengine_p.h> +#include <private/qqmlcomponent_p.h> +#include <private/qqmlincubator_p.h> + +#include <private/qv4value_p.h> +#include <private/qv4functionobject_p.h> +#include <private/qv4objectiterator_p.h> + +QT_BEGIN_NAMESPACE + +class QQmlDelegateModelItem; + +namespace QV4 { + +namespace Heap { + +struct DelegateModelGroupFunction : FunctionObject { + void init(QV4::ExecutionContext *scope, uint flag, QV4::ReturnedValue (*code)(QQmlDelegateModelItem *item, uint flag, const QV4::Value &arg)); + + QV4::ReturnedValue (*code)(QQmlDelegateModelItem *item, uint flag, const QV4::Value &arg); + uint flag; +}; + +struct QQmlDelegateModelGroupChange : Object { + void init() { Object::init(); } + + QQmlChangeSet::ChangeData change; +}; + +struct QQmlDelegateModelGroupChangeArray : Object { + void init(const QVector<QQmlChangeSet::Change> &changes); + void destroy() { + delete changes; + Object::destroy(); + } + + QVector<QQmlChangeSet::Change> *changes; +}; + + +} + +struct DelegateModelGroupFunction : QV4::FunctionObject +{ + V4_OBJECT2(DelegateModelGroupFunction, FunctionObject) + + static Heap::DelegateModelGroupFunction *create(QV4::ExecutionContext *scope, uint flag, QV4::ReturnedValue (*code)(QQmlDelegateModelItem *item, uint flag, const QV4::Value &arg)) + { + return scope->engine()->memoryManager->allocate<DelegateModelGroupFunction>(scope, flag, code); + } + + static ReturnedValue virtualCall(const QV4::FunctionObject *that, const Value *thisObject, const Value *argv, int argc) + { + QV4::Scope scope(that->engine()); + QV4::Scoped<DelegateModelGroupFunction> f(scope, static_cast<const DelegateModelGroupFunction *>(that)); + QV4::Scoped<QQmlDelegateModelItemObject> o(scope, thisObject); + if (!o) + return scope.engine->throwTypeError(QStringLiteral("Not a valid DelegateModel object")); + + QV4::ScopedValue v(scope, argc ? argv[0] : Value::undefinedValue()); + return f->d()->code(o->d()->item, f->d()->flag, v); + } +}; + +void Heap::DelegateModelGroupFunction::init(QV4::ExecutionContext *scope, uint flag, QV4::ReturnedValue (*code)(QQmlDelegateModelItem *item, uint flag, const QV4::Value &arg)) +{ + QV4::Heap::FunctionObject::init(scope, QStringLiteral("DelegateModelGroupFunction")); + this->flag = flag; + this->code = code; +} + +} + +DEFINE_OBJECT_VTABLE(QV4::DelegateModelGroupFunction); + + + +class QQmlDelegateModelEngineData : public QV8Engine::Deletable +{ +public: + QQmlDelegateModelEngineData(QV4::ExecutionEngine *v4); + ~QQmlDelegateModelEngineData(); + + QV4::ReturnedValue array(QV4::ExecutionEngine *engine, + const QVector<QQmlChangeSet::Change> &changes); + + QV4::PersistentValue changeProto; +}; + +V4_DEFINE_EXTENSION(QQmlDelegateModelEngineData, engineData) + + +void QQmlDelegateModelPartsMetaObject::propertyCreated(int, QMetaPropertyBuilder &prop) +{ + prop.setWritable(false); +} + +QVariant QQmlDelegateModelPartsMetaObject::initialValue(int id) +{ + QQmlDelegateModelParts *parts = static_cast<QQmlDelegateModelParts *>(object()); + QQmlPartsModel *m = new QQmlPartsModel( + parts->model, QString::fromUtf8(name(id)), parts); + parts->models.append(m); + return QVariant::fromValue(static_cast<QObject *>(m)); +} + +QQmlDelegateModelParts::QQmlDelegateModelParts(QQmlDelegateModel *parent) +: QObject(parent), model(parent) +{ + new QQmlDelegateModelPartsMetaObject(this); +} + +//--------------------------------------------------------------------------- + +/*! + \qmltype DelegateModel + \instantiates QQmlDelegateModel + \inqmlmodule QtQml.Models + \brief Encapsulates a model and delegate. + + The DelegateModel type encapsulates a model and the delegate that will + be instantiated for items in the model. + + It is usually not necessary to create a DelegateModel. + However, it can be useful for manipulating and accessing the \l modelIndex + when a QAbstractItemModel subclass is used as the + model. Also, DelegateModel is used together with \l Package to + provide delegates to multiple views, and with DelegateModelGroup to sort and filter + delegate items. + + The example below illustrates using a DelegateModel with a ListView. + + \snippet delegatemodel/delegatemodel.qml 0 +*/ + +QQmlDelegateModelPrivate::QQmlDelegateModelPrivate(QQmlContext *ctxt) + : m_delegateChooser(nullptr) + , m_cacheMetaType(nullptr) + , m_context(ctxt) + , m_parts(nullptr) + , m_filterGroup(QStringLiteral("items")) + , m_count(0) + , m_groupCount(Compositor::MinimumGroupCount) + , m_compositorGroup(Compositor::Cache) + , m_complete(false) + , m_delegateValidated(false) + , m_reset(false) + , m_transaction(false) + , m_incubatorCleanupScheduled(false) + , m_waitingToFetchMore(false) + , m_cacheItems(nullptr) + , m_items(nullptr) + , m_persistedItems(nullptr) +{ +} + +QQmlDelegateModelPrivate::~QQmlDelegateModelPrivate() +{ + qDeleteAll(m_finishedIncubating); + + if (m_cacheMetaType) + m_cacheMetaType->release(); +} + +int QQmlDelegateModelPrivate::adaptorModelCount() const +{ + // QQmlDelegateModel currently only support list models. + // So even if a model is a table model, only the first + // column will be used. + return m_adaptorModel.rowCount(); +} + +void QQmlDelegateModelPrivate::requestMoreIfNecessary() +{ + Q_Q(QQmlDelegateModel); + if (!m_waitingToFetchMore && m_adaptorModel.canFetchMore()) { + m_waitingToFetchMore = true; + QCoreApplication::postEvent(q, new QEvent(QEvent::UpdateRequest)); + } +} + +void QQmlDelegateModelPrivate::init() +{ + Q_Q(QQmlDelegateModel); + m_compositor.setRemoveGroups(Compositor::GroupMask & ~Compositor::PersistedFlag); + + m_items = new QQmlDelegateModelGroup(QStringLiteral("items"), q, Compositor::Default, q); + m_items->setDefaultInclude(true); + m_persistedItems = new QQmlDelegateModelGroup(QStringLiteral("persistedItems"), q, Compositor::Persisted, q); + QQmlDelegateModelGroupPrivate::get(m_items)->emitters.insert(this); +} + +QQmlDelegateModel::QQmlDelegateModel() + : QQmlDelegateModel(nullptr, nullptr) +{ +} + +QQmlDelegateModel::QQmlDelegateModel(QQmlContext *ctxt, QObject *parent) +: QQmlInstanceModel(*(new QQmlDelegateModelPrivate(ctxt)), parent) +{ + Q_D(QQmlDelegateModel); + d->init(); +} + +QQmlDelegateModel::~QQmlDelegateModel() +{ + Q_D(QQmlDelegateModel); + d->disconnectFromAbstractItemModel(); + d->m_adaptorModel.setObject(nullptr, this); + + for (QQmlDelegateModelItem *cacheItem : qAsConst(d->m_cache)) { + if (cacheItem->object) { + delete cacheItem->object; + + cacheItem->object = nullptr; + cacheItem->contextData->invalidate(); + Q_ASSERT(cacheItem->contextData->refCount == 1); + cacheItem->contextData = nullptr; + cacheItem->scriptRef -= 1; + } + cacheItem->groups &= ~Compositor::UnresolvedFlag; + cacheItem->objectRef = 0; + if (!cacheItem->isReferenced()) + delete cacheItem; + else if (cacheItem->incubationTask) + cacheItem->incubationTask->vdm = nullptr; + } +} + + +void QQmlDelegateModel::classBegin() +{ + Q_D(QQmlDelegateModel); + if (!d->m_context) + d->m_context = qmlContext(this); +} + +void QQmlDelegateModel::componentComplete() +{ + Q_D(QQmlDelegateModel); + d->m_complete = true; + + int defaultGroups = 0; + QStringList groupNames; + groupNames.append(QStringLiteral("items")); + groupNames.append(QStringLiteral("persistedItems")); + if (QQmlDelegateModelGroupPrivate::get(d->m_items)->defaultInclude) + defaultGroups |= Compositor::DefaultFlag; + if (QQmlDelegateModelGroupPrivate::get(d->m_persistedItems)->defaultInclude) + defaultGroups |= Compositor::PersistedFlag; + for (int i = Compositor::MinimumGroupCount; i < d->m_groupCount; ++i) { + QString name = d->m_groups[i]->name(); + if (name.isEmpty()) { + d->m_groups[i] = d->m_groups[d->m_groupCount - 1]; + --d->m_groupCount; + --i; + } else if (name.at(0).isUpper()) { + qmlWarning(d->m_groups[i]) << QQmlDelegateModelGroup::tr("Group names must start with a lower case letter"); + d->m_groups[i] = d->m_groups[d->m_groupCount - 1]; + --d->m_groupCount; + --i; + } else { + groupNames.append(name); + + QQmlDelegateModelGroupPrivate *group = QQmlDelegateModelGroupPrivate::get(d->m_groups[i]); + group->setModel(this, Compositor::Group(i)); + if (group->defaultInclude) + defaultGroups |= (1 << i); + } + } + + d->m_cacheMetaType = new QQmlDelegateModelItemMetaType( + d->m_context->engine()->handle(), this, groupNames); + + d->m_compositor.setGroupCount(d->m_groupCount); + d->m_compositor.setDefaultGroups(defaultGroups); + d->updateFilterGroup(); + + while (!d->m_pendingParts.isEmpty()) + static_cast<QQmlPartsModel *>(d->m_pendingParts.first())->updateFilterGroup(); + + QVector<Compositor::Insert> inserts; + d->m_count = d->adaptorModelCount(); + d->m_compositor.append( + &d->m_adaptorModel, + 0, + d->m_count, + defaultGroups | Compositor::AppendFlag | Compositor::PrependFlag, + &inserts); + d->itemsInserted(inserts); + d->emitChanges(); + d->requestMoreIfNecessary(); +} + +/*! + \qmlproperty model QtQml.Models::DelegateModel::model + This property holds the model providing data for the DelegateModel. + + The model provides a set of data that is used to create the items + for a view. For large or dynamic datasets the model is usually + provided by a C++ model object. The C++ model object must be a \l + {QAbstractItemModel} subclass or a simple list. + + Models can also be created directly in QML, using a \l{ListModel} or + \l{QtQuick.XmlListModel::XmlListModel}{XmlListModel}. + + \sa {qml-data-models}{Data Models} + \keyword dm-model-property +*/ +QVariant QQmlDelegateModel::model() const +{ + Q_D(const QQmlDelegateModel); + return d->m_adaptorModel.model(); +} + +void QQmlDelegateModelPrivate::connectToAbstractItemModel() +{ + Q_Q(QQmlDelegateModel); + if (!m_adaptorModel.adaptsAim()) + return; + + auto aim = m_adaptorModel.aim(); + + qmlobject_connect(aim, QAbstractItemModel, SIGNAL(rowsInserted(QModelIndex,int,int)), + q, QQmlDelegateModel, SLOT(_q_rowsInserted(QModelIndex,int,int))); + qmlobject_connect(aim, QAbstractItemModel, SIGNAL(rowsRemoved(QModelIndex,int,int)), + q, QQmlDelegateModel, SLOT(_q_rowsRemoved(QModelIndex,int,int))); + qmlobject_connect(aim, QAbstractItemModel, SIGNAL(rowsAboutToBeRemoved(QModelIndex,int,int)), + q, QQmlDelegateModel, SLOT(_q_rowsAboutToBeRemoved(QModelIndex,int,int))); + qmlobject_connect(aim, QAbstractItemModel, SIGNAL(dataChanged(QModelIndex,QModelIndex,QVector<int>)), + q, QQmlDelegateModel, SLOT(_q_dataChanged(QModelIndex,QModelIndex,QVector<int>))); + qmlobject_connect(aim, QAbstractItemModel, SIGNAL(rowsMoved(QModelIndex,int,int,QModelIndex,int)), + q, QQmlDelegateModel, SLOT(_q_rowsMoved(QModelIndex,int,int,QModelIndex,int))); + qmlobject_connect(aim, QAbstractItemModel, SIGNAL(modelReset()), + q, QQmlDelegateModel, SLOT(_q_modelReset())); + qmlobject_connect(aim, QAbstractItemModel, SIGNAL(layoutChanged(QList<QPersistentModelIndex>,QAbstractItemModel::LayoutChangeHint)), + q, QQmlDelegateModel, SLOT(_q_layoutChanged(QList<QPersistentModelIndex>,QAbstractItemModel::LayoutChangeHint))); +} + +void QQmlDelegateModelPrivate::disconnectFromAbstractItemModel() +{ + Q_Q(QQmlDelegateModel); + if (!m_adaptorModel.adaptsAim()) + return; + + auto aim = m_adaptorModel.aim(); + + QObject::disconnect(aim, SIGNAL(rowsInserted(QModelIndex,int,int)), + q, SLOT(_q_rowsInserted(QModelIndex,int,int))); + QObject::disconnect(aim, SIGNAL(rowsAboutToBeRemoved(QModelIndex,int,int)), + q, SLOT(_q_rowsAboutToBeRemoved(QModelIndex,int,int))); + QObject::disconnect(aim, SIGNAL(rowsRemoved(QModelIndex,int,int)), + q, SLOT(_q_rowsRemoved(QModelIndex,int,int))); + QObject::disconnect(aim, SIGNAL(dataChanged(QModelIndex,QModelIndex,QVector<int>)), + q, SLOT(_q_dataChanged(QModelIndex,QModelIndex,QVector<int>))); + QObject::disconnect(aim, SIGNAL(rowsMoved(QModelIndex,int,int,QModelIndex,int)), + q, SLOT(_q_rowsMoved(QModelIndex,int,int,QModelIndex,int))); + QObject::disconnect(aim, SIGNAL(modelReset()), + q, SLOT(_q_modelReset())); + QObject::disconnect(aim, SIGNAL(layoutChanged(QList<QPersistentModelIndex>,QAbstractItemModel::LayoutChangeHint)), + q, SLOT(_q_layoutChanged(QList<QPersistentModelIndex>,QAbstractItemModel::LayoutChangeHint))); +} + +void QQmlDelegateModel::setModel(const QVariant &model) +{ + Q_D(QQmlDelegateModel); + + if (d->m_complete) + _q_itemsRemoved(0, d->m_count); + + d->disconnectFromAbstractItemModel(); + d->m_adaptorModel.setModel(model, this, d->m_context->engine()); + d->connectToAbstractItemModel(); + + d->m_adaptorModel.replaceWatchedRoles(QList<QByteArray>(), d->m_watchedRoles); + for (int i = 0; d->m_parts && i < d->m_parts->models.count(); ++i) { + d->m_adaptorModel.replaceWatchedRoles( + QList<QByteArray>(), d->m_parts->models.at(i)->watchedRoles()); + } + + if (d->m_complete) { + _q_itemsInserted(0, d->adaptorModelCount()); + d->requestMoreIfNecessary(); + } +} + +/*! + \qmlproperty Component QtQml.Models::DelegateModel::delegate + + The delegate provides a template defining each item instantiated by a view. + The index is exposed as an accessible \c index property. Properties of the + model are also available depending upon the type of \l {qml-data-models}{Data Model}. +*/ +QQmlComponent *QQmlDelegateModel::delegate() const +{ + Q_D(const QQmlDelegateModel); + return d->m_delegate; +} + +void QQmlDelegateModel::setDelegate(QQmlComponent *delegate) +{ + Q_D(QQmlDelegateModel); + if (d->m_transaction) { + qmlWarning(this) << tr("The delegate of a DelegateModel cannot be changed within onUpdated."); + return; + } + if (d->m_delegate == delegate) + return; + bool wasValid = d->m_delegate != nullptr; + d->m_delegate.setObject(delegate, this); + d->m_delegateValidated = false; + if (d->m_delegateChooser) + QObject::disconnect(d->m_delegateChooserChanged); + + d->m_delegateChooser = nullptr; + if (delegate) { + QQmlAbstractDelegateComponent *adc = + qobject_cast<QQmlAbstractDelegateComponent *>(delegate); + if (adc) { + d->m_delegateChooser = adc; + d->m_delegateChooserChanged = connect(adc, &QQmlAbstractDelegateComponent::delegateChanged, + [d](){ d->delegateChanged(); }); + } + } + d->delegateChanged(d->m_delegate, wasValid); +} + +/*! + \qmlproperty QModelIndex QtQml.Models::DelegateModel::rootIndex + + QAbstractItemModel provides a hierarchical tree of data, whereas + QML only operates on list data. \c rootIndex allows the children of + any node in a QAbstractItemModel to be provided by this model. + + This property only affects models of type QAbstractItemModel that + are hierarchical (e.g, a tree model). + + For example, here is a simple interactive file system browser. + When a directory name is clicked, the view's \c rootIndex is set to the + QModelIndex node of the clicked directory, thus updating the view to show + the new directory's contents. + + \c main.cpp: + \snippet delegatemodel/delegatemodel_rootindex/main.cpp 0 + + \c view.qml: + \snippet delegatemodel/delegatemodel_rootindex/view.qml 0 + + If the \l {dm-model-property}{model} is a QAbstractItemModel subclass, + the delegate can also reference a \c hasModelChildren property (optionally + qualified by a \e model. prefix) that indicates whether the delegate's + model item has any child nodes. + + \sa modelIndex(), parentModelIndex() +*/ +QVariant QQmlDelegateModel::rootIndex() const +{ + Q_D(const QQmlDelegateModel); + return QVariant::fromValue(QModelIndex(d->m_adaptorModel.rootIndex)); +} + +void QQmlDelegateModel::setRootIndex(const QVariant &root) +{ + Q_D(QQmlDelegateModel); + + QModelIndex modelIndex = qvariant_cast<QModelIndex>(root); + const bool changed = d->m_adaptorModel.rootIndex != modelIndex; + if (changed || !d->m_adaptorModel.isValid()) { + const int oldCount = d->m_count; + d->m_adaptorModel.rootIndex = modelIndex; + if (!d->m_adaptorModel.isValid() && d->m_adaptorModel.aim()) { + // The previous root index was invalidated, so we need to reconnect the model. + d->disconnectFromAbstractItemModel(); + d->m_adaptorModel.setModel(d->m_adaptorModel.list.list(), this, d->m_context->engine()); + d->connectToAbstractItemModel(); + } + if (d->m_adaptorModel.canFetchMore()) + d->m_adaptorModel.fetchMore(); + if (d->m_complete) { + const int newCount = d->adaptorModelCount(); + if (oldCount) + _q_itemsRemoved(0, oldCount); + if (newCount) + _q_itemsInserted(0, newCount); + } + if (changed) + emit rootIndexChanged(); + } +} + +/*! + \qmlmethod QModelIndex QtQml.Models::DelegateModel::modelIndex(int index) + + QAbstractItemModel provides a hierarchical tree of data, whereas + QML only operates on list data. This function assists in using + tree models in QML. + + Returns a QModelIndex for the specified index. + This value can be assigned to rootIndex. + + \sa rootIndex +*/ +QVariant QQmlDelegateModel::modelIndex(int idx) const +{ + Q_D(const QQmlDelegateModel); + return d->m_adaptorModel.modelIndex(idx); +} + +/*! + \qmlmethod QModelIndex QtQml.Models::DelegateModel::parentModelIndex() + + QAbstractItemModel provides a hierarchical tree of data, whereas + QML only operates on list data. This function assists in using + tree models in QML. + + Returns a QModelIndex for the parent of the current rootIndex. + This value can be assigned to rootIndex. + + \sa rootIndex +*/ +QVariant QQmlDelegateModel::parentModelIndex() const +{ + Q_D(const QQmlDelegateModel); + return d->m_adaptorModel.parentModelIndex(); +} + +/*! + \qmlproperty int QtQml.Models::DelegateModel::count +*/ + +int QQmlDelegateModel::count() const +{ + Q_D(const QQmlDelegateModel); + if (!d->m_delegate) + return 0; + return d->m_compositor.count(d->m_compositorGroup); +} + +QQmlDelegateModel::ReleaseFlags QQmlDelegateModelPrivate::release(QObject *object) +{ + if (!object) + return QQmlDelegateModel::ReleaseFlags(0); + + QQmlDelegateModelItem *cacheItem = QQmlDelegateModelItem::dataForObject(object); + if (!cacheItem) + return QQmlDelegateModel::ReleaseFlags(0); + + if (!cacheItem->releaseObject()) + return QQmlDelegateModel::Referenced; + + cacheItem->destroyObject(); + emitDestroyingItem(object); + if (cacheItem->incubationTask) { + releaseIncubator(cacheItem->incubationTask); + cacheItem->incubationTask = nullptr; + } + cacheItem->Dispose(); + return QQmlInstanceModel::Destroyed; +} + +/* + Returns ReleaseStatus flags. +*/ + +QQmlDelegateModel::ReleaseFlags QQmlDelegateModel::release(QObject *item) +{ + Q_D(QQmlDelegateModel); + QQmlInstanceModel::ReleaseFlags stat = d->release(item); + return stat; +} + +// Cancel a requested async item +void QQmlDelegateModel::cancel(int index) +{ + Q_D(QQmlDelegateModel); + if (!d->m_delegate || index < 0 || index >= d->m_compositor.count(d->m_compositorGroup)) { + qWarning() << "DelegateModel::cancel: index out range" << index << d->m_compositor.count(d->m_compositorGroup); + return; + } + + Compositor::iterator it = d->m_compositor.find(d->m_compositorGroup, index); + QQmlDelegateModelItem *cacheItem = it->inCache() ? d->m_cache.at(it.cacheIndex) : 0; + if (cacheItem) { + if (cacheItem->incubationTask && !cacheItem->isObjectReferenced()) { + d->releaseIncubator(cacheItem->incubationTask); + cacheItem->incubationTask = nullptr; + + if (cacheItem->object) { + QObject *object = cacheItem->object; + cacheItem->destroyObject(); + if (QQuickPackage *package = qmlobject_cast<QQuickPackage *>(object)) + d->emitDestroyingPackage(package); + else + d->emitDestroyingItem(object); + } + + cacheItem->scriptRef -= 1; + } + if (!cacheItem->isReferenced()) { + d->m_compositor.clearFlags(Compositor::Cache, it.cacheIndex, 1, Compositor::CacheFlag); + d->m_cache.removeAt(it.cacheIndex); + delete cacheItem; + Q_ASSERT(d->m_cache.count() == d->m_compositor.count(Compositor::Cache)); + } + } +} + +void QQmlDelegateModelPrivate::group_append( + QQmlListProperty<QQmlDelegateModelGroup> *property, QQmlDelegateModelGroup *group) +{ + QQmlDelegateModelPrivate *d = static_cast<QQmlDelegateModelPrivate *>(property->data); + if (d->m_complete) + return; + if (d->m_groupCount == Compositor::MaximumGroupCount) { + qmlWarning(d->q_func()) << QQmlDelegateModel::tr("The maximum number of supported DelegateModelGroups is 8"); + return; + } + d->m_groups[d->m_groupCount] = group; + d->m_groupCount += 1; +} + +int QQmlDelegateModelPrivate::group_count( + QQmlListProperty<QQmlDelegateModelGroup> *property) +{ + QQmlDelegateModelPrivate *d = static_cast<QQmlDelegateModelPrivate *>(property->data); + return d->m_groupCount - 1; +} + +QQmlDelegateModelGroup *QQmlDelegateModelPrivate::group_at( + QQmlListProperty<QQmlDelegateModelGroup> *property, int index) +{ + QQmlDelegateModelPrivate *d = static_cast<QQmlDelegateModelPrivate *>(property->data); + return index >= 0 && index < d->m_groupCount - 1 + ? d->m_groups[index + 1] + : nullptr; +} + +/*! + \qmlproperty list<DelegateModelGroup> QtQml.Models::DelegateModel::groups + + This property holds a delegate model's group definitions. + + Groups define a sub-set of the items in a delegate model and can be used to filter + a model. + + For every group defined in a DelegateModel two attached properties are added to each + delegate item. The first of the form DelegateModel.in\e{GroupName} holds whether the + item belongs to the group and the second DelegateModel.\e{groupName}Index holds the + index of the item in that group. + + The following example illustrates using groups to select items in a model. + + \snippet delegatemodel/delegatemodelgroup.qml 0 + \keyword dm-groups-property +*/ + +QQmlListProperty<QQmlDelegateModelGroup> QQmlDelegateModel::groups() +{ + Q_D(QQmlDelegateModel); + return QQmlListProperty<QQmlDelegateModelGroup>( + this, + d, + QQmlDelegateModelPrivate::group_append, + QQmlDelegateModelPrivate::group_count, + QQmlDelegateModelPrivate::group_at, + nullptr); +} + +/*! + \qmlproperty DelegateModelGroup QtQml.Models::DelegateModel::items + + This property holds default group to which all new items are added. +*/ + +QQmlDelegateModelGroup *QQmlDelegateModel::items() +{ + Q_D(QQmlDelegateModel); + return d->m_items; +} + +/*! + \qmlproperty DelegateModelGroup QtQml.Models::DelegateModel::persistedItems + + This property holds delegate model's persisted items group. + + Items in this group are not destroyed when released by a view, instead they are persisted + until removed from the group. + + An item can be removed from the persistedItems group by setting the + DelegateModel.inPersistedItems property to false. If the item is not referenced by a view + at that time it will be destroyed. Adding an item to this group will not create a new + instance. + + Items returned by the \l QtQml.Models::DelegateModelGroup::create() function are automatically added + to this group. +*/ + +QQmlDelegateModelGroup *QQmlDelegateModel::persistedItems() +{ + Q_D(QQmlDelegateModel); + return d->m_persistedItems; +} + +/*! + \qmlproperty string QtQml.Models::DelegateModel::filterOnGroup + + This property holds name of the group that is used to filter the delegate model. + + Only items that belong to this group are visible to a view. + + By default this is the \l items group. +*/ + +QString QQmlDelegateModel::filterGroup() const +{ + Q_D(const QQmlDelegateModel); + return d->m_filterGroup; +} + +void QQmlDelegateModel::setFilterGroup(const QString &group) +{ + Q_D(QQmlDelegateModel); + + if (d->m_transaction) { + qmlWarning(this) << tr("The group of a DelegateModel cannot be changed within onChanged"); + return; + } + + if (d->m_filterGroup != group) { + d->m_filterGroup = group; + d->updateFilterGroup(); + emit filterGroupChanged(); + } +} + +void QQmlDelegateModel::resetFilterGroup() +{ + setFilterGroup(QStringLiteral("items")); +} + +void QQmlDelegateModelPrivate::updateFilterGroup() +{ + Q_Q(QQmlDelegateModel); + if (!m_cacheMetaType) + return; + + QQmlListCompositor::Group previousGroup = m_compositorGroup; + m_compositorGroup = Compositor::Default; + for (int i = 1; i < m_groupCount; ++i) { + if (m_filterGroup == m_cacheMetaType->groupNames.at(i - 1)) { + m_compositorGroup = Compositor::Group(i); + break; + } + } + + QQmlDelegateModelGroupPrivate::get(m_groups[m_compositorGroup])->emitters.insert(this); + if (m_compositorGroup != previousGroup) { + QVector<QQmlChangeSet::Change> removes; + QVector<QQmlChangeSet::Change> inserts; + m_compositor.transition(previousGroup, m_compositorGroup, &removes, &inserts); + + QQmlChangeSet changeSet; + changeSet.move(removes, inserts); + emit q->modelUpdated(changeSet, false); + + if (changeSet.difference() != 0) + emit q->countChanged(); + + if (m_parts) { + auto partsCopy = m_parts->models; // deliberate; this may alter m_parts + for (QQmlPartsModel *model : qAsConst(partsCopy)) + model->updateFilterGroup(m_compositorGroup, changeSet); + } + } +} + +/*! + \qmlproperty object QtQml.Models::DelegateModel::parts + + The \a parts property selects a DelegateModel which creates + delegates from the part named. This is used in conjunction with + the \l Package type. + + For example, the code below selects a model which creates + delegates named \e list from a \l Package: + + \code + DelegateModel { + id: visualModel + delegate: Package { + Item { Package.name: "list" } + } + model: myModel + } + + ListView { + width: 200; height:200 + model: visualModel.parts.list + } + \endcode + + \sa Package +*/ + +QObject *QQmlDelegateModel::parts() +{ + Q_D(QQmlDelegateModel); + if (!d->m_parts) + d->m_parts = new QQmlDelegateModelParts(this); + return d->m_parts; +} + +const QAbstractItemModel *QQmlDelegateModel::abstractItemModel() const +{ + Q_D(const QQmlDelegateModel); + return d->m_adaptorModel.adaptsAim() ? d->m_adaptorModel.aim() : nullptr; +} + +void QQmlDelegateModelPrivate::emitCreatedPackage(QQDMIncubationTask *incubationTask, QQuickPackage *package) +{ + for (int i = 1; i < m_groupCount; ++i) + QQmlDelegateModelGroupPrivate::get(m_groups[i])->createdPackage(incubationTask->index[i], package); +} + +void QQmlDelegateModelPrivate::emitInitPackage(QQDMIncubationTask *incubationTask, QQuickPackage *package) +{ + for (int i = 1; i < m_groupCount; ++i) + QQmlDelegateModelGroupPrivate::get(m_groups[i])->initPackage(incubationTask->index[i], package); +} + +void QQmlDelegateModelPrivate::emitDestroyingPackage(QQuickPackage *package) +{ + for (int i = 1; i < m_groupCount; ++i) + QQmlDelegateModelGroupPrivate::get(m_groups[i])->destroyingPackage(package); +} + +static bool isDoneIncubating(QQmlIncubator::Status status) +{ + return status == QQmlIncubator::Ready || status == QQmlIncubator::Error; +} + +void QQDMIncubationTask::statusChanged(Status status) +{ + if (vdm) { + vdm->incubatorStatusChanged(this, status); + } else if (isDoneIncubating(status)) { + Q_ASSERT(incubating); + // The model was deleted from under our feet, cleanup ourselves + delete incubating->object; + incubating->object = nullptr; + if (incubating->contextData) { + incubating->contextData->invalidate(); + Q_ASSERT(incubating->contextData->refCount == 1); + incubating->contextData = nullptr; + } + incubating->scriptRef = 0; + incubating->deleteLater(); + } +} + +void QQmlDelegateModelPrivate::releaseIncubator(QQDMIncubationTask *incubationTask) +{ + Q_Q(QQmlDelegateModel); + if (!incubationTask->isError()) + incubationTask->clear(); + m_finishedIncubating.append(incubationTask); + if (!m_incubatorCleanupScheduled) { + m_incubatorCleanupScheduled = true; + QCoreApplication::postEvent(q, new QEvent(QEvent::User)); + } +} + +void QQmlDelegateModelPrivate::addCacheItem(QQmlDelegateModelItem *item, Compositor::iterator it) +{ + m_cache.insert(it.cacheIndex, item); + m_compositor.setFlags(it, 1, Compositor::CacheFlag); + Q_ASSERT(m_cache.count() == m_compositor.count(Compositor::Cache)); +} + +void QQmlDelegateModelPrivate::removeCacheItem(QQmlDelegateModelItem *cacheItem) +{ + int cidx = m_cache.lastIndexOf(cacheItem); + if (cidx >= 0) { + m_compositor.clearFlags(Compositor::Cache, cidx, 1, Compositor::CacheFlag); + m_cache.removeAt(cidx); + } + Q_ASSERT(m_cache.count() == m_compositor.count(Compositor::Cache)); +} + +void QQmlDelegateModelPrivate::incubatorStatusChanged(QQDMIncubationTask *incubationTask, QQmlIncubator::Status status) +{ + if (!isDoneIncubating(status)) + return; + + const QList<QQmlError> incubationTaskErrors = incubationTask->errors(); + + QQmlDelegateModelItem *cacheItem = incubationTask->incubating; + cacheItem->incubationTask = nullptr; + incubationTask->incubating = nullptr; + releaseIncubator(incubationTask); + + if (status == QQmlIncubator::Ready) { + cacheItem->referenceObject(); + if (QQuickPackage *package = qmlobject_cast<QQuickPackage *>(cacheItem->object)) + emitCreatedPackage(incubationTask, package); + else + emitCreatedItem(incubationTask, cacheItem->object); + cacheItem->releaseObject(); + } else if (status == QQmlIncubator::Error) { + qmlInfo(m_delegate, incubationTaskErrors + m_delegate->errors()) << "Cannot create delegate"; + } + + if (!cacheItem->isObjectReferenced()) { + if (QQuickPackage *package = qmlobject_cast<QQuickPackage *>(cacheItem->object)) + emitDestroyingPackage(package); + else + emitDestroyingItem(cacheItem->object); + delete cacheItem->object; + cacheItem->object = nullptr; + cacheItem->scriptRef -= 1; + if (cacheItem->contextData) { + cacheItem->contextData->invalidate(); + Q_ASSERT(cacheItem->contextData->refCount == 1); + } + cacheItem->contextData = nullptr; + + if (!cacheItem->isReferenced()) { + removeCacheItem(cacheItem); + delete cacheItem; + } + } +} + +void QQDMIncubationTask::setInitialState(QObject *o) +{ + vdm->setInitialState(this, o); +} + +void QQmlDelegateModelPrivate::setInitialState(QQDMIncubationTask *incubationTask, QObject *o) +{ + QQmlDelegateModelItem *cacheItem = incubationTask->incubating; + cacheItem->object = o; + + if (QQuickPackage *package = qmlobject_cast<QQuickPackage *>(cacheItem->object)) + emitInitPackage(incubationTask, package); + else + emitInitItem(incubationTask, cacheItem->object); +} + +QObject *QQmlDelegateModelPrivate::object(Compositor::Group group, int index, QQmlIncubator::IncubationMode incubationMode) +{ + if (!m_delegate || index < 0 || index >= m_compositor.count(group)) { + qWarning() << "DelegateModel::item: index out range" << index << m_compositor.count(group); + return nullptr; + } else if (!m_context || !m_context->isValid()) { + return nullptr; + } + + Compositor::iterator it = m_compositor.find(group, index); + + QQmlDelegateModelItem *cacheItem = it->inCache() ? m_cache.at(it.cacheIndex) : 0; + + if (!cacheItem) { + cacheItem = m_adaptorModel.createItem(m_cacheMetaType, it.modelIndex()); + if (!cacheItem) + return nullptr; + + cacheItem->groups = it->flags; + addCacheItem(cacheItem, it); + } + + // Bump the reference counts temporarily so neither the content data or the delegate object + // are deleted if incubatorStatusChanged() is called synchronously. + cacheItem->scriptRef += 1; + cacheItem->referenceObject(); + + if (cacheItem->incubationTask) { + bool sync = (incubationMode == QQmlIncubator::Synchronous || incubationMode == QQmlIncubator::AsynchronousIfNested); + if (sync && cacheItem->incubationTask->incubationMode() == QQmlIncubator::Asynchronous) { + // previously requested async - now needed immediately + cacheItem->incubationTask->forceCompletion(); + } + } else if (!cacheItem->object) { + QQmlComponent *delegate = m_delegate; + if (m_delegateChooser) { + QQmlAbstractDelegateComponent *chooser = m_delegateChooser; + do { + delegate = chooser->delegate(&m_adaptorModel, index); + chooser = qobject_cast<QQmlAbstractDelegateComponent *>(delegate); + } while (chooser); + if (!delegate) + return nullptr; + } + + QQmlContext *creationContext = delegate->creationContext(); + + cacheItem->scriptRef += 1; + + cacheItem->incubationTask = new QQDMIncubationTask(this, incubationMode); + cacheItem->incubationTask->incubating = cacheItem; + cacheItem->incubationTask->clear(); + + for (int i = 1; i < m_groupCount; ++i) + cacheItem->incubationTask->index[i] = it.index[i]; + + QQmlContextData *ctxt = new QQmlContextData; + ctxt->setParent(QQmlContextData::get(creationContext ? creationContext : m_context.data())); + ctxt->contextObject = cacheItem; + cacheItem->contextData = ctxt; + + if (m_adaptorModel.hasProxyObject()) { + if (QQmlAdaptorModelProxyInterface *proxy + = qobject_cast<QQmlAdaptorModelProxyInterface *>(cacheItem)) { + ctxt = new QQmlContextData; + ctxt->setParent(cacheItem->contextData, /*stronglyReferencedByParent*/true); + QObject *proxied = proxy->proxiedObject(); + ctxt->contextObject = proxied; + // We don't own the proxied object. We need to clear it if it goes away. + QObject::connect(proxied, &QObject::destroyed, + cacheItem, &QQmlDelegateModelItem::childContextObjectDestroyed); + } + } + + QQmlComponentPrivate *cp = QQmlComponentPrivate::get(delegate); + cp->incubateObject( + cacheItem->incubationTask, + delegate, + m_context->engine(), + ctxt, + QQmlContextData::get(m_context)); + } + + if (index == m_compositor.count(group) - 1) + requestMoreIfNecessary(); + + // Remove the temporary reference count. + cacheItem->scriptRef -= 1; + if (cacheItem->object && (!cacheItem->incubationTask || isDoneIncubating(cacheItem->incubationTask->status()))) + return cacheItem->object; + + cacheItem->releaseObject(); + if (!cacheItem->isReferenced()) { + removeCacheItem(cacheItem); + delete cacheItem; + } + + return nullptr; +} + +/* + If asynchronous is true or the component is being loaded asynchronously due + to an ancestor being loaded asynchronously, object() may return 0. In this + case createdItem() will be emitted when the object is available. The object + at this stage does not have any references, so object() must be called again + to ensure a reference is held. Any call to object() which returns a valid object + must be matched by a call to release() in order to destroy the object. +*/ +QObject *QQmlDelegateModel::object(int index, QQmlIncubator::IncubationMode incubationMode) +{ + Q_D(QQmlDelegateModel); + if (!d->m_delegate || index < 0 || index >= d->m_compositor.count(d->m_compositorGroup)) { + qWarning() << "DelegateModel::item: index out range" << index << d->m_compositor.count(d->m_compositorGroup); + return nullptr; + } + + return d->object(d->m_compositorGroup, index, incubationMode); +} + +QQmlIncubator::Status QQmlDelegateModel::incubationStatus(int index) +{ + Q_D(QQmlDelegateModel); + Compositor::iterator it = d->m_compositor.find(d->m_compositorGroup, index); + if (!it->inCache()) + return QQmlIncubator::Null; + + if (auto incubationTask = d->m_cache.at(it.cacheIndex)->incubationTask) + return incubationTask->status(); + + return QQmlIncubator::Ready; +} + +QVariant QQmlDelegateModelPrivate::variantValue(QQmlListCompositor::Group group, int index, const QString &name) +{ + Compositor::iterator it = m_compositor.find(group, index); + if (QQmlAdaptorModel *model = it.list<QQmlAdaptorModel>()) { + QString role = name; + int dot = name.indexOf(QLatin1Char('.')); + if (dot > 0) + role = name.left(dot); + QVariant value = model->value(it.modelIndex(), role); + while (dot > 0) { + QObject *obj = qvariant_cast<QObject*>(value); + if (!obj) + return QVariant(); + const int from = dot + 1; + dot = name.indexOf(QLatin1Char('.'), from); + value = obj->property(name.midRef(from, dot - from).toUtf8()); + } + return value; + } + return QVariant(); +} + +QVariant QQmlDelegateModel::variantValue(int index, const QString &role) +{ + Q_D(QQmlDelegateModel); + return d->variantValue(d->m_compositorGroup, index, role); +} + +int QQmlDelegateModel::indexOf(QObject *item, QObject *) const +{ + Q_D(const QQmlDelegateModel); + if (QQmlDelegateModelItem *cacheItem = QQmlDelegateModelItem::dataForObject(item)) + return cacheItem->groupIndex(d->m_compositorGroup); + return -1; +} + +void QQmlDelegateModel::setWatchedRoles(const QList<QByteArray> &roles) +{ + Q_D(QQmlDelegateModel); + d->m_adaptorModel.replaceWatchedRoles(d->m_watchedRoles, roles); + d->m_watchedRoles = roles; +} + +void QQmlDelegateModelPrivate::addGroups( + Compositor::iterator from, int count, Compositor::Group group, int groupFlags) +{ + QVector<Compositor::Insert> inserts; + m_compositor.setFlags(from, count, group, groupFlags, &inserts); + itemsInserted(inserts); + emitChanges(); +} + +void QQmlDelegateModelPrivate::removeGroups( + Compositor::iterator from, int count, Compositor::Group group, int groupFlags) +{ + QVector<Compositor::Remove> removes; + m_compositor.clearFlags(from, count, group, groupFlags, &removes); + itemsRemoved(removes); + emitChanges(); +} + +void QQmlDelegateModelPrivate::setGroups( + Compositor::iterator from, int count, Compositor::Group group, int groupFlags) +{ + QVector<Compositor::Remove> removes; + QVector<Compositor::Insert> inserts; + + m_compositor.setFlags(from, count, group, groupFlags, &inserts); + itemsInserted(inserts); + const int removeFlags = ~groupFlags & Compositor::GroupMask; + + from = m_compositor.find(from.group, from.index[from.group]); + m_compositor.clearFlags(from, count, group, removeFlags, &removes); + itemsRemoved(removes); + emitChanges(); +} + +bool QQmlDelegateModel::event(QEvent *e) +{ + Q_D(QQmlDelegateModel); + if (e->type() == QEvent::UpdateRequest) { + d->m_waitingToFetchMore = false; + d->m_adaptorModel.fetchMore(); + } else if (e->type() == QEvent::User) { + d->m_incubatorCleanupScheduled = false; + qDeleteAll(d->m_finishedIncubating); + d->m_finishedIncubating.clear(); + } + return QQmlInstanceModel::event(e); +} + +void QQmlDelegateModelPrivate::itemsChanged(const QVector<Compositor::Change> &changes) +{ + if (!m_delegate) + return; + + QVarLengthArray<QVector<QQmlChangeSet::Change>, Compositor::MaximumGroupCount> translatedChanges(m_groupCount); + + for (const Compositor::Change &change : changes) { + for (int i = 1; i < m_groupCount; ++i) { + if (change.inGroup(i)) { + translatedChanges[i].append(QQmlChangeSet::Change(change.index[i], change.count)); + } + } + } + + for (int i = 1; i < m_groupCount; ++i) + QQmlDelegateModelGroupPrivate::get(m_groups[i])->changeSet.change(translatedChanges.at(i)); +} + +void QQmlDelegateModel::_q_itemsChanged(int index, int count, const QVector<int> &roles) +{ + Q_D(QQmlDelegateModel); + if (count <= 0 || !d->m_complete) + return; + + if (d->m_adaptorModel.notify(d->m_cache, index, count, roles)) { + QVector<Compositor::Change> changes; + d->m_compositor.listItemsChanged(&d->m_adaptorModel, index, count, &changes); + d->itemsChanged(changes); + d->emitChanges(); + } +} + +static void incrementIndexes(QQmlDelegateModelItem *cacheItem, int count, const int *deltas) +{ + if (QQDMIncubationTask *incubationTask = cacheItem->incubationTask) { + for (int i = 1; i < count; ++i) + incubationTask->index[i] += deltas[i]; + } + if (QQmlDelegateModelAttached *attached = cacheItem->attached) { + for (int i = 1; i < qMin<int>(count, Compositor::MaximumGroupCount); ++i) + attached->m_currentIndex[i] += deltas[i]; + } +} + +void QQmlDelegateModelPrivate::itemsInserted( + const QVector<Compositor::Insert> &inserts, + QVarLengthArray<QVector<QQmlChangeSet::Change>, Compositor::MaximumGroupCount> *translatedInserts, + QHash<int, QList<QQmlDelegateModelItem *> > *movedItems) +{ + int cacheIndex = 0; + + int inserted[Compositor::MaximumGroupCount]; + for (int i = 1; i < m_groupCount; ++i) + inserted[i] = 0; + + for (const Compositor::Insert &insert : inserts) { + for (; cacheIndex < insert.cacheIndex; ++cacheIndex) + incrementIndexes(m_cache.at(cacheIndex), m_groupCount, inserted); + + for (int i = 1; i < m_groupCount; ++i) { + if (insert.inGroup(i)) { + (*translatedInserts)[i].append( + QQmlChangeSet::Change(insert.index[i], insert.count, insert.moveId)); + inserted[i] += insert.count; + } + } + + if (!insert.inCache()) + continue; + + if (movedItems && insert.isMove()) { + QList<QQmlDelegateModelItem *> items = movedItems->take(insert.moveId); + Q_ASSERT(items.count() == insert.count); + m_cache = m_cache.mid(0, insert.cacheIndex) + items + m_cache.mid(insert.cacheIndex); + } + if (insert.inGroup()) { + for (int offset = 0; cacheIndex < insert.cacheIndex + insert.count; ++cacheIndex, ++offset) { + QQmlDelegateModelItem *cacheItem = m_cache.at(cacheIndex); + cacheItem->groups |= insert.flags & Compositor::GroupMask; + + if (QQDMIncubationTask *incubationTask = cacheItem->incubationTask) { + for (int i = 1; i < m_groupCount; ++i) + incubationTask->index[i] = cacheItem->groups & (1 << i) + ? insert.index[i] + offset + : insert.index[i]; + } + if (QQmlDelegateModelAttached *attached = cacheItem->attached) { + for (int i = 1; i < m_groupCount; ++i) + attached->m_currentIndex[i] = cacheItem->groups & (1 << i) + ? insert.index[i] + offset + : insert.index[i]; + } + } + } else { + cacheIndex = insert.cacheIndex + insert.count; + } + } + for (const QList<QQmlDelegateModelItem *> cache = m_cache; cacheIndex < cache.count(); ++cacheIndex) + incrementIndexes(cache.at(cacheIndex), m_groupCount, inserted); +} + +void QQmlDelegateModelPrivate::itemsInserted(const QVector<Compositor::Insert> &inserts) +{ + QVarLengthArray<QVector<QQmlChangeSet::Change>, Compositor::MaximumGroupCount> translatedInserts(m_groupCount); + itemsInserted(inserts, &translatedInserts); + Q_ASSERT(m_cache.count() == m_compositor.count(Compositor::Cache)); + if (!m_delegate) + return; + + for (int i = 1; i < m_groupCount; ++i) + QQmlDelegateModelGroupPrivate::get(m_groups[i])->changeSet.insert(translatedInserts.at(i)); +} + +void QQmlDelegateModel::_q_itemsInserted(int index, int count) +{ + + Q_D(QQmlDelegateModel); + if (count <= 0 || !d->m_complete) + return; + + d->m_count += count; + + const QList<QQmlDelegateModelItem *> cache = d->m_cache; + for (int i = 0, c = cache.count(); i < c; ++i) { + QQmlDelegateModelItem *item = cache.at(i); + if (item->modelIndex() >= index) { + const int newIndex = item->modelIndex() + count; + const int row = newIndex; + const int column = 0; + item->setModelIndex(newIndex, row, column); + } + } + + QVector<Compositor::Insert> inserts; + d->m_compositor.listItemsInserted(&d->m_adaptorModel, index, count, &inserts); + d->itemsInserted(inserts); + d->emitChanges(); +} + +//### This method should be split in two. It will remove delegates, and it will re-render the list. +// When e.g. QQmlListModel::remove is called, the removal of the delegates should be done on +// QAbstractItemModel::rowsAboutToBeRemoved, and the re-rendering on +// QAbstractItemModel::rowsRemoved. Currently both are done on the latter signal. The problem is +// that the destruction of an item will emit a changed signal that ends up at the delegate, which +// in turn will try to load the data from the model (which should have already freed it), resulting +// in a use-after-free. See QTBUG-59256. +void QQmlDelegateModelPrivate::itemsRemoved( + const QVector<Compositor::Remove> &removes, + QVarLengthArray<QVector<QQmlChangeSet::Change>, Compositor::MaximumGroupCount> *translatedRemoves, + QHash<int, QList<QQmlDelegateModelItem *> > *movedItems) +{ + int cacheIndex = 0; + int removedCache = 0; + + int removed[Compositor::MaximumGroupCount]; + for (int i = 1; i < m_groupCount; ++i) + removed[i] = 0; + + for (const Compositor::Remove &remove : removes) { + for (; cacheIndex < remove.cacheIndex; ++cacheIndex) + incrementIndexes(m_cache.at(cacheIndex), m_groupCount, removed); + + for (int i = 1; i < m_groupCount; ++i) { + if (remove.inGroup(i)) { + (*translatedRemoves)[i].append( + QQmlChangeSet::Change(remove.index[i], remove.count, remove.moveId)); + removed[i] -= remove.count; + } + } + + if (!remove.inCache()) + continue; + + if (movedItems && remove.isMove()) { + movedItems->insert(remove.moveId, m_cache.mid(remove.cacheIndex, remove.count)); + QList<QQmlDelegateModelItem *>::iterator begin = m_cache.begin() + remove.cacheIndex; + QList<QQmlDelegateModelItem *>::iterator end = begin + remove.count; + m_cache.erase(begin, end); + } else { + for (; cacheIndex < remove.cacheIndex + remove.count - removedCache; ++cacheIndex) { + QQmlDelegateModelItem *cacheItem = m_cache.at(cacheIndex); + if (remove.inGroup(Compositor::Persisted) && cacheItem->objectRef == 0 && cacheItem->object) { + QObject *object = cacheItem->object; + cacheItem->destroyObject(); + if (QQuickPackage *package = qmlobject_cast<QQuickPackage *>(object)) + emitDestroyingPackage(package); + else + emitDestroyingItem(object); + cacheItem->scriptRef -= 1; + } + if (!cacheItem->isReferenced()) { + m_compositor.clearFlags(Compositor::Cache, cacheIndex, 1, Compositor::CacheFlag); + m_cache.removeAt(cacheIndex); + delete cacheItem; + --cacheIndex; + ++removedCache; + Q_ASSERT(m_cache.count() == m_compositor.count(Compositor::Cache)); + } else if (remove.groups() == cacheItem->groups) { + cacheItem->groups = 0; + if (QQDMIncubationTask *incubationTask = cacheItem->incubationTask) { + for (int i = 1; i < m_groupCount; ++i) + incubationTask->index[i] = -1; + } + if (QQmlDelegateModelAttached *attached = cacheItem->attached) { + for (int i = 1; i < m_groupCount; ++i) + attached->m_currentIndex[i] = -1; + } + } else { + if (QQDMIncubationTask *incubationTask = cacheItem->incubationTask) { + if (!cacheItem->isObjectReferenced()) { + releaseIncubator(cacheItem->incubationTask); + cacheItem->incubationTask = nullptr; + if (cacheItem->object) { + QObject *object = cacheItem->object; + cacheItem->destroyObject(); + if (QQuickPackage *package = qmlobject_cast<QQuickPackage *>(object)) + emitDestroyingPackage(package); + else + emitDestroyingItem(object); + } + cacheItem->scriptRef -= 1; + } else { + for (int i = 1; i < m_groupCount; ++i) { + if (remove.inGroup(i)) + incubationTask->index[i] = remove.index[i]; + } + } + } + if (QQmlDelegateModelAttached *attached = cacheItem->attached) { + for (int i = 1; i < m_groupCount; ++i) { + if (remove.inGroup(i)) + attached->m_currentIndex[i] = remove.index[i]; + } + } + cacheItem->groups &= ~remove.flags; + } + } + } + } + + for (const QList<QQmlDelegateModelItem *> cache = m_cache; cacheIndex < cache.count(); ++cacheIndex) + incrementIndexes(cache.at(cacheIndex), m_groupCount, removed); +} + +void QQmlDelegateModelPrivate::itemsRemoved(const QVector<Compositor::Remove> &removes) +{ + QVarLengthArray<QVector<QQmlChangeSet::Change>, Compositor::MaximumGroupCount> translatedRemoves(m_groupCount); + itemsRemoved(removes, &translatedRemoves); + Q_ASSERT(m_cache.count() == m_compositor.count(Compositor::Cache)); + if (!m_delegate) + return; + + for (int i = 1; i < m_groupCount; ++i) + QQmlDelegateModelGroupPrivate::get(m_groups[i])->changeSet.remove(translatedRemoves.at(i)); +} + +void QQmlDelegateModel::_q_itemsRemoved(int index, int count) +{ + Q_D(QQmlDelegateModel); + if (count <= 0|| !d->m_complete) + return; + + d->m_count -= count; + const QList<QQmlDelegateModelItem *> cache = d->m_cache; + for (int i = 0, c = cache.count(); i < c; ++i) { + QQmlDelegateModelItem *item = cache.at(i); + // layout change triggered by removal of a previous item might have + // already invalidated this item in d->m_cache and deleted it + if (!d->m_cache.contains(item)) + continue; + + if (item->modelIndex() >= index + count) { + const int newIndex = item->modelIndex() - count; + const int row = newIndex; + const int column = 0; + item->setModelIndex(newIndex, row, column); + } else if (item->modelIndex() >= index) { + item->setModelIndex(-1, -1, -1); + } + } + + QVector<Compositor::Remove> removes; + d->m_compositor.listItemsRemoved(&d->m_adaptorModel, index, count, &removes); + d->itemsRemoved(removes); + + d->emitChanges(); +} + +void QQmlDelegateModelPrivate::itemsMoved( + const QVector<Compositor::Remove> &removes, const QVector<Compositor::Insert> &inserts) +{ + QHash<int, QList<QQmlDelegateModelItem *> > movedItems; + + QVarLengthArray<QVector<QQmlChangeSet::Change>, Compositor::MaximumGroupCount> translatedRemoves(m_groupCount); + itemsRemoved(removes, &translatedRemoves, &movedItems); + + QVarLengthArray<QVector<QQmlChangeSet::Change>, Compositor::MaximumGroupCount> translatedInserts(m_groupCount); + itemsInserted(inserts, &translatedInserts, &movedItems); + Q_ASSERT(m_cache.count() == m_compositor.count(Compositor::Cache)); + Q_ASSERT(movedItems.isEmpty()); + if (!m_delegate) + return; + + for (int i = 1; i < m_groupCount; ++i) { + QQmlDelegateModelGroupPrivate::get(m_groups[i])->changeSet.move( + translatedRemoves.at(i), + translatedInserts.at(i)); + } +} + +void QQmlDelegateModel::_q_itemsMoved(int from, int to, int count) +{ + Q_D(QQmlDelegateModel); + if (count <= 0 || !d->m_complete) + return; + + const int minimum = qMin(from, to); + const int maximum = qMax(from, to) + count; + const int difference = from > to ? count : -count; + + const QList<QQmlDelegateModelItem *> cache = d->m_cache; + for (int i = 0, c = cache.count(); i < c; ++i) { + QQmlDelegateModelItem *item = cache.at(i); + if (item->modelIndex() >= from && item->modelIndex() < from + count) { + const int newIndex = item->modelIndex() - from + to; + const int row = newIndex; + const int column = 0; + item->setModelIndex(newIndex, row, column); + } else if (item->modelIndex() >= minimum && item->modelIndex() < maximum) { + const int newIndex = item->modelIndex() + difference; + const int row = newIndex; + const int column = 0; + item->setModelIndex(newIndex, row, column); + } + } + + QVector<Compositor::Remove> removes; + QVector<Compositor::Insert> inserts; + d->m_compositor.listItemsMoved(&d->m_adaptorModel, from, to, count, &removes, &inserts); + d->itemsMoved(removes, inserts); + d->emitChanges(); +} + +void QQmlDelegateModelPrivate::emitModelUpdated(const QQmlChangeSet &changeSet, bool reset) +{ + Q_Q(QQmlDelegateModel); + emit q->modelUpdated(changeSet, reset); + if (changeSet.difference() != 0) + emit q->countChanged(); +} + +void QQmlDelegateModelPrivate::delegateChanged(bool add, bool remove) +{ + Q_Q(QQmlDelegateModel); + if (!m_complete) + return; + + if (m_transaction) { + qmlWarning(q) << QQmlDelegateModel::tr("The delegates of a DelegateModel cannot be changed within onUpdated."); + return; + } + + if (remove) { + for (int i = 1; i < m_groupCount; ++i) { + QQmlDelegateModelGroupPrivate::get(m_groups[i])->changeSet.remove( + 0, m_compositor.count(Compositor::Group(i))); + } + } + if (add) { + for (int i = 1; i < m_groupCount; ++i) { + QQmlDelegateModelGroupPrivate::get(m_groups[i])->changeSet.insert( + 0, m_compositor.count(Compositor::Group(i))); + } + } + emitChanges(); +} + +void QQmlDelegateModelPrivate::emitChanges() +{ + if (m_transaction || !m_complete || !m_context || !m_context->isValid()) + return; + + m_transaction = true; + QV4::ExecutionEngine *engine = m_context->engine()->handle(); + for (int i = 1; i < m_groupCount; ++i) + QQmlDelegateModelGroupPrivate::get(m_groups[i])->emitChanges(engine); + m_transaction = false; + + const bool reset = m_reset; + m_reset = false; + for (int i = 1; i < m_groupCount; ++i) + QQmlDelegateModelGroupPrivate::get(m_groups[i])->emitModelUpdated(reset); + + auto cacheCopy = m_cache; // deliberate; emitChanges may alter m_cache + for (QQmlDelegateModelItem *cacheItem : qAsConst(cacheCopy)) { + if (cacheItem->attached) + cacheItem->attached->emitChanges(); + } +} + +void QQmlDelegateModel::_q_modelReset() +{ + Q_D(QQmlDelegateModel); + if (!d->m_delegate) + return; + + int oldCount = d->m_count; + d->m_adaptorModel.rootIndex = QModelIndex(); + + if (d->m_complete) { + d->m_count = d->adaptorModelCount(); + + const QList<QQmlDelegateModelItem *> cache = d->m_cache; + for (int i = 0, c = cache.count(); i < c; ++i) { + QQmlDelegateModelItem *item = cache.at(i); + if (item->modelIndex() != -1) + item->setModelIndex(-1, -1, -1); + } + + QVector<Compositor::Remove> removes; + QVector<Compositor::Insert> inserts; + if (oldCount) + d->m_compositor.listItemsRemoved(&d->m_adaptorModel, 0, oldCount, &removes); + if (d->m_count) + d->m_compositor.listItemsInserted(&d->m_adaptorModel, 0, d->m_count, &inserts); + d->itemsMoved(removes, inserts); + d->m_reset = true; + + if (d->m_adaptorModel.canFetchMore()) + d->m_adaptorModel.fetchMore(); + + d->emitChanges(); + } + emit rootIndexChanged(); +} + +void QQmlDelegateModel::_q_rowsInserted(const QModelIndex &parent, int begin, int end) +{ + Q_D(QQmlDelegateModel); + if (parent == d->m_adaptorModel.rootIndex) + _q_itemsInserted(begin, end - begin + 1); +} + +void QQmlDelegateModel::_q_rowsAboutToBeRemoved(const QModelIndex &parent, int begin, int end) +{ + Q_D(QQmlDelegateModel); + if (!d->m_adaptorModel.rootIndex.isValid()) + return; + const QModelIndex index = d->m_adaptorModel.rootIndex; + if (index.parent() == parent && index.row() >= begin && index.row() <= end) { + const int oldCount = d->m_count; + d->m_count = 0; + d->disconnectFromAbstractItemModel(); + d->m_adaptorModel.invalidateModel(); + + if (d->m_complete && oldCount > 0) { + QVector<Compositor::Remove> removes; + d->m_compositor.listItemsRemoved(&d->m_adaptorModel, 0, oldCount, &removes); + d->itemsRemoved(removes); + d->emitChanges(); + } + } +} + +void QQmlDelegateModel::_q_rowsRemoved(const QModelIndex &parent, int begin, int end) +{ + Q_D(QQmlDelegateModel); + if (parent == d->m_adaptorModel.rootIndex) + _q_itemsRemoved(begin, end - begin + 1); +} + +void QQmlDelegateModel::_q_rowsMoved( + const QModelIndex &sourceParent, int sourceStart, int sourceEnd, + const QModelIndex &destinationParent, int destinationRow) +{ + Q_D(QQmlDelegateModel); + const int count = sourceEnd - sourceStart + 1; + if (destinationParent == d->m_adaptorModel.rootIndex && sourceParent == d->m_adaptorModel.rootIndex) { + _q_itemsMoved(sourceStart, sourceStart > destinationRow ? destinationRow : destinationRow - count, count); + } else if (sourceParent == d->m_adaptorModel.rootIndex) { + _q_itemsRemoved(sourceStart, count); + } else if (destinationParent == d->m_adaptorModel.rootIndex) { + _q_itemsInserted(destinationRow, count); + } +} + +void QQmlDelegateModel::_q_dataChanged(const QModelIndex &begin, const QModelIndex &end, const QVector<int> &roles) +{ + Q_D(QQmlDelegateModel); + if (begin.parent() == d->m_adaptorModel.rootIndex) + _q_itemsChanged(begin.row(), end.row() - begin.row() + 1, roles); +} + +bool QQmlDelegateModel::isDescendantOf(const QPersistentModelIndex& desc, const QList< QPersistentModelIndex >& parents) const +{ + for (int i = 0, c = parents.count(); i < c; ++i) { + for (QPersistentModelIndex parent = desc; parent.isValid(); parent = parent.parent()) { + if (parent == parents[i]) + return true; + } + } + + return false; +} + +void QQmlDelegateModel::_q_layoutChanged(const QList<QPersistentModelIndex> &parents, QAbstractItemModel::LayoutChangeHint hint) +{ + Q_D(QQmlDelegateModel); + if (!d->m_complete) + return; + + if (hint == QAbstractItemModel::VerticalSortHint) { + if (!parents.isEmpty() && d->m_adaptorModel.rootIndex.isValid() && !isDescendantOf(d->m_adaptorModel.rootIndex, parents)) { + return; + } + + // mark all items as changed + _q_itemsChanged(0, d->m_count, QVector<int>()); + + } else if (hint == QAbstractItemModel::HorizontalSortHint) { + // Ignored + } else { + // We don't know what's going on, so reset the model + _q_modelReset(); + } +} + +QQmlDelegateModelAttached *QQmlDelegateModel::qmlAttachedProperties(QObject *obj) +{ + if (QQmlDelegateModelItem *cacheItem = QQmlDelegateModelItem::dataForObject(obj)) { + if (cacheItem->object == obj) { // Don't create attached item for child objects. + cacheItem->attached = new QQmlDelegateModelAttached(cacheItem, obj); + return cacheItem->attached; + } + } + return new QQmlDelegateModelAttached(obj); +} + +bool QQmlDelegateModelPrivate::insert(Compositor::insert_iterator &before, const QV4::Value &object, int groups) +{ + if (!m_context || !m_context->isValid()) + return false; + + QQmlDelegateModelItem *cacheItem = m_adaptorModel.createItem(m_cacheMetaType, -1); + if (!cacheItem) + return false; + if (!object.isObject()) + return false; + + QV4::ExecutionEngine *v4 = object.as<QV4::Object>()->engine(); + QV4::Scope scope(v4); + QV4::ScopedObject o(scope, object); + if (!o) + return false; + + QV4::ObjectIterator it(scope, o, QV4::ObjectIterator::EnumerableOnly); + QV4::ScopedValue propertyName(scope); + QV4::ScopedValue v(scope); + while (1) { + propertyName = it.nextPropertyNameAsString(v); + if (propertyName->isNull()) + break; + cacheItem->setValue(propertyName->toQStringNoThrow(), scope.engine->toVariant(v, QVariant::Invalid)); + } + + cacheItem->groups = groups | Compositor::UnresolvedFlag | Compositor::CacheFlag; + + // Must be before the new object is inserted into the cache or its indexes will be adjusted too. + itemsInserted(QVector<Compositor::Insert>(1, Compositor::Insert(before, 1, cacheItem->groups & ~Compositor::CacheFlag))); + + before = m_compositor.insert(before, nullptr, 0, 1, cacheItem->groups); + m_cache.insert(before.cacheIndex, cacheItem); + + return true; +} + +//============================================================================ + +QQmlDelegateModelItemMetaType::QQmlDelegateModelItemMetaType( + QV4::ExecutionEngine *engine, QQmlDelegateModel *model, const QStringList &groupNames) + : model(model) + , groupCount(groupNames.count() + 1) + , v4Engine(engine) + , metaObject(nullptr) + , groupNames(groupNames) +{ +} + +QQmlDelegateModelItemMetaType::~QQmlDelegateModelItemMetaType() +{ + if (metaObject) + metaObject->release(); +} + +void QQmlDelegateModelItemMetaType::initializeMetaObject() +{ + QMetaObjectBuilder builder; + builder.setFlags(QMetaObjectBuilder::DynamicMetaObject); + builder.setClassName(QQmlDelegateModelAttached::staticMetaObject.className()); + builder.setSuperClass(&QQmlDelegateModelAttached::staticMetaObject); + + int notifierId = 0; + for (int i = 0; i < groupNames.count(); ++i, ++notifierId) { + QString propertyName = QLatin1String("in") + groupNames.at(i); + propertyName.replace(2, 1, propertyName.at(2).toUpper()); + builder.addSignal("__" + propertyName.toUtf8() + "Changed()"); + QMetaPropertyBuilder propertyBuilder = builder.addProperty( + propertyName.toUtf8(), "bool", notifierId); + propertyBuilder.setWritable(true); + } + for (int i = 0; i < groupNames.count(); ++i, ++notifierId) { + const QString propertyName = groupNames.at(i) + QLatin1String("Index"); + builder.addSignal("__" + propertyName.toUtf8() + "Changed()"); + QMetaPropertyBuilder propertyBuilder = builder.addProperty( + propertyName.toUtf8(), "int", notifierId); + propertyBuilder.setWritable(true); + } + + metaObject = new QQmlDelegateModelAttachedMetaObject(this, builder.toMetaObject()); +} + +void QQmlDelegateModelItemMetaType::initializePrototype() +{ + QV4::Scope scope(v4Engine); + + QV4::ScopedObject proto(scope, v4Engine->newObject()); + proto->defineAccessorProperty(QStringLiteral("model"), QQmlDelegateModelItem::get_model, nullptr); + proto->defineAccessorProperty(QStringLiteral("groups"), QQmlDelegateModelItem::get_groups, QQmlDelegateModelItem::set_groups); + QV4::ScopedString s(scope); + QV4::ScopedProperty p(scope); + + s = v4Engine->newString(QStringLiteral("isUnresolved")); + QV4::ScopedFunctionObject f(scope); + QV4::ExecutionContext *global = scope.engine->rootContext(); + p->setGetter((f = QV4::DelegateModelGroupFunction::create(global, 30, QQmlDelegateModelItem::get_member))); + p->setSetter(nullptr); + proto->insertMember(s, p, QV4::Attr_Accessor|QV4::Attr_NotConfigurable|QV4::Attr_NotEnumerable); + + s = v4Engine->newString(QStringLiteral("inItems")); + p->setGetter((f = QV4::DelegateModelGroupFunction::create(global, QQmlListCompositor::Default, QQmlDelegateModelItem::get_member))); + p->setSetter((f = QV4::DelegateModelGroupFunction::create(global, QQmlListCompositor::Default, QQmlDelegateModelItem::set_member))); + proto->insertMember(s, p, QV4::Attr_Accessor|QV4::Attr_NotConfigurable|QV4::Attr_NotEnumerable); + + s = v4Engine->newString(QStringLiteral("inPersistedItems")); + p->setGetter((f = QV4::DelegateModelGroupFunction::create(global, QQmlListCompositor::Persisted, QQmlDelegateModelItem::get_member))); + p->setSetter((f = QV4::DelegateModelGroupFunction::create(global, QQmlListCompositor::Persisted, QQmlDelegateModelItem::set_member))); + proto->insertMember(s, p, QV4::Attr_Accessor|QV4::Attr_NotConfigurable|QV4::Attr_NotEnumerable); + + s = v4Engine->newString(QStringLiteral("itemsIndex")); + p->setGetter((f = QV4::DelegateModelGroupFunction::create(global, QQmlListCompositor::Default, QQmlDelegateModelItem::get_index))); + proto->insertMember(s, p, QV4::Attr_Accessor|QV4::Attr_NotConfigurable|QV4::Attr_NotEnumerable); + + s = v4Engine->newString(QStringLiteral("persistedItemsIndex")); + p->setGetter((f = QV4::DelegateModelGroupFunction::create(global, QQmlListCompositor::Persisted, QQmlDelegateModelItem::get_index))); + p->setSetter(nullptr); + proto->insertMember(s, p, QV4::Attr_Accessor|QV4::Attr_NotConfigurable|QV4::Attr_NotEnumerable); + + for (int i = 2; i < groupNames.count(); ++i) { + QString propertyName = QLatin1String("in") + groupNames.at(i); + propertyName.replace(2, 1, propertyName.at(2).toUpper()); + s = v4Engine->newString(propertyName); + p->setGetter((f = QV4::DelegateModelGroupFunction::create(global, i + 1, QQmlDelegateModelItem::get_member))); + p->setSetter((f = QV4::DelegateModelGroupFunction::create(global, i + 1, QQmlDelegateModelItem::set_member))); + proto->insertMember(s, p, QV4::Attr_Accessor|QV4::Attr_NotConfigurable|QV4::Attr_NotEnumerable); + } + for (int i = 2; i < groupNames.count(); ++i) { + const QString propertyName = groupNames.at(i) + QLatin1String("Index"); + s = v4Engine->newString(propertyName); + p->setGetter((f = QV4::DelegateModelGroupFunction::create(global, i + 1, QQmlDelegateModelItem::get_index))); + p->setSetter(nullptr); + proto->insertMember(s, p, QV4::Attr_Accessor|QV4::Attr_NotConfigurable|QV4::Attr_NotEnumerable); + } + modelItemProto.set(v4Engine, proto); +} + +int QQmlDelegateModelItemMetaType::parseGroups(const QStringList &groups) const +{ + int groupFlags = 0; + for (const QString &groupName : groups) { + int index = groupNames.indexOf(groupName); + if (index != -1) + groupFlags |= 2 << index; + } + return groupFlags; +} + +int QQmlDelegateModelItemMetaType::parseGroups(const QV4::Value &groups) const +{ + int groupFlags = 0; + QV4::Scope scope(v4Engine); + + QV4::ScopedString s(scope, groups); + if (s) { + const QString groupName = s->toQString(); + int index = groupNames.indexOf(groupName); + if (index != -1) + groupFlags |= 2 << index; + return groupFlags; + } + + QV4::ScopedArrayObject array(scope, groups); + if (array) { + QV4::ScopedValue v(scope); + uint arrayLength = array->getLength(); + for (uint i = 0; i < arrayLength; ++i) { + v = array->get(i); + const QString groupName = v->toQString(); + int index = groupNames.indexOf(groupName); + if (index != -1) + groupFlags |= 2 << index; + } + } + return groupFlags; +} + +QV4::ReturnedValue QQmlDelegateModelItem::get_model(const QV4::FunctionObject *b, const QV4::Value *thisObject, const QV4::Value *, int) +{ + QV4::Scope scope(b); + QV4::Scoped<QQmlDelegateModelItemObject> o(scope, thisObject->as<QQmlDelegateModelItemObject>()); + if (!o) + return b->engine()->throwTypeError(QStringLiteral("Not a valid DelegateModel object")); + if (!o->d()->item->metaType->model) + RETURN_UNDEFINED(); + + return o->d()->item->get(); +} + +QV4::ReturnedValue QQmlDelegateModelItem::get_groups(const QV4::FunctionObject *b, const QV4::Value *thisObject, const QV4::Value *, int) +{ + QV4::Scope scope(b); + QV4::Scoped<QQmlDelegateModelItemObject> o(scope, thisObject->as<QQmlDelegateModelItemObject>()); + if (!o) + return scope.engine->throwTypeError(QStringLiteral("Not a valid DelegateModel object")); + + QStringList groups; + for (int i = 1; i < o->d()->item->metaType->groupCount; ++i) { + if (o->d()->item->groups & (1 << i)) + groups.append(o->d()->item->metaType->groupNames.at(i - 1)); + } + + return scope.engine->fromVariant(groups); +} + +QV4::ReturnedValue QQmlDelegateModelItem::set_groups(const QV4::FunctionObject *b, const QV4::Value *thisObject, const QV4::Value *argv, int argc) +{ + QV4::Scope scope(b); + QV4::Scoped<QQmlDelegateModelItemObject> o(scope, thisObject->as<QQmlDelegateModelItemObject>()); + if (!o) + return scope.engine->throwTypeError(QStringLiteral("Not a valid DelegateModel object")); + + if (!argc) + THROW_TYPE_ERROR(); + + if (!o->d()->item->metaType->model) + RETURN_UNDEFINED(); + QQmlDelegateModelPrivate *model = QQmlDelegateModelPrivate::get(o->d()->item->metaType->model); + + const int groupFlags = model->m_cacheMetaType->parseGroups(argv[0]); + const int cacheIndex = model->m_cache.indexOf(o->d()->item); + Compositor::iterator it = model->m_compositor.find(Compositor::Cache, cacheIndex); + model->setGroups(it, 1, Compositor::Cache, groupFlags); + return QV4::Encode::undefined(); +} + +QV4::ReturnedValue QQmlDelegateModelItem::get_member(QQmlDelegateModelItem *thisItem, uint flag, const QV4::Value &) +{ + return QV4::Encode(bool(thisItem->groups & (1 << flag))); +} + +QV4::ReturnedValue QQmlDelegateModelItem::set_member(QQmlDelegateModelItem *cacheItem, uint flag, const QV4::Value &arg) +{ + if (!cacheItem->metaType->model) + return QV4::Encode::undefined(); + + QQmlDelegateModelPrivate *model = QQmlDelegateModelPrivate::get(cacheItem->metaType->model); + + bool member = arg.toBoolean(); + uint groupFlag = (1 << flag); + if (member == ((cacheItem->groups & groupFlag) != 0)) + return QV4::Encode::undefined(); + + const int cacheIndex = model->m_cache.indexOf(cacheItem); + Compositor::iterator it = model->m_compositor.find(Compositor::Cache, cacheIndex); + if (member) + model->addGroups(it, 1, Compositor::Cache, groupFlag); + else + model->removeGroups(it, 1, Compositor::Cache, groupFlag); + return QV4::Encode::undefined(); +} + +QV4::ReturnedValue QQmlDelegateModelItem::get_index(QQmlDelegateModelItem *thisItem, uint flag, const QV4::Value &) +{ + return QV4::Encode((int)thisItem->groupIndex(Compositor::Group(flag))); +} + +void QQmlDelegateModelItem::childContextObjectDestroyed(QObject *childContextObject) +{ + if (!contextData) + return; + + for (QQmlContextData *ctxt = contextData->childContexts; ctxt; ctxt = ctxt->nextChild) { + if (ctxt->contextObject == childContextObject) + ctxt->contextObject = nullptr; + } +} + + +//--------------------------------------------------------------------------- + +DEFINE_OBJECT_VTABLE(QQmlDelegateModelItemObject); + +void QV4::Heap::QQmlDelegateModelItemObject::destroy() +{ + item->Dispose(); + Object::destroy(); +} + + +QQmlDelegateModelItem::QQmlDelegateModelItem(QQmlDelegateModelItemMetaType *metaType, + QQmlAdaptorModel::Accessors *accessor, + int modelIndex, int row, int column) + : v4(metaType->v4Engine) + , metaType(metaType) + , contextData(nullptr) + , object(nullptr) + , attached(nullptr) + , incubationTask(nullptr) + , delegate(nullptr) + , poolTime(0) + , objectRef(0) + , scriptRef(0) + , groups(0) + , index(modelIndex) + , row(row) + , column(column) +{ + metaType->addref(); + + if (accessor->propertyCache) { + // The property cache in the accessor is common for all the model + // items in the model it wraps. It describes available model roles, + // together with revisioned properties like row, column and index, all + // which should be available in the delegate. We assign this cache to the + // model item so that the QML engine can use the revision information + // when resolving the properties (rather than falling back to just + // inspecting the QObject in the model item directly). + QQmlData *qmldata = QQmlData::get(this, true); + if (qmldata->propertyCache) + qmldata->propertyCache->release(); + qmldata->propertyCache = accessor->propertyCache.data(); + qmldata->propertyCache->addref(); + } +} + +QQmlDelegateModelItem::~QQmlDelegateModelItem() +{ + Q_ASSERT(scriptRef == 0); + Q_ASSERT(objectRef == 0); + Q_ASSERT(!object); + + if (incubationTask) { + if (metaType->model) + QQmlDelegateModelPrivate::get(metaType->model)->releaseIncubator(incubationTask); + else + delete incubationTask; + } + + metaType->release(); + +} + +void QQmlDelegateModelItem::Dispose() +{ + --scriptRef; + if (isReferenced()) + return; + + if (metaType->model) { + QQmlDelegateModelPrivate *model = QQmlDelegateModelPrivate::get(metaType->model); + model->removeCacheItem(this); + } + delete this; +} + +void QQmlDelegateModelItem::setModelIndex(int idx, int newRow, int newColumn) +{ + const int prevIndex = index; + const int prevRow = row; + const int prevColumn = column; + + index = idx; + row = newRow; + column = newColumn; + + if (idx != prevIndex) + emit modelIndexChanged(); + if (row != prevRow) + emit rowChanged(); + if (column != prevColumn) + emit columnChanged(); +} + +void QQmlDelegateModelItem::destroyObject() +{ + Q_ASSERT(object); + Q_ASSERT(contextData); + + QQmlData *data = QQmlData::get(object); + Q_ASSERT(data); + if (data->ownContext) { + data->ownContext->clearContext(); + if (data->ownContext->contextObject == object) + data->ownContext->contextObject = nullptr; + data->ownContext = nullptr; + data->context = nullptr; + } + object->deleteLater(); + + if (attached) { + attached->m_cacheItem = nullptr; + attached = nullptr; + } + + contextData->invalidate(); + contextData = nullptr; + object = nullptr; +} + +QQmlDelegateModelItem *QQmlDelegateModelItem::dataForObject(QObject *object) +{ + QQmlData *d = QQmlData::get(object); + QQmlContextData *context = d ? d->context : nullptr; + for (context = context ? context->parent : nullptr; context; context = context->parent) { + if (QQmlDelegateModelItem *cacheItem = qobject_cast<QQmlDelegateModelItem *>( + context->contextObject)) { + return cacheItem; + } + } + return nullptr; +} + +int QQmlDelegateModelItem::groupIndex(Compositor::Group group) +{ + if (QQmlDelegateModelPrivate * const model = metaType->model + ? QQmlDelegateModelPrivate::get(metaType->model) + : nullptr) { + return model->m_compositor.find(Compositor::Cache, model->m_cache.indexOf(this)).index[group]; + } + return -1; +} + +//--------------------------------------------------------------------------- + +QQmlDelegateModelAttachedMetaObject::QQmlDelegateModelAttachedMetaObject( + QQmlDelegateModelItemMetaType *metaType, QMetaObject *metaObject) + : metaType(metaType) + , metaObject(metaObject) + , memberPropertyOffset(QQmlDelegateModelAttached::staticMetaObject.propertyCount()) + , indexPropertyOffset(QQmlDelegateModelAttached::staticMetaObject.propertyCount() + metaType->groupNames.count()) +{ + // Don't reference count the meta-type here as that would create a circular reference. + // Instead we rely the fact that the meta-type's reference count can't reach 0 without first + // destroying all delegates with attached objects. + *static_cast<QMetaObject *>(this) = *metaObject; +} + +QQmlDelegateModelAttachedMetaObject::~QQmlDelegateModelAttachedMetaObject() +{ + ::free(metaObject); +} + +void QQmlDelegateModelAttachedMetaObject::objectDestroyed(QObject *) +{ + release(); +} + +int QQmlDelegateModelAttachedMetaObject::metaCall(QObject *object, QMetaObject::Call call, int _id, void **arguments) +{ + QQmlDelegateModelAttached *attached = static_cast<QQmlDelegateModelAttached *>(object); + if (call == QMetaObject::ReadProperty) { + if (_id >= indexPropertyOffset) { + Compositor::Group group = Compositor::Group(_id - indexPropertyOffset + 1); + *static_cast<int *>(arguments[0]) = attached->m_currentIndex[group]; + return -1; + } else if (_id >= memberPropertyOffset) { + Compositor::Group group = Compositor::Group(_id - memberPropertyOffset + 1); + *static_cast<bool *>(arguments[0]) = attached->m_cacheItem->groups & (1 << group); + return -1; + } + } else if (call == QMetaObject::WriteProperty) { + if (_id >= memberPropertyOffset) { + if (!metaType->model) + return -1; + QQmlDelegateModelPrivate *model = QQmlDelegateModelPrivate::get(metaType->model); + Compositor::Group group = Compositor::Group(_id - memberPropertyOffset + 1); + const int groupFlag = 1 << group; + const bool member = attached->m_cacheItem->groups & groupFlag; + if (member && !*static_cast<bool *>(arguments[0])) { + Compositor::iterator it = model->m_compositor.find( + group, attached->m_currentIndex[group]); + model->removeGroups(it, 1, group, groupFlag); + } else if (!member && *static_cast<bool *>(arguments[0])) { + for (int i = 1; i < metaType->groupCount; ++i) { + if (attached->m_cacheItem->groups & (1 << i)) { + Compositor::iterator it = model->m_compositor.find( + Compositor::Group(i), attached->m_currentIndex[i]); + model->addGroups(it, 1, Compositor::Group(i), groupFlag); + break; + } + } + } + return -1; + } + } + return attached->qt_metacall(call, _id, arguments); +} + +QQmlDelegateModelAttached::QQmlDelegateModelAttached(QObject *parent) + : m_cacheItem(nullptr) + , m_previousGroups(0) +{ + QQml_setParent_noEvent(this, parent); +} + +QQmlDelegateModelAttached::QQmlDelegateModelAttached( + QQmlDelegateModelItem *cacheItem, QObject *parent) + : m_cacheItem(cacheItem) + , m_previousGroups(cacheItem->groups) +{ + QQml_setParent_noEvent(this, parent); + resetCurrentIndex(); + // Let m_previousIndex be equal to m_currentIndex + std::copy(std::begin(m_currentIndex), std::end(m_currentIndex), std::begin(m_previousIndex)); + + if (!cacheItem->metaType->metaObject) + cacheItem->metaType->initializeMetaObject(); + + QObjectPrivate::get(this)->metaObject = cacheItem->metaType->metaObject; + cacheItem->metaType->metaObject->addref(); +} + +void QQmlDelegateModelAttached::resetCurrentIndex() +{ + if (QQDMIncubationTask *incubationTask = m_cacheItem->incubationTask) { + for (int i = 1; i < qMin<int>(m_cacheItem->metaType->groupCount, Compositor::MaximumGroupCount); ++i) + m_currentIndex[i] = incubationTask->index[i]; + } else { + QQmlDelegateModelPrivate * const model = QQmlDelegateModelPrivate::get(m_cacheItem->metaType->model); + Compositor::iterator it = model->m_compositor.find( + Compositor::Cache, model->m_cache.indexOf(m_cacheItem)); + for (int i = 1; i < m_cacheItem->metaType->groupCount; ++i) + m_currentIndex[i] = it.index[i]; + } +} + +/*! + \qmlattachedproperty model QtQml.Models::DelegateModel::model + + This attached property holds the data model this delegate instance belongs to. + + It is attached to each instance of the delegate. +*/ + +QQmlDelegateModel *QQmlDelegateModelAttached::model() const +{ + return m_cacheItem ? m_cacheItem->metaType->model : nullptr; +} + +/*! + \qmlattachedproperty stringlist QtQml.Models::DelegateModel::groups + + This attached property holds the name of DelegateModelGroups the item belongs to. + + It is attached to each instance of the delegate. +*/ + +QStringList QQmlDelegateModelAttached::groups() const +{ + QStringList groups; + + if (!m_cacheItem) + return groups; + for (int i = 1; i < m_cacheItem->metaType->groupCount; ++i) { + if (m_cacheItem->groups & (1 << i)) + groups.append(m_cacheItem->metaType->groupNames.at(i - 1)); + } + return groups; +} + +void QQmlDelegateModelAttached::setGroups(const QStringList &groups) +{ + if (!m_cacheItem) + return; + + QQmlDelegateModelPrivate *model = QQmlDelegateModelPrivate::get(m_cacheItem->metaType->model); + + const int groupFlags = model->m_cacheMetaType->parseGroups(groups); + const int cacheIndex = model->m_cache.indexOf(m_cacheItem); + Compositor::iterator it = model->m_compositor.find(Compositor::Cache, cacheIndex); + model->setGroups(it, 1, Compositor::Cache, groupFlags); +} + +/*! + \qmlattachedproperty bool QtQml.Models::DelegateModel::isUnresolved + + This attached property indicates whether the visual item is bound to a data model index. + Returns true if the item is not bound to the model, and false if it is. + + An unresolved item can be bound to the data model using the DelegateModelGroup::resolve() + function. + + It is attached to each instance of the delegate. +*/ + +bool QQmlDelegateModelAttached::isUnresolved() const +{ + if (!m_cacheItem) + return false; + + return m_cacheItem->groups & Compositor::UnresolvedFlag; +} + +/*! + \qmlattachedproperty int QtQml.Models::DelegateModel::inItems + + This attached property holds whether the item belongs to the default \l items DelegateModelGroup. + + Changing this property will add or remove the item from the items group. + + It is attached to each instance of the delegate. +*/ + +/*! + \qmlattachedproperty int QtQml.Models::DelegateModel::itemsIndex + + This attached property holds the index of the item in the default \l items DelegateModelGroup. + + It is attached to each instance of the delegate. +*/ + +/*! + \qmlattachedproperty int QtQml.Models::DelegateModel::inPersistedItems + + This attached property holds whether the item belongs to the \l persistedItems DelegateModelGroup. + + Changing this property will add or remove the item from the items group. Change with caution + as removing an item from the persistedItems group will destroy the current instance if it is + not referenced by a model. + + It is attached to each instance of the delegate. +*/ + +/*! + \qmlattachedproperty int QtQml.Models::DelegateModel::persistedItemsIndex + + This attached property holds the index of the item in the \l persistedItems DelegateModelGroup. + + It is attached to each instance of the delegate. +*/ + +void QQmlDelegateModelAttached::emitChanges() +{ + const int groupChanges = m_previousGroups ^ m_cacheItem->groups; + m_previousGroups = m_cacheItem->groups; + + int indexChanges = 0; + for (int i = 1; i < m_cacheItem->metaType->groupCount; ++i) { + if (m_previousIndex[i] != m_currentIndex[i]) { + m_previousIndex[i] = m_currentIndex[i]; + indexChanges |= (1 << i); + } + } + + int notifierId = 0; + const QMetaObject *meta = metaObject(); + for (int i = 1; i < m_cacheItem->metaType->groupCount; ++i, ++notifierId) { + if (groupChanges & (1 << i)) + QMetaObject::activate(this, meta, notifierId, nullptr); + } + for (int i = 1; i < m_cacheItem->metaType->groupCount; ++i, ++notifierId) { + if (indexChanges & (1 << i)) + QMetaObject::activate(this, meta, notifierId, nullptr); + } + + if (groupChanges) + emit groupsChanged(); +} + +//============================================================================ + +void QQmlDelegateModelGroupPrivate::setModel(QQmlDelegateModel *m, Compositor::Group g) +{ + Q_ASSERT(!model); + model = m; + group = g; +} + +bool QQmlDelegateModelGroupPrivate::isChangedConnected() +{ + Q_Q(QQmlDelegateModelGroup); + IS_SIGNAL_CONNECTED(q, QQmlDelegateModelGroup, changed, (const QJSValue &,const QJSValue &)); +} + +void QQmlDelegateModelGroupPrivate::emitChanges(QV4::ExecutionEngine *v4) +{ + Q_Q(QQmlDelegateModelGroup); + if (isChangedConnected() && !changeSet.isEmpty()) { + emit q->changed(QJSValue(v4, engineData(v4)->array(v4, changeSet.removes())), + QJSValue(v4, engineData(v4)->array(v4, changeSet.inserts()))); + } + if (changeSet.difference() != 0) + emit q->countChanged(); +} + +void QQmlDelegateModelGroupPrivate::emitModelUpdated(bool reset) +{ + for (QQmlDelegateModelGroupEmitterList::iterator it = emitters.begin(); it != emitters.end(); ++it) + it->emitModelUpdated(changeSet, reset); + changeSet.clear(); +} + +typedef QQmlDelegateModelGroupEmitterList::iterator GroupEmitterListIt; + +void QQmlDelegateModelGroupPrivate::createdPackage(int index, QQuickPackage *package) +{ + for (GroupEmitterListIt it = emitters.begin(), end = emitters.end(); it != end; ++it) + it->createdPackage(index, package); +} + +void QQmlDelegateModelGroupPrivate::initPackage(int index, QQuickPackage *package) +{ + for (GroupEmitterListIt it = emitters.begin(), end = emitters.end(); it != end; ++it) + it->initPackage(index, package); +} + +void QQmlDelegateModelGroupPrivate::destroyingPackage(QQuickPackage *package) +{ + for (GroupEmitterListIt it = emitters.begin(), end = emitters.end(); it != end; ++it) + it->destroyingPackage(package); +} + +/*! + \qmltype DelegateModelGroup + \instantiates QQmlDelegateModelGroup + \inqmlmodule QtQml.Models + \ingroup qtquick-models + \brief Encapsulates a filtered set of visual data items. + + The DelegateModelGroup type provides a means to address the model data of a + DelegateModel's delegate items, as well as sort and filter these delegate + items. + + The initial set of instantiable delegate items in a DelegateModel is represented + by its \l {QtQml.Models::DelegateModel::items}{items} group, which normally directly reflects + the contents of the model assigned to DelegateModel::model. This set can be changed to + the contents of any other member of DelegateModel::groups by assigning the \l name of that + DelegateModelGroup to the DelegateModel::filterOnGroup property. + + The data of an item in a DelegateModelGroup can be accessed using the get() function, which returns + information about group membership and indexes as well as model data. In combination + with the move() function this can be used to implement view sorting, with remove() to filter + items out of a view, or with setGroups() and \l Package delegates to categorize items into + different views. Different groups can only be sorted independently if they are disjunct. Moving + an item in one group will also move it in all other groups it is a part of. + + Data from models can be supplemented by inserting data directly into a DelegateModelGroup + with the insert() function. This can be used to introduce mock items into a view, or + placeholder items that are later \l {resolve()}{resolved} to real model data when it becomes + available. + + Delegate items can also be instantiated directly from a DelegateModelGroup using the + create() function, making it possible to use DelegateModel without an accompanying view + type or to cherry-pick specific items that should be instantiated irregardless of whether + they're currently within a view's visible area. + + \sa {QML Dynamic View Ordering Tutorial} +*/ +QQmlDelegateModelGroup::QQmlDelegateModelGroup(QObject *parent) + : QObject(*new QQmlDelegateModelGroupPrivate, parent) +{ +} + +QQmlDelegateModelGroup::QQmlDelegateModelGroup( + const QString &name, QQmlDelegateModel *model, int index, QObject *parent) + : QQmlDelegateModelGroup(parent) +{ + Q_D(QQmlDelegateModelGroup); + d->name = name; + d->setModel(model, Compositor::Group(index)); +} + +QQmlDelegateModelGroup::~QQmlDelegateModelGroup() +{ +} + +/*! + \qmlproperty string QtQml.Models::DelegateModelGroup::name + + This property holds the name of the group. + + Each group in a model must have a unique name starting with a lower case letter. +*/ + +QString QQmlDelegateModelGroup::name() const +{ + Q_D(const QQmlDelegateModelGroup); + return d->name; +} + +void QQmlDelegateModelGroup::setName(const QString &name) +{ + Q_D(QQmlDelegateModelGroup); + if (d->model) + return; + if (d->name != name) { + d->name = name; + emit nameChanged(); + } +} + +/*! + \qmlproperty int QtQml.Models::DelegateModelGroup::count + + This property holds the number of items in the group. +*/ + +int QQmlDelegateModelGroup::count() const +{ + Q_D(const QQmlDelegateModelGroup); + if (!d->model) + return 0; + return QQmlDelegateModelPrivate::get(d->model)->m_compositor.count(d->group); +} + +/*! + \qmlproperty bool QtQml.Models::DelegateModelGroup::includeByDefault + + This property holds whether new items are assigned to this group by default. +*/ + +bool QQmlDelegateModelGroup::defaultInclude() const +{ + Q_D(const QQmlDelegateModelGroup); + return d->defaultInclude; +} + +void QQmlDelegateModelGroup::setDefaultInclude(bool include) +{ + Q_D(QQmlDelegateModelGroup); + if (d->defaultInclude != include) { + d->defaultInclude = include; + + if (d->model) { + if (include) + QQmlDelegateModelPrivate::get(d->model)->m_compositor.setDefaultGroup(d->group); + else + QQmlDelegateModelPrivate::get(d->model)->m_compositor.clearDefaultGroup(d->group); + } + emit defaultIncludeChanged(); + } +} + +/*! + \qmlmethod object QtQml.Models::DelegateModelGroup::get(int index) + + Returns a javascript object describing the item at \a index in the group. + + The returned object contains the same information that is available to a delegate from the + DelegateModel attached as well as the model for that item. It has the properties: + + \list + \li \b model The model data of the item. This is the same as the model context property in + a delegate + \li \b groups A list the of names of groups the item is a member of. This property can be + written to change the item's membership. + \li \b inItems Whether the item belongs to the \l {QtQml.Models::DelegateModel::items}{items} group. + Writing to this property will add or remove the item from the group. + \li \b itemsIndex The index of the item within the \l {QtQml.Models::DelegateModel::items}{items} group. + \li \b {in<GroupName>} Whether the item belongs to the dynamic group \e groupName. Writing to + this property will add or remove the item from the group. + \li \b {<groupName>Index} The index of the item within the dynamic group \e groupName. + \li \b isUnresolved Whether the item is bound to an index in the model assigned to + DelegateModel::model. Returns true if the item is not bound to the model, and false if it is. + \endlist +*/ + +QJSValue QQmlDelegateModelGroup::get(int index) +{ + Q_D(QQmlDelegateModelGroup); + if (!d->model) + return QJSValue(); + + QQmlDelegateModelPrivate *model = QQmlDelegateModelPrivate::get(d->model); + if (!model->m_context || !model->m_context->isValid()) { + return QJSValue(); + } else if (index < 0 || index >= model->m_compositor.count(d->group)) { + qmlWarning(this) << tr("get: index out of range"); + return QJSValue(); + } + + Compositor::iterator it = model->m_compositor.find(d->group, index); + QQmlDelegateModelItem *cacheItem = it->inCache() + ? model->m_cache.at(it.cacheIndex) + : 0; + + if (!cacheItem) { + cacheItem = model->m_adaptorModel.createItem( + model->m_cacheMetaType, it.modelIndex()); + if (!cacheItem) + return QJSValue(); + cacheItem->groups = it->flags; + + model->m_cache.insert(it.cacheIndex, cacheItem); + model->m_compositor.setFlags(it, 1, Compositor::CacheFlag); + } + + if (model->m_cacheMetaType->modelItemProto.isUndefined()) + model->m_cacheMetaType->initializePrototype(); + QV4::ExecutionEngine *v4 = model->m_cacheMetaType->v4Engine; + QV4::Scope scope(v4); + QV4::ScopedObject o(scope, v4->memoryManager->allocate<QQmlDelegateModelItemObject>(cacheItem)); + QV4::ScopedObject p(scope, model->m_cacheMetaType->modelItemProto.value()); + o->setPrototypeOf(p); + ++cacheItem->scriptRef; + + return QJSValue(v4, o->asReturnedValue()); +} + +bool QQmlDelegateModelGroupPrivate::parseIndex(const QV4::Value &value, int *index, Compositor::Group *group) const +{ + if (value.isNumber()) { + *index = value.toInt32(); + return true; + } + + if (!value.isObject()) + return false; + + QV4::ExecutionEngine *v4 = value.as<QV4::Object>()->engine(); + QV4::Scope scope(v4); + QV4::Scoped<QQmlDelegateModelItemObject> object(scope, value); + + if (object) { + QQmlDelegateModelItem * const cacheItem = object->d()->item; + if (QQmlDelegateModelPrivate *model = cacheItem->metaType->model + ? QQmlDelegateModelPrivate::get(cacheItem->metaType->model) + : nullptr) { + *index = model->m_cache.indexOf(cacheItem); + *group = Compositor::Cache; + return true; + } + } + return false; +} + +/*! + \qmlmethod QtQml.Models::DelegateModelGroup::insert(int index, jsdict data, array groups = undefined) + \qmlmethod QtQml.Models::DelegateModelGroup::insert(jsdict data, var groups = undefined) + + Creates a new entry at \a index in a DelegateModel with the values from \a data that + correspond to roles in the model assigned to DelegateModel::model. + + If no index is supplied the data is appended to the model. + + The optional \a groups parameter identifies the groups the new entry should belong to, + if unspecified this is equal to the group insert was called on. + + Data inserted into a DelegateModel can later be merged with an existing entry in + DelegateModel::model using the \l resolve() function. This can be used to create placeholder + items that are later replaced by actual data. +*/ + +void QQmlDelegateModelGroup::insert(QQmlV4Function *args) +{ + Q_D(QQmlDelegateModelGroup); + QQmlDelegateModelPrivate *model = QQmlDelegateModelPrivate::get(d->model); + + int index = model->m_compositor.count(d->group); + Compositor::Group group = d->group; + + if (args->length() == 0) + return; + + int i = 0; + QV4::Scope scope(args->v4engine()); + QV4::ScopedValue v(scope, (*args)[i]); + if (d->parseIndex(v, &index, &group)) { + if (index < 0 || index > model->m_compositor.count(group)) { + qmlWarning(this) << tr("insert: index out of range"); + return; + } + if (++i == args->length()) + return; + v = (*args)[i]; + } + + Compositor::insert_iterator before = index < model->m_compositor.count(group) + ? model->m_compositor.findInsertPosition(group, index) + : model->m_compositor.end(); + + int groups = 1 << d->group; + if (++i < args->length()) { + QV4::ScopedValue val(scope, (*args)[i]); + groups |= model->m_cacheMetaType->parseGroups(val); + } + + if (v->as<QV4::ArrayObject>()) { + return; + } else if (v->as<QV4::Object>()) { + model->insert(before, v, groups); + model->emitChanges(); + } +} + +/*! + \qmlmethod QtQml.Models::DelegateModelGroup::create(int index) + \qmlmethod QtQml.Models::DelegateModelGroup::create(int index, jsdict data, array groups = undefined) + \qmlmethod QtQml.Models::DelegateModelGroup::create(jsdict data, array groups = undefined) + + Returns a reference to the instantiated item at \a index in the group. + + If a \a data object is provided it will be \l {insert}{inserted} at \a index and an item + referencing this new entry will be returned. The optional \a groups parameter identifies + the groups the new entry should belong to, if unspecified this is equal to the group create() + was called on. + + All items returned by create are added to the + \l {QtQml.Models::DelegateModel::persistedItems}{persistedItems} group. Items in this + group remain instantiated when not referenced by any view. +*/ + +void QQmlDelegateModelGroup::create(QQmlV4Function *args) +{ + Q_D(QQmlDelegateModelGroup); + if (!d->model) + return; + + if (args->length() == 0) + return; + + QQmlDelegateModelPrivate *model = QQmlDelegateModelPrivate::get(d->model); + + int index = model->m_compositor.count(d->group); + Compositor::Group group = d->group; + + int i = 0; + QV4::Scope scope(args->v4engine()); + QV4::ScopedValue v(scope, (*args)[i]); + if (d->parseIndex(v, &index, &group)) + ++i; + + if (i < args->length() && index >= 0 && index <= model->m_compositor.count(group)) { + v = (*args)[i]; + if (v->as<QV4::Object>()) { + int groups = 1 << d->group; + if (++i < args->length()) { + QV4::ScopedValue val(scope, (*args)[i]); + groups |= model->m_cacheMetaType->parseGroups(val); + } + + Compositor::insert_iterator before = index < model->m_compositor.count(group) + ? model->m_compositor.findInsertPosition(group, index) + : model->m_compositor.end(); + + index = before.index[d->group]; + group = d->group; + + if (!model->insert(before, v, groups)) { + return; + } + } + } + if (index < 0 || index >= model->m_compositor.count(group)) { + qmlWarning(this) << tr("create: index out of range"); + return; + } + + QObject *object = model->object(group, index, QQmlIncubator::AsynchronousIfNested); + if (object) { + QVector<Compositor::Insert> inserts; + Compositor::iterator it = model->m_compositor.find(group, index); + model->m_compositor.setFlags(it, 1, d->group, Compositor::PersistedFlag, &inserts); + model->itemsInserted(inserts); + model->m_cache.at(it.cacheIndex)->releaseObject(); + } + + args->setReturnValue(QV4::QObjectWrapper::wrap(args->v4engine(), object)); + model->emitChanges(); +} + +/*! + \qmlmethod QtQml.Models::DelegateModelGroup::resolve(int from, int to) + + Binds an unresolved item at \a from to an item in DelegateModel::model at index \a to. + + Unresolved items are entries whose data has been \l {insert()}{inserted} into a DelegateModelGroup + instead of being derived from a DelegateModel::model index. Resolving an item will replace + the item at the target index with the unresolved item. A resolved an item will reflect the data + of the source model at its bound index and will move when that index moves like any other item. + + If a new item is replaced in the DelegateModelGroup onChanged() handler its insertion and + replacement will be communicated to views as an atomic operation, creating the appearance + that the model contents have not changed, or if the unresolved and model item are not adjacent + that the previously unresolved item has simply moved. + +*/ +void QQmlDelegateModelGroup::resolve(QQmlV4Function *args) +{ + Q_D(QQmlDelegateModelGroup); + if (!d->model) + return; + + QQmlDelegateModelPrivate *model = QQmlDelegateModelPrivate::get(d->model); + + if (args->length() < 2) + return; + + int from = -1; + int to = -1; + Compositor::Group fromGroup = d->group; + Compositor::Group toGroup = d->group; + + QV4::Scope scope(args->v4engine()); + QV4::ScopedValue v(scope, (*args)[0]); + if (d->parseIndex(v, &from, &fromGroup)) { + if (from < 0 || from >= model->m_compositor.count(fromGroup)) { + qmlWarning(this) << tr("resolve: from index out of range"); + return; + } + } else { + qmlWarning(this) << tr("resolve: from index invalid"); + return; + } + + v = (*args)[1]; + if (d->parseIndex(v, &to, &toGroup)) { + if (to < 0 || to >= model->m_compositor.count(toGroup)) { + qmlWarning(this) << tr("resolve: to index out of range"); + return; + } + } else { + qmlWarning(this) << tr("resolve: to index invalid"); + return; + } + + Compositor::iterator fromIt = model->m_compositor.find(fromGroup, from); + Compositor::iterator toIt = model->m_compositor.find(toGroup, to); + + if (!fromIt->isUnresolved()) { + qmlWarning(this) << tr("resolve: from is not an unresolved item"); + return; + } + if (!toIt->list) { + qmlWarning(this) << tr("resolve: to is not a model item"); + return; + } + + const int unresolvedFlags = fromIt->flags; + const int resolvedFlags = toIt->flags; + const int resolvedIndex = toIt.modelIndex(); + void * const resolvedList = toIt->list; + + QQmlDelegateModelItem *cacheItem = model->m_cache.at(fromIt.cacheIndex); + cacheItem->groups &= ~Compositor::UnresolvedFlag; + + if (toIt.cacheIndex > fromIt.cacheIndex) + toIt.decrementIndexes(1, unresolvedFlags); + if (!toIt->inGroup(fromGroup) || toIt.index[fromGroup] > from) + from += 1; + + model->itemsMoved( + QVector<Compositor::Remove>(1, Compositor::Remove(fromIt, 1, unresolvedFlags, 0)), + QVector<Compositor::Insert>(1, Compositor::Insert(toIt, 1, unresolvedFlags, 0))); + model->itemsInserted( + QVector<Compositor::Insert>(1, Compositor::Insert(toIt, 1, (resolvedFlags & ~unresolvedFlags) | Compositor::CacheFlag))); + toIt.incrementIndexes(1, resolvedFlags | unresolvedFlags); + model->itemsRemoved(QVector<Compositor::Remove>(1, Compositor::Remove(toIt, 1, resolvedFlags))); + + model->m_compositor.setFlags(toGroup, to, 1, unresolvedFlags & ~Compositor::UnresolvedFlag); + model->m_compositor.clearFlags(fromGroup, from, 1, unresolvedFlags); + + if (resolvedFlags & Compositor::CacheFlag) + model->m_compositor.insert(Compositor::Cache, toIt.cacheIndex, resolvedList, resolvedIndex, 1, Compositor::CacheFlag); + + Q_ASSERT(model->m_cache.count() == model->m_compositor.count(Compositor::Cache)); + + if (!cacheItem->isReferenced()) { + Q_ASSERT(toIt.cacheIndex == model->m_cache.indexOf(cacheItem)); + model->m_cache.removeAt(toIt.cacheIndex); + model->m_compositor.clearFlags(Compositor::Cache, toIt.cacheIndex, 1, Compositor::CacheFlag); + delete cacheItem; + Q_ASSERT(model->m_cache.count() == model->m_compositor.count(Compositor::Cache)); + } else { + cacheItem->resolveIndex(model->m_adaptorModel, resolvedIndex); + if (cacheItem->attached) + cacheItem->attached->emitUnresolvedChanged(); + } + + model->emitChanges(); +} + +/*! + \qmlmethod QtQml.Models::DelegateModelGroup::remove(int index, int count) + + Removes \a count items starting at \a index from the group. +*/ + +void QQmlDelegateModelGroup::remove(QQmlV4Function *args) +{ + Q_D(QQmlDelegateModelGroup); + if (!d->model) + return; + Compositor::Group group = d->group; + int index = -1; + int count = 1; + + if (args->length() == 0) + return; + + int i = 0; + QV4::Scope scope(args->v4engine()); + QV4::ScopedValue v(scope, (*args)[0]); + if (!d->parseIndex(v, &index, &group)) { + qmlWarning(this) << tr("remove: invalid index"); + return; + } + + if (++i < args->length()) { + v = (*args)[i]; + if (v->isNumber()) + count = v->toInt32(); + } + + QQmlDelegateModelPrivate *model = QQmlDelegateModelPrivate::get(d->model); + if (index < 0 || index >= model->m_compositor.count(group)) { + qmlWarning(this) << tr("remove: index out of range"); + } else if (count != 0) { + Compositor::iterator it = model->m_compositor.find(group, index); + if (count < 0 || count > model->m_compositor.count(d->group) - it.index[d->group]) { + qmlWarning(this) << tr("remove: invalid count"); + } else { + model->removeGroups(it, count, d->group, 1 << d->group); + } + } +} + +bool QQmlDelegateModelGroupPrivate::parseGroupArgs( + QQmlV4Function *args, Compositor::Group *group, int *index, int *count, int *groups) const +{ + if (!model || !QQmlDelegateModelPrivate::get(model)->m_cacheMetaType) + return false; + + if (args->length() < 2) + return false; + + int i = 0; + QV4::Scope scope(args->v4engine()); + QV4::ScopedValue v(scope, (*args)[i]); + if (!parseIndex(v, index, group)) + return false; + + v = (*args)[++i]; + if (v->isNumber()) { + *count = v->toInt32(); + + if (++i == args->length()) + return false; + v = (*args)[i]; + } + + *groups = QQmlDelegateModelPrivate::get(model)->m_cacheMetaType->parseGroups(v); + + return true; +} + +/*! + \qmlmethod QtQml.Models::DelegateModelGroup::addGroups(int index, int count, stringlist groups) + + Adds \a count items starting at \a index to \a groups. +*/ + +void QQmlDelegateModelGroup::addGroups(QQmlV4Function *args) +{ + Q_D(QQmlDelegateModelGroup); + Compositor::Group group = d->group; + int index = -1; + int count = 1; + int groups = 0; + + if (!d->parseGroupArgs(args, &group, &index, &count, &groups)) + return; + + QQmlDelegateModelPrivate *model = QQmlDelegateModelPrivate::get(d->model); + if (index < 0 || index >= model->m_compositor.count(group)) { + qmlWarning(this) << tr("addGroups: index out of range"); + } else if (count != 0) { + Compositor::iterator it = model->m_compositor.find(group, index); + if (count < 0 || count > model->m_compositor.count(d->group) - it.index[d->group]) { + qmlWarning(this) << tr("addGroups: invalid count"); + } else { + model->addGroups(it, count, d->group, groups); + } + } +} + +/*! + \qmlmethod QtQml.Models::DelegateModelGroup::removeGroups(int index, int count, stringlist groups) + + Removes \a count items starting at \a index from \a groups. +*/ + +void QQmlDelegateModelGroup::removeGroups(QQmlV4Function *args) +{ + Q_D(QQmlDelegateModelGroup); + Compositor::Group group = d->group; + int index = -1; + int count = 1; + int groups = 0; + + if (!d->parseGroupArgs(args, &group, &index, &count, &groups)) + return; + + QQmlDelegateModelPrivate *model = QQmlDelegateModelPrivate::get(d->model); + if (index < 0 || index >= model->m_compositor.count(group)) { + qmlWarning(this) << tr("removeGroups: index out of range"); + } else if (count != 0) { + Compositor::iterator it = model->m_compositor.find(group, index); + if (count < 0 || count > model->m_compositor.count(d->group) - it.index[d->group]) { + qmlWarning(this) << tr("removeGroups: invalid count"); + } else { + model->removeGroups(it, count, d->group, groups); + } + } +} + +/*! + \qmlmethod QtQml.Models::DelegateModelGroup::setGroups(int index, int count, stringlist groups) + + Sets the \a groups \a count items starting at \a index belong to. +*/ + +void QQmlDelegateModelGroup::setGroups(QQmlV4Function *args) +{ + Q_D(QQmlDelegateModelGroup); + Compositor::Group group = d->group; + int index = -1; + int count = 1; + int groups = 0; + + if (!d->parseGroupArgs(args, &group, &index, &count, &groups)) + return; + + QQmlDelegateModelPrivate *model = QQmlDelegateModelPrivate::get(d->model); + if (index < 0 || index >= model->m_compositor.count(group)) { + qmlWarning(this) << tr("setGroups: index out of range"); + } else if (count != 0) { + Compositor::iterator it = model->m_compositor.find(group, index); + if (count < 0 || count > model->m_compositor.count(d->group) - it.index[d->group]) { + qmlWarning(this) << tr("setGroups: invalid count"); + } else { + model->setGroups(it, count, d->group, groups); + } + } +} + +/*! + \qmlmethod QtQml.Models::DelegateModelGroup::setGroups(int index, int count, stringlist groups) + + Sets the \a groups \a count items starting at \a index belong to. +*/ + +/*! + \qmlmethod QtQml.Models::DelegateModelGroup::move(var from, var to, int count) + + Moves \a count at \a from in a group \a to a new position. + + \note The DelegateModel acts as a proxy model: it holds the delegates in a + different order than the \l{dm-model-property}{underlying model} has them. + Any subsequent changes to the underlying model will not undo whatever + reordering you have done via this function. +*/ + +void QQmlDelegateModelGroup::move(QQmlV4Function *args) +{ + Q_D(QQmlDelegateModelGroup); + + if (args->length() < 2) + return; + + Compositor::Group fromGroup = d->group; + Compositor::Group toGroup = d->group; + int from = -1; + int to = -1; + int count = 1; + + QV4::Scope scope(args->v4engine()); + QV4::ScopedValue v(scope, (*args)[0]); + if (!d->parseIndex(v, &from, &fromGroup)) { + qmlWarning(this) << tr("move: invalid from index"); + return; + } + + v = (*args)[1]; + if (!d->parseIndex(v, &to, &toGroup)) { + qmlWarning(this) << tr("move: invalid to index"); + return; + } + + if (args->length() > 2) { + v = (*args)[2]; + if (v->isNumber()) + count = v->toInt32(); + } + + QQmlDelegateModelPrivate *model = QQmlDelegateModelPrivate::get(d->model); + + if (count < 0) { + qmlWarning(this) << tr("move: invalid count"); + } else if (from < 0 || from + count > model->m_compositor.count(fromGroup)) { + qmlWarning(this) << tr("move: from index out of range"); + } else if (!model->m_compositor.verifyMoveTo(fromGroup, from, toGroup, to, count, d->group)) { + qmlWarning(this) << tr("move: to index out of range"); + } else if (count > 0) { + QVector<Compositor::Remove> removes; + QVector<Compositor::Insert> inserts; + + model->m_compositor.move(fromGroup, from, toGroup, to, count, d->group, &removes, &inserts); + model->itemsMoved(removes, inserts); + model->emitChanges(); + } + +} + +/*! + \qmlsignal QtQml.Models::DelegateModelGroup::changed(array removed, array inserted) + + This signal is emitted when items have been removed from or inserted into the group. + + Each object in the \a removed and \a inserted arrays has two values; the \e index of the first + item inserted or removed and a \e count of the number of consecutive items inserted or removed. + + Each index is adjusted for previous changes with all removed items preceding any inserted + items. + + The corresponding handler is \c onChanged. +*/ + +//============================================================================ + +QQmlPartsModel::QQmlPartsModel(QQmlDelegateModel *model, const QString &part, QObject *parent) + : QQmlInstanceModel(*new QObjectPrivate, parent) + , m_model(model) + , m_part(part) + , m_compositorGroup(Compositor::Cache) + , m_inheritGroup(true) +{ + QQmlDelegateModelPrivate *d = QQmlDelegateModelPrivate::get(m_model); + if (d->m_cacheMetaType) { + QQmlDelegateModelGroupPrivate::get(d->m_groups[1])->emitters.insert(this); + m_compositorGroup = Compositor::Default; + } else { + d->m_pendingParts.insert(this); + } +} + +QQmlPartsModel::~QQmlPartsModel() +{ +} + +QString QQmlPartsModel::filterGroup() const +{ + if (m_inheritGroup) + return m_model->filterGroup(); + return m_filterGroup; +} + +void QQmlPartsModel::setFilterGroup(const QString &group) +{ + if (QQmlDelegateModelPrivate::get(m_model)->m_transaction) { + qmlWarning(this) << tr("The group of a DelegateModel cannot be changed within onChanged"); + return; + } + + if (m_filterGroup != group || m_inheritGroup) { + m_filterGroup = group; + m_inheritGroup = false; + updateFilterGroup(); + + emit filterGroupChanged(); + } +} + +void QQmlPartsModel::resetFilterGroup() +{ + if (!m_inheritGroup) { + m_inheritGroup = true; + updateFilterGroup(); + emit filterGroupChanged(); + } +} + +void QQmlPartsModel::updateFilterGroup() +{ + QQmlDelegateModelPrivate *model = QQmlDelegateModelPrivate::get(m_model); + if (!model->m_cacheMetaType) + return; + + if (m_inheritGroup) { + if (m_filterGroup == model->m_filterGroup) + return; + m_filterGroup = model->m_filterGroup; + } + + QQmlListCompositor::Group previousGroup = m_compositorGroup; + m_compositorGroup = Compositor::Default; + QQmlDelegateModelGroupPrivate::get(model->m_groups[Compositor::Default])->emitters.insert(this); + for (int i = 1; i < model->m_groupCount; ++i) { + if (m_filterGroup == model->m_cacheMetaType->groupNames.at(i - 1)) { + m_compositorGroup = Compositor::Group(i); + break; + } + } + + QQmlDelegateModelGroupPrivate::get(model->m_groups[m_compositorGroup])->emitters.insert(this); + if (m_compositorGroup != previousGroup) { + QVector<QQmlChangeSet::Change> removes; + QVector<QQmlChangeSet::Change> inserts; + model->m_compositor.transition(previousGroup, m_compositorGroup, &removes, &inserts); + + QQmlChangeSet changeSet; + changeSet.move(removes, inserts); + if (!changeSet.isEmpty()) + emit modelUpdated(changeSet, false); + + if (changeSet.difference() != 0) + emit countChanged(); + } +} + +void QQmlPartsModel::updateFilterGroup( + Compositor::Group group, const QQmlChangeSet &changeSet) +{ + if (!m_inheritGroup) + return; + + m_compositorGroup = group; + QQmlDelegateModelGroupPrivate::get(QQmlDelegateModelPrivate::get(m_model)->m_groups[m_compositorGroup])->emitters.insert(this); + + if (!changeSet.isEmpty()) + emit modelUpdated(changeSet, false); + + if (changeSet.difference() != 0) + emit countChanged(); + + emit filterGroupChanged(); +} + +int QQmlPartsModel::count() const +{ + QQmlDelegateModelPrivate *model = QQmlDelegateModelPrivate::get(m_model); + return model->m_delegate + ? model->m_compositor.count(m_compositorGroup) + : 0; +} + +bool QQmlPartsModel::isValid() const +{ + return m_model->isValid(); +} + +QObject *QQmlPartsModel::object(int index, QQmlIncubator::IncubationMode incubationMode) +{ + QQmlDelegateModelPrivate *model = QQmlDelegateModelPrivate::get(m_model); + + if (!model->m_delegate || index < 0 || index >= model->m_compositor.count(m_compositorGroup)) { + qWarning() << "DelegateModel::item: index out range" << index << model->m_compositor.count(m_compositorGroup); + return nullptr; + } + + QObject *object = model->object(m_compositorGroup, index, incubationMode); + + if (QQuickPackage *package = qmlobject_cast<QQuickPackage *>(object)) { + QObject *part = package->part(m_part); + if (!part) + return nullptr; + m_packaged.insertMulti(part, package); + return part; + } + + model->release(object); + if (!model->m_delegateValidated) { + if (object) + qmlWarning(model->m_delegate) << tr("Delegate component must be Package type."); + model->m_delegateValidated = true; + } + + return nullptr; +} + +QQmlInstanceModel::ReleaseFlags QQmlPartsModel::release(QObject *item) +{ + QQmlInstanceModel::ReleaseFlags flags = nullptr; + + QHash<QObject *, QQuickPackage *>::iterator it = m_packaged.find(item); + if (it != m_packaged.end()) { + QQuickPackage *package = *it; + QQmlDelegateModelPrivate *model = QQmlDelegateModelPrivate::get(m_model); + flags = model->release(package); + m_packaged.erase(it); + if (!m_packaged.contains(item)) + flags &= ~Referenced; + if (flags & Destroyed) + QQmlDelegateModelPrivate::get(m_model)->emitDestroyingPackage(package); + } + return flags; +} + +QVariant QQmlPartsModel::variantValue(int index, const QString &role) +{ + return QQmlDelegateModelPrivate::get(m_model)->variantValue(m_compositorGroup, index, role); +} + +void QQmlPartsModel::setWatchedRoles(const QList<QByteArray> &roles) +{ + QQmlDelegateModelPrivate *model = QQmlDelegateModelPrivate::get(m_model); + model->m_adaptorModel.replaceWatchedRoles(m_watchedRoles, roles); + m_watchedRoles = roles; +} + +QQmlIncubator::Status QQmlPartsModel::incubationStatus(int index) +{ + QQmlDelegateModelPrivate *model = QQmlDelegateModelPrivate::get(m_model); + Compositor::iterator it = model->m_compositor.find(model->m_compositorGroup, index); + if (!it->inCache()) + return QQmlIncubator::Null; + + if (auto incubationTask = model->m_cache.at(it.cacheIndex)->incubationTask) + return incubationTask->status(); + + return QQmlIncubator::Ready; +} + +int QQmlPartsModel::indexOf(QObject *item, QObject *) const +{ + QHash<QObject *, QQuickPackage *>::const_iterator it = m_packaged.find(item); + if (it != m_packaged.end()) { + if (QQmlDelegateModelItem *cacheItem = QQmlDelegateModelItem::dataForObject(*it)) + return cacheItem->groupIndex(m_compositorGroup); + } + return -1; +} + +void QQmlPartsModel::createdPackage(int index, QQuickPackage *package) +{ + emit createdItem(index, package->part(m_part)); +} + +void QQmlPartsModel::initPackage(int index, QQuickPackage *package) +{ + if (m_modelUpdatePending) + m_pendingPackageInitializations << index; + else + emit initItem(index, package->part(m_part)); +} + +void QQmlPartsModel::destroyingPackage(QQuickPackage *package) +{ + QObject *item = package->part(m_part); + Q_ASSERT(!m_packaged.contains(item)); + emit destroyingItem(item); +} + +void QQmlPartsModel::emitModelUpdated(const QQmlChangeSet &changeSet, bool reset) +{ + m_modelUpdatePending = false; + emit modelUpdated(changeSet, reset); + if (changeSet.difference() != 0) + emit countChanged(); + + QQmlDelegateModelPrivate *model = QQmlDelegateModelPrivate::get(m_model); + QVector<int> pendingPackageInitializations; + qSwap(pendingPackageInitializations, m_pendingPackageInitializations); + for (int index : pendingPackageInitializations) { + if (!model->m_delegate || index < 0 || index >= model->m_compositor.count(m_compositorGroup)) + continue; + QObject *object = model->object(m_compositorGroup, index, QQmlIncubator::Asynchronous); + if (QQuickPackage *package = qmlobject_cast<QQuickPackage *>(object)) + emit initItem(index, package->part(m_part)); + model->release(object); + } +} + +//============================================================================ + +struct QQmlDelegateModelGroupChange : QV4::Object +{ + V4_OBJECT2(QQmlDelegateModelGroupChange, QV4::Object) + + static QV4::Heap::QQmlDelegateModelGroupChange *create(QV4::ExecutionEngine *e) { + return e->memoryManager->allocate<QQmlDelegateModelGroupChange>(); + } + + static QV4::ReturnedValue method_get_index(const QV4::FunctionObject *b, const QV4::Value *thisObject, const QV4::Value *, int) { + QV4::Scope scope(b); + QV4::Scoped<QQmlDelegateModelGroupChange> that(scope, thisObject->as<QQmlDelegateModelGroupChange>()); + if (!that) + THROW_TYPE_ERROR(); + return QV4::Encode(that->d()->change.index); + } + static QV4::ReturnedValue method_get_count(const QV4::FunctionObject *b, const QV4::Value *thisObject, const QV4::Value *, int) { + QV4::Scope scope(b); + QV4::Scoped<QQmlDelegateModelGroupChange> that(scope, thisObject->as<QQmlDelegateModelGroupChange>()); + if (!that) + THROW_TYPE_ERROR(); + return QV4::Encode(that->d()->change.count); + } + static QV4::ReturnedValue method_get_moveId(const QV4::FunctionObject *b, const QV4::Value *thisObject, const QV4::Value *, int) { + QV4::Scope scope(b); + QV4::Scoped<QQmlDelegateModelGroupChange> that(scope, thisObject->as<QQmlDelegateModelGroupChange>()); + if (!that) + THROW_TYPE_ERROR(); + if (that->d()->change.moveId < 0) + RETURN_UNDEFINED(); + return QV4::Encode(that->d()->change.moveId); + } +}; + +DEFINE_OBJECT_VTABLE(QQmlDelegateModelGroupChange); + +struct QQmlDelegateModelGroupChangeArray : public QV4::Object +{ + V4_OBJECT2(QQmlDelegateModelGroupChangeArray, QV4::Object) + V4_NEEDS_DESTROY +public: + static QV4::Heap::QQmlDelegateModelGroupChangeArray *create(QV4::ExecutionEngine *engine, const QVector<QQmlChangeSet::Change> &changes) + { + return engine->memoryManager->allocate<QQmlDelegateModelGroupChangeArray>(changes); + } + + quint32 count() const { return d()->changes->count(); } + const QQmlChangeSet::Change &at(int index) const { return d()->changes->at(index); } + + static QV4::ReturnedValue virtualGet(const QV4::Managed *m, QV4::PropertyKey id, const QV4::Value *receiver, bool *hasProperty) + { + if (id.isArrayIndex()) { + uint index = id.asArrayIndex(); + Q_ASSERT(m->as<QQmlDelegateModelGroupChangeArray>()); + QV4::ExecutionEngine *v4 = static_cast<const QQmlDelegateModelGroupChangeArray *>(m)->engine(); + QV4::Scope scope(v4); + QV4::Scoped<QQmlDelegateModelGroupChangeArray> array(scope, static_cast<const QQmlDelegateModelGroupChangeArray *>(m)); + + if (index >= array->count()) { + if (hasProperty) + *hasProperty = false; + return QV4::Value::undefinedValue().asReturnedValue(); + } + + const QQmlChangeSet::Change &change = array->at(index); + + QV4::ScopedObject changeProto(scope, engineData(v4)->changeProto.value()); + QV4::Scoped<QQmlDelegateModelGroupChange> object(scope, QQmlDelegateModelGroupChange::create(v4)); + object->setPrototypeOf(changeProto); + object->d()->change = change; + + if (hasProperty) + *hasProperty = true; + return object.asReturnedValue(); + } + + Q_ASSERT(m->as<QQmlDelegateModelGroupChangeArray>()); + const QQmlDelegateModelGroupChangeArray *array = static_cast<const QQmlDelegateModelGroupChangeArray *>(m); + + if (id == array->engine()->id_length()->propertyKey()) { + if (hasProperty) + *hasProperty = true; + return QV4::Encode(array->count()); + } + + return Object::virtualGet(m, id, receiver, hasProperty); + } +}; + +void QV4::Heap::QQmlDelegateModelGroupChangeArray::init(const QVector<QQmlChangeSet::Change> &changes) +{ + Object::init(); + this->changes = new QVector<QQmlChangeSet::Change>(changes); + QV4::Scope scope(internalClass->engine); + QV4::ScopedObject o(scope, this); + o->setArrayType(QV4::Heap::ArrayData::Custom); +} + +DEFINE_OBJECT_VTABLE(QQmlDelegateModelGroupChangeArray); + +QQmlDelegateModelEngineData::QQmlDelegateModelEngineData(QV4::ExecutionEngine *v4) +{ + QV4::Scope scope(v4); + + QV4::ScopedObject proto(scope, v4->newObject()); + proto->defineAccessorProperty(QStringLiteral("index"), QQmlDelegateModelGroupChange::method_get_index, nullptr); + proto->defineAccessorProperty(QStringLiteral("count"), QQmlDelegateModelGroupChange::method_get_count, nullptr); + proto->defineAccessorProperty(QStringLiteral("moveId"), QQmlDelegateModelGroupChange::method_get_moveId, nullptr); + changeProto.set(v4, proto); +} + +QQmlDelegateModelEngineData::~QQmlDelegateModelEngineData() +{ +} + +QV4::ReturnedValue QQmlDelegateModelEngineData::array(QV4::ExecutionEngine *v4, + const QVector<QQmlChangeSet::Change> &changes) +{ + QV4::Scope scope(v4); + QV4::ScopedObject o(scope, QQmlDelegateModelGroupChangeArray::create(v4, changes)); + return o.asReturnedValue(); +} + +QT_END_NAMESPACE + +#include "moc_qqmldelegatemodel_p.cpp" diff --git a/src/qmlmodels/qqmldelegatemodel_p.h b/src/qmlmodels/qqmldelegatemodel_p.h new file mode 100644 index 0000000000..21eaef02e0 --- /dev/null +++ b/src/qmlmodels/qqmldelegatemodel_p.h @@ -0,0 +1,246 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtQml module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QQMLDATAMODEL_P_H +#define QQMLDATAMODEL_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <private/qtqmlmodelsglobal_p.h> +#include <private/qqmllistcompositor_p.h> +#include <private/qqmlobjectmodel_p.h> +#include <private/qqmlincubator_p.h> + +#include <QtCore/qabstractitemmodel.h> +#include <QtCore/qstringlist.h> + +QT_REQUIRE_CONFIG(qml_delegate_model); + +QT_BEGIN_NAMESPACE + +class QQmlChangeSet; +class QQuickPackage; +class QQmlV4Function; +class QQmlDelegateModelGroup; +class QQmlDelegateModelAttached; +class QQmlDelegateModelPrivate; + + +class Q_QMLMODELS_PRIVATE_EXPORT QQmlDelegateModel : public QQmlInstanceModel, public QQmlParserStatus +{ + Q_OBJECT + Q_DECLARE_PRIVATE(QQmlDelegateModel) + + Q_PROPERTY(QVariant model READ model WRITE setModel) + Q_PROPERTY(QQmlComponent *delegate READ delegate WRITE setDelegate) + Q_PROPERTY(QString filterOnGroup READ filterGroup WRITE setFilterGroup NOTIFY filterGroupChanged RESET resetFilterGroup) + Q_PROPERTY(QQmlDelegateModelGroup *items READ items CONSTANT) //TODO : worth renaming? + Q_PROPERTY(QQmlDelegateModelGroup *persistedItems READ persistedItems CONSTANT) + Q_PROPERTY(QQmlListProperty<QQmlDelegateModelGroup> groups READ groups CONSTANT) + Q_PROPERTY(QObject *parts READ parts CONSTANT) + Q_PROPERTY(QVariant rootIndex READ rootIndex WRITE setRootIndex NOTIFY rootIndexChanged) + Q_CLASSINFO("DefaultProperty", "delegate") + Q_INTERFACES(QQmlParserStatus) +public: + QQmlDelegateModel(); + QQmlDelegateModel(QQmlContext *, QObject *parent=nullptr); + ~QQmlDelegateModel(); + + void classBegin() override; + void componentComplete() override; + + QVariant model() const; + void setModel(const QVariant &); + + QQmlComponent *delegate() const; + void setDelegate(QQmlComponent *); + + QVariant rootIndex() const; + void setRootIndex(const QVariant &root); + + Q_INVOKABLE QVariant modelIndex(int idx) const; + Q_INVOKABLE QVariant parentModelIndex() const; + + int count() const override; + bool isValid() const override { return delegate() != nullptr; } + QObject *object(int index, QQmlIncubator::IncubationMode incubationMode = QQmlIncubator::AsynchronousIfNested) override; + ReleaseFlags release(QObject *object) override; + void cancel(int index) override; + QVariant variantValue(int index, const QString &role) override; + void setWatchedRoles(const QList<QByteArray> &roles) override; + QQmlIncubator::Status incubationStatus(int index) override; + + int indexOf(QObject *object, QObject *objectContext) const override; + + QString filterGroup() const; + void setFilterGroup(const QString &group); + void resetFilterGroup(); + + QQmlDelegateModelGroup *items(); + QQmlDelegateModelGroup *persistedItems(); + QQmlListProperty<QQmlDelegateModelGroup> groups(); + QObject *parts(); + + const QAbstractItemModel *abstractItemModel() const override; + + bool event(QEvent *) override; + + static QQmlDelegateModelAttached *qmlAttachedProperties(QObject *obj); + +Q_SIGNALS: + void filterGroupChanged(); + void defaultGroupsChanged(); + void rootIndexChanged(); + +private Q_SLOTS: + void _q_itemsChanged(int index, int count, const QVector<int> &roles); + void _q_itemsInserted(int index, int count); + void _q_itemsRemoved(int index, int count); + void _q_itemsMoved(int from, int to, int count); + void _q_modelReset(); + void _q_rowsInserted(const QModelIndex &,int,int); + void _q_rowsAboutToBeRemoved(const QModelIndex &parent, int begin, int end); + void _q_rowsRemoved(const QModelIndex &,int,int); + void _q_rowsMoved(const QModelIndex &, int, int, const QModelIndex &, int); + void _q_dataChanged(const QModelIndex&,const QModelIndex&,const QVector<int> &); + void _q_layoutChanged(const QList<QPersistentModelIndex>&, QAbstractItemModel::LayoutChangeHint); + +private: + bool isDescendantOf(const QPersistentModelIndex &desc, const QList<QPersistentModelIndex> &parents) const; + + Q_DISABLE_COPY(QQmlDelegateModel) +}; + +class QQmlDelegateModelGroupPrivate; +class Q_QMLMODELS_PRIVATE_EXPORT QQmlDelegateModelGroup : public QObject +{ + Q_OBJECT + Q_PROPERTY(int count READ count NOTIFY countChanged) + Q_PROPERTY(QString name READ name WRITE setName NOTIFY nameChanged) + Q_PROPERTY(bool includeByDefault READ defaultInclude WRITE setDefaultInclude NOTIFY defaultIncludeChanged) +public: + QQmlDelegateModelGroup(QObject *parent = nullptr); + QQmlDelegateModelGroup(const QString &name, QQmlDelegateModel *model, int compositorType, QObject *parent = nullptr); + ~QQmlDelegateModelGroup(); + + QString name() const; + void setName(const QString &name); + + int count() const; + + bool defaultInclude() const; + void setDefaultInclude(bool include); + + Q_INVOKABLE QJSValue get(int index); + +public Q_SLOTS: + void insert(QQmlV4Function *); + void create(QQmlV4Function *); + void resolve(QQmlV4Function *); + void remove(QQmlV4Function *); + void addGroups(QQmlV4Function *); + void removeGroups(QQmlV4Function *); + void setGroups(QQmlV4Function *); + void move(QQmlV4Function *); + +Q_SIGNALS: + void countChanged(); + void nameChanged(); + void defaultIncludeChanged(); + void changed(const QJSValue &removed, const QJSValue &inserted); +private: + Q_DECLARE_PRIVATE(QQmlDelegateModelGroup) +}; + +class QQmlDelegateModelItem; +class QQmlDelegateModelAttachedMetaObject; +class QQmlDelegateModelAttached : public QObject +{ + Q_OBJECT + Q_PROPERTY(QQmlDelegateModel *model READ model CONSTANT) + Q_PROPERTY(QStringList groups READ groups WRITE setGroups NOTIFY groupsChanged) + Q_PROPERTY(bool isUnresolved READ isUnresolved NOTIFY unresolvedChanged) +public: + QQmlDelegateModelAttached(QObject *parent); + QQmlDelegateModelAttached(QQmlDelegateModelItem *cacheItem, QObject *parent); + ~QQmlDelegateModelAttached() {} + + void resetCurrentIndex(); + void setCacheItem(QQmlDelegateModelItem *item); + + QQmlDelegateModel *model() const; + + QStringList groups() const; + void setGroups(const QStringList &groups); + + bool isUnresolved() const; + + void emitChanges(); + + void emitUnresolvedChanged() { Q_EMIT unresolvedChanged(); } + +Q_SIGNALS: + void groupsChanged(); + void unresolvedChanged(); + +public: + QQmlDelegateModelItem *m_cacheItem; + int m_previousGroups; + int m_currentIndex[QQmlListCompositor::MaximumGroupCount]; + int m_previousIndex[QQmlListCompositor::MaximumGroupCount]; + + friend class QQmlDelegateModelAttachedMetaObject; +}; + +QT_END_NAMESPACE + +QML_DECLARE_TYPE(QQmlDelegateModel) +QML_DECLARE_TYPEINFO(QQmlDelegateModel, QML_HAS_ATTACHED_PROPERTIES) +QML_DECLARE_TYPE(QQmlDelegateModelGroup) + +#endif // QQMLDATAMODEL_P_H diff --git a/src/qmlmodels/qqmldelegatemodel_p_p.h b/src/qmlmodels/qqmldelegatemodel_p_p.h new file mode 100644 index 0000000000..92362b8876 --- /dev/null +++ b/src/qmlmodels/qqmldelegatemodel_p_p.h @@ -0,0 +1,450 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtQml module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QQMLDATAMODEL_P_P_H +#define QQMLDATAMODEL_P_P_H + +#include "qqmldelegatemodel_p.h" +#include <private/qv4qobjectwrapper_p.h> + +#include <QtQml/qqmlcontext.h> +#include <QtQml/qqmlincubator.h> + +#include <private/qqmladaptormodel_p.h> +#include <private/qqmlopenmetaobject_p.h> + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +QT_REQUIRE_CONFIG(qml_delegate_model); + +QT_BEGIN_NAMESPACE + +typedef QQmlListCompositor Compositor; + +class QQmlDelegateModelAttachedMetaObject; +class QQmlAbstractDelegateComponent; + +class Q_QMLMODELS_PRIVATE_EXPORT QQmlDelegateModelItemMetaType : public QQmlRefCount +{ +public: + QQmlDelegateModelItemMetaType(QV4::ExecutionEngine *engine, QQmlDelegateModel *model, const QStringList &groupNames); + ~QQmlDelegateModelItemMetaType(); + + void initializeMetaObject(); + void initializePrototype(); + + int parseGroups(const QStringList &groupNames) const; + int parseGroups(const QV4::Value &groupNames) const; + + QPointer<QQmlDelegateModel> model; + const int groupCount; + QV4::ExecutionEngine * const v4Engine; + QQmlDelegateModelAttachedMetaObject *metaObject; + const QStringList groupNames; + QV4::PersistentValue modelItemProto; +}; + +class QQmlAdaptorModel; +class QQDMIncubationTask; + +class QQmlDelegateModelItem : public QObject +{ + Q_OBJECT + Q_PROPERTY(int index READ modelIndex NOTIFY modelIndexChanged) + Q_PROPERTY(int row READ modelRow NOTIFY rowChanged REVISION 12) + Q_PROPERTY(int column READ modelColumn NOTIFY columnChanged REVISION 12) + Q_PROPERTY(QObject *model READ modelObject CONSTANT) +public: + QQmlDelegateModelItem(QQmlDelegateModelItemMetaType *metaType, + QQmlAdaptorModel::Accessors *accessor, int modelIndex, + int row, int column); + ~QQmlDelegateModelItem(); + + void referenceObject() { ++objectRef; } + bool releaseObject() { return --objectRef == 0 && !(groups & Compositor::PersistedFlag); } + bool isObjectReferenced() const { return objectRef != 0 || (groups & Compositor::PersistedFlag); } + void childContextObjectDestroyed(QObject *childContextObject); + + bool isReferenced() const { + return scriptRef + || incubationTask + || ((groups & Compositor::UnresolvedFlag) && (groups & Compositor::GroupMask)); + } + + void Dispose(); + + QObject *modelObject() { return this; } + + void destroyObject(); + + static QQmlDelegateModelItem *dataForObject(QObject *object); + + int groupIndex(Compositor::Group group); + + int modelRow() const { return row; } + int modelColumn() const { return column; } + int modelIndex() const { return index; } + virtual void setModelIndex(int idx, int newRow, int newColumn); + + virtual QV4::ReturnedValue get() { return QV4::QObjectWrapper::wrap(v4, this); } + + virtual void setValue(const QString &role, const QVariant &value) { Q_UNUSED(role); Q_UNUSED(value); } + virtual bool resolveIndex(const QQmlAdaptorModel &, int) { return false; } + + static QV4::ReturnedValue get_model(const QV4::FunctionObject *, const QV4::Value *thisObject, const QV4::Value *argv, int argc); + static QV4::ReturnedValue get_groups(const QV4::FunctionObject *, const QV4::Value *thisObject, const QV4::Value *argv, int argc); + static QV4::ReturnedValue set_groups(const QV4::FunctionObject *, const QV4::Value *thisObject, const QV4::Value *argv, int argc); + static QV4::ReturnedValue get_member(QQmlDelegateModelItem *thisItem, uint flag, const QV4::Value &); + static QV4::ReturnedValue set_member(QQmlDelegateModelItem *thisItem, uint flag, const QV4::Value &arg); + static QV4::ReturnedValue get_index(QQmlDelegateModelItem *thisItem, uint flag, const QV4::Value &arg); + + QV4::ExecutionEngine *v4; + QQmlDelegateModelItemMetaType * const metaType; + QQmlContextDataRef contextData; + QPointer<QObject> object; + QPointer<QQmlDelegateModelAttached> attached; + QQDMIncubationTask *incubationTask; + QQmlComponent *delegate; + int poolTime; + int objectRef; + int scriptRef; + int groups; + int index; + +Q_SIGNALS: + void modelIndexChanged(); + Q_REVISION(12) void rowChanged(); + Q_REVISION(12) void columnChanged(); + +protected: + void objectDestroyed(QObject *); + int row; + int column; +}; + +namespace QV4 { +namespace Heap { +struct QQmlDelegateModelItemObject : Object { + inline void init(QQmlDelegateModelItem *item); + void destroy(); + QQmlDelegateModelItem *item; +}; + +} +} + +struct QQmlDelegateModelItemObject : QV4::Object +{ + V4_OBJECT2(QQmlDelegateModelItemObject, QV4::Object) + V4_NEEDS_DESTROY +}; + +void QV4::Heap::QQmlDelegateModelItemObject::init(QQmlDelegateModelItem *item) +{ + Object::init(); + this->item = item; +} + + + +class QQmlDelegateModelPrivate; +class QQDMIncubationTask : public QQmlIncubator +{ +public: + QQDMIncubationTask(QQmlDelegateModelPrivate *l, IncubationMode mode) + : QQmlIncubator(mode) + , incubating(nullptr) + , vdm(l) {} + + void statusChanged(Status) override; + void setInitialState(QObject *) override; + + QQmlDelegateModelItem *incubating = nullptr; + QQmlDelegateModelPrivate *vdm = nullptr; + int index[QQmlListCompositor::MaximumGroupCount]; +}; + + +class QQmlDelegateModelGroupEmitter +{ +public: + virtual ~QQmlDelegateModelGroupEmitter() {} + virtual void emitModelUpdated(const QQmlChangeSet &changeSet, bool reset) = 0; + virtual void createdPackage(int, QQuickPackage *) {} + virtual void initPackage(int, QQuickPackage *) {} + virtual void destroyingPackage(QQuickPackage *) {} + + QIntrusiveListNode emitterNode; +}; + +typedef QIntrusiveList<QQmlDelegateModelGroupEmitter, &QQmlDelegateModelGroupEmitter::emitterNode> QQmlDelegateModelGroupEmitterList; + +class QQmlDelegateModelGroupPrivate : public QObjectPrivate +{ +public: + Q_DECLARE_PUBLIC(QQmlDelegateModelGroup) + + QQmlDelegateModelGroupPrivate() : group(Compositor::Cache), defaultInclude(false) {} + + static QQmlDelegateModelGroupPrivate *get(QQmlDelegateModelGroup *group) { + return static_cast<QQmlDelegateModelGroupPrivate *>(QObjectPrivate::get(group)); } + + void setModel(QQmlDelegateModel *model, Compositor::Group group); + bool isChangedConnected(); + void emitChanges(QV4::ExecutionEngine *engine); + void emitModelUpdated(bool reset); + + void createdPackage(int index, QQuickPackage *package); + void initPackage(int index, QQuickPackage *package); + void destroyingPackage(QQuickPackage *package); + + bool parseIndex(const QV4::Value &value, int *index, Compositor::Group *group) const; + bool parseGroupArgs( + QQmlV4Function *args, Compositor::Group *group, int *index, int *count, int *groups) const; + + Compositor::Group group; + QPointer<QQmlDelegateModel> model; + QQmlDelegateModelGroupEmitterList emitters; + QQmlChangeSet changeSet; + QString name; + bool defaultInclude; +}; + +class QQmlDelegateModelParts; + +class QQmlDelegateModelPrivate : public QObjectPrivate, public QQmlDelegateModelGroupEmitter +{ + Q_DECLARE_PUBLIC(QQmlDelegateModel) +public: + QQmlDelegateModelPrivate(QQmlContext *); + ~QQmlDelegateModelPrivate(); + + static QQmlDelegateModelPrivate *get(QQmlDelegateModel *m) { + return static_cast<QQmlDelegateModelPrivate *>(QObjectPrivate::get(m)); + } + + void init(); + void connectModel(QQmlAdaptorModel *model); + void connectToAbstractItemModel(); + void disconnectFromAbstractItemModel(); + + void requestMoreIfNecessary(); + QObject *object(Compositor::Group group, int index, QQmlIncubator::IncubationMode incubationMode); + QQmlDelegateModel::ReleaseFlags release(QObject *object); + QVariant variantValue(Compositor::Group group, int index, const QString &name); + void emitCreatedPackage(QQDMIncubationTask *incubationTask, QQuickPackage *package); + void emitInitPackage(QQDMIncubationTask *incubationTask, QQuickPackage *package); + void emitCreatedItem(QQDMIncubationTask *incubationTask, QObject *item) { + Q_EMIT q_func()->createdItem(incubationTask->index[m_compositorGroup], item); } + void emitInitItem(QQDMIncubationTask *incubationTask, QObject *item) { + Q_EMIT q_func()->initItem(incubationTask->index[m_compositorGroup], item); } + void emitDestroyingPackage(QQuickPackage *package); + void emitDestroyingItem(QObject *item) { Q_EMIT q_func()->destroyingItem(item); } + void addCacheItem(QQmlDelegateModelItem *item, Compositor::iterator it); + void removeCacheItem(QQmlDelegateModelItem *cacheItem); + + void updateFilterGroup(); + + void addGroups(Compositor::iterator from, int count, Compositor::Group group, int groupFlags); + void removeGroups(Compositor::iterator from, int count, Compositor::Group group, int groupFlags); + void setGroups(Compositor::iterator from, int count, Compositor::Group group, int groupFlags); + + void itemsInserted( + const QVector<Compositor::Insert> &inserts, + QVarLengthArray<QVector<QQmlChangeSet::Change>, Compositor::MaximumGroupCount> *translatedInserts, + QHash<int, QList<QQmlDelegateModelItem *> > *movedItems = nullptr); + void itemsInserted(const QVector<Compositor::Insert> &inserts); + void itemsRemoved( + const QVector<Compositor::Remove> &removes, + QVarLengthArray<QVector<QQmlChangeSet::Change>, Compositor::MaximumGroupCount> *translatedRemoves, + QHash<int, QList<QQmlDelegateModelItem *> > *movedItems = nullptr); + void itemsRemoved(const QVector<Compositor::Remove> &removes); + void itemsMoved( + const QVector<Compositor::Remove> &removes, const QVector<Compositor::Insert> &inserts); + void itemsChanged(const QVector<Compositor::Change> &changes); + void emitChanges(); + void emitModelUpdated(const QQmlChangeSet &changeSet, bool reset) override; + void delegateChanged(bool add = true, bool remove = true); + + bool insert(Compositor::insert_iterator &before, const QV4::Value &object, int groups); + + int adaptorModelCount() const; + + static void group_append(QQmlListProperty<QQmlDelegateModelGroup> *property, QQmlDelegateModelGroup *group); + static int group_count(QQmlListProperty<QQmlDelegateModelGroup> *property); + static QQmlDelegateModelGroup *group_at(QQmlListProperty<QQmlDelegateModelGroup> *property, int index); + + void releaseIncubator(QQDMIncubationTask *incubationTask); + void incubatorStatusChanged(QQDMIncubationTask *incubationTask, QQmlIncubator::Status status); + void setInitialState(QQDMIncubationTask *incubationTask, QObject *o); + + QQmlAdaptorModel m_adaptorModel; + QQmlListCompositor m_compositor; + QQmlStrongJSQObjectReference<QQmlComponent> m_delegate; + QQmlAbstractDelegateComponent *m_delegateChooser; + QMetaObject::Connection m_delegateChooserChanged; + QQmlDelegateModelItemMetaType *m_cacheMetaType; + QPointer<QQmlContext> m_context; + QQmlDelegateModelParts *m_parts; + QQmlDelegateModelGroupEmitterList m_pendingParts; + + QList<QQmlDelegateModelItem *> m_cache; + QList<QQDMIncubationTask *> m_finishedIncubating; + QList<QByteArray> m_watchedRoles; + + QString m_filterGroup; + + int m_count; + int m_groupCount; + + QQmlListCompositor::Group m_compositorGroup; + bool m_complete : 1; + bool m_delegateValidated : 1; + bool m_reset : 1; + bool m_transaction : 1; + bool m_incubatorCleanupScheduled : 1; + bool m_waitingToFetchMore : 1; + + union { + struct { + QQmlDelegateModelGroup *m_cacheItems; + QQmlDelegateModelGroup *m_items; + QQmlDelegateModelGroup *m_persistedItems; + }; + QQmlDelegateModelGroup *m_groups[Compositor::MaximumGroupCount]; + }; +}; + +class QQmlPartsModel : public QQmlInstanceModel, public QQmlDelegateModelGroupEmitter +{ + Q_OBJECT + Q_PROPERTY(QString filterOnGroup READ filterGroup WRITE setFilterGroup NOTIFY filterGroupChanged RESET resetFilterGroup) +public: + QQmlPartsModel(QQmlDelegateModel *model, const QString &part, QObject *parent = nullptr); + ~QQmlPartsModel(); + + QString filterGroup() const; + void setFilterGroup(const QString &group); + void resetFilterGroup(); + void updateFilterGroup(); + void updateFilterGroup(Compositor::Group group, const QQmlChangeSet &changeSet); + + int count() const override; + bool isValid() const override; + QObject *object(int index, QQmlIncubator::IncubationMode incubationMode = QQmlIncubator::AsynchronousIfNested) override; + ReleaseFlags release(QObject *item) override; + QVariant variantValue(int index, const QString &role) override; + QList<QByteArray> watchedRoles() const { return m_watchedRoles; } + void setWatchedRoles(const QList<QByteArray> &roles) override; + QQmlIncubator::Status incubationStatus(int index) override; + + int indexOf(QObject *item, QObject *objectContext) const override; + + void emitModelUpdated(const QQmlChangeSet &changeSet, bool reset) override; + + void createdPackage(int index, QQuickPackage *package) override; + void initPackage(int index, QQuickPackage *package) override; + void destroyingPackage(QQuickPackage *package) override; + +Q_SIGNALS: + void filterGroupChanged(); + +private: + QQmlDelegateModel *m_model; + QHash<QObject *, QQuickPackage *> m_packaged; + QString m_part; + QString m_filterGroup; + QList<QByteArray> m_watchedRoles; + QVector<int> m_pendingPackageInitializations; // vector holds model indices + Compositor::Group m_compositorGroup; + bool m_inheritGroup; + bool m_modelUpdatePending = true; +}; + +class QMetaPropertyBuilder; + +class QQmlDelegateModelPartsMetaObject : public QQmlOpenMetaObject +{ +public: + QQmlDelegateModelPartsMetaObject(QObject *parent) + : QQmlOpenMetaObject(parent) {} + + void propertyCreated(int, QMetaPropertyBuilder &) override; + QVariant initialValue(int) override; +}; + +class QQmlDelegateModelParts : public QObject +{ +Q_OBJECT +public: + QQmlDelegateModelParts(QQmlDelegateModel *parent); + + QQmlDelegateModel *model; + QList<QQmlPartsModel *> models; +}; + +class QQmlDelegateModelAttachedMetaObject : public QAbstractDynamicMetaObject, public QQmlRefCount +{ +public: + QQmlDelegateModelAttachedMetaObject( + QQmlDelegateModelItemMetaType *metaType, QMetaObject *metaObject); + ~QQmlDelegateModelAttachedMetaObject(); + + void objectDestroyed(QObject *) override; + int metaCall(QObject *, QMetaObject::Call, int _id, void **) override; + +private: + QQmlDelegateModelItemMetaType * const metaType; + QMetaObject * const metaObject; + const int memberPropertyOffset; + const int indexPropertyOffset; +}; + +QT_END_NAMESPACE + +#endif diff --git a/src/qmlmodels/qqmlinstantiator.cpp b/src/qmlmodels/qqmlinstantiator.cpp new file mode 100644 index 0000000000..af1b526e1d --- /dev/null +++ b/src/qmlmodels/qqmlinstantiator.cpp @@ -0,0 +1,509 @@ +/**************************************************************************** +** +** Copyright (C) 2016 Research In Motion. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtQml module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "qqmlinstantiator_p.h" +#include "qqmlinstantiator_p_p.h" +#include <QtQml/QQmlContext> +#include <QtQml/QQmlComponent> +#include <QtQml/QQmlInfo> +#include <QtQml/QQmlError> +#include <QtQmlModels/private/qqmlobjectmodel_p.h> +#if QT_CONFIG(qml_delegate_model) +#include <QtQmlModels/private/qqmldelegatemodel_p.h> +#endif + +QT_BEGIN_NAMESPACE + +QQmlInstantiatorPrivate::QQmlInstantiatorPrivate() + : componentComplete(true) + , effectiveReset(false) + , active(true) + , async(false) +#if QT_CONFIG(qml_delegate_model) + , ownModel(false) +#endif + , requestedIndex(-1) + , model(QVariant(1)) + , instanceModel(nullptr) + , delegate(nullptr) +{ +} + +QQmlInstantiatorPrivate::~QQmlInstantiatorPrivate() +{ + qDeleteAll(objects); +} + +void QQmlInstantiatorPrivate::clear() +{ + Q_Q(QQmlInstantiator); + if (!instanceModel) + return; + if (!objects.count()) + return; + + for (int i=0; i < objects.count(); i++) { + q->objectRemoved(i, objects[i]); + instanceModel->release(objects[i]); + } + objects.clear(); + q->objectChanged(); +} + +QObject *QQmlInstantiatorPrivate::modelObject(int index, bool async) +{ + requestedIndex = index; + QObject *o = instanceModel->object(index, async ? QQmlIncubator::Asynchronous : QQmlIncubator::AsynchronousIfNested); + requestedIndex = -1; + return o; +} + + +void QQmlInstantiatorPrivate::regenerate() +{ + Q_Q(QQmlInstantiator); + if (!componentComplete) + return; + + int prevCount = q->count(); + + clear(); + + if (!active || !instanceModel || !instanceModel->count() || !instanceModel->isValid()) { + if (prevCount) + q->countChanged(); + return; + } + + for (int i = 0; i < instanceModel->count(); i++) { + QObject *object = modelObject(i, async); + // If the item was already created we won't get a createdItem + if (object) + _q_createdItem(i, object); + } + if (q->count() != prevCount) + q->countChanged(); +} + +void QQmlInstantiatorPrivate::_q_createdItem(int idx, QObject* item) +{ + Q_Q(QQmlInstantiator); + if (objects.contains(item)) //Case when it was created synchronously in regenerate + return; + if (requestedIndex != idx) // Asynchronous creation, reference the object + (void)instanceModel->object(idx); + item->setParent(q); + if (objects.size() < idx + 1) { + int modelCount = instanceModel->count(); + if (objects.capacity() < modelCount) + objects.reserve(modelCount); + objects.resize(idx + 1); + } + if (QObject *o = objects.at(idx)) + instanceModel->release(o); + objects.replace(idx, item); + if (objects.count() == 1) + q->objectChanged(); + q->objectAdded(idx, item); +} + +void QQmlInstantiatorPrivate::_q_modelUpdated(const QQmlChangeSet &changeSet, bool reset) +{ + Q_Q(QQmlInstantiator); + + if (!componentComplete || effectiveReset) + return; + + if (reset) { + regenerate(); + if (changeSet.difference() != 0) + q->countChanged(); + return; + } + + int difference = 0; + QHash<int, QVector<QPointer<QObject> > > moved; + const QVector<QQmlChangeSet::Change> &removes = changeSet.removes(); + for (const QQmlChangeSet::Change &remove : removes) { + int index = qMin(remove.index, objects.count()); + int count = qMin(remove.index + remove.count, objects.count()) - index; + if (remove.isMove()) { + moved.insert(remove.moveId, objects.mid(index, count)); + objects.erase( + objects.begin() + index, + objects.begin() + index + count); + } else while (count--) { + QObject *obj = objects.at(index); + objects.remove(index); + q->objectRemoved(index, obj); + if (obj) + instanceModel->release(obj); + } + + difference -= remove.count; + } + + const QVector<QQmlChangeSet::Change> &inserts = changeSet.inserts(); + for (const QQmlChangeSet::Change &insert : inserts) { + int index = qMin(insert.index, objects.count()); + if (insert.isMove()) { + QVector<QPointer<QObject> > movedObjects = moved.value(insert.moveId); + objects = objects.mid(0, index) + movedObjects + objects.mid(index); + } else { + if (insert.index <= objects.size()) + objects.insert(insert.index, insert.count, nullptr); + for (int i = 0; i < insert.count; ++i) { + int modelIndex = index + i; + QObject* obj = modelObject(modelIndex, async); + if (obj) + _q_createdItem(modelIndex, obj); + } + } + difference += insert.count; + } + + if (difference != 0) + q->countChanged(); +} + +#if QT_CONFIG(qml_delegate_model) +void QQmlInstantiatorPrivate::makeModel() +{ + Q_Q(QQmlInstantiator); + QQmlDelegateModel* delegateModel = new QQmlDelegateModel(qmlContext(q), q); + instanceModel = delegateModel; + ownModel = true; + delegateModel->setDelegate(delegate); + delegateModel->classBegin(); //Pretend it was made in QML + if (componentComplete) + delegateModel->componentComplete(); +} +#endif + + +/*! + \qmltype Instantiator + \instantiates QQmlInstantiator + \inqmlmodule QtQml + \brief Dynamically creates objects. + + A Instantiator can be used to control the dynamic creation of objects, or to dynamically + create multiple objects from a template. + + The Instantiator element will manage the objects it creates. Those objects are parented to the + Instantiator and can also be deleted by the Instantiator if the Instantiator's properties change. Objects + can also be destroyed dynamically through other means, and the Instantiator will not recreate + them unless the properties of the Instantiator change. + +*/ +QQmlInstantiator::QQmlInstantiator(QObject *parent) + : QObject(*(new QQmlInstantiatorPrivate), parent) +{ +} + +QQmlInstantiator::~QQmlInstantiator() +{ +} + +/*! + \qmlsignal QtQml::Instantiator::objectAdded(int index, QtObject object) + + This signal is emitted when an object is added to the Instantiator. The \a index + parameter holds the index which the object has been given, and the \a object + parameter holds the \l QtObject that has been added. + + The corresponding handler is \c onObjectAdded. +*/ + +/*! + \qmlsignal QtQml::Instantiator::objectRemoved(int index, QtObject object) + + This signal is emitted when an object is removed from the Instantiator. The \a index + parameter holds the index which the object had been given, and the \a object + parameter holds the \l QtObject that has been removed. + + Do not keep a reference to \a object if it was created by this Instantiator, as + in these cases it will be deleted shortly after the signal is handled. + + The corresponding handler is \c onObjectRemoved. +*/ +/*! + \qmlproperty bool QtQml::Instantiator::active + + When active is true, and the delegate component is ready, the Instantiator will + create objects according to the model. When active is false, no objects + will be created and any previously created objects will be destroyed. + + Default is true. +*/ +bool QQmlInstantiator::isActive() const +{ + Q_D(const QQmlInstantiator); + return d->active; +} + +void QQmlInstantiator::setActive(bool newVal) +{ + Q_D(QQmlInstantiator); + if (newVal == d->active) + return; + d->active = newVal; + emit activeChanged(); + d->regenerate(); +} + +/*! + \qmlproperty bool QtQml::Instantiator::asynchronous + + When asynchronous is true the Instantiator will attempt to create objects + asynchronously. This means that objects may not be available immediately, + even if active is set to true. + + You can use the objectAdded signal to respond to items being created. + + Default is false. +*/ +bool QQmlInstantiator::isAsync() const +{ + Q_D(const QQmlInstantiator); + return d->async; +} + +void QQmlInstantiator::setAsync(bool newVal) +{ + Q_D(QQmlInstantiator); + if (newVal == d->async) + return; + d->async = newVal; + emit asynchronousChanged(); +} + + +/*! + \qmlproperty int QtQml::Instantiator::count + + The number of objects the Instantiator is currently managing. +*/ + +int QQmlInstantiator::count() const +{ + Q_D(const QQmlInstantiator); + return d->objects.count(); +} + +/*! + \qmlproperty QtQml::Component QtQml::Instantiator::delegate + \default + + The component used to create all objects. + + Note that an extra variable, index, will be available inside instances of the + delegate. This variable refers to the index of the instance inside the Instantiator, + and can be used to obtain the object through the objectAt method of the Instantiator. + + If this property is changed, all instances using the old delegate will be destroyed + and new instances will be created using the new delegate. +*/ +QQmlComponent* QQmlInstantiator::delegate() +{ + Q_D(QQmlInstantiator); + return d->delegate; +} + +void QQmlInstantiator::setDelegate(QQmlComponent* c) +{ + Q_D(QQmlInstantiator); + if (c == d->delegate) + return; + + d->delegate = c; + emit delegateChanged(); + +#if QT_CONFIG(qml_delegate_model) + if (!d->ownModel) + return; + + if (QQmlDelegateModel *dModel = qobject_cast<QQmlDelegateModel*>(d->instanceModel)) + dModel->setDelegate(c); + if (d->componentComplete) + d->regenerate(); +#endif +} + +/*! + \qmlproperty variant QtQml::Instantiator::model + + This property can be set to any of the supported \l {qml-data-models}{data models}: + + \list + \li A number that indicates the number of delegates to be created by the repeater + \li A model (e.g. a ListModel item, or a QAbstractItemModel subclass) + \li A string list + \li An object list + \endlist + + The type of model affects the properties that are exposed to the \l delegate. + + Default value is 1, which creates a single delegate instance. + + \sa {qml-data-models}{Data Models} +*/ + +QVariant QQmlInstantiator::model() const +{ + Q_D(const QQmlInstantiator); + return d->model; +} + +void QQmlInstantiator::setModel(const QVariant &v) +{ + Q_D(QQmlInstantiator); + if (d->model == v) + return; + + d->model = v; + //Don't actually set model until componentComplete in case it wants to create its delegates immediately + if (!d->componentComplete) + return; + + QQmlInstanceModel *prevModel = d->instanceModel; + QObject *object = qvariant_cast<QObject*>(v); + QQmlInstanceModel *vim = nullptr; + if (object && (vim = qobject_cast<QQmlInstanceModel *>(object))) { +#if QT_CONFIG(qml_delegate_model) + if (d->ownModel) { + delete d->instanceModel; + prevModel = nullptr; + d->ownModel = false; + } +#endif + d->instanceModel = vim; +#if QT_CONFIG(qml_delegate_model) + } else if (v != QVariant(0)){ + if (!d->ownModel) + d->makeModel(); + + if (QQmlDelegateModel *dataModel = qobject_cast<QQmlDelegateModel *>(d->instanceModel)) { + d->effectiveReset = true; + dataModel->setModel(v); + d->effectiveReset = false; + } +#endif + } + + if (d->instanceModel != prevModel) { + if (prevModel) { + disconnect(prevModel, SIGNAL(modelUpdated(QQmlChangeSet,bool)), + this, SLOT(_q_modelUpdated(QQmlChangeSet,bool))); + disconnect(prevModel, SIGNAL(createdItem(int,QObject*)), this, SLOT(_q_createdItem(int,QObject*))); + //disconnect(prevModel, SIGNAL(initItem(int,QObject*)), this, SLOT(initItem(int,QObject*))); + } + + if (d->instanceModel) { + connect(d->instanceModel, SIGNAL(modelUpdated(QQmlChangeSet,bool)), + this, SLOT(_q_modelUpdated(QQmlChangeSet,bool))); + connect(d->instanceModel, SIGNAL(createdItem(int,QObject*)), this, SLOT(_q_createdItem(int,QObject*))); + //connect(d->instanceModel, SIGNAL(initItem(int,QObject*)), this, SLOT(initItem(int,QObject*))); + } + } + + d->regenerate(); + emit modelChanged(); +} + +/*! + \qmlproperty QtObject QtQml::Instantiator::object + + This is a reference to the first created object, intended as a convenience + for the case where only one object has been created. +*/ +QObject *QQmlInstantiator::object() const +{ + Q_D(const QQmlInstantiator); + if (d->objects.count()) + return d->objects[0]; + return nullptr; +} + +/*! + \qmlmethod QtObject QtQml::Instantiator::objectAt(int index) + + Returns a reference to the object with the given \a index. +*/ +QObject *QQmlInstantiator::objectAt(int index) const +{ + Q_D(const QQmlInstantiator); + if (index >= 0 && index < d->objects.count()) + return d->objects[index]; + return nullptr; +} + +/*! + \internal +*/ +void QQmlInstantiator::classBegin() +{ + Q_D(QQmlInstantiator); + d->componentComplete = false; +} + +/*! + \internal +*/ +void QQmlInstantiator::componentComplete() +{ + Q_D(QQmlInstantiator); + d->componentComplete = true; +#if QT_CONFIG(qml_delegate_model) + if (d->ownModel) { + static_cast<QQmlDelegateModel*>(d->instanceModel)->componentComplete(); + d->regenerate(); + } else +#endif + { + QVariant realModel = d->model; + d->model = QVariant(0); + setModel(realModel); //If realModel == d->model this won't do anything, but that's fine since the model's 0 + //setModel calls regenerate + } +} + +QT_END_NAMESPACE + +#include "moc_qqmlinstantiator_p.cpp" diff --git a/src/qmlmodels/qqmlinstantiator_p.h b/src/qmlmodels/qqmlinstantiator_p.h new file mode 100644 index 0000000000..8b00a1e033 --- /dev/null +++ b/src/qmlmodels/qqmlinstantiator_p.h @@ -0,0 +1,119 @@ +/**************************************************************************** +** +** Copyright (C) 2016 Research In Motion. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtQml module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QQMLINSTANTIATOR_P_H +#define QQMLINSTANTIATOR_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <QtQml/qqmlcomponent.h> +#include <QtQml/qqmlparserstatus.h> +#include <QtQmlModels/private/qtqmlmodelsglobal_p.h> + +QT_BEGIN_NAMESPACE + +class QQmlInstantiatorPrivate; +class Q_QMLMODELS_PRIVATE_EXPORT QQmlInstantiator : public QObject, public QQmlParserStatus +{ + Q_OBJECT + Q_INTERFACES(QQmlParserStatus) + + Q_PROPERTY(bool active READ isActive WRITE setActive NOTIFY activeChanged) + Q_PROPERTY(bool asynchronous READ isAsync WRITE setAsync NOTIFY asynchronousChanged) + Q_PROPERTY(QVariant model READ model WRITE setModel NOTIFY modelChanged) + Q_PROPERTY(int count READ count NOTIFY countChanged) + Q_PROPERTY(QQmlComponent *delegate READ delegate WRITE setDelegate NOTIFY delegateChanged) + Q_PROPERTY(QObject *object READ object NOTIFY objectChanged) + Q_CLASSINFO("DefaultProperty", "delegate") + +public: + QQmlInstantiator(QObject *parent = nullptr); + ~QQmlInstantiator(); + + bool isActive() const; + void setActive(bool newVal); + + bool isAsync() const; + void setAsync(bool newVal); + + int count() const; + + QQmlComponent* delegate(); + void setDelegate(QQmlComponent* c); + + QVariant model() const; + void setModel(const QVariant &v); + + QObject *object() const; + + Q_INVOKABLE QObject *objectAt(int index) const; + + void classBegin() override; + void componentComplete() override; + +Q_SIGNALS: + void modelChanged(); + void delegateChanged(); + void countChanged(); + void objectChanged(); + void activeChanged(); + void asynchronousChanged(); + + void objectAdded(int index, QObject* object); + void objectRemoved(int index, QObject* object); + +private: + Q_DISABLE_COPY(QQmlInstantiator) + Q_DECLARE_PRIVATE(QQmlInstantiator) + Q_PRIVATE_SLOT(d_func(), void _q_createdItem(int, QObject *)) + Q_PRIVATE_SLOT(d_func(), void _q_modelUpdated(const QQmlChangeSet &, bool)) +}; + +QT_END_NAMESPACE + +#endif // QQMLCREATOR_P_H diff --git a/src/qmlmodels/qqmlinstantiator_p_p.h b/src/qmlmodels/qqmlinstantiator_p_p.h new file mode 100644 index 0000000000..bf153d8723 --- /dev/null +++ b/src/qmlmodels/qqmlinstantiator_p_p.h @@ -0,0 +1,98 @@ +/**************************************************************************** +** +** Copyright (C) 2016 Research In Motion. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtQml module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QQMLINSTANTIATOR_P_P_H +#define QQMLINSTANTIATOR_P_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include "qqmlinstantiator_p.h" +#include <QObject> +#include <private/qobject_p.h> +#include <private/qqmlchangeset_p.h> +#include <private/qqmlobjectmodel_p.h> + +QT_BEGIN_NAMESPACE + +class Q_QMLMODELS_PRIVATE_EXPORT QQmlInstantiatorPrivate : public QObjectPrivate +{ + Q_DECLARE_PUBLIC(QQmlInstantiator) + +public: + QQmlInstantiatorPrivate(); + ~QQmlInstantiatorPrivate(); + + void clear(); + void regenerate(); +#if QT_CONFIG(qml_delegate_model) + void makeModel(); +#endif + void _q_createdItem(int, QObject *); + void _q_modelUpdated(const QQmlChangeSet &, bool); + QObject *modelObject(int index, bool async); + + static QQmlInstantiatorPrivate *get(QQmlInstantiator *instantiator) { return instantiator->d_func(); } + static const QQmlInstantiatorPrivate *get(const QQmlInstantiator *instantiator) { return instantiator->d_func(); } + + bool componentComplete:1; + bool effectiveReset:1; + bool active:1; + bool async:1; +#if QT_CONFIG(qml_delegate_model) + bool ownModel:1; +#endif + int requestedIndex; + QVariant model; + QQmlInstanceModel *instanceModel; + QQmlComponent *delegate; + QVector<QPointer<QObject> > objects; +}; + +QT_END_NAMESPACE + +#endif // QQMLCREATOR_P_P_H diff --git a/src/qmlmodels/qqmlitemmodels.qdoc b/src/qmlmodels/qqmlitemmodels.qdoc new file mode 100644 index 0000000000..f6e1b0b1b9 --- /dev/null +++ b/src/qmlmodels/qqmlitemmodels.qdoc @@ -0,0 +1,110 @@ +/**************************************************************************** +** +** Copyright (C) 2017 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the documentation of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:FDL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Free Documentation License Usage +** Alternatively, this file may be used under the terms of the GNU Free +** Documentation License version 1.3 as published by the Free Software +** Foundation and appearing in the file included in the packaging of +** this file. Please review the following information to ensure +** the GNU Free Documentation License version 1.3 requirements +** will be met: https://www.gnu.org/licenses/fdl-1.3.html. +** $QT_END_LICENSE$ +** +****************************************************************************/ + +/*! + \page qmodelindex-and-related-classes-in-qml.html + \title QModelIndex and related Classes in QML + + Since Qt 5.5, QModelIndex and QPersistentModelIndex are exposed in QML as + value-based types. Also exposed in a similar fashion are QModelIndexList, + QItemSelectionRange and QItemSelection. All objects from these types can + be passed back and forth between QML and C++ as \c var properties or plain + JavaScript variables. + + Below you will find an overview of the API exposed to QML for these classes. + For more information, refer to their C++ documentation. + + \note Since all these types are exposed as \l{Q_GADGET}{gadgets}, there are no property + change notification signals emitted. Therefore binding to their properties + may not give the expected results. This is especially true for QPersistentModelIndex. + + \section1 QModelIndex and QPersistentModelIndex Types + + \list + \li \b row : int + \li \b column : int + \li \b parent : QModelIndex + \li \b valid : bool + \li \b model : QAbstractItemModel + \li \b internalId : quint64 + \endlist + + All these properties are read-only, as are their C++ counterparts. + + \note The usual caveats apply to QModelIndex in QML. If the underlying model changes + or gets deleted, it may become dangerous to access its properties. Therefore, you + should not store any QModelIndex objects. You can, however, store QPersistentModelIndexe + objects in a safe way. + + \section1 QModelIndexList Type + + \l QModelIndexList is exposed in QML as a JavaScript array. Conversions are + automatically made from and to C++. In fact, any JavaScript array can be + converted back to QModelIndexList, with non-QModelIndex objects replaced by + invalid \l{QModelIndex}es. + + \note QModelIndex to QPersistentModelIndex conversion happens when accessing + the array elements because any QModelIndexList property retains reference + semantics when exposed this way. + + \section1 \l QItemSelectionRange Type + + \list + \li \b top : int + \li \b left : int + \li \b bottom : int + \li \b right : int + \li \b width : int + \li \b height : int + \li \b topLeft : QPersistentModelIndex + \li \b bottomRight : QPersistentModelIndex + \li \b parent : QModelIndex + \li \b valid : bool + \li \b empty : bool + \li \b model : QAbstractItemModel + \endlist + + All these properties are read-only, as are their C++ counterparts. In addition, + we also expose the following functions: + + \list + \li bool \b{contains}(QModelIndex \e index) + \li bool \b{contains}(int \e row, int \e column, QModelIndex \e parentIndex) + \li bool \b{intersects}(QItemSelectionRange \e other) + \li QItemSelectionRange \b{intersected}(QItemSelectionRange \e other) + \endlist + + \section1 QItemSelection Type + + Similarly to QModelIndexList, \l QItemSelection is exposed in QML as a JavaScript + array of QItemSelectionRange objects. Conversions are automatically made from and to C++. + In fact, any JavaScript array can be converted back to QItemSelection, with + non-QItemSelectionRange objects replaced by empty \l {QItemSelectionRange}s. + + + \sa ItemSelectionModel +*/ diff --git a/src/qmlmodels/qqmlitemselectionmodel.qdoc b/src/qmlmodels/qqmlitemselectionmodel.qdoc new file mode 100644 index 0000000000..43da4f7a55 --- /dev/null +++ b/src/qmlmodels/qqmlitemselectionmodel.qdoc @@ -0,0 +1,239 @@ +/**************************************************************************** +** +** Copyright (C) 2017 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the documentation of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:FDL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Free Documentation License Usage +** Alternatively, this file may be used under the terms of the GNU Free +** Documentation License version 1.3 as published by the Free Software +** Foundation and appearing in the file included in the packaging of +** this file. Please review the following information to ensure +** the GNU Free Documentation License version 1.3 requirements +** will be met: https://www.gnu.org/licenses/fdl-1.3.html. +** $QT_END_LICENSE$ +** +****************************************************************************/ + +/*! + \qmltype ItemSelectionModel + \instantiates QItemSelectionModel + \inqmlmodule QtQml.Models + \since 5.5 + \ingroup qtquick-models + + \brief Instantiates a QItemSelectionModel to be used in conjunction + with a QAbstractItemModel and any view supporting it. + + \sa QItemSelectionModel, {Models and Views in Qt Quick} +*/ + + +/*! + \qmlproperty QAbstractItemModel ItemSelectionModel::model + + This property's value must match the view's model. +*/ + +/*! + \qmlproperty bool ItemSelectionModel::hasSelection + \readonly + + It will trigger property binding updates every time \l selectionChanged() + is emitted, even though its value hasn't changed. + + \sa selection, selectedIndexes, select(), selectionChanged() +*/ + +/*! + \qmlproperty QModelIndex ItemSelectionModel::currentIndex + \readonly + + Use \l setCurrentIndex() to set its value. + + \sa setCurrentIndex(), currentChanged() +*/ + +/*! + \qmlproperty QModelIndexList ItemSelectionModel::selectedIndexes + \readonly + + Contains the list of all the indexes in the selection model. +*/ + +/*! + \qmlmethod bool ItemSelectionModel::isSelected(QModelIndex index) + + Returns \c true if the given model item \a index is selected. +*/ + +/*! + \qmlmethod bool ItemSelectionModel::isRowSelected(int row, QModelIndex parent) + + Returns \c true if all items are selected in the \a row with the given + \a parent. + + Note that this function is usually faster than calling isSelected() + on all items in the same row, and that unselectable items are ignored. +*/ + +/*! + \qmlmethod bool ItemSelectionModel::isColumnSelected(int column, QModelIndex parent) + + Returns \c true if all items are selected in the \a column with the given + \a parent. + + Note that this function is usually faster than calling isSelected() + on all items in the same column, and that unselectable items are ignored. +*/ + +/*! + \qmlmethod bool ItemSelectionModel::rowIntersectsSelection(int row, QModelIndex parent) + + Returns \c true if there are any items selected in the \a row with the + given \a parent. +*/ + +/*! + \qmlmethod bool ItemSelectionModel::columnIntersectsSelection(int column, QModelIndex parent) + + Returns \c true if there are any items selected in the \a column with the + given \a parent. +*/ + +/*! + \qmlmethod QModelIndexList ItemSelectionModel::selectedRows(int column) + + Returns the indexes in the given \a column for the rows where all columns + are selected. + + \sa selectedColumns() +*/ + +/*! + \qmlmethod QModelIndexList ItemSelectionModel::selectedColumns(int row) + + Returns the indexes in the given \a row for columns where all rows are + selected. + + \sa selectedRows() +*/ + +/*! + \qmlproperty object ItemSelectionModel::selection + \readonly + + Holds the selection ranges stored in the selection model. +*/ + +/*! + \qmlmethod void ItemSelectionModel::setCurrentIndex(QModelIndex index, SelectionFlags command) + + Sets the model item \a index to be the current item, and emits + currentChanged(). The current item is used for keyboard navigation and + focus indication; it is independent of any selected items, although a + selected item can also be the current item. + + Depending on the specified \a command, the \a index can also become part + of the current selection. + + Valid \a command values are described in \l {itemselectionmodelselectindex} + {select(\e index, \e command)}. + + \sa select() +*/ + +/*! + \qmlmethod void ItemSelectionModel::select(QModelIndex index, SelectionFlags command) + \keyword itemselectionmodelselectindex + + Selects the model item \a index using the specified \a command, and emits + selectionChanged(). + + Valid values for the \a command parameter, are: + + \value NoUpdate No selection will be made. + \value Clear The complete selection will be cleared. + \value Select All specified indexes will be selected. + \value Deselect All specified indexes will be deselected. + \value Toggle All specified indexes will be selected or + deselected depending on their current state. + \value Current The current selection will be updated. + \value Rows All indexes will be expanded to span rows. + \value Columns All indexes will be expanded to span columns. + \value SelectCurrent A combination of Select and Current, provided for + convenience. + \value ToggleCurrent A combination of Toggle and Current, provided for + convenience. + \value ClearAndSelect A combination of Clear and Select, provided for + convenience. +*/ + +/*! + \qmlmethod void ItemSelectionModel::select(QItemSelection selection, SelectionFlags command) + + Selects the item \a selection using the specified \a command, and emits + selectionChanged(). + + Valid \a command values are described in \l {itemselectionmodelselectindex} + {select(\e index, \e command)}. +*/ + +/*! + \qmlmethod void ItemSelectionModel::clear() + + Clears the selection model. Emits selectionChanged() and currentChanged(). +*/ + +/*! + \qmlmethod void ItemSelectionModel::reset() + + Clears the selection model. Does not emit any signals. +*/ + +/*! + \qmlmethod void ItemSelectionModel::clearSelection() + + Clears the selection in the selection model. Emits selectionChanged(). +*/ + +/*! + \qmlmethod void ItemSelectionModel::clearCurrentIndex() + + Clears the current index. Emits currentChanged(). +*/ + +/*! + \qmlsignal ItemSelectionModel::selectionChanged(QItemSelection selected, QItemSelection deselected) + + This signal is emitted whenever the selection changes. The change in the + selection is represented as an item selection of \a deselected items and + an item selection of \a selected items. + + Note the that the current index changes independently from the selection. + Also note that this signal will not be emitted when the item model is reset. + + \sa select(), currentChanged() +*/ + +/*! + \qmlsignal ItemSelectionModel::currentChanged(QModelIndex current, QModelIndex previous) + + This signal is emitted whenever the current item changes. The \a previous + model item index is replaced by the \a current index as the selection's + current item. + + Note that this signal will not be emitted when the item model is reset. + + \sa currentIndex, setCurrentIndex(), selectionChanged() +*/ diff --git a/src/qmlmodels/qqmllistaccessor.cpp b/src/qmlmodels/qqmllistaccessor.cpp new file mode 100644 index 0000000000..46a11e2bc2 --- /dev/null +++ b/src/qmlmodels/qqmllistaccessor.cpp @@ -0,0 +1,160 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtQml module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "qqmllistaccessor_p.h" + +#include <private/qqmlmetatype_p.h> + +#include <QtCore/qstringlist.h> +#include <QtCore/qdebug.h> + +// ### Remove me +#include <private/qqmlengine_p.h> + +QT_BEGIN_NAMESPACE + +QQmlListAccessor::QQmlListAccessor() +: m_type(Invalid) +{ +} + +QQmlListAccessor::~QQmlListAccessor() +{ +} + +QVariant QQmlListAccessor::list() const +{ + return d; +} + +void QQmlListAccessor::setList(const QVariant &v, QQmlEngine *engine) +{ + d = v; + + // An incoming JS array as model is treated as a variant list, so we need to + // convert it first with toVariant(). + if (d.userType() == qMetaTypeId<QJSValue>()) + d = d.value<QJSValue>().toVariant(); + + QQmlEnginePrivate *enginePrivate = engine?QQmlEnginePrivate::get(engine):nullptr; + + if (!d.isValid()) { + m_type = Invalid; + } else if (d.userType() == QVariant::StringList) { + m_type = StringList; + } else if (d.userType() == QMetaType::QVariantList) { + m_type = VariantList; + } else if (d.canConvert(QVariant::Int)) { + // Here we have to check for an upper limit, because down the line code might (well, will) + // allocate memory depending on the number of elements. The upper limit cannot be INT_MAX: + // QVector<QPointer<QQuickItem>> something; + // something.resize(count()); + // (See e.g. QQuickRepeater::regenerate()) + // This will allocate data along the lines of: + // sizeof(QPointer<QQuickItem>) * count() + QVector::headerSize + // So, doing an approximate round-down-to-nice-number, we get: + const int upperLimit = 100 * 1000 * 1000; + + int i = v.toInt(); + if (i < 0) { + qWarning("Model size of %d is less than 0", i); + m_type = Invalid; + } else if (i > upperLimit) { + qWarning("Model size of %d is bigger than the upper limit %d", i, upperLimit); + m_type = Invalid; + } else { + m_type = Integer; + } + } else if ((!enginePrivate && QQmlMetaType::isQObject(d.userType())) || + (enginePrivate && enginePrivate->isQObject(d.userType()))) { + QObject *data = enginePrivate?enginePrivate->toQObject(d):QQmlMetaType::toQObject(d); + d = QVariant::fromValue(data); + m_type = Instance; + } else if (d.userType() == qMetaTypeId<QQmlListReference>()) { + m_type = ListProperty; + } else { + m_type = Instance; + } +} + +int QQmlListAccessor::count() const +{ + switch(m_type) { + case StringList: + return qvariant_cast<QStringList>(d).count(); + case VariantList: + return qvariant_cast<QVariantList>(d).count(); + case ListProperty: + return ((const QQmlListReference *)d.constData())->count(); + case Instance: + return 1; + case Integer: + return d.toInt(); + default: + case Invalid: + return 0; + } +} + +QVariant QQmlListAccessor::at(int idx) const +{ + Q_ASSERT(idx >= 0 && idx < count()); + switch(m_type) { + case StringList: + return QVariant::fromValue(qvariant_cast<QStringList>(d).at(idx)); + case VariantList: + return qvariant_cast<QVariantList>(d).at(idx); + case ListProperty: + return QVariant::fromValue(((const QQmlListReference *)d.constData())->at(idx)); + case Instance: + return d; + case Integer: + return QVariant(idx); + default: + case Invalid: + return QVariant(); + } +} + +bool QQmlListAccessor::isValid() const +{ + return m_type != Invalid; +} + +QT_END_NAMESPACE diff --git a/src/qmlmodels/qqmllistaccessor_p.h b/src/qmlmodels/qqmllistaccessor_p.h new file mode 100644 index 0000000000..bcd079adef --- /dev/null +++ b/src/qmlmodels/qqmllistaccessor_p.h @@ -0,0 +1,83 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtQml module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QQMLLISTACCESSOR_H +#define QQMLLISTACCESSOR_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <QtCore/QVariant> + +QT_BEGIN_NAMESPACE + +class QQmlEngine; +class Q_AUTOTEST_EXPORT QQmlListAccessor +{ +public: + QQmlListAccessor(); + ~QQmlListAccessor(); + + QVariant list() const; + void setList(const QVariant &, QQmlEngine * = nullptr); + + bool isValid() const; + + int count() const; + QVariant at(int) const; + + enum Type { Invalid, StringList, VariantList, ListProperty, Instance, Integer }; + Type type() const { return m_type; } + +private: + Type m_type; + QVariant d; +}; + +QT_END_NAMESPACE + +#endif // QQMLLISTACCESSOR_H diff --git a/src/qmlmodels/qqmllistcompositor.cpp b/src/qmlmodels/qqmllistcompositor.cpp new file mode 100644 index 0000000000..921e86f355 --- /dev/null +++ b/src/qmlmodels/qqmllistcompositor.cpp @@ -0,0 +1,1482 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtQml module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "qqmllistcompositor_p.h" + +#include <QtCore/qvarlengtharray.h> + +//#define QT_QML_VERIFY_MINIMAL +//#define QT_QML_VERIFY_INTEGRITY + +QT_BEGIN_NAMESPACE + +/*! + \class QQmlListCompositor + \brief The QQmlListCompositor class provides a lookup table for filtered, or re-ordered list + indexes. + \internal + + QQmlListCompositor is intended as an aid for developing proxy models. It doesn't however + directly proxy a list or model, instead a range of indexes from one or many lists can be + inserted into the compositor and then categorized and shuffled around and it will manage the + task of translating from an index in the combined space into an index in a particular list. + + Within a compositor indexes are categorized into groups where a group is a sub-set of the + total indexes referenced by the compositor, each with an address space ranging from 0 to + the number of indexes in the group - 1. Group memberships are independent of each other with + the one exception that items always retain the same order so if an index is moved within a + group, its position in other groups will change as well. + + The iterator classes encapsulate information about a specific position in a compositor group. + This includes a source list, the index of an item within that list and the groups that item + is a member of. The iterator for a specific position in a group can be retrieved with the + find() function and the addition and subtraction operators of the iterators can be used to + navigate to adjacent items in the same group. + + Items can be added to the compositor with the append() and insert() functions, group + membership can be changed with the setFlags() and clearFlags() functions, and the position + of items in the compositor can be changed with the move() function. Each of these functions + optionally returns a list of the changes made to indexes within each group which can then + be propagated to view so that it can correctly refresh its contents; e.g. 3 items + removed at index 6, and 5 items inserted at index 1. The notification changes are always + ordered from the start of the list to the end and accumulate, so if 5 items are removed at + index 4, one is skipped and then 3 move are removed, the changes returned are 5 items removed + at index 4, followed by 3 items removed at index 4. + + When the contents of a source list change, the mappings within the compositor can be updated + with the listItemsInserted(), listItemsRemoved(), listItemsMoved(), and listItemsChanged() + functions. Like the direct manipulation functions these too return a list of group indexes + affected by the change. If items are removed from a source list they are also removed from + any groups they belong to with the one exception being items belonging to the \l Cache group. + When items belonging to this group are removed the list, index, and other group membership + information are discarded but Cache membership is retained until explicitly removed. This + allows the cache index to be retained until cached resources for that item are actually + released. + + Internally the index mapping is stored as a list of Range objects, each has a list identifier, + a start index, a count, and a set of flags which represent group membership and some other + properties. The group index of a range is the sum of all preceding ranges that are members of + that group. To avoid the inefficiency of iterating over potentially all ranges when looking + for a specific index, each time a lookup is done the range and its indexes are cached and the + next lookup is done relative to this. This works out to near constant time in most relevant + use cases because successive index lookups are most frequently adjacent. The total number of + ranges is often quite small, which helps as well. If there is a need for faster random access + then a skip list like index may be an appropriate addition. + + \sa DelegateModel +*/ + +#ifdef QT_QML_VERIFY_MINIMAL +#define QT_QML_VERIFY_INTEGRITY +/* + Diagnostic to verify there are no consecutive ranges, or that the compositor contains the + most compact representation possible. + + Returns false and prints a warning if any range has a starting index equal to the end + (index + count) index of the previous range, and both ranges also have the same flags and list + property. + + If there are no consecutive ranges this will return true. +*/ + +static bool qt_verifyMinimal( + const QQmlListCompositor::iterator &begin, + const QQmlListCompositor::iterator &end) +{ + bool minimal = true; + int index = 0; + + for (const QQmlListCompositor::Range *range = begin->next; range != *end; range = range->next, ++index) { + if (range->previous->list == range->list + && range->previous->flags == (range->flags & ~QQmlListCompositor::AppendFlag) + && range->previous->end() == range->index) { + qWarning() << index << "Consecutive ranges"; + qWarning() << *range->previous; + qWarning() << *range; + minimal = false; + } + } + + return minimal; +} + +#endif + +#ifdef QT_QML_VERIFY_INTEGRITY +static bool qt_printInfo(const QQmlListCompositor &compositor) +{ + qWarning() << compositor; + return true; +} + +/* + Diagnostic to verify the integrity of a compositor. + + Per range this verifies there are no invalid range combinations, that non-append ranges have + positive non-zero counts, and that list ranges have non-negative indexes. + + Accumulatively this verifies that the cached total group counts match the sum of counts + of member ranges. +*/ + +static bool qt_verifyIntegrity( + const QQmlListCompositor::iterator &begin, + const QQmlListCompositor::iterator &end, + const QQmlListCompositor::iterator &cachedIt) +{ + bool valid = true; + + int index = 0; + QQmlListCompositor::iterator it; + for (it = begin; *it != *end; *it = it->next) { + if (it->count == 0 && !it->append()) { + qWarning() << index << "Empty non-append range"; + valid = false; + } + if (it->count < 0) { + qWarning() << index << "Negative count"; + valid = false; + } + if (it->list && it->flags != QQmlListCompositor::CacheFlag && it->index < 0) { + qWarning() << index <<"Negative index"; + valid = false; + } + if (it->previous->next != it.range) { + qWarning() << index << "broken list: it->previous->next != it.range"; + valid = false; + } + if (it->next->previous != it.range) { + qWarning() << index << "broken list: it->next->previous != it.range"; + valid = false; + } + if (*it == *cachedIt) { + for (int i = 0; i < end.groupCount; ++i) { + int groupIndex = it.index[i]; + if (cachedIt->flags & (1 << i)) + groupIndex += cachedIt.offset; + if (groupIndex != cachedIt.index[i]) { + qWarning() << index + << "invalid cached index" + << QQmlListCompositor::Group(i) + << "Expected:" + << groupIndex + << "Actual" + << cachedIt.index[i] + << cachedIt; + valid = false; + } + } + } + it.incrementIndexes(it->count); + ++index; + } + + for (int i = 0; i < end.groupCount; ++i) { + if (end.index[i] != it.index[i]) { + qWarning() << "Group" << i << "count invalid. Expected:" << end.index[i] << "Actual:" << it.index[i]; + valid = false; + } + } + return valid; +} +#endif + +#if defined(QT_QML_VERIFY_MINIMAL) +# define QT_QML_VERIFY_LISTCOMPOSITOR Q_ASSERT(!(!(qt_verifyIntegrity(iterator(m_ranges.next, 0, Default, m_groupCount), m_end, m_cacheIt) \ + && qt_verifyMinimal(iterator(m_ranges.next, 0, Default, m_groupCount), m_end)) \ + && qt_printInfo(*this))); +#elif defined(QT_QML_VERIFY_INTEGRITY) +# define QT_QML_VERIFY_LISTCOMPOSITOR Q_ASSERT(!(!qt_verifyIntegrity(iterator(m_ranges.next, 0, Default, m_groupCount), m_end, m_cacheIt) \ + && qt_printInfo(*this))); +#else +# define QT_QML_VERIFY_LISTCOMPOSITOR +#endif + +//#define QT_QML_TRACE_LISTCOMPOSITOR(args) qDebug() << m_end.index[1] << m_end.index[0] << Q_FUNC_INFO args; +#define QT_QML_TRACE_LISTCOMPOSITOR(args) + +QQmlListCompositor::iterator &QQmlListCompositor::iterator::operator +=(int difference) +{ + // Update all indexes to the start of the range. + decrementIndexes(offset); + + // If the iterator group isn't a member of the current range ignore the current offset. + if (!(range->flags & groupFlag)) + offset = 0; + + offset += difference; + + // Iterate backwards looking for a range with a positive offset. + while (offset <= 0 && range->previous->flags) { + range = range->previous; + if (range->flags & groupFlag) + offset += range->count; + decrementIndexes(range->count); + } + + // Iterate forwards looking for the first range which contains both the offset and the + // iterator group. + while (range->flags && (offset >= range->count || !(range->flags & groupFlag))) { + if (range->flags & groupFlag) + offset -= range->count; + incrementIndexes(range->count); + range = range->next; + } + + // Update all the indexes to inclue the remaining offset. + incrementIndexes(offset); + + return *this; +} + +QQmlListCompositor::insert_iterator &QQmlListCompositor::insert_iterator::operator +=(int difference) +{ + iterator::operator +=(difference); + + // If the previous range contains the append flag move the iterator to the tail of the previous + // range so that appended appear after the insert position. + if (offset == 0 && range->previous->append()) { + range = range->previous; + offset = range->inGroup() ? range->count : 0; + } + + return *this; +} + + +/*! + Constructs an empty list compositor. +*/ + +QQmlListCompositor::QQmlListCompositor() + : m_end(m_ranges.next, 0, Default, 2) + , m_cacheIt(m_end) + , m_groupCount(2) + , m_defaultFlags(PrependFlag | DefaultFlag) + , m_removeFlags(AppendFlag | PrependFlag | GroupMask) + , m_moveId(0) +{ +} + +/*! + Destroys a list compositor. +*/ + +QQmlListCompositor::~QQmlListCompositor() +{ + for (Range *next, *range = m_ranges.next; range != &m_ranges; range = next) { + next = range->next; + delete range; + } +} + +/*! + Inserts a range with the given source \a list, start \a index, \a count and \a flags, in front + of the existing range \a before. +*/ + +inline QQmlListCompositor::Range *QQmlListCompositor::insert( + Range *before, void *list, int index, int count, uint flags) +{ + return new Range(before, list, index, count, flags); +} + +/*! + Erases a \a range from the compositor. + + Returns a pointer to the next range in the compositor. +*/ + +inline QQmlListCompositor::Range *QQmlListCompositor::erase( + Range *range) +{ + Range *next = range->next; + next->previous = range->previous; + next->previous->next = range->next; + delete range; + return next; +} + +/*! + Sets the number (\a count) of possible groups that items may belong to in a compositor. +*/ + +void QQmlListCompositor::setGroupCount(int count) +{ + m_groupCount = count; + m_end = iterator(&m_ranges, 0, Default, m_groupCount); + m_cacheIt = m_end; +} + +/*! + Returns the number of items that belong to a \a group. +*/ + +int QQmlListCompositor::count(Group group) const +{ + return m_end.index[group]; +} + +/*! + Returns an iterator representing the item at \a index in a \a group. + + The index must be between 0 and count(group) - 1. +*/ + +QQmlListCompositor::iterator QQmlListCompositor::find(Group group, int index) +{ + QT_QML_TRACE_LISTCOMPOSITOR(<< group << index) + Q_ASSERT(index >=0 && index < count(group)); + if (m_cacheIt == m_end) { + m_cacheIt = iterator(m_ranges.next, 0, group, m_groupCount); + m_cacheIt += index; + } else { + const int offset = index - m_cacheIt.index[group]; + m_cacheIt.setGroup(group); + m_cacheIt += offset; + } + Q_ASSERT(m_cacheIt.index[group] == index); + Q_ASSERT(m_cacheIt->inGroup(group)); + QT_QML_VERIFY_LISTCOMPOSITOR + return m_cacheIt; +} + +/*! + Returns an iterator representing the item at \a index in a \a group. + + The index must be between 0 and count(group) - 1. +*/ + +QQmlListCompositor::iterator QQmlListCompositor::find(Group group, int index) const +{ + return const_cast<QQmlListCompositor *>(this)->find(group, index); +} + +/*! + Returns an iterator representing an insert position in front of the item at \a index in a + \a group. + + The iterator for an insert position can sometimes resolve to a different Range than a regular + iterator. This is because when items are inserted on a boundary between Ranges, if the first + range has the Append flag set then the items should be inserted into that range to ensure + that the append position for the existing range remains after the insert position. + + The index must be between 0 and count(group) - 1. +*/ + +QQmlListCompositor::insert_iterator QQmlListCompositor::findInsertPosition(Group group, int index) +{ + QT_QML_TRACE_LISTCOMPOSITOR(<< group << index) + Q_ASSERT(index >=0 && index <= count(group)); + insert_iterator it; + if (m_cacheIt == m_end) { + it = iterator(m_ranges.next, 0, group, m_groupCount); + it += index; + } else { + const int offset = index - m_cacheIt.index[group]; + it = m_cacheIt; + it.setGroup(group); + it += offset; + } + Q_ASSERT(it.index[group] == index); + return it; +} + +/*! + Appends a range of \a count indexes starting at \a index from a \a list into a compositor + with the given \a flags. + + If supplied the \a inserts list will be populated with the positions of the inserted items + in each group. +*/ + +void QQmlListCompositor::append( + void *list, int index, int count, uint flags, QVector<Insert> *inserts) +{ + QT_QML_TRACE_LISTCOMPOSITOR(<< list << index << count << flags) + insert(m_end, list, index, count, flags, inserts); +} + +/*! + Inserts a range of \a count indexes starting at \a index from a \a list with the given \a flags + into a \a group at index \a before. + + If supplied the \a inserts list will be populated with the positions of items inserted into + each group. +*/ + +void QQmlListCompositor::insert( + Group group, int before, void *list, int index, int count, uint flags, QVector<Insert> *inserts) +{ + QT_QML_TRACE_LISTCOMPOSITOR(<< group << before << list << index << count << flags) + insert(findInsertPosition(group, before), list, index, count, flags, inserts); +} + +/*! + Inserts a range of \a count indexes starting at \a index from a \a list with the given \a flags + into a compositor at position \a before. + + If supplied the \a inserts list will be populated with the positions of items inserted into + each group. +*/ + +QQmlListCompositor::iterator QQmlListCompositor::insert( + iterator before, void *list, int index, int count, uint flags, QVector<Insert> *inserts) +{ + QT_QML_TRACE_LISTCOMPOSITOR(<< before << list << index << count << flags) + if (inserts) { + inserts->append(Insert(before, count, flags & GroupMask)); + } + if (before.offset > 0) { + // Inserting into the middle of a range. Split it two and update the iterator so it's + // positioned at the start of the second half. + *before = insert( + *before, before->list, before->index, before.offset, before->flags & ~AppendFlag)->next; + before->index += before.offset; + before->count -= before.offset; + before.offset = 0; + } + + + if (!(flags & AppendFlag) && *before != m_ranges.next + && before->previous->list == list + && before->previous->flags == flags + && (!list || before->previous->end() == index)) { + // The insert arguments represent a continuation of the previous range so increment + // its count instead of inserting a new range. + before->previous->count += count; + before.incrementIndexes(count, flags); + } else { + *before = insert(*before, list, index, count, flags); + before.offset = 0; + } + + if (!(flags & AppendFlag) && before->next != &m_ranges + && before->list == before->next->list + && before->flags == before->next->flags + && (!list || before->end() == before->next->index)) { + // The current range and the next are continuous so add their counts and delete one. + before->next->index = before->index; + before->next->count += before->count; + *before = erase(*before); + } + + m_end.incrementIndexes(count, flags); + m_cacheIt = before; + QT_QML_VERIFY_LISTCOMPOSITOR + return before; +} + +/*! + Sets the given flags \a flags on \a count items belonging to \a group starting at the position + identified by \a fromGroup and the index \a from. + + If supplied the \a inserts list will be populated with insert notifications for affected groups. +*/ + +void QQmlListCompositor::setFlags( + Group fromGroup, int from, int count, Group group, int flags, QVector<Insert> *inserts) +{ + QT_QML_TRACE_LISTCOMPOSITOR(<< fromGroup << from << count << group << flags) + setFlags(find(fromGroup, from), count, group, flags, inserts); +} + +/*! + Sets the given flags \a flags on \a count items belonging to \a group starting at the position + \a from. + + If supplied the \a inserts list will be populated with insert notifications for affected groups. +*/ + +void QQmlListCompositor::setFlags( + iterator from, int count, Group group, uint flags, QVector<Insert> *inserts) +{ + QT_QML_TRACE_LISTCOMPOSITOR(<< from << count << flags) + if (!flags || !count) + return; + + if (from != group) { + // Skip to the next full range if the start one is not a member of the target group. + from.incrementIndexes(from->count - from.offset); + from.offset = 0; + *from = from->next; + } else if (from.offset > 0) { + // If the start position is mid range split off the portion unaffected. + *from = insert(*from, from->list, from->index, from.offset, from->flags & ~AppendFlag)->next; + from->index += from.offset; + from->count -= from.offset; + from.offset = 0; + } + + for (; count > 0; *from = from->next) { + if (from != from.group) { + // Skip ranges that are not members of the target group. + from.incrementIndexes(from->count); + continue; + } + // Find the number of items affected in the current range. + const int difference = qMin(count, from->count); + count -= difference; + + // Determine the actual changes made to the range and increment counts accordingly. + const uint insertFlags = ~from->flags & flags; + const uint setFlags = (from->flags | flags) & ~AppendFlag; + if (insertFlags && inserts) + inserts->append(Insert(from, difference, insertFlags | (from->flags & CacheFlag))); + m_end.incrementIndexes(difference, insertFlags); + from.incrementIndexes(difference, setFlags); + + if (from->previous != &m_ranges + && from->previous->list == from->list + && (!from->list || from->previous->end() == from->index) + && from->previous->flags == setFlags) { + // If the additional flags make the current range a continuation of the previous + // then move the affected items over to the previous range. + from->previous->count += difference; + from->index += difference; + from->count -= difference; + if (from->count == 0) { + // Delete the current range if it is now empty, preserving the append flag + // in the previous range. + if (from->append()) + from->previous->flags |= AppendFlag; + *from = erase(*from)->previous; + continue; + } else { + break; + } + } else if (!insertFlags) { + // No new flags, so roll onto the next range. + from.incrementIndexes(from->count - difference); + continue; + } else if (difference < from->count) { + // Create a new range with the updated flags, and remove the affected items + // from the current range. + *from = insert(*from, from->list, from->index, difference, setFlags)->next; + from->index += difference; + from->count -= difference; + } else { + // The whole range is affected so simply update the flags. + from->flags |= flags; + continue; + } + from.incrementIndexes(from->count); + } + + if (from->previous != &m_ranges + && from->previous->list == from->list + && (!from->list || from->previous->end() == from->index) + && from->previous->flags == (from->flags & ~AppendFlag)) { + // If the following range is now a continuation, merge it with its previous range. + from.offset = from->previous->count; + from->previous->count += from->count; + from->previous->flags = from->flags; + *from = erase(*from)->previous; + } + m_cacheIt = from; + QT_QML_VERIFY_LISTCOMPOSITOR +} + +/*! + Clears the given flags \a flags on \a count items belonging to \a group starting at the position + \a from. + + If supplied the \a removes list will be populated with remove notifications for affected groups. +*/ + +void QQmlListCompositor::clearFlags( + Group fromGroup, int from, int count, Group group, uint flags, QVector<Remove> *removes) +{ + QT_QML_TRACE_LISTCOMPOSITOR(<< fromGroup << from << count << group << flags) + clearFlags(find(fromGroup, from), count, group, flags, removes); +} + +/*! + Clears the given flags \a flags on \a count items belonging to \a group starting at the position + identified by \a fromGroup and the index \a from. + + If supplied the \a removes list will be populated with remove notifications for affected groups. +*/ + +void QQmlListCompositor::clearFlags( + iterator from, int count, Group group, uint flags, QVector<Remove> *removes) +{ + QT_QML_TRACE_LISTCOMPOSITOR(<< from << count << flags) + if (!flags || !count) + return; + + const bool clearCache = flags & CacheFlag; + + if (from != group) { + // Skip to the next full range if the start one is not a member of the target group. + from.incrementIndexes(from->count - from.offset); + from.offset = 0; + *from = from->next; + } else if (from.offset > 0) { + // If the start position is mid range split off the portion unaffected. + *from = insert(*from, from->list, from->index, from.offset, from->flags & ~AppendFlag)->next; + from->index += from.offset; + from->count -= from.offset; + from.offset = 0; + } + + for (; count > 0; *from = from->next) { + if (from != group) { + // Skip ranges that are not members of the target group. + from.incrementIndexes(from->count); + continue; + } + // Find the number of items affected in the current range. + const int difference = qMin(count, from->count); + count -= difference; + + + // Determine the actual changes made to the range and decrement counts accordingly. + const uint removeFlags = from->flags & flags & ~(AppendFlag | PrependFlag); + const uint clearedFlags = from->flags & ~(flags | AppendFlag | UnresolvedFlag); + if (removeFlags && removes) { + const int maskedFlags = clearCache + ? (removeFlags & ~CacheFlag) + : (removeFlags | (from->flags & CacheFlag)); + if (maskedFlags) + removes->append(Remove(from, difference, maskedFlags)); + } + m_end.decrementIndexes(difference, removeFlags); + from.incrementIndexes(difference, clearedFlags); + + if (from->previous != &m_ranges + && from->previous->list == from->list + && (!from->list || clearedFlags == CacheFlag || from->previous->end() == from->index) + && from->previous->flags == clearedFlags) { + // If the removed flags make the current range a continuation of the previous + // then move the affected items over to the previous range. + from->previous->count += difference; + from->index += difference; + from->count -= difference; + if (from->count == 0) { + // Delete the current range if it is now empty, preserving the append flag + if (from->append()) + from->previous->flags |= AppendFlag; + *from = erase(*from)->previous; + } else { + from.incrementIndexes(from->count); + } + } else if (difference < from->count) { + // Create a new range with the reduced flags, and remove the affected items from + // the current range. + if (clearedFlags) + *from = insert(*from, from->list, from->index, difference, clearedFlags)->next; + from->index += difference; + from->count -= difference; + from.incrementIndexes(from->count); + } else if (clearedFlags) { + // The whole range is affected so simply update the flags. + from->flags &= ~flags; + } else { + // All flags have been removed from the range so remove it. + *from = erase(*from)->previous; + } + } + + if (*from != &m_ranges && from->previous != &m_ranges + && from->previous->list == from->list + && (!from->list || from->previous->end() == from->index) + && from->previous->flags == (from->flags & ~AppendFlag)) { + // If the following range is now a continuation, merge it with its previous range. + from.offset = from->previous->count; + from->previous->count += from->count; + from->previous->flags = from->flags; + *from = erase(*from)->previous; + } + m_cacheIt = from; + QT_QML_VERIFY_LISTCOMPOSITOR +} + +bool QQmlListCompositor::verifyMoveTo( + Group fromGroup, int from, Group toGroup, int to, int count, Group group) const +{ + if (group != toGroup) { + // determine how many items from the destination group intersect with the source group. + iterator fromIt = find(fromGroup, from); + + int intersectingCount = 0; + + for (; count > 0; *fromIt = fromIt->next) { + if (*fromIt == &m_ranges) + return false; + if (!fromIt->inGroup(group)) + continue; + if (fromIt->inGroup(toGroup)) + intersectingCount += qMin(count, fromIt->count - fromIt.offset); + count -= fromIt->count - fromIt.offset; + fromIt.offset = 0; + } + count = intersectingCount; + } + + return to >= 0 && to + count <= m_end.index[toGroup]; +} + +/*! + \internal + + Moves \a count items belonging to \a moveGroup from the index \a from in \a fromGroup + to the index \a to in \a toGroup. + + If \a removes and \a inserts are not null they will be populated with per group notifications + of the items moved. + */ + +void QQmlListCompositor::move( + Group fromGroup, + int from, + Group toGroup, + int to, + int count, + Group moveGroup, + QVector<Remove> *removes, + QVector<Insert> *inserts) +{ + QT_QML_TRACE_LISTCOMPOSITOR(<< fromGroup << from << toGroup << to << count) + Q_ASSERT(count > 0); + Q_ASSERT(from >=0); + Q_ASSERT(verifyMoveTo(fromGroup, from, toGroup, to, count, moveGroup)); + + // Find the position of the first item to move. + iterator fromIt = find(fromGroup, from); + + if (fromIt != moveGroup) { + // If the range at the from index doesn't contain items from the move group; skip + // to the next range. + fromIt.incrementIndexes(fromIt->count - fromIt.offset); + fromIt.offset = 0; + *fromIt = fromIt->next; + } else if (fromIt.offset > 0) { + // If the range at the from index contains items from the move group and the index isn't + // at the start of the range; split the range at the index and move the iterator to start + // of the second range. + *fromIt = insert( + *fromIt, fromIt->list, fromIt->index, fromIt.offset, fromIt->flags & ~AppendFlag)->next; + fromIt->index += fromIt.offset; + fromIt->count -= fromIt.offset; + fromIt.offset = 0; + } + + // Remove count items belonging to the move group from the list. + Range movedFlags; + for (int moveId = m_moveId; count > 0;) { + if (fromIt != moveGroup) { + // Skip ranges not containing items from the move group. + fromIt.incrementIndexes(fromIt->count); + *fromIt = fromIt->next; + continue; + } + int difference = qMin(count, fromIt->count); + + // Create a new static range containing the moved items from an existing range. + new Range( + &movedFlags, + fromIt->list, + fromIt->index, + difference, + fromIt->flags & ~(PrependFlag | AppendFlag)); + // Remove moved items from the count, the existing range, and a remove notification. + if (removes) + removes->append(Remove(fromIt, difference, fromIt->flags, ++moveId)); + count -= difference; + fromIt->count -= difference; + + // If the existing range contains the prepend flag replace the removed items with + // a placeholder range for new items inserted into the source model. + int removeIndex = fromIt->index; + if (fromIt->prepend() + && fromIt->previous != &m_ranges + && fromIt->previous->flags == PrependFlag + && fromIt->previous->list == fromIt->list + && fromIt->previous->end() == fromIt->index) { + // Grow the previous range instead of creating a new one if possible. + fromIt->previous->count += difference; + } else if (fromIt->prepend()) { + *fromIt = insert(*fromIt, fromIt->list, removeIndex, difference, PrependFlag)->next; + } + fromIt->index += difference; + + if (fromIt->count == 0) { + // If the existing range has no items remaining; remove it from the list. + if (fromIt->append()) + fromIt->previous->flags |= AppendFlag; + *fromIt = erase(*fromIt); + + // If the ranges before and after the removed range can be joined, do so. + if (*fromIt != m_ranges.next && fromIt->flags == PrependFlag + && fromIt->previous != &m_ranges + && fromIt->previous->flags == PrependFlag + && fromIt->previous->list == fromIt->list + && fromIt->previous->end() == fromIt->index) { + fromIt.incrementIndexes(fromIt->count); + fromIt->previous->count += fromIt->count; + *fromIt = erase(*fromIt); + } + } else if (count > 0) { + *fromIt = fromIt->next; + } + } + + // Try and join the range following the removed items to the range preceding it. + if (*fromIt != m_ranges.next + && *fromIt != &m_ranges + && fromIt->previous->list == fromIt->list + && (!fromIt->list || fromIt->previous->end() == fromIt->index) + && fromIt->previous->flags == (fromIt->flags & ~AppendFlag)) { + if (fromIt == fromIt.group) + fromIt.offset = fromIt->previous->count; + fromIt.offset = fromIt->previous->count; + fromIt->previous->count += fromIt->count; + fromIt->previous->flags = fromIt->flags; + *fromIt = erase(*fromIt)->previous; + } + + // Find the destination position of the move. + insert_iterator toIt = fromIt; + toIt.setGroup(toGroup); + + const int difference = to - toIt.index[toGroup]; + toIt += difference; + + // If the insert position is part way through a range; split it and move the iterator to the + // start of the second range. + if (toIt.offset > 0) { + *toIt = insert(*toIt, toIt->list, toIt->index, toIt.offset, toIt->flags & ~AppendFlag)->next; + toIt->index += toIt.offset; + toIt->count -= toIt.offset; + toIt.offset = 0; + } + + // Insert the moved ranges before the insert iterator, growing the previous range if that + // is an option. + for (Range *range = movedFlags.previous; range != &movedFlags; range = range->previous) { + if (*toIt != &m_ranges + && range->list == toIt->list + && (!range->list || range->end() == toIt->index) + && range->flags == (toIt->flags & ~AppendFlag)) { + toIt->index -= range->count; + toIt->count += range->count; + } else { + *toIt = insert(*toIt, range->list, range->index, range->count, range->flags); + } + } + + // Try and join the range after the inserted ranges to the last range inserted. + if (*toIt != m_ranges.next + && toIt->previous->list == toIt->list + && (!toIt->list || (toIt->previous->end() == toIt->index && toIt->previous->flags == (toIt->flags & ~AppendFlag)))) { + toIt.offset = toIt->previous->count; + toIt->previous->count += toIt->count; + toIt->previous->flags = toIt->flags; + *toIt = erase(*toIt)->previous; + } + // Create insert notification for the ranges moved. + Insert insert(toIt, 0, 0, 0); + for (Range *next, *range = movedFlags.next; range != &movedFlags; range = next) { + insert.count = range->count; + insert.flags = range->flags; + if (inserts) { + insert.moveId = ++m_moveId; + inserts->append(insert); + } + for (int i = 0; i < m_groupCount; ++i) { + if (insert.inGroup(i)) + insert.index[i] += range->count; + } + + next = range->next; + delete range; + } + + m_cacheIt = toIt; + + QT_QML_VERIFY_LISTCOMPOSITOR +} + +/*! + Clears the contents of a compositor. +*/ + +void QQmlListCompositor::clear() +{ + QT_QML_TRACE_LISTCOMPOSITOR("") + for (Range *range = m_ranges.next; range != &m_ranges; range = erase(range)) {} + m_end = iterator(m_ranges.next, 0, Default, m_groupCount); + m_cacheIt = m_end; +} + +void QQmlListCompositor::listItemsInserted( + QVector<Insert> *translatedInsertions, + void *list, + const QVector<QQmlChangeSet::Change> &insertions, + const QVector<MovedFlags> *movedFlags) +{ + QT_QML_TRACE_LISTCOMPOSITOR(<< list << insertions) + for (iterator it(m_ranges.next, 0, Default, m_groupCount); *it != &m_ranges; *it = it->next) { + if (it->list != list || it->flags == CacheFlag) { + // Skip ranges that don't reference list. + it.incrementIndexes(it->count); + continue; + } else if (it->flags & MovedFlag) { + // Skip ranges that were already moved in listItemsRemoved. + it->flags &= ~MovedFlag; + it.incrementIndexes(it->count); + continue; + } + for (const QQmlChangeSet::Change &insertion : insertions) { + int offset = insertion.index - it->index; + if ((offset > 0 && offset < it->count) + || (offset == 0 && it->prepend()) + || (offset == it->count && it->append())) { + // The insert index is within the current range. + if (it->prepend()) { + // The range has the prepend flag set so we insert new items into the range. + uint flags = m_defaultFlags; + if (insertion.isMove()) { + // If the insert was part of a move replace the default flags with + // the flags from the source range. + for (QVector<MovedFlags>::const_iterator move = movedFlags->begin(); + move != movedFlags->end(); + ++move) { + if (move->moveId == insertion.moveId) { + flags = move->flags; + break; + } + } + } + if (flags & ~(AppendFlag | PrependFlag)) { + // If any items are added to groups append an insert notification. + Insert translatedInsert(it, insertion.count, flags, insertion.moveId); + for (int i = 0; i < m_groupCount; ++i) { + if (it->inGroup(i)) + translatedInsert.index[i] += offset; + } + translatedInsertions->append(translatedInsert); + } + if ((it->flags & ~AppendFlag) == flags) { + // Accumulate items on the current range it its flags are the same as + // the insert flags. + it->count += insertion.count; + } else if (offset == 0 + && it->previous != &m_ranges + && it->previous->list == list + && it->previous->end() == insertion.index + && it->previous->flags == flags) { + // Attempt to append to the previous range if the insert position is at + // the start of the current range. + it->previous->count += insertion.count; + it->index += insertion.count; + it.incrementIndexes(insertion.count); + } else { + if (offset > 0) { + // Divide the current range at the insert position. + it.incrementIndexes(offset); + *it = insert(*it, it->list, it->index, offset, it->flags & ~AppendFlag)->next; + } + // Insert a new range, and increment the start index of the current range. + *it = insert(*it, it->list, insertion.index, insertion.count, flags)->next; + it.incrementIndexes(insertion.count, flags); + it->index += offset + insertion.count; + it->count -= offset; + } + m_end.incrementIndexes(insertion.count, flags); + } else { + // The range doesn't have the prepend flag set so split the range into parts; + // one before the insert position and one after the last inserted item. + if (offset > 0) { + *it = insert(*it, it->list, it->index, offset, it->flags)->next; + it->index += offset; + it->count -= offset; + } + it->index += insertion.count; + } + } else if (offset <= 0) { + // The insert position was before the current range so increment the start index. + it->index += insertion.count; + } + } + it.incrementIndexes(it->count); + } + m_cacheIt = m_end; + QT_QML_VERIFY_LISTCOMPOSITOR +} + +/*! + Updates the contents of a compositor when \a count items are inserted into a \a list at + \a index. + + This corrects the indexes of each range for that list in the compositor, splitting the range + in two if the insert index is in the middle of that range. If a range at the insert position + has the Prepend flag set then a new range will be inserted at that position with the groups + specified in defaultGroups(). If the insert index corresponds to the end of a range with + the Append flag set a new range will be inserted before the end of the append range. + + The \a translatedInsertions list is populated with insert notifications for affected + groups. +*/ + +void QQmlListCompositor::listItemsInserted( + void *list, int index, int count, QVector<Insert> *translatedInsertions) +{ + QT_QML_TRACE_LISTCOMPOSITOR(<< list << index << count) + Q_ASSERT(count > 0); + + QVector<QQmlChangeSet::Change> insertions; + insertions.append(QQmlChangeSet::Change(index, count)); + + listItemsInserted(translatedInsertions, list, insertions); +} + +void QQmlListCompositor::listItemsRemoved( + QVector<Remove> *translatedRemovals, + void *list, + QVector<QQmlChangeSet::Change> *removals, + QVector<QQmlChangeSet::Change> *insertions, + QVector<MovedFlags> *movedFlags) +{ + QT_QML_TRACE_LISTCOMPOSITOR(<< list << *removals) + + for (iterator it(m_ranges.next, 0, Default, m_groupCount); *it != &m_ranges; *it = it->next) { + if (it->list != list || it->flags == CacheFlag) { + // Skip ranges that don't reference list. + it.incrementIndexes(it->count); + continue; + } + bool removed = false; + for (QVector<QQmlChangeSet::Change>::iterator removal = removals->begin(); + !removed && removal != removals->end(); + ++removal) { + int relativeIndex = removal->index - it->index; + int itemsRemoved = removal->count; + if (relativeIndex + removal->count > 0 && relativeIndex < it->count) { + // If the current range intersects the remove; remove the intersecting items. + const int offset = qMax(0, relativeIndex); + int removeCount = qMin(it->count, relativeIndex + removal->count) - offset; + it->count -= removeCount; + int removeFlags = it->flags & m_removeFlags; + Remove translatedRemoval(it, removeCount, it->flags); + for (int i = 0; i < m_groupCount; ++i) { + if (it->inGroup(i)) + translatedRemoval.index[i] += offset; + } + if (removal->isMove()) { + // If the removal was part of a move find the corresponding insert. + QVector<QQmlChangeSet::Change>::iterator insertion = insertions->begin(); + for (; insertion != insertions->end() && insertion->moveId != removal->moveId; + ++insertion) {} + Q_ASSERT(insertion != insertions->end()); + Q_ASSERT(insertion->count == removal->count); + + if (relativeIndex < 0) { + // If the remove started before the current range, split it and the + // corresponding insert so we're only working with intersecting part. + int splitMoveId = ++m_moveId; + removal = removals->insert(removal, QQmlChangeSet::Change( + removal->index, -relativeIndex, splitMoveId)); + ++removal; + removal->count -= -relativeIndex; + insertion = insertions->insert(insertion, QQmlChangeSet::Change( + insertion->index, -relativeIndex, splitMoveId)); + ++insertion; + insertion->index += -relativeIndex; + insertion->count -= -relativeIndex; + } + + if (it->prepend()) { + // If the current range has the prepend flag preserve its flags to transfer + // to its new location. + removeFlags |= it->flags & CacheFlag; + translatedRemoval.moveId = ++m_moveId; + movedFlags->append(MovedFlags(m_moveId, it->flags & ~AppendFlag)); + + if (removeCount < removal->count) { + // If the remove doesn't encompass all of the current range, + // split it and the corresponding insert. + removal = removals->insert(removal, QQmlChangeSet::Change( + removal->index, removeCount, translatedRemoval.moveId)); + ++removal; + insertion = insertions->insert(insertion, QQmlChangeSet::Change( + insertion->index, removeCount, translatedRemoval.moveId)); + ++insertion; + + removal->count -= removeCount; + insertion->index += removeCount; + insertion->count -= removeCount; + } else { + // If there's no need to split the move simply replace its moveId + // with that of the translated move. + removal->moveId = translatedRemoval.moveId; + insertion->moveId = translatedRemoval.moveId; + } + } else { + // If the current range doesn't have prepend flags then insert a new range + // with list indexes from the corresponding insert location. The MoveFlag + // is to notify listItemsInserted that it can skip evaluation of that range. + if (offset > 0) { + *it = insert(*it, it->list, it->index, offset, it->flags & ~AppendFlag)->next; + it->index += offset; + it->count -= offset; + it.incrementIndexes(offset); + } + if (it->previous != &m_ranges + && it->previous->list == it->list + && it->end() == insertion->index + && it->previous->flags == (it->flags | MovedFlag)) { + it->previous->count += removeCount; + } else { + *it = insert(*it, it->list, insertion->index, removeCount, it->flags | MovedFlag)->next; + } + // Clear the changed flags as the item hasn't been removed. + translatedRemoval.flags = 0; + removeFlags = 0; + } + } else if (it->inCache()) { + // If not moving and the current range has the cache flag, insert a new range + // with just the cache flag set to retain caching information for the removed + // range. + if (offset > 0) { + *it = insert(*it, it->list, it->index, offset, it->flags & ~AppendFlag)->next; + it->index += offset; + it->count -= offset; + it.incrementIndexes(offset); + } + if (it->previous != &m_ranges + && it->previous->list == it->list + && it->previous->flags == CacheFlag) { + it->previous->count += removeCount; + } else { + *it = insert(*it, it->list, -1, removeCount, CacheFlag)->next; + } + it.index[Cache] += removeCount; + } + if (removeFlags & GroupMask) + translatedRemovals->append(translatedRemoval); + m_end.decrementIndexes(removeCount, removeFlags); + if (it->count == 0 && !it->append()) { + // Erase empty non-append ranges. + *it = erase(*it)->previous; + removed = true; + } else if (relativeIndex <= 0) { + // If the remove started before the current range move the start index of + // the range to the remove index. + it->index = removal->index; + } + } else if (relativeIndex < 0) { + // If the remove was before the current range decrement the start index by the + // number of items removed. + it->index -= itemsRemoved; + + if (it->previous != &m_ranges + && it->previous->list == it->list + && it->previous->end() == it->index + && it->previous->flags == (it->flags & ~AppendFlag)) { + // Compress ranges made continuous by the removal of separating ranges. + it.decrementIndexes(it->previous->count); + it->previous->count += it->count; + it->previous->flags = it->flags; + *it = erase(*it)->previous; + } + } + } + if (it->flags == CacheFlag && it->next->flags == CacheFlag && it->next->list == it->list) { + // Compress consecutive cache only ranges. + it.index[Cache] += it->next->count; + it->count += it->next->count; + erase(it->next); + } else if (!removed) { + it.incrementIndexes(it->count); + } + } + m_cacheIt = m_end; + QT_QML_VERIFY_LISTCOMPOSITOR +} + + +/*! + Updates the contents of a compositor when \a count items are removed from a \a list at + \a index. + + Ranges that intersect the specified range are removed from the compositor and the indexes of + ranges after index + count are updated. + + The \a translatedRemovals list is populated with remove notifications for the affected + groups. +*/ + + +void QQmlListCompositor::listItemsRemoved( + void *list, int index, int count, QVector<Remove> *translatedRemovals) +{ + QT_QML_TRACE_LISTCOMPOSITOR(<< list << index << count) + Q_ASSERT(count >= 0); + + QVector<QQmlChangeSet::Change> removals; + removals.append(QQmlChangeSet::Change(index, count)); + listItemsRemoved(translatedRemovals, list, &removals, nullptr, nullptr); +} + +/*! + Updates the contents of a compositor when \a count items are removed from a \a list at + \a index. + + Ranges that intersect the specified range are removed from the compositor and the indexes of + ranges after index + count are updated. + + The \a translatedRemovals and translatedInserts lists are populated with move + notifications for the affected groups. +*/ + +void QQmlListCompositor::listItemsMoved( + void *list, + int from, + int to, + int count, + QVector<Remove> *translatedRemovals, + QVector<Insert> *translatedInsertions) +{ + QT_QML_TRACE_LISTCOMPOSITOR(<< list << from << to << count) + Q_ASSERT(count >= 0); + + QVector<QQmlChangeSet::Change> removals; + QVector<QQmlChangeSet::Change> insertions; + QVector<MovedFlags> movedFlags; + removals.append(QQmlChangeSet::Change(from, count, 0)); + insertions.append(QQmlChangeSet::Change(to, count, 0)); + + listItemsRemoved(translatedRemovals, list, &removals, &insertions, &movedFlags); + listItemsInserted(translatedInsertions, list, insertions, &movedFlags); +} + +void QQmlListCompositor::listItemsChanged( + QVector<Change> *translatedChanges, + void *list, + const QVector<QQmlChangeSet::Change> &changes) +{ + QT_QML_TRACE_LISTCOMPOSITOR(<< list << changes) + for (iterator it(m_ranges.next, 0, Default, m_groupCount); *it != &m_ranges; *it = it->next) { + if (it->list != list || it->flags == CacheFlag) { + it.incrementIndexes(it->count); + continue; + } else if (!it->inGroup()) { + continue; + } + for (const QQmlChangeSet::Change &change : changes) { + const int offset = change.index - it->index; + if (offset + change.count > 0 && offset < it->count) { + const int changeOffset = qMax(0, offset); + const int changeCount = qMin(it->count, offset + change.count) - changeOffset; + + Change translatedChange(it, changeCount, it->flags); + for (int i = 0; i < m_groupCount; ++i) { + if (it->inGroup(i)) + translatedChange.index[i] += changeOffset; + } + translatedChanges->append(translatedChange); + } + } + it.incrementIndexes(it->count); + } +} + + +/*! + Translates the positions of \a count changed items at \a index in a \a list. + + The \a translatedChanges list is populated with change notifications for the + affected groups. +*/ + +void QQmlListCompositor::listItemsChanged( + void *list, int index, int count, QVector<Change> *translatedChanges) +{ + QT_QML_TRACE_LISTCOMPOSITOR(<< list << index << count) + Q_ASSERT(count >= 0); + QVector<QQmlChangeSet::Change> changes; + changes.append(QQmlChangeSet::Change(index, count)); + listItemsChanged(translatedChanges, list, changes); +} + +void QQmlListCompositor::transition( + Group from, + Group to, + QVector<QQmlChangeSet::Change> *removes, + QVector<QQmlChangeSet::Change> *inserts) +{ + int removeCount = 0; + for (iterator it(m_ranges.next, 0, Default, m_groupCount); *it != &m_ranges; *it = it->next) { + if (it == from && it != to) { + removes->append(QQmlChangeSet::Change(it.index[from]- removeCount, it->count)); + removeCount += it->count; + } else if (it != from && it == to) { + inserts->append(QQmlChangeSet::Change(it.index[to], it->count)); + } + it.incrementIndexes(it->count); + } +} + +/*! + \internal + Writes the name of \a group to \a debug. +*/ + +QDebug operator <<(QDebug debug, const QQmlListCompositor::Group &group) +{ + switch (group) { + case QQmlListCompositor::Cache: return debug << "Cache"; + case QQmlListCompositor::Default: return debug << "Default"; + default: return (debug.nospace() << "Group" << int(group)).space(); + } + +} + +/*! + \internal + Writes the contents of \a range to \a debug. +*/ + +QDebug operator <<(QDebug debug, const QQmlListCompositor::Range &range) +{ + (debug.nospace() + << "Range(" + << range.list) << ' ' + << range.index << ' ' + << range.count << ' ' + << (range.isUnresolved() ? 'U' : '0') + << (range.append() ? 'A' : '0') + << (range.prepend() ? 'P' : '0'); + for (int i = QQmlListCompositor::MaximumGroupCount - 1; i >= 2; --i) + debug << (range.inGroup(i) ? '1' : '0'); + return (debug + << (range.inGroup(QQmlListCompositor::Default) ? 'D' : '0') + << (range.inGroup(QQmlListCompositor::Cache) ? 'C' : '0')); +} + +static void qt_print_indexes(QDebug &debug, int count, const int *indexes) +{ + for (int i = count - 1; i >= 0; --i) + debug << indexes[i]; +} + +/*! + \internal + Writes the contents of \a it to \a debug. +*/ + +QDebug operator <<(QDebug debug, const QQmlListCompositor::iterator &it) +{ + (debug.nospace() << "iterator(" << it.group).space() << "offset:" << it.offset; + qt_print_indexes(debug, it.groupCount, it.index); + return ((debug << **it).nospace() << ')').space(); +} + +static QDebug qt_print_change(QDebug debug, const char *name, const QQmlListCompositor::Change &change) +{ + debug.nospace() << name << '(' << change.moveId << ' ' << change.count << ' '; + for (int i = QQmlListCompositor::MaximumGroupCount - 1; i >= 2; --i) + debug << (change.inGroup(i) ? '1' : '0'); + debug << (change.inGroup(QQmlListCompositor::Default) ? 'D' : '0') + << (change.inGroup(QQmlListCompositor::Cache) ? 'C' : '0'); + int i = QQmlListCompositor::MaximumGroupCount - 1; + for (; i >= 0 && !change.inGroup(i); --i) {} + for (; i >= 0; --i) + debug << ' ' << change.index[i]; + return (debug << ')').maybeSpace(); +} + +/*! + \internal + Writes the contents of \a change to \a debug. +*/ + +QDebug operator <<(QDebug debug, const QQmlListCompositor::Change &change) +{ + return qt_print_change(debug, "Change", change); +} + +/*! + \internal + Writes the contents of \a remove to \a debug. +*/ + +QDebug operator <<(QDebug debug, const QQmlListCompositor::Remove &remove) +{ + return qt_print_change(debug, "Remove", remove); +} + +/*! + \internal + Writes the contents of \a insert to \a debug. +*/ + +QDebug operator <<(QDebug debug, const QQmlListCompositor::Insert &insert) +{ + return qt_print_change(debug, "Insert", insert); +} + +/*! + \internal + Writes the contents of \a list to \a debug. +*/ + +QDebug operator <<(QDebug debug, const QQmlListCompositor &list) +{ + int indexes[QQmlListCompositor::MaximumGroupCount]; + for (int i = 0; i < QQmlListCompositor::MaximumGroupCount; ++i) + indexes[i] = 0; + debug.nospace() << "QQmlListCompositor("; + qt_print_indexes(debug, list.m_groupCount, list.m_end.index); + for (QQmlListCompositor::Range *range = list.m_ranges.next; range != &list.m_ranges; range = range->next) { + (debug << '\n').space(); + qt_print_indexes(debug, list.m_groupCount, indexes); + debug << ' ' << *range; + + for (int i = 0; i < list.m_groupCount; ++i) { + if (range->inGroup(i)) + indexes[i] += range->count; + } + } + return (debug << ')').maybeSpace(); +} + +QT_END_NAMESPACE diff --git a/src/qmlmodels/qqmllistcompositor_p.h b/src/qmlmodels/qqmllistcompositor_p.h new file mode 100644 index 0000000000..172040559c --- /dev/null +++ b/src/qmlmodels/qqmllistcompositor_p.h @@ -0,0 +1,372 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtQml module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QQMLLISTCOMPOSITOR_P_H +#define QQMLLISTCOMPOSITOR_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <QtCore/qglobal.h> +#include <QtCore/qvector.h> + +#include <private/qqmlchangeset_p.h> + +#include <QtCore/qdebug.h> + +QT_BEGIN_NAMESPACE + +class Q_AUTOTEST_EXPORT QQmlListCompositor +{ +public: + enum { MinimumGroupCount = 3, MaximumGroupCount = 11 }; + + enum Group + { + Cache = 0, + Default = 1, + Persisted = 2 + }; + + enum Flag + { + CacheFlag = 1 << Cache, + DefaultFlag = 1 << Default, + PersistedFlag = 1 << Persisted, + PrependFlag = 0x10000000, + AppendFlag = 0x20000000, + UnresolvedFlag = 0x40000000, + MovedFlag = 0x80000000, + GroupMask = ~(PrependFlag | AppendFlag | UnresolvedFlag | MovedFlag | CacheFlag) + }; + + class Range + { + public: + Range() : next(this), previous(this) {} + Range(Range *next, void *list, int index, int count, uint flags) + : next(next), previous(next->previous), list(list), index(index), count(count), flags(flags) { + next->previous = this; previous->next = this; } + + Range *next; + Range *previous; + void *list = nullptr; + int index = 0; + int count = 0; + uint flags = 0; + + inline int start() const { return index; } + inline int end() const { return index + count; } + + inline int groups() const { return flags & GroupMask; } + + inline bool inGroup() const { return flags & GroupMask; } + inline bool inCache() const { return flags & CacheFlag; } + inline bool inGroup(int group) const { return flags & (1 << group); } + inline bool isUnresolved() const { return flags & UnresolvedFlag; } + + inline bool prepend() const { return flags & PrependFlag; } + inline bool append() const { return flags & AppendFlag; } + }; + + class Q_AUTOTEST_EXPORT iterator + { + public: + inline iterator(); + inline iterator(const iterator &it); + inline iterator(Range *range, int offset, Group group, int groupCount); + inline ~iterator() {} + + bool operator ==(const iterator &it) const { return range == it.range && offset == it.offset; } + bool operator !=(const iterator &it) const { return range != it.range || offset != it.offset; } + + bool operator ==(Group group) const { return range->flags & (1 << group); } + bool operator !=(Group group) const { return !(range->flags & (1 << group)); } + + Range *&operator *() { return range; } + Range * const &operator *() const { return range; } + Range *operator ->() { return range; } + const Range *operator ->() const { return range; } + + iterator &operator +=(int difference); + + template<typename T> T *list() const { return static_cast<T *>(range->list); } + int modelIndex() const { return range->index + offset; } + + void incrementIndexes(int difference) { incrementIndexes(difference, range->flags); } + void decrementIndexes(int difference) { decrementIndexes(difference, range->flags); } + + inline void incrementIndexes(int difference, uint flags); + inline void decrementIndexes(int difference, uint flags); + + void setGroup(Group g) { group = g; groupFlag = 1 << g; } + + Range *range = nullptr; + int offset = 0; + Group group = Default; + int groupFlag; + int groupCount = 0; + union { + struct { + int cacheIndex; + }; + int index[MaximumGroupCount]; + }; + }; + + class Q_AUTOTEST_EXPORT insert_iterator : public iterator + { + public: + inline insert_iterator() {} + inline insert_iterator(const iterator &it) : iterator(it) {} + inline insert_iterator(Range *, int, Group, int); + inline ~insert_iterator() {} + + insert_iterator &operator +=(int difference); + }; + + struct Change + { + inline Change() {} + inline Change(const iterator &it, int count, uint flags, int moveId = -1); + int count; + uint flags; + int moveId; + union { + struct { + int cacheIndex; + }; + int index[MaximumGroupCount]; + }; + + inline bool isMove() const { return moveId >= 0; } + inline bool inCache() const { return flags & CacheFlag; } + inline bool inGroup() const { return flags & GroupMask; } + inline bool inGroup(int group) const { return flags & (CacheFlag << group); } + + inline int groups() const { return flags & GroupMask; } + }; + + struct Insert : public Change + { + Insert() {} + Insert(const iterator &it, int count, uint flags, int moveId = -1) + : Change(it, count, flags, moveId) {} + }; + + struct Remove : public Change + { + Remove() {} + Remove(const iterator &it, int count, uint flags, int moveId = -1) + : Change(it, count, flags, moveId) {} + }; + + QQmlListCompositor(); + ~QQmlListCompositor(); + + int defaultGroups() const { return m_defaultFlags & ~PrependFlag; } + void setDefaultGroups(int groups) { m_defaultFlags = groups | PrependFlag; } + void setDefaultGroup(Group group) { m_defaultFlags |= (1 << group); } + void clearDefaultGroup(Group group) { m_defaultFlags &= ~(1 << group); } + void setRemoveGroups(int groups) { m_removeFlags = PrependFlag | AppendFlag | groups; } + void setGroupCount(int count); + + int count(Group group) const; + iterator find(Group group, int index); + iterator find(Group group, int index) const; + insert_iterator findInsertPosition(Group group, int index); + + const iterator &end() { return m_end; } + + void append(void *list, int index, int count, uint flags, QVector<Insert> *inserts = nullptr); + void insert(Group group, int before, void *list, int index, int count, uint flags, QVector<Insert> *inserts = nullptr); + iterator insert(iterator before, void *list, int index, int count, uint flags, QVector<Insert> *inserts = nullptr); + + void setFlags(Group fromGroup, int from, int count, Group group, int flags, QVector<Insert> *inserts = nullptr); + void setFlags(iterator from, int count, Group group, uint flags, QVector<Insert> *inserts = nullptr); + void setFlags(Group fromGroup, int from, int count, uint flags, QVector<Insert> *inserts = nullptr) { + setFlags(fromGroup, from, count, fromGroup, flags, inserts); } + void setFlags(const iterator from, int count, uint flags, QVector<Insert> *inserts = nullptr) { + setFlags(from, count, from.group, flags, inserts); } + + void clearFlags(Group fromGroup, int from, int count, Group group, uint flags, QVector<Remove> *removals = nullptr); + void clearFlags(iterator from, int count, Group group, uint flags, QVector<Remove> *removals = nullptr); + void clearFlags(Group fromGroup, int from, int count, uint flags, QVector<Remove> *removals = nullptr) { + clearFlags(fromGroup, from, count, fromGroup, flags, removals); } + void clearFlags(const iterator &from, int count, uint flags, QVector<Remove> *removals = nullptr) { + clearFlags(from, count, from.group, flags, removals); } + + bool verifyMoveTo(Group fromGroup, int from, Group toGroup, int to, int count, Group group) const; + + void move( + Group fromGroup, + int from, + Group toGroup, + int to, + int count, + Group group, + QVector<Remove> *removals = nullptr, + QVector<Insert> *inserts = nullptr); + void clear(); + + void listItemsInserted(void *list, int index, int count, QVector<Insert> *inserts); + void listItemsRemoved(void *list, int index, int count, QVector<Remove> *removals); + void listItemsMoved(void *list, int from, int to, int count, QVector<Remove> *removals, QVector<Insert> *inserts); + void listItemsChanged(void *list, int index, int count, QVector<Change> *changes); + + void transition( + Group from, + Group to, + QVector<QQmlChangeSet::Change> *removes, + QVector<QQmlChangeSet::Change> *inserts); + +private: + Range m_ranges; + iterator m_end; + iterator m_cacheIt; + int m_groupCount; + int m_defaultFlags; + int m_removeFlags; + int m_moveId; + + inline Range *insert(Range *before, void *list, int index, int count, uint flags); + inline Range *erase(Range *range); + + struct MovedFlags + { + MovedFlags() {} + MovedFlags(int moveId, uint flags) : moveId(moveId), flags(flags) {} + + int moveId; + uint flags; + }; + + void listItemsRemoved( + QVector<Remove> *translatedRemovals, + void *list, + QVector<QQmlChangeSet::Change> *removals, + QVector<QQmlChangeSet::Change> *insertions = nullptr, + QVector<MovedFlags> *movedFlags = nullptr); + void listItemsInserted( + QVector<Insert> *translatedInsertions, + void *list, + const QVector<QQmlChangeSet::Change> &insertions, + const QVector<MovedFlags> *movedFlags = nullptr); + void listItemsChanged( + QVector<Change> *translatedChanges, + void *list, + const QVector<QQmlChangeSet::Change> &changes); + + friend Q_AUTOTEST_EXPORT QDebug operator <<(QDebug debug, const QQmlListCompositor &list); +}; + +Q_DECLARE_TYPEINFO(QQmlListCompositor::Change, Q_PRIMITIVE_TYPE); +Q_DECLARE_TYPEINFO(QQmlListCompositor::Remove, Q_PRIMITIVE_TYPE); +Q_DECLARE_TYPEINFO(QQmlListCompositor::Insert, Q_PRIMITIVE_TYPE); + +inline QQmlListCompositor::iterator::iterator() {} +inline QQmlListCompositor::iterator::iterator(const iterator &it) + : range(it.range) + , offset(it.offset) + , group(it.group) + , groupFlag(it.groupFlag) + , groupCount(it.groupCount) +{ + for (int i = 0; i < groupCount; ++i) + index[i] = it.index[i]; +} + +inline QQmlListCompositor::iterator::iterator( + Range *range, int offset, Group group, int groupCount) + : range(range) + , offset(offset) + , group(group) + , groupFlag(1 << group) + , groupCount(groupCount) +{ + for (int i = 0; i < groupCount; ++i) + index[i] = 0; +} + +inline void QQmlListCompositor::iterator::incrementIndexes(int difference, uint flags) +{ + for (int i = 0; i < groupCount; ++i) { + if (flags & (1 << i)) + index[i] += difference; + } +} + +inline void QQmlListCompositor::iterator::decrementIndexes(int difference, uint flags) +{ + for (int i = 0; i < groupCount; ++i) { + if (flags & (1 << i)) + index[i] -= difference; + } +} + +inline QQmlListCompositor::insert_iterator::insert_iterator( + Range *range, int offset, Group group, int groupCount) + : iterator(range, offset, group, groupCount) {} + +inline QQmlListCompositor::Change::Change(const iterator &it, int count, uint flags, int moveId) + : count(count), flags(flags), moveId(moveId) +{ + for (int i = 0; i < MaximumGroupCount; ++i) + index[i] = it.index[i]; +} + +Q_AUTOTEST_EXPORT QDebug operator <<(QDebug debug, const QQmlListCompositor::Group &group); +Q_AUTOTEST_EXPORT QDebug operator <<(QDebug debug, const QQmlListCompositor::Range &range); +Q_AUTOTEST_EXPORT QDebug operator <<(QDebug debug, const QQmlListCompositor::iterator &it); +Q_AUTOTEST_EXPORT QDebug operator <<(QDebug debug, const QQmlListCompositor::Change &change); +Q_AUTOTEST_EXPORT QDebug operator <<(QDebug debug, const QQmlListCompositor::Remove &remove); +Q_AUTOTEST_EXPORT QDebug operator <<(QDebug debug, const QQmlListCompositor::Insert &insert); +Q_AUTOTEST_EXPORT QDebug operator <<(QDebug debug, const QQmlListCompositor &list); + +QT_END_NAMESPACE + +#endif diff --git a/src/qmlmodels/qqmllistmodel.cpp b/src/qmlmodels/qqmllistmodel.cpp new file mode 100644 index 0000000000..5b5bcd8464 --- /dev/null +++ b/src/qmlmodels/qqmllistmodel.cpp @@ -0,0 +1,2900 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtQml module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "qqmllistmodel_p_p.h" +#include "qqmllistmodelworkeragent_p.h" +#include <private/qqmlopenmetaobject_p.h> +#include <private/qqmljsast_p.h> +#include <private/qqmljsengine_p.h> +#include <private/qjsvalue_p.h> + +#include <private/qqmlcustomparser_p.h> +#include <private/qqmlengine_p.h> +#include <private/qqmlnotifier_p.h> + +#include <private/qv4object_p.h> +#include <private/qv4dateobject_p.h> +#include <private/qv4objectiterator_p.h> +#include <private/qv4alloca_p.h> +#include <private/qv4lookup_p.h> + +#include <qqmlcontext.h> +#include <qqmlinfo.h> + +#include <QtCore/qdebug.h> +#include <QtCore/qstack.h> +#include <QXmlStreamReader> +#include <QtCore/qdatetime.h> +#include <QScopedValueRollback> + +Q_DECLARE_METATYPE(const QV4::CompiledData::Binding*); + +QT_BEGIN_NAMESPACE + +// Set to 1024 as a debugging aid - easier to distinguish uids from indices of elements/models. +enum { MIN_LISTMODEL_UID = 1024 }; + +static QAtomicInt uidCounter(MIN_LISTMODEL_UID); + +template <typename T> +static bool isMemoryUsed(const char *mem) +{ + for (size_t i=0 ; i < sizeof(T) ; ++i) { + if (mem[i] != 0) + return true; + } + + return false; +} + +static QString roleTypeName(ListLayout::Role::DataType t) +{ + static const QString roleTypeNames[] = { + QStringLiteral("String"), QStringLiteral("Number"), QStringLiteral("Bool"), + QStringLiteral("List"), QStringLiteral("QObject"), QStringLiteral("VariantMap"), + QStringLiteral("DateTime"), QStringLiteral("Function") + }; + + if (t > ListLayout::Role::Invalid && t < ListLayout::Role::MaxDataType) + return roleTypeNames[t]; + + return QString(); +} + +const ListLayout::Role &ListLayout::getRoleOrCreate(const QString &key, Role::DataType type) +{ + QStringHash<Role *>::Node *node = roleHash.findNode(key); + if (node) { + const Role &r = *node->value; + if (type != r.type) + qmlWarning(nullptr) << QStringLiteral("Can't assign to existing role '%1' of different type [%2 -> %3]").arg(r.name).arg(roleTypeName(type)).arg(roleTypeName(r.type)); + return r; + } + + return createRole(key, type); +} + +const ListLayout::Role &ListLayout::getRoleOrCreate(QV4::String *key, Role::DataType type) +{ + QStringHash<Role *>::Node *node = roleHash.findNode(key); + if (node) { + const Role &r = *node->value; + if (type != r.type) + qmlWarning(nullptr) << QStringLiteral("Can't assign to existing role '%1' of different type [%2 -> %3]").arg(r.name).arg(roleTypeName(type)).arg(roleTypeName(r.type)); + return r; + } + + QString qkey = key->toQString(); + + return createRole(qkey, type); +} + +const ListLayout::Role &ListLayout::createRole(const QString &key, ListLayout::Role::DataType type) +{ + const int dataSizes[] = { sizeof(StringOrTranslation), sizeof(double), sizeof(bool), sizeof(ListModel *), sizeof(QPointer<QObject>), sizeof(QVariantMap), sizeof(QDateTime), sizeof(QJSValue) }; + const int dataAlignments[] = { sizeof(StringOrTranslation), sizeof(double), sizeof(bool), sizeof(ListModel *), sizeof(QObject *), sizeof(QVariantMap), sizeof(QDateTime), sizeof(QJSValue) }; + + Role *r = new Role; + r->name = key; + r->type = type; + + if (type == Role::List) { + r->subLayout = new ListLayout; + } else { + r->subLayout = nullptr; + } + + int dataSize = dataSizes[type]; + int dataAlignment = dataAlignments[type]; + + int dataOffset = (currentBlockOffset + dataAlignment-1) & ~(dataAlignment-1); + if (dataOffset + dataSize > ListElement::BLOCK_SIZE) { + r->blockIndex = ++currentBlock; + r->blockOffset = 0; + currentBlockOffset = dataSize; + } else { + r->blockIndex = currentBlock; + r->blockOffset = dataOffset; + currentBlockOffset = dataOffset + dataSize; + } + + int roleIndex = roles.count(); + r->index = roleIndex; + + roles.append(r); + roleHash.insert(key, r); + + return *r; +} + +ListLayout::ListLayout(const ListLayout *other) : currentBlock(0), currentBlockOffset(0) +{ + const int otherRolesCount = other->roles.count(); + roles.reserve(otherRolesCount); + for (int i=0 ; i < otherRolesCount; ++i) { + Role *role = new Role(other->roles[i]); + roles.append(role); + roleHash.insert(role->name, role); + } + currentBlockOffset = other->currentBlockOffset; + currentBlock = other->currentBlock; +} + +ListLayout::~ListLayout() +{ + qDeleteAll(roles); +} + +void ListLayout::sync(ListLayout *src, ListLayout *target) +{ + int roleOffset = target->roles.count(); + int newRoleCount = src->roles.count() - roleOffset; + + for (int i=0 ; i < newRoleCount ; ++i) { + Role *role = new Role(src->roles[roleOffset + i]); + target->roles.append(role); + target->roleHash.insert(role->name, role); + } + + target->currentBlockOffset = src->currentBlockOffset; + target->currentBlock = src->currentBlock; +} + +ListLayout::Role::Role(const Role *other) +{ + name = other->name; + type = other->type; + blockIndex = other->blockIndex; + blockOffset = other->blockOffset; + index = other->index; + if (other->subLayout) + subLayout = new ListLayout(other->subLayout); + else + subLayout = nullptr; +} + +ListLayout::Role::~Role() +{ + delete subLayout; +} + +const ListLayout::Role *ListLayout::getRoleOrCreate(const QString &key, const QVariant &data) +{ + Role::DataType type; + + switch (data.type()) { + case QVariant::Double: type = Role::Number; break; + case QVariant::Int: type = Role::Number; break; + case QVariant::Bool: type = Role::Bool; break; + case QVariant::String: type = Role::String; break; + case QVariant::Map: type = Role::VariantMap; break; + case QVariant::DateTime: type = Role::DateTime; break; + case QVariant::UserType: { + if (data.userType() == qMetaTypeId<QJSValue>() && + data.value<QJSValue>().isCallable()) { + type = Role::Function; + break; + } else if (data.userType() == qMetaTypeId<const QV4::CompiledData::Binding*>() + && data.value<const QV4::CompiledData::Binding*>()->isTranslationBinding()) { + type = Role::String; + break; + } else { + type = Role::List; + break; + } + } + default: type = Role::Invalid; break; + } + + if (type == Role::Invalid) { + qmlWarning(nullptr) << "Can't create role for unsupported data type"; + return nullptr; + } + + return &getRoleOrCreate(key, type); +} + +const ListLayout::Role *ListLayout::getExistingRole(const QString &key) const +{ + Role *r = nullptr; + QStringHash<Role *>::Node *node = roleHash.findNode(key); + if (node) + r = node->value; + return r; +} + +const ListLayout::Role *ListLayout::getExistingRole(QV4::String *key) const +{ + Role *r = nullptr; + QStringHash<Role *>::Node *node = roleHash.findNode(key); + if (node) + r = node->value; + return r; +} + +StringOrTranslation::StringOrTranslation(const QString &s) +{ + d.setFlag(); + setString(s); +} + +StringOrTranslation::StringOrTranslation(const QV4::CompiledData::Binding *binding) +{ + d.setFlag(); + clear(); + d = binding; +} + +StringOrTranslation::~StringOrTranslation() +{ + clear(); +} + +void StringOrTranslation::setString(const QString &s) +{ + d.setFlag(); + clear(); + QStringData *stringData = const_cast<QString &>(s).data_ptr(); + d = stringData; + if (stringData) + stringData->ref.ref(); +} + +void StringOrTranslation::setTranslation(const QV4::CompiledData::Binding *binding) +{ + d.setFlag(); + clear(); + d = binding; +} + +QString StringOrTranslation::toString(const QQmlListModel *owner) const +{ + if (d.isNull()) + return QString(); + if (d.isT1()) { + QStringDataPtr holder = { d.asT1() }; + holder.ptr->ref.ref(); + return QString(holder); + } + if (!owner) + return QString(); + return d.asT2()->valueAsString(owner->m_compilationUnit.data()); +} + +QString StringOrTranslation::asString() const +{ + if (d.isNull()) + return QString(); + if (!d.isT1()) + return QString(); + QStringDataPtr holder = { d.asT1() }; + holder.ptr->ref.ref(); + return QString(holder); +} + +void StringOrTranslation::clear() +{ + if (QStringData *strData = d.isT1() ? d.asT1() : nullptr) { + if (!strData->ref.deref()) + QStringData::deallocate(strData); + } + d = static_cast<QStringData *>(nullptr); +} + +QObject *ListModel::getOrCreateModelObject(QQmlListModel *model, int elementIndex) +{ + ListElement *e = elements[elementIndex]; + if (e->m_objectCache == nullptr) { + void *memory = operator new(sizeof(QObject) + sizeof(QQmlData)); + void *ddataMemory = ((char *)memory) + sizeof(QObject); + e->m_objectCache = new (memory) QObject; + QQmlData *ddata = new (ddataMemory) QQmlData; + ddata->ownMemory = false; + QObjectPrivate::get(e->m_objectCache)->declarativeData = ddata; + (void)new ModelNodeMetaObject(e->m_objectCache, model, elementIndex); + } + return e->m_objectCache; +} + +bool ListModel::sync(ListModel *src, ListModel *target) +{ + // Sanity check + + bool hasChanges = false; + + // Build hash of elements <-> uid for each of the lists + QHash<int, ElementSync> elementHash; + for (int i = 0; i < target->elements.count(); ++i) { + ListElement *e = target->elements.at(i); + int uid = e->getUid(); + ElementSync sync; + sync.target = e; + sync.targetIndex = i; + elementHash.insert(uid, sync); + } + for (int i = 0; i < src->elements.count(); ++i) { + ListElement *e = src->elements.at(i); + int uid = e->getUid(); + + QHash<int, ElementSync>::iterator it = elementHash.find(uid); + if (it == elementHash.end()) { + ElementSync sync; + sync.src = e; + sync.srcIndex = i; + elementHash.insert(uid, sync); + } else { + ElementSync &sync = it.value(); + sync.src = e; + sync.srcIndex = i; + } + } + + QQmlListModel *targetModel = target->m_modelCache; + + // Get list of elements that are in the target but no longer in the source. These get deleted first. + int rowsRemoved = 0; + for (int i = 0 ; i < target->elements.count() ; ++i) { + ListElement *element = target->elements.at(i); + ElementSync &s = elementHash.find(element->getUid()).value(); + Q_ASSERT(s.targetIndex >= 0); + // need to update the targetIndex, to keep it correct after removals + s.targetIndex -= rowsRemoved; + if (s.src == nullptr) { + Q_ASSERT(s.targetIndex == i); + hasChanges = true; + if (targetModel) + targetModel->beginRemoveRows(QModelIndex(), i, i); + s.target->destroy(target->m_layout); + target->elements.removeOne(s.target); + delete s.target; + if (targetModel) + targetModel->endRemoveRows(); + ++rowsRemoved; + --i; + continue; + } + } + + // Sync the layouts + ListLayout::sync(src->m_layout, target->m_layout); + + // Clear the target list, and append in correct order from the source + target->elements.clear(); + for (int i = 0; i < src->elements.count(); ++i) { + ListElement *srcElement = src->elements.at(i); + ElementSync &s = elementHash.find(srcElement->getUid()).value(); + Q_ASSERT(s.srcIndex >= 0); + ListElement *targetElement = s.target; + if (targetElement == nullptr) { + targetElement = new ListElement(srcElement->getUid()); + } + s.changedRoles = ListElement::sync(srcElement, src->m_layout, targetElement, target->m_layout); + target->elements.append(targetElement); + } + + target->updateCacheIndices(); + + // Update values stored in target meta objects + for (int i=0 ; i < target->elements.count() ; ++i) { + ListElement *e = target->elements[i]; + if (ModelNodeMetaObject *mo = e->objectCache()) + mo->updateValues(); + } + + // now emit the change notifications required. This can be safely done, as we're only emitting changes, moves and inserts, + // so the model indices can't be out of bounds + // + // to ensure things are kept in the correct order, emit inserts and moves first. This shouls ensure all persistent + // model indices are updated correctly + int rowsInserted = 0; + for (int i = 0 ; i < target->elements.count() ; ++i) { + ListElement *element = target->elements.at(i); + ElementSync &s = elementHash.find(element->getUid()).value(); + Q_ASSERT(s.srcIndex >= 0); + s.srcIndex += rowsInserted; + if (s.srcIndex != s.targetIndex) { + if (targetModel) { + if (s.targetIndex == -1) { + targetModel->beginInsertRows(QModelIndex(), i, i); + targetModel->endInsertRows(); + } else { + targetModel->beginMoveRows(QModelIndex(), i, i, QModelIndex(), s.srcIndex); + targetModel->endMoveRows(); + } + } + hasChanges = true; + ++rowsInserted; + } + if (s.targetIndex != -1 && !s.changedRoles.isEmpty()) { + QModelIndex idx = targetModel->createIndex(i, 0); + if (targetModel) + targetModel->dataChanged(idx, idx, s.changedRoles); + hasChanges = true; + } + } + return hasChanges; +} + +ListModel::ListModel(ListLayout *layout, QQmlListModel *modelCache) : m_layout(layout), m_modelCache(modelCache) +{ +} + +void ListModel::destroy() +{ + for (const auto &destroyer : remove(0, elements.count())) + destroyer(); + + m_layout = nullptr; + if (m_modelCache && m_modelCache->m_primary == false) + delete m_modelCache; + m_modelCache = nullptr; +} + +int ListModel::appendElement() +{ + int elementIndex = elements.count(); + newElement(elementIndex); + return elementIndex; +} + +void ListModel::insertElement(int index) +{ + newElement(index); + updateCacheIndices(index); +} + +void ListModel::move(int from, int to, int n) +{ + if (from > to) { + // Only move forwards - flip if backwards moving + int tfrom = from; + int tto = to; + from = tto; + to = tto+n; + n = tfrom-tto; + } + + QPODVector<ListElement *, 4> store; + for (int i=0 ; i < (to-from) ; ++i) + store.append(elements[from+n+i]); + for (int i=0 ; i < n ; ++i) + store.append(elements[from+i]); + for (int i=0 ; i < store.count() ; ++i) + elements[from+i] = store[i]; + + updateCacheIndices(from, to + n); +} + +void ListModel::newElement(int index) +{ + ListElement *e = new ListElement; + elements.insert(index, e); +} + +void ListModel::updateCacheIndices(int start, int end) +{ + int count = elements.count(); + + if (end < 0 || end > count) + end = count; + + for (int i = start; i < end; ++i) { + ListElement *e = elements.at(i); + if (ModelNodeMetaObject *mo = e->objectCache()) + mo->m_elementIndex = i; + } +} + +QVariant ListModel::getProperty(int elementIndex, int roleIndex, const QQmlListModel *owner, QV4::ExecutionEngine *eng) +{ + if (roleIndex >= m_layout->roleCount()) + return QVariant(); + ListElement *e = elements[elementIndex]; + const ListLayout::Role &r = m_layout->getExistingRole(roleIndex); + return e->getProperty(r, owner, eng); +} + +ListModel *ListModel::getListProperty(int elementIndex, const ListLayout::Role &role) +{ + ListElement *e = elements[elementIndex]; + return e->getListProperty(role); +} + +void ListModel::set(int elementIndex, QV4::Object *object, QVector<int> *roles) +{ + ListElement *e = elements[elementIndex]; + + QV4::ExecutionEngine *v4 = object->engine(); + QV4::Scope scope(v4); + QV4::ScopedObject o(scope); + + QV4::ObjectIterator it(scope, object, QV4::ObjectIterator::EnumerableOnly); + QV4::ScopedString propertyName(scope); + QV4::ScopedValue propertyValue(scope); + while (1) { + propertyName = it.nextPropertyNameAsString(propertyValue); + if (!propertyName) + break; + + // Check if this key exists yet + int roleIndex = -1; + + // Add the value now + if (const QV4::String *s = propertyValue->as<QV4::String>()) { + const ListLayout::Role &r = m_layout->getRoleOrCreate(propertyName, ListLayout::Role::String); + roleIndex = e->setStringProperty(r, s->toQString()); + } else if (propertyValue->isNumber()) { + const ListLayout::Role &r = m_layout->getRoleOrCreate(propertyName, ListLayout::Role::Number); + roleIndex = e->setDoubleProperty(r, propertyValue->asDouble()); + } else if (QV4::ArrayObject *a = propertyValue->as<QV4::ArrayObject>()) { + const ListLayout::Role &r = m_layout->getRoleOrCreate(propertyName, ListLayout::Role::List); + ListModel *subModel = new ListModel(r.subLayout, nullptr); + + int arrayLength = a->getLength(); + for (int j=0 ; j < arrayLength ; ++j) { + o = a->get(j); + subModel->append(o); + } + + roleIndex = e->setListProperty(r, subModel); + } else if (propertyValue->isBoolean()) { + const ListLayout::Role &r = m_layout->getRoleOrCreate(propertyName, ListLayout::Role::Bool); + roleIndex = e->setBoolProperty(r, propertyValue->booleanValue()); + } else if (QV4::DateObject *dd = propertyValue->as<QV4::DateObject>()) { + const ListLayout::Role &r = m_layout->getRoleOrCreate(propertyName, ListLayout::Role::DateTime); + QDateTime dt = dd->toQDateTime(); + roleIndex = e->setDateTimeProperty(r, dt); + } else if (QV4::FunctionObject *f = propertyValue->as<QV4::FunctionObject>()) { + const ListLayout::Role &r = m_layout->getRoleOrCreate(propertyName, ListLayout::Role::Function); + QV4::ScopedFunctionObject func(scope, f); + QJSValue jsv; + QJSValuePrivate::setValue(&jsv, v4, func); + roleIndex = e->setFunctionProperty(r, jsv); + } else if (QV4::Object *o = propertyValue->as<QV4::Object>()) { + if (QV4::QObjectWrapper *wrapper = o->as<QV4::QObjectWrapper>()) { + QObject *o = wrapper->object(); + const ListLayout::Role &role = m_layout->getRoleOrCreate(propertyName, ListLayout::Role::QObject); + if (role.type == ListLayout::Role::QObject) + roleIndex = e->setQObjectProperty(role, o); + } else { + const ListLayout::Role &role = m_layout->getRoleOrCreate(propertyName, ListLayout::Role::VariantMap); + if (role.type == ListLayout::Role::VariantMap) { + QV4::ScopedObject obj(scope, o); + roleIndex = e->setVariantMapProperty(role, obj); + } + } + } else if (propertyValue->isNullOrUndefined()) { + const ListLayout::Role *r = m_layout->getExistingRole(propertyName); + if (r) + e->clearProperty(*r); + } + + if (roleIndex != -1) + roles->append(roleIndex); + } + + if (ModelNodeMetaObject *mo = e->objectCache()) + mo->updateValues(*roles); +} + +void ListModel::set(int elementIndex, QV4::Object *object) +{ + if (!object) + return; + + ListElement *e = elements[elementIndex]; + + QV4::ExecutionEngine *v4 = object->engine(); + QV4::Scope scope(v4); + + QV4::ObjectIterator it(scope, object, QV4::ObjectIterator::EnumerableOnly); + QV4::ScopedString propertyName(scope); + QV4::ScopedValue propertyValue(scope); + QV4::ScopedObject o(scope); + while (1) { + propertyName = it.nextPropertyNameAsString(propertyValue); + if (!propertyName) + break; + + // Add the value now + if (QV4::String *s = propertyValue->stringValue()) { + const ListLayout::Role &r = m_layout->getRoleOrCreate(propertyName, ListLayout::Role::String); + if (r.type == ListLayout::Role::String) + e->setStringPropertyFast(r, s->toQString()); + } else if (propertyValue->isNumber()) { + const ListLayout::Role &r = m_layout->getRoleOrCreate(propertyName, ListLayout::Role::Number); + if (r.type == ListLayout::Role::Number) { + e->setDoublePropertyFast(r, propertyValue->asDouble()); + } + } else if (QV4::ArrayObject *a = propertyValue->as<QV4::ArrayObject>()) { + const ListLayout::Role &r = m_layout->getRoleOrCreate(propertyName, ListLayout::Role::List); + if (r.type == ListLayout::Role::List) { + ListModel *subModel = new ListModel(r.subLayout, nullptr); + + int arrayLength = a->getLength(); + for (int j=0 ; j < arrayLength ; ++j) { + o = a->get(j); + subModel->append(o); + } + + e->setListPropertyFast(r, subModel); + } + } else if (propertyValue->isBoolean()) { + const ListLayout::Role &r = m_layout->getRoleOrCreate(propertyName, ListLayout::Role::Bool); + if (r.type == ListLayout::Role::Bool) { + e->setBoolPropertyFast(r, propertyValue->booleanValue()); + } + } else if (QV4::DateObject *date = propertyValue->as<QV4::DateObject>()) { + const ListLayout::Role &r = m_layout->getRoleOrCreate(propertyName, ListLayout::Role::DateTime); + if (r.type == ListLayout::Role::DateTime) { + QDateTime dt = date->toQDateTime();; + e->setDateTimePropertyFast(r, dt); + } + } else if (QV4::Object *o = propertyValue->as<QV4::Object>()) { + if (QV4::QObjectWrapper *wrapper = o->as<QV4::QObjectWrapper>()) { + QObject *o = wrapper->object(); + const ListLayout::Role &r = m_layout->getRoleOrCreate(propertyName, ListLayout::Role::QObject); + if (r.type == ListLayout::Role::QObject) + e->setQObjectPropertyFast(r, o); + } else { + const ListLayout::Role &role = m_layout->getRoleOrCreate(propertyName, ListLayout::Role::VariantMap); + if (role.type == ListLayout::Role::VariantMap) + e->setVariantMapFast(role, o); + } + } else if (propertyValue->isNullOrUndefined()) { + const ListLayout::Role *r = m_layout->getExistingRole(propertyName); + if (r) + e->clearProperty(*r); + } + } +} + +QVector<std::function<void()>> ListModel::remove(int index, int count) +{ + QVector<std::function<void()>> toDestroy; + auto layout = m_layout; + for (int i=0 ; i < count ; ++i) { + auto element = elements[index+i]; + toDestroy.append([element, layout](){ + element->destroy(layout); + delete element; + }); + } + elements.remove(index, count); + updateCacheIndices(index); + return toDestroy; +} + +void ListModel::insert(int elementIndex, QV4::Object *object) +{ + insertElement(elementIndex); + set(elementIndex, object); +} + +int ListModel::append(QV4::Object *object) +{ + int elementIndex = appendElement(); + set(elementIndex, object); + return elementIndex; +} + +int ListModel::setOrCreateProperty(int elementIndex, const QString &key, const QVariant &data) +{ + int roleIndex = -1; + + if (elementIndex >= 0 && elementIndex < elements.count()) { + ListElement *e = elements[elementIndex]; + + const ListLayout::Role *r = m_layout->getRoleOrCreate(key, data); + if (r) { + roleIndex = e->setVariantProperty(*r, data); + + ModelNodeMetaObject *cache = e->objectCache(); + + if (roleIndex != -1 && cache) + cache->updateValues(QVector<int>(1, roleIndex)); + } + } + + return roleIndex; +} + +int ListModel::setExistingProperty(int elementIndex, const QString &key, const QV4::Value &data, QV4::ExecutionEngine *eng) +{ + int roleIndex = -1; + + if (elementIndex >= 0 && elementIndex < elements.count()) { + ListElement *e = elements[elementIndex]; + const ListLayout::Role *r = m_layout->getExistingRole(key); + if (r) + roleIndex = e->setJsProperty(*r, data, eng); + } + + return roleIndex; +} + +inline char *ListElement::getPropertyMemory(const ListLayout::Role &role) +{ + ListElement *e = this; + int blockIndex = 0; + while (blockIndex < role.blockIndex) { + if (e->next == nullptr) { + e->next = new ListElement; + e->next->uid = uid; + } + e = e->next; + ++blockIndex; + } + + char *mem = &e->data[role.blockOffset]; + return mem; +} + +ModelNodeMetaObject *ListElement::objectCache() +{ + if (!m_objectCache) + return nullptr; + return ModelNodeMetaObject::get(m_objectCache); +} + +StringOrTranslation *ListElement::getStringProperty(const ListLayout::Role &role) +{ + char *mem = getPropertyMemory(role); + StringOrTranslation *s = reinterpret_cast<StringOrTranslation *>(mem); + return s; +} + +QObject *ListElement::getQObjectProperty(const ListLayout::Role &role) +{ + char *mem = getPropertyMemory(role); + QPointer<QObject> *o = reinterpret_cast<QPointer<QObject> *>(mem); + return o->data(); +} + +QVariantMap *ListElement::getVariantMapProperty(const ListLayout::Role &role) +{ + QVariantMap *map = nullptr; + + char *mem = getPropertyMemory(role); + if (isMemoryUsed<QVariantMap>(mem)) + map = reinterpret_cast<QVariantMap *>(mem); + + return map; +} + +QDateTime *ListElement::getDateTimeProperty(const ListLayout::Role &role) +{ + QDateTime *dt = nullptr; + + char *mem = getPropertyMemory(role); + if (isMemoryUsed<QDateTime>(mem)) + dt = reinterpret_cast<QDateTime *>(mem); + + return dt; +} + +QJSValue *ListElement::getFunctionProperty(const ListLayout::Role &role) +{ + QJSValue *f = nullptr; + + char *mem = getPropertyMemory(role); + if (isMemoryUsed<QJSValue>(mem)) + f = reinterpret_cast<QJSValue *>(mem); + + return f; +} + +QPointer<QObject> *ListElement::getGuardProperty(const ListLayout::Role &role) +{ + char *mem = getPropertyMemory(role); + + bool existingGuard = false; + for (size_t i=0 ; i < sizeof(QPointer<QObject>) ; ++i) { + if (mem[i] != 0) { + existingGuard = true; + break; + } + } + + QPointer<QObject> *o = nullptr; + + if (existingGuard) + o = reinterpret_cast<QPointer<QObject> *>(mem); + + return o; +} + +ListModel *ListElement::getListProperty(const ListLayout::Role &role) +{ + char *mem = getPropertyMemory(role); + ListModel **value = reinterpret_cast<ListModel **>(mem); + return *value; +} + +QVariant ListElement::getProperty(const ListLayout::Role &role, const QQmlListModel *owner, QV4::ExecutionEngine *eng) +{ + char *mem = getPropertyMemory(role); + + QVariant data; + + switch (role.type) { + case ListLayout::Role::Number: + { + double *value = reinterpret_cast<double *>(mem); + data = *value; + } + break; + case ListLayout::Role::String: + { + StringOrTranslation *value = reinterpret_cast<StringOrTranslation *>(mem); + if (value->isSet()) + data = value->toString(owner); + } + break; + case ListLayout::Role::Bool: + { + bool *value = reinterpret_cast<bool *>(mem); + data = *value; + } + break; + case ListLayout::Role::List: + { + ListModel **value = reinterpret_cast<ListModel **>(mem); + ListModel *model = *value; + + if (model) { + if (model->m_modelCache == nullptr) { + model->m_modelCache = new QQmlListModel(owner, model, eng); + QQmlEngine::setContextForObject(model->m_modelCache, QQmlEngine::contextForObject(owner)); + } + + QObject *object = model->m_modelCache; + data = QVariant::fromValue(object); + } + } + break; + case ListLayout::Role::QObject: + { + QPointer<QObject> *guard = reinterpret_cast<QPointer<QObject> *>(mem); + QObject *object = guard->data(); + if (object) + data = QVariant::fromValue(object); + } + break; + case ListLayout::Role::VariantMap: + { + if (isMemoryUsed<QVariantMap>(mem)) { + QVariantMap *map = reinterpret_cast<QVariantMap *>(mem); + data = *map; + } + } + break; + case ListLayout::Role::DateTime: + { + if (isMemoryUsed<QDateTime>(mem)) { + QDateTime *dt = reinterpret_cast<QDateTime *>(mem); + data = *dt; + } + } + break; + case ListLayout::Role::Function: + { + if (isMemoryUsed<QJSValue>(mem)) { + QJSValue *func = reinterpret_cast<QJSValue *>(mem); + data = QVariant::fromValue(*func); + } + } + break; + default: + break; + } + + return data; +} + +int ListElement::setStringProperty(const ListLayout::Role &role, const QString &s) +{ + int roleIndex = -1; + + if (role.type == ListLayout::Role::String) { + char *mem = getPropertyMemory(role); + StringOrTranslation *c = reinterpret_cast<StringOrTranslation *>(mem); + bool changed; + if (!c->isSet() || c->isTranslation()) + changed = true; + else + changed = c->asString().compare(s) != 0; + c->setString(s); + if (changed) + roleIndex = role.index; + } + + return roleIndex; +} + +int ListElement::setDoubleProperty(const ListLayout::Role &role, double d) +{ + int roleIndex = -1; + + if (role.type == ListLayout::Role::Number) { + char *mem = getPropertyMemory(role); + double *value = reinterpret_cast<double *>(mem); + bool changed = *value != d; + *value = d; + if (changed) + roleIndex = role.index; + } + + return roleIndex; +} + +int ListElement::setBoolProperty(const ListLayout::Role &role, bool b) +{ + int roleIndex = -1; + + if (role.type == ListLayout::Role::Bool) { + char *mem = getPropertyMemory(role); + bool *value = reinterpret_cast<bool *>(mem); + bool changed = *value != b; + *value = b; + if (changed) + roleIndex = role.index; + } + + return roleIndex; +} + +int ListElement::setListProperty(const ListLayout::Role &role, ListModel *m) +{ + int roleIndex = -1; + + if (role.type == ListLayout::Role::List) { + char *mem = getPropertyMemory(role); + ListModel **value = reinterpret_cast<ListModel **>(mem); + if (*value && *value != m) { + (*value)->destroy(); + delete *value; + } + *value = m; + roleIndex = role.index; + } + + return roleIndex; +} + +int ListElement::setQObjectProperty(const ListLayout::Role &role, QObject *o) +{ + int roleIndex = -1; + + if (role.type == ListLayout::Role::QObject) { + char *mem = getPropertyMemory(role); + QPointer<QObject> *g = reinterpret_cast<QPointer<QObject> *>(mem); + bool existingGuard = false; + for (size_t i=0 ; i < sizeof(QPointer<QObject>) ; ++i) { + if (mem[i] != 0) { + existingGuard = true; + break; + } + } + bool changed; + if (existingGuard) { + changed = g->data() != o; + g->~QPointer(); + } else { + changed = true; + } + new (mem) QPointer<QObject>(o); + if (changed) + roleIndex = role.index; + } + + return roleIndex; +} + +int ListElement::setVariantMapProperty(const ListLayout::Role &role, QV4::Object *o) +{ + int roleIndex = -1; + + if (role.type == ListLayout::Role::VariantMap) { + char *mem = getPropertyMemory(role); + if (isMemoryUsed<QVariantMap>(mem)) { + QVariantMap *map = reinterpret_cast<QVariantMap *>(mem); + map->~QMap(); + } + new (mem) QVariantMap(o->engine()->variantMapFromJS(o)); + roleIndex = role.index; + } + + return roleIndex; +} + +int ListElement::setVariantMapProperty(const ListLayout::Role &role, QVariantMap *m) +{ + int roleIndex = -1; + + if (role.type == ListLayout::Role::VariantMap) { + char *mem = getPropertyMemory(role); + if (isMemoryUsed<QVariantMap>(mem)) { + QVariantMap *map = reinterpret_cast<QVariantMap *>(mem); + if (m && map->isSharedWith(*m)) + return roleIndex; + map->~QMap(); + } else if (!m) { + return roleIndex; + } + if (m) + new (mem) QVariantMap(*m); + else + new (mem) QVariantMap; + roleIndex = role.index; + } + + return roleIndex; +} + +int ListElement::setDateTimeProperty(const ListLayout::Role &role, const QDateTime &dt) +{ + int roleIndex = -1; + + if (role.type == ListLayout::Role::DateTime) { + char *mem = getPropertyMemory(role); + if (isMemoryUsed<QDateTime>(mem)) { + QDateTime *dt = reinterpret_cast<QDateTime *>(mem); + dt->~QDateTime(); + } + new (mem) QDateTime(dt); + roleIndex = role.index; + } + + return roleIndex; +} + +int ListElement::setFunctionProperty(const ListLayout::Role &role, const QJSValue &f) +{ + int roleIndex = -1; + + if (role.type == ListLayout::Role::Function) { + char *mem = getPropertyMemory(role); + if (isMemoryUsed<QJSValue>(mem)) { + QJSValue *f = reinterpret_cast<QJSValue *>(mem); + f->~QJSValue(); + } + new (mem) QJSValue(f); + roleIndex = role.index; + } + + return roleIndex; +} + +int ListElement::setTranslationProperty(const ListLayout::Role &role, const QV4::CompiledData::Binding *b) +{ + int roleIndex = -1; + + if (role.type == ListLayout::Role::String) { + char *mem = getPropertyMemory(role); + StringOrTranslation *s = reinterpret_cast<StringOrTranslation *>(mem); + s->setTranslation(b); + roleIndex = role.index; + } + + return roleIndex; +} + + +void ListElement::setStringPropertyFast(const ListLayout::Role &role, const QString &s) +{ + char *mem = getPropertyMemory(role); + new (mem) StringOrTranslation(s); +} + +void ListElement::setDoublePropertyFast(const ListLayout::Role &role, double d) +{ + char *mem = getPropertyMemory(role); + double *value = new (mem) double; + *value = d; +} + +void ListElement::setBoolPropertyFast(const ListLayout::Role &role, bool b) +{ + char *mem = getPropertyMemory(role); + bool *value = new (mem) bool; + *value = b; +} + +void ListElement::setQObjectPropertyFast(const ListLayout::Role &role, QObject *o) +{ + char *mem = getPropertyMemory(role); + new (mem) QPointer<QObject>(o); +} + +void ListElement::setListPropertyFast(const ListLayout::Role &role, ListModel *m) +{ + char *mem = getPropertyMemory(role); + ListModel **value = new (mem) ListModel *; + *value = m; +} + +void ListElement::setVariantMapFast(const ListLayout::Role &role, QV4::Object *o) +{ + char *mem = getPropertyMemory(role); + QVariantMap *map = new (mem) QVariantMap; + *map = o->engine()->variantMapFromJS(o); +} + +void ListElement::setDateTimePropertyFast(const ListLayout::Role &role, const QDateTime &dt) +{ + char *mem = getPropertyMemory(role); + new (mem) QDateTime(dt); +} + +void ListElement::setFunctionPropertyFast(const ListLayout::Role &role, const QJSValue &f) +{ + char *mem = getPropertyMemory(role); + new (mem) QJSValue(f); +} + +void ListElement::clearProperty(const ListLayout::Role &role) +{ + switch (role.type) { + case ListLayout::Role::String: + setStringProperty(role, QString()); + break; + case ListLayout::Role::Number: + setDoubleProperty(role, 0.0); + break; + case ListLayout::Role::Bool: + setBoolProperty(role, false); + break; + case ListLayout::Role::List: + setListProperty(role, nullptr); + break; + case ListLayout::Role::QObject: + setQObjectProperty(role, nullptr); + break; + case ListLayout::Role::DateTime: + setDateTimeProperty(role, QDateTime()); + break; + case ListLayout::Role::VariantMap: + setVariantMapProperty(role, (QVariantMap *)nullptr); + break; + case ListLayout::Role::Function: + setFunctionProperty(role, QJSValue()); + break; + default: + break; + } +} + +ListElement::ListElement() +{ + m_objectCache = nullptr; + uid = uidCounter.fetchAndAddOrdered(1); + next = nullptr; + memset(data, 0, sizeof(data)); +} + +ListElement::ListElement(int existingUid) +{ + m_objectCache = nullptr; + uid = existingUid; + next = nullptr; + memset(data, 0, sizeof(data)); +} + +ListElement::~ListElement() +{ + delete next; +} + +QVector<int> ListElement::sync(ListElement *src, ListLayout *srcLayout, ListElement *target, ListLayout *targetLayout) +{ + QVector<int> changedRoles; + for (int i=0 ; i < srcLayout->roleCount() ; ++i) { + const ListLayout::Role &srcRole = srcLayout->getExistingRole(i); + const ListLayout::Role &targetRole = targetLayout->getExistingRole(i); + + int roleIndex = -1; + switch (srcRole.type) { + case ListLayout::Role::List: + { + ListModel *srcSubModel = src->getListProperty(srcRole); + ListModel *targetSubModel = target->getListProperty(targetRole); + + if (srcSubModel) { + if (targetSubModel == nullptr) { + targetSubModel = new ListModel(targetRole.subLayout, nullptr); + target->setListPropertyFast(targetRole, targetSubModel); + } + if (ListModel::sync(srcSubModel, targetSubModel)) + roleIndex = targetRole.index; + } + } + break; + case ListLayout::Role::QObject: + { + QObject *object = src->getQObjectProperty(srcRole); + roleIndex = target->setQObjectProperty(targetRole, object); + } + break; + case ListLayout::Role::String: + case ListLayout::Role::Number: + case ListLayout::Role::Bool: + case ListLayout::Role::DateTime: + case ListLayout::Role::Function: + { + QVariant v = src->getProperty(srcRole, nullptr, nullptr); + roleIndex = target->setVariantProperty(targetRole, v); + } + break; + case ListLayout::Role::VariantMap: + { + QVariantMap *map = src->getVariantMapProperty(srcRole); + roleIndex = target->setVariantMapProperty(targetRole, map); + } + break; + default: + break; + } + if (roleIndex >= 0) + changedRoles << roleIndex; + } + + return changedRoles; +} + +void ListElement::destroy(ListLayout *layout) +{ + if (layout) { + for (int i=0 ; i < layout->roleCount() ; ++i) { + const ListLayout::Role &r = layout->getExistingRole(i); + + switch (r.type) { + case ListLayout::Role::String: + { + StringOrTranslation *string = getStringProperty(r); + if (string) + string->~StringOrTranslation(); + } + break; + case ListLayout::Role::List: + { + ListModel *model = getListProperty(r); + if (model) { + model->destroy(); + delete model; + } + } + break; + case ListLayout::Role::QObject: + { + QPointer<QObject> *guard = getGuardProperty(r); + if (guard) + guard->~QPointer(); + } + break; + case ListLayout::Role::VariantMap: + { + QVariantMap *map = getVariantMapProperty(r); + if (map) + map->~QMap(); + } + break; + case ListLayout::Role::DateTime: + { + QDateTime *dt = getDateTimeProperty(r); + if (dt) + dt->~QDateTime(); + } + break; + case ListLayout::Role::Function: + { + QJSValue *f = getFunctionProperty(r); + if (f) + f->~QJSValue(); + } + break; + default: + // other types don't need explicit cleanup. + break; + } + } + + if (m_objectCache) { + m_objectCache->~QObject(); + operator delete(m_objectCache); + } + } + + if (next) + next->destroy(nullptr); + uid = -1; +} + +int ListElement::setVariantProperty(const ListLayout::Role &role, const QVariant &d) +{ + int roleIndex = -1; + + switch (role.type) { + case ListLayout::Role::Number: + roleIndex = setDoubleProperty(role, d.toDouble()); + break; + case ListLayout::Role::String: + if (d.userType() == qMetaTypeId<const QV4::CompiledData::Binding *>()) + roleIndex = setTranslationProperty(role, d.value<const QV4::CompiledData::Binding*>()); + else + roleIndex = setStringProperty(role, d.toString()); + break; + case ListLayout::Role::Bool: + roleIndex = setBoolProperty(role, d.toBool()); + break; + case ListLayout::Role::List: + roleIndex = setListProperty(role, d.value<ListModel *>()); + break; + case ListLayout::Role::VariantMap: { + QVariantMap map = d.toMap(); + roleIndex = setVariantMapProperty(role, &map); + } + break; + case ListLayout::Role::DateTime: + roleIndex = setDateTimeProperty(role, d.toDateTime()); + break; + case ListLayout::Role::Function: + roleIndex = setFunctionProperty(role, d.value<QJSValue>()); + break; + default: + break; + } + + return roleIndex; +} + +int ListElement::setJsProperty(const ListLayout::Role &role, const QV4::Value &d, QV4::ExecutionEngine *eng) +{ + // Check if this key exists yet + int roleIndex = -1; + + QV4::Scope scope(eng); + + // Add the value now + if (d.isString()) { + QString qstr = d.toQString(); + roleIndex = setStringProperty(role, qstr); + } else if (d.isNumber()) { + roleIndex = setDoubleProperty(role, d.asDouble()); + } else if (d.as<QV4::ArrayObject>()) { + QV4::ScopedArrayObject a(scope, d); + if (role.type == ListLayout::Role::List) { + QV4::Scope scope(a->engine()); + QV4::ScopedObject o(scope); + + ListModel *subModel = new ListModel(role.subLayout, nullptr); + int arrayLength = a->getLength(); + for (int j=0 ; j < arrayLength ; ++j) { + o = a->get(j); + subModel->append(o); + } + roleIndex = setListProperty(role, subModel); + } else { + qmlWarning(nullptr) << QStringLiteral("Can't assign to existing role '%1' of different type [%2 -> %3]").arg(role.name).arg(roleTypeName(role.type)).arg(roleTypeName(ListLayout::Role::List)); + } + } else if (d.isBoolean()) { + roleIndex = setBoolProperty(role, d.booleanValue()); + } else if (d.as<QV4::DateObject>()) { + QV4::Scoped<QV4::DateObject> dd(scope, d); + QDateTime dt = dd->toQDateTime(); + roleIndex = setDateTimeProperty(role, dt); + } else if (d.as<QV4::FunctionObject>()) { + QV4::ScopedFunctionObject f(scope, d); + QJSValue jsv; + QJSValuePrivate::setValue(&jsv, eng, f); + roleIndex = setFunctionProperty(role, jsv); + } else if (d.isObject()) { + QV4::ScopedObject o(scope, d); + QV4::QObjectWrapper *wrapper = o->as<QV4::QObjectWrapper>(); + if (role.type == ListLayout::Role::QObject && wrapper) { + QObject *o = wrapper->object(); + roleIndex = setQObjectProperty(role, o); + } else if (role.type == ListLayout::Role::VariantMap) { + roleIndex = setVariantMapProperty(role, o); + } + } else if (d.isNullOrUndefined()) { + clearProperty(role); + } + + return roleIndex; +} + +ModelNodeMetaObject::ModelNodeMetaObject(QObject *object, QQmlListModel *model, int elementIndex) +: QQmlOpenMetaObject(object), m_enabled(false), m_model(model), m_elementIndex(elementIndex), m_initialized(false) +{} + +void ModelNodeMetaObject::initialize() +{ + const int roleCount = m_model->m_listModel->roleCount(); + QVector<QByteArray> properties; + properties.reserve(roleCount); + for (int i = 0 ; i < roleCount ; ++i) { + const ListLayout::Role &role = m_model->m_listModel->getExistingRole(i); + QByteArray name = role.name.toUtf8(); + properties << name; + } + type()->createProperties(properties); + updateValues(); + m_enabled = true; +} + +ModelNodeMetaObject::~ModelNodeMetaObject() +{ +} + +QAbstractDynamicMetaObject *ModelNodeMetaObject::toDynamicMetaObject(QObject *object) +{ + if (!m_initialized) { + m_initialized = true; + initialize(); + } + return QQmlOpenMetaObject::toDynamicMetaObject(object); +} + +ModelNodeMetaObject *ModelNodeMetaObject::get(QObject *obj) +{ + QObjectPrivate *op = QObjectPrivate::get(obj); + return static_cast<ModelNodeMetaObject*>(op->metaObject); +} + +void ModelNodeMetaObject::updateValues() +{ + const int roleCount = m_model->m_listModel->roleCount(); + if (!m_initialized) { + if (roleCount) { + Q_ALLOCA_VAR(int, changedRoles, roleCount * sizeof(int)); + for (int i = 0; i < roleCount; ++i) + changedRoles[i] = i; + emitDirectNotifies(changedRoles, roleCount); + } + return; + } + for (int i=0 ; i < roleCount ; ++i) { + const ListLayout::Role &role = m_model->m_listModel->getExistingRole(i); + QByteArray name = role.name.toUtf8(); + const QVariant &data = m_model->data(m_elementIndex, i); + setValue(name, data, role.type == ListLayout::Role::List); + } +} + +void ModelNodeMetaObject::updateValues(const QVector<int> &roles) +{ + if (!m_initialized) { + emitDirectNotifies(roles.constData(), roles.count()); + return; + } + int roleCount = roles.count(); + for (int i=0 ; i < roleCount ; ++i) { + int roleIndex = roles.at(i); + const ListLayout::Role &role = m_model->m_listModel->getExistingRole(roleIndex); + QByteArray name = role.name.toUtf8(); + const QVariant &data = m_model->data(m_elementIndex, roleIndex); + setValue(name, data, role.type == ListLayout::Role::List); + } +} + +void ModelNodeMetaObject::propertyWritten(int index) +{ + if (!m_enabled) + return; + + QString propName = QString::fromUtf8(name(index)); + const QVariant value = this->value(index); + + QV4::Scope scope(m_model->engine()); + QV4::ScopedValue v(scope, scope.engine->fromVariant(value)); + + int roleIndex = m_model->m_listModel->setExistingProperty(m_elementIndex, propName, v, scope.engine); + if (roleIndex != -1) + m_model->emitItemsChanged(m_elementIndex, 1, QVector<int>(1, roleIndex)); +} + +// Does the emission of the notifiers when we haven't created the meta-object yet +void ModelNodeMetaObject::emitDirectNotifies(const int *changedRoles, int roleCount) +{ + Q_ASSERT(!m_initialized); + QQmlData *ddata = QQmlData::get(object(), /*create*/false); + if (!ddata) + return; + // There's nothing to emit if we're a list model in a worker thread. + if (!qmlEngine(m_model)) + return; + for (int i = 0; i < roleCount; ++i) { + const int changedRole = changedRoles[i]; + QQmlNotifier::notify(ddata, changedRole); + } +} + +namespace QV4 { + +bool ModelObject::virtualPut(Managed *m, PropertyKey id, const Value &value, Value *receiver) +{ + if (!id.isString()) + return Object::virtualPut(m, id, value, receiver); + QString propName = id.toQString(); + + ModelObject *that = static_cast<ModelObject*>(m); + + ExecutionEngine *eng = that->engine(); + const int elementIndex = that->d()->elementIndex(); + int roleIndex = that->d()->m_model->m_listModel->setExistingProperty(elementIndex, propName, value, eng); + if (roleIndex != -1) + that->d()->m_model->emitItemsChanged(elementIndex, 1, QVector<int>(1, roleIndex)); + + ModelNodeMetaObject *mo = ModelNodeMetaObject::get(that->object()); + if (mo->initialized()) + mo->emitPropertyNotification(propName.toUtf8()); + return true; +} + +ReturnedValue ModelObject::virtualGet(const Managed *m, PropertyKey id, const Value *receiver, bool *hasProperty) +{ + if (!id.isString()) + return QObjectWrapper::virtualGet(m, id, receiver, hasProperty); + + const ModelObject *that = static_cast<const ModelObject*>(m); + Scope scope(that); + ScopedString name(scope, id.asStringOrSymbol()); + const ListLayout::Role *role = that->d()->m_model->m_listModel->getExistingRole(name); + if (!role) + return QObjectWrapper::virtualGet(m, id, receiver, hasProperty); + if (hasProperty) + *hasProperty = true; + + if (QQmlEngine *qmlEngine = that->engine()->qmlEngine()) { + QQmlEnginePrivate *ep = QQmlEnginePrivate::get(qmlEngine); + if (ep && ep->propertyCapture) + ep->propertyCapture->captureProperty(that->object(), -1, role->index, /*doNotify=*/ false); + } + + const int elementIndex = that->d()->elementIndex(); + QVariant value = that->d()->m_model->data(elementIndex, role->index); + return that->engine()->fromVariant(value); +} + +ReturnedValue ModelObject::virtualResolveLookupGetter(const Object *object, ExecutionEngine *engine, Lookup *lookup) +{ + lookup->getter = Lookup::getterFallback; + return lookup->getter(lookup, engine, *object); +} + +struct ModelObjectOwnPropertyKeyIterator : ObjectOwnPropertyKeyIterator +{ + int roleNameIndex = 0; + ~ModelObjectOwnPropertyKeyIterator() override = default; + PropertyKey next(const Object *o, Property *pd = nullptr, PropertyAttributes *attrs = nullptr) override; + +}; + +PropertyKey ModelObjectOwnPropertyKeyIterator::next(const Object *o, Property *pd, PropertyAttributes *attrs) +{ + const ModelObject *that = static_cast<const ModelObject *>(o); + + ExecutionEngine *v4 = that->engine(); + if (roleNameIndex < that->listModel()->roleCount()) { + Scope scope(that->engine()); + const ListLayout::Role &role = that->listModel()->getExistingRole(roleNameIndex); + ++roleNameIndex; + ScopedString roleName(scope, v4->newString(role.name)); + if (attrs) + *attrs = QV4::Attr_Data; + if (pd) { + QVariant value = that->d()->m_model->data(that->d()->elementIndex(), role.index); + pd->value = v4->fromVariant(value); + } + return roleName->toPropertyKey(); + } + + // Fall back to QV4::Object as opposed to QV4::QObjectWrapper otherwise it will add + // unnecessary entries that relate to the roles used. These just create extra work + // later on as they will just be ignored. + return ObjectOwnPropertyKeyIterator::next(o, pd, attrs); +} + +OwnPropertyKeyIterator *ModelObject::virtualOwnPropertyKeys(const Object *m, Value *target) +{ + *target = *m; + return new ModelObjectOwnPropertyKeyIterator; +} + +DEFINE_OBJECT_VTABLE(ModelObject); + +} // namespace QV4 + +DynamicRoleModelNode::DynamicRoleModelNode(QQmlListModel *owner, int uid) : m_owner(owner), m_uid(uid), m_meta(new DynamicRoleModelNodeMetaObject(this)) +{ + setNodeUpdatesEnabled(true); +} + +DynamicRoleModelNode *DynamicRoleModelNode::create(const QVariantMap &obj, QQmlListModel *owner) +{ + DynamicRoleModelNode *object = new DynamicRoleModelNode(owner, uidCounter.fetchAndAddOrdered(1)); + QVector<int> roles; + object->updateValues(obj, roles); + return object; +} + +QVector<int> DynamicRoleModelNode::sync(DynamicRoleModelNode *src, DynamicRoleModelNode *target) +{ + QVector<int> changedRoles; + for (int i = 0; i < src->m_meta->count(); ++i) { + const QByteArray &name = src->m_meta->name(i); + QVariant value = src->m_meta->value(i); + + QQmlListModel *srcModel = qobject_cast<QQmlListModel *>(value.value<QObject *>()); + QQmlListModel *targetModel = qobject_cast<QQmlListModel *>(target->m_meta->value(i).value<QObject *>()); + + bool modelHasChanges = false; + if (srcModel) { + if (targetModel == nullptr) + targetModel = QQmlListModel::createWithOwner(target->m_owner); + + modelHasChanges = QQmlListModel::sync(srcModel, targetModel); + + QObject *targetModelObject = targetModel; + value = QVariant::fromValue(targetModelObject); + } else if (targetModel) { + delete targetModel; + } + + if (target->setValue(name, value) || modelHasChanges) + changedRoles << target->m_owner->m_roles.indexOf(QString::fromUtf8(name)); + } + return changedRoles; +} + +void DynamicRoleModelNode::updateValues(const QVariantMap &object, QVector<int> &roles) +{ + for (auto it = object.cbegin(), end = object.cend(); it != end; ++it) { + const QString &key = it.key(); + + int roleIndex = m_owner->m_roles.indexOf(key); + if (roleIndex == -1) { + roleIndex = m_owner->m_roles.count(); + m_owner->m_roles.append(key); + } + + QVariant value = it.value(); + + // A JS array/object is translated into a (hierarchical) QQmlListModel, + // so translate to a variant map/list first with toVariant(). + if (value.userType() == qMetaTypeId<QJSValue>()) + value = value.value<QJSValue>().toVariant(); + + if (value.type() == QVariant::List) { + QQmlListModel *subModel = QQmlListModel::createWithOwner(m_owner); + + QVariantList subArray = value.toList(); + QVariantList::const_iterator subIt = subArray.cbegin(); + QVariantList::const_iterator subEnd = subArray.cend(); + while (subIt != subEnd) { + const QVariantMap &subObject = subIt->toMap(); + subModel->m_modelObjects.append(DynamicRoleModelNode::create(subObject, subModel)); + ++subIt; + } + + QObject *subModelObject = subModel; + value = QVariant::fromValue(subModelObject); + } + + const QByteArray &keyUtf8 = key.toUtf8(); + + QQmlListModel *existingModel = qobject_cast<QQmlListModel *>(m_meta->value(keyUtf8).value<QObject *>()); + delete existingModel; + + if (m_meta->setValue(keyUtf8, value)) + roles << roleIndex; + } +} + +DynamicRoleModelNodeMetaObject::DynamicRoleModelNodeMetaObject(DynamicRoleModelNode *object) + : QQmlOpenMetaObject(object), m_enabled(false), m_owner(object) +{ +} + +DynamicRoleModelNodeMetaObject::~DynamicRoleModelNodeMetaObject() +{ + for (int i=0 ; i < count() ; ++i) { + QQmlListModel *subModel = qobject_cast<QQmlListModel *>(value(i).value<QObject *>()); + delete subModel; + } +} + +void DynamicRoleModelNodeMetaObject::propertyWrite(int index) +{ + if (!m_enabled) + return; + + QVariant v = value(index); + QQmlListModel *model = qobject_cast<QQmlListModel *>(v.value<QObject *>()); + delete model; +} + +void DynamicRoleModelNodeMetaObject::propertyWritten(int index) +{ + if (!m_enabled) + return; + + QQmlListModel *parentModel = m_owner->m_owner; + + QVariant v = value(index); + + // A JS array/object is translated into a (hierarchical) QQmlListModel, + // so translate to a variant map/list first with toVariant(). + if (v.userType() == qMetaTypeId<QJSValue>()) + v= v.value<QJSValue>().toVariant(); + + if (v.type() == QVariant::List) { + QQmlListModel *subModel = QQmlListModel::createWithOwner(parentModel); + + QVariantList subArray = v.toList(); + QVariantList::const_iterator subIt = subArray.cbegin(); + QVariantList::const_iterator subEnd = subArray.cend(); + while (subIt != subEnd) { + const QVariantMap &subObject = subIt->toMap(); + subModel->m_modelObjects.append(DynamicRoleModelNode::create(subObject, subModel)); + ++subIt; + } + + QObject *subModelObject = subModel; + v = QVariant::fromValue(subModelObject); + + setValue(index, v); + } + + int elementIndex = parentModel->m_modelObjects.indexOf(m_owner); + if (elementIndex != -1) { + int roleIndex = parentModel->m_roles.indexOf(QString::fromLatin1(name(index).constData())); + if (roleIndex != -1) + parentModel->emitItemsChanged(elementIndex, 1, QVector<int>(1, roleIndex)); + } +} + +/*! + \qmltype ListModel + \instantiates QQmlListModel + \inqmlmodule QtQml.Models + \ingroup qtquick-models + \brief Defines a free-form list data source. + + The ListModel is a simple container of ListElement definitions, each + containing data roles. The contents can be defined dynamically, or + explicitly in QML. + + The number of elements in the model can be obtained from its \l count property. + A number of familiar methods are also provided to manipulate the contents of the + model, including append(), insert(), move(), remove() and set(). These methods + accept dictionaries as their arguments; these are translated to ListElement objects + by the model. + + Elements can be manipulated via the model using the setProperty() method, which + allows the roles of the specified element to be set and changed. + + \section1 Example Usage + + The following example shows a ListModel containing three elements, with the roles + "name" and "cost". + + \div {class="float-right"} + \inlineimage listmodel.png + \enddiv + + \snippet qml/listmodel/listmodel.qml 0 + + Roles (properties) in each element must begin with a lower-case letter and + should be common to all elements in a model. The ListElement documentation + provides more guidelines for how elements should be defined. + + Since the example model contains an \c id property, it can be referenced + by views, such as the ListView in this example: + + \snippet qml/listmodel/listmodel-simple.qml 0 + \dots 8 + \snippet qml/listmodel/listmodel-simple.qml 1 + + It is possible for roles to contain list data. In the following example we + create a list of fruit attributes: + + \snippet qml/listmodel/listmodel-nested.qml model + + The delegate displays all the fruit attributes: + + \div {class="float-right"} + \inlineimage listmodel-nested.png + \enddiv + + \snippet qml/listmodel/listmodel-nested.qml delegate + + \section1 Modifying List Models + + The content of a ListModel may be created and modified using the clear(), + append(), set(), insert() and setProperty() methods. For example: + + \snippet qml/listmodel/listmodel-modify.qml delegate + + Note that when creating content dynamically the set of available properties + cannot be changed once set. Whatever properties are first added to the model + are the only permitted properties in the model. + + \section1 Using Threaded List Models with WorkerScript + + ListModel can be used together with WorkerScript access a list model + from multiple threads. This is useful if list modifications are + synchronous and take some time: the list operations can be moved to a + different thread to avoid blocking of the main GUI thread. + + Here is an example that uses WorkerScript to periodically append the + current time to a list model: + + \snippet ../quick/threading/threadedlistmodel/timedisplay.qml 0 + + The included file, \tt dataloader.mjs, looks like this: + + \snippet ../quick/threading/threadedlistmodel/dataloader.mjs 0 + + The timer in the main example sends messages to the worker script by calling + \l WorkerScript::sendMessage(). When this message is received, + \c WorkerScript.onMessage() is invoked in \c dataloader.mjs, + which appends the current time to the list model. + + Note the call to sync() from the external thread. + You must call sync() or else the changes made to the list from that + thread will not be reflected in the list model in the main thread. + + \sa {qml-data-models}{Data Models}, {Qt Quick Examples - Threading}, {Qt QML} +*/ + +QQmlListModel::QQmlListModel(QObject *parent) +: QAbstractListModel(parent) +{ + m_mainThread = true; + m_primary = true; + m_agent = nullptr; + m_dynamicRoles = false; + + m_layout = new ListLayout; + m_listModel = new ListModel(m_layout, this); + + m_engine = nullptr; +} + +QQmlListModel::QQmlListModel(const QQmlListModel *owner, ListModel *data, QV4::ExecutionEngine *engine, QObject *parent) +: QAbstractListModel(parent) +{ + m_mainThread = owner->m_mainThread; + m_primary = false; + m_agent = owner->m_agent; + + Q_ASSERT(owner->m_dynamicRoles == false); + m_dynamicRoles = false; + m_layout = nullptr; + m_listModel = data; + + m_engine = engine; + m_compilationUnit = owner->m_compilationUnit; +} + +QQmlListModel::QQmlListModel(QQmlListModel *orig, QQmlListModelWorkerAgent *agent) +: QAbstractListModel(agent) +{ + m_mainThread = false; + m_primary = true; + m_agent = agent; + m_dynamicRoles = orig->m_dynamicRoles; + + m_layout = new ListLayout(orig->m_layout); + m_listModel = new ListModel(m_layout, this); + + if (m_dynamicRoles) + sync(orig, this); + else + ListModel::sync(orig->m_listModel, m_listModel); + + m_engine = nullptr; + m_compilationUnit = orig->m_compilationUnit; +} + +QQmlListModel::~QQmlListModel() +{ + qDeleteAll(m_modelObjects); + + if (m_primary) { + m_listModel->destroy(); + delete m_listModel; + + if (m_mainThread && m_agent) { + m_agent->modelDestroyed(); + m_agent->release(); + } + } + + m_listModel = nullptr; + + delete m_layout; + m_layout = nullptr; +} + +QQmlListModel *QQmlListModel::createWithOwner(QQmlListModel *newOwner) +{ + QQmlListModel *model = new QQmlListModel; + + model->m_mainThread = newOwner->m_mainThread; + model->m_engine = newOwner->m_engine; + model->m_agent = newOwner->m_agent; + model->m_dynamicRoles = newOwner->m_dynamicRoles; + + if (model->m_mainThread && model->m_agent) + model->m_agent->addref(); + + QQmlEngine::setContextForObject(model, QQmlEngine::contextForObject(newOwner)); + + return model; +} + +QV4::ExecutionEngine *QQmlListModel::engine() const +{ + if (m_engine == nullptr) { + m_engine = qmlEngine(this)->handle(); + } + + return m_engine; +} + +bool QQmlListModel::sync(QQmlListModel *src, QQmlListModel *target) +{ + Q_ASSERT(src->m_dynamicRoles && target->m_dynamicRoles); + + bool hasChanges = false; + + target->m_roles = src->m_roles; + + // Build hash of elements <-> uid for each of the lists + QHash<int, ElementSync> elementHash; + for (int i = 0 ; i < target->m_modelObjects.count(); ++i) { + DynamicRoleModelNode *e = target->m_modelObjects.at(i); + int uid = e->getUid(); + ElementSync sync; + sync.target = e; + sync.targetIndex = i; + elementHash.insert(uid, sync); + } + for (int i = 0 ; i < src->m_modelObjects.count(); ++i) { + DynamicRoleModelNode *e = src->m_modelObjects.at(i); + int uid = e->getUid(); + + QHash<int, ElementSync>::iterator it = elementHash.find(uid); + if (it == elementHash.end()) { + ElementSync sync; + sync.src = e; + sync.srcIndex = i; + elementHash.insert(uid, sync); + } else { + ElementSync &sync = it.value(); + sync.src = e; + sync.srcIndex = i; + } + } + + // Get list of elements that are in the target but no longer in the source. These get deleted first. + int rowsRemoved = 0; + for (int i = 0 ; i < target->m_modelObjects.count() ; ++i) { + DynamicRoleModelNode *element = target->m_modelObjects.at(i); + ElementSync &s = elementHash.find(element->getUid()).value(); + Q_ASSERT(s.targetIndex >= 0); + // need to update the targetIndex, to keep it correct after removals + s.targetIndex -= rowsRemoved; + if (s.src == nullptr) { + Q_ASSERT(s.targetIndex == i); + hasChanges = true; + target->beginRemoveRows(QModelIndex(), i, i); + target->m_modelObjects.remove(i, 1); + target->endRemoveRows(); + delete s.target; + ++rowsRemoved; + --i; + continue; + } + } + + // Clear the target list, and append in correct order from the source + target->m_modelObjects.clear(); + for (int i = 0 ; i < src->m_modelObjects.count() ; ++i) { + DynamicRoleModelNode *element = src->m_modelObjects.at(i); + ElementSync &s = elementHash.find(element->getUid()).value(); + Q_ASSERT(s.srcIndex >= 0); + DynamicRoleModelNode *targetElement = s.target; + if (targetElement == nullptr) { + targetElement = new DynamicRoleModelNode(target, element->getUid()); + } + s.changedRoles = DynamicRoleModelNode::sync(element, targetElement); + target->m_modelObjects.append(targetElement); + } + + // now emit the change notifications required. This can be safely done, as we're only emitting changes, moves and inserts, + // so the model indices can't be out of bounds + // + // to ensure things are kept in the correct order, emit inserts and moves first. This shouls ensure all persistent + // model indices are updated correctly + int rowsInserted = 0; + for (int i = 0 ; i < target->m_modelObjects.count() ; ++i) { + DynamicRoleModelNode *element = target->m_modelObjects.at(i); + ElementSync &s = elementHash.find(element->getUid()).value(); + Q_ASSERT(s.srcIndex >= 0); + s.srcIndex += rowsInserted; + if (s.srcIndex != s.targetIndex) { + if (s.targetIndex == -1) { + target->beginInsertRows(QModelIndex(), i, i); + target->endInsertRows(); + } else { + target->beginMoveRows(QModelIndex(), i, i, QModelIndex(), s.srcIndex); + target->endMoveRows(); + } + hasChanges = true; + ++rowsInserted; + } + if (s.targetIndex != -1 && !s.changedRoles.isEmpty()) { + QModelIndex idx = target->createIndex(i, 0); + emit target->dataChanged(idx, idx, s.changedRoles); + hasChanges = true; + } + } + return hasChanges; +} + +void QQmlListModel::emitItemsChanged(int index, int count, const QVector<int> &roles) +{ + if (count <= 0) + return; + + if (m_mainThread) + emit dataChanged(createIndex(index, 0), createIndex(index + count - 1, 0), roles);; +} + +void QQmlListModel::emitItemsAboutToBeInserted(int index, int count) +{ + Q_ASSERT(index >= 0 && count >= 0); + if (m_mainThread) + beginInsertRows(QModelIndex(), index, index + count - 1); +} + +void QQmlListModel::emitItemsInserted() +{ + if (m_mainThread) { + endInsertRows(); + emit countChanged(); + } +} + +QQmlListModelWorkerAgent *QQmlListModel::agent() +{ + if (m_agent) + return m_agent; + + m_agent = new QQmlListModelWorkerAgent(this); + return m_agent; +} + +QModelIndex QQmlListModel::index(int row, int column, const QModelIndex &parent) const +{ + return row >= 0 && row < count() && column == 0 && !parent.isValid() + ? createIndex(row, column) + : QModelIndex(); +} + +int QQmlListModel::rowCount(const QModelIndex &parent) const +{ + return !parent.isValid() ? count() : 0; +} + +QVariant QQmlListModel::data(const QModelIndex &index, int role) const +{ + return data(index.row(), role); +} + +bool QQmlListModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + const int row = index.row(); + if (row >= count() || row < 0) + return false; + + if (m_dynamicRoles) { + const QByteArray property = m_roles.at(role).toUtf8(); + if (m_modelObjects[row]->setValue(property, value)) { + emitItemsChanged(row, 1, QVector<int>(1, role)); + return true; + } + } else { + const ListLayout::Role &r = m_listModel->getExistingRole(role); + const int roleIndex = m_listModel->setOrCreateProperty(row, r.name, value); + if (roleIndex != -1) { + emitItemsChanged(row, 1, QVector<int>(1, role)); + return true; + } + } + + return false; +} + +QVariant QQmlListModel::data(int index, int role) const +{ + QVariant v; + + if (index >= count() || index < 0) + return v; + + if (m_dynamicRoles) + v = m_modelObjects[index]->getValue(m_roles[role]); + else + v = m_listModel->getProperty(index, role, this, engine()); + + return v; +} + +QHash<int, QByteArray> QQmlListModel::roleNames() const +{ + QHash<int, QByteArray> roleNames; + + if (m_dynamicRoles) { + for (int i = 0 ; i < m_roles.count() ; ++i) + roleNames.insert(i, m_roles.at(i).toUtf8()); + } else { + for (int i = 0 ; i < m_listModel->roleCount() ; ++i) { + const ListLayout::Role &r = m_listModel->getExistingRole(i); + roleNames.insert(i, r.name.toUtf8()); + } + } + + return roleNames; +} + +/*! + \qmlproperty bool ListModel::dynamicRoles + + By default, the type of a role is fixed the first time + the role is used. For example, if you create a role called + "data" and assign a number to it, you can no longer assign + a string to the "data" role. However, when the dynamicRoles + property is enabled, the type of a given role is not fixed + and can be different between elements. + + The dynamicRoles property must be set before any data is + added to the ListModel, and must be set from the main + thread. + + A ListModel that has data statically defined (via the + ListElement QML syntax) cannot have the dynamicRoles + property enabled. + + There is a significant performance cost to using a + ListModel with dynamic roles enabled. The cost varies + from platform to platform but is typically somewhere + between 4-6x slower than using static role types. + + Due to the performance cost of using dynamic roles, + they are disabled by default. +*/ +void QQmlListModel::setDynamicRoles(bool enableDynamicRoles) +{ + if (m_mainThread && m_agent == nullptr) { + if (enableDynamicRoles) { + if (m_layout->roleCount()) + qmlWarning(this) << tr("unable to enable dynamic roles as this model is not empty"); + else + m_dynamicRoles = true; + } else { + if (m_roles.count()) { + qmlWarning(this) << tr("unable to enable static roles as this model is not empty"); + } else { + m_dynamicRoles = false; + } + } + } else { + qmlWarning(this) << tr("dynamic role setting must be made from the main thread, before any worker scripts are created"); + } +} + +/*! + \qmlproperty int ListModel::count + The number of data entries in the model. +*/ +int QQmlListModel::count() const +{ + return m_dynamicRoles ? m_modelObjects.count() : m_listModel->elementCount(); +} + +/*! + \qmlmethod ListModel::clear() + + Deletes all content from the model. + + \sa append(), remove() +*/ +void QQmlListModel::clear() +{ + removeElements(0, count()); +} + +/*! + \qmlmethod ListModel::remove(int index, int count = 1) + + Deletes the content at \a index from the model. + + \sa clear() +*/ +void QQmlListModel::remove(QQmlV4Function *args) +{ + int argLength = args->length(); + + if (argLength == 1 || argLength == 2) { + QV4::Scope scope(args->v4engine()); + int index = QV4::ScopedValue(scope, (*args)[0])->toInt32(); + int removeCount = (argLength == 2 ? QV4::ScopedValue(scope, (*args)[1])->toInt32() : 1); + + if (index < 0 || index+removeCount > count() || removeCount <= 0) { + qmlWarning(this) << tr("remove: indices [%1 - %2] out of range [0 - %3]").arg(index).arg(index+removeCount).arg(count()); + return; + } + + removeElements(index, removeCount); + } else { + qmlWarning(this) << tr("remove: incorrect number of arguments"); + } +} + +void QQmlListModel::removeElements(int index, int removeCount) +{ + Q_ASSERT(index >= 0 && removeCount >= 0); + + if (!removeCount) + return; + + if (m_mainThread) + beginRemoveRows(QModelIndex(), index, index + removeCount - 1); + + QVector<std::function<void()>> toDestroy; + if (m_dynamicRoles) { + for (int i=0 ; i < removeCount ; ++i) { + auto modelObject = m_modelObjects[index+i]; + toDestroy.append([modelObject](){ + delete modelObject; + }); + } + m_modelObjects.remove(index, removeCount); + } else { + toDestroy = m_listModel->remove(index, removeCount); + } + + if (m_mainThread) { + endRemoveRows(); + emit countChanged(); + } + for (const auto &destroyer : toDestroy) + destroyer(); +} + +/*! + \qmlmethod ListModel::insert(int index, jsobject dict) + + Adds a new item to the list model at position \a index, with the + values in \a dict. + + \code + fruitModel.insert(2, {"cost": 5.95, "name":"Pizza"}) + \endcode + + The \a index must be to an existing item in the list, or one past + the end of the list (equivalent to append). + + \sa set(), append() +*/ + +void QQmlListModel::insert(QQmlV4Function *args) +{ + if (args->length() == 2) { + QV4::Scope scope(args->v4engine()); + QV4::ScopedValue arg0(scope, (*args)[0]); + int index = arg0->toInt32(); + + if (index < 0 || index > count()) { + qmlWarning(this) << tr("insert: index %1 out of range").arg(index); + return; + } + + QV4::ScopedObject argObject(scope, (*args)[1]); + QV4::ScopedArrayObject objectArray(scope, (*args)[1]); + if (objectArray) { + QV4::ScopedObject argObject(scope); + + int objectArrayLength = objectArray->getLength(); + emitItemsAboutToBeInserted(index, objectArrayLength); + for (int i=0 ; i < objectArrayLength ; ++i) { + argObject = objectArray->get(i); + + if (m_dynamicRoles) { + m_modelObjects.insert(index+i, DynamicRoleModelNode::create(scope.engine->variantMapFromJS(argObject), this)); + } else { + m_listModel->insert(index+i, argObject); + } + } + emitItemsInserted(); + } else if (argObject) { + emitItemsAboutToBeInserted(index, 1); + + if (m_dynamicRoles) { + m_modelObjects.insert(index, DynamicRoleModelNode::create(scope.engine->variantMapFromJS(argObject), this)); + } else { + m_listModel->insert(index, argObject); + } + + emitItemsInserted(); + } else { + qmlWarning(this) << tr("insert: value is not an object"); + } + } else { + qmlWarning(this) << tr("insert: value is not an object"); + } +} + +/*! + \qmlmethod ListModel::move(int from, int to, int n) + + Moves \a n items \a from one position \a to another. + + The from and to ranges must exist; for example, to move the first 3 items + to the end of the list: + + \code + fruitModel.move(0, fruitModel.count - 3, 3) + \endcode + + \sa append() +*/ +void QQmlListModel::move(int from, int to, int n) +{ + if (n == 0 || from == to) + return; + if (!canMove(from, to, n)) { + qmlWarning(this) << tr("move: out of range"); + return; + } + + if (m_mainThread) + beginMoveRows(QModelIndex(), from, from + n - 1, QModelIndex(), to > from ? to + n : to); + + if (m_dynamicRoles) { + + int realFrom = from; + int realTo = to; + int realN = n; + + if (from > to) { + // Only move forwards - flip if backwards moving + int tfrom = from; + int tto = to; + realFrom = tto; + realTo = tto+n; + realN = tfrom-tto; + } + + QPODVector<DynamicRoleModelNode *, 4> store; + for (int i=0 ; i < (realTo-realFrom) ; ++i) + store.append(m_modelObjects[realFrom+realN+i]); + for (int i=0 ; i < realN ; ++i) + store.append(m_modelObjects[realFrom+i]); + for (int i=0 ; i < store.count() ; ++i) + m_modelObjects[realFrom+i] = store[i]; + + } else { + m_listModel->move(from, to, n); + } + + if (m_mainThread) + endMoveRows(); +} + +/*! + \qmlmethod ListModel::append(jsobject dict) + + Adds a new item to the end of the list model, with the + values in \a dict. + + \code + fruitModel.append({"cost": 5.95, "name":"Pizza"}) + \endcode + + \sa set(), remove() +*/ +void QQmlListModel::append(QQmlV4Function *args) +{ + if (args->length() == 1) { + QV4::Scope scope(args->v4engine()); + QV4::ScopedObject argObject(scope, (*args)[0]); + QV4::ScopedArrayObject objectArray(scope, (*args)[0]); + + if (objectArray) { + QV4::ScopedObject argObject(scope); + + int objectArrayLength = objectArray->getLength(); + if (objectArrayLength > 0) { + int index = count(); + emitItemsAboutToBeInserted(index, objectArrayLength); + + for (int i=0 ; i < objectArrayLength ; ++i) { + argObject = objectArray->get(i); + + if (m_dynamicRoles) { + m_modelObjects.append(DynamicRoleModelNode::create(scope.engine->variantMapFromJS(argObject), this)); + } else { + m_listModel->append(argObject); + } + } + + emitItemsInserted(); + } + } else if (argObject) { + int index; + + if (m_dynamicRoles) { + index = m_modelObjects.count(); + emitItemsAboutToBeInserted(index, 1); + m_modelObjects.append(DynamicRoleModelNode::create(scope.engine->variantMapFromJS(argObject), this)); + } else { + index = m_listModel->elementCount(); + emitItemsAboutToBeInserted(index, 1); + m_listModel->append(argObject); + } + + emitItemsInserted(); + } else { + qmlWarning(this) << tr("append: value is not an object"); + } + } else { + qmlWarning(this) << tr("append: value is not an object"); + } +} + +/*! + \qmlmethod object ListModel::get(int index) + + Returns the item at \a index in the list model. This allows the item + data to be accessed or modified from JavaScript: + + \code + Component.onCompleted: { + fruitModel.append({"cost": 5.95, "name":"Jackfruit"}); + console.log(fruitModel.get(0).cost); + fruitModel.get(0).cost = 10.95; + } + \endcode + + The \a index must be an element in the list. + + Note that properties of the returned object that are themselves objects + will also be models, and this get() method is used to access elements: + + \code + fruitModel.append(..., "attributes": + [{"name":"spikes","value":"7mm"}, + {"name":"color","value":"green"}]); + fruitModel.get(0).attributes.get(1).value; // == "green" + \endcode + + \warning The returned object is not guaranteed to remain valid. It + should not be used in \l{Property Binding}{property bindings}. + + \sa append() +*/ +QJSValue QQmlListModel::get(int index) const +{ + QV4::Scope scope(engine()); + QV4::ScopedValue result(scope, QV4::Value::undefinedValue()); + + if (index >= 0 && index < count()) { + + if (m_dynamicRoles) { + DynamicRoleModelNode *object = m_modelObjects[index]; + result = QV4::QObjectWrapper::wrap(scope.engine, object); + } else { + QObject *object = m_listModel->getOrCreateModelObject(const_cast<QQmlListModel *>(this), index); + QQmlData *ddata = QQmlData::get(object); + if (ddata->jsWrapper.isNullOrUndefined()) { + result = scope.engine->memoryManager->allocate<QV4::ModelObject>(object, const_cast<QQmlListModel *>(this)); + // Keep track of the QObjectWrapper in persistent value storage + ddata->jsWrapper.set(scope.engine, result); + } else { + result = ddata->jsWrapper.value(); + } + } + } + + return QJSValue(engine(), result->asReturnedValue()); +} + +/*! + \qmlmethod ListModel::set(int index, jsobject dict) + + Changes the item at \a index in the list model with the + values in \a dict. Properties not appearing in \a dict + are left unchanged. + + \code + fruitModel.set(3, {"cost": 5.95, "name":"Pizza"}) + \endcode + + If \a index is equal to count() then a new item is appended to the + list. Otherwise, \a index must be an element in the list. + + \sa append() +*/ +void QQmlListModel::set(int index, const QJSValue &value) +{ + QV4::Scope scope(engine()); + QV4::ScopedObject object(scope, QJSValuePrivate::getValue(&value)); + + if (!object) { + qmlWarning(this) << tr("set: value is not an object"); + return; + } + if (index > count() || index < 0) { + qmlWarning(this) << tr("set: index %1 out of range").arg(index); + return; + } + + + if (index == count()) { + emitItemsAboutToBeInserted(index, 1); + + if (m_dynamicRoles) { + m_modelObjects.append(DynamicRoleModelNode::create(scope.engine->variantMapFromJS(object), this)); + } else { + m_listModel->insert(index, object); + } + + emitItemsInserted(); + } else { + + QVector<int> roles; + + if (m_dynamicRoles) { + m_modelObjects[index]->updateValues(scope.engine->variantMapFromJS(object), roles); + } else { + m_listModel->set(index, object, &roles); + } + + if (roles.count()) + emitItemsChanged(index, 1, roles); + } +} + +/*! + \qmlmethod ListModel::setProperty(int index, string property, variant value) + + Changes the \a property of the item at \a index in the list model to \a value. + + \code + fruitModel.setProperty(3, "cost", 5.95) + \endcode + + The \a index must be an element in the list. + + \sa append() +*/ +void QQmlListModel::setProperty(int index, const QString& property, const QVariant& value) +{ + if (count() == 0 || index >= count() || index < 0) { + qmlWarning(this) << tr("set: index %1 out of range").arg(index); + return; + } + + if (m_dynamicRoles) { + int roleIndex = m_roles.indexOf(property); + if (roleIndex == -1) { + roleIndex = m_roles.count(); + m_roles.append(property); + } + if (m_modelObjects[index]->setValue(property.toUtf8(), value)) + emitItemsChanged(index, 1, QVector<int>(1, roleIndex)); + } else { + int roleIndex = m_listModel->setOrCreateProperty(index, property, value); + if (roleIndex != -1) + emitItemsChanged(index, 1, QVector<int>(1, roleIndex)); + } +} + +/*! + \qmlmethod ListModel::sync() + + Writes any unsaved changes to the list model after it has been modified + from a worker script. +*/ +void QQmlListModel::sync() +{ + // This is just a dummy method to make it look like sync() exists in + // ListModel (and not just QQmlListModelWorkerAgent) and to let + // us document sync(). + qmlWarning(this) << "List sync() can only be called from a WorkerScript"; +} + +bool QQmlListModelParser::verifyProperty(const QQmlRefPointer<QV4::CompiledData::CompilationUnit> &compilationUnit, const QV4::CompiledData::Binding *binding) +{ + if (binding->type >= QV4::CompiledData::Binding::Type_Object) { + const quint32 targetObjectIndex = binding->value.objectIndex; + const QV4::CompiledData::Object *target = compilationUnit->objectAt(targetObjectIndex); + QString objName = compilationUnit->stringAt(target->inheritedTypeNameIndex); + if (objName != listElementTypeName) { + const QMetaObject *mo = resolveType(objName); + if (mo != &QQmlListElement::staticMetaObject) { + error(target, QQmlListModel::tr("ListElement: cannot contain nested elements")); + return false; + } + listElementTypeName = objName; // cache right name for next time + } + + if (!compilationUnit->stringAt(target->idNameIndex).isEmpty()) { + error(target->locationOfIdProperty, QQmlListModel::tr("ListElement: cannot use reserved \"id\" property")); + return false; + } + + const QV4::CompiledData::Binding *binding = target->bindingTable(); + for (quint32 i = 0; i < target->nBindings; ++i, ++binding) { + QString propName = compilationUnit->stringAt(binding->propertyNameIndex); + if (propName.isEmpty()) { + error(binding, QQmlListModel::tr("ListElement: cannot contain nested elements")); + return false; + } + if (!verifyProperty(compilationUnit, binding)) + return false; + } + } else if (binding->type == QV4::CompiledData::Binding::Type_Script) { + QString scriptStr = binding->valueAsScriptString(compilationUnit.data()); + if (!binding->isFunctionExpression() && !definesEmptyList(scriptStr)) { + QByteArray script = scriptStr.toUtf8(); + bool ok; + evaluateEnum(script, &ok); + if (!ok) { + error(binding, QQmlListModel::tr("ListElement: cannot use script for property value")); + return false; + } + } + } + + return true; +} + +bool QQmlListModelParser::applyProperty(const QQmlRefPointer<QV4::CompiledData::CompilationUnit> &compilationUnit, const QV4::CompiledData::Binding *binding, ListModel *model, int outterElementIndex) +{ + const QString elementName = compilationUnit->stringAt(binding->propertyNameIndex); + + bool roleSet = false; + if (binding->type >= QV4::CompiledData::Binding::Type_Object) { + const quint32 targetObjectIndex = binding->value.objectIndex; + const QV4::CompiledData::Object *target = compilationUnit->objectAt(targetObjectIndex); + + ListModel *subModel = nullptr; + if (outterElementIndex == -1) { + subModel = model; + } else { + const ListLayout::Role &role = model->getOrCreateListRole(elementName); + if (role.type == ListLayout::Role::List) { + subModel = model->getListProperty(outterElementIndex, role); + if (subModel == nullptr) { + subModel = new ListModel(role.subLayout, nullptr); + QVariant vModel = QVariant::fromValue(subModel); + model->setOrCreateProperty(outterElementIndex, elementName, vModel); + } + } + } + + int elementIndex = subModel ? subModel->appendElement() : -1; + + const QV4::CompiledData::Binding *subBinding = target->bindingTable(); + for (quint32 i = 0; i < target->nBindings; ++i, ++subBinding) { + roleSet |= applyProperty(compilationUnit, subBinding, subModel, elementIndex); + } + + } else { + QVariant value; + + if (binding->isTranslationBinding()) { + value = QVariant::fromValue<const QV4::CompiledData::Binding*>(binding); + } else if (binding->evaluatesToString()) { + value = binding->valueAsString(compilationUnit.data()); + } else if (binding->type == QV4::CompiledData::Binding::Type_Number) { + value = binding->valueAsNumber(compilationUnit->constants); + } else if (binding->type == QV4::CompiledData::Binding::Type_Boolean) { + value = binding->valueAsBoolean(); + } else if (binding->type == QV4::CompiledData::Binding::Type_Null) { + value = QVariant::fromValue(nullptr); + } else if (binding->type == QV4::CompiledData::Binding::Type_Script) { + QString scriptStr = binding->valueAsScriptString(compilationUnit.data()); + if (definesEmptyList(scriptStr)) { + const ListLayout::Role &role = model->getOrCreateListRole(elementName); + ListModel *emptyModel = new ListModel(role.subLayout, nullptr); + value = QVariant::fromValue(emptyModel); + } else if (binding->isFunctionExpression()) { + QQmlBinding::Identifier id = binding->value.compiledScriptIndex; + Q_ASSERT(id != QQmlBinding::Invalid); + + auto v4 = compilationUnit->engine; + QV4::Scope scope(v4); + // for now we do not provide a context object; data from the ListElement must be passed to the function + QV4::ScopedContext context(scope, QV4::QmlContext::create(v4->rootContext(), QQmlContextData::get(qmlContext(model->m_modelCache)), nullptr)); + QV4::ScopedFunctionObject function(scope, QV4::FunctionObject::createScriptFunction(context, compilationUnit->runtimeFunctions[id])); + + QV4::ReturnedValue result = function->call(v4->globalObject, nullptr, 0); + + QJSValue v; + QJSValuePrivate::setValue(&v, v4, result); + value.setValue<QJSValue>(v); + } else { + QByteArray script = scriptStr.toUtf8(); + bool ok; + value = evaluateEnum(script, &ok); + } + } else { + Q_UNREACHABLE(); + } + + model->setOrCreateProperty(outterElementIndex, elementName, value); + roleSet = true; + } + return roleSet; +} + +void QQmlListModelParser::verifyBindings(const QQmlRefPointer<QV4::CompiledData::CompilationUnit> &compilationUnit, const QList<const QV4::CompiledData::Binding *> &bindings) +{ + listElementTypeName = QString(); // unknown + + for (const QV4::CompiledData::Binding *binding : bindings) { + QString propName = compilationUnit->stringAt(binding->propertyNameIndex); + if (!propName.isEmpty()) { // isn't default property + error(binding, QQmlListModel::tr("ListModel: undefined property '%1'").arg(propName)); + return; + } + if (!verifyProperty(compilationUnit, binding)) + return; + } +} + +void QQmlListModelParser::applyBindings(QObject *obj, const QQmlRefPointer<QV4::CompiledData::CompilationUnit> &compilationUnit, const QList<const QV4::CompiledData::Binding *> &bindings) +{ + QQmlListModel *rv = static_cast<QQmlListModel *>(obj); + + rv->m_engine = qmlEngine(rv)->handle(); + rv->m_compilationUnit = compilationUnit; + + bool setRoles = false; + + for (const QV4::CompiledData::Binding *binding : bindings) { + if (binding->type != QV4::CompiledData::Binding::Type_Object) + continue; + setRoles |= applyProperty(compilationUnit, binding, rv->m_listModel, /*outter element index*/-1); + } + + if (setRoles == false) + qmlWarning(obj) << "All ListElement declarations are empty, no roles can be created unless dynamicRoles is set."; +} + +bool QQmlListModelParser::definesEmptyList(const QString &s) +{ + if (s.startsWith(QLatin1Char('[')) && s.endsWith(QLatin1Char(']'))) { + for (int i=1; i<s.length()-1; i++) { + if (!s[i].isSpace()) + return false; + } + return true; + } + return false; +} + + +/*! + \qmltype ListElement + \instantiates QQmlListElement + \inqmlmodule QtQml.Models + \brief Defines a data item in a ListModel. + \ingroup qtquick-models + + List elements are defined inside ListModel definitions, and represent items in a + list that will be displayed using ListView or \l Repeater items. + + List elements are defined like other QML elements except that they contain + a collection of \e role definitions instead of properties. Using the same + syntax as property definitions, roles both define how the data is accessed + and include the data itself. + + The names used for roles must begin with a lower-case letter and should be + common to all elements in a given model. Values must be simple constants; either + strings (quoted and optionally within a call to QT_TR_NOOP), boolean values + (true, false), numbers, or enumeration values (such as AlignText.AlignHCenter). + + Beginning with Qt 5.11 ListElement also allows assigning a function declaration to + a role. This allows the definition of ListElements with callable actions. + + \section1 Referencing Roles + + The role names are used by delegates to obtain data from list elements. + Each role name is accessible in the delegate's scope, and refers to the + corresponding role in the current element. Where a role name would be + ambiguous to use, it can be accessed via the \l{ListView::}{model} + property (e.g., \c{model.cost} instead of \c{cost}). + + \section1 Example Usage + + The following model defines a series of list elements, each of which + contain "name" and "cost" roles and their associated values. + + \snippet qml/listmodel/listelements.qml model + + The delegate obtains the name and cost for each element by simply referring + to \c name and \c cost: + + \snippet qml/listmodel/listelements.qml view + + \sa ListModel +*/ + +QT_END_NAMESPACE + +#include "moc_qqmllistmodel_p.cpp" diff --git a/src/qmlmodels/qqmllistmodel_p.h b/src/qmlmodels/qqmllistmodel_p.h new file mode 100644 index 0000000000..4aabd790a5 --- /dev/null +++ b/src/qmlmodels/qqmllistmodel_p.h @@ -0,0 +1,209 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtQml module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QQMLLISTMODEL_H +#define QQMLLISTMODEL_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <private/qtqmlmodelsglobal_p.h> +#include <private/qqmlcustomparser_p.h> + +#include <QtCore/QObject> +#include <QtCore/QStringList> +#include <QtCore/QHash> +#include <QtCore/QList> +#include <QtCore/QVariant> +#include <QtCore/qabstractitemmodel.h> + +#include <private/qv4engine_p.h> +#include <private/qpodvector_p.h> + +QT_REQUIRE_CONFIG(qml_list_model); + +QT_BEGIN_NAMESPACE + + +class QQmlListModelWorkerAgent; +class ListModel; +class ListLayout; + +namespace QV4 { +struct ModelObject; +} + +class Q_QMLMODELS_PRIVATE_EXPORT QQmlListModel : public QAbstractListModel +{ + Q_OBJECT + Q_PROPERTY(int count READ count NOTIFY countChanged) + Q_PROPERTY(bool dynamicRoles READ dynamicRoles WRITE setDynamicRoles) + Q_PROPERTY(QObject *agent READ agent CONSTANT REVISION(14)) + +public: + QQmlListModel(QObject *parent=nullptr); + ~QQmlListModel(); + + QModelIndex index(int row, int column, const QModelIndex &parent) const override; + int rowCount(const QModelIndex &parent) const override; + QVariant data(const QModelIndex &index, int role) const override; + bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override; + QHash<int,QByteArray> roleNames() const override; + + QVariant data(int index, int role) const; + int count() const; + + Q_INVOKABLE void clear(); + Q_INVOKABLE void remove(QQmlV4Function *args); + Q_INVOKABLE void append(QQmlV4Function *args); + Q_INVOKABLE void insert(QQmlV4Function *args); + Q_INVOKABLE QJSValue get(int index) const; + Q_INVOKABLE void set(int index, const QJSValue &value); + Q_INVOKABLE void setProperty(int index, const QString& property, const QVariant& value); + Q_INVOKABLE void move(int from, int to, int count); + Q_INVOKABLE void sync(); + + QQmlListModelWorkerAgent *agent(); + + bool dynamicRoles() const { return m_dynamicRoles; } + void setDynamicRoles(bool enableDynamicRoles); + +Q_SIGNALS: + void countChanged(); + +private: + friend class QQmlListModelParser; + friend class QQmlListModelWorkerAgent; + friend class ModelObject; + friend struct QV4::ModelObject; + friend class ModelNodeMetaObject; + friend class ListModel; + friend class ListElement; + friend class DynamicRoleModelNode; + friend class DynamicRoleModelNodeMetaObject; + friend struct StringOrTranslation; + + // Constructs a flat list model for a worker agent + QQmlListModel(QQmlListModel *orig, QQmlListModelWorkerAgent *agent); + QQmlListModel(const QQmlListModel *owner, ListModel *data, QV4::ExecutionEngine *engine, QObject *parent=nullptr); + + QV4::ExecutionEngine *engine() const; + + inline bool canMove(int from, int to, int n) const { return !(from+n > count() || to+n > count() || from < 0 || to < 0 || n < 0); } + + mutable QQmlListModelWorkerAgent *m_agent; + mutable QV4::ExecutionEngine *m_engine; + QQmlRefPointer<QV4::CompiledData::CompilationUnit> m_compilationUnit; + bool m_mainThread; + bool m_primary; + + bool m_dynamicRoles; + + ListLayout *m_layout; + ListModel *m_listModel; + + QVector<class DynamicRoleModelNode *> m_modelObjects; + QVector<QString> m_roles; + + struct ElementSync + { + DynamicRoleModelNode *src = nullptr; + DynamicRoleModelNode *target = nullptr; + int srcIndex = -1; + int targetIndex = -1; + QVector<int> changedRoles; + }; + + static bool sync(QQmlListModel *src, QQmlListModel *target); + static QQmlListModel *createWithOwner(QQmlListModel *newOwner); + + void emitItemsChanged(int index, int count, const QVector<int> &roles); + void emitItemsAboutToBeInserted(int index, int count); + void emitItemsInserted(); + + void removeElements(int index, int removeCount); +}; + +// ### FIXME +class QQmlListElement : public QObject +{ +Q_OBJECT +}; + +class QQmlListModelParser : public QQmlCustomParser +{ +public: + enum PropertyType { + Invalid, + Boolean, + Number, + String, + Script + }; + + + QQmlListModelParser() : QQmlCustomParser(QQmlCustomParser::AcceptsSignalHandlers) {} + + void verifyBindings(const QQmlRefPointer<QV4::CompiledData::CompilationUnit> &compilationUnit, const QList<const QV4::CompiledData::Binding *> &bindings) override; + void applyBindings(QObject *obj, const QQmlRefPointer<QV4::CompiledData::CompilationUnit> &compilationUnit, const QList<const QV4::CompiledData::Binding *> &bindings) override; + +private: + bool verifyProperty(const QQmlRefPointer<QV4::CompiledData::CompilationUnit> &compilationUnit, const QV4::CompiledData::Binding *binding); + // returns true if a role was set + bool applyProperty(const QQmlRefPointer<QV4::CompiledData::CompilationUnit> &compilationUnit, const QV4::CompiledData::Binding *binding, ListModel *model, int outterElementIndex); + + static bool definesEmptyList(const QString &); + + QString listElementTypeName; +}; + +QT_END_NAMESPACE + +QML_DECLARE_TYPE(QQmlListModel) +QML_DECLARE_TYPE(QQmlListElement) + +#endif // QQMLLISTMODEL_H diff --git a/src/qmlmodels/qqmllistmodel_p_p.h b/src/qmlmodels/qqmllistmodel_p_p.h new file mode 100644 index 0000000000..a0d0e9ad89 --- /dev/null +++ b/src/qmlmodels/qqmllistmodel_p_p.h @@ -0,0 +1,429 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtQml module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QQMLLISTMODEL_P_P_H +#define QQMLLISTMODEL_P_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include "qqmllistmodel_p.h" +#include <private/qtqmlmodelsglobal_p.h> +#include <private/qqmlengine_p.h> +#include <private/qqmlopenmetaobject_p.h> +#include <private/qv4qobjectwrapper_p.h> +#include <qqml.h> + +QT_REQUIRE_CONFIG(qml_list_model); + +QT_BEGIN_NAMESPACE + + +class DynamicRoleModelNode; + +class DynamicRoleModelNodeMetaObject : public QQmlOpenMetaObject +{ +public: + DynamicRoleModelNodeMetaObject(DynamicRoleModelNode *object); + ~DynamicRoleModelNodeMetaObject(); + + bool m_enabled; + +protected: + void propertyWrite(int index) override; + void propertyWritten(int index) override; + +private: + DynamicRoleModelNode *m_owner; +}; + +class DynamicRoleModelNode : public QObject +{ + Q_OBJECT +public: + DynamicRoleModelNode(QQmlListModel *owner, int uid); + + static DynamicRoleModelNode *create(const QVariantMap &obj, QQmlListModel *owner); + + void updateValues(const QVariantMap &object, QVector<int> &roles); + + QVariant getValue(const QString &name) const + { + return m_meta->value(name.toUtf8()); + } + + bool setValue(const QByteArray &name, const QVariant &val) + { + return m_meta->setValue(name, val); + } + + void setNodeUpdatesEnabled(bool enable) + { + m_meta->m_enabled = enable; + } + + int getUid() const + { + return m_uid; + } + + static QVector<int> sync(DynamicRoleModelNode *src, DynamicRoleModelNode *target); + +private: + QQmlListModel *m_owner; + int m_uid; + DynamicRoleModelNodeMetaObject *m_meta; + + friend class DynamicRoleModelNodeMetaObject; +}; + +class ModelNodeMetaObject : public QQmlOpenMetaObject +{ +public: + ModelNodeMetaObject(QObject *object, QQmlListModel *model, int elementIndex); + ~ModelNodeMetaObject(); + + QAbstractDynamicMetaObject *toDynamicMetaObject(QObject *object) override; + + static ModelNodeMetaObject *get(QObject *obj); + + bool m_enabled; + QQmlListModel *m_model; + int m_elementIndex; + + void updateValues(); + void updateValues(const QVector<int> &roles); + + bool initialized() const { return m_initialized; } + +protected: + void propertyWritten(int index) override; + +private: + using QQmlOpenMetaObject::setValue; + + void emitDirectNotifies(const int *changedRoles, int roleCount); + + void initialize(); + bool m_initialized; +}; + +namespace QV4 { + +namespace Heap { + +struct ModelObject : public QObjectWrapper { + void init(QObject *object, QQmlListModel *model) + { + QObjectWrapper::init(object); + m_model = model; + QObjectPrivate *op = QObjectPrivate::get(object); + m_nodeModelMetaObject = static_cast<ModelNodeMetaObject *>(op->metaObject); + } + void destroy() { QObjectWrapper::destroy(); } + int elementIndex() const { return m_nodeModelMetaObject->m_elementIndex; } + QQmlListModel *m_model; + ModelNodeMetaObject *m_nodeModelMetaObject; +}; + +} + +struct ModelObject : public QObjectWrapper +{ + V4_OBJECT2(ModelObject, QObjectWrapper) + V4_NEEDS_DESTROY + + ListModel *listModel() const { return d()->m_model->m_listModel; } + +protected: + static bool virtualPut(Managed *m, PropertyKey id, const Value& value, Value *receiver); + static ReturnedValue virtualGet(const Managed *m, PropertyKey id, const Value *receiver, bool *hasProperty); + static ReturnedValue virtualResolveLookupGetter(const Object *object, ExecutionEngine *engine, Lookup *lookup); + static ReturnedValue lookupGetter(Lookup *l, ExecutionEngine *engine, const Value &object); + static OwnPropertyKeyIterator *virtualOwnPropertyKeys(const Object *m, Value *target); +}; + +} // namespace QV4 + +class ListLayout +{ +public: + ListLayout() : currentBlock(0), currentBlockOffset(0) {} + ListLayout(const ListLayout *other); + ~ListLayout(); + + class Role + { + public: + + Role() : type(Invalid), blockIndex(-1), blockOffset(-1), index(-1), subLayout(0) {} + explicit Role(const Role *other); + ~Role(); + + // This enum must be kept in sync with the roleTypeNames variable in qqmllistmodel.cpp + enum DataType + { + Invalid = -1, + + String, + Number, + Bool, + List, + QObject, + VariantMap, + DateTime, + Function, + + MaxDataType + }; + + QString name; + DataType type; + int blockIndex; + int blockOffset; + int index; + ListLayout *subLayout; + }; + + const Role *getRoleOrCreate(const QString &key, const QVariant &data); + const Role &getRoleOrCreate(QV4::String *key, Role::DataType type); + const Role &getRoleOrCreate(const QString &key, Role::DataType type); + + const Role &getExistingRole(int index) const { return *roles.at(index); } + const Role *getExistingRole(const QString &key) const; + const Role *getExistingRole(QV4::String *key) const; + + int roleCount() const { return roles.count(); } + + static void sync(ListLayout *src, ListLayout *target); + +private: + const Role &createRole(const QString &key, Role::DataType type); + + int currentBlock; + int currentBlockOffset; + QVector<Role *> roles; + QStringHash<Role *> roleHash; +}; + +struct StringOrTranslation +{ + explicit StringOrTranslation(const QString &s); + explicit StringOrTranslation(const QV4::CompiledData::Binding *binding); + ~StringOrTranslation(); + bool isSet() const { return d.flag(); } + bool isTranslation() const { return d.isT2(); } + void setString(const QString &s); + void setTranslation(const QV4::CompiledData::Binding *binding); + QString toString(const QQmlListModel *owner) const; + QString asString() const; +private: + void clear(); + QBiPointer<QStringData, const QV4::CompiledData::Binding> d; +}; + +/*! +\internal +*/ +class ListElement +{ +public: + + ListElement(); + ListElement(int existingUid); + ~ListElement(); + + static QVector<int> sync(ListElement *src, ListLayout *srcLayout, ListElement *target, ListLayout *targetLayout); + + enum + { + BLOCK_SIZE = 64 - sizeof(int) - sizeof(ListElement *) - sizeof(ModelNodeMetaObject *) + }; + +private: + + void destroy(ListLayout *layout); + + int setVariantProperty(const ListLayout::Role &role, const QVariant &d); + + int setJsProperty(const ListLayout::Role &role, const QV4::Value &d, QV4::ExecutionEngine *eng); + + int setStringProperty(const ListLayout::Role &role, const QString &s); + int setDoubleProperty(const ListLayout::Role &role, double n); + int setBoolProperty(const ListLayout::Role &role, bool b); + int setListProperty(const ListLayout::Role &role, ListModel *m); + int setQObjectProperty(const ListLayout::Role &role, QObject *o); + int setVariantMapProperty(const ListLayout::Role &role, QV4::Object *o); + int setVariantMapProperty(const ListLayout::Role &role, QVariantMap *m); + int setDateTimeProperty(const ListLayout::Role &role, const QDateTime &dt); + int setFunctionProperty(const ListLayout::Role &role, const QJSValue &f); + int setTranslationProperty(const ListLayout::Role &role, const QV4::CompiledData::Binding *b); + + void setStringPropertyFast(const ListLayout::Role &role, const QString &s); + void setDoublePropertyFast(const ListLayout::Role &role, double n); + void setBoolPropertyFast(const ListLayout::Role &role, bool b); + void setQObjectPropertyFast(const ListLayout::Role &role, QObject *o); + void setListPropertyFast(const ListLayout::Role &role, ListModel *m); + void setVariantMapFast(const ListLayout::Role &role, QV4::Object *o); + void setDateTimePropertyFast(const ListLayout::Role &role, const QDateTime &dt); + void setFunctionPropertyFast(const ListLayout::Role &role, const QJSValue &f); + + void clearProperty(const ListLayout::Role &role); + + QVariant getProperty(const ListLayout::Role &role, const QQmlListModel *owner, QV4::ExecutionEngine *eng); + ListModel *getListProperty(const ListLayout::Role &role); + StringOrTranslation *getStringProperty(const ListLayout::Role &role); + QObject *getQObjectProperty(const ListLayout::Role &role); + QPointer<QObject> *getGuardProperty(const ListLayout::Role &role); + QVariantMap *getVariantMapProperty(const ListLayout::Role &role); + QDateTime *getDateTimeProperty(const ListLayout::Role &role); + QJSValue *getFunctionProperty(const ListLayout::Role &role); + + inline char *getPropertyMemory(const ListLayout::Role &role); + + int getUid() const { return uid; } + + ModelNodeMetaObject *objectCache(); + + char data[BLOCK_SIZE]; + ListElement *next; + + int uid; + QObject *m_objectCache; + + friend class ListModel; +}; + +/*! +\internal +*/ +class ListModel +{ +public: + + ListModel(ListLayout *layout, QQmlListModel *modelCache); + ~ListModel() {} + + void destroy(); + + int setOrCreateProperty(int elementIndex, const QString &key, const QVariant &data); + int setExistingProperty(int uid, const QString &key, const QV4::Value &data, QV4::ExecutionEngine *eng); + + QVariant getProperty(int elementIndex, int roleIndex, const QQmlListModel *owner, QV4::ExecutionEngine *eng); + ListModel *getListProperty(int elementIndex, const ListLayout::Role &role); + + int roleCount() const + { + return m_layout->roleCount(); + } + + const ListLayout::Role &getExistingRole(int index) const + { + return m_layout->getExistingRole(index); + } + + const ListLayout::Role *getExistingRole(QV4::String *key) const + { + return m_layout->getExistingRole(key); + } + + const ListLayout::Role &getOrCreateListRole(const QString &name) + { + return m_layout->getRoleOrCreate(name, ListLayout::Role::List); + } + + int elementCount() const + { + return elements.count(); + } + + void set(int elementIndex, QV4::Object *object, QVector<int> *roles); + void set(int elementIndex, QV4::Object *object); + + int append(QV4::Object *object); + void insert(int elementIndex, QV4::Object *object); + + Q_REQUIRED_RESULT QVector<std::function<void()>> remove(int index, int count); + + int appendElement(); + void insertElement(int index); + + void move(int from, int to, int n); + + static bool sync(ListModel *src, ListModel *target); + + QObject *getOrCreateModelObject(QQmlListModel *model, int elementIndex); + +private: + QPODVector<ListElement *, 4> elements; + ListLayout *m_layout; + + QQmlListModel *m_modelCache; + + struct ElementSync + { + ListElement *src = nullptr; + ListElement *target = nullptr; + int srcIndex = -1; + int targetIndex = -1; + QVector<int> changedRoles; + }; + + void newElement(int index); + + void updateCacheIndices(int start = 0, int end = -1); + + friend class ListElement; + friend class QQmlListModelWorkerAgent; + friend class QQmlListModelParser; +}; + +QT_END_NAMESPACE + +Q_DECLARE_METATYPE(ListModel *); + +#endif // QQUICKLISTMODEL_P_P_H diff --git a/src/qmlmodels/qqmllistmodelworkeragent.cpp b/src/qmlmodels/qqmllistmodelworkeragent.cpp new file mode 100644 index 0000000000..7e92810f78 --- /dev/null +++ b/src/qmlmodels/qqmllistmodelworkeragent.cpp @@ -0,0 +1,185 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtQml module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "qqmllistmodelworkeragent_p.h" +#include "qqmllistmodel_p_p.h" +#include <private/qqmldata_p.h> +#include <private/qqmlengine_p.h> +#include <qqmlinfo.h> + +#include <QtCore/qcoreevent.h> +#include <QtCore/qcoreapplication.h> +#include <QtCore/qdebug.h> + + +QT_BEGIN_NAMESPACE + +QQmlListModelWorkerAgent::Sync::~Sync() +{ +} + +QQmlListModelWorkerAgent::QQmlListModelWorkerAgent(QQmlListModel *model) +: m_ref(1), m_orig(model), m_copy(new QQmlListModel(model, this)) +{ +} + +QQmlListModelWorkerAgent::~QQmlListModelWorkerAgent() +{ + mutex.lock(); + syncDone.wakeAll(); + mutex.unlock(); +} + +QV4::ExecutionEngine *QQmlListModelWorkerAgent::engine() const +{ + return m_copy->m_engine; +} + +void QQmlListModelWorkerAgent::setEngine(QV4::ExecutionEngine *eng) +{ + if (eng != m_copy->m_engine) { + m_copy->m_engine = eng; + emit engineChanged(eng); + } +} + +void QQmlListModelWorkerAgent::addref() +{ + m_ref.ref(); +} + +void QQmlListModelWorkerAgent::release() +{ + bool del = !m_ref.deref(); + + if (del) + deleteLater(); +} + +void QQmlListModelWorkerAgent::modelDestroyed() +{ + m_orig = nullptr; +} + +int QQmlListModelWorkerAgent::count() const +{ + return m_copy->count(); +} + +void QQmlListModelWorkerAgent::clear() +{ + m_copy->clear(); +} + +void QQmlListModelWorkerAgent::remove(QQmlV4Function *args) +{ + m_copy->remove(args); +} + +void QQmlListModelWorkerAgent::append(QQmlV4Function *args) +{ + m_copy->append(args); +} + +void QQmlListModelWorkerAgent::insert(QQmlV4Function *args) +{ + m_copy->insert(args); +} + +QJSValue QQmlListModelWorkerAgent::get(int index) const +{ + return m_copy->get(index); +} + +void QQmlListModelWorkerAgent::set(int index, const QJSValue &value) +{ + m_copy->set(index, value); +} + +void QQmlListModelWorkerAgent::setProperty(int index, const QString& property, const QVariant& value) +{ + m_copy->setProperty(index, property, value); +} + +void QQmlListModelWorkerAgent::move(int from, int to, int count) +{ + m_copy->move(from, to, count); +} + +void QQmlListModelWorkerAgent::sync() +{ + Sync *s = new Sync(m_copy); + + mutex.lock(); + QCoreApplication::postEvent(this, s); + syncDone.wait(&mutex); + mutex.unlock(); +} + +bool QQmlListModelWorkerAgent::event(QEvent *e) +{ + if (e->type() == QEvent::User) { + bool cc = false; + QMutexLocker locker(&mutex); + if (m_orig) { + Sync *s = static_cast<Sync *>(e); + + cc = (m_orig->count() != s->list->count()); + + Q_ASSERT(m_orig->m_dynamicRoles == s->list->m_dynamicRoles); + if (m_orig->m_dynamicRoles) + QQmlListModel::sync(s->list, m_orig); + else + ListModel::sync(s->list->m_listModel, m_orig->m_listModel); + } + + syncDone.wakeAll(); + locker.unlock(); + + if (cc) + emit m_orig->countChanged(); + return true; + } + + return QObject::event(e); +} + +QT_END_NAMESPACE + +#include "moc_qqmllistmodelworkeragent_p.cpp" diff --git a/src/qmlmodels/qqmllistmodelworkeragent_p.h b/src/qmlmodels/qqmllistmodelworkeragent_p.h new file mode 100644 index 0000000000..f79c0c557a --- /dev/null +++ b/src/qmlmodels/qqmllistmodelworkeragent_p.h @@ -0,0 +1,128 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtQml module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QQUICKLISTMODELWORKERAGENT_P_H +#define QQUICKLISTMODELWORKERAGENT_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <qtqmlmodelsglobal_p.h> + +#include <QEvent> +#include <QMutex> +#include <QWaitCondition> + +#include <private/qv8engine_p.h> + +QT_REQUIRE_CONFIG(qml_list_model); + +QT_BEGIN_NAMESPACE + + +class QQmlListModel; + +class QQmlListModelWorkerAgent : public QObject +{ + Q_OBJECT + Q_PROPERTY(int count READ count) + Q_PROPERTY(QV4::ExecutionEngine *engine READ engine WRITE setEngine NOTIFY engineChanged) + +public: + QQmlListModelWorkerAgent(QQmlListModel *); + ~QQmlListModelWorkerAgent(); + + QV4::ExecutionEngine *engine() const; + void setEngine(QV4::ExecutionEngine *eng); + + Q_INVOKABLE void addref(); + Q_INVOKABLE void release(); + + int count() const; + + Q_INVOKABLE void clear(); + Q_INVOKABLE void remove(QQmlV4Function *args); + Q_INVOKABLE void append(QQmlV4Function *args); + Q_INVOKABLE void insert(QQmlV4Function *args); + Q_INVOKABLE QJSValue get(int index) const; + Q_INVOKABLE void set(int index, const QJSValue &value); + Q_INVOKABLE void setProperty(int index, const QString& property, const QVariant& value); + Q_INVOKABLE void move(int from, int to, int count); + Q_INVOKABLE void sync(); + + void modelDestroyed(); + +signals: + void engineChanged(QV4::ExecutionEngine *engine); + +protected: + bool event(QEvent *) override; + +private: + friend class QQuickWorkerScriptEnginePrivate; + friend class QQmlListModel; + + struct Sync : public QEvent { + Sync(QQmlListModel *l) + : QEvent(QEvent::User) + , list(l) + {} + ~Sync(); + QQmlListModel *list; + }; + + QAtomicInt m_ref; + QQmlListModel *m_orig; + QQmlListModel *m_copy; + QMutex mutex; + QWaitCondition syncDone; +}; + +QT_END_NAMESPACE + +#endif // QQUICKLISTMODELWORKERAGENT_P_H + diff --git a/src/qmlmodels/qqmlmodelsmodule.cpp b/src/qmlmodels/qqmlmodelsmodule.cpp new file mode 100644 index 0000000000..989fec9b7d --- /dev/null +++ b/src/qmlmodels/qqmlmodelsmodule.cpp @@ -0,0 +1,124 @@ +/**************************************************************************** +** +** Copyright (C) 2016 Research In Motion. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtQml module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "qqmlmodelsmodule_p.h" +#include <private/qtqmlmodelsglobal_p.h> +#include <QtCore/qitemselectionmodel.h> +#if QT_CONFIG(qml_list_model) +#include <private/qqmllistmodel_p.h> +#endif +#if QT_CONFIG(qml_delegate_model) +#include <private/qqmldelegatemodel_p.h> +#include <private/qqmldelegatecomponent_p.h> +#endif +#include <private/qqmlobjectmodel_p.h> +#include <private/qqmltablemodel_p.h> +#include <private/qqmltablemodelcolumn_p.h> +#include <private/qqmlinstantiator_p.h> +#include <private/qquickpackage_p.h> + +QT_BEGIN_NAMESPACE + +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + +void QQmlModelsModule::registerQmlTypes() +{ + // Don't add anything here. These are only for backwards compatibility. + qmlRegisterType<QQmlInstantiator>("QtQml", 2, 1, "Instantiator"); // Only available in >= 2.1 + qmlRegisterType<QQmlInstanceModel>(); +} + +void QQmlModelsModule::registerQuickTypes() +{ + // Don't add anything here. These are only for backwards compatibility. + + const char uri[] = "QtQuick"; + + qmlRegisterType<QQmlInstantiator>(uri, 2, 1, "Instantiator"); + qmlRegisterType<QQmlInstanceModel>(); +#if QT_CONFIG(qml_list_model) + qmlRegisterType<QQmlListElement>(uri, 2, 0, "ListElement"); + qmlRegisterCustomType<QQmlListModel>(uri, 2, 0, "ListModel", new QQmlListModelParser); +#endif + qmlRegisterType<QQuickPackage>(uri, 2, 0, "Package"); +#if QT_CONFIG(qml_delegate_model) + qmlRegisterType<QQmlDelegateModel>(uri, 2, 0, "VisualDataModel"); + qmlRegisterType<QQmlDelegateModelGroup>(uri, 2, 0, "VisualDataGroup"); +#endif + qmlRegisterType<QQmlObjectModel>(uri, 2, 0, "VisualItemModel"); +} + +#endif // QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + +void QQmlModelsModule::defineModule() +{ + const char uri[] = "QtQml.Models"; + +#if QT_CONFIG(qml_list_model) + qmlRegisterType<QQmlListElement>(uri, 2, 1, "ListElement"); + qmlRegisterCustomType<QQmlListModel>(uri, 2, 1, "ListModel", new QQmlListModelParser); +#endif +#if QT_CONFIG(qml_delegate_model) + qmlRegisterType<QQmlDelegateModel>(uri, 2, 1, "DelegateModel"); + qmlRegisterType<QQmlDelegateModelGroup>(uri, 2, 1, "DelegateModelGroup"); +#endif + qmlRegisterType<QQmlObjectModel>(uri, 2, 1, "ObjectModel"); + qmlRegisterType<QQmlObjectModel,3>(uri, 2, 3, "ObjectModel"); + + qmlRegisterType<QItemSelectionModel>(uri, 2, 2, "ItemSelectionModel"); + + qmlRegisterType<QQuickPackage>(uri, 2, 14, "Package"); + qmlRegisterType<QQmlInstantiator>(uri, 2, 14, "Instantiator"); + qmlRegisterType<QQmlInstanceModel>(); +} + +void QQmlModelsModule::defineLabsModule() +{ + const char uri[] = "Qt.labs.qmlmodels"; + +#if QT_CONFIG(qml_delegate_model) + qmlRegisterUncreatableType<QQmlAbstractDelegateComponent>(uri, 1, 0, "AbstractDelegateComponent", QQmlAbstractDelegateComponent::tr("Cannot create instance of abstract class AbstractDelegateComponent.")); + qmlRegisterType<QQmlDelegateChooser>(uri, 1, 0, "DelegateChooser"); + qmlRegisterType<QQmlDelegateChoice>(uri, 1, 0, "DelegateChoice"); +#endif + qmlRegisterType<QQmlTableModel>(uri, 1, 0, "TableModel"); + qmlRegisterType<QQmlTableModelColumn>(uri, 1, 0, "TableModelColumn"); +} + +QT_END_NAMESPACE diff --git a/src/qmlmodels/qqmlmodelsmodule_p.h b/src/qmlmodels/qqmlmodelsmodule_p.h new file mode 100644 index 0000000000..7e02578db9 --- /dev/null +++ b/src/qmlmodels/qqmlmodelsmodule_p.h @@ -0,0 +1,72 @@ +/**************************************************************************** +** +** Copyright (C) 2016 Research In Motion. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtQml module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QQMLMODELSMODULE_H +#define QQMLMODELSMODULE_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <private/qtqmlmodelsglobal_p.h> + +QT_BEGIN_NAMESPACE + +class Q_QMLMODELS_PRIVATE_EXPORT QQmlModelsModule +{ +public: +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + static void registerQmlTypes(); + static void registerQuickTypes(); +#endif + + static void defineModule(); + static void defineLabsModule(); +}; + +QT_END_NAMESPACE + +#endif diff --git a/src/qmlmodels/qqmlobjectmodel.cpp b/src/qmlmodels/qqmlobjectmodel.cpp new file mode 100644 index 0000000000..b6330b4295 --- /dev/null +++ b/src/qmlmodels/qqmlobjectmodel.cpp @@ -0,0 +1,431 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtQml module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "qqmlobjectmodel_p.h" + +#include <QtCore/qcoreapplication.h> +#include <QtQml/qqmlcontext.h> +#include <QtQml/qqmlengine.h> +#include <QtQml/qqmlinfo.h> + +#include <private/qqmlchangeset_p.h> +#include <private/qqmlglobal_p.h> +#include <private/qobject_p.h> +#include <private/qpodvector_p.h> + +#include <QtCore/qhash.h> +#include <QtCore/qlist.h> + +QT_BEGIN_NAMESPACE + +QHash<QObject*, QQmlObjectModelAttached*> QQmlObjectModelAttached::attachedProperties; + + +class QQmlObjectModelPrivate : public QObjectPrivate +{ + Q_DECLARE_PUBLIC(QQmlObjectModel) +public: + class Item { + public: + Item(QObject *i) : item(i), ref(0) {} + + void addRef() { ++ref; } + bool deref() { return --ref == 0; } + + QObject *item; + int ref; + }; + + QQmlObjectModelPrivate() : QObjectPrivate(), moveId(0) {} + + static void children_append(QQmlListProperty<QObject> *prop, QObject *item) { + int index = static_cast<QQmlObjectModelPrivate *>(prop->data)->children.count(); + static_cast<QQmlObjectModelPrivate *>(prop->data)->insert(index, item); + } + + static int children_count(QQmlListProperty<QObject> *prop) { + return static_cast<QQmlObjectModelPrivate *>(prop->data)->children.count(); + } + + static QObject *children_at(QQmlListProperty<QObject> *prop, int index) { + return static_cast<QQmlObjectModelPrivate *>(prop->data)->children.at(index).item; + } + + static void children_clear(QQmlListProperty<QObject> *prop) { + static_cast<QQmlObjectModelPrivate *>(prop->data)->clear(); + } + + void insert(int index, QObject *item) { + Q_Q(QQmlObjectModel); + children.insert(index, Item(item)); + for (int i = index; i < children.count(); ++i) { + QQmlObjectModelAttached *attached = QQmlObjectModelAttached::properties(children.at(i).item); + attached->setIndex(i); + } + QQmlChangeSet changeSet; + changeSet.insert(index, 1); + emit q->modelUpdated(changeSet, false); + emit q->countChanged(); + emit q->childrenChanged(); + } + + void move(int from, int to, int n) { + Q_Q(QQmlObjectModel); + if (from > to) { + // Only move forwards - flip if backwards moving + int tfrom = from; + int tto = to; + from = tto; + to = tto+n; + n = tfrom-tto; + } + + QPODVector<QQmlObjectModelPrivate::Item, 4> store; + for (int i = 0; i < to - from; ++i) + store.append(children[from + n + i]); + for (int i = 0; i < n; ++i) + store.append(children[from + i]); + + for (int i = 0; i < store.count(); ++i) { + children[from + i] = store[i]; + QQmlObjectModelAttached *attached = QQmlObjectModelAttached::properties(children.at(from + i).item); + attached->setIndex(from + i); + } + + QQmlChangeSet changeSet; + changeSet.move(from, to, n, ++moveId); + emit q->modelUpdated(changeSet, false); + emit q->childrenChanged(); + } + + void remove(int index, int n) { + Q_Q(QQmlObjectModel); + for (int i = index; i < index + n; ++i) { + QQmlObjectModelAttached *attached = QQmlObjectModelAttached::properties(children.at(i).item); + attached->setIndex(-1); + } + children.erase(children.begin() + index, children.begin() + index + n); + for (int i = index; i < children.count(); ++i) { + QQmlObjectModelAttached *attached = QQmlObjectModelAttached::properties(children.at(i).item); + attached->setIndex(i); + } + QQmlChangeSet changeSet; + changeSet.remove(index, n); + emit q->modelUpdated(changeSet, false); + emit q->countChanged(); + emit q->childrenChanged(); + } + + void clear() { + Q_Q(QQmlObjectModel); + for (const Item &child : qAsConst(children)) + emit q->destroyingItem(child.item); + remove(0, children.count()); + } + + int indexOf(QObject *item) const { + for (int i = 0; i < children.count(); ++i) + if (children.at(i).item == item) + return i; + return -1; + } + + uint moveId; + QList<Item> children; +}; + + +/*! + \qmltype ObjectModel + \instantiates QQmlObjectModel + \inqmlmodule QtQml.Models + \ingroup qtquick-models + \brief Defines a set of items to be used as a model. + + An ObjectModel contains the visual items to be used in a view. + When an ObjectModel is used in a view, the view does not require + a delegate since the ObjectModel already contains the visual + delegate (items). + + An item can determine its index within the + model via the \l{ObjectModel::index}{index} attached property. + + The example below places three colored rectangles in a ListView. + \code + import QtQuick 2.0 + import QtQml.Models 2.1 + + Rectangle { + ObjectModel { + id: itemModel + Rectangle { height: 30; width: 80; color: "red" } + Rectangle { height: 30; width: 80; color: "green" } + Rectangle { height: 30; width: 80; color: "blue" } + } + + ListView { + anchors.fill: parent + model: itemModel + } + } + \endcode + + \image objectmodel.png + + \sa {Qt Quick Examples - Views} +*/ + +QQmlObjectModel::QQmlObjectModel(QObject *parent) + : QQmlInstanceModel(*(new QQmlObjectModelPrivate), parent) +{ +} + +/*! + \qmlattachedproperty int QtQml.Models::ObjectModel::index + This attached property holds the index of this delegate's item within the model. + + It is attached to each instance of the delegate. +*/ + +QQmlListProperty<QObject> QQmlObjectModel::children() +{ + Q_D(QQmlObjectModel); + return QQmlListProperty<QObject>(this, + d, + d->children_append, + d->children_count, + d->children_at, + d->children_clear); +} + +/*! + \qmlproperty int QtQml.Models::ObjectModel::count + + The number of items in the model. This property is readonly. +*/ +int QQmlObjectModel::count() const +{ + Q_D(const QQmlObjectModel); + return d->children.count(); +} + +bool QQmlObjectModel::isValid() const +{ + return true; +} + +QObject *QQmlObjectModel::object(int index, QQmlIncubator::IncubationMode) +{ + Q_D(QQmlObjectModel); + QQmlObjectModelPrivate::Item &item = d->children[index]; + item.addRef(); + if (item.ref == 1) { + emit initItem(index, item.item); + emit createdItem(index, item.item); + } + return item.item; +} + +QQmlInstanceModel::ReleaseFlags QQmlObjectModel::release(QObject *item) +{ + Q_D(QQmlObjectModel); + int idx = d->indexOf(item); + if (idx >= 0) { + if (!d->children[idx].deref()) + return QQmlInstanceModel::Referenced; + } + return nullptr; +} + +QVariant QQmlObjectModel::variantValue(int index, const QString &role) +{ + Q_D(QQmlObjectModel); + if (index < 0 || index >= d->children.count()) + return QString(); + return QQmlEngine::contextForObject(d->children.at(index).item)->contextProperty(role); +} + +QQmlIncubator::Status QQmlObjectModel::incubationStatus(int) +{ + return QQmlIncubator::Ready; +} + +int QQmlObjectModel::indexOf(QObject *item, QObject *) const +{ + Q_D(const QQmlObjectModel); + return d->indexOf(item); +} + +QQmlObjectModelAttached *QQmlObjectModel::qmlAttachedProperties(QObject *obj) +{ + return QQmlObjectModelAttached::properties(obj); +} + +/*! + \qmlmethod object QtQml.Models::ObjectModel::get(int index) + \since 5.6 + + Returns the item at \a index in the model. This allows the item + to be accessed or modified from JavaScript: + + \code + Component.onCompleted: { + objectModel.append(objectComponent.createObject()) + console.log(objectModel.get(0).objectName); + objectModel.get(0).objectName = "first"; + } + \endcode + + The \a index must be an element in the list. + + \sa append() +*/ +QObject *QQmlObjectModel::get(int index) const +{ + Q_D(const QQmlObjectModel); + if (index < 0 || index >= d->children.count()) + return nullptr; + return d->children.at(index).item; +} + +/*! + \qmlmethod QtQml.Models::ObjectModel::append(object item) + \since 5.6 + + Appends a new item to the end of the model. + + \code + objectModel.append(objectComponent.createObject()) + \endcode + + \sa insert(), remove() +*/ +void QQmlObjectModel::append(QObject *object) +{ + Q_D(QQmlObjectModel); + d->insert(count(), object); +} + +/*! + \qmlmethod QtQml.Models::ObjectModel::insert(int index, object item) + \since 5.6 + + Inserts a new item to the model at position \a index. + + \code + objectModel.insert(2, objectComponent.createObject()) + \endcode + + The \a index must be to an existing item in the list, or one past + the end of the list (equivalent to append). + + \sa append(), remove() +*/ +void QQmlObjectModel::insert(int index, QObject *object) +{ + Q_D(QQmlObjectModel); + if (index < 0 || index > count()) { + qmlWarning(this) << tr("insert: index %1 out of range").arg(index); + return; + } + d->insert(index, object); +} + +/*! + \qmlmethod QtQml.Models::ObjectModel::move(int from, int to, int n = 1) + \since 5.6 + + Moves \a n items \a from one position \a to another. + + The from and to ranges must exist; for example, to move the first 3 items + to the end of the model: + + \code + objectModel.move(0, objectModel.count - 3, 3) + \endcode + + \sa append() +*/ +void QQmlObjectModel::move(int from, int to, int n) +{ + Q_D(QQmlObjectModel); + if (n <= 0 || from == to) + return; + if (from < 0 || to < 0 || from + n > count() || to + n > count()) { + qmlWarning(this) << tr("move: out of range"); + return; + } + d->move(from, to, n); +} + +/*! + \qmlmethod QtQml.Models::ObjectModel::remove(int index, int n = 1) + \since 5.6 + + Removes the items at \a index from the model. + + \sa clear() +*/ +void QQmlObjectModel::remove(int index, int n) +{ + Q_D(QQmlObjectModel); + if (index < 0 || n <= 0 || index + n > count()) { + qmlWarning(this) << tr("remove: indices [%1 - %2] out of range [0 - %3]").arg(index).arg(index+n).arg(count()); + return; + } + d->remove(index, n); +} + +/*! + \qmlmethod QtQml.Models::ObjectModel::clear() + \since 5.6 + + Clears all items from the model. + + \sa append(), remove() +*/ +void QQmlObjectModel::clear() +{ + Q_D(QQmlObjectModel); + d->clear(); +} + +QT_END_NAMESPACE + +#include "moc_qqmlobjectmodel_p.cpp" diff --git a/src/qmlmodels/qqmlobjectmodel_p.h b/src/qmlmodels/qqmlobjectmodel_p.h new file mode 100644 index 0000000000..99bfd86269 --- /dev/null +++ b/src/qmlmodels/qqmlobjectmodel_p.h @@ -0,0 +1,194 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtQml module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QQMLINSTANCEMODEL_P_H +#define QQMLINSTANCEMODEL_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <private/qtqmlmodelsglobal_p.h> +#include <private/qqmlincubator_p.h> +#include <QtQml/qqml.h> +#include <QtCore/qobject.h> + +QT_BEGIN_NAMESPACE + +class QObject; +class QQmlChangeSet; +class QAbstractItemModel; + +class Q_QMLMODELS_PRIVATE_EXPORT QQmlInstanceModel : public QObject +{ + Q_OBJECT + + Q_PROPERTY(int count READ count NOTIFY countChanged) + +public: + virtual ~QQmlInstanceModel() {} + + enum ReleaseFlag { Referenced = 0x01, Destroyed = 0x02 }; + Q_DECLARE_FLAGS(ReleaseFlags, ReleaseFlag) + + virtual int count() const = 0; + virtual bool isValid() const = 0; + virtual QObject *object(int index, QQmlIncubator::IncubationMode incubationMode = QQmlIncubator::AsynchronousIfNested) = 0; + virtual ReleaseFlags release(QObject *object) = 0; + virtual void cancel(int) {} + QString stringValue(int index, const QString &role) { return variantValue(index, role).toString(); } + virtual QVariant variantValue(int, const QString &) = 0; + virtual void setWatchedRoles(const QList<QByteArray> &roles) = 0; + virtual QQmlIncubator::Status incubationStatus(int index) = 0; + + virtual int indexOf(QObject *object, QObject *objectContext) const = 0; + virtual const QAbstractItemModel *abstractItemModel() const { return nullptr; } + +Q_SIGNALS: + void countChanged(); + void modelUpdated(const QQmlChangeSet &changeSet, bool reset); + void createdItem(int index, QObject *object); + void initItem(int index, QObject *object); + void destroyingItem(QObject *object); + +protected: + QQmlInstanceModel(QObjectPrivate &dd, QObject *parent = nullptr) + : QObject(dd, parent) {} + +private: + Q_DISABLE_COPY(QQmlInstanceModel) +}; + +class QQmlObjectModelAttached; +class QQmlObjectModelPrivate; +class Q_QMLMODELS_PRIVATE_EXPORT QQmlObjectModel : public QQmlInstanceModel +{ + Q_OBJECT + Q_DECLARE_PRIVATE(QQmlObjectModel) + + Q_PROPERTY(QQmlListProperty<QObject> children READ children NOTIFY childrenChanged DESIGNABLE false) + Q_CLASSINFO("DefaultProperty", "children") + +public: + QQmlObjectModel(QObject *parent=nullptr); + ~QQmlObjectModel() {} + + int count() const override; + bool isValid() const override; + QObject *object(int index, QQmlIncubator::IncubationMode incubationMode = QQmlIncubator::AsynchronousIfNested) override; + ReleaseFlags release(QObject *object) override; + QVariant variantValue(int index, const QString &role) override; + void setWatchedRoles(const QList<QByteArray> &) override {} + QQmlIncubator::Status incubationStatus(int index) override; + + int indexOf(QObject *object, QObject *objectContext) const override; + + QQmlListProperty<QObject> children(); + + static QQmlObjectModelAttached *qmlAttachedProperties(QObject *obj); + + Q_REVISION(3) Q_INVOKABLE QObject *get(int index) const; + Q_REVISION(3) Q_INVOKABLE void append(QObject *object); + Q_REVISION(3) Q_INVOKABLE void insert(int index, QObject *object); + Q_REVISION(3) Q_INVOKABLE void move(int from, int to, int n = 1); + Q_REVISION(3) Q_INVOKABLE void remove(int index, int n = 1); + +public Q_SLOTS: + Q_REVISION(3) void clear(); + +Q_SIGNALS: + void childrenChanged(); + +private: + Q_DISABLE_COPY(QQmlObjectModel) +}; + +class QQmlObjectModelAttached : public QObject +{ + Q_OBJECT + +public: + QQmlObjectModelAttached(QObject *parent) + : QObject(parent), m_index(-1) {} + ~QQmlObjectModelAttached() { + attachedProperties.remove(parent()); + } + + Q_PROPERTY(int index READ index NOTIFY indexChanged) + int index() const { return m_index; } + void setIndex(int idx) { + if (m_index != idx) { + m_index = idx; + Q_EMIT indexChanged(); + } + } + + static QQmlObjectModelAttached *properties(QObject *obj) { + QQmlObjectModelAttached *rv = attachedProperties.value(obj); + if (!rv) { + rv = new QQmlObjectModelAttached(obj); + attachedProperties.insert(obj, rv); + } + return rv; + } + +Q_SIGNALS: + void indexChanged(); + +public: + int m_index; + + static QHash<QObject*, QQmlObjectModelAttached*> attachedProperties; +}; + + +QT_END_NAMESPACE + +QML_DECLARE_TYPE(QQmlInstanceModel) +QML_DECLARE_TYPE(QQmlObjectModel) +QML_DECLARE_TYPEINFO(QQmlObjectModel, QML_HAS_ATTACHED_PROPERTIES) + +#endif // QQMLINSTANCEMODEL_P_H diff --git a/src/qmlmodels/qqmltableinstancemodel.cpp b/src/qmlmodels/qqmltableinstancemodel.cpp new file mode 100644 index 0000000000..b244a007e5 --- /dev/null +++ b/src/qmlmodels/qqmltableinstancemodel.cpp @@ -0,0 +1,547 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtQml module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "qqmltableinstancemodel_p.h" +#include "qqmldelegatecomponent_p.h" + +#include <QtCore/QTimer> + +#include <QtQml/private/qqmlincubator_p.h> +#include <QtQmlModels/private/qqmlchangeset_p.h> +#include <QtQml/private/qqmlcomponent_p.h> + +QT_BEGIN_NAMESPACE + +const char* kModelItemTag = "_tableinstancemodel_modelItem"; + +bool QQmlTableInstanceModel::isDoneIncubating(QQmlDelegateModelItem *modelItem) +{ + if (!modelItem->incubationTask) + return true; + + const auto status = modelItem->incubationTask->status(); + return (status == QQmlIncubator::Ready) || (status == QQmlIncubator::Error); +} + +void QQmlTableInstanceModel::deleteModelItemLater(QQmlDelegateModelItem *modelItem) +{ + Q_ASSERT(modelItem); + + delete modelItem->object; + modelItem->object = nullptr; + + if (modelItem->contextData) { + modelItem->contextData->invalidate(); + Q_ASSERT(modelItem->contextData->refCount == 1); + modelItem->contextData = nullptr; + } + + modelItem->deleteLater(); +} + +QQmlTableInstanceModel::QQmlTableInstanceModel(QQmlContext *qmlContext, QObject *parent) + : QQmlInstanceModel(*(new QObjectPrivate()), parent) + , m_qmlContext(qmlContext) + , m_metaType(new QQmlDelegateModelItemMetaType(m_qmlContext->engine()->handle(), nullptr, QStringList())) +{ +} + +void QQmlTableInstanceModel::useImportVersion(int minorVersion) +{ + m_adaptorModel.useImportVersion(minorVersion); +} + +QQmlTableInstanceModel::~QQmlTableInstanceModel() +{ + for (const auto modelItem : m_modelItems) { + // No item in m_modelItems should be referenced at this point. The view + // should release all its items before it deletes this model. Only model items + // that are still being incubated should be left for us to delete. + Q_ASSERT(modelItem->objectRef == 0); + Q_ASSERT(modelItem->incubationTask); + // Check that we are not being deleted while we're + // in the process of e.g emitting a created signal. + Q_ASSERT(modelItem->scriptRef == 0); + + if (modelItem->object) { + delete modelItem->object; + modelItem->object = nullptr; + modelItem->contextData->invalidate(); + modelItem->contextData = nullptr; + } + } + + deleteAllFinishedIncubationTasks(); + qDeleteAll(m_modelItems); + drainReusableItemsPool(0); +} + +QQmlComponent *QQmlTableInstanceModel::resolveDelegate(int index) +{ + if (m_delegateChooser) { + const int row = m_adaptorModel.rowAt(index); + const int column = m_adaptorModel.columnAt(index); + QQmlComponent *delegate = nullptr; + QQmlAbstractDelegateComponent *chooser = m_delegateChooser; + do { + delegate = chooser->delegate(&m_adaptorModel, row, column); + chooser = qobject_cast<QQmlAbstractDelegateComponent *>(delegate); + } while (chooser); + return delegate; + } + + return m_delegate; +} + +QQmlDelegateModelItem *QQmlTableInstanceModel::resolveModelItem(int index) +{ + // Check if an item for the given index is already loaded and ready + QQmlDelegateModelItem *modelItem = m_modelItems.value(index, nullptr); + if (modelItem) + return modelItem; + + QQmlComponent *delegate = resolveDelegate(index); + if (!delegate) + return nullptr; + + // Check if the pool contains an item that can be reused + modelItem = takeFromReusableItemsPool(delegate); + if (modelItem) { + reuseItem(modelItem, index); + m_modelItems.insert(index, modelItem); + return modelItem; + } + + // Create a new item from scratch + modelItem = m_adaptorModel.createItem(m_metaType, index); + if (modelItem) { + modelItem->delegate = delegate; + m_modelItems.insert(index, modelItem); + return modelItem; + } + + qWarning() << Q_FUNC_INFO << "failed creating a model item for index: " << index; + return nullptr; +} + +QObject *QQmlTableInstanceModel::object(int index, QQmlIncubator::IncubationMode incubationMode) +{ + Q_ASSERT(m_delegate); + Q_ASSERT(index >= 0 && index < m_adaptorModel.count()); + Q_ASSERT(m_qmlContext && m_qmlContext->isValid()); + + QQmlDelegateModelItem *modelItem = resolveModelItem(index); + if (!modelItem) + return nullptr; + + if (modelItem->object) { + // The model item has already been incubated. So + // just bump the ref-count and return it. + modelItem->referenceObject(); + return modelItem->object; + } + + // The object is not ready, and needs to be incubated + incubateModelItem(modelItem, incubationMode); + if (!isDoneIncubating(modelItem)) + return nullptr; + + // Incubation is done, so the task should be removed + Q_ASSERT(!modelItem->incubationTask); + + if (!modelItem->object) { + // The object was incubated synchronously (otherwise we would return above). But since + // we have no object, the incubation must have failed. And when we have no object, there + // should be no object references either. And there should also not be any internal script + // refs at this point. So we delete the model item. + Q_ASSERT(!modelItem->isObjectReferenced()); + Q_ASSERT(!modelItem->isReferenced()); + m_modelItems.remove(modelItem->index); + delete modelItem; + return nullptr; + } + + // Incubation was completed sync and successful + modelItem->referenceObject(); + return modelItem->object; +} + +QQmlInstanceModel::ReleaseFlags QQmlTableInstanceModel::release(QObject *object, ReusableFlag reusable) +{ + Q_ASSERT(object); + auto modelItem = qvariant_cast<QQmlDelegateModelItem *>(object->property(kModelItemTag)); + Q_ASSERT(modelItem); + + if (!modelItem->releaseObject()) + return QQmlDelegateModel::Referenced; + + if (modelItem->isReferenced()) { + // We still have an internal reference to this object, which means that we are told to release an + // object while the createdItem signal for it is still on the stack. This can happen when objects + // are e.g delivered async, and the user flicks back and forth quicker than the loading can catch + // up with. The view might then find that the object is no longer visible and should be released. + // We detect this case in incubatorStatusChanged(), and delete it there instead. But from the callers + // point of view, it should consider it destroyed. + return QQmlDelegateModel::Destroyed; + } + + // The item is not referenced by anyone + m_modelItems.remove(modelItem->index); + + if (reusable == Reusable) { + insertIntoReusableItemsPool(modelItem); + return QQmlInstanceModel::Referenced; + } + + // The item is not reused or referenced by anyone, so just delete it + modelItem->destroyObject(); + emit destroyingItem(object); + + delete modelItem; + return QQmlInstanceModel::Destroyed; +} + +void QQmlTableInstanceModel::cancel(int index) +{ + auto modelItem = m_modelItems.value(index); + Q_ASSERT(modelItem); + + // Since the view expects the item to be incubating, there should be + // an incubation task. And since the incubation is not done, no-one + // should yet have received, and therfore hold a reference to, the object. + Q_ASSERT(modelItem->incubationTask); + Q_ASSERT(!modelItem->isObjectReferenced()); + + m_modelItems.remove(index); + + if (modelItem->object) + delete modelItem->object; + + // modelItem->incubationTask will be deleted from the modelItems destructor + delete modelItem; +} + +void QQmlTableInstanceModel::insertIntoReusableItemsPool(QQmlDelegateModelItem *modelItem) +{ + // Currently, the only way for a view to reuse items is to call QQmlTableInstanceModel::release() + // with the second argument explicitly set to QQmlTableInstanceModel::Reusable. If the released + // item is no longer referenced, it will be added to the pool. Reusing of items can be specified + // per item, in case certain items cannot be recycled. + // A QQmlDelegateModelItem knows which delegate its object was created from. So when we are + // about to create a new item, we first check if the pool contains an item based on the same + // delegate from before. If so, we take it out of the pool (instead of creating a new item), and + // update all its context-, and attached properties. + // When a view is recycling items, it should call QQmlTableInstanceModel::drainReusableItemsPool() + // regularly. As there is currently no logic to 'hibernate' items in the pool, they are only + // meant to rest there for a short while, ideally only from the time e.g a row is unloaded + // on one side of the view, and until a new row is loaded on the opposite side. In-between + // this time, the application will see the item as fully functional and 'alive' (just not + // visible on screen). Since this time is supposed to be short, we don't take any action to + // notify the application about it, since we don't want to trigger any bindings that can + // disturb performance. + // A recommended time for calling drainReusableItemsPool() is each time a view has finished + // loading e.g a new row or column. If there are more items in the pool after that, it means + // that the view most likely doesn't need them anytime soon. Those items should be destroyed to + // not consume resources. + // Depending on if a view is a list or a table, it can sometimes be performant to keep + // items in the pool for a bit longer than one "row out/row in" cycle. E.g for a table, if the + // number of visible rows in a view is much larger than the number of visible columns. + // In that case, if you flick out a row, and then flick in a column, you would throw away a lot + // of items in the pool if completely draining it. The reason is that unloading a row places more + // items in the pool than what ends up being recycled when loading a new column. And then, when you + // next flick in a new row, you would need to load all those drained items again from scratch. For + // that reason, you can specify a maxPoolTime to the drainReusableItemsPool() that allows you to keep + // items in the pool for a bit longer, effectively keeping more items in circulation. + // A recommended maxPoolTime would be equal to the number of dimenstions in the view, which + // means 1 for a list view and 2 for a table view. If you specify 0, all items will be drained. + Q_ASSERT(!modelItem->incubationTask); + Q_ASSERT(!modelItem->isObjectReferenced()); + Q_ASSERT(!modelItem->isReferenced()); + Q_ASSERT(modelItem->object); + + modelItem->poolTime = 0; + m_reusableItemsPool.append(modelItem); + emit itemPooled(modelItem->index, modelItem->object); +} + +QQmlDelegateModelItem *QQmlTableInstanceModel::takeFromReusableItemsPool(const QQmlComponent *delegate) +{ + // Find the oldest item in the pool that was made from the same delegate as + // the given argument, remove it from the pool, and return it. + if (m_reusableItemsPool.isEmpty()) + return nullptr; + + for (auto it = m_reusableItemsPool.begin(); it != m_reusableItemsPool.end(); ++it) { + if ((*it)->delegate != delegate) + continue; + auto modelItem = *it; + m_reusableItemsPool.erase(it); + return modelItem; + } + + return nullptr; +} + +void QQmlTableInstanceModel::drainReusableItemsPool(int maxPoolTime) +{ + // Rather than releasing all pooled items upon a call to this function, each + // item has a poolTime. The poolTime specifies for how many loading cycles an item + // has been resting in the pool. And for each invocation of this function, poolTime + // will increase. If poolTime is equal to, or exceeds, maxPoolTime, it will be removed + // from the pool and released. This way, the view can tweak a bit for how long + // items should stay in "circulation", even if they are not recycled right away. + for (auto it = m_reusableItemsPool.begin(); it != m_reusableItemsPool.end();) { + auto modelItem = *it; + modelItem->poolTime++; + if (modelItem->poolTime <= maxPoolTime) { + ++it; + } else { + it = m_reusableItemsPool.erase(it); + release(modelItem->object, NotReusable); + } + } +} + +void QQmlTableInstanceModel::reuseItem(QQmlDelegateModelItem *item, int newModelIndex) +{ + // Update the context properties index, row and column on + // the delegate item, and inform the application about it. + const int newRow = m_adaptorModel.rowAt(newModelIndex); + const int newColumn = m_adaptorModel.columnAt(newModelIndex); + item->setModelIndex(newModelIndex, newRow, newColumn); + + // Notify the application that all 'dynamic'/role-based context data has + // changed as well (their getter function will use the updated index). + auto const itemAsList = QList<QQmlDelegateModelItem *>() << item; + auto const updateAllRoles = QVector<int>(); + m_adaptorModel.notify(itemAsList, newModelIndex, 1, updateAllRoles); + + // Inform the view that the item is recycled. This will typically result + // in the view updating its own attached delegate item properties. + emit itemReused(newModelIndex, item->object); +} + +void QQmlTableInstanceModel::incubateModelItem(QQmlDelegateModelItem *modelItem, QQmlIncubator::IncubationMode incubationMode) +{ + // Guard the model item temporarily so that it's not deleted from + // incubatorStatusChanged(), in case the incubation is done synchronously. + modelItem->scriptRef++; + + if (modelItem->incubationTask) { + // We're already incubating the model item from a previous request. If the previous call requested + // the item async, but the current request needs it sync, we need to force-complete the incubation. + const bool sync = (incubationMode == QQmlIncubator::Synchronous || incubationMode == QQmlIncubator::AsynchronousIfNested); + if (sync && modelItem->incubationTask->incubationMode() == QQmlIncubator::Asynchronous) + modelItem->incubationTask->forceCompletion(); + } else { + modelItem->incubationTask = new QQmlTableInstanceModelIncubationTask(this, modelItem, incubationMode); + + QQmlContextData *ctxt = new QQmlContextData; + QQmlContext *creationContext = modelItem->delegate->creationContext(); + ctxt->setParent(QQmlContextData::get(creationContext ? creationContext : m_qmlContext.data())); + ctxt->contextObject = modelItem; + modelItem->contextData = ctxt; + + QQmlComponentPrivate::get(modelItem->delegate)->incubateObject( + modelItem->incubationTask, + modelItem->delegate, + m_qmlContext->engine(), + ctxt, + QQmlContextData::get(m_qmlContext)); + } + + // Remove the temporary guard + modelItem->scriptRef--; +} + +void QQmlTableInstanceModel::incubatorStatusChanged(QQmlTableInstanceModelIncubationTask *incubationTask, QQmlIncubator::Status status) +{ + QQmlDelegateModelItem *modelItem = incubationTask->modelItemToIncubate; + Q_ASSERT(modelItem->incubationTask); + + modelItem->incubationTask = nullptr; + incubationTask->modelItemToIncubate = nullptr; + + if (status == QQmlIncubator::Ready) { + // Tag the incubated object with the model item for easy retrieval upon release etc. + modelItem->object->setProperty(kModelItemTag, QVariant::fromValue(modelItem)); + + // Emit that the item has been created. What normally happens next is that the view + // upon receiving the signal asks for the model item once more. And since the item is + // now in the map, it will be returned directly. + Q_ASSERT(modelItem->object); + modelItem->scriptRef++; + emit createdItem(modelItem->index, modelItem->object); + modelItem->scriptRef--; + } else if (status == QQmlIncubator::Error) { + qWarning() << "Error incubating delegate:" << incubationTask->errors(); + } + + if (!modelItem->isReferenced() && !modelItem->isObjectReferenced()) { + // We have no internal reference to the model item, and the view has no + // reference to the incubated object. So just delete the model item. + // Note that being here means that the object was incubated _async_ + // (otherwise modelItem->isReferenced() would be true). + m_modelItems.remove(modelItem->index); + + if (modelItem->object) { + modelItem->scriptRef++; + emit destroyingItem(modelItem->object); + modelItem->scriptRef--; + Q_ASSERT(!modelItem->isReferenced()); + } + + deleteModelItemLater(modelItem); + } + + deleteIncubationTaskLater(incubationTask); +} + +QQmlIncubator::Status QQmlTableInstanceModel::incubationStatus(int index) { + const auto modelItem = m_modelItems.value(index, nullptr); + if (!modelItem) + return QQmlIncubator::Null; + + if (modelItem->incubationTask) + return modelItem->incubationTask->status(); + + // Since we clear the incubation task when we're done + // incubating, it means that the status is Ready. + return QQmlIncubator::Ready; +} + +void QQmlTableInstanceModel::deleteIncubationTaskLater(QQmlIncubator *incubationTask) +{ + // We often need to post-delete incubation tasks, since we cannot + // delete them while we're in the middle of an incubation change callback. + Q_ASSERT(!m_finishedIncubationTasks.contains(incubationTask)); + m_finishedIncubationTasks.append(incubationTask); + if (m_finishedIncubationTasks.count() == 1) + QTimer::singleShot(1, this, &QQmlTableInstanceModel::deleteAllFinishedIncubationTasks); +} + +void QQmlTableInstanceModel::deleteAllFinishedIncubationTasks() +{ + qDeleteAll(m_finishedIncubationTasks); + m_finishedIncubationTasks.clear(); +} + +QVariant QQmlTableInstanceModel::model() const +{ + return m_adaptorModel.model(); +} + +void QQmlTableInstanceModel::setModel(const QVariant &model) +{ + // Pooled items are still accessible/alive for the application, and + // needs to stay in sync with the model. So we need to drain the pool + // completely when the model changes. + drainReusableItemsPool(0); + if (auto const aim = abstractItemModel()) + disconnect(aim, &QAbstractItemModel::dataChanged, this, &QQmlTableInstanceModel::dataChangedCallback); + m_adaptorModel.setModel(model, this, m_qmlContext->engine()); + if (auto const aim = abstractItemModel()) + connect(aim, &QAbstractItemModel::dataChanged, this, &QQmlTableInstanceModel::dataChangedCallback); +} + +void QQmlTableInstanceModel::dataChangedCallback(const QModelIndex &begin, const QModelIndex &end, const QVector<int> &roles) +{ + // This function is called when model data has changed. In that case, we tell the adaptor model + // to go through all the items we have created, find the ones that are affected, and notify that + // their model data has changed. This will in turn update QML bindings inside the delegate items. + int numberOfRowsChanged = end.row() - begin.row() + 1; + int numberOfColumnsChanged = end.column() - begin.column() + 1; + + for (int column = 0; column < numberOfColumnsChanged; ++column) { + const int columnIndex = begin.column() + column; + const int rowIndex = begin.row() + (columnIndex * rows()); + m_adaptorModel.notify(m_modelItems.values(), rowIndex, numberOfRowsChanged, roles); + } +} + +QQmlComponent *QQmlTableInstanceModel::delegate() const +{ + return m_delegate; +} + +void QQmlTableInstanceModel::setDelegate(QQmlComponent *delegate) +{ + if (m_delegate == delegate) + return; + + m_delegateChooser = nullptr; + if (delegate) { + QQmlAbstractDelegateComponent *adc = + qobject_cast<QQmlAbstractDelegateComponent *>(delegate); + if (adc) + m_delegateChooser = adc; + } + + m_delegate = delegate; +} + +const QAbstractItemModel *QQmlTableInstanceModel::abstractItemModel() const +{ + return m_adaptorModel.adaptsAim() ? m_adaptorModel.aim() : nullptr; +} + +// -------------------------------------------------------- + +void QQmlTableInstanceModelIncubationTask::setInitialState(QObject *object) +{ + modelItemToIncubate->object = object; + emit tableInstanceModel->initItem(modelItemToIncubate->index, object); +} + +void QQmlTableInstanceModelIncubationTask::statusChanged(QQmlIncubator::Status status) +{ + if (!QQmlTableInstanceModel::isDoneIncubating(modelItemToIncubate)) + return; + + // We require the view to cancel any ongoing load + // requests before the tableInstanceModel is destructed. + Q_ASSERT(tableInstanceModel); + + tableInstanceModel->incubatorStatusChanged(this, status); +} + +#include "moc_qqmltableinstancemodel_p.cpp" + +QT_END_NAMESPACE + diff --git a/src/qmlmodels/qqmltableinstancemodel_p.h b/src/qmlmodels/qqmltableinstancemodel_p.h new file mode 100644 index 0000000000..20331df5cc --- /dev/null +++ b/src/qmlmodels/qqmltableinstancemodel_p.h @@ -0,0 +1,162 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtQml module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QQMLTABLEINSTANCEMODEL_P_H +#define QQMLTABLEINSTANCEMODEL_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <QtQmlModels/private/qqmldelegatemodel_p.h> +#include <QtQmlModels/private/qqmldelegatemodel_p_p.h> + +QT_BEGIN_NAMESPACE + +class QQmlTableInstanceModel; +class QQmlAbstractDelegateComponent; + +class QQmlTableInstanceModelIncubationTask : public QQDMIncubationTask +{ +public: + QQmlTableInstanceModelIncubationTask( + QQmlTableInstanceModel *tableInstanceModel + , QQmlDelegateModelItem* modelItemToIncubate + , IncubationMode mode) + : QQDMIncubationTask(nullptr, mode) + , modelItemToIncubate(modelItemToIncubate) + , tableInstanceModel(tableInstanceModel) { + clear(); + } + + void statusChanged(Status status) override; + void setInitialState(QObject *object) override; + + QQmlDelegateModelItem *modelItemToIncubate = nullptr; + QQmlTableInstanceModel *tableInstanceModel = nullptr; +}; + +class Q_QMLMODELS_PRIVATE_EXPORT QQmlTableInstanceModel : public QQmlInstanceModel +{ + Q_OBJECT + +public: + + enum ReusableFlag { + NotReusable, + Reusable + }; + + QQmlTableInstanceModel(QQmlContext *qmlContext, QObject *parent = nullptr); + ~QQmlTableInstanceModel() override; + + void useImportVersion(int minorVersion); + + int count() const override { return m_adaptorModel.count(); } + int rows() const { return m_adaptorModel.rowCount(); } + int columns() const { return m_adaptorModel.columnCount(); } + + bool isValid() const override { return true; } + + QVariant model() const; + void setModel(const QVariant &model); + + QQmlComponent *delegate() const; + void setDelegate(QQmlComponent *); + + const QAbstractItemModel *abstractItemModel() const override; + + QObject *object(int index, QQmlIncubator::IncubationMode incubationMode = QQmlIncubator::AsynchronousIfNested) override; + ReleaseFlags release(QObject *object) override { return release(object, NotReusable); } + ReleaseFlags release(QObject *object, ReusableFlag reusable); + void cancel(int) override; + + void insertIntoReusableItemsPool(QQmlDelegateModelItem *modelItem); + QQmlDelegateModelItem *takeFromReusableItemsPool(const QQmlComponent *delegate); + void drainReusableItemsPool(int maxPoolTime); + int poolSize() { return m_reusableItemsPool.size(); } + void reuseItem(QQmlDelegateModelItem *item, int newModelIndex); + + QQmlIncubator::Status incubationStatus(int index) override; + + QVariant variantValue(int, const QString &) override { Q_UNREACHABLE(); return QVariant(); } + void setWatchedRoles(const QList<QByteArray> &) override { Q_UNREACHABLE(); } + int indexOf(QObject *, QObject *) const override { Q_UNREACHABLE(); return 0; } + +Q_SIGNALS: + void itemPooled(int index, QObject *object); + void itemReused(int index, QObject *object); + +private: + QQmlComponent *resolveDelegate(int index); + + QQmlAdaptorModel m_adaptorModel; + QQmlAbstractDelegateComponent *m_delegateChooser = nullptr; + QQmlComponent *m_delegate = nullptr; + QPointer<QQmlContext> m_qmlContext; + QQmlDelegateModelItemMetaType *m_metaType; + + QHash<int, QQmlDelegateModelItem *> m_modelItems; + QList<QQmlDelegateModelItem *> m_reusableItemsPool; + QList<QQmlIncubator *> m_finishedIncubationTasks; + + void incubateModelItem(QQmlDelegateModelItem *modelItem, QQmlIncubator::IncubationMode incubationMode); + void incubatorStatusChanged(QQmlTableInstanceModelIncubationTask *dmIncubationTask, QQmlIncubator::Status status); + void deleteIncubationTaskLater(QQmlIncubator *incubationTask); + void deleteAllFinishedIncubationTasks(); + QQmlDelegateModelItem *resolveModelItem(int index); + + void dataChangedCallback(const QModelIndex &begin, const QModelIndex &end, const QVector<int> &roles); + + static bool isDoneIncubating(QQmlDelegateModelItem *modelItem); + static void deleteModelItemLater(QQmlDelegateModelItem *modelItem); + + friend class QQmlTableInstanceModelIncubationTask; +}; + +QT_END_NAMESPACE + +#endif // QQMLTABLEINSTANCEMODEL_P_H diff --git a/src/qmlmodels/qqmltablemodel.cpp b/src/qmlmodels/qqmltablemodel.cpp new file mode 100644 index 0000000000..4a96e7a46b --- /dev/null +++ b/src/qmlmodels/qqmltablemodel.cpp @@ -0,0 +1,1059 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtQml module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "qqmltablemodel_p.h" + +#include <QtCore/qloggingcategory.h> +#include <QtQml/qqmlinfo.h> +#include <QtQml/qqmlengine.h> + +QT_BEGIN_NAMESPACE + +Q_LOGGING_CATEGORY(lcTableModel, "qt.qml.tablemodel") + +/*! + \qmltype TableModel + \instantiates QQmlTableModel + \inqmlmodule Qt.labs.qmlmodels + \brief Encapsulates a simple table model. + \since 5.14 + + The TableModel type stores JavaScript/JSON objects as data for a table + model that can be used with \l TableView. It is intended to support + very simple models without requiring the creation of a custom + QAbstractTableModel subclass in C++. + + \snippet qml/tablemodel/fruit-example-simpledelegate.qml file + + The model's initial row data is set with either the \l rows property or by + calling \l appendRow(). Each column in the model is specified by declaring + a \l TableModelColumn instance, where the order of each instance determines + its column index. Once the model's \l Component.completed() signal has been + emitted, the columns and roles will have been established and are then + fixed for the lifetime of the model. + + To access a specific row, the \l getRow() function can be used. + It's also possible to access the model's JavaScript data + directly via the \l rows property, but it is not possible to + modify the model data this way. + + To add new rows, use \l appendRow() and \l insertRow(). To modify + existing rows, use \l setRow(), \l moveRow(), \l removeRow(), and + \l clear(). + + It is also possible to modify the model's data via the delegate, + as shown in the example above: + + \snippet qml/tablemodel/fruit-example-simpledelegate.qml delegate + + If the type of the data at the modified role does not match the type of the + data that is set, it will be automatically converted via + \l {QVariant::canConvert()}{QVariant}. + + \section1 Supported Row Data Structures + + TableModel is designed to work with JavaScript/JSON data, where each row + is a simple key-pair object: + + \code + { + // Each property is one cell/column. + checked: false, + amount: 1, + fruitType: "Apple", + fruitName: "Granny Smith", + fruitPrice: 1.50 + }, + // ... + \endcode + + As model manipulation in Qt is done via row and column indices, + and because object keys are unordered, each column must be specified via + TableModelColumn. This allows mapping Qt's built-in roles to any property + in each row object. + + Complex row structures are supported, but with limited functionality. + As TableModel has no way of knowing how each row is structured, + it cannot manipulate it. As a consequence of this, the copy of the + model data that TableModel has stored in \l rows is not kept in sync + with the source data that was set in QML. For these reasons, TableModel + relies on the user to handle simple data manipulation. + + For example, suppose you wanted to have several roles per column. One way + of doing this is to use a data source where each row is an array and each + cell is an object. To use this data source with TableModel, define a + getter and setter: + + \code + TableModel { + TableModelColumn { + display: function(modelIndex) { return rows[modelIndex.row][0].checked } + setDisplay: function(modelIndex, cellData) { rows[modelIndex.row][0].checked = cellData } + } + // ... + + rows: [ + [ + { checked: false, checkable: true }, + { amount: 1 }, + { fruitType: "Apple" }, + { fruitName: "Granny Smith" }, + { fruitPrice: 1.50 } + ] + // ... + ] + } + \endcode + + The row above is one example of a complex row. + + \note Row manipulation functions such as \l appendRow(), \l removeRow(), + etc. are not supported when using complex rows. + + \section1 Using DelegateChooser with TableModel + + For most real world use cases, it is recommended to use DelegateChooser + as the delegate of a TableView that uses TableModel. This allows you to + use specific roles in the relevant delegates. For example, the snippet + above can be rewritten to use DelegateChooser like so: + + \snippet qml/tablemodel/fruit-example-delegatechooser.qml file + + The most specific delegates are declared first: the columns at index \c 0 + and \c 1 have \c bool and \c integer data types, so they use a + \l [QtQuickControls2]{CheckBox} and \l [QtQuickControls2]{SpinBox}, + respectively. The remaining columns can simply use a + \l [QtQuickControls2]{TextField}, and so that delegate is declared + last as a fallback. + + \sa TableModelColumn, TableView, QAbstractTableModel +*/ + +QQmlTableModel::QQmlTableModel(QObject *parent) + : QAbstractTableModel(parent) +{ +} + +QQmlTableModel::~QQmlTableModel() +{ +} + +/*! + \qmlproperty object TableModel::rows + + This property holds the model data in the form of an array of rows: + + \snippet qml/tablemodel/fruit-example-simpledelegate.qml rows + + \sa getRow(), setRow(), moveRow(), appendRow(), insertRow(), clear(), rowCount, columnCount +*/ +QVariant QQmlTableModel::rows() const +{ + return mRows; +} + +void QQmlTableModel::setRows(const QVariant &rows) +{ + if (rows.userType() != qMetaTypeId<QJSValue>()) { + qmlWarning(this) << "setRows(): \"rows\" must be an array; actual type is " << rows.typeName(); + return; + } + + const QJSValue rowsAsJSValue = rows.value<QJSValue>(); + const QVariantList rowsAsVariantList = rowsAsJSValue.toVariant().toList(); + if (rowsAsVariantList == mRows) { + // No change. + return; + } + + if (!componentCompleted) { + // Store the rows until we can call doSetRows() after component completion. + mRows = rowsAsVariantList; + return; + } + + doSetRows(rowsAsVariantList); +} + +void QQmlTableModel::doSetRows(const QVariantList &rowsAsVariantList) +{ + Q_ASSERT(componentCompleted); + + // By now, all TableModelColumns should have been set. + if (mColumns.isEmpty()) { + qmlWarning(this) << "No TableModelColumns were set; model will be empty"; + return; + } + + const bool firstTimeValidRowsHaveBeenSet = mColumnMetadata.isEmpty(); + if (!firstTimeValidRowsHaveBeenSet) { + // This is not the first time rows have been set; validate each one. + for (int rowIndex = 0; rowIndex < rowsAsVariantList.size(); ++rowIndex) { + // validateNewRow() expects a QVariant wrapping a QJSValue, so to + // simplify the code, just create one here. + const QVariant row = QVariant::fromValue(rowsAsVariantList.at(rowIndex)); + if (!validateNewRow("setRows()", row, rowIndex, SetRowsOperation)) + return; + } + } + + const int oldRowCount = mRowCount; + const int oldColumnCount = mColumnCount; + + beginResetModel(); + + // We don't clear the column or role data, because a TableModel should not be reused in that way. + // Once it has valid data, its columns and roles are fixed. + mRows = rowsAsVariantList; + mRowCount = mRows.size(); + + // Gather metadata the first time rows is set. + if (firstTimeValidRowsHaveBeenSet && !mRows.isEmpty()) + fetchColumnMetadata(); + + endResetModel(); + + emit rowsChanged(); + + if (mRowCount != oldRowCount) + emit rowCountChanged(); + if (mColumnCount != oldColumnCount) + emit columnCountChanged(); +} + +QQmlTableModel::ColumnRoleMetadata QQmlTableModel::fetchColumnRoleData(const QString &roleNameKey, + QQmlTableModelColumn *tableModelColumn, int columnIndex) const +{ + const QVariant firstRow = mRows.first(); + ColumnRoleMetadata roleData; + + QJSValue columnRoleGetter = tableModelColumn->getterAtRole(roleNameKey); + if (columnRoleGetter.isUndefined()) { + // This role is not defined, which is fine; just skip it. + return roleData; + } + + if (columnRoleGetter.isString()) { + // The role is set as a string, so we assume the row is a simple object. + if (firstRow.type() != QVariant::Map) { + qmlWarning(this).quote() << "expected row for role " + << roleNameKey << " of TableModelColumn at index " + << columnIndex << " to be a simple object, but it's " + << firstRow.typeName() << " instead: " << firstRow; + return roleData; + } + const QVariantMap firstRowAsMap = firstRow.toMap(); + const QString rolePropertyName = columnRoleGetter.toString(); + const QVariant roleProperty = firstRowAsMap.value(rolePropertyName); + + roleData.isStringRole = true; + roleData.name = rolePropertyName; + roleData.type = roleProperty.type(); + roleData.typeName = QString::fromLatin1(roleProperty.typeName()); + } else if (columnRoleGetter.isCallable()) { + // The role is provided via a function, which means the row is complex and + // the user needs to provide the data for it. + const auto modelIndex = index(0, columnIndex); + const auto args = QJSValueList() << qmlEngine(this)->toScriptValue(modelIndex); + const QVariant cellData = columnRoleGetter.call(args).toVariant(); + + // We don't know the property name since it's provided through the function. + // roleData.name = ??? + roleData.isStringRole = false; + roleData.type = cellData.type(); + roleData.typeName = QString::fromLatin1(cellData.typeName()); + } else { + // Invalid role. + qmlWarning(this) << "TableModelColumn role for column at index " + << columnIndex << " must be either a string or a function; actual type is: " + << columnRoleGetter.toString(); + } + + return roleData; +} + +void QQmlTableModel::fetchColumnMetadata() +{ + qCDebug(lcTableModel) << "gathering metadata for" << mColumnCount << "columns from first row:"; + + static const auto supportedRoleNames = QQmlTableModelColumn::supportedRoleNames(); + + // Since we support different data structures at the row level, we require that there + // is a TableModelColumn for each column. + // Collect and cache metadata for each column. This makes data lookup faster. + for (int columnIndex = 0; columnIndex < mColumns.size(); ++columnIndex) { + QQmlTableModelColumn *column = mColumns.at(columnIndex); + qCDebug(lcTableModel).nospace() << "- column " << columnIndex << ":"; + + ColumnMetadata metaData; + const auto builtInRoleKeys = supportedRoleNames.keys(); + for (const int builtInRoleKey : builtInRoleKeys) { + const QString builtInRoleName = supportedRoleNames.value(builtInRoleKey); + ColumnRoleMetadata roleData = fetchColumnRoleData(builtInRoleName, column, columnIndex); + if (roleData.type == QVariant::Invalid) { + // This built-in role was not specified in this column. + continue; + } + + qCDebug(lcTableModel).nospace() << " - added metadata for built-in role " + << builtInRoleName << " at column index " << columnIndex + << ": name=" << roleData.name << " typeName=" << roleData.typeName + << " type=" << roleData.type; + + // This column now supports this specific built-in role. + metaData.roles.insert(builtInRoleName, roleData); + // Add it if it doesn't already exist. + mRoleNames[builtInRoleKey] = builtInRoleName.toLatin1(); + } + mColumnMetadata.insert(columnIndex, metaData); + } +} + +/*! + \qmlmethod TableModel::appendRow(object row) + + Adds a new row to the end of the model, with the + values (cells) in \a row. + + \code + model.appendRow({ + checkable: true, + amount: 1, + fruitType: "Pear", + fruitName: "Williams", + fruitPrice: 1.50, + }) + \endcode + + \sa insertRow(), setRow(), removeRow() +*/ +void QQmlTableModel::appendRow(const QVariant &row) +{ + if (!validateNewRow("appendRow()", row, -1, AppendOperation)) + return; + + doInsert(mRowCount, row); +} + +/*! + \qmlmethod TableModel::clear() + + Removes all rows from the model. + + \sa removeRow() +*/ +void QQmlTableModel::clear() +{ + QQmlEngine *engine = qmlEngine(this); + Q_ASSERT(engine); + setRows(QVariant::fromValue(engine->newArray())); +} + +/*! + \qmlmethod object TableModel::getRow(int rowIndex) + + Returns the row at \a rowIndex in the model. + + Note that this equivalent to accessing the row directly + through the \l rows property: + + \code + Component.onCompleted: { + // These two lines are equivalent. + console.log(model.getRow(0).display); + console.log(model.rows[0].fruitName); + } + \endcode + + \note the returned object cannot be used to modify the contents of the + model; use setRow() instead. + + \sa setRow(), appendRow(), insertRow(), removeRow(), moveRow() +*/ +QVariant QQmlTableModel::getRow(int rowIndex) +{ + if (!validateRowIndex("getRow()", "rowIndex", rowIndex)) + return QVariant(); + + return mRows.at(rowIndex); +} + +/*! + \qmlmethod TableModel::insertRow(int rowIndex, object row) + + Adds a new row to the list model at position \a rowIndex, with the + values (cells) in \a row. + + \code + model.insertRow(2, { + checkable: true, checked: false, + amount: 1, + fruitType: "Pear", + fruitName: "Williams", + fruitPrice: 1.50, + }) + \endcode + + The \a rowIndex must be to an existing item in the list, or one past + the end of the list (equivalent to \l appendRow()). + + \sa appendRow(), setRow(), removeRow(), rowCount +*/ +void QQmlTableModel::insertRow(int rowIndex, const QVariant &row) +{ + if (!validateNewRow("insertRow()", row, rowIndex)) + return; + + doInsert(rowIndex, row); +} + +void QQmlTableModel::doInsert(int rowIndex, const QVariant &row) +{ + beginInsertRows(QModelIndex(), rowIndex, rowIndex); + + // Adding rowAsVariant.toList() will add each invidual variant in the list, + // which is definitely not what we want. + const QVariant rowAsVariant = row.value<QJSValue>().toVariant(); + mRows.insert(rowIndex, rowAsVariant); + ++mRowCount; + + qCDebug(lcTableModel).nospace() << "inserted the following row to the model at index " + << rowIndex << ":\n" << rowAsVariant.toMap(); + + // Gather metadata the first time a row is added. + if (mColumnMetadata.isEmpty()) + fetchColumnMetadata(); + + endInsertRows(); + emit rowCountChanged(); +} + +void QQmlTableModel::classBegin() +{ +} + +void QQmlTableModel::componentComplete() +{ + componentCompleted = true; + + mColumnCount = mColumns.size(); + if (mColumnCount > 0) + emit columnCountChanged(); + + doSetRows(mRows); +} + +/*! + \qmlmethod TableModel::moveRow(int fromRowIndex, int toRowIndex, int rows) + + Moves \a rows from the index at \a fromRowIndex to the index at + \a toRowIndex. + + The from and to ranges must exist; for example, to move the first 3 items + to the end of the list: + + \code + model.moveRow(0, model.rowCount - 3, 3) + \endcode + + \sa appendRow(), insertRow(), removeRow(), rowCount +*/ +void QQmlTableModel::moveRow(int fromRowIndex, int toRowIndex, int rows) +{ + if (fromRowIndex == toRowIndex) { + qmlWarning(this) << "moveRow(): \"fromRowIndex\" cannot be equal to \"toRowIndex\""; + return; + } + + if (rows <= 0) { + qmlWarning(this) << "moveRow(): \"rows\" is less than or equal to 0"; + return; + } + + if (!validateRowIndex("moveRow()", "fromRowIndex", fromRowIndex)) + return; + + if (!validateRowIndex("moveRow()", "toRowIndex", toRowIndex)) + return; + + if (fromRowIndex + rows > mRowCount) { + qmlWarning(this) << "moveRow(): \"fromRowIndex\" (" << fromRowIndex + << ") + \"rows\" (" << rows << ") = " << (fromRowIndex + rows) + << ", which is greater than rowCount() of " << mRowCount; + return; + } + + if (toRowIndex + rows > mRowCount) { + qmlWarning(this) << "moveRow(): \"toRowIndex\" (" << toRowIndex + << ") + \"rows\" (" << rows << ") = " << (toRowIndex + rows) + << ", which is greater than rowCount() of " << mRowCount; + return; + } + + qCDebug(lcTableModel).nospace() << "moving " << rows + << " row(s) from index " << fromRowIndex + << " to index " << toRowIndex; + + // Based on the same call in QQmlListModel::moveRow(). + beginMoveRows(QModelIndex(), fromRowIndex, fromRowIndex + rows - 1, QModelIndex(), + toRowIndex > fromRowIndex ? toRowIndex + rows : toRowIndex); + + // Based on ListModel::moveRow(). + if (fromRowIndex > toRowIndex) { + // Only move forwards - flip if moving backwards. + const int from = fromRowIndex; + const int to = toRowIndex; + fromRowIndex = to; + toRowIndex = to + rows; + rows = from - to; + } + + QVector<QVariant> store; + store.reserve(rows); + for (int i = 0; i < (toRowIndex - fromRowIndex); ++i) + store.append(mRows.at(fromRowIndex + rows + i)); + for (int i = 0; i < rows; ++i) + store.append(mRows.at(fromRowIndex + i)); + for (int i = 0; i < store.size(); ++i) + mRows[fromRowIndex + i] = store[i]; + + qCDebug(lcTableModel).nospace() << "after moving, rows are:\n" << mRows; + + endMoveRows(); +} + +/*! + \qmlmethod TableModel::removeRow(int rowIndex, int rows = 1) + + Removes the row at \a rowIndex from the model. + + \sa clear(), rowCount +*/ +void QQmlTableModel::removeRow(int rowIndex, int rows) +{ + if (!validateRowIndex("removeRow()", "rowIndex", rowIndex)) + return; + + if (rows <= 0) { + qmlWarning(this) << "removeRow(): \"rows\" is less than or equal to zero"; + return; + } + + if (rowIndex + rows - 1 >= mRowCount) { + qmlWarning(this) << "removeRow(): \"rows\" " << rows + << " exceeds available rowCount() of " << mRowCount + << " when removing from \"rowIndex\" " << rowIndex; + return; + } + + beginRemoveRows(QModelIndex(), rowIndex, rowIndex + rows - 1); + + auto firstIterator = mRows.begin() + rowIndex; + // The "last" argument to erase() is exclusive, so we go one past the last item. + auto lastIterator = firstIterator + rows; + mRows.erase(firstIterator, lastIterator); + mRowCount -= rows; + + endRemoveRows(); + emit rowCountChanged(); + + qCDebug(lcTableModel).nospace() << "removed " << rows + << " items from the model, starting at index " << rowIndex; +} + +/*! + \qmlmethod TableModel::setRow(int rowIndex, object row) + + Changes the row at \a rowIndex in the model with \a row. + + All columns/cells must be present in \c row, and in the correct order. + + \code + model.setRow(0, { + checkable: true, + amount: 1, + fruitType: "Pear", + fruitName: "Williams", + fruitPrice: 1.50, + }) + \endcode + + If \a rowIndex is equal to \c rowCount(), then a new row is appended to the + model. Otherwise, \a rowIndex must point to an existing row in the model. + + \sa appendRow(), insertRow(), rowCount +*/ +void QQmlTableModel::setRow(int rowIndex, const QVariant &row) +{ + if (!validateNewRow("setRow()", row, rowIndex)) + return; + + if (rowIndex != mRowCount) { + // Setting an existing row. + mRows[rowIndex] = row; + + // For now we just assume the whole row changed, as it's simpler. + const QModelIndex topLeftModelIndex(createIndex(rowIndex, 0)); + const QModelIndex bottomRightModelIndex(createIndex(rowIndex, mColumnCount - 1)); + emit dataChanged(topLeftModelIndex, bottomRightModelIndex); + } else { + // Appending a row. + doInsert(rowIndex, row); + } +} + +QQmlListProperty<QQmlTableModelColumn> QQmlTableModel::columns() +{ + return QQmlListProperty<QQmlTableModelColumn>(this, nullptr, + &QQmlTableModel::columns_append, + &QQmlTableModel::columns_count, + &QQmlTableModel::columns_at, + &QQmlTableModel::columns_clear); +} + +void QQmlTableModel::columns_append(QQmlListProperty<QQmlTableModelColumn> *property, + QQmlTableModelColumn *value) +{ + QQmlTableModel *model = static_cast<QQmlTableModel*>(property->object); + QQmlTableModelColumn *column = qobject_cast<QQmlTableModelColumn*>(value); + if (column) + model->mColumns.append(column); +} + +int QQmlTableModel::columns_count(QQmlListProperty<QQmlTableModelColumn> *property) +{ + const QQmlTableModel *model = static_cast<QQmlTableModel*>(property->object); + return model->mColumns.count(); +} + +QQmlTableModelColumn *QQmlTableModel::columns_at(QQmlListProperty<QQmlTableModelColumn> *property, int index) +{ + const QQmlTableModel *model = static_cast<QQmlTableModel*>(property->object); + return model->mColumns.at(index); +} + +void QQmlTableModel::columns_clear(QQmlListProperty<QQmlTableModelColumn> *property) +{ + QQmlTableModel *model = static_cast<QQmlTableModel*>(property->object); + return model->mColumns.clear(); +} + +/*! + \qmlmethod QModelIndex TableModel::index(int row, int column) + + Returns a \l QModelIndex object referencing the given \a row and \a column, + which can be passed to the data() function to get the data from that cell, + or to setData() to edit the contents of that cell. + + \code + import QtQml 2.14 + import Qt.labs.qmlmodels 1.0 + + TableModel { + id: model + + TableModelColumn { display: "fruitType" } + TableModelColumn { display: "fruitPrice" } + + rows: [ + { fruitType: "Apple", fruitPrice: 1.50 }, + { fruitType: "Orange", fruitPrice: 2.50 } + ] + + Component.onCompleted: { + for (var r = 0; r < model.rowCount; ++r) { + console.log("An " + model.data(model.index(r, 0)).display + + " costs " + model.data(model.index(r, 1)).display.toFixed(2)) + } + } + } + \endcode + + \sa {QModelIndex and related Classes in QML}, data() +*/ +// Note: we don't document the parent argument, because you never need it, because +// cells in a TableModel don't have parents. But it is there because this function is an override. +QModelIndex QQmlTableModel::index(int row, int column, const QModelIndex &parent) const +{ + return row >= 0 && row < rowCount() && column >= 0 && column < columnCount() && !parent.isValid() + ? createIndex(row, column) + : QModelIndex(); +} + +/*! + \qmlproperty int TableModel::rowCount + \readonly + + This read-only property holds the number of rows in the model. + + This value changes whenever rows are added or removed from the model. +*/ +int QQmlTableModel::rowCount(const QModelIndex &parent) const +{ + if (parent.isValid()) + return 0; + + return mRowCount; +} + +/*! + \qmlproperty int TableModel::columnCount + \readonly + + This read-only property holds the number of columns in the model. + + The number of columns is fixed for the lifetime of the model + after the \l rows property is set or \l appendRow() is called for the first + time. +*/ +int QQmlTableModel::columnCount(const QModelIndex &parent) const +{ + if (parent.isValid()) + return 0; + + return mColumnCount; +} + +/*! + \qmlmethod variant TableModel::data(QModelIndex index, string role) + + Returns the data from the table cell at the given \a index belonging to the + given \a role. + + \sa index() +*/ +QVariant QQmlTableModel::data(const QModelIndex &index, const QString &role) const +{ + const int iRole = mRoleNames.key(role.toUtf8(), -1); + if (iRole >= 0) + return data(index, iRole); + return QVariant(); +} + +QVariant QQmlTableModel::data(const QModelIndex &index, int role) const +{ + const int row = index.row(); + if (row < 0 || row >= rowCount()) + return QVariant(); + + const int column = index.column(); + if (column < 0 || column >= columnCount()) + return QVariant(); + + const ColumnMetadata columnMetadata = mColumnMetadata.at(index.column()); + const QString roleName = QString::fromUtf8(mRoleNames.value(role)); + if (!columnMetadata.roles.contains(roleName)) { + qmlWarning(this) << "setData(): no role named " << roleName + << " at column index " << column << ". The available roles for that column are: " + << columnMetadata.roles.keys(); + return QVariant(); + } + + const ColumnRoleMetadata roleData = columnMetadata.roles.value(roleName); + if (roleData.isStringRole) { + // We know the data structure, so we can get the data for the user. + const QVariantMap rowData = mRows.at(row).toMap(); + const QString propertyName = columnMetadata.roles.value(roleName).name; + const QVariant value = rowData.value(propertyName); + return value; + } + + // We don't know the data structure, so the user has to modify their data themselves. + // First, find the getter for this column and role. + QJSValue getter = mColumns.at(column)->getterAtRole(roleName); + + // Then, call it and return what it returned. + const auto args = QJSValueList() << qmlEngine(this)->toScriptValue(index); + return getter.call(args).toVariant(); +} + +/*! + \qmlmethod bool TableModel::setData(QModelIndex index, string role, variant value) + + Inserts or updates the data field named by \a role in the table cell at the + given \a index with \a value. Returns true if sucessful, false if not. + + \sa index() +*/ +bool QQmlTableModel::setData(const QModelIndex &index, const QString &role, const QVariant &value) +{ + const int intRole = mRoleNames.key(role.toUtf8(), -1); + if (intRole >= 0) + return setData(index, value, intRole); + return false; +} + +bool QQmlTableModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + const int row = index.row(); + if (row < 0 || row >= rowCount()) + return false; + + const int column = index.column(); + if (column < 0 || column >= columnCount()) + return false; + + const QString roleName = QString::fromUtf8(mRoleNames.value(role)); + + qCDebug(lcTableModel).nospace() << "setData() called with index " + << index << ", value " << value << " and role " << roleName; + + // Verify that the role exists for this column. + const ColumnMetadata columnMetadata = mColumnMetadata.at(index.column()); + if (!columnMetadata.roles.contains(roleName)) { + qmlWarning(this) << "setData(): no role named \"" << roleName + << "\" at column index " << column << ". The available roles for that column are: " + << columnMetadata.roles.keys(); + return false; + } + + // Verify that the type of the value is what we expect. + // If the value set is not of the expected type, we can try to convert it automatically. + const ColumnRoleMetadata roleData = columnMetadata.roles.value(roleName); + QVariant effectiveValue = value; + if (value.type() != roleData.type) { + if (!value.canConvert(int(roleData.type))) { + qmlWarning(this).nospace() << "setData(): the value " << value + << " set at row " << row << " column " << column << " with role " << roleName + << " cannot be converted to " << roleData.typeName; + return false; + } + + if (!effectiveValue.convert(int(roleData.type))) { + qmlWarning(this).nospace() << "setData(): failed converting value " << value + << " set at row " << row << " column " << column << " with role " << roleName + << " to " << roleData.typeName; + return false; + } + } + + if (roleData.isStringRole) { + // We know the data structure, so we can set it for the user. + QVariantMap modifiedRow = mRows.at(row).toMap(); + modifiedRow[roleData.name] = value; + + mRows[row] = modifiedRow; + } else { + // We don't know the data structure, so the user has to modify their data themselves. + auto engine = qmlEngine(this); + auto args = QJSValueList() + // arg 0: modelIndex. + << engine->toScriptValue(index) + // arg 1: cellData. + << engine->toScriptValue(value); + // Do the actual setting. + QJSValue setter = mColumns.at(column)->setterAtRole(roleName); + setter.call(args); + + /* + The chain of events so far: + + - User did e.g.: model.edit = textInput.text + - setData() is called + - setData() calls the setter + (remember that we need to emit the dataChanged() signal, + which is why the user can't just set the data directly in the delegate) + + Now the user's setter function has modified *their* copy of the + data, but *our* copy of the data is old. Imagine the getters and setters looked like this: + + display: function(modelIndex) { return rows[modelIndex.row][1].amount } + setDisplay: function(modelIndex, cellData) { rows[modelIndex.row][1].amount = cellData } + + We don't know the structure of the user's data, so we can't just do + what we do above for the isStringRole case: + + modifiedRow[column][roleName] = value + + This means that, besides getting the implicit row count when rows is initially set, + our copy of the data is unused when it comes to complex columns. + + Another point to note is that we can't pass rowData in to the getter as a convenience, + because we would be passing in *our* copy of the row, which is not up-to-date. + Since the user already has access to the data, it's not a big deal for them to do: + + display: function(modelIndex) { return rows[modelIndex.row][1].amount } + + instead of: + + display: function(modelIndex, rowData) { return rowData[1].amount } + */ + } + + QVector<int> rolesChanged; + rolesChanged.append(role); + emit dataChanged(index, index, rolesChanged); + + return true; +} + +QHash<int, QByteArray> QQmlTableModel::roleNames() const +{ + return mRoleNames; +} + +QQmlTableModel::ColumnRoleMetadata::ColumnRoleMetadata() +{ +} + +QQmlTableModel::ColumnRoleMetadata::ColumnRoleMetadata( + bool isStringRole, const QString &name, QVariant::Type type, const QString &typeName) : + isStringRole(isStringRole), + name(name), + type(type), + typeName(typeName) +{ +} + +bool QQmlTableModel::ColumnRoleMetadata::isValid() const +{ + return !name.isEmpty(); +} + +bool QQmlTableModel::validateRowType(const char *functionName, const QVariant &row) const +{ + if (!row.canConvert<QJSValue>()) { + qmlWarning(this) << functionName << ": expected \"row\" argument to be a QJSValue," + << " but got " << row.typeName() << " instead:\n" << row; + return false; + } + + const QJSValue rowAsJSValue = row.value<QJSValue>(); + if (!rowAsJSValue.isObject() && !rowAsJSValue.isArray()) { + qmlWarning(this) << functionName << ": expected \"row\" argument " + << "to be an object or array, but got:\n" << rowAsJSValue.toString(); + return false; + } + + return true; +} + +bool QQmlTableModel::validateNewRow(const char *functionName, const QVariant &row, + int rowIndex, NewRowOperationFlag operation) const +{ + if (mColumnMetadata.isEmpty()) { + // There is no column metadata, so we have nothing to validate the row against. + // Rows have to be added before we can gather metadata from them, so just this + // once we'll return true to allow the rows to be added. + return true; + } + + // Don't require each row to be a QJSValue when setting all rows, + // as they won't be; they'll be QVariantMap. + if (operation != SetRowsOperation && !validateRowType(functionName, row)) + return false; + + if (operation == OtherOperation) { + // Inserting/setting. + if (rowIndex < 0) { + qmlWarning(this) << functionName << ": \"rowIndex\" cannot be negative"; + return false; + } + + if (rowIndex > mRowCount) { + qmlWarning(this) << functionName << ": \"rowIndex\" " << rowIndex + << " is greater than rowCount() of " << mRowCount; + return false; + } + } + + const QVariant rowAsVariant = operation == SetRowsOperation + ? row : row.value<QJSValue>().toVariant(); + if (rowAsVariant.type() != QVariant::Map) { + qmlWarning(this) << functionName << ": row manipulation functions " + << "do not support complex rows (row index: " << rowIndex << ")"; + return false; + } + + const QVariantMap rowAsMap = rowAsVariant.toMap(); + const int columnCount = rowAsMap.size(); + if (columnCount < mColumnCount) { + qmlWarning(this) << functionName << ": expected " << mColumnCount + << " columns, but only got " << columnCount; + return false; + } + + // We can't validate complex structures, but we can make sure that + // each simple string-based role in each column is correct. + for (int columnIndex = 0; columnIndex < mColumns.size(); ++columnIndex) { + QQmlTableModelColumn *column = mColumns.at(columnIndex); + const QHash<QString, QJSValue> getters = column->getters(); + const auto roleNames = getters.keys(); + const ColumnMetadata columnMetadata = mColumnMetadata.at(columnIndex); + for (const QString &roleName : roleNames) { + const ColumnRoleMetadata roleData = columnMetadata.roles.value(roleName); + if (!roleData.isStringRole) + continue; + + if (!rowAsMap.contains(roleData.name)) { + qmlWarning(this).quote() << functionName << ": expected a property named " + << roleData.name << " in row at index " << rowIndex << ", but couldn't find one"; + return false; + } + + const QVariant rolePropertyValue = rowAsMap.value(roleData.name); + if (rolePropertyValue.type() != roleData.type) { + qmlWarning(this).quote() << functionName << ": expected the property named " + << roleData.name << " to be of type " << roleData.typeName + << ", but got " << QString::fromLatin1(rolePropertyValue.typeName()) << " instead"; + return false; + } + } + } + + return true; +} + +bool QQmlTableModel::validateRowIndex(const char *functionName, const char *argumentName, int rowIndex) const +{ + if (rowIndex < 0) { + qmlWarning(this) << functionName << ": \"" << argumentName << "\" cannot be negative"; + return false; + } + + if (rowIndex >= mRowCount) { + qmlWarning(this) << functionName << ": \"" << argumentName + << "\" " << rowIndex << " is greater than or equal to rowCount() of " << mRowCount; + return false; + } + + return true; +} + +QT_END_NAMESPACE diff --git a/src/qmlmodels/qqmltablemodel_p.h b/src/qmlmodels/qqmltablemodel_p.h new file mode 100644 index 0000000000..114b162e5c --- /dev/null +++ b/src/qmlmodels/qqmltablemodel_p.h @@ -0,0 +1,170 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtQml module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QQMLTABLEMODEL_P_H +#define QQMLTABLEMODEL_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <QtCore/QObject> +#include <QtCore/QAbstractTableModel> +#include <QtQml/qqml.h> +#include <QtQmlModels/private/qtqmlmodelsglobal_p.h> +#include <QtQmlModels/private/qqmltablemodelcolumn_p.h> +#include <QtQml/QJSValue> +#include <QtQml/QQmlListProperty> + +QT_BEGIN_NAMESPACE + +class Q_QMLMODELS_PRIVATE_EXPORT QQmlTableModel : public QAbstractTableModel, public QQmlParserStatus +{ + Q_OBJECT + Q_PROPERTY(int columnCount READ columnCount NOTIFY columnCountChanged FINAL) + Q_PROPERTY(int rowCount READ rowCount NOTIFY rowCountChanged FINAL) + Q_PROPERTY(QVariant rows READ rows WRITE setRows NOTIFY rowsChanged FINAL) + Q_PROPERTY(QQmlListProperty<QQmlTableModelColumn> columns READ columns CONSTANT FINAL) + Q_INTERFACES(QQmlParserStatus) + Q_CLASSINFO("DefaultProperty", "columns") + +public: + QQmlTableModel(QObject *parent = nullptr); + ~QQmlTableModel() override; + + QVariant rows() const; + void setRows(const QVariant &rows); + + Q_INVOKABLE void appendRow(const QVariant &row); + Q_INVOKABLE void clear(); + Q_INVOKABLE QVariant getRow(int rowIndex); + Q_INVOKABLE void insertRow(int rowIndex, const QVariant &row); + Q_INVOKABLE void moveRow(int fromRowIndex, int toRowIndex, int rows = 1); + Q_INVOKABLE void removeRow(int rowIndex, int rows = 1); + Q_INVOKABLE void setRow(int rowIndex, const QVariant &row); + + QQmlListProperty<QQmlTableModelColumn> columns(); + + static void columns_append(QQmlListProperty<QQmlTableModelColumn> *property, QQmlTableModelColumn *value); + static int columns_count(QQmlListProperty<QQmlTableModelColumn> *property); + static QQmlTableModelColumn *columns_at(QQmlListProperty<QQmlTableModelColumn> *property, int index); + static void columns_clear(QQmlListProperty<QQmlTableModelColumn> *property); + + QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override; + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + int columnCount(const QModelIndex &parent = QModelIndex()) const override; + Q_INVOKABLE QVariant data(const QModelIndex &index, const QString &role) const; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + Q_INVOKABLE bool setData(const QModelIndex &index, const QString &role, const QVariant &value); + bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::DisplayRole) override; + QHash<int, QByteArray> roleNames() const override; + +Q_SIGNALS: + void columnCountChanged(); + void rowCountChanged(); + void rowsChanged(); + +private: + class ColumnRoleMetadata + { + public: + ColumnRoleMetadata(); + ColumnRoleMetadata(bool isStringRole, const QString &name, QVariant::Type type, const QString &typeName); + + bool isValid() const; + + // If this is false, it's a function role. + bool isStringRole = false; + QString name; + QVariant::Type type = QVariant::Invalid; + QString typeName; + }; + + struct ColumnMetadata + { + // Key = role name that will be made visible to the delegate + // Value = metadata about that role, including actual name in the model data, type, etc. + QHash<QString, ColumnRoleMetadata> roles; + }; + + enum NewRowOperationFlag { + OtherOperation, // insert(), set(), etc. + SetRowsOperation, + AppendOperation + }; + + void doSetRows(const QVariantList &rowsAsVariantList); + ColumnRoleMetadata fetchColumnRoleData(const QString &roleNameKey, + QQmlTableModelColumn *tableModelColumn, int columnIndex) const; + void fetchColumnMetadata(); + + bool validateRowType(const char *functionName, const QVariant &row) const; + bool validateNewRow(const char *functionName, const QVariant &row, + int rowIndex, NewRowOperationFlag operation = OtherOperation) const; + bool validateRowIndex(const char *functionName, const char *argumentName, int rowIndex) const; + + void doInsert(int rowIndex, const QVariant &row); + + void classBegin() override; + void componentComplete() override; + + bool componentCompleted = false; + QVariantList mRows; + QList<QQmlTableModelColumn *> mColumns; + int mRowCount = 0; + int mColumnCount = 0; + // Each entry contains information about the properties of the column at that index. + QVector<ColumnMetadata> mColumnMetadata; + // key = property index (0 to number of properties across all columns) + // value = role name + QHash<int, QByteArray> mRoleNames; +}; + +QT_END_NAMESPACE + +QML_DECLARE_TYPE(QQmlTableModel) + +#endif // QQMLTABLEMODEL_P_H diff --git a/src/qmlmodels/qqmltablemodelcolumn.cpp b/src/qmlmodels/qqmltablemodelcolumn.cpp new file mode 100644 index 0000000000..93da0642de --- /dev/null +++ b/src/qmlmodels/qqmltablemodelcolumn.cpp @@ -0,0 +1,200 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtQml module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "qqmltablemodelcolumn_p.h" + +#include <QtQml/qqmlinfo.h> + +QT_BEGIN_NAMESPACE + +/*! + \qmltype TableModelColumn + \instantiates QQmlTableModelColumn + \inqmlmodule Qt.labs.qmlmodels + \brief Represents a column in a model. + \since 5.14 + + \section1 Supported Roles + + TableModelColumn supports all of \l {Qt::ItemDataRole}{Qt's roles}, + with the exception of \c Qt::InitialSortOrderRole. + + \sa TableModel, TableView +*/ + +static const QString displayRoleName = QStringLiteral("display"); +static const QString decorationRoleName = QStringLiteral("decoration"); +static const QString editRoleName = QStringLiteral("edit"); +static const QString toolTipRoleName = QStringLiteral("toolTip"); +static const QString statusTipRoleName = QStringLiteral("statusTip"); +static const QString whatsThisRoleName = QStringLiteral("whatsThis"); + +static const QString fontRoleName = QStringLiteral("font"); +static const QString textAlignmentRoleName = QStringLiteral("textAlignment"); +static const QString backgroundRoleName = QStringLiteral("background"); +static const QString foregroundRoleName = QStringLiteral("foreground"); +static const QString checkStateRoleName = QStringLiteral("checkState"); + +static const QString accessibleTextRoleName = QStringLiteral("accessibleText"); +static const QString accessibleDescriptionRoleName = QStringLiteral("accessibleDescription"); + +static const QString sizeHintRoleName = QStringLiteral("sizeHint"); + + +QQmlTableModelColumn::QQmlTableModelColumn(QObject *parent) + : QObject(parent) +{ +} + +QQmlTableModelColumn::~QQmlTableModelColumn() +{ +} + +#define DEFINE_ROLE_PROPERTIES(getterGetterName, getterSetterName, getterSignal, setterGetterName, setterSetterName, setterSignal, roleName) \ +QJSValue QQmlTableModelColumn::getterGetterName() const \ +{ \ + return mGetters.value(roleName); \ +} \ +\ +void QQmlTableModelColumn::getterSetterName(const QJSValue &stringOrFunction) \ +{ \ + if (!stringOrFunction.isString() && !stringOrFunction.isCallable()) { \ + qmlWarning(this).quote() << "getter for " << roleName << " must be a function"; \ + return; \ + } \ + if (stringOrFunction.strictlyEquals(decoration())) \ + return; \ +\ + mGetters[roleName] = stringOrFunction; \ + emit decorationChanged(); \ +} \ +\ +QJSValue QQmlTableModelColumn::setterGetterName() const \ +{ \ + return mSetters.value(roleName); \ +} \ +\ +void QQmlTableModelColumn::setterSetterName(const QJSValue &function) \ +{ \ + if (!function.isCallable()) { \ + qmlWarning(this).quote() << "setter for " << roleName << " must be a function"; \ + return; \ + } \ +\ + if (function.strictlyEquals(getSetDisplay())) \ + return; \ +\ + mSetters[roleName] = function; \ + emit setDisplayChanged(); \ +} + +DEFINE_ROLE_PROPERTIES(display, setDisplay, displayChanged, + getSetDisplay, setSetDisplay, setDisplayChanged, displayRoleName) +DEFINE_ROLE_PROPERTIES(decoration, setDecoration, decorationChanged, + getSetDecoration, setSetDecoration, setDecorationChanged, decorationRoleName) +DEFINE_ROLE_PROPERTIES(edit, setEdit, editChanged, + getSetEdit, setSetEdit, setEditChanged, editRoleName) +DEFINE_ROLE_PROPERTIES(toolTip, setToolTip, toolTipChanged, + getSetToolTip, setSetToolTip, setToolTipChanged, toolTipRoleName) +DEFINE_ROLE_PROPERTIES(statusTip, setStatusTip, statusTipChanged, + getSetStatusTip, setSetStatusTip, setStatusTipChanged, statusTipRoleName) +DEFINE_ROLE_PROPERTIES(whatsThis, setWhatsThis, whatsThisChanged, + getSetWhatsThis, setSetWhatsThis, setWhatsThisChanged, whatsThisRoleName) + +DEFINE_ROLE_PROPERTIES(font, setFont, fontChanged, + getSetFont, setSetFont, setFontChanged, fontRoleName) +DEFINE_ROLE_PROPERTIES(textAlignment, setTextAlignment, textAlignmentChanged, + getSetTextAlignment, setSetTextAlignment, setTextAlignmentChanged, textAlignmentRoleName) +DEFINE_ROLE_PROPERTIES(background, setBackground, backgroundChanged, + getSetBackground, setSetBackground, setBackgroundChanged, backgroundRoleName) +DEFINE_ROLE_PROPERTIES(foreground, setForeground, foregroundChanged, + getSetForeground, setSetForeground, setForegroundChanged, foregroundRoleName) +DEFINE_ROLE_PROPERTIES(checkState, setCheckState, checkStateChanged, + getSetCheckState, setSetCheckState, setCheckStateChanged, checkStateRoleName) + +DEFINE_ROLE_PROPERTIES(accessibleText, setAccessibleText, accessibleTextChanged, + getSetAccessibleText, setSetAccessibleText, setAccessibleTextChanged, accessibleTextRoleName) +DEFINE_ROLE_PROPERTIES(accessibleDescription, setAccessibleDescription, accessibleDescriptionChanged, + getSetAccessibleDescription, setSetAccessibleDescription, setAccessibleDescriptionChanged, accessibleDescriptionRoleName) + +DEFINE_ROLE_PROPERTIES(sizeHint, setSizeHint, sizeHintChanged, + getSetSizeHint, setSetSizeHint, setSizeHintChanged, sizeHintRoleName) + +QJSValue QQmlTableModelColumn::getterAtRole(const QString &roleName) +{ + auto it = mGetters.find(roleName); + if (it == mGetters.end()) + return QJSValue(); + return *it; +} + +QJSValue QQmlTableModelColumn::setterAtRole(const QString &roleName) +{ + auto it = mSetters.find(roleName); + if (it == mSetters.end()) + return QJSValue(); + return *it; +} + +const QHash<QString, QJSValue> QQmlTableModelColumn::getters() const +{ + return mGetters; +} + +const QHash<int, QString> QQmlTableModelColumn::supportedRoleNames() +{ + QHash<int, QString> names; + names[Qt::DisplayRole] = QLatin1String("display"); + names[Qt::DecorationRole] = QLatin1String("decoration"); + names[Qt::EditRole] = QLatin1String("edit"); + names[Qt::ToolTipRole] = QLatin1String("toolTip"); + names[Qt::StatusTipRole] = QLatin1String("statusTip"); + names[Qt::WhatsThisRole] = QLatin1String("whatsThis"); + names[Qt::FontRole] = QLatin1String("font"); + names[Qt::TextAlignmentRole] = QLatin1String("textAlignment"); + names[Qt::BackgroundRole] = QLatin1String("background"); + names[Qt::ForegroundRole] = QLatin1String("foreground"); + names[Qt::CheckStateRole] = QLatin1String("checkState"); + names[Qt::AccessibleTextRole] = QLatin1String("accessibleText"); + names[Qt::AccessibleDescriptionRole] = QLatin1String("accessibleDescription"); + names[Qt::SizeHintRole] = QLatin1String("sizeHint"); + return names; +} + +QT_END_NAMESPACE diff --git a/src/qmlmodels/qqmltablemodelcolumn_p.h b/src/qmlmodels/qqmltablemodelcolumn_p.h new file mode 100644 index 0000000000..d125f8bb16 --- /dev/null +++ b/src/qmlmodels/qqmltablemodelcolumn_p.h @@ -0,0 +1,224 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtQml module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QQMLTABLEMODELCOLUMN_P_H +#define QQMLTABLEMODELCOLUMN_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <QtCore/QObject> +#include <QtQml/qqml.h> +#include <QtQmlModels/private/qtqmlmodelsglobal_p.h> +#include <QtQml/qjsvalue.h> + +QT_BEGIN_NAMESPACE + +class Q_QMLMODELS_PRIVATE_EXPORT QQmlTableModelColumn : public QObject +{ + Q_OBJECT + Q_PROPERTY(QJSValue display READ display WRITE setDisplay NOTIFY displayChanged FINAL) + Q_PROPERTY(QJSValue setDisplay READ getSetDisplay WRITE setSetDisplay NOTIFY setDisplayChanged) + Q_PROPERTY(QJSValue decoration READ decoration WRITE setDecoration NOTIFY decorationChanged FINAL) + Q_PROPERTY(QJSValue setDecoration READ getSetDecoration WRITE setSetDecoration NOTIFY setDecorationChanged FINAL) + Q_PROPERTY(QJSValue edit READ edit WRITE setEdit NOTIFY editChanged FINAL) + Q_PROPERTY(QJSValue setEdit READ getSetEdit WRITE setSetEdit NOTIFY setEditChanged FINAL) + Q_PROPERTY(QJSValue toolTip READ toolTip WRITE setToolTip NOTIFY toolTipChanged FINAL) + Q_PROPERTY(QJSValue setToolTip READ getSetToolTip WRITE setSetToolTip NOTIFY setToolTipChanged FINAL) + Q_PROPERTY(QJSValue statusTip READ statusTip WRITE setStatusTip NOTIFY statusTipChanged FINAL) + Q_PROPERTY(QJSValue setStatusTip READ getSetStatusTip WRITE setSetStatusTip NOTIFY setStatusTipChanged FINAL) + Q_PROPERTY(QJSValue whatsThis READ whatsThis WRITE setWhatsThis NOTIFY whatsThisChanged FINAL) + Q_PROPERTY(QJSValue setWhatsThis READ getSetWhatsThis WRITE setSetWhatsThis NOTIFY setWhatsThisChanged FINAL) + + Q_PROPERTY(QJSValue font READ font WRITE setFont NOTIFY fontChanged FINAL) + Q_PROPERTY(QJSValue setFont READ getSetFont WRITE setSetFont NOTIFY setFontChanged FINAL) + Q_PROPERTY(QJSValue textAlignment READ textAlignment WRITE setTextAlignment NOTIFY textAlignmentChanged FINAL) + Q_PROPERTY(QJSValue setTextAlignment READ getSetTextAlignment WRITE setSetTextAlignment NOTIFY setTextAlignmentChanged FINAL) + Q_PROPERTY(QJSValue background READ background WRITE setBackground NOTIFY backgroundChanged FINAL) + Q_PROPERTY(QJSValue setBackground READ getSetBackground WRITE setSetBackground NOTIFY setBackgroundChanged FINAL) + Q_PROPERTY(QJSValue foreground READ foreground WRITE setForeground NOTIFY foregroundChanged FINAL) + Q_PROPERTY(QJSValue setForeground READ getSetForeground WRITE setSetForeground NOTIFY setForegroundChanged FINAL) + Q_PROPERTY(QJSValue checkState READ checkState WRITE setCheckState NOTIFY checkStateChanged FINAL) + Q_PROPERTY(QJSValue setCheckState READ getSetCheckState WRITE setSetCheckState NOTIFY setCheckStateChanged FINAL) + + Q_PROPERTY(QJSValue accessibleText READ accessibleText WRITE setAccessibleText NOTIFY accessibleTextChanged FINAL) + Q_PROPERTY(QJSValue setAccessibleText READ getSetAccessibleText WRITE setSetAccessibleText NOTIFY setAccessibleTextChanged FINAL) + Q_PROPERTY(QJSValue accessibleDescription READ accessibleDescription + WRITE setAccessibleDescription NOTIFY accessibleDescriptionChanged FINAL) + Q_PROPERTY(QJSValue setAccessibleDescription READ getSetAccessibleDescription + WRITE setSetAccessibleDescription NOTIFY setAccessibleDescriptionChanged FINAL) + + Q_PROPERTY(QJSValue sizeHint READ sizeHint WRITE setSizeHint NOTIFY sizeHintChanged FINAL) + Q_PROPERTY(QJSValue setSizeHint READ getSetSizeHint WRITE setSetSizeHint NOTIFY setSizeHintChanged FINAL) + +public: + QQmlTableModelColumn(QObject *parent = nullptr); + ~QQmlTableModelColumn() override; + + QJSValue display() const; + void setDisplay(const QJSValue &stringOrFunction); + QJSValue getSetDisplay() const; + void setSetDisplay(const QJSValue &function); + + QJSValue decoration() const; + void setDecoration(const QJSValue &stringOrFunction); + QJSValue getSetDecoration() const; + void setSetDecoration(const QJSValue &function); + + QJSValue edit() const; + void setEdit(const QJSValue &stringOrFunction); + QJSValue getSetEdit() const; + void setSetEdit(const QJSValue &function); + + QJSValue toolTip() const; + void setToolTip(const QJSValue &stringOrFunction); + QJSValue getSetToolTip() const; + void setSetToolTip(const QJSValue &function); + + QJSValue statusTip() const; + void setStatusTip(const QJSValue &stringOrFunction); + QJSValue getSetStatusTip() const; + void setSetStatusTip(const QJSValue &function); + + QJSValue whatsThis() const; + void setWhatsThis(const QJSValue &stringOrFunction); + QJSValue getSetWhatsThis() const; + void setSetWhatsThis(const QJSValue &function); + + QJSValue font() const; + void setFont(const QJSValue &stringOrFunction); + QJSValue getSetFont() const; + void setSetFont(const QJSValue &function); + + QJSValue textAlignment() const; + void setTextAlignment(const QJSValue &stringOrFunction); + QJSValue getSetTextAlignment() const; + void setSetTextAlignment(const QJSValue &function); + + QJSValue background() const; + void setBackground(const QJSValue &stringOrFunction); + QJSValue getSetBackground() const; + void setSetBackground(const QJSValue &function); + + QJSValue foreground() const; + void setForeground(const QJSValue &stringOrFunction); + QJSValue getSetForeground() const; + void setSetForeground(const QJSValue &function); + + QJSValue checkState() const; + void setCheckState(const QJSValue &stringOrFunction); + QJSValue getSetCheckState() const; + void setSetCheckState(const QJSValue &function); + + QJSValue accessibleText() const; + void setAccessibleText(const QJSValue &stringOrFunction); + QJSValue getSetAccessibleText() const; + void setSetAccessibleText(const QJSValue &function); + + QJSValue accessibleDescription() const; + void setAccessibleDescription(const QJSValue &stringOrFunction); + QJSValue getSetAccessibleDescription() const; + void setSetAccessibleDescription(const QJSValue &function); + + QJSValue sizeHint() const; + void setSizeHint(const QJSValue &stringOrFunction); + QJSValue getSetSizeHint() const; + void setSetSizeHint(const QJSValue &function); + + QJSValue getterAtRole(const QString &roleName); + QJSValue setterAtRole(const QString &roleName); + + const QHash<QString, QJSValue> getters() const; + + static const QHash<int, QString> supportedRoleNames(); + +Q_SIGNALS: + void indexChanged(); + void displayChanged(); + void setDisplayChanged(); + void decorationChanged(); + void setDecorationChanged(); + void editChanged(); + void setEditChanged(); + void toolTipChanged(); + void setToolTipChanged(); + void statusTipChanged(); + void setStatusTipChanged(); + void whatsThisChanged(); + void setWhatsThisChanged(); + + void fontChanged(); + void setFontChanged(); + void textAlignmentChanged(); + void setTextAlignmentChanged(); + void backgroundChanged(); + void setBackgroundChanged(); + void foregroundChanged(); + void setForegroundChanged(); + void checkStateChanged(); + void setCheckStateChanged(); + + void accessibleTextChanged(); + void setAccessibleTextChanged(); + void accessibleDescriptionChanged(); + void setAccessibleDescriptionChanged(); + void sizeHintChanged(); + void setSizeHintChanged(); + +private: + int mIndex = -1; + + // We store these in hashes because QQuickTableModel needs string-based lookup in certain situations. + QHash<QString, QJSValue> mGetters; + QHash<QString, QJSValue> mSetters; +}; + +QT_END_NAMESPACE + +QML_DECLARE_TYPE(QQmlTableModelColumn) + +#endif // QQMLTABLEMODELCOLUMN_P_H diff --git a/src/qmlmodels/qquickpackage.cpp b/src/qmlmodels/qquickpackage.cpp new file mode 100644 index 0000000000..03539d8737 --- /dev/null +++ b/src/qmlmodels/qquickpackage.cpp @@ -0,0 +1,198 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtQml module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "qquickpackage_p.h" + +#include <private/qobject_p.h> +#include <private/qqmlguard_p.h> + +QT_BEGIN_NAMESPACE + +/*! + \qmltype Package + \instantiates QQuickPackage + \inqmlmodule QtQuick + \ingroup qtquick-views + \brief Specifies a collection of named items. + + The Package type is used in conjunction with + DelegateModel to enable delegates with a shared context + to be provided to multiple views. + + Any item within a Package may be assigned a name via the + \l{Package::name}{Package.name} attached property. + + The example below creates a Package containing two named items; + \e list and \e grid. The third item in the package (the \l Rectangle) is parented to whichever + delegate it should appear in. This allows an item to move + between views. + + \snippet package/Delegate.qml 0 + + These named items are used as the delegates by the two views who + reference the special \l{DelegateModel::parts} property to select + a model which provides the chosen delegate. + + \snippet package/view.qml 0 + + \sa {Qt Quick Examples - Views}, {Qt Quick Demo - Photo Viewer}, {Qt QML} +*/ + +/*! + \qmlattachedproperty string QtQuick::Package::name + This attached property holds the name of an item within a Package. +*/ + + +class QQuickPackagePrivate : public QObjectPrivate +{ +public: + QQuickPackagePrivate() {} + + struct DataGuard : public QQmlGuard<QObject> + { + DataGuard(QObject *obj, QList<DataGuard> *l) : list(l) { (QQmlGuard<QObject>&)*this = obj; } + QList<DataGuard> *list; + void objectDestroyed(QObject *) override { + // we assume priv will always be destroyed after objectDestroyed calls + list->removeOne(*this); + } + }; + + QList<DataGuard> dataList; + static void data_append(QQmlListProperty<QObject> *prop, QObject *o) { + QList<DataGuard> *list = static_cast<QList<DataGuard> *>(prop->data); + list->append(DataGuard(o, list)); + } + static void data_clear(QQmlListProperty<QObject> *prop) { + QList<DataGuard> *list = static_cast<QList<DataGuard> *>(prop->data); + list->clear(); + } + static QObject *data_at(QQmlListProperty<QObject> *prop, int index) { + QList<DataGuard> *list = static_cast<QList<DataGuard> *>(prop->data); + return list->at(index); + } + static int data_count(QQmlListProperty<QObject> *prop) { + QList<DataGuard> *list = static_cast<QList<DataGuard> *>(prop->data); + return list->count(); + } +}; + +QHash<QObject *, QQuickPackageAttached *> QQuickPackageAttached::attached; + +QQuickPackageAttached::QQuickPackageAttached(QObject *parent) +: QObject(parent) +{ + attached.insert(parent, this); +} + +QQuickPackageAttached::~QQuickPackageAttached() +{ + attached.remove(parent()); +} + +QString QQuickPackageAttached::name() const +{ + return _name; +} + +void QQuickPackageAttached::setName(const QString &n) +{ + _name = n; +} + +QQuickPackage::QQuickPackage(QObject *parent) + : QObject(*(new QQuickPackagePrivate), parent) +{ +} + +QQuickPackage::~QQuickPackage() +{ +} + +QQmlListProperty<QObject> QQuickPackage::data() +{ + Q_D(QQuickPackage); + return QQmlListProperty<QObject>(this, &d->dataList, QQuickPackagePrivate::data_append, + QQuickPackagePrivate::data_count, + QQuickPackagePrivate::data_at, + QQuickPackagePrivate::data_clear); +} + +bool QQuickPackage::hasPart(const QString &name) +{ + Q_D(QQuickPackage); + for (int ii = 0; ii < d->dataList.count(); ++ii) { + QObject *obj = d->dataList.at(ii); + QQuickPackageAttached *a = QQuickPackageAttached::attached.value(obj); + if (a && a->name() == name) + return true; + } + return false; +} + +QObject *QQuickPackage::part(const QString &name) +{ + Q_D(QQuickPackage); + if (name.isEmpty() && !d->dataList.isEmpty()) + return d->dataList.at(0); + + for (int ii = 0; ii < d->dataList.count(); ++ii) { + QObject *obj = d->dataList.at(ii); + QQuickPackageAttached *a = QQuickPackageAttached::attached.value(obj); + if (a && a->name() == name) + return obj; + } + + if (name == QLatin1String("default") && !d->dataList.isEmpty()) + return d->dataList.at(0); + + return nullptr; +} + +QQuickPackageAttached *QQuickPackage::qmlAttachedProperties(QObject *o) +{ + return new QQuickPackageAttached(o); +} + + + +QT_END_NAMESPACE + +#include "moc_qquickpackage_p.cpp" diff --git a/src/qmlmodels/qquickpackage_p.h b/src/qmlmodels/qquickpackage_p.h new file mode 100644 index 0000000000..122c7fcb30 --- /dev/null +++ b/src/qmlmodels/qquickpackage_p.h @@ -0,0 +1,101 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtQml module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QQUICKPACKAGE_H +#define QQUICKPACKAGE_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <qqml.h> + +QT_BEGIN_NAMESPACE + +class QQuickPackagePrivate; +class QQuickPackageAttached; +class Q_AUTOTEST_EXPORT QQuickPackage : public QObject +{ + Q_OBJECT + Q_DECLARE_PRIVATE(QQuickPackage) + + Q_CLASSINFO("DefaultProperty", "data") + Q_PROPERTY(QQmlListProperty<QObject> data READ data) + +public: + QQuickPackage(QObject *parent=nullptr); + virtual ~QQuickPackage(); + + QQmlListProperty<QObject> data(); + + QObject *part(const QString & = QString()); + bool hasPart(const QString &); + + static QQuickPackageAttached *qmlAttachedProperties(QObject *); +}; + +class QQuickPackageAttached : public QObject +{ +Q_OBJECT +Q_PROPERTY(QString name READ name WRITE setName) +public: + QQuickPackageAttached(QObject *parent); + virtual ~QQuickPackageAttached(); + + QString name() const; + void setName(const QString &n); + + static QHash<QObject *, QQuickPackageAttached *> attached; +private: + QString _name; +}; + +QT_END_NAMESPACE + +QML_DECLARE_TYPE(QQuickPackage) +QML_DECLARE_TYPEINFO(QQuickPackage, QML_HAS_ATTACHED_PROPERTIES) + +#endif // QQUICKPACKAGE_H diff --git a/src/qmlmodels/qtqmlmodelsglobal.h b/src/qmlmodels/qtqmlmodelsglobal.h new file mode 100644 index 0000000000..6e6cf299b2 --- /dev/null +++ b/src/qmlmodels/qtqmlmodelsglobal.h @@ -0,0 +1,59 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtQml module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QTQMLMODELSGLOBAL_H +#define QTQMLMODELSGLOBAL_H + +#include <QtQml/qtqmlglobal.h> +#include <QtQmlModels/qtqmlmodels-config.h> + +QT_BEGIN_NAMESPACE + +#if !defined(QT_STATIC) +# if defined(QT_BUILD_QMLMODELS_LIB) +# define Q_QMLMODELS_EXPORT Q_DECL_EXPORT +# else +# define Q_QMLMODELS_EXPORT Q_DECL_IMPORT +# endif +#else +# define Q_QMLMODELS_EXPORT +#endif + +QT_END_NAMESPACE +#endif // QTQMLMODELSGLOBAL_H diff --git a/src/qmlmodels/qtqmlmodelsglobal_p.h b/src/qmlmodels/qtqmlmodelsglobal_p.h new file mode 100644 index 0000000000..145112c9c1 --- /dev/null +++ b/src/qmlmodels/qtqmlmodelsglobal_p.h @@ -0,0 +1,61 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtQml module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QTQMLMODELSGLOBAL_P_H +#define QTQMLMODELSGLOBAL_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <QtQml/private/qtqmlglobal_p.h> +#include <QtQmlModels/qtqmlmodelsglobal.h> +#include <QtQmlModels/private/qtqmlmodels-config_p.h> + +#define Q_QMLMODELS_PRIVATE_EXPORT Q_QMLMODELS_EXPORT +#define Q_QMLMODELS_AUTOTEST_EXPORT Q_AUTOTEST_EXPORT + +#endif // QTQMLMODELSGLOBAL_P_H |