QGIM: implement itemData/setItemData for gadgets

If the item at index is a gadget (or QObject type), then use the
properties that match the names of the roles to get and set the item
data.

This is transactional for gadgets, where we can modify a copy until all
entries have been successfully written, and then swap the copy and the
original value if all writes were successful. It cannot be transactional
for QObject instances.

Change-Id: I514853cbd67aaf6e86c100e815c8af579b8278c2
Reviewed-by: Artem Dyomin <artem.dyomin@qt.io>
This commit is contained in:
Volker Hilsheimer 2025-03-06 21:20:11 +01:00
parent 8da1ec6a09
commit ff14e6fe08
3 changed files with 110 additions and 19 deletions

View File

@ -358,6 +358,12 @@ bool QGenericItemModel::setData(const QModelIndex &index, const QVariant &data,
from either \c{int}, Qt::ItemDataRole, or QString to a QVariant, then the
data from that container is returned.
If the item type is a gadget or QObject subclass, then the values of those
properties that match a \l{roleNames()}{role name} are returned.
If the item is not an associative container, gadget, or QObject subclass,
then this calls the base class implementation.
\sa setItemData(), Qt::ItemDataRole, data()
*/
QMap<int, QVariant> QGenericItemModel::itemData(const QModelIndex &index) const
@ -374,14 +380,20 @@ QMap<int, QVariant> QGenericItemModel::itemData(const QModelIndex &index) const
QString to QVariant, then only those values in \a data are stored for which
there is a mapping in the \l{roleNames()}{role names} table.
If the item type is a gadget or QObject subclass, then those properties that
match a \l{roleNames()}{role name} are set to the corresponding value in
\a data.
Roles for which there is no entry in \a data are not modified.
This implementation is transactional, and returns true if all the entries
from \a data could be stored. If any entry could not be updated, then the
original container is not modified at all, and the function returns false.
For item types that can be copied, this implementation is transactional,
and returns true if all the entries from \a data could be stored. If any
entry could not be updated, then the original container is not modified at
all, and the function returns false.
If the item is not an associative container, then this calls the base class
implementation, which calls setData() for each entry in \a data.
If the item is not an associative container, gadget, or QObject subclass,
then this calls the base class implementation, which calls setData() for
each entry in \a data.
\sa itemData(), setData(), Qt::ItemDataRole
*/

View File

@ -347,6 +347,28 @@ public:
result.insert(role, QGenericItemModelDetails::value(it));
}
}
} else if constexpr (has_metaobject<value_type>) {
if (row_traits::fixed_size() <= 1) {
tried = true;
using meta_type = std::remove_pointer_t<value_type>;
const QMetaObject &mo = meta_type::staticMetaObject;
for (auto &&[role, roleName] : roleNames().asKeyValueRange()) {
QVariant data;
if constexpr (std::is_base_of_v<QObject, meta_type>) {
if (value)
data = value->property(roleName);
} else {
const int pi = mo.indexOfProperty(roleName.constData());
if (pi >= 0) {
const QMetaProperty prop = mo.property(pi);
if (prop.isValid())
data = prop.readOnGadget(QGenericItemModelDetails::pointerTo(value));
}
}
if (data.isValid())
result[role] = data;
}
}
}
};
@ -477,6 +499,52 @@ public:
target[QString::fromUtf8(roleName(role))] = value;
}
return true;
} else if constexpr (has_metaobject<value_type>) {
if (row_traits::fixed_size() <= 1) {
tried = true;
using meta_type = std::remove_pointer_t<value_type>;
const QMetaObject &mo = meta_type::staticMetaObject;
// transactional: if possible, modify a copy and only
// update target if all values from data could be stored.
auto targetCopy = [](auto &&origin) {
if constexpr (std::is_base_of_v<QObject, meta_type>)
return origin; // can't copy, no transaction support
else if constexpr (std::is_pointer_v<decltype(target)>)
return *origin;
else if constexpr (std::is_copy_assignable_v<value_type>)
return origin;
else // can't copy - targetCopy is now a pointer
return &origin;
}(target);
for (auto &&[role, value] : data.asKeyValueRange()) {
const QByteArray roleName = roleNames().value(role);
bool written = false;
if constexpr (std::is_base_of_v<QObject, meta_type>) {
if (targetCopy)
written = targetCopy->setProperty(roleName, value);
} else {
const int pi = mo.indexOfProperty(roleName.constData());
if (pi >= 0) {
const QMetaProperty prop = mo.property(pi);
if (prop.isValid())
written = prop.writeOnGadget(QGenericItemModelDetails::pointerTo(targetCopy), value);
}
}
if (!written) {
qWarning("Failed to write value for %s", roleName.data());
return false;
}
}
if constexpr (std::is_base_of_v<QObject, meta_type>)
target = targetCopy; // nothing actually copied
else if constexpr (std::is_pointer_v<decltype(target)>)
qSwap(*target, targetCopy);
else if constexpr (std::is_pointer_v<decltype(targetCopy)>)
; // couldn't copy
else
qSwap(target, targetCopy);
return true;
}
}
return false;
};

View File

@ -333,7 +333,7 @@ void tst_QGenericItemModel::createTestData()
<< 1 << ChangeActions(ChangeAction::SetData);
ADD_POINTER(cArrayFixedColumns)
<< int(std::tuple_size_v<Row>) << ChangeActions(ChangeAction::SetData);
<< int(std::tuple_size_v<Row>) << (ChangeAction::SetData | ChangeAction::SetItemData);
ADD_COPY(vectorOfFixedColumns)
<< 2 << (ChangeAction::ChangeRows | ChangeAction::SetData);
@ -344,22 +344,24 @@ void tst_QGenericItemModel::createTestData()
ADD_POINTER(vectorOfArrays)
<< 10 << (ChangeAction::ChangeRows | ChangeAction::SetData);
ADD_COPY(vectorOfStructs)
<< int(std::tuple_size_v<Row>) << (ChangeAction::ChangeRows | ChangeAction::SetData);
<< int(std::tuple_size_v<Row>) << (ChangeAction::ChangeRows | ChangeAction::SetData
| ChangeAction::SetItemData);
ADD_POINTER(vectorOfStructs)
<< int(std::tuple_size_v<Row>) << (ChangeAction::ChangeRows | ChangeAction::SetData);
<< int(std::tuple_size_v<Row>) << (ChangeAction::ChangeRows | ChangeAction::SetData
| ChangeAction::SetItemData);
ADD_COPY(vectorOfConstStructs)
<< int(std::tuple_size_v<ConstRow>) << ChangeActions(ChangeAction::ChangeRows);
ADD_POINTER(vectorOfConstStructs)
<< int(std::tuple_size_v<ConstRow>) << ChangeActions(ChangeAction::ChangeRows);
ADD_COPY(vectorOfGadgets)
<< 3 << (ChangeAction::ChangeRows | ChangeAction::SetData);
<< 3 << (ChangeAction::ChangeRows | ChangeAction::SetData | ChangeAction::SetItemData);
ADD_POINTER(vectorOfGadgets)
<< 3 << (ChangeAction::ChangeRows | ChangeAction::SetData);
<< 3 << (ChangeAction::ChangeRows | ChangeAction::SetData | ChangeAction::SetItemData);
ADD_COPY(listOfGadgets)
<< 1 << (ChangeAction::ChangeRows | ChangeAction::SetData);
<< 1 << (ChangeAction::ChangeRows | ChangeAction::SetData | ChangeAction::SetItemData);
ADD_POINTER(listOfGadgets)
<< 1 << (ChangeAction::ChangeRows | ChangeAction::SetData);
<< 1 << (ChangeAction::ChangeRows | ChangeAction::SetData | ChangeAction::SetItemData);
ADD_COPY(listOfObjects)
<< 2 << (ChangeAction::ChangeRows | ChangeAction::SetData);
@ -369,9 +371,10 @@ void tst_QGenericItemModel::createTestData()
<< 5 << ChangeActions(ChangeAction::All);
// only adding as pointer, copy would operate on the same data
ADD_POINTER(tableOfPointers)
<< 2 << ChangeActions(ChangeAction::All);
<< 2 << ChangeActions(ChangeAction::All | ChangeAction::SetItemData);
ADD_POINTER(tableOfRowPointers)
<< int(std::tuple_size_v<Row>) << (ChangeAction::ChangeRows | ChangeAction::SetData);
<< int(std::tuple_size_v<Row>) << (ChangeAction::ChangeRows | ChangeAction::SetData
| ChangeAction::SetItemData);
ADD_COPY(arrayOfConstNumbers)
<< 1 << ChangeActions(ChangeAction::ReadOnly);
@ -577,8 +580,11 @@ void tst_QGenericItemModel::itemData()
const QModelIndex index = model->index(0, 0);
const QMap<int, QVariant> itemData = model->itemData(index);
for (int role = 0; role < Qt::UserRole; ++role)
for (int role = 0; role < Qt::UserRole; ++role) {
if (role == Qt::EditRole) // we fake that in data()
continue;
QCOMPARE(itemData.value(role), index.data(role));
}
}
void tst_QGenericItemModel::setItemData()
@ -592,14 +598,16 @@ void tst_QGenericItemModel::setItemData()
const QModelIndex index = model->index(0, 0);
QMap<int, QVariant> itemData = model->itemData(index);
// we only care about multi-role models
if (itemData.keys() == QList<int>{Qt::DisplayRole, Qt::EditRole})
const auto roles = itemData.keys();
if (roles == QList<int>{Qt::DisplayRole, Qt::EditRole})
QSKIP("Can't test setItemData on models with single values!");
itemData = {};
const auto roles = model->roleNames().keys();
for (int role : roles) {
QVariant data = QStringLiteral("Role %1").arg(role);
if (role == Qt::EditRole) // faked
continue;
QVariant data = role != Qt::DecorationRole ? QVariant(QStringLiteral("Role %1").arg(role))
: QVariant(QColor(Qt::magenta));
itemData.insert(role, data);
}
@ -621,6 +629,9 @@ void tst_QGenericItemModel::setItemData()
}
for (int role = 0; role < Qt::UserRole; ++role) {
if (role == Qt::EditRole) // faked role
continue;
QVariant data = index.data(role);
auto diagnostics = qScopeGuard([&]{
qDebug() << "Mismatch for" << Qt::ItemDataRole(role);