QRangeModel: implement roleNames for ranges of gadgets

If the item type for all columns is identical, and has a meta object,
then we can use the property names and map them to Qt::UserRole values.

Add the necessary traits to detect whether the types of all columns are
identical, and what the type is. They are identical for rows that are
ranges or single values (including MultiRole gadgets). For tuples, we
test if all elements have the same type.

Adapt the test, and improve the diagnostic output for failing calls to
setItemData.

Discussed during API review.

Pick-to: 6.10
Change-Id: Idd806e85f9eeec636aedf381ca69611afd9dbb29
Reviewed-by: Artem Dyomin <artem.dyomin@qt.io>
This commit is contained in:
Volker Hilsheimer 2025-07-11 17:25:25 +02:00
parent c35676b9d2
commit f101e9931b
3 changed files with 106 additions and 3 deletions

View File

@ -1023,6 +1023,15 @@ void QRangeModel::multiData(const QModelIndex &index, QModelRoleDataSpan roleDat
\property QRangeModel::roleNames
\brief the role names for the model.
If all columns in the range are of the same type, and if that type provides
a meta object (i.e., it is a gadget, or a QObject subclass), then this
property holds the names of the properties of that type, mapped to values of
Qt::ItemDataRole values from Qt::UserRole and up.
Override this default behavior by setting this property explicitly to a non-
empty mapping. Setting this property to an empty mapping, or using
resetRoleNames(), restores the default behavior.
\sa QAbstractItemModel::roleNames()
*/
@ -1036,7 +1045,8 @@ QHash<int, QByteArray> QRangeModel::roleNames() const
{
Q_D(const QRangeModel);
if (d->m_roleNames.isEmpty())
d->m_roleNames = QAbstractItemModel::roleNames();
d->m_roleNames = d->call<QHash<int, QByteArray>>(QRangeModelImplBase::RoleNames);
return d->m_roleNames;
}

View File

@ -330,6 +330,7 @@ namespace QRangeModelDetails
// A static size of 0 indicates that the specified type doesn't
// represent static or dynamic range.
static constexpr int static_size = is_range ? -1 : 0;
using item_type = std::conditional_t<is_range, typename range_traits<T>::value_type, void>;
static constexpr int fixed_size() { return 1; }
static constexpr bool hasMetaObject = false;
};
@ -340,6 +341,17 @@ namespace QRangeModelDetails
static constexpr std::size_t size64 = std::tuple_size_v<T>;
static_assert(q20::in_range<int>(size64));
static constexpr int static_size = int(size64);
// are the types in a tuple all the same
template <std::size_t ...I>
static constexpr bool allSameTypes(std::index_sequence<I...>)
{
return (std::is_same_v<std::tuple_element_t<0, T>,
std::tuple_element_t<I, T>> && ...);
}
using item_type = std::conditional_t<allSameTypes(std::make_index_sequence<size64>{}),
std::tuple_element_t<0, T>, void>;
static constexpr int fixed_size() { return 0; }
static constexpr bool hasMetaObject = false;
};
@ -354,6 +366,7 @@ namespace QRangeModelDetails
{
static_assert(q20::in_range<int>(N));
static constexpr int static_size = int(N);
using item_type = T;
static constexpr int fixed_size() { return 0; }
static constexpr bool hasMetaObject = false;
};
@ -362,6 +375,7 @@ namespace QRangeModelDetails
struct metaobject_row_traits
{
static constexpr int static_size = 0;
using item_type = std::conditional_t<row_category<T>::isMultiRole, T, void>;
static int fixed_size() {
if constexpr (row_category<T>::isMultiRole) {
return 1;
@ -629,6 +643,7 @@ public:
HeaderData,
Data,
ItemData,
RoleNames,
};
enum Op {
@ -832,6 +847,8 @@ public:
break;
case ItemData: makeCall(that, &Self::itemData, r, args);
break;
case RoleNames: makeCall(that, &Self::roleNames, r, args);
break;
}
}
@ -1204,7 +1221,8 @@ public:
}
}
if (!written) {
qWarning("Failed to write value for %s", roleName.data());
qWarning("Failed to write value '%s' to role '%s'",
qPrintable(QDebug::toString(value)), roleName.data());
return false;
}
}
@ -1266,6 +1284,29 @@ public:
return success;
}
QHash<int, QByteArray> roleNames() const
{
// will be 'void' if columns don't all have the same type
using item_type = typename row_traits::item_type;
if constexpr (QRangeModelDetails::has_metaobject_v<item_type>) {
const QMetaObject &metaObject = QRangeModelDetails::wrapped_t<item_type>::staticMetaObject;
const auto defaults = itemModel().QAbstractItemModel::roleNames();
QHash<int, QByteArray> result;
const int offset = metaObject.propertyOffset();
for (int i = offset; i < metaObject.propertyCount(); ++i) {
const auto name = metaObject.property(i).name();
const int defaultRole = defaults.key(name, -1);
if (defaultRole != -1)
result[defaultRole] = name;
else
result[Qt::UserRole + i - offset] = name;
}
return result;
}
return itemModel().QAbstractItemModel::roleNames();
}
bool insertColumns(int column, int count, const QModelIndex &parent)
{
if constexpr (dynamicColumns() && isMutable() && row_features::has_insert) {

View File

@ -306,6 +306,7 @@ private slots:
void ownership();
void overrideRoleNames();
void setRoleNames();
void defaultRoleNames();
void dimensions_data() { createTestData(); }
void dimensions();
@ -1082,6 +1083,57 @@ void tst_QRangeModel::setRoleNames()
QCOMPARE(model.roleNames(), roleNames);
}
void tst_QRangeModel::defaultRoleNames()
{
[]{
const QHash<int, QByteArray> expectedRoleNames = {
{Qt::UserRole, "string"},
{Qt::UserRole + 1, "number"},
};
QCOMPARE(QRangeModel(QList<QRangeModel::SingleColumn<Object *>>{}).roleNames(),
expectedRoleNames);
QCOMPARE(QRangeModel(QList<std::tuple<Object *, Object *>>{}).roleNames(),
expectedRoleNames);
}();
[]{
const QHash<int, QByteArray> expectedRoleNames = {
{Qt::DisplayRole, "display"},
{Qt::DecorationRole, "decoration"},
{Qt::ToolTipRole, "toolTip"},
};
QCOMPARE(QRangeModel(QList<QRangeModel::SingleColumn<Item>>{}).roleNames(),
expectedRoleNames);
QCOMPARE(QRangeModel(QList<std::tuple<Item, Item, Item>>{}).roleNames(),
expectedRoleNames);
QCOMPARE(QRangeModel(QList<QList<Item>>{}).roleNames(),
expectedRoleNames);
}();
[]{
const QHash<int, QByteArray> expectedRoleNames = {
{Qt::DisplayRole, "display"},
{Qt::DecorationRole, "decoration"},
};
QCOMPARE(QRangeModel(QList<MultiRoleGadget>{}).roleNames(),
expectedRoleNames);
QCOMPARE(QRangeModel(QList<QList<MultiRoleGadget>>{}).roleNames(),
expectedRoleNames);
QCOMPARE(QRangeModel(std::vector<std::array<MultiRoleGadget, 5>>{}).roleNames(),
expectedRoleNames);
}();
[]{
const auto expectedRoleNames = QRangeModel(QList<Row>{}).QAbstractItemModel::roleNames();
QCOMPARE(QRangeModel(QList<Row>{}).roleNames(), expectedRoleNames);
QCOMPARE(QRangeModel(QList<std::tuple<Item, MultiRoleGadget>>{}).roleNames(),
expectedRoleNames);
QCOMPARE(QRangeModel(QList<int>{}).roleNames(), expectedRoleNames);
QCOMPARE(QRangeModel(QList<QList<QString>>{}).roleNames(), expectedRoleNames);
}();
}
void tst_QRangeModel::dimensions()
{
QFETCH(Factory, factory);
@ -1208,7 +1260,7 @@ void tst_QRangeModel::setItemData()
for (int role : roles) {
if (role == Qt::EditRole) // faked
continue;
QVariant data = role != Qt::DecorationRole ? QVariant(QStringLiteral("Role %1").arg(role))
QVariant data = role != Qt::DecorationRole ? QVariant(QStringLiteral("%1").arg(role))
: QVariant(QColor(Qt::magenta));
itemData.insert(role, data);
}