Add support for recursive filtering in QSFPM

You can now use the recursiveFiltering property to recurse into
children to find potential matching children to filter in.

Change-Id: I411a2fb29489fd56b9c881b3e6b8d1860cce630c
Reviewed-by: Stephen Kelly <steveire@gmail.com>
This commit is contained in:
Filipe Azevedo 2017-03-02 18:55:20 +01:00 committed by David Faure
parent 29bcbeab90
commit 7f1a220ee4
6 changed files with 991 additions and 99 deletions

View File

@ -56,6 +56,15 @@ QT_BEGIN_NAMESPACE
typedef QVector<QPair<QModelIndex, QPersistentModelIndex> > QModelIndexPairList;
struct QSortFilterProxyModelDataChanged
{
QSortFilterProxyModelDataChanged(const QModelIndex &tl, const QModelIndex &br)
: topLeft(tl), bottomRight(br) { }
QModelIndex topLeft;
QModelIndex bottomRight;
};
static inline QSet<int> qVectorToSet(const QVector<int> &vector)
{
QSet<int> set;
@ -164,9 +173,12 @@ public:
bool sort_localeaware;
int filter_column;
QRegExp filter_regexp;
int filter_role;
QRegExp filter_regexp;
QModelIndex last_top_source;
bool filter_recursive;
bool complete_insert;
bool dynamic_sortfilter;
QRowsRemoval itemsBeingRemoved;
@ -289,6 +301,9 @@ public:
Qt::Orientation orient, int start, int end, int delta_item_count, bool remove);
virtual void _q_sourceModelDestroyed() Q_DECL_OVERRIDE;
bool filterAcceptsRowInternal(int source_row, const QModelIndex &source_parent) const;
bool filterRecursiveAcceptsRow(int source_row, const QModelIndex &source_parent) const;
};
typedef QHash<QModelIndex, QSortFilterProxyModelPrivate::Mapping *> IndexMap;
@ -300,6 +315,32 @@ void QSortFilterProxyModelPrivate::_q_sourceModelDestroyed()
source_index_mapping.clear();
}
bool QSortFilterProxyModelPrivate::filterAcceptsRowInternal(int source_row, const QModelIndex &source_parent) const
{
Q_Q(const QSortFilterProxyModel);
return filter_recursive
? filterRecursiveAcceptsRow(source_row, source_parent)
: q->filterAcceptsRow(source_row, source_parent);
}
bool QSortFilterProxyModelPrivate::filterRecursiveAcceptsRow(int source_row, const QModelIndex &source_parent) const
{
Q_Q(const QSortFilterProxyModel);
if (q->filterAcceptsRow(source_row, source_parent))
return true;
const QModelIndex index = model->index(source_row, 0, source_parent);
const int count = model->rowCount(index);
for (int i = 0; i < count; ++i) {
if (filterRecursiveAcceptsRow(i, index))
return true;
}
return false;
}
void QSortFilterProxyModelPrivate::remove_from_mapping(const QModelIndex &source_parent)
{
if (Mapping *m = source_index_mapping.take(source_parent)) {
@ -340,7 +381,7 @@ IndexMap::const_iterator QSortFilterProxyModelPrivate::create_mapping(
int source_rows = model->rowCount(source_parent);
m->source_rows.reserve(source_rows);
for (int i = 0; i < source_rows; ++i) {
if (q->filterAcceptsRow(i, source_parent))
if (filterAcceptsRowInternal(i, source_parent))
m->source_rows.append(i);
}
int source_cols = model->columnCount(source_parent);
@ -794,7 +835,7 @@ void QSortFilterProxyModelPrivate::source_items_inserted(
QVector<int> source_items;
for (int i = start; i <= end; ++i) {
if ((orient == Qt::Vertical)
? q->filterAcceptsRow(i, source_parent)
? filterAcceptsRowInternal(i, source_parent)
: q->filterAcceptsColumn(i, source_parent)) {
source_items.append(i);
}
@ -814,7 +855,7 @@ void QSortFilterProxyModelPrivate::source_items_inserted(
orthogonal_source_to_proxy.resize(ortho_end);
for (int ortho_item = 0; ortho_item < ortho_end; ++ortho_item) {
if ((orient == Qt::Horizontal) ? q->filterAcceptsRow(ortho_item, source_parent)
if ((orient == Qt::Horizontal) ? filterAcceptsRowInternal(ortho_item, source_parent)
: q->filterAcceptsColumn(ortho_item, source_parent)) {
orthogonal_proxy_to_source.append(ortho_item);
}
@ -1125,7 +1166,7 @@ QSet<int> QSortFilterProxyModelPrivate::handle_filter_changed(
for (int i = 0; i < proxy_to_source.count(); ++i) {
const int source_item = proxy_to_source.at(i);
if ((orient == Qt::Vertical)
? !q->filterAcceptsRow(source_item, source_parent)
? !filterAcceptsRowInternal(source_item, source_parent)
: !q->filterAcceptsColumn(source_item, source_parent)) {
// This source item does not satisfy the filter, so it must be removed
source_items_remove.append(source_item);
@ -1137,7 +1178,7 @@ QSet<int> QSortFilterProxyModelPrivate::handle_filter_changed(
for (int source_item = 0; source_item < source_count; ++source_item) {
if (source_to_proxy.at(source_item) == -1) {
if ((orient == Qt::Vertical)
? q->filterAcceptsRow(source_item, source_parent)
? filterAcceptsRowInternal(source_item, source_parent)
: q->filterAcceptsColumn(source_item, source_parent)) {
// This source item satisfies the filter, so it must be added
source_items_insert.append(source_item);
@ -1163,107 +1204,126 @@ void QSortFilterProxyModelPrivate::_q_sourceDataChanged(const QModelIndex &sourc
Q_Q(QSortFilterProxyModel);
if (!source_top_left.isValid() || !source_bottom_right.isValid())
return;
QModelIndex source_parent = source_top_left.parent();
IndexMap::const_iterator it = source_index_mapping.constFind(source_parent);
if (it == source_index_mapping.constEnd()) {
// Don't care, since we don't have mapping for this index
return;
}
Mapping *m = it.value();
// Figure out how the source changes affect us
QVector<int> source_rows_remove;
QVector<int> source_rows_insert;
QVector<int> source_rows_change;
QVector<int> source_rows_resort;
int end = qMin(source_bottom_right.row(), m->proxy_rows.count() - 1);
for (int source_row = source_top_left.row(); source_row <= end; ++source_row) {
if (dynamic_sortfilter) {
if (m->proxy_rows.at(source_row) != -1) {
if (!q->filterAcceptsRow(source_row, source_parent)) {
// This source row no longer satisfies the filter, so it must be removed
source_rows_remove.append(source_row);
} else if (source_sort_column >= source_top_left.column() && source_sort_column <= source_bottom_right.column()) {
// This source row has changed in a way that may affect sorted order
source_rows_resort.append(source_row);
std::vector<QSortFilterProxyModelDataChanged> data_changed_list;
data_changed_list.emplace_back(source_top_left, source_bottom_right);
// Do check parents if the filter role have changed and we are recursive
if (filter_recursive && (roles.isEmpty() || roles.contains(filter_role))) {
QModelIndex source_parent = source_top_left.parent();
while (source_parent.isValid()) {
data_changed_list.emplace_back(source_parent, source_parent);
source_parent = source_parent.parent();
}
}
for (const QSortFilterProxyModelDataChanged &data_changed : data_changed_list) {
const QModelIndex &source_top_left = data_changed.topLeft;
const QModelIndex &source_bottom_right = data_changed.bottomRight;
const QModelIndex source_parent = source_top_left.parent();
IndexMap::const_iterator it = source_index_mapping.constFind(source_parent);
if (it == source_index_mapping.constEnd()) {
// Don't care, since we don't have mapping for this index
continue;
}
Mapping *m = it.value();
// Figure out how the source changes affect us
QVector<int> source_rows_remove;
QVector<int> source_rows_insert;
QVector<int> source_rows_change;
QVector<int> source_rows_resort;
int end = qMin(source_bottom_right.row(), m->proxy_rows.count() - 1);
for (int source_row = source_top_left.row(); source_row <= end; ++source_row) {
if (dynamic_sortfilter) {
if (m->proxy_rows.at(source_row) != -1) {
if (!filterAcceptsRowInternal(source_row, source_parent)) {
// This source row no longer satisfies the filter, so it must be removed
source_rows_remove.append(source_row);
} else if (source_sort_column >= source_top_left.column() && source_sort_column <= source_bottom_right.column()) {
// This source row has changed in a way that may affect sorted order
source_rows_resort.append(source_row);
} else {
// This row has simply changed, without affecting filtering nor sorting
source_rows_change.append(source_row);
}
} else {
// This row has simply changed, without affecting filtering nor sorting
source_rows_change.append(source_row);
if (!itemsBeingRemoved.contains(source_parent, source_row) && filterAcceptsRowInternal(source_row, source_parent)) {
// This source row now satisfies the filter, so it must be added
source_rows_insert.append(source_row);
}
}
} else {
if (!itemsBeingRemoved.contains(source_parent, source_row) && q->filterAcceptsRow(source_row, source_parent)) {
// This source row now satisfies the filter, so it must be added
source_rows_insert.append(source_row);
if (m->proxy_rows.at(source_row) != -1)
source_rows_change.append(source_row);
}
}
if (!source_rows_remove.isEmpty()) {
remove_source_items(m->proxy_rows, m->source_rows,
source_rows_remove, source_parent, Qt::Vertical);
QSet<int> source_rows_remove_set = qVectorToSet(source_rows_remove);
QVector<QModelIndex>::iterator childIt = m->mapped_children.end();
while (childIt != m->mapped_children.begin()) {
--childIt;
const QModelIndex source_child_index = *childIt;
if (source_rows_remove_set.contains(source_child_index.row())) {
childIt = m->mapped_children.erase(childIt);
remove_from_mapping(source_child_index);
}
}
} else {
if (m->proxy_rows.at(source_row) != -1)
source_rows_change.append(source_row);
}
}
if (!source_rows_remove.isEmpty()) {
remove_source_items(m->proxy_rows, m->source_rows,
source_rows_remove, source_parent, Qt::Vertical);
QSet<int> source_rows_remove_set = qVectorToSet(source_rows_remove);
QVector<QModelIndex>::iterator childIt = m->mapped_children.end();
while (childIt != m->mapped_children.begin()) {
--childIt;
const QModelIndex source_child_index = *childIt;
if (source_rows_remove_set.contains(source_child_index.row())) {
childIt = m->mapped_children.erase(childIt);
remove_from_mapping(source_child_index);
if (!source_rows_resort.isEmpty()) {
// Re-sort the rows of this level
QList<QPersistentModelIndex> parents;
parents << q->mapFromSource(source_parent);
emit q->layoutAboutToBeChanged(parents, QAbstractItemModel::VerticalSortHint);
QModelIndexPairList source_indexes = store_persistent_indexes();
remove_source_items(m->proxy_rows, m->source_rows, source_rows_resort,
source_parent, Qt::Vertical, false);
sort_source_rows(source_rows_resort, source_parent);
insert_source_items(m->proxy_rows, m->source_rows, source_rows_resort,
source_parent, Qt::Vertical, false);
update_persistent_indexes(source_indexes);
emit q->layoutChanged(parents, QAbstractItemModel::VerticalSortHint);
// Make sure we also emit dataChanged for the rows
source_rows_change += source_rows_resort;
}
if (!source_rows_change.isEmpty()) {
// Find the proxy row range
int proxy_start_row;
int proxy_end_row;
proxy_item_range(m->proxy_rows, source_rows_change,
proxy_start_row, proxy_end_row);
// ### Find the proxy column range also
if (proxy_end_row >= 0) {
// the row was accepted, but some columns might still be filtered out
int source_left_column = source_top_left.column();
while (source_left_column < source_bottom_right.column()
&& m->proxy_columns.at(source_left_column) == -1)
++source_left_column;
const QModelIndex proxy_top_left = create_index(
proxy_start_row, m->proxy_columns.at(source_left_column), it);
int source_right_column = source_bottom_right.column();
while (source_right_column > source_top_left.column()
&& m->proxy_columns.at(source_right_column) == -1)
--source_right_column;
const QModelIndex proxy_bottom_right = create_index(
proxy_end_row, m->proxy_columns.at(source_right_column), it);
emit q->dataChanged(proxy_top_left, proxy_bottom_right, roles);
}
}
}
if (!source_rows_resort.isEmpty()) {
// Re-sort the rows of this level
QList<QPersistentModelIndex> parents;
parents << q->mapFromSource(source_parent);
emit q->layoutAboutToBeChanged(parents, QAbstractItemModel::VerticalSortHint);
QModelIndexPairList source_indexes = store_persistent_indexes();
remove_source_items(m->proxy_rows, m->source_rows, source_rows_resort,
source_parent, Qt::Vertical, false);
sort_source_rows(source_rows_resort, source_parent);
insert_source_items(m->proxy_rows, m->source_rows, source_rows_resort,
source_parent, Qt::Vertical, false);
update_persistent_indexes(source_indexes);
emit q->layoutChanged(parents, QAbstractItemModel::VerticalSortHint);
// Make sure we also emit dataChanged for the rows
source_rows_change += source_rows_resort;
}
if (!source_rows_change.isEmpty()) {
// Find the proxy row range
int proxy_start_row;
int proxy_end_row;
proxy_item_range(m->proxy_rows, source_rows_change,
proxy_start_row, proxy_end_row);
// ### Find the proxy column range also
if (proxy_end_row >= 0) {
// the row was accepted, but some columns might still be filtered out
int source_left_column = source_top_left.column();
while (source_left_column < source_bottom_right.column()
&& m->proxy_columns.at(source_left_column) == -1)
++source_left_column;
const QModelIndex proxy_top_left = create_index(
proxy_start_row, m->proxy_columns.at(source_left_column), it);
int source_right_column = source_bottom_right.column();
while (source_right_column > source_top_left.column()
&& m->proxy_columns.at(source_right_column) == -1)
--source_right_column;
const QModelIndex proxy_bottom_right = create_index(
proxy_end_row, m->proxy_columns.at(source_right_column), it);
emit q->dataChanged(proxy_top_left, proxy_bottom_right, roles);
if (!source_rows_insert.isEmpty()) {
sort_source_rows(source_rows_insert, source_parent);
insert_source_items(m->proxy_rows, m->source_rows,
source_rows_insert, source_parent, Qt::Vertical);
}
}
if (!source_rows_insert.isEmpty()) {
sort_source_rows(source_rows_insert, source_parent);
insert_source_items(m->proxy_rows, m->source_rows,
source_rows_insert, source_parent, Qt::Vertical);
}
}
void QSortFilterProxyModelPrivate::_q_sourceHeaderDataChanged(Qt::Orientation orientation,
@ -1386,18 +1446,60 @@ void QSortFilterProxyModelPrivate::_q_sourceRowsAboutToBeInserted(
{
Q_UNUSED(start);
Q_UNUSED(end);
const bool toplevel = !source_parent.isValid();
const bool recursive_accepted = filter_recursive && !toplevel && filterAcceptsRowInternal(source_parent.row(), source_parent.parent());
//Force the creation of a mapping now, even if its empty.
//We need it because the proxy can be acessed at the moment it emits rowsAboutToBeInserted in insert_source_items
if (can_create_mapping(source_parent))
create_mapping(source_parent);
if (!filter_recursive || toplevel || recursive_accepted) {
if (can_create_mapping(source_parent))
create_mapping(source_parent);
if (filter_recursive)
complete_insert = true;
} else {
// The row could have been rejected or the parent might be not yet known... let's try to discover it
QModelIndex top_source_parent = source_parent;
QModelIndex parent = source_parent.parent();
QModelIndex grandParent = parent.parent();
while (parent.isValid() && !filterAcceptsRowInternal(parent.row(), grandParent)) {
top_source_parent = parent;
parent = grandParent;
grandParent = parent.parent();
}
last_top_source = top_source_parent;
}
}
void QSortFilterProxyModelPrivate::_q_sourceRowsInserted(
const QModelIndex &source_parent, int start, int end)
{
source_items_inserted(source_parent, start, end, Qt::Vertical);
if (update_source_sort_column() && dynamic_sortfilter) //previous call to update_source_sort_column may fail if the model has no column.
sort(); // now it should succeed so we need to make sure to sort again
if (!filter_recursive || complete_insert) {
if (filter_recursive)
complete_insert = false;
source_items_inserted(source_parent, start, end, Qt::Vertical);
if (update_source_sort_column() && dynamic_sortfilter) //previous call to update_source_sort_column may fail if the model has no column.
sort(); // now it should succeed so we need to make sure to sort again
return;
}
if (filter_recursive) {
bool accept = false;
for (int row = start; row <= end; ++row) {
if (filterAcceptsRowInternal(row, source_parent)) {
accept = true;
break;
}
}
if (!accept) // the new rows have no descendants that match the filter, filter them out.
return;
// last_top_source should now become visible
_q_sourceDataChanged(last_top_source, last_top_source, QVector<int>());
}
}
void QSortFilterProxyModelPrivate::_q_sourceRowsAboutToBeRemoved(
@ -1413,6 +1515,27 @@ void QSortFilterProxyModelPrivate::_q_sourceRowsRemoved(
{
itemsBeingRemoved = QRowsRemoval();
source_items_removed(source_parent, start, end, Qt::Vertical);
if (filter_recursive) {
// Find out if removing this visible row means that some ascendant
// row can now be hidden.
// We go up until we find a row that should still be visible
// and then make QSFPM re-evaluate the last one we saw before that, to hide it.
QModelIndex to_hide;
QModelIndex source_ascendant = source_parent;
while (source_ascendant.isValid()) {
if (filterAcceptsRowInternal(source_ascendant.row(), source_ascendant.parent()))
break;
to_hide = source_ascendant;
source_ascendant = source_ascendant.parent();
}
if (to_hide.isValid())
_q_sourceDataChanged(to_hide, to_hide, QVector<int>());
}
}
void QSortFilterProxyModelPrivate::_q_sourceRowsAboutToBeMoved(
@ -1685,7 +1808,9 @@ QSortFilterProxyModel::QSortFilterProxyModel(QObject *parent)
d->sort_localeaware = false;
d->filter_column = 0;
d->filter_role = Qt::DisplayRole;
d->filter_recursive = false;
d->dynamic_sortfilter = true;
d->complete_insert = false;
connect(this, SIGNAL(modelReset()), this, SLOT(_q_clearMapping()));
}
@ -2505,6 +2630,32 @@ void QSortFilterProxyModel::setFilterRole(int role)
d->filter_changed();
}
/*!
\since 5.9
\property QSortFilterProxyModel::recursiveFiltering
\brief whether the filter to be applied recursively on children, and for
any matching child, its parents will be visible as well.
The default value is false.
\sa filterAcceptsRow()
*/
bool QSortFilterProxyModel::recursiveFiltering() const
{
Q_D(const QSortFilterProxyModel);
return d->filter_recursive;
}
void QSortFilterProxyModel::setRecursiveFiltering(bool recursive)
{
Q_D(QSortFilterProxyModel);
if (d->filter_recursive == recursive)
return;
d->filter_about_to_be_changed();
d->filter_recursive = recursive;
d->filter_changed();
}
/*!
\obsolete

View File

@ -67,6 +67,7 @@ class Q_CORE_EXPORT QSortFilterProxyModel : public QAbstractProxyModel
Q_PROPERTY(bool isSortLocaleAware READ isSortLocaleAware WRITE setSortLocaleAware)
Q_PROPERTY(int sortRole READ sortRole WRITE setSortRole)
Q_PROPERTY(int filterRole READ filterRole WRITE setFilterRole)
Q_PROPERTY(bool recursiveFiltering READ recursiveFiltering WRITE setRecursiveFiltering)
public:
explicit QSortFilterProxyModel(QObject *parent = Q_NULLPTR);
@ -107,6 +108,9 @@ public:
int filterRole() const;
void setFilterRole(int role);
bool recursiveFiltering() const;
void setRecursiveFiltering(bool recursive);
public Q_SLOTS:
void setFilterRegExp(const QString &pattern);
void setFilterWildcard(const QString &pattern);

View File

@ -7,6 +7,7 @@ qtHaveModule(gui): SUBDIRS += \
qabstractproxymodel \
qidentityproxymodel \
qitemselectionmodel \
qsortfilterproxymodel_recursive \
qtHaveModule(widgets): SUBDIRS += \
qitemmodel \

View File

@ -0,0 +1 @@
tst_qsortfilterproxymodel_recursive

View File

@ -0,0 +1,8 @@
CONFIG += testcase
CONFIG += parallel_test
TARGET = tst_qsortfilterproxymodel_recursive
QT += testlib
SOURCES += tst_qsortfilterproxymodel_recursive.cpp
DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0

View File

@ -0,0 +1,727 @@
/****************************************************************************
**
** Copyright (C) 2016 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, authors Filipe Azevedo <filipe.azevedo@kdab.com> and David Faure <david.faure@kdab.com>
** Contact: https://www.qt.io/licensing/
**
** This file is part of the test suite of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:LGPL21$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see http://www.qt.io/terms-conditions. For further
** information use the contact form at http://www.qt.io/contact-us.
**
** GNU Lesser General Public License Usage
** Alternatively, this file may be used under the terms of the GNU Lesser
** General Public License version 2.1 or version 3 as published by the Free
** Software Foundation and appearing in the file LICENSE.LGPLv21 and
** LICENSE.LGPLv3 included in the packaging of this file. Please review the
** following information to ensure the GNU Lesser General Public License
** requirements will be met: https://www.gnu.org/licenses/lgpl.html and
** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
**
** As a special exception, The Qt Company gives you certain additional
** rights. These rights are described in The Qt Company LGPL Exception
** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
**
** $QT_END_LICENSE$
**
****************************************************************************/
#include <QTest>
#include <QSignalSpy>
#include <QtCore/QSortFilterProxyModel>
#include <QtGui/QStandardItem>
Q_DECLARE_METATYPE(QModelIndex)
class ModelSignalSpy : public QObject {
Q_OBJECT
public:
explicit ModelSignalSpy(QAbstractItemModel &model) {
connect(&model, &QAbstractItemModel::rowsInserted, this, &ModelSignalSpy::onRowsInserted);
connect(&model, &QAbstractItemModel::rowsRemoved, this, &ModelSignalSpy::onRowsRemoved);
connect(&model, &QAbstractItemModel::rowsAboutToBeInserted, this, &ModelSignalSpy::onRowsAboutToBeInserted);
connect(&model, &QAbstractItemModel::rowsAboutToBeRemoved, this, &ModelSignalSpy::onRowsAboutToBeRemoved);
connect(&model, &QAbstractItemModel::rowsMoved, this, &ModelSignalSpy::onRowsMoved);
connect(&model, &QAbstractItemModel::dataChanged, this, &ModelSignalSpy::onDataChanged);
connect(&model, &QAbstractItemModel::layoutChanged, this, &ModelSignalSpy::onLayoutChanged);
connect(&model, &QAbstractItemModel::modelReset, this, &ModelSignalSpy::onModelReset);
}
QStringList mSignals;
private Q_SLOTS:
void onRowsInserted(QModelIndex p, int start, int end) {
mSignals << QLatin1String("rowsInserted(") + textForRowSpy(p, start, end) + ')';
}
void onRowsRemoved(QModelIndex p, int start, int end) {
mSignals << QLatin1String("rowsRemoved(") + textForRowSpy(p, start, end) + ')';
}
void onRowsAboutToBeInserted(QModelIndex p, int start, int end) {
mSignals << QLatin1String("rowsAboutToBeInserted(") + textForRowSpy(p, start, end) + ')';
}
void onRowsAboutToBeRemoved(QModelIndex p, int start, int end) {
mSignals << QLatin1String("rowsAboutToBeRemoved(") + textForRowSpy(p, start, end) + ')';
}
void onRowsMoved(QModelIndex,int,int,QModelIndex,int) {
mSignals << QStringLiteral("rowsMoved");
}
void onDataChanged(const QModelIndex &from, const QModelIndex& ) {
mSignals << QStringLiteral("dataChanged(%1)").arg(from.data().toString());
}
void onLayoutChanged() {
mSignals << QStringLiteral("layoutChanged");
}
void onModelReset() {
mSignals << QStringLiteral("modelReset");
}
private:
QString textForRowSpy(const QModelIndex &parent, int start, int end)
{
QString txt = parent.data().toString();
if (!txt.isEmpty())
txt += QLatin1Char('.');
txt += QString::number(start+1);
if (start != end)
txt += QLatin1Char('-') + QString::number(end+1);
return txt;
}
};
class TestModel : public QSortFilterProxyModel
{
Q_OBJECT
public:
TestModel(QAbstractItemModel *sourceModel)
: QSortFilterProxyModel()
{
setRecursiveFiltering(true);
setSourceModel(sourceModel);
}
virtual bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override
{
return sourceModel()->index(sourceRow, 0, sourceParent).data(Qt::UserRole +1).toBool()
&& QSortFilterProxyModel::filterAcceptsRow(sourceRow, sourceParent);
}
};
// Represents this tree
// - A
// - - B
// - - - C
// - - - D
// - - E
// as a single string, englobing children in brackets, like this:
// [A[B[C D] E]]
// In addition, items that match the filtering (data(UserRole+1) == true) have a * after their value.
static QString treeAsString(const QAbstractItemModel &model, const QModelIndex &parent = QModelIndex())
{
QString ret;
const int rowCount = model.rowCount(parent);
if (rowCount > 0) {
ret += QLatin1Char('[');
for (int row = 0 ; row < rowCount; ++row) {
if (row > 0) {
ret += ' ';
}
const QModelIndex child = model.index(row, 0, parent);
ret += child.data().toString();
if (child.data(Qt::UserRole+1).toBool())
ret += QLatin1Char('*');
ret += treeAsString(model, child);
}
ret += QLatin1Char(']');
}
return ret;
}
// Fill a tree model based on a string representation (see treeAsString)
static void fillModel(QStandardItemModel &model, const QString &str)
{
QCOMPARE(str.count('['), str.count(']'));
QStandardItem *item = 0;
QString data;
for ( int i = 0 ; i < str.length() ; ++i ) {
const QChar ch = str.at(i);
if ((ch == '[' || ch == ']' || ch == ' ') && !data.isEmpty()) {
if (data.endsWith('*')) {
item->setData(true, Qt::UserRole + 1);
data.chop(1);
}
item->setText(data);
data.clear();
}
if (ch == '[') {
// Create new child
QStandardItem *child = new QStandardItem;
if (item)
item->appendRow(child);
else
model.appendRow(child);
item = child;
} else if (ch == ']') {
// Go up to parent
item = item->parent();
} else if (ch == ' ') {
// Create new sibling
QStandardItem *child = new QStandardItem;
QStandardItem *parent = item->parent();
if (parent)
parent->appendRow(child);
else
model.appendRow(child);
item = child;
} else {
data += ch;
}
}
}
class tst_QSortFilterProxyModel_Recursive : public QObject
{
Q_OBJECT
private:
private Q_SLOTS:
void testInitialFiltering_data()
{
QTest::addColumn<QString>("sourceStr");
QTest::addColumn<QString>("proxyStr");
QTest::newRow("empty") << "[]" << "";
QTest::newRow("no") << "[1]" << "";
QTest::newRow("yes") << "[1*]" << "[1*]";
QTest::newRow("second") << "[1 2*]" << "[2*]";
QTest::newRow("child_yes") << "[1 2[2.1*]]" << "[2[2.1*]]";
QTest::newRow("grandchild_yes") << "[1 2[2.1[2.1.1*]]]" << "[2[2.1[2.1.1*]]]";
// 1, 3.1 and 4.2.1 match, so their parents are in the model
QTest::newRow("more") << "[1* 2[2.1] 3[3.1*] 4[4.1 4.2[4.2.1*]]]" << "[1* 3[3.1*] 4[4.2[4.2.1*]]]";
}
void testInitialFiltering()
{
QFETCH(QString, sourceStr);
QFETCH(QString, proxyStr);
QStandardItemModel model;
fillModel(model, sourceStr);
QCOMPARE(treeAsString(model), sourceStr);
TestModel proxy(&model);
QCOMPARE(treeAsString(proxy), proxyStr);
}
// Test changing a role that is unrelated to the filtering.
void testUnrelatedDataChange()
{
QStandardItemModel model;
const QString sourceStr = QStringLiteral("[1[1.1[1.1.1*]]]");
fillModel(model, sourceStr);
QCOMPARE(treeAsString(model), sourceStr);
TestModel proxy(&model);
QCOMPARE(treeAsString(proxy), sourceStr);
ModelSignalSpy spy(proxy);
QStandardItem *item_1_1_1 = model.item(0)->child(0)->child(0);
// When changing the text on the item
item_1_1_1->setText(QStringLiteral("ME"));
QCOMPARE(treeAsString(proxy), QStringLiteral("[1[1.1[ME*]]]"));
QCOMPARE(spy.mSignals, QStringList()
<< QStringLiteral("dataChanged(ME)")
<< QStringLiteral("dataChanged(1.1)")
<< QStringLiteral("dataChanged(1)"));
}
// Test changing a role that is unrelated to the filtering, in a hidden item.
void testHiddenDataChange()
{
QStandardItemModel model;
const QString sourceStr = QStringLiteral("[1[1.1[1.1.1]]]");
fillModel(model, sourceStr);
QCOMPARE(treeAsString(model), sourceStr);
TestModel proxy(&model);
QCOMPARE(treeAsString(proxy), QString());
ModelSignalSpy spy(proxy);
QStandardItem *item_1_1_1 = model.item(0)->child(0)->child(0);
// When changing the text on a hidden item
item_1_1_1->setText(QStringLiteral("ME"));
QCOMPARE(treeAsString(proxy), QString());
QCOMPARE(spy.mSignals, QStringList());
}
// Test that we properly react to a data-changed signal in a descendant and include all required rows
void testDataChangeIn_data()
{
QTest::addColumn<QString>("sourceStr");
QTest::addColumn<QString>("initialProxyStr");
QTest::addColumn<QString>("add"); // set the flag on this item
QTest::addColumn<QString>("expectedProxyStr");
QTest::addColumn<QStringList>("expectedSignals");
QTest::newRow("toplevel") << "[1]" << "" << "1" << "[1*]"
<< (QStringList() << QStringLiteral("rowsAboutToBeInserted(1)") << QStringLiteral("rowsInserted(1)"));
QTest::newRow("show_parents") << "[1[1.1[1.1.1]]]" << "" << "1.1.1" << "[1[1.1[1.1.1*]]]"
<< (QStringList() << QStringLiteral("rowsAboutToBeInserted(1)") << QStringLiteral("rowsInserted(1)"));
const QStringList insert_1_1_1 = QStringList()
<< QStringLiteral("rowsAboutToBeInserted(1.1.1)")
<< QStringLiteral("rowsInserted(1.1.1)")
<< QStringLiteral("dataChanged(1.1)")
<< QStringLiteral("dataChanged(1)")
;
QTest::newRow("parent_visible") << "[1[1.1*[1.1.1]]]" << "[1[1.1*]]" << "1.1.1" << "[1[1.1*[1.1.1*]]]"
<< insert_1_1_1;
QTest::newRow("sibling_visible") << "[1[1.1[1.1.1 1.1.2*]]]" << "[1[1.1[1.1.2*]]]" << "1.1.1" << "[1[1.1[1.1.1* 1.1.2*]]]"
<< insert_1_1_1;
QTest::newRow("visible_cousin") << "[1[1.1[1.1.1 1.1.2[1.1.2.1*]]]]" << "[1[1.1[1.1.2[1.1.2.1*]]]]" << "1.1.1" << "[1[1.1[1.1.1* 1.1.2[1.1.2.1*]]]]"
<< insert_1_1_1;
QTest::newRow("show_parent") << "[1[1.1[1.1.1 1.1.2] 1.2*]]" << "[1[1.2*]]" << "1.1.1" << "[1[1.1[1.1.1*] 1.2*]]"
<< (QStringList()
<< QStringLiteral("rowsAboutToBeInserted(1.1)")
<< QStringLiteral("rowsInserted(1.1)")
<< QStringLiteral("dataChanged(1)"));
QTest::newRow("with_children") << "[1[1.1[1.1.1[1.1.1.1*]]] 2*]" << "[1[1.1[1.1.1[1.1.1.1*]]] 2*]" << "1.1.1" << "[1[1.1[1.1.1*[1.1.1.1*]]] 2*]"
<< (QStringList()
<< QStringLiteral("dataChanged(1.1.1)")
<< QStringLiteral("dataChanged(1.1)")
<< QStringLiteral("dataChanged(1)"));
}
void testDataChangeIn()
{
QFETCH(QString, sourceStr);
QFETCH(QString, initialProxyStr);
QFETCH(QString, add);
QFETCH(QString, expectedProxyStr);
QFETCH(QStringList, expectedSignals);
QStandardItemModel model;
fillModel(model, sourceStr);
QCOMPARE(treeAsString(model), sourceStr);
TestModel proxy(&model);
QCOMPARE(treeAsString(proxy), initialProxyStr);
ModelSignalSpy spy(proxy);
// When changing the data on the designated item to show this row
QStandardItem *itemToChange = itemByText(model, add);
QVERIFY(!itemToChange->data().toBool());
itemToChange->setData(true);
// The proxy should update as expected
QCOMPARE(treeAsString(proxy), expectedProxyStr);
//qDebug() << spy.mSignals;
QCOMPARE(spy.mSignals, expectedSignals);
}
void testDataChangeOut_data()
{
QTest::addColumn<QString>("sourceStr");
QTest::addColumn<QString>("initialProxyStr");
QTest::addColumn<QString>("remove"); // unset the flag on this item
QTest::addColumn<QString>("expectedProxyStr");
QTest::addColumn<QStringList>("expectedSignals");
const QStringList remove1_1_1 = (QStringList()
<< QStringLiteral("rowsAboutToBeRemoved(1.1.1)")
<< QStringLiteral("rowsRemoved(1.1.1)")
<< QStringLiteral("dataChanged(1.1)")
<< QStringLiteral("dataChanged(1)"));
QTest::newRow("toplevel") << "[1*]" << "[1*]" << "1" << ""
<< (QStringList() << QStringLiteral("rowsAboutToBeRemoved(1)") << QStringLiteral("rowsRemoved(1)"));
QTest::newRow("hide_parent") << "[1[1.1[1.1.1*]]]" << "[1[1.1[1.1.1*]]]" << "1.1.1" << "" <<
(QStringList()
<< QStringLiteral("rowsAboutToBeRemoved(1.1.1)")
<< QStringLiteral("rowsRemoved(1.1.1)")
<< QStringLiteral("rowsAboutToBeRemoved(1.1)")
<< QStringLiteral("rowsRemoved(1.1)")
<< QStringLiteral("rowsAboutToBeRemoved(1)")
<< QStringLiteral("rowsRemoved(1)"));
QTest::newRow("parent_visible") << "[1[1.1*[1.1.1*]]]" << "[1[1.1*[1.1.1*]]]" << "1.1.1" << "[1[1.1*]]"
<< remove1_1_1;
QTest::newRow("visible") << "[1[1.1[1.1.1* 1.1.2*]]]" << "[1[1.1[1.1.1* 1.1.2*]]]" << "1.1.1" << "[1[1.1[1.1.2*]]]"
<< remove1_1_1;
QTest::newRow("visible_cousin") << "[1[1.1[1.1.1* 1.1.2[1.1.2.1*]]]]" << "[1[1.1[1.1.1* 1.1.2[1.1.2.1*]]]]" << "1.1.1" << "[1[1.1[1.1.2[1.1.2.1*]]]]"
<< remove1_1_1;
// The following tests trigger the removal of an ascendant.
QTest::newRow("remove_parent") << "[1[1.1[1.1.1* 1.1.2] 1.2*]]" << "[1[1.1[1.1.1*] 1.2*]]" << "1.1.1" << "[1[1.2*]]"
<< (QStringList()
<< QStringLiteral("rowsAboutToBeRemoved(1.1.1)")
<< QStringLiteral("rowsRemoved(1.1.1)")
<< QStringLiteral("rowsAboutToBeRemoved(1.1)")
<< QStringLiteral("rowsRemoved(1.1)")
<< QStringLiteral("dataChanged(1)"));
QTest::newRow("with_children") << "[1[1.1[1.1.1*[1.1.1.1*]]] 2*]" << "[1[1.1[1.1.1*[1.1.1.1*]]] 2*]" << "1.1.1" << "[1[1.1[1.1.1[1.1.1.1*]]] 2*]"
<< (QStringList()
<< QStringLiteral("dataChanged(1.1.1)")
<< QStringLiteral("dataChanged(1.1)")
<< QStringLiteral("dataChanged(1)"));
QTest::newRow("last_visible") << "[1[1.1[1.1.1* 1.1.2]]]" << "[1[1.1[1.1.1*]]]" << "1.1.1" << ""
<< (QStringList()
<< QStringLiteral("rowsAboutToBeRemoved(1.1.1)")
<< QStringLiteral("rowsRemoved(1.1.1)")
<< QStringLiteral("rowsAboutToBeRemoved(1.1)")
<< QStringLiteral("rowsRemoved(1.1)")
<< QStringLiteral("rowsAboutToBeRemoved(1)")
<< QStringLiteral("rowsRemoved(1)"));
}
void testDataChangeOut()
{
QFETCH(QString, sourceStr);
QFETCH(QString, initialProxyStr);
QFETCH(QString, remove);
QFETCH(QString, expectedProxyStr);
QFETCH(QStringList, expectedSignals);
QStandardItemModel model;
fillModel(model, sourceStr);
QCOMPARE(treeAsString(model), sourceStr);
TestModel proxy(&model);
QCOMPARE(treeAsString(proxy), initialProxyStr);
ModelSignalSpy spy(proxy);
// When changing the data on the designated item to exclude this row again
QStandardItem *itemToChange = itemByText(model, remove);
QVERIFY(itemToChange->data().toBool());
itemToChange->setData(false);
// The proxy should update as expected
QCOMPARE(treeAsString(proxy), expectedProxyStr);
//qDebug() << spy.mSignals;
QCOMPARE(spy.mSignals, expectedSignals);
}
void testInsert()
{
QStandardItemModel model;
const QString sourceStr = QStringLiteral("[1[1.1[1.1.1]]]");
fillModel(model, sourceStr);
QCOMPARE(treeAsString(model), sourceStr);
TestModel proxy(&model);
QCOMPARE(treeAsString(proxy), QString());
ModelSignalSpy spy(proxy);
QStandardItem *item_1_1_1 = model.item(0)->child(0)->child(0);
QStandardItem *item_1_1_1_1 = new QStandardItem(QStringLiteral("1.1.1.1"));
item_1_1_1_1->setData(true);
item_1_1_1->appendRow(item_1_1_1_1);
QCOMPARE(treeAsString(proxy), QStringLiteral("[1[1.1[1.1.1[1.1.1.1*]]]]"));
QCOMPARE(spy.mSignals, QStringList() << QStringLiteral("rowsAboutToBeInserted(1)")
<< QStringLiteral("rowsInserted(1)"));
}
// Start from [1[1.1[1.1.1 1.1.2[1.1.2.1*]]]]
// where 1.1.1 is hidden but 1.1 is shown, we want to insert a shown child in 1.1.1.
// The proxy ensures dataChanged is called on 1.1,
// so that 1.1.1 and 1.1.1.1 are included in the model.
void testInsertCousin()
{
QStandardItemModel model;
const QString sourceStr = QStringLiteral("[1[1.1[1.1.1 1.1.2[1.1.2.1*]]]]");
fillModel(model, sourceStr);
QCOMPARE(treeAsString(model), sourceStr);
TestModel proxy(&model);
QCOMPARE(treeAsString(proxy), QStringLiteral("[1[1.1[1.1.2[1.1.2.1*]]]]"));
ModelSignalSpy spy(proxy);
{
QStandardItem *item_1_1_1_1 = new QStandardItem(QStringLiteral("1.1.1.1"));
item_1_1_1_1->setData(true);
QStandardItem *item_1_1_1 = model.item(0)->child(0)->child(0);
item_1_1_1->appendRow(item_1_1_1_1);
}
QCOMPARE(treeAsString(proxy), QStringLiteral("[1[1.1[1.1.1[1.1.1.1*] 1.1.2[1.1.2.1*]]]]"));
//qDebug() << spy.mSignals;
QCOMPARE(spy.mSignals, QStringList()
<< QStringLiteral("rowsAboutToBeInserted(1.1.1)")
<< QStringLiteral("rowsInserted(1.1.1)")
<< QStringLiteral("dataChanged(1.1)")
<< QStringLiteral("dataChanged(1)"));
}
void testInsertWithChildren()
{
QStandardItemModel model;
const QString sourceStr = QStringLiteral("[1[1.1]]");
fillModel(model, sourceStr);
QCOMPARE(treeAsString(model), sourceStr);
TestModel proxy(&model);
QCOMPARE(treeAsString(proxy), QString());
ModelSignalSpy spy(proxy);
{
QStandardItem *item_1_1_1 = new QStandardItem(QStringLiteral("1.1.1"));
QStandardItem *item_1_1_1_1 = new QStandardItem(QStringLiteral("1.1.1.1"));
item_1_1_1_1->setData(true);
item_1_1_1->appendRow(item_1_1_1_1);
QStandardItem *item_1_1 = model.item(0)->child(0);
item_1_1->appendRow(item_1_1_1);
}
QCOMPARE(treeAsString(proxy), QStringLiteral("[1[1.1[1.1.1[1.1.1.1*]]]]"));
QCOMPARE(spy.mSignals, QStringList()
<< QStringLiteral("rowsAboutToBeInserted(1)")
<< QStringLiteral("rowsInserted(1)"));
}
void testInsertIntoVisibleWithChildren()
{
QStandardItemModel model;
const QString sourceStr = QStringLiteral("[1[1.1[1.1.1*]]]");
fillModel(model, sourceStr);
QCOMPARE(treeAsString(model), sourceStr);
TestModel proxy(&model);
QCOMPARE(treeAsString(proxy), sourceStr);
ModelSignalSpy spy(proxy);
{
QStandardItem *item_1_1_2 = new QStandardItem(QStringLiteral("1.1.2"));
QStandardItem *item_1_1_2_1 = new QStandardItem(QStringLiteral("1.1.2.1"));
item_1_1_2_1->setData(true);
item_1_1_2->appendRow(item_1_1_2_1);
QStandardItem *item_1_1 = model.item(0)->child(0);
item_1_1->appendRow(item_1_1_2);
}
QCOMPARE(treeAsString(proxy), QStringLiteral("[1[1.1[1.1.1* 1.1.2[1.1.2.1*]]]]"));
QCOMPARE(spy.mSignals, QStringList()
<< QStringLiteral("rowsAboutToBeInserted(1.1.2)")
<< QStringLiteral("rowsInserted(1.1.2)"));
}
void testInsertBefore()
{
QStandardItemModel model;
const QString sourceStr = "[1[1.1[1.1.2*]]]";
fillModel(model, sourceStr);
QCOMPARE(treeAsString(model), sourceStr);
TestModel proxy(&model);
QCOMPARE(treeAsString(proxy), sourceStr);
ModelSignalSpy spy(proxy);
{
QStandardItem *item_1_1_1 = new QStandardItem("1.1.1");
QStandardItem *item_1_1 = model.item(0)->child(0);
item_1_1->insertRow(0, item_1_1_1);
}
QCOMPARE(treeAsString(proxy), QString("[1[1.1[1.1.2*]]]"));
QCOMPARE(spy.mSignals, QStringList());
}
void testInsertHidden() // inserting filtered-out rows shouldn't emit anything
{
QStandardItemModel model;
const QString sourceStr = QStringLiteral("[1[1.1]]");
fillModel(model, sourceStr);
QCOMPARE(treeAsString(model), sourceStr);
TestModel proxy(&model);
QCOMPARE(treeAsString(proxy), QString());
ModelSignalSpy spy(proxy);
{
QStandardItem *item_1_1_1 = new QStandardItem(QStringLiteral("1.1.1"));
QStandardItem *item_1_1_1_1 = new QStandardItem(QStringLiteral("1.1.1.1"));
item_1_1_1->appendRow(item_1_1_1_1);
QStandardItem *item_1_1 = model.item(0)->child(0);
item_1_1->appendRow(item_1_1_1);
}
QCOMPARE(treeAsString(proxy), QString());
QCOMPARE(spy.mSignals, QStringList());
}
void testConsecutiveInserts_data()
{
testInitialFiltering_data();
}
void testConsecutiveInserts()
{
QFETCH(QString, sourceStr);
QFETCH(QString, proxyStr);
QStandardItemModel model;
TestModel proxy(&model); // this time the proxy listens to the model while we fill it
fillModel(model, sourceStr);
QCOMPARE(treeAsString(model), sourceStr);
QCOMPARE(treeAsString(proxy), proxyStr);
}
void testRemove_data()
{
QTest::addColumn<QString>("sourceStr");
QTest::addColumn<QString>("initialProxyStr");
QTest::addColumn<QString>("remove"); // remove this item
QTest::addColumn<QString>("expectedProxyStr");
QTest::addColumn<QStringList>("expectedSignals");
const QStringList remove1_1_1 = (QStringList() << QStringLiteral("rowsAboutToBeRemoved(1.1.1)") << QStringLiteral("rowsRemoved(1.1.1)"));
QTest::newRow("toplevel") << "[1* 2* 3*]" << "[1* 2* 3*]" << "1" << "[2* 3*]"
<< (QStringList() << QStringLiteral("rowsAboutToBeRemoved(1)") << QStringLiteral("rowsRemoved(1)"));
QTest::newRow("remove_hidden") << "[1 2* 3*]" << "[2* 3*]" << "1" << "[2* 3*]" << QStringList();
QTest::newRow("parent_hidden") << "[1[1.1[1.1.1]]]" << "" << "1.1.1" << "" << QStringList();
QTest::newRow("child_hidden") << "[1[1.1*[1.1.1]]]" << "[1[1.1*]]" << "1.1.1" << "[1[1.1*]]" << QStringList();
QTest::newRow("parent_visible") << "[1[1.1*[1.1.1*]]]" << "[1[1.1*[1.1.1*]]]" << "1.1.1" << "[1[1.1*]]"
<< remove1_1_1;
QTest::newRow("visible") << "[1[1.1[1.1.1* 1.1.2*]]]" << "[1[1.1[1.1.1* 1.1.2*]]]" << "1.1.1" << "[1[1.1[1.1.2*]]]"
<< remove1_1_1;
QTest::newRow("visible_cousin") << "[1[1.1[1.1.1* 1.1.2[1.1.2.1*]]]]" << "[1[1.1[1.1.1* 1.1.2[1.1.2.1*]]]]" << "1.1.1" << "[1[1.1[1.1.2[1.1.2.1*]]]]"
<< remove1_1_1;
// The following tests trigger the removal of an ascendant.
// We could optimize the rows{AboutToBe,}Removed(1.1.1) away...
QTest::newRow("remove_parent") << "[1[1.1[1.1.1* 1.1.2] 1.2*]]" << "[1[1.1[1.1.1*] 1.2*]]" << "1.1.1" << "[1[1.2*]]"
<< (QStringList()
<< QStringLiteral("rowsAboutToBeRemoved(1.1.1)")
<< QStringLiteral("rowsRemoved(1.1.1)")
<< QStringLiteral("rowsAboutToBeRemoved(1.1)")
<< QStringLiteral("rowsRemoved(1.1)")
<< QStringLiteral("dataChanged(1)"));
QTest::newRow("with_children") << "[1[1.1[1.1.1[1.1.1.1*]]] 2*]" << "[1[1.1[1.1.1[1.1.1.1*]]] 2*]" << "1.1.1" << "[2*]"
<< (QStringList()
<< QStringLiteral("rowsAboutToBeRemoved(1.1.1)")
<< QStringLiteral("rowsRemoved(1.1.1)")
<< QStringLiteral("rowsAboutToBeRemoved(1)")
<< QStringLiteral("rowsRemoved(1)"));
QTest::newRow("last_visible") << "[1[1.1[1.1.1* 1.1.2]]]" << "[1[1.1[1.1.1*]]]" << "1.1.1" << ""
<< (QStringList()
<< QStringLiteral("rowsAboutToBeRemoved(1.1.1)")
<< QStringLiteral("rowsRemoved(1.1.1)")
<< QStringLiteral("rowsAboutToBeRemoved(1)")
<< QStringLiteral("rowsRemoved(1)"));
}
void testRemove()
{
QFETCH(QString, sourceStr);
QFETCH(QString, initialProxyStr);
QFETCH(QString, remove);
QFETCH(QString, expectedProxyStr);
QFETCH(QStringList, expectedSignals);
QStandardItemModel model;
fillModel(model, sourceStr);
QCOMPARE(treeAsString(model), sourceStr);
TestModel proxy(&model);
QCOMPARE(treeAsString(proxy), initialProxyStr);
ModelSignalSpy spy(proxy);
QStandardItem *itemToRemove = itemByText(model, remove);
QVERIFY(itemToRemove);
if (itemToRemove->parent())
itemToRemove->parent()->removeRow(itemToRemove->row());
else
model.removeRow(itemToRemove->row());
QCOMPARE(treeAsString(proxy), expectedProxyStr);
//qDebug() << spy.mSignals;
QCOMPARE(spy.mSignals, expectedSignals);
}
void testStandardFiltering_data()
{
QTest::addColumn<QString>("sourceStr");
QTest::addColumn<QString>("initialProxyStr");
QTest::addColumn<QString>("filter");
QTest::addColumn<QString>("expectedProxyStr");
QTest::newRow("select_child") << "[1[1.1[1.1.1* 1.1.2*]]]" << "[1[1.1[1.1.1* 1.1.2*]]]"
<< "1.1.2" << "[1[1.1[1.1.2*]]]";
QTest::newRow("filter_all_out") << "[1[1.1[1.1.1*]]]" << "[1[1.1[1.1.1*]]]"
<< "test" << "";
QTest::newRow("select_parent") << "[1[1.1[1.1.1*[child*] 1.1.2*]]]" << "[1[1.1[1.1.1*[child*] 1.1.2*]]]"
<< "1.1.1" << "[1[1.1[1.1.1*]]]";
}
void testStandardFiltering()
{
QFETCH(QString, sourceStr);
QFETCH(QString, initialProxyStr);
QFETCH(QString, filter);
QFETCH(QString, expectedProxyStr);
QStandardItemModel model;
fillModel(model, sourceStr);
QCOMPARE(treeAsString(model), sourceStr);
TestModel proxy(&model);
QCOMPARE(treeAsString(proxy), initialProxyStr);
ModelSignalSpy spy(proxy);
//qDebug() << "setFilterFixedString";
proxy.setFilterFixedString(filter);
QCOMPARE(treeAsString(proxy), expectedProxyStr);
}
private:
QStandardItem *itemByText(const QStandardItemModel& model, const QString &text) const {
QModelIndexList list = model.match(model.index(0, 0), Qt::DisplayRole, text, 1, Qt::MatchRecursive);
return list.isEmpty() ? 0 : model.itemFromIndex(list.first());
}
};
QTEST_GUILESS_MAIN(tst_QSortFilterProxyModel_Recursive)
#include "tst_qsortfilterproxymodel_recursive.moc"