Long live QGenericItemModel for lists and tables

QGenericItemModel provides a QAbstractItemModel implementation that can
make any C++ range with a cbegin/cend iterator pair available to Qt's
model/view framework.

To avoid subclassing a polymorphic type with a template, resulting
in weak vtables, the calls are dispatched through a set of static
helpers similar to existing pattern in e.g. QObject or QRunnable.

The public QGenericItemModel class is then a subclass of
QAbstractItemModel, where only the constructor is a template that
instantiates the specialization of QGenericItemModelImpl. The virtual
function overrides dispatch the call through the static helper to the
template class operating on the range.

The core of the implementation is the template class
QGenericItemModelImpl, that implements the QAbstractItemModel
functionality on top of iterator APIs. Implementations that are
specific to the structure of the model, i.e. whether it is a flat table
or list model, or (in a later commit) a tree, are in a base class of
QGenericItemModelImpl, QGenericItemModelStructureImpl. It uses CRTP
with decltype(auto) where we would otherwise have a return type that
is dependent on the (not yet fully specialized) implementation type.

The data for the model can be provided by copy, rvalue reference, or
pointer. If provided by pointer, the model will modify the original
data. Functionality that requires that the model itself, or the data
in the model, can be modified is compiled out if the model or data
is const.

The initial implementation supports lists and tables, where tables can
be nested containers (like `std::vector<std::vector<item_type>>`), or
as structs that implement the C++ tuple protocol.

Include test framework and documentation, as well as a snippet-project
that tests the code snippets.

[ChangeLog][QtCore] Added QGenericItemModel, a QAbstractItemModel
implementation that make any C++ range with a cbegin/cend iterator pair
available to Qt's model/view framework.

Change-Id: I8ff2b95cde2566f422f3aee7912660a8cb217397
Reviewed-by: Artem Dyomin <artem.dyomin@qt.io>
This commit is contained in:
Volker Hilsheimer 2025-02-05 19:59:02 +01:00
parent 17a9343ace
commit f5115a9137
9 changed files with 2108 additions and 0 deletions

View File

@ -1156,6 +1156,7 @@ qt_internal_extend_target(Core CONDITION QT_FEATURE_itemmodel
SOURCES SOURCES
itemmodels/qabstractitemmodel.cpp itemmodels/qabstractitemmodel.h itemmodels/qabstractitemmodel_p.h itemmodels/qabstractitemmodel.cpp itemmodels/qabstractitemmodel.h itemmodels/qabstractitemmodel_p.h
itemmodels/qitemselectionmodel.cpp itemmodels/qitemselectionmodel.h itemmodels/qitemselectionmodel_p.h itemmodels/qitemselectionmodel.cpp itemmodels/qitemselectionmodel.h itemmodels/qitemselectionmodel_p.h
itemmodels/qgenericitemmodel.h itemmodels/qgenericitemmodel_impl.h itemmodels/qgenericitemmodel.cpp
) )
qt_internal_extend_target(Core CONDITION QT_FEATURE_proxymodel qt_internal_extend_target(Core CONDITION QT_FEATURE_proxymodel
@ -1620,3 +1621,7 @@ function(qt_internal_library_deprecation_level)
set_source_files_properties("${output_header}" PROPERTIES GENERATED TRUE) set_source_files_properties("${output_header}" PROPERTIES GENERATED TRUE)
endfunction() endfunction()
qt_internal_library_deprecation_level() qt_internal_library_deprecation_level()
if(QT_FEATURE_doc_snippets)
add_subdirectory(doc/snippets)
endif()

View File

@ -0,0 +1,18 @@
# Copyright (C) 2025 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
add_library(corelib_snippets OBJECT
qgenericitemmodel/main.cpp
)
target_link_libraries(corelib_snippets PRIVATE
Qt::Core
Qt::Gui
Qt::Widgets
)
if ("${CMAKE_CXX_COMPILE_FEATURES}" MATCHES "cxx_std_23")
set_property(TARGET corelib_snippets PROPERTY CXX_STANDARD 23)
endif()
set_target_properties(corelib_snippets PROPERTIES UNITY_BUILD OFF)

View File

@ -0,0 +1,108 @@
// Copyright (C) 2025 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
#include <QtCore/qgenericitemmodel.h>
#include <QtWidgets/qlistview.h>
#include <QtWidgets/qtableview.h>
#include <QtWidgets/qtreeview.h>
#include <array>
#include <vector>
void array()
{
QListView listView;
//! [array]
std::array<int, 5> numbers = {1, 2, 3, 4, 5};
QGenericItemModel model(numbers);
listView.setModel(&model);
//! [array]
}
void const_array()
{
//! [const_array]
const std::array<int, 5> numbers = {1, 2, 3, 4, 5};
//! [const_array]
QGenericItemModel model(numbers);
}
void const_values()
{
//! [const_values]
std::array<const int, 5> numbers = {1, 2, 3, 4, 5};
//! [const_values]
QGenericItemModel model(numbers);
}
void list_of_int()
{
//! [list_of_int]
QList<int> numbers = {1, 2, 3, 4, 5};
QGenericItemModel model(numbers); // columnCount() == 1
QListView listView;
listView.setModel(&model);
//! [list_of_int]
}
void grid_of_numbers()
{
//! [grid_of_numbers]
std::vector<std::vector<int>> gridOfNumbers = {
{1, 2, 3, 4, 5},
{6, 7, 8, 9, 10},
{11, 12, 13, 14, 15},
};
QGenericItemModel model(&gridOfNumbers); // columnCount() == 5
QTableView tableView;
tableView.setModel(&model);
//! [grid_of_numbers]
}
void pair_int_QString()
{
//! [pair_int_QString]
using TableRow = std::tuple<int, QString>;
QList<TableRow> numberNames = {
{1, "one"},
{2, "two"},
{3, "three"}
};
QGenericItemModel model(&numberNames); // columnCount() == 2
QTableView tableView;
tableView.setModel(&model);
//! [pair_int_QString]
}
#if defined(__cpp_concepts) && defined(__cpp_lib_forward_like)
//! [tuple_protocol]
struct Book
{
QString title;
QString author;
QString summary;
int rating = 0;
template <size_t I, typename T>
requires ((I <= 3) && std::is_same_v<std::remove_cvref_t<T>, Book>)
friend inline decltype(auto) get(T &&book)
{
if constexpr (I == 0)
return std::as_const(book.title);
else if constexpr (I == 1)
return std::as_const(book.author);
else if constexpr (I == 2)
return std::forward_like<T>(book.summary);
else if constexpr (I == 3)
return std::forward_like<T>(book.rating);
}
};
namespace std {
template <> struct tuple_size<Book> : std::integral_constant<size_t, 4> {};
template <size_t I> struct tuple_element<I, Book>
{ using type = decltype(get<I>(std::declval<Book>())); };
}
//! [tuple_protocol]
#endif // __cpp_concepts && forward_like

View File

@ -0,0 +1,387 @@
// Copyright (C) 2025 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
#include "qgenericitemmodel.h"
QT_BEGIN_NAMESPACE
/*!
\class QGenericItemModel
\inmodule QtCore
\since 6.10
\ingroup model-view
\brief QGenericItemModel implements QAbstractItemModel for any C++ range.
\reentrant
QGenericItemModel can make the data in any sequentially iterable C++ type
available to the \l{Model/View Programming}{model/view framework} of Qt.
This makes it easy to display existing data structures in the Qt Widgets
and Qt Quick item views, and to allow the user of the application to
manipulate the data using a graphical user interface.
To use QGenericItemModel, instantiate it with a C++ range and set it as
the model of one or more views:
\snippet qgenericitemmodel/main.cpp array
The range can be any C++ type for which the standard methods
\c{std::cbegin} and \c{std::cend} are implemented, and for which the
returned iterator type satisfies \c{std::forward_iterator}. Certain model
operations will perform better if \c{std::size} is available, and if the
iterator satisfies \c{std::random_access_iterator}.
The range can be provided by pointer or by value, and has to be provided
when constructing the model. If the range is provided by pointer, then
QAbstractItemModel APIs that modify the model, such as setData() or
insertRows(), modify the range. The caller must make sure that the
range's lifetime exceeds the lifetime of the model. Methods that modify
the structure of the range, such as insertRows() or removeColumns(), use
standard C++ container APIs \c{resize()}, \c{insert()}, \c{erase()}, in
addition to dereferencing a mutating iterator to set or clear the data.
There is no API to retrieve the range again, so constructing the model
from a range by value is mostly only useful for displaying data.
Changes to the data can be monitored using the signals emitted by the
model, such as \l{QAbstractItemModel}{dataChanged()}.
\section2 Read-only or mutable
For ranges that are const objects, for which access always yields
constant values, or where the required container APIs are not available,
QGenericItemModel implements write-access APIs to do nothing and return
\c{false}. In the example above, the model cannot add or remove rows, as
the number of entries in a C++ array is fixed. But the values can be
changed using setData(), and the user can trigger editing of the values in
the list view. By making the array const, the values also become read-only.
\snippet qgenericitemmodel/main.cpp const_array
The values are also read-only if the element type is const, like in
\snippet qgenericitemmodel/main.cpp const_values
\note If the values in the range are const, then it's also not possible
to remove or insert columns and rows through the QAbstractItemModel API.
For more granular control, implement \l{the C++ tuple protocol}.
\section1 List or Table
The elements in the range are interpreted as rows of the model. Depending
on the type of these rows, QGenericItemModel exposes the range as a list or
a table.
If the row type is not an iterable range, and does not implement the
C++ tuple protocol, then the range gets represented as a list.
\snippet qgenericitemmodel/main.cpp list_of_int
If the row type is an iterable range, then the range gets represented as a
table.
\snippet qgenericitemmodel/main.cpp grid_of_numbers
With such a row type, the number of columns can be changed via
insertColumns() and removeColumns(). However, all rows are expected to have
the same number of columns.
\section2 Fixed-size rows
If the row type implements \l{the C++ tuple protocol}, then the range gets
represented as a table with a fixed number of columns.
\snippet qgenericitemmodel/main.cpp pair_int_QString
\section2 Item Types
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.
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).
\section2 The C++ tuple protocol
As seen in the \c{numberNames} example above, the row type can be a tuple,
and in fact any type that implements the tuple protocol. This protocol is
implemented by specializing \c{std::tuple_size} and \c{std::tuple_element},
and overloading the unqualified \c{get} function. Do so for your custom row
type to make existing structured data available to the model/view framework
in Qt.
\snippet qgenericitemmodel/main.cpp tuple_protocol
In the above implementation, the \c{title} and \c{author} values of the
\c{Book} type are returned as \c{const}, so the model flags items in those
two columns as read-only. The user won't be able to trigger editing, and
setData() does nothing and returns false. For \c{summary} and \c{rating}
the implementation returns the same value category as the book, so when
\c{get} is called with a mutable reference to a \c{Book}, then it will
return a mutable reference of the respective variable. The model makes
those columns editable, both for the user and for programmatic access.
\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.
\sa {Model/View Programming}
*/
/*!
\fn template <typename Range, QGenericItemModelDetails::if_is_range<Range>> QGenericItemModel::QGenericItemModel(Range &&range, QObject *parent)
Constructs a generic item model instance that operates on the data in
\a range. The \a range has to be a sequential range for which
\c{std::cbegin} and \c{std::cend} are available. The model instance becomes
a child of \a parent.
The \a range can be a pointer, in which case mutating model APIs will
modify the data in that range instance. If \a range is a value (or moved
into the model), then use the signals emitted by the model to respond to
changes to the data.
\note While the model does not take ownership of the range object, you
must not modify the \a range directly once the model has been
constructed. Such modifications will not emit signals necessary to keep
model users (other models or views) synchronized with the model, resulting
in inconsistent results, undefined behavior, and crashes.
*/
/*!
Destroys the generic item model.
The range that the model was constructed from is not destroyed.
*/
QGenericItemModel::~QGenericItemModel() = default;
/*!
\reimp
Returns the index of the model item at \a row and \a column in \a parent.
Passing a valid parent produces an invalid index for models that operate on
list and table ranges.
\sa parent()
*/
QModelIndex QGenericItemModel::index(int row, int column, const QModelIndex &parent) const
{
return impl->callConst<QModelIndex>(QGenericItemModelImplBase::Index, row, column, parent);
}
/*!
\reimp
Returns the parent of the item at the \a child index.
This function always produces an invalid index for models that operate on
list and table ranges.
\sa index(), hasChildren()
*/
QModelIndex QGenericItemModel::parent(const QModelIndex &child) const
{
return impl->callConst<QModelIndex>(QGenericItemModelImplBase::Parent, child);
}
/*!
\reimp
Returns the number of rows under the given \a parent. This is the number of
items in the root range for an invalid \a parent index.
If the \a parent index is valid, then this function always returns 0 for
models that operate on list and table ranges.
\sa columnCount(), insertRows(), hasChildren()
*/
int QGenericItemModel::rowCount(const QModelIndex &parent) const
{
return impl->callConst<int>(QGenericItemModelImplBase::RowCount, parent);
}
/*!
\reimp
Returns the number of columns of the model. This function returns the same
value for all \a parent indexes.
For models operating on a statically sized row type, this returned value is
always the same throughout the lifetime of the model. For models operating
on dynamically sized row type, the model returns the number of items in the
first row, or 0 if the model has no rows.
\sa rowCount, insertColumns()
*/
int QGenericItemModel::columnCount(const QModelIndex &parent) const
{
return impl->callConst<int>(QGenericItemModelImplBase::ColumnCount, parent);
}
/*!
\reimp
Returns the item flags for the given \a index.
The implementation returns a combination of flags that enables the item
(\c ItemIsEnabled) and allows it to be selected (\c ItemIsSelectable). For
models operating on a range with mutable data, it also sets the flag
that allows the item to be editable (\c ItemIsEditable).
\sa Qt::ItemFlags
*/
Qt::ItemFlags QGenericItemModel::flags(const QModelIndex &index) const
{
return impl->callConst<Qt::ItemFlags>(QGenericItemModelImplBase::Flags, index);
}
/*!
\reimp
Returns the data for the given \a role and \a section in the header with
the specified \a orientation.
For horizontal headers, the section number corresponds to the column
number. Similarly, for vertical headers, the section number corresponds to
the row number.
\sa Qt::ItemDataRole, setHeaderData(), QHeaderView
*/
QVariant QGenericItemModel::headerData(int section, Qt::Orientation orientation, int role) const
{
return impl->callConst<QVariant>(QGenericItemModelImplBase::HeaderData,
section, orientation, role);
}
/*!
\reimp
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
(default-constructed) QVariant.
\sa Qt::ItemDataRole, setData(), headerData()
*/
QVariant QGenericItemModel::data(const QModelIndex &index, int role) const
{
return impl->callConst<QVariant>(QGenericItemModelImplBase::Data, index, role);
}
/*!
\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}.
//! [read-only-setData]
For models operating on a read-only range, or on a read-only column in
a row type that implements \l{the C++ tuple protocol}, this implementation
returns \c{false} immediately.
//! [read-only-setData]
*/
bool QGenericItemModel::setData(const QModelIndex &index, const QVariant &data, int role)
{
return impl->call<bool>(QGenericItemModelImplBase::SetData, index, data, role);
}
/*!
\reimp
Replaces the value stored in the range at \a index with a default-
constructed value.
\include qgenericitemmodel.cpp read-only-setData
*/
bool QGenericItemModel::clearItemData(const QModelIndex &index)
{
return impl->call<bool>(QGenericItemModelImplBase::ClearItemData, index);
}
/*
//! [column-change-requirement]
\note A dynamically sized row type needs to provide a \c{\1} member function.
For models operating on a read-only range, or on a range with a
statically sized row type (such as a tuple, array, or struct), this
implementation does nothing and returns \c{false} immediately.
//! [column-change-requirement]
*/
/*!
\reimp
Inserts \a count empty columns before the item at \a column in all rows
of the range at \a parent. Returns \c{true} if successful; otherwise
returns \c{false}.
\include qgenericitemmodel.cpp {column-change-requirement} {insert(const_iterator, size_t, value_type)}
*/
bool QGenericItemModel::insertColumns(int column, int count, const QModelIndex &parent)
{
return impl->call<bool>(QGenericItemModelImplBase::InsertColumns, column, count, parent);
}
/*!
\reimp
Removes \a count columns from the item at \a column on in all rows of the
range at \a parent. Returns \c{true} if successful, otherwise returns
\c{false}.
\include qgenericitemmodel.cpp {column-change-requirement} {erase(const_iterator, size_t)}
*/
bool QGenericItemModel::removeColumns(int column, int count, const QModelIndex &parent)
{
return impl->call<bool>(QGenericItemModelImplBase::RemoveColumns, column, count, parent);
}
/*
//! [row-change-requirement]
\note The range needs to be dynamically sized and provide a \c{\1}
member function.
For models operating on a read-only or statically-sized range (such as
an array), this implementation does nothing and returns \c{false}
immediately.
//! [row-change-requirement]
*/
/*!
\reimp
Inserts \a count empty rows before the given \a row into the range at
\a parent. Returns \c{true} if successful; otherwise returns \c{false}.
\include qgenericitemmodel.cpp {row-change-requirement} {insert(const_iterator, size_t, value_type)}
\note For ranges with a dynamically sized column type, the column needs
to provide a \c{resize(size_t)} member function.
*/
bool QGenericItemModel::insertRows(int row, int count, const QModelIndex &parent)
{
return impl->call<bool>(QGenericItemModelImplBase::InsertRows, row, count, parent);
}
/*!
\reimp
Removes \a count rows from the range at \a parent, starting with the
given \a row. Returns \c{true} if successful, otherwise returns \c{false}.
\include qgenericitemmodel.cpp {row-change-requirement} {erase(const_iterator, size_t)}
*/
bool QGenericItemModel::removeRows(int row, int count, const QModelIndex &parent)
{
return impl->call<bool>(QGenericItemModelImplBase::RemoveRows, row, count, parent);
}
QT_END_NAMESPACE
#include "moc_qgenericitemmodel.cpp"

View File

@ -0,0 +1,657 @@
// Copyright (C) 2025 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
#ifndef QGENERICITEMMODEL_H
#define QGENERICITEMMODEL_H
#include <QtCore/qgenericitemmodel_impl.h>
QT_BEGIN_NAMESPACE
class Q_CORE_EXPORT QGenericItemModel : public QAbstractItemModel
{
Q_OBJECT
public:
template <typename Range,
QGenericItemModelDetails::if_is_range<Range> = true>
explicit QGenericItemModel(Range &&range, QObject *parent = nullptr);
~QGenericItemModel() override;
QModelIndex index(int row, int column, const QModelIndex &parent = {}) const override;
QModelIndex parent(const QModelIndex &child) const override;
int rowCount(const QModelIndex &parent = {}) const override;
int columnCount(const QModelIndex &parent = {}) const override;
Qt::ItemFlags flags(const QModelIndex &index) const override;
QVariant headerData(int section, Qt::Orientation orientation, int role) 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 clearItemData(const QModelIndex &index) override;
bool insertColumns(int column, int count, const QModelIndex &parent = {}) override;
bool removeColumns(int column, int count, const QModelIndex &parent = {}) override;
bool insertRows(int row, int count, const QModelIndex &parent = {}) override;
bool removeRows(int row, int count, const QModelIndex &parent = {}) override;
private:
Q_DISABLE_COPY_MOVE(QGenericItemModel)
friend class QGenericItemModelImplBase;
struct Deleter { void operator()(QGenericItemModelImplBase *that) { that->destroy(); } };
std::unique_ptr<QGenericItemModelImplBase, Deleter> impl;
};
// implementation of forwarders
QModelIndex QGenericItemModelImplBase::createIndex(int row, int column, const void *ptr) const
{
return m_itemModel->createIndex(row, column, ptr);
}
QHash<int, QByteArray> QGenericItemModelImplBase::roleNames() const
{
return m_itemModel->roleNames();
}
void QGenericItemModelImplBase::dataChanged(const QModelIndex &from, const QModelIndex &to,
const QList<int> &roles)
{
m_itemModel->dataChanged(from, to, roles);
}
void QGenericItemModelImplBase::beginInsertColumns(const QModelIndex &parent, int start, int count)
{
m_itemModel->beginInsertColumns(parent, start, count);
}
void QGenericItemModelImplBase::endInsertColumns()
{
m_itemModel->endInsertColumns();
}
void QGenericItemModelImplBase::beginRemoveColumns(const QModelIndex &parent, int start, int count)
{
m_itemModel->beginRemoveColumns(parent, start, count);
}
void QGenericItemModelImplBase::endRemoveColumns()
{
m_itemModel->endRemoveColumns();
}
void QGenericItemModelImplBase::beginInsertRows(const QModelIndex &parent, int start, int count)
{
m_itemModel->beginInsertRows(parent, start, count);
}
void QGenericItemModelImplBase::endInsertRows()
{
m_itemModel->endInsertRows();
}
void QGenericItemModelImplBase::beginRemoveRows(const QModelIndex &parent, int start, int count)
{
m_itemModel->beginRemoveRows(parent, start, count);
}
void QGenericItemModelImplBase::endRemoveRows()
{
m_itemModel->endRemoveRows();
}
template <typename Structure, typename Range>
class QGenericItemModelImpl : public QGenericItemModelImplBase
{
Q_DISABLE_COPY_MOVE(QGenericItemModelImpl)
public:
using range_type = std::remove_pointer_t<std::remove_reference_t<Range>>;
using row_reference = decltype(*std::begin(std::declval<range_type&>()));
using const_row_reference = decltype(*std::cbegin(std::declval<range_type&>()));
using row_type = std::remove_reference_t<row_reference>;
protected:
using Self = QGenericItemModelImpl<Structure, Range>;
Structure& that() { return static_cast<Structure &>(*this); }
const Structure& that() const { return static_cast<const Structure &>(*this); }
template <typename C>
static constexpr auto size(const C &c)
{
if constexpr (QGenericItemModelDetails::test_size<C>())
return std::size(c);
else
#if defined(__cpp_lib_ranges)
return std::ranges::distance(std::begin(c), std::end(c));
#else
return std::distance(std::begin(c), std::end(c));
#endif
}
friend class tst_QGenericItemModel;
using range_features = QGenericItemModelDetails::range_traits<range_type>;
using row_features = QGenericItemModelDetails::range_traits<row_type>;
using row_traits = QGenericItemModelDetails::row_traits<q20::remove_cvref_t<
std::remove_pointer_t<row_type>>>;
static constexpr bool isMutable()
{
return range_features::is_mutable && row_features::is_mutable
&& std::is_reference_v<row_reference>;
}
static constexpr int static_row_count = QGenericItemModelDetails::static_size_v<range_type>;
static constexpr bool rows_are_pointers = std::is_pointer_v<row_type>;
static constexpr int static_column_count = QGenericItemModelDetails::static_size_v<row_type>;
static constexpr bool dynamicRows() { return isMutable() && static_row_count < 0; }
static constexpr bool dynamicColumns() { return static_column_count < 0; }
// A row might be a value (or range of values), or a pointer.
// row_ptr is always a pointer, and const_row_ptr is a pointer to const.
using row_ptr = std::conditional_t<rows_are_pointers, row_type, row_type *>;
using const_row_ptr = const std::remove_pointer_t<row_type> *;
using ModelData = QGenericItemModelDetails::ModelData<std::conditional_t<
std::is_pointer_v<Range>,
Range, std::remove_reference_t<Range>>
>;
public:
explicit QGenericItemModelImpl(Range &&model, QGenericItemModel *itemModel)
: QGenericItemModelImplBase(itemModel)
, m_data{std::forward<Range>(model)}
{
initFrom(this);
}
// static interface, called by QGenericItemModelImplBase
static void callConst(ConstOp op, const QGenericItemModelImplBase *that, void *r, const void *args)
{
switch (op) {
case Index: makeCall(that, &Self::index, r, args);
break;
case Parent: makeCall(that, &Structure::parent, r, args);
break;
case RowCount: makeCall(that, &Structure::rowCount, r, args);
break;
case ColumnCount: makeCall(that, &Structure::columnCount, r, args);
break;
case Flags: makeCall(that, &Self::flags, r, args);
break;
case HeaderData: makeCall(that, &Self::headerData, r, args);
break;
case Data: makeCall(that, &Self::data, r, args);
break;
}
}
static void call(Op op, QGenericItemModelImplBase *that, void *r, const void *args)
{
switch (op) {
case Destroy: delete static_cast<Structure *>(that);
break;
case SetData: makeCall(that, &Self::setData, r, args);
break;
case ClearItemData: makeCall(that, &Self::clearItemData, r, args);
break;
case InsertColumns: makeCall(that, &Self::insertColumns, r, args);
break;
case RemoveColumns: makeCall(that, &Self::removeColumns, r, args);
break;
case InsertRows: makeCall(that, &Self::insertRows, r, args);
break;
case RemoveRows: makeCall(that, &Self::removeRows, r, args);
break;
}
}
// actual implementations
QModelIndex index(int row, int column, const QModelIndex &parent) const
{
if (row < 0 || column < 0 || column >= that().columnCount(parent)
|| row >= that().rowCount(parent)) {
return {};
}
return that().indexImpl(row, column, parent);
}
Qt::ItemFlags flags(const QModelIndex &index) const
{
if (!index.isValid())
return Qt::NoItemFlags;
Qt::ItemFlags f = Structure::defaultFlags();
if constexpr (static_column_count <= 0) {
if constexpr (isMutable())
f |= Qt::ItemIsEditable;
} else if constexpr (std::is_reference_v<row_reference> && !std::is_const_v<row_reference>) {
// we want to know if the elements in the tuple are const; they'd always be, if
// we didn't remove the const of the range first.
const_row_reference row = rowData(index);
row_reference mutableRow = const_cast<row_reference>(row);
for_element_at(mutableRow, index.column(), [&f](auto &&ref){
using target_type = decltype(ref);
if constexpr (std::is_const_v<std::remove_reference_t<target_type>>)
f &= ~Qt::ItemIsEditable;
else if constexpr (std::is_lvalue_reference_v<target_type>)
f |= Qt::ItemIsEditable;
});
}
return f;
}
QVariant headerData(int section, Qt::Orientation orientation, int role) const
{
QVariant result;
if (role != Qt::DisplayRole || orientation != Qt::Horizontal
|| section < 0 || section >= that().columnCount({})) {
return m_itemModel->QAbstractItemModel::headerData(section, orientation, role);
}
if constexpr (static_column_count >= 1) {
const QMetaType metaType = meta_type_at<row_type>(section);
if (metaType.isValid())
result = QString::fromUtf8(metaType.name());
}
if (!result.isValid())
result = m_itemModel->QAbstractItemModel::headerData(section, orientation, role);
return result;
}
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)
result = read(value);
};
if (index.isValid()) {
const_row_reference row = rowData(index);
if constexpr (dynamicColumns())
readData(*std::next(std::cbegin(row), index.column()));
else if constexpr (static_column_count == 0)
readData(row);
else
for_element_at(row, index.column(), readData);
}
return result;
}
bool setData(const QModelIndex &index, const QVariant &data, int role)
{
if (!index.isValid())
return false;
bool success = false;
if constexpr (isMutable()) {
auto emitDataChanged = qScopeGuard([&success, this, &index, &role]{
if (success) {
Q_EMIT dataChanged(index, index, role == Qt::EditRole
? QList<int>{} : QList{role});
}
});
const auto writeData = [&data, role](auto &&target) -> bool {
if (role == Qt::DisplayRole || role == Qt::EditRole)
return write(target, data);
return false;
};
row_reference row = rowData(index);
if constexpr (dynamicColumns()) {
success = writeData(*std::next(std::begin(row), index.column()));
} else if constexpr (static_column_count == 0) {
success = writeData(row);
} else {
for_element_at(row, index.column(), [&writeData, &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 = writeData(std::forward<target_type>(target));
}
});
}
}
return success;
}
bool clearItemData(const QModelIndex &index)
{
if (!index.isValid())
return false;
bool success = false;
if constexpr (isMutable()) {
auto emitDataChanged = qScopeGuard([&success, this, &index]{
if (success)
Q_EMIT dataChanged(index, index, {});
});
row_reference row = rowData(index);
if constexpr (dynamicColumns()) {
*std::next(std::begin(row), index.column()) = {};
success = true;
} else if constexpr (static_column_count == 0) {
row = row_type{};
success = true;
} else {
for_element_at(row, index.column(), [&success](auto &&target){
using target_type = decltype(target);
if constexpr (std::is_lvalue_reference_v<target_type>
&& !std::is_const_v<std::remove_reference_t<target_type>>) {
target = {};
success = true;
}
});
}
}
return success;
}
bool insertColumns(int column, int count, const QModelIndex &parent)
{
if constexpr (dynamicColumns() && isMutable() && row_features::has_insert) {
if (count == 0)
return false;
range_type * const children = childRange(parent);
if (!children)
return false;
beginInsertColumns(parent, column, column + count - 1);
for (auto &child : *children)
child.insert(std::next(std::begin(child), column), count, {});
endInsertColumns();
return true;
}
return false;
}
bool removeColumns(int column, int count, const QModelIndex &parent)
{
if constexpr (dynamicColumns() && isMutable() && row_features::has_erase) {
if (column < 0 || column + count > that().columnCount(parent))
return false;
range_type * const children = childRange(parent);
if (!children)
return false;
beginRemoveColumns(parent, column, column + count - 1);
for (auto &child : *children) {
const auto start = std::next(std::begin(child), column);
child.erase(start, std::next(start, count));
}
endRemoveColumns();
return true;
}
return false;
}
bool insertRows(int row, int count, const QModelIndex &parent)
{
if constexpr (Structure::canInsertRows()) {
// If we operate on dynamic columns and cannot resize a newly
// constructed row, then we cannot insert.
if constexpr (dynamicColumns() && !row_features::has_resize)
return false;
range_type *children = childRange(parent);
if (!children)
return false;
beginInsertRows(parent, row, row + count - 1);
const auto pos = std::next(std::begin(*children), row);
if constexpr (rows_are_pointers) {
auto start = children->insert(pos, count, nullptr);
auto end = std::next(start, count);
for (auto it = start; it != end; ++it)
*it = that().makeEmptyRow(parent);
} else {
row_type empty_value = that().makeEmptyRow(parent);
children->insert(pos, count, empty_value);
}
endInsertRows();
return true;
} else {
return false;
}
}
bool removeRows(int row, int count, const QModelIndex &parent = {})
{
if constexpr (Structure::canRemoveRows()) {
const int prevRowCount = that().rowCount(parent);
if (row < 0 || row + count > prevRowCount)
return false;
range_type *children = childRange(parent);
if (!children)
return false;
beginRemoveRows(parent, row, row + count - 1);
[[maybe_unused]] bool callEndRemoveColumns = false;
if constexpr (dynamicColumns()) {
// if we remove the last row in a dynamic model, then we no longer
// know how many columns we should have, so they will be reported as 0.
if (prevRowCount == count) {
if (const int columns = that().columnCount(parent)) {
callEndRemoveColumns = true;
beginRemoveColumns(parent, 0, columns - 1);
}
}
}
{ // erase invalidates iterators
const auto start = std::next(std::begin(*children), row);
children->erase(start, std::next(start, count));
}
if constexpr (dynamicColumns()) {
if (callEndRemoveColumns) {
Q_ASSERT(that().columnCount(parent) == 0);
endRemoveColumns();
}
}
endRemoveRows();
return true;
} else {
return false;
}
}
protected:
template <typename Value>
static QVariant read(const Value &value)
{
if constexpr (std::is_constructible_v<QVariant, Value>)
return QVariant(value);
else
return QVariant::fromValue(value);
}
template <typename Value>
static QVariant read(Value *value)
{
if (value) {
if constexpr (std::is_constructible_v<QVariant, Value *>)
return QVariant(value);
else
return read(*value);
}
return {};
}
template <typename Target>
static bool write(Target &target, const QVariant &value)
{
using Type = std::remove_reference_t<Target>;
if constexpr (std::is_constructible_v<Target, QVariant>) {
target = value;
return true;
} else if (value.canConvert<Type>()) {
target = value.value<Type>();
return true;
}
return false;
}
template <typename Target>
static bool write(Target *target, const QVariant &value)
{
if (target)
return write(*target, value);
return false;
}
// helpers
const_row_reference rowData(const QModelIndex &index) const
{
Q_ASSERT(index.isValid());
return that().rowDataImpl(index);
}
row_reference rowData(const QModelIndex &index)
{
Q_ASSERT(index.isValid());
return that().rowDataImpl(index);
}
const range_type *childRange(const QModelIndex &index) const
{
if (!index.isValid())
return m_data.model();
if (index.column()) // only items at column 0 can have children
return nullptr;
return that().childRangeImpl(index);
}
range_type *childRange(const QModelIndex &index)
{
if (!index.isValid())
return m_data.model();
if (index.column()) // only items at column 0 can have children
return nullptr;
return that().childRangeImpl(index);
}
ModelData m_data;
};
template <typename Range, typename Protocol>
class QGenericItemModelStructureImpl : public QGenericItemModelImpl<
QGenericItemModelStructureImpl<Range, Protocol>,
Range>
{};
// specialization for flat models without protocol
template <typename Range>
class QGenericItemModelStructureImpl<Range, void> : public QGenericItemModelImpl<
QGenericItemModelStructureImpl<Range, void>,
Range>
{
using Base = QGenericItemModelImpl<QGenericItemModelStructureImpl<Range, void>, Range>;
friend class QGenericItemModelImpl<QGenericItemModelStructureImpl<Range, void>, Range>;
using range_features = typename Base::range_features;
using row_type = typename Base::row_type;
using row_traits = typename Base::row_traits;
using row_features = typename Base::row_features;
public:
explicit QGenericItemModelStructureImpl(Range &&model, QGenericItemModel *itemModel)
: Base(std::forward<Range>(model), itemModel)
{}
protected:
QModelIndex indexImpl(int row, int column, const QModelIndex &) const
{
if constexpr (Base::dynamicColumns()) {
if (column < int(Base::size(*std::next(std::cbegin(*this->m_data.model()), row))))
return this->createIndex(row, column);
// if we got here, then column < columnCount(), but this row is to short
qCritical("QGenericItemModel: Column-range at row %d is not large enough!", row);
return {};
} else {
return this->createIndex(row, column);
}
}
QModelIndex parent(const QModelIndex &) const
{
return {};
}
int rowCount(const QModelIndex &parent) const
{
if (parent.isValid())
return 0;
return int(Base::size(*this->m_data.model()));
}
int columnCount(const QModelIndex &parent) const
{
if (parent.isValid())
return 0;
// in a table, all rows have the same number of columns (as the first row)
if constexpr (Base::dynamicColumns()) {
return int(Base::size(*this->m_data.model()) == 0
? 0
: Base::size(*std::cbegin(*this->m_data.model())));
} else if constexpr (Base::static_column_count == 0) {
return row_traits::fixed_size();
} else {
return Base::static_column_count;
}
}
static constexpr Qt::ItemFlags defaultFlags()
{
return Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemNeverHasChildren;
}
static constexpr bool canInsertRows()
{
return Base::dynamicRows() && range_features::has_insert;
}
static constexpr bool canRemoveRows()
{
return Base::dynamicRows() && range_features::has_erase;
}
auto makeEmptyRow(const QModelIndex &)
{
if constexpr (Base::dynamicColumns()) {
// all rows have to have the same column count
row_type empty_row;
if constexpr (row_features::has_resize)
empty_row.resize(Base::m_itemModel->columnCount());
return empty_row;
} else {
return row_type{};
}
}
decltype(auto) rowDataImpl(const QModelIndex &index) const
{
Q_ASSERT(index.row() < int(Base::size(*this->m_data.model())));
return *(std::next(std::cbegin(*this->m_data.model()), index.row()));
}
decltype(auto) rowDataImpl(const QModelIndex &index)
{
Q_ASSERT(index.row() < int(Base::size(*this->m_data.model())));
return *(std::next(std::begin(*this->m_data.model()), index.row()));
}
auto childRangeImpl(const QModelIndex &) const
{
return nullptr;
}
auto childRangeImpl(const QModelIndex &)
{
return nullptr;
}
};
template <typename Range,
QGenericItemModelDetails::if_is_range<Range>>
QGenericItemModel::QGenericItemModel(Range &&range, QObject *parent)
: QAbstractItemModel(parent)
, impl(new QGenericItemModelStructureImpl<Range, void>(std::forward<Range>(range), this))
{}
QT_END_NAMESPACE
#endif // QGENERICITEMMODEL_H

View File

@ -0,0 +1,334 @@
// Copyright (C) 2025 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
#ifndef QGENERICITEMMODEL_IMPL_H
#define QGENERICITEMMODEL_IMPL_H
#ifndef Q_QDOC
#ifndef QGENERICITEMMODEL_H
#error Do not include qgenericitemmodel_impl.h directly
#endif
#if 0
#pragma qt_sync_skip_header_check
#pragma qt_sync_stop_processing
#endif
#include <QtCore/qabstractitemmodel.h>
#include <QtCore/qmetaobject.h>
#include <QtCore/qvariant.h>
#include <functional>
#include <type_traits>
#include <QtCore/q20type_traits.h>
#include <tuple>
#include <QtCore/q23utility.h>
QT_BEGIN_NAMESPACE
namespace QGenericItemModelDetails
{
// Test if a type is a range, and whether we can modify it using the
// standard C++ container member functions insert, erase, and resize.
// For the sake of QAIM, we cannot modify a range if it holds const data
// even if the range itself is not const; we'd need to initialize new rows
// and columns, and move old row and column data.
template <typename C, typename = void>
struct test_insert : std::false_type {};
template <typename C>
struct test_insert<C, std::void_t<decltype(std::declval<C>().insert(
std::declval<typename C::const_iterator>(),
std::declval<typename C::size_type>(),
std::declval<typename C::value_type>()
))>>
: std::true_type
{};
template <typename C, typename = void>
struct test_erase : std::false_type {};
template <typename C>
struct test_erase<C, std::void_t<decltype(std::declval<C>().erase(
std::declval<typename C::const_iterator>(),
std::declval<typename C::const_iterator>()
))>>
: std::true_type
{};
template <typename C, typename = void>
struct test_resize : std::false_type {};
template <typename C>
struct test_resize<C, std::void_t<decltype(std::declval<C>().resize(
std::declval<typename C::size_type>(),
std::declval<typename C::value_type>()
))>>
: std::true_type
{};
template <typename C, typename = void>
struct test_size : std::false_type {};
template <typename C>
struct test_size<C, std::void_t<decltype(std::size(std::declval<C&>()))>> : std::true_type {};
template <typename C, typename = void>
struct range_traits : std::false_type {
static constexpr bool is_mutable = !std::is_const_v<C>;
static constexpr bool has_insert = false;
static constexpr bool has_erase = false;
static constexpr bool has_resize = false;
};
template <typename C>
struct range_traits<C, std::void_t<decltype(std::cbegin(std::declval<C&>())),
decltype(std::cend(std::declval<C&>()))
>> : std::true_type
{
using value_type = std::remove_reference_t<decltype(*std::begin(std::declval<C&>()))>;
static constexpr bool is_mutable = !std::is_const_v<C> && !std::is_const_v<value_type>;
static constexpr bool has_insert = test_insert<C>();
static constexpr bool has_erase = test_erase<C>();
static constexpr bool has_resize = test_resize<C>();
};
// Specializations for types that look like ranges, but should be
// treated as values.
enum class Mutable { Yes, No };
template <Mutable IsMutable>
struct iterable_value : std::false_type {
static constexpr bool is_mutable = IsMutable == Mutable::Yes;
static constexpr bool has_insert = false;
static constexpr bool has_erase = false;
static constexpr bool has_resize = false;
};
template <> struct range_traits<QByteArray> : iterable_value<Mutable::Yes> {};
template <> struct range_traits<QString> : iterable_value<Mutable::Yes> {};
template <class CharT, class Traits, class Allocator>
struct range_traits<std::basic_string<CharT, Traits, Allocator>> : iterable_value<Mutable::Yes>
{};
// const T * and views are read-only
template <typename T> struct range_traits<const T *> : iterable_value<Mutable::No> {};
template <> struct range_traits<QLatin1StringView> : iterable_value<Mutable::No> {};
template <typename C>
[[maybe_unused]] static constexpr bool is_range_v = range_traits<C>();
template <typename CC>
using if_is_range = std::enable_if_t<
is_range_v<std::remove_pointer_t<std::remove_reference_t<CC>>>, bool>;
// Find out how many fixed elements can be retrieved from a row element.
// main template for simple values and ranges. Specializing for ranges
// is ambiguous with arrays, as they are also ranges
template <typename T, typename = void>
struct row_traits {
static constexpr bool is_range = is_range_v<q20::remove_cvref_t<T>>;
// a static size of -1 indicates dynamically sized range
static constexpr int static_size = is_range ? -1 : 0;
static constexpr int fixed_size() { return 1; }
};
// Specialization for tuples, using std::tuple_size
template <typename T>
struct row_traits<T, std::void_t<std::tuple_element_t<0, T>>> {
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);
static constexpr int fixed_size() { return 0; }
};
// Specialization for C arrays
template <typename T, std::size_t N>
struct row_traits<T[N]>
{
static_assert(q20::in_range<int>(N));
static constexpr int static_size = int(N);
static constexpr int fixed_size() { return 0; }
};
template <typename T>
[[maybe_unused]] static constexpr int static_size_v =
row_traits<q20::remove_cvref_t<std::remove_pointer_t<T>>>::static_size;
template <typename T> static auto pointerTo(T *t) { return t; }
template <typename T> static auto pointerTo(T &t) { return std::addressof(t); }
template <typename T> static auto pointerTo(const T &&t) = delete;
// The storage of the model data. We might store it as a pointer, or as a
// (copied- or moved-into) value. But we always return a pointer.
template <typename ModelStorage>
struct ModelData
{
using ModelPtr = std::conditional_t<std::is_pointer_v<ModelStorage>,
ModelStorage, ModelStorage *>;
using ConstModelPtr = std::conditional_t<std::is_pointer_v<ModelStorage>,
const ModelStorage, const ModelStorage *>;
ModelPtr model() { return pointerTo(m_model); }
ConstModelPtr model() const { return pointerTo(m_model); }
ModelStorage m_model;
};
} // QGenericItemModelDetails
class QGenericItemModel;
class QGenericItemModelImplBase
{
Q_DISABLE_COPY_MOVE(QGenericItemModelImplBase)
protected:
// Helpers for calling a lambda with the tuple element at a runtime index.
template <typename Tuple, typename F, size_t ...Is>
static void call_at(Tuple &&tuple, size_t idx, std::index_sequence<Is...>, F &&function)
{
((Is == idx ? static_cast<void>(function(get<Is>(std::forward<Tuple>(tuple))))
: static_cast<void>(0)), ...);
}
template <typename Tuple, typename F, size_t ...Is>
static void call_at(Tuple *tuple, size_t idx, std::index_sequence<Is...> seq, F &&function)
{
if (tuple)
call_at(*tuple, idx, seq, std::forward<F>(function));
}
template <typename T, typename F>
static auto for_element_at(T &&tuple, size_t idx, F &&function)
{
using type = std::remove_pointer_t<std::remove_reference_t<T>>;
constexpr size_t size = std::tuple_size_v<type>;
Q_ASSERT(idx < size);
return call_at(std::forward<T>(tuple), idx, std::make_index_sequence<size>{},
std::forward<F>(function));
}
// Get the QMetaType for a tuple-element at a runtime index.
// Used in the headerData implementation.
template <typename Tuple, std::size_t ...I>
static constexpr std::array<QMetaType, sizeof...(I)> makeMetaTypes(std::index_sequence<I...>)
{
return {{QMetaType::fromType<q20::remove_cvref_t<std::tuple_element_t<I, Tuple>>>()...}};
}
template <typename T>
static constexpr QMetaType meta_type_at(size_t idx)
{
using type = std::remove_pointer_t<std::remove_reference_t<T>>;
constexpr auto size = std::tuple_size_v<type>;
Q_ASSERT(idx < size);
return makeMetaTypes<type>(std::make_index_sequence<size>{}).at(idx);
}
// Helpers to call a given member function with the correct arguments.
template <typename Class, typename T, typename F, size_t...I>
static auto apply(std::integer_sequence<size_t, I...>, Class* obj, F&& fn, T&& tuple)
{
return std::invoke(fn, obj, std::get<I>(tuple)...);
}
template <typename Ret, typename Class, typename ...Args>
static void makeCall(QGenericItemModelImplBase *obj, Ret(Class::* &&fn)(Args...),
void *ret, const void *args)
{
const auto &tuple = *static_cast<const std::tuple<Args&...> *>(args);
*static_cast<Ret *>(ret) = apply(std::make_index_sequence<sizeof...(Args)>{},
static_cast<Class *>(obj), fn, tuple);
}
template <typename Ret, typename Class, typename ...Args>
static void makeCall(const QGenericItemModelImplBase *obj, Ret(Class::* &&fn)(Args...) const,
void *ret, const void *args)
{
const auto &tuple = *static_cast<const std::tuple<Args&...> *>(args);
*static_cast<Ret *>(ret) = apply(std::make_index_sequence<sizeof...(Args)>{},
static_cast<const Class *>(obj), fn, tuple);
}
public:
enum ConstOp {
Index,
Parent,
RowCount,
ColumnCount,
Flags,
HeaderData,
Data,
};
enum Op {
Destroy,
SetData,
ClearItemData,
InsertColumns,
RemoveColumns,
InsertRows,
RemoveRows,
};
void destroy()
{
call<bool>(Destroy);
}
private:
// prototypes
static void callConst(ConstOp, const QGenericItemModelImplBase *, void *, const void *);
static void call(Op, QGenericItemModelImplBase *, void *, const void *);
using CallConstFN = decltype(callConst);
using CallTupleFN = decltype(call);
CallConstFN *callConst_fn;
CallTupleFN *call_fn;
protected:
explicit QGenericItemModelImplBase(QGenericItemModel *itemModel)
: m_itemModel(itemModel)
{}
~QGenericItemModelImplBase() = default;
QGenericItemModel *m_itemModel;
inline QModelIndex createIndex(int row, int column, const void *ptr = nullptr) const;
inline QHash<int, QByteArray> roleNames() const;
inline void dataChanged(const QModelIndex &from, const QModelIndex &to,
const QList<int> &roles);
inline void beginInsertColumns(const QModelIndex &parent, int start, int count);
inline void endInsertColumns();
inline void beginRemoveColumns(const QModelIndex &parent, int start, int count);
inline void endRemoveColumns();
inline void beginInsertRows(const QModelIndex &parent, int start, int count);
inline void endInsertRows();
inline void beginRemoveRows(const QModelIndex &parent, int start, int count);
inline void endRemoveRows();
template <typename Impl>
void initFrom(Impl *)
{
callConst_fn = &Impl::callConst;
call_fn = &Impl::call;
}
public:
template <typename Ret, typename ...Args>
Ret callConst(ConstOp op, const Args &...args) const
{
Ret ret = {};
const auto tuple = std::tie(args...);
callConst_fn(op, this, &ret, &tuple);
return ret;
}
template <typename Ret, typename ...Args>
Ret call(Op op, const Args &...args)
{
Ret ret = {};
const auto tuple = std::tie(args...);
call_fn(op, this, &ret, &tuple);
return ret;
}
};
QT_END_NAMESPACE
#endif // Q_QDOC
#endif // QGENERICITEMMODEL_IMPL_H

View File

@ -2,6 +2,7 @@
# SPDX-License-Identifier: BSD-3-Clause # SPDX-License-Identifier: BSD-3-Clause
add_subdirectory(qstringlistmodel) add_subdirectory(qstringlistmodel)
add_subdirectory(qgenericitemmodel)
if(TARGET Qt::Gui) if(TARGET Qt::Gui)
add_subdirectory(qabstractitemmodel) add_subdirectory(qabstractitemmodel)
if(QT_FEATURE_proxymodel) if(QT_FEATURE_proxymodel)

View File

@ -0,0 +1,25 @@
# Copyright (C) 2025 The Qt Company Ltd.
# SPDX-License-Identifier: BSD-3-Clause
#####################################################################
## tst_qgenericitemmodel Test:
#####################################################################
if(NOT QT_BUILD_STANDALONE_TESTS AND NOT QT_BUILDING_QT)
cmake_minimum_required(VERSION 3.16)
project(tst_qgenericitemmodel LANGUAGES CXX)
find_package(Qt6BuildInternals REQUIRED COMPONENTS STANDALONE_TEST)
endif()
qt_internal_add_test(tst_qgenericitemmodel
SOURCES
tst_qgenericitemmodel.cpp
LIBRARIES
Qt::Gui
)
if (NOT INTEGRITY AND NOT VXWORKS)
if ("${CMAKE_CXX_COMPILE_FEATURES}" MATCHES "cxx_std_20")
set_property(TARGET tst_qgenericitemmodel PROPERTY CXX_STANDARD 20)
endif()
endif()

View File

@ -0,0 +1,573 @@
// Copyright (C) 2025 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
#include <QTest>
#include <QtCore/qgenericitemmodel.h>
#include <QtCore/qjsondocument.h>
#include <QtCore/qjsonarray.h>
#include <QtGui/qcolor.h>
#if QT_CONFIG(itemmodeltester)
#include <QtTest/qabstractitemmodeltester.h>
#endif
#include <vector>
#if defined(__cpp_lib_ranges)
#include <ranges>
#endif
struct Row
{
QString m_item;
int m_number;
QString m_description;
template <size_t I, typename RowType,
std::enable_if_t<(I < 3), bool> = true,
std::enable_if_t<std::is_same_v<q20::remove_cvref_t<RowType>, Row>, bool> = true
>
friend inline decltype(auto) get(RowType &&item)
{
if constexpr (I == 0)
return q23::forward_like<RowType>(item.m_item);
else if constexpr (I == 1)
return q23::forward_like<RowType>(item.m_number);
else // if constexpr (I == 2)
return q23::forward_like<RowType>(item.m_description);
}
};
namespace std {
template <> struct tuple_size<Row> : std::integral_constant<size_t, 3> {};
template <> struct tuple_element<0, Row> { using type = QString; };
template <> struct tuple_element<1, Row> { using type = int; };
template <> struct tuple_element<2, Row> { using type = QString; };
}
struct ConstRow
{
QString value;
template<size_t I,
std::enable_if_t<I == 0, bool> = true
>
friend inline decltype(auto) get(const ConstRow &row)
{
if constexpr (I == 0)
return row.value;
}
};
namespace std {
template <> struct tuple_size<ConstRow> : std::integral_constant<size_t, 1> {};
template <> struct tuple_element<0, ConstRow> { using type = QString; };
}
class tst_QGenericItemModel : public QObject
{
Q_OBJECT
private slots:
void basics_data() { createTestData(); }
void basics();
void minimalIterator();
void ranges();
void json();
void dimensions_data() { createTestData(); }
void dimensions();
void flags_data() { createTestData(); }
void flags();
void data_data() { createTestData(); }
void data();
void setData_data() { createTestData(); }
void setData();
void clearItemData_data() { createTestData(); }
void clearItemData();
void insertRows_data() { createTestData(); }
void insertRows();
void removeRows_data() { createTestData(); }
void removeRows();
void insertColumns_data() { createTestData(); }
void insertColumns();
void removeColumns_data() { createTestData(); }
void removeColumns();
void inconsistentColumnCount();
private:
void createTestData();
struct Data {
// fixed number of columns and rows
std::array<int, 5> fixedArrayOfNumbers = {1, 2, 3, 4, 5};
int cArrayOfNumbers[5] = {1, 2, 3, 4, 5};
Row cArrayFixedColumns[3] = {
{"red", 0xff0000, "The color red"},
{"green", 0x00ff00, "The color green"},
{"blue", 0x0000ff, "The color blue"}
};
// dynamic number of rows, fixed number of columns
std::vector<std::tuple<int, QString>> vectorOfFixedColumns = {
{0, "null"},
{1, "one"},
{2, "two"},
{3, "three"},
{4, "four"},
};
std::vector<std::array<int, 10>> vectorOfArrays = {
{1, 2, 3, 4, 5, 6, 7, 8, 9, 10},
{11, 12, 13, 14, 15, 16, 17, 18, 19, 20},
{21, 22, 23, 24, 25, 26, 27, 28, 29, 30},
{31, 32, 33, 34, 35, 36, 37, 38, 39, 40},
{41, 42, 43, 44, 45, 46, 47, 48, 49, 50},
};
std::vector<Row> vectorOfStructs = {
{"red", 1, "one"},
{"green", 2, "two"},
{"blue", 3, "three"},
};
// bad (but legal) get() overload that never returns a mutable reference
std::vector<ConstRow> vectorOfConstStructs = {
{"one"},
{"two"},
{"three"},
};
// dynamic number of rows and columns
std::vector<std::vector<double>> tableOfNumbers = {
{1.0, 2.0, 3.0, 4.0, 5.0},
{6.0, 7.0, 8.0, 9.0, 10.0},
{11.0, 12.0, 13.0, 14.0, 15.0},
{16.0, 17.0, 18.0, 19.0, 20.0},
{21.0, 22.0, 23.0, 24.0, 25.0},
};
// rows are pointers
Row rowAsPointer = {"blue", 0x0000ff, "Blau"};
std::vector<Row *> tableOfRowPointers = {
&rowAsPointer,
&rowAsPointer,
&rowAsPointer,
};
// constness
std::array<const int, 5> arrayOfConstNumbers = { 1, 2, 3, 4 };
// note: std::vector doesn't allow for const value types
const std::vector<int> constListOfNumbers = { 1, 2, 3 };
// const model is read-only
const std::vector<std::vector<double>> constTableOfNumbers = {
{1.0, 2.0, 3.0, 4.0, 5.0},
{6.0, 7.0, 8.0, 9.0, 10.0},
{11.0, 12.0, 13.0, 14.0, 15.0},
{16.0, 17.0, 18.0, 19.0, 20.0},
{21.0, 22.0, 23.0, 24.0, 25.0},
};
};
std::unique_ptr<Data> m_data;
public:
enum class ChangeAction
{
ReadOnly = 0x00,
InsertRows = 0x01,
RemoveRows = 0x02,
ChangeRows = InsertRows | RemoveRows,
InsertColumns = 0x04,
RemoveColumns = 0x08,
ChangeColumns = InsertColumns | RemoveColumns,
SetData = 0x10,
All = ChangeRows | ChangeColumns | SetData
};
Q_DECLARE_FLAGS(ChangeActions, ChangeAction);
};
Q_DECLARE_OPERATORS_FOR_FLAGS(tst_QGenericItemModel::ChangeActions)
using Factory = std::function<std::unique_ptr<QAbstractItemModel>()>;
void tst_QGenericItemModel::createTestData()
{
m_data.reset(new Data);
QTest::addColumn<Factory>("factory");
QTest::addColumn<int>("expectedRowCount");
QTest::addColumn<int>("expectedColumnCount");
QTest::addColumn<ChangeActions>("changeActions");
Factory factory;
#define ADD_HELPER(Model, Tag, Ref) \
factory = [this]() -> std::unique_ptr<QAbstractItemModel> { \
return std::unique_ptr<QAbstractItemModel>(new QGenericItemModel(Ref->Model)); \
}; \
QTest::addRow(#Model #Tag) << factory << int(std::size(m_data->Model)) \
#define ADD_POINTER(Model) \
ADD_HELPER(Model, Pointer, &m_data) \
#define ADD_COPY(Model) \
ADD_HELPER(Model, Copy, m_data) \
// POINTER-tests will modify the data structure that lives in m_data,
// so we have to run tests on copies of that data first for each type,
// or only run POINTER-tests.
// The entire test data is recreated for each test function, but test
// functions must not change data structures other than the one tested.
ADD_COPY(fixedArrayOfNumbers)
<< 1 << ChangeActions(ChangeAction::SetData);
ADD_POINTER(fixedArrayOfNumbers)
<< 1 << ChangeActions(ChangeAction::SetData);
ADD_POINTER(cArrayOfNumbers)
<< 1 << ChangeActions(ChangeAction::SetData);
ADD_POINTER(cArrayFixedColumns)
<< int(std::tuple_size_v<Row>) << ChangeActions(ChangeAction::SetData);
ADD_COPY(vectorOfFixedColumns)
<< 2 << (ChangeAction::ChangeRows | ChangeAction::SetData);
ADD_POINTER(vectorOfFixedColumns)
<< 2 << (ChangeAction::ChangeRows | ChangeAction::SetData);
ADD_COPY(vectorOfArrays)
<< 10 << (ChangeAction::ChangeRows | ChangeAction::SetData);
ADD_POINTER(vectorOfArrays)
<< 10 << (ChangeAction::ChangeRows | ChangeAction::SetData);
ADD_COPY(vectorOfStructs)
<< int(std::tuple_size_v<Row>) << (ChangeAction::ChangeRows | ChangeAction::SetData);
ADD_POINTER(vectorOfStructs)
<< int(std::tuple_size_v<Row>) << (ChangeAction::ChangeRows | ChangeAction::SetData);
ADD_COPY(vectorOfConstStructs)
<< int(std::tuple_size_v<ConstRow>) << ChangeActions(ChangeAction::ChangeRows);
ADD_POINTER(vectorOfConstStructs)
<< int(std::tuple_size_v<ConstRow>) << ChangeActions(ChangeAction::ChangeRows);
ADD_COPY(tableOfNumbers)
<< 5 << ChangeActions(ChangeAction::All);
ADD_POINTER(tableOfNumbers)
<< 5 << ChangeActions(ChangeAction::All);
// only adding as pointer, copy would operate on the same data
ADD_POINTER(tableOfRowPointers)
<< int(std::tuple_size_v<Row>) << (ChangeAction::ChangeRows | ChangeAction::SetData);
ADD_COPY(arrayOfConstNumbers)
<< 1 << ChangeActions(ChangeAction::ReadOnly);
ADD_POINTER(arrayOfConstNumbers)
<< 1 << ChangeActions(ChangeAction::ReadOnly);
ADD_COPY(constListOfNumbers)
<< 1 << ChangeActions(ChangeAction::ReadOnly);
ADD_POINTER(constListOfNumbers)
<< 1 << ChangeActions(ChangeAction::ReadOnly);
ADD_COPY(constTableOfNumbers)
<< 5 << ChangeActions(ChangeAction::ReadOnly);
ADD_POINTER(constTableOfNumbers)
<< 5 << ChangeActions(ChangeAction::ReadOnly);
#undef ADD_COPY
#undef ADD_POINTER
#undef ADD_HELPER
QTest::addRow("Moved table") << Factory([]{
QList<std::vector<QString>> movedTable = {
{"0/0", "0/1", "0/2", "0/3"},
{"1/0", "1/1", "1/2", "1/3"},
{"2/0", "2/1", "2/2", "2/3"},
{"3/0", "3/1", "3/2", "3/3"},
};
return std::unique_ptr<QAbstractItemModel>(new QGenericItemModel(std::move(movedTable)));
}) << 4 << 4 << ChangeActions(ChangeAction::All);
}
void tst_QGenericItemModel::basics()
{
#if QT_CONFIG(itemmodeltester)
QFETCH(Factory, factory);
auto model = factory();
QAbstractItemModelTester modelTest(model.get(), this);
#else
QSKIP("QAbstractItemModelTester not available");
#endif
}
void tst_QGenericItemModel::minimalIterator()
{
struct Minimal
{
struct iterator
{
using value_type = QString;
using size_type = int;
using difference_type = int;
using reference = value_type;
using pointer = value_type;
using iterator_category = std::forward_iterator_tag;
constexpr iterator &operator++()
{ ++m_index; return *this; }
constexpr iterator operator++(int)
{ auto copy = *this; ++m_index; return copy; }
reference operator*() const
{ return QString::number(m_index); }
constexpr bool operator==(const iterator &other) const noexcept
{ return m_index == other.m_index; }
constexpr bool operator!=(const iterator &other) const noexcept
{ return m_index != other.m_index; }
size_type m_index;
};
#if defined (__cpp_concepts)
static_assert(std::forward_iterator<iterator>);
#endif
iterator begin() const { return iterator{0}; }
iterator end() const { return iterator{m_size}; }
int m_size;
} minimal{100};
QGenericItemModel model(minimal);
QCOMPARE(model.rowCount(), minimal.m_size);
for (int row = model.rowCount() - 1; row >= 0; --row) {
const QModelIndex index = model.index(row, 0);
QCOMPARE(index.data(), QString::number(row));
QVERIFY(!index.flags().testFlag(Qt::ItemIsEditable));
}
}
void tst_QGenericItemModel::ranges()
{
#if defined(__cpp_lib_ranges)
const int lowest = 1;
const int highest = 10;
QGenericItemModel model(std::views::iota(lowest, highest));
QCOMPARE(model.rowCount(), highest - lowest);
QCOMPARE(model.columnCount(), 1);
#else
QSKIP("C++ ranges library not available");
#endif
}
void tst_QGenericItemModel::json()
{
QJsonDocument json = QJsonDocument::fromJson(R"([ "one", "two" ])");
QVERIFY(json.isArray());
QGenericItemModel model(json.array());
QCOMPARE(model.rowCount(), 2);
const QModelIndex index = model.index(1, 0);
QVERIFY(index.isValid());
QCOMPARE(index.data().toString(), "two");
}
void tst_QGenericItemModel::dimensions()
{
QFETCH(Factory, factory);
auto model = factory();
QFETCH(const int, expectedRowCount);
QFETCH(const int, expectedColumnCount);
QCOMPARE(model->rowCount(), expectedRowCount);
QCOMPARE(model->columnCount(), expectedColumnCount);
}
void tst_QGenericItemModel::flags()
{
QFETCH(Factory, factory);
auto model = factory();
QFETCH(const ChangeActions, changeActions);
const QModelIndex first = model->index(0, 0);
QVERIFY(first.isValid());
const QModelIndex last = model->index(model->rowCount() - 1, model->columnCount() - 1);
QVERIFY(last.isValid());
QCOMPARE(first.flags().testFlag(Qt::ItemIsEditable),
changeActions.testFlags(ChangeAction::SetData));
QCOMPARE(last.flags().testFlag(Qt::ItemIsEditable),
changeActions.testFlags(ChangeAction::SetData));
}
void tst_QGenericItemModel::data()
{
QFETCH(Factory, factory);
auto model = factory();
QVERIFY(!model->data({}).isValid());
const QModelIndex first = model->index(0, 0);
QVERIFY(first.isValid());
const QModelIndex last = model->index(model->rowCount() - 1, model->columnCount() - 1);
QVERIFY(last.isValid());
QVERIFY(first.data().isValid());
QVERIFY(last.data().isValid());
}
void tst_QGenericItemModel::setData()
{
QFETCH(Factory, factory);
auto model = factory();
QFETCH(const ChangeActions, changeActions);
QVERIFY(!model->setData({}, {}));
const QModelIndex first = model->index(0, 0);
QVERIFY(first.isValid());
QVariant newValue = 12345;
const QVariant oldValue = first.data();
QVERIFY(oldValue.isValid());
if (!newValue.canConvert(oldValue.metaType()))
newValue = QVariant(oldValue.metaType());
QCOMPARE(first.data(), oldValue);
QCOMPARE(model->setData(first, newValue), changeActions.testFlag(ChangeAction::SetData));
QCOMPARE(first.data() == oldValue, !changeActions.testFlag(ChangeAction::SetData));
}
void tst_QGenericItemModel::clearItemData()
{
QFETCH(Factory, factory);
auto model = factory();
QFETCH(const ChangeActions, changeActions);
QVERIFY(!model->clearItemData({}));
const QModelIndex index0 = model->index(1, 0);
const QModelIndex index1 = model->index(1, 1);
const QVariant oldDataAt0 = index0.data();
const QVariant oldDataAt1 = index1.data();
QCOMPARE(model->clearItemData(index0), changeActions.testFlags(ChangeAction::SetData));
QCOMPARE(index0.data() == oldDataAt0, !changeActions.testFlags(ChangeAction::SetData));
QCOMPARE(index1.data(), oldDataAt1);
}
void tst_QGenericItemModel::insertRows()
{
QFETCH(Factory, factory);
auto model = factory();
QFETCH(const int, expectedRowCount);
QFETCH(const int, expectedColumnCount);
QFETCH(const ChangeActions, changeActions);
const bool canSetData = changeActions.testFlag(ChangeAction::SetData);
QCOMPARE(model->rowCount(), expectedRowCount);
QCOMPARE(model->insertRow(0), changeActions.testFlag(ChangeAction::InsertRows));
QCOMPARE(model->rowCount() == expectedRowCount + 1,
changeActions.testFlag(ChangeAction::InsertRows));
// get and put data into the new row
const QModelIndex firstItem = model->index(0, 0);
const QModelIndex lastItem = model->index(0, expectedColumnCount - 1);
QVERIFY(firstItem.isValid());
QVERIFY(lastItem.isValid());
const QVariant firstValue = firstItem.data();
const QVariant lastValue = lastItem.data();
QEXPECT_FAIL("tableOfPointersPointer", "No item created", Continue);
QEXPECT_FAIL("tableOfRowPointersPointer", "No row created", Continue);
QVERIFY(firstValue.isValid() && lastValue.isValid());
QCOMPARE(model->setData(firstItem, lastValue), canSetData && lastValue.isValid());
QCOMPARE(model->setData(lastItem, firstValue), canSetData && firstValue.isValid());
// append more rows
QCOMPARE(model->insertRows(model->rowCount(), 5),
changeActions.testFlag(ChangeAction::InsertRows));
QCOMPARE(model->rowCount() == expectedRowCount + 6,
changeActions.testFlag(ChangeAction::InsertRows));
}
void tst_QGenericItemModel::removeRows()
{
QFETCH(Factory, factory);
auto model = factory();
QFETCH(const int, expectedRowCount);
QFETCH(const ChangeActions, changeActions);
QCOMPARE(model->rowCount(), expectedRowCount);
QCOMPARE(model->removeRow(0), changeActions.testFlag(ChangeAction::RemoveRows));
QCOMPARE(model->rowCount() == expectedRowCount - 1,
changeActions.testFlag(ChangeAction::RemoveRows));
QCOMPARE(model->removeRows(model->rowCount() - 2, 2),
changeActions.testFlag(ChangeAction::RemoveRows));
QCOMPARE(model->rowCount() == expectedRowCount - 3,
changeActions.testFlag(ChangeAction::RemoveRows));
const int newRowCount = model->rowCount();
// make sure we don't crash when removing more than exist
const bool couldRemove = model->removeRows(model->rowCount() - 5, model->rowCount() * 2);
QCOMPARE_LE(model->rowCount(), newRowCount);
QCOMPARE(couldRemove, model->rowCount() != newRowCount);
}
void tst_QGenericItemModel::insertColumns()
{
QFETCH(Factory, factory);
auto model = factory();
QFETCH(const int, expectedColumnCount);
QFETCH(const ChangeActions, changeActions);
QCOMPARE(model->columnCount(), expectedColumnCount);
QCOMPARE(model->insertColumn(0), changeActions.testFlag(ChangeAction::InsertColumns));
QCOMPARE(model->columnCount() == expectedColumnCount + 1,
changeActions.testFlag(ChangeAction::InsertColumns));
// append
QCOMPARE(model->insertColumns(model->columnCount(), 5),
changeActions.testFlag(ChangeAction::InsertColumns));
QCOMPARE(model->columnCount() == expectedColumnCount + 6,
changeActions.testFlag(ChangeAction::InsertColumns));
}
void tst_QGenericItemModel::removeColumns()
{
QFETCH(Factory, factory);
auto model = factory();
QFETCH(const int, expectedColumnCount);
QFETCH(const ChangeActions, changeActions);
QCOMPARE(model->columnCount(), expectedColumnCount);
QCOMPARE(model->removeColumn(0),
changeActions.testFlag(ChangeAction::RemoveColumns));
}
void tst_QGenericItemModel::inconsistentColumnCount()
{
QTest::ignoreMessage(QtCriticalMsg, "QGenericItemModel: "
"Column-range at row 1 is not large enough!");
std::vector<std::vector<int>> fuzzyTable = {
{0},
{},
{2},
};
QGenericItemModel model(fuzzyTable);
QCOMPARE(model.columnCount(), 1);
for (int row = 0; row < model.rowCount(); ++row) {
auto debug = qScopeGuard([&]{
qCritical() << "Test failed for row" << row << fuzzyTable.at(row).size();
});
const bool shouldWork = int(fuzzyTable.at(row).size()) >= model.columnCount();
const auto index = model.index(row, model.columnCount() - 1);
QCOMPARE(index.isValid(), shouldWork);
// none of these should crash
QCOMPARE(index.data().isValid(), shouldWork);
QCOMPARE(model.setData(index, row + 5), shouldWork);
QCOMPARE(model.clearItemData(index), shouldWork);
debug.dismiss();
}
}
QTEST_MAIN(tst_QGenericItemModel)
#include "tst_qgenericitemmodel.moc"