3

I have a polymorphic (as in arbitrary roles) QObject model that is mostly instantiated declaratively from QML, as in this answer, and I would like to be able to have custom data "views" that sort and filter the model via arbitrary, and potentially - runtime generated from code strings JS functors, something like that:

  DataView {
    sourceModel: model
    filter: function(o) { return o.size > 3 }
    sort: function(a, b) { return a.size > b.size }
  }

The QSortFilterProxyModel interface doesn't seem to be particularly well suited to the task, instead being fixated on static roles and pre-compiled rules.

I tried using QJSValue properties on the C++ side, but it seems like it is not possible, the C++ code just doesn't compile with that property type. And if I set the property type to QVariant I get error messages from QML that functions can only be bound to var properties. Evidently, var to QVariant conversion doesn't kick in here as it does for return values.

dtech
  • 47,916
  • 17
  • 112
  • 190

2 Answers2

2

Update:

Revisiting the issue, I finally came with a finalized solution, so I decided to drop in some updates. First, the relevant code:

void set_filter(QJSValue f) {
  if (f != m_filter) {
    m_filter = f;
    filterChanged();
    invalidate();
  }
}

void set_sorter(QJSValue f) {
  if (f != m_sort) {
    m_sort = f;
    sorterChanged();
    sort(0, Qt::DescendingOrder);
  }
}

bool filterAcceptsRow(int sourceRow, const QModelIndex & sourceParent) const {
  if (!m_filter.isCallable()) return true;
  QJSValueList l;
  l.append(_engine->newQObject(sourceModel()->index(sourceRow, 0, sourceParent).data().value<QObject*>()));
  return m_filter.call(l).toBool();
}

bool lessThan(const QModelIndex & left, const QModelIndex & right) const {
  if (!m_sort.isCallable()) return false;
  QJSValueList l;
  l.append(_engine->newQObject(sourceModel()->data(left).value<QObject*>()));
  l.append(_engine->newQObject(sourceModel()->data(right).value<QObject*>()));
  return m_sort.call(l).toBool();
}

I found this solution to be simpler, safer and better performing than the QQmlScriptString & QQmlExpression duo, which does offer automatic updates on notifications, but as already elaborated in the comments below GrecKo's answer, was kinda flaky and not really worth it.

The hack to get auto-updates for external context property changes is to simply reference them before returning the actual functor:

filter: { expanded; SS.showHidden; o => expanded && (SS.showHidden ? true : !o.hidden) }

Here is a simple expression using the new shorthand function syntax, it references expanded; SS.showHidden; in order to trigger reevaluations if those change, then implicitly returns the functor

o => expanded && (SS.showHidden ? true : !o.hidden)

which is analogous to:

return function(o) { return expanded && (SS.showHidden ? true : !o.hidden) }

which filters out objects based on whether the parent node is expanded, whether the child node is hidden and whether hidden objects are still displayed.

This solution has no way to automatically respond to changes to o.hidden, as o is inserted into the functor upon evaluation and can't be referenced in the binding expression, but this can easily be implemented in the delegates of views that need to dynamically respond to such changes:

Connections {
      target: obj
      onHiddenChanged: triggerExplicitEvaluation()
}

Remember that the use case involves a schema-less / single QObject* role model that facilitates a metamorphic data model where model item data is implemented via QML properties, so none of the role or regex stock filtering mechanisms are applicable here, but at the same time, this gives the genericity to use a single mechanism to implement sorting and filtering based on any criteria and arbitrary item data, and performance is very good, despite my initial concerns. It doesn't implement a sorting order, that is easily achievable by simply flipping the comparison expression result.

dtech
  • 47,916
  • 17
  • 112
  • 190
2

As you mentionned, you could use QJSValue. But that's pretty static. What if you want to use a filter like filter: function(o) { return o.size > slider.value; } with a dynamic slider ? You'll have to manually call invalidateFilter().

As a more practical alternative, you could instead use QQmlScriptString as a property & QQmlExpression to execute it. Using QQmlExpression allows you to be notified of context changes with setNotifyOnValueChanged.

Your syntax would change to be like so : filter: o.size > slider.value.

If you are looking for an out of the box solution, I've implemented this in a library of mine : SortFilterProxyModel on GitHub

You can take a look at ExpressionFilter & ExpressionSorter, those do the same as what you initially wanted. You can check the complete source code in the repo.

How to use it :

import SortFilterProxyModel 0.2

// ...

SortFilterProxyModel {
    sourceModel: model
    filters: ExpressionFilter  { expression: model.size > 3 }
    sorters: ExpressionSorter { expression: modelLeft.size < modelRight.size }
}

But as @dtech mentionned, the overhead of going back and forth between qml and c++ for each row of the model is quite noticeable. That's why I created more specific filters and sorters. In your case, we would use RangeFilter and RoleSorter :

import SortFilterProxyModel 0.2

// ...

SortFilterProxyModel {
    sourceModel: model
    filters: RangeFilter  {
        roleName: "size"
        minimumValue > 3
        minimumInclusive: true
    }
    sorters: RoleSorter { roleName: "size" }
}

Doing like this, we have a nice declarative API and the parameters are only passed once from qml to c++. All the filtering and sorting is then entirely done on the c++ side.

GrecKo
  • 6,615
  • 19
  • 23
  • Performance is actually not all that bad, the QML side profiling was also catching the GUI updates, giving a much higher time than what the sorting takes. About 2 msec for a 1000 objects, which is tolerable. As for the invalidation, QML bindings take care of that automatically in my case. And it is definitely not static, all the JS and QML dynamism is still there. – dtech Jun 05 '18 at 22:25
  • As for the invalidation, I'm pretty sure it is not done automatically if the change comes from QML. The proxymodel won't be aware of the need to call `filterAcceptRow` again. Take a look at the notes for `reSort` and `reFilter` here : https://github.com/Cutehacks/gel/blob/master/README.md#resort--function – GrecKo Jun 06 '18 at 12:41
  • Revisiting this issue, the solution was actually pretty simple - just reference the properties, whose changes you want to trigger filter.sort, which although a minor inconvenience to do manually, also offers the advantage to reference properties that are not used in the expression but you still want to trigger reevaluation. Using the newly available (still unrecognized by Creator but working) shorthand syntax, it is as simple as `filter: { someProp; obj => obj.someVal < someProp }`, or the traditional, more verbose `filter: { someProp; return function(obj) { return obj.someVal < someProp }}` – dtech Feb 17 '19 at 23:12
  • OK, so I implemented a solution using QQmlScriptString & QQmlExpression, and it turned out all the object creation and context property setting made it slower than the QJSValue solution, albeit by a margin low enough that delegate creation still constitutes roughly the same bottleneck. However, looking at how this solution creates a context that only lasts for the duration of the filter evaluation, it is not really something that can track and apply changes on notifications, so it would require context persistence and additional management and related complications and overheads. – dtech Feb 18 '19 at 11:49
  • I managed to make it roughly as fast by reusing the context and expression, however, this requires to disable notification on value changes for the duration of the filtering, as setting the context property to a new value forces another reevaluation, which against sets the property, which as expected, results in an infinite recursion and a crash. Also, in my case I am not using a flat but a nested and rather deep model which increases complexity and error proneness, since objects come and go at a rapid pace, which for some reason the qml engine cannot deal with all the time. – dtech Feb 18 '19 at 11:57
  • All in all, for my use case, the QJSValue solution offers better performance and stability. The downsides is all external triggers for reevaluation have to be referenced manually and the syntax is a tad more verbose. As for the inability to monitor and respond to internal data changes (since I am using a scheme-less / role-less model, I found out that this can be implemented by incorporating the notification in the delegates of the views that require sorting of filtering to react to internal model data changes, which while not perfect is still a perfectly viable solution. – dtech Feb 18 '19 at 12:03