Long live QGenericItemModel for lists and tables
QGenericItemModel provides a QAbstractItemModel implementation that can make any C++ range with a cbegin/cend iterator pair available to Qt's model/view framework. To avoid subclassing a polymorphic type with a template, resulting in weak vtables, the calls are dispatched through a set of static helpers similar to existing pattern in e.g. QObject or QRunnable. The public QGenericItemModel class is then a subclass of QAbstractItemModel, where only the constructor is a template that instantiates the specialization of QGenericItemModelImpl. The virtual function overrides dispatch the call through the static helper to the template class operating on the range. The core of the implementation is the template class QGenericItemModelImpl, that implements the QAbstractItemModel functionality on top of iterator APIs. Implementations that are specific to the structure of the model, i.e. whether it is a flat table or list model, or (in a later commit) a tree, are in a base class of QGenericItemModelImpl, QGenericItemModelStructureImpl. It uses CRTP with decltype(auto) where we would otherwise have a return type that is dependent on the (not yet fully specialized) implementation type. The data for the model can be provided by copy, rvalue reference, or pointer. If provided by pointer, the model will modify the original data. Functionality that requires that the model itself, or the data in the model, can be modified is compiled out if the model or data is const. The initial implementation supports lists and tables, where tables can be nested containers (like `std::vector<std::vector<item_type>>`), or as structs that implement the C++ tuple protocol. Include test framework and documentation, as well as a snippet-project that tests the code snippets. [ChangeLog][QtCore] Added QGenericItemModel, a QAbstractItemModel implementation that make any C++ range with a cbegin/cend iterator pair available to Qt's model/view framework. Change-Id: I8ff2b95cde2566f422f3aee7912660a8cb217397 Reviewed-by: Artem Dyomin <artem.dyomin@qt.io>
This commit is contained in:
parent
17a9343ace
commit
f5115a9137
@ -1156,6 +1156,7 @@ qt_internal_extend_target(Core CONDITION QT_FEATURE_itemmodel
|
||||
SOURCES
|
||||
itemmodels/qabstractitemmodel.cpp itemmodels/qabstractitemmodel.h itemmodels/qabstractitemmodel_p.h
|
||||
itemmodels/qitemselectionmodel.cpp itemmodels/qitemselectionmodel.h itemmodels/qitemselectionmodel_p.h
|
||||
itemmodels/qgenericitemmodel.h itemmodels/qgenericitemmodel_impl.h itemmodels/qgenericitemmodel.cpp
|
||||
)
|
||||
|
||||
qt_internal_extend_target(Core CONDITION QT_FEATURE_proxymodel
|
||||
@ -1620,3 +1621,7 @@ function(qt_internal_library_deprecation_level)
|
||||
set_source_files_properties("${output_header}" PROPERTIES GENERATED TRUE)
|
||||
endfunction()
|
||||
qt_internal_library_deprecation_level()
|
||||
|
||||
if(QT_FEATURE_doc_snippets)
|
||||
add_subdirectory(doc/snippets)
|
||||
endif()
|
||||
|
18
src/corelib/doc/snippets/CMakeLists.txt
Normal file
18
src/corelib/doc/snippets/CMakeLists.txt
Normal file
@ -0,0 +1,18 @@
|
||||
# Copyright (C) 2025 The Qt Company Ltd.
|
||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||
|
||||
add_library(corelib_snippets OBJECT
|
||||
qgenericitemmodel/main.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(corelib_snippets PRIVATE
|
||||
Qt::Core
|
||||
Qt::Gui
|
||||
Qt::Widgets
|
||||
)
|
||||
|
||||
if ("${CMAKE_CXX_COMPILE_FEATURES}" MATCHES "cxx_std_23")
|
||||
set_property(TARGET corelib_snippets PROPERTY CXX_STANDARD 23)
|
||||
endif()
|
||||
|
||||
set_target_properties(corelib_snippets PROPERTIES UNITY_BUILD OFF)
|
108
src/corelib/doc/snippets/qgenericitemmodel/main.cpp
Normal file
108
src/corelib/doc/snippets/qgenericitemmodel/main.cpp
Normal file
@ -0,0 +1,108 @@
|
||||
// Copyright (C) 2025 The Qt Company Ltd.
|
||||
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||
|
||||
#include <QtCore/qgenericitemmodel.h>
|
||||
#include <QtWidgets/qlistview.h>
|
||||
#include <QtWidgets/qtableview.h>
|
||||
#include <QtWidgets/qtreeview.h>
|
||||
|
||||
#include <array>
|
||||
#include <vector>
|
||||
|
||||
void array()
|
||||
{
|
||||
QListView listView;
|
||||
|
||||
//! [array]
|
||||
std::array<int, 5> numbers = {1, 2, 3, 4, 5};
|
||||
QGenericItemModel model(numbers);
|
||||
listView.setModel(&model);
|
||||
//! [array]
|
||||
}
|
||||
|
||||
void const_array()
|
||||
{
|
||||
//! [const_array]
|
||||
const std::array<int, 5> numbers = {1, 2, 3, 4, 5};
|
||||
//! [const_array]
|
||||
QGenericItemModel model(numbers);
|
||||
}
|
||||
|
||||
void const_values()
|
||||
{
|
||||
//! [const_values]
|
||||
std::array<const int, 5> numbers = {1, 2, 3, 4, 5};
|
||||
//! [const_values]
|
||||
QGenericItemModel model(numbers);
|
||||
}
|
||||
|
||||
void list_of_int()
|
||||
{
|
||||
//! [list_of_int]
|
||||
QList<int> numbers = {1, 2, 3, 4, 5};
|
||||
QGenericItemModel model(numbers); // columnCount() == 1
|
||||
QListView listView;
|
||||
listView.setModel(&model);
|
||||
//! [list_of_int]
|
||||
}
|
||||
|
||||
void grid_of_numbers()
|
||||
{
|
||||
//! [grid_of_numbers]
|
||||
std::vector<std::vector<int>> gridOfNumbers = {
|
||||
{1, 2, 3, 4, 5},
|
||||
{6, 7, 8, 9, 10},
|
||||
{11, 12, 13, 14, 15},
|
||||
};
|
||||
QGenericItemModel model(&gridOfNumbers); // columnCount() == 5
|
||||
QTableView tableView;
|
||||
tableView.setModel(&model);
|
||||
//! [grid_of_numbers]
|
||||
}
|
||||
|
||||
void pair_int_QString()
|
||||
{
|
||||
//! [pair_int_QString]
|
||||
using TableRow = std::tuple<int, QString>;
|
||||
QList<TableRow> numberNames = {
|
||||
{1, "one"},
|
||||
{2, "two"},
|
||||
{3, "three"}
|
||||
};
|
||||
QGenericItemModel model(&numberNames); // columnCount() == 2
|
||||
QTableView tableView;
|
||||
tableView.setModel(&model);
|
||||
//! [pair_int_QString]
|
||||
}
|
||||
|
||||
#if defined(__cpp_concepts) && defined(__cpp_lib_forward_like)
|
||||
//! [tuple_protocol]
|
||||
struct Book
|
||||
{
|
||||
QString title;
|
||||
QString author;
|
||||
QString summary;
|
||||
int rating = 0;
|
||||
|
||||
template <size_t I, typename T>
|
||||
requires ((I <= 3) && std::is_same_v<std::remove_cvref_t<T>, Book>)
|
||||
friend inline decltype(auto) get(T &&book)
|
||||
{
|
||||
if constexpr (I == 0)
|
||||
return std::as_const(book.title);
|
||||
else if constexpr (I == 1)
|
||||
return std::as_const(book.author);
|
||||
else if constexpr (I == 2)
|
||||
return std::forward_like<T>(book.summary);
|
||||
else if constexpr (I == 3)
|
||||
return std::forward_like<T>(book.rating);
|
||||
}
|
||||
};
|
||||
|
||||
namespace std {
|
||||
template <> struct tuple_size<Book> : std::integral_constant<size_t, 4> {};
|
||||
template <size_t I> struct tuple_element<I, Book>
|
||||
{ using type = decltype(get<I>(std::declval<Book>())); };
|
||||
}
|
||||
//! [tuple_protocol]
|
||||
#endif // __cpp_concepts && forward_like
|
387
src/corelib/itemmodels/qgenericitemmodel.cpp
Normal file
387
src/corelib/itemmodels/qgenericitemmodel.cpp
Normal file
@ -0,0 +1,387 @@
|
||||
// Copyright (C) 2025 The Qt Company Ltd.
|
||||
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
|
||||
|
||||
#include "qgenericitemmodel.h"
|
||||
|
||||
QT_BEGIN_NAMESPACE
|
||||
|
||||
/*!
|
||||
\class QGenericItemModel
|
||||
\inmodule QtCore
|
||||
\since 6.10
|
||||
\ingroup model-view
|
||||
\brief QGenericItemModel implements QAbstractItemModel for any C++ range.
|
||||
\reentrant
|
||||
|
||||
QGenericItemModel can make the data in any sequentially iterable C++ type
|
||||
available to the \l{Model/View Programming}{model/view framework} of Qt.
|
||||
This makes it easy to display existing data structures in the Qt Widgets
|
||||
and Qt Quick item views, and to allow the user of the application to
|
||||
manipulate the data using a graphical user interface.
|
||||
|
||||
To use QGenericItemModel, instantiate it with a C++ range and set it as
|
||||
the model of one or more views:
|
||||
|
||||
\snippet qgenericitemmodel/main.cpp array
|
||||
|
||||
The range can be any C++ type for which the standard methods
|
||||
\c{std::cbegin} and \c{std::cend} are implemented, and for which the
|
||||
returned iterator type satisfies \c{std::forward_iterator}. Certain model
|
||||
operations will perform better if \c{std::size} is available, and if the
|
||||
iterator satisfies \c{std::random_access_iterator}.
|
||||
|
||||
The range can be provided by pointer or by value, and has to be provided
|
||||
when constructing the model. If the range is provided by pointer, then
|
||||
QAbstractItemModel APIs that modify the model, such as setData() or
|
||||
insertRows(), modify the range. The caller must make sure that the
|
||||
range's lifetime exceeds the lifetime of the model. Methods that modify
|
||||
the structure of the range, such as insertRows() or removeColumns(), use
|
||||
standard C++ container APIs \c{resize()}, \c{insert()}, \c{erase()}, in
|
||||
addition to dereferencing a mutating iterator to set or clear the data.
|
||||
|
||||
There is no API to retrieve the range again, so constructing the model
|
||||
from a range by value is mostly only useful for displaying data.
|
||||
Changes to the data can be monitored using the signals emitted by the
|
||||
model, such as \l{QAbstractItemModel}{dataChanged()}.
|
||||
|
||||
\section2 Read-only or mutable
|
||||
|
||||
For ranges that are const objects, for which access always yields
|
||||
constant values, or where the required container APIs are not available,
|
||||
QGenericItemModel implements write-access APIs to do nothing and return
|
||||
\c{false}. In the example above, the model cannot add or remove rows, as
|
||||
the number of entries in a C++ array is fixed. But the values can be
|
||||
changed using setData(), and the user can trigger editing of the values in
|
||||
the list view. By making the array const, the values also become read-only.
|
||||
|
||||
\snippet qgenericitemmodel/main.cpp const_array
|
||||
|
||||
The values are also read-only if the element type is const, like in
|
||||
|
||||
\snippet qgenericitemmodel/main.cpp const_values
|
||||
|
||||
\note If the values in the range are const, then it's also not possible
|
||||
to remove or insert columns and rows through the QAbstractItemModel API.
|
||||
For more granular control, implement \l{the C++ tuple protocol}.
|
||||
|
||||
\section1 List or Table
|
||||
|
||||
The elements in the range are interpreted as rows of the model. Depending
|
||||
on the type of these rows, QGenericItemModel exposes the range as a list or
|
||||
a table.
|
||||
|
||||
If the row type is not an iterable range, and does not implement the
|
||||
C++ tuple protocol, then the range gets represented as a list.
|
||||
|
||||
\snippet qgenericitemmodel/main.cpp list_of_int
|
||||
|
||||
If the row type is an iterable range, then the range gets represented as a
|
||||
table.
|
||||
|
||||
\snippet qgenericitemmodel/main.cpp grid_of_numbers
|
||||
|
||||
With such a row type, the number of columns can be changed via
|
||||
insertColumns() and removeColumns(). However, all rows are expected to have
|
||||
the same number of columns.
|
||||
|
||||
\section2 Fixed-size rows
|
||||
|
||||
If the row type implements \l{the C++ tuple protocol}, then the range gets
|
||||
represented as a table with a fixed number of columns.
|
||||
|
||||
\snippet qgenericitemmodel/main.cpp pair_int_QString
|
||||
|
||||
\section2 Item Types
|
||||
|
||||
The type of the items that the implementations of data(), setData(), and
|
||||
clearItemData() operate on can be the same across the entire model - like
|
||||
in the gridOfNumbers example above. But the range can also have different
|
||||
item types for different columns, like in the \c{numberNames} case.
|
||||
|
||||
By default, the value gets used for the Qt::DisplayRole and Qt::EditRole
|
||||
roles. Most views expect the value to be \l{QVariant::canConvert}{convertible
|
||||
to and from a QString} (but a custom delegate might provide more flexibility).
|
||||
|
||||
\section2 The C++ tuple protocol
|
||||
|
||||
As seen in the \c{numberNames} example above, the row type can be a tuple,
|
||||
and in fact any type that implements the tuple protocol. This protocol is
|
||||
implemented by specializing \c{std::tuple_size} and \c{std::tuple_element},
|
||||
and overloading the unqualified \c{get} function. Do so for your custom row
|
||||
type to make existing structured data available to the model/view framework
|
||||
in Qt.
|
||||
|
||||
\snippet qgenericitemmodel/main.cpp tuple_protocol
|
||||
|
||||
In the above implementation, the \c{title} and \c{author} values of the
|
||||
\c{Book} type are returned as \c{const}, so the model flags items in those
|
||||
two columns as read-only. The user won't be able to trigger editing, and
|
||||
setData() does nothing and returns false. For \c{summary} and \c{rating}
|
||||
the implementation returns the same value category as the book, so when
|
||||
\c{get} is called with a mutable reference to a \c{Book}, then it will
|
||||
return a mutable reference of the respective variable. The model makes
|
||||
those columns editable, both for the user and for programmatic access.
|
||||
|
||||
\note The implementation of \c{get} above requires C++23. A C++17 compliant
|
||||
implementation can be found in the unit test code for QGenericItemModel.
|
||||
|
||||
\sa {Model/View Programming}
|
||||
*/
|
||||
|
||||
/*!
|
||||
\fn template <typename Range, QGenericItemModelDetails::if_is_range<Range>> QGenericItemModel::QGenericItemModel(Range &&range, QObject *parent)
|
||||
|
||||
Constructs a generic item model instance that operates on the data in
|
||||
\a range. The \a range has to be a sequential range for which
|
||||
\c{std::cbegin} and \c{std::cend} are available. The model instance becomes
|
||||
a child of \a parent.
|
||||
|
||||
The \a range can be a pointer, in which case mutating model APIs will
|
||||
modify the data in that range instance. If \a range is a value (or moved
|
||||
into the model), then use the signals emitted by the model to respond to
|
||||
changes to the data.
|
||||
|
||||
\note While the model does not take ownership of the range object, you
|
||||
must not modify the \a range directly once the model has been
|
||||
constructed. Such modifications will not emit signals necessary to keep
|
||||
model users (other models or views) synchronized with the model, resulting
|
||||
in inconsistent results, undefined behavior, and crashes.
|
||||
*/
|
||||
|
||||
/*!
|
||||
Destroys the generic item model.
|
||||
|
||||
The range that the model was constructed from is not destroyed.
|
||||
*/
|
||||
QGenericItemModel::~QGenericItemModel() = default;
|
||||
|
||||
/*!
|
||||
\reimp
|
||||
|
||||
Returns the index of the model item at \a row and \a column in \a parent.
|
||||
|
||||
Passing a valid parent produces an invalid index for models that operate on
|
||||
list and table ranges.
|
||||
|
||||
\sa parent()
|
||||
*/
|
||||
QModelIndex QGenericItemModel::index(int row, int column, const QModelIndex &parent) const
|
||||
{
|
||||
return impl->callConst<QModelIndex>(QGenericItemModelImplBase::Index, row, column, parent);
|
||||
}
|
||||
|
||||
/*!
|
||||
\reimp
|
||||
|
||||
Returns the parent of the item at the \a child index.
|
||||
|
||||
This function always produces an invalid index for models that operate on
|
||||
list and table ranges.
|
||||
|
||||
\sa index(), hasChildren()
|
||||
*/
|
||||
QModelIndex QGenericItemModel::parent(const QModelIndex &child) const
|
||||
{
|
||||
return impl->callConst<QModelIndex>(QGenericItemModelImplBase::Parent, child);
|
||||
}
|
||||
|
||||
/*!
|
||||
\reimp
|
||||
|
||||
Returns the number of rows under the given \a parent. This is the number of
|
||||
items in the root range for an invalid \a parent index.
|
||||
|
||||
If the \a parent index is valid, then this function always returns 0 for
|
||||
models that operate on list and table ranges.
|
||||
|
||||
\sa columnCount(), insertRows(), hasChildren()
|
||||
*/
|
||||
int QGenericItemModel::rowCount(const QModelIndex &parent) const
|
||||
{
|
||||
return impl->callConst<int>(QGenericItemModelImplBase::RowCount, parent);
|
||||
}
|
||||
|
||||
/*!
|
||||
\reimp
|
||||
|
||||
Returns the number of columns of the model. This function returns the same
|
||||
value for all \a parent indexes.
|
||||
|
||||
For models operating on a statically sized row type, this returned value is
|
||||
always the same throughout the lifetime of the model. For models operating
|
||||
on dynamically sized row type, the model returns the number of items in the
|
||||
first row, or 0 if the model has no rows.
|
||||
|
||||
\sa rowCount, insertColumns()
|
||||
*/
|
||||
int QGenericItemModel::columnCount(const QModelIndex &parent) const
|
||||
{
|
||||
return impl->callConst<int>(QGenericItemModelImplBase::ColumnCount, parent);
|
||||
}
|
||||
|
||||
/*!
|
||||
\reimp
|
||||
|
||||
Returns the item flags for the given \a index.
|
||||
|
||||
The implementation returns a combination of flags that enables the item
|
||||
(\c ItemIsEnabled) and allows it to be selected (\c ItemIsSelectable). For
|
||||
models operating on a range with mutable data, it also sets the flag
|
||||
that allows the item to be editable (\c ItemIsEditable).
|
||||
|
||||
\sa Qt::ItemFlags
|
||||
*/
|
||||
Qt::ItemFlags QGenericItemModel::flags(const QModelIndex &index) const
|
||||
{
|
||||
return impl->callConst<Qt::ItemFlags>(QGenericItemModelImplBase::Flags, index);
|
||||
}
|
||||
|
||||
/*!
|
||||
\reimp
|
||||
|
||||
Returns the data for the given \a role and \a section in the header with
|
||||
the specified \a orientation.
|
||||
|
||||
For horizontal headers, the section number corresponds to the column
|
||||
number. Similarly, for vertical headers, the section number corresponds to
|
||||
the row number.
|
||||
|
||||
\sa Qt::ItemDataRole, setHeaderData(), QHeaderView
|
||||
*/
|
||||
QVariant QGenericItemModel::headerData(int section, Qt::Orientation orientation, int role) const
|
||||
{
|
||||
return impl->callConst<QVariant>(QGenericItemModelImplBase::HeaderData,
|
||||
section, orientation, role);
|
||||
}
|
||||
|
||||
/*!
|
||||
\reimp
|
||||
|
||||
Returns the data stored under the given \a role for the value in the
|
||||
range referred to by the \a index.
|
||||
|
||||
The implementation returns a QVariant constructed from the item at the
|
||||
\a index via \c{QVariant::fromValue()} for \c{Qt::DisplayRole} or
|
||||
\c{Qt::EditRole}. For other roles, the implementation returns an \b invalid
|
||||
(default-constructed) QVariant.
|
||||
|
||||
\sa Qt::ItemDataRole, setData(), headerData()
|
||||
*/
|
||||
QVariant QGenericItemModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
return impl->callConst<QVariant>(QGenericItemModelImplBase::Data, index, role);
|
||||
}
|
||||
|
||||
/*!
|
||||
\reimp
|
||||
|
||||
Sets the \a role data for the item at \a index to \a data. This
|
||||
implementation assigns the value in \a data to the item at the \a index
|
||||
in the range for \c{Qt::DisplayRole} and \c{Qt::EditRole}, and returns
|
||||
\c{true}. For other roles, the implementation returns \c{false}.
|
||||
|
||||
//! [read-only-setData]
|
||||
For models operating on a read-only range, or on a read-only column in
|
||||
a row type that implements \l{the C++ tuple protocol}, this implementation
|
||||
returns \c{false} immediately.
|
||||
//! [read-only-setData]
|
||||
*/
|
||||
bool QGenericItemModel::setData(const QModelIndex &index, const QVariant &data, int role)
|
||||
{
|
||||
return impl->call<bool>(QGenericItemModelImplBase::SetData, index, data, role);
|
||||
}
|
||||
|
||||
/*!
|
||||
\reimp
|
||||
|
||||
Replaces the value stored in the range at \a index with a default-
|
||||
constructed value.
|
||||
|
||||
\include qgenericitemmodel.cpp read-only-setData
|
||||
*/
|
||||
bool QGenericItemModel::clearItemData(const QModelIndex &index)
|
||||
{
|
||||
return impl->call<bool>(QGenericItemModelImplBase::ClearItemData, index);
|
||||
}
|
||||
|
||||
/*
|
||||
//! [column-change-requirement]
|
||||
\note A dynamically sized row type needs to provide a \c{\1} member function.
|
||||
|
||||
For models operating on a read-only range, or on a range with a
|
||||
statically sized row type (such as a tuple, array, or struct), this
|
||||
implementation does nothing and returns \c{false} immediately.
|
||||
//! [column-change-requirement]
|
||||
*/
|
||||
|
||||
/*!
|
||||
\reimp
|
||||
|
||||
Inserts \a count empty columns before the item at \a column in all rows
|
||||
of the range at \a parent. Returns \c{true} if successful; otherwise
|
||||
returns \c{false}.
|
||||
|
||||
\include qgenericitemmodel.cpp {column-change-requirement} {insert(const_iterator, size_t, value_type)}
|
||||
*/
|
||||
bool QGenericItemModel::insertColumns(int column, int count, const QModelIndex &parent)
|
||||
{
|
||||
return impl->call<bool>(QGenericItemModelImplBase::InsertColumns, column, count, parent);
|
||||
}
|
||||
|
||||
/*!
|
||||
\reimp
|
||||
|
||||
Removes \a count columns from the item at \a column on in all rows of the
|
||||
range at \a parent. Returns \c{true} if successful, otherwise returns
|
||||
\c{false}.
|
||||
|
||||
\include qgenericitemmodel.cpp {column-change-requirement} {erase(const_iterator, size_t)}
|
||||
*/
|
||||
bool QGenericItemModel::removeColumns(int column, int count, const QModelIndex &parent)
|
||||
{
|
||||
return impl->call<bool>(QGenericItemModelImplBase::RemoveColumns, column, count, parent);
|
||||
}
|
||||
|
||||
/*
|
||||
//! [row-change-requirement]
|
||||
\note The range needs to be dynamically sized and provide a \c{\1}
|
||||
member function.
|
||||
|
||||
For models operating on a read-only or statically-sized range (such as
|
||||
an array), this implementation does nothing and returns \c{false}
|
||||
immediately.
|
||||
//! [row-change-requirement]
|
||||
*/
|
||||
|
||||
/*!
|
||||
\reimp
|
||||
|
||||
Inserts \a count empty rows before the given \a row into the range at
|
||||
\a parent. Returns \c{true} if successful; otherwise returns \c{false}.
|
||||
|
||||
\include qgenericitemmodel.cpp {row-change-requirement} {insert(const_iterator, size_t, value_type)}
|
||||
|
||||
\note For ranges with a dynamically sized column type, the column needs
|
||||
to provide a \c{resize(size_t)} member function.
|
||||
*/
|
||||
bool QGenericItemModel::insertRows(int row, int count, const QModelIndex &parent)
|
||||
{
|
||||
return impl->call<bool>(QGenericItemModelImplBase::InsertRows, row, count, parent);
|
||||
}
|
||||
|
||||
/*!
|
||||
\reimp
|
||||
|
||||
Removes \a count rows from the range at \a parent, starting with the
|
||||
given \a row. Returns \c{true} if successful, otherwise returns \c{false}.
|
||||
|
||||
\include qgenericitemmodel.cpp {row-change-requirement} {erase(const_iterator, size_t)}
|
||||
*/
|
||||
bool QGenericItemModel::removeRows(int row, int count, const QModelIndex &parent)
|
||||
{
|
||||
return impl->call<bool>(QGenericItemModelImplBase::RemoveRows, row, count, parent);
|
||||
}
|
||||
|
||||
QT_END_NAMESPACE
|
||||
|
||||
#include "moc_qgenericitemmodel.cpp"
|
657
src/corelib/itemmodels/qgenericitemmodel.h
Normal file
657
src/corelib/itemmodels/qgenericitemmodel.h
Normal file
@ -0,0 +1,657 @@
|
||||
// Copyright (C) 2025 The Qt Company Ltd.
|
||||
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
|
||||
|
||||
#ifndef QGENERICITEMMODEL_H
|
||||
#define QGENERICITEMMODEL_H
|
||||
|
||||
#include <QtCore/qgenericitemmodel_impl.h>
|
||||
|
||||
QT_BEGIN_NAMESPACE
|
||||
|
||||
class Q_CORE_EXPORT QGenericItemModel : public QAbstractItemModel
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
template <typename Range,
|
||||
QGenericItemModelDetails::if_is_range<Range> = true>
|
||||
explicit QGenericItemModel(Range &&range, QObject *parent = nullptr);
|
||||
|
||||
~QGenericItemModel() override;
|
||||
|
||||
QModelIndex index(int row, int column, const QModelIndex &parent = {}) const override;
|
||||
QModelIndex parent(const QModelIndex &child) const override;
|
||||
int rowCount(const QModelIndex &parent = {}) const override;
|
||||
int columnCount(const QModelIndex &parent = {}) const override;
|
||||
Qt::ItemFlags flags(const QModelIndex &index) const override;
|
||||
QVariant headerData(int section, Qt::Orientation orientation, int role) const override;
|
||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||
bool setData(const QModelIndex &index, const QVariant &data, int role = Qt::EditRole) override;
|
||||
bool clearItemData(const QModelIndex &index) override;
|
||||
bool insertColumns(int column, int count, const QModelIndex &parent = {}) override;
|
||||
bool removeColumns(int column, int count, const QModelIndex &parent = {}) override;
|
||||
bool insertRows(int row, int count, const QModelIndex &parent = {}) override;
|
||||
bool removeRows(int row, int count, const QModelIndex &parent = {}) override;
|
||||
|
||||
private:
|
||||
Q_DISABLE_COPY_MOVE(QGenericItemModel)
|
||||
|
||||
friend class QGenericItemModelImplBase;
|
||||
struct Deleter { void operator()(QGenericItemModelImplBase *that) { that->destroy(); } };
|
||||
std::unique_ptr<QGenericItemModelImplBase, Deleter> impl;
|
||||
};
|
||||
|
||||
// implementation of forwarders
|
||||
QModelIndex QGenericItemModelImplBase::createIndex(int row, int column, const void *ptr) const
|
||||
{
|
||||
return m_itemModel->createIndex(row, column, ptr);
|
||||
}
|
||||
QHash<int, QByteArray> QGenericItemModelImplBase::roleNames() const
|
||||
{
|
||||
return m_itemModel->roleNames();
|
||||
}
|
||||
void QGenericItemModelImplBase::dataChanged(const QModelIndex &from, const QModelIndex &to,
|
||||
const QList<int> &roles)
|
||||
{
|
||||
m_itemModel->dataChanged(from, to, roles);
|
||||
}
|
||||
void QGenericItemModelImplBase::beginInsertColumns(const QModelIndex &parent, int start, int count)
|
||||
{
|
||||
m_itemModel->beginInsertColumns(parent, start, count);
|
||||
}
|
||||
void QGenericItemModelImplBase::endInsertColumns()
|
||||
{
|
||||
m_itemModel->endInsertColumns();
|
||||
}
|
||||
void QGenericItemModelImplBase::beginRemoveColumns(const QModelIndex &parent, int start, int count)
|
||||
{
|
||||
m_itemModel->beginRemoveColumns(parent, start, count);
|
||||
}
|
||||
void QGenericItemModelImplBase::endRemoveColumns()
|
||||
{
|
||||
m_itemModel->endRemoveColumns();
|
||||
}
|
||||
void QGenericItemModelImplBase::beginInsertRows(const QModelIndex &parent, int start, int count)
|
||||
{
|
||||
m_itemModel->beginInsertRows(parent, start, count);
|
||||
}
|
||||
void QGenericItemModelImplBase::endInsertRows()
|
||||
{
|
||||
m_itemModel->endInsertRows();
|
||||
}
|
||||
void QGenericItemModelImplBase::beginRemoveRows(const QModelIndex &parent, int start, int count)
|
||||
{
|
||||
m_itemModel->beginRemoveRows(parent, start, count);
|
||||
}
|
||||
void QGenericItemModelImplBase::endRemoveRows()
|
||||
{
|
||||
m_itemModel->endRemoveRows();
|
||||
}
|
||||
|
||||
template <typename Structure, typename Range>
|
||||
class QGenericItemModelImpl : public QGenericItemModelImplBase
|
||||
{
|
||||
Q_DISABLE_COPY_MOVE(QGenericItemModelImpl)
|
||||
public:
|
||||
using range_type = std::remove_pointer_t<std::remove_reference_t<Range>>;
|
||||
using row_reference = decltype(*std::begin(std::declval<range_type&>()));
|
||||
using const_row_reference = decltype(*std::cbegin(std::declval<range_type&>()));
|
||||
using row_type = std::remove_reference_t<row_reference>;
|
||||
|
||||
protected:
|
||||
using Self = QGenericItemModelImpl<Structure, Range>;
|
||||
Structure& that() { return static_cast<Structure &>(*this); }
|
||||
const Structure& that() const { return static_cast<const Structure &>(*this); }
|
||||
|
||||
template <typename C>
|
||||
static constexpr auto size(const C &c)
|
||||
{
|
||||
if constexpr (QGenericItemModelDetails::test_size<C>())
|
||||
return std::size(c);
|
||||
else
|
||||
#if defined(__cpp_lib_ranges)
|
||||
return std::ranges::distance(std::begin(c), std::end(c));
|
||||
#else
|
||||
return std::distance(std::begin(c), std::end(c));
|
||||
#endif
|
||||
}
|
||||
|
||||
friend class tst_QGenericItemModel;
|
||||
using range_features = QGenericItemModelDetails::range_traits<range_type>;
|
||||
using row_features = QGenericItemModelDetails::range_traits<row_type>;
|
||||
|
||||
using row_traits = QGenericItemModelDetails::row_traits<q20::remove_cvref_t<
|
||||
std::remove_pointer_t<row_type>>>;
|
||||
|
||||
static constexpr bool isMutable()
|
||||
{
|
||||
return range_features::is_mutable && row_features::is_mutable
|
||||
&& std::is_reference_v<row_reference>;
|
||||
}
|
||||
|
||||
static constexpr int static_row_count = QGenericItemModelDetails::static_size_v<range_type>;
|
||||
static constexpr bool rows_are_pointers = std::is_pointer_v<row_type>;
|
||||
static constexpr int static_column_count = QGenericItemModelDetails::static_size_v<row_type>;
|
||||
|
||||
static constexpr bool dynamicRows() { return isMutable() && static_row_count < 0; }
|
||||
static constexpr bool dynamicColumns() { return static_column_count < 0; }
|
||||
|
||||
// A row might be a value (or range of values), or a pointer.
|
||||
// row_ptr is always a pointer, and const_row_ptr is a pointer to const.
|
||||
using row_ptr = std::conditional_t<rows_are_pointers, row_type, row_type *>;
|
||||
using const_row_ptr = const std::remove_pointer_t<row_type> *;
|
||||
|
||||
using ModelData = QGenericItemModelDetails::ModelData<std::conditional_t<
|
||||
std::is_pointer_v<Range>,
|
||||
Range, std::remove_reference_t<Range>>
|
||||
>;
|
||||
public:
|
||||
explicit QGenericItemModelImpl(Range &&model, QGenericItemModel *itemModel)
|
||||
: QGenericItemModelImplBase(itemModel)
|
||||
, m_data{std::forward<Range>(model)}
|
||||
{
|
||||
initFrom(this);
|
||||
}
|
||||
|
||||
// static interface, called by QGenericItemModelImplBase
|
||||
static void callConst(ConstOp op, const QGenericItemModelImplBase *that, void *r, const void *args)
|
||||
{
|
||||
switch (op) {
|
||||
case Index: makeCall(that, &Self::index, r, args);
|
||||
break;
|
||||
case Parent: makeCall(that, &Structure::parent, r, args);
|
||||
break;
|
||||
case RowCount: makeCall(that, &Structure::rowCount, r, args);
|
||||
break;
|
||||
case ColumnCount: makeCall(that, &Structure::columnCount, r, args);
|
||||
break;
|
||||
case Flags: makeCall(that, &Self::flags, r, args);
|
||||
break;
|
||||
case HeaderData: makeCall(that, &Self::headerData, r, args);
|
||||
break;
|
||||
case Data: makeCall(that, &Self::data, r, args);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void call(Op op, QGenericItemModelImplBase *that, void *r, const void *args)
|
||||
{
|
||||
switch (op) {
|
||||
case Destroy: delete static_cast<Structure *>(that);
|
||||
break;
|
||||
case SetData: makeCall(that, &Self::setData, r, args);
|
||||
break;
|
||||
case ClearItemData: makeCall(that, &Self::clearItemData, r, args);
|
||||
break;
|
||||
case InsertColumns: makeCall(that, &Self::insertColumns, r, args);
|
||||
break;
|
||||
case RemoveColumns: makeCall(that, &Self::removeColumns, r, args);
|
||||
break;
|
||||
case InsertRows: makeCall(that, &Self::insertRows, r, args);
|
||||
break;
|
||||
case RemoveRows: makeCall(that, &Self::removeRows, r, args);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// actual implementations
|
||||
QModelIndex index(int row, int column, const QModelIndex &parent) const
|
||||
{
|
||||
if (row < 0 || column < 0 || column >= that().columnCount(parent)
|
||||
|| row >= that().rowCount(parent)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return that().indexImpl(row, column, parent);
|
||||
}
|
||||
|
||||
Qt::ItemFlags flags(const QModelIndex &index) const
|
||||
{
|
||||
if (!index.isValid())
|
||||
return Qt::NoItemFlags;
|
||||
|
||||
Qt::ItemFlags f = Structure::defaultFlags();
|
||||
|
||||
if constexpr (static_column_count <= 0) {
|
||||
if constexpr (isMutable())
|
||||
f |= Qt::ItemIsEditable;
|
||||
} else if constexpr (std::is_reference_v<row_reference> && !std::is_const_v<row_reference>) {
|
||||
// we want to know if the elements in the tuple are const; they'd always be, if
|
||||
// we didn't remove the const of the range first.
|
||||
const_row_reference row = rowData(index);
|
||||
row_reference mutableRow = const_cast<row_reference>(row);
|
||||
for_element_at(mutableRow, index.column(), [&f](auto &&ref){
|
||||
using target_type = decltype(ref);
|
||||
if constexpr (std::is_const_v<std::remove_reference_t<target_type>>)
|
||||
f &= ~Qt::ItemIsEditable;
|
||||
else if constexpr (std::is_lvalue_reference_v<target_type>)
|
||||
f |= Qt::ItemIsEditable;
|
||||
});
|
||||
}
|
||||
return f;
|
||||
}
|
||||
|
||||
QVariant headerData(int section, Qt::Orientation orientation, int role) const
|
||||
{
|
||||
QVariant result;
|
||||
if (role != Qt::DisplayRole || orientation != Qt::Horizontal
|
||||
|| section < 0 || section >= that().columnCount({})) {
|
||||
return m_itemModel->QAbstractItemModel::headerData(section, orientation, role);
|
||||
}
|
||||
|
||||
if constexpr (static_column_count >= 1) {
|
||||
const QMetaType metaType = meta_type_at<row_type>(section);
|
||||
if (metaType.isValid())
|
||||
result = QString::fromUtf8(metaType.name());
|
||||
}
|
||||
if (!result.isValid())
|
||||
result = m_itemModel->QAbstractItemModel::headerData(section, orientation, role);
|
||||
return result;
|
||||
}
|
||||
|
||||
QVariant data(const QModelIndex &index, int role) const
|
||||
{
|
||||
QVariant result;
|
||||
const auto readData = [&result, role](const auto &value) {
|
||||
if (role == Qt::DisplayRole || role == Qt::EditRole)
|
||||
result = read(value);
|
||||
};
|
||||
|
||||
if (index.isValid()) {
|
||||
const_row_reference row = rowData(index);
|
||||
if constexpr (dynamicColumns())
|
||||
readData(*std::next(std::cbegin(row), index.column()));
|
||||
else if constexpr (static_column_count == 0)
|
||||
readData(row);
|
||||
else
|
||||
for_element_at(row, index.column(), readData);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
bool setData(const QModelIndex &index, const QVariant &data, int role)
|
||||
{
|
||||
if (!index.isValid())
|
||||
return false;
|
||||
|
||||
bool success = false;
|
||||
if constexpr (isMutable()) {
|
||||
auto emitDataChanged = qScopeGuard([&success, this, &index, &role]{
|
||||
if (success) {
|
||||
Q_EMIT dataChanged(index, index, role == Qt::EditRole
|
||||
? QList<int>{} : QList{role});
|
||||
}
|
||||
});
|
||||
|
||||
const auto writeData = [&data, role](auto &&target) -> bool {
|
||||
if (role == Qt::DisplayRole || role == Qt::EditRole)
|
||||
return write(target, data);
|
||||
return false;
|
||||
};
|
||||
|
||||
row_reference row = rowData(index);
|
||||
if constexpr (dynamicColumns()) {
|
||||
success = writeData(*std::next(std::begin(row), index.column()));
|
||||
} else if constexpr (static_column_count == 0) {
|
||||
success = writeData(row);
|
||||
} else {
|
||||
for_element_at(row, index.column(), [&writeData, &success](auto &&target){
|
||||
using target_type = decltype(target);
|
||||
// we can only assign to an lvalue reference
|
||||
if constexpr (std::is_lvalue_reference_v<target_type>
|
||||
&& !std::is_const_v<std::remove_reference_t<target_type>>) {
|
||||
success = writeData(std::forward<target_type>(target));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
bool clearItemData(const QModelIndex &index)
|
||||
{
|
||||
if (!index.isValid())
|
||||
return false;
|
||||
|
||||
bool success = false;
|
||||
if constexpr (isMutable()) {
|
||||
auto emitDataChanged = qScopeGuard([&success, this, &index]{
|
||||
if (success)
|
||||
Q_EMIT dataChanged(index, index, {});
|
||||
});
|
||||
|
||||
row_reference row = rowData(index);
|
||||
if constexpr (dynamicColumns()) {
|
||||
*std::next(std::begin(row), index.column()) = {};
|
||||
success = true;
|
||||
} else if constexpr (static_column_count == 0) {
|
||||
row = row_type{};
|
||||
success = true;
|
||||
} else {
|
||||
for_element_at(row, index.column(), [&success](auto &&target){
|
||||
using target_type = decltype(target);
|
||||
if constexpr (std::is_lvalue_reference_v<target_type>
|
||||
&& !std::is_const_v<std::remove_reference_t<target_type>>) {
|
||||
target = {};
|
||||
success = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
bool insertColumns(int column, int count, const QModelIndex &parent)
|
||||
{
|
||||
if constexpr (dynamicColumns() && isMutable() && row_features::has_insert) {
|
||||
if (count == 0)
|
||||
return false;
|
||||
range_type * const children = childRange(parent);
|
||||
if (!children)
|
||||
return false;
|
||||
|
||||
beginInsertColumns(parent, column, column + count - 1);
|
||||
for (auto &child : *children)
|
||||
child.insert(std::next(std::begin(child), column), count, {});
|
||||
endInsertColumns();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool removeColumns(int column, int count, const QModelIndex &parent)
|
||||
{
|
||||
if constexpr (dynamicColumns() && isMutable() && row_features::has_erase) {
|
||||
if (column < 0 || column + count > that().columnCount(parent))
|
||||
return false;
|
||||
|
||||
range_type * const children = childRange(parent);
|
||||
if (!children)
|
||||
return false;
|
||||
|
||||
beginRemoveColumns(parent, column, column + count - 1);
|
||||
for (auto &child : *children) {
|
||||
const auto start = std::next(std::begin(child), column);
|
||||
child.erase(start, std::next(start, count));
|
||||
}
|
||||
endRemoveColumns();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool insertRows(int row, int count, const QModelIndex &parent)
|
||||
{
|
||||
if constexpr (Structure::canInsertRows()) {
|
||||
// If we operate on dynamic columns and cannot resize a newly
|
||||
// constructed row, then we cannot insert.
|
||||
if constexpr (dynamicColumns() && !row_features::has_resize)
|
||||
return false;
|
||||
range_type *children = childRange(parent);
|
||||
if (!children)
|
||||
return false;
|
||||
|
||||
beginInsertRows(parent, row, row + count - 1);
|
||||
|
||||
const auto pos = std::next(std::begin(*children), row);
|
||||
if constexpr (rows_are_pointers) {
|
||||
auto start = children->insert(pos, count, nullptr);
|
||||
auto end = std::next(start, count);
|
||||
for (auto it = start; it != end; ++it)
|
||||
*it = that().makeEmptyRow(parent);
|
||||
} else {
|
||||
row_type empty_value = that().makeEmptyRow(parent);
|
||||
children->insert(pos, count, empty_value);
|
||||
}
|
||||
|
||||
endInsertRows();
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool removeRows(int row, int count, const QModelIndex &parent = {})
|
||||
{
|
||||
if constexpr (Structure::canRemoveRows()) {
|
||||
const int prevRowCount = that().rowCount(parent);
|
||||
if (row < 0 || row + count > prevRowCount)
|
||||
return false;
|
||||
|
||||
range_type *children = childRange(parent);
|
||||
if (!children)
|
||||
return false;
|
||||
|
||||
beginRemoveRows(parent, row, row + count - 1);
|
||||
[[maybe_unused]] bool callEndRemoveColumns = false;
|
||||
if constexpr (dynamicColumns()) {
|
||||
// if we remove the last row in a dynamic model, then we no longer
|
||||
// know how many columns we should have, so they will be reported as 0.
|
||||
if (prevRowCount == count) {
|
||||
if (const int columns = that().columnCount(parent)) {
|
||||
callEndRemoveColumns = true;
|
||||
beginRemoveColumns(parent, 0, columns - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
{ // erase invalidates iterators
|
||||
const auto start = std::next(std::begin(*children), row);
|
||||
children->erase(start, std::next(start, count));
|
||||
}
|
||||
if constexpr (dynamicColumns()) {
|
||||
if (callEndRemoveColumns) {
|
||||
Q_ASSERT(that().columnCount(parent) == 0);
|
||||
endRemoveColumns();
|
||||
}
|
||||
}
|
||||
endRemoveRows();
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
protected:
|
||||
template <typename Value>
|
||||
static QVariant read(const Value &value)
|
||||
{
|
||||
if constexpr (std::is_constructible_v<QVariant, Value>)
|
||||
return QVariant(value);
|
||||
else
|
||||
return QVariant::fromValue(value);
|
||||
}
|
||||
template <typename Value>
|
||||
static QVariant read(Value *value)
|
||||
{
|
||||
if (value) {
|
||||
if constexpr (std::is_constructible_v<QVariant, Value *>)
|
||||
return QVariant(value);
|
||||
else
|
||||
return read(*value);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
template <typename Target>
|
||||
static bool write(Target &target, const QVariant &value)
|
||||
{
|
||||
using Type = std::remove_reference_t<Target>;
|
||||
if constexpr (std::is_constructible_v<Target, QVariant>) {
|
||||
target = value;
|
||||
return true;
|
||||
} else if (value.canConvert<Type>()) {
|
||||
target = value.value<Type>();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
template <typename Target>
|
||||
static bool write(Target *target, const QVariant &value)
|
||||
{
|
||||
if (target)
|
||||
return write(*target, value);
|
||||
return false;
|
||||
}
|
||||
|
||||
// helpers
|
||||
const_row_reference rowData(const QModelIndex &index) const
|
||||
{
|
||||
Q_ASSERT(index.isValid());
|
||||
return that().rowDataImpl(index);
|
||||
}
|
||||
|
||||
row_reference rowData(const QModelIndex &index)
|
||||
{
|
||||
Q_ASSERT(index.isValid());
|
||||
return that().rowDataImpl(index);
|
||||
}
|
||||
|
||||
const range_type *childRange(const QModelIndex &index) const
|
||||
{
|
||||
if (!index.isValid())
|
||||
return m_data.model();
|
||||
if (index.column()) // only items at column 0 can have children
|
||||
return nullptr;
|
||||
return that().childRangeImpl(index);
|
||||
}
|
||||
|
||||
range_type *childRange(const QModelIndex &index)
|
||||
{
|
||||
if (!index.isValid())
|
||||
return m_data.model();
|
||||
if (index.column()) // only items at column 0 can have children
|
||||
return nullptr;
|
||||
return that().childRangeImpl(index);
|
||||
}
|
||||
|
||||
ModelData m_data;
|
||||
};
|
||||
|
||||
template <typename Range, typename Protocol>
|
||||
class QGenericItemModelStructureImpl : public QGenericItemModelImpl<
|
||||
QGenericItemModelStructureImpl<Range, Protocol>,
|
||||
Range>
|
||||
{};
|
||||
|
||||
// specialization for flat models without protocol
|
||||
template <typename Range>
|
||||
class QGenericItemModelStructureImpl<Range, void> : public QGenericItemModelImpl<
|
||||
QGenericItemModelStructureImpl<Range, void>,
|
||||
Range>
|
||||
{
|
||||
using Base = QGenericItemModelImpl<QGenericItemModelStructureImpl<Range, void>, Range>;
|
||||
friend class QGenericItemModelImpl<QGenericItemModelStructureImpl<Range, void>, Range>;
|
||||
|
||||
using range_features = typename Base::range_features;
|
||||
using row_type = typename Base::row_type;
|
||||
using row_traits = typename Base::row_traits;
|
||||
using row_features = typename Base::row_features;
|
||||
|
||||
public:
|
||||
explicit QGenericItemModelStructureImpl(Range &&model, QGenericItemModel *itemModel)
|
||||
: Base(std::forward<Range>(model), itemModel)
|
||||
{}
|
||||
|
||||
protected:
|
||||
QModelIndex indexImpl(int row, int column, const QModelIndex &) const
|
||||
{
|
||||
if constexpr (Base::dynamicColumns()) {
|
||||
if (column < int(Base::size(*std::next(std::cbegin(*this->m_data.model()), row))))
|
||||
return this->createIndex(row, column);
|
||||
// if we got here, then column < columnCount(), but this row is to short
|
||||
qCritical("QGenericItemModel: Column-range at row %d is not large enough!", row);
|
||||
return {};
|
||||
} else {
|
||||
return this->createIndex(row, column);
|
||||
}
|
||||
}
|
||||
|
||||
QModelIndex parent(const QModelIndex &) const
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
int rowCount(const QModelIndex &parent) const
|
||||
{
|
||||
if (parent.isValid())
|
||||
return 0;
|
||||
return int(Base::size(*this->m_data.model()));
|
||||
}
|
||||
|
||||
int columnCount(const QModelIndex &parent) const
|
||||
{
|
||||
if (parent.isValid())
|
||||
return 0;
|
||||
|
||||
// in a table, all rows have the same number of columns (as the first row)
|
||||
if constexpr (Base::dynamicColumns()) {
|
||||
return int(Base::size(*this->m_data.model()) == 0
|
||||
? 0
|
||||
: Base::size(*std::cbegin(*this->m_data.model())));
|
||||
} else if constexpr (Base::static_column_count == 0) {
|
||||
return row_traits::fixed_size();
|
||||
} else {
|
||||
return Base::static_column_count;
|
||||
}
|
||||
}
|
||||
|
||||
static constexpr Qt::ItemFlags defaultFlags()
|
||||
{
|
||||
return Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemNeverHasChildren;
|
||||
}
|
||||
|
||||
static constexpr bool canInsertRows()
|
||||
{
|
||||
return Base::dynamicRows() && range_features::has_insert;
|
||||
}
|
||||
|
||||
static constexpr bool canRemoveRows()
|
||||
{
|
||||
return Base::dynamicRows() && range_features::has_erase;
|
||||
}
|
||||
|
||||
auto makeEmptyRow(const QModelIndex &)
|
||||
{
|
||||
if constexpr (Base::dynamicColumns()) {
|
||||
// all rows have to have the same column count
|
||||
row_type empty_row;
|
||||
if constexpr (row_features::has_resize)
|
||||
empty_row.resize(Base::m_itemModel->columnCount());
|
||||
return empty_row;
|
||||
} else {
|
||||
return row_type{};
|
||||
}
|
||||
}
|
||||
|
||||
decltype(auto) rowDataImpl(const QModelIndex &index) const
|
||||
{
|
||||
Q_ASSERT(index.row() < int(Base::size(*this->m_data.model())));
|
||||
return *(std::next(std::cbegin(*this->m_data.model()), index.row()));
|
||||
}
|
||||
|
||||
decltype(auto) rowDataImpl(const QModelIndex &index)
|
||||
{
|
||||
Q_ASSERT(index.row() < int(Base::size(*this->m_data.model())));
|
||||
return *(std::next(std::begin(*this->m_data.model()), index.row()));
|
||||
}
|
||||
|
||||
auto childRangeImpl(const QModelIndex &) const
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto childRangeImpl(const QModelIndex &)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
};
|
||||
|
||||
template <typename Range,
|
||||
QGenericItemModelDetails::if_is_range<Range>>
|
||||
QGenericItemModel::QGenericItemModel(Range &&range, QObject *parent)
|
||||
: QAbstractItemModel(parent)
|
||||
, impl(new QGenericItemModelStructureImpl<Range, void>(std::forward<Range>(range), this))
|
||||
{}
|
||||
|
||||
QT_END_NAMESPACE
|
||||
|
||||
#endif // QGENERICITEMMODEL_H
|
334
src/corelib/itemmodels/qgenericitemmodel_impl.h
Normal file
334
src/corelib/itemmodels/qgenericitemmodel_impl.h
Normal file
@ -0,0 +1,334 @@
|
||||
// Copyright (C) 2025 The Qt Company Ltd.
|
||||
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
|
||||
|
||||
#ifndef QGENERICITEMMODEL_IMPL_H
|
||||
#define QGENERICITEMMODEL_IMPL_H
|
||||
|
||||
#ifndef Q_QDOC
|
||||
|
||||
#ifndef QGENERICITEMMODEL_H
|
||||
#error Do not include qgenericitemmodel_impl.h directly
|
||||
#endif
|
||||
|
||||
#if 0
|
||||
#pragma qt_sync_skip_header_check
|
||||
#pragma qt_sync_stop_processing
|
||||
#endif
|
||||
|
||||
#include <QtCore/qabstractitemmodel.h>
|
||||
#include <QtCore/qmetaobject.h>
|
||||
#include <QtCore/qvariant.h>
|
||||
|
||||
#include <functional>
|
||||
#include <type_traits>
|
||||
#include <QtCore/q20type_traits.h>
|
||||
#include <tuple>
|
||||
#include <QtCore/q23utility.h>
|
||||
|
||||
QT_BEGIN_NAMESPACE
|
||||
|
||||
namespace QGenericItemModelDetails
|
||||
{
|
||||
// Test if a type is a range, and whether we can modify it using the
|
||||
// standard C++ container member functions insert, erase, and resize.
|
||||
// For the sake of QAIM, we cannot modify a range if it holds const data
|
||||
// even if the range itself is not const; we'd need to initialize new rows
|
||||
// and columns, and move old row and column data.
|
||||
template <typename C, typename = void>
|
||||
struct test_insert : std::false_type {};
|
||||
|
||||
template <typename C>
|
||||
struct test_insert<C, std::void_t<decltype(std::declval<C>().insert(
|
||||
std::declval<typename C::const_iterator>(),
|
||||
std::declval<typename C::size_type>(),
|
||||
std::declval<typename C::value_type>()
|
||||
))>>
|
||||
: std::true_type
|
||||
{};
|
||||
|
||||
template <typename C, typename = void>
|
||||
struct test_erase : std::false_type {};
|
||||
|
||||
template <typename C>
|
||||
struct test_erase<C, std::void_t<decltype(std::declval<C>().erase(
|
||||
std::declval<typename C::const_iterator>(),
|
||||
std::declval<typename C::const_iterator>()
|
||||
))>>
|
||||
: std::true_type
|
||||
{};
|
||||
|
||||
template <typename C, typename = void>
|
||||
struct test_resize : std::false_type {};
|
||||
|
||||
template <typename C>
|
||||
struct test_resize<C, std::void_t<decltype(std::declval<C>().resize(
|
||||
std::declval<typename C::size_type>(),
|
||||
std::declval<typename C::value_type>()
|
||||
))>>
|
||||
: std::true_type
|
||||
{};
|
||||
|
||||
template <typename C, typename = void>
|
||||
struct test_size : std::false_type {};
|
||||
template <typename C>
|
||||
struct test_size<C, std::void_t<decltype(std::size(std::declval<C&>()))>> : std::true_type {};
|
||||
|
||||
template <typename C, typename = void>
|
||||
struct range_traits : std::false_type {
|
||||
static constexpr bool is_mutable = !std::is_const_v<C>;
|
||||
static constexpr bool has_insert = false;
|
||||
static constexpr bool has_erase = false;
|
||||
static constexpr bool has_resize = false;
|
||||
};
|
||||
template <typename C>
|
||||
struct range_traits<C, std::void_t<decltype(std::cbegin(std::declval<C&>())),
|
||||
decltype(std::cend(std::declval<C&>()))
|
||||
>> : std::true_type
|
||||
{
|
||||
using value_type = std::remove_reference_t<decltype(*std::begin(std::declval<C&>()))>;
|
||||
static constexpr bool is_mutable = !std::is_const_v<C> && !std::is_const_v<value_type>;
|
||||
static constexpr bool has_insert = test_insert<C>();
|
||||
static constexpr bool has_erase = test_erase<C>();
|
||||
static constexpr bool has_resize = test_resize<C>();
|
||||
};
|
||||
|
||||
// Specializations for types that look like ranges, but should be
|
||||
// treated as values.
|
||||
enum class Mutable { Yes, No };
|
||||
template <Mutable IsMutable>
|
||||
struct iterable_value : std::false_type {
|
||||
static constexpr bool is_mutable = IsMutable == Mutable::Yes;
|
||||
static constexpr bool has_insert = false;
|
||||
static constexpr bool has_erase = false;
|
||||
static constexpr bool has_resize = false;
|
||||
};
|
||||
template <> struct range_traits<QByteArray> : iterable_value<Mutable::Yes> {};
|
||||
template <> struct range_traits<QString> : iterable_value<Mutable::Yes> {};
|
||||
template <class CharT, class Traits, class Allocator>
|
||||
struct range_traits<std::basic_string<CharT, Traits, Allocator>> : iterable_value<Mutable::Yes>
|
||||
{};
|
||||
|
||||
// const T * and views are read-only
|
||||
template <typename T> struct range_traits<const T *> : iterable_value<Mutable::No> {};
|
||||
template <> struct range_traits<QLatin1StringView> : iterable_value<Mutable::No> {};
|
||||
|
||||
template <typename C>
|
||||
[[maybe_unused]] static constexpr bool is_range_v = range_traits<C>();
|
||||
template <typename CC>
|
||||
using if_is_range = std::enable_if_t<
|
||||
is_range_v<std::remove_pointer_t<std::remove_reference_t<CC>>>, bool>;
|
||||
|
||||
// Find out how many fixed elements can be retrieved from a row element.
|
||||
// main template for simple values and ranges. Specializing for ranges
|
||||
// is ambiguous with arrays, as they are also ranges
|
||||
template <typename T, typename = void>
|
||||
struct row_traits {
|
||||
static constexpr bool is_range = is_range_v<q20::remove_cvref_t<T>>;
|
||||
// a static size of -1 indicates dynamically sized range
|
||||
static constexpr int static_size = is_range ? -1 : 0;
|
||||
static constexpr int fixed_size() { return 1; }
|
||||
};
|
||||
|
||||
// Specialization for tuples, using std::tuple_size
|
||||
template <typename T>
|
||||
struct row_traits<T, std::void_t<std::tuple_element_t<0, T>>> {
|
||||
static constexpr std::size_t size64= std::tuple_size_v<T>;
|
||||
static_assert(q20::in_range<int>(size64));
|
||||
static constexpr int static_size = int(size64);
|
||||
static constexpr int fixed_size() { return 0; }
|
||||
};
|
||||
|
||||
// Specialization for C arrays
|
||||
template <typename T, std::size_t N>
|
||||
struct row_traits<T[N]>
|
||||
{
|
||||
static_assert(q20::in_range<int>(N));
|
||||
static constexpr int static_size = int(N);
|
||||
static constexpr int fixed_size() { return 0; }
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
[[maybe_unused]] static constexpr int static_size_v =
|
||||
row_traits<q20::remove_cvref_t<std::remove_pointer_t<T>>>::static_size;
|
||||
|
||||
template <typename T> static auto pointerTo(T *t) { return t; }
|
||||
template <typename T> static auto pointerTo(T &t) { return std::addressof(t); }
|
||||
template <typename T> static auto pointerTo(const T &&t) = delete;
|
||||
|
||||
// The storage of the model data. We might store it as a pointer, or as a
|
||||
// (copied- or moved-into) value. But we always return a pointer.
|
||||
template <typename ModelStorage>
|
||||
struct ModelData
|
||||
{
|
||||
using ModelPtr = std::conditional_t<std::is_pointer_v<ModelStorage>,
|
||||
ModelStorage, ModelStorage *>;
|
||||
using ConstModelPtr = std::conditional_t<std::is_pointer_v<ModelStorage>,
|
||||
const ModelStorage, const ModelStorage *>;
|
||||
|
||||
ModelPtr model() { return pointerTo(m_model); }
|
||||
ConstModelPtr model() const { return pointerTo(m_model); }
|
||||
|
||||
ModelStorage m_model;
|
||||
};
|
||||
} // QGenericItemModelDetails
|
||||
|
||||
class QGenericItemModel;
|
||||
|
||||
class QGenericItemModelImplBase
|
||||
{
|
||||
Q_DISABLE_COPY_MOVE(QGenericItemModelImplBase)
|
||||
protected:
|
||||
// Helpers for calling a lambda with the tuple element at a runtime index.
|
||||
template <typename Tuple, typename F, size_t ...Is>
|
||||
static void call_at(Tuple &&tuple, size_t idx, std::index_sequence<Is...>, F &&function)
|
||||
{
|
||||
((Is == idx ? static_cast<void>(function(get<Is>(std::forward<Tuple>(tuple))))
|
||||
: static_cast<void>(0)), ...);
|
||||
}
|
||||
|
||||
template <typename Tuple, typename F, size_t ...Is>
|
||||
static void call_at(Tuple *tuple, size_t idx, std::index_sequence<Is...> seq, F &&function)
|
||||
{
|
||||
if (tuple)
|
||||
call_at(*tuple, idx, seq, std::forward<F>(function));
|
||||
}
|
||||
|
||||
template <typename T, typename F>
|
||||
static auto for_element_at(T &&tuple, size_t idx, F &&function)
|
||||
{
|
||||
using type = std::remove_pointer_t<std::remove_reference_t<T>>;
|
||||
constexpr size_t size = std::tuple_size_v<type>;
|
||||
Q_ASSERT(idx < size);
|
||||
return call_at(std::forward<T>(tuple), idx, std::make_index_sequence<size>{},
|
||||
std::forward<F>(function));
|
||||
}
|
||||
|
||||
// Get the QMetaType for a tuple-element at a runtime index.
|
||||
// Used in the headerData implementation.
|
||||
template <typename Tuple, std::size_t ...I>
|
||||
static constexpr std::array<QMetaType, sizeof...(I)> makeMetaTypes(std::index_sequence<I...>)
|
||||
{
|
||||
return {{QMetaType::fromType<q20::remove_cvref_t<std::tuple_element_t<I, Tuple>>>()...}};
|
||||
}
|
||||
template <typename T>
|
||||
static constexpr QMetaType meta_type_at(size_t idx)
|
||||
{
|
||||
using type = std::remove_pointer_t<std::remove_reference_t<T>>;
|
||||
constexpr auto size = std::tuple_size_v<type>;
|
||||
Q_ASSERT(idx < size);
|
||||
return makeMetaTypes<type>(std::make_index_sequence<size>{}).at(idx);
|
||||
}
|
||||
|
||||
// Helpers to call a given member function with the correct arguments.
|
||||
template <typename Class, typename T, typename F, size_t...I>
|
||||
static auto apply(std::integer_sequence<size_t, I...>, Class* obj, F&& fn, T&& tuple)
|
||||
{
|
||||
return std::invoke(fn, obj, std::get<I>(tuple)...);
|
||||
}
|
||||
template <typename Ret, typename Class, typename ...Args>
|
||||
static void makeCall(QGenericItemModelImplBase *obj, Ret(Class::* &&fn)(Args...),
|
||||
void *ret, const void *args)
|
||||
{
|
||||
const auto &tuple = *static_cast<const std::tuple<Args&...> *>(args);
|
||||
*static_cast<Ret *>(ret) = apply(std::make_index_sequence<sizeof...(Args)>{},
|
||||
static_cast<Class *>(obj), fn, tuple);
|
||||
}
|
||||
template <typename Ret, typename Class, typename ...Args>
|
||||
static void makeCall(const QGenericItemModelImplBase *obj, Ret(Class::* &&fn)(Args...) const,
|
||||
void *ret, const void *args)
|
||||
{
|
||||
const auto &tuple = *static_cast<const std::tuple<Args&...> *>(args);
|
||||
*static_cast<Ret *>(ret) = apply(std::make_index_sequence<sizeof...(Args)>{},
|
||||
static_cast<const Class *>(obj), fn, tuple);
|
||||
}
|
||||
|
||||
public:
|
||||
enum ConstOp {
|
||||
Index,
|
||||
Parent,
|
||||
RowCount,
|
||||
ColumnCount,
|
||||
Flags,
|
||||
HeaderData,
|
||||
Data,
|
||||
};
|
||||
|
||||
enum Op {
|
||||
Destroy,
|
||||
SetData,
|
||||
ClearItemData,
|
||||
InsertColumns,
|
||||
RemoveColumns,
|
||||
InsertRows,
|
||||
RemoveRows,
|
||||
};
|
||||
|
||||
void destroy()
|
||||
{
|
||||
call<bool>(Destroy);
|
||||
}
|
||||
|
||||
private:
|
||||
// prototypes
|
||||
static void callConst(ConstOp, const QGenericItemModelImplBase *, void *, const void *);
|
||||
static void call(Op, QGenericItemModelImplBase *, void *, const void *);
|
||||
|
||||
using CallConstFN = decltype(callConst);
|
||||
using CallTupleFN = decltype(call);
|
||||
|
||||
CallConstFN *callConst_fn;
|
||||
CallTupleFN *call_fn;
|
||||
|
||||
protected:
|
||||
explicit QGenericItemModelImplBase(QGenericItemModel *itemModel)
|
||||
: m_itemModel(itemModel)
|
||||
{}
|
||||
~QGenericItemModelImplBase() = default;
|
||||
|
||||
QGenericItemModel *m_itemModel;
|
||||
|
||||
inline QModelIndex createIndex(int row, int column, const void *ptr = nullptr) const;
|
||||
inline QHash<int, QByteArray> roleNames() const;
|
||||
inline void dataChanged(const QModelIndex &from, const QModelIndex &to,
|
||||
const QList<int> &roles);
|
||||
inline void beginInsertColumns(const QModelIndex &parent, int start, int count);
|
||||
inline void endInsertColumns();
|
||||
inline void beginRemoveColumns(const QModelIndex &parent, int start, int count);
|
||||
inline void endRemoveColumns();
|
||||
inline void beginInsertRows(const QModelIndex &parent, int start, int count);
|
||||
inline void endInsertRows();
|
||||
inline void beginRemoveRows(const QModelIndex &parent, int start, int count);
|
||||
inline void endRemoveRows();
|
||||
|
||||
template <typename Impl>
|
||||
void initFrom(Impl *)
|
||||
{
|
||||
callConst_fn = &Impl::callConst;
|
||||
call_fn = &Impl::call;
|
||||
}
|
||||
|
||||
public:
|
||||
template <typename Ret, typename ...Args>
|
||||
Ret callConst(ConstOp op, const Args &...args) const
|
||||
{
|
||||
Ret ret = {};
|
||||
const auto tuple = std::tie(args...);
|
||||
callConst_fn(op, this, &ret, &tuple);
|
||||
return ret;
|
||||
}
|
||||
|
||||
template <typename Ret, typename ...Args>
|
||||
Ret call(Op op, const Args &...args)
|
||||
{
|
||||
Ret ret = {};
|
||||
const auto tuple = std::tie(args...);
|
||||
call_fn(op, this, &ret, &tuple);
|
||||
return ret;
|
||||
}
|
||||
};
|
||||
|
||||
QT_END_NAMESPACE
|
||||
|
||||
#endif // Q_QDOC
|
||||
|
||||
#endif // QGENERICITEMMODEL_IMPL_H
|
@ -2,6 +2,7 @@
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
add_subdirectory(qstringlistmodel)
|
||||
add_subdirectory(qgenericitemmodel)
|
||||
if(TARGET Qt::Gui)
|
||||
add_subdirectory(qabstractitemmodel)
|
||||
if(QT_FEATURE_proxymodel)
|
||||
|
@ -0,0 +1,25 @@
|
||||
# Copyright (C) 2025 The Qt Company Ltd.
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
#####################################################################
|
||||
## tst_qgenericitemmodel Test:
|
||||
#####################################################################
|
||||
|
||||
if(NOT QT_BUILD_STANDALONE_TESTS AND NOT QT_BUILDING_QT)
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
project(tst_qgenericitemmodel LANGUAGES CXX)
|
||||
find_package(Qt6BuildInternals REQUIRED COMPONENTS STANDALONE_TEST)
|
||||
endif()
|
||||
|
||||
qt_internal_add_test(tst_qgenericitemmodel
|
||||
SOURCES
|
||||
tst_qgenericitemmodel.cpp
|
||||
LIBRARIES
|
||||
Qt::Gui
|
||||
)
|
||||
|
||||
if (NOT INTEGRITY AND NOT VXWORKS)
|
||||
if ("${CMAKE_CXX_COMPILE_FEATURES}" MATCHES "cxx_std_20")
|
||||
set_property(TARGET tst_qgenericitemmodel PROPERTY CXX_STANDARD 20)
|
||||
endif()
|
||||
endif()
|
@ -0,0 +1,573 @@
|
||||
// Copyright (C) 2025 The Qt Company Ltd.
|
||||
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
|
||||
|
||||
#include <QTest>
|
||||
#include <QtCore/qgenericitemmodel.h>
|
||||
#include <QtCore/qjsondocument.h>
|
||||
#include <QtCore/qjsonarray.h>
|
||||
|
||||
#include <QtGui/qcolor.h>
|
||||
|
||||
#if QT_CONFIG(itemmodeltester)
|
||||
#include <QtTest/qabstractitemmodeltester.h>
|
||||
#endif
|
||||
|
||||
#include <vector>
|
||||
|
||||
#if defined(__cpp_lib_ranges)
|
||||
#include <ranges>
|
||||
#endif
|
||||
|
||||
struct Row
|
||||
{
|
||||
QString m_item;
|
||||
int m_number;
|
||||
QString m_description;
|
||||
|
||||
template <size_t I, typename RowType,
|
||||
std::enable_if_t<(I < 3), bool> = true,
|
||||
std::enable_if_t<std::is_same_v<q20::remove_cvref_t<RowType>, Row>, bool> = true
|
||||
>
|
||||
friend inline decltype(auto) get(RowType &&item)
|
||||
{
|
||||
if constexpr (I == 0)
|
||||
return q23::forward_like<RowType>(item.m_item);
|
||||
else if constexpr (I == 1)
|
||||
return q23::forward_like<RowType>(item.m_number);
|
||||
else // if constexpr (I == 2)
|
||||
return q23::forward_like<RowType>(item.m_description);
|
||||
}
|
||||
};
|
||||
|
||||
namespace std {
|
||||
template <> struct tuple_size<Row> : std::integral_constant<size_t, 3> {};
|
||||
template <> struct tuple_element<0, Row> { using type = QString; };
|
||||
template <> struct tuple_element<1, Row> { using type = int; };
|
||||
template <> struct tuple_element<2, Row> { using type = QString; };
|
||||
}
|
||||
|
||||
struct ConstRow
|
||||
{
|
||||
QString value;
|
||||
|
||||
template<size_t I,
|
||||
std::enable_if_t<I == 0, bool> = true
|
||||
>
|
||||
friend inline decltype(auto) get(const ConstRow &row)
|
||||
{
|
||||
if constexpr (I == 0)
|
||||
return row.value;
|
||||
}
|
||||
};
|
||||
|
||||
namespace std {
|
||||
template <> struct tuple_size<ConstRow> : std::integral_constant<size_t, 1> {};
|
||||
template <> struct tuple_element<0, ConstRow> { using type = QString; };
|
||||
}
|
||||
|
||||
class tst_QGenericItemModel : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private slots:
|
||||
void basics_data() { createTestData(); }
|
||||
void basics();
|
||||
void minimalIterator();
|
||||
void ranges();
|
||||
void json();
|
||||
|
||||
void dimensions_data() { createTestData(); }
|
||||
void dimensions();
|
||||
void flags_data() { createTestData(); }
|
||||
void flags();
|
||||
void data_data() { createTestData(); }
|
||||
void data();
|
||||
void setData_data() { createTestData(); }
|
||||
void setData();
|
||||
void clearItemData_data() { createTestData(); }
|
||||
void clearItemData();
|
||||
void insertRows_data() { createTestData(); }
|
||||
void insertRows();
|
||||
void removeRows_data() { createTestData(); }
|
||||
void removeRows();
|
||||
void insertColumns_data() { createTestData(); }
|
||||
void insertColumns();
|
||||
void removeColumns_data() { createTestData(); }
|
||||
void removeColumns();
|
||||
|
||||
void inconsistentColumnCount();
|
||||
|
||||
private:
|
||||
void createTestData();
|
||||
|
||||
struct Data {
|
||||
|
||||
// fixed number of columns and rows
|
||||
std::array<int, 5> fixedArrayOfNumbers = {1, 2, 3, 4, 5};
|
||||
int cArrayOfNumbers[5] = {1, 2, 3, 4, 5};
|
||||
Row cArrayFixedColumns[3] = {
|
||||
{"red", 0xff0000, "The color red"},
|
||||
{"green", 0x00ff00, "The color green"},
|
||||
{"blue", 0x0000ff, "The color blue"}
|
||||
};
|
||||
|
||||
// dynamic number of rows, fixed number of columns
|
||||
std::vector<std::tuple<int, QString>> vectorOfFixedColumns = {
|
||||
{0, "null"},
|
||||
{1, "one"},
|
||||
{2, "two"},
|
||||
{3, "three"},
|
||||
{4, "four"},
|
||||
};
|
||||
std::vector<std::array<int, 10>> vectorOfArrays = {
|
||||
{1, 2, 3, 4, 5, 6, 7, 8, 9, 10},
|
||||
{11, 12, 13, 14, 15, 16, 17, 18, 19, 20},
|
||||
{21, 22, 23, 24, 25, 26, 27, 28, 29, 30},
|
||||
{31, 32, 33, 34, 35, 36, 37, 38, 39, 40},
|
||||
{41, 42, 43, 44, 45, 46, 47, 48, 49, 50},
|
||||
};
|
||||
std::vector<Row> vectorOfStructs = {
|
||||
{"red", 1, "one"},
|
||||
{"green", 2, "two"},
|
||||
{"blue", 3, "three"},
|
||||
};
|
||||
|
||||
// bad (but legal) get() overload that never returns a mutable reference
|
||||
std::vector<ConstRow> vectorOfConstStructs = {
|
||||
{"one"},
|
||||
{"two"},
|
||||
{"three"},
|
||||
};
|
||||
|
||||
// dynamic number of rows and columns
|
||||
std::vector<std::vector<double>> tableOfNumbers = {
|
||||
{1.0, 2.0, 3.0, 4.0, 5.0},
|
||||
{6.0, 7.0, 8.0, 9.0, 10.0},
|
||||
{11.0, 12.0, 13.0, 14.0, 15.0},
|
||||
{16.0, 17.0, 18.0, 19.0, 20.0},
|
||||
{21.0, 22.0, 23.0, 24.0, 25.0},
|
||||
};
|
||||
|
||||
// rows are pointers
|
||||
Row rowAsPointer = {"blue", 0x0000ff, "Blau"};
|
||||
std::vector<Row *> tableOfRowPointers = {
|
||||
&rowAsPointer,
|
||||
&rowAsPointer,
|
||||
&rowAsPointer,
|
||||
};
|
||||
|
||||
// constness
|
||||
std::array<const int, 5> arrayOfConstNumbers = { 1, 2, 3, 4 };
|
||||
// note: std::vector doesn't allow for const value types
|
||||
const std::vector<int> constListOfNumbers = { 1, 2, 3 };
|
||||
|
||||
// const model is read-only
|
||||
const std::vector<std::vector<double>> constTableOfNumbers = {
|
||||
{1.0, 2.0, 3.0, 4.0, 5.0},
|
||||
{6.0, 7.0, 8.0, 9.0, 10.0},
|
||||
{11.0, 12.0, 13.0, 14.0, 15.0},
|
||||
{16.0, 17.0, 18.0, 19.0, 20.0},
|
||||
{21.0, 22.0, 23.0, 24.0, 25.0},
|
||||
};
|
||||
};
|
||||
|
||||
std::unique_ptr<Data> m_data;
|
||||
|
||||
public:
|
||||
enum class ChangeAction
|
||||
{
|
||||
ReadOnly = 0x00,
|
||||
InsertRows = 0x01,
|
||||
RemoveRows = 0x02,
|
||||
ChangeRows = InsertRows | RemoveRows,
|
||||
InsertColumns = 0x04,
|
||||
RemoveColumns = 0x08,
|
||||
ChangeColumns = InsertColumns | RemoveColumns,
|
||||
SetData = 0x10,
|
||||
All = ChangeRows | ChangeColumns | SetData
|
||||
};
|
||||
Q_DECLARE_FLAGS(ChangeActions, ChangeAction);
|
||||
};
|
||||
|
||||
Q_DECLARE_OPERATORS_FOR_FLAGS(tst_QGenericItemModel::ChangeActions)
|
||||
|
||||
using Factory = std::function<std::unique_ptr<QAbstractItemModel>()>;
|
||||
|
||||
void tst_QGenericItemModel::createTestData()
|
||||
{
|
||||
m_data.reset(new Data);
|
||||
|
||||
QTest::addColumn<Factory>("factory");
|
||||
QTest::addColumn<int>("expectedRowCount");
|
||||
QTest::addColumn<int>("expectedColumnCount");
|
||||
QTest::addColumn<ChangeActions>("changeActions");
|
||||
|
||||
Factory factory;
|
||||
|
||||
#define ADD_HELPER(Model, Tag, Ref) \
|
||||
factory = [this]() -> std::unique_ptr<QAbstractItemModel> { \
|
||||
return std::unique_ptr<QAbstractItemModel>(new QGenericItemModel(Ref->Model)); \
|
||||
}; \
|
||||
QTest::addRow(#Model #Tag) << factory << int(std::size(m_data->Model)) \
|
||||
|
||||
#define ADD_POINTER(Model) \
|
||||
ADD_HELPER(Model, Pointer, &m_data) \
|
||||
|
||||
#define ADD_COPY(Model) \
|
||||
ADD_HELPER(Model, Copy, m_data) \
|
||||
|
||||
// POINTER-tests will modify the data structure that lives in m_data,
|
||||
// so we have to run tests on copies of that data first for each type,
|
||||
// or only run POINTER-tests.
|
||||
// The entire test data is recreated for each test function, but test
|
||||
// functions must not change data structures other than the one tested.
|
||||
|
||||
ADD_COPY(fixedArrayOfNumbers)
|
||||
<< 1 << ChangeActions(ChangeAction::SetData);
|
||||
ADD_POINTER(fixedArrayOfNumbers)
|
||||
<< 1 << ChangeActions(ChangeAction::SetData);
|
||||
ADD_POINTER(cArrayOfNumbers)
|
||||
<< 1 << ChangeActions(ChangeAction::SetData);
|
||||
|
||||
ADD_POINTER(cArrayFixedColumns)
|
||||
<< int(std::tuple_size_v<Row>) << ChangeActions(ChangeAction::SetData);
|
||||
|
||||
ADD_COPY(vectorOfFixedColumns)
|
||||
<< 2 << (ChangeAction::ChangeRows | ChangeAction::SetData);
|
||||
ADD_POINTER(vectorOfFixedColumns)
|
||||
<< 2 << (ChangeAction::ChangeRows | ChangeAction::SetData);
|
||||
ADD_COPY(vectorOfArrays)
|
||||
<< 10 << (ChangeAction::ChangeRows | ChangeAction::SetData);
|
||||
ADD_POINTER(vectorOfArrays)
|
||||
<< 10 << (ChangeAction::ChangeRows | ChangeAction::SetData);
|
||||
ADD_COPY(vectorOfStructs)
|
||||
<< int(std::tuple_size_v<Row>) << (ChangeAction::ChangeRows | ChangeAction::SetData);
|
||||
ADD_POINTER(vectorOfStructs)
|
||||
<< int(std::tuple_size_v<Row>) << (ChangeAction::ChangeRows | ChangeAction::SetData);
|
||||
|
||||
ADD_COPY(vectorOfConstStructs)
|
||||
<< int(std::tuple_size_v<ConstRow>) << ChangeActions(ChangeAction::ChangeRows);
|
||||
ADD_POINTER(vectorOfConstStructs)
|
||||
<< int(std::tuple_size_v<ConstRow>) << ChangeActions(ChangeAction::ChangeRows);
|
||||
|
||||
ADD_COPY(tableOfNumbers)
|
||||
<< 5 << ChangeActions(ChangeAction::All);
|
||||
ADD_POINTER(tableOfNumbers)
|
||||
<< 5 << ChangeActions(ChangeAction::All);
|
||||
// only adding as pointer, copy would operate on the same data
|
||||
ADD_POINTER(tableOfRowPointers)
|
||||
<< int(std::tuple_size_v<Row>) << (ChangeAction::ChangeRows | ChangeAction::SetData);
|
||||
|
||||
ADD_COPY(arrayOfConstNumbers)
|
||||
<< 1 << ChangeActions(ChangeAction::ReadOnly);
|
||||
ADD_POINTER(arrayOfConstNumbers)
|
||||
<< 1 << ChangeActions(ChangeAction::ReadOnly);
|
||||
|
||||
ADD_COPY(constListOfNumbers)
|
||||
<< 1 << ChangeActions(ChangeAction::ReadOnly);
|
||||
ADD_POINTER(constListOfNumbers)
|
||||
<< 1 << ChangeActions(ChangeAction::ReadOnly);
|
||||
|
||||
ADD_COPY(constTableOfNumbers)
|
||||
<< 5 << ChangeActions(ChangeAction::ReadOnly);
|
||||
ADD_POINTER(constTableOfNumbers)
|
||||
<< 5 << ChangeActions(ChangeAction::ReadOnly);
|
||||
|
||||
#undef ADD_COPY
|
||||
#undef ADD_POINTER
|
||||
#undef ADD_HELPER
|
||||
|
||||
QTest::addRow("Moved table") << Factory([]{
|
||||
QList<std::vector<QString>> movedTable = {
|
||||
{"0/0", "0/1", "0/2", "0/3"},
|
||||
{"1/0", "1/1", "1/2", "1/3"},
|
||||
{"2/0", "2/1", "2/2", "2/3"},
|
||||
{"3/0", "3/1", "3/2", "3/3"},
|
||||
};
|
||||
return std::unique_ptr<QAbstractItemModel>(new QGenericItemModel(std::move(movedTable)));
|
||||
}) << 4 << 4 << ChangeActions(ChangeAction::All);
|
||||
}
|
||||
|
||||
void tst_QGenericItemModel::basics()
|
||||
{
|
||||
#if QT_CONFIG(itemmodeltester)
|
||||
QFETCH(Factory, factory);
|
||||
auto model = factory();
|
||||
|
||||
QAbstractItemModelTester modelTest(model.get(), this);
|
||||
#else
|
||||
QSKIP("QAbstractItemModelTester not available");
|
||||
#endif
|
||||
}
|
||||
|
||||
void tst_QGenericItemModel::minimalIterator()
|
||||
{
|
||||
struct Minimal
|
||||
{
|
||||
struct iterator
|
||||
{
|
||||
using value_type = QString;
|
||||
using size_type = int;
|
||||
using difference_type = int;
|
||||
using reference = value_type;
|
||||
using pointer = value_type;
|
||||
using iterator_category = std::forward_iterator_tag;
|
||||
|
||||
constexpr iterator &operator++()
|
||||
{ ++m_index; return *this; }
|
||||
constexpr iterator operator++(int)
|
||||
{ auto copy = *this; ++m_index; return copy; }
|
||||
|
||||
reference operator*() const
|
||||
{ return QString::number(m_index); }
|
||||
constexpr bool operator==(const iterator &other) const noexcept
|
||||
{ return m_index == other.m_index; }
|
||||
constexpr bool operator!=(const iterator &other) const noexcept
|
||||
{ return m_index != other.m_index; }
|
||||
|
||||
size_type m_index;
|
||||
};
|
||||
|
||||
#if defined (__cpp_concepts)
|
||||
static_assert(std::forward_iterator<iterator>);
|
||||
#endif
|
||||
iterator begin() const { return iterator{0}; }
|
||||
iterator end() const { return iterator{m_size}; }
|
||||
|
||||
int m_size;
|
||||
} minimal{100};
|
||||
|
||||
QGenericItemModel model(minimal);
|
||||
QCOMPARE(model.rowCount(), minimal.m_size);
|
||||
for (int row = model.rowCount() - 1; row >= 0; --row) {
|
||||
const QModelIndex index = model.index(row, 0);
|
||||
QCOMPARE(index.data(), QString::number(row));
|
||||
QVERIFY(!index.flags().testFlag(Qt::ItemIsEditable));
|
||||
}
|
||||
}
|
||||
|
||||
void tst_QGenericItemModel::ranges()
|
||||
{
|
||||
#if defined(__cpp_lib_ranges)
|
||||
const int lowest = 1;
|
||||
const int highest = 10;
|
||||
QGenericItemModel model(std::views::iota(lowest, highest));
|
||||
QCOMPARE(model.rowCount(), highest - lowest);
|
||||
QCOMPARE(model.columnCount(), 1);
|
||||
#else
|
||||
QSKIP("C++ ranges library not available");
|
||||
#endif
|
||||
}
|
||||
|
||||
void tst_QGenericItemModel::json()
|
||||
{
|
||||
QJsonDocument json = QJsonDocument::fromJson(R"([ "one", "two" ])");
|
||||
QVERIFY(json.isArray());
|
||||
QGenericItemModel model(json.array());
|
||||
QCOMPARE(model.rowCount(), 2);
|
||||
const QModelIndex index = model.index(1, 0);
|
||||
QVERIFY(index.isValid());
|
||||
QCOMPARE(index.data().toString(), "two");
|
||||
}
|
||||
|
||||
void tst_QGenericItemModel::dimensions()
|
||||
{
|
||||
QFETCH(Factory, factory);
|
||||
auto model = factory();
|
||||
QFETCH(const int, expectedRowCount);
|
||||
QFETCH(const int, expectedColumnCount);
|
||||
|
||||
QCOMPARE(model->rowCount(), expectedRowCount);
|
||||
QCOMPARE(model->columnCount(), expectedColumnCount);
|
||||
}
|
||||
|
||||
void tst_QGenericItemModel::flags()
|
||||
{
|
||||
QFETCH(Factory, factory);
|
||||
auto model = factory();
|
||||
QFETCH(const ChangeActions, changeActions);
|
||||
|
||||
const QModelIndex first = model->index(0, 0);
|
||||
QVERIFY(first.isValid());
|
||||
const QModelIndex last = model->index(model->rowCount() - 1, model->columnCount() - 1);
|
||||
QVERIFY(last.isValid());
|
||||
|
||||
QCOMPARE(first.flags().testFlag(Qt::ItemIsEditable),
|
||||
changeActions.testFlags(ChangeAction::SetData));
|
||||
QCOMPARE(last.flags().testFlag(Qt::ItemIsEditable),
|
||||
changeActions.testFlags(ChangeAction::SetData));
|
||||
}
|
||||
|
||||
void tst_QGenericItemModel::data()
|
||||
{
|
||||
QFETCH(Factory, factory);
|
||||
auto model = factory();
|
||||
|
||||
QVERIFY(!model->data({}).isValid());
|
||||
|
||||
const QModelIndex first = model->index(0, 0);
|
||||
QVERIFY(first.isValid());
|
||||
const QModelIndex last = model->index(model->rowCount() - 1, model->columnCount() - 1);
|
||||
QVERIFY(last.isValid());
|
||||
|
||||
QVERIFY(first.data().isValid());
|
||||
QVERIFY(last.data().isValid());
|
||||
}
|
||||
|
||||
void tst_QGenericItemModel::setData()
|
||||
{
|
||||
QFETCH(Factory, factory);
|
||||
auto model = factory();
|
||||
QFETCH(const ChangeActions, changeActions);
|
||||
|
||||
QVERIFY(!model->setData({}, {}));
|
||||
|
||||
const QModelIndex first = model->index(0, 0);
|
||||
QVERIFY(first.isValid());
|
||||
|
||||
QVariant newValue = 12345;
|
||||
const QVariant oldValue = first.data();
|
||||
QVERIFY(oldValue.isValid());
|
||||
|
||||
if (!newValue.canConvert(oldValue.metaType()))
|
||||
newValue = QVariant(oldValue.metaType());
|
||||
QCOMPARE(first.data(), oldValue);
|
||||
QCOMPARE(model->setData(first, newValue), changeActions.testFlag(ChangeAction::SetData));
|
||||
QCOMPARE(first.data() == oldValue, !changeActions.testFlag(ChangeAction::SetData));
|
||||
}
|
||||
|
||||
void tst_QGenericItemModel::clearItemData()
|
||||
{
|
||||
QFETCH(Factory, factory);
|
||||
auto model = factory();
|
||||
QFETCH(const ChangeActions, changeActions);
|
||||
|
||||
QVERIFY(!model->clearItemData({}));
|
||||
|
||||
const QModelIndex index0 = model->index(1, 0);
|
||||
const QModelIndex index1 = model->index(1, 1);
|
||||
const QVariant oldDataAt0 = index0.data();
|
||||
const QVariant oldDataAt1 = index1.data();
|
||||
QCOMPARE(model->clearItemData(index0), changeActions.testFlags(ChangeAction::SetData));
|
||||
QCOMPARE(index0.data() == oldDataAt0, !changeActions.testFlags(ChangeAction::SetData));
|
||||
QCOMPARE(index1.data(), oldDataAt1);
|
||||
}
|
||||
|
||||
void tst_QGenericItemModel::insertRows()
|
||||
{
|
||||
QFETCH(Factory, factory);
|
||||
auto model = factory();
|
||||
QFETCH(const int, expectedRowCount);
|
||||
QFETCH(const int, expectedColumnCount);
|
||||
QFETCH(const ChangeActions, changeActions);
|
||||
const bool canSetData = changeActions.testFlag(ChangeAction::SetData);
|
||||
|
||||
QCOMPARE(model->rowCount(), expectedRowCount);
|
||||
QCOMPARE(model->insertRow(0), changeActions.testFlag(ChangeAction::InsertRows));
|
||||
QCOMPARE(model->rowCount() == expectedRowCount + 1,
|
||||
changeActions.testFlag(ChangeAction::InsertRows));
|
||||
|
||||
// get and put data into the new row
|
||||
const QModelIndex firstItem = model->index(0, 0);
|
||||
const QModelIndex lastItem = model->index(0, expectedColumnCount - 1);
|
||||
QVERIFY(firstItem.isValid());
|
||||
QVERIFY(lastItem.isValid());
|
||||
const QVariant firstValue = firstItem.data();
|
||||
const QVariant lastValue = lastItem.data();
|
||||
QEXPECT_FAIL("tableOfPointersPointer", "No item created", Continue);
|
||||
QEXPECT_FAIL("tableOfRowPointersPointer", "No row created", Continue);
|
||||
|
||||
QVERIFY(firstValue.isValid() && lastValue.isValid());
|
||||
QCOMPARE(model->setData(firstItem, lastValue), canSetData && lastValue.isValid());
|
||||
QCOMPARE(model->setData(lastItem, firstValue), canSetData && firstValue.isValid());
|
||||
|
||||
// append more rows
|
||||
QCOMPARE(model->insertRows(model->rowCount(), 5),
|
||||
changeActions.testFlag(ChangeAction::InsertRows));
|
||||
QCOMPARE(model->rowCount() == expectedRowCount + 6,
|
||||
changeActions.testFlag(ChangeAction::InsertRows));
|
||||
}
|
||||
|
||||
void tst_QGenericItemModel::removeRows()
|
||||
{
|
||||
QFETCH(Factory, factory);
|
||||
auto model = factory();
|
||||
QFETCH(const int, expectedRowCount);
|
||||
QFETCH(const ChangeActions, changeActions);
|
||||
|
||||
QCOMPARE(model->rowCount(), expectedRowCount);
|
||||
QCOMPARE(model->removeRow(0), changeActions.testFlag(ChangeAction::RemoveRows));
|
||||
QCOMPARE(model->rowCount() == expectedRowCount - 1,
|
||||
changeActions.testFlag(ChangeAction::RemoveRows));
|
||||
QCOMPARE(model->removeRows(model->rowCount() - 2, 2),
|
||||
changeActions.testFlag(ChangeAction::RemoveRows));
|
||||
QCOMPARE(model->rowCount() == expectedRowCount - 3,
|
||||
changeActions.testFlag(ChangeAction::RemoveRows));
|
||||
|
||||
const int newRowCount = model->rowCount();
|
||||
// make sure we don't crash when removing more than exist
|
||||
const bool couldRemove = model->removeRows(model->rowCount() - 5, model->rowCount() * 2);
|
||||
QCOMPARE_LE(model->rowCount(), newRowCount);
|
||||
QCOMPARE(couldRemove, model->rowCount() != newRowCount);
|
||||
}
|
||||
|
||||
void tst_QGenericItemModel::insertColumns()
|
||||
{
|
||||
QFETCH(Factory, factory);
|
||||
auto model = factory();
|
||||
QFETCH(const int, expectedColumnCount);
|
||||
QFETCH(const ChangeActions, changeActions);
|
||||
|
||||
QCOMPARE(model->columnCount(), expectedColumnCount);
|
||||
QCOMPARE(model->insertColumn(0), changeActions.testFlag(ChangeAction::InsertColumns));
|
||||
QCOMPARE(model->columnCount() == expectedColumnCount + 1,
|
||||
changeActions.testFlag(ChangeAction::InsertColumns));
|
||||
|
||||
// append
|
||||
QCOMPARE(model->insertColumns(model->columnCount(), 5),
|
||||
changeActions.testFlag(ChangeAction::InsertColumns));
|
||||
QCOMPARE(model->columnCount() == expectedColumnCount + 6,
|
||||
changeActions.testFlag(ChangeAction::InsertColumns));
|
||||
}
|
||||
|
||||
void tst_QGenericItemModel::removeColumns()
|
||||
{
|
||||
QFETCH(Factory, factory);
|
||||
auto model = factory();
|
||||
QFETCH(const int, expectedColumnCount);
|
||||
QFETCH(const ChangeActions, changeActions);
|
||||
|
||||
QCOMPARE(model->columnCount(), expectedColumnCount);
|
||||
QCOMPARE(model->removeColumn(0),
|
||||
changeActions.testFlag(ChangeAction::RemoveColumns));
|
||||
}
|
||||
|
||||
void tst_QGenericItemModel::inconsistentColumnCount()
|
||||
{
|
||||
QTest::ignoreMessage(QtCriticalMsg, "QGenericItemModel: "
|
||||
"Column-range at row 1 is not large enough!");
|
||||
|
||||
std::vector<std::vector<int>> fuzzyTable = {
|
||||
{0},
|
||||
{},
|
||||
{2},
|
||||
};
|
||||
QGenericItemModel model(fuzzyTable);
|
||||
QCOMPARE(model.columnCount(), 1);
|
||||
for (int row = 0; row < model.rowCount(); ++row) {
|
||||
auto debug = qScopeGuard([&]{
|
||||
qCritical() << "Test failed for row" << row << fuzzyTable.at(row).size();
|
||||
});
|
||||
const bool shouldWork = int(fuzzyTable.at(row).size()) >= model.columnCount();
|
||||
const auto index = model.index(row, model.columnCount() - 1);
|
||||
QCOMPARE(index.isValid(), shouldWork);
|
||||
// none of these should crash
|
||||
QCOMPARE(index.data().isValid(), shouldWork);
|
||||
QCOMPARE(model.setData(index, row + 5), shouldWork);
|
||||
QCOMPARE(model.clearItemData(index), shouldWork);
|
||||
debug.dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
QTEST_MAIN(tst_QGenericItemModel)
|
||||
#include "tst_qgenericitemmodel.moc"
|
Loading…
x
Reference in New Issue
Block a user