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] //! [tuple_protocol]
#endif // __cpp_concepts && forward_like #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 \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 The type of the items that the implementations of data(), setData(),
clearItemData() operate on can be the same across the entire model - like clearItemData() etc. operate on can be the same across the entire model -
in the gridOfNumbers example above. But the range can also have different like in the \c{gridOfNumbers} example above. But the range can also have
item types for different columns, like in the \c{numberNames} case. 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 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 roles. Most views expect the value to be
to and from a QString} (but a custom delegate might provide more flexibility). \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 \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 Returns the data stored under the given \a role for the value in the
range referred to by the \a index. range referred to by the \a index.
The implementation returns a QVariant constructed from the item at the If the item type for that index is an associative container that maps from
\a index via \c{QVariant::fromValue()} for \c{Qt::DisplayRole} or either \c{int}, Qt::ItemDataRole, or QString to a QVariant, then the role
\c{Qt::EditRole}. For other roles, the implementation returns an \b invalid 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. (default-constructed) QVariant.
\sa Qt::ItemDataRole, setData(), headerData() \sa Qt::ItemDataRole, setData(), headerData()
@ -275,10 +293,16 @@ QVariant QGenericItemModel::data(const QModelIndex &index, int role) const
/*! /*!
\reimp \reimp
Sets the \a role data for the item at \a index to \a data. This Sets the \a role data for the item at \a index to \a data.
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 If the item type for that \a index is an associative container that maps
\c{true}. For other roles, the implementation returns \c{false}. 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] //! [read-only-setData]
For models operating on a read-only range, or on a read-only column in 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 data(const QModelIndex &index, int role) const
{ {
QVariant result; QVariant result;
const auto readData = [&result, role](const auto &value) { const auto readData = [this, &result, role](const auto &value) {
if (role == Qt::DisplayRole || role == Qt::EditRole) 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); result = read(value);
}
}; };
if (index.isValid()) { if (index.isValid()) {
@ -282,9 +297,29 @@ public:
} }
}); });
const auto writeData = [&data, role](auto &&target) -> bool { const auto writeData = [this, &data, role](auto &&target) -> bool {
if (role == Qt::DisplayRole || role == Qt::EditRole) 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 write(target, data);
}
return false; return false;
}; };

View File

@ -68,6 +68,28 @@ namespace QGenericItemModelDetails
: std::true_type : 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> template <typename C, typename = void>
struct test_size : std::false_type {}; struct test_size : std::false_type {};
template <typename C> template <typename C>
@ -82,7 +104,8 @@ namespace QGenericItemModelDetails
}; };
template <typename C> template <typename C>
struct range_traits<C, std::void_t<decltype(std::cbegin(std::declval<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 >> : std::true_type
{ {
using value_type = std::remove_reference_t<decltype(*std::begin(std::declval<C&>()))>; using value_type = std::remove_reference_t<decltype(*std::begin(std::declval<C&>()))>;
@ -182,7 +205,7 @@ namespace QGenericItemModelDetails
ModelStorage m_model; ModelStorage m_model;
}; };
} // QGenericItemModelDetails }
class QGenericItemModel; class QGenericItemModel;

View File

@ -169,6 +169,20 @@ private:
{16.0, 17.0, 18.0, 19.0, 20.0}, {16.0, 17.0, 18.0, 19.0, 20.0},
{21.0, 22.0, 23.0, 24.0, 25.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; std::unique_ptr<Data> m_data;
@ -273,6 +287,15 @@ void tst_QGenericItemModel::createTestData()
ADD_POINTER(constTableOfNumbers) ADD_POINTER(constTableOfNumbers)
<< 5 << ChangeActions(ChangeAction::ReadOnly); << 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_COPY
#undef ADD_POINTER #undef ADD_POINTER
#undef ADD_HELPER #undef ADD_HELPER
@ -467,6 +490,12 @@ void tst_QGenericItemModel::insertRows()
QCOMPARE(model->rowCount() == expectedRowCount + 1, QCOMPARE(model->rowCount() == expectedRowCount + 1,
changeActions.testFlag(ChangeAction::InsertRows)); 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 // get and put data into the new row
const QModelIndex firstItem = model->index(0, 0); const QModelIndex firstItem = model->index(0, 0);
const QModelIndex lastItem = model->index(0, expectedColumnCount - 1); 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("tableOfPointersPointer", "No item created", Continue);
QEXPECT_FAIL("tableOfRowPointersPointer", "No row created", Continue); QEXPECT_FAIL("tableOfRowPointersPointer", "No row created", Continue);
// associative containers are default constructed with no valid data
ignoreFailureFromAssociativeContainers();
QVERIFY(firstValue.isValid() && lastValue.isValid()); QVERIFY(firstValue.isValid() && lastValue.isValid());
ignoreFailureFromAssociativeContainers();
QCOMPARE(model->setData(firstItem, lastValue), canSetData && lastValue.isValid()); QCOMPARE(model->setData(firstItem, lastValue), canSetData && lastValue.isValid());
ignoreFailureFromAssociativeContainers();
QCOMPARE(model->setData(lastItem, firstValue), canSetData && firstValue.isValid()); QCOMPARE(model->setData(lastItem, firstValue), canSetData && firstValue.isValid());
// append more rows // append more rows