aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJan Arve Sæther <[email protected]>2025-05-29 12:13:57 +0200
committerSanthosh Kumar <[email protected]>2025-05-30 08:17:04 +0200
commit9efc1fb4ac7982f105a13781fccff74a61907601 (patch)
treed5ed4e7f82f15c978a6adfa8052bc5cab6758d3a
parent5b17c03634dcf8f9da6e6570f579f5c2fa280830 (diff)
Provide a way to sieve data in QML through the SortFilterProxyModelHEADdev
Enhance QSortFilterProxyModel to be available in QML with changes as mentioned below and export the type as SortFilterProxyModel (Note: adopted most of these features from existing project - https://github.com/oKcerG/SortFilterProxyModel) * Inherit QQmlSortFilterProxyModelPrivate from QSortFilterProxyModelHelper and use the mapping logic of source to proxy indexes from it. * Allow the model to be configurable with multiple filters and sorters. The filter and sorter components shall be inherited from QQmlFilterBase and QQmlSorterBase respectively. The components are maintained within the respective compositor classes. The filter and sorting operation from QQmlSortFilterProxyModel will be forwarded to the compositor which then iterate through the configured components to sieve the data. This patch allows the following filters and sorters configurable in SFPM, Filters: ValueFilter - Filters the data that matching with the provided value or role name or combined together if both are specified. FunctionFilter - Filters the data according to the result of the evaluated js method. Sorters: RoleSorter - Sorts the data according to the provided role name. StringSorter - Sorts the data by considering the locale. FunctionSorter - Sorts the data according to the evaluated js method. * Add support for 'enabled', 'column' property for both filters and sorters, and 'priority' property for the sorters. Task-number: QTBUG-71348 Change-Id: I65b84936642e5f0f382d83413648d2c6794c18aa Reviewed-by: Jan Arve Sæther <[email protected]>
-rw-r--r--src/qmlmodels/CMakeLists.txt18
-rw-r--r--src/qmlmodels/configure.cmake6
-rw-r--r--src/qmlmodels/doc/snippets/qml/sortfilterproxymodel/qml-sortfilterproxymodel.qml57
-rw-r--r--src/qmlmodels/sfpm/filters/qqmlfilterbase.cpp112
-rw-r--r--src/qmlmodels/sfpm/filters/qqmlfilterbase_p.h80
-rw-r--r--src/qmlmodels/sfpm/filters/qqmlfiltercompositor.cpp171
-rw-r--r--src/qmlmodels/sfpm/filters/qqmlfiltercompositor_p.h76
-rw-r--r--src/qmlmodels/sfpm/filters/qqmlfunctionfilter.cpp172
-rw-r--r--src/qmlmodels/sfpm/filters/qqmlfunctionfilter_p.h58
-rw-r--r--src/qmlmodels/sfpm/filters/qqmlrolefilter.cpp66
-rw-r--r--src/qmlmodels/sfpm/filters/qqmlrolefilter_p.h59
-rw-r--r--src/qmlmodels/sfpm/filters/qqmlvaluefilter.cpp108
-rw-r--r--src/qmlmodels/sfpm/filters/qqmlvaluefilter_p.h58
-rw-r--r--src/qmlmodels/sfpm/qqmlsortfilterproxymodel.cpp1720
-rw-r--r--src/qmlmodels/sfpm/qqmlsortfilterproxymodel_p.h141
-rw-r--r--src/qmlmodels/sfpm/qsortfilterproxymodelhelper.cpp760
-rw-r--r--src/qmlmodels/sfpm/qsortfilterproxymodelhelper_p.h233
-rw-r--r--src/qmlmodels/sfpm/sorters/qqmlfunctionsorter.cpp174
-rw-r--r--src/qmlmodels/sfpm/sorters/qqmlfunctionsorter_p.h57
-rw-r--r--src/qmlmodels/sfpm/sorters/qqmlrolesorter.cpp81
-rw-r--r--src/qmlmodels/sfpm/sorters/qqmlrolesorter_p.h58
-rw-r--r--src/qmlmodels/sfpm/sorters/qqmlsorterbase.cpp136
-rw-r--r--src/qmlmodels/sfpm/sorters/qqmlsorterbase_p.h89
-rw-r--r--src/qmlmodels/sfpm/sorters/qqmlsortercompositor.cpp225
-rw-r--r--src/qmlmodels/sfpm/sorters/qqmlsortercompositor_p.h82
-rw-r--r--src/qmlmodels/sfpm/sorters/qqmlstringsorter.cpp150
-rw-r--r--src/qmlmodels/sfpm/sorters/qqmlstringsorter_p.h73
-rw-r--r--tests/auto/qml/CMakeLists.txt1
-rw-r--r--tests/auto/qml/qqmlsortfilterproxymodel/CMakeLists.txt46
-rw-r--r--tests/auto/qml/qqmlsortfilterproxymodel/data/Utility.js18
-rw-r--r--tests/auto/qml/qqmlsortfilterproxymodel/data/sfpmCommon.qml41
-rw-r--r--tests/auto/qml/qqmlsortfilterproxymodel/tst_qqmlsortfilterproxymodel.cpp1050
32 files changed, 6176 insertions, 0 deletions
diff --git a/src/qmlmodels/CMakeLists.txt b/src/qmlmodels/CMakeLists.txt
index dd3c3dec0e..29ab77c793 100644
--- a/src/qmlmodels/CMakeLists.txt
+++ b/src/qmlmodels/CMakeLists.txt
@@ -73,6 +73,24 @@ qt_internal_extend_target(QmlModels CONDITION QT_FEATURE_qml_delegate_model
qquickpackage.cpp qquickpackage_p.h
)
+qt_internal_extend_target(QmlModels CONDITION QT_FEATURE_qml_sfpm_model AND QT_FEATURE_proxymodel
+ SOURCES
+ sfpm/qqmlsortfilterproxymodel_p.h sfpm/qqmlsortfilterproxymodel.cpp
+ sfpm/qsortfilterproxymodelhelper_p.h sfpm/qsortfilterproxymodelhelper.cpp
+ # sfpm filter specific source files
+ sfpm/filters/qqmlfilterbase_p.h sfpm/filters/qqmlfilterbase.cpp
+ sfpm/filters/qqmlfiltercompositor_p.h sfpm/filters/qqmlfiltercompositor.cpp
+ sfpm/filters/qqmlrolefilter_p.h sfpm/filters/qqmlrolefilter.cpp
+ sfpm/filters/qqmlvaluefilter_p.h sfpm/filters/qqmlvaluefilter.cpp
+ sfpm/filters/qqmlfunctionfilter_p.h sfpm/filters/qqmlfunctionfilter.cpp
+ # sfpm sorter specific source files
+ sfpm/sorters/qqmlsorterbase_p.h sfpm/sorters/qqmlsorterbase.cpp
+ sfpm/sorters/qqmlsortercompositor_p.h sfpm/sorters/qqmlsortercompositor.cpp
+ sfpm/sorters/qqmlstringsorter_p.h sfpm/sorters/qqmlstringsorter.cpp
+ sfpm/sorters/qqmlrolesorter_p.h sfpm/sorters/qqmlrolesorter.cpp
+ sfpm/sorters/qqmlfunctionsorter_p.h sfpm/sorters/qqmlfunctionsorter.cpp
+)
+
qt_internal_add_docs(QmlModels
doc/qtqmlmodels.qdocconf
)
diff --git a/src/qmlmodels/configure.cmake b/src/qmlmodels/configure.cmake
index 6b27201b3d..876dd571ed 100644
--- a/src/qmlmodels/configure.cmake
+++ b/src/qmlmodels/configure.cmake
@@ -46,7 +46,13 @@ qt_feature("qml-tree-model" PRIVATE
PURPOSE "Provides the TreeModel QML type."
CONDITION QT_FEATURE_qml_itemmodel AND QT_FEATURE_qml_delegate_model
)
+qt_feature("qml-sfpm-model" PRIVATE
+ SECTION "QML"
+ LABEL "QML sortfilterproxy model"
+ PURPOSE "Provides the SortFilterProxyModel QML type."
+)
qt_configure_add_summary_section(NAME "Qt QML Models")
qt_configure_add_summary_entry(ARGS "qml-list-model")
qt_configure_add_summary_entry(ARGS "qml-delegate-model")
+qt_configure_add_summary_entry(ARGS "qml-sfpm-model")
qt_configure_end_summary_section() # end of "Qt QML Models" section
diff --git a/src/qmlmodels/doc/snippets/qml/sortfilterproxymodel/qml-sortfilterproxymodel.qml b/src/qmlmodels/doc/snippets/qml/sortfilterproxymodel/qml-sortfilterproxymodel.qml
new file mode 100644
index 0000000000..9f7c2e508f
--- /dev/null
+++ b/src/qmlmodels/doc/snippets/qml/sortfilterproxymodel/qml-sortfilterproxymodel.qml
@@ -0,0 +1,57 @@
+// Copyright (C) 2025 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+//![0]
+import QtQuick
+import QtQuick.Controls
+
+ApplicationWindow {
+ width: 640
+ height: 480
+ visible: true
+ title: qsTr("Sort Filter Proxy Model")
+
+ //! [sfpm-usage]
+ ListModel {
+ id: listModel
+ ListElement { name: "Adan"; age: 25; department: "Process"; pid: 209711; country: "Norway" }
+ ListElement { name: "Hannah"; age: 48; department: "HR"; pid: 154916; country: "Germany" }
+ ListElement { name: "Divina"; age: 63; department: "Marketing"; pid: 158038; country: "Spain" }
+ ListElement { name: "Rohith"; age: 35; department: "Process"; pid: 202582; country: "India" }
+ ListElement { name: "Latesha"; age: 23; department: "Quality"; pid: 232582; country: "UK" }
+ }
+
+ SortFilterProxyModel {
+ id: ageFilterModel
+ model: listModel
+ filters: [
+ FunctionFilter {
+ roleData: QtObject { property int age }
+ function filter(data: QtObject) : bool {
+ return data.age > 30
+ }
+ }
+ ]
+ sorters: [
+ RoleSorter { roleName: "department" }
+ ]
+ }
+
+ ListView {
+ anchors.fill: parent
+ clip: true
+ model: sfpm
+ delegate: Rectangle {
+ implicitWidth: 100
+ implicitHeight: 50
+ border.width: 1
+ Text {
+ text: name
+ anchors.centerIn: parent
+ }
+ }
+ }
+ //! [sfpm-usage]
+}
+
+//![0]
diff --git a/src/qmlmodels/sfpm/filters/qqmlfilterbase.cpp b/src/qmlmodels/sfpm/filters/qqmlfilterbase.cpp
new file mode 100644
index 0000000000..16b2711f53
--- /dev/null
+++ b/src/qmlmodels/sfpm/filters/qqmlfilterbase.cpp
@@ -0,0 +1,112 @@
+// Copyright (C) 2025 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#include <QtQmlModels/private/qqmlsortfilterproxymodel_p.h>
+#include <QtQmlModels/private/qqmlfilterbase_p.h>
+
+QT_BEGIN_NAMESPACE
+
+/*!
+ \qmltype Filter
+ \inherits QtObject
+ \inqmlmodule QtQml.Models
+ \since 6.10
+ \preliminary
+ \brief Abstract base type providing functionality common to filters.
+
+ Filter provides a set of common properties for all the filters that they
+ inherit from.
+*/
+
+QQmlFilterBase::QQmlFilterBase(QQmlFilterBasePrivate *privObj, QObject *parent)
+ : QObject (*privObj, parent)
+{
+}
+
+/*!
+ \qmlproperty bool Filter::enabled
+
+ This property enables the \l SortFilterProxyModel to consider this filter
+ while filtering the model data.
+
+ The default value is \c true.
+*/
+bool QQmlFilterBase::enabled() const
+{
+ Q_D(const QQmlFilterBase);
+ return d->m_enabled;
+}
+
+void QQmlFilterBase::setEnabled(const bool enabled)
+{
+ Q_D(QQmlFilterBase);
+ if (d->m_enabled == enabled)
+ return;
+ d->m_enabled = enabled;
+ invalidate(true);
+ emit enabledChanged();
+}
+
+/*!
+ \qmlproperty bool Filter::invert
+
+ This property inverts the filter, causing the data that would normally be
+ filtered out to be presented instead.
+
+ The default value is \c false.
+*/
+bool QQmlFilterBase::invert() const
+{
+ Q_D(const QQmlFilterBase);
+ return d->m_invert;
+}
+
+void QQmlFilterBase::setInvert(const bool invert)
+{
+ Q_D(QQmlFilterBase);
+ if (d->m_invert == invert)
+ return;
+ d->m_invert = invert;
+ invalidate();
+ emit invertChanged();
+}
+
+/*!
+ \qmlproperty int Filter::column
+
+ This property specifies which column in the model the filter should be
+ applied on. If the value is \c -1, the filter will be applied to all
+ the columns in the model.
+
+ The default value is \c -1.
+*/
+int QQmlFilterBase::column() const
+{
+ Q_D(const QQmlFilterBase);
+ return d->m_filterColumn;
+}
+
+void QQmlFilterBase::setColumn(const int column)
+{
+ Q_D(QQmlFilterBase);
+ if (d->m_filterColumn == column)
+ return;
+ d->m_filterColumn = column;
+ invalidate();
+ emit columnChanged();
+}
+
+/*!
+ \internal
+*/
+void QQmlFilterBase::invalidate(bool updateCache)
+{
+ // Update the cached filters and invalidate the model
+ if (updateCache)
+ emit invalidateCache(this);
+ emit invalidateModel();
+}
+
+QT_END_NAMESPACE
+
+#include "moc_qqmlfilterbase_p.cpp"
diff --git a/src/qmlmodels/sfpm/filters/qqmlfilterbase_p.h b/src/qmlmodels/sfpm/filters/qqmlfilterbase_p.h
new file mode 100644
index 0000000000..4baed7e569
--- /dev/null
+++ b/src/qmlmodels/sfpm/filters/qqmlfilterbase_p.h
@@ -0,0 +1,80 @@
+// Copyright (C) 2025 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#ifndef QQMLFILTERBASE_H
+#define QQMLFILTERBASE_H
+
+//
+// W A R N I N G
+// -------------
+//
+// This file is not part of the Qt API. It exists purely as an
+// implementation detail. This header file may change from version to
+// version without notice, or even be removed.
+//
+// We mean it.
+//
+
+#include <QtCore/QObject>
+#include <QtCore/QAbstractItemModel>
+#include <QtQml/private/qqmlcustomparser_p.h>
+#include <QtQmlModels/private/qtqmlmodelsglobal_p.h>
+
+QT_BEGIN_NAMESPACE
+
+class QQmlSortFilterProxyModel;
+class QQmlFilterBasePrivate;
+
+class Q_QMLMODELS_EXPORT QQmlFilterBase: public QObject
+{
+ Q_OBJECT
+ Q_PROPERTY(bool enabled READ enabled WRITE setEnabled NOTIFY enabledChanged FINAL)
+ Q_PROPERTY(bool invert READ invert WRITE setInvert NOTIFY invertChanged FINAL)
+ Q_PROPERTY(int column READ column WRITE setColumn NOTIFY columnChanged FINAL)
+ QML_ELEMENT
+ QML_UNCREATABLE("")
+
+public:
+ explicit QQmlFilterBase(QQmlFilterBasePrivate *privObj, QObject *parent = nullptr);
+ virtual ~QQmlFilterBase() = default;
+
+ bool enabled() const;
+ void setEnabled(const bool bEnable);
+
+ bool invert() const;
+ void setInvert(const bool bInvert);
+
+ int column() const;
+ void setColumn(const int column);
+
+ virtual bool filterAcceptsRowInternal(int, const QModelIndex&, const QQmlSortFilterProxyModel *) const { return true; }
+ virtual bool filterAcceptsColumnInternal(int, const QModelIndex&, const QQmlSortFilterProxyModel *) const { return true; }
+ virtual void update(const QQmlSortFilterProxyModel *) { /* do nothing */ };
+
+Q_SIGNALS:
+ void invalidateModel();
+ void invalidateCache(QQmlFilterBase *filter);
+ void enabledChanged();
+ void invertChanged();
+ void columnChanged();
+
+public slots:
+ void invalidate(bool updateCache = false);
+
+private:
+ Q_DECLARE_PRIVATE(QQmlFilterBase)
+};
+
+class QQmlFilterBasePrivate: public QObjectPrivate
+{
+ Q_DECLARE_PUBLIC(QQmlFilterBase)
+
+private:
+ bool m_enabled = true;
+ bool m_invert = false;
+ int m_filterColumn = -1;
+};
+
+QT_END_NAMESPACE
+
+#endif // QQMLFILTERBASE_H
diff --git a/src/qmlmodels/sfpm/filters/qqmlfiltercompositor.cpp b/src/qmlmodels/sfpm/filters/qqmlfiltercompositor.cpp
new file mode 100644
index 0000000000..0a99fb5ec0
--- /dev/null
+++ b/src/qmlmodels/sfpm/filters/qqmlfiltercompositor.cpp
@@ -0,0 +1,171 @@
+// Copyright (C) 2025 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#include <QtQmlModels/private/qqmlfiltercompositor_p.h>
+#include <QtQmlModels/private/qqmlsortfilterproxymodel_p.h>
+
+QT_BEGIN_NAMESPACE
+
+Q_LOGGING_CATEGORY (lcSfpmFilterCompositor, "qt.qml.sfpmfiltercompositor")
+
+QQmlFilterCompositor::QQmlFilterCompositor(QObject *parent)
+ : QQmlFilterBase(new QQmlFilterCompositorPrivate, parent)
+{
+ Q_D(QQmlFilterCompositor);
+ d->init();
+ // Connect the model reset with the update in the filter
+ // The cache need to be updated once the model is reset with the
+ // source model data. This is because, there are chances that
+ // the filter can be enabled or disabled in effective filter list
+ // such as the configured role name in the filter doesn't match
+ // with any role name in the model
+ connect(d->m_sfpmModel, &QQmlSortFilterProxyModel::modelReset,
+ this, &QQmlFilterCompositor::updateFilters);
+}
+
+QQmlFilterCompositor::~QQmlFilterCompositor()
+{
+
+}
+
+void QQmlFilterCompositorPrivate::init()
+{
+ Q_Q(QQmlFilterCompositor);
+ m_sfpmModel = qobject_cast<QQmlSortFilterProxyModel *>(q->parent());
+}
+
+void QQmlFilterCompositor::append(QQmlListProperty<QQmlFilterBase> *filterComp, QQmlFilterBase* filter)
+{
+ auto *filterCompositor = reinterpret_cast<QQmlFilterCompositor *> (filterComp->object);
+ filterCompositor->append(filter);
+}
+
+qsizetype QQmlFilterCompositor::count(QQmlListProperty<QQmlFilterBase> *filterComp)
+{
+ auto *filterCompositor = reinterpret_cast<QQmlFilterCompositor *> (filterComp->object);
+ return filterCompositor->count();
+}
+
+QQmlFilterBase *QQmlFilterCompositor::at(QQmlListProperty<QQmlFilterBase> *filterComp, qsizetype index)
+{
+ auto *filterCompositor = reinterpret_cast<QQmlFilterCompositor *> (filterComp->object);
+ return filterCompositor->at(index);
+}
+
+void QQmlFilterCompositor::clear(QQmlListProperty<QQmlFilterBase> *filterComp)
+{
+ auto *filterCompositor = reinterpret_cast<QQmlFilterCompositor *> (filterComp->object);
+ filterCompositor->clear();
+}
+
+void QQmlFilterCompositor::append(QQmlFilterBase *filter)
+{
+ if (!filter)
+ return;
+
+ Q_D(QQmlFilterCompositor);
+ d->m_filters.append(filter);
+ // Connect the filter to the corresponding slot to invalidate the model
+ // and the filter cache
+ QObject::connect(filter, &QQmlFilterBase::invalidateModel,
+ d->m_sfpmModel, &QQmlSortFilterProxyModel::invalidate);
+ // This is needed as its required to update cache when there is any
+ // change in the filter itself (for instance, a change in the priority of
+ // the filter)
+ QObject::connect(filter, &QQmlFilterBase::invalidateCache,
+ this, &QQmlFilterCompositor::updateCache);
+ // Validate the filter for any precondition which can be compared with
+ // sfpm and update the filter cache accordingly
+ filter->update(d->m_sfpmModel);
+ updateCache();
+ // Since we added new filter to the list, emit the filter changed signal
+ // for the filters that have been appended to the list
+ emit d->m_sfpmModel->filtersChanged();
+}
+
+qsizetype QQmlFilterCompositor::count()
+{
+ Q_D(QQmlFilterCompositor);
+ return d->m_filters.count();
+}
+
+QQmlFilterBase *QQmlFilterCompositor::at(qsizetype index)
+{
+ Q_D(QQmlFilterCompositor);
+ return d->m_filters.at(index);
+}
+
+void QQmlFilterCompositor::clear()
+{
+ Q_D(QQmlFilterCompositor);
+ d->m_effectiveFilters.clear();
+ d->m_filters.clear();
+ // Emit the filter changed signal as we cleared the filter list
+ emit d->m_sfpmModel->filtersChanged();
+}
+
+QList<QQmlFilterBase *> QQmlFilterCompositor::filters()
+{
+ Q_D(QQmlFilterCompositor);
+ return d->m_filters;
+}
+
+QQmlListProperty<QQmlFilterBase> QQmlFilterCompositor::filtersListProperty()
+{
+ Q_D(QQmlFilterCompositor);
+ return QQmlListProperty<QQmlFilterBase>(reinterpret_cast<QObject*>(this), &d->m_filters,
+ QQmlFilterCompositor::append,
+ QQmlFilterCompositor::count,
+ QQmlFilterCompositor::at,
+ QQmlFilterCompositor::clear);
+}
+
+void QQmlFilterCompositor::updateFilters()
+{
+ Q_D(QQmlFilterCompositor);
+ // Update filters that have dependencies with the model data
+ for (auto &filter: d->m_filters)
+ filter->update(d->m_sfpmModel);
+ // Update the cache
+ updateCache();
+}
+
+void QQmlFilterCompositor::updateCache()
+{
+ Q_D(QQmlFilterCompositor);
+ // Clear the existing cache
+ d->m_effectiveFilters.clear();
+ if (d->m_sfpmModel && d->m_sfpmModel->sourceModel()) {
+ QList<QQmlFilterBase *> filters = d->m_filters;
+ // Cache only the filters that need to be evaluated (in order)
+ std::copy_if(filters.begin(), filters.end(), std::back_inserter(d->m_effectiveFilters),
+ [](QQmlFilterBase *filter){ return filter->enabled(); });
+ }
+}
+
+bool QQmlFilterCompositor::filterAcceptsRowInternal(int row, const QModelIndex& sourceParent, const QQmlSortFilterProxyModel *proxyModel) const
+{
+ Q_D(const QQmlFilterCompositor);
+ // Check the data against the configured filters and if nothing configured,
+ // dont filter the data
+ return std::all_of(d->m_effectiveFilters.begin(), d->m_effectiveFilters.end(),
+ [row, &sourceParent, proxyModel](const QQmlFilterBase *filter) {
+ const bool filterStatus = filter->filterAcceptsRowInternal(row, sourceParent, proxyModel);
+ return !(filter->invert()) ? filterStatus : !filterStatus;
+ });
+}
+
+bool QQmlFilterCompositor::filterAcceptsColumnInternal(int column, const QModelIndex& sourceParent, const QQmlSortFilterProxyModel *proxyModel) const
+{
+ Q_D(const QQmlFilterCompositor);
+ // Check the data against the configured filters and if nothing configured,
+ // dont filter the data
+ return std::all_of(d->m_effectiveFilters.begin(), d->m_effectiveFilters.end(),
+ [column, &sourceParent, proxyModel](const QQmlFilterBase *filter) {
+ return filter->filterAcceptsColumnInternal(column, sourceParent, proxyModel);
+ });
+}
+
+QT_END_NAMESPACE
+
+#include "moc_qqmlfiltercompositor_p.cpp"
diff --git a/src/qmlmodels/sfpm/filters/qqmlfiltercompositor_p.h b/src/qmlmodels/sfpm/filters/qqmlfiltercompositor_p.h
new file mode 100644
index 0000000000..b07fdc99b9
--- /dev/null
+++ b/src/qmlmodels/sfpm/filters/qqmlfiltercompositor_p.h
@@ -0,0 +1,76 @@
+// Copyright (C) 2025 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#ifndef QQMLFILTERCOMPOSITOR_P_H
+#define QQMLFILTERCOMPOSITOR_P_H
+
+//
+// W A R N I N G
+// -------------
+//
+// This file is not part of the Qt API. It exists purely as an
+// implementation detail. This header file may change from version to
+// version without notice, or even be removed.
+//
+// We mean it.
+//
+
+#include <QtQmlModels/private/qqmlfilterbase_p.h>
+
+QT_BEGIN_NAMESPACE
+
+class QQmlSortFilterProxyModel;
+class QQmlFilterCompositorPrivate;
+
+class QQmlFilterCompositor: public QQmlFilterBase
+{
+ Q_OBJECT
+ QML_ELEMENT
+ QML_UNCREATABLE("")
+
+public:
+ explicit QQmlFilterCompositor(QObject *parent = nullptr);
+ ~QQmlFilterCompositor() override;
+
+ QList<QQmlFilterBase *> filters();
+ QQmlListProperty<QQmlFilterBase> filtersListProperty();
+
+ bool filterAcceptsRowInternal(int, const QModelIndex&, const QQmlSortFilterProxyModel *) const override;
+ bool filterAcceptsColumnInternal(int, const QModelIndex&, const QQmlSortFilterProxyModel *) const override;
+ void updateFilters();
+
+ static void append(QQmlListProperty<QQmlFilterBase> *filterComp, QQmlFilterBase *filter);
+ static qsizetype count(QQmlListProperty<QQmlFilterBase> *filterComp);
+ static QQmlFilterBase* at(QQmlListProperty<QQmlFilterBase> *filterComp, qsizetype index);
+ static void clear(QQmlListProperty<QQmlFilterBase> *filterComp);
+
+private:
+ void append(QQmlFilterBase *);
+ qsizetype count();
+ QQmlFilterBase* at(qsizetype);
+ void clear();
+
+public slots:
+ void updateCache();
+
+private:
+ Q_DECLARE_PRIVATE(QQmlFilterCompositor)
+};
+
+class QQmlFilterCompositorPrivate: public QQmlFilterBasePrivate
+{
+ Q_DECLARE_PUBLIC(QQmlFilterCompositor)
+
+public:
+ void init();
+ // Holds filters in the same order as declared in the qml
+ QList<QQmlFilterBase *> m_filters;
+ // Holds effective filters that will be evaluated with the
+ // model content
+ QList<QQmlFilterBase *> m_effectiveFilters;
+ QQmlSortFilterProxyModel *m_sfpmModel = nullptr;
+};
+
+QT_END_NAMESPACE
+
+#endif // QQMLFILTERCOMPOSITOR_P_H
diff --git a/src/qmlmodels/sfpm/filters/qqmlfunctionfilter.cpp b/src/qmlmodels/sfpm/filters/qqmlfunctionfilter.cpp
new file mode 100644
index 0000000000..4dc09fbbf9
--- /dev/null
+++ b/src/qmlmodels/sfpm/filters/qqmlfunctionfilter.cpp
@@ -0,0 +1,172 @@
+// Copyright (C) 2025 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#include <QtQmlModels/private/qqmlfunctionfilter_p.h>
+#include <QtQmlModels/private/qqmlsortfilterproxymodel_p.h>
+#include <QtQml/private/qqmlobjectcreator_p.h>
+#include <QObject>
+#include <QMetaMethod>
+
+QT_BEGIN_NAMESPACE
+
+/*!
+ \qmltype FunctionFilter
+ \inherits Filter
+ \inqmlmodule QtQml.Models
+ \since 6.10
+ \preliminary
+ \brief Filters data in a \l SortFilterProxyModel based on the evaluation
+ of the designated 'filter' method.
+
+ FunctionFilter allows user to define the designated 'filter' method and it
+ will be evaluated to filter the data. The 'filter' method takes one
+ argument and it can be defined as inline component as below:
+
+ \qml
+ SortFilterProxyModel {
+ model: sourceModel
+ filters: [
+ FunctionFilter {
+ id: functionFilter
+ property int ageLimit: 20
+ component RoleData: QtObject {
+ property real age
+ }
+ function filter(data: RoleData) : bool {
+ return (data.age <= ageLimit)
+ }
+ }
+ ]
+ }
+ \endqml
+
+ \note The user needs to explicitly invoke
+ \l{SortFilterProxyModel::invalidate} whenever any external qml property
+ used within the designated 'filter' method changes. This behaviour is
+ subject to change in the future, like implicit invalidation and thus the
+ user doesn't need to explicitly invoke
+ \l{SortFilterProxyModel::invalidate}.
+*/
+
+QQmlFunctionFilter::QQmlFunctionFilter(QObject *parent)
+ : QQmlFilterBase (new QQmlFunctionFilterPrivate, parent)
+{
+}
+
+QQmlFunctionFilter::~QQmlFunctionFilter()
+{
+ Q_D(QQmlFunctionFilter);
+ if (d->m_parameterData.metaType().flags() & QMetaType::PointerToQObject)
+ delete d->m_parameterData.value<QObject *>();
+}
+
+void QQmlFunctionFilter::componentComplete()
+{
+ Q_D(QQmlFunctionFilter);
+ const auto *metaObj = metaObject();
+ for (int idx = metaObj->methodOffset(); idx < metaObj->methodCount(); idx++) {
+ // Once we find the method signature, break the loop
+ QMetaMethod method = metaObj->method(idx);
+ if (method.nameView() == "filter") {
+ d->m_method = method;
+ break;
+ }
+ }
+
+ if (!d->m_method.isValid())
+ return;
+
+ if (d->m_method.parameterCount() != 1) {
+ qWarning("filter method requires a single parameter");
+ return;
+ }
+
+ QQmlData *data = QQmlData::get(this);
+ if (!data || !data->outerContext) {
+ qWarning("filter requires a QML context");
+ return;
+ }
+
+ QQmlRefPointer<QQmlContextData> context = data->outerContext;
+ QQmlEngine *engine = context->engine();
+
+ const QMetaType parameterType = d->m_method.parameterMetaType(0);
+ auto cu = QQmlMetaType::obtainCompilationUnit(parameterType);
+ const QQmlType parameterQmlType = QQmlMetaType::qmlType(parameterType);
+
+ if (!parameterQmlType.isValid()) {
+ qWarning("filter method parameter needs to be a QML-registered type");
+ return;
+ }
+
+ // The code below creates an instance of the inline component, composite,
+ // or specific C++ QObject types. The created instance, along with the
+ // data, is passed as an argument to the 'filter' method, which is invoked
+ // during the call to QQmlFunctionFilter::filterAcceptsRowInternal.
+ // To create an instance of required component types (be it inline or
+ // composite), an executable compilation unit is required, and this can be
+ // obtained by looking up via metatype in the type registry
+ // (QQmlMetaType::obtainCompilationUnit). Pass it through the QML engine to
+ // make it executable. Further, use the executable compilation unit to run
+ // an object creator and produce an instance.
+ if (parameterType.flags() & QMetaType::PointerToQObject) {
+ QObject *created = nullptr;
+ if (parameterQmlType.isInlineComponentType()) {
+ const auto executableCu = engine->handle()->executableCompilationUnit(std::move(cu));
+ const QString icName = parameterQmlType.elementName();
+ created = QQmlObjectCreator(context, executableCu, context, icName).create(
+ executableCu->inlineComponentId(icName), nullptr, nullptr,
+ QQmlObjectCreator::InlineComponent);
+ } else if (parameterQmlType.isComposite()) {
+ const auto executableCu = engine->handle()->executableCompilationUnit(std::move(cu));
+ created = QQmlObjectCreator(context, executableCu, context, QString()).create();
+ } else {
+ created = parameterQmlType.metaObject()->newInstance();
+ }
+
+ const auto names = d->m_method.parameterNames();
+ created->setObjectName(names[0]);
+ d->m_parameterData = QVariant::fromValue(created);
+ } else {
+ d->m_parameterData = QVariant(parameterType);
+ }
+}
+
+/*!
+ \internal
+*/
+bool QQmlFunctionFilter::filterAcceptsRowInternal(int row, const QModelIndex& sourceParent, const QQmlSortFilterProxyModel *proxyModel) const
+{
+ Q_D(const QQmlFunctionFilter);
+ if (!d->m_method.isValid() || !d->m_parameterData.isValid())
+ return true;
+
+ bool retVal = false;
+ if (column() > -1) {
+ QSortFilterProxyModelHelper::setProperties(
+ &d->m_parameterData, proxyModel,
+ proxyModel->sourceModel()->index(row, column(), sourceParent));
+ void *argv[] = {&retVal, d->m_parameterData.data()};
+ QMetaObject::metacall(
+ const_cast<QQmlFunctionFilter *>(this), QMetaObject::InvokeMetaMethod,
+ d->m_method.methodIndex(), argv);
+ } else {
+ const int columnCount = proxyModel->sourceModel()->columnCount(sourceParent);
+ for (int column = 0; column < columnCount; column++) {
+ QSortFilterProxyModelHelper::setProperties(
+ &d->m_parameterData, proxyModel,
+ proxyModel->sourceModel()->index(row, column, sourceParent));
+ void *argv[] = {&retVal, d->m_parameterData.data()};
+ QMetaObject::metacall(
+ const_cast<QQmlFunctionFilter *>(this), QMetaObject::InvokeMetaMethod,
+ d->m_method.methodIndex(), argv);
+ if (retVal)
+ return retVal;
+ }
+ }
+ return retVal;
+}
+
+QT_END_NAMESPACE
+
+#include "moc_qqmlfunctionfilter_p.cpp"
diff --git a/src/qmlmodels/sfpm/filters/qqmlfunctionfilter_p.h b/src/qmlmodels/sfpm/filters/qqmlfunctionfilter_p.h
new file mode 100644
index 0000000000..dd174e6ceb
--- /dev/null
+++ b/src/qmlmodels/sfpm/filters/qqmlfunctionfilter_p.h
@@ -0,0 +1,58 @@
+// Copyright (C) 2025 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#ifndef QQMLFUNCTIONFILTER_P_H
+#define QQMLFUNCTIONFILTER_P_H
+
+//
+// W A R N I N G
+// -------------
+//
+// This file is not part of the Qt API. It exists purely as an
+// implementation detail. This header file may change from version to
+// version without notice, or even be removed.
+//
+// We mean it.
+//
+
+#include <QList>
+#include <QQmlScriptString>
+#include <QtQmlModels/private/qqmlfilterbase_p.h>
+
+QT_BEGIN_NAMESPACE
+
+class QQmlSortFilterProxyModel;
+class QQmlFunctionFilterPrivate;
+
+class Q_QMLMODELS_EXPORT QQmlFunctionFilter : public QQmlFilterBase, public QQmlParserStatus
+{
+ Q_OBJECT
+ Q_INTERFACES(QQmlParserStatus)
+ QML_NAMED_ELEMENT(FunctionFilter)
+
+public:
+ explicit QQmlFunctionFilter(QObject *parent = nullptr);
+ ~QQmlFunctionFilter() override;
+
+ bool filterAcceptsRowInternal(int row, const QModelIndex& sourceIndex, const QQmlSortFilterProxyModel *) const override;
+
+private:
+ void classBegin() override {};
+ void componentComplete() override;
+
+private:
+ Q_DECLARE_PRIVATE(QQmlFunctionFilter)
+};
+
+class QQmlFunctionFilterPrivate : public QQmlFilterBasePrivate
+{
+ Q_DECLARE_PUBLIC (QQmlFunctionFilter)
+
+public:
+ QMetaMethod m_method;
+ mutable QVariant m_parameterData;
+};
+
+QT_END_NAMESPACE
+
+#endif // QQMLFUNCTIONFILTER_P_H
diff --git a/src/qmlmodels/sfpm/filters/qqmlrolefilter.cpp b/src/qmlmodels/sfpm/filters/qqmlrolefilter.cpp
new file mode 100644
index 0000000000..04fcfa478c
--- /dev/null
+++ b/src/qmlmodels/sfpm/filters/qqmlrolefilter.cpp
@@ -0,0 +1,66 @@
+// Copyright (C) 2025 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#include <QtQmlModels/private/qqmlrolefilter_p.h>
+#include <QtQmlModels/private/qqmlsortfilterproxymodel_p.h>
+
+QT_BEGIN_NAMESPACE
+
+/*!
+ \qmltype RoleFilter
+ \inherits Filter
+ \inqmlmodule QtQml.Models
+ \since 6.10
+ \preliminary
+ \brief Abstract base type providing functionality to role-dependent filters.
+*/
+
+QQmlRoleFilter::QQmlRoleFilter(QObject *parent) :
+ QQmlFilterBase (new QQmlRoleFilterPrivate, parent)
+{
+}
+
+QQmlRoleFilter::QQmlRoleFilter(QQmlFilterBasePrivate *priv, QObject *parent) :
+ QQmlFilterBase (priv, parent)
+{
+
+}
+
+/*!
+ \qmlproperty string RoleFilter::roleName
+
+ This property holds the role name that will be used to filter the data.
+
+ The default value is the display role.
+*/
+const QString& QQmlRoleFilter::roleName() const
+{
+ Q_D(const QQmlRoleFilter);
+ return d->m_roleName;
+}
+
+void QQmlRoleFilter::setRoleName(const QString& roleName)
+{
+ Q_D(QQmlRoleFilter);
+ if (d->m_roleName == roleName)
+ return;
+ d->m_roleName = roleName;
+ emit roleNameChanged();
+ // Invalidate the model
+ invalidate();
+}
+
+/*!
+ \internal
+*/
+int QQmlRoleFilter::itemRole(const QQmlSortFilterProxyModel *proxyModel) const
+{
+ Q_D(const QQmlRoleFilter);
+ if (!d->m_roleName.isNull())
+ return proxyModel->itemRoleForName(d->m_roleName);
+ return -1;
+}
+
+QT_END_NAMESPACE
+
+#include "moc_qqmlrolefilter_p.cpp"
diff --git a/src/qmlmodels/sfpm/filters/qqmlrolefilter_p.h b/src/qmlmodels/sfpm/filters/qqmlrolefilter_p.h
new file mode 100644
index 0000000000..97d6fad707
--- /dev/null
+++ b/src/qmlmodels/sfpm/filters/qqmlrolefilter_p.h
@@ -0,0 +1,59 @@
+// Copyright (C) 2025 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#ifndef QQMLROLEFILTER_P_H
+#define QQMLROLEFILTER_P_H
+
+//
+// W A R N I N G
+// -------------
+//
+// This file is not part of the Qt API. It exists purely as an
+// implementation detail. This header file may change from version to
+// version without notice, or even be removed.
+//
+// We mean it.
+//
+
+#include <QtQmlModels/private/qqmlfilterbase_p.h>
+
+QT_BEGIN_NAMESPACE
+
+class QQmlSortFilterProxyModel;
+class QQmlRoleFilterPrivate;
+
+class Q_QMLMODELS_EXPORT QQmlRoleFilter : public QQmlFilterBase
+{
+ Q_OBJECT
+ Q_PROPERTY(QString roleName READ roleName WRITE setRoleName NOTIFY roleNameChanged)
+ QML_UNCREATABLE("")
+
+public:
+ explicit QQmlRoleFilter(QObject *parent = nullptr);
+ QQmlRoleFilter(QQmlFilterBasePrivate *priv, QObject *parent = nullptr);
+ ~QQmlRoleFilter() = default;
+
+ const QString& roleName() const;
+ void setRoleName(const QString& roleName);
+
+Q_SIGNALS:
+ void roleNameChanged();
+
+protected:
+ int itemRole(const QQmlSortFilterProxyModel *proxyModel) const;
+
+private:
+ Q_DECLARE_PRIVATE(QQmlRoleFilter)
+};
+
+class QQmlRoleFilterPrivate : public QQmlFilterBasePrivate
+{
+ Q_DECLARE_PUBLIC (QQmlRoleFilter)
+
+public:
+ QString m_roleName = QString::fromUtf8("display");
+};
+
+QT_END_NAMESPACE
+
+#endif // QQMLROLEFILTER_P_H
diff --git a/src/qmlmodels/sfpm/filters/qqmlvaluefilter.cpp b/src/qmlmodels/sfpm/filters/qqmlvaluefilter.cpp
new file mode 100644
index 0000000000..3d373fed9a
--- /dev/null
+++ b/src/qmlmodels/sfpm/filters/qqmlvaluefilter.cpp
@@ -0,0 +1,108 @@
+// Copyright (C) 2025 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#include <QtQmlModels/private/qqmlvaluefilter_p.h>
+#include <QtQmlModels/private/qqmlsortfilterproxymodel_p.h>
+
+QT_BEGIN_NAMESPACE
+
+/*!
+ \qmltype ValueFilter
+ \inherits RoleFilter
+ \inqmlmodule QtQml.Models
+ \since 6.10
+ \preliminary
+ \brief Filters data in a \l SortFilterProxyModel based on role name and
+ value.
+
+ ValueFilter allows the user to filter the data according to the role name
+ or specified value or both as configured in the source model. The role name
+ used to filter the data shall be based on model
+ \l{QAbstractItemModel::roleNames()}{role name}. The default value for
+ role name is \c "display".
+
+ The following snippet shows how ValueFilter can be used to only include
+ data from the source model where the value of the role name \c "favorite"
+ is \c "true":
+
+ \qml
+ SortFilterProxyModel {
+ model: sourceModel
+ filters: [
+ ValueFilter {
+ roleName: "favorite"
+ value: true
+ }
+ ]
+ }
+ \endqml
+*/
+
+QQmlValueFilter::QQmlValueFilter(QObject *parent) :
+ QQmlRoleFilter (new QQmlValueFilterPrivate, parent)
+{
+
+}
+
+/*!
+ \qmlproperty string ValueFilter::value
+
+ This property holds specific value that can be used to filter the data.
+
+ The default value is empty string.
+*/
+const QVariant& QQmlValueFilter::value() const
+{
+ Q_D(const QQmlValueFilter);
+ return d->m_value;
+}
+
+void QQmlValueFilter::setValue(const QVariant& value)
+{
+ Q_D(QQmlValueFilter);
+ if (d->m_value == value)
+ return;
+ d->m_value = value;
+ // Update the model for the change in the role name
+ emit valueChanged();
+ // Invalidate the model for the change in the role name
+ invalidate();
+}
+
+void QQmlValueFilter::resetValue()
+{
+ Q_D(QQmlValueFilter);
+ d->m_value = QVariant();
+}
+
+/*!
+ \internal
+*/
+bool QQmlValueFilter::filterAcceptsRowInternal(int row, const QModelIndex& sourceParent, const QQmlSortFilterProxyModel *proxyModel) const
+{
+ Q_D(const QQmlValueFilter);
+ if (d->m_roleName.isEmpty())
+ return true;
+ int role = itemRole(proxyModel);
+ const bool isValidVal = (!d->m_value.isValid() || !d->m_value.isNull());
+ if (role > -1) {
+ if (column() > -1) {
+ const QModelIndex &index = proxyModel->sourceModel()->index(row, column(), sourceParent);
+ const QVariant &value = proxyModel->sourceModel()->data(index, role);
+ return (value.isValid() && (!isValidVal || d->m_value == value));
+ } else {
+ const int columnCount = proxyModel->sourceModel()->columnCount(sourceParent);
+ for (int column = 0; column < columnCount; column++) {
+ const QModelIndex &index = proxyModel->sourceModel()->index(row, column, sourceParent);
+ const QVariant &value = proxyModel->sourceModel()->data(index, role);
+ if (value.isValid() && (!isValidVal || d->m_value == value))
+ return true;
+ }
+ }
+ }
+ return false;
+}
+
+QT_END_NAMESPACE
+
+#include "moc_qqmlvaluefilter_p.cpp"
diff --git a/src/qmlmodels/sfpm/filters/qqmlvaluefilter_p.h b/src/qmlmodels/sfpm/filters/qqmlvaluefilter_p.h
new file mode 100644
index 0000000000..ccffe1928b
--- /dev/null
+++ b/src/qmlmodels/sfpm/filters/qqmlvaluefilter_p.h
@@ -0,0 +1,58 @@
+// Copyright (C) 2025 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#ifndef QQMLVALUEFILTER_P_H
+#define QQMLVALUEFILTER_P_H
+
+//
+// W A R N I N G
+// -------------
+//
+// This file is not part of the Qt API. It exists purely as an
+// implementation detail. This header file may change from version to
+// version without notice, or even be removed.
+//
+// We mean it.
+//
+
+#include <QtQmlModels/private/qqmlrolefilter_p.h>
+
+QT_BEGIN_NAMESPACE
+
+class QQmlSortFilterProxyModel;
+class QQmlValueFilterPrivate;
+
+class Q_QMLMODELS_EXPORT QQmlValueFilter : public QQmlRoleFilter
+{
+ Q_OBJECT
+ Q_PROPERTY(QVariant value READ value WRITE setValue RESET resetValue NOTIFY valueChanged)
+ QML_NAMED_ELEMENT(ValueFilter)
+
+public:
+ explicit QQmlValueFilter(QObject *parent = nullptr);
+ ~QQmlValueFilter() = default;
+
+ const QVariant& value() const;
+ void setValue(const QVariant& value);
+ void resetValue();
+
+ bool filterAcceptsRowInternal(int row, const QModelIndex& sourceIndex, const QQmlSortFilterProxyModel *) const override;
+
+Q_SIGNALS:
+ void valueChanged();
+
+private:
+ Q_DECLARE_PRIVATE(QQmlValueFilter)
+};
+
+class QQmlValueFilterPrivate : public QQmlRoleFilterPrivate
+{
+ Q_DECLARE_PUBLIC (QQmlValueFilter)
+
+public:
+ QVariant m_value;
+};
+
+QT_END_NAMESPACE
+
+#endif // QQMLVALUEFILTER_P_H
diff --git a/src/qmlmodels/sfpm/qqmlsortfilterproxymodel.cpp b/src/qmlmodels/sfpm/qqmlsortfilterproxymodel.cpp
new file mode 100644
index 0000000000..970722d69d
--- /dev/null
+++ b/src/qmlmodels/sfpm/qqmlsortfilterproxymodel.cpp
@@ -0,0 +1,1720 @@
+// Copyright (C) 2025 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#include <QtCore/qitemselectionmodel.h>
+#include <QtQmlModels/private/qqmlsortfilterproxymodel_p.h>
+#include <QtQmlModels/private/qqmlfiltercompositor_p.h>
+#include <QtQmlModels/private/qqmlsortercompositor_p.h>
+#include <QtQmlModels/private/qqmlrolefilter_p.h>
+
+QT_BEGIN_NAMESPACE
+
+Q_LOGGING_CATEGORY (lcSortFilterProxyModel, "qt.qml.sortfilterproxymodel")
+
+/*!
+ \qmltype SortFilterProxyModel
+ //! \nativetype QQmlSortFilterProxyModel
+ \inqmlmodule QtQml.Models
+ \since 6.10
+ \preliminary
+ \brief Provides sorting and filtering capabilities for a
+ \l QAbstractItemModel.
+
+ SortFilterProxyModel inherits from \l QAbstractProxyModel, which handles
+ the transformation of source indexes into proxy indexes. Sorting and
+ filtering are controlled via the \l{SortFilterProxyModel::sorters}{sorters}
+ and \l{SortFilterProxyModel::filters}{filters} properties, which
+ determine the order of execution as specified in QML. This order can be
+ overridden using the priority property available for each sorter. By
+ default, all sorters share the same priority.
+
+ SortFilterProxyModel enables users to sort and filter a
+ \l QAbstractItemModel. The item views such as \l TableView or \l TreeView
+ can utilize this proxy model to display filtered content.
+
+ \note Currently, SortFilterProxyModel supports only QAbstractItemModel
+ as the source model.
+
+ The snippet below shows the configuration of sorters and filters in the
+ QML SortFilterProxyModel:
+
+ \qml
+ ProcessModel { id: processModel }
+
+ SortFilterProxyModel {
+ id: sfpm
+ model: processModel
+ sorters: [
+ RoleSorter: {
+ roleName: "user"
+ priority: 0
+ },
+ RoleSorter: {
+ roleName: "pid"
+ priority: 1
+ }
+ ]
+ filters: [
+ FunctionFilter: {
+ component RoleData: QtObject { property qreal cpuUsage }
+ function filter(data: RoleData) : bool {
+ return (data.cpuUsage > 90)
+ }
+ }
+ ]
+ }
+ \endqml
+
+ The SortFilterProxyModel dynamically sorts and filters data whenever there
+ is a change to the data in the source model and can be disabled through the
+ \l{SortFilterProxyModel::dynamicSortFilter}{dynamicSortFilter} property.
+
+ The sorters \l RoleSorter, \l ValueSorter and \l StringSorter can be
+ configured in SortFilterProxyModel. Each sorter can be configured with a
+ specific column index through \l{Sorter::column}{column} property. If a
+ column index is not specified, the sorting will be applied to the column
+ index 0 of the model by default. The execution order of the sorter can be
+ modified through the \l{Sorter::priority}{priority} property. This is
+ particularly useful when performing hierarchical sorting, such as sorting
+ data in the first column and then applying sorting to subsequent columns.
+
+ To disable a specific sorter, \l{Sorter::enabled}{enabled} can be set to
+ \c false.
+
+ The sorter priority can also be overridden by setting the primary sorter
+ through the method call
+ \l{SortFilterProxyModel::setPrimarySorter(sorter)}{setPrimarySorter}. This
+ would be helpful in the case where the view wants to sort the data of any
+ specific column by clicking on the column header such as in \l TableView,
+ when there are other sorters also configured for the model.
+
+ The filter \l ValueFilter and \l FunctionFilter can be configured
+ in SortFilterProxyModel. Each filter can be set with the
+ \l{Filter::column}{column} property, similar to the
+ sorter, to filter data in a specific column. If no column is specified,
+ then the filter will be applied to all the column indexes in
+ the model. To reduce the overhead of unwanted checks during filtering,
+ it's recommended to specify the column index.
+
+ To disable a specific filter, \l{Filter::enabled}{enabled} can be set to
+ \c false.
+
+ \snippet qml/sortfilterproxymodel/qml-sortfilterproxymodel.qml sfpm-usage
+
+ \note This API is considered tech preview and may change or be removed in
+ future versions of Qt.
+*/
+/*!
+ \qmlproperty list<Filter> SortFilterProxyModel::filters
+
+ This property holds the list of filters for the \l SortFilterProxyModel.
+ If no priority is set, the \l SortFilterProxyModel applies a filter in the
+ order as specified in the list.
+*/
+/*!
+ \qmlproperty list<Sorter> SortFilterProxyModel::sorters
+
+ This property holds the list of sorters for the \l SortFilterProxyModel.
+ If no priority is set, the \l SortFilterProxyModel applies a sorter in the
+ order as specified in the list.
+*/
+
+class QQmlSortFilterProxyModelPrivate : public QAbstractProxyModelPrivate, public QSortFilterProxyModelHelper
+{
+ Q_DECLARE_PUBLIC(QQmlSortFilterProxyModel)
+
+public:
+ void init();
+
+ bool containRoleForRecursiveFilter(const QList<int> &roles) const;
+ bool recursiveParentAcceptsRow(const QModelIndex &source_parent) const;
+ bool recursiveChildAcceptsRow(int source_row, const QModelIndex &source_parent) const;
+
+ QList<std::pair<int, QList<int>>> proxy_intervals_for_source_items_to_add(
+ const QList<int> &proxy_to_source, const QList<int> &source_items,
+ const QModelIndex &source_parent, QSortFilterProxyModelHelper::Direction direction) const override;
+ bool needsReorder(const QList<int> &source_rows, const QModelIndex &source_parent) const;
+ bool updatePrimaryColumn();
+ int findPrimarySortColumn() const;
+
+ inline QModelIndex create_index(int row, int column,
+ QHash<QtPrivate::QModelIndexWrapper, QSortFilterProxyModelHelper::Mapping *>::const_iterator it) const {
+ return q_func()->createIndex(row, column, *it);
+ }
+ void changePersistentIndexList(const QModelIndexList &from, const QModelIndexList &to) override {
+ Q_Q(QQmlSortFilterProxyModel);
+ q->changePersistentIndexList(from, to);
+ }
+
+ void beginInsertRows(const QModelIndex &parent, int first, int last) override {
+ Q_Q(QQmlSortFilterProxyModel);
+ q->beginInsertRows(parent, first, last);
+ }
+
+ void beginInsertColumns(const QModelIndex &parent, int first, int last) override {
+ Q_Q(QQmlSortFilterProxyModel);
+ q->beginInsertColumns(parent, first, last);
+ }
+
+ void endInsertRows() override {
+ Q_Q(QQmlSortFilterProxyModel);
+ q->endInsertRows();
+ }
+
+ void endInsertColumns() override {
+ Q_Q(QQmlSortFilterProxyModel);
+ q->endInsertColumns();
+ }
+
+ void beginRemoveRows(const QModelIndex &parent, int first, int last) override {
+ Q_Q(QQmlSortFilterProxyModel);
+ q->beginRemoveRows(parent, first, last);
+ }
+
+ void beginRemoveColumns(const QModelIndex &parent, int first, int last) override {
+ Q_Q(QQmlSortFilterProxyModel);
+ q->beginRemoveColumns(parent, first, last);
+ }
+
+ void endRemoveRows() override {
+ Q_Q(QQmlSortFilterProxyModel);
+ q->endRemoveRows();
+ }
+
+ void endRemoveColumns() override {
+ Q_Q(QQmlSortFilterProxyModel);
+ q->endRemoveColumns();
+ }
+
+ void beginResetModel() override {
+ Q_Q(QQmlSortFilterProxyModel);
+ q->beginResetModel();
+ }
+
+ void endResetModel() override {
+ Q_Q(QQmlSortFilterProxyModel);
+ q->endResetModel();
+ }
+
+ // Update the proxy model when there is any change in the source model
+ void _q_sourceDataChanged(const QModelIndex &source_top_left,
+ const QModelIndex &source_bottom_right,
+ const QList<int> &roles);
+ void _q_sourceHeaderDataChanged(Qt::Orientation orientation,
+ int start, int end);
+ void _q_sourceAboutToBeReset();
+ void _q_sourceReset();
+ void _q_clearMapping();
+ void _q_sourceLayoutAboutToBeChanged(const QList<QPersistentModelIndex> &sourceParents,
+ QAbstractItemModel::LayoutChangeHint hint);
+ void _q_sourceLayoutChanged(const QList<QPersistentModelIndex> &sourceParents,
+ QAbstractItemModel::LayoutChangeHint hint);
+ void _q_sourceRowsAboutToBeInserted(const QModelIndex &source_parent,
+ int start, int end);
+ void _q_sourceRowsInserted(const QModelIndex &source_parent,
+ int start, int end);
+ void _q_sourceRowsAboutToBeRemoved(const QModelIndex &source_parent,
+ int start, int end);
+ void _q_sourceRowsRemoved(const QModelIndex &source_parent,
+ int start, int end);
+ void _q_sourceRowsAboutToBeMoved(const QModelIndex &sourceParent,
+ int sourceStart, int sourceEnd,
+ const QModelIndex &destParent, int dest);
+ void _q_sourceRowsMoved(const QModelIndex &sourceParent,
+ int sourceStart, int sourceEnd,
+ const QModelIndex &destParent, int dest);
+ void _q_sourceColumnsAboutToBeInserted(const QModelIndex &source_parent,
+ int start, int end);
+ void _q_sourceColumnsInserted(const QModelIndex &source_parent,
+ int start, int end);
+ void _q_sourceColumnsAboutToBeRemoved(const QModelIndex &source_parent,
+ int start, int end);
+ void _q_sourceColumnsRemoved(const QModelIndex &source_parent,
+ int start, int end);
+ void _q_sourceColumnsAboutToBeMoved(const QModelIndex &sourceParent,
+ int sourceStart, int sourceEnd,
+ const QModelIndex &destParent, int dest);
+ void _q_sourceColumnsMoved(const QModelIndex &sourceParent,
+ int sourceStart, int sourceEnd,
+ const QModelIndex &destParent, int dest);
+
+ const QAbstractProxyModel *proxyModel() const override { return q_func(); }
+ QModelIndex createIndex(int row, int column,
+ QHash<QtPrivate::QModelIndexWrapper, QSortFilterProxyModelHelper::Mapping *>::const_iterator it) const override {
+ return create_index(row, column, it);
+ }
+ bool filterAcceptsRowInternal(int sourceRow, const QModelIndex &sourceIndex) const override;
+ bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override {
+ return q_func()->filterAcceptsRow(source_row, source_parent);
+ }
+ bool filterAcceptsColumnInternal(int sourceColumn, const QModelIndex &sourceIndex) const override;
+ bool filterAcceptsColumn(int source_column, const QModelIndex &source_parent) const override {
+ return q_func()->filterAcceptsColumn(source_column, source_parent);
+ }
+ void sort_source_rows(QList<int> &source_rows, const QModelIndex &source_parent) const override;
+ bool lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const override {
+ return q_func()->lessThan(source_left, source_right);
+ }
+
+ // Internal
+ QModelIndex m_lastTopSource;
+ QRowsRemoval m_itemsBeingRemoved;
+ bool m_completeInsert = false;
+ QModelIndexPairList m_savedPersistentIndexes;
+ QList<QPersistentModelIndex> m_savedLayoutChangeParents;
+ std::array<QMetaObject::Connection, 18> m_sourceConnections;
+ bool m_componentCompleted = false;
+
+ // Properties exposed to the user
+ QQmlFilterCompositor* m_filters;
+ QQmlSorterCompositor* m_sorters;
+ bool m_dynamicSortFilter = true;
+ bool m_recursiveFiltering = false;
+ bool m_autoAcceptChildRows = false;
+ int m_primarySortColumn = -1;
+ int m_proxySortColumn = -1;
+ Qt::SortOrder m_sortOrder = Qt::AscendingOrder;
+ QVariant m_sourceModel;
+};
+
+QQmlSortFilterProxyModel::QQmlSortFilterProxyModel(QObject *parent)
+ : QAbstractProxyModel (*new QQmlSortFilterProxyModelPrivate, parent)
+{
+ Q_D(QQmlSortFilterProxyModel);
+ d->init();
+}
+
+/*!
+ * Clear the filters and sorters and deletes the sort filter proxy model
+ */
+QQmlSortFilterProxyModel::~QQmlSortFilterProxyModel()
+{
+ Q_D(QQmlSortFilterProxyModel);
+ delete d->m_filters;
+ delete d->m_sorters;
+}
+
+/*!
+ * Provides the filters configured in the sort filter proxy model
+ */
+QQmlListProperty<QQmlFilterBase> QQmlSortFilterProxyModel::filters()
+{
+ Q_D(QQmlSortFilterProxyModel);
+ return d->m_filters->filtersListProperty();
+}
+
+/*!
+ * Provides the sorters configured in the sort filter proxy model
+ */
+QQmlListProperty<QQmlSorterBase> QQmlSortFilterProxyModel::sorters()
+{
+ Q_D(QQmlSortFilterProxyModel);
+ return d->m_sorters->sortersListProperty();
+}
+
+/*!
+ \qmlproperty bool SortFilterProxyModel::dynamicSortFilter
+
+ This property holds whether the proxy model is dynamically sorted
+ and filtered whenever the contents of the source model change.
+
+ The default value is \c true.
+*/
+bool QQmlSortFilterProxyModel::dynamicSortFilter() const
+{
+ Q_D(const QQmlSortFilterProxyModel);
+ return d->m_dynamicSortFilter;
+}
+
+void QQmlSortFilterProxyModel::setDynamicSortFilter(const bool enabled)
+{
+ Q_D(QQmlSortFilterProxyModel);
+ if (d->m_dynamicSortFilter == enabled)
+ return;
+ d->m_dynamicSortFilter = enabled;
+ emit dynamicSortFilterChanged();
+}
+
+/*!
+ \qmlproperty bool SortFilterProxyModel::recursiveFiltering
+
+ This property allows all the configured filters to be applied recursively
+ on children. The behavior is similar to that of
+ \l recursiveFilteringEnabled in \l QSortFilterProxyModel.
+
+ The default value is \c false.
+*/
+bool QQmlSortFilterProxyModel::recursiveFiltering() const
+{
+ Q_D(const QQmlSortFilterProxyModel);
+ return d->m_recursiveFiltering;
+}
+
+void QQmlSortFilterProxyModel::setRecursiveFiltering(const bool enabled)
+{
+ Q_D(QQmlSortFilterProxyModel);
+ if (d->m_recursiveFiltering == enabled)
+ return;
+ d->m_recursiveFiltering = enabled;
+ emit recursiveFilteringChanged();
+}
+
+/*!
+ \qmlproperty bool SortFilterProxyModel::autoAcceptChildRows
+
+ This property will not filter out children of accepted rows. The behavior
+ is similar to that of \l autoAcceptChildRows in \l QSortFilterProxyModel.
+
+ The default value is \c false.
+*/
+bool QQmlSortFilterProxyModel::autoAcceptChildRows() const
+{
+ Q_D(const QQmlSortFilterProxyModel);
+ return d->m_autoAcceptChildRows;
+}
+
+void QQmlSortFilterProxyModel::setAutoAcceptChildRows(const bool enabled)
+{
+ Q_D(QQmlSortFilterProxyModel);
+ if (d->m_autoAcceptChildRows == enabled)
+ return;
+ d->m_autoAcceptChildRows = enabled;
+ emit autoAcceptChildRowsChanged();
+}
+
+/*!
+ \qmlproperty var SortFilterProxyModel::model
+
+ This property allows to set source model for the sort filter proxy model.
+*/
+QVariant QQmlSortFilterProxyModel::model() const
+{
+ Q_D(const QQmlSortFilterProxyModel);
+ return d->m_sourceModel;
+}
+
+void QQmlSortFilterProxyModel::setModel(QVariant &model)
+{
+ Q_D(QQmlSortFilterProxyModel);
+ if (d->m_sourceModel == model)
+ return;
+
+ auto *itemModel = qobject_cast<QAbstractItemModel *>(qvariant_cast<QObject*>(model));
+ if (!itemModel ) {
+ qWarning("QQmlSortFilterProxyModel: supports only QAIM for now");
+ return;
+ }
+ d->m_sourceModel = model;
+ setSourceModel(itemModel);
+ emit modelChanged();
+}
+
+/*! internal
+ *
+ */
+void QQmlSortFilterProxyModel::invalidateFilter()
+{
+ Q_D(QQmlSortFilterProxyModel);
+ if (d->m_componentCompleted)
+ d->filter_changed();
+}
+
+/*!
+ \qmlmethod SortFilterProxyModel::invalidate()
+
+ This method invalidates the model by reevaluating the configured filters
+ and sorters on the source model data.
+ */
+void QQmlSortFilterProxyModel::invalidate()
+{
+ Q_D(QQmlSortFilterProxyModel);
+ if (d->m_componentCompleted) {
+ d->filter_changed();
+ d->sort();
+ }
+}
+
+/*!
+ \qmlmethod SortFilterProxyModel::invalidateSorter()
+
+ This method force the sort filter proxy model to reevaluate the configured
+ sorters against the data. It can used in the case where dynamic sorting
+ was disabled through property \l dynamicSortFilter
+*/
+void QQmlSortFilterProxyModel::invalidateSorter()
+{
+ Q_D(QQmlSortFilterProxyModel);
+ if (d->m_componentCompleted)
+ d->sort();
+}
+
+/*!
+ \qmlmethod SortFilterProxyModel::setPrimarySorter(sorter)
+
+ This method allows to set the primary sorter in the sort filter proxy
+ model. The primary sorter will be evaluated before all other sorters
+ configured as part of \a sorter property. If not configured or passed
+ \c null, the sorter with higher priority shall be considered as the
+ primary sorter.
+*/
+void QQmlSortFilterProxyModel::setPrimarySorter(QQmlSorterBase *sorter)
+{
+ Q_D(QQmlSortFilterProxyModel);
+ if (auto *sortCompPriv = static_cast<QQmlSorterCompositorPrivate *>(QQmlSorterCompositorPrivate::get(d->m_sorters))) {
+ auto primarySorter = sortCompPriv->primarySorter();
+ if (sorter == primarySorter.get())
+ return;
+ sortCompPriv->setPrimarySorter(sorter);
+ emit primarySorterChanged();
+ invalidateSorter();
+ }
+}
+
+/*!
+ \internal
+ */
+void QQmlSortFilterProxyModel::setSourceModel(QAbstractItemModel *sourceModel)
+{
+ Q_D(QQmlSortFilterProxyModel);
+
+ if (sourceModel == d->model)
+ return;
+
+ beginResetModel();
+
+ if (d->model) {
+ for (const QMetaObject::Connection &connection : std::as_const(d->m_sourceConnections))
+ disconnect(connection);
+ }
+
+ // same as in _q_sourceReset()
+ d->invalidatePersistentIndexes();
+ d->_q_clearMapping();
+
+ QAbstractProxyModel::setSourceModel(sourceModel);
+
+ d->m_sourceConnections = std::array<QMetaObject::Connection, 18>{
+ QObjectPrivate::connect(d->model, &QAbstractItemModel::dataChanged, d,
+ &QQmlSortFilterProxyModelPrivate::_q_sourceDataChanged),
+
+ QObjectPrivate::connect(d->model, &QAbstractItemModel::headerDataChanged, d,
+ &QQmlSortFilterProxyModelPrivate::_q_sourceHeaderDataChanged),
+
+ QObjectPrivate::connect(d->model, &QAbstractItemModel::rowsAboutToBeInserted, d,
+ &QQmlSortFilterProxyModelPrivate::_q_sourceRowsAboutToBeInserted),
+
+ QObjectPrivate::connect(d->model, &QAbstractItemModel::rowsInserted, d,
+ &QQmlSortFilterProxyModelPrivate::_q_sourceRowsInserted),
+
+ QObjectPrivate::connect(d->model, &QAbstractItemModel::columnsAboutToBeInserted, d,
+ &QQmlSortFilterProxyModelPrivate::_q_sourceColumnsAboutToBeInserted),
+
+ QObjectPrivate::connect(d->model, &QAbstractItemModel::columnsInserted, d,
+ &QQmlSortFilterProxyModelPrivate::_q_sourceColumnsInserted),
+
+ QObjectPrivate::connect(d->model, &QAbstractItemModel::rowsAboutToBeRemoved, d,
+ &QQmlSortFilterProxyModelPrivate::_q_sourceRowsAboutToBeRemoved),
+
+ QObjectPrivate::connect(d->model, &QAbstractItemModel::rowsRemoved, d,
+ &QQmlSortFilterProxyModelPrivate::_q_sourceRowsRemoved),
+
+ QObjectPrivate::connect(d->model, &QAbstractItemModel::columnsAboutToBeRemoved, d,
+ &QQmlSortFilterProxyModelPrivate::_q_sourceColumnsAboutToBeRemoved),
+
+ QObjectPrivate::connect(d->model, &QAbstractItemModel::columnsRemoved, d,
+ &QQmlSortFilterProxyModelPrivate::_q_sourceColumnsRemoved),
+
+ QObjectPrivate::connect(d->model, &QAbstractItemModel::rowsAboutToBeMoved, d,
+ &QQmlSortFilterProxyModelPrivate::_q_sourceRowsAboutToBeMoved),
+
+ QObjectPrivate::connect(d->model, &QAbstractItemModel::rowsMoved, d,
+ &QQmlSortFilterProxyModelPrivate::_q_sourceRowsMoved),
+
+ QObjectPrivate::connect(d->model, &QAbstractItemModel::columnsAboutToBeMoved, d,
+ &QQmlSortFilterProxyModelPrivate::_q_sourceColumnsAboutToBeMoved),
+
+ QObjectPrivate::connect(d->model, &QAbstractItemModel::columnsMoved, d,
+ &QQmlSortFilterProxyModelPrivate::_q_sourceColumnsMoved),
+
+ QObjectPrivate::connect(d->model, &QAbstractItemModel::layoutAboutToBeChanged, d,
+ &QQmlSortFilterProxyModelPrivate::_q_sourceLayoutAboutToBeChanged),
+
+ QObjectPrivate::connect(d->model, &QAbstractItemModel::layoutChanged, d,
+ &QQmlSortFilterProxyModelPrivate::_q_sourceLayoutChanged),
+
+ QObjectPrivate::connect(d->model, &QAbstractItemModel::modelAboutToBeReset, d,
+ &QQmlSortFilterProxyModelPrivate::_q_sourceAboutToBeReset),
+
+ QObjectPrivate::connect(d->model, &QAbstractItemModel::modelReset, d,
+ &QQmlSortFilterProxyModelPrivate::_q_sourceReset)
+ };
+ endResetModel();
+
+ if (d->m_dynamicSortFilter && d->updatePrimaryColumn())
+ d->sort();
+}
+
+/*!
+ Returns the source model index corresponding to the given \a
+ proxyIndex from the sorting filter model.
+*/
+QModelIndex QQmlSortFilterProxyModel::mapToSource(const QModelIndex &proxyIndex) const
+{
+ Q_D(const QQmlSortFilterProxyModel);
+ return d->proxy_to_source(proxyIndex);
+}
+
+/*!
+ Returns the model index in the QQmlSortFilterProxyModel given the \a
+ sourceIndex from the source model.
+*/
+QModelIndex QQmlSortFilterProxyModel::mapFromSource(const QModelIndex &sourceIndex) const
+{
+ Q_D(const QQmlSortFilterProxyModel);
+ return d->source_to_proxy(sourceIndex);
+}
+
+/*!
+ \reimp
+*/
+QItemSelection QQmlSortFilterProxyModel::mapSelectionToSource(const QItemSelection &proxySelection) const
+{
+ return QAbstractProxyModel::mapSelectionToSource(proxySelection);
+}
+
+/*!
+ \reimp
+*/
+QItemSelection QQmlSortFilterProxyModel::mapSelectionFromSource(const QItemSelection &sourceSelection) const
+{
+ return QAbstractProxyModel::mapSelectionFromSource(sourceSelection);
+}
+
+
+/*!
+ \reimp
+*/
+QModelIndex QQmlSortFilterProxyModel::index(int row, int column, const QModelIndex &parent) const
+{
+ Q_D(const QQmlSortFilterProxyModel);
+ if (row < 0 || column < 0)
+ return QModelIndex();
+
+ QModelIndex source_parent = mapToSource(parent); // parent is already mapped at this point
+ QSortFilterProxyModelHelper::IndexMap::const_iterator it = d->create_mapping(source_parent); // but make sure that the children are mapped
+ if (it.value()->source_rows.size() <= row || it.value()->source_columns.size() <= column)
+ return QModelIndex();
+
+ return d->create_index(row, column, it);
+}
+
+/*!
+ \reimp
+*/
+QModelIndex QQmlSortFilterProxyModel::parent(const QModelIndex &child) const
+{
+ Q_D(const QQmlSortFilterProxyModel);
+ if (!d->indexValid(child))
+ return QModelIndex();
+ QSortFilterProxyModelHelper::IndexMap::const_iterator it = d->index_to_iterator(child);
+ Q_ASSERT(it != d->source_index_mapping.constEnd());
+ QModelIndex source_parent = it.key();
+ QModelIndex proxy_parent = mapFromSource(source_parent);
+ return proxy_parent;
+}
+
+/*!
+ \reimp
+*/
+QModelIndex QQmlSortFilterProxyModel::sibling(int row, int column, const QModelIndex &idx) const
+{
+ Q_D(const QQmlSortFilterProxyModel);
+ if (!d->indexValid(idx))
+ return QModelIndex();
+
+ const QSortFilterProxyModelHelper::IndexMap::const_iterator it = d->index_to_iterator(idx);
+ if (it.value()->source_rows.size() <= row || it.value()->source_columns.size() <= column)
+ return QModelIndex();
+
+ return d->create_index(row, column, it);
+}
+
+/*!
+ \reimp
+*/
+bool QQmlSortFilterProxyModel::hasChildren(const QModelIndex &parent) const
+{
+ Q_D(const QQmlSortFilterProxyModel);
+ QModelIndex source_parent = mapToSource(parent);
+ if (parent.isValid() && !source_parent.isValid())
+ return false;
+ if (!d->model->hasChildren(source_parent))
+ return false;
+
+ if (d->model->canFetchMore(source_parent))
+ return true; //we assume we might have children that can be fetched
+
+ QSortFilterProxyModelHelper::Mapping *m = d->create_mapping(source_parent).value();
+ return m->source_rows.size() != 0 && m->source_columns.size() != 0;
+}
+
+int QQmlSortFilterProxyModel::columnCount(const QModelIndex &parent) const
+{
+ Q_D(const QQmlSortFilterProxyModel);
+ if (d->m_sourceModel.isNull() || !d->m_sourceModel.isValid())
+ return 0;
+ QModelIndex source_parent = mapToSource(parent);
+ if (parent.isValid() && !source_parent.isValid())
+ return 0;
+ QSortFilterProxyModelHelper::IndexMap::const_iterator it = d->create_mapping(source_parent);
+ return it.value()->source_columns.size();
+}
+
+int QQmlSortFilterProxyModel::rowCount(const QModelIndex &parent) const
+{
+ Q_D(const QQmlSortFilterProxyModel);
+ if (d->m_sourceModel.isNull() || !d->m_sourceModel.isValid())
+ return 0;
+ QModelIndex source_parent = mapToSource(parent);
+ if (parent.isValid() && !source_parent.isValid())
+ return 0;
+ QSortFilterProxyModelHelper::IndexMap::const_iterator it = d->create_mapping(source_parent);
+ return it.value()->source_rows.size();
+}
+
+QVariant QQmlSortFilterProxyModel::data(const QModelIndex &index, int role) const
+{
+ Q_D(const QQmlSortFilterProxyModel);
+ QModelIndex source_index = mapToSource(index);
+ if (index.isValid() && !source_index.isValid())
+ return QVariant();
+ return d->model->data(source_index, role);
+}
+
+bool QQmlSortFilterProxyModel::setData(const QModelIndex &index, const QVariant &value, int role)
+{
+ Q_D(QQmlSortFilterProxyModel);
+ QModelIndex source_index = mapToSource(index);
+ if (index.isValid() && !source_index.isValid())
+ return false;
+ return d->model->setData(source_index, value, role);
+}
+
+/*!
+ \reimp
+*/
+QVariant QQmlSortFilterProxyModel::headerData(int section, Qt::Orientation orientation, int role) const
+{
+ Q_D(const QQmlSortFilterProxyModel);
+ QSortFilterProxyModelHelper::IndexMap::const_iterator it = d->create_mapping(QModelIndex());
+ if (it.value()->source_rows.size() * it.value()->source_columns.size() > 0)
+ return QAbstractProxyModel::headerData(section, orientation, role);
+ int source_section;
+ if (orientation == Qt::Vertical) {
+ if (section < 0 || section >= it.value()->source_rows.size())
+ return QVariant();
+ source_section = it.value()->source_rows.at(section);
+ } else {
+ if (section < 0 || section >= it.value()->source_columns.size())
+ return QVariant();
+ source_section = it.value()->source_columns.at(section);
+ }
+ return d->model->headerData(source_section, orientation, role);
+}
+
+/*!
+ \reimp
+*/
+bool QQmlSortFilterProxyModel::setHeaderData(int section, Qt::Orientation orientation,
+ const QVariant &value, int role)
+{
+ Q_D(QQmlSortFilterProxyModel);
+ QSortFilterProxyModelHelper::IndexMap::const_iterator it = d->create_mapping(QModelIndex());
+ if (it.value()->source_rows.size() * it.value()->source_columns.size() > 0)
+ return QAbstractProxyModel::setHeaderData(section, orientation, value, role);
+ int source_section;
+ if (orientation == Qt::Vertical) {
+ if (section < 0 || section >= it.value()->source_rows.size())
+ return false;
+ source_section = it.value()->source_rows.at(section);
+ } else {
+ if (section < 0 || section >= it.value()->source_columns.size())
+ return false;
+ source_section = it.value()->source_columns.at(section);
+ }
+ return d->model->setHeaderData(source_section, orientation, value, role);
+}
+
+/*!
+ \reimp
+*/
+bool QQmlSortFilterProxyModel::insertRows(int row, int count, const QModelIndex &parent)
+{
+ Q_D(QQmlSortFilterProxyModel);
+ if (row < 0 || count <= 0)
+ return false;
+ QModelIndex source_parent = mapToSource(parent);
+ if (parent.isValid() && !source_parent.isValid())
+ return false;
+ QSortFilterProxyModelHelper::Mapping *m = d->create_mapping(source_parent).value();
+ if (row > m->source_rows.size())
+ return false;
+ int source_row = (row >= m->source_rows.size()
+ ? m->proxy_rows.size()
+ : m->source_rows.at(row));
+ return d->model->insertRows(source_row, count, source_parent);
+}
+
+/*!
+ \reimp
+*/
+bool QQmlSortFilterProxyModel::insertColumns(int column, int count, const QModelIndex &parent)
+{
+ Q_D(QQmlSortFilterProxyModel);
+ if (column < 0|| count <= 0)
+ return false;
+ QModelIndex source_parent = mapToSource(parent);
+ if (parent.isValid() && !source_parent.isValid())
+ return false;
+ QSortFilterProxyModelHelper::Mapping *m = d->create_mapping(source_parent).value();
+ if (column > m->source_columns.size())
+ return false;
+ int source_column = (column >= m->source_columns.size()
+ ? m->proxy_columns.size()
+ : m->source_columns.at(column));
+ return d->model->insertColumns(source_column, count, source_parent);
+}
+
+/*!
+ \reimp
+*/
+bool QQmlSortFilterProxyModel::removeRows(int row, int count, const QModelIndex &parent)
+{
+ Q_D(QQmlSortFilterProxyModel);
+ if (row < 0 || count <= 0)
+ return false;
+ QModelIndex source_parent = mapToSource(parent);
+ if (parent.isValid() && !source_parent.isValid())
+ return false;
+ QSortFilterProxyModelHelper::Mapping *m = d->create_mapping(source_parent).value();
+ if (row + count > m->source_rows.size())
+ return false;
+ if ((count == 1)
+ || ((d->m_primarySortColumn < 0) && (m->proxy_rows.size() == m->source_rows.size()))) {
+ int source_row = m->source_rows.at(row);
+ return d->model->removeRows(source_row, count, source_parent);
+ }
+ // remove corresponding source intervals
+ // ### if this proves to be slow, we can switch to single-row removal
+ QList<int> rows;
+ rows.reserve(count);
+ for (int i = row; i < row + count; ++i)
+ rows.append(m->source_rows.at(i));
+ std::sort(rows.begin(), rows.end());
+
+ int pos = rows.size() - 1;
+ bool ok = true;
+ while (pos >= 0) {
+ const int source_end = rows.at(pos--);
+ int source_start = source_end;
+ while ((pos >= 0) && (rows.at(pos) == (source_start - 1))) {
+ --source_start;
+ --pos;
+ }
+ ok = ok && d->model->removeRows(source_start, source_end - source_start + 1,
+ source_parent);
+ }
+ return ok;
+}
+
+/*!
+ \reimp
+*/
+bool QQmlSortFilterProxyModel::removeColumns(int column, int count, const QModelIndex &parent)
+{
+ Q_D(QQmlSortFilterProxyModel);
+ if (column < 0 || count <= 0)
+ return false;
+ QModelIndex source_parent = mapToSource(parent);
+ if (parent.isValid() && !source_parent.isValid())
+ return false;
+ QSortFilterProxyModelHelper::Mapping *m = d->create_mapping(source_parent).value();
+ if (column + count > m->source_columns.size())
+ return false;
+ if ((count == 1) || (m->proxy_columns.size() == m->source_columns.size())) {
+ int source_column = m->source_columns.at(column);
+ return d->model->removeColumns(source_column, count, source_parent);
+ }
+ // remove corresponding source intervals
+ QList<int> columns;
+ columns.reserve(count);
+ for (int i = column; i < column + count; ++i)
+ columns.append(m->source_columns.at(i));
+
+ int pos = columns.size() - 1;
+ bool ok = true;
+ while (pos >= 0) {
+ const int source_end = columns.at(pos--);
+ int source_start = source_end;
+ while ((pos >= 0) && (columns.at(pos) == (source_start - 1))) {
+ --source_start;
+ --pos;
+ }
+ ok = ok && d->model->removeColumns(source_start, source_end - source_start + 1,
+ source_parent);
+ }
+ return ok;
+}
+
+/*!
+ \internal
+ */
+QHash<int, QByteArray> QQmlSortFilterProxyModel::roleNames() const
+{
+ if (const auto srcModel = sourceModel())
+ return srcModel->roleNames();
+ return {};
+}
+
+/*!
+ \internal
+ */
+QVariant QQmlSortFilterProxyModel::sourceData(const QModelIndex &sourceIndex, int role) const
+{
+ return sourceModel()->data(sourceIndex, role);
+}
+
+/*!
+ \internal
+ */
+bool QQmlSortFilterProxyModel::filterAcceptsRow(int row, const QModelIndex& sourceParent) const
+{
+ Q_D(const QQmlSortFilterProxyModel);
+ if (!d->m_componentCompleted)
+ return true;
+ return d->m_filters->filterAcceptsRowInternal(row, sourceParent, this);
+}
+
+/*!
+ \internal
+ */
+bool QQmlSortFilterProxyModel::filterAcceptsColumn(int column, const QModelIndex &sourceParent) const
+{
+ Q_D(const QQmlSortFilterProxyModel);
+ if (!d->m_componentCompleted)
+ return true;
+ return d->m_filters->filterAcceptsColumnInternal(column, sourceParent, this);
+}
+
+/*!
+ \internal
+ */
+bool QQmlSortFilterProxyModel::lessThan(const QModelIndex &sourceLeft, const QModelIndex &sourceRight) const
+{
+ Q_D(const QQmlSortFilterProxyModel);
+ if (d->m_sorters)
+ return d->m_sorters->lessThan(sourceLeft, sourceRight, this);
+ return false;
+}
+
+/*!
+ \internal
+ */
+void QQmlSortFilterProxyModel::componentComplete()
+{
+ // We need to explicitly call sort here. This is because, as of now the
+ // actual implementation for sorting comes from QSortFilterProxyModelBase
+ // and it trigger the sorting operation with the corresponding column set
+ // and it happens only after parsing the configured sorters.
+ Q_D(QQmlSortFilterProxyModel);
+ d->m_componentCompleted = true;
+ d->filter_changed();
+ if (d->m_dynamicSortFilter)
+ d->sort();
+}
+
+/*!
+ \internal
+ */
+void QQmlSortFilterProxyModel::sort(int column, Qt::SortOrder order)
+{
+ Q_D(QQmlSortFilterProxyModel);
+ if (!d->m_componentCompleted ||
+ (d->m_dynamicSortFilter && d->m_proxySortColumn == column && d->m_sortOrder == order))
+ return;
+ d->m_sortOrder = order;
+ d->m_proxySortColumn = column;
+ d->updatePrimaryColumn();
+ d->sort();
+}
+
+/*!
+ \internal
+ */
+int QQmlSortFilterProxyModel::itemRoleForName(const QString& roleName) const
+{
+ QHash<int, QByteArray> itemRoleNames = roleNames();
+ return !itemRoleNames.isEmpty() ? itemRoleNames.key(roleName.toUtf8(), -1) : -1;
+}
+
+/*!
+ \internal
+ */
+void QQmlSortFilterProxyModel::setPrimarySortColumn(const int column)
+{
+ Q_D(QQmlSortFilterProxyModel);
+ d->m_proxySortColumn = column;
+ d->updatePrimaryColumn();
+}
+
+/*!
+ \internal
+ */
+void QQmlSortFilterProxyModel::setPrimarySortOrder(const Qt::SortOrder order)
+{
+ Q_D(QQmlSortFilterProxyModel);
+ d->m_sortOrder = order;
+}
+
+/*!
+ \internal
+ */
+void QQmlSortFilterProxyModelPrivate::init()
+{
+ Q_Q(QQmlSortFilterProxyModel);
+ m_filters = new QQmlFilterCompositor(q);
+ QObject::connect(q, &QQmlSortFilterProxyModel::filtersChanged,
+ q, &QQmlSortFilterProxyModel::invalidateFilter);
+ m_sorters = new QQmlSorterCompositor(q);
+ QObject::connect(q, &QQmlSortFilterProxyModel::sortersChanged,
+ q, &QQmlSortFilterProxyModel::invalidateSorter);
+}
+
+void QQmlSortFilterProxyModelPrivate::_q_clearMapping()
+{
+ // store the persistent indexes
+ QModelIndexPairList source_indexes = store_persistent_indexes();
+
+ clearSourceIndexMapping();
+ if (m_dynamicSortFilter)
+ m_primarySortColumn = findPrimarySortColumn();
+
+ // update the persistent indexes
+ update_persistent_indexes(source_indexes);
+}
+
+bool QQmlSortFilterProxyModelPrivate::recursiveParentAcceptsRow(const QModelIndex &source_parent) const
+{
+ Q_Q(const QQmlSortFilterProxyModel);
+ if (source_parent.isValid()) {
+ const QModelIndex index = source_parent.parent();
+ if (q->filterAcceptsRow(source_parent.row(), index))
+ return true;
+ return recursiveParentAcceptsRow(index);
+ }
+ return false;
+}
+
+bool QQmlSortFilterProxyModelPrivate::recursiveChildAcceptsRow(int source_row, const QModelIndex &source_parent) const
+{
+ Q_Q(const QQmlSortFilterProxyModel);
+ const QModelIndex index = model->index(source_row, 0, source_parent);
+ const int count = model->rowCount(index);
+ for (int i = 0; i < count; ++i) {
+ if (q->filterAcceptsRow(i, index))
+ return true;
+ if (recursiveChildAcceptsRow(i, index))
+ return true;
+ }
+ return false;
+}
+
+/*!
+ \internal
+
+ Update the primary sort column according to the m_proxySortColumn
+ return true if the column was changed
+*/
+bool QQmlSortFilterProxyModelPrivate::updatePrimaryColumn()
+{
+ int old_primayColumn = m_primarySortColumn;
+
+ if (m_proxySortColumn == -1) {
+ m_primarySortColumn = -1;
+ } else {
+ // We cannot use index mapping here because in case of a still-empty
+ // proxy model there's no valid proxy index we could map to source.
+ // So always use the root mapping directly instead.
+ QSortFilterProxyModelHelper::Mapping *m = create_mapping(QModelIndex()).value();
+ if (m_proxySortColumn < m->source_columns.size())
+ m_primarySortColumn = m->source_columns.at(m_proxySortColumn);
+ else
+ m_primarySortColumn = -1;
+ }
+
+ return old_primayColumn != m_primarySortColumn;
+}
+
+/*!
+ \internal
+
+ Find the primary sort column without creating a full mapping and
+ without updating anything.
+*/
+int QQmlSortFilterProxyModelPrivate::findPrimarySortColumn() const
+{
+ if (m_proxySortColumn == -1)
+ return -1;
+
+ const QModelIndex rootIndex;
+ const int source_cols = model->columnCount();
+ int accepted_columns = -1;
+
+ Q_Q(const QQmlSortFilterProxyModel);
+ for (int i = 0; i < source_cols; ++i) {
+ if (q->filterAcceptsColumn(i, rootIndex)) {
+ if (++accepted_columns == m_proxySortColumn)
+ return i;
+ }
+ }
+
+ return -1;
+}
+
+/*!
+ \internal
+
+ Sorts the given \a source_rows according to current sort column and order.
+*/
+void QQmlSortFilterProxyModelPrivate::sort_source_rows(
+ QList<int> &source_rows, const QModelIndex &source_parent) const
+{
+ if (m_primarySortColumn >= 0) {
+ if (m_sortOrder == Qt::AscendingOrder) {
+ QSortFilterProxyModelLessThan lt(m_primarySortColumn, source_parent, model, this);
+ std::stable_sort(source_rows.begin(), source_rows.end(), lt);
+ } else {
+ QSortFilterProxyModelGreaterThan gt(m_primarySortColumn, source_parent, model, this);
+ std::stable_sort(source_rows.begin(), source_rows.end(), gt);
+ }
+ } else if (m_sortOrder == Qt::AscendingOrder) {
+ std::stable_sort(source_rows.begin(), source_rows.end(), std::less{});
+ } else {
+ std::stable_sort(source_rows.begin(), source_rows.end(), std::greater{});
+ }
+}
+
+/*!
+ \internal
+
+ Given proxy-to-source mapping \a proxy_to_source and a set of
+ unmapped source items \a source_items, determines the proxy item
+ intervals at which the subsets of source items should be inserted
+ (but does not actually add them to the mapping).
+
+ The result is a vector of pairs, each pair representing a tuple (start,
+ items), where items is a vector containing the (sorted) source items that
+ should be inserted at that proxy model location.
+*/
+QList<std::pair<int, QList<int>>> QQmlSortFilterProxyModelPrivate::proxy_intervals_for_source_items_to_add(
+ const QList<int> &proxy_to_source, const QList<int> &source_items,
+ const QModelIndex &source_parent, QSortFilterProxyModelHelper::Direction direction) const
+{
+ Q_Q(const QQmlSortFilterProxyModel);
+ QList<std::pair<int, QList<int>>> proxy_intervals;
+ if (source_items.isEmpty())
+ return proxy_intervals;
+
+ int proxy_low = 0;
+ int proxy_item = 0;
+ int source_items_index = 0;
+ bool compare = (direction == QSortFilterProxyModelHelper::Direction::Rows && m_primarySortColumn >= 0 && m_dynamicSortFilter);
+ while (source_items_index < source_items.size()) {
+ QList<int> source_items_in_interval;
+ int first_new_source_item = source_items.at(source_items_index);
+ source_items_in_interval.append(first_new_source_item);
+ ++source_items_index;
+
+ // Find proxy item at which insertion should be started
+ int proxy_high = proxy_to_source.size() - 1;
+ QModelIndex i1 = compare ? model->index(first_new_source_item, m_primarySortColumn, source_parent) : QModelIndex();
+ while (proxy_low <= proxy_high) {
+ proxy_item = (proxy_low + proxy_high) / 2;
+ if (compare) {
+ QModelIndex i2 = model->index(proxy_to_source.at(proxy_item), m_primarySortColumn, source_parent);
+ if ((m_sortOrder == Qt::AscendingOrder) ? q->lessThan(i1, i2) : q->lessThan(i2, i1))
+ proxy_high = proxy_item - 1;
+ else
+ proxy_low = proxy_item + 1;
+ } else {
+ if (first_new_source_item < proxy_to_source.at(proxy_item))
+ proxy_high = proxy_item - 1;
+ else
+ proxy_low = proxy_item + 1;
+ }
+ }
+ proxy_item = proxy_low;
+
+ // Find the sequence of new source items that should be inserted here
+ if (proxy_item >= proxy_to_source.size()) {
+ for ( ; source_items_index < source_items.size(); ++source_items_index)
+ source_items_in_interval.append(source_items.at(source_items_index));
+ } else {
+ i1 = compare ? model->index(proxy_to_source.at(proxy_item), m_primarySortColumn, source_parent) : QModelIndex();
+ for ( ; source_items_index < source_items.size(); ++source_items_index) {
+ int new_source_item = source_items.at(source_items_index);
+ if (compare) {
+ QModelIndex i2 = model->index(new_source_item, m_primarySortColumn, source_parent);
+ if ((m_sortOrder == Qt::AscendingOrder) ? q->lessThan(i1, i2) : q->lessThan(i2, i1))
+ break;
+ } else {
+ if (proxy_to_source.at(proxy_item) < new_source_item)
+ break;
+ }
+ source_items_in_interval.append(new_source_item);
+ }
+ }
+ // Add interval to result
+ proxy_intervals.emplace_back(proxy_item, std::move(source_items_in_interval));
+ }
+ return proxy_intervals;
+}
+
+bool QQmlSortFilterProxyModelPrivate::needsReorder(const QList<int> &source_rows, const QModelIndex &source_parent) const
+{
+ Q_Q(const QQmlSortFilterProxyModel);
+ Q_ASSERT(m_primarySortColumn != -1);
+ const int proxyRowCount = q->rowCount(source_to_proxy(source_parent));
+ // If any modified proxy row no longer passes lessThan(previous, current)
+ // or lessThan(current, next) then we need to reorder.
+ return std::any_of(source_rows.begin(), source_rows.end(),
+ [this, q, proxyRowCount, source_parent](int sourceRow) -> bool {
+ const QModelIndex sourceIndex = model->index(sourceRow, m_primarySortColumn, source_parent);
+ const QModelIndex proxyIndex = source_to_proxy(sourceIndex);
+ Q_ASSERT(proxyIndex.isValid()); // caller ensured source_rows were not filtered out
+ if (proxyIndex.row() > 0) {
+ const QModelIndex prevProxyIndex = q->sibling(proxyIndex.row() - 1, m_proxySortColumn, proxyIndex);
+ const QModelIndex prevSourceIndex = proxy_to_source(prevProxyIndex);
+ if (m_sortOrder == Qt::AscendingOrder ? q->lessThan(sourceIndex, prevSourceIndex) : q->lessThan(prevSourceIndex, sourceIndex))
+ return true;
+ }
+ if (proxyIndex.row() < proxyRowCount - 1) {
+ const QModelIndex nextProxyIndex = q->sibling(proxyIndex.row() + 1, m_proxySortColumn, proxyIndex);
+ const QModelIndex nextSourceIndex = proxy_to_source(nextProxyIndex);
+ if (m_sortOrder == Qt::AscendingOrder ? q->lessThan(nextSourceIndex, sourceIndex) : q->lessThan(sourceIndex, nextSourceIndex))
+ return true;
+ }
+ return false;
+ });
+}
+
+void QQmlSortFilterProxyModelPrivate::_q_sourceDataChanged(const QModelIndex &source_top_left,
+ const QModelIndex &source_bottom_right,
+ const QList<int> &roles)
+{
+ Q_Q(QQmlSortFilterProxyModel);
+ if (!source_top_left.isValid() || !source_bottom_right.isValid())
+ return;
+
+ std::vector<QSortFilterProxyModelDataChanged> 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 (containRoleForRecursiveFilter(roles)) {
+ 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();
+
+ bool change_in_unmapped_parent = false;
+ QSortFilterProxyModelHelper::IndexMap::const_iterator it = source_index_mapping.constFind(source_parent);
+ if (it == source_index_mapping.constEnd()) {
+ // We don't have mapping for this index, so we cannot know how
+ // things changed (in case the change affects filtering) in order
+ // to forward the change correctly.
+ // But we can at least forward the signal "as is", if the row isn't
+ // filtered out, this is better than nothing.
+ it = create_mapping_recursive(source_parent);
+ if (it == source_index_mapping.constEnd())
+ continue;
+ change_in_unmapped_parent = true;
+ }
+
+ QSortFilterProxyModelHelper::Mapping *m = it.value();
+
+ // Figure out how the source changes affect us
+ QList<int> source_rows_remove;
+ QList<int> source_rows_insert;
+ QList<int> source_rows_change;
+ QList<int> source_rows_resort;
+ int end = qMin(source_bottom_right.row(), m->proxy_rows.size() - 1);
+ for (int source_row = source_top_left.row(); source_row <= end; ++source_row) {
+ if (m_dynamicSortFilter && !change_in_unmapped_parent) {
+ 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 (m_primarySortColumn >= source_top_left.column() && m_primarySortColumn <= 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 {
+ if (!m_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 (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, QSortFilterProxyModelHelper::Direction::Rows);
+ QSet<int> source_rows_remove_set = qListToSet(source_rows_remove);
+ QList<QModelIndex>::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()) {
+ if (needsReorder(source_rows_resort, source_parent)) {
+ // Re-sort the rows of this level
+ QList<QPersistentModelIndex> 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, QSortFilterProxyModelHelper::Direction::Rows, false);
+ sort_source_rows(source_rows_resort, source_parent);
+ insert_source_items(m->proxy_rows, m->source_rows, source_rows_resort,
+ source_parent, QSortFilterProxyModelHelper::Direction::Rows, 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;
+ if (m->proxy_columns.at(source_left_column) != -1) {
+ 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;
+ if (m->proxy_columns.at(source_right_column) != -1) {
+ 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, QSortFilterProxyModelHelper::Direction::Rows);
+ }
+ }
+}
+
+void QQmlSortFilterProxyModelPrivate::_q_sourceHeaderDataChanged(Qt::Orientation orientation,
+ int start, int end)
+{
+ Q_ASSERT(start <= end);
+
+ Q_Q(QQmlSortFilterProxyModel);
+ QSortFilterProxyModelHelper::Mapping *m = create_mapping(QModelIndex()).value();
+
+ const QList<int> &source_to_proxy = (orientation == Qt::Vertical) ? m->proxy_rows : m->proxy_columns;
+
+ QList<int> proxy_positions;
+ proxy_positions.reserve(end - start + 1);
+ {
+ Q_ASSERT(source_to_proxy.size() > end);
+ QList<int>::const_iterator it = source_to_proxy.constBegin() + start;
+ const QList<int>::const_iterator endIt = source_to_proxy.constBegin() + end + 1;
+ for ( ; it != endIt; ++it) {
+ if (*it != -1)
+ proxy_positions.push_back(*it);
+ }
+ }
+
+ std::sort(proxy_positions.begin(), proxy_positions.end());
+
+ int last_index = 0;
+ const int numItems = proxy_positions.size();
+ while (last_index < numItems) {
+ const int proxyStart = proxy_positions.at(last_index);
+ int proxyEnd = proxyStart;
+ ++last_index;
+ for (int i = last_index; i < numItems; ++i) {
+ if (proxy_positions.at(i) == proxyEnd + 1) {
+ ++last_index;
+ ++proxyEnd;
+ } else {
+ break;
+ }
+ }
+ emit q->headerDataChanged(orientation, proxyStart, proxyEnd);
+ }
+}
+
+void QQmlSortFilterProxyModelPrivate::_q_sourceAboutToBeReset()
+{
+ Q_Q(QQmlSortFilterProxyModel);
+ q->beginResetModel();
+}
+
+void QQmlSortFilterProxyModelPrivate::_q_sourceReset()
+{
+ Q_Q(QQmlSortFilterProxyModel);
+ invalidatePersistentIndexes();
+ _q_clearMapping();
+ // All internal structures are deleted in clear()
+ q->endResetModel();
+ if (updatePrimaryColumn() && m_dynamicSortFilter)
+ sort();
+}
+
+void QQmlSortFilterProxyModelPrivate::_q_sourceLayoutAboutToBeChanged(const QList<QPersistentModelIndex> &sourceParents, QAbstractItemModel::LayoutChangeHint hint)
+{
+ Q_Q(QQmlSortFilterProxyModel);
+ Q_UNUSED(hint); // We can't forward Hint because we might filter additional rows or columns
+ m_savedPersistentIndexes.clear();
+
+ m_savedLayoutChangeParents.clear();
+ for (const QPersistentModelIndex &parent : sourceParents) {
+ if (!parent.isValid()) {
+ m_savedLayoutChangeParents << QPersistentModelIndex();
+ continue;
+ }
+ const QModelIndex mappedParent = q->mapFromSource(parent);
+ // Might be filtered out.
+ if (mappedParent.isValid())
+ m_savedLayoutChangeParents << mappedParent;
+ }
+
+ // All parents filtered out.
+ if (!sourceParents.isEmpty() && m_savedLayoutChangeParents.isEmpty())
+ return;
+
+ emit q->layoutAboutToBeChanged(m_savedLayoutChangeParents);
+ if (persistent.indexes.isEmpty())
+ return;
+
+ m_savedPersistentIndexes = store_persistent_indexes();
+}
+
+void QQmlSortFilterProxyModelPrivate::_q_sourceLayoutChanged(const QList<QPersistentModelIndex> &sourceParents, QAbstractItemModel::LayoutChangeHint hint)
+{
+ Q_Q(QQmlSortFilterProxyModel);
+ Q_UNUSED(hint); // We can't forward Hint because we might filter additional rows or columns
+
+ if (!sourceParents.isEmpty() && m_savedLayoutChangeParents.isEmpty())
+ return;
+
+ // Optimize: We only actually have to clear the mapping related to the
+ // contents of sourceParents, not everything.
+ clearSourceIndexMapping();
+
+ update_persistent_indexes(m_savedPersistentIndexes);
+ m_savedPersistentIndexes.clear();
+
+ if (m_dynamicSortFilter)
+ m_primarySortColumn = findPrimarySortColumn();
+
+ emit q->layoutChanged(m_savedLayoutChangeParents);
+ m_savedLayoutChangeParents.clear();
+}
+
+void QQmlSortFilterProxyModelPrivate::_q_sourceRowsAboutToBeInserted(
+ const QModelIndex &source_parent, int start, int end)
+{
+ Q_UNUSED(start);
+ Q_UNUSED(end);
+
+ const bool toplevel = !source_parent.isValid();
+ const bool recursive_accepted = m_recursiveFiltering && !toplevel && filterAcceptsRowInternal(source_parent.row(), source_parent.parent());
+ // Force the creation of a mapping now, even if it's empty.
+ // We need it because the proxy can be accessed at the moment it emits
+ // rowsAboutToBeInserted in insert_source_items
+ if (!m_recursiveFiltering || toplevel || recursive_accepted) {
+ if (can_create_mapping(source_parent))
+ create_mapping(source_parent);
+ if (m_recursiveFiltering)
+ m_completeInsert = 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();
+ }
+
+ m_lastTopSource = top_source_parent;
+ }
+}
+
+void QQmlSortFilterProxyModelPrivate::_q_sourceRowsInserted(
+ const QModelIndex &source_parent, int start, int end)
+{
+ if (!m_recursiveFiltering || m_completeInsert) {
+ if (m_recursiveFiltering)
+ m_completeInsert = false;
+ source_items_inserted(source_parent, start, end, QSortFilterProxyModelHelper::Direction::Rows);
+ if (updatePrimaryColumn() && m_dynamicSortFilter) //previous call to updatePrimaryColumn may fail if the model has no column.
+ sort(); // now it should succeed so we need to make sure to sort again
+ return;
+ }
+ if (m_recursiveFiltering) {
+ 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;
+ // m_lastTopSource should now become visible
+ _q_sourceDataChanged(m_lastTopSource, m_lastTopSource, QList<int>());
+ }
+}
+
+void QQmlSortFilterProxyModelPrivate::_q_sourceRowsAboutToBeRemoved(
+ const QModelIndex &source_parent, int start, int end)
+{
+ m_itemsBeingRemoved = QRowsRemoval(source_parent, start, end);
+ source_items_about_to_be_removed(source_parent, start, end,
+ QSortFilterProxyModelHelper::Direction::Rows);
+}
+
+void QQmlSortFilterProxyModelPrivate::_q_sourceRowsRemoved(
+ const QModelIndex &source_parent, int start, int end)
+{
+ m_itemsBeingRemoved = QRowsRemoval();
+ source_items_removed(source_parent, start, end, QSortFilterProxyModelHelper::Direction::Rows);
+
+ if (m_recursiveFiltering) {
+ // 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, QList<int>());
+ }
+}
+
+void QQmlSortFilterProxyModelPrivate::_q_sourceRowsAboutToBeMoved(
+ const QModelIndex &sourceParent, int /* sourceStart */, int /* sourceEnd */, const QModelIndex &destParent, int /* dest */)
+{
+ // Because rows which are contiguous in the source model might not be
+ // contiguous in the proxy due to sorting, the best thing we can do here is
+ // be specific about what parents are having their children changed.
+ // Optimize: Emit move signals if the proxy is not sorted. Will need to
+ // account for rows being filtered out though.
+ QList<QPersistentModelIndex> parents;
+ parents << sourceParent;
+ if (sourceParent != destParent)
+ parents << destParent;
+ _q_sourceLayoutAboutToBeChanged(parents, QAbstractItemModel::NoLayoutChangeHint);
+}
+
+void QQmlSortFilterProxyModelPrivate::_q_sourceRowsMoved(
+ const QModelIndex &sourceParent, int /* sourceStart */, int /* sourceEnd */, const QModelIndex &destParent, int /* dest */)
+{
+ QList<QPersistentModelIndex> parents;
+ parents << sourceParent;
+ if (sourceParent != destParent)
+ parents << destParent;
+ _q_sourceLayoutChanged(parents, QAbstractItemModel::NoLayoutChangeHint);
+}
+
+void QQmlSortFilterProxyModelPrivate::_q_sourceColumnsAboutToBeInserted(
+ const QModelIndex &source_parent, int start, int end)
+{
+ Q_UNUSED(start);
+ Q_UNUSED(end);
+ // Force the creation of a mapping now, even if it's empty.
+ // We need it because the proxy can be accessed at the moment it emits
+ // columnsAboutToBeInserted in insert_source_items
+ if (can_create_mapping(source_parent))
+ create_mapping(source_parent);
+}
+
+void QQmlSortFilterProxyModelPrivate::_q_sourceColumnsInserted(
+ const QModelIndex &source_parent, int start, int end)
+{
+ Q_Q(const QQmlSortFilterProxyModel);
+ source_items_inserted(source_parent, start, end, QSortFilterProxyModelHelper::Direction::Columns);
+
+ if (source_parent.isValid())
+ return; //we sort according to the root column only
+ if (m_primarySortColumn == -1) {
+ //we update the primayColumn depending on the m_proxySortColumn
+ if (updatePrimaryColumn() && m_dynamicSortFilter)
+ sort();
+ } else {
+ if (start <= m_primarySortColumn)
+ m_primarySortColumn += end - start + 1;
+
+ m_proxySortColumn = q->mapFromSource(model->index(0,m_primarySortColumn, source_parent)).column();
+ }
+}
+
+void QQmlSortFilterProxyModelPrivate::_q_sourceColumnsAboutToBeRemoved(
+ const QModelIndex &source_parent, int start, int end)
+{
+ source_items_about_to_be_removed(source_parent, start, end,
+ QSortFilterProxyModelHelper::Direction::Columns);
+}
+
+void QQmlSortFilterProxyModelPrivate::_q_sourceColumnsRemoved(
+ const QModelIndex &source_parent, int start, int end)
+{
+ Q_Q(const QQmlSortFilterProxyModel);
+ source_items_removed(source_parent, start, end, QSortFilterProxyModelHelper::Direction::Columns);
+
+ if (source_parent.isValid())
+ return; //we sort according to the root column only
+ if (start <= m_primarySortColumn) {
+ if (end < m_primarySortColumn)
+ m_primarySortColumn -= end - start + 1;
+ else
+ m_primarySortColumn = -1;
+ }
+
+ if (m_primarySortColumn >= 0)
+ m_proxySortColumn = q->mapFromSource(model->index(0,m_primarySortColumn, source_parent)).column();
+ else
+ m_proxySortColumn = -1;
+}
+
+void QQmlSortFilterProxyModelPrivate::_q_sourceColumnsAboutToBeMoved(
+ const QModelIndex &sourceParent, int /* sourceStart */, int /* sourceEnd */, const QModelIndex &destParent, int /* dest */)
+{
+ QList<QPersistentModelIndex> parents;
+ parents << sourceParent;
+ if (sourceParent != destParent)
+ parents << destParent;
+ _q_sourceLayoutAboutToBeChanged(parents, QAbstractItemModel::NoLayoutChangeHint);
+}
+
+void QQmlSortFilterProxyModelPrivate::_q_sourceColumnsMoved(
+ const QModelIndex &sourceParent, int /* sourceStart */, int /* sourceEnd */, const QModelIndex &destParent, int /* dest */)
+{
+ QList<QPersistentModelIndex> parents;
+ parents << sourceParent;
+ if (sourceParent != destParent)
+ parents << destParent;
+ _q_sourceLayoutChanged(parents, QAbstractItemModel::NoLayoutChangeHint);
+}
+
+/*!
+ \internal
+ */
+bool QQmlSortFilterProxyModelPrivate::containRoleForRecursiveFilter(const QList<int> &roles) const
+{
+ if (!model || roles.isEmpty())
+ return false;
+ // Get role names for the provided roles (roles arg.)
+ const QHash<int, QByteArray> &roleNames = model->roleNames();
+ QList<QString> filterRoles;
+ std::for_each(roles.constBegin(), roles.constEnd(), [&roleNames, &filterRoles](const int role){
+ if (roleNames.contains(role))
+ filterRoles.append(QString::fromUtf8(roleNames[role]));
+ });
+
+ // Check if the configured role filter matches with the provided roles
+ bool filterRoleConfigured = false;
+ if (!filterRoles.isEmpty() && (m_filters && m_filters->filters().isEmpty())) {
+ const QList<QQmlFilterBase *> proxyFilters = m_filters->filters();
+ filterRoleConfigured = std::any_of(proxyFilters.constBegin(), proxyFilters.constEnd(), [&filterRoles](QQmlFilterBase* filterComp) {
+ if (const auto *roleFilter = qobject_cast<QQmlRoleFilter *>(filterComp))
+ return filterRoles.contains(roleFilter->roleName());
+ return true;
+ });
+ }
+ return m_recursiveFiltering && filterRoleConfigured;
+}
+
+/*!
+ \internal
+ */
+bool QQmlSortFilterProxyModelPrivate::filterAcceptsRowInternal(int row, const QModelIndex &sourceIndex) const
+{
+ Q_Q(const QQmlSortFilterProxyModel);
+ const bool retVal = (q->filterAcceptsRow(row, sourceIndex) ||
+ (m_autoAcceptChildRows && recursiveParentAcceptsRow(sourceIndex)) ||
+ (m_recursiveFiltering && recursiveChildAcceptsRow(row, sourceIndex)));
+ return retVal;
+}
+
+/*!
+ \internal
+ */
+bool QQmlSortFilterProxyModelPrivate::filterAcceptsColumnInternal(int row, const QModelIndex &sourceIndex) const
+{
+ Q_Q(const QQmlSortFilterProxyModel);
+ return q->filterAcceptsColumn(row, sourceIndex);
+}
+
+QT_END_NAMESPACE
+
+#include "moc_qqmlsortfilterproxymodel_p.cpp"
diff --git a/src/qmlmodels/sfpm/qqmlsortfilterproxymodel_p.h b/src/qmlmodels/sfpm/qqmlsortfilterproxymodel_p.h
new file mode 100644
index 0000000000..5361da7fb7
--- /dev/null
+++ b/src/qmlmodels/sfpm/qqmlsortfilterproxymodel_p.h
@@ -0,0 +1,141 @@
+// Copyright (C) 2025 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#ifndef QQMLSORTFILTERPROXYMODEL_H
+#define QQMLSORTFILTERPROXYMODEL_H
+
+//
+// W A R N I N G
+// -------------
+//
+// This file is not part of the Qt API. It exists purely as an
+// implementation detail. This header file may change from version to
+// version without notice, or even be removed.
+//
+// We mean it.
+//
+
+#include <QtQml/private/qqmlcustomparser_p.h>
+#include <QtQmlModels/private/qtqmlmodelsglobal_p.h>
+#include <QtQmlModels/private/qqmlsorterbase_p.h>
+#include <QtQmlModels/private/qqmlfilterbase_p.h>
+#include <QtQmlModels/private/qsortfilterproxymodelhelper_p.h>
+
+QT_REQUIRE_CONFIG(qml_sfpm_model);
+
+QT_BEGIN_NAMESPACE
+
+class QQmlFilterBase;
+class QQmlFilterCompositor;
+class QQmlSorterBase;
+class QQmlSorterCompositor;
+class QQmlSortFilterProxyModelPrivate;
+class QSortFilterProxyModelLessThan;
+class QSortFilterProxyModelGreaterThan;
+
+class Q_QMLMODELS_EXPORT QQmlSortFilterProxyModel : public QAbstractProxyModel, public QQmlParserStatus
+{
+ Q_OBJECT
+ Q_INTERFACES(QQmlParserStatus)
+
+ Q_PROPERTY(QQmlListProperty<QQmlFilterBase> filters READ filters NOTIFY filtersChanged FINAL)
+ Q_PROPERTY(QQmlListProperty<QQmlSorterBase> sorters READ sorters NOTIFY sortersChanged FINAL)
+ Q_PROPERTY(QVariant model READ model WRITE setModel NOTIFY modelChanged FINAL)
+ Q_PROPERTY(bool dynamicSortFilter READ dynamicSortFilter WRITE setDynamicSortFilter NOTIFY dynamicSortFilterChanged FINAL)
+ Q_PROPERTY(bool recursiveFiltering READ recursiveFiltering WRITE setRecursiveFiltering NOTIFY recursiveFilteringChanged FINAL)
+ Q_PROPERTY(bool autoAcceptChildRows READ autoAcceptChildRows WRITE setAutoAcceptChildRows NOTIFY autoAcceptChildRowsChanged FINAL)
+
+ QML_NAMED_ELEMENT(SortFilterProxyModel)
+ QML_ADDED_IN_VERSION(6, 10)
+
+public:
+ explicit QQmlSortFilterProxyModel(QObject *parent = nullptr);
+ ~QQmlSortFilterProxyModel() override;
+
+ // Provides configured filters in this model
+ QQmlListProperty<QQmlFilterBase> filters();
+ // Provides configured sorters in this model
+ QQmlListProperty<QQmlSorterBase> sorters();
+
+ bool dynamicSortFilter() const;
+ void setDynamicSortFilter(const bool enabled);
+
+ bool recursiveFiltering() const;
+ void setRecursiveFiltering(const bool enabled);
+
+ bool autoAcceptChildRows() const;
+ void setAutoAcceptChildRows(const bool enabled);
+
+ QVariant model() const;
+ void setModel(QVariant &sourceModel);
+
+ Q_INVOKABLE void invalidate();
+ Q_INVOKABLE void invalidateSorter();
+ Q_INVOKABLE void setPrimarySorter(QQmlSorterBase *sorter);
+
+ Q_INVOKABLE QModelIndex mapToSource(const QModelIndex& proxyIndex) const override;
+ Q_INVOKABLE QModelIndex mapFromSource(const QModelIndex& sourceIndex) const override;
+
+ // Reimplemented methods
+ QModelIndex index(int row, int column, const QModelIndex &parent) const override;
+ QModelIndex parent(const QModelIndex &child) const override;
+ QModelIndex sibling(int row, int column, const QModelIndex &idx) const override;
+
+ bool hasChildren(const QModelIndex &parent = QModelIndex()) const override;
+ int columnCount(const QModelIndex &parent = QModelIndex()) const override;
+ int rowCount(const QModelIndex &parent = QModelIndex()) const override;
+
+ QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
+ bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override;
+
+ QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
+ bool setHeaderData(int section, Qt::Orientation orientation,
+ const QVariant &value, int role = Qt::EditRole) override;
+
+ bool insertRows(int row, int count, const QModelIndex &parent = QModelIndex()) override;
+ bool insertColumns(int column, int count, const QModelIndex &parent = QModelIndex()) override;
+ bool removeRows(int row, int count, const QModelIndex &parent = QModelIndex()) override;
+ bool removeColumns(int column, int count, const QModelIndex &parent = QModelIndex()) override;
+
+ QItemSelection mapSelectionToSource(const QItemSelection &proxySelection) const override;
+ QItemSelection mapSelectionFromSource(const QItemSelection &sourceSelection) const override;
+
+ // Internal methods
+ void setPrimarySortColumn(const int column);
+ void setPrimarySortOrder(const Qt::SortOrder sortOrder);
+ QVariant sourceData(const QModelIndex &sourceIndex, int role) const;
+ QHash<int, QByteArray> roleNames() const override;
+ int itemRoleForName(const QString& roleName) const;
+
+protected:
+ bool filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const ;
+ bool filterAcceptsColumn(int sourceColumn, const QModelIndex& sourceParent) const ;
+ bool lessThan(const QModelIndex &sourceLeft, const QModelIndex &sourceRight) const ;
+ void sort(int column = 0, Qt::SortOrder sortOrder = Qt::AscendingOrder) override;
+
+private:
+ void classBegin() override {};
+ void componentComplete() override;
+ void setSourceModel(QAbstractItemModel *sourceModel) override;
+ void invalidateFilter();
+
+Q_SIGNALS:
+ void dynamicSortFilterChanged();
+ void recursiveFilteringChanged();
+ void autoAcceptChildRowsChanged();
+ void filtersChanged();
+ void sortersChanged();
+ void modelChanged();
+ void primarySorterChanged();
+
+private:
+ Q_DISABLE_COPY(QQmlSortFilterProxyModel)
+ Q_DECLARE_PRIVATE(QQmlSortFilterProxyModel)
+
+ friend class QSortFilterProxyModelLessThan;
+ friend class QSortFilterProxyModelGreaterThan;
+};
+
+QT_END_NAMESPACE
+
+#endif // QQMLSORTFILTERPROXYMODEL_H
diff --git a/src/qmlmodels/sfpm/qsortfilterproxymodelhelper.cpp b/src/qmlmodels/sfpm/qsortfilterproxymodelhelper.cpp
new file mode 100644
index 0000000000..026931bcf8
--- /dev/null
+++ b/src/qmlmodels/sfpm/qsortfilterproxymodelhelper.cpp
@@ -0,0 +1,760 @@
+// Copyright (C) 2025 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#include <QtQmlModels/private/qsortfilterproxymodelhelper_p.h>
+#include <QtQmlModels/private/qqmlsortfilterproxymodel_p.h>
+
+QT_BEGIN_NAMESPACE
+
+using IndexMap = QSortFilterProxyModelHelper::IndexMap;
+
+
+QSortFilterProxyModelHelper::QSortFilterProxyModelHelper()
+{
+
+}
+
+QSortFilterProxyModelHelper::~QSortFilterProxyModelHelper()
+{
+ clearSourceIndexMapping();
+}
+
+template<typename F>
+void forEachProperty(
+ const QMetaObject *metaObject, const QModelIndex &sourceIndex,
+ const QQmlSortFilterProxyModel *proxyModel, F &&handler)
+{
+ for (int i = 0, end = metaObject->propertyCount(); i != end; ++i) {
+ const QMetaProperty property = metaObject->property(i);
+ const int roleId = proxyModel->itemRoleForName(QString::fromUtf8(property.name()));
+ if (roleId < 0)
+ continue;
+
+ handler(property, proxyModel->sourceModel()->data(sourceIndex, roleId));
+ }
+}
+
+void QSortFilterProxyModelHelper::setProperties(
+ QVariant *target, const QQmlSortFilterProxyModel *proxyModel,
+ const QModelIndex &sourceIndex)
+{
+ const QMetaType metaType = target->metaType();
+
+ if (metaType.flags() & QMetaType::PointerToQObject) {
+ QObject *object = target->value<QObject *>();
+ forEachProperty(
+ object->metaObject(), sourceIndex, proxyModel,
+ [&](const QMetaProperty &property, const QVariant &value) {
+ property.write(object, value);
+ });
+ return;
+ }
+
+ const QMetaObject *metaObject = QQmlMetaType::metaObjectForValueType(metaType);
+ if (!metaObject)
+ return;
+
+ forEachProperty(
+ metaObject, sourceIndex, proxyModel,
+ [&](const QMetaProperty &property, const QVariant &value) {
+ property.writeOnGadget(target->data(), value);
+ });
+}
+
+void QSortFilterProxyModelHelper::clearSourceIndexMapping()
+{
+ qDeleteAll(source_index_mapping);
+ source_index_mapping.clear();
+}
+
+IndexMap::const_iterator QSortFilterProxyModelHelper::create_mapping(
+ const QModelIndex &source_parent) const
+{
+ IndexMap::const_iterator it = source_index_mapping.constFind(source_parent);
+ if (it != source_index_mapping.constEnd()) // was mapped already
+ return it;
+
+ auto *model = proxyModel()->sourceModel();
+ Q_ASSERT(model != nullptr);
+
+ Mapping *m = new Mapping;
+
+ int source_rows = model->rowCount(source_parent);
+ m->source_rows.reserve(source_rows);
+ for (int i = 0; i < source_rows; ++i) {
+ if (filterAcceptsRowInternal(i, source_parent))
+ m->source_rows.append(i);
+ }
+ int source_cols = model->columnCount(source_parent);
+ m->source_columns.reserve(source_cols);
+ for (int i = 0; i < source_cols; ++i) {
+ if (filterAcceptsColumn(i, source_parent))
+ m->source_columns.append(i);
+ }
+
+ sort_source_rows(m->source_rows, source_parent);
+ m->proxy_rows.resize(source_rows);
+ build_source_to_proxy_mapping(m->source_rows, m->proxy_rows);
+ m->proxy_columns.resize(source_cols);
+ build_source_to_proxy_mapping(m->source_columns, m->proxy_columns);
+
+ m->source_parent = source_parent;
+
+ if (source_parent.isValid()) {
+ QModelIndex source_grand_parent = source_parent.parent();
+ IndexMap::const_iterator it2 = create_mapping(source_grand_parent);
+ Q_ASSERT(it2 != source_index_mapping.constEnd());
+ it2.value()->mapped_children.append(source_parent);
+ }
+
+ it = IndexMap::const_iterator(source_index_mapping.insert(source_parent, m));
+ Q_ASSERT(it != source_index_mapping.constEnd());
+ Q_ASSERT(it.value());
+
+ return it;
+}
+
+QSortFilterProxyModelHelper::IndexMap::const_iterator QSortFilterProxyModelHelper::create_mapping_recursive(
+ const QModelIndex &source_parent) const
+{
+ if (source_parent.isValid()) {
+ const QModelIndex source_grand_parent = source_parent.parent();
+ IndexMap::const_iterator it = source_index_mapping.constFind(source_grand_parent);
+ IndexMap::const_iterator end = source_index_mapping.constEnd();
+ if (it == end) {
+ it = create_mapping_recursive(source_grand_parent);
+ end = source_index_mapping.constEnd();
+ if (it == end)
+ return end;
+ }
+ Mapping *gm = it.value();
+ if (gm->proxy_rows.at(source_parent.row()) == -1 ||
+ gm->proxy_columns.at(source_parent.column()) == -1) {
+ // Can't do, parent is filtered
+ return end;
+ }
+ }
+ return create_mapping(source_parent);
+}
+
+/*!
+ \internal
+
+ updates the mapping of the children when inserting or removing items
+*/
+void QSortFilterProxyModelHelper::updateChildrenMapping(const QModelIndex &source_parent, Mapping *parent_mapping,
+ Direction direction, int start, int end, int delta_item_count, bool remove)
+{
+ // see if any mapped children should be (re)moved
+ QList<std::pair<QModelIndex, Mapping *>> moved_source_index_mappings;
+ auto it2 = parent_mapping->mapped_children.begin();
+ for ( ; it2 != parent_mapping->mapped_children.end();) {
+ const QModelIndex source_child_index = *it2;
+ const int pos = (direction == Direction::Rows)
+ ? source_child_index.row()
+ : source_child_index.column();
+ if (pos < start) {
+ // not affected
+ ++it2;
+ } else if (remove && pos <= end) {
+ // in the removed interval
+ it2 = parent_mapping->mapped_children.erase(it2);
+ remove_from_mapping(source_child_index);
+ } else {
+ // below the removed items -- recompute the index
+ QModelIndex new_index;
+ auto model = proxyModel()->sourceModel();
+ const int newpos = remove ? pos - delta_item_count : pos + delta_item_count;
+ if (direction == Direction::Rows) {
+ new_index = model->index(newpos,
+ source_child_index.column(),
+ source_parent);
+ } else {
+ new_index = model->index(source_child_index.row(),
+ newpos,
+ source_parent);
+ }
+ *it2 = new_index;
+ ++it2;
+ // update mapping
+ Mapping *cm = source_index_mapping.take(source_child_index);
+ Q_ASSERT(cm);
+ // we do not reinsert right away, because the new index might be identical with another, old index
+ moved_source_index_mappings.emplace_back(new_index, cm);
+ }
+ }
+
+ // reinsert moved, mapped indexes
+ for (auto &pair : std::as_const(moved_source_index_mappings)) {
+ pair.second->source_parent = pair.first;
+ source_index_mapping.insert(pair.first, pair.second);
+ }
+}
+
+QModelIndex QSortFilterProxyModelHelper::source_to_proxy(const QModelIndex &source_index) const
+{
+ if (!source_index.isValid())
+ return QModelIndex(); // for now; we may want to be able to set a root index later
+ if (source_index.model() != proxyModel()->sourceModel()) {
+ qWarning("QSortFilterProxyModel: index from wrong model passed to mapFromSource");
+ Q_ASSERT(!"QSortFilterProxyModel: index from wrong model passed to mapFromSource");
+ return QModelIndex();
+ }
+ QModelIndex source_parent = source_index.parent();
+ IndexMap::const_iterator it = create_mapping(source_parent);
+ Mapping *m = it.value();
+ if ((source_index.row() >= m->proxy_rows.size()) || (source_index.column() >= m->proxy_columns.size()))
+ return QModelIndex();
+ int proxy_row = m->proxy_rows.at(source_index.row());
+ int proxy_column = m->proxy_columns.at(source_index.column());
+ if (proxy_row == -1 || proxy_column == -1)
+ return QModelIndex();
+ return createIndex(proxy_row, proxy_column, it);
+}
+
+QModelIndex QSortFilterProxyModelHelper::proxy_to_source(const QModelIndex &proxy_index) const
+{
+ if (!proxy_index.isValid())
+ return QModelIndex(); // for now; we may want to be able to set a root index later
+ if (proxy_index.model() != proxyModel()) {
+ qWarning("QSortFilterProxyModel: index from wrong model passed to mapToSource");
+ Q_ASSERT(!"QSortFilterProxyModel: index from wrong model passed to mapToSource");
+ return QModelIndex();
+ }
+ IndexMap::const_iterator it = index_to_iterator(proxy_index);
+ Mapping *m = it.value();
+ if ((proxy_index.row() >= m->source_rows.size()) || (proxy_index.column() >= m->source_columns.size()))
+ return QModelIndex();
+ int source_row = m->source_rows.at(proxy_index.row());
+ int source_col = m->source_columns.at(proxy_index.column());
+ auto *model = proxyModel()->sourceModel();
+ return model->index(source_row, source_col, it.key());
+}
+
+void QSortFilterProxyModelHelper::build_source_to_proxy_mapping(
+ QList<int> &proxy_to_source, QList<int> &source_to_proxy, int start) const
+{
+ if (start == 0)
+ source_to_proxy.fill(-1);
+ const int proxy_count = proxy_to_source.size();
+ for (int i = start; i < proxy_count; ++i)
+ source_to_proxy[proxy_to_source.at(i)] = i;
+}
+
+bool QSortFilterProxyModelHelper::can_create_mapping(const QModelIndex &source_parent) const
+{
+ if (source_parent.isValid()) {
+ QModelIndex source_grand_parent = source_parent.parent();
+ IndexMap::const_iterator it = source_index_mapping.constFind(source_grand_parent);
+ if (it == source_index_mapping.constEnd()) {
+ // Don't care, since we don't have mapping for the grand parent
+ return false;
+ }
+ Mapping *gm = it.value();
+ if (gm->proxy_rows.at(source_parent.row()) == -1 ||
+ gm->proxy_columns.at(source_parent.column()) == -1) {
+ // Don't care, since parent is filtered
+ return false;
+ }
+ }
+ return true;
+}
+
+void QSortFilterProxyModelHelper::remove_from_mapping(const QModelIndex &source_parent)
+{
+ if (Mapping *m = source_index_mapping.take(source_parent)) {
+ for (const QModelIndex &mappedIdx : std::as_const(m->mapped_children))
+ remove_from_mapping(mappedIdx);
+ delete m;
+ }
+}
+
+void QSortFilterProxyModelHelper::proxy_item_range(const QList<int> &source_to_proxy,
+ const QList<int> &source_items, int &proxy_low, int &proxy_high) const
+{
+ proxy_low = INT_MAX;
+ proxy_high = INT_MIN;
+ for (int i = 0; i < source_items.size(); ++i) {
+ int proxy_item = source_to_proxy.at(source_items.at(i));
+ Q_ASSERT(proxy_item != -1);
+ if (proxy_item < proxy_low)
+ proxy_low = proxy_item;
+ if (proxy_item > proxy_high)
+ proxy_high = proxy_item;
+ }
+}
+
+
+QModelIndexPairList QSortFilterProxyModelHelper::store_persistent_indexes() const
+{
+ QModelIndexPairList source_indexes;
+ auto *proxyPriv = QAbstractProxyModelPrivate::get(proxyModel());
+ source_indexes.reserve(proxyPriv->persistent.indexes.size());
+ for (const QPersistentModelIndexData *data : std::as_const(proxyPriv->persistent.indexes)) {
+ const QModelIndex &proxy_index = data->index;
+ const QModelIndex source_index = proxyModel()->mapToSource(proxy_index);
+ source_indexes.emplace_back(proxy_index, source_index);
+ }
+ return source_indexes;
+}
+
+void QSortFilterProxyModelHelper::update_persistent_indexes(const QModelIndexPairList &source_indexes)
+{
+ QModelIndexList from, to;
+ const int numSourceIndexes = source_indexes.size();
+ from.reserve(numSourceIndexes);
+ to.reserve(numSourceIndexes);
+ for (const auto &indexPair : source_indexes) {
+ const QPersistentModelIndex &source_index = indexPair.second;
+ const QModelIndex &old_proxy_index = indexPair.first;
+ create_mapping(source_index.parent());
+ const QModelIndex proxy_index = proxyModel()->mapFromSource(source_index);
+ from << old_proxy_index;
+ to << proxy_index;
+ }
+ changePersistentIndexList(from, to);
+}
+
+/*!
+ \internal
+
+ Given source-to-proxy mapping \a source_to_proxy and the set of
+ source items \a source_items (which are part of that mapping),
+ determines the corresponding proxy item intervals that should
+ be removed from the proxy model.
+
+ The result is a vector of pairs, where each pair represents a
+ (start, end) tuple, sorted in ascending order.
+*/
+QList<std::pair<int, int>> QSortFilterProxyModelHelper::proxy_intervals_for_source_items(
+ const QList<int> &source_to_proxy, const QList<int> &source_items) const
+{
+ QList<std::pair<int, int>> proxy_intervals;
+ if (source_items.isEmpty())
+ return proxy_intervals;
+
+ int source_items_index = 0;
+ while (source_items_index < source_items.size()) {
+ int first_proxy_item = source_to_proxy.at(source_items.at(source_items_index));
+ Q_ASSERT(first_proxy_item != -1);
+ int last_proxy_item = first_proxy_item;
+ ++source_items_index;
+ // Find end of interval
+ while ((source_items_index < source_items.size())
+ && (source_to_proxy.at(source_items.at(source_items_index)) == last_proxy_item + 1)) {
+ ++last_proxy_item;
+ ++source_items_index;
+ }
+ // Add interval to result
+ proxy_intervals.emplace_back(first_proxy_item, last_proxy_item);
+ }
+ std::stable_sort(proxy_intervals.begin(), proxy_intervals.end());
+ // Consolidate adjacent intervals
+ for (int i = proxy_intervals.size()-1; i > 0; --i) {
+ std::pair<int, int> &interval = proxy_intervals[i];
+ std::pair<int, int> &preceeding_interval = proxy_intervals[i - 1];
+ if (interval.first == preceeding_interval.second + 1) {
+ preceeding_interval.second = interval.second;
+ interval.first = interval.second = -1;
+ }
+ }
+ proxy_intervals.removeIf([](std::pair<int, int> interval) { return interval.first < 0; });
+ return proxy_intervals;
+}
+
+/*!
+ \internal
+
+ Given source-to-proxy mapping \a source_to_proxy and proxy-to-source mapping
+ \a proxy_to_source, removes items from \a proxy_start to \a proxy_end
+ (inclusive) from this proxy model.
+*/
+void QSortFilterProxyModelHelper::remove_proxy_interval(
+ QList<int> &source_to_proxy, QList<int> &proxy_to_source, int proxy_start, int proxy_end,
+ const QModelIndex &proxy_parent, Direction direction, bool emit_signal)
+{
+ if (emit_signal) {
+ if (direction == Direction::Rows)
+ beginRemoveRows(proxy_parent, proxy_start, proxy_end);
+ else
+ beginRemoveColumns(proxy_parent, proxy_start, proxy_end);
+ }
+ // Remove items from proxy-to-source mapping
+ for (int i = proxy_start; i <= proxy_end; ++i)
+ source_to_proxy[proxy_to_source.at(i)] = -1;
+ proxy_to_source.remove(proxy_start, proxy_end - proxy_start + 1);
+ build_source_to_proxy_mapping(proxy_to_source, source_to_proxy, proxy_start);
+ if (emit_signal) {
+ if (direction == Direction::Rows)
+ endRemoveRows();
+ else
+ endRemoveColumns();
+ }
+}
+
+/*!
+ \internal
+
+ Handles source model items insertion (columnsInserted(), rowsInserted()).
+ Determines
+ 1) which of the inserted items to also insert into proxy model (filtering),
+ 2) where to insert the items into the proxy model (sorting), then inserts
+ those items.
+ The items are inserted into the proxy model in intervals (based on
+ sorted order), so that the proper rows/columnsInserted(start, end)
+ signals will be generated.
+*/
+void QSortFilterProxyModelHelper::source_items_inserted(
+ const QModelIndex &source_parent, int start, int end, Direction direction)
+{
+ if ((start < 0) || (end < 0))
+ return;
+ QSortFilterProxyModelHelper::IndexMap::const_iterator it = source_index_mapping.constFind(source_parent);
+ if (it == source_index_mapping.constEnd()) {
+ if (!can_create_mapping(source_parent))
+ return;
+ it = create_mapping(source_parent);
+ Mapping *m = it.value();
+ QModelIndex proxy_parent = proxyModel()->mapFromSource(source_parent);
+ if (m->source_rows.size() > 0) {
+ beginInsertRows(proxy_parent, 0, m->source_rows.size() - 1);
+ endInsertRows();
+ }
+ if (m->source_columns.size() > 0) {
+ beginInsertColumns(proxy_parent, 0, m->source_columns.size() - 1);
+ endInsertColumns();
+ }
+ return;
+ }
+
+ Mapping *m = it.value();
+ QList<int> &source_to_proxy = (direction == Direction::Rows) ? m->proxy_rows : m->proxy_columns;
+ QList<int> &proxy_to_source = (direction == Direction::Rows) ? m->source_rows : m->source_columns;
+
+ int delta_item_count = end - start + 1;
+ int old_item_count = source_to_proxy.size();
+
+ updateChildrenMapping(source_parent, m, direction, start, end, delta_item_count, false);
+
+ // Expand source-to-proxy mapping to account for new items
+ if (start < 0 || start > source_to_proxy.size()) {
+ qWarning("QSortFilterProxyModel: invalid inserted rows reported by source model");
+ remove_from_mapping(source_parent);
+ return;
+ }
+ source_to_proxy.insert(start, delta_item_count, -1);
+
+ if (start < old_item_count) {
+ // Adjust existing "stale" indexes in proxy-to-source mapping
+ int proxy_count = proxy_to_source.size();
+ for (int proxy_item = 0; proxy_item < proxy_count; ++proxy_item) {
+ int source_item = proxy_to_source.at(proxy_item);
+ if (source_item >= start)
+ proxy_to_source.replace(proxy_item, source_item + delta_item_count);
+ }
+ build_source_to_proxy_mapping(proxy_to_source, source_to_proxy);
+ }
+
+ // Figure out which items to add to mapping based on filter
+ QList<int> source_items;
+ for (int i = start; i <= end; ++i) {
+ if ((direction == Direction::Rows)
+ ? filterAcceptsRowInternal(i, source_parent)
+ : filterAcceptsColumn(i, source_parent)) {
+ source_items.append(i);
+ }
+ }
+
+ auto model = proxyModel()->sourceModel();
+ if (model->rowCount(source_parent) == delta_item_count) {
+ // Items were inserted where there were none before.
+ // If it was new rows make sure to create mappings for columns so that a
+ // valid mapping can be retrieved later and vice-versa.
+ QList<int> &orthogonal_proxy_to_source = (direction == Direction::Columns) ? m->source_rows : m->source_columns;
+ QList<int> &orthogonal_source_to_proxy = (direction == Direction::Columns) ? m->proxy_rows : m->proxy_columns;
+ if (orthogonal_source_to_proxy.isEmpty()) {
+ const int ortho_end = (direction == Direction::Columns) ? model->rowCount(source_parent) :
+ model->columnCount(source_parent);
+ orthogonal_source_to_proxy.resize(ortho_end);
+ for (int ortho_item = 0; ortho_item < ortho_end; ++ortho_item) {
+ if ((direction == Direction::Columns) ? filterAcceptsRowInternal(ortho_item, source_parent)
+ : filterAcceptsColumn(ortho_item, source_parent)) {
+ orthogonal_proxy_to_source.append(ortho_item);
+ }
+ }
+ if (direction == Direction::Columns) {
+ // We're reacting to columnsInserted, but we've just inserted new rows. Sort them.
+ sort_source_rows(orthogonal_proxy_to_source, source_parent);
+ }
+ build_source_to_proxy_mapping(orthogonal_proxy_to_source, orthogonal_source_to_proxy);
+ }
+ }
+
+ // Sort and insert the items
+ if (direction == Direction::Rows) // Only sort rows
+ sort_source_rows(source_items, source_parent);
+ insert_source_items(source_to_proxy, proxy_to_source, source_items, source_parent, direction);
+}
+
+/*!
+ \internal
+
+ Handles source model items removal (columnsAboutToBeRemoved(), rowsAboutToBeRemoved()).
+*/
+void QSortFilterProxyModelHelper::source_items_about_to_be_removed(
+ const QModelIndex &source_parent, int start, int end, Direction direction)
+{
+ if ((start < 0) || (end < 0))
+ return;
+ QSortFilterProxyModelHelper::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();
+ QList<int> &source_to_proxy = (direction == Direction::Rows) ? m->proxy_rows : m->proxy_columns;
+ QList<int> &proxy_to_source = (direction == Direction::Rows) ? m->source_rows : m->source_columns;
+
+ // figure out which items to remove
+ QList<int> source_items_to_remove;
+ int proxy_count = proxy_to_source.size();
+ for (int proxy_item = 0; proxy_item < proxy_count; ++proxy_item) {
+ int source_item = proxy_to_source.at(proxy_item);
+ if ((source_item >= start) && (source_item <= end))
+ source_items_to_remove.append(source_item);
+ }
+
+ remove_source_items(source_to_proxy, proxy_to_source, source_items_to_remove,
+ source_parent, direction);
+}
+
+/*!
+ \internal
+
+ Handles source model items removal (columnsRemoved(), rowsRemoved()).
+*/
+void QSortFilterProxyModelHelper::source_items_removed(
+ const QModelIndex &source_parent, int start, int end, Direction direction)
+{
+ if ((start < 0) || (end < 0))
+ return;
+ QSortFilterProxyModelHelper::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();
+ QList<int> &source_to_proxy = (direction == Direction::Rows) ? m->proxy_rows : m->proxy_columns;
+ QList<int> &proxy_to_source = (direction == Direction::Rows) ? m->source_rows : m->source_columns;
+
+ if (end >= source_to_proxy.size())
+ end = source_to_proxy.size() - 1;
+
+ // Shrink the source-to-proxy mapping to reflect the new item count
+ int delta_item_count = end - start + 1;
+ source_to_proxy.remove(start, delta_item_count);
+
+ int proxy_count = proxy_to_source.size();
+ if (proxy_count > source_to_proxy.size()) {
+ // mapping is in an inconsistent state -- redo the whole mapping
+ beginResetModel();
+ remove_from_mapping(source_parent);
+ endResetModel();
+ return;
+ }
+
+ // Adjust "stale" indexes in proxy-to-source mapping
+ for (int proxy_item = 0; proxy_item < proxy_count; ++proxy_item) {
+ int source_item = proxy_to_source.at(proxy_item);
+ if (source_item >= start) {
+ Q_ASSERT(source_item - delta_item_count >= 0);
+ proxy_to_source.replace(proxy_item, source_item - delta_item_count);
+ }
+ }
+
+ build_source_to_proxy_mapping(proxy_to_source, source_to_proxy);
+ updateChildrenMapping(source_parent, m, direction, start, end, delta_item_count, true);
+}
+
+
+/*!
+ \internal
+
+ Given source-to-proxy mapping \a source_to_proxy and proxy-to-source mapping
+ \a proxy_to_source, inserts the given \a source_items into this proxy model.
+ The source items are inserted in intervals (based on some sorted order), so
+ that the proper rows/columnsInserted(start, end) signals will be generated.
+*/
+void QSortFilterProxyModelHelper::insert_source_items(
+ QList<int> &source_to_proxy, QList<int> &proxy_to_source,
+ const QList<int> &source_items, const QModelIndex &source_parent,
+ Direction direction, bool emit_signal)
+{
+ QModelIndex proxy_parent = proxyModel()->mapFromSource(source_parent);
+ if (!proxy_parent.isValid() && source_parent.isValid())
+ return; // nothing to do (source_parent is not mapped)
+
+ const auto proxy_intervals = proxy_intervals_for_source_items_to_add(
+ proxy_to_source, source_items, source_parent, direction);
+
+ const auto end = proxy_intervals.rend();
+ for (auto it = proxy_intervals.rbegin(); it != end; ++it) {
+ const std::pair<int, QList<int>> &interval = *it;
+ const int proxy_start = interval.first;
+ const QList<int> &source_items = interval.second;
+ const int proxy_end = proxy_start + source_items.size() - 1;
+
+ if (emit_signal) {
+ if (direction == Direction::Rows)
+ beginInsertRows(proxy_parent, proxy_start, proxy_end);
+ else
+ beginInsertColumns(proxy_parent, proxy_start, proxy_end);
+ }
+
+ // TODO: use the range QList::insert() overload once it is implemented (QTBUG-58633).
+ proxy_to_source.insert(proxy_start, source_items.size(), 0);
+ std::copy(source_items.cbegin(), source_items.cend(), proxy_to_source.begin() + proxy_start);
+ build_source_to_proxy_mapping(proxy_to_source, source_to_proxy, proxy_start);
+
+ if (emit_signal) {
+ if (direction == Direction::Rows)
+ endInsertRows();
+ else
+ endInsertColumns();
+ }
+ }
+}
+
+/*!
+ \internal
+
+ Given source-to-proxy mapping \a src_to_proxy and proxy-to-source mapping
+ \a proxy_to_source, removes \a source_items from this proxy model.
+ The corresponding proxy items are removed in intervals, so that the proper
+ rows/columnsRemoved(start, end) signals will be generated.
+*/
+void QSortFilterProxyModelHelper::remove_source_items(QList<int> &source_to_proxy,
+ QList<int> &proxy_to_source, const QList<int> &source_items,
+ const QModelIndex &source_parent, Direction direction,
+ bool emit_signal)
+{
+ QModelIndex proxy_parent = proxyModel()->mapFromSource(source_parent);
+ if (!proxy_parent.isValid() && source_parent.isValid()) {
+ proxy_to_source.clear();
+ return; // nothing to do (already removed)
+ }
+ const auto proxy_intervals = proxy_intervals_for_source_items(
+ source_to_proxy, source_items);
+ const auto end = proxy_intervals.rend();
+ for (auto it = proxy_intervals.rbegin(); it != end; ++it) {
+ const std::pair<int, int> &interval = *it;
+ const int proxy_start = interval.first;
+ const int proxy_end = interval.second;
+ remove_proxy_interval(source_to_proxy, proxy_to_source, proxy_start, proxy_end,
+ proxy_parent, direction, emit_signal);
+ }
+}
+
+QSet<int> QSortFilterProxyModelHelper::handle_filter_changed(
+ QList<int> &source_to_proxy, QList<int> &proxy_to_source,
+ const QModelIndex &source_parent, Direction direction)
+{
+ // Figure out which mapped items to remove
+ QList<int> source_items_remove;
+ for (int i = 0; i < proxy_to_source.size(); ++i) {
+ const int source_item = proxy_to_source.at(i);
+ if ((direction == Direction::Rows)
+ ? !filterAcceptsRowInternal(source_item, source_parent)
+ : !filterAcceptsColumn(source_item, source_parent)) {
+ // This source item does not satisfy the filter, so it must be removed
+ source_items_remove.append(source_item);
+ }
+ }
+ // Figure out which non-mapped items to insert
+ QList<int> source_items_insert;
+ int source_count = source_to_proxy.size();
+ for (int source_item = 0; source_item < source_count; ++source_item) {
+ if (source_to_proxy.at(source_item) == -1) {
+ if ((direction == Direction::Rows)
+ ? filterAcceptsRowInternal(source_item, source_parent)
+ : filterAcceptsColumn(source_item, source_parent)) {
+ // This source item satisfies the filter, so it must be added
+ source_items_insert.append(source_item);
+ }
+ }
+ }
+ if (!source_items_remove.isEmpty() || !source_items_insert.isEmpty()) {
+ // Do item removal and insertion
+ remove_source_items(source_to_proxy, proxy_to_source,
+ source_items_remove, source_parent, direction);
+ if (direction == Direction::Rows)
+ sort_source_rows(source_items_insert, source_parent);
+ insert_source_items(source_to_proxy, proxy_to_source,
+ source_items_insert, source_parent, direction);
+ }
+ return qListToSet(source_items_remove);
+}
+
+
+void QSortFilterProxyModelHelper::filter_changed(Direction dir, const QModelIndex &source_parent)
+{
+ QSortFilterProxyModelHelper::IndexMap::const_iterator it = source_index_mapping.constFind(source_parent);
+ if (it == source_index_mapping.constEnd())
+ return;
+ Mapping *m = it.value();
+ const QSet<int> rows_removed = (dir & Direction::Rows) ? handle_filter_changed(m->proxy_rows, m->source_rows, source_parent, Direction::Rows) : QSet<int>();
+ const QSet<int> columns_removed = (dir & Direction::Columns) ? handle_filter_changed(m->proxy_columns, m->source_columns, source_parent, Direction::Columns) : QSet<int>();
+
+ // We need to iterate over a copy of m->mapped_children because otherwise it may be changed by
+ // other code, invalidating the iterator it2.
+ // The m->mapped_children vector can be appended to with indexes which are no longer filtered
+ // out (in create_mapping) when this function recurses for child indexes.
+ const QList<QModelIndex> mappedChildren = m->mapped_children;
+ QList<int> indexesToRemove;
+ for (int i = 0; i < mappedChildren.size(); ++i) {
+ const QModelIndex &source_child_index = mappedChildren.at(i);
+ if (rows_removed.contains(source_child_index.row()) || columns_removed.contains(source_child_index.column())) {
+ indexesToRemove.push_back(i);
+ remove_from_mapping(source_child_index);
+ } else {
+ filter_changed(dir, source_child_index);
+ }
+ }
+ QList<int>::const_iterator removeIt = indexesToRemove.constEnd();
+ const QList<int>::const_iterator removeBegin = indexesToRemove.constBegin();
+
+ // We can't just remove these items from mappedChildren while iterating above and then
+ // do something like m->mapped_children = mappedChildren, because mapped_children might
+ // be appended to in create_mapping, and we would lose those new items.
+ // Because they are always appended in create_mapping, we can still remove them by
+ // position here.
+ while (removeIt != removeBegin) {
+ --removeIt;
+ m->mapped_children.remove(*removeIt);
+ }
+}
+
+/*!
+ \internal
+
+ Sorts the existing mappings.
+*/
+void QSortFilterProxyModelHelper::sort()
+{
+ auto *pModel = const_cast<QAbstractProxyModel *>(proxyModel());
+ emit pModel->layoutAboutToBeChanged(QList<QPersistentModelIndex>(), QAbstractItemModel::VerticalSortHint);
+ QModelIndexPairList source_indexes = store_persistent_indexes();
+ for (auto [key, value]: source_index_mapping.asKeyValueRange()) {
+ const QModelIndex &source_parent = key;
+ Mapping *m = value;
+ sort_source_rows(m->source_rows, source_parent);
+ build_source_to_proxy_mapping(m->source_rows, m->proxy_rows);
+ }
+ update_persistent_indexes(source_indexes);
+ emit pModel->layoutChanged(QList<QPersistentModelIndex>(), QAbstractItemModel::VerticalSortHint);
+}
+
+
+QT_END_NAMESPACE
diff --git a/src/qmlmodels/sfpm/qsortfilterproxymodelhelper_p.h b/src/qmlmodels/sfpm/qsortfilterproxymodelhelper_p.h
new file mode 100644
index 0000000000..f270c75284
--- /dev/null
+++ b/src/qmlmodels/sfpm/qsortfilterproxymodelhelper_p.h
@@ -0,0 +1,233 @@
+// Copyright (C) 2025 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#ifndef QSORTFILTERPROXYMODELHELPER_H
+#define QSORTFILTERPROXYMODELHELPER_H
+
+//
+// W A R N I N G
+// -------------
+//
+// This file is not part of the Qt API. It exists for the convenience
+// of QAbstractItemModel*. This header file may change from version
+// to version without notice, or even be removed.
+//
+// We mean it.
+//
+//
+
+#include <QtCore/private/qabstractitemmodel_p.h>
+#include <QtCore/private/qabstractproxymodel_p.h>
+#include <QtQmlModels/private/qtqmlmodelsglobal_p.h>
+
+QT_BEGIN_NAMESPACE
+
+class QSortFilterProxyModelLessThan;
+class QSortFilterProxyModelGreaterThan;
+class QQmlSortFilterProxyModel;
+
+using QModelIndexPairList = QList<std::pair<QModelIndex, QPersistentModelIndex>>;
+
+class Q_QMLMODELS_EXPORT QSortFilterProxyModelHelper
+{
+ friend class QSortFilterProxyModelGreaterThan;
+ friend class QSortFilterProxyModelLessThan;
+
+public:
+ QSortFilterProxyModelHelper();
+ virtual ~QSortFilterProxyModelHelper();
+
+ static void setProperties(
+ QVariant *target, const QQmlSortFilterProxyModel *proxyModel,
+ const QModelIndex &sourceIndex);
+
+ enum Direction {
+ Rows = 0x01,
+ Columns = 0x02,
+ Both = Rows | Columns,
+ };
+
+ struct Mapping {
+ QList<int> source_rows;
+ QList<int> source_columns;
+ QList<int> proxy_rows;
+ QList<int> proxy_columns;
+ QList<QModelIndex> mapped_children;
+ QModelIndex source_parent;
+ };
+
+ using IndexMap = QHash<QtPrivate::QModelIndexWrapper, Mapping *>;
+ mutable IndexMap source_index_mapping;
+
+ static inline QSet<int> qListToSet(const QList<int> &vector) { return {vector.begin(), vector.end()}; }
+
+ inline IndexMap::const_iterator index_to_iterator(
+ const QModelIndex &proxy_index) const {
+ Q_ASSERT(proxy_index.isValid());
+ Q_ASSERT(proxy_index.model() == proxyModel());
+ const void *p = proxy_index.internalPointer();
+ Q_ASSERT(p);
+ IndexMap::const_iterator it =
+ source_index_mapping.constFind(static_cast<const Mapping*>(p)->source_parent);
+ Q_ASSERT(it != source_index_mapping.constEnd());
+ Q_ASSERT(it.value());
+ return it;
+ }
+
+ // Core mapping APIs
+ IndexMap::const_iterator create_mapping(const QModelIndex &source_parent) const;
+ IndexMap::const_iterator create_mapping_recursive(const QModelIndex &source_parent) const;
+ bool can_create_mapping(const QModelIndex &source_parent) const;
+ void remove_from_mapping(const QModelIndex &source_parent);
+ void clearSourceIndexMapping();
+ QModelIndex source_to_proxy(const QModelIndex &source_index) const;
+ void build_source_to_proxy_mapping(
+ QList<int> &proxy_to_source, QList<int> &source_to_proxy, int start = 0) const;
+ QModelIndex proxy_to_source(const QModelIndex &proxy_index) const;
+ void updateChildrenMapping(const QModelIndex &source_parent, Mapping *parent_mapping,
+ Direction direction, int start, int end, int delta_item_count, bool remove);
+ void proxy_item_range(const QList<int> &source_to_proxy, const QList<int> &source_items,
+ int &proxy_low, int &proxy_high) const;
+
+ // Model update APIs
+ QModelIndexPairList store_persistent_indexes() const;
+ void update_persistent_indexes(const QModelIndexPairList &source_indexes);
+
+ // Sort filter proxy model update APIs
+ virtual void filter_changed(Direction dir = Direction::Both,
+ const QModelIndex &source_parent = QModelIndex());
+ virtual QSet<int> handle_filter_changed(QList<int> &source_to_proxy, QList<int> &proxy_to_source,
+ const QModelIndex &source_parent, Direction direction);
+
+ virtual void insert_source_items(QList<int> &source_to_proxy, QList<int> &proxy_to_source,
+ const QList<int> &source_items, const QModelIndex &source_parent,
+ Direction direction, bool emit_signal = true);
+ virtual void source_items_inserted(const QModelIndex &source_parent,
+ int start, int end, Direction direction);
+ virtual void source_items_about_to_be_removed(const QModelIndex &source_parent, int start,
+ int end, Direction direction);
+ virtual void source_items_removed(const QModelIndex &source_parent, int start, int end,
+ Direction direction);
+ virtual void remove_source_items(QList<int> &source_to_proxy, QList<int> &proxy_to_source,
+ const QList<int> &source_items, const QModelIndex &source_parent,
+ Direction direction, bool emit_signal = true);
+ virtual void remove_proxy_interval(QList<int> &source_to_proxy, QList<int> &proxy_to_source, int proxy_start, int proxy_end,
+ const QModelIndex &proxy_parent, Direction direction, bool emit_signal = true);
+
+ virtual QList<std::pair<int, int>> proxy_intervals_for_source_items(const QList<int> &source_to_proxy,
+ const QList<int> &source_items) const;
+ virtual QList<std::pair<int, QList<int>>> proxy_intervals_for_source_items_to_add(const QList<int> &,
+ const QList<int> &,const QModelIndex &, Direction) const { return {}; }
+ virtual void sort();
+
+protected:
+ virtual const QAbstractProxyModel *proxyModel() const = 0;
+
+ // Proxy model protected functions need to be overridden in the corresponding model
+ virtual void beginInsertRows(const QModelIndex &, int, int) {};
+ virtual void beginInsertColumns(const QModelIndex &, int, int) {};
+ virtual void endInsertRows() {};
+ virtual void endInsertColumns() {};
+ virtual void beginRemoveRows(const QModelIndex &, int , int) {};
+ virtual void beginRemoveColumns(const QModelIndex &, int , int) {};
+ virtual void endRemoveRows() {};
+ virtual void endRemoveColumns() {};
+ virtual void beginResetModel() {};
+ virtual void endResetModel() {};
+
+ virtual QModelIndex createIndex(int , int , IndexMap::const_iterator ) const { return QModelIndex(); }
+ virtual void changePersistentIndexList(const QModelIndexList &, const QModelIndexList &) { };
+ virtual bool filterAcceptsRowInternal(int , const QModelIndex &) const { return true; }
+ virtual bool filterAcceptsRow(int , const QModelIndex &) const { return true; }
+ virtual bool filterAcceptsColumnInternal(int , const QModelIndex &) const { return true; }
+ virtual bool filterAcceptsColumn(int , const QModelIndex &) const { return true; }
+ virtual void sort_source_rows(QList<int> &, const QModelIndex &) const {}
+ virtual bool lessThan(const QModelIndex&, const QModelIndex &) const { return true; }
+};
+
+struct QSortFilterProxyModelDataChanged
+{
+ QSortFilterProxyModelDataChanged(const QModelIndex &tl, const QModelIndex &br)
+ : topLeft(tl), bottomRight(br) { }
+
+ QModelIndex topLeft;
+ QModelIndex bottomRight;
+};
+
+class QSortFilterProxyModelLessThan
+{
+public:
+ inline QSortFilterProxyModelLessThan(int column, const QModelIndex &parent,
+ const QAbstractItemModel *source,
+ const QSortFilterProxyModelHelper *helper)
+ : sort_column(column), source_parent(parent), source_model(source), proxy_model(helper) {}
+
+ inline bool operator()(int r1, int r2) const
+ {
+ QModelIndex i1 = source_model->index(r1, sort_column, source_parent);
+ QModelIndex i2 = source_model->index(r2, sort_column, source_parent);
+ return proxy_model->lessThan(i1, i2);
+ }
+
+private:
+ int sort_column;
+ QModelIndex source_parent;
+ const QAbstractItemModel *source_model;
+ const QSortFilterProxyModelHelper *proxy_model;
+};
+
+class QSortFilterProxyModelGreaterThan
+{
+public:
+ inline QSortFilterProxyModelGreaterThan(int column, const QModelIndex &parent,
+ const QAbstractItemModel *source,
+ const QSortFilterProxyModelHelper *helper)
+ : sort_column(column), source_parent(parent),
+ source_model(source), proxy_model(helper) {}
+
+ inline bool operator()(int r1, int r2) const
+ {
+ QModelIndex i1 = source_model->index(r1, sort_column, source_parent);
+ QModelIndex i2 = source_model->index(r2, sort_column, source_parent);
+ return proxy_model->lessThan(i2, i1);
+ }
+
+private:
+ int sort_column;
+ QModelIndex source_parent;
+ const QAbstractItemModel *source_model;
+ const QSortFilterProxyModelHelper *proxy_model;
+};
+
+//this struct is used to store what are the rows that are removed
+//between a call to rowsAboutToBeRemoved and rowsRemoved
+//it avoids readding rows to the mapping that are currently being removed
+struct QRowsRemoval
+{
+ QRowsRemoval(const QModelIndex &parent_source, int start, int end) : parent_source(parent_source), start(start), end(end)
+ {
+ }
+
+ QRowsRemoval() : start(-1), end(-1)
+ {
+ }
+
+ bool contains(QModelIndex parent, int row) const
+ {
+ do {
+ if (parent == parent_source)
+ return row >= start && row <= end;
+ row = parent.row();
+ parent = parent.parent();
+ } while (row >= 0);
+ return false;
+ }
+private:
+ QModelIndex parent_source;
+ int start;
+ int end;
+};
+
+QT_END_NAMESPACE
+
+#endif // QSORTFILTERPROXYMODELHELPER_H
diff --git a/src/qmlmodels/sfpm/sorters/qqmlfunctionsorter.cpp b/src/qmlmodels/sfpm/sorters/qqmlfunctionsorter.cpp
new file mode 100644
index 0000000000..99ab04b05a
--- /dev/null
+++ b/src/qmlmodels/sfpm/sorters/qqmlfunctionsorter.cpp
@@ -0,0 +1,174 @@
+// Copyright (C) 2025 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#include <QtQmlModels/private/qqmlfunctionsorter_p.h>
+#include <QtQmlModels/private/qqmlsortfilterproxymodel_p.h>
+#include <QtQml/private/qqmlobjectcreator_p.h>
+
+QT_BEGIN_NAMESPACE
+
+/*!
+ \qmltype FunctionSorter
+ \inherits Sorter
+ \inqmlmodule QtQml.Models
+ \since 6.10
+ \preliminary
+ \brief Sorts data in a \l SortFilterProxyModel based on the evaluation of
+ the designated 'sort' method.
+
+ FunctionSorter allows user to define the designated 'sort' method and it
+ will be evaluated to sort the data. The method takes two arguments
+ (lhs and rhs) of the specified parameter type and the data can
+ be accessed as below for evaluation,
+
+ \qml
+ SortFilterProxyModel {
+ model: sourceModel
+ sorters: [
+ FunctionSorter {
+ id: functionSorter
+ component RoleData: QtObject {
+ property real age
+ }
+ function sort(lhsData: RoleData, rhsData: RoleData) : int {
+ return (lhsData.age < rhsData.age) ? -1 : ((lhsData === rhsData.age) ? 0 : 1)
+ }
+ }
+ ]
+ }
+ \endqml
+
+ \note The user needs to explicitly invoke
+ \l{SortFilterProxyModel::invalidateSorter} whenever any external qml
+ property used within the designated 'sort' method changes. This behaviour
+ is subject to change in the future, like implicit invalidation and thus the
+ user doesn't need to explicitly invoke
+ \l{SortFilterProxyModel::invalidateSorter}.
+*/
+
+QQmlFunctionSorter::QQmlFunctionSorter(QObject *parent)
+ : QQmlSorterBase (new QQmlFunctionSorterPrivate, parent)
+{
+}
+
+QQmlFunctionSorter::~QQmlFunctionSorter()
+{
+ Q_D(QQmlFunctionSorter);
+ if (d->m_lhsParameterData.metaType().flags() & QMetaType::PointerToQObject)
+ delete d->m_lhsParameterData.value<QObject *>();
+ if (d->m_rhsParameterData.metaType().flags() & QMetaType::PointerToQObject)
+ delete d->m_rhsParameterData.value<QObject *>();
+}
+
+void QQmlFunctionSorter::componentComplete()
+{
+ Q_D(QQmlFunctionSorter);
+ const auto *metaObj = this->metaObject();
+ for (int idx = metaObj->methodOffset(); idx < metaObj->methodCount(); idx++) {
+ // Once we find the method signature, break the loop
+ QMetaMethod method = metaObj->method(idx);
+ if (method.nameView() == "sort") {
+ d->m_method = method;
+ break;
+ }
+ }
+
+ if (!d->m_method.isValid())
+ return;
+
+ if (d->m_method.parameterCount() != 2) {
+ qWarning("sort method requires two parameters");
+ return;
+ }
+
+ QQmlData *data = QQmlData::get(this);
+ if (!data || !data->outerContext) {
+ qWarning("sort requires a QML context");
+ return;
+ }
+
+ const QMetaType parameterType = d->m_method.parameterMetaType(0);
+ if (parameterType != d->m_method.parameterMetaType(1)) {
+ qWarning("sort parameters have to be equal");
+ return;
+ }
+
+ auto cu = QQmlMetaType::obtainCompilationUnit(parameterType);
+ const QQmlType parameterQmlType = QQmlMetaType::qmlType(parameterType);
+
+ QQmlRefPointer<QQmlContextData> context = data->outerContext;
+ QQmlEngine *engine = context->engine();
+
+ // The code below creates an instance of the inline component, composite,
+ // or specific C++ QObject types. The created instance, along with the
+ // data, are passed as an arguments to the 'sort' method, which is invoked
+ // during the call to QQmlFunctionSorter::compare.
+ // To create an instance of required component types (be it inline or
+ // composite), an executable compilation unit is required, and this can be
+ // obtained by looking up via metatype in the type registry
+ // (QQmlMetaType::obtainCompilationUnit). Pass it through the QML engine to
+ // make it executable. Further, use the executable compilation unit to run
+ // an object creator and produce an instance.
+ if (parameterType.flags() & QMetaType::PointerToQObject) {
+ QObject *created0 = nullptr;
+ QObject *created1 = nullptr;
+ if (parameterQmlType.isInlineComponentType()) {
+ const auto executableCu = engine->handle()->executableCompilationUnit(std::move(cu));
+ const QString icName = parameterQmlType.elementName();
+ created0 = QQmlObjectCreator(context, executableCu, context, icName).create(
+ executableCu->inlineComponentId(icName), nullptr, nullptr,
+ QQmlObjectCreator::InlineComponent);
+ created1 = QQmlObjectCreator(context, executableCu, context, icName).create(
+ executableCu->inlineComponentId(icName), nullptr, nullptr,
+ QQmlObjectCreator::InlineComponent);
+ } else if (parameterQmlType.isComposite()) {
+ const auto executableCu = engine->handle()->executableCompilationUnit(std::move(cu));
+ created0 = QQmlObjectCreator(context, executableCu, context, QString()).create();
+ created1 = QQmlObjectCreator(context, executableCu, context, QString()).create();
+ } else {
+ created0 = parameterQmlType.metaObject()->newInstance();
+ created1 = parameterQmlType.metaObject()->newInstance();
+ }
+
+ const auto names = d->m_method.parameterNames();
+ created0->setObjectName(names[0]);
+ created1->setObjectName(names[1]);
+ d->m_lhsParameterData = QVariant::fromValue(created0);
+ d->m_rhsParameterData = QVariant::fromValue(created1);
+ } else {
+ d->m_lhsParameterData = QVariant(parameterType);
+ d->m_rhsParameterData = QVariant(parameterType);
+ }
+}
+
+/*!
+ \internal
+*/
+QPartialOrdering QQmlFunctionSorter::compare(
+ const QModelIndex& sourceLeft, const QModelIndex& sourceRight,
+ const QQmlSortFilterProxyModel *proxyModel) const
+{
+ Q_D(const QQmlFunctionSorter);
+ if (!d->m_method.isValid()
+ || !d->m_lhsParameterData.isValid()
+ || !d->m_rhsParameterData.isValid()) {
+ return QPartialOrdering::Unordered;
+ }
+
+ int retVal = 0;
+ QSortFilterProxyModelHelper::setProperties(&d->m_lhsParameterData, proxyModel, sourceLeft);
+ QSortFilterProxyModelHelper::setProperties(&d->m_rhsParameterData, proxyModel, sourceRight);
+
+ void *argv[] = {&retVal, d->m_lhsParameterData.data(), d->m_rhsParameterData.data()};
+ QMetaObject::metacall(
+ const_cast<QQmlFunctionSorter *>(this), QMetaObject::InvokeMetaMethod,
+ d->m_method.methodIndex(), argv);
+
+ return (retVal == 0)
+ ? QPartialOrdering::Equivalent
+ : ((retVal < 0) ? QPartialOrdering::Less : QPartialOrdering::Greater);
+}
+
+QT_END_NAMESPACE
+
+#include "moc_qqmlfunctionsorter_p.cpp"
diff --git a/src/qmlmodels/sfpm/sorters/qqmlfunctionsorter_p.h b/src/qmlmodels/sfpm/sorters/qqmlfunctionsorter_p.h
new file mode 100644
index 0000000000..a473093600
--- /dev/null
+++ b/src/qmlmodels/sfpm/sorters/qqmlfunctionsorter_p.h
@@ -0,0 +1,57 @@
+// Copyright (C) 2025 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#ifndef QQMLFUNCTIONSORTER_P_H
+#define QQMLFUNCTIONSORTER_P_H
+
+// W A R N I N G
+// -------------
+//
+// This file is not part of the Qt API. It exists purely as an
+// implementation detail. This header file may change from version to
+// version without notice, or even be removed.
+//
+// We mean it.
+//
+
+#include <QtQmlModels/private/qqmlrolesorter_p.h>
+#include <QtQmlModels/private/qqmlsorterbase_p.h>
+
+QT_BEGIN_NAMESPACE
+
+class QQmlSortFilterProxyModel;
+class QQmlFunctionSorterPrivate;
+
+class Q_QMLMODELS_EXPORT QQmlFunctionSorter : public QQmlSorterBase, public QQmlParserStatus
+{
+ Q_OBJECT
+ Q_INTERFACES(QQmlParserStatus)
+ QML_NAMED_ELEMENT(FunctionSorter)
+
+public:
+ explicit QQmlFunctionSorter(QObject *parent = nullptr);
+ ~QQmlFunctionSorter() override;
+
+ virtual QPartialOrdering compare(const QModelIndex&, const QModelIndex&, const QQmlSortFilterProxyModel *) const override;
+
+private:
+ void classBegin() override {};
+ void componentComplete() override;
+
+private:
+ Q_DECLARE_PRIVATE(QQmlFunctionSorter)
+};
+
+class QQmlFunctionSorterPrivate : public QQmlRoleSorterPrivate
+{
+ Q_DECLARE_PUBLIC (QQmlFunctionSorter)
+
+public:
+ QMetaMethod m_method;
+ mutable QVariant m_lhsParameterData;
+ mutable QVariant m_rhsParameterData;
+};
+
+QT_END_NAMESPACE
+
+#endif // QQMLFUNCTIONSORTER_P_H
diff --git a/src/qmlmodels/sfpm/sorters/qqmlrolesorter.cpp b/src/qmlmodels/sfpm/sorters/qqmlrolesorter.cpp
new file mode 100644
index 0000000000..67d2f755dd
--- /dev/null
+++ b/src/qmlmodels/sfpm/sorters/qqmlrolesorter.cpp
@@ -0,0 +1,81 @@
+// Copyright (C) 2025 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#include <QtQmlModels/private/qqmlrolesorter_p.h>
+#include <QtQmlModels/private/qqmlsortfilterproxymodel_p.h>
+
+QT_BEGIN_NAMESPACE
+
+/*!
+ \qmltype RoleSorter
+ \inherits Sorter
+ \inqmlmodule QtQml.Models
+ \since 6.10
+ \preliminary
+ \brief Sort data in a \l SortFilterProxyModel based on configured role
+ name.
+
+ RoleSorter allows the user to sort the data according to the role name
+ as configured in the source model.
+
+ The RoleSorter can be configured in the sort filter proxy model as below,
+
+ \qml
+ SortFilterProxyModel {
+ model: sourceModel
+ sorters: [
+ RoleSorter { roleName: "firstname" }
+ ]
+ }
+ \endqml
+*/
+
+QQmlRoleSorter::QQmlRoleSorter(QObject *parent) :
+ QQmlSorterBase (new QQmlRoleSorterPrivate, parent)
+{
+}
+
+QQmlRoleSorter::QQmlRoleSorter(QQmlSorterBasePrivate *priv, QObject *parent) :
+ QQmlSorterBase (priv, parent)
+{
+}
+
+/*!
+ \qmlproperty string RoleSorter::roleName
+
+ This property holds the role name that will be used to sort the data.
+
+ The default value is display role.
+*/
+void QQmlRoleSorter::setRoleName(const QString& roleName)
+{
+ Q_D(QQmlRoleSorter);
+ if (d->m_roleName == roleName)
+ return;
+ d->m_roleName = roleName;
+ // Update the model for the change in the role name
+ emit roleNameChanged();
+ // Invalidate the model for the change in the role name
+ invalidate();
+}
+
+const QString& QQmlRoleSorter::roleName() const
+{
+ Q_D(const QQmlRoleSorter);
+ return d->m_roleName;
+}
+
+/*!
+ \internal
+*/
+QPartialOrdering QQmlRoleSorter::compare(const QModelIndex& sourceLeft, const QModelIndex& sourceRight, const QQmlSortFilterProxyModel *proxyModel) const
+{
+ Q_D(const QQmlRoleSorter);
+ if (int role = proxyModel->itemRoleForName(d->m_roleName); role > -1)
+ return QVariant::compare(proxyModel->sourceData(sourceLeft, role), proxyModel->sourceData(sourceRight, role));
+ return QPartialOrdering::Unordered;
+}
+
+QT_END_NAMESPACE
+
+#include "moc_qqmlrolesorter_p.cpp"
diff --git a/src/qmlmodels/sfpm/sorters/qqmlrolesorter_p.h b/src/qmlmodels/sfpm/sorters/qqmlrolesorter_p.h
new file mode 100644
index 0000000000..0d2c0d16ff
--- /dev/null
+++ b/src/qmlmodels/sfpm/sorters/qqmlrolesorter_p.h
@@ -0,0 +1,58 @@
+// Copyright (C) 2025 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#ifndef QQMLROLESORTER_P_H
+#define QQMLROLESORTER_P_H
+
+//
+// W A R N I N G
+// -------------
+//
+// This file is not part of the Qt API. It exists purely as an
+// implementation detail. This header file may change from version to
+// version without notice, or even be removed.
+//
+// We mean it.
+//
+
+#include <QtQmlModels/private/qqmlsorterbase_p.h>
+
+QT_BEGIN_NAMESPACE
+
+class QQmlSortFilterProxyModel;
+class QQmlRoleSorterPrivate;
+
+class Q_QMLMODELS_EXPORT QQmlRoleSorter : public QQmlSorterBase
+{
+ Q_OBJECT
+ Q_PROPERTY(QString roleName READ roleName WRITE setRoleName NOTIFY roleNameChanged)
+ QML_NAMED_ELEMENT(RoleSorter)
+
+public:
+ explicit QQmlRoleSorter(QObject *parent = nullptr);
+ QQmlRoleSorter(QQmlSorterBasePrivate *priv, QObject *parent = nullptr);
+ ~QQmlRoleSorter() = default;
+
+ const QString& roleName() const;
+ void setRoleName(const QString& roleName);
+
+ QPartialOrdering compare(const QModelIndex& sourceLeft, const QModelIndex& sourceRight, const QQmlSortFilterProxyModel *proxyModel) const override;
+
+Q_SIGNALS:
+ void roleNameChanged();
+
+private:
+ Q_DECLARE_PRIVATE(QQmlRoleSorter)
+};
+
+class QQmlRoleSorterPrivate : public QQmlSorterBasePrivate
+{
+ Q_DECLARE_PUBLIC (QQmlRoleSorter)
+
+public:
+ QString m_roleName = QString::fromUtf8("display");
+};
+
+QT_END_NAMESPACE
+
+#endif // QQMLROLESORTER_P_H
diff --git a/src/qmlmodels/sfpm/sorters/qqmlsorterbase.cpp b/src/qmlmodels/sfpm/sorters/qqmlsorterbase.cpp
new file mode 100644
index 0000000000..eb0b9888cc
--- /dev/null
+++ b/src/qmlmodels/sfpm/sorters/qqmlsorterbase.cpp
@@ -0,0 +1,136 @@
+// Copyright (C) 2025 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#include <QtQmlModels/private/qqmlsorterbase_p.h>
+#include <QtQmlModels/private/qqmlsortfilterproxymodel_p.h>
+
+QT_BEGIN_NAMESPACE
+
+/*!
+ \qmltype Sorter
+ \inherits QtObject
+ \inqmlmodule QtQml.Models
+ \since 6.10
+ \preliminary
+ \brief Abstract base type providing functionality common to sorters.
+
+ Sorter provides a set of common properties for all the sorters that they
+ inherit from.
+*/
+
+QQmlSorterBase::QQmlSorterBase(QQmlSorterBasePrivate *privObj, QObject *parent)
+ : QObject (*privObj, parent)
+{
+}
+
+/*!
+ \qmlproperty bool Sorter::enabled
+
+ This property enables the \l SortFilterProxyModel to consider this sorter
+ while sorting the model data.
+
+ The default value is \c true.
+*/
+bool QQmlSorterBase::enabled() const
+{
+ Q_D(const QQmlSorterBase);
+ return d->m_enabled;
+
+}
+
+void QQmlSorterBase::setEnabled(const bool enabled)
+{
+ Q_D(QQmlSorterBase);
+ if (d->m_enabled == enabled)
+ return;
+ d->m_enabled = enabled;
+ invalidate(true);
+ emit enabledChanged();
+}
+
+/*!
+ \qmlproperty Qt::SortOrder Sorter::sortOrder
+
+ This property holds the order used by \l SortFilterProxyModel when sorting
+ the model data.
+
+ The default value is \c Qt::AscendingOrder.
+*/
+Qt::SortOrder QQmlSorterBase::sortOrder() const
+{
+ Q_D(const QQmlSorterBase);
+ return d->m_sortOrder;
+}
+
+void QQmlSorterBase::setSortOrder(const Qt::SortOrder sortOrder)
+{
+ Q_D(QQmlSorterBase);
+ if (d->m_sortOrder == sortOrder)
+ return;
+ d->m_sortOrder = sortOrder;
+ invalidate();
+ emit sortOrderChanged();
+}
+
+/*!
+ \qmlproperty int Sorter::priority
+
+ This property holds the priority that is given to this sorter compared to
+ other sorters. The lesser value results in a higher priority and the higher
+ value results in a lower priority.
+
+ The default value is \c -1.
+*/
+int QQmlSorterBase::priority() const
+{
+ Q_D(const QQmlSorterBase);
+ return d->m_sorterPriority;
+}
+
+void QQmlSorterBase::setPriority(const int priority)
+{
+ Q_D(QQmlSorterBase);
+ if (d->m_sorterPriority == priority)
+ return;
+ d->m_sorterPriority = priority;
+ invalidate(true);
+ emit priorityChanged();
+}
+
+/*!
+ \qmlproperty int Sorter::column
+
+ This property holds the column that this sorter is applied to.
+
+ The default value is \c 0.
+*/
+int QQmlSorterBase::column() const
+{
+ Q_D(const QQmlSorterBase);
+ return d->m_sortColumn;
+}
+
+void QQmlSorterBase::setColumn(const int column)
+{
+ Q_D(QQmlSorterBase);
+ if (d->m_sortColumn == column)
+ return;
+ d->m_sortColumn = column;
+ invalidate();
+ emit columnChanged();
+}
+
+/*!
+ \internal
+*/
+void QQmlSorterBase::invalidate(bool updateCache)
+{
+ // Update the cached filters and invalidate the model
+ if (updateCache)
+ emit invalidateCache(this);
+ emit invalidateModel();
+}
+
+QT_END_NAMESPACE
+
+#include "moc_qqmlsorterbase_p.cpp"
diff --git a/src/qmlmodels/sfpm/sorters/qqmlsorterbase_p.h b/src/qmlmodels/sfpm/sorters/qqmlsorterbase_p.h
new file mode 100644
index 0000000000..52a95a26c1
--- /dev/null
+++ b/src/qmlmodels/sfpm/sorters/qqmlsorterbase_p.h
@@ -0,0 +1,89 @@
+// Copyright (C) 2025 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#ifndef QQMLSORTERBASE_H
+#define QQMLSORTERBASE_H
+
+//
+// W A R N I N G
+// -------------
+//
+// This file is not part of the Qt API. It exists purely as an
+// implementation detail. This header file may change from version to
+// version without notice, or even be removed.
+//
+// We mean it.
+//
+
+#include <QtQml/qqml.h>
+#include <QtCore/QObject>
+#include <QtCore/QAbstractItemModel>
+#include <QtQmlModels/private/qtqmlmodelsglobal_p.h>
+#include <QtQml/private/qqmlcustomparser_p.h>
+
+QT_BEGIN_NAMESPACE
+
+class QQmlSortFilterProxyModel;
+class QQmlSorterBasePrivate;
+
+class Q_QMLMODELS_EXPORT QQmlSorterBase : public QObject
+{
+ Q_OBJECT
+ Q_PROPERTY(bool enabled READ enabled WRITE setEnabled NOTIFY enabledChanged FINAL)
+ Q_PROPERTY(Qt::SortOrder sortOrder READ sortOrder WRITE setSortOrder NOTIFY sortOrderChanged FINAL)
+ Q_PROPERTY(int priority READ priority WRITE setPriority NOTIFY priorityChanged FINAL)
+ Q_PROPERTY(int column READ column WRITE setColumn NOTIFY columnChanged FINAL)
+ QML_ELEMENT
+ QML_UNCREATABLE("")
+
+public:
+ explicit QQmlSorterBase(QQmlSorterBasePrivate *privObj, QObject *parent = nullptr);
+ virtual ~QQmlSorterBase() = default;
+
+ bool enabled() const;
+ void setEnabled(const bool enabled);
+
+ Qt::SortOrder sortOrder() const;
+ void setSortOrder(const Qt::SortOrder sortOrder);
+
+ int priority() const;
+ void setPriority(const int priority);
+
+ int column() const;
+ void setColumn(const int column);
+
+ virtual QPartialOrdering compare(const QModelIndex&, const QModelIndex&, const QQmlSortFilterProxyModel *) const = 0;
+ virtual void update(const QQmlSortFilterProxyModel *) { /* do nothing */ }
+
+Q_SIGNALS:
+ void enabledChanged();
+ void sortOrderChanged();
+ void priorityChanged();
+ void columnChanged();
+ void invalidateModel();
+ void invalidateCache(QQmlSorterBase *filter);
+
+public slots:
+ void invalidate(bool updateCache = true);
+
+private:
+ Q_DECLARE_PRIVATE(QQmlSorterBase)
+};
+
+class QQmlSorterBasePrivate: public QObjectPrivate
+{
+ Q_DECLARE_PUBLIC(QQmlSorterBase)
+
+public:
+ QQmlSorterBasePrivate() = default;
+ virtual ~QQmlSorterBasePrivate() = default;
+
+ bool m_enabled = true;
+ Qt::SortOrder m_sortOrder = Qt::AscendingOrder;
+ int m_sorterPriority = std::numeric_limits<int>::max();
+ int m_sortColumn = 0;
+};
+
+QT_END_NAMESPACE
+
+#endif // QQMLSORTERBASE_H
diff --git a/src/qmlmodels/sfpm/sorters/qqmlsortercompositor.cpp b/src/qmlmodels/sfpm/sorters/qqmlsortercompositor.cpp
new file mode 100644
index 0000000000..4409b8c3e3
--- /dev/null
+++ b/src/qmlmodels/sfpm/sorters/qqmlsortercompositor.cpp
@@ -0,0 +1,225 @@
+// Copyright (C) 2025 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#include <QtQmlModels/private/qqmlsortercompositor_p.h>
+#include <QtQmlModels/private/qqmlsortfilterproxymodel_p.h>
+
+QT_BEGIN_NAMESPACE
+
+QQmlSorterCompositor::QQmlSorterCompositor(QObject *parent)
+ : QQmlSorterBase(new QQmlSorterCompositorPrivate, parent)
+{
+ Q_D(QQmlSorterCompositor);
+ d->init();
+ // Connect the model reset with the update in the filter
+ // The cache need to be updated once the model is reset with the
+ // source model data.
+ connect(d->m_sfpmModel, &QQmlSortFilterProxyModel::modelReset,
+ this, &QQmlSorterCompositor::updateSorters);
+ connect(d->m_sfpmModel, &QQmlSortFilterProxyModel::primarySorterChanged,
+ this, &QQmlSorterCompositor::updateEffectiveSorters);
+}
+
+QQmlSorterCompositor::~QQmlSorterCompositor()
+{
+
+}
+
+void QQmlSorterCompositorPrivate::init()
+{
+ Q_Q(QQmlSorterCompositor);
+ m_sfpmModel = qobject_cast<QQmlSortFilterProxyModel *>(q->parent());
+}
+
+void QQmlSorterCompositor::append(QQmlListProperty<QQmlSorterBase> *sorterComp, QQmlSorterBase* sorter)
+{
+ auto *sorterCompositor = reinterpret_cast<QQmlSorterCompositor *>(sorterComp->object);
+ sorterCompositor->append(sorter);
+}
+
+qsizetype QQmlSorterCompositor::count(QQmlListProperty<QQmlSorterBase> *sorterComp)
+{
+ auto *sorterCompositor = reinterpret_cast<QQmlSorterCompositor *> (sorterComp->object);
+ return sorterCompositor->count();
+}
+
+QQmlSorterBase *QQmlSorterCompositor::at(QQmlListProperty<QQmlSorterBase> *sorterComp, qsizetype index)
+{
+ auto *sorterCompositor = reinterpret_cast<QQmlSorterCompositor *> (sorterComp->object);
+ return sorterCompositor->at(index);
+}
+
+void QQmlSorterCompositor::clear(QQmlListProperty<QQmlSorterBase> *sorterComp)
+{
+ auto *sorterCompositor = reinterpret_cast<QQmlSorterCompositor *> (sorterComp->object);
+ sorterCompositor->clear();
+}
+
+void QQmlSorterCompositor::append(QQmlSorterBase *sorter)
+{
+ if (!sorter)
+ return;
+ Q_D(QQmlSorterCompositor);
+ d->m_sorters.append(sorter);
+ // Update sorter cache depending on the priority
+ updateCache();
+ // Connect the sorter to the corresponding slot to invalidate the model
+ // and the sorter cache
+ QObject::connect(sorter, &QQmlSorterBase::invalidateModel,
+ d->m_sfpmModel, &QQmlSortFilterProxyModel::invalidate);
+ // This is needed as its required to update cache when there is any
+ // change in the filter itself (for instance, a change in the priority of
+ // the filter)
+ QObject::connect(sorter, &QQmlSorterBase::invalidateCache,
+ this, &QQmlSorterCompositor::updateCache);
+ // Reset the primary sort column when any sort order or column
+ // changed
+ QObject::connect(sorter, &QQmlSorterBase::sortOrderChanged,
+ this, [d] { d->resetPrimarySorter(); });
+ QObject::connect(sorter, &QQmlSorterBase::columnChanged,
+ this, [d] { d->resetPrimarySorter(); });
+ // Since we added new filter to the list, emit the filter changed signal
+ // for the filters thats been appended to the list
+ emit d->m_sfpmModel->sortersChanged();
+}
+
+qsizetype QQmlSorterCompositor::count()
+{
+ Q_D(QQmlSorterCompositor);
+ return d->m_sorters.count();
+}
+
+QQmlSorterBase* QQmlSorterCompositor::at(qsizetype index)
+{
+ Q_D(QQmlSorterCompositor);
+ return d->m_sorters.at(index);
+}
+
+void QQmlSorterCompositor::clear()
+{
+ Q_D(QQmlSorterCompositor);
+ d->m_effectiveSorters.clear();
+ d->m_sorters.clear();
+ // Emit the filter changed signal as we cleared the filter list
+ emit d->m_sfpmModel->sortersChanged();
+}
+
+QList<QQmlSorterBase *> QQmlSorterCompositor::sorters()
+{
+ Q_D(QQmlSorterCompositor);
+ return d->m_sorters;
+}
+
+QQmlListProperty<QQmlSorterBase> QQmlSorterCompositor::sortersListProperty()
+{
+ Q_D(QQmlSorterCompositor);
+ return QQmlListProperty<QQmlSorterBase>(reinterpret_cast<QObject*>(this), &d->m_sorters,
+ QQmlSorterCompositor::append,
+ QQmlSorterCompositor::count,
+ QQmlSorterCompositor::at,
+ QQmlSorterCompositor::clear);
+}
+
+void QQmlSorterCompositor::updateEffectiveSorters()
+{
+ Q_D(QQmlSorterCompositor);
+
+ if (!d->m_primarySorter || !d->m_primarySorter->enabled()) {
+ updateCache();
+ return;
+ }
+
+ QList<QQmlSorterBase *> sorters;
+ sorters.append(d->m_primarySorter);
+ std::copy_if(d->m_effectiveSorters.constBegin(), d->m_effectiveSorters.constEnd(),
+ std::back_inserter(sorters), [d](QQmlSorterBase *sorter){
+ // Consider only the filters that are enabled and exclude the primary
+ // sorter as its already added to the list
+ return (sorter != d->m_primarySorter);
+ });
+ d->m_effectiveSorters = sorters;
+}
+
+void QQmlSorterCompositor::updateSorters()
+{
+ Q_D(QQmlSorterCompositor);
+ // Update sorters that has dependency with the model data to determine
+ // whether it needs to be included or not
+ for (auto &sorter: d->m_sorters)
+ sorter->update(d->m_sfpmModel);
+ updateCache();
+}
+
+void QQmlSorterCompositor::updateCache()
+{
+ Q_D(QQmlSorterCompositor);
+ // Clear the existing cache
+ d->m_effectiveSorters.clear();
+ if (d->m_sfpmModel && d->m_sfpmModel->sourceModel()) {
+ // Sort the filter according to their priority
+ QList<QQmlSorterBase *> sorters = d->m_sorters;
+ std::stable_sort(sorters.begin(), sorters.end(),
+ [](QQmlSorterBase *sorterLeft, QQmlSorterBase *sorterRight) {
+ return sorterLeft->priority() < sorterRight->priority();
+ });
+ // Cache only the filters that are need to be evaluated (in order)
+ std::copy_if(sorters.begin(), sorters.end(), std::back_inserter(d->m_effectiveSorters),
+ [](QQmlSorterBase *sorter) { return sorter->enabled(); });
+ // If there is no primary sorter set by the user explicitly, reset the
+ // primary sorter according to the sorters in the lists
+ d->resetPrimarySorter();
+ }
+}
+
+bool QQmlSorterCompositor::lessThan(const QModelIndex& sourceLeft, const QModelIndex& sourceRight, const QQmlSortFilterProxyModel *proxyModel) const
+{
+ Q_D(const QQmlSorterCompositor);
+ for (const auto &sorter : d->m_effectiveSorters) {
+ const int sortSection = sorter->column();
+ if ((sortSection > -1) && (sortSection < proxyModel->sourceModel()->columnCount())) {
+ const auto *sourceModel = proxyModel->sourceModel();
+ const QPartialOrdering result = sorter->compare(sourceModel->index(sourceLeft.row(), sortSection),
+ sourceModel->index(sourceRight.row(), sortSection),
+ proxyModel);
+ if ((result == QPartialOrdering::Less) || (result == QPartialOrdering::Greater))
+ return (result < 0);
+ }
+ }
+ // Verify the index order when the ordering is either equal or unordered
+ return sourceLeft.row() < sourceRight.row();
+}
+
+void QQmlSorterCompositorPrivate::setPrimarySorter(QQmlSorterBase *sorter)
+{
+ if (sorter == nullptr ||
+ (std::find(m_sorters.constBegin(), m_sorters.constEnd(), sorter) != m_sorters.constEnd())) {
+ m_primarySorter = sorter;
+ if (m_primarySorter && m_primarySorter->enabled()) {
+ m_sfpmModel->setPrimarySortOrder(m_primarySorter->sortOrder());
+ m_sfpmModel->setPrimarySortColumn(m_primarySorter->column());
+ return;
+ }
+ }
+ resetPrimarySorter();
+}
+
+void QQmlSorterCompositorPrivate::resetPrimarySorter()
+{
+ if (!m_primarySorter) {
+ if (!m_effectiveSorters.isEmpty()) {
+ // Set the primary sort column and its order to the proxy model
+ const auto *sorter = m_effectiveSorters.at(0);
+ m_sfpmModel->setPrimarySortOrder(sorter->sortOrder());
+ m_sfpmModel->setPrimarySortColumn(sorter->column());
+ } else {
+ // By default reset the sort order to ascending order and the
+ // column to 0
+ m_sfpmModel->setPrimarySortOrder(Qt::AscendingOrder);
+ m_sfpmModel->setPrimarySortColumn(0);
+ }
+ }
+}
+
+QT_END_NAMESPACE
+
+#include "moc_qqmlsortercompositor_p.cpp"
diff --git a/src/qmlmodels/sfpm/sorters/qqmlsortercompositor_p.h b/src/qmlmodels/sfpm/sorters/qqmlsortercompositor_p.h
new file mode 100644
index 0000000000..f99b06dd0f
--- /dev/null
+++ b/src/qmlmodels/sfpm/sorters/qqmlsortercompositor_p.h
@@ -0,0 +1,82 @@
+// Copyright (C) 2025 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#ifndef QQMLSORTERCOMPOSITOR_H
+#define QQMLSORTERCOMPOSITOR_H
+
+//
+// W A R N I N G
+// -------------
+//
+// This file is not part of the Qt API. It exists purely as an
+// implementation detail. This header file may change from version to
+// version without notice, or even be removed.
+//
+// We mean it.
+//
+
+#include <QtQmlModels/private/qqmlsorterbase_p.h>
+
+QT_BEGIN_NAMESPACE
+
+class QQmlSortFilterProxyModel;
+class QQmlSorterCompositorPrivate;
+
+class QQmlSorterCompositor: public QQmlSorterBase
+{
+ Q_OBJECT
+ QML_ELEMENT
+ QML_UNCREATABLE("")
+
+public:
+ explicit QQmlSorterCompositor(QObject *parent = nullptr);
+ ~QQmlSorterCompositor() override;
+
+ QList<QQmlSorterBase *> sorters();
+ QQmlListProperty<QQmlSorterBase> sortersListProperty();
+
+ static void append(QQmlListProperty<QQmlSorterBase> *sorterComp, QQmlSorterBase *sorter);
+ static qsizetype count(QQmlListProperty<QQmlSorterBase> *sorterComp);
+ static QQmlSorterBase* at(QQmlListProperty<QQmlSorterBase> *sorterComp, qsizetype index);
+ static void clear(QQmlListProperty<QQmlSorterBase> *sorterComp);
+
+ QPartialOrdering compare(const QModelIndex &, const QModelIndex &, const QQmlSortFilterProxyModel *) const override { return QPartialOrdering::Unordered; };
+ bool lessThan(const QModelIndex &sourceLeft, const QModelIndex &sourceRight, const QQmlSortFilterProxyModel *proxyModel) const;
+ void updateSorters();
+ void updateEffectiveSorters();
+
+private:
+ void append(QQmlSorterBase *sorter);
+ qsizetype count();
+ QQmlSorterBase* at(qsizetype index);
+ void clear();
+
+public slots:
+ void updateCache();
+
+private:
+ Q_DECLARE_PRIVATE(QQmlSorterCompositor)
+};
+
+class QQmlSorterCompositorPrivate: public QQmlSorterBasePrivate
+{
+ Q_DECLARE_PUBLIC(QQmlSorterCompositor)
+
+public:
+ void init();
+ void setPrimarySorter(QQmlSorterBase *sorter);
+ QPointer<QQmlSorterBase> primarySorter() const { return m_primarySorter; }
+ void resetPrimarySorter();
+
+ // Holds sorters in the same order as declared in the qml
+ QList<QQmlSorterBase *> m_sorters;
+ // Holds effective sorters that will be evaluated with the
+ // model content
+ QList<QQmlSorterBase *> m_effectiveSorters;
+ QQmlSortFilterProxyModel *m_sfpmModel = nullptr;
+ QPointer<QQmlSorterBase> m_primarySorter;
+};
+
+QT_END_NAMESPACE
+
+#endif // QQMLSORTERCOMPOSITOR_H
diff --git a/src/qmlmodels/sfpm/sorters/qqmlstringsorter.cpp b/src/qmlmodels/sfpm/sorters/qqmlstringsorter.cpp
new file mode 100644
index 0000000000..ee61ad5abd
--- /dev/null
+++ b/src/qmlmodels/sfpm/sorters/qqmlstringsorter.cpp
@@ -0,0 +1,150 @@
+// Copyright (C) 2025 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#include <QtQmlModels/private/qqmlstringsorter_p.h>
+#include <QtQmlModels/private/qqmlsortfilterproxymodel_p.h>
+
+QT_BEGIN_NAMESPACE
+
+/*!
+ \qmltype StringSorter
+ \inherits Sorter
+ \inqmlmodule QtQml.Models
+ \since 6.10
+ \preliminary
+ \brief Sort data in a \l SortFilterProxyModel based on ordering of the
+ locale.
+
+ StringSorter allows the user to sort the data according to the role name
+ as configured in the source model. StringSorter compares strings according
+ to a localized collation algorithm.
+
+ The StringSorter can be configured in the sort filter proxy model as below,
+
+ \qml
+ SortFilterProxyModel {
+ model: sourceModel
+ sorters: [
+ StringSorter { roleName: "name" }
+ ]
+ }
+ \endqml
+*/
+
+QQmlStringSorter::QQmlStringSorter(QObject *parent) :
+ QQmlRoleSorter (new QQmlStringSorterPrivate, parent)
+{
+}
+
+/*!
+ \qmlproperty Qt::CaseSensitivity StringSorter::caseSensitivity
+
+ This property holds the case sensitivity of the sorter.
+
+ The default value is Qt::CaseSensitive.
+*/
+Qt::CaseSensitivity QQmlStringSorter::caseSensitivity() const
+{
+ Q_D(const QQmlStringSorter);
+ return d->m_collator.caseSensitivity();
+}
+
+void QQmlStringSorter::setCaseSensitivity(Qt::CaseSensitivity caseSensitivity)
+{
+ Q_D(QQmlStringSorter);
+ if (d->m_collator.caseSensitivity() == caseSensitivity)
+ return;
+ d->m_collator.setCaseSensitivity(caseSensitivity);
+ emit caseSensitivityChanged();
+ invalidate();
+}
+
+/*!
+ \qmlproperty bool StringSorter::ignorePunctuation
+
+ This property holds whether the sorter ignores punctation.
+ If \c ignorePunctuation is \c true, punctuation characters and symbols are
+ ignored when determining sort order.
+
+ The default value is \c false.
+*/
+bool QQmlStringSorter::ignorePunctuation() const
+{
+ Q_D(const QQmlStringSorter);
+ return d->m_collator.ignorePunctuation();
+}
+
+void QQmlStringSorter::setIgnorePunctuation(bool ignorePunctuation)
+{
+ Q_D(QQmlStringSorter);
+ if (d->m_collator.ignorePunctuation() == ignorePunctuation)
+ return;
+ d->m_collator.setIgnorePunctuation(ignorePunctuation);
+ emit ignorePunctuationChanged();
+ invalidate();
+}
+
+/*!
+ \qmlproperty Locale StringSorter::locale
+
+ This property holds the locale of the sorter.
+
+ The default value is \l QLocale::system()
+*/
+QLocale QQmlStringSorter::locale() const
+{
+ Q_D(const QQmlStringSorter);
+ return d->m_collator.locale();
+}
+
+void QQmlStringSorter::setLocale(const QLocale &locale)
+{
+ Q_D(QQmlStringSorter);
+ if (d->m_collator.locale() == locale)
+ return;
+ d->m_collator.setLocale(locale);
+ emit localeChanged();
+ invalidate();
+}
+
+/*!
+ \qmlproperty bool StringSorter::numericMode
+
+ This property holds whether the numeric mode of the sorter is enabled.
+
+ The default value is \c false.
+*/
+bool QQmlStringSorter::numericMode() const
+{
+ Q_D(const QQmlStringSorter);
+ return d->m_collator.numericMode();
+}
+
+void QQmlStringSorter::setNumericMode(bool numericMode)
+{
+ Q_D(QQmlStringSorter);
+ if (d->m_collator.numericMode() == numericMode)
+ return;
+
+ d->m_collator.setNumericMode(numericMode);
+ emit numericModeChanged();
+ invalidate();
+}
+/*!
+ \internal
+*/
+QPartialOrdering QQmlStringSorter::compare(const QModelIndex &sourceLeft, const QModelIndex &sourceRight, const QQmlSortFilterProxyModel* proxyModel) const
+{
+ Q_D(const QQmlStringSorter);
+ if (int role = proxyModel->itemRoleForName(d->m_roleName); role > -1) {
+ const QVariant first = proxyModel->sourceData(sourceLeft, role);
+ const QVariant second = proxyModel->sourceData(sourceRight, role);
+ const int result = d->m_collator.compare(first.toString(), second.toString());
+ return (result <= 0) ? ((result < 0) ? QPartialOrdering::Less : QPartialOrdering::Equivalent) : QPartialOrdering::Greater;
+ }
+ return QPartialOrdering::Unordered;
+}
+
+QT_END_NAMESPACE
+
+#include "moc_qqmlstringsorter_p.cpp"
diff --git a/src/qmlmodels/sfpm/sorters/qqmlstringsorter_p.h b/src/qmlmodels/sfpm/sorters/qqmlstringsorter_p.h
new file mode 100644
index 0000000000..c4dd3ec0d9
--- /dev/null
+++ b/src/qmlmodels/sfpm/sorters/qqmlstringsorter_p.h
@@ -0,0 +1,73 @@
+// Copyright (C) 2025 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#ifndef QQMLSTRINGSORTER_H
+#define QQMLSTRINGSORTER_H
+
+//
+// W A R N I N G
+// -------------
+//
+// This file is not part of the Qt API. It exists purely as an
+// implementation detail. This header file may change from version to
+// version without notice, or even be removed.
+//
+// We mean it.
+//
+
+#include <QCollator>
+#include <QtQmlModels/private/qqmlrolesorter_p.h>
+
+QT_BEGIN_NAMESPACE
+
+class QQmlSortFilterProxyModel;
+class QQmlStringSorterPrivate;
+
+class Q_QMLMODELS_EXPORT QQmlStringSorter : public QQmlRoleSorter
+{
+ Q_OBJECT
+ Q_PROPERTY(Qt::CaseSensitivity caseSensitivity READ caseSensitivity WRITE setCaseSensitivity NOTIFY caseSensitivityChanged)
+ Q_PROPERTY(bool ignorePunctuation READ ignorePunctuation WRITE setIgnorePunctuation NOTIFY ignorePunctuationChanged)
+ Q_PROPERTY(QLocale locale READ locale WRITE setLocale NOTIFY localeChanged)
+ Q_PROPERTY(bool numericMode READ numericMode WRITE setNumericMode NOTIFY numericModeChanged)
+ QML_NAMED_ELEMENT(StringSorter)
+
+public:
+ explicit QQmlStringSorter(QObject *parent = nullptr);
+ ~QQmlStringSorter() = default;
+
+ Qt::CaseSensitivity caseSensitivity() const;
+ void setCaseSensitivity(Qt::CaseSensitivity caseSensitivity);
+
+ bool ignorePunctuation() const;
+ void setIgnorePunctuation(bool ignorePunctation);
+
+ QLocale locale() const;
+ void setLocale(const QLocale& locale);
+
+ bool numericMode() const;
+ void setNumericMode(bool numericMode);
+
+ QPartialOrdering compare(const QModelIndex& sourceLeft, const QModelIndex& sourceRight, const QQmlSortFilterProxyModel *proxyModel) const override;
+
+Q_SIGNALS:
+ void caseSensitivityChanged();
+ void ignorePunctuationChanged();
+ void localeChanged();
+ void numericModeChanged();
+
+private:
+ Q_DECLARE_PRIVATE(QQmlStringSorter)
+};
+
+class QQmlStringSorterPrivate : public QQmlRoleSorterPrivate
+{
+ Q_DECLARE_PUBLIC (QQmlStringSorter)
+
+public:
+ QCollator m_collator;
+};
+
+QT_END_NAMESPACE
+
+#endif // QQMLSTRINGSORTER_H
diff --git a/tests/auto/qml/CMakeLists.txt b/tests/auto/qml/CMakeLists.txt
index 6f7cd7f1d8..3f8ea50d9c 100644
--- a/tests/auto/qml/CMakeLists.txt
+++ b/tests/auto/qml/CMakeLists.txt
@@ -148,6 +148,7 @@ if(QT_FEATURE_private_tests)
add_subdirectory(qqmlimport)
add_subdirectory(qqmlobjectmodel)
add_subdirectory(qqmltablemodel)
+ add_subdirectory(qqmlsortfilterproxymodel)
add_subdirectory(qqmltreemodeltotablemodel)
add_subdirectory(qv4assembler)
add_subdirectory(qv4mm)
diff --git a/tests/auto/qml/qqmlsortfilterproxymodel/CMakeLists.txt b/tests/auto/qml/qqmlsortfilterproxymodel/CMakeLists.txt
new file mode 100644
index 0000000000..38d1a65886
--- /dev/null
+++ b/tests/auto/qml/qqmlsortfilterproxymodel/CMakeLists.txt
@@ -0,0 +1,46 @@
+# Copyright (C) 2025 The Qt Company Ltd.
+# SPDX-License-Identifier: BSD-3-Clause
+
+#####################################################################
+## tst_qqmlsortfilterproxymodel Test:
+#####################################################################
+
+if(NOT QT_BUILD_STANDALONE_TESTS AND NOT QT_BUILDING_QT)
+ cmake_minimum_required(VERSION 3.16)
+ project(tst_qqmlsortfilterproxymodel LANGUAGES CXX)
+ find_package(Qt6BuildInternals REQUIRED COMPONENTS STANDALONE_TEST)
+endif()
+
+# Collect test data
+file(GLOB_RECURSE test_data_glob
+ RELATIVE ${CMAKE_CURRENT_SOURCE_DIR}
+ data/*)
+list(APPEND test_data ${test_data_glob})
+
+qt_internal_add_test(tst_qqmlsortfilterproxymodel
+ SOURCES
+ tst_qqmlsortfilterproxymodel.cpp
+ LIBRARIES
+ Qt::Gui
+ Qt::Qml
+ Qt::QmlPrivate
+ Qt::Quick
+ Qt::QuickPrivate
+ Qt::QuickTestUtilsPrivate
+ Qt::LabsQmlModels
+ Qt::LabsQmlModelsPrivate
+ TESTDATA ${test_data}
+)
+
+## Scopes:
+#####################################################################
+
+qt_internal_extend_target(tst_qqmlsortfilterproxymodel CONDITION ANDROID OR IOS
+ DEFINES
+ QT_QMLTEST_DATADIR=":/data"
+)
+
+qt_internal_extend_target(tst_qqmlsortfilterproxymodel CONDITION NOT ANDROID AND NOT IOS
+ DEFINES
+ QT_QMLTEST_DATADIR="${CMAKE_CURRENT_SOURCE_DIR}/data"
+)
diff --git a/tests/auto/qml/qqmlsortfilterproxymodel/data/Utility.js b/tests/auto/qml/qqmlsortfilterproxymodel/data/Utility.js
new file mode 100644
index 0000000000..caa49fa3f7
--- /dev/null
+++ b/tests/auto/qml/qqmlsortfilterproxymodel/data/Utility.js
@@ -0,0 +1,18 @@
+.pragma library
+
+const romanTable = {
+ "I" : 1,
+ "II" : 2,
+ "III" : 3,
+ "IV" : 4,
+ "V" : 5,
+ "VI" : 6,
+ "VII" : 7,
+ "VIII": 8,
+ "IX" : 9,
+ "X" : 10,
+};
+
+function getInteger(strIndex) {
+ return romanTable[strIndex];
+}
diff --git a/tests/auto/qml/qqmlsortfilterproxymodel/data/sfpmCommon.qml b/tests/auto/qml/qqmlsortfilterproxymodel/data/sfpmCommon.qml
new file mode 100644
index 0000000000..3691407f15
--- /dev/null
+++ b/tests/auto/qml/qqmlsortfilterproxymodel/data/sfpmCommon.qml
@@ -0,0 +1,41 @@
+import QtQml
+import "Utility.js" as Sfpmutility
+
+QtObject {
+ id: sfpmTestObject
+
+ function getValue(value) {
+ return Sfpmutility.getInteger(value)
+ }
+
+ component SorterRoleData: QtObject { property string display }
+ component FilterRoleData0: QtObject { property string column0 }
+ component FilterRoleData1: QtObject { property string column1 }
+
+ // Filters
+ property ValueFilter valueFilter: ValueFilter {}
+ property FunctionFilter functionFilter0: FunctionFilter {
+ property string expression: ""
+ function filter(data: FilterRoleData0) : bool {
+ return eval(expression)
+ }
+ }
+ property FunctionFilter functionFilter1: FunctionFilter {
+ property string expression: ""
+ function filter(data: FilterRoleData1) : bool {
+ return eval(expression)
+ }
+ }
+
+ // Sorters
+ property RoleSorter roleSorter: RoleSorter {}
+ property StringSorter stringSorter: StringSorter {}
+ property FunctionSorter functionSorter: FunctionSorter {
+ property string expression: ""
+ function sort(lhsData: SorterRoleData, rhsData: SorterRoleData) : int {
+ return eval(expression)
+ }
+ }
+
+ property SortFilterProxyModel sfpmProxyModel: SortFilterProxyModel {}
+}
diff --git a/tests/auto/qml/qqmlsortfilterproxymodel/tst_qqmlsortfilterproxymodel.cpp b/tests/auto/qml/qqmlsortfilterproxymodel/tst_qqmlsortfilterproxymodel.cpp
new file mode 100644
index 0000000000..1cde600684
--- /dev/null
+++ b/tests/auto/qml/qqmlsortfilterproxymodel/tst_qqmlsortfilterproxymodel.cpp
@@ -0,0 +1,1050 @@
+// Copyright (C) 2025 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+
+#include <QStandardItemModel>
+#include <QAbstractItemModelTester>
+#include <QSignalSpy>
+#include <QQmlExpression>
+#include <QtQml/qqmlcomponent.h>
+#include <QtQuickTestUtils/private/qmlutils_p.h>
+#include <QtQmlModels/private/qqmlsortfilterproxymodel_p.h>
+#include <QtQmlModels/private/qqmlvaluefilter_p.h>
+#include <QtQmlModels/private/qqmlfunctionfilter_p.h>
+#include <QtQmlModels/private/qqmlstringsorter_p.h>
+#include <QtQmlModels/private/qqmlfunctionsorter_p.h>
+
+class tst_QQmlSortFilterProxyModel : public QQmlDataTest
+{
+ Q_OBJECT
+
+public:
+ tst_QQmlSortFilterProxyModel() : QQmlDataTest(QT_QMLTEST_DATADIR) {}
+
+private slots:
+ void checkProxyModel();
+
+ void validateSfpmFilterListProperty();
+ void validateSfpmSorterListProperty();
+
+ void validateFilters();
+ void multipleFilters();
+ void filterAbility();
+ void filterInverted();
+ void validateValueFilterProperties();
+ void validateFunctionFilterProperties();
+ void verifyDynamicSortFilterProperty();
+
+ void validateSorters_data();
+ void validateSorters();
+ void multipleSorters_data();
+ void multipleSorters();
+ void sorterAbility_data();
+ void sorterAbility();
+ void validateRoleSorterProperties_data();
+ void validateRoleSorterProperties();
+ void validateStringSorterProperties_data();
+ void validateStringSorterProperties();
+ void validateFunctionSorterProperties_data();
+ void validateFunctionSorterProperties();
+
+ void primarySorter_data();
+ void primarySorter();
+};
+
+class CustomTableModel : public QAbstractTableModel
+{
+ Q_OBJECT
+public:
+ static const int s_rowCount = 5;
+ static const int s_columnCount = 3;
+ CustomTableModel(QObject *parent = nullptr) : QAbstractTableModel(parent) {
+ m_data.resize(s_rowCount);
+ for (int r = 0; r < s_rowCount; ++r) {
+ auto &curRow = m_data[r];
+ curRow.resize(s_columnCount);
+ for (int c = 0; c < s_columnCount; ++c)
+ m_data[r][c] = QString::fromLatin1("Data") + QString::number(r) + QString::number(c);
+ }
+ }
+ QHash<int, QByteArray> roleNames() const override {
+ QHash<int, QByteArray> roles;
+ for (int columnIndex = 0; columnIndex < s_columnCount; columnIndex++)
+ roles.insertOrAssign(Qt::UserRole + columnIndex,
+ QString(QString::fromLatin1("column") + QString::number(columnIndex)).toUtf8());
+ return roles;
+ }
+ int rowCount(const QModelIndex &parent = QModelIndex()) const override {
+ return parent.isValid() ? 0 : m_data.count();
+ }
+ int columnCount(const QModelIndex &parent = QModelIndex()) const override {
+ return parent.isValid() ? 0 : s_columnCount;
+ }
+ // 0, 1, 2 =>
+ // if row == 0:
+ // first = 0
+ // last = count
+ // else if (row == rowCount())
+ // first = rowCount - 1
+ // last = rowCount() - 1 + count
+ // else
+ // first = row
+ // last = row + count
+ bool insertRows(int row, int count, const QModelIndex &parent = QModelIndex()) override {
+ int first = (row ? (row < rowCount() ? row : rowCount() - 1) : count - 1);
+ int last = (row ? (row < rowCount() ? row + count - 1: rowCount() - 1 + count - 1) : count - 1);
+ beginInsertRows(parent, first, last);
+ int totalRowCount = rowCount() + count;
+ m_data.resize(totalRowCount);
+ for (int r = 0; r < totalRowCount; ++r) {
+ auto &curRow = m_data[r];
+ curRow.resize(columnCount());
+ for (int c = 0; c < columnCount(); ++c)
+ m_data[r][c] = QString::fromLatin1("Data") + QString::number(r) + QString::number(c);
+ }
+ endInsertRows();
+ return true;
+ }
+ QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override {
+ if ((role < Qt::UserRole && role >= Qt::UserRole + s_columnCount) || !index.isValid() || (index.column() != (role - Qt::UserRole)))
+ return QVariant();
+ return m_data[index.row()][role - Qt::UserRole];
+ }
+ bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override {
+ if ((role < Qt::UserRole && role >= Qt::UserRole + s_columnCount) || !index.isValid() || (index.column() != (role - Qt::UserRole)))
+ return false;
+ m_data[index.row()][role - Qt::UserRole] = value;
+ dataChanged(index, index, {role});
+ return true;
+ }
+ QList<QList<QVariant>> m_data;
+};
+
+class ExpressionObject : public QObject
+{
+ Q_OBJECT
+ Q_PROPERTY(QQmlScriptString scriptString READ scriptString WRITE setScriptString)
+
+public:
+ ExpressionObject(QObject *parent = nullptr) : QObject(parent) {}
+
+ QQmlScriptString scriptString() const { return m_scriptString; }
+ void setScriptString(QQmlScriptString scriptString) { m_scriptString = scriptString; }
+
+private:
+ QQmlScriptString m_scriptString;
+};
+
+void tst_QQmlSortFilterProxyModel::checkProxyModel()
+{
+ std::unique_ptr<QStandardItemModel> standardModel(new QStandardItemModel(2, 2, this));
+ std::unique_ptr<QQmlSortFilterProxyModel> proxyModel(new QQmlSortFilterProxyModel(this));
+
+ QVariant sourceModel = QVariant::fromValue(standardModel.get());
+ // Set the standard model as the source model to the proxy
+ proxyModel->setModel(sourceModel);
+
+ // Test proxy model to ensure that it abides by the rules of item models
+ // QAbstractItemModelTester verifies it internally when its created
+ {
+ std::unique_ptr<QAbstractItemModelTester> abstractModelTest(new QAbstractItemModelTester(proxyModel.get(), this));
+ }
+
+ // Set data in the source model and verify the same can be accessed through the proxy model
+ for (int row = 0; row < standardModel->rowCount(); ++row) {
+ for (int column = 0; column < standardModel->columnCount(); ++column) {
+ QString testData;
+ testData += QString("Data") + QString::number(row) + QString::number(column);
+ standardModel->setData(standardModel->index(row, column), testData, Qt::DisplayRole);
+ }
+ }
+
+ QVariant modelVar = QVariant::fromValue(standardModel.get());
+ // Reset the model to the standard model
+ proxyModel->setModel(modelVar);
+ QCOMPARE(proxyModel->rowCount(), standardModel->rowCount());
+ QCOMPARE(proxyModel->columnCount(), standardModel->columnCount());
+ for (int row = 0; row < proxyModel->rowCount(); ++row) {
+ for (int column = 0; column < proxyModel->columnCount(); ++column) {
+ QString testData;
+ testData += QString("Data") + QString::number(row) + QString::number(column);
+ QCOMPARE(testData, proxyModel->data(proxyModel->index(row, column, QModelIndex()), Qt::DisplayRole));
+ }
+ }
+}
+
+void tst_QQmlSortFilterProxyModel::validateSfpmFilterListProperty()
+{
+ QQmlEngine engine;
+ QQmlComponent component(&engine, testFileUrl("sfpmCommon.qml"));
+ QVERIFY2(component.errorString().isEmpty(), component.errorString().toUtf8());
+ QScopedPointer<QObject> object(component.create());
+ QVERIFY(!object.isNull());
+
+ auto *sfpmModel = object->property("sfpmProxyModel").value<QQmlSortFilterProxyModel *>();
+ QVERIFY(sfpmModel);
+ QSignalSpy filterChangedSignal(sfpmModel, SIGNAL(filtersChanged()));
+ QVERIFY(filterChangedSignal.isValid());
+ auto filters = sfpmModel->property("filters").value<QQmlListProperty<QQmlFilterBase>>();
+ auto *valueFilter = object->property("valueFilter").value<QQmlValueFilter *>();
+ QVERIFY(valueFilter);
+
+ // Append filter to the sfpm filters list
+ sfpmModel->filters().append(&filters, valueFilter);
+ QCOMPARE(filterChangedSignal.count(), 1);
+ QCOMPARE(filters.count(&filters), 1);
+
+ auto *functionFilter = object->property("functionFilter0").value<QQmlFunctionFilter *>();
+ QVERIFY(functionFilter);
+ sfpmModel->filters().append(&filters, functionFilter);
+ // Validate the count of the filters in the list
+ QCOMPARE(filterChangedSignal.count(), 2);
+ QCOMPARE(filters.count(&filters), 2);
+
+ // Access the filters in the filter list
+ QCOMPARE(filters.at(&filters, 0), valueFilter);
+ QCOMPARE(filters.at(&filters, 1), functionFilter);
+
+ // Clear the filter list
+ sfpmModel->filters().clear(&filters);
+ QCOMPARE(filterChangedSignal.count(), 3);
+ QCOMPARE(filters.count(&filters), 0);
+}
+
+void tst_QQmlSortFilterProxyModel::validateFilters()
+{
+ QQmlEngine engine;
+ QQmlComponent component(&engine, testFileUrl("sfpmCommon.qml"));
+ QVERIFY2(component.errorString().isEmpty(), component.errorString().toUtf8());
+ QScopedPointer<QObject> object(component.create());
+ QVERIFY(!object.isNull());
+
+ CustomTableModel tableModel;
+ QVariant sourceModel = QVariant::fromValue(&tableModel);
+ QCOMPARE(tableModel.rowCount(), CustomTableModel::s_rowCount);
+ QCOMPARE(tableModel.columnCount(), CustomTableModel::s_columnCount);
+
+ // Set the value filter in Qml SFPM
+ auto *sfpmModel = object->property("sfpmProxyModel").value<QQmlSortFilterProxyModel *>();
+ QVERIFY(sfpmModel);
+ sfpmModel->setModel(sourceModel);
+ auto filters = sfpmModel->property("filters").value<QQmlListProperty<QQmlFilterBase>>();
+
+ auto resetFilters = [sfpmModel, &filters] {
+ sfpmModel->filters().clear(&filters);
+ QCOMPARE(sfpmModel->rowCount(), CustomTableModel::s_rowCount);
+ QCOMPARE(sfpmModel->columnCount(), CustomTableModel::s_columnCount);
+ };
+
+ // Value filter
+ {
+ auto *valueFilter = object->property("valueFilter").value<QQmlValueFilter *>();
+ QVERIFY(valueFilter);
+ valueFilter->setValue(tableModel.data(tableModel.index(0, 0, QModelIndex()), Qt::UserRole));
+ valueFilter->setColumn(0);
+ valueFilter->setRoleName(QString::fromLatin1("column0"));
+
+ sfpmModel->filters().append(&filters, valueFilter);
+ QCOMPARE(sfpmModel->rowCount(), 1);
+ QCOMPARE(sfpmModel->data(sfpmModel->index(0, 0, QModelIndex()), Qt::UserRole),
+ tableModel.data(tableModel.index(0, 0, QModelIndex()), Qt::UserRole));
+ }
+
+ // Function filter
+ {
+ resetFilters();
+ auto *functionFilter = object->property("functionFilter1").value<QQmlFunctionFilter *>();
+ QVERIFY(functionFilter);
+ functionFilter->setProperty("expression", "data.column1 === \"Data01\"");
+
+ sfpmModel->filters().append(&filters, functionFilter);
+ QCOMPARE(sfpmModel->rowCount(), 1);
+ QCOMPARE(sfpmModel->columnCount(), CustomTableModel::s_columnCount);
+ }
+
+ resetFilters();
+}
+
+void tst_QQmlSortFilterProxyModel::multipleFilters()
+{
+ QQmlEngine engine;
+ QQmlComponent component(&engine, testFileUrl("sfpmCommon.qml"));
+ QVERIFY2(component.errorString().isEmpty(), component.errorString().toUtf8());
+ QScopedPointer<QObject> object(component.create());
+ QVERIFY(!object.isNull());
+
+ CustomTableModel tableModel;
+ QVariant sourceModel = QVariant::fromValue(&tableModel);
+ QCOMPARE(tableModel.rowCount(), CustomTableModel::s_rowCount);
+ QCOMPARE(tableModel.columnCount(), CustomTableModel::s_columnCount);
+
+ // Set the value filter in Qml SFPM
+ auto *sfpmModel = object->property("sfpmProxyModel").value<QQmlSortFilterProxyModel *>();
+ QVERIFY(sfpmModel);
+ sfpmModel->setModel(sourceModel);
+ auto *functionFilter = object->property("functionFilter0").value<QQmlFunctionFilter *>();
+ QVERIFY(functionFilter);
+ functionFilter->setProperty("expression", "true");
+
+ auto *valueFilter = object->property("valueFilter").value<QQmlValueFilter *>();
+ QVERIFY(valueFilter);
+ valueFilter->setValue(tableModel.data(tableModel.index(0, 0, QModelIndex()), Qt::UserRole));
+ valueFilter->setColumn(0);
+ valueFilter->setRoleName(QString::fromLatin1("column0"));
+
+ auto filters = sfpmModel->property("filters").value<QQmlListProperty<QQmlFilterBase>>();
+ QSignalSpy filterChangedSignal(sfpmModel, SIGNAL(filtersChanged()));
+ sfpmModel->filters().append(&filters, functionFilter);
+ QCOMPARE(filterChangedSignal.count(), 1);
+ sfpmModel->filters().append(&filters, valueFilter);
+ QCOMPARE(filterChangedSignal.count(), 2);
+ QCOMPARE(sfpmModel->rowCount(), 1);
+ QCOMPARE(sfpmModel->columnCount(), 3);
+}
+
+void tst_QQmlSortFilterProxyModel::filterAbility()
+{
+ QQmlEngine engine;
+ QQmlComponent component(&engine, testFileUrl("sfpmCommon.qml"));
+ QVERIFY2(component.errorString().isEmpty(), component.errorString().toUtf8());
+ QScopedPointer<QObject> object(component.create());
+ QVERIFY(!object.isNull());
+
+ CustomTableModel tableModel;
+ QVariant sourceModel = QVariant::fromValue(&tableModel);
+ QCOMPARE(tableModel.rowCount(), CustomTableModel::s_rowCount);
+ QCOMPARE(tableModel.columnCount(), CustomTableModel::s_columnCount);
+
+ // Set the value filter in Qml SFPM
+ auto *sfpmModel = object->property("sfpmProxyModel").value<QQmlSortFilterProxyModel *>();
+ QVERIFY(sfpmModel);
+ sfpmModel->setModel(sourceModel);
+
+ auto *functionFilter = object->property("functionFilter0").value<QQmlFunctionFilter *>();
+ QVERIFY(functionFilter);
+ // Filter first two rows of data through regex
+ functionFilter->setProperty("expression", "/Data[0-1]{2}/.exec(data.column0) !== null");
+
+ auto *valueFilter = object->property("valueFilter").value<QQmlValueFilter *>();
+ QVERIFY(valueFilter);
+ valueFilter->setValue(tableModel.data(tableModel.index(0, 0, QModelIndex()), Qt::UserRole));
+ valueFilter->setColumn(0);
+ valueFilter->setRoleName(QString::fromLatin1("column0"));
+
+ auto filters = sfpmModel->property("filters").value<QQmlListProperty<QQmlFilterBase>>();
+ QSignalSpy filterChangedSignal(sfpmModel, SIGNAL(filtersChanged()));
+ sfpmModel->filters().append(&filters, functionFilter);
+ QCOMPARE(filterChangedSignal.count(), 1);
+ sfpmModel->filters().append(&filters, valueFilter);
+ QCOMPARE(filterChangedSignal.count(), 2);
+ // Function and role both filters the data
+ QCOMPARE(sfpmModel->rowCount(), 1);
+ QCOMPARE(sfpmModel->columnCount(), 3);
+ // Disable the value filter and check the filtered data
+ valueFilter->setEnabled(false);
+ QCOMPARE(sfpmModel->filters().count(&filters), 2);
+ // Function filter still enabled in the filters list
+ QCOMPARE(sfpmModel->rowCount(), 2);
+ QCOMPARE(sfpmModel->columnCount(), 3);
+}
+
+void tst_QQmlSortFilterProxyModel::filterInverted()
+{
+ QQmlEngine engine;
+ QQmlComponent component(&engine, testFileUrl("sfpmCommon.qml"));
+ QVERIFY2(component.errorString().isEmpty(), component.errorString().toUtf8());
+ QScopedPointer<QObject> object(component.create());
+ QVERIFY(!object.isNull());
+
+ CustomTableModel tableModel;
+ QVariant sourceModel = QVariant::fromValue(&tableModel);
+ QCOMPARE(tableModel.rowCount(), CustomTableModel::s_rowCount);
+ QCOMPARE(tableModel.columnCount(), CustomTableModel::s_columnCount);
+
+ // Set the value filter in Qml SFPM
+ auto *sfpmModel = object->property("sfpmProxyModel").value<QQmlSortFilterProxyModel *>();
+ QVERIFY(sfpmModel);
+ sfpmModel->setModel(sourceModel);
+ auto *valueFilter = object->property("valueFilter").value<QQmlValueFilter *>();
+ QVERIFY(valueFilter);
+ valueFilter->setValue(tableModel.data(tableModel.index(0, 0, QModelIndex()), Qt::UserRole));
+ valueFilter->setColumn(0);
+ valueFilter->setRoleName(QString::fromLatin1("column0"));
+ valueFilter->setInvert(true);
+ auto filters = sfpmModel->property("filters").value<QQmlListProperty<QQmlFilterBase>>();
+ QSignalSpy filterChangedSignal(sfpmModel, SIGNAL(filtersChanged()));
+ sfpmModel->filters().append(&filters, valueFilter);
+ QCOMPARE(filterChangedSignal.count(), 1);
+ QCOMPARE(sfpmModel->rowCount(), 4);
+ QCOMPARE(sfpmModel->columnCount(), CustomTableModel::s_columnCount);
+}
+
+void tst_QQmlSortFilterProxyModel::validateValueFilterProperties()
+{
+ QQmlEngine engine;
+ QQmlComponent component(&engine, testFileUrl("sfpmCommon.qml"));
+ QVERIFY2(component.errorString().isEmpty(), component.errorString().toUtf8());
+ QScopedPointer<QObject> object(component.create());
+ QVERIFY(!object.isNull());
+
+ CustomTableModel tableModel;
+ QVariant sourceModel = QVariant::fromValue(&tableModel);
+ QCOMPARE(tableModel.rowCount(), CustomTableModel::s_rowCount);
+ QCOMPARE(tableModel.columnCount(), CustomTableModel::s_columnCount);
+
+ // Set the value filter in Qml SFPM
+ auto *sfpmModel = object->property("sfpmProxyModel").value<QQmlSortFilterProxyModel *>();
+ QVERIFY(sfpmModel);
+ sfpmModel->setModel(sourceModel);
+ auto filters = sfpmModel->property("filters").value<QQmlListProperty<QQmlFilterBase>>();
+ auto *valueFilter = object->property("valueFilter").value<QQmlValueFilter *>();
+ QVERIFY(valueFilter);
+ sfpmModel->filters().append(&filters, valueFilter);
+ QCOMPARE(sfpmModel->filters().count(&filters), 1);
+
+ QSignalSpy valueChngdSpy(valueFilter, &QQmlValueFilter::valueChanged);
+ QVERIFY(valueChngdSpy.isValid());
+ QSignalSpy roleNameChangedSpy(valueFilter, &QQmlValueFilter::roleNameChanged);
+ QVERIFY(roleNameChangedSpy.isValid());
+ QSignalSpy columnChangedSpy(valueFilter, &QQmlValueFilter::columnChanged);
+ QVERIFY(columnChangedSpy.isValid());
+
+ valueFilter->setRoleName(QString::fromLatin1("column1"));
+ QCOMPARE(roleNameChangedSpy.count(), 1);
+ valueFilter->setColumn(1);
+ QCOMPARE(columnChangedSpy.count(), 1);
+ valueFilter->setValue(tableModel.data(tableModel.index(1, 1, QModelIndex()), Qt::UserRole + 1));
+ QCOMPARE(valueChngdSpy.count(), 1);
+ QCOMPARE(sfpmModel->rowCount(), 1);
+ QCOMPARE(sfpmModel->columnCount(), CustomTableModel::s_columnCount);
+ QCOMPARE(sfpmModel->data(sfpmModel->index(0, 1, QModelIndex()), Qt::UserRole + 1),
+ tableModel.data(tableModel.index(1, 1, QModelIndex()), Qt::UserRole + 1));
+
+ valueFilter->setRoleName(QString::fromLatin1("column2"));
+ QCOMPARE(roleNameChangedSpy.count(), 2);
+ QCOMPARE(sfpmModel->rowCount(), 0);
+ QCOMPARE(sfpmModel->columnCount(), CustomTableModel::s_columnCount);
+ valueFilter->setValue(tableModel.data(tableModel.index(2, 2, QModelIndex()), Qt::UserRole + 2));
+ QCOMPARE(valueChngdSpy.count(), 2);
+ valueFilter->setColumn(2);
+ QCOMPARE(columnChangedSpy.count(), 2);
+ QCOMPARE(sfpmModel->rowCount(), 1);
+ QCOMPARE(sfpmModel->columnCount(), CustomTableModel::s_columnCount);
+ QCOMPARE(sfpmModel->data(sfpmModel->index(0, 2, QModelIndex()), Qt::UserRole + 2),
+ tableModel.data(tableModel.index(2, 2, QModelIndex()), Qt::UserRole + 2));
+
+ valueFilter->setColumn(-1);
+ QCOMPARE(sfpmModel->rowCount(), 1);
+ QCOMPARE(sfpmModel->columnCount(), CustomTableModel::s_columnCount);
+ QCOMPARE(sfpmModel->data(sfpmModel->index(0, 2, QModelIndex()), Qt::UserRole + 2),
+ tableModel.data(tableModel.index(2, 2, QModelIndex()), Qt::UserRole + 2));
+}
+
+void tst_QQmlSortFilterProxyModel::validateFunctionFilterProperties()
+{
+ QQmlEngine engine;
+ QQmlComponent component(&engine, testFileUrl("sfpmCommon.qml"));
+ QVERIFY2(component.errorString().isEmpty(), component.errorString().toUtf8());
+ QScopedPointer<QObject> object(component.create());
+ QVERIFY(!object.isNull());
+
+ CustomTableModel tableModel;
+ QVariant sourceModel = QVariant::fromValue(&tableModel);
+ QCOMPARE(tableModel.rowCount(), CustomTableModel::s_rowCount);
+ QCOMPARE(tableModel.columnCount(), CustomTableModel::s_columnCount);
+
+ // Set the value filter in Qml SFPM
+ auto *sfpmModel = object->property("sfpmProxyModel").value<QQmlSortFilterProxyModel *>();
+ QVERIFY(sfpmModel);
+ sfpmModel->setModel(sourceModel);
+ auto filters = sfpmModel->property("filters").value<QQmlListProperty<QQmlFilterBase>>();
+
+ auto *functionFilter = object->property("functionFilter1").value<QQmlFunctionFilter *>();
+ QVERIFY(functionFilter);
+ functionFilter->setProperty("expression", "true");
+
+ filters.append(&filters, functionFilter);
+ QCOMPARE(sfpmModel->rowCount(), CustomTableModel::s_rowCount);
+ QCOMPARE(sfpmModel->columnCount(), CustomTableModel::s_columnCount);
+
+ functionFilter->setProperty("expression", "data.column1 === \"Data01\"");
+ // Need to reset here because the model does not see the change in the filter's expression.
+ filters.clear(&filters);
+ filters.append(&filters, functionFilter);
+
+ QCOMPARE(sfpmModel->rowCount(), 1);
+ QCOMPARE(sfpmModel->columnCount(), CustomTableModel::s_columnCount);
+}
+
+void tst_QQmlSortFilterProxyModel::verifyDynamicSortFilterProperty()
+{
+ QQmlEngine engine;
+ QQmlComponent component(&engine, testFileUrl("sfpmCommon.qml"));
+ QVERIFY2(component.errorString().isEmpty(), component.errorString().toUtf8());
+ QScopedPointer<QObject> object(component.create());
+ QVERIFY(!object.isNull());
+
+ CustomTableModel tableModel;
+ QVariant sourceModel = QVariant::fromValue(&tableModel);
+ QCOMPARE(tableModel.rowCount(), CustomTableModel::s_rowCount);
+ QCOMPARE(tableModel.columnCount(), CustomTableModel::s_columnCount);
+
+ // Set the value filter in Qml SFPM
+ auto *sfpmModel = object->property("sfpmProxyModel").value<QQmlSortFilterProxyModel *>();
+ QVERIFY(sfpmModel);
+ sfpmModel->setModel(sourceModel);
+ QCOMPARE(sfpmModel->rowCount(), CustomTableModel::s_rowCount);
+ QCOMPARE(sfpmModel->columnCount(), CustomTableModel::s_columnCount);
+
+ auto filters = sfpmModel->property("filters").value<QQmlListProperty<QQmlFilterBase>>();
+ QCOMPARE(filters.count(&filters), 0);
+
+ auto *functionFilter = object->property("functionFilter1").value<QQmlFunctionFilter *>();
+ functionFilter->setProperty("expression", "data.column1 === \"Data01\"");
+ QVERIFY(functionFilter);
+ filters.append(&filters, functionFilter);
+ QCOMPARE(filters.count(&filters), 1);
+ QCOMPARE(sfpmModel->rowCount(), 1);
+ QCOMPARE(sfpmModel->columnCount(), CustomTableModel::s_columnCount);
+
+ QCOMPARE(sfpmModel->dynamicSortFilter(), true);
+ sfpmModel->setDynamicSortFilter(false);
+ QCOMPARE(sfpmModel->dynamicSortFilter(), false);
+ tableModel.setData(tableModel.index(0, 1), QString::fromLatin1("Data99"), Qt::UserRole + 1);
+ QCOMPARE(sfpmModel->rowCount(), 1);
+ QCOMPARE(sfpmModel->columnCount(), CustomTableModel::s_columnCount);
+
+ sfpmModel->invalidate();
+ QCOMPARE(sfpmModel->rowCount(), 0);
+}
+
+void tst_QQmlSortFilterProxyModel::validateSfpmSorterListProperty()
+{
+ QQmlEngine engine;
+ QQmlComponent component(&engine, testFileUrl("sfpmCommon.qml"));
+ QVERIFY2(component.errorString().isEmpty(), component.errorString().toUtf8());
+ QScopedPointer<QObject> object(component.create());
+ QVERIFY(!object.isNull());
+
+ auto *sfpmModel = object->property("sfpmProxyModel").value<QQmlSortFilterProxyModel *>();
+ QVERIFY(sfpmModel);
+ QSignalSpy sorterChangedSignal(sfpmModel, SIGNAL(sortersChanged()));
+ QVERIFY(sorterChangedSignal.isValid());
+ auto sorters = sfpmModel->property("sorters").value<QQmlListProperty<QQmlSorterBase>>();
+ auto *roleSorter = object->property("roleSorter").value<QQmlRoleSorter *>();
+ QVERIFY(roleSorter);
+
+ // Append filter to the sfpm filters list
+ sfpmModel->sorters().append(&sorters, roleSorter);
+ QCOMPARE(sorterChangedSignal.count(), 1);
+ QCOMPARE(sorters.count(&sorters), 1);
+
+ auto *stringSorter = object->property("stringSorter").value<QQmlStringSorter *>();
+ QVERIFY(stringSorter);
+ sfpmModel->sorters().append(&sorters, stringSorter);
+ // Validate the count of the filters in the list
+ QCOMPARE(sorterChangedSignal.count(), 2);
+ QCOMPARE(sorters.count(&sorters), 2);
+
+ // Access the filters in the filter list
+ QCOMPARE(sorters.at(&sorters, 0), roleSorter);
+ QCOMPARE(sorters.at(&sorters, 1), stringSorter);
+
+ // Clear the filter list
+ sfpmModel->sorters().clear(&sorters);
+ QCOMPARE(sorterChangedSignal.count(), 3);
+ QCOMPARE(sorters.count(&sorters), 0);
+}
+
+void tst_QQmlSortFilterProxyModel::validateSorters_data()
+{
+ QTest::addColumn<QString>("sorterType");
+ QTest::addColumn<Qt::SortOrder>("sortOrder");
+ QTest::addColumn<QStringList>("initial");
+ QTest::addColumn<QStringList>("expected");
+
+ QTest::newRow("role sorter ascending") << "roleSorter"
+ << Qt::AscendingOrder
+ << QStringList{"canvas", "test", "shine", "beneficiary", "withdrawal"}
+ << QStringList{"beneficiary", "canvas", "shine", "test", "withdrawal"};
+ QTest::newRow("role sorter descending") << "roleSorter"
+ << Qt::DescendingOrder
+ << QStringList{"canvas", "test", "shine", "beneficiary", "withdrawal"}
+ << QStringList{"withdrawal", "test", "shine", "canvas", "beneficiary"};
+
+ QTest::newRow("string sorter ascending") << "stringSorter"
+ << Qt::AscendingOrder
+ << QStringList{"æfgt", "abcd", "zyd", "Æagt", "Øtyu"}
+ << QStringList{"abcd", "zyd", "Æagt", "æfgt", "Øtyu"};
+ QTest::newRow("string sorter descending") << "stringSorter"
+ << Qt::DescendingOrder
+ << QStringList{"æfgt", "abcd", "zyd", "Æagt", "Øtyu"}
+ << QStringList{"Øtyu", "æfgt", "Æagt", "zyd", "abcd"};
+
+
+ QTest::newRow("function sorter ascending") << "functionSorter"
+ << Qt::AscendingOrder
+ << QStringList{"I", "X", "IV", "IX", "V"}
+ << QStringList{"I", "IV", "V", "IX", "X"};
+ QTest::newRow("function sorter descending") << "functionSorter"
+ << Qt::DescendingOrder
+ << QStringList{"I", "X", "IV", "IX", "V"}
+ << QStringList{"X", "IX", "V", "IV", "I"};
+}
+
+void tst_QQmlSortFilterProxyModel::validateSorters()
+{
+ QFETCH(QString, sorterType);
+ QFETCH(Qt::SortOrder, sortOrder);
+ QFETCH(QStringList, initial);
+ QFETCH(QStringList, expected);
+
+ QQmlEngine engine;
+ QQmlComponent component(&engine, testFileUrl("sfpmCommon.qml"));
+ QVERIFY2(component.errorString().isEmpty(), component.errorString().toUtf8());
+ QScopedPointer<QObject> object(component.create());
+ QVERIFY(!object.isNull());
+
+ QStandardItemModel standardModel;
+ for (const auto &data : initial)
+ standardModel.appendRow(new QStandardItem(data));
+
+ auto *sfpmModel = object->property("sfpmProxyModel").value<QQmlSortFilterProxyModel *>();
+ QVERIFY(sfpmModel);
+ QVariant sourceModel = QVariant::fromValue(&standardModel);
+ sfpmModel->setModel(sourceModel);
+ auto sorters = sfpmModel->property("sorters").value<QQmlListProperty<QQmlSorterBase>>();
+ auto *sorter = object->property(sorterType.toLatin1()).value<QQmlSorterBase *>();
+ QVERIFY(sorter);
+ sorter->setSortOrder(sortOrder);
+
+ // Set the language and terrritory for the collation
+ if (auto stringSorter = qobject_cast<QQmlStringSorter *>(sorter))
+ stringSorter->setLocale(QLocale(QLocale::NorwegianBokmal, QLocale::Country::Norway));
+
+ // Create expression on the fly and use it for the function sorter
+ if (auto functionSorter = qobject_cast<QQmlFunctionSorter *>(sorter)) {
+ functionSorter->setProperty("expression",
+ "(sfpmTestObject.getValue(lhsData.display) < sfpmTestObject.getValue(rhsData.display)) ? -1 : \
+ (sfpmTestObject.getValue(lhsData.display) === sfpmTestObject.getValue(rhsData.display)) ? 0 : 1");
+ }
+
+ sorters.append(&sorters, sorter);
+ QCOMPARE(sorters.count(&sorters), 1);
+ int row = 0;
+ for (const auto &data: expected)
+ QCOMPARE(sfpmModel->data(sfpmModel->index(row++, 0, QModelIndex())), data);
+}
+
+void tst_QQmlSortFilterProxyModel::multipleSorters_data()
+{
+ QTest::addColumn<QStringList>("sortersList");
+ QTest::addColumn<Qt::SortOrder>("sortOrder");
+ QTest::addColumn<QList<QVariantList>>("initialColumnData");
+ QTest::addColumn<QList<QVariantList>>("expectedColumnData");
+
+ QTest::newRow("multiple sorters") << QStringList({"roleSorter", "stringSorter"})
+ << Qt::AscendingOrder
+ << QList<QVariantList>({{"abc", "ghi", "def", "xyz", "abc"}, {25, 36, 20, 30, 17}})
+ << QList<QVariantList>({{"abc", "abc", "def", "ghi", "xyz"}, {17, 25, 20, 36, 30}});
+}
+
+void tst_QQmlSortFilterProxyModel::multipleSorters()
+{
+ QFETCH(QStringList, sortersList);
+ QFETCH(Qt::SortOrder, sortOrder);
+ QFETCH(QList<QVariantList>, initialColumnData);
+ QFETCH(QList<QVariantList>, expectedColumnData);
+
+ QQmlEngine engine;
+ QQmlComponent component(&engine, testFileUrl("sfpmCommon.qml"));
+ QVERIFY2(component.errorString().isEmpty(), component.errorString().toUtf8());
+ QScopedPointer<QObject> object(component.create());
+ QVERIFY(!object.isNull());
+
+ QStandardItemModel standardModel(initialColumnData.at(0).count(), initialColumnData.count());
+ int column = -1;
+ for (const auto &columndata : initialColumnData) {
+ int row = 0; ++column;
+ for (const auto &data: columndata)
+ standardModel.setData(standardModel.index(row++, column, QModelIndex()), data, Qt::DisplayRole);
+ }
+
+ auto *sorter1 = object->property(sortersList.at(0).toLatin1()).value<QQmlSorterBase *>(); // RoleSorter
+ QVERIFY(sorter1);
+ sorter1->setSortOrder(sortOrder);
+ sorter1->setColumn(1);
+
+ auto *sorter2 = object->property(sortersList.at(1).toLatin1()).value<QQmlSorterBase *>(); // StringSorter
+ QVERIFY(sorter2);
+ sorter2->setSortOrder(sortOrder);
+ sorter2->setColumn(0);
+
+ auto *sfpmModel = object->property("sfpmProxyModel").value<QQmlSortFilterProxyModel *>();
+ QVERIFY(sfpmModel);
+ QVariant sourceModel = QVariant::fromValue(&standardModel);
+ sfpmModel->setModel(sourceModel);
+ auto sorters = sfpmModel->property("sorters").value<QQmlListProperty<QQmlSorterBase>>();
+ sorters.append(&sorters, sorter2); // String sorter (for column 0)
+ sorters.append(&sorters, sorter1); // Role sorter (for column 1)
+
+ int expcolumn = -1;
+ for (const auto &columndata : expectedColumnData) {
+ int row = 0; ++expcolumn;
+ for (const auto &data: columndata)
+ QCOMPARE(sfpmModel->data(sfpmModel->index(row++, expcolumn, QModelIndex()), Qt::DisplayRole), data);
+ }
+}
+
+void tst_QQmlSortFilterProxyModel::sorterAbility_data()
+{
+ QTest::addColumn<QStringList>("sortersList");
+ QTest::addColumn<QVariantList>("sortersAbility");
+ QTest::addColumn<Qt::SortOrder>("sortOrder");
+ QTest::addColumn<QList<QVariantList>>("initialColumnData");
+ QTest::addColumn<QList<QVariantList>>("expectedColumnData");
+
+ QTest::newRow("enable or disable sorter") << QStringList({"roleSorter", "stringSorter"})
+ << QVariantList({false, true})
+ << Qt::AscendingOrder
+ << QList<QVariantList>({{"abc", "ghi", "def", "xyz", "abc"}, {25, 36, 20, 30, 17}})
+ << QList<QVariantList>({{"abc", "abc", "def", "ghi", "xyz"}, {25, 17, 20, 36, 30}});
+}
+
+void tst_QQmlSortFilterProxyModel::sorterAbility()
+{
+ QFETCH(QStringList, sortersList);
+ QFETCH(Qt::SortOrder, sortOrder);
+ QFETCH(QVariantList, sortersAbility);
+ QFETCH(QList<QVariantList>, initialColumnData);
+ QFETCH(QList<QVariantList>, expectedColumnData);
+
+ QQmlEngine engine;
+ QQmlComponent component(&engine, testFileUrl("sfpmCommon.qml"));
+ QVERIFY2(component.errorString().isEmpty(), component.errorString().toUtf8());
+ QScopedPointer<QObject> object(component.create());
+ QVERIFY(!object.isNull());
+
+ QStandardItemModel standardModel(initialColumnData.at(0).count(), initialColumnData.count());
+ int column = -1;
+ for (const auto &columnData : initialColumnData) {
+ int row = 0; ++column;
+ for (const auto &data: columnData)
+ standardModel.setData(standardModel.index(row++, column, QModelIndex()), data, Qt::DisplayRole);
+ }
+
+ auto *sfpmModel = object->property("sfpmProxyModel").value<QQmlSortFilterProxyModel *>();
+ QVERIFY(sfpmModel);
+ QVariant sourceModel = QVariant::fromValue(&standardModel);
+ sfpmModel->setModel(sourceModel);
+
+ auto sorters = sfpmModel->property("sorters").value<QQmlListProperty<QQmlSorterBase>>();
+ auto *sorter1 = object->property(sortersList.at(0).toLatin1()).value<QQmlSorterBase *>();
+ QVERIFY(sorter1);
+ sorter1->setSortOrder(sortOrder);
+ auto *sorter2 = object->property(sortersList.at(1).toLatin1()).value<QQmlSorterBase *>();
+ QVERIFY(sorter2);
+ sorter2->setSortOrder(sortOrder);
+
+ sorters.append(&sorters, sorter2);
+ sorters.append(&sorters, sorter1);
+
+ // Disable the role sorter and thus sorting shall be restored to the previous state
+ if (!sortersAbility.at(0).value<bool>())
+ sorter1->setEnabled(false);
+
+ column = -1;
+ for (const auto &columnData : expectedColumnData) {
+ int row = 0; ++column;
+ for (const auto &data: columnData)
+ QCOMPARE(sfpmModel->data(sfpmModel->index(row++, column, QModelIndex()), Qt::DisplayRole), data);
+ }
+}
+
+void tst_QQmlSortFilterProxyModel::validateRoleSorterProperties_data()
+{
+ QTest::addColumn<QString>("sorterType");
+ QTest::addColumn<Qt::SortOrder>("sortOrder");
+ QTest::addColumn<int>("sortColumn");
+ QTest::addColumn<bool>("sorterAbility");
+ QTest::addColumn<QList<QVariantList>>("initialColumnData");
+ QTest::addColumn<QList<QVariantList>>("expectedColumnData");
+
+ QTest::newRow("role sorter enabled_ascend_column0") << "roleSorter"
+ << Qt::AscendingOrder << 0 << true
+ << QList<QVariantList>({{"abc", "ghi", "def", "xyz", "abc"}, {25, 36, 20, 30, 17}})
+ << QList<QVariantList>({{"abc", "abc", "def", "ghi", "xyz"}, {25, 17, 20, 36, 30}});
+
+ QTest::newRow("role sorter enabled_descend_column1") << "roleSorter"
+ << Qt::DescendingOrder << 1 << true
+ << QList<QVariantList>({{"abc", "ghi", "def", "xyz", "abc"}, {25, 36, 20, 30, 17}})
+ << QList<QVariantList>({{"ghi", "xyz", "abc", "def", "abc"}, {36, 30, 25, 20, 17}});
+
+ QTest::newRow("role sorter disabled_descend_column0") << "roleSorter"
+ << Qt::AscendingOrder << 0 << false
+ << QList<QVariantList>({{"abc", "ghi", "def", "xyz", "abc"}, {25, 36, 20, 30, 17}})
+ << QList<QVariantList>({{"abc", "ghi", "def", "xyz", "abc"}, {25, 36, 20, 30, 17}});
+}
+
+void tst_QQmlSortFilterProxyModel::validateRoleSorterProperties()
+{
+ QFETCH(QString, sorterType);
+ QFETCH(Qt::SortOrder, sortOrder);
+ QFETCH(int, sortColumn);
+ QFETCH(bool, sorterAbility);
+ QFETCH(QList<QVariantList>, initialColumnData);
+ QFETCH(QList<QVariantList>, expectedColumnData);
+
+ QQmlEngine engine;
+ QQmlComponent component(&engine, testFileUrl("sfpmCommon.qml"));
+ QVERIFY2(component.errorString().isEmpty(), component.errorString().toUtf8());
+ QScopedPointer<QObject> object(component.create());
+ QVERIFY(!object.isNull());
+
+ QStandardItemModel standardModel(initialColumnData.at(0).count(), initialColumnData.count());
+ int column = -1;
+ for (const auto &columnData: initialColumnData) {
+ int row = 0; ++column;
+ for (const auto &data: columnData)
+ standardModel.setData(standardModel.index(row++, column, QModelIndex()), data, Qt::DisplayRole);
+ }
+
+ auto *sorter = object->property(sorterType.toLatin1()).value<QQmlSorterBase *>();
+ QVERIFY(sorter);
+ sorter->setColumn(sortColumn);
+ sorter->setSortOrder(sortOrder);
+ sorter->setEnabled(sorterAbility);
+
+ auto *sfpmModel = object->property("sfpmProxyModel").value<QQmlSortFilterProxyModel *>();
+ QVERIFY(sfpmModel);
+ QVariant sourceModel = QVariant::fromValue(&standardModel);
+ sfpmModel->setModel(sourceModel);
+
+ auto sorters = sfpmModel->property("sorters").value<QQmlListProperty<QQmlSorterBase>>();
+ sorters.append(&sorters, sorter);
+
+ column = -1;
+ for (const auto &columnData : expectedColumnData) {
+ int row = 0; ++column;
+ for (const auto &data: columnData)
+ QCOMPARE(sfpmModel->data(sfpmModel->index(row++, column, QModelIndex()), Qt::DisplayRole), data);
+ }
+}
+
+void tst_QQmlSortFilterProxyModel::validateStringSorterProperties_data()
+{
+ QTest::addColumn<QString>("sorterType");
+ QTest::addColumn<bool>("ignorePunctuation");
+ QTest::addColumn<QString>("locale");
+ QTest::addColumn<bool>("numericMode");
+
+ QTest::addColumn<QVariantList>("initialColumnData");
+ QTest::addColumn<QVariantList>("expectedColumnData");
+
+ QTest::newRow("string sorter incorrect locale") << "stringSorter"
+ << false << "en_GB" << false
+ << QVariantList({"æfgt", "abcd", "zyd", "Æagt", "Øtyu"})
+ << QVariantList({"abcd", "Æagt", "æfgt", "Øtyu", "zyd"});
+
+ QTest::newRow("string sorter correct locale") << "stringSorter"
+ << false << "nb_NO" << false
+ << QVariantList({"æfgt", "abcd", "zyd", "Æagt", "Øtyu"})
+ << QVariantList({"abcd", "zyd", "Æagt", "æfgt", "Øtyu"});
+
+ QTest::newRow("string sorter ignore punctuation") << "stringSorter"
+ << true << "nb_NO" << false
+ << QVariantList({"æfgt", "&abcd", "!zyd", "Æagt", "Øtyu"})
+ << QVariantList({"&abcd", "!zyd", "Æagt", "æfgt", "Øtyu"});
+
+ QTest::newRow("string sorter enable punctuation") << "stringSorter"
+ << false << "nb_NO" << false
+ << QVariantList({"æfgt", "&abcd", "!zyd", "Æagt", "Øtyu"})
+ << QVariantList({"!zyd", "&abcd", "Æagt", "æfgt", "Øtyu"});
+
+ QTest::newRow("string sorter enable numeral mode") << "stringSorter"
+ << false << "nb_NO" << true
+ << QVariantList({"æ97fgt", "1000abcd", "30zyd", "100Æagt", "99Øtyu"})
+ << QVariantList({"30zyd", "99Øtyu", "100Æagt", "1000abcd", "æ97fgt"});
+
+ QTest::newRow("string sorter unset numeral mode") << "stringSorter"
+ << false << "nb_NO" << false
+ << QVariantList({"æ97fgt", "1000abcd", "30zyd", "100Æagt", "99Øtyu"})
+ << QVariantList({"1000abcd", "100Æagt", "30zyd", "99Øtyu", "æ97fgt"});
+}
+
+void tst_QQmlSortFilterProxyModel::validateStringSorterProperties()
+{
+ QFETCH(QString, sorterType);
+ QFETCH(bool, ignorePunctuation);
+ QFETCH(QString, locale);
+ QFETCH(bool, numericMode);
+
+ QFETCH(QVariantList, initialColumnData);
+ QFETCH(QVariantList, expectedColumnData);
+
+ QQmlEngine engine;
+ QQmlComponent component(&engine, testFileUrl("sfpmCommon.qml"));
+ QVERIFY2(component.errorString().isEmpty(), component.errorString().toUtf8());
+ QScopedPointer<QObject> object(component.create());
+ QVERIFY(!object.isNull());
+
+ QStandardItemModel standardModel(initialColumnData.count(), 1);
+ int row = -1;
+ for (const auto &data: initialColumnData)
+ standardModel.setData(standardModel.index(++row, 0, QModelIndex()), data, Qt::DisplayRole);
+
+ auto *sfpmModel = object->property("sfpmProxyModel").value<QQmlSortFilterProxyModel *>();
+ QVERIFY(sfpmModel);
+ QVariant sourceModel = QVariant::fromValue(&standardModel);
+ sfpmModel->setModel(sourceModel);
+
+ auto *sorter = object->property(sorterType.toLatin1()).value<QQmlStringSorter *>();
+ QVERIFY(sorter);
+ sorter->setIgnorePunctuation(ignorePunctuation);
+ sorter->setNumericMode(numericMode);
+ sorter->setLocale(QLocale(locale));
+
+ auto sorters = sfpmModel->property("sorters").value<QQmlListProperty<QQmlSorterBase>>();
+ sorters.append(&sorters, sorter);
+
+ row = -1;
+ for (const auto &data: expectedColumnData)
+ QCOMPARE(sfpmModel->data(sfpmModel->index(++row, 0, QModelIndex()), Qt::DisplayRole), data);
+}
+
+void tst_QQmlSortFilterProxyModel::validateFunctionSorterProperties_data()
+{
+ QTest::addColumn<QString>("sorterType");
+ QTest::addColumn<QString>("expression");
+ QTest::addColumn<QVariantList>("initialColumnData");
+ QTest::addColumn<QVariantList>("expectedColumnData");
+
+ QTest::newRow("function sorter no role data")
+ << "functionSorter"
+ << "(lhsData.display < rhsData.display) ? -1 : 1"
+ << QVariantList({"V", "IV", "X", "IX", "III"})
+ << QVariantList({"III", "IV", "IX", "V", "X"});
+
+ QTest::newRow("function sorter access incorrect role names")
+ << "functionSorter"
+ << "(lhsData.display1 < rhsData.display2) ? -1 : 1"
+ << QVariantList({"V", "IV", "X", "IX", "III"})
+ << QVariantList({"V", "IV", "X", "IX", "III"});
+
+ QTest::newRow("function sorter access valid role name")
+ << "functionSorter"
+ << "(sfpmTestObject.getValue(lhsData.display) < sfpmTestObject.getValue(rhsData.display) ? 1 : -1)"
+ << QVariantList({"V", "IV", "X", "IX", "III"})
+ << QVariantList({"X", "IX", "V", "IV", "III"});
+}
+
+void tst_QQmlSortFilterProxyModel::validateFunctionSorterProperties()
+{
+ QFETCH(QString, sorterType);
+ QFETCH(QString, expression);
+ QFETCH(QVariantList, initialColumnData);
+ QFETCH(QVariantList, expectedColumnData);
+
+ QQmlEngine engine;
+ QQmlComponent component(&engine, testFileUrl("sfpmCommon.qml"));
+ QVERIFY2(component.errorString().isEmpty(), component.errorString().toUtf8());
+ QScopedPointer<QObject> object(component.create());
+ QVERIFY(!object.isNull());
+
+ QStandardItemModel standardModel(initialColumnData.count(), 1);
+ int row = -1;
+ for (const auto &data: initialColumnData)
+ standardModel.setData(standardModel.index(++row, 0, QModelIndex()), data, Qt::DisplayRole);
+
+ auto *sfpmModel = object->property("sfpmProxyModel").value<QQmlSortFilterProxyModel *>();
+ QVERIFY(sfpmModel);
+ QVariant sourceModel = QVariant::fromValue(&standardModel);
+ sfpmModel->setModel(sourceModel);
+
+ auto *sorter = object->property(sorterType.toLatin1()).value<QQmlFunctionSorter *>();
+ QVERIFY(sorter);
+
+ // Custom qml property specifically for testing
+ if (!expression.isEmpty())
+ sorter->setProperty("expression", expression);
+
+ auto sorters = sfpmModel->property("sorters").value<QQmlListProperty<QQmlSorterBase>>();
+ sorters.append(&sorters, sorter);
+
+ row = -1;
+ for (const auto &data: expectedColumnData)
+ QCOMPARE(sfpmModel->data(sfpmModel->index(++row, 0, QModelIndex()), Qt::DisplayRole), data);
+}
+
+void tst_QQmlSortFilterProxyModel::primarySorter_data()
+{
+ QTest::addColumn<QString>("sorter");
+ QTest::addColumn<QString>("primarySorter");
+ QTest::addColumn<int>("primarySortColumn");
+ QTest::addColumn<Qt::SortOrder>("sortOrder");
+ QTest::addColumn<QList<QVariantList>>("initialColumnData");
+ QTest::addColumn<QList<QVariantList>>("expectedColumnData");
+
+ QTest::newRow("multiple sorters") << "stringSorter"
+ << "roleSorter"
+ << 1
+ << Qt::AscendingOrder
+ << QList<QVariantList>({{"abc", "ghi", "def", "xyz", "abc"}, {25, 36, 20, 30, 17}})
+ << QList<QVariantList>({{"abc", "def", "abc", "xyz", "ghi"}, {17, 20, 25, 30, 36}});
+}
+
+void tst_QQmlSortFilterProxyModel::primarySorter()
+{
+ QFETCH(QString, sorter);
+ QFETCH(QString, primarySorter);
+ QFETCH(int, primarySortColumn);
+ QFETCH(Qt::SortOrder, sortOrder);
+ QFETCH(QList<QVariantList>, initialColumnData);
+ QFETCH(QList<QVariantList>, expectedColumnData);
+
+ QQmlEngine engine;
+ QQmlComponent component(&engine, testFileUrl("sfpmCommon.qml"));
+ QVERIFY2(component.errorString().isEmpty(), component.errorString().toUtf8());
+ QScopedPointer<QObject> object(component.create());
+ QVERIFY(!object.isNull());
+
+ QStandardItemModel standardModel(initialColumnData.at(0).count(), initialColumnData.count());
+ int column = -1;
+ for (const auto &columnData : initialColumnData) {
+ int row = 0; ++column;
+ for (const auto &data: columnData)
+ standardModel.setData(standardModel.index(row++, column, QModelIndex()), data, Qt::DisplayRole);
+ }
+
+ auto *sorter1 = object->property(sorter.toLatin1()).value<QQmlSorterBase *>(); // String Sorter
+ QVERIFY(sorter1);
+ sorter1->setSortOrder(sortOrder);
+ sorter1->setColumn(0);
+ sorter1->setPriority(0);
+
+ auto *primarySorter1 = object->property(primarySorter.toLatin1()).value<QQmlSorterBase *>(); // Role Sorter as the primary sorter
+ QVERIFY(primarySorter1);
+ primarySorter1->setSortOrder(sortOrder);
+ primarySorter1->setColumn(primarySortColumn);
+ primarySorter1->setPriority(1);
+
+ auto *sfpmModel = object->property("sfpmProxyModel").value<QQmlSortFilterProxyModel *>();
+ QVERIFY(sfpmModel);
+ QVariant sourceModel = QVariant::fromValue(&standardModel);
+ sfpmModel->setModel(sourceModel);
+ auto sorters = sfpmModel->property("sorters").value<QQmlListProperty<QQmlSorterBase>>();
+ sorters.append(&sorters, sorter1);
+ sorters.append(&sorters, primarySorter1);
+
+ sfpmModel->setPrimarySorter(primarySorter1);
+
+ column = -1;
+ for (const auto &columnData : expectedColumnData) {
+ int row = 0; ++column;
+ for (const auto &data: columnData)
+ QCOMPARE(sfpmModel->data(sfpmModel->index(row++, column, QModelIndex()), Qt::DisplayRole), data);
+ }
+}
+
+QTEST_MAIN(tst_QQmlSortFilterProxyModel)
+
+#include "tst_qqmlsortfilterproxymodel.moc"