From 9e37e58eef4a8d6aa67bcce9db6fd698c18af853 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Siret Date: Tue, 12 Sep 2017 01:25:11 +0200 Subject: [PATCH] feature: add StringSorter type --- sorter.cpp | 128 ++++++++++++++++++++++++++++++++- sorter.h | 38 ++++++++++ tests/SortFilterProxyModel.pro | 3 +- tests/tst_stringsorter.qml | 87 ++++++++++++++++++++++ 4 files changed, 253 insertions(+), 3 deletions(-) create mode 100644 tests/tst_stringsorter.qml diff --git a/sorter.cpp b/sorter.cpp index 5677ec1..ef86a16 100644 --- a/sorter.cpp +++ b/sorter.cpp @@ -154,10 +154,22 @@ void RoleSorter::setRoleName(const QString& roleName) sorterChanged(); } +QPair RoleSorter::sourceData(const QModelIndex &sourceLeft, const QModelIndex& sourceRight) const +{ + QPair pair; + if (QQmlSortFilterProxyModel* proxy = proxyModel()) { + int role = proxy->roleForName(m_roleName); + pair.first = proxy->sourceData(sourceLeft, role); + pair.second = proxy->sourceData(sourceRight, role); + } + return pair; +} + int RoleSorter::compare(const QModelIndex &sourceLeft, const QModelIndex& sourceRight) const { - QVariant leftValue = proxyModel()->sourceData(sourceLeft, m_roleName); - QVariant rightValue = proxyModel()->sourceData(sourceRight, m_roleName); + QPair pair = sourceData(sourceLeft, sourceRight); + QVariant leftValue = pair.first; + QVariant rightValue = pair.second; if (leftValue < rightValue) return -1; if (leftValue > rightValue) @@ -165,6 +177,117 @@ int RoleSorter::compare(const QModelIndex &sourceLeft, const QModelIndex& source 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(); + sorterChanged(); +} + +/*! + \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(); + sorterChanged(); +} + +/*! + \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(); + sorterChanged(); +} + +/*! + \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(); + sorterChanged(); +} + +int StringSorter::compare(const QModelIndex &sourceLeft, const QModelIndex &sourceRight) const +{ + QPair pair = sourceData(sourceLeft, sourceRight); + QString leftValue = pair.first.toString(); + QString rightValue = pair.second.toString(); + return m_collator.compare(leftValue, rightValue); +} + /*! \qmltype ExpressionSorter \inherits Sorter @@ -298,6 +421,7 @@ void ExpressionSorter::updateExpression() 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"); } diff --git a/sorter.h b/sorter.h index d331931..822b1df 100644 --- a/sorter.h +++ b/sorter.h @@ -4,6 +4,7 @@ #include #include #include "qqmlsortfilterproxymodel.h" +#include "QCollator" namespace qqsfpm { @@ -65,12 +66,49 @@ Q_SIGNALS: void roleNameChanged(); protected: + QPair sourceData(const QModelIndex &sourceLeft, const QModelIndex& sourceRight) const; int compare(const QModelIndex& sourceLeft, const QModelIndex& sourceRight) 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 override; + +private: + QCollator m_collator; +}; + class ExpressionSorter : public Sorter { Q_OBJECT diff --git a/tests/SortFilterProxyModel.pro b/tests/SortFilterProxyModel.pro index 751f2bb..4488b3f 100644 --- a/tests/SortFilterProxyModel.pro +++ b/tests/SortFilterProxyModel.pro @@ -19,4 +19,5 @@ OTHER_FILES += \ tst_sorters.qml \ tst_helpers.qml \ tst_builtins.qml \ - tst_rolesorter.qml + tst_rolesorter.qml \ + tst_stringsorter.qml diff --git a/tests/tst_stringsorter.qml b/tests/tst_stringsorter.qml new file mode 100644 index 0000000..51c213b --- /dev/null +++ b/tests/tst_stringsorter.qml @@ -0,0 +1,87 @@ +import QtQuick 2.0 +import SortFilterProxyModel 0.2 +import QtQml.Models 2.2 +import QtTest 1.1 + +Item { + property list sorters: [ + StringSorter { + property string tag: "normal" + property var expectedValues: ["haha", "hähä", "hehe", "héhé", "hihi", "huhu"] + roleName: "accentRole" + }, + StringSorter { + property string tag: "numericMode" + property var expectedValues: ["a1", "a20", "a30", "a99", "a100", "a1000"] + roleName: "numericRole" + numericMode: true + }, + StringSorter { + property string tag: "nonNumericMode" + property var expectedValues: ["a1", "a100", "a1000", "a20", "a30", "a99"] + roleName: "numericRole" + numericMode: false + }, + StringSorter { + //fails because of QTBUG-57034 and QTBUG-58621 + property string tag: "caseSensitive" + property var expectedValues: ["A", "Z", "a", "b", "c", "z"] + roleName: "caseRole" + caseSensitivity: Qt.CaseSensitive + }, + StringSorter { + property string tag: "nonCaseSensitive" + property var expectedValues: ["A", "a", "b", "c", "Z", "z"] + roleName: "caseRole" + caseSensitivity: Qt.CaseInsensitive + }, + StringSorter { + property string tag: "ignorePunctuation" + property var expectedValues: ["a-a", "aa", "b-b", "b-c", "b.c", "bc"] + roleName: "punctuationRole" + ignorePunctation: true + }, + StringSorter { + property string tag: "doNotIgnorePunctuation" + property var expectedValues: ["aa", "a-a", "b.c", "b-b", "bc", "b-c"] + roleName: "punctuationRole" + ignorePunctation: false + } + ] + + ListModel { + id: dataModel + ListElement { accentRole: "héhé"; numericRole: "a20"; caseRole: "b"; punctuationRole: "a-a"} + ListElement { accentRole: "hehe"; numericRole: "a1"; caseRole: "A"; punctuationRole: "aa"} + ListElement { accentRole: "haha"; numericRole: "a100"; caseRole: "a"; punctuationRole: "b-c"} + ListElement { accentRole: "huhu"; numericRole: "a99"; caseRole: "c"; punctuationRole: "b.c"} + ListElement { accentRole: "hihi"; numericRole: "a30"; caseRole: "Z"; punctuationRole: "bc"} + ListElement { accentRole: "hähä"; numericRole: "a1000"; caseRole: "z"; punctuationRole: "b-b"} + } + + SortFilterProxyModel { + id: testModel + sourceModel: dataModel + } + + TestCase { + name: "StringSorterTests" + + function test_stringSorters_data() { + return sorters; + } + + function test_stringSorters(sorter) { + testModel.sorters = sorter; + + verify(testModel.count === sorter.expectedValues.length, + "Expected count " + sorter.expectedValues.length + ", actual count: " + testModel.count); + for (var i = 0; i < testModel.count; i++) + { + var modelValue = testModel.get(i, sorter.roleName); + verify(modelValue === sorter.expectedValues[i], + "Expected testModel value " + sorter.expectedValues[i] + ", actual: " + modelValue); + } + } + } +}