commit 23d37f3e68c46345fb7b58c67ef9c56a2474e4f9 Author: David Edmundson Date: Thu Dec 12 23:53:47 2019 +0100 Move Plasma's SortFilterProxyModel into KItemModel's QML plugin Summary: It exposes QSFPM in a usable way from QML, but also exposes a way to perform JS callbacks as an advanced filter method. This is mostly a 1-1 move from plasma-frameworks, but with the following change. - Removing a broken workaround for trying to handle Plasma's DataModel having dynamic role names. - port to the new connect syntax - removing the plasma namespace I don't know if we want to change the name to match the others having a K prefix? Subscribers: broulik, ahiemstra, mart, kde-frameworks-devel Tags: #frameworks Differential Revision: https://phabricator.kde.org/D25326 diff --git a/autotests/CMakeLists.txt b/autotests/CMakeLists.txt index 719e56c..a9f3f89 100644 --- a/autotests/CMakeLists.txt +++ b/autotests/CMakeLists.txt @@ -32,7 +32,8 @@ ecm_add_tests( if (${Qt5Qml_FOUND}) ecm_add_tests( kconcatenaterows_qml.cpp - LINK_LIBRARIES KF5::ItemModels Qt5::Test Qt5::Qml + ksortfilterproxymodel_qml.cpp + LINK_LIBRARIES KF5::ItemModels Qt5::Test Qt5::Qml Qt5::Gui ) endif() diff --git a/autotests/ksortfilterproxymodel_qml.cpp b/autotests/ksortfilterproxymodel_qml.cpp new file mode 100644 index 0000000..3d4291b --- /dev/null +++ b/autotests/ksortfilterproxymodel_qml.cpp @@ -0,0 +1,186 @@ +/* + 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 testFilterRegExpReset(); +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" + " 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" + " };\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"); +} + + +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 ID - display") << "KSortFilterProxyModel {" + " sourceModel: testModel;" + " sortRole: Qt.DisplayRole" + "}" + << "April"; + QTest::newRow("sort by role name - value") << "KSortFilterProxyModel {" + " sourceModel: testModel;" + " sortRole: \"user\";" + "}" + << "January"; + QTest::newRow("sort by role ID - value") << "KSortFilterProxyModel {" + " sourceModel: testModel;" + " sortRole: Qt.UserRole" + "}" + << "January"; + QTest::newRow("sort by role name - reset") << "KSortFilterProxyModel {" + " sourceModel: testModel;" + " sortRole: \"display\";" + " Component.onCompleted: sortRole = undefined;" + "}" + << "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() +{ + 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::testFilterRegExpReset() +{ + 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" + " Component.onCompleted: filterRegExp = undefined" + "}\n"); + + QCOMPARE(app.rootObjects().count(), 1); + auto filterModel = qobject_cast(app.rootObjects().first()); + QVERIFY(filterModel); + QCOMPARE(filterModel->rowCount(), 12); +} + + +QTEST_GUILESS_MAIN(tst_KSortFilterProxyModelQml) + +#include "ksortfilterproxymodel_qml.moc" diff --git a/autotests/ksortfilterproxymodeltest_callback.qml b/autotests/ksortfilterproxymodeltest_callback.qml new file mode 100644 index 0000000..9f9f6a4 --- /dev/null +++ b/autotests/ksortfilterproxymodeltest_callback.qml @@ -0,0 +1,16 @@ +import QtQuick 2.0 +import org.kde.kitemmodels 1.0 + +// only include odd_numbers + +KSortFilterProxyModel +{ + sourceModel: KNumberModel { + minimumValue: 1 + maximumValue: 10 + } + filterRowCallback: function(source_row, source_parent) { + return sourceModel.data(sourceModel.index(source_row, 0, source_parent), Qt.DisplayRole) % 2 == 1; + }; +} + diff --git a/autotests/ksortfilterproxymodeltest_sortRoleDisplay.qml b/autotests/ksortfilterproxymodeltest_sortRoleDisplay.qml new file mode 100644 index 0000000..22b4fb2 --- /dev/null +++ b/autotests/ksortfilterproxymodeltest_sortRoleDisplay.qml @@ -0,0 +1,9 @@ +import QtQuick 2.0 +import org.kde.kitemmodels 1.0 + +KSortFilterProxyModel +{ + sourceModel: testModel + sortRole: Qt.DisplayRole +} + diff --git a/autotests/ksortfilterproxymodeltest_sortRoleNameDisplay.qml b/autotests/ksortfilterproxymodeltest_sortRoleNameDisplay.qml new file mode 100644 index 0000000..ad9ace4 --- /dev/null +++ b/autotests/ksortfilterproxymodeltest_sortRoleNameDisplay.qml @@ -0,0 +1,9 @@ +import QtQuick 2.0 +import org.kde.kitemmodels 1.0 + +KSortFilterProxyModel +{ + sourceModel: testModel + sortRole: "text" +} + diff --git a/autotests/ksortfilterproxymodeltest_sortRoleNameValue.qml b/autotests/ksortfilterproxymodeltest_sortRoleNameValue.qml new file mode 100644 index 0000000..c886e15 --- /dev/null +++ b/autotests/ksortfilterproxymodeltest_sortRoleNameValue.qml @@ -0,0 +1,9 @@ +import QtQuick 2.0 +import org.kde.kitemmodels 1.0 + +KSortFilterProxyModel +{ + sourceModel: testModel + sortRole: "value" +} + diff --git a/autotests/ksortfilterproxymodeltest_sortRoleValue.qml b/autotests/ksortfilterproxymodeltest_sortRoleValue.qml new file mode 100644 index 0000000..9c7a381 --- /dev/null +++ b/autotests/ksortfilterproxymodeltest_sortRoleValue.qml @@ -0,0 +1,9 @@ +import QtQuick 2.0 +import org.kde.kitemmodels 1.0 + +KSortFilterProxyModel +{ + sourceModel: testModel + sortRole: Qt.UserRole +} + diff --git a/src/qml/CMakeLists.txt b/src/qml/CMakeLists.txt index 5597bf2..da37ab7 100644 --- a/src/qml/CMakeLists.txt +++ b/src/qml/CMakeLists.txt @@ -1,8 +1,11 @@ set(corebindings_SRCS plugin.cpp kconcatenaterowsproxymodel_qml.cpp + ksortfilterproxymodel.cpp ) +ecm_qt_declare_logging_category(corebindings_SRCS HEADER kitemmodels_debug.h IDENTIFIER KITEMMODELS_LOG CATEGORY_NAME kf5.kitemmodels) + add_library(itemmodelsplugin SHARED ${corebindings_SRCS}) target_link_libraries(itemmodelsplugin Qt5::Qml diff --git a/src/qml/ksortfilterproxymodel.cpp b/src/qml/ksortfilterproxymodel.cpp new file mode 100644 index 0000000..918cd5a --- /dev/null +++ b/src/qml/ksortfilterproxymodel.cpp @@ -0,0 +1,206 @@ +/* + * 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); + syncRoleNames(); + setFilterRole(m_filterRole); + setSortRole(m_sortRole); +} + +bool KSortFilterProxyModel::filterAcceptsRow(int source_row, const QModelIndex& source_parent) const +{ + if (m_filterCallback.isCallable()) { + auto engine = qjsEngine(this); + QJSValueList args = {QJSValue(source_row), engine->toScriptValue(source_parent)}; + return const_cast(this)->m_filterCallback.call(args).toBool(); + } + + return QSortFilterProxyModel::filterAcceptsRow(source_row, source_parent); +} + +bool KSortFilterProxyModel::filterAcceptsColumn(int source_column, const QModelIndex &source_parent) const +{ + if (m_filterColumnCallback.isCallable()) { + auto engine = qjsEngine(this); + QJSValueList args = {QJSValue(source_column), engine->toScriptValue(source_parent)}; + return const_cast(this)->m_filterCallback.call(args).toBool(); + } + + return QSortFilterProxyModel::filterAcceptsColumn(source_column, source_parent); +} + +void KSortFilterProxyModel::setFilterRegularExpression(const QRegularExpression &exp) +{ + if (exp == filterRegularExpression()) { + return; + } + QSortFilterProxyModel::setFilterRegularExpression(exp); + Q_EMIT filterRegularExpressionChanged(); +} + +QRegularExpression KSortFilterProxyModel::filterRegularExpression() const +{ + return QSortFilterProxyModel::filterRegularExpression(); +} + +void KSortFilterProxyModel::setFilterString(const QString &filterString) +{ + if (filterString == m_filterString) { + return; + } + m_filterString = filterString; + QSortFilterProxyModel::setFilterFixedString(filterString); + Q_EMIT filterStringChanged(filterString); +} + +QString KSortFilterProxyModel::filterString() const +{ + return m_filterString; +} + +QJSValue KSortFilterProxyModel::filterRowCallback() const +{ + return m_filterCallback; +} + +void KSortFilterProxyModel::setFilterRowCallback(const QJSValue& callback) +{ + if (m_filterCallback.strictlyEquals(callback)) { + return; + } + + if (!callback.isNull() && !callback.isCallable()) { + return; + } + + m_filterCallback = 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; +} + +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()); + } +} + +QString KSortFilterProxyModel::sortRole() const +{ + return m_sortRole; +} + +void KSortFilterProxyModel::resetSortRole() +{ + setSortRole(QString()); +} + +void KSortFilterProxyModel::setSortOrder(const Qt::SortOrder order) +{ + sort(std::max(sortColumn(), 0), order); +} + +void KSortFilterProxyModel::setSortColumn(int column) +{ + if (column == sortColumn()) { + return; + } + sort(column, sortOrder()); + emit sortColumnChanged(); +} diff --git a/src/qml/ksortfilterproxymodel.h b/src/qml/ksortfilterproxymodel.h new file mode 100644 index 0000000..d8eb169 --- /dev/null +++ b/src/qml/ksortfilterproxymodel.h @@ -0,0 +1,151 @@ +/* + * 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 + +/** + * @class SortFilterModel + * @short Filter and sort an existing QAbstractItemModel + * @since 5.66 + */ +class KSortFilterProxyModel : public QSortFilterProxyModel +{ + Q_OBJECT + /** + * The source model of this sorting proxy model. + */ + Q_PROPERTY(QAbstractItemModel *sourceModel READ sourceModel WRITE setModel NOTIFY sourceModelChanged) + + /** + * The regular expression for the filter, only items with their filterRole matching filterRegExp will be displayed + */ + Q_PROPERTY(QRegularExpression filterRegularExpression READ filterRegularExpression WRITE setFilterRegularExpression NOTIFY filterRegularExpressionChanged) + + /** + * The string for the filter, only items 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 filterRegExp will be applied. + */ + Q_PROPERTY(QString filterRole READ filterRole WRITE setFilterRole) + + /** + * 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 RESET resetSortRole) + + /** + * One of Qt.Ascending or Qt.Descending + */ + Q_PROPERTY(Qt::SortOrder sortOrder READ sortOrder WRITE setSortOrder) + + /** + * Specify which column shoud be used for sorting + */ + Q_PROPERTY(int sortColumn READ sortColumn WRITE setSortColumn NOTIFY sortColumnChanged) + +public: + explicit KSortFilterProxyModel(QObject *parent = nullptr); + ~KSortFilterProxyModel(); + + void setModel(QAbstractItemModel *source); + + void setFilterRegularExpression(const QRegularExpression &exp); + QRegularExpression filterRegularExpression() const; + + void setFilterString(const QString &filterString); + QString filterString() const; + + void setFilterRowCallback(const QJSValue &callback); + QJSValue filterRowCallback() 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 resetSortRole(); + + void setSortOrder(const Qt::SortOrder order); + void setSortColumn(int column); + +Q_SIGNALS: + void sortColumnChanged(); + void sourceModelChanged(QObject *); + void filterRegularExpressionChanged(); + void filterStringChanged(const QString &); + 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: + QString m_filterRole; + QString m_sortRole; + QString m_filterString; + QJSValue m_filterCallback; + QJSValue m_filterColumnCallback; + QHash m_roleIds; +}; +#endif diff --git a/src/qml/plugin.cpp b/src/qml/plugin.cpp index 4448cef..8949302 100644 --- a/src/qml/plugin.cpp +++ b/src/qml/plugin.cpp @@ -25,6 +25,7 @@ #include #include #include +#include "ksortfilterproxymodel.h" #include "kconcatenaterowsproxymodel_qml.h" void Plugin::initializeEngine(QQmlEngine *engine, const char *uri) @@ -44,4 +45,5 @@ void Plugin::registerTypes(const char *uri) qmlRegisterType(uri, 1, 0, "KDescendantsProxyModel"); qmlRegisterType(uri, 1, 0, "KNumberModel"); qmlRegisterType(uri, 1, 0, "KColumnHeadersModel"); + qmlRegisterType(uri, 1, 0, "KSortFilterProxyModel"); }