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"