expressionsorter.cpp
1 #include "expressionsorter.h" 2 #include "qqmlsortfilterproxymodel.h" 3 #include <QtQml> 4 5 namespace qqsfpm { 6 7 /*! 8 \qmltype ExpressionSorter 9 \inherits Sorter 10 \inqmlmodule SortFilterProxyModel 11 \ingroup Sorters 12 \brief Sorts row with a custom javascript expression. 13 14 An ExpressionSorter is a \l Sorter allowing to implement custom sorting based on a javascript expression. 15 */ 16 17 /*! 18 \qmlproperty expression ExpressionSorter::expression 19 20 An expression to implement custom sorting. It must evaluate to a bool. 21 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. 22 Model data is accessible for both rows with the \c modelLeft, and \c modelRight properties: 23 24 \code 25 sorters: ExpressionSorter { 26 expression: { 27 return modelLeft.someRole < modelRight.someRole; 28 } 29 } 30 \endcode 31 32 The \c index of the row is also available through \c modelLeft and \c modelRight. 33 34 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. 35 36 This expression is reevaluated for a row every time its model data changes. 37 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. 38 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. 39 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. 40 41 A workaround to this problem is to access all the properties the expressions depends unconditionally at the beggining of the expression. 42 */ 43 const QQmlScriptString& ExpressionSorter::expression() const 44 { 45 return m_scriptString; 46 } 47 48 void ExpressionSorter::setExpression(const QQmlScriptString& scriptString) 49 { 50 if (m_scriptString == scriptString) 51 return; 52 53 m_scriptString = scriptString; 54 updateExpression(); 55 56 Q_EMIT expressionChanged(); 57 invalidate(); 58 } 59 60 void ExpressionSorter::proxyModelCompleted(const QQmlSortFilterProxyModel& proxyModel) 61 { 62 updateContext(proxyModel); 63 } 64 65 bool evaluateBoolExpression(QQmlExpression& expression) 66 { 67 QVariant variantResult = expression.evaluate(); 68 if (expression.hasError()) { 69 qWarning() << expression.error(); 70 return false; 71 } 72 if (variantResult.canConvert<bool>()) { 73 return variantResult.toBool(); 74 } else { 75 qWarning("%s:%i:%i : Can't convert result to bool", 76 expression.sourceFile().toUtf8().data(), 77 expression.lineNumber(), 78 expression.columnNumber()); 79 return false; 80 } 81 } 82 83 int ExpressionSorter::compare(const QModelIndex& sourceLeft, const QModelIndex& sourceRight, const QQmlSortFilterProxyModel& proxyModel) const 84 { 85 if (!m_scriptString.isEmpty()) { 86 QVariantMap modelLeftMap, modelRightMap; 87 QHash<int, QByteArray> roles = proxyModel.roleNames(); 88 89 QQmlContext context(qmlContext(this)); 90 91 for (auto it = roles.cbegin(); it != roles.cend(); ++it) { 92 modelLeftMap.insert(it.value(), proxyModel.sourceData(sourceLeft, it.key())); 93 modelRightMap.insert(it.value(), proxyModel.sourceData(sourceRight, it.key())); 94 } 95 modelLeftMap.insert(QStringLiteral("index"), sourceLeft.row()); 96 modelRightMap.insert(QStringLiteral("index"), sourceRight.row()); 97 98 QQmlExpression expression(m_scriptString, &context); 99 100 context.setContextProperty(QStringLiteral("modelLeft"), modelLeftMap); 101 context.setContextProperty(QStringLiteral("modelRight"), modelRightMap); 102 if (evaluateBoolExpression(expression)) 103 return -1; 104 105 context.setContextProperty(QStringLiteral("modelLeft"), modelRightMap); 106 context.setContextProperty(QStringLiteral("modelRight"), modelLeftMap); 107 if (evaluateBoolExpression(expression)) 108 return 1; 109 } 110 return 0; 111 } 112 113 void ExpressionSorter::updateContext(const QQmlSortFilterProxyModel& proxyModel) 114 { 115 delete m_context; 116 m_context = new QQmlContext(qmlContext(this), this); 117 118 QVariantMap modelLeftMap, modelRightMap; 119 // what about roles changes ? 120 121 const auto roleNames = proxyModel.roleNames(); 122 for (const QByteArray& roleName : roleNames) { 123 modelLeftMap.insert(roleName, QVariant()); 124 modelRightMap.insert(roleName, QVariant()); 125 } 126 modelLeftMap.insert(QStringLiteral("index"), -1); 127 modelRightMap.insert(QStringLiteral("index"), -1); 128 129 m_context->setContextProperty(QStringLiteral("modelLeft"), modelLeftMap); 130 m_context->setContextProperty(QStringLiteral("modelRight"), modelRightMap); 131 132 updateExpression(); 133 } 134 135 void ExpressionSorter::updateExpression() 136 { 137 if (!m_context) 138 return; 139 140 delete m_expression; 141 m_expression = new QQmlExpression(m_scriptString, m_context, nullptr, this); 142 connect(m_expression, &QQmlExpression::valueChanged, this, &ExpressionSorter::invalidate); 143 m_expression->setNotifyOnValueChanged(true); 144 m_expression->evaluate(); 145 } 146 147 }