/ proxyroles / expressionrole.cpp
expressionrole.cpp
  1  #include "expressionrole.h"
  2  #include "qqmlsortfilterproxymodel.h"
  3  #include <QtQml>
  4  
  5  namespace qqsfpm {
  6  
  7  /*!
  8      \qmltype ExpressionRole
  9      \inherits SingleRole
 10      \inqmlmodule SortFilterProxyModel
 11      \ingroup ProxyRoles
 12      \brief A custom role computed from a javascript expression.
 13  
 14      An ExpressionRole is a \l ProxyRole allowing to implement a custom role based on a javascript expression.
 15  
 16      In the following example, the \c c role is computed by adding the \c a role and \c b role of the model :
 17      \code
 18      SortFilterProxyModel {
 19         sourceModel: numberModel
 20         proxyRoles: ExpressionRole {
 21             name: "c"
 22             expression: model.a + model.b
 23        }
 24      }
 25      \endcode
 26  */
 27  
 28  /*!
 29      \qmlproperty expression ExpressionRole::expression
 30  
 31      An expression to implement a custom role.
 32      It has the same syntax has a \l {http://doc.qt.io/qt-5/qtqml-syntax-propertybinding.html} {Property Binding} except it will be evaluated for each of the source model's rows.
 33      The data for this role will be the retuned valued of the expression.
 34      Data for each row is exposed like for a delegate of a QML View.
 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& ExpressionRole::expression() const
 44  {
 45      return m_scriptString;
 46  }
 47  
 48  void ExpressionRole::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 ExpressionRole::proxyModelCompleted(const QQmlSortFilterProxyModel& proxyModel)
 61  {
 62      updateContext(proxyModel);
 63  }
 64  
 65  QVariant ExpressionRole::data(const QModelIndex& sourceIndex, const QQmlSortFilterProxyModel& proxyModel)
 66  {
 67      if (!m_scriptString.isEmpty()) {
 68          QVariantMap modelMap;
 69          QHash<int, QByteArray> roles = proxyModel.roleNames();
 70  
 71          QQmlContext context(qmlContext(this));
 72          auto addToContext = [&] (const QString &name, const QVariant& value) {
 73              context.setContextProperty(name, value);
 74              modelMap.insert(name, value);
 75          };
 76  
 77          for (auto it = roles.cbegin(); it != roles.cend(); ++it)
 78              addToContext(it.value(), proxyModel.sourceData(sourceIndex, it.key()));
 79          addToContext(QStringLiteral("index"), sourceIndex.row());
 80  
 81          context.setContextProperty(QStringLiteral("model"), modelMap);
 82  
 83          QQmlExpression expression(m_scriptString, &context);
 84          QVariant result = expression.evaluate();
 85  
 86          if (expression.hasError()) {
 87              qWarning() << expression.error();
 88              return true;
 89          }
 90          return result;
 91      }
 92      return {};
 93  }
 94  
 95  void ExpressionRole::updateContext(const QQmlSortFilterProxyModel& proxyModel)
 96  {
 97      delete m_context;
 98      m_context = new QQmlContext(qmlContext(this), this);
 99      // what about roles changes ?
100      QVariantMap modelMap;
101  
102      auto addToContext = [&] (const QString &name, const QVariant& value) {
103          m_context->setContextProperty(name, value);
104          modelMap.insert(name, value);
105      };
106  
107      const auto roleNames = proxyModel.roleNames();
108      for (const QByteArray& roleName : roleNames)
109          addToContext(roleName, QVariant());
110  
111      addToContext(QStringLiteral("index"), -1);
112  
113      m_context->setContextProperty(QStringLiteral("model"), modelMap);
114      updateExpression();
115  }
116  
117  void ExpressionRole::updateExpression()
118  {
119      if (!m_context)
120          return;
121  
122      delete m_expression;
123      m_expression = new QQmlExpression(m_scriptString, m_context, nullptr, this);
124      connect(m_expression, &QQmlExpression::valueChanged, this, &ExpressionRole::invalidate);
125      m_expression->setNotifyOnValueChanged(true);
126      m_expression->evaluate();
127  }
128  
129  }