diff --git a/src/qmlmodels/CMakeLists.txt b/src/qmlmodels/CMakeLists.txt index dd3c3dec0e..29ab77c793 100644 --- a/src/qmlmodels/CMakeLists.txt +++ b/src/qmlmodels/CMakeLists.txt @@ -73,6 +73,24 @@ qt_internal_extend_target(QmlModels CONDITION QT_FEATURE_qml_delegate_model qquickpackage.cpp qquickpackage_p.h ) +qt_internal_extend_target(QmlModels CONDITION QT_FEATURE_qml_sfpm_model AND QT_FEATURE_proxymodel + SOURCES + sfpm/qqmlsortfilterproxymodel_p.h sfpm/qqmlsortfilterproxymodel.cpp + sfpm/qsortfilterproxymodelhelper_p.h sfpm/qsortfilterproxymodelhelper.cpp + # sfpm filter specific source files + sfpm/filters/qqmlfilterbase_p.h sfpm/filters/qqmlfilterbase.cpp + sfpm/filters/qqmlfiltercompositor_p.h sfpm/filters/qqmlfiltercompositor.cpp + sfpm/filters/qqmlrolefilter_p.h sfpm/filters/qqmlrolefilter.cpp + sfpm/filters/qqmlvaluefilter_p.h sfpm/filters/qqmlvaluefilter.cpp + sfpm/filters/qqmlfunctionfilter_p.h sfpm/filters/qqmlfunctionfilter.cpp + # sfpm sorter specific source files + sfpm/sorters/qqmlsorterbase_p.h sfpm/sorters/qqmlsorterbase.cpp + sfpm/sorters/qqmlsortercompositor_p.h sfpm/sorters/qqmlsortercompositor.cpp + sfpm/sorters/qqmlstringsorter_p.h sfpm/sorters/qqmlstringsorter.cpp + sfpm/sorters/qqmlrolesorter_p.h sfpm/sorters/qqmlrolesorter.cpp + sfpm/sorters/qqmlfunctionsorter_p.h sfpm/sorters/qqmlfunctionsorter.cpp +) + qt_internal_add_docs(QmlModels doc/qtqmlmodels.qdocconf ) diff --git a/src/qmlmodels/configure.cmake b/src/qmlmodels/configure.cmake index 6b27201b3d..876dd571ed 100644 --- a/src/qmlmodels/configure.cmake +++ b/src/qmlmodels/configure.cmake @@ -46,7 +46,13 @@ qt_feature("qml-tree-model" PRIVATE PURPOSE "Provides the TreeModel QML type." CONDITION QT_FEATURE_qml_itemmodel AND QT_FEATURE_qml_delegate_model ) +qt_feature("qml-sfpm-model" PRIVATE + SECTION "QML" + LABEL "QML sortfilterproxy model" + PURPOSE "Provides the SortFilterProxyModel QML type." +) qt_configure_add_summary_section(NAME "Qt QML Models") qt_configure_add_summary_entry(ARGS "qml-list-model") qt_configure_add_summary_entry(ARGS "qml-delegate-model") +qt_configure_add_summary_entry(ARGS "qml-sfpm-model") qt_configure_end_summary_section() # end of "Qt QML Models" section diff --git a/src/qmlmodels/doc/snippets/qml/sortfilterproxymodel/qml-sortfilterproxymodel.qml b/src/qmlmodels/doc/snippets/qml/sortfilterproxymodel/qml-sortfilterproxymodel.qml new file mode 100644 index 0000000000..9f7c2e508f --- /dev/null +++ b/src/qmlmodels/doc/snippets/qml/sortfilterproxymodel/qml-sortfilterproxymodel.qml @@ -0,0 +1,57 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +//![0] +import QtQuick +import QtQuick.Controls + +ApplicationWindow { + width: 640 + height: 480 + visible: true + title: qsTr("Sort Filter Proxy Model") + + //! [sfpm-usage] + ListModel { + id: listModel + ListElement { name: "Adan"; age: 25; department: "Process"; pid: 209711; country: "Norway" } + ListElement { name: "Hannah"; age: 48; department: "HR"; pid: 154916; country: "Germany" } + ListElement { name: "Divina"; age: 63; department: "Marketing"; pid: 158038; country: "Spain" } + ListElement { name: "Rohith"; age: 35; department: "Process"; pid: 202582; country: "India" } + ListElement { name: "Latesha"; age: 23; department: "Quality"; pid: 232582; country: "UK" } + } + + SortFilterProxyModel { + id: ageFilterModel + model: listModel + filters: [ + FunctionFilter { + roleData: QtObject { property int age } + function filter(data: QtObject) : bool { + return data.age > 30 + } + } + ] + sorters: [ + RoleSorter { roleName: "department" } + ] + } + + ListView { + anchors.fill: parent + clip: true + model: sfpm + delegate: Rectangle { + implicitWidth: 100 + implicitHeight: 50 + border.width: 1 + Text { + text: name + anchors.centerIn: parent + } + } + } + //! [sfpm-usage] +} + +//![0] diff --git a/src/qmlmodels/sfpm/filters/qqmlfilterbase.cpp b/src/qmlmodels/sfpm/filters/qqmlfilterbase.cpp new file mode 100644 index 0000000000..16b2711f53 --- /dev/null +++ b/src/qmlmodels/sfpm/filters/qqmlfilterbase.cpp @@ -0,0 +1,112 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include +#include + +QT_BEGIN_NAMESPACE + +/*! + \qmltype Filter + \inherits QtObject + \inqmlmodule QtQml.Models + \since 6.10 + \preliminary + \brief Abstract base type providing functionality common to filters. + + Filter provides a set of common properties for all the filters that they + inherit from. +*/ + +QQmlFilterBase::QQmlFilterBase(QQmlFilterBasePrivate *privObj, QObject *parent) + : QObject (*privObj, parent) +{ +} + +/*! + \qmlproperty bool Filter::enabled + + This property enables the \l SortFilterProxyModel to consider this filter + while filtering the model data. + + The default value is \c true. +*/ +bool QQmlFilterBase::enabled() const +{ + Q_D(const QQmlFilterBase); + return d->m_enabled; +} + +void QQmlFilterBase::setEnabled(const bool enabled) +{ + Q_D(QQmlFilterBase); + if (d->m_enabled == enabled) + return; + d->m_enabled = enabled; + invalidate(true); + emit enabledChanged(); +} + +/*! + \qmlproperty bool Filter::invert + + This property inverts the filter, causing the data that would normally be + filtered out to be presented instead. + + The default value is \c false. +*/ +bool QQmlFilterBase::invert() const +{ + Q_D(const QQmlFilterBase); + return d->m_invert; +} + +void QQmlFilterBase::setInvert(const bool invert) +{ + Q_D(QQmlFilterBase); + if (d->m_invert == invert) + return; + d->m_invert = invert; + invalidate(); + emit invertChanged(); +} + +/*! + \qmlproperty int Filter::column + + This property specifies which column in the model the filter should be + applied on. If the value is \c -1, the filter will be applied to all + the columns in the model. + + The default value is \c -1. +*/ +int QQmlFilterBase::column() const +{ + Q_D(const QQmlFilterBase); + return d->m_filterColumn; +} + +void QQmlFilterBase::setColumn(const int column) +{ + Q_D(QQmlFilterBase); + if (d->m_filterColumn == column) + return; + d->m_filterColumn = column; + invalidate(); + emit columnChanged(); +} + +/*! + \internal +*/ +void QQmlFilterBase::invalidate(bool updateCache) +{ + // Update the cached filters and invalidate the model + if (updateCache) + emit invalidateCache(this); + emit invalidateModel(); +} + +QT_END_NAMESPACE + +#include "moc_qqmlfilterbase_p.cpp" diff --git a/src/qmlmodels/sfpm/filters/qqmlfilterbase_p.h b/src/qmlmodels/sfpm/filters/qqmlfilterbase_p.h new file mode 100644 index 0000000000..4baed7e569 --- /dev/null +++ b/src/qmlmodels/sfpm/filters/qqmlfilterbase_p.h @@ -0,0 +1,80 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QQMLFILTERBASE_H +#define QQMLFILTERBASE_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include +#include +#include +#include + +QT_BEGIN_NAMESPACE + +class QQmlSortFilterProxyModel; +class QQmlFilterBasePrivate; + +class Q_QMLMODELS_EXPORT QQmlFilterBase: public QObject +{ + Q_OBJECT + Q_PROPERTY(bool enabled READ enabled WRITE setEnabled NOTIFY enabledChanged FINAL) + Q_PROPERTY(bool invert READ invert WRITE setInvert NOTIFY invertChanged FINAL) + Q_PROPERTY(int column READ column WRITE setColumn NOTIFY columnChanged FINAL) + QML_ELEMENT + QML_UNCREATABLE("") + +public: + explicit QQmlFilterBase(QQmlFilterBasePrivate *privObj, QObject *parent = nullptr); + virtual ~QQmlFilterBase() = default; + + bool enabled() const; + void setEnabled(const bool bEnable); + + bool invert() const; + void setInvert(const bool bInvert); + + int column() const; + void setColumn(const int column); + + virtual bool filterAcceptsRowInternal(int, const QModelIndex&, const QQmlSortFilterProxyModel *) const { return true; } + virtual bool filterAcceptsColumnInternal(int, const QModelIndex&, const QQmlSortFilterProxyModel *) const { return true; } + virtual void update(const QQmlSortFilterProxyModel *) { /* do nothing */ }; + +Q_SIGNALS: + void invalidateModel(); + void invalidateCache(QQmlFilterBase *filter); + void enabledChanged(); + void invertChanged(); + void columnChanged(); + +public slots: + void invalidate(bool updateCache = false); + +private: + Q_DECLARE_PRIVATE(QQmlFilterBase) +}; + +class QQmlFilterBasePrivate: public QObjectPrivate +{ + Q_DECLARE_PUBLIC(QQmlFilterBase) + +private: + bool m_enabled = true; + bool m_invert = false; + int m_filterColumn = -1; +}; + +QT_END_NAMESPACE + +#endif // QQMLFILTERBASE_H diff --git a/src/qmlmodels/sfpm/filters/qqmlfiltercompositor.cpp b/src/qmlmodels/sfpm/filters/qqmlfiltercompositor.cpp new file mode 100644 index 0000000000..0a99fb5ec0 --- /dev/null +++ b/src/qmlmodels/sfpm/filters/qqmlfiltercompositor.cpp @@ -0,0 +1,171 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include +#include + +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(q->parent()); +} + +void QQmlFilterCompositor::append(QQmlListProperty *filterComp, QQmlFilterBase* filter) +{ + auto *filterCompositor = reinterpret_cast (filterComp->object); + filterCompositor->append(filter); +} + +qsizetype QQmlFilterCompositor::count(QQmlListProperty *filterComp) +{ + auto *filterCompositor = reinterpret_cast (filterComp->object); + return filterCompositor->count(); +} + +QQmlFilterBase *QQmlFilterCompositor::at(QQmlListProperty *filterComp, qsizetype index) +{ + auto *filterCompositor = reinterpret_cast (filterComp->object); + return filterCompositor->at(index); +} + +void QQmlFilterCompositor::clear(QQmlListProperty *filterComp) +{ + auto *filterCompositor = reinterpret_cast (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 QQmlFilterCompositor::filters() +{ + Q_D(QQmlFilterCompositor); + return d->m_filters; +} + +QQmlListProperty QQmlFilterCompositor::filtersListProperty() +{ + Q_D(QQmlFilterCompositor); + return QQmlListProperty(reinterpret_cast(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 filters = d->m_filters; + // Cache only the filters that need to be evaluated (in order) + std::copy_if(filters.begin(), filters.end(), std::back_inserter(d->m_effectiveFilters), + [](QQmlFilterBase *filter){ return filter->enabled(); }); + } +} + +bool QQmlFilterCompositor::filterAcceptsRowInternal(int row, const QModelIndex& sourceParent, const QQmlSortFilterProxyModel *proxyModel) const +{ + Q_D(const QQmlFilterCompositor); + // Check the data against the configured filters and if nothing configured, + // dont filter the data + return std::all_of(d->m_effectiveFilters.begin(), d->m_effectiveFilters.end(), + [row, &sourceParent, proxyModel](const QQmlFilterBase *filter) { + const bool filterStatus = filter->filterAcceptsRowInternal(row, sourceParent, proxyModel); + return !(filter->invert()) ? filterStatus : !filterStatus; + }); +} + +bool QQmlFilterCompositor::filterAcceptsColumnInternal(int column, const QModelIndex& sourceParent, const QQmlSortFilterProxyModel *proxyModel) const +{ + Q_D(const QQmlFilterCompositor); + // Check the data against the configured filters and if nothing configured, + // dont filter the data + return std::all_of(d->m_effectiveFilters.begin(), d->m_effectiveFilters.end(), + [column, &sourceParent, proxyModel](const QQmlFilterBase *filter) { + return filter->filterAcceptsColumnInternal(column, sourceParent, proxyModel); + }); +} + +QT_END_NAMESPACE + +#include "moc_qqmlfiltercompositor_p.cpp" diff --git a/src/qmlmodels/sfpm/filters/qqmlfiltercompositor_p.h b/src/qmlmodels/sfpm/filters/qqmlfiltercompositor_p.h new file mode 100644 index 0000000000..b07fdc99b9 --- /dev/null +++ b/src/qmlmodels/sfpm/filters/qqmlfiltercompositor_p.h @@ -0,0 +1,76 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QQMLFILTERCOMPOSITOR_P_H +#define QQMLFILTERCOMPOSITOR_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include + +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 filters(); + QQmlListProperty 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 *filterComp, QQmlFilterBase *filter); + static qsizetype count(QQmlListProperty *filterComp); + static QQmlFilterBase* at(QQmlListProperty *filterComp, qsizetype index); + static void clear(QQmlListProperty *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 m_filters; + // Holds effective filters that will be evaluated with the + // model content + QList m_effectiveFilters; + QQmlSortFilterProxyModel *m_sfpmModel = nullptr; +}; + +QT_END_NAMESPACE + +#endif // QQMLFILTERCOMPOSITOR_P_H diff --git a/src/qmlmodels/sfpm/filters/qqmlfunctionfilter.cpp b/src/qmlmodels/sfpm/filters/qqmlfunctionfilter.cpp new file mode 100644 index 0000000000..4dc09fbbf9 --- /dev/null +++ b/src/qmlmodels/sfpm/filters/qqmlfunctionfilter.cpp @@ -0,0 +1,172 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include +#include +#include +#include +#include + +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(); +} + +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 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(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(this), QMetaObject::InvokeMetaMethod, + d->m_method.methodIndex(), argv); + if (retVal) + return retVal; + } + } + return retVal; +} + +QT_END_NAMESPACE + +#include "moc_qqmlfunctionfilter_p.cpp" diff --git a/src/qmlmodels/sfpm/filters/qqmlfunctionfilter_p.h b/src/qmlmodels/sfpm/filters/qqmlfunctionfilter_p.h new file mode 100644 index 0000000000..dd174e6ceb --- /dev/null +++ b/src/qmlmodels/sfpm/filters/qqmlfunctionfilter_p.h @@ -0,0 +1,58 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QQMLFUNCTIONFILTER_P_H +#define QQMLFUNCTIONFILTER_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include +#include +#include + +QT_BEGIN_NAMESPACE + +class QQmlSortFilterProxyModel; +class QQmlFunctionFilterPrivate; + +class Q_QMLMODELS_EXPORT QQmlFunctionFilter : public QQmlFilterBase, public QQmlParserStatus +{ + Q_OBJECT + Q_INTERFACES(QQmlParserStatus) + QML_NAMED_ELEMENT(FunctionFilter) + +public: + explicit QQmlFunctionFilter(QObject *parent = nullptr); + ~QQmlFunctionFilter() override; + + bool filterAcceptsRowInternal(int row, const QModelIndex& sourceIndex, const QQmlSortFilterProxyModel *) const override; + +private: + void classBegin() override {}; + void componentComplete() override; + +private: + Q_DECLARE_PRIVATE(QQmlFunctionFilter) +}; + +class QQmlFunctionFilterPrivate : public QQmlFilterBasePrivate +{ + Q_DECLARE_PUBLIC (QQmlFunctionFilter) + +public: + QMetaMethod m_method; + mutable QVariant m_parameterData; +}; + +QT_END_NAMESPACE + +#endif // QQMLFUNCTIONFILTER_P_H diff --git a/src/qmlmodels/sfpm/filters/qqmlrolefilter.cpp b/src/qmlmodels/sfpm/filters/qqmlrolefilter.cpp new file mode 100644 index 0000000000..04fcfa478c --- /dev/null +++ b/src/qmlmodels/sfpm/filters/qqmlrolefilter.cpp @@ -0,0 +1,66 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include +#include + +QT_BEGIN_NAMESPACE + +/*! + \qmltype RoleFilter + \inherits Filter + \inqmlmodule QtQml.Models + \since 6.10 + \preliminary + \brief Abstract base type providing functionality to role-dependent filters. +*/ + +QQmlRoleFilter::QQmlRoleFilter(QObject *parent) : + QQmlFilterBase (new QQmlRoleFilterPrivate, parent) +{ +} + +QQmlRoleFilter::QQmlRoleFilter(QQmlFilterBasePrivate *priv, QObject *parent) : + QQmlFilterBase (priv, parent) +{ + +} + +/*! + \qmlproperty string RoleFilter::roleName + + This property holds the role name that will be used to filter the data. + + The default value is the display role. +*/ +const QString& QQmlRoleFilter::roleName() const +{ + Q_D(const QQmlRoleFilter); + return d->m_roleName; +} + +void QQmlRoleFilter::setRoleName(const QString& roleName) +{ + Q_D(QQmlRoleFilter); + if (d->m_roleName == roleName) + return; + d->m_roleName = roleName; + emit roleNameChanged(); + // Invalidate the model + invalidate(); +} + +/*! + \internal +*/ +int QQmlRoleFilter::itemRole(const QQmlSortFilterProxyModel *proxyModel) const +{ + Q_D(const QQmlRoleFilter); + if (!d->m_roleName.isNull()) + return proxyModel->itemRoleForName(d->m_roleName); + return -1; +} + +QT_END_NAMESPACE + +#include "moc_qqmlrolefilter_p.cpp" diff --git a/src/qmlmodels/sfpm/filters/qqmlrolefilter_p.h b/src/qmlmodels/sfpm/filters/qqmlrolefilter_p.h new file mode 100644 index 0000000000..97d6fad707 --- /dev/null +++ b/src/qmlmodels/sfpm/filters/qqmlrolefilter_p.h @@ -0,0 +1,59 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QQMLROLEFILTER_P_H +#define QQMLROLEFILTER_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include + +QT_BEGIN_NAMESPACE + +class QQmlSortFilterProxyModel; +class QQmlRoleFilterPrivate; + +class Q_QMLMODELS_EXPORT QQmlRoleFilter : public QQmlFilterBase +{ + Q_OBJECT + Q_PROPERTY(QString roleName READ roleName WRITE setRoleName NOTIFY roleNameChanged) + QML_UNCREATABLE("") + +public: + explicit QQmlRoleFilter(QObject *parent = nullptr); + QQmlRoleFilter(QQmlFilterBasePrivate *priv, QObject *parent = nullptr); + ~QQmlRoleFilter() = default; + + const QString& roleName() const; + void setRoleName(const QString& roleName); + +Q_SIGNALS: + void roleNameChanged(); + +protected: + int itemRole(const QQmlSortFilterProxyModel *proxyModel) const; + +private: + Q_DECLARE_PRIVATE(QQmlRoleFilter) +}; + +class QQmlRoleFilterPrivate : public QQmlFilterBasePrivate +{ + Q_DECLARE_PUBLIC (QQmlRoleFilter) + +public: + QString m_roleName = QString::fromUtf8("display"); +}; + +QT_END_NAMESPACE + +#endif // QQMLROLEFILTER_P_H diff --git a/src/qmlmodels/sfpm/filters/qqmlvaluefilter.cpp b/src/qmlmodels/sfpm/filters/qqmlvaluefilter.cpp new file mode 100644 index 0000000000..3d373fed9a --- /dev/null +++ b/src/qmlmodels/sfpm/filters/qqmlvaluefilter.cpp @@ -0,0 +1,108 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include +#include + +QT_BEGIN_NAMESPACE + +/*! + \qmltype ValueFilter + \inherits RoleFilter + \inqmlmodule QtQml.Models + \since 6.10 + \preliminary + \brief Filters data in a \l SortFilterProxyModel based on role name and + value. + + ValueFilter allows the user to filter the data according to the role name + or specified value or both as configured in the source model. The role name + used to filter the data shall be based on model + \l{QAbstractItemModel::roleNames()}{role name}. The default value for + role name is \c "display". + + The following snippet shows how ValueFilter can be used to only include + data from the source model where the value of the role name \c "favorite" + is \c "true": + + \qml + SortFilterProxyModel { + model: sourceModel + filters: [ + ValueFilter { + roleName: "favorite" + value: true + } + ] + } + \endqml +*/ + +QQmlValueFilter::QQmlValueFilter(QObject *parent) : + QQmlRoleFilter (new QQmlValueFilterPrivate, parent) +{ + +} + +/*! + \qmlproperty string ValueFilter::value + + This property holds specific value that can be used to filter the data. + + The default value is empty string. +*/ +const QVariant& QQmlValueFilter::value() const +{ + Q_D(const QQmlValueFilter); + return d->m_value; +} + +void QQmlValueFilter::setValue(const QVariant& value) +{ + Q_D(QQmlValueFilter); + if (d->m_value == value) + return; + d->m_value = value; + // Update the model for the change in the role name + emit valueChanged(); + // Invalidate the model for the change in the role name + invalidate(); +} + +void QQmlValueFilter::resetValue() +{ + Q_D(QQmlValueFilter); + d->m_value = QVariant(); +} + +/*! + \internal +*/ +bool QQmlValueFilter::filterAcceptsRowInternal(int row, const QModelIndex& sourceParent, const QQmlSortFilterProxyModel *proxyModel) const +{ + Q_D(const QQmlValueFilter); + if (d->m_roleName.isEmpty()) + return true; + int role = itemRole(proxyModel); + const bool isValidVal = (!d->m_value.isValid() || !d->m_value.isNull()); + if (role > -1) { + if (column() > -1) { + const QModelIndex &index = proxyModel->sourceModel()->index(row, column(), sourceParent); + const QVariant &value = proxyModel->sourceModel()->data(index, role); + return (value.isValid() && (!isValidVal || d->m_value == value)); + } else { + const int columnCount = proxyModel->sourceModel()->columnCount(sourceParent); + for (int column = 0; column < columnCount; column++) { + const QModelIndex &index = proxyModel->sourceModel()->index(row, column, sourceParent); + const QVariant &value = proxyModel->sourceModel()->data(index, role); + if (value.isValid() && (!isValidVal || d->m_value == value)) + return true; + } + } + } + return false; +} + +QT_END_NAMESPACE + +#include "moc_qqmlvaluefilter_p.cpp" diff --git a/src/qmlmodels/sfpm/filters/qqmlvaluefilter_p.h b/src/qmlmodels/sfpm/filters/qqmlvaluefilter_p.h new file mode 100644 index 0000000000..ccffe1928b --- /dev/null +++ b/src/qmlmodels/sfpm/filters/qqmlvaluefilter_p.h @@ -0,0 +1,58 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QQMLVALUEFILTER_P_H +#define QQMLVALUEFILTER_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include + +QT_BEGIN_NAMESPACE + +class QQmlSortFilterProxyModel; +class QQmlValueFilterPrivate; + +class Q_QMLMODELS_EXPORT QQmlValueFilter : public QQmlRoleFilter +{ + Q_OBJECT + Q_PROPERTY(QVariant value READ value WRITE setValue RESET resetValue NOTIFY valueChanged) + QML_NAMED_ELEMENT(ValueFilter) + +public: + explicit QQmlValueFilter(QObject *parent = nullptr); + ~QQmlValueFilter() = default; + + const QVariant& value() const; + void setValue(const QVariant& value); + void resetValue(); + + bool filterAcceptsRowInternal(int row, const QModelIndex& sourceIndex, const QQmlSortFilterProxyModel *) const override; + +Q_SIGNALS: + void valueChanged(); + +private: + Q_DECLARE_PRIVATE(QQmlValueFilter) +}; + +class QQmlValueFilterPrivate : public QQmlRoleFilterPrivate +{ + Q_DECLARE_PUBLIC (QQmlValueFilter) + +public: + QVariant m_value; +}; + +QT_END_NAMESPACE + +#endif // QQMLVALUEFILTER_P_H diff --git a/src/qmlmodels/sfpm/qqmlsortfilterproxymodel.cpp b/src/qmlmodels/sfpm/qqmlsortfilterproxymodel.cpp new file mode 100644 index 0000000000..970722d69d --- /dev/null +++ b/src/qmlmodels/sfpm/qqmlsortfilterproxymodel.cpp @@ -0,0 +1,1720 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include +#include +#include +#include +#include + +QT_BEGIN_NAMESPACE + +Q_LOGGING_CATEGORY (lcSortFilterProxyModel, "qt.qml.sortfilterproxymodel") + +/*! + \qmltype SortFilterProxyModel + //! \nativetype QQmlSortFilterProxyModel + \inqmlmodule QtQml.Models + \since 6.10 + \preliminary + \brief Provides sorting and filtering capabilities for a + \l QAbstractItemModel. + + SortFilterProxyModel inherits from \l QAbstractProxyModel, which handles + the transformation of source indexes into proxy indexes. Sorting and + filtering are controlled via the \l{SortFilterProxyModel::sorters}{sorters} + and \l{SortFilterProxyModel::filters}{filters} properties, which + determine the order of execution as specified in QML. This order can be + overridden using the priority property available for each sorter. By + default, all sorters share the same priority. + + SortFilterProxyModel enables users to sort and filter a + \l QAbstractItemModel. The item views such as \l TableView or \l TreeView + can utilize this proxy model to display filtered content. + + \note Currently, SortFilterProxyModel supports only QAbstractItemModel + as the source model. + + The snippet below shows the configuration of sorters and filters in the + QML SortFilterProxyModel: + + \qml + ProcessModel { id: processModel } + + SortFilterProxyModel { + id: sfpm + model: processModel + sorters: [ + RoleSorter: { + roleName: "user" + priority: 0 + }, + RoleSorter: { + roleName: "pid" + priority: 1 + } + ] + filters: [ + FunctionFilter: { + component RoleData: QtObject { property qreal cpuUsage } + function filter(data: RoleData) : bool { + return (data.cpuUsage > 90) + } + } + ] + } + \endqml + + The SortFilterProxyModel dynamically sorts and filters data whenever there + is a change to the data in the source model and can be disabled through the + \l{SortFilterProxyModel::dynamicSortFilter}{dynamicSortFilter} property. + + The sorters \l RoleSorter, \l ValueSorter and \l StringSorter can be + configured in SortFilterProxyModel. Each sorter can be configured with a + specific column index through \l{Sorter::column}{column} property. If a + column index is not specified, the sorting will be applied to the column + index 0 of the model by default. The execution order of the sorter can be + modified through the \l{Sorter::priority}{priority} property. This is + particularly useful when performing hierarchical sorting, such as sorting + data in the first column and then applying sorting to subsequent columns. + + To disable a specific sorter, \l{Sorter::enabled}{enabled} can be set to + \c false. + + The sorter priority can also be overridden by setting the primary sorter + through the method call + \l{SortFilterProxyModel::setPrimarySorter(sorter)}{setPrimarySorter}. This + would be helpful in the case where the view wants to sort the data of any + specific column by clicking on the column header such as in \l TableView, + when there are other sorters also configured for the model. + + The filter \l ValueFilter and \l FunctionFilter can be configured + in SortFilterProxyModel. Each filter can be set with the + \l{Filter::column}{column} property, similar to the + sorter, to filter data in a specific column. If no column is specified, + then the filter will be applied to all the column indexes in + the model. To reduce the overhead of unwanted checks during filtering, + it's recommended to specify the column index. + + To disable a specific filter, \l{Filter::enabled}{enabled} can be set to + \c false. + + \snippet qml/sortfilterproxymodel/qml-sortfilterproxymodel.qml sfpm-usage + + \note This API is considered tech preview and may change or be removed in + future versions of Qt. +*/ +/*! + \qmlproperty list SortFilterProxyModel::filters + + This property holds the list of filters for the \l SortFilterProxyModel. + If no priority is set, the \l SortFilterProxyModel applies a filter in the + order as specified in the list. +*/ +/*! + \qmlproperty list SortFilterProxyModel::sorters + + This property holds the list of sorters for the \l SortFilterProxyModel. + If no priority is set, the \l SortFilterProxyModel applies a sorter in the + order as specified in the list. +*/ + +class QQmlSortFilterProxyModelPrivate : public QAbstractProxyModelPrivate, public QSortFilterProxyModelHelper +{ + Q_DECLARE_PUBLIC(QQmlSortFilterProxyModel) + +public: + void init(); + + bool containRoleForRecursiveFilter(const QList &roles) const; + bool recursiveParentAcceptsRow(const QModelIndex &source_parent) const; + bool recursiveChildAcceptsRow(int source_row, const QModelIndex &source_parent) const; + + QList>> proxy_intervals_for_source_items_to_add( + const QList &proxy_to_source, const QList &source_items, + const QModelIndex &source_parent, QSortFilterProxyModelHelper::Direction direction) const override; + bool needsReorder(const QList &source_rows, const QModelIndex &source_parent) const; + bool updatePrimaryColumn(); + int findPrimarySortColumn() const; + + inline QModelIndex create_index(int row, int column, + QHash::const_iterator it) const { + return q_func()->createIndex(row, column, *it); + } + void changePersistentIndexList(const QModelIndexList &from, const QModelIndexList &to) override { + Q_Q(QQmlSortFilterProxyModel); + q->changePersistentIndexList(from, to); + } + + void beginInsertRows(const QModelIndex &parent, int first, int last) override { + Q_Q(QQmlSortFilterProxyModel); + q->beginInsertRows(parent, first, last); + } + + void beginInsertColumns(const QModelIndex &parent, int first, int last) override { + Q_Q(QQmlSortFilterProxyModel); + q->beginInsertColumns(parent, first, last); + } + + void endInsertRows() override { + Q_Q(QQmlSortFilterProxyModel); + q->endInsertRows(); + } + + void endInsertColumns() override { + Q_Q(QQmlSortFilterProxyModel); + q->endInsertColumns(); + } + + void beginRemoveRows(const QModelIndex &parent, int first, int last) override { + Q_Q(QQmlSortFilterProxyModel); + q->beginRemoveRows(parent, first, last); + } + + void beginRemoveColumns(const QModelIndex &parent, int first, int last) override { + Q_Q(QQmlSortFilterProxyModel); + q->beginRemoveColumns(parent, first, last); + } + + void endRemoveRows() override { + Q_Q(QQmlSortFilterProxyModel); + q->endRemoveRows(); + } + + void endRemoveColumns() override { + Q_Q(QQmlSortFilterProxyModel); + q->endRemoveColumns(); + } + + void beginResetModel() override { + Q_Q(QQmlSortFilterProxyModel); + q->beginResetModel(); + } + + void endResetModel() override { + Q_Q(QQmlSortFilterProxyModel); + q->endResetModel(); + } + + // Update the proxy model when there is any change in the source model + void _q_sourceDataChanged(const QModelIndex &source_top_left, + const QModelIndex &source_bottom_right, + const QList &roles); + void _q_sourceHeaderDataChanged(Qt::Orientation orientation, + int start, int end); + void _q_sourceAboutToBeReset(); + void _q_sourceReset(); + void _q_clearMapping(); + void _q_sourceLayoutAboutToBeChanged(const QList &sourceParents, + QAbstractItemModel::LayoutChangeHint hint); + void _q_sourceLayoutChanged(const QList &sourceParents, + QAbstractItemModel::LayoutChangeHint hint); + void _q_sourceRowsAboutToBeInserted(const QModelIndex &source_parent, + int start, int end); + void _q_sourceRowsInserted(const QModelIndex &source_parent, + int start, int end); + void _q_sourceRowsAboutToBeRemoved(const QModelIndex &source_parent, + int start, int end); + void _q_sourceRowsRemoved(const QModelIndex &source_parent, + int start, int end); + void _q_sourceRowsAboutToBeMoved(const QModelIndex &sourceParent, + int sourceStart, int sourceEnd, + const QModelIndex &destParent, int dest); + void _q_sourceRowsMoved(const QModelIndex &sourceParent, + int sourceStart, int sourceEnd, + const QModelIndex &destParent, int dest); + void _q_sourceColumnsAboutToBeInserted(const QModelIndex &source_parent, + int start, int end); + void _q_sourceColumnsInserted(const QModelIndex &source_parent, + int start, int end); + void _q_sourceColumnsAboutToBeRemoved(const QModelIndex &source_parent, + int start, int end); + void _q_sourceColumnsRemoved(const QModelIndex &source_parent, + int start, int end); + void _q_sourceColumnsAboutToBeMoved(const QModelIndex &sourceParent, + int sourceStart, int sourceEnd, + const QModelIndex &destParent, int dest); + void _q_sourceColumnsMoved(const QModelIndex &sourceParent, + int sourceStart, int sourceEnd, + const QModelIndex &destParent, int dest); + + const QAbstractProxyModel *proxyModel() const override { return q_func(); } + QModelIndex createIndex(int row, int column, + QHash::const_iterator it) const override { + return create_index(row, column, it); + } + bool filterAcceptsRowInternal(int sourceRow, const QModelIndex &sourceIndex) const override; + bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override { + return q_func()->filterAcceptsRow(source_row, source_parent); + } + bool filterAcceptsColumnInternal(int sourceColumn, const QModelIndex &sourceIndex) const override; + bool filterAcceptsColumn(int source_column, const QModelIndex &source_parent) const override { + return q_func()->filterAcceptsColumn(source_column, source_parent); + } + void sort_source_rows(QList &source_rows, const QModelIndex &source_parent) const override; + bool lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const override { + return q_func()->lessThan(source_left, source_right); + } + + // Internal + QModelIndex m_lastTopSource; + QRowsRemoval m_itemsBeingRemoved; + bool m_completeInsert = false; + QModelIndexPairList m_savedPersistentIndexes; + QList m_savedLayoutChangeParents; + std::array m_sourceConnections; + bool m_componentCompleted = false; + + // Properties exposed to the user + QQmlFilterCompositor* m_filters; + QQmlSorterCompositor* m_sorters; + bool m_dynamicSortFilter = true; + bool m_recursiveFiltering = false; + bool m_autoAcceptChildRows = false; + int m_primarySortColumn = -1; + int m_proxySortColumn = -1; + Qt::SortOrder m_sortOrder = Qt::AscendingOrder; + QVariant m_sourceModel; +}; + +QQmlSortFilterProxyModel::QQmlSortFilterProxyModel(QObject *parent) + : QAbstractProxyModel (*new QQmlSortFilterProxyModelPrivate, parent) +{ + Q_D(QQmlSortFilterProxyModel); + d->init(); +} + +/*! + * Clear the filters and sorters and deletes the sort filter proxy model + */ +QQmlSortFilterProxyModel::~QQmlSortFilterProxyModel() +{ + Q_D(QQmlSortFilterProxyModel); + delete d->m_filters; + delete d->m_sorters; +} + +/*! + * Provides the filters configured in the sort filter proxy model + */ +QQmlListProperty QQmlSortFilterProxyModel::filters() +{ + Q_D(QQmlSortFilterProxyModel); + return d->m_filters->filtersListProperty(); +} + +/*! + * Provides the sorters configured in the sort filter proxy model + */ +QQmlListProperty QQmlSortFilterProxyModel::sorters() +{ + Q_D(QQmlSortFilterProxyModel); + return d->m_sorters->sortersListProperty(); +} + +/*! + \qmlproperty bool SortFilterProxyModel::dynamicSortFilter + + This property holds whether the proxy model is dynamically sorted + and filtered whenever the contents of the source model change. + + The default value is \c true. +*/ +bool QQmlSortFilterProxyModel::dynamicSortFilter() const +{ + Q_D(const QQmlSortFilterProxyModel); + return d->m_dynamicSortFilter; +} + +void QQmlSortFilterProxyModel::setDynamicSortFilter(const bool enabled) +{ + Q_D(QQmlSortFilterProxyModel); + if (d->m_dynamicSortFilter == enabled) + return; + d->m_dynamicSortFilter = enabled; + emit dynamicSortFilterChanged(); +} + +/*! + \qmlproperty bool SortFilterProxyModel::recursiveFiltering + + This property allows all the configured filters to be applied recursively + on children. The behavior is similar to that of + \l recursiveFilteringEnabled in \l QSortFilterProxyModel. + + The default value is \c false. +*/ +bool QQmlSortFilterProxyModel::recursiveFiltering() const +{ + Q_D(const QQmlSortFilterProxyModel); + return d->m_recursiveFiltering; +} + +void QQmlSortFilterProxyModel::setRecursiveFiltering(const bool enabled) +{ + Q_D(QQmlSortFilterProxyModel); + if (d->m_recursiveFiltering == enabled) + return; + d->m_recursiveFiltering = enabled; + emit recursiveFilteringChanged(); +} + +/*! + \qmlproperty bool SortFilterProxyModel::autoAcceptChildRows + + This property will not filter out children of accepted rows. The behavior + is similar to that of \l autoAcceptChildRows in \l QSortFilterProxyModel. + + The default value is \c false. +*/ +bool QQmlSortFilterProxyModel::autoAcceptChildRows() const +{ + Q_D(const QQmlSortFilterProxyModel); + return d->m_autoAcceptChildRows; +} + +void QQmlSortFilterProxyModel::setAutoAcceptChildRows(const bool enabled) +{ + Q_D(QQmlSortFilterProxyModel); + if (d->m_autoAcceptChildRows == enabled) + return; + d->m_autoAcceptChildRows = enabled; + emit autoAcceptChildRowsChanged(); +} + +/*! + \qmlproperty var SortFilterProxyModel::model + + This property allows to set source model for the sort filter proxy model. +*/ +QVariant QQmlSortFilterProxyModel::model() const +{ + Q_D(const QQmlSortFilterProxyModel); + return d->m_sourceModel; +} + +void QQmlSortFilterProxyModel::setModel(QVariant &model) +{ + Q_D(QQmlSortFilterProxyModel); + if (d->m_sourceModel == model) + return; + + auto *itemModel = qobject_cast(qvariant_cast(model)); + if (!itemModel ) { + qWarning("QQmlSortFilterProxyModel: supports only QAIM for now"); + return; + } + d->m_sourceModel = model; + setSourceModel(itemModel); + emit modelChanged(); +} + +/*! internal + * + */ +void QQmlSortFilterProxyModel::invalidateFilter() +{ + Q_D(QQmlSortFilterProxyModel); + if (d->m_componentCompleted) + d->filter_changed(); +} + +/*! + \qmlmethod SortFilterProxyModel::invalidate() + + This method invalidates the model by reevaluating the configured filters + and sorters on the source model data. + */ +void QQmlSortFilterProxyModel::invalidate() +{ + Q_D(QQmlSortFilterProxyModel); + if (d->m_componentCompleted) { + d->filter_changed(); + d->sort(); + } +} + +/*! + \qmlmethod SortFilterProxyModel::invalidateSorter() + + This method force the sort filter proxy model to reevaluate the configured + sorters against the data. It can used in the case where dynamic sorting + was disabled through property \l dynamicSortFilter +*/ +void QQmlSortFilterProxyModel::invalidateSorter() +{ + Q_D(QQmlSortFilterProxyModel); + if (d->m_componentCompleted) + d->sort(); +} + +/*! + \qmlmethod SortFilterProxyModel::setPrimarySorter(sorter) + + This method allows to set the primary sorter in the sort filter proxy + model. The primary sorter will be evaluated before all other sorters + configured as part of \a sorter property. If not configured or passed + \c null, the sorter with higher priority shall be considered as the + primary sorter. +*/ +void QQmlSortFilterProxyModel::setPrimarySorter(QQmlSorterBase *sorter) +{ + Q_D(QQmlSortFilterProxyModel); + if (auto *sortCompPriv = static_cast(QQmlSorterCompositorPrivate::get(d->m_sorters))) { + auto primarySorter = sortCompPriv->primarySorter(); + if (sorter == primarySorter.get()) + return; + sortCompPriv->setPrimarySorter(sorter); + emit primarySorterChanged(); + invalidateSorter(); + } +} + +/*! + \internal + */ +void QQmlSortFilterProxyModel::setSourceModel(QAbstractItemModel *sourceModel) +{ + Q_D(QQmlSortFilterProxyModel); + + if (sourceModel == d->model) + return; + + beginResetModel(); + + if (d->model) { + for (const QMetaObject::Connection &connection : std::as_const(d->m_sourceConnections)) + disconnect(connection); + } + + // same as in _q_sourceReset() + d->invalidatePersistentIndexes(); + d->_q_clearMapping(); + + QAbstractProxyModel::setSourceModel(sourceModel); + + d->m_sourceConnections = std::array{ + QObjectPrivate::connect(d->model, &QAbstractItemModel::dataChanged, d, + &QQmlSortFilterProxyModelPrivate::_q_sourceDataChanged), + + QObjectPrivate::connect(d->model, &QAbstractItemModel::headerDataChanged, d, + &QQmlSortFilterProxyModelPrivate::_q_sourceHeaderDataChanged), + + QObjectPrivate::connect(d->model, &QAbstractItemModel::rowsAboutToBeInserted, d, + &QQmlSortFilterProxyModelPrivate::_q_sourceRowsAboutToBeInserted), + + QObjectPrivate::connect(d->model, &QAbstractItemModel::rowsInserted, d, + &QQmlSortFilterProxyModelPrivate::_q_sourceRowsInserted), + + QObjectPrivate::connect(d->model, &QAbstractItemModel::columnsAboutToBeInserted, d, + &QQmlSortFilterProxyModelPrivate::_q_sourceColumnsAboutToBeInserted), + + QObjectPrivate::connect(d->model, &QAbstractItemModel::columnsInserted, d, + &QQmlSortFilterProxyModelPrivate::_q_sourceColumnsInserted), + + QObjectPrivate::connect(d->model, &QAbstractItemModel::rowsAboutToBeRemoved, d, + &QQmlSortFilterProxyModelPrivate::_q_sourceRowsAboutToBeRemoved), + + QObjectPrivate::connect(d->model, &QAbstractItemModel::rowsRemoved, d, + &QQmlSortFilterProxyModelPrivate::_q_sourceRowsRemoved), + + QObjectPrivate::connect(d->model, &QAbstractItemModel::columnsAboutToBeRemoved, d, + &QQmlSortFilterProxyModelPrivate::_q_sourceColumnsAboutToBeRemoved), + + QObjectPrivate::connect(d->model, &QAbstractItemModel::columnsRemoved, d, + &QQmlSortFilterProxyModelPrivate::_q_sourceColumnsRemoved), + + QObjectPrivate::connect(d->model, &QAbstractItemModel::rowsAboutToBeMoved, d, + &QQmlSortFilterProxyModelPrivate::_q_sourceRowsAboutToBeMoved), + + QObjectPrivate::connect(d->model, &QAbstractItemModel::rowsMoved, d, + &QQmlSortFilterProxyModelPrivate::_q_sourceRowsMoved), + + QObjectPrivate::connect(d->model, &QAbstractItemModel::columnsAboutToBeMoved, d, + &QQmlSortFilterProxyModelPrivate::_q_sourceColumnsAboutToBeMoved), + + QObjectPrivate::connect(d->model, &QAbstractItemModel::columnsMoved, d, + &QQmlSortFilterProxyModelPrivate::_q_sourceColumnsMoved), + + QObjectPrivate::connect(d->model, &QAbstractItemModel::layoutAboutToBeChanged, d, + &QQmlSortFilterProxyModelPrivate::_q_sourceLayoutAboutToBeChanged), + + QObjectPrivate::connect(d->model, &QAbstractItemModel::layoutChanged, d, + &QQmlSortFilterProxyModelPrivate::_q_sourceLayoutChanged), + + QObjectPrivate::connect(d->model, &QAbstractItemModel::modelAboutToBeReset, d, + &QQmlSortFilterProxyModelPrivate::_q_sourceAboutToBeReset), + + QObjectPrivate::connect(d->model, &QAbstractItemModel::modelReset, d, + &QQmlSortFilterProxyModelPrivate::_q_sourceReset) + }; + endResetModel(); + + if (d->m_dynamicSortFilter && d->updatePrimaryColumn()) + d->sort(); +} + +/*! + Returns the source model index corresponding to the given \a + proxyIndex from the sorting filter model. +*/ +QModelIndex QQmlSortFilterProxyModel::mapToSource(const QModelIndex &proxyIndex) const +{ + Q_D(const QQmlSortFilterProxyModel); + return d->proxy_to_source(proxyIndex); +} + +/*! + Returns the model index in the QQmlSortFilterProxyModel given the \a + sourceIndex from the source model. +*/ +QModelIndex QQmlSortFilterProxyModel::mapFromSource(const QModelIndex &sourceIndex) const +{ + Q_D(const QQmlSortFilterProxyModel); + return d->source_to_proxy(sourceIndex); +} + +/*! + \reimp +*/ +QItemSelection QQmlSortFilterProxyModel::mapSelectionToSource(const QItemSelection &proxySelection) const +{ + return QAbstractProxyModel::mapSelectionToSource(proxySelection); +} + +/*! + \reimp +*/ +QItemSelection QQmlSortFilterProxyModel::mapSelectionFromSource(const QItemSelection &sourceSelection) const +{ + return QAbstractProxyModel::mapSelectionFromSource(sourceSelection); +} + + +/*! + \reimp +*/ +QModelIndex QQmlSortFilterProxyModel::index(int row, int column, const QModelIndex &parent) const +{ + Q_D(const QQmlSortFilterProxyModel); + if (row < 0 || column < 0) + return QModelIndex(); + + QModelIndex source_parent = mapToSource(parent); // parent is already mapped at this point + QSortFilterProxyModelHelper::IndexMap::const_iterator it = d->create_mapping(source_parent); // but make sure that the children are mapped + if (it.value()->source_rows.size() <= row || it.value()->source_columns.size() <= column) + return QModelIndex(); + + return d->create_index(row, column, it); +} + +/*! + \reimp +*/ +QModelIndex QQmlSortFilterProxyModel::parent(const QModelIndex &child) const +{ + Q_D(const QQmlSortFilterProxyModel); + if (!d->indexValid(child)) + return QModelIndex(); + QSortFilterProxyModelHelper::IndexMap::const_iterator it = d->index_to_iterator(child); + Q_ASSERT(it != d->source_index_mapping.constEnd()); + QModelIndex source_parent = it.key(); + QModelIndex proxy_parent = mapFromSource(source_parent); + return proxy_parent; +} + +/*! + \reimp +*/ +QModelIndex QQmlSortFilterProxyModel::sibling(int row, int column, const QModelIndex &idx) const +{ + Q_D(const QQmlSortFilterProxyModel); + if (!d->indexValid(idx)) + return QModelIndex(); + + const QSortFilterProxyModelHelper::IndexMap::const_iterator it = d->index_to_iterator(idx); + if (it.value()->source_rows.size() <= row || it.value()->source_columns.size() <= column) + return QModelIndex(); + + return d->create_index(row, column, it); +} + +/*! + \reimp +*/ +bool QQmlSortFilterProxyModel::hasChildren(const QModelIndex &parent) const +{ + Q_D(const QQmlSortFilterProxyModel); + QModelIndex source_parent = mapToSource(parent); + if (parent.isValid() && !source_parent.isValid()) + return false; + if (!d->model->hasChildren(source_parent)) + return false; + + if (d->model->canFetchMore(source_parent)) + return true; //we assume we might have children that can be fetched + + QSortFilterProxyModelHelper::Mapping *m = d->create_mapping(source_parent).value(); + return m->source_rows.size() != 0 && m->source_columns.size() != 0; +} + +int QQmlSortFilterProxyModel::columnCount(const QModelIndex &parent) const +{ + Q_D(const QQmlSortFilterProxyModel); + if (d->m_sourceModel.isNull() || !d->m_sourceModel.isValid()) + return 0; + QModelIndex source_parent = mapToSource(parent); + if (parent.isValid() && !source_parent.isValid()) + return 0; + QSortFilterProxyModelHelper::IndexMap::const_iterator it = d->create_mapping(source_parent); + return it.value()->source_columns.size(); +} + +int QQmlSortFilterProxyModel::rowCount(const QModelIndex &parent) const +{ + Q_D(const QQmlSortFilterProxyModel); + if (d->m_sourceModel.isNull() || !d->m_sourceModel.isValid()) + return 0; + QModelIndex source_parent = mapToSource(parent); + if (parent.isValid() && !source_parent.isValid()) + return 0; + QSortFilterProxyModelHelper::IndexMap::const_iterator it = d->create_mapping(source_parent); + return it.value()->source_rows.size(); +} + +QVariant QQmlSortFilterProxyModel::data(const QModelIndex &index, int role) const +{ + Q_D(const QQmlSortFilterProxyModel); + QModelIndex source_index = mapToSource(index); + if (index.isValid() && !source_index.isValid()) + return QVariant(); + return d->model->data(source_index, role); +} + +bool QQmlSortFilterProxyModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + Q_D(QQmlSortFilterProxyModel); + QModelIndex source_index = mapToSource(index); + if (index.isValid() && !source_index.isValid()) + return false; + return d->model->setData(source_index, value, role); +} + +/*! + \reimp +*/ +QVariant QQmlSortFilterProxyModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + Q_D(const QQmlSortFilterProxyModel); + QSortFilterProxyModelHelper::IndexMap::const_iterator it = d->create_mapping(QModelIndex()); + if (it.value()->source_rows.size() * it.value()->source_columns.size() > 0) + return QAbstractProxyModel::headerData(section, orientation, role); + int source_section; + if (orientation == Qt::Vertical) { + if (section < 0 || section >= it.value()->source_rows.size()) + return QVariant(); + source_section = it.value()->source_rows.at(section); + } else { + if (section < 0 || section >= it.value()->source_columns.size()) + return QVariant(); + source_section = it.value()->source_columns.at(section); + } + return d->model->headerData(source_section, orientation, role); +} + +/*! + \reimp +*/ +bool QQmlSortFilterProxyModel::setHeaderData(int section, Qt::Orientation orientation, + const QVariant &value, int role) +{ + Q_D(QQmlSortFilterProxyModel); + QSortFilterProxyModelHelper::IndexMap::const_iterator it = d->create_mapping(QModelIndex()); + if (it.value()->source_rows.size() * it.value()->source_columns.size() > 0) + return QAbstractProxyModel::setHeaderData(section, orientation, value, role); + int source_section; + if (orientation == Qt::Vertical) { + if (section < 0 || section >= it.value()->source_rows.size()) + return false; + source_section = it.value()->source_rows.at(section); + } else { + if (section < 0 || section >= it.value()->source_columns.size()) + return false; + source_section = it.value()->source_columns.at(section); + } + return d->model->setHeaderData(source_section, orientation, value, role); +} + +/*! + \reimp +*/ +bool QQmlSortFilterProxyModel::insertRows(int row, int count, const QModelIndex &parent) +{ + Q_D(QQmlSortFilterProxyModel); + if (row < 0 || count <= 0) + return false; + QModelIndex source_parent = mapToSource(parent); + if (parent.isValid() && !source_parent.isValid()) + return false; + QSortFilterProxyModelHelper::Mapping *m = d->create_mapping(source_parent).value(); + if (row > m->source_rows.size()) + return false; + int source_row = (row >= m->source_rows.size() + ? m->proxy_rows.size() + : m->source_rows.at(row)); + return d->model->insertRows(source_row, count, source_parent); +} + +/*! + \reimp +*/ +bool QQmlSortFilterProxyModel::insertColumns(int column, int count, const QModelIndex &parent) +{ + Q_D(QQmlSortFilterProxyModel); + if (column < 0|| count <= 0) + return false; + QModelIndex source_parent = mapToSource(parent); + if (parent.isValid() && !source_parent.isValid()) + return false; + QSortFilterProxyModelHelper::Mapping *m = d->create_mapping(source_parent).value(); + if (column > m->source_columns.size()) + return false; + int source_column = (column >= m->source_columns.size() + ? m->proxy_columns.size() + : m->source_columns.at(column)); + return d->model->insertColumns(source_column, count, source_parent); +} + +/*! + \reimp +*/ +bool QQmlSortFilterProxyModel::removeRows(int row, int count, const QModelIndex &parent) +{ + Q_D(QQmlSortFilterProxyModel); + if (row < 0 || count <= 0) + return false; + QModelIndex source_parent = mapToSource(parent); + if (parent.isValid() && !source_parent.isValid()) + return false; + QSortFilterProxyModelHelper::Mapping *m = d->create_mapping(source_parent).value(); + if (row + count > m->source_rows.size()) + return false; + if ((count == 1) + || ((d->m_primarySortColumn < 0) && (m->proxy_rows.size() == m->source_rows.size()))) { + int source_row = m->source_rows.at(row); + return d->model->removeRows(source_row, count, source_parent); + } + // remove corresponding source intervals + // ### if this proves to be slow, we can switch to single-row removal + QList rows; + rows.reserve(count); + for (int i = row; i < row + count; ++i) + rows.append(m->source_rows.at(i)); + std::sort(rows.begin(), rows.end()); + + int pos = rows.size() - 1; + bool ok = true; + while (pos >= 0) { + const int source_end = rows.at(pos--); + int source_start = source_end; + while ((pos >= 0) && (rows.at(pos) == (source_start - 1))) { + --source_start; + --pos; + } + ok = ok && d->model->removeRows(source_start, source_end - source_start + 1, + source_parent); + } + return ok; +} + +/*! + \reimp +*/ +bool QQmlSortFilterProxyModel::removeColumns(int column, int count, const QModelIndex &parent) +{ + Q_D(QQmlSortFilterProxyModel); + if (column < 0 || count <= 0) + return false; + QModelIndex source_parent = mapToSource(parent); + if (parent.isValid() && !source_parent.isValid()) + return false; + QSortFilterProxyModelHelper::Mapping *m = d->create_mapping(source_parent).value(); + if (column + count > m->source_columns.size()) + return false; + if ((count == 1) || (m->proxy_columns.size() == m->source_columns.size())) { + int source_column = m->source_columns.at(column); + return d->model->removeColumns(source_column, count, source_parent); + } + // remove corresponding source intervals + QList columns; + columns.reserve(count); + for (int i = column; i < column + count; ++i) + columns.append(m->source_columns.at(i)); + + int pos = columns.size() - 1; + bool ok = true; + while (pos >= 0) { + const int source_end = columns.at(pos--); + int source_start = source_end; + while ((pos >= 0) && (columns.at(pos) == (source_start - 1))) { + --source_start; + --pos; + } + ok = ok && d->model->removeColumns(source_start, source_end - source_start + 1, + source_parent); + } + return ok; +} + +/*! + \internal + */ +QHash QQmlSortFilterProxyModel::roleNames() const +{ + if (const auto srcModel = sourceModel()) + return srcModel->roleNames(); + return {}; +} + +/*! + \internal + */ +QVariant QQmlSortFilterProxyModel::sourceData(const QModelIndex &sourceIndex, int role) const +{ + return sourceModel()->data(sourceIndex, role); +} + +/*! + \internal + */ +bool QQmlSortFilterProxyModel::filterAcceptsRow(int row, const QModelIndex& sourceParent) const +{ + Q_D(const QQmlSortFilterProxyModel); + if (!d->m_componentCompleted) + return true; + return d->m_filters->filterAcceptsRowInternal(row, sourceParent, this); +} + +/*! + \internal + */ +bool QQmlSortFilterProxyModel::filterAcceptsColumn(int column, const QModelIndex &sourceParent) const +{ + Q_D(const QQmlSortFilterProxyModel); + if (!d->m_componentCompleted) + return true; + return d->m_filters->filterAcceptsColumnInternal(column, sourceParent, this); +} + +/*! + \internal + */ +bool QQmlSortFilterProxyModel::lessThan(const QModelIndex &sourceLeft, const QModelIndex &sourceRight) const +{ + Q_D(const QQmlSortFilterProxyModel); + if (d->m_sorters) + return d->m_sorters->lessThan(sourceLeft, sourceRight, this); + return false; +} + +/*! + \internal + */ +void QQmlSortFilterProxyModel::componentComplete() +{ + // We need to explicitly call sort here. This is because, as of now the + // actual implementation for sorting comes from QSortFilterProxyModelBase + // and it trigger the sorting operation with the corresponding column set + // and it happens only after parsing the configured sorters. + Q_D(QQmlSortFilterProxyModel); + d->m_componentCompleted = true; + d->filter_changed(); + if (d->m_dynamicSortFilter) + d->sort(); +} + +/*! + \internal + */ +void QQmlSortFilterProxyModel::sort(int column, Qt::SortOrder order) +{ + Q_D(QQmlSortFilterProxyModel); + if (!d->m_componentCompleted || + (d->m_dynamicSortFilter && d->m_proxySortColumn == column && d->m_sortOrder == order)) + return; + d->m_sortOrder = order; + d->m_proxySortColumn = column; + d->updatePrimaryColumn(); + d->sort(); +} + +/*! + \internal + */ +int QQmlSortFilterProxyModel::itemRoleForName(const QString& roleName) const +{ + QHash itemRoleNames = roleNames(); + return !itemRoleNames.isEmpty() ? itemRoleNames.key(roleName.toUtf8(), -1) : -1; +} + +/*! + \internal + */ +void QQmlSortFilterProxyModel::setPrimarySortColumn(const int column) +{ + Q_D(QQmlSortFilterProxyModel); + d->m_proxySortColumn = column; + d->updatePrimaryColumn(); +} + +/*! + \internal + */ +void QQmlSortFilterProxyModel::setPrimarySortOrder(const Qt::SortOrder order) +{ + Q_D(QQmlSortFilterProxyModel); + d->m_sortOrder = order; +} + +/*! + \internal + */ +void QQmlSortFilterProxyModelPrivate::init() +{ + Q_Q(QQmlSortFilterProxyModel); + m_filters = new QQmlFilterCompositor(q); + QObject::connect(q, &QQmlSortFilterProxyModel::filtersChanged, + q, &QQmlSortFilterProxyModel::invalidateFilter); + m_sorters = new QQmlSorterCompositor(q); + QObject::connect(q, &QQmlSortFilterProxyModel::sortersChanged, + q, &QQmlSortFilterProxyModel::invalidateSorter); +} + +void QQmlSortFilterProxyModelPrivate::_q_clearMapping() +{ + // store the persistent indexes + QModelIndexPairList source_indexes = store_persistent_indexes(); + + clearSourceIndexMapping(); + if (m_dynamicSortFilter) + m_primarySortColumn = findPrimarySortColumn(); + + // update the persistent indexes + update_persistent_indexes(source_indexes); +} + +bool QQmlSortFilterProxyModelPrivate::recursiveParentAcceptsRow(const QModelIndex &source_parent) const +{ + Q_Q(const QQmlSortFilterProxyModel); + if (source_parent.isValid()) { + const QModelIndex index = source_parent.parent(); + if (q->filterAcceptsRow(source_parent.row(), index)) + return true; + return recursiveParentAcceptsRow(index); + } + return false; +} + +bool QQmlSortFilterProxyModelPrivate::recursiveChildAcceptsRow(int source_row, const QModelIndex &source_parent) const +{ + Q_Q(const QQmlSortFilterProxyModel); + const QModelIndex index = model->index(source_row, 0, source_parent); + const int count = model->rowCount(index); + for (int i = 0; i < count; ++i) { + if (q->filterAcceptsRow(i, index)) + return true; + if (recursiveChildAcceptsRow(i, index)) + return true; + } + return false; +} + +/*! + \internal + + Update the primary sort column according to the m_proxySortColumn + return true if the column was changed +*/ +bool QQmlSortFilterProxyModelPrivate::updatePrimaryColumn() +{ + int old_primayColumn = m_primarySortColumn; + + if (m_proxySortColumn == -1) { + m_primarySortColumn = -1; + } else { + // We cannot use index mapping here because in case of a still-empty + // proxy model there's no valid proxy index we could map to source. + // So always use the root mapping directly instead. + QSortFilterProxyModelHelper::Mapping *m = create_mapping(QModelIndex()).value(); + if (m_proxySortColumn < m->source_columns.size()) + m_primarySortColumn = m->source_columns.at(m_proxySortColumn); + else + m_primarySortColumn = -1; + } + + return old_primayColumn != m_primarySortColumn; +} + +/*! + \internal + + Find the primary sort column without creating a full mapping and + without updating anything. +*/ +int QQmlSortFilterProxyModelPrivate::findPrimarySortColumn() const +{ + if (m_proxySortColumn == -1) + return -1; + + const QModelIndex rootIndex; + const int source_cols = model->columnCount(); + int accepted_columns = -1; + + Q_Q(const QQmlSortFilterProxyModel); + for (int i = 0; i < source_cols; ++i) { + if (q->filterAcceptsColumn(i, rootIndex)) { + if (++accepted_columns == m_proxySortColumn) + return i; + } + } + + return -1; +} + +/*! + \internal + + Sorts the given \a source_rows according to current sort column and order. +*/ +void QQmlSortFilterProxyModelPrivate::sort_source_rows( + QList &source_rows, const QModelIndex &source_parent) const +{ + if (m_primarySortColumn >= 0) { + if (m_sortOrder == Qt::AscendingOrder) { + QSortFilterProxyModelLessThan lt(m_primarySortColumn, source_parent, model, this); + std::stable_sort(source_rows.begin(), source_rows.end(), lt); + } else { + QSortFilterProxyModelGreaterThan gt(m_primarySortColumn, source_parent, model, this); + std::stable_sort(source_rows.begin(), source_rows.end(), gt); + } + } else if (m_sortOrder == Qt::AscendingOrder) { + std::stable_sort(source_rows.begin(), source_rows.end(), std::less{}); + } else { + std::stable_sort(source_rows.begin(), source_rows.end(), std::greater{}); + } +} + +/*! + \internal + + Given proxy-to-source mapping \a proxy_to_source and a set of + unmapped source items \a source_items, determines the proxy item + intervals at which the subsets of source items should be inserted + (but does not actually add them to the mapping). + + The result is a vector of pairs, each pair representing a tuple (start, + items), where items is a vector containing the (sorted) source items that + should be inserted at that proxy model location. +*/ +QList>> QQmlSortFilterProxyModelPrivate::proxy_intervals_for_source_items_to_add( + const QList &proxy_to_source, const QList &source_items, + const QModelIndex &source_parent, QSortFilterProxyModelHelper::Direction direction) const +{ + Q_Q(const QQmlSortFilterProxyModel); + QList>> proxy_intervals; + if (source_items.isEmpty()) + return proxy_intervals; + + int proxy_low = 0; + int proxy_item = 0; + int source_items_index = 0; + bool compare = (direction == QSortFilterProxyModelHelper::Direction::Rows && m_primarySortColumn >= 0 && m_dynamicSortFilter); + while (source_items_index < source_items.size()) { + QList source_items_in_interval; + int first_new_source_item = source_items.at(source_items_index); + source_items_in_interval.append(first_new_source_item); + ++source_items_index; + + // Find proxy item at which insertion should be started + int proxy_high = proxy_to_source.size() - 1; + QModelIndex i1 = compare ? model->index(first_new_source_item, m_primarySortColumn, source_parent) : QModelIndex(); + while (proxy_low <= proxy_high) { + proxy_item = (proxy_low + proxy_high) / 2; + if (compare) { + QModelIndex i2 = model->index(proxy_to_source.at(proxy_item), m_primarySortColumn, source_parent); + if ((m_sortOrder == Qt::AscendingOrder) ? q->lessThan(i1, i2) : q->lessThan(i2, i1)) + proxy_high = proxy_item - 1; + else + proxy_low = proxy_item + 1; + } else { + if (first_new_source_item < proxy_to_source.at(proxy_item)) + proxy_high = proxy_item - 1; + else + proxy_low = proxy_item + 1; + } + } + proxy_item = proxy_low; + + // Find the sequence of new source items that should be inserted here + if (proxy_item >= proxy_to_source.size()) { + for ( ; source_items_index < source_items.size(); ++source_items_index) + source_items_in_interval.append(source_items.at(source_items_index)); + } else { + i1 = compare ? model->index(proxy_to_source.at(proxy_item), m_primarySortColumn, source_parent) : QModelIndex(); + for ( ; source_items_index < source_items.size(); ++source_items_index) { + int new_source_item = source_items.at(source_items_index); + if (compare) { + QModelIndex i2 = model->index(new_source_item, m_primarySortColumn, source_parent); + if ((m_sortOrder == Qt::AscendingOrder) ? q->lessThan(i1, i2) : q->lessThan(i2, i1)) + break; + } else { + if (proxy_to_source.at(proxy_item) < new_source_item) + break; + } + source_items_in_interval.append(new_source_item); + } + } + // Add interval to result + proxy_intervals.emplace_back(proxy_item, std::move(source_items_in_interval)); + } + return proxy_intervals; +} + +bool QQmlSortFilterProxyModelPrivate::needsReorder(const QList &source_rows, const QModelIndex &source_parent) const +{ + Q_Q(const QQmlSortFilterProxyModel); + Q_ASSERT(m_primarySortColumn != -1); + const int proxyRowCount = q->rowCount(source_to_proxy(source_parent)); + // If any modified proxy row no longer passes lessThan(previous, current) + // or lessThan(current, next) then we need to reorder. + return std::any_of(source_rows.begin(), source_rows.end(), + [this, q, proxyRowCount, source_parent](int sourceRow) -> bool { + const QModelIndex sourceIndex = model->index(sourceRow, m_primarySortColumn, source_parent); + const QModelIndex proxyIndex = source_to_proxy(sourceIndex); + Q_ASSERT(proxyIndex.isValid()); // caller ensured source_rows were not filtered out + if (proxyIndex.row() > 0) { + const QModelIndex prevProxyIndex = q->sibling(proxyIndex.row() - 1, m_proxySortColumn, proxyIndex); + const QModelIndex prevSourceIndex = proxy_to_source(prevProxyIndex); + if (m_sortOrder == Qt::AscendingOrder ? q->lessThan(sourceIndex, prevSourceIndex) : q->lessThan(prevSourceIndex, sourceIndex)) + return true; + } + if (proxyIndex.row() < proxyRowCount - 1) { + const QModelIndex nextProxyIndex = q->sibling(proxyIndex.row() + 1, m_proxySortColumn, proxyIndex); + const QModelIndex nextSourceIndex = proxy_to_source(nextProxyIndex); + if (m_sortOrder == Qt::AscendingOrder ? q->lessThan(nextSourceIndex, sourceIndex) : q->lessThan(sourceIndex, nextSourceIndex)) + return true; + } + return false; + }); +} + +void QQmlSortFilterProxyModelPrivate::_q_sourceDataChanged(const QModelIndex &source_top_left, + const QModelIndex &source_bottom_right, + const QList &roles) +{ + Q_Q(QQmlSortFilterProxyModel); + if (!source_top_left.isValid() || !source_bottom_right.isValid()) + return; + + std::vector data_changed_list; + data_changed_list.emplace_back(source_top_left, source_bottom_right); + + // Do check parents if the filter role have changed and we are recursive + if (containRoleForRecursiveFilter(roles)) { + QModelIndex source_parent = source_top_left.parent(); + while (source_parent.isValid()) { + data_changed_list.emplace_back(source_parent, source_parent); + source_parent = source_parent.parent(); + } + } + + for (const QSortFilterProxyModelDataChanged &data_changed : data_changed_list) { + const QModelIndex &source_top_left = data_changed.topLeft; + const QModelIndex &source_bottom_right = data_changed.bottomRight; + const QModelIndex source_parent = source_top_left.parent(); + + bool change_in_unmapped_parent = false; + QSortFilterProxyModelHelper::IndexMap::const_iterator it = source_index_mapping.constFind(source_parent); + if (it == source_index_mapping.constEnd()) { + // We don't have mapping for this index, so we cannot know how + // things changed (in case the change affects filtering) in order + // to forward the change correctly. + // But we can at least forward the signal "as is", if the row isn't + // filtered out, this is better than nothing. + it = create_mapping_recursive(source_parent); + if (it == source_index_mapping.constEnd()) + continue; + change_in_unmapped_parent = true; + } + + QSortFilterProxyModelHelper::Mapping *m = it.value(); + + // Figure out how the source changes affect us + QList source_rows_remove; + QList source_rows_insert; + QList source_rows_change; + QList source_rows_resort; + int end = qMin(source_bottom_right.row(), m->proxy_rows.size() - 1); + for (int source_row = source_top_left.row(); source_row <= end; ++source_row) { + if (m_dynamicSortFilter && !change_in_unmapped_parent) { + if (m->proxy_rows.at(source_row) != -1) { + if (!filterAcceptsRowInternal(source_row, source_parent)) { + // This source row no longer satisfies the filter, so + // it must be removed + source_rows_remove.append(source_row); + } else if (m_primarySortColumn >= source_top_left.column() && m_primarySortColumn <= source_bottom_right.column()) { + // This source row has changed in a way that may affect + // sorted order + source_rows_resort.append(source_row); + } else { + // This row has simply changed, without affecting + // filtering nor sorting + source_rows_change.append(source_row); + } + } else { + if (!m_itemsBeingRemoved.contains(source_parent, source_row) && filterAcceptsRowInternal(source_row, source_parent)) { + // This source row now satisfies the filter, so it must + // be added + source_rows_insert.append(source_row); + } + } + } else { + if (m->proxy_rows.at(source_row) != -1) + source_rows_change.append(source_row); + } + } + + if (!source_rows_remove.isEmpty()) { + remove_source_items(m->proxy_rows, m->source_rows, + source_rows_remove, source_parent, QSortFilterProxyModelHelper::Direction::Rows); + QSet source_rows_remove_set = qListToSet(source_rows_remove); + QList::iterator childIt = m->mapped_children.end(); + while (childIt != m->mapped_children.begin()) { + --childIt; + const QModelIndex source_child_index = *childIt; + if (source_rows_remove_set.contains(source_child_index.row())) { + childIt = m->mapped_children.erase(childIt); + remove_from_mapping(source_child_index); + } + } + } + + if (!source_rows_resort.isEmpty()) { + if (needsReorder(source_rows_resort, source_parent)) { + // Re-sort the rows of this level + QList parents; + parents << q->mapFromSource(source_parent); + emit q->layoutAboutToBeChanged(parents, QAbstractItemModel::VerticalSortHint); + QModelIndexPairList source_indexes = store_persistent_indexes(); + remove_source_items(m->proxy_rows, m->source_rows, source_rows_resort, + source_parent, QSortFilterProxyModelHelper::Direction::Rows, false); + sort_source_rows(source_rows_resort, source_parent); + insert_source_items(m->proxy_rows, m->source_rows, source_rows_resort, + source_parent, QSortFilterProxyModelHelper::Direction::Rows, false); + update_persistent_indexes(source_indexes); + emit q->layoutChanged(parents, QAbstractItemModel::VerticalSortHint); + } + // Make sure we also emit dataChanged for the rows + source_rows_change += source_rows_resort; + } + + if (!source_rows_change.isEmpty()) { + // Find the proxy row range + int proxy_start_row; + int proxy_end_row; + proxy_item_range(m->proxy_rows, source_rows_change, + proxy_start_row, proxy_end_row); + // ### Find the proxy column range also + if (proxy_end_row >= 0) { + // the row was accepted, but some columns might still be + // filtered out + int source_left_column = source_top_left.column(); + while (source_left_column < source_bottom_right.column() + && m->proxy_columns.at(source_left_column) == -1) + ++source_left_column; + if (m->proxy_columns.at(source_left_column) != -1) { + const QModelIndex proxy_top_left = create_index( + proxy_start_row, m->proxy_columns.at(source_left_column), it); + int source_right_column = source_bottom_right.column(); + while (source_right_column > source_top_left.column() + && m->proxy_columns.at(source_right_column) == -1) + --source_right_column; + if (m->proxy_columns.at(source_right_column) != -1) { + const QModelIndex proxy_bottom_right = create_index( + proxy_end_row, m->proxy_columns.at(source_right_column), it); + emit q->dataChanged(proxy_top_left, proxy_bottom_right, roles); + } + } + } + } + + if (!source_rows_insert.isEmpty()) { + sort_source_rows(source_rows_insert, source_parent); + insert_source_items(m->proxy_rows, m->source_rows, + source_rows_insert, source_parent, QSortFilterProxyModelHelper::Direction::Rows); + } + } +} + +void QQmlSortFilterProxyModelPrivate::_q_sourceHeaderDataChanged(Qt::Orientation orientation, + int start, int end) +{ + Q_ASSERT(start <= end); + + Q_Q(QQmlSortFilterProxyModel); + QSortFilterProxyModelHelper::Mapping *m = create_mapping(QModelIndex()).value(); + + const QList &source_to_proxy = (orientation == Qt::Vertical) ? m->proxy_rows : m->proxy_columns; + + QList proxy_positions; + proxy_positions.reserve(end - start + 1); + { + Q_ASSERT(source_to_proxy.size() > end); + QList::const_iterator it = source_to_proxy.constBegin() + start; + const QList::const_iterator endIt = source_to_proxy.constBegin() + end + 1; + for ( ; it != endIt; ++it) { + if (*it != -1) + proxy_positions.push_back(*it); + } + } + + std::sort(proxy_positions.begin(), proxy_positions.end()); + + int last_index = 0; + const int numItems = proxy_positions.size(); + while (last_index < numItems) { + const int proxyStart = proxy_positions.at(last_index); + int proxyEnd = proxyStart; + ++last_index; + for (int i = last_index; i < numItems; ++i) { + if (proxy_positions.at(i) == proxyEnd + 1) { + ++last_index; + ++proxyEnd; + } else { + break; + } + } + emit q->headerDataChanged(orientation, proxyStart, proxyEnd); + } +} + +void QQmlSortFilterProxyModelPrivate::_q_sourceAboutToBeReset() +{ + Q_Q(QQmlSortFilterProxyModel); + q->beginResetModel(); +} + +void QQmlSortFilterProxyModelPrivate::_q_sourceReset() +{ + Q_Q(QQmlSortFilterProxyModel); + invalidatePersistentIndexes(); + _q_clearMapping(); + // All internal structures are deleted in clear() + q->endResetModel(); + if (updatePrimaryColumn() && m_dynamicSortFilter) + sort(); +} + +void QQmlSortFilterProxyModelPrivate::_q_sourceLayoutAboutToBeChanged(const QList &sourceParents, QAbstractItemModel::LayoutChangeHint hint) +{ + Q_Q(QQmlSortFilterProxyModel); + Q_UNUSED(hint); // We can't forward Hint because we might filter additional rows or columns + m_savedPersistentIndexes.clear(); + + m_savedLayoutChangeParents.clear(); + for (const QPersistentModelIndex &parent : sourceParents) { + if (!parent.isValid()) { + m_savedLayoutChangeParents << QPersistentModelIndex(); + continue; + } + const QModelIndex mappedParent = q->mapFromSource(parent); + // Might be filtered out. + if (mappedParent.isValid()) + m_savedLayoutChangeParents << mappedParent; + } + + // All parents filtered out. + if (!sourceParents.isEmpty() && m_savedLayoutChangeParents.isEmpty()) + return; + + emit q->layoutAboutToBeChanged(m_savedLayoutChangeParents); + if (persistent.indexes.isEmpty()) + return; + + m_savedPersistentIndexes = store_persistent_indexes(); +} + +void QQmlSortFilterProxyModelPrivate::_q_sourceLayoutChanged(const QList &sourceParents, QAbstractItemModel::LayoutChangeHint hint) +{ + Q_Q(QQmlSortFilterProxyModel); + Q_UNUSED(hint); // We can't forward Hint because we might filter additional rows or columns + + if (!sourceParents.isEmpty() && m_savedLayoutChangeParents.isEmpty()) + return; + + // Optimize: We only actually have to clear the mapping related to the + // contents of sourceParents, not everything. + clearSourceIndexMapping(); + + update_persistent_indexes(m_savedPersistentIndexes); + m_savedPersistentIndexes.clear(); + + if (m_dynamicSortFilter) + m_primarySortColumn = findPrimarySortColumn(); + + emit q->layoutChanged(m_savedLayoutChangeParents); + m_savedLayoutChangeParents.clear(); +} + +void QQmlSortFilterProxyModelPrivate::_q_sourceRowsAboutToBeInserted( + const QModelIndex &source_parent, int start, int end) +{ + Q_UNUSED(start); + Q_UNUSED(end); + + const bool toplevel = !source_parent.isValid(); + const bool recursive_accepted = m_recursiveFiltering && !toplevel && filterAcceptsRowInternal(source_parent.row(), source_parent.parent()); + // Force the creation of a mapping now, even if it's empty. + // We need it because the proxy can be accessed at the moment it emits + // rowsAboutToBeInserted in insert_source_items + if (!m_recursiveFiltering || toplevel || recursive_accepted) { + if (can_create_mapping(source_parent)) + create_mapping(source_parent); + if (m_recursiveFiltering) + m_completeInsert = true; + } else { + // The row could have been rejected or the parent might be not yet + // known... let's try to discover it + QModelIndex top_source_parent = source_parent; + QModelIndex parent = source_parent.parent(); + QModelIndex grandParent = parent.parent(); + + while (parent.isValid() && !filterAcceptsRowInternal(parent.row(), grandParent)) { + top_source_parent = parent; + parent = grandParent; + grandParent = parent.parent(); + } + + m_lastTopSource = top_source_parent; + } +} + +void QQmlSortFilterProxyModelPrivate::_q_sourceRowsInserted( + const QModelIndex &source_parent, int start, int end) +{ + if (!m_recursiveFiltering || m_completeInsert) { + if (m_recursiveFiltering) + m_completeInsert = false; + source_items_inserted(source_parent, start, end, QSortFilterProxyModelHelper::Direction::Rows); + if (updatePrimaryColumn() && m_dynamicSortFilter) //previous call to updatePrimaryColumn may fail if the model has no column. + sort(); // now it should succeed so we need to make sure to sort again + return; + } + if (m_recursiveFiltering) { + bool accept = false; + for (int row = start; row <= end; ++row) { + if (filterAcceptsRowInternal(row, source_parent)) { + accept = true; + break; + } + } + if (!accept) // the new rows have no descendants that match the filter, filter them out. + return; + // m_lastTopSource should now become visible + _q_sourceDataChanged(m_lastTopSource, m_lastTopSource, QList()); + } +} + +void QQmlSortFilterProxyModelPrivate::_q_sourceRowsAboutToBeRemoved( + const QModelIndex &source_parent, int start, int end) +{ + m_itemsBeingRemoved = QRowsRemoval(source_parent, start, end); + source_items_about_to_be_removed(source_parent, start, end, + QSortFilterProxyModelHelper::Direction::Rows); +} + +void QQmlSortFilterProxyModelPrivate::_q_sourceRowsRemoved( + const QModelIndex &source_parent, int start, int end) +{ + m_itemsBeingRemoved = QRowsRemoval(); + source_items_removed(source_parent, start, end, QSortFilterProxyModelHelper::Direction::Rows); + + if (m_recursiveFiltering) { + // Find out if removing this visible row means that some ascendant + // row can now be hidden. + // We go up until we find a row that should still be visible + // and then make QSFPM re-evaluate the last one we saw before that, + // to hide it. + QModelIndex to_hide; + QModelIndex source_ascendant = source_parent; + + while (source_ascendant.isValid()) { + if (filterAcceptsRowInternal(source_ascendant.row(), source_ascendant.parent())) + break; + + to_hide = source_ascendant; + source_ascendant = source_ascendant.parent(); + } + + if (to_hide.isValid()) + _q_sourceDataChanged(to_hide, to_hide, QList()); + } +} + +void QQmlSortFilterProxyModelPrivate::_q_sourceRowsAboutToBeMoved( + const QModelIndex &sourceParent, int /* sourceStart */, int /* sourceEnd */, const QModelIndex &destParent, int /* dest */) +{ + // Because rows which are contiguous in the source model might not be + // contiguous in the proxy due to sorting, the best thing we can do here is + // be specific about what parents are having their children changed. + // Optimize: Emit move signals if the proxy is not sorted. Will need to + // account for rows being filtered out though. + QList parents; + parents << sourceParent; + if (sourceParent != destParent) + parents << destParent; + _q_sourceLayoutAboutToBeChanged(parents, QAbstractItemModel::NoLayoutChangeHint); +} + +void QQmlSortFilterProxyModelPrivate::_q_sourceRowsMoved( + const QModelIndex &sourceParent, int /* sourceStart */, int /* sourceEnd */, const QModelIndex &destParent, int /* dest */) +{ + QList parents; + parents << sourceParent; + if (sourceParent != destParent) + parents << destParent; + _q_sourceLayoutChanged(parents, QAbstractItemModel::NoLayoutChangeHint); +} + +void QQmlSortFilterProxyModelPrivate::_q_sourceColumnsAboutToBeInserted( + const QModelIndex &source_parent, int start, int end) +{ + Q_UNUSED(start); + Q_UNUSED(end); + // Force the creation of a mapping now, even if it's empty. + // We need it because the proxy can be accessed at the moment it emits + // columnsAboutToBeInserted in insert_source_items + if (can_create_mapping(source_parent)) + create_mapping(source_parent); +} + +void QQmlSortFilterProxyModelPrivate::_q_sourceColumnsInserted( + const QModelIndex &source_parent, int start, int end) +{ + Q_Q(const QQmlSortFilterProxyModel); + source_items_inserted(source_parent, start, end, QSortFilterProxyModelHelper::Direction::Columns); + + if (source_parent.isValid()) + return; //we sort according to the root column only + if (m_primarySortColumn == -1) { + //we update the primayColumn depending on the m_proxySortColumn + if (updatePrimaryColumn() && m_dynamicSortFilter) + sort(); + } else { + if (start <= m_primarySortColumn) + m_primarySortColumn += end - start + 1; + + m_proxySortColumn = q->mapFromSource(model->index(0,m_primarySortColumn, source_parent)).column(); + } +} + +void QQmlSortFilterProxyModelPrivate::_q_sourceColumnsAboutToBeRemoved( + const QModelIndex &source_parent, int start, int end) +{ + source_items_about_to_be_removed(source_parent, start, end, + QSortFilterProxyModelHelper::Direction::Columns); +} + +void QQmlSortFilterProxyModelPrivate::_q_sourceColumnsRemoved( + const QModelIndex &source_parent, int start, int end) +{ + Q_Q(const QQmlSortFilterProxyModel); + source_items_removed(source_parent, start, end, QSortFilterProxyModelHelper::Direction::Columns); + + if (source_parent.isValid()) + return; //we sort according to the root column only + if (start <= m_primarySortColumn) { + if (end < m_primarySortColumn) + m_primarySortColumn -= end - start + 1; + else + m_primarySortColumn = -1; + } + + if (m_primarySortColumn >= 0) + m_proxySortColumn = q->mapFromSource(model->index(0,m_primarySortColumn, source_parent)).column(); + else + m_proxySortColumn = -1; +} + +void QQmlSortFilterProxyModelPrivate::_q_sourceColumnsAboutToBeMoved( + const QModelIndex &sourceParent, int /* sourceStart */, int /* sourceEnd */, const QModelIndex &destParent, int /* dest */) +{ + QList parents; + parents << sourceParent; + if (sourceParent != destParent) + parents << destParent; + _q_sourceLayoutAboutToBeChanged(parents, QAbstractItemModel::NoLayoutChangeHint); +} + +void QQmlSortFilterProxyModelPrivate::_q_sourceColumnsMoved( + const QModelIndex &sourceParent, int /* sourceStart */, int /* sourceEnd */, const QModelIndex &destParent, int /* dest */) +{ + QList parents; + parents << sourceParent; + if (sourceParent != destParent) + parents << destParent; + _q_sourceLayoutChanged(parents, QAbstractItemModel::NoLayoutChangeHint); +} + +/*! + \internal + */ +bool QQmlSortFilterProxyModelPrivate::containRoleForRecursiveFilter(const QList &roles) const +{ + if (!model || roles.isEmpty()) + return false; + // Get role names for the provided roles (roles arg.) + const QHash &roleNames = model->roleNames(); + QList filterRoles; + std::for_each(roles.constBegin(), roles.constEnd(), [&roleNames, &filterRoles](const int role){ + if (roleNames.contains(role)) + filterRoles.append(QString::fromUtf8(roleNames[role])); + }); + + // Check if the configured role filter matches with the provided roles + bool filterRoleConfigured = false; + if (!filterRoles.isEmpty() && (m_filters && m_filters->filters().isEmpty())) { + const QList proxyFilters = m_filters->filters(); + filterRoleConfigured = std::any_of(proxyFilters.constBegin(), proxyFilters.constEnd(), [&filterRoles](QQmlFilterBase* filterComp) { + if (const auto *roleFilter = qobject_cast(filterComp)) + return filterRoles.contains(roleFilter->roleName()); + return true; + }); + } + return m_recursiveFiltering && filterRoleConfigured; +} + +/*! + \internal + */ +bool QQmlSortFilterProxyModelPrivate::filterAcceptsRowInternal(int row, const QModelIndex &sourceIndex) const +{ + Q_Q(const QQmlSortFilterProxyModel); + const bool retVal = (q->filterAcceptsRow(row, sourceIndex) || + (m_autoAcceptChildRows && recursiveParentAcceptsRow(sourceIndex)) || + (m_recursiveFiltering && recursiveChildAcceptsRow(row, sourceIndex))); + return retVal; +} + +/*! + \internal + */ +bool QQmlSortFilterProxyModelPrivate::filterAcceptsColumnInternal(int row, const QModelIndex &sourceIndex) const +{ + Q_Q(const QQmlSortFilterProxyModel); + return q->filterAcceptsColumn(row, sourceIndex); +} + +QT_END_NAMESPACE + +#include "moc_qqmlsortfilterproxymodel_p.cpp" diff --git a/src/qmlmodels/sfpm/qqmlsortfilterproxymodel_p.h b/src/qmlmodels/sfpm/qqmlsortfilterproxymodel_p.h new file mode 100644 index 0000000000..5361da7fb7 --- /dev/null +++ b/src/qmlmodels/sfpm/qqmlsortfilterproxymodel_p.h @@ -0,0 +1,141 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QQMLSORTFILTERPROXYMODEL_H +#define QQMLSORTFILTERPROXYMODEL_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include +#include +#include +#include +#include + +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 filters READ filters NOTIFY filtersChanged FINAL) + Q_PROPERTY(QQmlListProperty 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 filters(); + // Provides configured sorters in this model + QQmlListProperty 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 roleNames() const override; + int itemRoleForName(const QString& roleName) const; + +protected: + bool filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const ; + bool filterAcceptsColumn(int sourceColumn, const QModelIndex& sourceParent) const ; + bool lessThan(const QModelIndex &sourceLeft, const QModelIndex &sourceRight) const ; + void sort(int column = 0, Qt::SortOrder sortOrder = Qt::AscendingOrder) override; + +private: + void classBegin() override {}; + void componentComplete() override; + void setSourceModel(QAbstractItemModel *sourceModel) override; + void invalidateFilter(); + +Q_SIGNALS: + void dynamicSortFilterChanged(); + void recursiveFilteringChanged(); + void autoAcceptChildRowsChanged(); + void filtersChanged(); + void sortersChanged(); + void modelChanged(); + void primarySorterChanged(); + +private: + Q_DISABLE_COPY(QQmlSortFilterProxyModel) + Q_DECLARE_PRIVATE(QQmlSortFilterProxyModel) + + friend class QSortFilterProxyModelLessThan; + friend class QSortFilterProxyModelGreaterThan; +}; + +QT_END_NAMESPACE + +#endif // QQMLSORTFILTERPROXYMODEL_H diff --git a/src/qmlmodels/sfpm/qsortfilterproxymodelhelper.cpp b/src/qmlmodels/sfpm/qsortfilterproxymodelhelper.cpp new file mode 100644 index 0000000000..026931bcf8 --- /dev/null +++ b/src/qmlmodels/sfpm/qsortfilterproxymodelhelper.cpp @@ -0,0 +1,760 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include +#include + +QT_BEGIN_NAMESPACE + +using IndexMap = QSortFilterProxyModelHelper::IndexMap; + + +QSortFilterProxyModelHelper::QSortFilterProxyModelHelper() +{ + +} + +QSortFilterProxyModelHelper::~QSortFilterProxyModelHelper() +{ + clearSourceIndexMapping(); +} + +template +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(); + 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> 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 &proxy_to_source, QList &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 &source_to_proxy, + const QList &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> QSortFilterProxyModelHelper::proxy_intervals_for_source_items( + const QList &source_to_proxy, const QList &source_items) const +{ + QList> 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 &interval = proxy_intervals[i]; + std::pair &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 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 &source_to_proxy, QList &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 &source_to_proxy = (direction == Direction::Rows) ? m->proxy_rows : m->proxy_columns; + QList &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 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 &orthogonal_proxy_to_source = (direction == Direction::Columns) ? m->source_rows : m->source_columns; + QList &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 &source_to_proxy = (direction == Direction::Rows) ? m->proxy_rows : m->proxy_columns; + QList &proxy_to_source = (direction == Direction::Rows) ? m->source_rows : m->source_columns; + + // figure out which items to remove + QList 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 &source_to_proxy = (direction == Direction::Rows) ? m->proxy_rows : m->proxy_columns; + QList &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 &source_to_proxy, QList &proxy_to_source, + const QList &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> &interval = *it; + const int proxy_start = interval.first; + const QList &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 &source_to_proxy, + QList &proxy_to_source, const QList &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 &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 QSortFilterProxyModelHelper::handle_filter_changed( + QList &source_to_proxy, QList &proxy_to_source, + const QModelIndex &source_parent, Direction direction) +{ + // Figure out which mapped items to remove + QList 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 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 rows_removed = (dir & Direction::Rows) ? handle_filter_changed(m->proxy_rows, m->source_rows, source_parent, Direction::Rows) : QSet(); + const QSet columns_removed = (dir & Direction::Columns) ? handle_filter_changed(m->proxy_columns, m->source_columns, source_parent, Direction::Columns) : QSet(); + + // 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 mappedChildren = m->mapped_children; + QList 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::const_iterator removeIt = indexesToRemove.constEnd(); + const QList::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(proxyModel()); + emit pModel->layoutAboutToBeChanged(QList(), 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(), QAbstractItemModel::VerticalSortHint); +} + + +QT_END_NAMESPACE diff --git a/src/qmlmodels/sfpm/qsortfilterproxymodelhelper_p.h b/src/qmlmodels/sfpm/qsortfilterproxymodelhelper_p.h new file mode 100644 index 0000000000..f270c75284 --- /dev/null +++ b/src/qmlmodels/sfpm/qsortfilterproxymodelhelper_p.h @@ -0,0 +1,233 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QSORTFILTERPROXYMODELHELPER_H +#define QSORTFILTERPROXYMODELHELPER_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists for the convenience +// of QAbstractItemModel*. This header file may change from version +// to version without notice, or even be removed. +// +// We mean it. +// +// + +#include +#include +#include + +QT_BEGIN_NAMESPACE + +class QSortFilterProxyModelLessThan; +class QSortFilterProxyModelGreaterThan; +class QQmlSortFilterProxyModel; + +using QModelIndexPairList = QList>; + +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 source_rows; + QList source_columns; + QList proxy_rows; + QList proxy_columns; + QList mapped_children; + QModelIndex source_parent; + }; + + using IndexMap = QHash; + mutable IndexMap source_index_mapping; + + static inline QSet qListToSet(const QList &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(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 &proxy_to_source, QList &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 &source_to_proxy, const QList &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 handle_filter_changed(QList &source_to_proxy, QList &proxy_to_source, + const QModelIndex &source_parent, Direction direction); + + virtual void insert_source_items(QList &source_to_proxy, QList &proxy_to_source, + const QList &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 &source_to_proxy, QList &proxy_to_source, + const QList &source_items, const QModelIndex &source_parent, + Direction direction, bool emit_signal = true); + virtual void remove_proxy_interval(QList &source_to_proxy, QList &proxy_to_source, int proxy_start, int proxy_end, + const QModelIndex &proxy_parent, Direction direction, bool emit_signal = true); + + virtual QList> proxy_intervals_for_source_items(const QList &source_to_proxy, + const QList &source_items) const; + virtual QList>> proxy_intervals_for_source_items_to_add(const QList &, + const QList &,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 &, const QModelIndex &) const {} + virtual bool lessThan(const QModelIndex&, const QModelIndex &) const { return true; } +}; + +struct QSortFilterProxyModelDataChanged +{ + QSortFilterProxyModelDataChanged(const QModelIndex &tl, const QModelIndex &br) + : topLeft(tl), bottomRight(br) { } + + QModelIndex topLeft; + QModelIndex bottomRight; +}; + +class QSortFilterProxyModelLessThan +{ +public: + inline QSortFilterProxyModelLessThan(int column, const QModelIndex &parent, + const QAbstractItemModel *source, + const QSortFilterProxyModelHelper *helper) + : sort_column(column), source_parent(parent), source_model(source), proxy_model(helper) {} + + inline bool operator()(int r1, int r2) const + { + QModelIndex i1 = source_model->index(r1, sort_column, source_parent); + QModelIndex i2 = source_model->index(r2, sort_column, source_parent); + return proxy_model->lessThan(i1, i2); + } + +private: + int sort_column; + QModelIndex source_parent; + const QAbstractItemModel *source_model; + const QSortFilterProxyModelHelper *proxy_model; +}; + +class QSortFilterProxyModelGreaterThan +{ +public: + inline QSortFilterProxyModelGreaterThan(int column, const QModelIndex &parent, + const QAbstractItemModel *source, + const QSortFilterProxyModelHelper *helper) + : sort_column(column), source_parent(parent), + source_model(source), proxy_model(helper) {} + + inline bool operator()(int r1, int r2) const + { + QModelIndex i1 = source_model->index(r1, sort_column, source_parent); + QModelIndex i2 = source_model->index(r2, sort_column, source_parent); + return proxy_model->lessThan(i2, i1); + } + +private: + int sort_column; + QModelIndex source_parent; + const QAbstractItemModel *source_model; + const QSortFilterProxyModelHelper *proxy_model; +}; + +//this struct is used to store what are the rows that are removed +//between a call to rowsAboutToBeRemoved and rowsRemoved +//it avoids readding rows to the mapping that are currently being removed +struct QRowsRemoval +{ + QRowsRemoval(const QModelIndex &parent_source, int start, int end) : parent_source(parent_source), start(start), end(end) + { + } + + QRowsRemoval() : start(-1), end(-1) + { + } + + bool contains(QModelIndex parent, int row) const + { + do { + if (parent == parent_source) + return row >= start && row <= end; + row = parent.row(); + parent = parent.parent(); + } while (row >= 0); + return false; + } +private: + QModelIndex parent_source; + int start; + int end; +}; + +QT_END_NAMESPACE + +#endif // QSORTFILTERPROXYMODELHELPER_H diff --git a/src/qmlmodels/sfpm/sorters/qqmlfunctionsorter.cpp b/src/qmlmodels/sfpm/sorters/qqmlfunctionsorter.cpp new file mode 100644 index 0000000000..99ab04b05a --- /dev/null +++ b/src/qmlmodels/sfpm/sorters/qqmlfunctionsorter.cpp @@ -0,0 +1,174 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include +#include +#include + +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(); + if (d->m_rhsParameterData.metaType().flags() & QMetaType::PointerToQObject) + delete d->m_rhsParameterData.value(); +} + +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 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(this), QMetaObject::InvokeMetaMethod, + d->m_method.methodIndex(), argv); + + return (retVal == 0) + ? QPartialOrdering::Equivalent + : ((retVal < 0) ? QPartialOrdering::Less : QPartialOrdering::Greater); +} + +QT_END_NAMESPACE + +#include "moc_qqmlfunctionsorter_p.cpp" diff --git a/src/qmlmodels/sfpm/sorters/qqmlfunctionsorter_p.h b/src/qmlmodels/sfpm/sorters/qqmlfunctionsorter_p.h new file mode 100644 index 0000000000..a473093600 --- /dev/null +++ b/src/qmlmodels/sfpm/sorters/qqmlfunctionsorter_p.h @@ -0,0 +1,57 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QQMLFUNCTIONSORTER_P_H +#define QQMLFUNCTIONSORTER_P_H + +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include +#include + +QT_BEGIN_NAMESPACE + +class QQmlSortFilterProxyModel; +class QQmlFunctionSorterPrivate; + +class Q_QMLMODELS_EXPORT QQmlFunctionSorter : public QQmlSorterBase, public QQmlParserStatus +{ + Q_OBJECT + Q_INTERFACES(QQmlParserStatus) + QML_NAMED_ELEMENT(FunctionSorter) + +public: + explicit QQmlFunctionSorter(QObject *parent = nullptr); + ~QQmlFunctionSorter() override; + + virtual QPartialOrdering compare(const QModelIndex&, const QModelIndex&, const QQmlSortFilterProxyModel *) const override; + +private: + void classBegin() override {}; + void componentComplete() override; + +private: + Q_DECLARE_PRIVATE(QQmlFunctionSorter) +}; + +class QQmlFunctionSorterPrivate : public QQmlRoleSorterPrivate +{ + Q_DECLARE_PUBLIC (QQmlFunctionSorter) + +public: + QMetaMethod m_method; + mutable QVariant m_lhsParameterData; + mutable QVariant m_rhsParameterData; +}; + +QT_END_NAMESPACE + +#endif // QQMLFUNCTIONSORTER_P_H diff --git a/src/qmlmodels/sfpm/sorters/qqmlrolesorter.cpp b/src/qmlmodels/sfpm/sorters/qqmlrolesorter.cpp new file mode 100644 index 0000000000..67d2f755dd --- /dev/null +++ b/src/qmlmodels/sfpm/sorters/qqmlrolesorter.cpp @@ -0,0 +1,81 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include +#include + +QT_BEGIN_NAMESPACE + +/*! + \qmltype RoleSorter + \inherits Sorter + \inqmlmodule QtQml.Models + \since 6.10 + \preliminary + \brief Sort data in a \l SortFilterProxyModel based on configured role + name. + + RoleSorter allows the user to sort the data according to the role name + as configured in the source model. + + The RoleSorter can be configured in the sort filter proxy model as below, + + \qml + SortFilterProxyModel { + model: sourceModel + sorters: [ + RoleSorter { roleName: "firstname" } + ] + } + \endqml +*/ + +QQmlRoleSorter::QQmlRoleSorter(QObject *parent) : + QQmlSorterBase (new QQmlRoleSorterPrivate, parent) +{ +} + +QQmlRoleSorter::QQmlRoleSorter(QQmlSorterBasePrivate *priv, QObject *parent) : + QQmlSorterBase (priv, parent) +{ +} + +/*! + \qmlproperty string RoleSorter::roleName + + This property holds the role name that will be used to sort the data. + + The default value is display role. +*/ +void QQmlRoleSorter::setRoleName(const QString& roleName) +{ + Q_D(QQmlRoleSorter); + if (d->m_roleName == roleName) + return; + d->m_roleName = roleName; + // Update the model for the change in the role name + emit roleNameChanged(); + // Invalidate the model for the change in the role name + invalidate(); +} + +const QString& QQmlRoleSorter::roleName() const +{ + Q_D(const QQmlRoleSorter); + return d->m_roleName; +} + +/*! + \internal +*/ +QPartialOrdering QQmlRoleSorter::compare(const QModelIndex& sourceLeft, const QModelIndex& sourceRight, const QQmlSortFilterProxyModel *proxyModel) const +{ + Q_D(const QQmlRoleSorter); + if (int role = proxyModel->itemRoleForName(d->m_roleName); role > -1) + return QVariant::compare(proxyModel->sourceData(sourceLeft, role), proxyModel->sourceData(sourceRight, role)); + return QPartialOrdering::Unordered; +} + +QT_END_NAMESPACE + +#include "moc_qqmlrolesorter_p.cpp" diff --git a/src/qmlmodels/sfpm/sorters/qqmlrolesorter_p.h b/src/qmlmodels/sfpm/sorters/qqmlrolesorter_p.h new file mode 100644 index 0000000000..0d2c0d16ff --- /dev/null +++ b/src/qmlmodels/sfpm/sorters/qqmlrolesorter_p.h @@ -0,0 +1,58 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QQMLROLESORTER_P_H +#define QQMLROLESORTER_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include + +QT_BEGIN_NAMESPACE + +class QQmlSortFilterProxyModel; +class QQmlRoleSorterPrivate; + +class Q_QMLMODELS_EXPORT QQmlRoleSorter : public QQmlSorterBase +{ + Q_OBJECT + Q_PROPERTY(QString roleName READ roleName WRITE setRoleName NOTIFY roleNameChanged) + QML_NAMED_ELEMENT(RoleSorter) + +public: + explicit QQmlRoleSorter(QObject *parent = nullptr); + QQmlRoleSorter(QQmlSorterBasePrivate *priv, QObject *parent = nullptr); + ~QQmlRoleSorter() = default; + + const QString& roleName() const; + void setRoleName(const QString& roleName); + + QPartialOrdering compare(const QModelIndex& sourceLeft, const QModelIndex& sourceRight, const QQmlSortFilterProxyModel *proxyModel) const override; + +Q_SIGNALS: + void roleNameChanged(); + +private: + Q_DECLARE_PRIVATE(QQmlRoleSorter) +}; + +class QQmlRoleSorterPrivate : public QQmlSorterBasePrivate +{ + Q_DECLARE_PUBLIC (QQmlRoleSorter) + +public: + QString m_roleName = QString::fromUtf8("display"); +}; + +QT_END_NAMESPACE + +#endif // QQMLROLESORTER_P_H diff --git a/src/qmlmodels/sfpm/sorters/qqmlsorterbase.cpp b/src/qmlmodels/sfpm/sorters/qqmlsorterbase.cpp new file mode 100644 index 0000000000..eb0b9888cc --- /dev/null +++ b/src/qmlmodels/sfpm/sorters/qqmlsorterbase.cpp @@ -0,0 +1,136 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include +#include + +QT_BEGIN_NAMESPACE + +/*! + \qmltype Sorter + \inherits QtObject + \inqmlmodule QtQml.Models + \since 6.10 + \preliminary + \brief Abstract base type providing functionality common to sorters. + + Sorter provides a set of common properties for all the sorters that they + inherit from. +*/ + +QQmlSorterBase::QQmlSorterBase(QQmlSorterBasePrivate *privObj, QObject *parent) + : QObject (*privObj, parent) +{ +} + +/*! + \qmlproperty bool Sorter::enabled + + This property enables the \l SortFilterProxyModel to consider this sorter + while sorting the model data. + + The default value is \c true. +*/ +bool QQmlSorterBase::enabled() const +{ + Q_D(const QQmlSorterBase); + return d->m_enabled; + +} + +void QQmlSorterBase::setEnabled(const bool enabled) +{ + Q_D(QQmlSorterBase); + if (d->m_enabled == enabled) + return; + d->m_enabled = enabled; + invalidate(true); + emit enabledChanged(); +} + +/*! + \qmlproperty Qt::SortOrder Sorter::sortOrder + + This property holds the order used by \l SortFilterProxyModel when sorting + the model data. + + The default value is \c Qt::AscendingOrder. +*/ +Qt::SortOrder QQmlSorterBase::sortOrder() const +{ + Q_D(const QQmlSorterBase); + return d->m_sortOrder; +} + +void QQmlSorterBase::setSortOrder(const Qt::SortOrder sortOrder) +{ + Q_D(QQmlSorterBase); + if (d->m_sortOrder == sortOrder) + return; + d->m_sortOrder = sortOrder; + invalidate(); + emit sortOrderChanged(); +} + +/*! + \qmlproperty int Sorter::priority + + This property holds the priority that is given to this sorter compared to + other sorters. The lesser value results in a higher priority and the higher + value results in a lower priority. + + The default value is \c -1. +*/ +int QQmlSorterBase::priority() const +{ + Q_D(const QQmlSorterBase); + return d->m_sorterPriority; +} + +void QQmlSorterBase::setPriority(const int priority) +{ + Q_D(QQmlSorterBase); + if (d->m_sorterPriority == priority) + return; + d->m_sorterPriority = priority; + invalidate(true); + emit priorityChanged(); +} + +/*! + \qmlproperty int Sorter::column + + This property holds the column that this sorter is applied to. + + The default value is \c 0. +*/ +int QQmlSorterBase::column() const +{ + Q_D(const QQmlSorterBase); + return d->m_sortColumn; +} + +void QQmlSorterBase::setColumn(const int column) +{ + Q_D(QQmlSorterBase); + if (d->m_sortColumn == column) + return; + d->m_sortColumn = column; + invalidate(); + emit columnChanged(); +} + +/*! + \internal +*/ +void QQmlSorterBase::invalidate(bool updateCache) +{ + // Update the cached filters and invalidate the model + if (updateCache) + emit invalidateCache(this); + emit invalidateModel(); +} + +QT_END_NAMESPACE + +#include "moc_qqmlsorterbase_p.cpp" diff --git a/src/qmlmodels/sfpm/sorters/qqmlsorterbase_p.h b/src/qmlmodels/sfpm/sorters/qqmlsorterbase_p.h new file mode 100644 index 0000000000..52a95a26c1 --- /dev/null +++ b/src/qmlmodels/sfpm/sorters/qqmlsorterbase_p.h @@ -0,0 +1,89 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QQMLSORTERBASE_H +#define QQMLSORTERBASE_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include +#include +#include +#include +#include + +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::max(); + int m_sortColumn = 0; +}; + +QT_END_NAMESPACE + +#endif // QQMLSORTERBASE_H diff --git a/src/qmlmodels/sfpm/sorters/qqmlsortercompositor.cpp b/src/qmlmodels/sfpm/sorters/qqmlsortercompositor.cpp new file mode 100644 index 0000000000..4409b8c3e3 --- /dev/null +++ b/src/qmlmodels/sfpm/sorters/qqmlsortercompositor.cpp @@ -0,0 +1,225 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include +#include + +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(q->parent()); +} + +void QQmlSorterCompositor::append(QQmlListProperty *sorterComp, QQmlSorterBase* sorter) +{ + auto *sorterCompositor = reinterpret_cast(sorterComp->object); + sorterCompositor->append(sorter); +} + +qsizetype QQmlSorterCompositor::count(QQmlListProperty *sorterComp) +{ + auto *sorterCompositor = reinterpret_cast (sorterComp->object); + return sorterCompositor->count(); +} + +QQmlSorterBase *QQmlSorterCompositor::at(QQmlListProperty *sorterComp, qsizetype index) +{ + auto *sorterCompositor = reinterpret_cast (sorterComp->object); + return sorterCompositor->at(index); +} + +void QQmlSorterCompositor::clear(QQmlListProperty *sorterComp) +{ + auto *sorterCompositor = reinterpret_cast (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 QQmlSorterCompositor::sorters() +{ + Q_D(QQmlSorterCompositor); + return d->m_sorters; +} + +QQmlListProperty QQmlSorterCompositor::sortersListProperty() +{ + Q_D(QQmlSorterCompositor); + return QQmlListProperty(reinterpret_cast(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 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 sorters = d->m_sorters; + std::stable_sort(sorters.begin(), sorters.end(), + [](QQmlSorterBase *sorterLeft, QQmlSorterBase *sorterRight) { + return sorterLeft->priority() < sorterRight->priority(); + }); + // Cache only the filters that are need to be evaluated (in order) + std::copy_if(sorters.begin(), sorters.end(), std::back_inserter(d->m_effectiveSorters), + [](QQmlSorterBase *sorter) { return sorter->enabled(); }); + // If there is no primary sorter set by the user explicitly, reset the + // primary sorter according to the sorters in the lists + d->resetPrimarySorter(); + } +} + +bool QQmlSorterCompositor::lessThan(const QModelIndex& sourceLeft, const QModelIndex& sourceRight, const QQmlSortFilterProxyModel *proxyModel) const +{ + Q_D(const QQmlSorterCompositor); + for (const auto &sorter : d->m_effectiveSorters) { + const int sortSection = sorter->column(); + if ((sortSection > -1) && (sortSection < proxyModel->sourceModel()->columnCount())) { + const auto *sourceModel = proxyModel->sourceModel(); + const QPartialOrdering result = sorter->compare(sourceModel->index(sourceLeft.row(), sortSection), + sourceModel->index(sourceRight.row(), sortSection), + proxyModel); + if ((result == QPartialOrdering::Less) || (result == QPartialOrdering::Greater)) + return (result < 0); + } + } + // Verify the index order when the ordering is either equal or unordered + return sourceLeft.row() < sourceRight.row(); +} + +void QQmlSorterCompositorPrivate::setPrimarySorter(QQmlSorterBase *sorter) +{ + if (sorter == nullptr || + (std::find(m_sorters.constBegin(), m_sorters.constEnd(), sorter) != m_sorters.constEnd())) { + m_primarySorter = sorter; + if (m_primarySorter && m_primarySorter->enabled()) { + m_sfpmModel->setPrimarySortOrder(m_primarySorter->sortOrder()); + m_sfpmModel->setPrimarySortColumn(m_primarySorter->column()); + return; + } + } + resetPrimarySorter(); +} + +void QQmlSorterCompositorPrivate::resetPrimarySorter() +{ + if (!m_primarySorter) { + if (!m_effectiveSorters.isEmpty()) { + // Set the primary sort column and its order to the proxy model + const auto *sorter = m_effectiveSorters.at(0); + m_sfpmModel->setPrimarySortOrder(sorter->sortOrder()); + m_sfpmModel->setPrimarySortColumn(sorter->column()); + } else { + // By default reset the sort order to ascending order and the + // column to 0 + m_sfpmModel->setPrimarySortOrder(Qt::AscendingOrder); + m_sfpmModel->setPrimarySortColumn(0); + } + } +} + +QT_END_NAMESPACE + +#include "moc_qqmlsortercompositor_p.cpp" diff --git a/src/qmlmodels/sfpm/sorters/qqmlsortercompositor_p.h b/src/qmlmodels/sfpm/sorters/qqmlsortercompositor_p.h new file mode 100644 index 0000000000..f99b06dd0f --- /dev/null +++ b/src/qmlmodels/sfpm/sorters/qqmlsortercompositor_p.h @@ -0,0 +1,82 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QQMLSORTERCOMPOSITOR_H +#define QQMLSORTERCOMPOSITOR_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include + +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 sorters(); + QQmlListProperty sortersListProperty(); + + static void append(QQmlListProperty *sorterComp, QQmlSorterBase *sorter); + static qsizetype count(QQmlListProperty *sorterComp); + static QQmlSorterBase* at(QQmlListProperty *sorterComp, qsizetype index); + static void clear(QQmlListProperty *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 primarySorter() const { return m_primarySorter; } + void resetPrimarySorter(); + + // Holds sorters in the same order as declared in the qml + QList m_sorters; + // Holds effective sorters that will be evaluated with the + // model content + QList m_effectiveSorters; + QQmlSortFilterProxyModel *m_sfpmModel = nullptr; + QPointer m_primarySorter; +}; + +QT_END_NAMESPACE + +#endif // QQMLSORTERCOMPOSITOR_H diff --git a/src/qmlmodels/sfpm/sorters/qqmlstringsorter.cpp b/src/qmlmodels/sfpm/sorters/qqmlstringsorter.cpp new file mode 100644 index 0000000000..ee61ad5abd --- /dev/null +++ b/src/qmlmodels/sfpm/sorters/qqmlstringsorter.cpp @@ -0,0 +1,150 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include +#include + +QT_BEGIN_NAMESPACE + +/*! + \qmltype StringSorter + \inherits Sorter + \inqmlmodule QtQml.Models + \since 6.10 + \preliminary + \brief Sort data in a \l SortFilterProxyModel based on ordering of the + locale. + + StringSorter allows the user to sort the data according to the role name + as configured in the source model. StringSorter compares strings according + to a localized collation algorithm. + + The StringSorter can be configured in the sort filter proxy model as below, + + \qml + SortFilterProxyModel { + model: sourceModel + sorters: [ + StringSorter { roleName: "name" } + ] + } + \endqml +*/ + +QQmlStringSorter::QQmlStringSorter(QObject *parent) : + QQmlRoleSorter (new QQmlStringSorterPrivate, parent) +{ +} + +/*! + \qmlproperty Qt::CaseSensitivity StringSorter::caseSensitivity + + This property holds the case sensitivity of the sorter. + + The default value is Qt::CaseSensitive. +*/ +Qt::CaseSensitivity QQmlStringSorter::caseSensitivity() const +{ + Q_D(const QQmlStringSorter); + return d->m_collator.caseSensitivity(); +} + +void QQmlStringSorter::setCaseSensitivity(Qt::CaseSensitivity caseSensitivity) +{ + Q_D(QQmlStringSorter); + if (d->m_collator.caseSensitivity() == caseSensitivity) + return; + d->m_collator.setCaseSensitivity(caseSensitivity); + emit caseSensitivityChanged(); + invalidate(); +} + +/*! + \qmlproperty bool StringSorter::ignorePunctuation + + This property holds whether the sorter ignores punctation. + If \c ignorePunctuation is \c true, punctuation characters and symbols are + ignored when determining sort order. + + The default value is \c false. +*/ +bool QQmlStringSorter::ignorePunctuation() const +{ + Q_D(const QQmlStringSorter); + return d->m_collator.ignorePunctuation(); +} + +void QQmlStringSorter::setIgnorePunctuation(bool ignorePunctuation) +{ + Q_D(QQmlStringSorter); + if (d->m_collator.ignorePunctuation() == ignorePunctuation) + return; + d->m_collator.setIgnorePunctuation(ignorePunctuation); + emit ignorePunctuationChanged(); + invalidate(); +} + +/*! + \qmlproperty Locale StringSorter::locale + + This property holds the locale of the sorter. + + The default value is \l QLocale::system() +*/ +QLocale QQmlStringSorter::locale() const +{ + Q_D(const QQmlStringSorter); + return d->m_collator.locale(); +} + +void QQmlStringSorter::setLocale(const QLocale &locale) +{ + Q_D(QQmlStringSorter); + if (d->m_collator.locale() == locale) + return; + d->m_collator.setLocale(locale); + emit localeChanged(); + invalidate(); +} + +/*! + \qmlproperty bool StringSorter::numericMode + + This property holds whether the numeric mode of the sorter is enabled. + + The default value is \c false. +*/ +bool QQmlStringSorter::numericMode() const +{ + Q_D(const QQmlStringSorter); + return d->m_collator.numericMode(); +} + +void QQmlStringSorter::setNumericMode(bool numericMode) +{ + Q_D(QQmlStringSorter); + if (d->m_collator.numericMode() == numericMode) + return; + + d->m_collator.setNumericMode(numericMode); + emit numericModeChanged(); + invalidate(); +} +/*! + \internal +*/ +QPartialOrdering QQmlStringSorter::compare(const QModelIndex &sourceLeft, const QModelIndex &sourceRight, const QQmlSortFilterProxyModel* proxyModel) const +{ + Q_D(const QQmlStringSorter); + if (int role = proxyModel->itemRoleForName(d->m_roleName); role > -1) { + const QVariant first = proxyModel->sourceData(sourceLeft, role); + const QVariant second = proxyModel->sourceData(sourceRight, role); + const int result = d->m_collator.compare(first.toString(), second.toString()); + return (result <= 0) ? ((result < 0) ? QPartialOrdering::Less : QPartialOrdering::Equivalent) : QPartialOrdering::Greater; + } + return QPartialOrdering::Unordered; +} + +QT_END_NAMESPACE + +#include "moc_qqmlstringsorter_p.cpp" diff --git a/src/qmlmodels/sfpm/sorters/qqmlstringsorter_p.h b/src/qmlmodels/sfpm/sorters/qqmlstringsorter_p.h new file mode 100644 index 0000000000..c4dd3ec0d9 --- /dev/null +++ b/src/qmlmodels/sfpm/sorters/qqmlstringsorter_p.h @@ -0,0 +1,73 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QQMLSTRINGSORTER_H +#define QQMLSTRINGSORTER_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include +#include + +QT_BEGIN_NAMESPACE + +class QQmlSortFilterProxyModel; +class QQmlStringSorterPrivate; + +class Q_QMLMODELS_EXPORT QQmlStringSorter : public QQmlRoleSorter +{ + Q_OBJECT + Q_PROPERTY(Qt::CaseSensitivity caseSensitivity READ caseSensitivity WRITE setCaseSensitivity NOTIFY caseSensitivityChanged) + Q_PROPERTY(bool ignorePunctuation READ ignorePunctuation WRITE setIgnorePunctuation NOTIFY ignorePunctuationChanged) + Q_PROPERTY(QLocale locale READ locale WRITE setLocale NOTIFY localeChanged) + Q_PROPERTY(bool numericMode READ numericMode WRITE setNumericMode NOTIFY numericModeChanged) + QML_NAMED_ELEMENT(StringSorter) + +public: + explicit QQmlStringSorter(QObject *parent = nullptr); + ~QQmlStringSorter() = default; + + Qt::CaseSensitivity caseSensitivity() const; + void setCaseSensitivity(Qt::CaseSensitivity caseSensitivity); + + bool ignorePunctuation() const; + void setIgnorePunctuation(bool ignorePunctation); + + QLocale locale() const; + void setLocale(const QLocale& locale); + + bool numericMode() const; + void setNumericMode(bool numericMode); + + QPartialOrdering compare(const QModelIndex& sourceLeft, const QModelIndex& sourceRight, const QQmlSortFilterProxyModel *proxyModel) const override; + +Q_SIGNALS: + void caseSensitivityChanged(); + void ignorePunctuationChanged(); + void localeChanged(); + void numericModeChanged(); + +private: + Q_DECLARE_PRIVATE(QQmlStringSorter) +}; + +class QQmlStringSorterPrivate : public QQmlRoleSorterPrivate +{ + Q_DECLARE_PUBLIC (QQmlStringSorter) + +public: + QCollator m_collator; +}; + +QT_END_NAMESPACE + +#endif // QQMLSTRINGSORTER_H diff --git a/tests/auto/qml/CMakeLists.txt b/tests/auto/qml/CMakeLists.txt index 6f7cd7f1d8..3f8ea50d9c 100644 --- a/tests/auto/qml/CMakeLists.txt +++ b/tests/auto/qml/CMakeLists.txt @@ -148,6 +148,7 @@ if(QT_FEATURE_private_tests) add_subdirectory(qqmlimport) add_subdirectory(qqmlobjectmodel) add_subdirectory(qqmltablemodel) + add_subdirectory(qqmlsortfilterproxymodel) add_subdirectory(qqmltreemodeltotablemodel) add_subdirectory(qv4assembler) add_subdirectory(qv4mm) diff --git a/tests/auto/qml/qqmlsortfilterproxymodel/CMakeLists.txt b/tests/auto/qml/qqmlsortfilterproxymodel/CMakeLists.txt new file mode 100644 index 0000000000..38d1a65886 --- /dev/null +++ b/tests/auto/qml/qqmlsortfilterproxymodel/CMakeLists.txt @@ -0,0 +1,46 @@ +# Copyright (C) 2025 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +##################################################################### +## tst_qqmlsortfilterproxymodel Test: +##################################################################### + +if(NOT QT_BUILD_STANDALONE_TESTS AND NOT QT_BUILDING_QT) + cmake_minimum_required(VERSION 3.16) + project(tst_qqmlsortfilterproxymodel LANGUAGES CXX) + find_package(Qt6BuildInternals REQUIRED COMPONENTS STANDALONE_TEST) +endif() + +# Collect test data +file(GLOB_RECURSE test_data_glob + RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} + data/*) +list(APPEND test_data ${test_data_glob}) + +qt_internal_add_test(tst_qqmlsortfilterproxymodel + SOURCES + tst_qqmlsortfilterproxymodel.cpp + LIBRARIES + Qt::Gui + Qt::Qml + Qt::QmlPrivate + Qt::Quick + Qt::QuickPrivate + Qt::QuickTestUtilsPrivate + Qt::LabsQmlModels + Qt::LabsQmlModelsPrivate + TESTDATA ${test_data} +) + +## Scopes: +##################################################################### + +qt_internal_extend_target(tst_qqmlsortfilterproxymodel CONDITION ANDROID OR IOS + DEFINES + QT_QMLTEST_DATADIR=":/data" +) + +qt_internal_extend_target(tst_qqmlsortfilterproxymodel CONDITION NOT ANDROID AND NOT IOS + DEFINES + QT_QMLTEST_DATADIR="${CMAKE_CURRENT_SOURCE_DIR}/data" +) diff --git a/tests/auto/qml/qqmlsortfilterproxymodel/data/Utility.js b/tests/auto/qml/qqmlsortfilterproxymodel/data/Utility.js new file mode 100644 index 0000000000..caa49fa3f7 --- /dev/null +++ b/tests/auto/qml/qqmlsortfilterproxymodel/data/Utility.js @@ -0,0 +1,18 @@ +.pragma library + +const romanTable = { + "I" : 1, + "II" : 2, + "III" : 3, + "IV" : 4, + "V" : 5, + "VI" : 6, + "VII" : 7, + "VIII": 8, + "IX" : 9, + "X" : 10, +}; + +function getInteger(strIndex) { + return romanTable[strIndex]; +} diff --git a/tests/auto/qml/qqmlsortfilterproxymodel/data/sfpmCommon.qml b/tests/auto/qml/qqmlsortfilterproxymodel/data/sfpmCommon.qml new file mode 100644 index 0000000000..3691407f15 --- /dev/null +++ b/tests/auto/qml/qqmlsortfilterproxymodel/data/sfpmCommon.qml @@ -0,0 +1,41 @@ +import QtQml +import "Utility.js" as Sfpmutility + +QtObject { + id: sfpmTestObject + + function getValue(value) { + return Sfpmutility.getInteger(value) + } + + component SorterRoleData: QtObject { property string display } + component FilterRoleData0: QtObject { property string column0 } + component FilterRoleData1: QtObject { property string column1 } + + // Filters + property ValueFilter valueFilter: ValueFilter {} + property FunctionFilter functionFilter0: FunctionFilter { + property string expression: "" + function filter(data: FilterRoleData0) : bool { + return eval(expression) + } + } + property FunctionFilter functionFilter1: FunctionFilter { + property string expression: "" + function filter(data: FilterRoleData1) : bool { + return eval(expression) + } + } + + // Sorters + property RoleSorter roleSorter: RoleSorter {} + property StringSorter stringSorter: StringSorter {} + property FunctionSorter functionSorter: FunctionSorter { + property string expression: "" + function sort(lhsData: SorterRoleData, rhsData: SorterRoleData) : int { + return eval(expression) + } + } + + property SortFilterProxyModel sfpmProxyModel: SortFilterProxyModel {} +} diff --git a/tests/auto/qml/qqmlsortfilterproxymodel/tst_qqmlsortfilterproxymodel.cpp b/tests/auto/qml/qqmlsortfilterproxymodel/tst_qqmlsortfilterproxymodel.cpp new file mode 100644 index 0000000000..1cde600684 --- /dev/null +++ b/tests/auto/qml/qqmlsortfilterproxymodel/tst_qqmlsortfilterproxymodel.cpp @@ -0,0 +1,1050 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class tst_QQmlSortFilterProxyModel : public QQmlDataTest +{ + Q_OBJECT + +public: + tst_QQmlSortFilterProxyModel() : QQmlDataTest(QT_QMLTEST_DATADIR) {} + +private slots: + void checkProxyModel(); + + void validateSfpmFilterListProperty(); + void validateSfpmSorterListProperty(); + + void validateFilters(); + void multipleFilters(); + void filterAbility(); + void filterInverted(); + void validateValueFilterProperties(); + void validateFunctionFilterProperties(); + void verifyDynamicSortFilterProperty(); + + void validateSorters_data(); + void validateSorters(); + void multipleSorters_data(); + void multipleSorters(); + void sorterAbility_data(); + void sorterAbility(); + void validateRoleSorterProperties_data(); + void validateRoleSorterProperties(); + void validateStringSorterProperties_data(); + void validateStringSorterProperties(); + void validateFunctionSorterProperties_data(); + void validateFunctionSorterProperties(); + + void primarySorter_data(); + void primarySorter(); +}; + +class CustomTableModel : public QAbstractTableModel +{ + Q_OBJECT +public: + static const int s_rowCount = 5; + static const int s_columnCount = 3; + CustomTableModel(QObject *parent = nullptr) : QAbstractTableModel(parent) { + m_data.resize(s_rowCount); + for (int r = 0; r < s_rowCount; ++r) { + auto &curRow = m_data[r]; + curRow.resize(s_columnCount); + for (int c = 0; c < s_columnCount; ++c) + m_data[r][c] = QString::fromLatin1("Data") + QString::number(r) + QString::number(c); + } + } + QHash roleNames() const override { + QHash roles; + for (int columnIndex = 0; columnIndex < s_columnCount; columnIndex++) + roles.insertOrAssign(Qt::UserRole + columnIndex, + QString(QString::fromLatin1("column") + QString::number(columnIndex)).toUtf8()); + return roles; + } + int rowCount(const QModelIndex &parent = QModelIndex()) const override { + return parent.isValid() ? 0 : m_data.count(); + } + int columnCount(const QModelIndex &parent = QModelIndex()) const override { + return parent.isValid() ? 0 : s_columnCount; + } + // 0, 1, 2 => + // if row == 0: + // first = 0 + // last = count + // else if (row == rowCount()) + // first = rowCount - 1 + // last = rowCount() - 1 + count + // else + // first = row + // last = row + count + bool insertRows(int row, int count, const QModelIndex &parent = QModelIndex()) override { + int first = (row ? (row < rowCount() ? row : rowCount() - 1) : count - 1); + int last = (row ? (row < rowCount() ? row + count - 1: rowCount() - 1 + count - 1) : count - 1); + beginInsertRows(parent, first, last); + int totalRowCount = rowCount() + count; + m_data.resize(totalRowCount); + for (int r = 0; r < totalRowCount; ++r) { + auto &curRow = m_data[r]; + curRow.resize(columnCount()); + for (int c = 0; c < columnCount(); ++c) + m_data[r][c] = QString::fromLatin1("Data") + QString::number(r) + QString::number(c); + } + endInsertRows(); + return true; + } + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override { + if ((role < Qt::UserRole && role >= Qt::UserRole + s_columnCount) || !index.isValid() || (index.column() != (role - Qt::UserRole))) + return QVariant(); + return m_data[index.row()][role - Qt::UserRole]; + } + bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override { + if ((role < Qt::UserRole && role >= Qt::UserRole + s_columnCount) || !index.isValid() || (index.column() != (role - Qt::UserRole))) + return false; + m_data[index.row()][role - Qt::UserRole] = value; + dataChanged(index, index, {role}); + return true; + } + QList> m_data; +}; + +class ExpressionObject : public QObject +{ + Q_OBJECT + Q_PROPERTY(QQmlScriptString scriptString READ scriptString WRITE setScriptString) + +public: + ExpressionObject(QObject *parent = nullptr) : QObject(parent) {} + + QQmlScriptString scriptString() const { return m_scriptString; } + void setScriptString(QQmlScriptString scriptString) { m_scriptString = scriptString; } + +private: + QQmlScriptString m_scriptString; +}; + +void tst_QQmlSortFilterProxyModel::checkProxyModel() +{ + std::unique_ptr standardModel(new QStandardItemModel(2, 2, this)); + std::unique_ptr proxyModel(new QQmlSortFilterProxyModel(this)); + + QVariant sourceModel = QVariant::fromValue(standardModel.get()); + // Set the standard model as the source model to the proxy + proxyModel->setModel(sourceModel); + + // Test proxy model to ensure that it abides by the rules of item models + // QAbstractItemModelTester verifies it internally when its created + { + std::unique_ptr abstractModelTest(new QAbstractItemModelTester(proxyModel.get(), this)); + } + + // Set data in the source model and verify the same can be accessed through the proxy model + for (int row = 0; row < standardModel->rowCount(); ++row) { + for (int column = 0; column < standardModel->columnCount(); ++column) { + QString testData; + testData += QString("Data") + QString::number(row) + QString::number(column); + standardModel->setData(standardModel->index(row, column), testData, Qt::DisplayRole); + } + } + + QVariant modelVar = QVariant::fromValue(standardModel.get()); + // Reset the model to the standard model + proxyModel->setModel(modelVar); + QCOMPARE(proxyModel->rowCount(), standardModel->rowCount()); + QCOMPARE(proxyModel->columnCount(), standardModel->columnCount()); + for (int row = 0; row < proxyModel->rowCount(); ++row) { + for (int column = 0; column < proxyModel->columnCount(); ++column) { + QString testData; + testData += QString("Data") + QString::number(row) + QString::number(column); + QCOMPARE(testData, proxyModel->data(proxyModel->index(row, column, QModelIndex()), Qt::DisplayRole)); + } + } +} + +void tst_QQmlSortFilterProxyModel::validateSfpmFilterListProperty() +{ + QQmlEngine engine; + QQmlComponent component(&engine, testFileUrl("sfpmCommon.qml")); + QVERIFY2(component.errorString().isEmpty(), component.errorString().toUtf8()); + QScopedPointer object(component.create()); + QVERIFY(!object.isNull()); + + auto *sfpmModel = object->property("sfpmProxyModel").value(); + QVERIFY(sfpmModel); + QSignalSpy filterChangedSignal(sfpmModel, SIGNAL(filtersChanged())); + QVERIFY(filterChangedSignal.isValid()); + auto filters = sfpmModel->property("filters").value>(); + auto *valueFilter = object->property("valueFilter").value(); + QVERIFY(valueFilter); + + // Append filter to the sfpm filters list + sfpmModel->filters().append(&filters, valueFilter); + QCOMPARE(filterChangedSignal.count(), 1); + QCOMPARE(filters.count(&filters), 1); + + auto *functionFilter = object->property("functionFilter0").value(); + QVERIFY(functionFilter); + sfpmModel->filters().append(&filters, functionFilter); + // Validate the count of the filters in the list + QCOMPARE(filterChangedSignal.count(), 2); + QCOMPARE(filters.count(&filters), 2); + + // Access the filters in the filter list + QCOMPARE(filters.at(&filters, 0), valueFilter); + QCOMPARE(filters.at(&filters, 1), functionFilter); + + // Clear the filter list + sfpmModel->filters().clear(&filters); + QCOMPARE(filterChangedSignal.count(), 3); + QCOMPARE(filters.count(&filters), 0); +} + +void tst_QQmlSortFilterProxyModel::validateFilters() +{ + QQmlEngine engine; + QQmlComponent component(&engine, testFileUrl("sfpmCommon.qml")); + QVERIFY2(component.errorString().isEmpty(), component.errorString().toUtf8()); + QScopedPointer object(component.create()); + QVERIFY(!object.isNull()); + + CustomTableModel tableModel; + QVariant sourceModel = QVariant::fromValue(&tableModel); + QCOMPARE(tableModel.rowCount(), CustomTableModel::s_rowCount); + QCOMPARE(tableModel.columnCount(), CustomTableModel::s_columnCount); + + // Set the value filter in Qml SFPM + auto *sfpmModel = object->property("sfpmProxyModel").value(); + QVERIFY(sfpmModel); + sfpmModel->setModel(sourceModel); + auto filters = sfpmModel->property("filters").value>(); + + auto resetFilters = [sfpmModel, &filters] { + sfpmModel->filters().clear(&filters); + QCOMPARE(sfpmModel->rowCount(), CustomTableModel::s_rowCount); + QCOMPARE(sfpmModel->columnCount(), CustomTableModel::s_columnCount); + }; + + // Value filter + { + auto *valueFilter = object->property("valueFilter").value(); + QVERIFY(valueFilter); + valueFilter->setValue(tableModel.data(tableModel.index(0, 0, QModelIndex()), Qt::UserRole)); + valueFilter->setColumn(0); + valueFilter->setRoleName(QString::fromLatin1("column0")); + + sfpmModel->filters().append(&filters, valueFilter); + QCOMPARE(sfpmModel->rowCount(), 1); + QCOMPARE(sfpmModel->data(sfpmModel->index(0, 0, QModelIndex()), Qt::UserRole), + tableModel.data(tableModel.index(0, 0, QModelIndex()), Qt::UserRole)); + } + + // Function filter + { + resetFilters(); + auto *functionFilter = object->property("functionFilter1").value(); + QVERIFY(functionFilter); + functionFilter->setProperty("expression", "data.column1 === \"Data01\""); + + sfpmModel->filters().append(&filters, functionFilter); + QCOMPARE(sfpmModel->rowCount(), 1); + QCOMPARE(sfpmModel->columnCount(), CustomTableModel::s_columnCount); + } + + resetFilters(); +} + +void tst_QQmlSortFilterProxyModel::multipleFilters() +{ + QQmlEngine engine; + QQmlComponent component(&engine, testFileUrl("sfpmCommon.qml")); + QVERIFY2(component.errorString().isEmpty(), component.errorString().toUtf8()); + QScopedPointer object(component.create()); + QVERIFY(!object.isNull()); + + CustomTableModel tableModel; + QVariant sourceModel = QVariant::fromValue(&tableModel); + QCOMPARE(tableModel.rowCount(), CustomTableModel::s_rowCount); + QCOMPARE(tableModel.columnCount(), CustomTableModel::s_columnCount); + + // Set the value filter in Qml SFPM + auto *sfpmModel = object->property("sfpmProxyModel").value(); + QVERIFY(sfpmModel); + sfpmModel->setModel(sourceModel); + auto *functionFilter = object->property("functionFilter0").value(); + QVERIFY(functionFilter); + functionFilter->setProperty("expression", "true"); + + auto *valueFilter = object->property("valueFilter").value(); + QVERIFY(valueFilter); + valueFilter->setValue(tableModel.data(tableModel.index(0, 0, QModelIndex()), Qt::UserRole)); + valueFilter->setColumn(0); + valueFilter->setRoleName(QString::fromLatin1("column0")); + + auto filters = sfpmModel->property("filters").value>(); + QSignalSpy filterChangedSignal(sfpmModel, SIGNAL(filtersChanged())); + sfpmModel->filters().append(&filters, functionFilter); + QCOMPARE(filterChangedSignal.count(), 1); + sfpmModel->filters().append(&filters, valueFilter); + QCOMPARE(filterChangedSignal.count(), 2); + QCOMPARE(sfpmModel->rowCount(), 1); + QCOMPARE(sfpmModel->columnCount(), 3); +} + +void tst_QQmlSortFilterProxyModel::filterAbility() +{ + QQmlEngine engine; + QQmlComponent component(&engine, testFileUrl("sfpmCommon.qml")); + QVERIFY2(component.errorString().isEmpty(), component.errorString().toUtf8()); + QScopedPointer object(component.create()); + QVERIFY(!object.isNull()); + + CustomTableModel tableModel; + QVariant sourceModel = QVariant::fromValue(&tableModel); + QCOMPARE(tableModel.rowCount(), CustomTableModel::s_rowCount); + QCOMPARE(tableModel.columnCount(), CustomTableModel::s_columnCount); + + // Set the value filter in Qml SFPM + auto *sfpmModel = object->property("sfpmProxyModel").value(); + QVERIFY(sfpmModel); + sfpmModel->setModel(sourceModel); + + auto *functionFilter = object->property("functionFilter0").value(); + QVERIFY(functionFilter); + // Filter first two rows of data through regex + functionFilter->setProperty("expression", "/Data[0-1]{2}/.exec(data.column0) !== null"); + + auto *valueFilter = object->property("valueFilter").value(); + QVERIFY(valueFilter); + valueFilter->setValue(tableModel.data(tableModel.index(0, 0, QModelIndex()), Qt::UserRole)); + valueFilter->setColumn(0); + valueFilter->setRoleName(QString::fromLatin1("column0")); + + auto filters = sfpmModel->property("filters").value>(); + QSignalSpy filterChangedSignal(sfpmModel, SIGNAL(filtersChanged())); + sfpmModel->filters().append(&filters, functionFilter); + QCOMPARE(filterChangedSignal.count(), 1); + sfpmModel->filters().append(&filters, valueFilter); + QCOMPARE(filterChangedSignal.count(), 2); + // Function and role both filters the data + QCOMPARE(sfpmModel->rowCount(), 1); + QCOMPARE(sfpmModel->columnCount(), 3); + // Disable the value filter and check the filtered data + valueFilter->setEnabled(false); + QCOMPARE(sfpmModel->filters().count(&filters), 2); + // Function filter still enabled in the filters list + QCOMPARE(sfpmModel->rowCount(), 2); + QCOMPARE(sfpmModel->columnCount(), 3); +} + +void tst_QQmlSortFilterProxyModel::filterInverted() +{ + QQmlEngine engine; + QQmlComponent component(&engine, testFileUrl("sfpmCommon.qml")); + QVERIFY2(component.errorString().isEmpty(), component.errorString().toUtf8()); + QScopedPointer object(component.create()); + QVERIFY(!object.isNull()); + + CustomTableModel tableModel; + QVariant sourceModel = QVariant::fromValue(&tableModel); + QCOMPARE(tableModel.rowCount(), CustomTableModel::s_rowCount); + QCOMPARE(tableModel.columnCount(), CustomTableModel::s_columnCount); + + // Set the value filter in Qml SFPM + auto *sfpmModel = object->property("sfpmProxyModel").value(); + QVERIFY(sfpmModel); + sfpmModel->setModel(sourceModel); + auto *valueFilter = object->property("valueFilter").value(); + QVERIFY(valueFilter); + valueFilter->setValue(tableModel.data(tableModel.index(0, 0, QModelIndex()), Qt::UserRole)); + valueFilter->setColumn(0); + valueFilter->setRoleName(QString::fromLatin1("column0")); + valueFilter->setInvert(true); + auto filters = sfpmModel->property("filters").value>(); + QSignalSpy filterChangedSignal(sfpmModel, SIGNAL(filtersChanged())); + sfpmModel->filters().append(&filters, valueFilter); + QCOMPARE(filterChangedSignal.count(), 1); + QCOMPARE(sfpmModel->rowCount(), 4); + QCOMPARE(sfpmModel->columnCount(), CustomTableModel::s_columnCount); +} + +void tst_QQmlSortFilterProxyModel::validateValueFilterProperties() +{ + QQmlEngine engine; + QQmlComponent component(&engine, testFileUrl("sfpmCommon.qml")); + QVERIFY2(component.errorString().isEmpty(), component.errorString().toUtf8()); + QScopedPointer object(component.create()); + QVERIFY(!object.isNull()); + + CustomTableModel tableModel; + QVariant sourceModel = QVariant::fromValue(&tableModel); + QCOMPARE(tableModel.rowCount(), CustomTableModel::s_rowCount); + QCOMPARE(tableModel.columnCount(), CustomTableModel::s_columnCount); + + // Set the value filter in Qml SFPM + auto *sfpmModel = object->property("sfpmProxyModel").value(); + QVERIFY(sfpmModel); + sfpmModel->setModel(sourceModel); + auto filters = sfpmModel->property("filters").value>(); + auto *valueFilter = object->property("valueFilter").value(); + QVERIFY(valueFilter); + sfpmModel->filters().append(&filters, valueFilter); + QCOMPARE(sfpmModel->filters().count(&filters), 1); + + QSignalSpy valueChngdSpy(valueFilter, &QQmlValueFilter::valueChanged); + QVERIFY(valueChngdSpy.isValid()); + QSignalSpy roleNameChangedSpy(valueFilter, &QQmlValueFilter::roleNameChanged); + QVERIFY(roleNameChangedSpy.isValid()); + QSignalSpy columnChangedSpy(valueFilter, &QQmlValueFilter::columnChanged); + QVERIFY(columnChangedSpy.isValid()); + + valueFilter->setRoleName(QString::fromLatin1("column1")); + QCOMPARE(roleNameChangedSpy.count(), 1); + valueFilter->setColumn(1); + QCOMPARE(columnChangedSpy.count(), 1); + valueFilter->setValue(tableModel.data(tableModel.index(1, 1, QModelIndex()), Qt::UserRole + 1)); + QCOMPARE(valueChngdSpy.count(), 1); + QCOMPARE(sfpmModel->rowCount(), 1); + QCOMPARE(sfpmModel->columnCount(), CustomTableModel::s_columnCount); + QCOMPARE(sfpmModel->data(sfpmModel->index(0, 1, QModelIndex()), Qt::UserRole + 1), + tableModel.data(tableModel.index(1, 1, QModelIndex()), Qt::UserRole + 1)); + + valueFilter->setRoleName(QString::fromLatin1("column2")); + QCOMPARE(roleNameChangedSpy.count(), 2); + QCOMPARE(sfpmModel->rowCount(), 0); + QCOMPARE(sfpmModel->columnCount(), CustomTableModel::s_columnCount); + valueFilter->setValue(tableModel.data(tableModel.index(2, 2, QModelIndex()), Qt::UserRole + 2)); + QCOMPARE(valueChngdSpy.count(), 2); + valueFilter->setColumn(2); + QCOMPARE(columnChangedSpy.count(), 2); + QCOMPARE(sfpmModel->rowCount(), 1); + QCOMPARE(sfpmModel->columnCount(), CustomTableModel::s_columnCount); + QCOMPARE(sfpmModel->data(sfpmModel->index(0, 2, QModelIndex()), Qt::UserRole + 2), + tableModel.data(tableModel.index(2, 2, QModelIndex()), Qt::UserRole + 2)); + + valueFilter->setColumn(-1); + QCOMPARE(sfpmModel->rowCount(), 1); + QCOMPARE(sfpmModel->columnCount(), CustomTableModel::s_columnCount); + QCOMPARE(sfpmModel->data(sfpmModel->index(0, 2, QModelIndex()), Qt::UserRole + 2), + tableModel.data(tableModel.index(2, 2, QModelIndex()), Qt::UserRole + 2)); +} + +void tst_QQmlSortFilterProxyModel::validateFunctionFilterProperties() +{ + QQmlEngine engine; + QQmlComponent component(&engine, testFileUrl("sfpmCommon.qml")); + QVERIFY2(component.errorString().isEmpty(), component.errorString().toUtf8()); + QScopedPointer object(component.create()); + QVERIFY(!object.isNull()); + + CustomTableModel tableModel; + QVariant sourceModel = QVariant::fromValue(&tableModel); + QCOMPARE(tableModel.rowCount(), CustomTableModel::s_rowCount); + QCOMPARE(tableModel.columnCount(), CustomTableModel::s_columnCount); + + // Set the value filter in Qml SFPM + auto *sfpmModel = object->property("sfpmProxyModel").value(); + QVERIFY(sfpmModel); + sfpmModel->setModel(sourceModel); + auto filters = sfpmModel->property("filters").value>(); + + auto *functionFilter = object->property("functionFilter1").value(); + QVERIFY(functionFilter); + functionFilter->setProperty("expression", "true"); + + filters.append(&filters, functionFilter); + QCOMPARE(sfpmModel->rowCount(), CustomTableModel::s_rowCount); + QCOMPARE(sfpmModel->columnCount(), CustomTableModel::s_columnCount); + + functionFilter->setProperty("expression", "data.column1 === \"Data01\""); + // Need to reset here because the model does not see the change in the filter's expression. + filters.clear(&filters); + filters.append(&filters, functionFilter); + + QCOMPARE(sfpmModel->rowCount(), 1); + QCOMPARE(sfpmModel->columnCount(), CustomTableModel::s_columnCount); +} + +void tst_QQmlSortFilterProxyModel::verifyDynamicSortFilterProperty() +{ + QQmlEngine engine; + QQmlComponent component(&engine, testFileUrl("sfpmCommon.qml")); + QVERIFY2(component.errorString().isEmpty(), component.errorString().toUtf8()); + QScopedPointer object(component.create()); + QVERIFY(!object.isNull()); + + CustomTableModel tableModel; + QVariant sourceModel = QVariant::fromValue(&tableModel); + QCOMPARE(tableModel.rowCount(), CustomTableModel::s_rowCount); + QCOMPARE(tableModel.columnCount(), CustomTableModel::s_columnCount); + + // Set the value filter in Qml SFPM + auto *sfpmModel = object->property("sfpmProxyModel").value(); + QVERIFY(sfpmModel); + sfpmModel->setModel(sourceModel); + QCOMPARE(sfpmModel->rowCount(), CustomTableModel::s_rowCount); + QCOMPARE(sfpmModel->columnCount(), CustomTableModel::s_columnCount); + + auto filters = sfpmModel->property("filters").value>(); + QCOMPARE(filters.count(&filters), 0); + + auto *functionFilter = object->property("functionFilter1").value(); + functionFilter->setProperty("expression", "data.column1 === \"Data01\""); + QVERIFY(functionFilter); + filters.append(&filters, functionFilter); + QCOMPARE(filters.count(&filters), 1); + QCOMPARE(sfpmModel->rowCount(), 1); + QCOMPARE(sfpmModel->columnCount(), CustomTableModel::s_columnCount); + + QCOMPARE(sfpmModel->dynamicSortFilter(), true); + sfpmModel->setDynamicSortFilter(false); + QCOMPARE(sfpmModel->dynamicSortFilter(), false); + tableModel.setData(tableModel.index(0, 1), QString::fromLatin1("Data99"), Qt::UserRole + 1); + QCOMPARE(sfpmModel->rowCount(), 1); + QCOMPARE(sfpmModel->columnCount(), CustomTableModel::s_columnCount); + + sfpmModel->invalidate(); + QCOMPARE(sfpmModel->rowCount(), 0); +} + +void tst_QQmlSortFilterProxyModel::validateSfpmSorterListProperty() +{ + QQmlEngine engine; + QQmlComponent component(&engine, testFileUrl("sfpmCommon.qml")); + QVERIFY2(component.errorString().isEmpty(), component.errorString().toUtf8()); + QScopedPointer object(component.create()); + QVERIFY(!object.isNull()); + + auto *sfpmModel = object->property("sfpmProxyModel").value(); + QVERIFY(sfpmModel); + QSignalSpy sorterChangedSignal(sfpmModel, SIGNAL(sortersChanged())); + QVERIFY(sorterChangedSignal.isValid()); + auto sorters = sfpmModel->property("sorters").value>(); + auto *roleSorter = object->property("roleSorter").value(); + QVERIFY(roleSorter); + + // Append filter to the sfpm filters list + sfpmModel->sorters().append(&sorters, roleSorter); + QCOMPARE(sorterChangedSignal.count(), 1); + QCOMPARE(sorters.count(&sorters), 1); + + auto *stringSorter = object->property("stringSorter").value(); + QVERIFY(stringSorter); + sfpmModel->sorters().append(&sorters, stringSorter); + // Validate the count of the filters in the list + QCOMPARE(sorterChangedSignal.count(), 2); + QCOMPARE(sorters.count(&sorters), 2); + + // Access the filters in the filter list + QCOMPARE(sorters.at(&sorters, 0), roleSorter); + QCOMPARE(sorters.at(&sorters, 1), stringSorter); + + // Clear the filter list + sfpmModel->sorters().clear(&sorters); + QCOMPARE(sorterChangedSignal.count(), 3); + QCOMPARE(sorters.count(&sorters), 0); +} + +void tst_QQmlSortFilterProxyModel::validateSorters_data() +{ + QTest::addColumn("sorterType"); + QTest::addColumn("sortOrder"); + QTest::addColumn("initial"); + QTest::addColumn("expected"); + + QTest::newRow("role sorter ascending") << "roleSorter" + << Qt::AscendingOrder + << QStringList{"canvas", "test", "shine", "beneficiary", "withdrawal"} + << QStringList{"beneficiary", "canvas", "shine", "test", "withdrawal"}; + QTest::newRow("role sorter descending") << "roleSorter" + << Qt::DescendingOrder + << QStringList{"canvas", "test", "shine", "beneficiary", "withdrawal"} + << QStringList{"withdrawal", "test", "shine", "canvas", "beneficiary"}; + + QTest::newRow("string sorter ascending") << "stringSorter" + << Qt::AscendingOrder + << QStringList{"æfgt", "abcd", "zyd", "Æagt", "Øtyu"} + << QStringList{"abcd", "zyd", "Æagt", "æfgt", "Øtyu"}; + QTest::newRow("string sorter descending") << "stringSorter" + << Qt::DescendingOrder + << QStringList{"æfgt", "abcd", "zyd", "Æagt", "Øtyu"} + << QStringList{"Øtyu", "æfgt", "Æagt", "zyd", "abcd"}; + + + QTest::newRow("function sorter ascending") << "functionSorter" + << Qt::AscendingOrder + << QStringList{"I", "X", "IV", "IX", "V"} + << QStringList{"I", "IV", "V", "IX", "X"}; + QTest::newRow("function sorter descending") << "functionSorter" + << Qt::DescendingOrder + << QStringList{"I", "X", "IV", "IX", "V"} + << QStringList{"X", "IX", "V", "IV", "I"}; +} + +void tst_QQmlSortFilterProxyModel::validateSorters() +{ + QFETCH(QString, sorterType); + QFETCH(Qt::SortOrder, sortOrder); + QFETCH(QStringList, initial); + QFETCH(QStringList, expected); + + QQmlEngine engine; + QQmlComponent component(&engine, testFileUrl("sfpmCommon.qml")); + QVERIFY2(component.errorString().isEmpty(), component.errorString().toUtf8()); + QScopedPointer object(component.create()); + QVERIFY(!object.isNull()); + + QStandardItemModel standardModel; + for (const auto &data : initial) + standardModel.appendRow(new QStandardItem(data)); + + auto *sfpmModel = object->property("sfpmProxyModel").value(); + QVERIFY(sfpmModel); + QVariant sourceModel = QVariant::fromValue(&standardModel); + sfpmModel->setModel(sourceModel); + auto sorters = sfpmModel->property("sorters").value>(); + auto *sorter = object->property(sorterType.toLatin1()).value(); + QVERIFY(sorter); + sorter->setSortOrder(sortOrder); + + // Set the language and terrritory for the collation + if (auto stringSorter = qobject_cast(sorter)) + stringSorter->setLocale(QLocale(QLocale::NorwegianBokmal, QLocale::Country::Norway)); + + // Create expression on the fly and use it for the function sorter + if (auto functionSorter = qobject_cast(sorter)) { + functionSorter->setProperty("expression", + "(sfpmTestObject.getValue(lhsData.display) < sfpmTestObject.getValue(rhsData.display)) ? -1 : \ + (sfpmTestObject.getValue(lhsData.display) === sfpmTestObject.getValue(rhsData.display)) ? 0 : 1"); + } + + sorters.append(&sorters, sorter); + QCOMPARE(sorters.count(&sorters), 1); + int row = 0; + for (const auto &data: expected) + QCOMPARE(sfpmModel->data(sfpmModel->index(row++, 0, QModelIndex())), data); +} + +void tst_QQmlSortFilterProxyModel::multipleSorters_data() +{ + QTest::addColumn("sortersList"); + QTest::addColumn("sortOrder"); + QTest::addColumn>("initialColumnData"); + QTest::addColumn>("expectedColumnData"); + + QTest::newRow("multiple sorters") << QStringList({"roleSorter", "stringSorter"}) + << Qt::AscendingOrder + << QList({{"abc", "ghi", "def", "xyz", "abc"}, {25, 36, 20, 30, 17}}) + << QList({{"abc", "abc", "def", "ghi", "xyz"}, {17, 25, 20, 36, 30}}); +} + +void tst_QQmlSortFilterProxyModel::multipleSorters() +{ + QFETCH(QStringList, sortersList); + QFETCH(Qt::SortOrder, sortOrder); + QFETCH(QList, initialColumnData); + QFETCH(QList, expectedColumnData); + + QQmlEngine engine; + QQmlComponent component(&engine, testFileUrl("sfpmCommon.qml")); + QVERIFY2(component.errorString().isEmpty(), component.errorString().toUtf8()); + QScopedPointer object(component.create()); + QVERIFY(!object.isNull()); + + QStandardItemModel standardModel(initialColumnData.at(0).count(), initialColumnData.count()); + int column = -1; + for (const auto &columndata : initialColumnData) { + int row = 0; ++column; + for (const auto &data: columndata) + standardModel.setData(standardModel.index(row++, column, QModelIndex()), data, Qt::DisplayRole); + } + + auto *sorter1 = object->property(sortersList.at(0).toLatin1()).value(); // RoleSorter + QVERIFY(sorter1); + sorter1->setSortOrder(sortOrder); + sorter1->setColumn(1); + + auto *sorter2 = object->property(sortersList.at(1).toLatin1()).value(); // StringSorter + QVERIFY(sorter2); + sorter2->setSortOrder(sortOrder); + sorter2->setColumn(0); + + auto *sfpmModel = object->property("sfpmProxyModel").value(); + QVERIFY(sfpmModel); + QVariant sourceModel = QVariant::fromValue(&standardModel); + sfpmModel->setModel(sourceModel); + auto sorters = sfpmModel->property("sorters").value>(); + sorters.append(&sorters, sorter2); // String sorter (for column 0) + sorters.append(&sorters, sorter1); // Role sorter (for column 1) + + int expcolumn = -1; + for (const auto &columndata : expectedColumnData) { + int row = 0; ++expcolumn; + for (const auto &data: columndata) + QCOMPARE(sfpmModel->data(sfpmModel->index(row++, expcolumn, QModelIndex()), Qt::DisplayRole), data); + } +} + +void tst_QQmlSortFilterProxyModel::sorterAbility_data() +{ + QTest::addColumn("sortersList"); + QTest::addColumn("sortersAbility"); + QTest::addColumn("sortOrder"); + QTest::addColumn>("initialColumnData"); + QTest::addColumn>("expectedColumnData"); + + QTest::newRow("enable or disable sorter") << QStringList({"roleSorter", "stringSorter"}) + << QVariantList({false, true}) + << Qt::AscendingOrder + << QList({{"abc", "ghi", "def", "xyz", "abc"}, {25, 36, 20, 30, 17}}) + << QList({{"abc", "abc", "def", "ghi", "xyz"}, {25, 17, 20, 36, 30}}); +} + +void tst_QQmlSortFilterProxyModel::sorterAbility() +{ + QFETCH(QStringList, sortersList); + QFETCH(Qt::SortOrder, sortOrder); + QFETCH(QVariantList, sortersAbility); + QFETCH(QList, initialColumnData); + QFETCH(QList, expectedColumnData); + + QQmlEngine engine; + QQmlComponent component(&engine, testFileUrl("sfpmCommon.qml")); + QVERIFY2(component.errorString().isEmpty(), component.errorString().toUtf8()); + QScopedPointer object(component.create()); + QVERIFY(!object.isNull()); + + QStandardItemModel standardModel(initialColumnData.at(0).count(), initialColumnData.count()); + int column = -1; + for (const auto &columnData : initialColumnData) { + int row = 0; ++column; + for (const auto &data: columnData) + standardModel.setData(standardModel.index(row++, column, QModelIndex()), data, Qt::DisplayRole); + } + + auto *sfpmModel = object->property("sfpmProxyModel").value(); + QVERIFY(sfpmModel); + QVariant sourceModel = QVariant::fromValue(&standardModel); + sfpmModel->setModel(sourceModel); + + auto sorters = sfpmModel->property("sorters").value>(); + auto *sorter1 = object->property(sortersList.at(0).toLatin1()).value(); + QVERIFY(sorter1); + sorter1->setSortOrder(sortOrder); + auto *sorter2 = object->property(sortersList.at(1).toLatin1()).value(); + QVERIFY(sorter2); + sorter2->setSortOrder(sortOrder); + + sorters.append(&sorters, sorter2); + sorters.append(&sorters, sorter1); + + // Disable the role sorter and thus sorting shall be restored to the previous state + if (!sortersAbility.at(0).value()) + sorter1->setEnabled(false); + + column = -1; + for (const auto &columnData : expectedColumnData) { + int row = 0; ++column; + for (const auto &data: columnData) + QCOMPARE(sfpmModel->data(sfpmModel->index(row++, column, QModelIndex()), Qt::DisplayRole), data); + } +} + +void tst_QQmlSortFilterProxyModel::validateRoleSorterProperties_data() +{ + QTest::addColumn("sorterType"); + QTest::addColumn("sortOrder"); + QTest::addColumn("sortColumn"); + QTest::addColumn("sorterAbility"); + QTest::addColumn>("initialColumnData"); + QTest::addColumn>("expectedColumnData"); + + QTest::newRow("role sorter enabled_ascend_column0") << "roleSorter" + << Qt::AscendingOrder << 0 << true + << QList({{"abc", "ghi", "def", "xyz", "abc"}, {25, 36, 20, 30, 17}}) + << QList({{"abc", "abc", "def", "ghi", "xyz"}, {25, 17, 20, 36, 30}}); + + QTest::newRow("role sorter enabled_descend_column1") << "roleSorter" + << Qt::DescendingOrder << 1 << true + << QList({{"abc", "ghi", "def", "xyz", "abc"}, {25, 36, 20, 30, 17}}) + << QList({{"ghi", "xyz", "abc", "def", "abc"}, {36, 30, 25, 20, 17}}); + + QTest::newRow("role sorter disabled_descend_column0") << "roleSorter" + << Qt::AscendingOrder << 0 << false + << QList({{"abc", "ghi", "def", "xyz", "abc"}, {25, 36, 20, 30, 17}}) + << QList({{"abc", "ghi", "def", "xyz", "abc"}, {25, 36, 20, 30, 17}}); +} + +void tst_QQmlSortFilterProxyModel::validateRoleSorterProperties() +{ + QFETCH(QString, sorterType); + QFETCH(Qt::SortOrder, sortOrder); + QFETCH(int, sortColumn); + QFETCH(bool, sorterAbility); + QFETCH(QList, initialColumnData); + QFETCH(QList, expectedColumnData); + + QQmlEngine engine; + QQmlComponent component(&engine, testFileUrl("sfpmCommon.qml")); + QVERIFY2(component.errorString().isEmpty(), component.errorString().toUtf8()); + QScopedPointer object(component.create()); + QVERIFY(!object.isNull()); + + QStandardItemModel standardModel(initialColumnData.at(0).count(), initialColumnData.count()); + int column = -1; + for (const auto &columnData: initialColumnData) { + int row = 0; ++column; + for (const auto &data: columnData) + standardModel.setData(standardModel.index(row++, column, QModelIndex()), data, Qt::DisplayRole); + } + + auto *sorter = object->property(sorterType.toLatin1()).value(); + QVERIFY(sorter); + sorter->setColumn(sortColumn); + sorter->setSortOrder(sortOrder); + sorter->setEnabled(sorterAbility); + + auto *sfpmModel = object->property("sfpmProxyModel").value(); + QVERIFY(sfpmModel); + QVariant sourceModel = QVariant::fromValue(&standardModel); + sfpmModel->setModel(sourceModel); + + auto sorters = sfpmModel->property("sorters").value>(); + sorters.append(&sorters, sorter); + + column = -1; + for (const auto &columnData : expectedColumnData) { + int row = 0; ++column; + for (const auto &data: columnData) + QCOMPARE(sfpmModel->data(sfpmModel->index(row++, column, QModelIndex()), Qt::DisplayRole), data); + } +} + +void tst_QQmlSortFilterProxyModel::validateStringSorterProperties_data() +{ + QTest::addColumn("sorterType"); + QTest::addColumn("ignorePunctuation"); + QTest::addColumn("locale"); + QTest::addColumn("numericMode"); + + QTest::addColumn("initialColumnData"); + QTest::addColumn("expectedColumnData"); + + QTest::newRow("string sorter incorrect locale") << "stringSorter" + << false << "en_GB" << false + << QVariantList({"æfgt", "abcd", "zyd", "Æagt", "Øtyu"}) + << QVariantList({"abcd", "Æagt", "æfgt", "Øtyu", "zyd"}); + + QTest::newRow("string sorter correct locale") << "stringSorter" + << false << "nb_NO" << false + << QVariantList({"æfgt", "abcd", "zyd", "Æagt", "Øtyu"}) + << QVariantList({"abcd", "zyd", "Æagt", "æfgt", "Øtyu"}); + + QTest::newRow("string sorter ignore punctuation") << "stringSorter" + << true << "nb_NO" << false + << QVariantList({"æfgt", "&abcd", "!zyd", "Æagt", "Øtyu"}) + << QVariantList({"&abcd", "!zyd", "Æagt", "æfgt", "Øtyu"}); + + QTest::newRow("string sorter enable punctuation") << "stringSorter" + << false << "nb_NO" << false + << QVariantList({"æfgt", "&abcd", "!zyd", "Æagt", "Øtyu"}) + << QVariantList({"!zyd", "&abcd", "Æagt", "æfgt", "Øtyu"}); + + QTest::newRow("string sorter enable numeral mode") << "stringSorter" + << false << "nb_NO" << true + << QVariantList({"æ97fgt", "1000abcd", "30zyd", "100Æagt", "99Øtyu"}) + << QVariantList({"30zyd", "99Øtyu", "100Æagt", "1000abcd", "æ97fgt"}); + + QTest::newRow("string sorter unset numeral mode") << "stringSorter" + << false << "nb_NO" << false + << QVariantList({"æ97fgt", "1000abcd", "30zyd", "100Æagt", "99Øtyu"}) + << QVariantList({"1000abcd", "100Æagt", "30zyd", "99Øtyu", "æ97fgt"}); +} + +void tst_QQmlSortFilterProxyModel::validateStringSorterProperties() +{ + QFETCH(QString, sorterType); + QFETCH(bool, ignorePunctuation); + QFETCH(QString, locale); + QFETCH(bool, numericMode); + + QFETCH(QVariantList, initialColumnData); + QFETCH(QVariantList, expectedColumnData); + + QQmlEngine engine; + QQmlComponent component(&engine, testFileUrl("sfpmCommon.qml")); + QVERIFY2(component.errorString().isEmpty(), component.errorString().toUtf8()); + QScopedPointer object(component.create()); + QVERIFY(!object.isNull()); + + QStandardItemModel standardModel(initialColumnData.count(), 1); + int row = -1; + for (const auto &data: initialColumnData) + standardModel.setData(standardModel.index(++row, 0, QModelIndex()), data, Qt::DisplayRole); + + auto *sfpmModel = object->property("sfpmProxyModel").value(); + QVERIFY(sfpmModel); + QVariant sourceModel = QVariant::fromValue(&standardModel); + sfpmModel->setModel(sourceModel); + + auto *sorter = object->property(sorterType.toLatin1()).value(); + QVERIFY(sorter); + sorter->setIgnorePunctuation(ignorePunctuation); + sorter->setNumericMode(numericMode); + sorter->setLocale(QLocale(locale)); + + auto sorters = sfpmModel->property("sorters").value>(); + sorters.append(&sorters, sorter); + + row = -1; + for (const auto &data: expectedColumnData) + QCOMPARE(sfpmModel->data(sfpmModel->index(++row, 0, QModelIndex()), Qt::DisplayRole), data); +} + +void tst_QQmlSortFilterProxyModel::validateFunctionSorterProperties_data() +{ + QTest::addColumn("sorterType"); + QTest::addColumn("expression"); + QTest::addColumn("initialColumnData"); + QTest::addColumn("expectedColumnData"); + + QTest::newRow("function sorter no role data") + << "functionSorter" + << "(lhsData.display < rhsData.display) ? -1 : 1" + << QVariantList({"V", "IV", "X", "IX", "III"}) + << QVariantList({"III", "IV", "IX", "V", "X"}); + + QTest::newRow("function sorter access incorrect role names") + << "functionSorter" + << "(lhsData.display1 < rhsData.display2) ? -1 : 1" + << QVariantList({"V", "IV", "X", "IX", "III"}) + << QVariantList({"V", "IV", "X", "IX", "III"}); + + QTest::newRow("function sorter access valid role name") + << "functionSorter" + << "(sfpmTestObject.getValue(lhsData.display) < sfpmTestObject.getValue(rhsData.display) ? 1 : -1)" + << QVariantList({"V", "IV", "X", "IX", "III"}) + << QVariantList({"X", "IX", "V", "IV", "III"}); +} + +void tst_QQmlSortFilterProxyModel::validateFunctionSorterProperties() +{ + QFETCH(QString, sorterType); + QFETCH(QString, expression); + QFETCH(QVariantList, initialColumnData); + QFETCH(QVariantList, expectedColumnData); + + QQmlEngine engine; + QQmlComponent component(&engine, testFileUrl("sfpmCommon.qml")); + QVERIFY2(component.errorString().isEmpty(), component.errorString().toUtf8()); + QScopedPointer object(component.create()); + QVERIFY(!object.isNull()); + + QStandardItemModel standardModel(initialColumnData.count(), 1); + int row = -1; + for (const auto &data: initialColumnData) + standardModel.setData(standardModel.index(++row, 0, QModelIndex()), data, Qt::DisplayRole); + + auto *sfpmModel = object->property("sfpmProxyModel").value(); + QVERIFY(sfpmModel); + QVariant sourceModel = QVariant::fromValue(&standardModel); + sfpmModel->setModel(sourceModel); + + auto *sorter = object->property(sorterType.toLatin1()).value(); + QVERIFY(sorter); + + // Custom qml property specifically for testing + if (!expression.isEmpty()) + sorter->setProperty("expression", expression); + + auto sorters = sfpmModel->property("sorters").value>(); + sorters.append(&sorters, sorter); + + row = -1; + for (const auto &data: expectedColumnData) + QCOMPARE(sfpmModel->data(sfpmModel->index(++row, 0, QModelIndex()), Qt::DisplayRole), data); +} + +void tst_QQmlSortFilterProxyModel::primarySorter_data() +{ + QTest::addColumn("sorter"); + QTest::addColumn("primarySorter"); + QTest::addColumn("primarySortColumn"); + QTest::addColumn("sortOrder"); + QTest::addColumn>("initialColumnData"); + QTest::addColumn>("expectedColumnData"); + + QTest::newRow("multiple sorters") << "stringSorter" + << "roleSorter" + << 1 + << Qt::AscendingOrder + << QList({{"abc", "ghi", "def", "xyz", "abc"}, {25, 36, 20, 30, 17}}) + << QList({{"abc", "def", "abc", "xyz", "ghi"}, {17, 20, 25, 30, 36}}); +} + +void tst_QQmlSortFilterProxyModel::primarySorter() +{ + QFETCH(QString, sorter); + QFETCH(QString, primarySorter); + QFETCH(int, primarySortColumn); + QFETCH(Qt::SortOrder, sortOrder); + QFETCH(QList, initialColumnData); + QFETCH(QList, expectedColumnData); + + QQmlEngine engine; + QQmlComponent component(&engine, testFileUrl("sfpmCommon.qml")); + QVERIFY2(component.errorString().isEmpty(), component.errorString().toUtf8()); + QScopedPointer object(component.create()); + QVERIFY(!object.isNull()); + + QStandardItemModel standardModel(initialColumnData.at(0).count(), initialColumnData.count()); + int column = -1; + for (const auto &columnData : initialColumnData) { + int row = 0; ++column; + for (const auto &data: columnData) + standardModel.setData(standardModel.index(row++, column, QModelIndex()), data, Qt::DisplayRole); + } + + auto *sorter1 = object->property(sorter.toLatin1()).value(); // String Sorter + QVERIFY(sorter1); + sorter1->setSortOrder(sortOrder); + sorter1->setColumn(0); + sorter1->setPriority(0); + + auto *primarySorter1 = object->property(primarySorter.toLatin1()).value(); // Role Sorter as the primary sorter + QVERIFY(primarySorter1); + primarySorter1->setSortOrder(sortOrder); + primarySorter1->setColumn(primarySortColumn); + primarySorter1->setPriority(1); + + auto *sfpmModel = object->property("sfpmProxyModel").value(); + QVERIFY(sfpmModel); + QVariant sourceModel = QVariant::fromValue(&standardModel); + sfpmModel->setModel(sourceModel); + auto sorters = sfpmModel->property("sorters").value>(); + sorters.append(&sorters, sorter1); + sorters.append(&sorters, primarySorter1); + + sfpmModel->setPrimarySorter(primarySorter1); + + column = -1; + for (const auto &columnData : expectedColumnData) { + int row = 0; ++column; + for (const auto &data: columnData) + QCOMPARE(sfpmModel->data(sfpmModel->index(row++, column, QModelIndex()), Qt::DisplayRole), data); + } +} + +QTEST_MAIN(tst_QQmlSortFilterProxyModel) + +#include "tst_qqmlsortfilterproxymodel.moc"