QGIM: support associative containers for multi-role items

If the range used with QGenericItemModel contains associative containers
that map from Qt::ItemDataRole, int, or QString to QVariant, then we
interpret such values as multi-role items, and look up the respective
value for the role requested.

Add test coverage and documentation.

This change does not implement QAbstractItemModel::itemData/setItemData.

Change-Id: I00aa8da9369a20ea4ddb58bb24c45fef9465b33f
Reviewed-by: Artem Dyomin <artem.dyomin@qt.io>
This commit is contained in:
Volker Hilsheimer 2025-02-07 14:58:57 +01:00
parent 984074eea2
commit c4cef19d58
5 changed files with 156 additions and 20 deletions

View File

@ -106,3 +106,23 @@ namespace std {
}
//! [tuple_protocol]
#endif // __cpp_concepts && forward_like
void color_map()
{
//! [color_map]
using ColorEntry = QMap<Qt::ItemDataRole, QVariant>;
const QStringList colorNames = QColor::colorNames();
QList<ColorEntry> colors;
colors.reserve(colorNames.size());
for (const QString &name : colorNames) {
const QColor color = QColor::fromString(name);
colors << ColorEntry{{Qt::DisplayRole, name},
{Qt::DecorationRole, color},
{Qt::ToolTipRole, color.name()}};
}
QGenericItemModel colorModel(colors);
QListView list;
list.setModel(&colorModel);
//! [color_map]
}

View File

@ -91,16 +91,30 @@ QT_BEGIN_NAMESPACE
\snippet qgenericitemmodel/main.cpp pair_int_QString
\section2 Item Types
\section2 Multi-role items
The type of the items that the implementations of data(), setData(), and
clearItemData() operate on can be the same across the entire model - like
in the gridOfNumbers example above. But the range can also have different
item types for different columns, like in the \c{numberNames} case.
The type of the items that the implementations of data(), setData(),
clearItemData() etc. operate on can be the same across the entire model -
like in the \c{gridOfNumbers} example above. But the range can also have
different item types for different columns, like in the \c{numberNames}
case.
By default, the value gets used for the Qt::DisplayRole and Qt::EditRole
roles. Most views expect the value to be \l{QVariant::canConvert}{convertible
to and from a QString} (but a custom delegate might provide more flexibility).
roles. Most views expect the value to be
\l{QVariant::canConvert}{convertible to and from a QString} (but a custom
delegate might provide more flexibility).
If the item is an associative container that uses \c{int},
\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.
\snippet qgenericitemmodel/main.cpp color_map
The most efficient data type to use as the key is Qt::ItemDataRole or
\c{int}.
\section2 The C++ tuple protocol
@ -260,9 +274,13 @@ QVariant QGenericItemModel::headerData(int section, Qt::Orientation orientation,
Returns the data stored under the given \a role for the value in the
range referred to by the \a index.
The implementation returns a QVariant constructed from the item at the
\a index via \c{QVariant::fromValue()} for \c{Qt::DisplayRole} or
\c{Qt::EditRole}. For other roles, the implementation returns an \b invalid
If the item type for that index is an associative container that maps from
either \c{int}, Qt::ItemDataRole, or QString to a QVariant, then the role
data is looked up in that container and returned.
Otherwise, the implementation returns a QVariant constructed from the item
via \c{QVariant::fromValue()} for \c{Qt::DisplayRole} or \c{Qt::EditRole}.
For other roles, the implementation returns an \b invalid
(default-constructed) QVariant.
\sa Qt::ItemDataRole, setData(), headerData()
@ -275,10 +293,16 @@ QVariant QGenericItemModel::data(const QModelIndex &index, int role) const
/*!
\reimp
Sets the \a role data for the item at \a index to \a data. This
implementation assigns the value in \a data to the item at the \a index
in the range for \c{Qt::DisplayRole} and \c{Qt::EditRole}, and returns
\c{true}. For other roles, the implementation returns \c{false}.
Sets the \a role data for the item at \a index to \a data.
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
\a data is stored in that container for the key specified by \a role.
Otherwise, this implementation assigns the value in \a data to the item at
the \a index in the range for \c{Qt::DisplayRole} and \c{Qt::EditRole},
and returns \c{true}. For other roles, the implementation returns
\c{false}.
//! [read-only-setData]
For models operating on a read-only range, or on a read-only column in

View File

@ -251,9 +251,24 @@ public:
QVariant data(const QModelIndex &index, int role) const
{
QVariant result;
const auto readData = [&result, role](const auto &value) {
if (role == Qt::DisplayRole || role == Qt::EditRole)
const auto readData = [this, &result, role](const 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::value) {
const auto it = [this, &value, role]{
Q_UNUSED(this);
if constexpr (multi_role::int_key)
return std::as_const(value).find(Qt::ItemDataRole(role));
else
return std::as_const(value).find(roleNames().value(role));
}();
if (it != value.cend()) {
result = QGenericItemModelDetails::value(it);
}
} else if (role == Qt::DisplayRole || role == Qt::EditRole) {
result = read(value);
}
};
if (index.isValid()) {
@ -282,9 +297,29 @@ public:
}
});
const auto writeData = [&data, role](auto &&target) -> bool {
if (role == Qt::DisplayRole || role == Qt::EditRole)
const auto writeData = [this, &data, role](auto &&target) -> bool {
using value_type = q20::remove_cvref_t<decltype(target)>;
using multi_role = QGenericItemModelDetails::is_multi_role<value_type>;
if constexpr (multi_role::value) {
Qt::ItemDataRole roleToSet = Qt::ItemDataRole(role);
// If there is an entry for EditRole, overwrite that; otherwise,
// set the entry for DisplayRole.
if (role == Qt::EditRole) {
if constexpr (multi_role::int_key) {
if (target.find(roleToSet) == target.end())
roleToSet = Qt::DisplayRole;
} else {
if (target.find(roleNames().value(roleToSet)) == target.end())
roleToSet = Qt::DisplayRole;
}
}
if constexpr (multi_role::int_key)
return write(target[roleToSet], data);
else
return write(target[roleNames().value(roleToSet)], data);
} else if (role == Qt::DisplayRole || role == Qt::EditRole) {
return write(target, data);
}
return false;
};

View File

@ -68,6 +68,28 @@ namespace QGenericItemModelDetails
: std::true_type
{};
// Test if a type is an associative container that we can use for multi-role
// data, i.e. has a key_type and a mapped_type typedef, and maps from int,
// Qt::ItemDataRole, or QString to QVariant. This excludes std::set (and
// unordered_set), which are not useful for us anyway even though they are
// considered associative containers.
template <typename C, typename = void> struct is_multi_role : std::false_type
{
static constexpr bool int_key = false;
};
template <typename C> // Qt::ItemDataRole -> QVariant, or QString -> QVariant, int -> QVariant
struct is_multi_role<C, std::void_t<typename C::key_type, typename C::mapped_type>>
: std::conjunction<std::disjunction<std::is_same<typename C::key_type, int>,
std::is_same<typename C::key_type, Qt::ItemDataRole>,
std::is_same<typename C::key_type, QString>>,
std::is_same<typename C::mapped_type, QVariant>>
{
static constexpr bool int_key = !std::is_same_v<typename C::key_type, QString>;
};
template <typename C>
[[maybe_unused]]
static constexpr bool is_multi_role_v = is_multi_role<C>::value;
template <typename C, typename = void>
struct test_size : std::false_type {};
template <typename C>
@ -82,7 +104,8 @@ namespace QGenericItemModelDetails
};
template <typename C>
struct range_traits<C, std::void_t<decltype(std::cbegin(std::declval<C&>())),
decltype(std::cend(std::declval<C&>()))
decltype(std::cend(std::declval<C&>())),
std::enable_if_t<!is_multi_role_v<C>>
>> : std::true_type
{
using value_type = std::remove_reference_t<decltype(*std::begin(std::declval<C&>()))>;
@ -182,7 +205,7 @@ namespace QGenericItemModelDetails
ModelStorage m_model;
};
} // QGenericItemModelDetails
}
class QGenericItemModel;

View File

@ -169,6 +169,20 @@ private:
{16.0, 17.0, 18.0, 19.0, 20.0},
{21.0, 22.0, 23.0, 24.0, 25.0},
};
// values are associative containers
std::vector<QVariantMap> listOfNamedRoles = {
{{"display", "DISPLAY0"}, {"decoration", "DECORATION0"}},
{{"display", "DISPLAY1"}, {"decoration", "DECORATION1"}},
{{"display", "DISPLAY2"}, {"decoration", "DECORATION2"}},
{{"display", "DISPLAY3"}, {"decoration", "DECORATION3"}},
};
std::vector<std::vector<std::map<Qt::ItemDataRole, QVariant>>> tableOfEnumRoles = {
{{{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;
@ -273,6 +287,15 @@ void tst_QGenericItemModel::createTestData()
ADD_POINTER(constTableOfNumbers)
<< 5 << ChangeActions(ChangeAction::ReadOnly);
ADD_COPY(listOfNamedRoles)
<< 1 << (ChangeAction::ChangeRows | ChangeAction::SetData);
ADD_POINTER(listOfNamedRoles)
<< 1 << (ChangeAction::ChangeRows | ChangeAction::SetData);
ADD_COPY(tableOfEnumRoles)
<< 1 << ChangeActions(ChangeAction::All);
ADD_POINTER(tableOfEnumRoles)
<< 1 << ChangeActions(ChangeAction::All);
#undef ADD_COPY
#undef ADD_POINTER
#undef ADD_HELPER
@ -467,6 +490,12 @@ void tst_QGenericItemModel::insertRows()
QCOMPARE(model->rowCount() == expectedRowCount + 1,
changeActions.testFlag(ChangeAction::InsertRows));
auto ignoreFailureFromAssociativeContainers = []{
QEXPECT_FAIL("listOfNamedRolesPointer", "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("tableOfEnumRolesCopy", "QVariantMap is empty by design", Continue);
};
// get and put data into the new row
const QModelIndex firstItem = model->index(0, 0);
const QModelIndex lastItem = model->index(0, expectedColumnCount - 1);
@ -477,8 +506,13 @@ void tst_QGenericItemModel::insertRows()
QEXPECT_FAIL("tableOfPointersPointer", "No item created", Continue);
QEXPECT_FAIL("tableOfRowPointersPointer", "No row created", Continue);
// associative containers are default constructed with no valid data
ignoreFailureFromAssociativeContainers();
QVERIFY(firstValue.isValid() && lastValue.isValid());
ignoreFailureFromAssociativeContainers();
QCOMPARE(model->setData(firstItem, lastValue), canSetData && lastValue.isValid());
ignoreFailureFromAssociativeContainers();
QCOMPARE(model->setData(lastItem, firstValue), canSetData && firstValue.isValid());
// append more rows