QGIM: add MultiColumn wrapper for disambiguation

If a type has both a metaobject and implements the tuple protocol, then
we need to disambiguate as row_traits would be ambiguous.

Add  a wrapper that holds the type and provides a forwarding tuple
protocol implementation. With either or the SingleColumn wrapper, client
code can decide whether the type should be a a multi-column row in a
table (accessed via tuple protocol), or  a multi-role item in a list
(accessed via the meta object).

Add documentation.

Change-Id: I0145f7a35a125a138254397bc7eafa5506e5e3ad
Reviewed-by: Artem Dyomin <artem.dyomin@qt.io>
This commit is contained in:
Volker Hilsheimer 2025-03-22 18:14:37 +01:00
parent 5901f0ba09
commit 75db2305cb
4 changed files with 163 additions and 1 deletions

View File

@ -172,9 +172,79 @@ QT_BEGIN_NAMESPACE
\note The implementation of \c{get} above requires C++23. A C++17 compliant
implementation can be found in the unit test code for QGenericItemModel.
Types that have a meta objects, and implement the C++ tuple protocol, also
can cause compile-time ambiguity when used as the row type, as the framework
won't know which API to use to access the individual values. Use the
QGenericItemModel::SingleColumn and QGenericItemModel::MultiColumns wrapper
to disambiguate.
\sa {Model/View Programming}
*/
/*!
\typedef QGenericItemModel::SingleColumn
Use this type to disambiguate when using the type \c{T} as the row type in
the range. If \c{T} provides a metaobject, then the framework will by
default represent the type as multiple columns, resulting in a table model.
\snippet qgenericitemmodel/main.cpp color_gadget_0
When stored in a sequential range, this type will be interpreted as
multi-column rows with each property being one column. The range will be
represented as a table.
\code
QList<ColorEntry> colors = {
// ...
};
QGenericItemModel tableModel(colors); // columnCount() == 3
\endcode
When wrapped into QGenericItemModel::SingleColumn, the model will be a list,
with each instance of \c{T} represented as an item with multiple roles.
\code
QList<QGenericItemModel::SingleColumn<ColorEntry>> colors = {
// ...
};
QGenericItemModel listModel(colors); // columnCount() == 1
\endcode
\sa QGenericItemModel::MultiColumn
*/
/*!
\class QGenericItemModel::MultiColumn
\brief Represents the wrapped type \c{T} as multiple columns in a QGenericItemModel.
\inmodule QtCore
\ingroup model-view
\since 6.10
Use this type to disambiguate when the type \c{T} has both a metaobject, and
implements \l{the C++ tuple protocol}. The type will be represented as
multiple columns, and the individual values will be accessed through the
tuple protocol.
\snippet qgenericitemmodel/main.cpp color_gadget_0
\code
namespace std {
template <> struct tuple_size<ColorEntry> : integral_constant<size_t, 3> {};
// ...
}
QList<QGenericItemModel::MultiColumn<ColorEntry>> colors = {
// ...
};
QGenericItemModel colorList(colors);
\endcode
To represent the type a single column value with multiple roles, use
QGenericItemModel::SingleColumn instead.
\sa QGenericItemModel::SingleColumn
*/
/*!
\fn template <typename Range, QGenericItemModelDetails::if_is_range<Range>> QGenericItemModel::QGenericItemModel(Range &&range, QObject *parent)

View File

@ -16,6 +16,32 @@ public:
template <typename T>
using SingleColumn = std::tuple<T>;
template <typename T>
struct MultiColumn
{
using type = std::remove_pointer_t<T>;
T data{};
template <typename X>
using if_get_matches = std::enable_if_t<std::is_same_v<q20::remove_cvref_t<X>,
MultiColumn<T>>, bool>;
template <typename V = T,
std::enable_if_t<std::is_constructible_v<bool, V>, bool> = true>
constexpr explicit operator bool() const noexcept { return bool(data); }
// unconstrained on size_t I, gcc internal error #3280
template <std::size_t I, typename V, if_get_matches<V> = true>
friend inline decltype(auto) get(V &&multiColumn)
{
static_assert(I < std::tuple_size_v<type>, "Index out of bounds for wrapped type");
Q_ASSERT(multiColumn);
if constexpr (std::is_pointer_v<T>)
return get<I>(*multiColumn.data);
else
return get<I>(q23::forward_like<V>(multiColumn.data));
}
};
template <typename Range,
QGenericItemModelDetails::if_is_range<Range> = true>
explicit QGenericItemModel(Range &&range, QObject *parent = nullptr);
@ -1057,4 +1083,15 @@ QGenericItemModel::QGenericItemModel(Range &&range, QObject *parent)
QT_END_NAMESPACE
namespace std {
template <typename T>
struct tuple_size<QT_PREPEND_NAMESPACE(QGenericItemModel)::MultiColumn<T>>
: tuple_size<typename QT_PREPEND_NAMESPACE(QGenericItemModel)::MultiColumn<T>::type>
{};
template <std::size_t I, typename T>
struct tuple_element<I, QT_PREPEND_NAMESPACE(QGenericItemModel)::MultiColumn<T>>
: tuple_element<I, typename QT_PREPEND_NAMESPACE(QGenericItemModel)::MultiColumn<T>::type>
{};
}
#endif // QGENERICITEMMODEL_H

View File

@ -198,7 +198,7 @@ namespace QGenericItemModelDetails
template <typename T> static auto pointerTo(const T &&t) = delete;
template <typename T>
static bool isValid(T &&t)
static constexpr bool isValid(T &&t) noexcept
{
if constexpr (std::is_constructible_v<bool, T>)
return bool(t);

View File

@ -65,6 +65,38 @@ private:
int m_number = 42;
};
// a class that can be both and requires disambiguation
class MetaObjectTuple : public QObject
{
Q_OBJECT
Q_PROPERTY(QString display MEMBER m_string)
Q_PROPERTY(int number MEMBER m_number)
public:
using QObject::QObject;
private:
QString m_string = "4321";
int m_number = 24;
template <size_t I, typename G,
std::enable_if_t<(I < 2), bool> = true,
std::enable_if_t<std::is_same_v<q20::remove_cvref_t<G>, MetaObjectTuple>, bool> = true
>
friend inline decltype(auto) get(G &&item)
{
if constexpr (I == 0)
return q23::forward_like<G>(item.m_string);
else if constexpr (I == 1)
return q23::forward_like<G>(item.m_number);
}
};
namespace std {
template <> struct tuple_size<MetaObjectTuple> : std::integral_constant<size_t, 2> {};
template <> struct tuple_element<0, MetaObjectTuple> { using type = QString; };
template <> struct tuple_element<1, MetaObjectTuple> { using type = int; };
}
struct Row
{
Item m_item;
@ -200,6 +232,23 @@ private:
&row1, &row2, &row3
};
MetaObjectTuple mot1;
MetaObjectTuple mot2;
MetaObjectTuple mot3;
std::vector<QGenericItemModel::SingleColumn<MetaObjectTuple *>> listOfMetaObjectTuple = {
&mot1,
&mot2,
&mot3,
};
MetaObjectTuple mot4;
MetaObjectTuple mot5;
MetaObjectTuple mot6;
std::vector<QGenericItemModel::MultiColumn<MetaObjectTuple *>> tableOfMetaObjectTuple = {
{&mot4},
{&mot5},
{&mot6},
};
// bad (but legal) get() overload that never returns a mutable reference
std::vector<ConstRow> vectorOfConstStructs = {
{"one"},
@ -364,6 +413,10 @@ void tst_QGenericItemModel::createTestData()
<< 1 << (ChangeAction::ChangeRows | ChangeAction::SetData | ChangeAction::SetItemData);
ADD_COPY(listOfObjects)
<< 2 << (ChangeAction::ChangeRows | ChangeAction::SetData);
ADD_COPY(listOfMetaObjectTuple)
<< 1 << (ChangeAction::ChangeRows | ChangeAction::SetData | ChangeAction::SetItemData);
ADD_COPY(tableOfMetaObjectTuple)
<< 2 << (ChangeAction::ChangeRows | ChangeAction::SetData);
ADD_COPY(tableOfNumbers)
<< 5 << ChangeActions(ChangeAction::All);
@ -694,6 +747,8 @@ void tst_QGenericItemModel::insertRows()
QEXPECT_FAIL("tableOfPointersPointer", "No item created", Continue);
QEXPECT_FAIL("tableOfRowPointersPointer", "No row created", Continue);
QEXPECT_FAIL("listOfObjectsCopy", "No object created", Continue);
QEXPECT_FAIL("listOfMetaObjectTupleCopy", "No object created", Continue);
QEXPECT_FAIL("tableOfMetaObjectTupleCopy", "No object created", Continue);
// associative containers are default constructed with no valid data
ignoreFailureFromAssociativeContainers();