diff --git a/autotests/ksortfilterproxymodel_qml.cpp b/autotests/ksortfilterproxymodel_qml.cpp index 98ffbd4..7bf4cf2 100644 --- a/autotests/ksortfilterproxymodel_qml.cpp +++ b/autotests/ksortfilterproxymodel_qml.cpp @@ -1,181 +1,201 @@ /* Copyright (C) 2019 David Edmundson This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include #include #include #include #include #include #include class tst_KSortFilterProxyModelQml : public QObject { Q_OBJECT private Q_SLOTS: void testFilterCallback(); void testSortRole_data(); void testSortRole(); void testFilterRegExp(); void testFilterRegExpRole(); private: QAbstractItemModel* createMonthTestModel(QObject *parent); }; QAbstractItemModel* tst_KSortFilterProxyModelQml::createMonthTestModel(QObject *parent) { auto testModel = new QStandardItemModel(parent); for (int i = 1; i <= 12 ; i++) { auto entry = new QStandardItem(); entry->setData(QLocale::c().monthName(i), Qt::DisplayRole); entry->setData(i, Qt::UserRole); testModel->appendRow(entry); } testModel->setItemRoleNames({{Qt::UserRole, "user"}, {Qt::DisplayRole, "display"}}); return testModel; } void tst_KSortFilterProxyModelQml::testFilterCallback() { QQmlApplicationEngine app; app.loadData("import QtQml 2.0\n" "import org.kde.kitemmodels 1.0\n" "KSortFilterProxyModel\n" "{\n" + " property int modulo: 2\n" " sourceModel: KNumberModel {\n" " minimumValue: 1\n" " maximumValue: 10\n" " }\n" " filterRowCallback: function(source_row, source_parent) {\n" - " return sourceModel.data(sourceModel.index(source_row, 0, source_parent), Qt.DisplayRole) % 2 == 1;\n" + " return sourceModel.data(sourceModel.index(source_row, 0, source_parent), Qt.DisplayRole) % modulo == 1;\n" " };\n" "}\n"); QCOMPARE(app.rootObjects().count(), 1); auto filterModel = qobject_cast(app.rootObjects().first()); QVERIFY(filterModel); QCOMPARE(filterModel->rowCount(), 5); QCOMPARE(filterModel->data(filterModel->index(0, 0)).toString(), "1"); QCOMPARE(filterModel->data(filterModel->index(1, 0)).toString(), "3"); QCOMPARE(filterModel->data(filterModel->index(2, 0)).toString(), "5"); QCOMPARE(filterModel->data(filterModel->index(3, 0)).toString(), "7"); QCOMPARE(filterModel->data(filterModel->index(4, 0)).toString(), "9"); -} + filterModel->setProperty("modulo", 3); + + // Nothing should change until we call invalidateFilter + QCOMPARE(filterModel->rowCount(), 5); + QCOMPARE(filterModel->data(filterModel->index(0, 0)).toString(), "1"); + QCOMPARE(filterModel->data(filterModel->index(1, 0)).toString(), "3"); + QCOMPARE(filterModel->data(filterModel->index(2, 0)).toString(), "5"); + QCOMPARE(filterModel->data(filterModel->index(3, 0)).toString(), "7"); + QCOMPARE(filterModel->data(filterModel->index(4, 0)).toString(), "9"); + + // Simulate call from QML by going through metaobject rather than calling it directly + const bool invalidateFilterCallOk = QMetaObject::invokeMethod(filterModel, "invalidateFilter"); + QVERIFY(invalidateFilterCallOk); + + QCOMPARE(filterModel->rowCount(), 4); + QCOMPARE(filterModel->data(filterModel->index(0, 0)).toString(), "1"); + QCOMPARE(filterModel->data(filterModel->index(1, 0)).toString(), "4"); + QCOMPARE(filterModel->data(filterModel->index(2, 0)).toString(), "7"); + QCOMPARE(filterModel->data(filterModel->index(3, 0)).toString(), "10"); +} void tst_KSortFilterProxyModelQml::testSortRole_data() { // test model consists of all month names + month number as Display and UserRoler respectively QTest::addColumn("qmlContents"); QTest::addColumn("result"); QTest::newRow("sort by role name - display") << "KSortFilterProxyModel {" " sourceModel: testModel;" " sortRole: \"display\";" "}" << "April"; QTest::newRow("sort by role name - value") << "KSortFilterProxyModel {" " sourceModel: testModel;" " sortRole: \"user\";" "}" << "January"; QTest::newRow("sort by role name - reset") << "KSortFilterProxyModel {" " sourceModel: testModel;" " sortRole: \"\";" " Component.onCompleted: sortRole = \"\";" "}" << "January"; } void tst_KSortFilterProxyModelQml::testSortRole() { QQmlApplicationEngine app; QFETCH(QString, qmlContents); QFETCH(QString, result); qmlContents = "import org.kde.kitemmodels 1.0\n" "import QtQuick 2.0\n" + qmlContents; app.rootContext()->setContextProperty("testModel", createMonthTestModel(&app)); app.loadData(qmlContents.toLatin1()); QCOMPARE(app.rootObjects().count(), 1); auto filterModel = qobject_cast(app.rootObjects().first()); QVERIFY(filterModel); QCOMPARE(filterModel->rowCount(), 12); QCOMPARE(filterModel->data(filterModel->index(0, 0), Qt::DisplayRole).toString(), result); } void tst_KSortFilterProxyModelQml::testFilterRegExp() { // filterRegExp comes from the QSortFilterProxyModel direclty, confirm it still works QQmlApplicationEngine app; app.rootContext()->setContextProperty("testModel", createMonthTestModel(&app)); app.loadData("import QtQml 2.0\n" "import org.kde.kitemmodels 1.0\n" "KSortFilterProxyModel {\n" " sourceModel: testModel\n" " filterRegExp: /Ma.*/\n" "}\n"); QCOMPARE(app.rootObjects().count(), 1); auto filterModel = qobject_cast(app.rootObjects().first()); QVERIFY(filterModel); QCOMPARE(filterModel->rowCount(), 2); QCOMPARE(filterModel->data(filterModel->index(0, 0), Qt::DisplayRole).toString(), "March"); QCOMPARE(filterModel->data(filterModel->index(1, 0), Qt::DisplayRole).toString(), "May"); } void tst_KSortFilterProxyModelQml::testFilterRegExpRole() { // filterRegExp comes from the QSortFilterProxyModel direclty, confirm it still works QQmlApplicationEngine app; app.rootContext()->setContextProperty("testModel", createMonthTestModel(&app)); app.loadData("import QtQml 2.0\n" "import org.kde.kitemmodels 1.0\n" "KSortFilterProxyModel {\n" " sourceModel: testModel\n" " filterRole: \"user\"\n" " filterRegExp: /1[0-9]/\n" // month value is 10 or more "}\n"); QCOMPARE(app.rootObjects().count(), 1); auto filterModel = qobject_cast(app.rootObjects().first()); QVERIFY(filterModel); QCOMPARE(filterModel->rowCount(), 3); QCOMPARE(filterModel->data(filterModel->index(0, 0), Qt::DisplayRole).toString(), "October"); QCOMPARE(filterModel->data(filterModel->index(1, 0), Qt::DisplayRole).toString(), "November"); QCOMPARE(filterModel->data(filterModel->index(2, 0), Qt::DisplayRole).toString(), "December"); } QTEST_GUILESS_MAIN(tst_KSortFilterProxyModelQml) #include "ksortfilterproxymodel_qml.moc" diff --git a/src/qml/ksortfilterproxymodel.cpp b/src/qml/ksortfilterproxymodel.cpp index 0513055..c134449 100644 --- a/src/qml/ksortfilterproxymodel.cpp +++ b/src/qml/ksortfilterproxymodel.cpp @@ -1,222 +1,227 @@ /* * Copyright 2010 by Marco Martin * Copyright 2019 by David Edmundson * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Library General Public License as * published by the Free Software Foundation; either version 2, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details * * You should have received a copy of the GNU Library General Public * License along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "ksortfilterproxymodel.h" #include #include #include "kitemmodels_debug.h" KSortFilterProxyModel::KSortFilterProxyModel(QObject *parent) : QSortFilterProxyModel(parent) { setDynamicSortFilter(true); } KSortFilterProxyModel::~KSortFilterProxyModel() { } void KSortFilterProxyModel::syncRoleNames() { if (!sourceModel()) { return; } m_roleIds.clear(); const QHash rNames = roleNames(); m_roleIds.reserve(rNames.count()); for (auto i = rNames.constBegin(); i != rNames.constEnd(); ++i) { m_roleIds[QString::fromUtf8(i.value())] = i.key(); } } int KSortFilterProxyModel::roleNameToId(const QString &name) const { return m_roleIds.value(name, Qt::DisplayRole); } void KSortFilterProxyModel::setModel(QAbstractItemModel *model) { if (model == sourceModel()) { return; } QSortFilterProxyModel::setSourceModel(model); if (m_componentCompleted) { syncRoleNames(); setFilterRole(m_filterRole); setSortRole(m_sortRole); } } bool KSortFilterProxyModel::filterAcceptsRow(int source_row, const QModelIndex& source_parent) const { if (m_filterRowCallback.isCallable()) { QJSEngine *engine = qjsEngine(this); QJSValueList args = {QJSValue(source_row), engine->toScriptValue(source_parent)}; QJSValue result = const_cast(this)->m_filterRowCallback.call(args); if (result.isError()) { qCWarning(KITEMMODELS_LOG) << "Row filter callback produced an error:"; qCWarning(KITEMMODELS_LOG) << result.toString(); return true; } else { return result.toBool(); } } return QSortFilterProxyModel::filterAcceptsRow(source_row, source_parent); } bool KSortFilterProxyModel::filterAcceptsColumn(int source_column, const QModelIndex &source_parent) const { if (m_filterColumnCallback.isCallable()) { QJSEngine *engine = qjsEngine(this); QJSValueList args = {QJSValue(source_column), engine->toScriptValue(source_parent)}; QJSValue result = const_cast(this)->m_filterColumnCallback.call(args); if (result.isError()) { qCWarning(KITEMMODELS_LOG) << "Row filter callback produced an error:"; qCWarning(KITEMMODELS_LOG) << result.toString(); return true; } else { return result.toBool(); } } return QSortFilterProxyModel::filterAcceptsColumn(source_column, source_parent); } void KSortFilterProxyModel::setFilterString(const QString &filterString) { if (filterString == m_filterString) { return; } m_filterString = filterString; QSortFilterProxyModel::setFilterFixedString(filterString); Q_EMIT filterStringChanged(); } QString KSortFilterProxyModel::filterString() const { return m_filterString; } QJSValue KSortFilterProxyModel::filterRowCallback() const { return m_filterRowCallback; } void KSortFilterProxyModel::setFilterRowCallback(const QJSValue& callback) { if (m_filterRowCallback.strictlyEquals(callback)) { return; } if (!callback.isNull() && !callback.isCallable()) { return; } m_filterRowCallback = callback; invalidateFilter(); Q_EMIT filterRowCallbackChanged(callback); } void KSortFilterProxyModel::setFilterColumnCallback(const QJSValue &callback) { if (m_filterColumnCallback.strictlyEquals(callback)) { return; } if (!callback.isNull() && !callback.isCallable()) { return; } m_filterColumnCallback = callback; invalidateFilter(); Q_EMIT filterColumnCallbackChanged(callback); } QJSValue KSortFilterProxyModel::filterColumnCallback() const { return m_filterColumnCallback; } void KSortFilterProxyModel::setFilterRole(const QString &role) { QSortFilterProxyModel::setFilterRole(roleNameToId(role)); m_filterRole = role; Q_EMIT filterRoleChanged(); } QString KSortFilterProxyModel::filterRole() const { return m_filterRole; } void KSortFilterProxyModel::setSortRole(const QString &role) { m_sortRole = role; if (role.isEmpty()) { sort(-1, Qt::AscendingOrder); } else if (sourceModel()) { QSortFilterProxyModel::setSortRole(roleNameToId(role)); sort(std::max(sortColumn(), 0), sortOrder()); } Q_EMIT sortRoleChanged(); } QString KSortFilterProxyModel::sortRole() const { return m_sortRole; } void KSortFilterProxyModel::setSortOrder(const Qt::SortOrder order) { sort(std::max(sortColumn(), 0), order); Q_EMIT sortOrderChanged(); } void KSortFilterProxyModel::setSortColumn(int column) { if (column == sortColumn()) { return; } sort(column, sortOrder()); Q_EMIT sortColumnChanged(); } void KSortFilterProxyModel::classBegin() { } void KSortFilterProxyModel::componentComplete() { m_componentCompleted = true; if (sourceModel()) { syncRoleNames(); setFilterRole(m_filterRole); setSortRole(m_sortRole); } } + +void KSortFilterProxyModel::invalidateFilter() +{ + QSortFilterProxyModel::invalidateFilter(); +} diff --git a/src/qml/ksortfilterproxymodel.h b/src/qml/ksortfilterproxymodel.h index 430d3c1..1261876 100644 --- a/src/qml/ksortfilterproxymodel.h +++ b/src/qml/ksortfilterproxymodel.h @@ -1,153 +1,164 @@ /* * Copyright 2010 by Marco Martin * Copyright 2019 by David Edmundson * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Library General Public License as * published by the Free Software Foundation; either version 2, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details * * You should have received a copy of the GNU Library General Public * License along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef KSORTFILTERPROXYMODEL_H #define KSORTFILTERPROXYMODEL_H #include #include #include #include #include /** * @class SortFilterModel * @short Filter and sort an existing QAbstractItemModel * * @since 5.67 */ class KSortFilterProxyModel : public QSortFilterProxyModel, public QQmlParserStatus { Q_OBJECT Q_INTERFACES(QQmlParserStatus) /** * The source model of this sorting proxy model. */ Q_PROPERTY(QAbstractItemModel *sourceModel READ sourceModel WRITE setModel NOTIFY sourceModelChanged) /** * The string for the filter, only rows with their filterRole matching filterString will be displayed */ Q_PROPERTY(QString filterString READ filterString WRITE setFilterString NOTIFY filterStringChanged) /** * A JavaScript callable that can be used to perform advanced filters on a given row. * The callback is passed the source row, and source parent for a given row as arguments * * The callable's return value is evaluated as boolean to determine * whether the row is accepted (true) or filtered out (false). It overrides the default implementation * that uses filterRegExp or filterString; while filterCallback is set those two properties are * ignored. Attempts to write a non-callable to this property are silently ignored, but you can set * it to null. * * @code * filterRowCallback: function(source_row, source_parent) { * return sourceModel.data(sourceModel.index(source_row, 0, source_parent), Qt.DisplayRole) == "..."; * }; * @endcode */ Q_PROPERTY(QJSValue filterRowCallback READ filterRowCallback WRITE setFilterRowCallback NOTIFY filterRowCallbackChanged) /** * A JavaScript callable that can be used to perform advanced filters on a given column. * The callback is passed the source column, and source parent for a given column as arguments. * * @see filterRowCallback */ Q_PROPERTY(QJSValue filterColumnCallback READ filterColumnCallback WRITE setFilterColumnCallback NOTIFY filterColumnCallbackChanged) /** * The role of the sourceModel on which the filter will be applied. */ Q_PROPERTY(QString filterRole READ filterRole WRITE setFilterRole NOTIFY filterRoleChanged) /** * The role of the sourceModel that will be used for sorting. if empty the order will be left unaltered */ Q_PROPERTY(QString sortRole READ sortRole WRITE setSortRole NOTIFY sortRoleChanged) /** * One of Qt.Ascending or Qt.Descending */ Q_PROPERTY(Qt::SortOrder sortOrder READ sortOrder WRITE setSortOrder NOTIFY sortOrderChanged) /** * Specify which column shoud be used for sorting * The default value is -1. * If \a sortRole is set, the default value is 0. */ Q_PROPERTY(int sortColumn READ sortColumn WRITE setSortColumn NOTIFY sortColumnChanged) public: explicit KSortFilterProxyModel(QObject *parent = nullptr); ~KSortFilterProxyModel(); void setModel(QAbstractItemModel *source); void setFilterRowCallback(const QJSValue &callback); QJSValue filterRowCallback() const; void setFilterString(const QString &filterString); QString filterString() const; void setFilterColumnCallback(const QJSValue &callback); QJSValue filterColumnCallback() const; void setFilterRole(const QString &role); QString filterRole() const; void setSortRole(const QString &role); QString sortRole() const; void setSortOrder(const Qt::SortOrder order); void setSortColumn(int column); void classBegin() override; void componentComplete() override; +public Q_SLOTS: + /** + * Invalidates the current filtering. + * + * This function should be called if you are implementing custom filtering through + * filterRowCallback or filterColumnCallback, and your filter parameters have changed. + * + * @since 5.70 + */ + void invalidateFilter(); + Q_SIGNALS: void filterStringChanged(); void filterRoleChanged(); void sortRoleChanged(); void sortOrderChanged(); void sortColumnChanged(); void sourceModelChanged(QObject *); void filterRowCallbackChanged(const QJSValue &); void filterColumnCallbackChanged(const QJSValue &); protected: int roleNameToId(const QString &name) const; bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override; bool filterAcceptsColumn(int source_column, const QModelIndex &source_parent) const override; protected Q_SLOTS: void syncRoleNames(); private: bool m_componentCompleted = false; QString m_filterRole; QString m_filterString; QString m_sortRole; QJSValue m_filterRowCallback; QJSValue m_filterColumnCallback; QHash m_roleIds; }; #endif