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 <artem.dyomin@qt.io>
This commit is contained in:
Volker Hilsheimer 2025-03-29 09:37:10 +01:00
parent efe41182fd
commit c99816b033
6 changed files with 1469 additions and 31 deletions

View File

@ -5,6 +5,7 @@
#ifndef QT_NO_WIDGETS
#include <QtWidgets/qapplication.h>
#include <QtWidgets/qlistview.h>
#include <QtWidgets/qtableview.h>
#include <QtWidgets/qtreeview.h>
@ -145,6 +146,199 @@ private:
//! [gadget]
} // namespace gadget
namespace tree_protocol
{
//! [tree_protocol_0]
class TreeRow;
using Tree = std::vector<TreeRow>;
//! [tree_protocol_0]
//! [tree_protocol_1]
class TreeRow
{
Q_GADGET
// properties
TreeRow *m_parent;
std::optional<Tree> 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<Tree> &childRows() const { return m_children; }
//! [tree_protocol_2]
//! [tree_protocol_3]
void setParentRow(TreeRow *parent) { m_parent = parent; }
std::optional<Tree> &childRows() { return m_children; }
//! [tree_protocol_3]
//! [tree_protocol_4]
// Helper to assembly a tree of rows, not used by QGenericItemModel
template <typename ...Args>
TreeRow &addChild(Args &&...args)
{
if (!m_children)
m_children.emplace(Tree{});
auto &child = m_children->emplace_back(std::forward<Args>(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<Tree> &childRows(const TreeRow &row) const { return row.m_children; }
std::optional<Tree> &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<TreeRow *>;
//! [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 <typename ...Args>
TreeRow *addChild(Args &&...args)
{
if (!m_children)
m_children.emplace(Tree{});
auto *child = m_children->emplace_back(new TreeRow(std::forward<Args>(args)...));
child->m_parent = this;
return child;
}
private:
friend struct TreeTraversal;
QString m_value;
std::optional<Tree> 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<Tree> &childRows(const TreeRow *row) const { return row->m_children; }
std::optional<Tree> &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]

View File

@ -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 <typename Range, QGenericItemModelDetails::if_is_range<Range>> QGenericItemModel::QGenericItemModel(Range &&range, QObject *parent)
\fn template <typename Range, QGenericItemModelDetails::if_is_table_range<Range>> QGenericItemModel::QGenericItemModel(Range &&range, QObject *parent)
\fn template <typename Range, QGenericItemModelDetails::if_is_tree_range<Range>> QGenericItemModel::QGenericItemModel(Range &&range, QObject *parent)
\fn template <typename Range, typename Protocol, QGenericItemModelDetails::if_is_tree_range<Range, Protocol>> 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]
*/

View File

@ -43,9 +43,17 @@ public:
};
template <typename Range,
QGenericItemModelDetails::if_is_range<Range> = true>
QGenericItemModelDetails::if_is_table_range<Range> = true>
explicit QGenericItemModel(Range &&range, QObject *parent = nullptr);
template <typename Range,
QGenericItemModelDetails::if_is_tree_range<Range> = true>
explicit QGenericItemModel(Range &&range, QObject *parent = nullptr);
template <typename Range, typename Protocol,
QGenericItemModelDetails::if_is_tree_range<Range, Protocol> = 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<int, QByteArray> 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<row_reference>;
&& std::is_reference_v<row_reference>
&& Structure::is_mutable_impl;
}
static constexpr int static_row_count = QGenericItemModelDetails::static_size_v<range_type>;
@ -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<Range>) {
// If data with rows as pointers was moved in, then we own it and
// have to delete those rows.
using ref = decltype(std::forward<Range>(std::declval<range_type>()));
if constexpr (std::is_rvalue_reference_v<ref>)
that().destroyOwnedModel(*m_data.model());
}
}
template <typename Value>
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 <typename Range, typename Protocol>
class QGenericItemModelStructureImpl : public QGenericItemModelImpl<
QGenericItemModelStructureImpl<Range, Protocol>,
Range>
{};
class QGenericTreeItemModelImpl
: public QGenericItemModelImpl<QGenericTreeItemModelImpl<Range, Protocol>, Range>
{
using Base = QGenericItemModelImpl<QGenericTreeItemModelImpl<Range, Protocol>, Range>;
friend class QGenericItemModelImpl<QGenericTreeItemModelImpl<Range, Protocol>, Range>;
std::remove_reference_t<Protocol> 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<typename Base::range_type,
typename Base::row_type,
Protocol>;
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<Range>(model), itemModel)
, m_protocol(std::forward<Protocol>(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<const_row_ptr>(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<const_row_ptr>(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 <typename R>
void destroyOwnedModel(R &&range)
{
const auto begin = std::begin(range);
const auto end = std::end(range);
deleteRemovedRows(begin, end);
}
template <typename It, typename Sentinel>
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<const_row_ptr>(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<row_ptr>(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<const range_type *>(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<range_type *>(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 <typename Range>
class QGenericItemModelStructureImpl<Range, void> : public QGenericItemModelImpl<
QGenericItemModelStructureImpl<Range, void>,
Range>
class QGenericTableItemModelImpl
: public QGenericItemModelImpl<QGenericTableItemModelImpl<Range>, Range>
{
using Base = QGenericItemModelImpl<QGenericItemModelStructureImpl<Range, void>, Range>;
friend class QGenericItemModelImpl<QGenericItemModelStructureImpl<Range, void>, Range>;
using Base = QGenericItemModelImpl<QGenericTableItemModelImpl<Range>, Range>;
friend class QGenericItemModelImpl<QGenericTableItemModelImpl<Range>, 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<Range>(model), itemModel)
{}
@ -1051,6 +1313,21 @@ protected:
}
}
template <typename R>
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 <typename It, typename Sentinel>
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 <typename Range,
QGenericItemModelDetails::if_is_range<Range>>
QGenericItemModelDetails::if_is_table_range<Range>>
QGenericItemModel::QGenericItemModel(Range &&range, QObject *parent)
: QAbstractItemModel(parent)
, impl(new QGenericItemModelStructureImpl<Range, void>(std::forward<Range>(range), this))
, impl(new QGenericTableItemModelImpl<Range>(std::forward<Range>(range), this))
{}
template <typename Range,
QGenericItemModelDetails::if_is_tree_range<Range>>
QGenericItemModel::QGenericItemModel(Range &&range, QObject *parent)
: QGenericItemModel(std::forward<Range>(range),
QGenericItemModelDetails::tree_protocol_t<Range>{}, parent)
{}
template <typename Range, typename Protocol,
QGenericItemModelDetails::if_is_tree_range<Range, Protocol>>
QGenericItemModel::QGenericItemModel(Range &&range, Protocol &&protocol, QObject *parent)
: QAbstractItemModel(parent)
, impl(new QGenericTreeItemModelImpl<Range,
QGenericItemModelDetails::tree_protocol_t<Range, Protocol>
>(std::forward<Range>(range),
std::forward<Protocol>(protocol), this))
{}
QT_END_NAMESPACE

View File

@ -169,8 +169,6 @@ namespace QGenericItemModelDetails
template <typename C>
[[maybe_unused]] static constexpr bool is_range_v = range_traits<C>();
template <typename CC>
using if_is_range = std::enable_if_t<is_range_v<remove_ptr_and_ref_t<CC>>, bool>;
// Find out how many fixed elements can be retrieved from a row element.
// main template for simple values and ranges. Specializing for ranges
@ -224,6 +222,156 @@ namespace QGenericItemModelDetails
[[maybe_unused]] static constexpr int static_size_v =
row_traits<q20::remove_cvref_t<std::remove_pointer_t<T>>>::static_size;
// tests for tree protocol implementation in the row type
template <typename R, typename = void>
struct test_parentRow : std::false_type {};
template <typename R>
struct test_parentRow<R, std::void_t<decltype(std::declval<R>().parentRow())>>
: std::true_type
{};
template <typename R, typename = void>
struct test_childRows : std::false_type {};
template <typename R>
struct test_childRows<R, std::void_t<decltype(std::declval<const R>().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 <typename row_type>
struct DefaultTreeProtocol
{
using row_ptr = std::remove_pointer_t<row_type> *;
template <typename R = row_type>
auto newRow() const -> std::enable_if_t<std::is_pointer_v<R>, row_ptr>
{
return new std::remove_pointer_t<row_ptr>{};
}
template <typename R = row_type>
auto newRow(...) const -> decltype(R{})
{
return R{};
}
template <typename R>
auto deleteRow(R& row) -> decltype(delete row)
{
delete row;
}
template <typename R>
auto parentRow(const R &row) const -> decltype(row.parentRow())
{
return row.parentRow();
}
template <typename R>
auto parentRow(const R &row) const -> decltype(row->parentRow())
{
return row->parentRow();
}
template <typename R>
auto childRows(const R &row) const -> decltype(row.childRows())
{
return row.childRows();
}
template <typename R>
auto childRows(const R &row) const -> decltype(row->childRows())
{
return row->childRows();
}
template <typename R>
auto setParentRow(R &row, row_ptr parent) -> decltype(row.setParentRow(parent))
{
row.setParentRow(parent);
}
template <typename R>
auto setParentRow(R &row, row_ptr parent) -> decltype(row->setParentRow(parent))
{
row->setParentRow(parent);
}
template <typename R>
auto childRows(R &row) -> decltype(row.childRows())
{
return row.childRows();
}
template <typename R>
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 <typename P, typename R, typename = void>
struct protocol_setParentRow : std::false_type {};
template <typename P, typename R>
struct protocol_setParentRow<P, R, std::void_t<decltype(std::declval<P>().
setParentRow(std::declval<R&>(), nullptr))>>
: std::true_type {};
template <typename P, typename R, typename = void>
struct protocol_mutable_childRows : std::false_type {};
template <typename P, typename R>
struct protocol_mutable_childRows<P, R, std::void_t<decltype(std::declval<P>().
childRows(std::declval<R&>()) = {})>>
: 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 <typename C, typename R, typename TreeProtocol, typename = void>
struct tree_traits : std::integral_constant<bool, !std::is_void_v<TreeProtocol>>
{
using tree_protocol = TreeProtocol;
static constexpr bool has_setParentRow = protocol_setParentRow<tree_protocol, R>::value;
static constexpr bool has_mutable_childRows =
protocol_mutable_childRows<tree_protocol, R>::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 <typename C, typename R, typename TreeProtocol>
struct tree_traits<C, R, TreeProtocol,
std::enable_if_t<std::conjunction_v<test_parentRow<R>,
test_childRows<R>>
>
> : std::true_type
{
using tree_protocol = std::conditional_t<std::is_void_v<TreeProtocol>,
DefaultTreeProtocol<R>, TreeProtocol>;
static constexpr bool has_setParentRow = protocol_setParentRow<tree_protocol, R>::value;
static constexpr bool has_mutable_childRows =
protocol_mutable_childRows<tree_protocol, R>::value;
};
template <typename C, typename TreeProtocol = void,
typename range_type = remove_ptr_and_ref_t<C>>
using tree_protocol_t = typename tree_traits<
range_type,
std::remove_reference_t<decltype(*std::begin(std::declval<range_type&>()))>,
TreeProtocol>::tree_protocol;
template <typename C, typename range_type = remove_ptr_and_ref_t<C>>
using if_is_table_range = std::enable_if_t<
is_range_v<range_type> && std::is_void_v<tree_protocol_t<range_type>>,
bool>;
template <typename C, typename Protocol = void, typename range_type = remove_ptr_and_ref_t<C>>
using if_is_tree_range = std::enable_if_t<
is_range_v<range_type> && !std::is_void_v<tree_protocol_t<range_type, Protocol>>,
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 <typename ModelStorage>
@ -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<int, QByteArray> roleNames() const;
inline void dataChanged(const QModelIndex &from, const QModelIndex &to,
const QList<int> &roles);

View File

@ -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

View File

@ -144,6 +144,140 @@ namespace std {
template <> struct tuple_element<0, ConstRow> { using type = QString; };
}
struct tree_row;
using value_tree = QList<tree_row>;
using pointer_tree = QList<tree_row *>;
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 <typename ...Args>
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 <typename ...Args>
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<value_tree> &childRows() const { return m_children; }
std::optional<value_tree> &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<pointer_tree> &childRows(const tree_row *row) const
{ return row->m_childrenPointers; }
std::optional<pointer_tree> &childRows(tree_row *row)
{ return row->m_childrenPointers; }
};
private:
QString m_value;
QString m_description;
tree_row *m_parent = nullptr;
std::optional<value_tree> m_children = std::nullopt;
std::optional<pointer_tree> 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<size_t I, typename Row,
std::enable_if_t<std::is_same_v<q20::remove_cvref_t<Row>, 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<tree_row> : std::integral_constant<size_t, 2> {};
template <size_t I> struct tuple_element<I, tree_row>
{ using type = decltype(get<I>(std::declval<tree_row>())); };
}
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<QPersistentModelIndex> allIndexes(QAbstractItemModel *model, const QModelIndex &parent = {})
{
QList<QPersistentModelIndex> 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<QPersistentModelIndex> &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 <typename Tree>
static bool integrityCheck(const Tree &tree, int depth = 0)
{
static constexpr bool pointerTree = std::is_pointer_v<typename std::remove_reference_t<Tree>::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<QAbstractItemModel> makeTreeModel();
struct Data {
@ -320,6 +561,17 @@ private:
{{{Qt::DisplayRole, "DISPLAY2"}, {Qt::DecorationRole, "DECORATION2"}}},
{{{Qt::DisplayRole, "DISPLAY3"}, {Qt::DecorationRole, "DECORATION3"}}},
};
std::unique_ptr<value_tree> m_tree;
struct TreeDeleter {
void operator()(pointer_tree *tree)
{
for (auto *row : *tree)
delete row;
delete tree;
}
};
std::unique_ptr<pointer_tree, TreeDeleter> m_pointer_tree;
};
std::unique_ptr<Data> m_data;
@ -357,10 +609,12 @@ void createBackup(QObject* object, T& model) {
template <typename T, std::enable_if_t<!std::is_copy_assignable_v<T>, 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>("factory");
QTest::addColumn<int>("expectedRowCount");
QTest::addColumn<int>("expectedColumnCount");
@ -448,6 +702,33 @@ void tst_QGenericItemModel::createTestData()
};
return std::unique_ptr<QAbstractItemModel>(new QGenericItemModel(std::move(movedTable)));
}) << 4 << 4 << ChangeActions(ChangeAction::All);
// moved list of pointers -> model takes ownership
QTest::addRow("movedListOfObjects") << Factory([]{
std::list<Object *> movedListOfObjects = {
new Object, new Object, new Object,
new Object, new Object, new Object
};
return std::unique_ptr<QAbstractItemModel>(
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<QAbstractItemModel>(new QGenericItemModel(m_data->m_tree.get()));
}) << int(std::size(*m_data->m_tree.get())) << int(std::tuple_size_v<tree_row>)
<< (ChangeAction::ChangeRows | ChangeAction::SetData);
QTest::addRow("pointer tree") << Factory([this]{
return std::unique_ptr<QAbstractItemModel>(
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<tree_row>)
<< (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<Object *> 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<std::shared_ptr<Object>> objects {
std::shared_ptr<Object>(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<std::vector<Object *>> 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<QPersistentModelIndex> 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<TreeProtocol>("protocol");
QTest::addColumn<int>("expectedRootRowCount");
QTest::addColumn<int>("expectedColumnCount");
QTest::addColumn<QList<int>>("rowsWithChildren");
QTest::addColumn<ChangeActions>("changeActions");
const int expectedRootRowCount = int(m_data->m_tree->size());
const int expectedColumnCount = int(std::tuple_size_v<tree_row>);
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<QAbstractItemModel> tst_QGenericItemModel::makeTreeModel()
{
createTree();
std::unique_ptr<QAbstractItemModel> 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<value_tree> &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<int>, 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<int>, 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<int>, rowsWithChildren);
QFETCH(const ChangeActions, changeActions);
#if QT_CONFIG(itemmodeltester)
QAbstractItemModelTester modelTest(model.get());
#endif
const QList<QPersistentModelIndex> 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<int>, 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"