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 > QModelIndexPairList; +struct QSortFilterProxyModelDataChanged +{ + QSortFilterProxyModelDataChanged(const QModelIndex &tl, const QModelIndex &br) + : topLeft(tl), bottomRight(br) { } + + QModelIndex topLeft; + QModelIndex bottomRight; +}; + static inline QSet qVectorToSet(const QVector &vector) { QSet 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 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 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 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 QSortFilterProxyModelPrivate::handle_filter_changed( for (int source_item = 0; source_item < source_count; ++source_item) { if (source_to_proxy.at(source_item) == -1) { if ((orient == Qt::Vertical) - ? q->filterAcceptsRow(source_item, source_parent) + ? filterAcceptsRowInternal(source_item, source_parent) : q->filterAcceptsColumn(source_item, source_parent)) { // This source item satisfies the filter, so it must be added source_items_insert.append(source_item); @@ -1163,107 +1204,126 @@ void QSortFilterProxyModelPrivate::_q_sourceDataChanged(const QModelIndex &sourc Q_Q(QSortFilterProxyModel); if (!source_top_left.isValid() || !source_bottom_right.isValid()) return; - QModelIndex source_parent = source_top_left.parent(); - IndexMap::const_iterator it = source_index_mapping.constFind(source_parent); - if (it == source_index_mapping.constEnd()) { - // Don't care, since we don't have mapping for this index - return; - } - Mapping *m = it.value(); - // Figure out how the source changes affect us - QVector source_rows_remove; - QVector source_rows_insert; - QVector source_rows_change; - QVector source_rows_resort; - int end = qMin(source_bottom_right.row(), m->proxy_rows.count() - 1); - for (int source_row = source_top_left.row(); source_row <= end; ++source_row) { - if (dynamic_sortfilter) { - if (m->proxy_rows.at(source_row) != -1) { - if (!q->filterAcceptsRow(source_row, source_parent)) { - // This source row no longer satisfies the filter, so it must be removed - source_rows_remove.append(source_row); - } else if (source_sort_column >= source_top_left.column() && source_sort_column <= source_bottom_right.column()) { - // This source row has changed in a way that may affect sorted order - source_rows_resort.append(source_row); + std::vector data_changed_list; + data_changed_list.emplace_back(source_top_left, source_bottom_right); + + // Do check parents if the filter role have changed and we are recursive + if (filter_recursive && (roles.isEmpty() || roles.contains(filter_role))) { + QModelIndex source_parent = source_top_left.parent(); + + while (source_parent.isValid()) { + data_changed_list.emplace_back(source_parent, source_parent); + source_parent = source_parent.parent(); + } + } + + for (const QSortFilterProxyModelDataChanged &data_changed : data_changed_list) { + const QModelIndex &source_top_left = data_changed.topLeft; + const QModelIndex &source_bottom_right = data_changed.bottomRight; + const QModelIndex source_parent = source_top_left.parent(); + + IndexMap::const_iterator it = source_index_mapping.constFind(source_parent); + if (it == source_index_mapping.constEnd()) { + // Don't care, since we don't have mapping for this index + continue; + } + Mapping *m = it.value(); + + // Figure out how the source changes affect us + QVector source_rows_remove; + QVector source_rows_insert; + QVector source_rows_change; + QVector source_rows_resort; + int end = qMin(source_bottom_right.row(), m->proxy_rows.count() - 1); + for (int source_row = source_top_left.row(); source_row <= end; ++source_row) { + if (dynamic_sortfilter) { + if (m->proxy_rows.at(source_row) != -1) { + if (!filterAcceptsRowInternal(source_row, source_parent)) { + // This source row no longer satisfies the filter, so it must be removed + source_rows_remove.append(source_row); + } else if (source_sort_column >= source_top_left.column() && source_sort_column <= source_bottom_right.column()) { + // This source row has changed in a way that may affect sorted order + source_rows_resort.append(source_row); + } else { + // This row has simply changed, without affecting filtering nor sorting + source_rows_change.append(source_row); + } } else { - // This row has simply changed, without affecting filtering nor sorting - source_rows_change.append(source_row); + if (!itemsBeingRemoved.contains(source_parent, source_row) && filterAcceptsRowInternal(source_row, source_parent)) { + // This source row now satisfies the filter, so it must be added + source_rows_insert.append(source_row); + } } } else { - if (!itemsBeingRemoved.contains(source_parent, source_row) && q->filterAcceptsRow(source_row, source_parent)) { - // This source row now satisfies the filter, so it must be added - source_rows_insert.append(source_row); + if (m->proxy_rows.at(source_row) != -1) + source_rows_change.append(source_row); + } + } + + if (!source_rows_remove.isEmpty()) { + remove_source_items(m->proxy_rows, m->source_rows, + source_rows_remove, source_parent, Qt::Vertical); + QSet source_rows_remove_set = qVectorToSet(source_rows_remove); + QVector::iterator childIt = m->mapped_children.end(); + while (childIt != m->mapped_children.begin()) { + --childIt; + const QModelIndex source_child_index = *childIt; + if (source_rows_remove_set.contains(source_child_index.row())) { + childIt = m->mapped_children.erase(childIt); + remove_from_mapping(source_child_index); } } - } else { - if (m->proxy_rows.at(source_row) != -1) - source_rows_change.append(source_row); } - } - if (!source_rows_remove.isEmpty()) { - remove_source_items(m->proxy_rows, m->source_rows, - source_rows_remove, source_parent, Qt::Vertical); - QSet source_rows_remove_set = qVectorToSet(source_rows_remove); - QVector::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 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 parents; - parents << q->mapFromSource(source_parent); - emit q->layoutAboutToBeChanged(parents, QAbstractItemModel::VerticalSortHint); - QModelIndexPairList source_indexes = store_persistent_indexes(); - remove_source_items(m->proxy_rows, m->source_rows, source_rows_resort, - source_parent, Qt::Vertical, false); - sort_source_rows(source_rows_resort, source_parent); - insert_source_items(m->proxy_rows, m->source_rows, source_rows_resort, - source_parent, Qt::Vertical, false); - update_persistent_indexes(source_indexes); - emit q->layoutChanged(parents, QAbstractItemModel::VerticalSortHint); - // Make sure we also emit dataChanged for the rows - source_rows_change += source_rows_resort; - } - - if (!source_rows_change.isEmpty()) { - // Find the proxy row range - int proxy_start_row; - int proxy_end_row; - proxy_item_range(m->proxy_rows, source_rows_change, - proxy_start_row, proxy_end_row); - // ### Find the proxy column range also - if (proxy_end_row >= 0) { - // the row was accepted, but some columns might still be filtered out - int source_left_column = source_top_left.column(); - while (source_left_column < source_bottom_right.column() - && m->proxy_columns.at(source_left_column) == -1) - ++source_left_column; - const QModelIndex proxy_top_left = create_index( - proxy_start_row, m->proxy_columns.at(source_left_column), it); - int source_right_column = source_bottom_right.column(); - while (source_right_column > source_top_left.column() - && m->proxy_columns.at(source_right_column) == -1) - --source_right_column; - const QModelIndex proxy_bottom_right = create_index( - proxy_end_row, m->proxy_columns.at(source_right_column), it); - emit q->dataChanged(proxy_top_left, proxy_bottom_right, roles); + if (!source_rows_insert.isEmpty()) { + sort_source_rows(source_rows_insert, source_parent); + insert_source_items(m->proxy_rows, m->source_rows, + source_rows_insert, source_parent, Qt::Vertical); } } - - if (!source_rows_insert.isEmpty()) { - sort_source_rows(source_rows_insert, source_parent); - insert_source_items(m->proxy_rows, m->source_rows, - source_rows_insert, source_parent, Qt::Vertical); - } } void QSortFilterProxyModelPrivate::_q_sourceHeaderDataChanged(Qt::Orientation orientation, @@ -1386,18 +1446,60 @@ void QSortFilterProxyModelPrivate::_q_sourceRowsAboutToBeInserted( { Q_UNUSED(start); Q_UNUSED(end); + + const bool toplevel = !source_parent.isValid(); + const bool recursive_accepted = filter_recursive && !toplevel && filterAcceptsRowInternal(source_parent.row(), source_parent.parent()); //Force the creation of a mapping now, even if its empty. //We need it because the proxy can be acessed at the moment it emits rowsAboutToBeInserted in insert_source_items - if (can_create_mapping(source_parent)) - create_mapping(source_parent); + if (!filter_recursive || toplevel || recursive_accepted) { + if (can_create_mapping(source_parent)) + create_mapping(source_parent); + if (filter_recursive) + complete_insert = true; + } else { + // The row could have been rejected or the parent might be not yet known... let's try to discover it + QModelIndex top_source_parent = source_parent; + QModelIndex parent = source_parent.parent(); + QModelIndex grandParent = parent.parent(); + + while (parent.isValid() && !filterAcceptsRowInternal(parent.row(), grandParent)) { + top_source_parent = parent; + parent = grandParent; + grandParent = parent.parent(); + } + + last_top_source = top_source_parent; + } } void QSortFilterProxyModelPrivate::_q_sourceRowsInserted( const QModelIndex &source_parent, int start, int end) { - source_items_inserted(source_parent, start, end, Qt::Vertical); - if (update_source_sort_column() && dynamic_sortfilter) //previous call to update_source_sort_column may fail if the model has no column. - sort(); // now it should succeed so we need to make sure to sort again + if (!filter_recursive || complete_insert) { + if (filter_recursive) + complete_insert = false; + source_items_inserted(source_parent, start, end, Qt::Vertical); + if (update_source_sort_column() && dynamic_sortfilter) //previous call to update_source_sort_column may fail if the model has no column. + sort(); // now it should succeed so we need to make sure to sort again + return; + } + + if (filter_recursive) { + bool accept = false; + + for (int row = start; row <= end; ++row) { + if (filterAcceptsRowInternal(row, source_parent)) { + accept = true; + break; + } + } + + if (!accept) // the new rows have no descendants that match the filter, filter them out. + return; + + // last_top_source should now become visible + _q_sourceDataChanged(last_top_source, last_top_source, QVector()); + } } 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()); + } } void QSortFilterProxyModelPrivate::_q_sourceRowsAboutToBeMoved( @@ -1685,7 +1808,9 @@ QSortFilterProxyModel::QSortFilterProxyModel(QObject *parent) d->sort_localeaware = false; d->filter_column = 0; d->filter_role = Qt::DisplayRole; + d->filter_recursive = false; d->dynamic_sortfilter = true; + d->complete_insert = false; connect(this, SIGNAL(modelReset()), this, SLOT(_q_clearMapping())); } @@ -2505,6 +2630,32 @@ void QSortFilterProxyModel::setFilterRole(int role) d->filter_changed(); } +/*! + \since 5.9 + \property QSortFilterProxyModel::recursiveFiltering + \brief whether the filter to be applied recursively on children, and for + any matching child, its parents will be visible as well. + + The default value is false. + + \sa filterAcceptsRow() +*/ +bool QSortFilterProxyModel::recursiveFiltering() const +{ + Q_D(const QSortFilterProxyModel); + return d->filter_recursive; +} + +void QSortFilterProxyModel::setRecursiveFiltering(bool recursive) +{ + Q_D(QSortFilterProxyModel); + if (d->filter_recursive == recursive) + return; + d->filter_about_to_be_changed(); + d->filter_recursive = recursive; + d->filter_changed(); +} + /*! \obsolete 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 and David Faure +** 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 +#include + +#include +#include + +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("sourceStr"); + QTest::addColumn("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("sourceStr"); + QTest::addColumn("initialProxyStr"); + QTest::addColumn("add"); // set the flag on this item + QTest::addColumn("expectedProxyStr"); + QTest::addColumn("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("sourceStr"); + QTest::addColumn("initialProxyStr"); + QTest::addColumn("remove"); // unset the flag on this item + QTest::addColumn("expectedProxyStr"); + QTest::addColumn("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("sourceStr"); + QTest::addColumn("initialProxyStr"); + QTest::addColumn("remove"); // remove this item + QTest::addColumn("expectedProxyStr"); + QTest::addColumn("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("sourceStr"); + QTest::addColumn("initialProxyStr"); + QTest::addColumn("filter"); + QTest::addColumn("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"