From c99816b03304969216a8863dbcf4d60e6258d76d Mon Sep 17 00:00:00 2001 From: Volker Hilsheimer Date: Sat, 29 Mar 2025 09:37:10 +0100 Subject: [PATCH] QGIM: add support for tree data structures QAbstractItemModel represents a table of tables where each item can have an entire table-hierarchy as children. In practice, it is enough to support a hierarchy of rows: that's what our views can display, and what users are familiar with. With this limitation, a data structure represents a tree if it's possible to traverse that hierarchy. For each row, we need to be able to navigate to the parent row (which might be nil if the row is a top-level row), and get the (optional) list of children. To enable this, we introduce a protocol type that QGIM can be instantiated with, either explicitly or implicitly. An explicit protocol provides implementations for parentRow and childRows. Const overloads are mandatory, mutable overloads are optional and allow modifying the tree structure as well. A modifyable tree might in addition need to create a row object, as new rows have to be hooked up with parent and child relationships. And a tree must be able to destroy row objects as well when rows are removed. An implicit protocol means that the row type implements this protocol through member functions. We detect this at compile time and implement tree support for ranges with an appropriate row type. This implements a first ownership model as well: if the range was moved into the model, then the owner of the model cannot/must not do anything with the container anymore, so also cannot delete the rows. We have to do so. Detect that we are operating on a moved-in container, and delete all rows if so. To make the code more readable, use explicitly named types for the implementation of the model structure, with dedicated and clearly constrained QGIM constructors. Change-Id: Iaf19c27da6ec8604923ec7c0c2523c7d62edee50 Reviewed-by: Artem Dyomin --- .../doc/snippets/qgenericitemmodel/main.cpp | 194 ++++++ src/corelib/itemmodels/qgenericitemmodel.cpp | 171 ++++- src/corelib/itemmodels/qgenericitemmodel.h | 330 ++++++++- .../itemmodels/qgenericitemmodel_impl.h | 153 ++++- .../itemmodels/qgenericitemmodel/BLACKLIST | 23 + .../tst_qgenericitemmodel.cpp | 629 +++++++++++++++++- 6 files changed, 1469 insertions(+), 31 deletions(-) create mode 100644 tests/auto/corelib/itemmodels/qgenericitemmodel/BLACKLIST diff --git a/src/corelib/doc/snippets/qgenericitemmodel/main.cpp b/src/corelib/doc/snippets/qgenericitemmodel/main.cpp index 0004ffd967e..a9d5884d8a5 100644 --- a/src/corelib/doc/snippets/qgenericitemmodel/main.cpp +++ b/src/corelib/doc/snippets/qgenericitemmodel/main.cpp @@ -5,6 +5,7 @@ #ifndef QT_NO_WIDGETS +#include #include #include #include @@ -145,6 +146,199 @@ private: //! [gadget] } // namespace gadget +namespace tree_protocol +{ +//! [tree_protocol_0] +class TreeRow; + +using Tree = std::vector; +//! [tree_protocol_0] + +//! [tree_protocol_1] +class TreeRow +{ + Q_GADGET + // properties + + TreeRow *m_parent; + std::optional m_children; + +public: + TreeRow() = default; + + // rule of 0: copy, move, and destructor implicitly defaulted +//! [tree_protocol_1] + + friend struct TreeTraversal; + TreeRow(const QString &) {} + +//! [tree_protocol_2] + // tree traversal protocol implementation + const TreeRow *parentRow() const { return m_parent; } + const std::optional &childRows() const { return m_children; } +//! [tree_protocol_2] +//! [tree_protocol_3] + void setParentRow(TreeRow *parent) { m_parent = parent; } + std::optional &childRows() { return m_children; } +//! [tree_protocol_3] +//! [tree_protocol_4] + // Helper to assembly a tree of rows, not used by QGenericItemModel + template + TreeRow &addChild(Args &&...args) + { + if (!m_children) + m_children.emplace(Tree{}); + auto &child = m_children->emplace_back(std::forward(args)...); + child.m_parent = this; + return child; + } +}; +//! [tree_protocol_4] + +void tree_protocol() +{ +//! [tree_protocol_5] +Tree tree = { + {"..."}, + {"..."}, + {"..."}, +}; + +// each toplevel row has three children +tree[0].addChild("..."); +tree[0].addChild("..."); +tree[0].addChild("..."); + +tree[1].addChild("..."); +tree[1].addChild("..."); +tree[1].addChild("..."); + +tree[2].addChild("..."); +tree[2].addChild("..."); +tree[2].addChild("..."); +//! [tree_protocol_5] + +{ +//! [tree_protocol_6] +// instantiate the model with a pointer to the tree, not a copy! +QGenericItemModel model(&tree); +QTreeView view; +view.setModel(&model); +//! [tree_protocol_6] +} +} + +//! [explicit_tree_protocol_0] +struct TreeTraversal +{ + TreeRow newRow() const { return TreeRow{}; } + const TreeRow *parentRow(const TreeRow &row) const { return row.m_parent; } + void setParentRow(TreeRow &row, TreeRow *parent) const { row.m_parent = parent; } + const std::optional &childRows(const TreeRow &row) const { return row.m_children; } + std::optional &childRows(TreeRow &row) const { return row.m_children; } +}; +//! [explicit_tree_protocol_0] +void explicit_tree_protocol() +{ +Tree tree; +//! [explicit_tree_protocol_1] +QGenericItemModel model(&tree, TreeTraversal{}); +//! [explicit_tree_protocol_1] +} +} // namespace tree_protocol + +namespace tree_of_pointers +{ +//! [tree_of_pointers_0] +struct TreeRow; +using Tree = std::vector; +//! [tree_of_pointers_0] + +//! [tree_of_pointers_1] +struct TreeRow +{ + Q_GADGET +public: + TreeRow(const QString &value = {}) + : m_value(value) + {} + ~TreeRow() + { + if (m_children) + qDeleteAll(*m_children); + } + + // move-only + TreeRow(TreeRow &&) = default; + TreeRow &operator=(TreeRow &&) = default; + + // helper to populate + template + TreeRow *addChild(Args &&...args) + { + if (!m_children) + m_children.emplace(Tree{}); + auto *child = m_children->emplace_back(new TreeRow(std::forward(args)...)); + child->m_parent = this; + return child; + } + +private: + friend struct TreeTraversal; + QString m_value; + std::optional m_children; + TreeRow *m_parent = nullptr; +}; +//! [tree_of_pointers_1] + +Tree make_tree_of_pointers() +{ +//! [tree_of_pointers_2] +Tree tree = { + new TreeRow("1"), + new TreeRow("2"), + new TreeRow("3"), + new TreeRow("4"), +}; +tree[0]->addChild("1.1"); +tree[1]->addChild("2.1"); +tree[2]->addChild("3.1")->addChild("3.1.1"); +tree[3]->addChild("4.1"); +//! [tree_of_pointers_2] +return tree; +} + +//! [tree_of_pointers_3] +struct TreeTraversal +{ + TreeRow *newRow() const { return new TreeRow; } + void deleteRow(TreeRow *row) { delete row; } + + const TreeRow *parentRow(const TreeRow *row) const { return row->m_parent; } + void setParentRow(TreeRow *row, TreeRow *parent) { row->m_parent = parent; } + const std::optional &childRows(const TreeRow *row) const { return row->m_children; } + std::optional &childRows(TreeRow *row) { return row->m_children; } +}; +//! [tree_of_pointers_3] + +//! [tree_of_pointers_4] +int main(int argc, char **argv) +{ + QApplication app(argc, argv); + + Tree tree = make_tree_of_pointers(); + + QGenericItemModel model(std::move(tree), TreeTraversal{}); + QTreeView treeView; + treeView.setModel(&model); + treeView.show(); + + return app.exec(); +} +//! [tree_of_pointers_4] + +} // namespace tree_of_pointers + void color_map() { //! [color_map] diff --git a/src/corelib/itemmodels/qgenericitemmodel.cpp b/src/corelib/itemmodels/qgenericitemmodel.cpp index 6419958199e..109d192e12e 100644 --- a/src/corelib/itemmodels/qgenericitemmodel.cpp +++ b/src/corelib/itemmodels/qgenericitemmodel.cpp @@ -64,11 +64,11 @@ QT_BEGIN_NAMESPACE 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 + \section1 List, Table, or Tree 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. + on the type of these rows, QGenericItemModel exposes the range as a list, + a table, or a tree. 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. @@ -109,6 +109,147 @@ QT_BEGIN_NAMESPACE implementing the tuple protocol for compile-time generation of the access code. + \section2 Trees of data + + QGenericItemModel can represent a data structure as a tree model. Such a + tree data structure needs to be homomorphic: on all levels of the tree, the + list of child rows needs to use the exact same representation as the tree + itself. In addition, the row type needs be of a static size: either a gadget + or QObject type, or a type that implements the {C++ tuple protocol}. + + To represent such data as a tree, the row type has to implement a traversal + protocol that allows QGenericItemModel to navigate up and down the tree. + For any given row, the model needs to be able to retrieve the parent row, + and the span of children for any given row. + + \snippet qgenericitemmodel/main.cpp tree_protocol_0 + + The tree itself is a vector of \c{TreeRow} values. See \l{Rows as pointers + or values} for the considerations on whether to use values or pointers of + items for the rows. + + \snippet qgenericitemmodel/main.cpp tree_protocol_1 + + The row class can be of any fixed-size type described above: a type that + implements the tuple protocol, a gadget, or a QObject. In this example, we + use a gadget. + + Each row item needs to maintain a pointer to the parent row, as well as an + optional range of child rows that is identical to the structure used for the + tree. + + Making the row type default constructible is optional, and allows the model + to construct new row data elements, for instance in the insertRow() or + moveRows() implementations. + + \snippet qgenericitemmodel/main.cpp tree_protocol_2 + + The tree traversal protocol can then be implemented as member functions of + the row data type. A const \c{parentRow()} function has to return a pointer + to a const row item; and the \c{childRows()} function has to return a + reference to a const \c{std::optional} that can hold the optional child + range. + + These two functions are sufficient for the model to navigate the tree as a + read-only data structure. To allow the user to edit data in a view, and the + model to implement mutating model APIs such as insertRows(), removeRows(), + and moveRows(), we have to implement additional functions for write-access: + + \snippet qgenericitemmodel/main.cpp tree_protocol_3 + + The model calls the \c{setParentRow()} function and mutable \c{childRows()} + overload to move or insert rows into an existing tree branch, and to update + the parent pointer should the old value have become invalid. The non-const + overload of \c{childRows()} provides in addition write-access to the row + data. + + \note The model performs setting the parent of a row, removing that row + from the old parent, and adding it to the list of the new parent's children, + as separate steps. This keeps the protocol interface small. + + \dots + \snippet qgenericitemmodel/main.cpp tree_protocol_4 + + The rest of the class implementation is not relevant for the model, but + a \c{addChild()} helper provides us with a convenient way to construct the + initial state of the tree. + + \snippet qgenericitemmodel/main.cpp tree_protocol_5 + + A QGenericItemModel instantiated with an instance of such a range will + represent the data as a tree. + + \snippet qgenericitemmodel/main.cpp tree_protocol_6 + + \section3 Tree traversal protocol in a separate class + + The tree traversal protocol can also be implemented in a separate class. + + \snippet qgenericitemmodel/main.cpp explicit_tree_protocol_0 + + Pass an instance of this protocol implementation to the QGenericItemModel + constructor: + + \snippet qgenericitemmodel/main.cpp explicit_tree_protocol_1 + + \section2 Rows as pointers or values + + The row type of the data range can be either a value, or a pointer. In + the code above we have been using the tree rows as values in a vector, + which avoids that we have to deal with explicit memory management. However, + a vector as a contiguous block of memory invalidates all iterators and + references when it has to reallocate the storage, or when inserting or + removing elements. This impacts the pointer to the parent item, which is + the location of the parent row within the vector. Making sure that this + parent (and QPersistentModelIndex instances referring to items within it) + stays valid can incurr substantial performance overhead. The + QGenericItemModel implementation has to assume that all references into the + range become invalid when modifying the range. + + Alternatively, we can also use a range of row pointers as the tree type: + + \snippet qgenericitemmodel/main.cpp tree_of_pointers_0 + + In this case, we have to allocate all TreeRow instances explicitly using + operator \c{new}, and implement the destructor to \c{delete} all items in + the vector of children. + + \snippet qgenericitemmodel/main.cpp tree_of_pointers_1 + \snippet qgenericitemmodel/main.cpp tree_of_pointers_2 + + Before we can construct a model that represents this data as a tree, we need + to also implement the tree traversal protocol. + + \snippet qgenericitemmodel/main.cpp tree_of_pointers_3 + + An explicit protocol implementation for mutable trees of pointers has to + provide two additional member functions, \c{newRow()} and + \c{deleteRow(RowType *)}. + + \snippet qgenericitemmodel/main.cpp tree_of_pointers_4 + + The model will call those functions when creating new rows in insertRows(), + and when removing rows in removeRows(). In addition, if the model has + ownership of the data, then it will also delete all top-level rows upon + destruction. Note how in this example, we move the tree into the model, so + we must no longer perform any operations on it. QGenericItemModel, when + constructed by moving tree-data with row-pointers into it, will take + ownership of the data, and delete the row pointers in it's destructor. + + \note This is not the case for tables and lists that use pointers as their + row type. QGenericItemModel will never allocate new rows in lists and tables + using operator new, and will never free any rows. + + So, using pointers at rows comes with some memory allocation and management + overhead. However, when using rows through pointers the references to the + row items remain stable, even when they are moved around in the range, + or when the range reallocates. This can significantly reduce the cost + of making modifications to the model's structure when using insertRows(), + removeRows(), or moveRows(). + + So, each choice has different performance and memory overhead trade-offs. + The best option depends on the exact use case and data structure used. + \section2 Multi-role items The type of the items that the implementations of data(), setData(), @@ -246,12 +387,15 @@ QT_BEGIN_NAMESPACE */ /*! - \fn template > QGenericItemModel::QGenericItemModel(Range &&range, QObject *parent) + \fn template > QGenericItemModel::QGenericItemModel(Range &&range, QObject *parent) + \fn template > QGenericItemModel::QGenericItemModel(Range &&range, QObject *parent) + \fn template > QGenericItemModel::QGenericItemModel(Range &&range, Protocol &&protocol, 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. + 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. If \a protocol is provided, then the model + will represent the range as a tree using the protocol implementation. + 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 @@ -293,7 +437,9 @@ QModelIndex QGenericItemModel::index(int row, int column, const QModelIndex &par 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. + list and table ranges. For models operation on a tree, this function + returns the index for the row item returned by the parent() implementation + of the tree traversal protocol. \sa index(), hasChildren() */ @@ -309,7 +455,9 @@ QModelIndex QGenericItemModel::parent(const QModelIndex &child) const 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. + models that operate on list and table ranges. For trees, this returns the + size of the range returned by the childRows() implementation of the tree + traversal protocol. \sa columnCount(), insertRows(), hasChildren() */ @@ -491,7 +639,8 @@ bool QGenericItemModel::clearItemData(const QModelIndex &index) 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. + implementation does nothing and returns \c{false} immediately. This is + always the case for tree models. //! [column-change-requirement] */ diff --git a/src/corelib/itemmodels/qgenericitemmodel.h b/src/corelib/itemmodels/qgenericitemmodel.h index 30da0e84e49..f5b97dce789 100644 --- a/src/corelib/itemmodels/qgenericitemmodel.h +++ b/src/corelib/itemmodels/qgenericitemmodel.h @@ -43,9 +43,17 @@ public: }; template = true> + QGenericItemModelDetails::if_is_table_range = true> explicit QGenericItemModel(Range &&range, QObject *parent = nullptr); + template = true> + explicit QGenericItemModel(Range &&range, QObject *parent = nullptr); + + template = true> + explicit QGenericItemModel(Range &&range, Protocol &&protocol, QObject *parent = nullptr); + ~QGenericItemModel() override; QModelIndex index(int row, int column, const QModelIndex &parent = {}) const override; @@ -77,6 +85,11 @@ QModelIndex QGenericItemModelImplBase::createIndex(int row, int column, const vo { return m_itemModel->createIndex(row, column, ptr); } +void QGenericItemModelImplBase::changePersistentIndexList(const QModelIndexList &from, + const QModelIndexList &to) +{ + m_itemModel->changePersistentIndexList(from, to); +} QHash QGenericItemModelImplBase::roleNames() const { return m_itemModel->roleNames(); @@ -157,7 +170,8 @@ protected: static constexpr bool isMutable() { return range_features::is_mutable && row_features::is_mutable - && std::is_reference_v; + && std::is_reference_v + && Structure::is_mutable_impl; } static constexpr int static_row_count = QGenericItemModelDetails::static_size_v; @@ -712,6 +726,9 @@ public: } else { row_type empty_value = that().makeEmptyRow(parent); children->insert(pos, count, empty_value); + // fix the parent in all children of the modified row, as the + // references back to the parent might have become invalid. + that().resetParentInChildren(children); } endInsertRows(); @@ -745,9 +762,16 @@ public: } } { // erase invalidates iterators - const auto start = std::next(std::begin(*children), row); - children->erase(start, std::next(start, count)); + const auto begin = std::next(std::begin(*children), row); + const auto end = std::next(begin, count); + if constexpr (rows_are_pointers) + that().deleteRemovedRows(begin, end); + children->erase(begin, end); } + // fix the parent in all children of the modified row, as the + // references back to the parent might have become invalid. + if constexpr (!rows_are_pointers) + that().resetParentInChildren(children); if constexpr (dynamicColumns()) { if (callEndRemoveColumns) { Q_ASSERT(that().columnCount(parent) == 0); @@ -762,6 +786,17 @@ public: } protected: + ~QGenericItemModelImpl() + { + if constexpr (rows_are_pointers && !std::is_pointer_v) { + // If data with rows as pointers was moved in, then we own it and + // have to delete those rows. + using ref = decltype(std::forward(std::declval())); + if constexpr (std::is_rvalue_reference_v) + that().destroyOwnedModel(*m_data.model()); + } + } + template static QVariant read(const Value &value) { @@ -955,28 +990,255 @@ protected: ModelData m_data; }; +// Implementations that depends on the model structure (flat vs tree) that will +// be specialized based on a protocol type. The main template implements tree +// support through a protocol type. template -class QGenericItemModelStructureImpl : public QGenericItemModelImpl< - QGenericItemModelStructureImpl, - Range> -{}; +class QGenericTreeItemModelImpl + : public QGenericItemModelImpl, Range> +{ + using Base = QGenericItemModelImpl, Range>; + friend class QGenericItemModelImpl, Range>; + + std::remove_reference_t m_protocol; + + using range_type = typename Base::range_type; + using range_features = typename Base::range_features; + using row_type = typename Base::row_type; + using row_ptr = typename Base::row_ptr; + using const_row_ptr = typename Base::const_row_ptr; + + using tree_traits = QGenericItemModelDetails::tree_traits; + static constexpr bool is_mutable_impl = tree_traits::has_mutable_childRows; + + + static_assert(!Base::dynamicColumns(), "A tree must have a static number of columns!"); + +public: + QGenericTreeItemModelImpl(Range &&model, Protocol &&p, QGenericItemModel *itemModel) + : Base(std::forward(model), itemModel) + , m_protocol(std::forward(p)) + {}; + +protected: + QModelIndex indexImpl(int row, int column, const QModelIndex &parent) const + { + if (!parent.isValid()) + return this->createIndex(row, column); + // only items at column 0 can have children + if (parent.column()) + return QModelIndex(); + + const_row_ptr grandParent = static_cast(parent.constInternalPointer()); + const auto &parentSiblings = childrenOf(grandParent); + const auto it = std::next(std::cbegin(parentSiblings), parent.row()); + return this->createIndex(row, column, QGenericItemModelDetails::pointerTo(*it)); + } + + QModelIndex parent(const QModelIndex &child) const + { + if (!child.isValid()) + return child; + + // no pointer to parent row - no parent + const_row_ptr parentRow = static_cast(child.constInternalPointer()); + if (!parentRow) + return {}; + + // get the siblings of the parent via the grand parent + const_row_ptr grandParent; + if constexpr (Base::rows_are_pointers) + grandParent = m_protocol.parentRow(parentRow); + else + grandParent = m_protocol.parentRow(*parentRow); + const range_type &parentSiblings = childrenOf(grandParent); + // find the index of parentRow + const auto begin = std::cbegin(parentSiblings); + const auto end = std::cend(parentSiblings); + const auto it = std::find_if(begin, end, [parentRow](auto &&s){ + return QGenericItemModelDetails::pointerTo(s) == parentRow; + }); + if (it != end) + return this->createIndex(std::distance(begin, it), 0, grandParent); + return {}; + } + + int rowCount(const QModelIndex &parent) const + { + const auto *children = this->childRange(parent); + return int(children ? Base::size(*children) : 0); + } + + int columnCount(const QModelIndex &) const + { + // all levels of a tree have to have the same, static, column count + return Base::static_column_count; + } + + static constexpr Qt::ItemFlags defaultFlags() + { + return Qt::ItemIsEnabled | Qt::ItemIsSelectable; + } + + static constexpr bool canInsertRows() + { + // We must not insert rows if we cannot adjust the parents of the + // children of the following rows. We don't have to do that if the + // range operates on pointers. + return (Base::rows_are_pointers || tree_traits::has_setParentRow) + && Base::dynamicRows() && range_features::has_insert; + } + + static constexpr bool canRemoveRows() + { + // We must not remove rows if we cannot adjust the parents of the + // children of the following rows. We don't have to do that if the + // range operates on pointers. + return (Base::rows_are_pointers || tree_traits::has_setParentRow) + && Base::dynamicRows() && range_features::has_erase; + } + + auto makeEmptyRow(const QModelIndex &parent) + { + // tree traversal protocol: if we are here, then it must be possible + // to change the parent of a row. + static_assert(tree_traits::has_setParentRow); + row_type empty_row = m_protocol.newRow(); + if (parent.isValid()) { + m_protocol.setParentRow(empty_row, + QGenericItemModelDetails::pointerTo(this->rowData(parent))); + } + return empty_row; + } + + template + void destroyOwnedModel(R &&range) + { + const auto begin = std::begin(range); + const auto end = std::end(range); + deleteRemovedRows(begin, end); + } + + template + void deleteRemovedRows(It &&begin, Sentinel &&end) + { + for (auto it = begin; it != end; ++it) + m_protocol.deleteRow(*it); + } + + void resetParentInChildren(range_type *children) + { + if constexpr (tree_traits::has_setParentRow) { + const auto begin = std::begin(*children); + const auto end = std::end(*children); + for (auto it = begin; it != end; ++it) { + if (auto &maybeChildren = m_protocol.childRows(*it)) { + QModelIndexList fromIndexes; + QModelIndexList toIndexes; + fromIndexes.reserve(Base::size(*maybeChildren)); + toIndexes.reserve(Base::size(*maybeChildren)); + auto *parentRow = QGenericItemModelDetails::pointerTo(*it); + + int row = 0; + for (auto &child : *maybeChildren) { + const_row_ptr oldParent = m_protocol.parentRow(child); + if (oldParent != parentRow) { + fromIndexes.append(this->createIndex(row, 0, oldParent)); + toIndexes.append(this->createIndex(row, 0, parentRow)); + m_protocol.setParentRow(child, parentRow); + } + ++row; + } + this->changePersistentIndexList(fromIndexes, toIndexes); + resetParentInChildren(QGenericItemModelDetails::pointerTo(*maybeChildren)); + } + } + } + } + + decltype(auto) rowDataImpl(const QModelIndex &index) const + { + const_row_ptr parentRow = static_cast(index.constInternalPointer()); + const range_type &siblings = childrenOf(parentRow); + Q_ASSERT(index.row() < int(Base::size(siblings))); + return *(std::next(std::cbegin(siblings), index.row())); + } + + decltype(auto) rowDataImpl(const QModelIndex &index) + { + row_ptr parentRow = static_cast(index.internalPointer()); + range_type &siblings = childrenOf(parentRow); + Q_ASSERT(index.row() < int(Base::size(siblings))); + return *(std::next(std::begin(siblings), index.row())); + } + + auto childRangeImpl(const QModelIndex &index) const + { + const auto &row = this->rowData(index); + if constexpr (Base::rows_are_pointers) { + if (!row) + return static_cast(nullptr); + } + const auto &children = m_protocol.childRows(row); + return children ? std::addressof(*children) : nullptr; + } + + auto childRangeImpl(const QModelIndex &index) + { + auto &row = this->rowData(index); + if constexpr (Base::rows_are_pointers) { + if (!row) + return static_cast(nullptr); + } + + auto &children = m_protocol.childRows(row); + if (!children) + children.emplace(typename Base::range_type{}); + return std::addressof(*children); + } + +private: + const range_type &childrenOf(const_row_ptr row) const + { + if (!row) + return *this->m_data.model(); + if constexpr (Base::rows_are_pointers) + return *m_protocol.childRows(row); + else + return *m_protocol.childRows(*row); + } + + range_type &childrenOf(row_ptr row) + { + if (!row) + return *this->m_data.model(); + if constexpr (Base::rows_are_pointers) + return *m_protocol.childRows(row); + else + return *m_protocol.childRows(*row); + } +}; // specialization for flat models without protocol template -class QGenericItemModelStructureImpl : public QGenericItemModelImpl< - QGenericItemModelStructureImpl, - Range> +class QGenericTableItemModelImpl + : public QGenericItemModelImpl, Range> { - using Base = QGenericItemModelImpl, Range>; - friend class QGenericItemModelImpl, Range>; + using Base = QGenericItemModelImpl, Range>; + friend class QGenericItemModelImpl, Range>; + using range_type = typename Base::range_type; 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; + static constexpr bool is_mutable_impl = true; + public: - explicit QGenericItemModelStructureImpl(Range &&model, QGenericItemModel *itemModel) + explicit QGenericTableItemModelImpl(Range &&model, QGenericItemModel *itemModel) : Base(std::forward(model), itemModel) {} @@ -1051,6 +1313,21 @@ protected: } } + template + void destroyOwnedModel(R &&range) + { + const auto begin = std::begin(range); + const auto end = std::end(range); + for (auto it = begin; it != end; ++it) + delete *it; + } + + template + void deleteRemovedRows(It &&, Sentinel &&) + { + // nothing to do for tables and lists as we never create rows either + } + decltype(auto) rowDataImpl(const QModelIndex &index) const { Q_ASSERT(index.row() < int(Base::size(*this->m_data.model()))); @@ -1072,13 +1349,34 @@ protected: { return nullptr; } + + void resetParentInChildren(range_type *) + { + } }; template > + QGenericItemModelDetails::if_is_table_range> QGenericItemModel::QGenericItemModel(Range &&range, QObject *parent) : QAbstractItemModel(parent) - , impl(new QGenericItemModelStructureImpl(std::forward(range), this)) + , impl(new QGenericTableItemModelImpl(std::forward(range), this)) +{} + +template > +QGenericItemModel::QGenericItemModel(Range &&range, QObject *parent) + : QGenericItemModel(std::forward(range), + QGenericItemModelDetails::tree_protocol_t{}, parent) +{} + +template > +QGenericItemModel::QGenericItemModel(Range &&range, Protocol &&protocol, QObject *parent) + : QAbstractItemModel(parent) + , impl(new QGenericTreeItemModelImpl + >(std::forward(range), + std::forward(protocol), this)) {} QT_END_NAMESPACE diff --git a/src/corelib/itemmodels/qgenericitemmodel_impl.h b/src/corelib/itemmodels/qgenericitemmodel_impl.h index 8f986a2a702..47e84bade5f 100644 --- a/src/corelib/itemmodels/qgenericitemmodel_impl.h +++ b/src/corelib/itemmodels/qgenericitemmodel_impl.h @@ -169,8 +169,6 @@ namespace QGenericItemModelDetails template [[maybe_unused]] static constexpr bool is_range_v = range_traits(); - template - using if_is_range = std::enable_if_t>, bool>; // Find out how many fixed elements can be retrieved from a row element. // main template for simple values and ranges. Specializing for ranges @@ -224,6 +222,156 @@ namespace QGenericItemModelDetails [[maybe_unused]] static constexpr int static_size_v = row_traits>>::static_size; + // tests for tree protocol implementation in the row type + template + struct test_parentRow : std::false_type {}; + + template + struct test_parentRow().parentRow())>> + : std::true_type + {}; + + template + struct test_childRows : std::false_type {}; + template + struct test_childRows().childRows())>> + : std::true_type + {}; + + // Default tree traversal protocol implementation for row types that have + // the respective member functions. The trailing return type implicitly + // removes those functions that are not available. + template + struct DefaultTreeProtocol + { + using row_ptr = std::remove_pointer_t *; + + template + auto newRow() const -> std::enable_if_t, row_ptr> + { + return new std::remove_pointer_t{}; + } + template + auto newRow(...) const -> decltype(R{}) + { + return R{}; + } + + template + auto deleteRow(R& row) -> decltype(delete row) + { + delete row; + } + + template + auto parentRow(const R &row) const -> decltype(row.parentRow()) + { + return row.parentRow(); + } + template + auto parentRow(const R &row) const -> decltype(row->parentRow()) + { + return row->parentRow(); + } + + template + auto childRows(const R &row) const -> decltype(row.childRows()) + { + return row.childRows(); + } + template + auto childRows(const R &row) const -> decltype(row->childRows()) + { + return row->childRows(); + } + + template + auto setParentRow(R &row, row_ptr parent) -> decltype(row.setParentRow(parent)) + { + row.setParentRow(parent); + } + template + auto setParentRow(R &row, row_ptr parent) -> decltype(row->setParentRow(parent)) + { + row->setParentRow(parent); + } + + template + auto childRows(R &row) -> decltype(row.childRows()) + { + return row.childRows(); + } + template + auto childRows(R &row) -> decltype(row->childRows()) + { + return row->childRows(); + } + }; + + // the protocol must implement getters for parent/children, but setters are + // optional, so test for those. + template + struct protocol_setParentRow : std::false_type {}; + template + struct protocol_setParentRow(). + setParentRow(std::declval(), nullptr))>> + : std::true_type {}; + template + struct protocol_mutable_childRows : std::false_type {}; + template + struct protocol_mutable_childRows(). + childRows(std::declval()) = {})>> + : std::true_type {}; + + // Selected for row-types R that don't have parent/children member + // functions. If the TreeProtocol is explicitly set (to not be void *), + // then we have a tree. + template + struct tree_traits : std::integral_constant> + { + using tree_protocol = TreeProtocol; + + static constexpr bool has_setParentRow = protocol_setParentRow::value; + static constexpr bool has_mutable_childRows = + protocol_mutable_childRows::value; + }; + + // Selected for row-types that do have const parent/children member functions. + // If the TreeProtocol is explicitly set (not to be void *), then we use it + // (ignoring, for now, whether it correctly implements the protocol functions). + // Otherwise we use the default tree protocol implementation that calls member + // functions of the row type. + template + struct tree_traits, + test_childRows> + > + > : std::true_type + { + using tree_protocol = std::conditional_t, + DefaultTreeProtocol, TreeProtocol>; + static constexpr bool has_setParentRow = protocol_setParentRow::value; + static constexpr bool has_mutable_childRows = + protocol_mutable_childRows::value; + }; + + template > + using tree_protocol_t = typename tree_traits< + range_type, + std::remove_reference_t()))>, + TreeProtocol>::tree_protocol; + + template > + using if_is_table_range = std::enable_if_t< + is_range_v && std::is_void_v>, + bool>; + + template > + using if_is_tree_range = std::enable_if_t< + is_range_v && !std::is_void_v>, + bool>; + // 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 @@ -355,6 +503,7 @@ protected: QGenericItemModel *m_itemModel; inline QModelIndex createIndex(int row, int column, const void *ptr = nullptr) const; + inline void changePersistentIndexList(const QModelIndexList &from, const QModelIndexList &to); inline QHash roleNames() const; inline void dataChanged(const QModelIndex &from, const QModelIndex &to, const QList &roles); diff --git a/tests/auto/corelib/itemmodels/qgenericitemmodel/BLACKLIST b/tests/auto/corelib/itemmodels/qgenericitemmodel/BLACKLIST new file mode 100644 index 00000000000..9c6f8b06de8 --- /dev/null +++ b/tests/auto/corelib/itemmodels/qgenericitemmodel/BLACKLIST @@ -0,0 +1,23 @@ +# QTBUG-135419 +[setData:value tree] +vxworks +[setData:pointer tree] +vxworks +[clearItemData:value tree] +vxworks +[clearItemData:pointer tree] +vxworks +[insertRows:value tree] +vxworks +[insertRows:pointer tree] +vxworks +[removeRows:value tree] +vxworks +[removeRows:pointer tree] +vxworks +[treeModifyBranch] +vxworks +[treeCreateBranch] +vxworks +[treeRemoveBranch] +vxworks diff --git a/tests/auto/corelib/itemmodels/qgenericitemmodel/tst_qgenericitemmodel.cpp b/tests/auto/corelib/itemmodels/qgenericitemmodel/tst_qgenericitemmodel.cpp index 10d1fb34ab6..ddb2b204b30 100644 --- a/tests/auto/corelib/itemmodels/qgenericitemmodel/tst_qgenericitemmodel.cpp +++ b/tests/auto/corelib/itemmodels/qgenericitemmodel/tst_qgenericitemmodel.cpp @@ -144,6 +144,140 @@ namespace std { template <> struct tuple_element<0, ConstRow> { using type = QString; }; } +struct tree_row; +using value_tree = QList; +using pointer_tree = QList; + +struct tree_row +{ +public: + tree_row(const QString &value = {}, const QString &description = {}) + : m_value(value), m_description(description) + {} + + ~tree_row() + { + if (m_childrenPointers) + qDeleteAll(*m_childrenPointers); + } + + tree_row(const tree_row &other) + : m_value(other.m_value), m_description(other.m_description) + , m_parent(other.m_parent), m_children(other.m_children) + , m_childrenPointers(other.m_childrenPointers) + {} + + tree_row &operator=(const tree_row &other) + { + m_parent = other.m_parent; + m_children = other.m_children; + m_childrenPointers = other.m_childrenPointers; + m_value = other.m_value; + m_description = other.m_description; + return *this; + } + + tree_row(tree_row &&other) = default; + tree_row &operator=(tree_row &&other) = default; + + QString &value() { return m_value; } + const QString &value() const { return m_value; } + QString &description() { return m_description; } + const QString &description() const { return m_description; } + + template + tree_row &addChild(Args&& ...args) + { + if (!m_children) + m_children.emplace(value_tree{}); + tree_row &res = m_children->emplace_back(args...); + res.m_parent = this; + return res; + } + + template + tree_row *addChildPointer(Args&& ...args) + { + if (!m_childrenPointers) + m_childrenPointers.emplace(pointer_tree{}); + auto *res = new tree_row(args...); + m_childrenPointers->push_back(res); + res->m_parent = this; + return res; + } + + const tree_row *parentRow() const { return m_parent; } + void setParentRow(tree_row *parent) { m_parent = parent; } + const std::optional &childRows() const { return m_children; } + std::optional &childRows() { return m_children; } + + static void prettyPrint(QDebug dbg, const value_tree &tree, int depth = 0) + { + dbg.nospace().noquote(); + const QString indent(depth * 2, ' '); + bool first = true; + for (const auto &row : tree) { + dbg << indent; + if (first && depth) { + dbg << "\\"; + first = false; + } else { + dbg << "|"; + } + dbg << row << "\n"; + if (const auto &children = row.childRows()) + prettyPrint(dbg, *children, depth + 1); + } + } + + struct ProtocolPointerImpl { + tree_row *newRow() const { return new tree_row; } + void deleteRow(tree_row *row) { delete row; } + const tree_row *parentRow(const tree_row *row) const { return row->m_parent; } + void setParentRow(tree_row *row, tree_row *parent) { row->m_parent = parent; } + + const std::optional &childRows(const tree_row *row) const + { return row->m_childrenPointers; } + std::optional &childRows(tree_row *row) + { return row->m_childrenPointers; } + }; + +private: + QString m_value; + QString m_description; + + tree_row *m_parent = nullptr; + std::optional m_children = std::nullopt; + std::optional m_childrenPointers = std::nullopt; + + friend inline QDebug operator<<(QDebug dbg, const tree_row &row) + { + QDebugStateSaver saver(dbg); + dbg.nospace() << row.m_value << " : " << row.m_description; + if (row.parentRow()) + dbg << " ^ " << row.parentRow()->value(); + if (row.childRows()) + dbg << " v " << row.childRows()->size(); + return dbg; + } + + template, tree_row>, bool> = true> + friend inline decltype(auto) get(Row &&row) + { + if constexpr (I == 0) + return row.value(); + else if constexpr (I == 1) + return row.description(); + } +}; + +namespace std { + template <> struct tuple_size : std::integral_constant {}; + template struct tuple_element + { using type = decltype(get(std::declval())); }; +} + class tst_QGenericItemModel : public QObject { Q_OBJECT @@ -154,6 +288,7 @@ private slots: void minimalIterator(); void ranges(); void json(); + void ownership(); void dimensions_data() { createTestData(); } void dimensions(); @@ -180,8 +315,114 @@ private slots: void inconsistentColumnCount(); + void tree_data(); + void tree(); + void treeModifyBranch_data() { tree_data(); } + void treeModifyBranch(); + void treeCreateBranch_data() { tree_data(); } + void treeCreateBranch(); + void treeRemoveBranch_data() { tree_data(); } + void treeRemoveBranch(); + private: - void createTestData(); + enum TestedModels { + Lists = 0x01, + Tables = 0x02, + Trees = 0x04, + All = Lists|Tables|Trees, + }; + void createTestData(TestedModels tested = All); + void createTree(); + + QList allIndexes(QAbstractItemModel *model, const QModelIndex &parent = {}) + { + QList pmiList; + for (int row = 0; row < model->rowCount(parent); ++row) { + const QModelIndex mi = model->index(row, 0, parent); + pmiList += mi; + if (model->hasChildren(mi)) + pmiList += allIndexes(model, mi); + } + return pmiList; + } + + void verifyPmiList(const QList &pmiList) + { + for (const auto &pmi : pmiList) { + auto debug = qScopeGuard([&pmi]{ + qCritical() << "Failing index" << pmi << pmi.isValid(); + }); + QVERIFY(pmi.isValid()); + QVERIFY(pmi.data().isValid()); + QCOMPARE(pmi.parent().isValid(), pmi.parent().data().isValid()); + debug.dismiss(); + } + } + + template + static bool integrityCheck(const Tree &tree, int depth = 0) + { + static constexpr bool pointerTree = std::is_pointer_v::value_type>; + bool result = true; + for (const auto &row : tree) { + if constexpr (pointerTree) { + if (!row) { + qCritical() << "Unexpected null pointer in tree!"; + return false; + } + } + const auto &children = [&row]() -> const auto &{ + if constexpr (pointerTree) { + const auto protocol = tree_row::ProtocolPointerImpl{}; + return protocol.childRows(row); + } else { + return row.childRows(); + } + }(); + if (children) { + for (const auto &child : *children) { + const bool match = [&child, &row]{ + if constexpr (pointerTree) { + if (child->parentRow() != row) { + qCritical().noquote() << "Parent out of sync for:" << *child; + qCritical().noquote() << " Actual: " << child->parentRow() + << (child->parentRow() ? *child->parentRow() : tree_row{}); + qCritical().noquote() << "Expected: " << row << *row; + return false; + } + } else { + if (child.parentRow() != std::addressof(row)) { + qCritical().noquote() << "Parent out of sync for:" << child; + qCritical().noquote() << " Actual: " << child.parentRow() + << (child.parentRow() ? *child.parentRow() : tree_row{}); + qCritical().noquote() << "Expected: " << std::addressof(row) << row; + return false; + } + } + return true; + }(); + if (!match) + return false; + } + result &= integrityCheck(*children, depth + 1); + } + } + return result; + } + bool treeIntegrityCheck() + { + if (!integrityCheck(*m_data->m_tree)) { + tree_row::prettyPrint(qDebug().nospace() << "\nTree of Values:\n", *m_data->m_tree); + return false; + } + if (!integrityCheck(*m_data->m_pointer_tree)) { + tree_row::prettyPrint(qDebug().nospace() << "\nTree of Pointers:\n", *m_data->m_tree); + return false; + } + return true; + } + + std::unique_ptr makeTreeModel(); struct Data { @@ -320,6 +561,17 @@ private: {{{Qt::DisplayRole, "DISPLAY2"}, {Qt::DecorationRole, "DECORATION2"}}}, {{{Qt::DisplayRole, "DISPLAY3"}, {Qt::DecorationRole, "DECORATION3"}}}, }; + + std::unique_ptr m_tree; + struct TreeDeleter { + void operator()(pointer_tree *tree) + { + for (auto *row : *tree) + delete row; + delete tree; + } + }; + std::unique_ptr m_pointer_tree; }; std::unique_ptr m_data; @@ -357,10 +609,12 @@ void createBackup(QObject* object, T& model) { template , bool> = true> void createBackup(QObject* , T& ) {} -void tst_QGenericItemModel::createTestData() +void tst_QGenericItemModel::createTestData(TestedModels tested) { m_data.reset(new Data); + createTree(); + QTest::addColumn("factory"); QTest::addColumn("expectedRowCount"); QTest::addColumn("expectedColumnCount"); @@ -448,6 +702,33 @@ void tst_QGenericItemModel::createTestData() }; return std::unique_ptr(new QGenericItemModel(std::move(movedTable))); }) << 4 << 4 << ChangeActions(ChangeAction::All); + + // moved list of pointers -> model takes ownership + QTest::addRow("movedListOfObjects") << Factory([]{ + std::list movedListOfObjects = { + new Object, new Object, new Object, + new Object, new Object, new Object + }; + + return std::unique_ptr( + new QGenericItemModel(std::move(movedListOfObjects)) + ); + }) << 6 << 2 << (ChangeAction::ChangeRows | ChangeAction::SetData); + + // special case: tree + if (tested & Trees) { + QTest::addRow("value tree") << Factory([this]{ + return std::unique_ptr(new QGenericItemModel(m_data->m_tree.get())); + }) << int(std::size(*m_data->m_tree.get())) << int(std::tuple_size_v) + << (ChangeAction::ChangeRows | ChangeAction::SetData); + + QTest::addRow("pointer tree") << Factory([this]{ + return std::unique_ptr( + new QGenericItemModel(m_data->m_pointer_tree.get(), tree_row::ProtocolPointerImpl{}) + ); + }) << int(std::size(*m_data->m_pointer_tree.get())) << int(std::tuple_size_v) + << (ChangeAction::ChangeRows | ChangeAction::SetData); + } } void tst_QGenericItemModel::basics() @@ -532,6 +813,74 @@ void tst_QGenericItemModel::json() QCOMPARE(index.data().toString(), "two"); } +void tst_QGenericItemModel::ownership() +{ + { // a list of pointers to objects + Object *object = new Object; + QPointer guard = object; + std::vector objects { + object + }; + { // model does not take ownership + QGenericItemModel modelOnCopy(objects); + } + QVERIFY(guard); + { // model does not take ownership + QGenericItemModel modelOnRef(&objects); + } + QVERIFY(guard); + { // model does take ownership + QGenericItemModel movedIntoModel(std::move(objects)); + } + QVERIFY(!guard); + } + + { // a list of shared_ptr + Object *object = new Object; + QPointer guard = object; + std::vector> objects { + std::shared_ptr(object) + }; + { // model does not take ownership + QGenericItemModel modelOnCopy(objects); + QCOMPARE(modelOnCopy.rowCount(), 1); + QCOMPARE(objects[0].use_count(), 2); + } + QCOMPARE(objects[0].use_count(), 1); + { // model does not take ownership + QGenericItemModel modelOnRef(&objects); + QCOMPARE(objects[0].use_count(), 1); + } + QCOMPARE(objects[0].use_count(), 1); + QVERIFY(guard); + { // model owns the last shared copy + QGenericItemModel movedIntoModel(std::move(objects)); + } + QVERIFY(!guard); + } + + { // a table of pointers + Object *object = new Object; + QPointer guard = object; + std::vector> table { + {object} + }; + { // model does not take ownership + QGenericItemModel modelOnCopy(table); + } + QVERIFY(guard); + { // model does not take ownership + QGenericItemModel modelOnRef(&table); + } + QVERIFY(guard); + { // model does take ownership of rows, but not of objects within each row + QGenericItemModel movedIntoModel(std::move(table)); + } + QVERIFY(guard); + delete object; + } +} + void tst_QGenericItemModel::dimensions() { QFETCH(Factory, factory); @@ -696,6 +1045,8 @@ void tst_QGenericItemModel::insertRows() QFETCH(const ChangeActions, changeActions); const bool canSetData = changeActions.testFlag(ChangeAction::SetData); + const QList pmiList = allIndexes(model.get()); + QCOMPARE(model->rowCount(), expectedRowCount); QCOMPARE(model->insertRow(0), changeActions.testFlag(ChangeAction::InsertRows)); QCOMPARE(model->rowCount() == expectedRowCount + 1, @@ -727,6 +1078,7 @@ void tst_QGenericItemModel::insertRows() QEXPECT_FAIL("listOfObjectsCopy", "No object created", Continue); QEXPECT_FAIL("listOfMetaObjectTupleCopy", "No object created", Continue); QEXPECT_FAIL("tableOfMetaObjectTupleCopy", "No object created", Continue); + QEXPECT_FAIL("movedListOfObjects", "No object created", Continue); // associative containers are default constructed with no valid data ignoreFailureFromAssociativeContainers(); @@ -742,6 +1094,8 @@ void tst_QGenericItemModel::insertRows() changeActions.testFlag(ChangeAction::InsertRows)); QCOMPARE(model->rowCount() == expectedRowCount + 6, changeActions.testFlag(ChangeAction::InsertRows)); + + verifyPmiList(pmiList); } void tst_QGenericItemModel::removeRows() @@ -825,5 +1179,276 @@ void tst_QGenericItemModel::inconsistentColumnCount() } } +enum class TreeProtocol { ValueImplicit, ValueReadOnly, PointerExplicit, PointerExplicitMoved }; + +void tst_QGenericItemModel::createTree() +{ + m_data->m_tree.reset(new value_tree{ + {"1", "one"}, + {"2", "two"}, + {"3", "three"}, + {"4", "four"}, + {"5", "five"}, + }); + + (*m_data->m_tree)[1].addChild("2.1", "two.one"); + (*m_data->m_tree)[1].addChild("2.2", "two.two"); + + tree_row &row23 = (*m_data->m_tree)[1].addChild("2.3", "two.three"); + + row23.addChild("2.3.1", "two.three.one"); + row23.addChild("2.3.2", "two.three.two"); + row23.addChild("2.3.3", "two.three.three"); + + // assert the integrity of the tree; this is not a test. + Q_ASSERT(!m_data->m_tree->at(0).childRows()); + Q_ASSERT(m_data->m_tree->at(1).childRows()); + Q_ASSERT(!m_data->m_tree->at(1).childRows()->at(1).childRows()); + Q_ASSERT(m_data->m_tree->at(1).childRows()->at(2).childRows()); + + m_data->m_pointer_tree.reset(new pointer_tree{ + new tree_row("1", "one"), + new tree_row("2", "one"), + new tree_row("3", "one"), + new tree_row("4", "one"), + new tree_row("5", "one"), + }); + + m_data->m_pointer_tree->at(1)->addChildPointer("2.1", "two.one"); + m_data->m_pointer_tree->at(1)->addChildPointer("2.2", "two.two"); +} + +void tst_QGenericItemModel::tree_data() +{ + m_data.reset(new Data); + createTree(); + + QTest::addColumn("protocol"); + QTest::addColumn("expectedRootRowCount"); + QTest::addColumn("expectedColumnCount"); + QTest::addColumn>("rowsWithChildren"); + QTest::addColumn("changeActions"); + + const int expectedRootRowCount = int(m_data->m_tree->size()); + const int expectedColumnCount = int(std::tuple_size_v); + const auto rowsWithChildren = QList{1}; + + QTest::addRow("ValueImplicit") + << TreeProtocol::ValueImplicit + << expectedRootRowCount << expectedColumnCount << rowsWithChildren + << ChangeActions(ChangeAction::All); + QTest::addRow("ValueReadOnly") + << TreeProtocol::ValueReadOnly + << expectedRootRowCount << expectedColumnCount << rowsWithChildren + << ChangeActions(ChangeAction::ReadOnly); + QTest::addRow("PointerExplicit") + << TreeProtocol::PointerExplicit + << expectedRootRowCount << expectedColumnCount << rowsWithChildren + << ChangeActions(ChangeAction::All); + QTest::addRow("PointerExplicitMoved") + << TreeProtocol::PointerExplicitMoved + << expectedRootRowCount << expectedColumnCount << rowsWithChildren + << ChangeActions(ChangeAction::All); +} + +std::unique_ptr tst_QGenericItemModel::makeTreeModel() +{ + createTree(); + + std::unique_ptr model; + + QFETCH(const TreeProtocol, protocol); + switch (protocol) { + case TreeProtocol::ValueImplicit: + model.reset(new QGenericItemModel(m_data->m_tree.get())); + break; + case TreeProtocol::ValueReadOnly: { + struct { // minimal (read-only) implementation of the tree traversal protocol + const tree_row *parentRow(const tree_row &row) const { return row.parentRow(); } + const std::optional &childRows(const tree_row &row) const + { return row.childRows(); } + } readOnlyProtocol; + model.reset(new QGenericItemModel(m_data->m_tree.get(), readOnlyProtocol)); + break; + } + case TreeProtocol::PointerExplicit: + model.reset(new QGenericItemModel(m_data->m_pointer_tree.get(), + tree_row::ProtocolPointerImpl{})); + break; + case TreeProtocol::PointerExplicitMoved: { + pointer_tree moved_tree{ + new tree_row("m1", "m_one"), + new tree_row("m2", "m_two"), + new tree_row("m3", "m_three"), + new tree_row("m4", "m_four"), + new tree_row("m5", "m_five"), + }; + moved_tree.at(1)->addChildPointer("2.1", "two.one"); + moved_tree.at(1)->addChildPointer("2.2", "two.two"); + + model.reset(new QGenericItemModel(std::move(moved_tree), + tree_row::ProtocolPointerImpl{})); + break; + } + } + + return model; +} + +void tst_QGenericItemModel::tree() +{ + auto model = makeTreeModel(); + QFETCH(const int, expectedRootRowCount); + QFETCH(const int, expectedColumnCount); + QFETCH(QList, rowsWithChildren); + + QCOMPARE(model->rowCount(), expectedRootRowCount); + QCOMPARE(model->columnCount(), expectedColumnCount); + + for (int row = 0; row < model->rowCount(); ++row) { + const bool expectedChildren = rowsWithChildren.contains(row); + const QModelIndex parent = model->index(row, 0); + QVERIFY(parent.isValid()); + QCOMPARE(model->hasChildren(parent), expectedChildren); + if (expectedChildren) + QCOMPARE_GT(model->rowCount(parent), 0); + else + QCOMPARE(model->rowCount(parent), 0); + QCOMPARE(model->columnCount(parent), expectedColumnCount); + const QModelIndex child = model->index(0, 0, parent); + QCOMPARE(child.isValid(), expectedChildren); + if (expectedChildren) + QCOMPARE(child.parent(), parent); + else + QCOMPARE(child.parent(), QModelIndex()); + } + +#if QT_CONFIG(itemmodeltester) + QAbstractItemModelTester modelTest(model.get()); +#endif +} + +void tst_QGenericItemModel::treeModifyBranch() +{ + auto model = makeTreeModel(); + QFETCH(QList, rowsWithChildren); + QFETCH(const ChangeActions, changeActions); + + int rowWithChildren = rowsWithChildren.first(); + QCOMPARE_GT(rowWithChildren, 0); + + // removing or inserting a row adjusts the parents of the direct children + // of the following branches + { + QVERIFY(treeIntegrityCheck()); + QCOMPARE(model->removeRow(--rowWithChildren), + changeActions.testFlag(ChangeAction::RemoveRows)); + QVERIFY(treeIntegrityCheck()); + QCOMPARE(model->insertRow(rowWithChildren++), + changeActions.testFlag(ChangeAction::InsertRows)); + QVERIFY(treeIntegrityCheck()); + if (!changeActions.testFlag(ChangeAction::ChangeRows)) + return; // nothing else to test with a read-only model + } + + const QModelIndex parent = model->index(rowWithChildren, 0); + int oldRowCount = model->rowCount(parent); + + // append + { + QVERIFY(model->insertRow(oldRowCount, parent)); + QModelIndex newChild = model->index(oldRowCount, 0, parent); + QVERIFY(newChild.isValid()); + QCOMPARE(model->rowCount(parent), ++oldRowCount); + QCOMPARE(newChild.parent(), parent); + } + + // prepend + { + QVERIFY(model->insertRow(0, parent)); + QModelIndex newChild = model->index(0, 0, parent); + QVERIFY(newChild.isValid()); + QCOMPARE(model->rowCount(parent), ++oldRowCount); + QCOMPARE(newChild.parent(), parent); + } + + // remove last + { + QVERIFY(model->removeRow(model->rowCount(parent) - 1, parent)); + QCOMPARE(model->rowCount(parent), --oldRowCount); + } + + // remove first + { + QVERIFY(model->rowCount(parent) > 0); + QVERIFY(model->removeRow(0, parent)); + QCOMPARE(model->rowCount(parent), --oldRowCount); + } + +#if QT_CONFIG(itemmodeltester) + QAbstractItemModelTester modelTest(model.get()); +#endif +} + +void tst_QGenericItemModel::treeCreateBranch() +{ + auto model = makeTreeModel(); + QFETCH(QList, rowsWithChildren); + QFETCH(const ChangeActions, changeActions); + +#if QT_CONFIG(itemmodeltester) + QAbstractItemModelTester modelTest(model.get()); +#endif + + const QList pmiList = allIndexes(model.get()); + + // new branch + QVERIFY(!rowsWithChildren.contains(0)); + const QModelIndex parent = model->index(0, 0); + QVERIFY(!model->hasChildren(parent)); + QCOMPARE(model->insertRows(0, 5, parent), + changeActions.testFlag(ChangeAction::InsertRows)); + if (!changeActions.testFlag(ChangeAction::InsertRows)) + return; // nothing else to test with a read-only model + QVERIFY(model->hasChildren(parent)); + QCOMPARE(model->rowCount(parent), 5); + + for (int i = 0; i < model->rowCount(parent); ++i) { + QModelIndex newChild = model->index(i, 0, parent); + QVERIFY(newChild.isValid()); + QCOMPARE(newChild.parent(), parent); + QVERIFY(!model->hasChildren(newChild)); + } + + verifyPmiList(pmiList); +} + +void tst_QGenericItemModel::treeRemoveBranch() +{ + auto model = makeTreeModel(); + QFETCH(QList, rowsWithChildren); + QFETCH(const ChangeActions, changeActions); + +#if QT_CONFIG(itemmodeltester) + QAbstractItemModelTester modelTest(model.get()); +#endif + + const QModelIndex parent = model->index(rowsWithChildren.first(), 0); + QVERIFY(parent.isValid()); + QVERIFY(model->hasChildren(parent)); + const int oldRowCount = model->rowCount(parent); + QCOMPARE_GT(oldRowCount, 0); + + // out of bounds asserts in QAIM::removeRows + // QVERIFY(model->removeRows(0, oldRowCount * 2, parent)); + + QCOMPARE(model->removeRows(0, oldRowCount, parent), + changeActions.testFlag(ChangeAction::RemoveRows)); + if (!changeActions.testFlag(ChangeAction::RemoveRows)) + return; // nothing else to test with a read-only model + QVERIFY(!model->hasChildren(parent)); + QCOMPARE(model->rowCount(parent), 0); +} + QTEST_MAIN(tst_QGenericItemModel) #include "tst_qgenericitemmodel.moc"