Provide a way to sieve data in QML through the SortFilterProxyModel

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 <jan-arve.saether@qt.io>
This commit is contained in:
Jan Arve Sæther 2025-05-29 12:13:57 +02:00 committed by Santhosh Kumar
parent 5b17c03634
commit 9efc1fb4ac
32 changed files with 6176 additions and 0 deletions

View File

@ -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
)

View File

@ -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

View File

@ -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]

View File

@ -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"

View File

@ -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

View File

@ -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"

View File

@ -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

View File

@ -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"

View File

@ -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

View File

@ -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"

View File

@ -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

View File

@ -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"

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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"

View File

@ -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

View File

@ -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"

View File

@ -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

View File

@ -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"

View File

@ -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

View File

@ -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"

View File

@ -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

View File

@ -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"

View File

@ -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

View File

@ -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)

View File

@ -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"
)

View File

@ -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];
}

View File

@ -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 {}
}

File diff suppressed because it is too large Load Diff