diff --git a/src/corelib/CMakeLists.txt b/src/corelib/CMakeLists.txt index b92c8f0b116..c18344cfc81 100644 --- a/src/corelib/CMakeLists.txt +++ b/src/corelib/CMakeLists.txt @@ -1156,6 +1156,7 @@ qt_internal_extend_target(Core CONDITION QT_FEATURE_itemmodel SOURCES itemmodels/qabstractitemmodel.cpp itemmodels/qabstractitemmodel.h itemmodels/qabstractitemmodel_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 @@ -1620,3 +1621,7 @@ function(qt_internal_library_deprecation_level) set_source_files_properties("${output_header}" PROPERTIES GENERATED TRUE) endfunction() qt_internal_library_deprecation_level() + +if(QT_FEATURE_doc_snippets) + add_subdirectory(doc/snippets) +endif() diff --git a/src/corelib/doc/snippets/CMakeLists.txt b/src/corelib/doc/snippets/CMakeLists.txt new file mode 100644 index 00000000000..b8b1d2c1049 --- /dev/null +++ b/src/corelib/doc/snippets/CMakeLists.txt @@ -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) diff --git a/src/corelib/doc/snippets/qgenericitemmodel/main.cpp b/src/corelib/doc/snippets/qgenericitemmodel/main.cpp new file mode 100644 index 00000000000..b99c600684c --- /dev/null +++ b/src/corelib/doc/snippets/qgenericitemmodel/main.cpp @@ -0,0 +1,108 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#include +#include +#include +#include + +#include +#include + +void array() +{ + QListView listView; + +//! [array] +std::array numbers = {1, 2, 3, 4, 5}; +QGenericItemModel model(numbers); +listView.setModel(&model); +//! [array] +} + +void const_array() +{ +//! [const_array] +const std::array numbers = {1, 2, 3, 4, 5}; +//! [const_array] +QGenericItemModel model(numbers); +} + +void const_values() +{ +//! [const_values] +std::array numbers = {1, 2, 3, 4, 5}; +//! [const_values] +QGenericItemModel model(numbers); +} + +void list_of_int() +{ +//! [list_of_int] +QList 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> 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; +QList 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 + requires ((I <= 3) && std::is_same_v, 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(book.summary); + else if constexpr (I == 3) + return std::forward_like(book.rating); + } +}; + +namespace std { + template <> struct tuple_size : std::integral_constant {}; + template struct tuple_element + { using type = decltype(get(std::declval())); }; +} +//! [tuple_protocol] +#endif // __cpp_concepts && forward_like diff --git a/src/corelib/itemmodels/qgenericitemmodel.cpp b/src/corelib/itemmodels/qgenericitemmodel.cpp new file mode 100644 index 00000000000..9eb0983cccf --- /dev/null +++ b/src/corelib/itemmodels/qgenericitemmodel.cpp @@ -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 > 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(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(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(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(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(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(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(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(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(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(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(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(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(QGenericItemModelImplBase::RemoveRows, row, count, parent); +} + +QT_END_NAMESPACE + +#include "moc_qgenericitemmodel.cpp" diff --git a/src/corelib/itemmodels/qgenericitemmodel.h b/src/corelib/itemmodels/qgenericitemmodel.h new file mode 100644 index 00000000000..7667164a088 --- /dev/null +++ b/src/corelib/itemmodels/qgenericitemmodel.h @@ -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 + +QT_BEGIN_NAMESPACE + +class Q_CORE_EXPORT QGenericItemModel : public QAbstractItemModel +{ + Q_OBJECT +public: + template = 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 impl; +}; + +// implementation of forwarders +QModelIndex QGenericItemModelImplBase::createIndex(int row, int column, const void *ptr) const +{ + return m_itemModel->createIndex(row, column, ptr); +} +QHash QGenericItemModelImplBase::roleNames() const +{ + return m_itemModel->roleNames(); +} +void QGenericItemModelImplBase::dataChanged(const QModelIndex &from, const QModelIndex &to, + const QList &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 +class QGenericItemModelImpl : public QGenericItemModelImplBase +{ + Q_DISABLE_COPY_MOVE(QGenericItemModelImpl) +public: + using range_type = std::remove_pointer_t>; + using row_reference = decltype(*std::begin(std::declval())); + using const_row_reference = decltype(*std::cbegin(std::declval())); + using row_type = std::remove_reference_t; + +protected: + using Self = QGenericItemModelImpl; + Structure& that() { return static_cast(*this); } + const Structure& that() const { return static_cast(*this); } + + template + static constexpr auto size(const C &c) + { + if constexpr (QGenericItemModelDetails::test_size()) + 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; + using row_features = QGenericItemModelDetails::range_traits; + + using row_traits = QGenericItemModelDetails::row_traits>>; + + static constexpr bool isMutable() + { + return range_features::is_mutable && row_features::is_mutable + && std::is_reference_v; + } + + static constexpr int static_row_count = QGenericItemModelDetails::static_size_v; + static constexpr bool rows_are_pointers = std::is_pointer_v; + static constexpr int static_column_count = QGenericItemModelDetails::static_size_v; + + 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; + using const_row_ptr = const std::remove_pointer_t *; + + using ModelData = QGenericItemModelDetails::ModelData, + Range, std::remove_reference_t> + >; +public: + explicit QGenericItemModelImpl(Range &&model, QGenericItemModel *itemModel) + : QGenericItemModelImplBase(itemModel) + , m_data{std::forward(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(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 && !std::is_const_v) { + // 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); + for_element_at(mutableRow, index.column(), [&f](auto &&ref){ + using target_type = decltype(ref); + if constexpr (std::is_const_v>) + f &= ~Qt::ItemIsEditable; + else if constexpr (std::is_lvalue_reference_v) + 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(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{} : 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 + && !std::is_const_v>) { + success = writeData(std::forward(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 + && !std::is_const_v>) { + 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 + static QVariant read(const Value &value) + { + if constexpr (std::is_constructible_v) + return QVariant(value); + else + return QVariant::fromValue(value); + } + template + static QVariant read(Value *value) + { + if (value) { + if constexpr (std::is_constructible_v) + return QVariant(value); + else + return read(*value); + } + return {}; + } + + template + static bool write(Target &target, const QVariant &value) + { + using Type = std::remove_reference_t; + if constexpr (std::is_constructible_v) { + target = value; + return true; + } else if (value.canConvert()) { + target = value.value(); + return true; + } + return false; + } + template + 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 +class QGenericItemModelStructureImpl : public QGenericItemModelImpl< + QGenericItemModelStructureImpl, + Range> +{}; + +// specialization for flat models without protocol +template +class QGenericItemModelStructureImpl : public QGenericItemModelImpl< + QGenericItemModelStructureImpl, + Range> +{ + using Base = QGenericItemModelImpl, Range>; + friend class QGenericItemModelImpl, 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(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 > +QGenericItemModel::QGenericItemModel(Range &&range, QObject *parent) + : QAbstractItemModel(parent) + , impl(new QGenericItemModelStructureImpl(std::forward(range), this)) +{} + +QT_END_NAMESPACE + +#endif // QGENERICITEMMODEL_H diff --git a/src/corelib/itemmodels/qgenericitemmodel_impl.h b/src/corelib/itemmodels/qgenericitemmodel_impl.h new file mode 100644 index 00000000000..b545a8eade8 --- /dev/null +++ b/src/corelib/itemmodels/qgenericitemmodel_impl.h @@ -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 +#include +#include + +#include +#include +#include +#include +#include + +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 + struct test_insert : std::false_type {}; + + template + struct test_insert().insert( + std::declval(), + std::declval(), + std::declval() + ))>> + : std::true_type + {}; + + template + struct test_erase : std::false_type {}; + + template + struct test_erase().erase( + std::declval(), + std::declval() + ))>> + : std::true_type + {}; + + template + struct test_resize : std::false_type {}; + + template + struct test_resize().resize( + std::declval(), + std::declval() + ))>> + : std::true_type + {}; + + template + struct test_size : std::false_type {}; + template + struct test_size()))>> : std::true_type {}; + + template + struct range_traits : std::false_type { + static constexpr bool is_mutable = !std::is_const_v; + static constexpr bool has_insert = false; + static constexpr bool has_erase = false; + static constexpr bool has_resize = false; + }; + template + struct range_traits())), + decltype(std::cend(std::declval())) + >> : std::true_type + { + using value_type = std::remove_reference_t()))>; + static constexpr bool is_mutable = !std::is_const_v && !std::is_const_v; + static constexpr bool has_insert = test_insert(); + static constexpr bool has_erase = test_erase(); + static constexpr bool has_resize = test_resize(); + }; + + // Specializations for types that look like ranges, but should be + // treated as values. + enum class Mutable { Yes, No }; + template + 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 : iterable_value {}; + template <> struct range_traits : iterable_value {}; + template + struct range_traits> : iterable_value + {}; + + // const T * and views are read-only + template struct range_traits : iterable_value {}; + template <> struct range_traits : iterable_value {}; + + template + [[maybe_unused]] static constexpr bool is_range_v = range_traits(); + template + using if_is_range = std::enable_if_t< + is_range_v>>, 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 + struct row_traits { + static constexpr bool is_range = is_range_v>; + // 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 + struct row_traits>> { + static constexpr std::size_t size64= std::tuple_size_v; + static_assert(q20::in_range(size64)); + static constexpr int static_size = int(size64); + static constexpr int fixed_size() { return 0; } + }; + + // Specialization for C arrays + template + struct row_traits + { + static_assert(q20::in_range(N)); + static constexpr int static_size = int(N); + static constexpr int fixed_size() { return 0; } + }; + + template + [[maybe_unused]] static constexpr int static_size_v = + row_traits>>::static_size; + + template static auto pointerTo(T *t) { return t; } + template static auto pointerTo(T &t) { return std::addressof(t); } + template 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 + struct ModelData + { + using ModelPtr = std::conditional_t, + ModelStorage, ModelStorage *>; + using ConstModelPtr = std::conditional_t, + 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 + static void call_at(Tuple &&tuple, size_t idx, std::index_sequence, F &&function) + { + ((Is == idx ? static_cast(function(get(std::forward(tuple)))) + : static_cast(0)), ...); + } + + template + static void call_at(Tuple *tuple, size_t idx, std::index_sequence seq, F &&function) + { + if (tuple) + call_at(*tuple, idx, seq, std::forward(function)); + } + + template + static auto for_element_at(T &&tuple, size_t idx, F &&function) + { + using type = std::remove_pointer_t>; + constexpr size_t size = std::tuple_size_v; + Q_ASSERT(idx < size); + return call_at(std::forward(tuple), idx, std::make_index_sequence{}, + std::forward(function)); + } + + // Get the QMetaType for a tuple-element at a runtime index. + // Used in the headerData implementation. + template + static constexpr std::array makeMetaTypes(std::index_sequence) + { + return {{QMetaType::fromType>>()...}}; + } + template + static constexpr QMetaType meta_type_at(size_t idx) + { + using type = std::remove_pointer_t>; + constexpr auto size = std::tuple_size_v; + Q_ASSERT(idx < size); + return makeMetaTypes(std::make_index_sequence{}).at(idx); + } + + // Helpers to call a given member function with the correct arguments. + template + static auto apply(std::integer_sequence, Class* obj, F&& fn, T&& tuple) + { + return std::invoke(fn, obj, std::get(tuple)...); + } + template + static void makeCall(QGenericItemModelImplBase *obj, Ret(Class::* &&fn)(Args...), + void *ret, const void *args) + { + const auto &tuple = *static_cast *>(args); + *static_cast(ret) = apply(std::make_index_sequence{}, + static_cast(obj), fn, tuple); + } + template + static void makeCall(const QGenericItemModelImplBase *obj, Ret(Class::* &&fn)(Args...) const, + void *ret, const void *args) + { + const auto &tuple = *static_cast *>(args); + *static_cast(ret) = apply(std::make_index_sequence{}, + static_cast(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(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 roleNames() const; + inline void dataChanged(const QModelIndex &from, const QModelIndex &to, + const QList &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 + void initFrom(Impl *) + { + callConst_fn = &Impl::callConst; + call_fn = &Impl::call; + } + +public: + template + 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 + 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 diff --git a/tests/auto/corelib/itemmodels/CMakeLists.txt b/tests/auto/corelib/itemmodels/CMakeLists.txt index a8aa7438877..642c7599c44 100644 --- a/tests/auto/corelib/itemmodels/CMakeLists.txt +++ b/tests/auto/corelib/itemmodels/CMakeLists.txt @@ -2,6 +2,7 @@ # SPDX-License-Identifier: BSD-3-Clause add_subdirectory(qstringlistmodel) +add_subdirectory(qgenericitemmodel) if(TARGET Qt::Gui) add_subdirectory(qabstractitemmodel) if(QT_FEATURE_proxymodel) diff --git a/tests/auto/corelib/itemmodels/qgenericitemmodel/CMakeLists.txt b/tests/auto/corelib/itemmodels/qgenericitemmodel/CMakeLists.txt new file mode 100644 index 00000000000..72ed3b43cfb --- /dev/null +++ b/tests/auto/corelib/itemmodels/qgenericitemmodel/CMakeLists.txt @@ -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() diff --git a/tests/auto/corelib/itemmodels/qgenericitemmodel/tst_qgenericitemmodel.cpp b/tests/auto/corelib/itemmodels/qgenericitemmodel/tst_qgenericitemmodel.cpp new file mode 100644 index 00000000000..f545f54d8ee --- /dev/null +++ b/tests/auto/corelib/itemmodels/qgenericitemmodel/tst_qgenericitemmodel.cpp @@ -0,0 +1,573 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#include +#include +#include +#include + +#include + +#if QT_CONFIG(itemmodeltester) +#include +#endif + +#include + +#if defined(__cpp_lib_ranges) +#include +#endif + +struct Row +{ + QString m_item; + int m_number; + QString m_description; + + template = true, + std::enable_if_t, Row>, bool> = true + > + friend inline decltype(auto) get(RowType &&item) + { + if constexpr (I == 0) + return q23::forward_like(item.m_item); + else if constexpr (I == 1) + return q23::forward_like(item.m_number); + else // if constexpr (I == 2) + return q23::forward_like(item.m_description); + } +}; + +namespace std { + template <> struct tuple_size : std::integral_constant {}; + 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 = true + > + friend inline decltype(auto) get(const ConstRow &row) + { + if constexpr (I == 0) + return row.value; + } +}; + +namespace std { + template <> struct tuple_size : std::integral_constant {}; + 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 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> vectorOfFixedColumns = { + {0, "null"}, + {1, "one"}, + {2, "two"}, + {3, "three"}, + {4, "four"}, + }; + std::vector> 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 vectorOfStructs = { + {"red", 1, "one"}, + {"green", 2, "two"}, + {"blue", 3, "three"}, + }; + + // bad (but legal) get() overload that never returns a mutable reference + std::vector vectorOfConstStructs = { + {"one"}, + {"two"}, + {"three"}, + }; + + // dynamic number of rows and columns + std::vector> 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 tableOfRowPointers = { + &rowAsPointer, + &rowAsPointer, + &rowAsPointer, + }; + + // constness + std::array arrayOfConstNumbers = { 1, 2, 3, 4 }; + // note: std::vector doesn't allow for const value types + const std::vector constListOfNumbers = { 1, 2, 3 }; + + // const model is read-only + const std::vector> 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 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()>; + +void tst_QGenericItemModel::createTestData() +{ + m_data.reset(new Data); + + QTest::addColumn("factory"); + QTest::addColumn("expectedRowCount"); + QTest::addColumn("expectedColumnCount"); + QTest::addColumn("changeActions"); + + Factory factory; + +#define ADD_HELPER(Model, Tag, Ref) \ + factory = [this]() -> std::unique_ptr { \ + return std::unique_ptr(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) << 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) << (ChangeAction::ChangeRows | ChangeAction::SetData); + ADD_POINTER(vectorOfStructs) + << int(std::tuple_size_v) << (ChangeAction::ChangeRows | ChangeAction::SetData); + + ADD_COPY(vectorOfConstStructs) + << int(std::tuple_size_v) << ChangeActions(ChangeAction::ChangeRows); + ADD_POINTER(vectorOfConstStructs) + << int(std::tuple_size_v) << 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) << (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> 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(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); +#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> 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"