diff --git a/qqmlsortfilterproxymodel.cpp b/qqmlsortfilterproxymodel.cpp index 6c17144..5eb4bc1 100644 --- a/qqmlsortfilterproxymodel.cpp +++ b/qqmlsortfilterproxymodel.cpp @@ -26,7 +26,12 @@ namespace qqsfpm { */ QQmlSortFilterProxyModel::QQmlSortFilterProxyModel(QObject *parent) : - QSortFilterProxyModel(parent) + QSortFilterProxyModel(parent), +#ifdef SFPM_DELAYED + m_delayed(true) +#else + m_delayed(false) +#endif { connect(this, &QAbstractProxyModel::sourceModelChanged, this, &QQmlSortFilterProxyModel::updateRoles); connect(this, &QAbstractItemModel::modelReset, this, &QQmlSortFilterProxyModel::updateRoles); @@ -55,6 +60,30 @@ int QQmlSortFilterProxyModel::count() const return rowCount(); } +/*! + \qmlproperty int SortFilterProxyModel::delayed + + Delay the execution of filters, sorters and proxyRoles until the next event loop. + This can be used as an optimization when multiple filters, sorters or proxyRoles are changed in a single event loop. + They will be executed once in a single batch at the next event loop instead of being executed in multiple sequential batches. + + By default, the SortFilterProxyModel is not delayed, unless the SFPM_DELAYED is defined at compile time. +*/ + +bool QQmlSortFilterProxyModel::delayed() const +{ + return m_delayed; +} + +void QQmlSortFilterProxyModel::setDelayed(bool delayed) +{ + if (m_delayed == delayed) + return; + + m_delayed = delayed; + Q_EMIT delayedChanged(); +} + const QString& QQmlSortFilterProxyModel::filterRoleName() const { return m_filterRoleName; @@ -114,7 +143,7 @@ void QQmlSortFilterProxyModel::setFilterValue(const QVariant& filterValue) return; m_filterValue = filterValue; - invalidateFilter(); + queueInvalidateFilter(); Q_EMIT filterValueChanged(); } @@ -153,7 +182,7 @@ void QQmlSortFilterProxyModel::setAscendingSortOrder(bool ascendingSortOrder) m_ascendingSortOrder = ascendingSortOrder; Q_EMIT ascendingSortOrderChanged(); - invalidate(); + queueInvalidate(); } /*! @@ -362,14 +391,40 @@ void QQmlSortFilterProxyModel::setSourceModel(QAbstractItemModel *sourceModel) QSortFilterProxyModel::setSourceModel(sourceModel); } +void QQmlSortFilterProxyModel::queueInvalidateFilter() +{ + if (m_delayed) { + if (!m_invalidateFilterQueued && !m_invalidateQueued) { + m_invalidateFilterQueued = true; + QMetaObject::invokeMethod(this, "invalidateFilter", Qt::QueuedConnection); + } + } else { + invalidateFilter(); + } +} + void QQmlSortFilterProxyModel::invalidateFilter() { - if (m_completed) + m_invalidateFilterQueued = false; + if (m_completed && !m_invalidateQueued) QSortFilterProxyModel::invalidateFilter(); } +void QQmlSortFilterProxyModel::queueInvalidate() +{ + if (m_delayed) { + if (!m_invalidateQueued) { + m_invalidateQueued = true; + QMetaObject::invokeMethod(this, "invalidate", Qt::QueuedConnection); + } + } else { + invalidate(); + } +} + void QQmlSortFilterProxyModel::invalidate() { + m_invalidateQueued = false; if (m_completed) QSortFilterProxyModel::invalidate(); } @@ -410,7 +465,7 @@ void QQmlSortFilterProxyModel::updateSortRole() if (!sortRoles.empty()) { setSortRole(sortRoles.first()); - invalidate(); + queueInvalidate(); } } @@ -434,10 +489,24 @@ void QQmlSortFilterProxyModel::onDataChanged(const QModelIndex& topLeft, const Q Q_EMIT dataChanged(topLeft, bottomRight, m_proxyRoleNumbers); } -void QQmlSortFilterProxyModel::emitProxyRolesChanged() +void QQmlSortFilterProxyModel::queueInvalidateProxyRoles() { - invalidate(); - Q_EMIT dataChanged(index(0,0), index(rowCount() - 1, columnCount() - 1), m_proxyRoleNumbers); + queueInvalidate(); + if (m_delayed) { + if (!m_invalidateProxyRolesQueued) { + m_invalidateProxyRolesQueued = true; + QMetaObject::invokeMethod(this, "invalidateProxyRoles", Qt::QueuedConnection); + } + } else { + invalidateProxyRoles(); + } +} + +void QQmlSortFilterProxyModel::invalidateProxyRoles() +{ + m_invalidateProxyRolesQueued = false; + if (m_completed) + Q_EMIT dataChanged(index(0,0), index(rowCount() - 1, columnCount() - 1), m_proxyRoleNumbers); } QVariantMap QQmlSortFilterProxyModel::modelDataMap(const QModelIndex& modelIndex) const @@ -451,42 +520,42 @@ QVariantMap QQmlSortFilterProxyModel::modelDataMap(const QModelIndex& modelIndex void QQmlSortFilterProxyModel::onFilterAppended(Filter* filter) { - connect(filter, &Filter::invalidated, this, &QQmlSortFilterProxyModel::invalidateFilter); - this->invalidateFilter(); + connect(filter, &Filter::invalidated, this, &QQmlSortFilterProxyModel::queueInvalidateFilter); + queueInvalidateFilter(); } void QQmlSortFilterProxyModel::onFilterRemoved(Filter* filter) { Q_UNUSED(filter) - invalidateFilter(); + queueInvalidateFilter(); } void QQmlSortFilterProxyModel::onFiltersCleared() { - invalidateFilter(); + queueInvalidateFilter(); } void QQmlSortFilterProxyModel::onSorterAppended(Sorter* sorter) { - connect(sorter, &Sorter::invalidated, this, &QQmlSortFilterProxyModel::invalidate); - invalidate(); + connect(sorter, &Sorter::invalidated, this, &QQmlSortFilterProxyModel::queueInvalidate); + queueInvalidate(); } void QQmlSortFilterProxyModel::onSorterRemoved(Sorter* sorter) { Q_UNUSED(sorter) - invalidate(); + queueInvalidate(); } void QQmlSortFilterProxyModel::onSortersCleared() { - invalidate(); + queueInvalidate(); } void QQmlSortFilterProxyModel::onProxyRoleAppended(ProxyRole *proxyRole) { beginResetModel(); - connect(proxyRole, &ProxyRole::invalidated, this, &QQmlSortFilterProxyModel::emitProxyRolesChanged); + connect(proxyRole, &ProxyRole::invalidated, this, &QQmlSortFilterProxyModel::queueInvalidateProxyRoles); connect(proxyRole, &ProxyRole::namesAboutToBeChanged, this, &QQmlSortFilterProxyModel::beginResetModel); connect(proxyRole, &ProxyRole::namesChanged, this, &QQmlSortFilterProxyModel::endResetModel); endResetModel(); diff --git a/qqmlsortfilterproxymodel.h b/qqmlsortfilterproxymodel.h index 647ab38..dbe0229 100644 --- a/qqmlsortfilterproxymodel.h +++ b/qqmlsortfilterproxymodel.h @@ -22,6 +22,7 @@ class QQmlSortFilterProxyModel : public QSortFilterProxyModel, Q_INTERFACES(qqsfpm::ProxyRoleContainer) Q_PROPERTY(int count READ count NOTIFY countChanged) + Q_PROPERTY(bool delayed READ delayed WRITE setDelayed NOTIFY delayedChanged) Q_PROPERTY(QString filterRoleName READ filterRoleName WRITE setFilterRoleName NOTIFY filterRoleNameChanged) Q_PROPERTY(QString filterPattern READ filterPattern WRITE setFilterPattern NOTIFY filterPatternChanged) @@ -49,6 +50,9 @@ public: int count() const; + bool delayed() const; + void setDelayed(bool delayed); + const QString& filterRoleName() const; void setFilterRoleName(const QString& filterRoleName); @@ -90,6 +94,7 @@ public: Q_SIGNALS: void countChanged(); + void delayedChanged(); void filterRoleNameChanged(); void filterPatternSyntaxChanged(); @@ -107,7 +112,9 @@ protected Q_SLOTS: void resetInternalData(); private Q_SLOTS: + void queueInvalidateFilter(); void invalidateFilter(); + void queueInvalidate(); void invalidate(); void updateRoleNames(); void updateFilterRole(); @@ -115,7 +122,8 @@ private Q_SLOTS: void updateRoles(); void initRoles(); void onDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight, const QVector& roles); - void emitProxyRolesChanged(); + void queueInvalidateProxyRoles(); + void invalidateProxyRoles(); private: QVariantMap modelDataMap(const QModelIndex& modelIndex) const; @@ -132,6 +140,7 @@ private: void onProxyRoleRemoved(ProxyRole *proxyRole) override; void onProxyRolesCleared() override; + bool m_delayed; QString m_filterRoleName; QVariant m_filterValue; QString m_sortRoleName; @@ -140,6 +149,10 @@ private: QHash m_roleNames; QHash> m_proxyRoleMap; QVector m_proxyRoleNumbers; + + bool m_invalidateFilterQueued = false; + bool m_invalidateQueued = false; + bool m_invalidateProxyRolesQueued = false; }; } diff --git a/tests/SortFilterProxyModel.pro b/tests/SortFilterProxyModel.pro index 743af57..8c708ce 100644 --- a/tests/SortFilterProxyModel.pro +++ b/tests/SortFilterProxyModel.pro @@ -32,4 +32,5 @@ DISTFILES += \ tst_filtercontainers.qml \ tst_regexprole.qml \ tst_filtersorter.qml \ - tst_filterrole.qml + tst_filterrole.qml \ + tst_delayed.qml diff --git a/tests/tst_delayed.qml b/tests/tst_delayed.qml new file mode 100644 index 0000000..7102a6e --- /dev/null +++ b/tests/tst_delayed.qml @@ -0,0 +1,207 @@ +import QtQuick 2.0 +import QtQml 2.2 +import QtTest 1.1 +import SortFilterProxyModel 0.2 +import SortFilterProxyModel.Test 0.2 + +Item { + ListModel { + id: testModel1 + ListElement{ role1: 1 } + } + SortFilterProxyModel { + id: testFilterProxyModel + sourceModel: testModel1 + property int foo: 1 + filters: [ + ExpressionFilter { + id: expressionFilter + property var w: ({count : 0}) // wrap count in a js object so modifying it doesn't bind it in the expression + expression: { + ++w.count; + testFilterProxyModel.foo; + return true; + } + }, + ValueFilter { + roleName: "role1" + value: testFilterProxyModel.foo + }, + ValueFilter { + roleName: "role1" + value: testFilterProxyModel.foo + } + ] + sorters: RoleSorter { + roleName: "role1" + sortOrder: testFilterProxyModel.foo === 1 ? Qt.AscendingOrder : Qt.DescendingOrder + } + } + + ListModel { + id: testModel2 + ListElement{ role1: 1 } + ListElement{ role1: 2 } + } + SortFilterProxyModel { + id: testSorterProxyModel + sourceModel: testModel2 + property bool foo: true + sorters: [ + ExpressionSorter { + id: expressionSorter + property var w: ({count : 0}) // wrap count in a js object so modifying it doesn't bind it in the expression + expression: { + ++w.count; + testSorterProxyModel.foo; + return false; + } + }, + RoleSorter { + roleName: "role1" + sortOrder: testSorterProxyModel.foo ? Qt.AscendingOrder : Qt.DescendingOrder + }, + RoleSorter { + roleName: "role1" + sortOrder: testSorterProxyModel.foo ? Qt.AscendingOrder : Qt.DescendingOrder + } + ] + } + + SortFilterProxyModel { + id: testRolesProxyModel + sourceModel: testModel1 + property bool foo: true + proxyRoles: [ + StaticRole { + name: "display" + value: 5 + }, + ExpressionRole { + id: expressionRole + name: "expressionRole" + property var w: ({count : 0}) // wrap count in a js object so modifying it doesn't bind it in the expression + expression: { + ++w.count; + return testRolesProxyModel.foo; + } + }, + StaticRole { + name: "role1" + value: testRolesProxyModel.foo + }, + StaticRole { + name: "role2" + value: testRolesProxyModel.foo + } + ] + } + + SignalSpy { + id: dataChangedSpy + target: testRolesProxyModel + signalName: "dataChanged" + } + + Instantiator { + id: instantiator + model: testRolesProxyModel + delegate: QtObject { property bool foo: model.expressionRole; property bool foo2: model.expressionRole } + } + + TestCase { + name: "DelayedTest" + + function test_directFilters() { + testFilterProxyModel.delayed = false; + expressionFilter.w.count = 0; + testFilterProxyModel.foo = 2; + compare(testFilterProxyModel.count, 0); + compare(expressionFilter.w.count, 4); + wait(0); + compare(testFilterProxyModel.count, 0); + compare(expressionFilter.w.count, 4); + } + + function test_delayedFilters() { + testFilterProxyModel.delayed = false; + testFilterProxyModel.foo = 2; + compare(testFilterProxyModel.count, 0); + testFilterProxyModel.delayed = true; + expressionFilter.w.count = 0; + testFilterProxyModel.foo = 0; + testFilterProxyModel.foo = 1; + compare(testFilterProxyModel.count, 0); + compare(expressionFilter.w.count, 0); + wait(0); + compare(testFilterProxyModel.count, 1); + compare(expressionFilter.w.count, 1); + } + + function test_directSorters() { + testSorterProxyModel.delayed = false; + testSorterProxyModel.foo = true; + compare(testSorterProxyModel.get(0).role1, 1); + expressionSorter.w.count = 0; + testSorterProxyModel.foo = false; + compare(testSorterProxyModel.get(0).role1, 2); + compare(expressionSorter.w.count, 6); + wait(0); + compare(testSorterProxyModel.get(0).role1, 2); + compare(expressionSorter.w.count, 6); + } + + function test_delayedSorters() { + testSorterProxyModel.delayed = false; + testSorterProxyModel.foo = true; + compare(testSorterProxyModel.get(0).role1, 1); + testSorterProxyModel.delayed = true; + expressionSorter.w.count = 0; + testSorterProxyModel.foo = false; + testSorterProxyModel.foo = true; + testSorterProxyModel.foo = false; + compare(testSorterProxyModel.get(0).role1, 1); + compare(expressionSorter.w.count, 0); + wait(0); + compare(testSorterProxyModel.get(0).role1, 2); + compare(expressionSorter.w.count, 2); + } + + function test_proxyRoles() { + // init not delayed + testRolesProxyModel.delayed = false; + testRolesProxyModel.foo = true; + compare(instantiator.object.foo, true); + expressionRole.w.count = 0; + dataChangedSpy.clear(); + + // test not delayed + testRolesProxyModel.foo = false; + compare(instantiator.object.foo, false); + compare(dataChangedSpy.count, 3); + var notDelayedCount = expressionRole.w.count; // why is it 12 and not just 3 ? + wait(0); + compare(instantiator.object.foo, false); + compare(dataChangedSpy.count, 3); + compare(expressionRole.w.count, notDelayedCount); + + // init delayed + testRolesProxyModel.delayed = true; + expressionRole.w.count = 0; + dataChangedSpy.clear(); + + // test delayed + testRolesProxyModel.foo = true; + testRolesProxyModel.foo = false; + testRolesProxyModel.foo = true; + compare(instantiator.object.foo, false); + compare(dataChangedSpy.count, 0); + compare(expressionRole.w.count, 0); + wait(0); + compare(instantiator.object.foo, true); + compare(dataChangedSpy.count, 1); + var expectedDelayedCount = notDelayedCount / 3; + compare(expressionRole.w.count, expectedDelayedCount); + } + } +}