/ sorters / expressionsorter.cpp
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  }