summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorFilipe Azevedo <filipe.azevedo@kdab.com>2017-03-02 18:55:20 +0100
committerDavid Faure <david.faure@kdab.com>2017-03-14 21:34:37 +0000
commit7f1a220ee423a72e802452bb11f34f11e37a74d1 (patch)
tree216f7a3590e5738af63e049e744456e0c23855d4
parent29bcbeab90210da80234529905d17280374f9684 (diff)
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>
-rw-r--r--src/corelib/itemmodels/qsortfilterproxymodel.cpp347
-rw-r--r--src/corelib/itemmodels/qsortfilterproxymodel.h4
-rw-r--r--tests/auto/corelib/itemmodels/itemmodels.pro1
-rw-r--r--tests/auto/corelib/itemmodels/qsortfilterproxymodel_recursive/.gitignore1
-rw-r--r--tests/auto/corelib/itemmodels/qsortfilterproxymodel_recursive/qsortfilterproxymodel_recursive.pro8
-rw-r--r--tests/auto/corelib/itemmodels/qsortfilterproxymodel_recursive/tst_qsortfilterproxymodel_recursive.cpp727
6 files changed, 990 insertions, 98 deletions
diff --git a/src/corelib/itemmodels/qsortfilterproxymodel.cpp b/src/corelib/itemmodels/qsortfilterproxymodel.cpp
index 226a2401e1..4d19d5e31c 100644
--- a/src/corelib/itemmodels/qsortfilterproxymodel.cpp
+++ b/src/corelib/itemmodels/qsortfilterproxymodel.cpp
@@ -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,106 +1204,125 @@ 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;
+
+ 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();
+ }
}
- 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);
+ 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);
}
- } 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_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);
+ }
}
}
@@ -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()));
}
@@ -2506,6 +2631,32 @@ void QSortFilterProxyModel::setFilterRole(int role)
}
/*!
+ \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
This function is obsolete. Use invalidate() instead.
diff --git a/src/corelib/itemmodels/qsortfilterproxymodel.h b/src/corelib/itemmodels/qsortfilterproxymodel.h
index 06ff79ef5f..6f2e9806ed 100644
--- a/src/corelib/itemmodels/qsortfilterproxymodel.h
+++ b/src/corelib/itemmodels/qsortfilterproxymodel.h
@@ -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);
diff --git a/tests/auto/corelib/itemmodels/itemmodels.pro b/tests/auto/corelib/itemmodels/itemmodels.pro
index 7e0e3a0944..3552f30632 100644
--- a/tests/auto/corelib/itemmodels/itemmodels.pro
+++ b/tests/auto/corelib/itemmodels/itemmodels.pro
@@ -7,6 +7,7 @@ qtHaveModule(gui): SUBDIRS += \
qabstractproxymodel \
qidentityproxymodel \
qitemselectionmodel \
+ qsortfilterproxymodel_recursive \
qtHaveModule(widgets): SUBDIRS += \
qitemmodel \
diff --git a/tests/auto/corelib/itemmodels/qsortfilterproxymodel_recursive/.gitignore b/tests/auto/corelib/itemmodels/qsortfilterproxymodel_recursive/.gitignore
new file mode 100644
index 0000000000..2007aaabbd
--- /dev/null
+++ b/tests/auto/corelib/itemmodels/qsortfilterproxymodel_recursive/.gitignore
@@ -0,0 +1 @@
+tst_qsortfilterproxymodel_recursive
diff --git a/tests/auto/corelib/itemmodels/qsortfilterproxymodel_recursive/qsortfilterproxymodel_recursive.pro b/tests/auto/corelib/itemmodels/qsortfilterproxymodel_recursive/qsortfilterproxymodel_recursive.pro
new file mode 100644
index 0000000000..a8b793dbc6
--- /dev/null
+++ b/tests/auto/corelib/itemmodels/qsortfilterproxymodel_recursive/qsortfilterproxymodel_recursive.pro
@@ -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
diff --git a/tests/auto/corelib/itemmodels/qsortfilterproxymodel_recursive/tst_qsortfilterproxymodel_recursive.cpp b/tests/auto/corelib/itemmodels/qsortfilterproxymodel_recursive/tst_qsortfilterproxymodel_recursive.cpp
new file mode 100644
index 0000000000..54c79e0893
--- /dev/null
+++ b/tests/auto/corelib/itemmodels/qsortfilterproxymodel_recursive/tst_qsortfilterproxymodel_recursive.cpp
@@ -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"