diff --git a/autotests/CMakeLists.txt b/autotests/CMakeLists.txt --- a/autotests/CMakeLists.txt +++ b/autotests/CMakeLists.txt @@ -32,7 +32,8 @@ 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 --- /dev/null +++ b/autotests/ksortfilterproxymodel_qml.cpp @@ -0,0 +1,181 @@ +/* + 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" + " 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 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/CMakeLists.txt b/src/qml/CMakeLists.txt --- 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.h b/src/qml/ksortfilterproxymodel.h new file mode 100644 --- /dev/null +++ b/src/qml/ksortfilterproxymodel.h @@ -0,0 +1,153 @@ +/* + * 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; + +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 diff --git a/src/qml/ksortfilterproxymodel.cpp b/src/qml/ksortfilterproxymodel.cpp new file mode 100644 --- /dev/null +++ b/src/qml/ksortfilterproxymodel.cpp @@ -0,0 +1,228 @@ +/* + * 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) +{ + if (role == m_filterRole) { + return; + } + QSortFilterProxyModel::setFilterRole(roleNameToId(role)); + m_filterRole = role; + Q_EMIT filterRoleChanged(); +} + +QString KSortFilterProxyModel::filterRole() const +{ + return m_filterRole; +} + +void KSortFilterProxyModel::setSortRole(const QString &role) +{ + if (role == m_sortRole) { + return; + } + 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); + } +} diff --git a/src/qml/plugin.cpp b/src/qml/plugin.cpp --- 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 @@ qmlRegisterType(uri, 1, 0, "KDescendantsProxyModel"); qmlRegisterType(uri, 1, 0, "KNumberModel"); qmlRegisterType(uri, 1, 0, "KColumnHeadersModel"); + qmlRegisterType(uri, 1, 0, "KSortFilterProxyModel"); }