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 \l{Qt::ItemDataRole}, or QString as the key type, and QVariant as the
mapped type, then QGenericItemModel interprets that container as the storage mapped type, then QGenericItemModel interprets that container as the storage
of the data for multiple roles. The data() and setData() functions return of the data for multiple roles. The data() and setData() functions return
and modify the mapped value in the container, and clearItemData() clears and modify the mapped value in the container, and setItemData() modifies all
the entire container. provided values, itemData() returns all stored values, and clearItemData()
clears the entire container.
\snippet qgenericitemmodel/main.cpp color_map \snippet qgenericitemmodel/main.cpp color_map
The most efficient data type to use as the key is Qt::ItemDataRole or 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 \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); 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 \reimp

View File

@ -5,6 +5,7 @@
#define QGENERICITEMMODEL_H #define QGENERICITEMMODEL_H
#include <QtCore/qgenericitemmodel_impl.h> #include <QtCore/qgenericitemmodel_impl.h>
#include <QtCore/qmap.h>
QT_BEGIN_NAMESPACE QT_BEGIN_NAMESPACE
@ -26,6 +27,8 @@ public:
QVariant headerData(int section, Qt::Orientation orientation, int role) const override; QVariant headerData(int section, Qt::Orientation orientation, int role) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) 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; 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 clearItemData(const QModelIndex &index) override;
bool insertColumns(int column, int count, const QModelIndex &parent = {}) override; bool insertColumns(int column, int count, const QModelIndex &parent = {}) override;
bool removeColumns(int column, int count, const QModelIndex &parent = {}) override; bool removeColumns(int column, int count, const QModelIndex &parent = {}) override;
@ -170,6 +173,8 @@ public:
break; break;
case Data: makeCall(that, &Self::data, r, args); case Data: makeCall(that, &Self::data, r, args);
break; break;
case ItemData: makeCall(that, &Self::itemData, r, args);
break;
} }
} }
@ -180,6 +185,8 @@ public:
break; break;
case SetData: makeCall(that, &Self::setData, r, args); case SetData: makeCall(that, &Self::setData, r, args);
break; break;
case SetItemData: makeCall(that, &Self::setItemData, r, args);
break;
case ClearItemData: makeCall(that, &Self::clearItemData, r, args); case ClearItemData: makeCall(that, &Self::clearItemData, r, args);
break; break;
case InsertColumns: makeCall(that, &Self::insertColumns, r, args); case InsertColumns: makeCall(that, &Self::insertColumns, r, args);
@ -283,6 +290,50 @@ public:
return result; 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) bool setData(const QModelIndex &index, const QVariant &data, int role)
{ {
if (!index.isValid()) if (!index.isValid())
@ -342,6 +393,78 @@ public:
return success; 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) bool clearItemData(const QModelIndex &index)
{ {
if (!index.isValid()) if (!index.isValid())

View File

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

View File

@ -84,6 +84,10 @@ private slots:
void data(); void data();
void setData_data() { createTestData(); } void setData_data() { createTestData(); }
void setData(); void setData();
void itemData_data() { createTestData(); }
void itemData();
void setItemData_data() { createTestData(); }
void setItemData();
void clearItemData_data() { createTestData(); } void clearItemData_data() { createTestData(); }
void clearItemData(); void clearItemData();
void insertRows_data() { createTestData(); } void insertRows_data() { createTestData(); }
@ -183,6 +187,18 @@ private:
{{{Qt::DisplayRole, "DISPLAY2"}, {Qt::DecorationRole, "DECORATION2"}}}, {{{Qt::DisplayRole, "DISPLAY2"}, {Qt::DecorationRole, "DECORATION2"}}},
{{{Qt::DisplayRole, "DISPLAY3"}, {Qt::DecorationRole, "DECORATION3"}}}, {{{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; std::unique_ptr<Data> m_data;
@ -198,7 +214,8 @@ public:
RemoveColumns = 0x08, RemoveColumns = 0x08,
ChangeColumns = InsertColumns | RemoveColumns, ChangeColumns = InsertColumns | RemoveColumns,
SetData = 0x10, SetData = 0x10,
All = ChangeRows | ChangeColumns | SetData All = ChangeRows | ChangeColumns | SetData,
SetItemData = 0x20,
}; };
Q_DECLARE_FLAGS(ChangeActions, ChangeAction); Q_DECLARE_FLAGS(ChangeActions, ChangeAction);
}; };
@ -288,13 +305,21 @@ void tst_QGenericItemModel::createTestData()
<< 5 << ChangeActions(ChangeAction::ReadOnly); << 5 << ChangeActions(ChangeAction::ReadOnly);
ADD_COPY(listOfNamedRoles) ADD_COPY(listOfNamedRoles)
<< 1 << (ChangeAction::ChangeRows | ChangeAction::SetData); << 1 << (ChangeAction::ChangeRows | ChangeAction::SetData | ChangeAction::SetItemData);
ADD_POINTER(listOfNamedRoles) ADD_POINTER(listOfNamedRoles)
<< 1 << (ChangeAction::ChangeRows | ChangeAction::SetData); << 1 << (ChangeAction::ChangeRows | ChangeAction::SetData | ChangeAction::SetItemData);
ADD_COPY(tableOfEnumRoles) ADD_COPY(tableOfEnumRoles)
<< 1 << ChangeActions(ChangeAction::All); << 1 << ChangeActions(ChangeAction::All | ChangeAction::SetItemData);
ADD_POINTER(tableOfEnumRoles) 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_COPY
#undef ADD_POINTER #undef ADD_POINTER
@ -459,6 +484,70 @@ void tst_QGenericItemModel::setData()
QCOMPARE(first.data() == oldValue, !changeActions.testFlag(ChangeAction::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() void tst_QGenericItemModel::clearItemData()
{ {
QFETCH(Factory, factory); QFETCH(Factory, factory);
@ -495,6 +584,10 @@ void tst_QGenericItemModel::insertRows()
QEXPECT_FAIL("listOfNamedRolesCopy", "QVariantMap is empty by design", Continue); QEXPECT_FAIL("listOfNamedRolesCopy", "QVariantMap is empty by design", Continue);
QEXPECT_FAIL("tableOfEnumRolesPointer", "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("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 // get and put data into the new row
const QModelIndex firstItem = model->index(0, 0); const QModelIndex firstItem = model->index(0, 0);