3040 lines
100 KiB
C++
3040 lines
100 KiB
C++
// Copyright (C) 2016 The Qt Company Ltd.
|
|
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
|
|
|
|
#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/qv4urlobject_p.h>
|
|
#include <private/qv4objectiterator_p.h>
|
|
#include <private/qv4alloca_p.h>
|
|
#include <private/qv4lookup_p.h>
|
|
#include <private/qv4qmlcontext_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("Url"), 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(QV4::PersistentValue),
|
|
sizeof(QVariantMap),
|
|
sizeof(QDateTime),
|
|
sizeof(QUrl),
|
|
sizeof(QJSValue)
|
|
};
|
|
const int dataAlignments[] = {
|
|
alignof(StringOrTranslation),
|
|
alignof(double),
|
|
alignof(bool),
|
|
alignof(ListModel *),
|
|
alignof(QV4::PersistentValue),
|
|
alignof(QVariantMap),
|
|
alignof(QDateTime),
|
|
alignof(QUrl),
|
|
alignof(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.size();
|
|
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.size();
|
|
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.size();
|
|
int newRoleCount = src->roles.size() - 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.userType()) {
|
|
case QMetaType::Double: type = Role::Number; break;
|
|
case QMetaType::Int: type = Role::Number; break;
|
|
case QMetaType::Bool: type = Role::Bool; break;
|
|
case QMetaType::QString: type = Role::String; break;
|
|
case QMetaType::QVariantMap: type = Role::VariantMap; break;
|
|
case QMetaType::QDateTime: type = Role::DateTime; break;
|
|
case QMetaType::QUrl: type = Role::Url; break;
|
|
default: {
|
|
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 if (data.userType() >= QMetaType::User) {
|
|
type = Role::List;
|
|
break;
|
|
} else {
|
|
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()
|
|
{
|
|
clear();
|
|
}
|
|
|
|
void StringOrTranslation::setString(const QString &s)
|
|
{
|
|
clear();
|
|
if (s.isEmpty())
|
|
return;
|
|
QString mutableString(s);
|
|
QString::DataPointer dataPointer = mutableString.data_ptr();
|
|
arrayData = dataPointer->d_ptr();
|
|
stringData = dataPointer->data();
|
|
stringSize = mutableString.size();
|
|
if (arrayData)
|
|
arrayData->ref();
|
|
}
|
|
|
|
void StringOrTranslation::setTranslation(const QV4::CompiledData::Binding *binding)
|
|
{
|
|
clear();
|
|
this->binding = binding;
|
|
}
|
|
|
|
QString StringOrTranslation::toString(const QQmlListModel *owner) const
|
|
{
|
|
if (stringSize) {
|
|
if (arrayData)
|
|
arrayData->ref();
|
|
return QString(QStringPrivate(arrayData, stringData, stringSize));
|
|
}
|
|
if (!owner)
|
|
return QString();
|
|
return owner->m_compilationUnit->bindingValueAsString(binding);
|
|
}
|
|
|
|
QString StringOrTranslation::asString() const
|
|
{
|
|
if (!arrayData)
|
|
return QString();
|
|
arrayData->ref();
|
|
return QString(QStringPrivate(arrayData, stringData, stringSize));
|
|
}
|
|
|
|
void StringOrTranslation::clear()
|
|
{
|
|
if (arrayData && !arrayData->deref())
|
|
QTypedArrayData<ushort>::deallocate(arrayData);
|
|
arrayData = nullptr;
|
|
stringData = nullptr;
|
|
stringSize = 0;
|
|
binding = 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;
|
|
|
|
const QAbstractDeclarativeData *old = std::exchange(
|
|
QObjectPrivate::get(e->m_objectCache)->declarativeData,
|
|
new (ddataMemory) QQmlData(QQmlData::DoesNotOwnMemory));
|
|
Q_ASSERT(!old); // QObject should really not manipulate QQmlData
|
|
|
|
(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;
|
|
const int targetElementCount = target->elements.count();
|
|
for (int i = 0 ; i < targetElementCount ; ++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();
|
|
++rowsInserted;
|
|
} else {
|
|
bool validMove = targetModel->beginMoveRows(QModelIndex(), s.targetIndex, s.targetIndex, QModelIndex(), i);
|
|
Q_ASSERT(validMove);
|
|
targetModel->endMoveRows();
|
|
// fixup target indices of elements that still need to move
|
|
for (int j=i+1; j < targetElementCount; ++j) {
|
|
ListElement *eToFix = target->elements.at(j);
|
|
ElementSync &sToFix = elementHash.find(eToFix->getUid()).value();
|
|
if (i < s.targetIndex) {
|
|
// element was moved down
|
|
if (sToFix.targetIndex > s.targetIndex || sToFix.targetIndex < i)
|
|
continue; // unaffected by reordering
|
|
else
|
|
sToFix.targetIndex += 1;
|
|
} else {
|
|
// element was moved up
|
|
if (sToFix.targetIndex < s.targetIndex || sToFix.targetIndex > i)
|
|
continue; // unaffected by reordering
|
|
else
|
|
sToFix.targetIndex -= 1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
hasChanges = true;
|
|
}
|
|
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::updateTranslations()
|
|
{
|
|
for (int index = 0; index != elements.count(); ++index) {
|
|
ListElement *e = elements[index];
|
|
if (ModelNodeMetaObject *cache = e->objectCache()) {
|
|
// TODO: more fine grained tracking?
|
|
cache->updateValues();
|
|
}
|
|
}
|
|
}
|
|
|
|
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::UrlObject *url = propertyValue->as<QV4::UrlObject>()) {
|
|
const ListLayout::Role &r = m_layout->getRoleOrCreate(propertyName, ListLayout::Role::Url);
|
|
QUrl qurl = QUrl(url->href());
|
|
roleIndex = e->setUrlProperty(r, qurl);
|
|
} 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, func);
|
|
roleIndex = e->setFunctionProperty(r, jsv);
|
|
} else if (QV4::Object *o = propertyValue->as<QV4::Object>()) {
|
|
if (QV4::QObjectWrapper *wrapper = o->as<QV4::QObjectWrapper>()) {
|
|
const ListLayout::Role &role = m_layout->getRoleOrCreate(propertyName, ListLayout::Role::QObject);
|
|
if (role.type == ListLayout::Role::QObject)
|
|
roleIndex = e->setQObjectProperty(role, wrapper);
|
|
} else if (QVariant maybeUrl = QV4::ExecutionEngine::toVariant(
|
|
o->asReturnedValue(), QMetaType::fromType<QUrl>(), true);
|
|
maybeUrl.metaType() == QMetaType::fromType<QUrl>()) {
|
|
const ListLayout::Role &r = m_layout->getRoleOrCreate(propertyName, ListLayout::Role::Url);
|
|
QUrl qurl = maybeUrl.toUrl();
|
|
roleIndex = e->setUrlProperty(r, qurl);
|
|
} 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, ListModel::SetElement reason)
|
|
{
|
|
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::UrlObject *url = propertyValue->as<QV4::UrlObject>()){
|
|
const ListLayout::Role &r = m_layout->getRoleOrCreate(propertyName, ListLayout::Role::Url);
|
|
if (r.type == ListLayout::Role::Url) {
|
|
QUrl qurl = QUrl(url->href()); // does what the private UrlObject->toQUrl would do
|
|
e->setUrlPropertyFast(r, qurl);
|
|
}
|
|
} else if (QV4::Object *o = propertyValue->as<QV4::Object>()) {
|
|
if (QV4::QObjectWrapper *wrapper = o->as<QV4::QObjectWrapper>()) {
|
|
const ListLayout::Role &r = m_layout->getRoleOrCreate(propertyName, ListLayout::Role::QObject);
|
|
if (r.type == ListLayout::Role::QObject)
|
|
e->setQObjectPropertyFast(r, wrapper);
|
|
} else {
|
|
QVariant maybeUrl = QV4::ExecutionEngine::toVariant(
|
|
o->asReturnedValue(), QMetaType::fromType<QUrl>(), true);
|
|
if (maybeUrl.metaType() == QMetaType::fromType<QUrl>()) {
|
|
const QUrl qurl = maybeUrl.toUrl();
|
|
const ListLayout::Role &r = m_layout->getRoleOrCreate(propertyName, ListLayout::Role::Url);
|
|
if (r.type == ListLayout::Role::Url)
|
|
e->setUrlPropertyFast(r, qurl);
|
|
} 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()) {
|
|
if (reason == SetElement::WasJustInserted) {
|
|
QQmlError err;
|
|
auto memberName = propertyName->toString(v4)->toQString();
|
|
err.setDescription(QString::fromLatin1("%1 is %2. Adding an object with a %2 member does not create a role for it.").arg(memberName, propertyValue->isNull() ? QLatin1String("null") : QLatin1String("undefined")));
|
|
qmlWarning(nullptr, err);
|
|
} else {
|
|
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, SetElement::WasJustInserted);
|
|
}
|
|
|
|
int ListModel::append(QV4::Object *object)
|
|
{
|
|
int elementIndex = appendElement();
|
|
set(elementIndex, object, SetElement::WasJustInserted);
|
|
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;
|
|
}
|
|
|
|
QV4::QObjectWrapper *ListElement::getQObjectProperty(const ListLayout::Role &role)
|
|
{
|
|
char *mem = getPropertyMemory(role);
|
|
QV4::PersistentValue *g = reinterpret_cast<QV4::PersistentValue *>(mem);
|
|
return g->as<QV4::QObjectWrapper>();
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
QUrl *ListElement::getUrlProperty(const ListLayout::Role &role)
|
|
{
|
|
QUrl *url = nullptr;
|
|
|
|
char *mem = getPropertyMemory(role);
|
|
if (isMemoryUsed<QUrl>(mem))
|
|
url = reinterpret_cast<QUrl *>(mem);
|
|
|
|
return url;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
QV4::PersistentValue *
|
|
ListElement::getGuardProperty(const ListLayout::Role &role)
|
|
{
|
|
char *mem = getPropertyMemory(role);
|
|
|
|
bool existingGuard = false;
|
|
for (size_t i = 0; i < sizeof(QV4::PersistentValue);
|
|
++i) {
|
|
if (mem[i] != 0) {
|
|
existingGuard = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
QV4::PersistentValue *g = nullptr;
|
|
|
|
if (existingGuard)
|
|
g = reinterpret_cast<QV4::PersistentValue *>(mem);
|
|
|
|
return g;
|
|
}
|
|
|
|
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);
|
|
else
|
|
data = QString();
|
|
}
|
|
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:
|
|
{
|
|
QV4::PersistentValue *guard = reinterpret_cast<QV4::PersistentValue *>(mem);
|
|
data = QVariant::fromValue(guard->as<QV4::QObjectWrapper>()->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::Url:
|
|
{
|
|
if (isMemoryUsed<QUrl>(mem)) {
|
|
QUrl *url = reinterpret_cast<QUrl *>(mem);
|
|
data = *url;
|
|
}
|
|
}
|
|
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, QV4::QObjectWrapper *o)
|
|
{
|
|
int roleIndex = -1;
|
|
|
|
if (role.type == ListLayout::Role::QObject) {
|
|
char *mem = getPropertyMemory(role);
|
|
if (isMemoryUsed<QVariantMap>(mem))
|
|
reinterpret_cast<QV4::PersistentValue *>(mem)->set(o->engine(), *o);
|
|
else
|
|
new (mem) QV4::PersistentValue(o->engine(), o);
|
|
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::setUrlProperty(const ListLayout::Role &role, const QUrl &url)
|
|
{
|
|
int roleIndex = -1;
|
|
|
|
if (role.type == ListLayout::Role::Url) {
|
|
char *mem = getPropertyMemory(role);
|
|
if (isMemoryUsed<QUrl>(mem)) {
|
|
QUrl *qurl = reinterpret_cast<QUrl *>(mem);
|
|
qurl->~QUrl();
|
|
}
|
|
new (mem) QUrl(url);
|
|
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);
|
|
reinterpret_cast<StringOrTranslation *>(mem)->setString(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, QV4::QObjectWrapper *o)
|
|
{
|
|
char *mem = getPropertyMemory(role);
|
|
new (mem) QV4::PersistentValue(o->engine(), 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::setUrlPropertyFast(const ListLayout::Role &role, const QUrl &url)
|
|
{
|
|
char *mem = getPropertyMemory(role);
|
|
new (mem) QUrl(url);
|
|
}
|
|
|
|
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::Url:
|
|
setUrlProperty(role, QUrl());
|
|
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:
|
|
{
|
|
QV4::QObjectWrapper *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:
|
|
{
|
|
if (QV4::PersistentValue *guard = getGuardProperty(r))
|
|
guard->~PersistentValue();
|
|
}
|
|
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::Url:
|
|
{
|
|
QUrl *url = getUrlProperty(r);
|
|
if (url)
|
|
url->~QUrl();
|
|
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::Url:
|
|
roleIndex = setUrlProperty(role, d.toUrl());
|
|
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::UrlObject>()) {
|
|
QV4::Scoped<QV4::UrlObject> url(scope, d);
|
|
QUrl qurl = QUrl(url->href());
|
|
roleIndex = setUrlProperty(role, qurl);
|
|
} else if (d.as<QV4::FunctionObject>()) {
|
|
QV4::ScopedFunctionObject f(scope, d);
|
|
QJSValue jsv;
|
|
QJSValuePrivate::setValue(&jsv, 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) {
|
|
roleIndex = setQObjectProperty(role, wrapper);
|
|
} else if (role.type == ListLayout::Role::VariantMap) {
|
|
roleIndex = setVariantMapProperty(role, o);
|
|
} else if (role.type == ListLayout::Role::Url) {
|
|
QVariant maybeUrl = QV4::ExecutionEngine::toVariant(
|
|
o.asReturnedValue(), QMetaType::fromType<QUrl>(), true);
|
|
if (maybeUrl.metaType() == QMetaType::fromType<QUrl>()) {
|
|
roleIndex = setUrlProperty(role, maybeUrl.toUrl());
|
|
}
|
|
}
|
|
} 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()
|
|
{
|
|
}
|
|
|
|
QMetaObject *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.size());
|
|
return;
|
|
}
|
|
int roleCount = roles.size();
|
|
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);
|
|
if (auto recursiveListModel = qvariant_cast<QQmlListModel*>(value)) {
|
|
auto size = recursiveListModel->count();
|
|
auto array = ScopedArrayObject{scope, v4->newArrayObject(size)};
|
|
for (auto i = 0; i < size; i++) {
|
|
array->arrayPut(i, QJSValuePrivate::convertToReturnedValue(
|
|
v4, recursiveListModel->get(i)));
|
|
}
|
|
pd->value = array;
|
|
} else {
|
|
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.size();
|
|
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.userType() == QMetaType::QVariantList) {
|
|
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.userType() == QMetaType::QVariantList) {
|
|
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 to 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 qml/listmodel/WorkerScript.qml 0
|
|
|
|
The included file, \tt dataloader.mjs, looks like this:
|
|
|
|
\snippet qml/listmodel/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 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.size(); ++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.size(); ++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.size() ; ++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.size() ; ++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.size() ; ++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.size() ; ++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.size()) {
|
|
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.size() : 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 \a count number of items 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();
|
|
}
|
|
|
|
void QQmlListModel::updateTranslations()
|
|
{
|
|
// assumption: it is impossible to have retranslatable strings in a
|
|
// dynamic list model, as they would already have "decayed" to strings
|
|
// when they were inserted
|
|
if (m_dynamicRoles)
|
|
return;
|
|
Q_ASSERT(m_listModel);
|
|
|
|
QList<int> roles;
|
|
for (int i = 0, end = m_listModel->roleCount(); i != end; ++i) {
|
|
if (m_listModel->getExistingRole(i).type == ListLayout::Role::String)
|
|
roles.append(i);
|
|
}
|
|
|
|
if (!roles.isEmpty())
|
|
emitItemsChanged(0, rowCount(QModelIndex()), roles);
|
|
|
|
m_listModel->updateTranslations();
|
|
}
|
|
|
|
/*!
|
|
\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.size();
|
|
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 QJSValuePrivate::fromReturnedValue(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::asReturnedValue(&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.size())
|
|
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.size();
|
|
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::ExecutableCompilationUnit> &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 = compilationUnit->bindingValueAsScriptString(binding);
|
|
if (!binding->isFunctionExpression() && !definesEmptyList(scriptStr)) {
|
|
bool ok;
|
|
evaluateEnum(scriptStr, &ok);
|
|
if (!ok) {
|
|
error(binding, QQmlListModel::tr("ListElement: cannot use script for property value"));
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool QQmlListModelParser::applyProperty(
|
|
const QQmlRefPointer<QV4::ExecutableCompilationUnit> &compilationUnit,
|
|
const QV4::CompiledData::Binding *binding, ListModel *model, int outterElementIndex)
|
|
{
|
|
const QString elementName = compilationUnit->stringAt(binding->propertyNameIndex);
|
|
|
|
bool roleSet = false;
|
|
const QV4::CompiledData::Binding::Type bindingType = binding->type();
|
|
if (bindingType >= 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;
|
|
|
|
const bool isTranslationBinding = binding->isTranslationBinding();
|
|
if (isTranslationBinding) {
|
|
value = QVariant::fromValue<const QV4::CompiledData::Binding*>(binding);
|
|
} else if (binding->evaluatesToString()) {
|
|
value = compilationUnit->bindingValueAsString(binding);
|
|
} else if (bindingType == QV4::CompiledData::Binding::Type_Number) {
|
|
value = compilationUnit->bindingValueAsNumber(binding);
|
|
} else if (bindingType == QV4::CompiledData::Binding::Type_Boolean) {
|
|
value = binding->valueAsBoolean();
|
|
} else if (bindingType == QV4::CompiledData::Binding::Type_Null) {
|
|
value = QVariant::fromValue(nullptr);
|
|
} else if (bindingType == QV4::CompiledData::Binding::Type_Script) {
|
|
QString scriptStr = compilationUnit->bindingValueAsScriptString(binding);
|
|
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]));
|
|
|
|
QJSValue v;
|
|
QV4::ScopedValue result(scope, function->call(v4->globalObject, nullptr, 0));
|
|
if (v4->hasException)
|
|
v4->catchException();
|
|
else
|
|
QJSValuePrivate::setValue(&v, result->asReturnedValue());
|
|
value.setValue(v);
|
|
} else {
|
|
bool ok;
|
|
value = evaluateEnum(scriptStr, &ok);
|
|
}
|
|
} else {
|
|
Q_UNREACHABLE();
|
|
}
|
|
|
|
if (!model)
|
|
return roleSet;
|
|
model->setOrCreateProperty(outterElementIndex, elementName, value);
|
|
auto listModel = model->m_modelCache;
|
|
if (isTranslationBinding && listModel) {
|
|
if (!listModel->translationChangeHandler) {
|
|
auto ep = QQmlEnginePrivate::get(compilationUnit->engine);
|
|
model->m_modelCache->translationChangeHandler = std::make_unique<QPropertyNotifier>(
|
|
ep->translationLanguage.addNotifier([listModel](){ listModel->updateTranslations(); }));
|
|
}
|
|
}
|
|
roleSet = true;
|
|
}
|
|
return roleSet;
|
|
}
|
|
|
|
void QQmlListModelParser::verifyBindings(const QQmlRefPointer<QV4::ExecutableCompilationUnit> &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::ExecutableCompilationUnit> &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.size()-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_p.cpp"
|
|
|
|
#include "moc_qqmllistmodel_p.cpp"
|