Add new variable form of current feature expression functions

This adds a newer style variable form of referencing the current
feature and its attributes in expressions.

The newly introduced variables are:

- @feature: a replacement for $currentfeature, contains the
current feature
- @id: a replacement for $id, contains the current feature id
- @geometry: a replacement for $geometry, contains the current
feature geometry

This is intended as a step towards eventually deprecating the
older $ style functions, and providing a more consistent
approach to expressions instead of the older unpredictable mix of @/$.
For now these old functions still work (and likely will ALWAYS
remain working for old project compatibility), AND they are also
still exposed in the UI just to avoid user confusion (eventually
we can hide them).
This commit is contained in:
Nyall Dawson 2022-09-08 11:07:34 +10:00
parent 8fcd7913ea
commit 341add8cb9
9 changed files with 106 additions and 9 deletions

View File

@ -2,7 +2,7 @@
"name": "$currentfeature",
"type": "function",
"groups": ["Record and Attributes"],
"description": "Returns the current feature being evaluated. This can be used with the 'attribute' function to evaluate attribute values from the current feature.",
"description": "Returns the current feature being evaluated. This can be used with the 'attribute' function to evaluate attribute values from the current feature. <b>WARNING: This function is deprecated. It is recommended to use the replacement @feature variable instead.</b>",
"examples": [{
"expression": "attribute( $currentfeature, 'name' )",
"returns": "value stored in 'name' attribute for the current feature"

View File

@ -2,7 +2,7 @@
"name": "$geometry",
"type": "function",
"groups": ["GeometryGroup"],
"description": "Returns the geometry of the current feature. Can be used for processing with other functions.",
"description": "Returns the geometry of the current feature. Can be used for processing with other functions. <b>WARNING: This function is deprecated. It is recommended to use the replacement @geometry variable instead.</b>",
"examples": [{
"expression": "geom_to_wkt( $geometry )",
"returns": "'POINT(6 50)'"

View File

@ -2,7 +2,7 @@
"name": "$id",
"type": "function",
"groups": ["Record and Attributes"],
"description": "Returns the feature id of the current row.",
"description": "Returns the feature id of the current row. <b>WARNING: This function is deprecated. It is recommended to use the replacement @id variable instead.</b>",
"examples": [{
"expression": "$id",
"returns": "42"

View File

@ -748,6 +748,11 @@ void QgsExpression::initVariableHelp()
sVariableHelpTexts()->insert( QStringLiteral( "layer_crs" ), QCoreApplication::translate( "variable_help", "CRS Authority ID of current layer." ) );
sVariableHelpTexts()->insert( QStringLiteral( "layer" ), QCoreApplication::translate( "variable_help", "The current layer." ) );
//feature variables
sVariableHelpTexts()->insert( QStringLiteral( "current_feature" ), QCoreApplication::translate( "variable_help", "The current feature being evaluated. This can be used with the 'attribute' function to evaluate attribute values from the current feature." ) );
sVariableHelpTexts()->insert( QStringLiteral( "id" ), QCoreApplication::translate( "variable_help", "The ID of the current feature being evaluated." ) );
sVariableHelpTexts()->insert( QStringLiteral( "geometry" ), QCoreApplication::translate( "variable_help", "The geometry of the current feature being evaluated." ) );
//composition variables
sVariableHelpTexts()->insert( QStringLiteral( "layout_name" ), QCoreApplication::translate( "variable_help", "Name of composition." ) );
sVariableHelpTexts()->insert( QStringLiteral( "layout_numpages" ), QCoreApplication::translate( "variable_help", "Number of pages in composition." ) );

View File

@ -286,8 +286,28 @@ static QVariant fcnGetVariable( const QVariantList &values, const QgsExpressionC
if ( !context )
return QVariant();
QString name = QgsExpressionUtils::getStringValue( values.at( 0 ), parent );
return context->variable( name );
const QString name = QgsExpressionUtils::getStringValue( values.at( 0 ), parent );
if ( name == QLatin1String( "feature" ) )
{
return context->hasFeature() ? QVariant::fromValue( context->feature() ) : QVariant();
}
else if ( name == QLatin1String( "id" ) )
{
return context->hasFeature() ? QVariant::fromValue( context->feature().id() ) : QVariant();
}
else if ( name == QLatin1String( "geometry" ) )
{
if ( !context->hasFeature() )
return QVariant();
const QgsFeature feature = context->feature();
return feature.hasGeometry() ? QVariant::fromValue( feature.geometry() ) : QVariant();
}
else
{
return context->variable( name );
}
}
static QVariant fcnEvalTemplate( const QVariantList &values, const QgsExpressionContext *context, QgsExpression *parent, const QgsExpressionNodeFunction * )
@ -8383,7 +8403,9 @@ const QList<QgsExpressionFunction *> &QgsExpression::Functions()
if ( !argNode->isStatic( parent, context ) )
return false;
QString varName = argNode->eval( parent, context ).toString();
const QString varName = argNode->eval( parent, context ).toString();
if ( varName == QLatin1String( "feature" ) || varName == QLatin1String( "id" ) || varName == QLatin1String( "geometry" ) )
return false;
const QgsExpressionContextScope *scope = context->activeScopeForVariable( varName );
return scope ? scope->isStatic( varName ) : false;

View File

@ -119,6 +119,14 @@ QVariant QgsExpressionContextScope::variable( const QString &name ) const
QStringList QgsExpressionContextScope::variableNames() const
{
QStringList names = mVariables.keys();
if ( hasFeature() )
{
names.append( QStringLiteral( "feature" ) );
names.append( QStringLiteral( "id" ) );
names.append( QStringLiteral( "geometry" ) );
}
return names;
}

View File

@ -42,6 +42,12 @@ void QgsCodeEditorExpression::setExpressionContext( const QgsExpressionContext &
mVariables << '@' + var;
}
// always show feature variables in autocomplete -- they may not be available in the context
// at time of showing an expression builder, but they'll generally be available at evaluation time.
mVariables << QStringLiteral( "@feature" );
mVariables << QStringLiteral( "@id" );
mVariables << QStringLiteral( "@geometry" );
mContextFunctions = context.functionNames();
mFunctions.clear();

View File

@ -19,7 +19,6 @@
#include "qgsexpressiontreeview.h"
#include "qgis.h"
#include "qgsfieldformatterregistry.h"
#include "qgsvectorlayer.h"
#include "qgsexpressioncontextutils.h"
#include "qgssettings.h"
@ -463,11 +462,32 @@ void QgsExpressionTreeView::loadFieldNames()
{
QgsExpressionItem *node = mExpressionGroups.value( QStringLiteral( "Fields and Values" ) );
node->removeRows( 0, node->rowCount() );
// Re-add NULL
// use -1 as sort order here -- NULL should always show before the field list
// Re-add NULL and feature variables
// use -1 as sort order here -- NULL and feature variables should always show before the field list
registerItem( QStringLiteral( "Fields and Values" ), QStringLiteral( "NULL" ), QStringLiteral( "NULL" ), QString(), QgsExpressionItem::ExpressionNode, false, -1 );
}
if ( mLayer )
{
// Add feature variables to record and attributes group (and highlighted items)
const QString currentFeatureHelp = formatVariableHelp( QStringLiteral( "feature" ), QgsExpression::variableHelpText( QStringLiteral( "feature" ) ), false, QVariant() );
const QString currentFeatureIdHelp = formatVariableHelp( QStringLiteral( "id" ), QgsExpression::variableHelpText( QStringLiteral( "id" ) ), false, QVariant() );
const QString currentGeometryHelp = formatVariableHelp( QStringLiteral( "geometry" ), QgsExpression::variableHelpText( QStringLiteral( "geometry" ) ), false, QVariant() );
registerItem( QStringLiteral( "Fields and Values" ), QStringLiteral( "feature" ), QStringLiteral( "@feature" ), currentFeatureHelp, QgsExpressionItem::ExpressionNode, false, -1 );
registerItem( QStringLiteral( "Fields and Values" ), QStringLiteral( "id" ), QStringLiteral( "@id" ), currentFeatureIdHelp, QgsExpressionItem::ExpressionNode, false, -1 );
registerItem( QStringLiteral( "Fields and Values" ), QStringLiteral( "geometry" ), QStringLiteral( "@geometry" ), currentGeometryHelp, QgsExpressionItem::ExpressionNode, false, -1 );
registerItem( tr( "Variables" ), QStringLiteral( "feature" ), QStringLiteral( "@feature" ), currentFeatureHelp, QgsExpressionItem::ExpressionNode );
registerItem( tr( "Variables" ), QStringLiteral( "id" ), QStringLiteral( "@id" ), currentFeatureIdHelp, QgsExpressionItem::ExpressionNode );
registerItem( tr( "Variables" ), QStringLiteral( "geometry" ), QStringLiteral( "@geometry" ), currentGeometryHelp, QgsExpressionItem::ExpressionNode, false );
registerItem( tr( "Record and Attributes" ), QStringLiteral( "feature" ), QStringLiteral( "@feature" ), currentFeatureHelp, QgsExpressionItem::ExpressionNode, true, -1 );
registerItem( tr( "Record and Attributes" ), QStringLiteral( "id" ), QStringLiteral( "@id" ), currentFeatureIdHelp, QgsExpressionItem::ExpressionNode, true, -1 );
registerItem( tr( "Record and Attributes" ), QStringLiteral( "geometry" ), QStringLiteral( "@geometry" ), currentGeometryHelp, QgsExpressionItem::ExpressionNode, true, -1 );
}
// this can happen if fields are manually set
if ( !mLayer )
return;

View File

@ -2298,20 +2298,56 @@ class TestQgsExpression: public QObject
void eval_feature_id()
{
QgsFeature f( 100 );
// older form
QgsExpression exp( QStringLiteral( "$id * 2" ) );
QgsExpressionContext context = QgsExpressionContextUtils::createFeatureBasedContext( f, QgsFields() );
QVariant v = exp.evaluate( &context );
QCOMPARE( v.toInt(), 200 );
// newer form
QgsExpression exp2( QStringLiteral( "@id * 2" ) );
v = exp.evaluate( &context );
QCOMPARE( v.toInt(), 200 );
}
void eval_current_feature()
{
QgsFeature f( 100 );
// older form
QgsExpression exp( QStringLiteral( "$currentfeature" ) );
QgsExpressionContext context = QgsExpressionContextUtils::createFeatureBasedContext( f, QgsFields() );
QVariant v = exp.evaluate( &context );
QgsFeature evalFeature = v.value<QgsFeature>();
QCOMPARE( evalFeature.id(), f.id() );
// newer form
QgsExpression exp2( QStringLiteral( "@feature" ) );
v = exp2.evaluate( &context );
evalFeature = v.value<QgsFeature>();
QCOMPARE( evalFeature.id(), f.id() );
}
void eval_current_geometry()
{
QgsFeature featureWithGeometry( 100 );
featureWithGeometry.setGeometry( QgsGeometry::fromPointXY( QgsPointXY( 1, 2 ) ) );
QgsFeature featureWithNoGeometry( 100 );
// older form
QgsExpression exp( QStringLiteral( "geom_to_wkt($geometry)" ) );
QgsExpressionContext contextWithGeometry = QgsExpressionContextUtils::createFeatureBasedContext( featureWithGeometry, QgsFields() );
QgsExpressionContext contextWithNoGeometry = QgsExpressionContextUtils::createFeatureBasedContext( featureWithNoGeometry, QgsFields() );
QVariant v = exp.evaluate( &contextWithGeometry );
QCOMPARE( v.toString(), QStringLiteral( "Point (1 2)" ) );
v = exp.evaluate( &contextWithNoGeometry );
QVERIFY( v.isNull() );
// newer form
QgsExpression exp2( QStringLiteral( "geom_to_wkt(@geometry)" ) );
v = exp2.evaluate( &contextWithGeometry );
QCOMPARE( v.toString(), QStringLiteral( "Point (1 2)" ) );
v = exp2.evaluate( &contextWithNoGeometry );
QVERIFY( v.isNull() );
}
void eval_feature_attribute()