feat(StatusQ): Faster version of SFPM's ExpressionFilter
Provides FastExpressionFilter component compatible with SortFilterProxyModel. In comparison to original ExpressionFilter it allows to define which role values should be provided to the expression's context and improves performance significantly. Closes: #13063
This commit is contained in:
parent
ed89ba77b8
commit
a6cf37278c
|
@ -91,6 +91,7 @@ add_library(StatusQ SHARED
|
|||
include/StatusQ/QClipboardProxy.h
|
||||
include/StatusQ/aggregator.h
|
||||
include/StatusQ/concatmodel.h
|
||||
include/StatusQ/fastexpressionfilter.h
|
||||
include/StatusQ/fastexpressionrole.h
|
||||
include/StatusQ/leftjoinmodel.h
|
||||
include/StatusQ/modelutilsinternal.h
|
||||
|
@ -106,6 +107,7 @@ add_library(StatusQ SHARED
|
|||
src/QClipboardProxy.cpp
|
||||
src/aggregator.cpp
|
||||
src/concatmodel.cpp
|
||||
src/fastexpressionfilter.cpp
|
||||
src/fastexpressionrole.cpp
|
||||
src/leftjoinmodel.cpp
|
||||
src/modelutilsinternal.cpp
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
#pragma once
|
||||
|
||||
#include <filters/filter.h>
|
||||
|
||||
#include <QQmlContext>
|
||||
#include <QQmlExpression>
|
||||
#include <QQmlScriptString>
|
||||
|
||||
#include <memory>
|
||||
|
||||
class FastExpressionFilter : public qqsfpm::Filter
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(QQmlScriptString expression READ expression WRITE setExpression
|
||||
NOTIFY expressionChanged)
|
||||
|
||||
Q_PROPERTY(QStringList expectedRoles READ expectedRoles
|
||||
WRITE setExpectedRoles NOTIFY expectedRolesChanged)
|
||||
public:
|
||||
using Filter::Filter;
|
||||
|
||||
const QQmlScriptString& expression() const;
|
||||
void setExpression(const QQmlScriptString& scriptString);
|
||||
|
||||
void proxyModelCompleted(
|
||||
const qqsfpm::QQmlSortFilterProxyModel& proxyModel) override;
|
||||
|
||||
void setExpectedRoles(const QStringList& expectedRoles);
|
||||
const QStringList& expectedRoles() const;
|
||||
|
||||
protected:
|
||||
bool filterRow(
|
||||
const QModelIndex& sourceIndex,
|
||||
const qqsfpm::QQmlSortFilterProxyModel& proxyModel) const override;
|
||||
|
||||
Q_SIGNALS:
|
||||
void expressionChanged();
|
||||
void expectedRolesChanged();
|
||||
|
||||
private:
|
||||
void updateContext(const qqsfpm::QQmlSortFilterProxyModel& proxyModel);
|
||||
void updateExpression();
|
||||
|
||||
QQmlScriptString m_scriptString;
|
||||
std::unique_ptr<QQmlExpression> m_expression;
|
||||
std::unique_ptr<QQmlContext> m_context;
|
||||
|
||||
QStringList m_expectedRoles;
|
||||
};
|
|
@ -2,9 +2,11 @@
|
|||
|
||||
#include <proxyroles/singlerole.h>
|
||||
|
||||
#include <QQmlContext>
|
||||
#include <QQmlExpression>
|
||||
#include <QQmlScriptString>
|
||||
|
||||
class QQmlExpression;
|
||||
#include <memory>
|
||||
|
||||
class FastExpressionRole : public qqsfpm::SingleRole
|
||||
{
|
||||
|
@ -38,8 +40,8 @@ private:
|
|||
void updateExpression();
|
||||
|
||||
QQmlScriptString m_scriptString;
|
||||
QQmlExpression* m_expression = nullptr;
|
||||
QQmlContext* m_context = nullptr;
|
||||
std::unique_ptr<QQmlExpression> m_expression;
|
||||
std::unique_ptr<QQmlContext> m_context;
|
||||
|
||||
QStringList m_expectedRoles;
|
||||
};
|
||||
|
|
|
@ -0,0 +1,167 @@
|
|||
#include "StatusQ/fastexpressionfilter.h"
|
||||
|
||||
#include <qqmlsortfilterproxymodel.h>
|
||||
|
||||
using namespace qqsfpm;
|
||||
|
||||
/*!
|
||||
\qmltype FastExpressionFilter
|
||||
\inherits Filter
|
||||
\inqmlmodule StatusQ
|
||||
\brief A custom filter similar to (and based on) SFPM's ExpressionFilter but
|
||||
optimized to access only explicitly indicated roles.
|
||||
|
||||
A FastExpressionFilter, similarly as \l ExpressionFilter, is a \l Filter
|
||||
allowing to implement a filtering based on a javascript expression.
|
||||
However in FastExpressionFilter's expression context there are available
|
||||
only roles explicitly listed in \l expectedRoles property:
|
||||
|
||||
\code
|
||||
SortFilterProxyModel {
|
||||
sourceModel: mySourceModel
|
||||
filters: FastExpressionFilter {
|
||||
expression: model.a < 4 && model.b < 10
|
||||
expectedRoles: ["a", "b"]
|
||||
}
|
||||
}
|
||||
\endcode
|
||||
|
||||
By accessing only needed roles, the performance is significantly better in
|
||||
comparison to ExpressionFilter.
|
||||
*/
|
||||
|
||||
/*!
|
||||
\qmlproperty expression FastExpressionFilter::expression
|
||||
|
||||
See ExpressionFilter::expression for details. Unlike the original
|
||||
ExpressionFilter only roles explicitly declared via expectedRoles are accessible.
|
||||
*/
|
||||
const QQmlScriptString& FastExpressionFilter::expression() const
|
||||
{
|
||||
return m_scriptString;
|
||||
}
|
||||
|
||||
void FastExpressionFilter::setExpression(const QQmlScriptString& scriptString)
|
||||
{
|
||||
if (m_scriptString == scriptString)
|
||||
return;
|
||||
|
||||
m_scriptString = scriptString;
|
||||
updateExpression();
|
||||
|
||||
emit expressionChanged();
|
||||
invalidate();
|
||||
}
|
||||
|
||||
void FastExpressionFilter::proxyModelCompleted(const QQmlSortFilterProxyModel& proxyModel)
|
||||
{
|
||||
updateContext(proxyModel);
|
||||
}
|
||||
|
||||
/*!
|
||||
\qmlproperty list<string> FastExpressionFilter::expectedRoles
|
||||
|
||||
List of role names intended to be available in the expression's context.
|
||||
*/
|
||||
void FastExpressionFilter::setExpectedRoles(const QStringList& expectedRoles)
|
||||
{
|
||||
if (m_expectedRoles == expectedRoles)
|
||||
return;
|
||||
|
||||
m_expectedRoles = expectedRoles;
|
||||
emit expectedRolesChanged();
|
||||
|
||||
invalidate();
|
||||
}
|
||||
|
||||
const QStringList &FastExpressionFilter::expectedRoles() const
|
||||
{
|
||||
return m_expectedRoles;
|
||||
}
|
||||
|
||||
bool FastExpressionFilter::filterRow(const QModelIndex& sourceIndex,
|
||||
const QQmlSortFilterProxyModel& proxyModel) const
|
||||
{
|
||||
if (m_scriptString.isEmpty())
|
||||
return true;
|
||||
|
||||
QVariantMap modelMap;
|
||||
auto roles = proxyModel.roleNames();
|
||||
|
||||
QQmlContext context(qmlContext(this));
|
||||
auto addToContext = [&] (const QString &name, const QVariant& value) {
|
||||
context.setContextProperty(name, value);
|
||||
modelMap.insert(name, value);
|
||||
};
|
||||
|
||||
for (auto it = roles.cbegin(); it != roles.cend(); ++it) {
|
||||
auto name = it.value();
|
||||
|
||||
if (!m_expectedRoles.contains(name))
|
||||
continue;
|
||||
|
||||
addToContext(it.value(), proxyModel.sourceData(sourceIndex, it.key()));
|
||||
}
|
||||
|
||||
addToContext(QStringLiteral("index"), sourceIndex.row());
|
||||
context.setContextProperty(QStringLiteral("model"), modelMap);
|
||||
|
||||
QQmlExpression expression(m_scriptString, &context);
|
||||
QVariant result = expression.evaluate();
|
||||
|
||||
if (expression.hasError()) {
|
||||
qWarning() << expression.error();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (result.canConvert<bool>()) {
|
||||
return result.toBool();
|
||||
} else {
|
||||
qWarning("%s:%i:%i : Can't convert result to bool",
|
||||
expression.sourceFile().toUtf8().data(),
|
||||
expression.lineNumber(),
|
||||
expression.columnNumber());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
void FastExpressionFilter::updateContext(const QQmlSortFilterProxyModel& proxyModel)
|
||||
{
|
||||
m_context = std::make_unique<QQmlContext>(qmlContext(this));
|
||||
|
||||
QVariantMap modelMap;
|
||||
|
||||
auto addToContext = [&] (const QString &name, const QVariant& value) {
|
||||
m_context->setContextProperty(name, value);
|
||||
modelMap.insert(name, value);
|
||||
};
|
||||
|
||||
const auto roles = proxyModel.roleNames();
|
||||
|
||||
for (auto it = roles.cbegin(); it != roles.cend(); ++it) {
|
||||
auto name = it.value();
|
||||
|
||||
if (!m_expectedRoles.contains(name))
|
||||
continue;
|
||||
|
||||
addToContext(name, {});
|
||||
}
|
||||
|
||||
addToContext(QStringLiteral("index"), -1);
|
||||
|
||||
m_context->setContextProperty(QStringLiteral("model"), modelMap);
|
||||
updateExpression();
|
||||
}
|
||||
|
||||
void FastExpressionFilter::updateExpression()
|
||||
{
|
||||
if (!m_context)
|
||||
return;
|
||||
|
||||
m_expression = std::make_unique<QQmlExpression>(m_scriptString,
|
||||
m_context.get());
|
||||
connect(m_expression.get(), &QQmlExpression::valueChanged, this,
|
||||
&FastExpressionFilter::invalidate);
|
||||
m_expression->setNotifyOnValueChanged(true);
|
||||
m_expression->evaluate();
|
||||
}
|
|
@ -1,9 +1,6 @@
|
|||
#include "StatusQ/fastexpressionrole.h"
|
||||
|
||||
#include "qqmlsortfilterproxymodel.h"
|
||||
|
||||
#include <QQmlContext>
|
||||
#include <QQmlExpression>
|
||||
#include <qqmlsortfilterproxymodel.h>
|
||||
|
||||
using namespace qqsfpm;
|
||||
|
||||
|
@ -38,7 +35,7 @@ using namespace qqsfpm;
|
|||
/*!
|
||||
\qmlproperty expression FastExpressionRole::expression
|
||||
|
||||
See ExpressionRole::expression for details. Unline the original
|
||||
See ExpressionRole::expression for details. Unlike the original
|
||||
ExpressionRole only roles explicitly declared via expectedRoles are accessible.
|
||||
*/
|
||||
const QQmlScriptString& FastExpressionRole::expression() const
|
||||
|
@ -123,8 +120,7 @@ QVariant FastExpressionRole::data(const QModelIndex& sourceIndex,
|
|||
|
||||
void FastExpressionRole::updateContext(const QQmlSortFilterProxyModel& proxyModel)
|
||||
{
|
||||
delete m_context;
|
||||
m_context = new QQmlContext(qmlContext(this), this);
|
||||
m_context = std::make_unique<QQmlContext>(qmlContext(this));
|
||||
|
||||
QVariantMap modelMap;
|
||||
|
||||
|
@ -155,9 +151,10 @@ void FastExpressionRole::updateExpression()
|
|||
if (!m_context)
|
||||
return;
|
||||
|
||||
delete m_expression;
|
||||
m_expression = new QQmlExpression(m_scriptString, m_context, nullptr, this);
|
||||
connect(m_expression, &QQmlExpression::valueChanged, this, &FastExpressionRole::invalidate);
|
||||
m_expression = std::make_unique<QQmlExpression>(m_scriptString,
|
||||
m_context.get());
|
||||
connect(m_expression.get(), &QQmlExpression::valueChanged, this,
|
||||
&FastExpressionRole::invalidate);
|
||||
m_expression->setNotifyOnValueChanged(true);
|
||||
m_expression->evaluate();
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
|
||||
#include "StatusQ/QClipboardProxy.h"
|
||||
#include "StatusQ/concatmodel.h"
|
||||
#include "StatusQ/fastexpressionfilter.h"
|
||||
#include "StatusQ/fastexpressionrole.h"
|
||||
#include "StatusQ/leftjoinmodel.h"
|
||||
#include "StatusQ/modelutilsinternal.h"
|
||||
|
@ -39,6 +40,7 @@ public:
|
|||
qmlRegisterType<SourceModel>("StatusQ", 0, 1, "SourceModel");
|
||||
qmlRegisterType<ConcatModel>("StatusQ", 0, 1, "ConcatModel");
|
||||
|
||||
qmlRegisterType<FastExpressionFilter>("StatusQ", 0, 1, "FastExpressionFilter");
|
||||
qmlRegisterType<FastExpressionRole>("StatusQ", 0, 1, "FastExpressionRole");
|
||||
|
||||
qmlRegisterType<LeftJoinModel>("StatusQ", 0, 1, "LeftJoinModel");
|
||||
|
|
|
@ -0,0 +1,96 @@
|
|||
import QtQml 2.15
|
||||
import QtQuick 2.15
|
||||
import QtTest 1.15
|
||||
|
||||
import SortFilterProxyModel 0.2
|
||||
|
||||
import StatusQ 0.1
|
||||
import StatusQ.TestHelpers 0.1
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
Component {
|
||||
id: testComponent
|
||||
|
||||
QtObject {
|
||||
property int d: 0
|
||||
|
||||
property alias filterEnabled: filter.enabled
|
||||
|
||||
readonly property ListModel source: ListModel {
|
||||
id: listModel
|
||||
|
||||
ListElement { a: 1; b: 11; c: 101 }
|
||||
ListElement { a: 2; b: 12; c: 102 }
|
||||
ListElement { a: 3; b: 13; c: 103 }
|
||||
ListElement { a: 4; b: 14; c: 104 }
|
||||
ListElement { a: 5; b: 15; c: 105 }
|
||||
ListElement { a: 6; b: 16; c: 106 }
|
||||
ListElement { a: 7; b: 17; c: 107 }
|
||||
}
|
||||
|
||||
readonly property ModelAccessObserverProxy observer: ModelAccessObserverProxy {
|
||||
id: observerProxy
|
||||
|
||||
property int accessCounter: 0
|
||||
|
||||
sourceModel: listModel
|
||||
|
||||
onDataAccessed: accessCounter++
|
||||
}
|
||||
|
||||
readonly property SortFilterProxyModel model: SortFilterProxyModel {
|
||||
id: testModel
|
||||
|
||||
sourceModel: observerProxy
|
||||
|
||||
filters: FastExpressionFilter {
|
||||
id: filter
|
||||
|
||||
expression: a > d && a < 5
|
||||
expectedRoles: ["a"]
|
||||
}
|
||||
}
|
||||
|
||||
readonly property SignalSpy rowsRemovedSpy: SignalSpy {
|
||||
target: testModel
|
||||
signalName: "rowsRemoved"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TestCase {
|
||||
name: "FastExpressionFilter"
|
||||
|
||||
function test_basicFiltering() {
|
||||
const obj = createTemporaryObject(testComponent, root)
|
||||
|
||||
compare(obj.model.count, 4)
|
||||
compare(obj.observer.accessCounter, 7)
|
||||
}
|
||||
|
||||
function test_filteringAfterContextChange() {
|
||||
const obj = createTemporaryObject(testComponent, root)
|
||||
|
||||
compare(obj.rowsRemovedSpy.count, 0)
|
||||
obj.d = 1
|
||||
compare(obj.rowsRemovedSpy.count, 1)
|
||||
|
||||
compare(obj.observer.accessCounter, 14)
|
||||
}
|
||||
|
||||
function test_enabled() {
|
||||
const obj = createTemporaryObject(testComponent, root,
|
||||
{ filterEnabled: false })
|
||||
|
||||
compare(obj.model.count, 7)
|
||||
compare(obj.observer.accessCounter, 0)
|
||||
|
||||
obj.filterEnabled = true
|
||||
|
||||
compare(obj.model.count, 4)
|
||||
compare(obj.observer.accessCounter, 7)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -64,6 +64,11 @@ Item {
|
|||
property string expressionRole: model.expressionRole
|
||||
}
|
||||
}
|
||||
|
||||
readonly property SignalSpy modelSignalSpy: SignalSpy {
|
||||
target: testModel
|
||||
signalName: "dataChanged"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -107,8 +112,11 @@ Item {
|
|||
const instantiator = obj.instantiator
|
||||
|
||||
observerProxy.accessCounter = 0
|
||||
compare(obj.modelSignalSpy.count, 0)
|
||||
|
||||
obj.d = 1
|
||||
|
||||
compare(obj.modelSignalSpy.count, 1)
|
||||
compare(observerProxy.accessCounter, 4)
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue