From 2ee193654598db5afc51fc770fcc76053a4f33f4 Mon Sep 17 00:00:00 2001 From: Grecko Date: Mon, 30 Apr 2018 01:10:36 +0200 Subject: [PATCH] refactor: move sorters in separate files --- SortFilterProxyModel.pri | 17 +- qqmlsortfilterproxymodel.cpp | 2 +- sorter.cpp | 502 ----------------------------------- sorter.h | 172 ------------ sorters/expressionsorter.cpp | 145 ++++++++++ sorters/expressionsorter.h | 43 +++ sorters/rolesorter.cpp | 68 +++++ sorters/rolesorter.h | 32 +++ sorters/sorter.cpp | 116 ++++++++ sorters/sorter.h | 52 ++++ sorters/sortercontainer.cpp | 69 +++++ sorters/sortercontainer.h | 42 +++ sorters/sortersqmltypes.cpp | 19 ++ sorters/stringsorter.cpp | 116 ++++++++ sorters/stringsorter.h | 47 ++++ tests/indexsorter.h | 2 +- 16 files changed, 764 insertions(+), 680 deletions(-) delete mode 100644 sorter.cpp delete mode 100644 sorter.h create mode 100644 sorters/expressionsorter.cpp create mode 100644 sorters/expressionsorter.h create mode 100644 sorters/rolesorter.cpp create mode 100644 sorters/rolesorter.h create mode 100644 sorters/sorter.cpp create mode 100644 sorters/sorter.h create mode 100644 sorters/sortercontainer.cpp create mode 100644 sorters/sortercontainer.h create mode 100644 sorters/sortersqmltypes.cpp create mode 100644 sorters/stringsorter.cpp create mode 100644 sorters/stringsorter.h diff --git a/SortFilterProxyModel.pri b/SortFilterProxyModel.pri index 45143a3..fdaf7a5 100644 --- a/SortFilterProxyModel.pri +++ b/SortFilterProxyModel.pri @@ -3,7 +3,6 @@ INCLUDEPATH += $$PWD HEADERS += $$PWD/qqmlsortfilterproxymodel.h \ - $$PWD/sorter.h \ $$PWD/proxyrole.h \ $$PWD/filters/filter.h \ $$PWD/filters/filtercontainer.h \ @@ -15,10 +14,14 @@ HEADERS += $$PWD/qqmlsortfilterproxymodel.h \ $$PWD/filters/expressionfilter.h \ $$PWD/filters/filtercontainerfilter.h \ $$PWD/filters/anyoffilter.h \ - $$PWD/filters/alloffilter.h + $$PWD/filters/alloffilter.h \ + $$PWD/sorters/sorter.h \ + $$PWD/sorters/sortercontainer.h \ + $$PWD/sorters/rolesorter.h \ + $$PWD/sorters/stringsorter.h \ + $$PWD/sorters/expressionsorter.h SOURCES += $$PWD/qqmlsortfilterproxymodel.cpp \ - $$PWD/sorter.cpp \ $$PWD/proxyrole.cpp \ $$PWD/filters/filter.cpp \ $$PWD/filters/filtercontainer.cpp \ @@ -31,4 +34,10 @@ SOURCES += $$PWD/qqmlsortfilterproxymodel.cpp \ $$PWD/filters/filtercontainerfilter.cpp \ $$PWD/filters/anyoffilter.cpp \ $$PWD/filters/alloffilter.cpp \ - $$PWD/filters/filtersqmltypes.cpp + $$PWD/filters/filtersqmltypes.cpp \ + $$PWD/sorters/sorter.cpp \ + $$PWD/sorters/sortercontainer.cpp \ + $$PWD/sorters/rolesorter.cpp \ + $$PWD/sorters/stringsorter.cpp \ + $$PWD/sorters/expressionsorter.cpp \ + $$PWD/sorters/sortersqmltypes.cpp diff --git a/qqmlsortfilterproxymodel.cpp b/qqmlsortfilterproxymodel.cpp index 31a268b..1547caf 100644 --- a/qqmlsortfilterproxymodel.cpp +++ b/qqmlsortfilterproxymodel.cpp @@ -2,7 +2,7 @@ #include #include #include "filters/filter.h" -#include "sorter.h" +#include "sorters/sorter.h" #include "proxyrole.h" namespace qqsfpm { diff --git a/sorter.cpp b/sorter.cpp deleted file mode 100644 index 0d9ac9b..0000000 --- a/sorter.cpp +++ /dev/null @@ -1,502 +0,0 @@ -#include "sorter.h" -#include "qqmlsortfilterproxymodel.h" -#include - -namespace qqsfpm { - - -SorterContainer::~SorterContainer() -{ - -} - -QList SorterContainer::sorters() const -{ - return m_sorters; -} - -void SorterContainer::appendSorter(Sorter* sorter) -{ - m_sorters.append(sorter); - onSorterAppended(sorter); -} - -void SorterContainer::removeSorter(Sorter *sorter) -{ - m_sorters.removeOne(sorter); - onSorterRemoved(sorter); -} - -void SorterContainer::clearSorters() -{ - m_sorters.clear(); - onSortersCleared(); -} - -QQmlListProperty SorterContainer::sortersListProperty() -{ - return QQmlListProperty(reinterpret_cast(this), &m_sorters, - &SorterContainer::append_sorter, - &SorterContainer::count_sorter, - &SorterContainer::at_sorter, - &SorterContainer::clear_sorters); -} - -void SorterContainer::append_sorter(QQmlListProperty* list, Sorter* sorter) -{ - if (!sorter) - return; - - SorterContainer* that = reinterpret_cast(list->object); - that->appendSorter(sorter); -} - -int SorterContainer::count_sorter(QQmlListProperty* list) -{ - QList* sorters = static_cast*>(list->data); - return sorters->count(); -} - -Sorter* SorterContainer::at_sorter(QQmlListProperty* list, int index) -{ - QList* sorters = static_cast*>(list->data); - return sorters->at(index); -} - -void SorterContainer::clear_sorters(QQmlListProperty *list) -{ - SorterContainer* that = reinterpret_cast(list->object); - that->clearSorters(); -} - -/*! - \qmltype Sorter - \inqmlmodule SortFilterProxyModel - \brief Base type for the \l SortFilterProxyModel sorters - - The Sorter type cannot be used directly in a QML file. - It exists to provide a set of common properties and methods, - available across all the other sorters types that inherit from it. - Attempting to use the Sorter type directly will result in an error. -*/ - -Sorter::Sorter(QObject *parent) : QObject(parent) -{ -} - -Sorter::~Sorter() = default; - -/*! - \qmlproperty bool Sorter::enabled - - This property holds whether the sorter is enabled. - A disabled sorter will not change the order of the rows. - - By default, sorters are enabled. -*/ -bool Sorter::enabled() const -{ - return m_enabled; -} - -void Sorter::setEnabled(bool enabled) -{ - if (m_enabled == enabled) - return; - - m_enabled = enabled; - Q_EMIT enabledChanged(); - Q_EMIT invalidated(); -} - -bool Sorter::ascendingOrder() const -{ - return sortOrder() == Qt::AscendingOrder; -} - -void Sorter::setAscendingOrder(bool ascendingOrder) -{ - setSortOrder(ascendingOrder ? Qt::AscendingOrder : Qt::DescendingOrder); -} - - -/*! - \qmlproperty Qt::SortOrder Sorter::sortOrder - - This property holds the sort order of this sorter. - - \value Qt.AscendingOrder The items are sorted ascending e.g. starts with 'AAA' ends with 'ZZZ' in Latin-1 locales - \value Qt.DescendingOrder The items are sorted descending e.g. starts with 'ZZZ' ends with 'AAA' in Latin-1 locales - - By default, sorting is in ascending order. -*/ -Qt::SortOrder Sorter::sortOrder() const -{ - return m_sortOrder; -} - -void Sorter::setSortOrder(Qt::SortOrder sortOrder) -{ - if (m_sortOrder == sortOrder) - return; - - m_sortOrder = sortOrder; - Q_EMIT sortOrderChanged(); - invalidate(); -} - -int Sorter::compareRows(const QModelIndex &source_left, const QModelIndex &source_right, const QQmlSortFilterProxyModel& proxyModel) const -{ - int comparison = compare(source_left, source_right, proxyModel); - return (m_sortOrder == Qt::AscendingOrder) ? comparison : -comparison; -} - -int Sorter::compare(const QModelIndex &sourceLeft, const QModelIndex &sourceRight, const QQmlSortFilterProxyModel& proxyModel) const -{ - if (lessThan(sourceLeft, sourceRight, proxyModel)) - return -1; - if (lessThan(sourceRight, sourceLeft, proxyModel)) - return 1; - return 0; -} - -void Sorter::proxyModelCompleted(const QQmlSortFilterProxyModel& proxyModel) -{ - Q_UNUSED(proxyModel) -} - -bool Sorter::lessThan(const QModelIndex &sourceLeft, const QModelIndex &sourceRight, const QQmlSortFilterProxyModel& proxyModel) const -{ - Q_UNUSED(sourceLeft) - Q_UNUSED(sourceRight) - Q_UNUSED(proxyModel) - return false; -} - -void Sorter::invalidate() -{ - if (m_enabled) - Q_EMIT invalidated(); -} - -const QString& RoleSorter::roleName() const -{ - return m_roleName; -} - -/*! - \qmltype RoleSorter - \inherits Sorter - \inqmlmodule SortFilterProxyModel - \brief Sorts rows based on a source model role - - A RoleSorter is a simple \l Sorter that sorts rows based on a source model role. - - In the following example, rows with be sorted by their \c lastName role : - \code - SortFilterProxyModel { - sourceModel: contactModel - sorters: RoleSorter { roleName: "lastName" } - } - \endcode -*/ - -/*! - \qmlproperty string RoleSorter::roleName - - This property holds the role name that the sorter is using to query the source model's data when sorting items. -*/ -void RoleSorter::setRoleName(const QString& roleName) -{ - if (m_roleName == roleName) - return; - - m_roleName = roleName; - Q_EMIT roleNameChanged(); - invalidate(); -} - -QPair RoleSorter::sourceData(const QModelIndex &sourceLeft, const QModelIndex& sourceRight, const QQmlSortFilterProxyModel& proxyModel) const -{ - QPair pair; - int role = proxyModel.roleForName(m_roleName); - - if (role == -1) - return pair; - - pair.first = proxyModel.sourceData(sourceLeft, role); - pair.second = proxyModel.sourceData(sourceRight, role); - return pair; -} - -int RoleSorter::compare(const QModelIndex &sourceLeft, const QModelIndex& sourceRight, const QQmlSortFilterProxyModel& proxyModel) const -{ - QPair pair = sourceData(sourceLeft, sourceRight, proxyModel); - QVariant leftValue = pair.first; - QVariant rightValue = pair.second; - if (leftValue < rightValue) - return -1; - if (leftValue > rightValue) - return 1; - return 0; -} - -/*! - \qmltype StringSorter - \inherits RoleSorter - \inqmlmodule SortFilterProxyModel - \brief Sorts rows based on a source model string role - - \l StringSorter is a specialized \l RoleSorter that sorts rows based on a source model string role. - \l StringSorter compares strings according to a localized collation algorithm. - - In the following example, rows with be sorted by their \c lastName role : - \code - SortFilterProxyModel { - sourceModel: contactModel - sorters: StringSorter { roleName: "lastName" } - } - \endcode -*/ - -/*! - \qmlproperty Qt.CaseSensitivity StringSorter::caseSensitivity - - This property holds the case sensitivity of the sorter. -*/ -Qt::CaseSensitivity StringSorter::caseSensitivity() const -{ - return m_collator.caseSensitivity(); -} - -void StringSorter::setCaseSensitivity(Qt::CaseSensitivity caseSensitivity) -{ - if (m_collator.caseSensitivity() == caseSensitivity) - return; - - m_collator.setCaseSensitivity(caseSensitivity); - Q_EMIT caseSensitivityChanged(); - invalidate(); -} - -/*! - \qmlproperty bool StringSorter::ignorePunctation - - This property holds whether the sorter ignores punctation. - if \c ignorePunctuation is \c true, punctuation characters and symbols are ignored when determining sort order. - - \note This property is not currently supported on Apple platforms or if Qt is configured to not use ICU on Linux. -*/ -bool StringSorter::ignorePunctation() const -{ - return m_collator.ignorePunctuation(); -} - -void StringSorter::setIgnorePunctation(bool ignorePunctation) -{ - if (m_collator.ignorePunctuation() == ignorePunctation) - return; - - m_collator.setIgnorePunctuation(ignorePunctation); - Q_EMIT ignorePunctationChanged(); - invalidate(); -} - -/*! - \qmlproperty Locale StringSorter::locale - - This property holds the locale of the sorter. -*/ -QLocale StringSorter::locale() const -{ - return m_collator.locale(); -} - -void StringSorter::setLocale(const QLocale &locale) -{ - if (m_collator.locale() == locale) - return; - - m_collator.setLocale(locale); - Q_EMIT localeChanged(); - invalidate(); -} - -/*! - \qmlproperty bool StringSorter::numericMode - - This property holds whether the numeric mode of the sorter is enabled. - This will enable proper sorting of numeric digits, so that e.g. 100 sorts after 99. - By default this mode is off. -*/ -bool StringSorter::numericMode() const -{ - return m_collator.numericMode(); -} - -void StringSorter::setNumericMode(bool numericMode) -{ - if (m_collator.numericMode() == numericMode) - return; - - m_collator.setNumericMode(numericMode); - Q_EMIT numericModeChanged(); - invalidate(); -} - -int StringSorter::compare(const QModelIndex &sourceLeft, const QModelIndex &sourceRight, const QQmlSortFilterProxyModel& proxyModel) const -{ - QPair pair = sourceData(sourceLeft, sourceRight, proxyModel); - QString leftValue = pair.first.toString(); - QString rightValue = pair.second.toString(); - return m_collator.compare(leftValue, rightValue); -} - -/*! - \qmltype ExpressionSorter - \inherits Sorter - \inqmlmodule SortFilterProxyModel - \brief Sorts row with a custom sorting - - An ExpressionSorter is a \l Sorter allowing to implement custom sorting based on a javascript expression. -*/ - -/*! - \qmlproperty expression ExpressionSorter::expression - - An expression to implement custom sorting. It must evaluate to a bool. - It has the same syntax has a \l {http://doc.qt.io/qt-5/qtqml-syntax-propertybinding.html} {Property Binding}, except that it will be evaluated for each of the source model's rows. - Model data is accessible for both rows with the \c modelLeft, and \c modelRight properties: - - \code - sorters: ExpressionSorter { - expression: { - return modelLeft.someRole < modelRight.someRole; - } - } - \endcode - - The \c index of the row is also available through \c modelLeft and \c modelRight. - - The expression should return \c true if the value of the left item is less than the value of the right item, otherwise returns false. - - This expression is reevaluated for a row every time its model data changes. - When an external property (not \c index* or in \c model*) the expression depends on changes, the expression is reevaluated for every row of the source model. - To capture the properties the expression depends on, the expression is first executed with invalid data and each property access is detected by the QML engine. - This means that if a property is not accessed because of a conditional, it won't be captured and the expression won't be reevaluted when this property changes. - - A workaround to this problem is to access all the properties the expressions depends unconditionally at the beggining of the expression. -*/ -const QQmlScriptString& ExpressionSorter::expression() const -{ - return m_scriptString; -} - -void ExpressionSorter::setExpression(const QQmlScriptString& scriptString) -{ - if (m_scriptString == scriptString) - return; - - m_scriptString = scriptString; - updateExpression(); - - Q_EMIT expressionChanged(); - invalidate(); -} - -void ExpressionSorter::proxyModelCompleted(const QQmlSortFilterProxyModel& proxyModel) -{ - updateContext(proxyModel); -} - -bool evaluateBoolExpression(QQmlExpression& expression) -{ - QVariant variantResult = expression.evaluate(); - if (expression.hasError()) { - qWarning() << expression.error(); - return false; - } - if (variantResult.canConvert()) { - return variantResult.toBool(); - } else { - qWarning("%s:%i:%i : Can't convert result to bool", - expression.sourceFile().toUtf8().data(), - expression.lineNumber(), - expression.columnNumber()); - return false; - } -} - -int ExpressionSorter::compare(const QModelIndex& sourceLeft, const QModelIndex& sourceRight, const QQmlSortFilterProxyModel& proxyModel) const -{ - if (!m_scriptString.isEmpty()) { - QVariantMap modelLeftMap, modelRightMap; - QHash roles = proxyModel.roleNames(); - - QQmlContext context(qmlContext(this)); - - for (auto it = roles.cbegin(); it != roles.cend(); ++it) { - modelLeftMap.insert(it.value(), proxyModel.sourceData(sourceLeft, it.key())); - modelRightMap.insert(it.value(), proxyModel.sourceData(sourceRight, it.key())); - } - modelLeftMap.insert("index", sourceLeft.row()); - modelRightMap.insert("index", sourceRight.row()); - - QQmlExpression expression(m_scriptString, &context); - - context.setContextProperty("modelLeft", modelLeftMap); - context.setContextProperty("modelRight", modelRightMap); - if (evaluateBoolExpression(expression)) - return -1; - - context.setContextProperty("modelLeft", modelRightMap); - context.setContextProperty("modelRight", modelLeftMap); - if (evaluateBoolExpression(expression)) - return 1; - } - return 0; -} - -void ExpressionSorter::updateContext(const QQmlSortFilterProxyModel& proxyModel) -{ - delete m_context; - m_context = new QQmlContext(qmlContext(this), this); - - QVariantMap modelLeftMap, modelRightMap; - // what about roles changes ? - - for (const QByteArray& roleName : proxyModel.roleNames().values()) { - modelLeftMap.insert(roleName, QVariant()); - modelRightMap.insert(roleName, QVariant()); - } - modelLeftMap.insert("index", -1); - modelRightMap.insert("index", -1); - - m_context->setContextProperty("modelLeft", modelLeftMap); - m_context->setContextProperty("modelRight", modelRightMap); - - updateExpression(); -} - -void ExpressionSorter::updateExpression() -{ - if (!m_context) - return; - - delete m_expression; - m_expression = new QQmlExpression(m_scriptString, m_context, 0, this); - connect(m_expression, &QQmlExpression::valueChanged, this, &ExpressionSorter::invalidate); - m_expression->setNotifyOnValueChanged(true); - m_expression->evaluate(); -} - -void registerSorterTypes() { - qmlRegisterUncreatableType("SortFilterProxyModel", 0, 2, "Sorter", "Sorter is an abstract class"); - qmlRegisterType("SortFilterProxyModel", 0, 2, "RoleSorter"); - qmlRegisterType("SortFilterProxyModel", 0, 2, "StringSorter"); - qmlRegisterType("SortFilterProxyModel", 0, 2, "ExpressionSorter"); -} - -Q_COREAPP_STARTUP_FUNCTION(registerSorterTypes) - -} diff --git a/sorter.h b/sorter.h deleted file mode 100644 index 8fd0714..0000000 --- a/sorter.h +++ /dev/null @@ -1,172 +0,0 @@ -#ifndef SORTER_H -#define SORTER_H - -#include -#include -#include -#include - -namespace qqsfpm { - -class Sorter; -class QQmlSortFilterProxyModel; - -class SorterContainer { -public: - virtual ~SorterContainer(); - - QList sorters() const; - void appendSorter(Sorter* sorter); - void removeSorter(Sorter* sorter); - void clearSorters(); - - QQmlListProperty sortersListProperty(); - -protected: - QList m_sorters; - -private: - virtual void onSorterAppended(Sorter* sorter) = 0; - virtual void onSorterRemoved(Sorter* sorter) = 0; - virtual void onSortersCleared() = 0; - - static void append_sorter(QQmlListProperty* list, Sorter* sorter); - static int count_sorter(QQmlListProperty* list); - static Sorter* at_sorter(QQmlListProperty* list, int index); - static void clear_sorters(QQmlListProperty* list); -}; -} -#define SorterContainer_iid "fr.grecko.SortFilterProxyModel.SorterContainer" -Q_DECLARE_INTERFACE(qqsfpm::SorterContainer, SorterContainer_iid) - -namespace qqsfpm { - -class Sorter : public QObject -{ - Q_OBJECT - Q_PROPERTY(bool enabled READ enabled WRITE setEnabled NOTIFY enabledChanged) - Q_PROPERTY(bool ascendingOrder READ ascendingOrder WRITE setAscendingOrder NOTIFY sortOrderChanged) - Q_PROPERTY(Qt::SortOrder sortOrder READ sortOrder WRITE setSortOrder NOTIFY sortOrderChanged) - -public: - Sorter(QObject* parent = nullptr); - virtual ~Sorter() = 0; - - bool enabled() const; - void setEnabled(bool enabled); - - bool ascendingOrder() const; - void setAscendingOrder(bool ascendingOrder); - - Qt::SortOrder sortOrder() const; - void setSortOrder(Qt::SortOrder sortOrder); - - int compareRows(const QModelIndex& source_left, const QModelIndex& source_right, const QQmlSortFilterProxyModel& proxyModel) const; - - virtual void proxyModelCompleted(const QQmlSortFilterProxyModel& proxyModel); - -Q_SIGNALS: - void enabledChanged(); - void sortOrderChanged(); - - void invalidated(); - -protected: - virtual int compare(const QModelIndex& sourceLeft, const QModelIndex& sourceRight, const QQmlSortFilterProxyModel& proxyModel) const; - virtual bool lessThan(const QModelIndex& sourceLeft, const QModelIndex& sourceRight, const QQmlSortFilterProxyModel& proxyModel) const; - void invalidate(); - -private: - bool m_enabled = true; - Qt::SortOrder m_sortOrder = Qt::AscendingOrder; -}; - -class RoleSorter : public Sorter -{ - Q_OBJECT - Q_PROPERTY(QString roleName READ roleName WRITE setRoleName NOTIFY roleNameChanged) - -public: - using Sorter::Sorter; - - const QString& roleName() const; - void setRoleName(const QString& roleName); - -Q_SIGNALS: - void roleNameChanged(); - -protected: - QPair sourceData(const QModelIndex &sourceLeft, const QModelIndex& sourceRight, const QQmlSortFilterProxyModel& proxyModel) const; - int compare(const QModelIndex& sourceLeft, const QModelIndex& sourceRight, const QQmlSortFilterProxyModel& proxyModel) const override; - -private: - QString m_roleName; -}; - -class StringSorter : public RoleSorter -{ - Q_OBJECT - Q_PROPERTY(Qt::CaseSensitivity caseSensitivity READ caseSensitivity WRITE setCaseSensitivity NOTIFY caseSensitivityChanged) - Q_PROPERTY(bool ignorePunctation READ ignorePunctation WRITE setIgnorePunctation NOTIFY ignorePunctationChanged) - Q_PROPERTY(QLocale locale READ locale WRITE setLocale NOTIFY localeChanged) - Q_PROPERTY(bool numericMode READ numericMode WRITE setNumericMode NOTIFY numericModeChanged) - -public: - using RoleSorter::RoleSorter; - - Qt::CaseSensitivity caseSensitivity() const; - void setCaseSensitivity(Qt::CaseSensitivity caseSensitivity); - - bool ignorePunctation() const; - void setIgnorePunctation(bool ignorePunctation); - - QLocale locale() const; - void setLocale(const QLocale& locale); - - bool numericMode() const; - void setNumericMode(bool numericMode); - -Q_SIGNALS: - void caseSensitivityChanged(); - void ignorePunctationChanged(); - void localeChanged(); - void numericModeChanged(); - -protected: - int compare(const QModelIndex& sourceLeft, const QModelIndex& sourceRight, const QQmlSortFilterProxyModel& proxyModel) const override; - -private: - QCollator m_collator; -}; - -class ExpressionSorter : public Sorter -{ - Q_OBJECT - Q_PROPERTY(QQmlScriptString expression READ expression WRITE setExpression NOTIFY expressionChanged) - -public: - using Sorter::Sorter; - - const QQmlScriptString& expression() const; - void setExpression(const QQmlScriptString& scriptString); - - void proxyModelCompleted(const QQmlSortFilterProxyModel& proxyModel) override; - -Q_SIGNALS: - void expressionChanged(); - -protected: - int compare(const QModelIndex& sourceLeft, const QModelIndex& sourceRight, const QQmlSortFilterProxyModel& proxyModel) const override; - -private: - void updateContext(const QQmlSortFilterProxyModel& proxyModel); - void updateExpression(); - - QQmlScriptString m_scriptString; - QQmlExpression* m_expression = nullptr; - QQmlContext* m_context = nullptr; -}; - -} - -#endif // SORTER_H diff --git a/sorters/expressionsorter.cpp b/sorters/expressionsorter.cpp new file mode 100644 index 0000000..e1fbd0a --- /dev/null +++ b/sorters/expressionsorter.cpp @@ -0,0 +1,145 @@ +#include "expressionsorter.h" +#include "qqmlsortfilterproxymodel.h" +#include + +namespace qqsfpm { + +/*! + \qmltype ExpressionSorter + \inherits Sorter + \inqmlmodule SortFilterProxyModel + \brief Sorts row with a custom sorting + + An ExpressionSorter is a \l Sorter allowing to implement custom sorting based on a javascript expression. +*/ + +/*! + \qmlproperty expression ExpressionSorter::expression + + An expression to implement custom sorting. It must evaluate to a bool. + It has the same syntax has a \l {http://doc.qt.io/qt-5/qtqml-syntax-propertybinding.html} {Property Binding}, except that it will be evaluated for each of the source model's rows. + Model data is accessible for both rows with the \c modelLeft, and \c modelRight properties: + + \code + sorters: ExpressionSorter { + expression: { + return modelLeft.someRole < modelRight.someRole; + } + } + \endcode + + The \c index of the row is also available through \c modelLeft and \c modelRight. + + The expression should return \c true if the value of the left item is less than the value of the right item, otherwise returns false. + + This expression is reevaluated for a row every time its model data changes. + When an external property (not \c index* or in \c model*) the expression depends on changes, the expression is reevaluated for every row of the source model. + To capture the properties the expression depends on, the expression is first executed with invalid data and each property access is detected by the QML engine. + This means that if a property is not accessed because of a conditional, it won't be captured and the expression won't be reevaluted when this property changes. + + A workaround to this problem is to access all the properties the expressions depends unconditionally at the beggining of the expression. +*/ +const QQmlScriptString& ExpressionSorter::expression() const +{ + return m_scriptString; +} + +void ExpressionSorter::setExpression(const QQmlScriptString& scriptString) +{ + if (m_scriptString == scriptString) + return; + + m_scriptString = scriptString; + updateExpression(); + + Q_EMIT expressionChanged(); + invalidate(); +} + +void ExpressionSorter::proxyModelCompleted(const QQmlSortFilterProxyModel& proxyModel) +{ + updateContext(proxyModel); +} + +bool evaluateBoolExpression(QQmlExpression& expression) +{ + QVariant variantResult = expression.evaluate(); + if (expression.hasError()) { + qWarning() << expression.error(); + return false; + } + if (variantResult.canConvert()) { + return variantResult.toBool(); + } else { + qWarning("%s:%i:%i : Can't convert result to bool", + expression.sourceFile().toUtf8().data(), + expression.lineNumber(), + expression.columnNumber()); + return false; + } +} + +int ExpressionSorter::compare(const QModelIndex& sourceLeft, const QModelIndex& sourceRight, const QQmlSortFilterProxyModel& proxyModel) const +{ + if (!m_scriptString.isEmpty()) { + QVariantMap modelLeftMap, modelRightMap; + QHash roles = proxyModel.roleNames(); + + QQmlContext context(qmlContext(this)); + + for (auto it = roles.cbegin(); it != roles.cend(); ++it) { + modelLeftMap.insert(it.value(), proxyModel.sourceData(sourceLeft, it.key())); + modelRightMap.insert(it.value(), proxyModel.sourceData(sourceRight, it.key())); + } + modelLeftMap.insert("index", sourceLeft.row()); + modelRightMap.insert("index", sourceRight.row()); + + QQmlExpression expression(m_scriptString, &context); + + context.setContextProperty("modelLeft", modelLeftMap); + context.setContextProperty("modelRight", modelRightMap); + if (evaluateBoolExpression(expression)) + return -1; + + context.setContextProperty("modelLeft", modelRightMap); + context.setContextProperty("modelRight", modelLeftMap); + if (evaluateBoolExpression(expression)) + return 1; + } + return 0; +} + +void ExpressionSorter::updateContext(const QQmlSortFilterProxyModel& proxyModel) +{ + delete m_context; + m_context = new QQmlContext(qmlContext(this), this); + + QVariantMap modelLeftMap, modelRightMap; + // what about roles changes ? + + for (const QByteArray& roleName : proxyModel.roleNames().values()) { + modelLeftMap.insert(roleName, QVariant()); + modelRightMap.insert(roleName, QVariant()); + } + modelLeftMap.insert("index", -1); + modelRightMap.insert("index", -1); + + m_context->setContextProperty("modelLeft", modelLeftMap); + m_context->setContextProperty("modelRight", modelRightMap); + + updateExpression(); +} + +void ExpressionSorter::updateExpression() +{ + if (!m_context) + return; + + delete m_expression; + m_expression = new QQmlExpression(m_scriptString, m_context, 0, this); + connect(m_expression, &QQmlExpression::valueChanged, this, &ExpressionSorter::invalidate); + m_expression->setNotifyOnValueChanged(true); + m_expression->evaluate(); +} + +} diff --git a/sorters/expressionsorter.h b/sorters/expressionsorter.h new file mode 100644 index 0000000..6ca17fd --- /dev/null +++ b/sorters/expressionsorter.h @@ -0,0 +1,43 @@ +#ifndef EXPRESSIONSORTER_H +#define EXPRESSIONSORTER_H + +#include "sorter.h" +#include + +class QQmlExpression; + +namespace qqsfpm { + +class QQmlSortFilterProxyModel; + +class ExpressionSorter : public Sorter +{ + Q_OBJECT + Q_PROPERTY(QQmlScriptString expression READ expression WRITE setExpression NOTIFY expressionChanged) + +public: + using Sorter::Sorter; + + const QQmlScriptString& expression() const; + void setExpression(const QQmlScriptString& scriptString); + + void proxyModelCompleted(const QQmlSortFilterProxyModel& proxyModel) override; + +Q_SIGNALS: + void expressionChanged(); + +protected: + int compare(const QModelIndex& sourceLeft, const QModelIndex& sourceRight, const QQmlSortFilterProxyModel& proxyModel) const override; + +private: + void updateContext(const QQmlSortFilterProxyModel& proxyModel); + void updateExpression(); + + QQmlScriptString m_scriptString; + QQmlExpression* m_expression = nullptr; + QQmlContext* m_context = nullptr; +}; + +} + +#endif // EXPRESSIONSORTER_H diff --git a/sorters/rolesorter.cpp b/sorters/rolesorter.cpp new file mode 100644 index 0000000..f4932eb --- /dev/null +++ b/sorters/rolesorter.cpp @@ -0,0 +1,68 @@ +#include "rolesorter.h" +#include "qqmlsortfilterproxymodel.h" + +namespace qqsfpm { + +/*! + \qmltype RoleSorter + \inherits Sorter + \inqmlmodule SortFilterProxyModel + \brief Sorts rows based on a source model role + + A RoleSorter is a simple \l Sorter that sorts rows based on a source model role. + + In the following example, rows with be sorted by their \c lastName role : + \code + SortFilterProxyModel { + sourceModel: contactModel + sorters: RoleSorter { roleName: "lastName" } + } + \endcode +*/ + +/*! + \qmlproperty string RoleSorter::roleName + + This property holds the role name that the sorter is using to query the source model's data when sorting items. +*/ +const QString& RoleSorter::roleName() const +{ + return m_roleName; +} + +void RoleSorter::setRoleName(const QString& roleName) +{ + if (m_roleName == roleName) + return; + + m_roleName = roleName; + Q_EMIT roleNameChanged(); + invalidate(); +} + +QPair RoleSorter::sourceData(const QModelIndex &sourceLeft, const QModelIndex& sourceRight, const QQmlSortFilterProxyModel& proxyModel) const +{ + QPair pair; + int role = proxyModel.roleForName(m_roleName); + + if (role == -1) + return pair; + + pair.first = proxyModel.sourceData(sourceLeft, role); + pair.second = proxyModel.sourceData(sourceRight, role); + return pair; +} + +int RoleSorter::compare(const QModelIndex &sourceLeft, const QModelIndex& sourceRight, const QQmlSortFilterProxyModel& proxyModel) const +{ + QPair pair = sourceData(sourceLeft, sourceRight, proxyModel); + QVariant leftValue = pair.first; + QVariant rightValue = pair.second; + if (leftValue < rightValue) + return -1; + if (leftValue > rightValue) + return 1; + return 0; +} + +} diff --git a/sorters/rolesorter.h b/sorters/rolesorter.h new file mode 100644 index 0000000..9cc11bd --- /dev/null +++ b/sorters/rolesorter.h @@ -0,0 +1,32 @@ +#ifndef ROLESORTER_H +#define ROLESORTER_H + +#include "sorter.h" + +namespace qqsfpm { + +class RoleSorter : public Sorter +{ + Q_OBJECT + Q_PROPERTY(QString roleName READ roleName WRITE setRoleName NOTIFY roleNameChanged) + +public: + using Sorter::Sorter; + + const QString& roleName() const; + void setRoleName(const QString& roleName); + +Q_SIGNALS: + void roleNameChanged(); + +protected: + QPair sourceData(const QModelIndex &sourceLeft, const QModelIndex& sourceRight, const QQmlSortFilterProxyModel& proxyModel) const; + int compare(const QModelIndex& sourceLeft, const QModelIndex& sourceRight, const QQmlSortFilterProxyModel& proxyModel) const override; + +private: + QString m_roleName; +}; + +} + +#endif // ROLESORTER_H diff --git a/sorters/sorter.cpp b/sorters/sorter.cpp new file mode 100644 index 0000000..6876573 --- /dev/null +++ b/sorters/sorter.cpp @@ -0,0 +1,116 @@ +#include "sorter.h" +#include "qqmlsortfilterproxymodel.h" + +namespace qqsfpm { + +/*! + \qmltype Sorter + \inqmlmodule SortFilterProxyModel + \brief Base type for the \l SortFilterProxyModel sorters + + The Sorter type cannot be used directly in a QML file. + It exists to provide a set of common properties and methods, + available across all the other sorters types that inherit from it. + Attempting to use the Sorter type directly will result in an error. +*/ + +Sorter::Sorter(QObject *parent) : QObject(parent) +{ +} + +Sorter::~Sorter() = default; + +/*! + \qmlproperty bool Sorter::enabled + + This property holds whether the sorter is enabled. + A disabled sorter will not change the order of the rows. + + By default, sorters are enabled. +*/ +bool Sorter::enabled() const +{ + return m_enabled; +} + +void Sorter::setEnabled(bool enabled) +{ + if (m_enabled == enabled) + return; + + m_enabled = enabled; + Q_EMIT enabledChanged(); + Q_EMIT invalidated(); +} + +bool Sorter::ascendingOrder() const +{ + return sortOrder() == Qt::AscendingOrder; +} + +void Sorter::setAscendingOrder(bool ascendingOrder) +{ + setSortOrder(ascendingOrder ? Qt::AscendingOrder : Qt::DescendingOrder); +} + + +/*! + \qmlproperty Qt::SortOrder Sorter::sortOrder + + This property holds the sort order of this sorter. + + \value Qt.AscendingOrder The items are sorted ascending e.g. starts with 'AAA' ends with 'ZZZ' in Latin-1 locales + \value Qt.DescendingOrder The items are sorted descending e.g. starts with 'ZZZ' ends with 'AAA' in Latin-1 locales + + By default, sorting is in ascending order. +*/ +Qt::SortOrder Sorter::sortOrder() const +{ + return m_sortOrder; +} + +void Sorter::setSortOrder(Qt::SortOrder sortOrder) +{ + if (m_sortOrder == sortOrder) + return; + + m_sortOrder = sortOrder; + Q_EMIT sortOrderChanged(); + invalidate(); +} + +int Sorter::compareRows(const QModelIndex &source_left, const QModelIndex &source_right, const QQmlSortFilterProxyModel& proxyModel) const +{ + int comparison = compare(source_left, source_right, proxyModel); + return (m_sortOrder == Qt::AscendingOrder) ? comparison : -comparison; +} + +int Sorter::compare(const QModelIndex &sourceLeft, const QModelIndex &sourceRight, const QQmlSortFilterProxyModel& proxyModel) const +{ + if (lessThan(sourceLeft, sourceRight, proxyModel)) + return -1; + if (lessThan(sourceRight, sourceLeft, proxyModel)) + return 1; + return 0; +} + +void Sorter::proxyModelCompleted(const QQmlSortFilterProxyModel& proxyModel) +{ + Q_UNUSED(proxyModel) +} + +bool Sorter::lessThan(const QModelIndex &sourceLeft, const QModelIndex &sourceRight, const QQmlSortFilterProxyModel& proxyModel) const +{ + Q_UNUSED(sourceLeft) + Q_UNUSED(sourceRight) + Q_UNUSED(proxyModel) + return false; +} + +void Sorter::invalidate() +{ + if (m_enabled) + Q_EMIT invalidated(); +} + +} diff --git a/sorters/sorter.h b/sorters/sorter.h new file mode 100644 index 0000000..27549f0 --- /dev/null +++ b/sorters/sorter.h @@ -0,0 +1,52 @@ +#ifndef SORTER_H +#define SORTER_H + +#include + +namespace qqsfpm { + +class QQmlSortFilterProxyModel; + +class Sorter : public QObject +{ + Q_OBJECT + Q_PROPERTY(bool enabled READ enabled WRITE setEnabled NOTIFY enabledChanged) + Q_PROPERTY(bool ascendingOrder READ ascendingOrder WRITE setAscendingOrder NOTIFY sortOrderChanged) + Q_PROPERTY(Qt::SortOrder sortOrder READ sortOrder WRITE setSortOrder NOTIFY sortOrderChanged) + +public: + Sorter(QObject* parent = nullptr); + virtual ~Sorter() = 0; + + bool enabled() const; + void setEnabled(bool enabled); + + bool ascendingOrder() const; + void setAscendingOrder(bool ascendingOrder); + + Qt::SortOrder sortOrder() const; + void setSortOrder(Qt::SortOrder sortOrder); + + int compareRows(const QModelIndex& source_left, const QModelIndex& source_right, const QQmlSortFilterProxyModel& proxyModel) const; + + virtual void proxyModelCompleted(const QQmlSortFilterProxyModel& proxyModel); + +Q_SIGNALS: + void enabledChanged(); + void sortOrderChanged(); + + void invalidated(); + +protected: + virtual int compare(const QModelIndex& sourceLeft, const QModelIndex& sourceRight, const QQmlSortFilterProxyModel& proxyModel) const; + virtual bool lessThan(const QModelIndex& sourceLeft, const QModelIndex& sourceRight, const QQmlSortFilterProxyModel& proxyModel) const; + void invalidate(); + +private: + bool m_enabled = true; + Qt::SortOrder m_sortOrder = Qt::AscendingOrder; +}; + +} + +#endif // SORTER_H diff --git a/sorters/sortercontainer.cpp b/sorters/sortercontainer.cpp new file mode 100644 index 0000000..d7b6740 --- /dev/null +++ b/sorters/sortercontainer.cpp @@ -0,0 +1,69 @@ +#include "sortercontainer.h" + +namespace qqsfpm { + +SorterContainer::~SorterContainer() +{ + +} + +QList SorterContainer::sorters() const +{ + return m_sorters; +} + +void SorterContainer::appendSorter(Sorter* sorter) +{ + m_sorters.append(sorter); + onSorterAppended(sorter); +} + +void SorterContainer::removeSorter(Sorter *sorter) +{ + m_sorters.removeOne(sorter); + onSorterRemoved(sorter); +} + +void SorterContainer::clearSorters() +{ + m_sorters.clear(); + onSortersCleared(); +} + +QQmlListProperty SorterContainer::sortersListProperty() +{ + return QQmlListProperty(reinterpret_cast(this), &m_sorters, + &SorterContainer::append_sorter, + &SorterContainer::count_sorter, + &SorterContainer::at_sorter, + &SorterContainer::clear_sorters); +} + +void SorterContainer::append_sorter(QQmlListProperty* list, Sorter* sorter) +{ + if (!sorter) + return; + + SorterContainer* that = reinterpret_cast(list->object); + that->appendSorter(sorter); +} + +int SorterContainer::count_sorter(QQmlListProperty* list) +{ + QList* sorters = static_cast*>(list->data); + return sorters->count(); +} + +Sorter* SorterContainer::at_sorter(QQmlListProperty* list, int index) +{ + QList* sorters = static_cast*>(list->data); + return sorters->at(index); +} + +void SorterContainer::clear_sorters(QQmlListProperty *list) +{ + SorterContainer* that = reinterpret_cast(list->object); + that->clearSorters(); +} + +} diff --git a/sorters/sortercontainer.h b/sorters/sortercontainer.h new file mode 100644 index 0000000..5a09c09 --- /dev/null +++ b/sorters/sortercontainer.h @@ -0,0 +1,42 @@ +#ifndef SORTERSSORTERCONTAINER_H +#define SORTERSSORTERCONTAINER_H + +#include +#include + +namespace qqsfpm { + +class Sorter; +class QQmlSortFilterProxyModel; + +class SorterContainer { +public: + virtual ~SorterContainer(); + + QList sorters() const; + void appendSorter(Sorter* sorter); + void removeSorter(Sorter* sorter); + void clearSorters(); + + QQmlListProperty sortersListProperty(); + +protected: + QList m_sorters; + +private: + virtual void onSorterAppended(Sorter* sorter) = 0; + virtual void onSorterRemoved(Sorter* sorter) = 0; + virtual void onSortersCleared() = 0; + + static void append_sorter(QQmlListProperty* list, Sorter* sorter); + static int count_sorter(QQmlListProperty* list); + static Sorter* at_sorter(QQmlListProperty* list, int index); + static void clear_sorters(QQmlListProperty* list); +}; + +} + +#define SorterContainer_iid "fr.grecko.SortFilterProxyModel.SorterContainer" +Q_DECLARE_INTERFACE(qqsfpm::SorterContainer, SorterContainer_iid) + +#endif // SORTERSSORTERCONTAINER_H diff --git a/sorters/sortersqmltypes.cpp b/sorters/sortersqmltypes.cpp new file mode 100644 index 0000000..ada7850 --- /dev/null +++ b/sorters/sortersqmltypes.cpp @@ -0,0 +1,19 @@ +#include "sorter.h" +#include "rolesorter.h" +#include "stringsorter.h" +#include "expressionsorter.h" +#include +#include + +namespace qqsfpm { + +void registerSorterTypes() { + qmlRegisterUncreatableType("SortFilterProxyModel", 0, 2, "Sorter", "Sorter is an abstract class"); + qmlRegisterType("SortFilterProxyModel", 0, 2, "RoleSorter"); + qmlRegisterType("SortFilterProxyModel", 0, 2, "StringSorter"); + qmlRegisterType("SortFilterProxyModel", 0, 2, "ExpressionSorter"); +} + +Q_COREAPP_STARTUP_FUNCTION(registerSorterTypes) + +} diff --git a/sorters/stringsorter.cpp b/sorters/stringsorter.cpp new file mode 100644 index 0000000..451d226 --- /dev/null +++ b/sorters/stringsorter.cpp @@ -0,0 +1,116 @@ +#include "stringsorter.h" + +namespace qqsfpm { + +/*! + \qmltype StringSorter + \inherits RoleSorter + \inqmlmodule SortFilterProxyModel + \brief Sorts rows based on a source model string role + + \l StringSorter is a specialized \l RoleSorter that sorts rows based on a source model string role. + \l StringSorter compares strings according to a localized collation algorithm. + + In the following example, rows with be sorted by their \c lastName role : + \code + SortFilterProxyModel { + sourceModel: contactModel + sorters: StringSorter { roleName: "lastName" } + } + \endcode +*/ + +/*! + \qmlproperty Qt.CaseSensitivity StringSorter::caseSensitivity + + This property holds the case sensitivity of the sorter. +*/ +Qt::CaseSensitivity StringSorter::caseSensitivity() const +{ + return m_collator.caseSensitivity(); +} + +void StringSorter::setCaseSensitivity(Qt::CaseSensitivity caseSensitivity) +{ + if (m_collator.caseSensitivity() == caseSensitivity) + return; + + m_collator.setCaseSensitivity(caseSensitivity); + Q_EMIT caseSensitivityChanged(); + invalidate(); +} + +/*! + \qmlproperty bool StringSorter::ignorePunctation + + This property holds whether the sorter ignores punctation. + if \c ignorePunctuation is \c true, punctuation characters and symbols are ignored when determining sort order. + + \note This property is not currently supported on Apple platforms or if Qt is configured to not use ICU on Linux. +*/ +bool StringSorter::ignorePunctation() const +{ + return m_collator.ignorePunctuation(); +} + +void StringSorter::setIgnorePunctation(bool ignorePunctation) +{ + if (m_collator.ignorePunctuation() == ignorePunctation) + return; + + m_collator.setIgnorePunctuation(ignorePunctation); + Q_EMIT ignorePunctationChanged(); + invalidate(); +} + +/*! + \qmlproperty Locale StringSorter::locale + + This property holds the locale of the sorter. +*/ +QLocale StringSorter::locale() const +{ + return m_collator.locale(); +} + +void StringSorter::setLocale(const QLocale &locale) +{ + if (m_collator.locale() == locale) + return; + + m_collator.setLocale(locale); + Q_EMIT localeChanged(); + invalidate(); +} + +/*! + \qmlproperty bool StringSorter::numericMode + + This property holds whether the numeric mode of the sorter is enabled. + This will enable proper sorting of numeric digits, so that e.g. 100 sorts after 99. + By default this mode is off. +*/ +bool StringSorter::numericMode() const +{ + return m_collator.numericMode(); +} + +void StringSorter::setNumericMode(bool numericMode) +{ + if (m_collator.numericMode() == numericMode) + return; + + m_collator.setNumericMode(numericMode); + Q_EMIT numericModeChanged(); + invalidate(); +} + +int StringSorter::compare(const QModelIndex &sourceLeft, const QModelIndex &sourceRight, const QQmlSortFilterProxyModel& proxyModel) const +{ + QPair pair = sourceData(sourceLeft, sourceRight, proxyModel); + QString leftValue = pair.first.toString(); + QString rightValue = pair.second.toString(); + return m_collator.compare(leftValue, rightValue); +} + +} diff --git a/sorters/stringsorter.h b/sorters/stringsorter.h new file mode 100644 index 0000000..3368482 --- /dev/null +++ b/sorters/stringsorter.h @@ -0,0 +1,47 @@ +#ifndef STRINGSORTER_H +#define STRINGSORTER_H + +#include "rolesorter.h" +#include + +namespace qqsfpm { + +class StringSorter : public RoleSorter +{ + Q_OBJECT + Q_PROPERTY(Qt::CaseSensitivity caseSensitivity READ caseSensitivity WRITE setCaseSensitivity NOTIFY caseSensitivityChanged) + Q_PROPERTY(bool ignorePunctation READ ignorePunctation WRITE setIgnorePunctation NOTIFY ignorePunctationChanged) + Q_PROPERTY(QLocale locale READ locale WRITE setLocale NOTIFY localeChanged) + Q_PROPERTY(bool numericMode READ numericMode WRITE setNumericMode NOTIFY numericModeChanged) + +public: + using RoleSorter::RoleSorter; + + Qt::CaseSensitivity caseSensitivity() const; + void setCaseSensitivity(Qt::CaseSensitivity caseSensitivity); + + bool ignorePunctation() const; + void setIgnorePunctation(bool ignorePunctation); + + QLocale locale() const; + void setLocale(const QLocale& locale); + + bool numericMode() const; + void setNumericMode(bool numericMode); + +Q_SIGNALS: + void caseSensitivityChanged(); + void ignorePunctationChanged(); + void localeChanged(); + void numericModeChanged(); + +protected: + int compare(const QModelIndex& sourceLeft, const QModelIndex& sourceRight, const QQmlSortFilterProxyModel& proxyModel) const override; + +private: + QCollator m_collator; +}; + +} + +#endif // STRINGSORTER_H diff --git a/tests/indexsorter.h b/tests/indexsorter.h index 1065325..2169df2 100644 --- a/tests/indexsorter.h +++ b/tests/indexsorter.h @@ -1,7 +1,7 @@ #ifndef INDEXSORTER_H #define INDEXSORTER_H -#include +#include "sorters/sorter.h" class IndexSorter : public qqsfpm::Sorter {