summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-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"