QGIM: implement itemData/setItemData for associative containers

If the item at index is an associative container, then we can operate
directly on that container. Otherwise, call the base class
implementation.

In contrast to the default implementation, the setItemData
implementation is transactional: nothing will be written to a multi-role
storage unless all entries could be written.

Change-Id: I883c647dac82b3a068afd77b36c245525d59b44b
Reviewed-by: Artem Dyomin <artem.dyomin@qt.io>
This commit is contained in:
Volker Hilsheimer 2025-03-06 18:15:36 +01:00
parent 9f63577ddd
commit dc3fb041e9
4 changed files with 270 additions and 8 deletions

View File

@ -108,13 +108,15 @@ QT_BEGIN_NAMESPACE
\l{Qt::ItemDataRole}, or QString as the key type, and QVariant as the
mapped type, then QGenericItemModel interprets that container as the storage
of the data for multiple roles. The data() and setData() functions return
and modify the mapped value in the container, and clearItemData() clears
the entire container.
and modify the mapped value in the container, and setItemData() modifies all
provided values, itemData() returns all stored values, and clearItemData()
clears the entire container.
\snippet qgenericitemmodel/main.cpp color_map
The most efficient data type to use as the key is Qt::ItemDataRole or
\c{int}.
\c{int}. When using \c{int}, itemData() returns the container as is, and
doesn't have to create a copy of the data.
\section2 The C++ tuple protocol
@ -315,6 +317,48 @@ bool QGenericItemModel::setData(const QModelIndex &index, const QVariant &data,
return impl->call<bool>(QGenericItemModelImplBase::SetData, index, data, role);
}
/*!
\reimp
Returns a map with values for all predefined roles in the model for the
item at the given \a index.
If the item type for that \a index is an associative container that maps
from either \c{int}, Qt::ItemDataRole, or QString to a QVariant, then the
data from that container is returned.
\sa setItemData(), Qt::ItemDataRole, data()
*/
QMap<int, QVariant> QGenericItemModel::itemData(const QModelIndex &index) const
{
return impl->callConst<QMap<int, QVariant>>(QGenericItemModelImplBase::ItemData, index);
}
/*!
\reimp
If the item type for that \a index is an associative container that maps
from either \c{int} or Qt::ItemDataRole to a QVariant, then the entries in
\a data are stored in that container. If the associative container maps from
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.
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.
If the item is not an associative container, then this calls the base class
implementation, which calls setData() for each entry in \a data.
\sa itemData(), setData(), Qt::ItemDataRole
*/
bool QGenericItemModel::setItemData(const QModelIndex &index, const QMap<int, QVariant> &data)
{
return impl->call<bool>(QGenericItemModelImplBase::SetItemData, index, data);
}
/*!
\reimp

View File

@ -5,6 +5,7 @@
#define QGENERICITEMMODEL_H
#include <QtCore/qgenericitemmodel_impl.h>
#include <QtCore/qmap.h>
QT_BEGIN_NAMESPACE
@ -26,6 +27,8 @@ public:
QVariant headerData(int section, Qt::Orientation orientation, int role) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
bool setData(const QModelIndex &index, const QVariant &data, int role = Qt::EditRole) override;
QMap<int, QVariant> itemData(const QModelIndex &index) const override;
bool setItemData(const QModelIndex &index, const QMap<int, QVariant> &data) override;
bool clearItemData(const QModelIndex &index) override;
bool insertColumns(int column, int count, const QModelIndex &parent = {}) override;
bool removeColumns(int column, int count, const QModelIndex &parent = {}) override;
@ -170,6 +173,8 @@ public:
break;
case Data: makeCall(that, &Self::data, r, args);
break;
case ItemData: makeCall(that, &Self::itemData, r, args);
break;
}
}
@ -180,6 +185,8 @@ public:
break;
case SetData: makeCall(that, &Self::setData, r, args);
break;
case SetItemData: makeCall(that, &Self::setItemData, r, args);
break;
case ClearItemData: makeCall(that, &Self::clearItemData, r, args);
break;
case InsertColumns: makeCall(that, &Self::insertColumns, r, args);
@ -283,6 +290,50 @@ public:
return result;
}
QMap<int, QVariant> itemData(const QModelIndex &index) const
{
QMap<int, QVariant> result;
bool tried = false;
const auto readItemData = [this, &result, &tried](auto &&value){
Q_UNUSED(this);
using value_type = q20::remove_cvref_t<decltype(value)>;
using multi_role = QGenericItemModelDetails::is_multi_role<value_type>;
if constexpr (multi_role()) {
tried = true;
if constexpr (std::is_convertible_v<value_type, decltype(result)>) {
result = value;
} else {
for (auto it = std::cbegin(value); it != std::cend(value); ++it) {
int role = [this, key = QGenericItemModelDetails::key(it)]() {
Q_UNUSED(this);
if constexpr (multi_role::int_key)
return int(key);
else
return roleNames().key(key.toUtf8(), -1);
}();
if (role != -1)
result.insert(role, QGenericItemModelDetails::value(it));
}
}
}
};
if (index.isValid()) {
const_row_reference row = rowData(index);
if constexpr (dynamicColumns())
readItemData(*std::next(std::cbegin(row), index.column()));
else if constexpr (static_column_count == 0)
readItemData(row);
else if (QGenericItemModelDetails::isValid(row))
for_element_at(row, index.column(), readItemData);
if (!tried) // no multi-role item found
return m_itemModel->QAbstractItemModel::itemData(index);
}
return result;
}
bool setData(const QModelIndex &index, const QVariant &data, int role)
{
if (!index.isValid())
@ -342,6 +393,78 @@ public:
return success;
}
bool setItemData(const QModelIndex &index, const QMap<int, QVariant> &data)
{
if (!index.isValid() || data.isEmpty())
return false;
bool success = false;
if constexpr (isMutable()) {
auto emitDataChanged = qScopeGuard([&success, this, &index, &data]{
if (success)
Q_EMIT dataChanged(index, index, data.keys());
});
bool tried = false;
auto writeItemData = [this, &tried, &data](auto &target) -> bool {
Q_UNUSED(this);
using value_type = q20::remove_cvref_t<decltype(target)>;
using multi_role = QGenericItemModelDetails::is_multi_role<value_type>;
if constexpr (multi_role()) {
using key_type = typename value_type::key_type;
tried = true;
const auto roleName = [map = roleNames()](int role) { return map.value(role); };
// transactional: only update target if all values from data
// can be stored. Storing never fails with int-keys.
if constexpr (!multi_role::int_key)
{
auto invalid = std::find_if(data.keyBegin(), data.keyEnd(),
[&roleName](int role) { return roleName(role).isEmpty(); }
);
if (invalid != data.keyEnd()) {
qWarning("No role name set for %d", *invalid);
return false;
}
}
for (auto &&[role, value] : data.asKeyValueRange()) {
if constexpr (multi_role::int_key)
target[static_cast<key_type>(role)] = value;
else
target[QString::fromUtf8(roleName(role))] = value;
}
return true;
}
return false;
};
row_reference row = rowData(index);
if constexpr (dynamicColumns()) {
success = writeItemData(*std::next(std::begin(row), index.column()));
} else if constexpr (static_column_count == 0) {
success = writeItemData(row);
} else if (QGenericItemModelDetails::isValid(row)) {
for_element_at(row, index.column(), [&writeItemData, &success](auto &&target){
using target_type = decltype(target);
// we can only assign to an lvalue reference
if constexpr (std::is_lvalue_reference_v<target_type>
&& !std::is_const_v<std::remove_reference_t<target_type>>) {
success = writeItemData(std::forward<target_type>(target));
}
});
}
if (!tried) {
// setItemData will emit the dataChanged signal
emitDataChanged.dismiss();
success = m_itemModel->QAbstractItemModel::setItemData(index, data);
}
}
return success;
}
bool clearItemData(const QModelIndex &index)
{
if (!index.isValid())

View File

@ -295,11 +295,13 @@ public:
Flags,
HeaderData,
Data,
ItemData,
};
enum Op {
Destroy,
SetData,
SetItemData,
ClearItemData,
InsertColumns,
RemoveColumns,

View File

@ -84,6 +84,10 @@ private slots:
void data();
void setData_data() { createTestData(); }
void setData();
void itemData_data() { createTestData(); }
void itemData();
void setItemData_data() { createTestData(); }
void setItemData();
void clearItemData_data() { createTestData(); }
void clearItemData();
void insertRows_data() { createTestData(); }
@ -183,6 +187,18 @@ private:
{{{Qt::DisplayRole, "DISPLAY2"}, {Qt::DecorationRole, "DECORATION2"}}},
{{{Qt::DisplayRole, "DISPLAY3"}, {Qt::DecorationRole, "DECORATION3"}}},
};
std::vector<std::vector<QMap<int, QVariant>>> tableOfIntRoles = {
{{{Qt::DisplayRole, "DISPLAY0"}, {Qt::DecorationRole, "DECORATION0"}}},
{{{Qt::DisplayRole, "DISPLAY1"}, {Qt::DecorationRole, "DECORATION1"}}},
{{{Qt::DisplayRole, "DISPLAY2"}, {Qt::DecorationRole, "DECORATION2"}}},
{{{Qt::DisplayRole, "DISPLAY3"}, {Qt::DecorationRole, "DECORATION3"}}},
};
std::vector<std::vector<std::map<int, QVariant>>> stdTableOfIntRoles = {
{{{Qt::DisplayRole, "DISPLAY0"}, {Qt::DecorationRole, "DECORATION0"}}},
{{{Qt::DisplayRole, "DISPLAY1"}, {Qt::DecorationRole, "DECORATION1"}}},
{{{Qt::DisplayRole, "DISPLAY2"}, {Qt::DecorationRole, "DECORATION2"}}},
{{{Qt::DisplayRole, "DISPLAY3"}, {Qt::DecorationRole, "DECORATION3"}}},
};
};
std::unique_ptr<Data> m_data;
@ -198,7 +214,8 @@ public:
RemoveColumns = 0x08,
ChangeColumns = InsertColumns | RemoveColumns,
SetData = 0x10,
All = ChangeRows | ChangeColumns | SetData
All = ChangeRows | ChangeColumns | SetData,
SetItemData = 0x20,
};
Q_DECLARE_FLAGS(ChangeActions, ChangeAction);
};
@ -288,13 +305,21 @@ void tst_QGenericItemModel::createTestData()
<< 5 << ChangeActions(ChangeAction::ReadOnly);
ADD_COPY(listOfNamedRoles)
<< 1 << (ChangeAction::ChangeRows | ChangeAction::SetData);
<< 1 << (ChangeAction::ChangeRows | ChangeAction::SetData | ChangeAction::SetItemData);
ADD_POINTER(listOfNamedRoles)
<< 1 << (ChangeAction::ChangeRows | ChangeAction::SetData);
<< 1 << (ChangeAction::ChangeRows | ChangeAction::SetData | ChangeAction::SetItemData);
ADD_COPY(tableOfEnumRoles)
<< 1 << ChangeActions(ChangeAction::All);
<< 1 << ChangeActions(ChangeAction::All | ChangeAction::SetItemData);
ADD_POINTER(tableOfEnumRoles)
<< 1 << ChangeActions(ChangeAction::All);
<< 1 << ChangeActions(ChangeAction::All | ChangeAction::SetItemData);
ADD_COPY(tableOfIntRoles)
<< 1 << ChangeActions(ChangeAction::All | ChangeAction::SetItemData);
ADD_POINTER(tableOfIntRoles)
<< 1 << ChangeActions(ChangeAction::All | ChangeAction::SetItemData);
ADD_COPY(stdTableOfIntRoles)
<< 1 << ChangeActions(ChangeAction::All | ChangeAction::SetItemData);
ADD_POINTER(stdTableOfIntRoles)
<< 1 << ChangeActions(ChangeAction::All | ChangeAction::SetItemData);
#undef ADD_COPY
#undef ADD_POINTER
@ -459,6 +484,70 @@ void tst_QGenericItemModel::setData()
QCOMPARE(first.data() == oldValue, !changeActions.testFlag(ChangeAction::SetData));
}
void tst_QGenericItemModel::itemData()
{
QFETCH(Factory, factory);
auto model = factory();
QVERIFY(model->itemData({}).isEmpty());
const QModelIndex index = model->index(0, 0);
const QMap<int, QVariant> itemData = model->itemData(index);
for (int role = 0; role < Qt::UserRole; ++role)
QCOMPARE(itemData.value(role), index.data(role));
}
void tst_QGenericItemModel::setItemData()
{
QFETCH(Factory, factory);
auto model = factory();
QFETCH(const ChangeActions, changeActions);
QVERIFY(!model->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})
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);
itemData.insert(role, data);
}
QCOMPARE_NE(model->itemData(index), itemData);
QCOMPARE(model->setItemData(index, itemData),
changeActions.testFlag(ChangeAction::SetItemData));
if (!changeActions.testFlag(ChangeAction::SetItemData))
return; // nothing more to test for those models
{
const auto newItemData = model->itemData(index);
auto diagnostics = qScopeGuard([&]{
qDebug() << "Mismatch";
qDebug() << " Actual:" << newItemData;
qDebug() << " Expected:" << itemData;
});
QCOMPARE(newItemData == itemData, changeActions.testFlag(ChangeAction::SetItemData));
diagnostics.dismiss();
}
for (int role = 0; role < Qt::UserRole; ++role) {
QVariant data = index.data(role);
auto diagnostics = qScopeGuard([&]{
qDebug() << "Mismatch for" << Qt::ItemDataRole(role);
qDebug() << " Actual:" << data;
qDebug() << " Expected:" << itemData.value(role);
});
QCOMPARE(data == itemData.value(role), changeActions.testFlag(ChangeAction::SetData));
diagnostics.dismiss();
}
}
void tst_QGenericItemModel::clearItemData()
{
QFETCH(Factory, factory);
@ -495,6 +584,10 @@ void tst_QGenericItemModel::insertRows()
QEXPECT_FAIL("listOfNamedRolesCopy", "QVariantMap is empty by design", Continue);
QEXPECT_FAIL("tableOfEnumRolesPointer", "QVariantMap is empty by design", Continue);
QEXPECT_FAIL("tableOfEnumRolesCopy", "QVariantMap is empty by design", Continue);
QEXPECT_FAIL("tableOfIntRolesPointer", "QVariantMap is empty by design", Continue);
QEXPECT_FAIL("tableOfIntRolesCopy", "QVariantMap is empty by design", Continue);
QEXPECT_FAIL("stdTableOfIntRolesPointer", "std::map is empty by design", Continue);
QEXPECT_FAIL("stdTableOfIntRolesCopy", "std::map is empty by design", Continue);
};
// get and put data into the new row
const QModelIndex firstItem = model->index(0, 0);