From 01f3c9ae38de51e82a4a18a48488695deb51f72f Mon Sep 17 00:00:00 2001 From: Matthias Kuhn Date: Thu, 27 Oct 2016 14:45:13 +0200 Subject: [PATCH] [FEATURE] Add is_selected and num_selected functions * is_selected() returns if the current feature is selected * num_selected() returns the number of selected features on the current layer * is_selected(layer, feature) returns if the "feature" is selected. "feature" must be on "layer". * num_selected(layer) returns the number of selected features on "layer" --- resources/function_help/json/is_selected | 13 +++ resources/function_help/json/num_selected | 12 +++ src/core/qgsexpression.cpp | 106 ++++++++++++++++++++-- src/core/qgsexpressioncontext.cpp | 1 + src/core/qgsexpressioncontext.h | 2 +- tests/src/core/testqgsexpression.cpp | 49 ++++++++++ 6 files changed, 173 insertions(+), 10 deletions(-) create mode 100644 resources/function_help/json/is_selected create mode 100644 resources/function_help/json/num_selected diff --git a/resources/function_help/json/is_selected b/resources/function_help/json/is_selected new file mode 100644 index 00000000000..ca1e7a4344d --- /dev/null +++ b/resources/function_help/json/is_selected @@ -0,0 +1,13 @@ +{ + "name": "is_selected", + "type": "function", + "description": "Returns if a feature is selected. If called with no parameters checks the current feature.", + "arguments": [ + {"arg":"feature","description":"The feature which should be checked for selection"}, + {"arg":"layer","description":"The layer (or its id or name) on which the selection will be checked"} + ], + "examples": [ + { "expression":"is_selected()", "returns" : "True if the current feature is selected."}, + { "expression":"is_selected(get_feature('streets', 'name', \"street_name\"), 'streets')", "returns":"True if the current building's street is selected."} + ] +} diff --git a/resources/function_help/json/num_selected b/resources/function_help/json/num_selected new file mode 100644 index 00000000000..7c61f288bf3 --- /dev/null +++ b/resources/function_help/json/num_selected @@ -0,0 +1,12 @@ +{ + "name": "num_selected", + "type": "function", + "description": "Returns the number of selected features on a given layer. By default works on the layer on which the expression is evaluated.", + "arguments": [ + {"arg":"layer","description":"The layer (or its id or name) on which the selection will be checked"} + ], + "examples": [ + { "expression":"num_selected()", "returns":"The number of selected features on the current layer."}, + { "expression":"num_selected('streets')", "returns":"The number of selected features on the layer streets"} + ] +} diff --git a/src/core/qgsexpression.cpp b/src/core/qgsexpression.cpp index 2ca74e461c8..46b2208b921 100644 --- a/src/core/qgsexpression.cpp +++ b/src/core/qgsexpression.cpp @@ -312,14 +312,19 @@ static QgsExpression::Node* getNode( const QVariant& value, QgsExpression* paren QgsVectorLayer* getVectorLayer( const QVariant& value, QgsExpression* ) { - QString layerString = value.toString(); - QgsVectorLayer* vl = qobject_cast( QgsMapLayerRegistry::instance()->mapLayer( layerString ) ); //search by id first + QgsVectorLayer* vl = value.value(); if ( !vl ) { - QList layersByName = QgsMapLayerRegistry::instance()->mapLayersByName( layerString ); - if ( !layersByName.isEmpty() ) + QString layerString = value.toString(); + vl = qobject_cast( QgsMapLayerRegistry::instance()->mapLayer( layerString ) ); //search by id first + + if ( !vl ) { - vl = qobject_cast( layersByName.at( 0 ) ); + QList layersByName = QgsMapLayerRegistry::instance()->mapLayersByName( layerString ); + if ( !layersByName.isEmpty() ) + { + vl = qobject_cast( layersByName.at( 0 ) ); + } } } @@ -707,8 +712,7 @@ static QVariant fcnAggregateRelation( const QVariantList& values, const QgsExpre } // first step - find current layer - QString layerId = context->variable( QStringLiteral( "layer_id" ) ).toString(); - QgsVectorLayer* vl = qobject_cast( QgsMapLayerRegistry::instance()->mapLayer( layerId ) ); + QgsVectorLayer* vl = getVectorLayer( context->variable( "layer" ), parent ); if ( !vl ) { parent->setEvalErrorString( QObject::tr( "Cannot use relation aggregate function in this context" ) ); @@ -810,8 +814,7 @@ static QVariant fcnAggregateGeneric( QgsAggregateCalculator::Aggregate aggregate } // first step - find current layer - QString layerId = context->variable( QStringLiteral( "layer_id" ) ).toString(); - QgsVectorLayer* vl = qobject_cast( QgsMapLayerRegistry::instance()->mapLayer( layerId ) ); + QgsVectorLayer* vl = getVectorLayer( context->variable( "layer" ), parent ); if ( !vl ) { parent->setEvalErrorString( QObject::tr( "Cannot use aggregate function in this context" ) ); @@ -1358,6 +1361,63 @@ static QVariant fcnAttribute( const QVariantList& values, const QgsExpressionCon return feat.attribute( attr ); } + +static QVariant fcnIsSelected( const QVariantList& values, const QgsExpressionContext* context, QgsExpression* parent ) +{ + QgsVectorLayer* layer = nullptr; + QgsFeature feature; + + if ( values.isEmpty() ) + { + feature = context->feature(); + layer = getVectorLayer( context->variable( "layer" ), parent ); + } + else if ( values.size() == 1 ) + { + layer = getVectorLayer( context->variable( "layer" ), parent ); + feature = getFeature( values.at( 0 ), parent ); + } + else if ( values.size() == 2 ) + { + layer = getVectorLayer( values.at( 0 ), parent ); + feature = getFeature( values.at( 1 ), parent ); + } + else + { + parent->setEvalErrorString( QObject::tr( "Function `is_selected` requires no more than two parameters. %1 given." ).arg( values.length() ) ); + return QVariant(); + } + + if ( !layer || !feature.isValid() ) + { + return QVariant( QVariant::Bool ); + } + + return layer->selectedFeaturesIds().contains( feature.id() ); +} + +static QVariant fcnNumSelected( const QVariantList& values, const QgsExpressionContext* context, QgsExpression* parent ) +{ + QgsVectorLayer* layer = nullptr; + + if ( values.isEmpty() ) + layer = getVectorLayer( context->variable( "layer" ), parent ); + else if ( values.count() == 1 ) + layer = getVectorLayer( values.at( 0 ), parent ); + else + { + parent->setEvalErrorString( QObject::tr( "Function `num_selected` requires no more than one parameter. %1 given." ).arg( values.length() ) ); + return QVariant(); + } + + if ( !layer ) + { + return QVariant( QVariant::LongLong ); + } + + return layer->selectedFeatureCount(); +} + static QVariant fcnConcat( const QVariantList& values, const QgsExpressionContext*, QgsExpression *parent ) { QString concat; @@ -3665,10 +3725,37 @@ const QList& QgsExpression::Functions() << Parameter( QStringLiteral( "vertex" ) ), fcnAngleAtVertex, QStringLiteral( "GeometryGroup" ) ) << new StaticFunction( QStringLiteral( "distance_to_vertex" ), ParameterList() << Parameter( QStringLiteral( "geometry" ) ) << Parameter( QStringLiteral( "vertex" ) ), fcnDistanceToVertex, QStringLiteral( "GeometryGroup" ) ) + + + // **Record** functions + << new StaticFunction( QStringLiteral( "$id" ), 0, fcnFeatureId, QStringLiteral( "Record" ) ) << new StaticFunction( QStringLiteral( "$currentfeature" ), 0, fcnFeature, QStringLiteral( "Record" ) ) << new StaticFunction( QStringLiteral( "uuid" ), 0, fcnUuid, QStringLiteral( "Record" ), QString(), false, QSet(), false, QStringList() << QStringLiteral( "$uuid" ) ) << new StaticFunction( QStringLiteral( "get_feature" ), 3, fcnGetFeature, QStringLiteral( "Record" ), QString(), false, QSet(), false, QStringList() << QStringLiteral( "getFeature" ) ) + + << new StaticFunction( + QStringLiteral( "is_selected" ), + -1, + fcnIsSelected, + QStringLiteral( "Record" ), + QString(), + false, + QSet() + ) + + << new StaticFunction( + QStringLiteral( "num_selected" ), + -1, + fcnNumSelected, + QStringLiteral( "Record" ), + QString(), + false, + QSet() + ) + + // **General** functions + << new StaticFunction( QStringLiteral( "layer_property" ), 2, fcnGetLayerProperty, QStringLiteral( "General" ) ) << new StaticFunction( QStringLiteral( "var" ), 1, fcnGetVariable, QStringLiteral( "General" ) ) @@ -5290,6 +5377,7 @@ void QgsExpression::initVariableHelp() //layer variables gVariableHelpTexts.insert( QStringLiteral( "layer_name" ), QCoreApplication::translate( "variable_help", "Name of current layer." ) ); gVariableHelpTexts.insert( QStringLiteral( "layer_id" ), QCoreApplication::translate( "variable_help", "ID of current layer." ) ); + gVariableHelpTexts.insert( QStringLiteral( "layer" ), QCoreApplication::translate( "variable_help", "The current layer." ) ); //composition variables gVariableHelpTexts.insert( QStringLiteral( "layout_numpages" ), QCoreApplication::translate( "variable_help", "Number of pages in composition." ) ); diff --git a/src/core/qgsexpressioncontext.cpp b/src/core/qgsexpressioncontext.cpp index cd150b2fa7f..c7a1cbbfd72 100644 --- a/src/core/qgsexpressioncontext.cpp +++ b/src/core/qgsexpressioncontext.cpp @@ -691,6 +691,7 @@ QgsExpressionContextScope* QgsExpressionContextUtils::layerScope( const QgsMapLa scope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "layer_name" ), layer->name(), true ) ); scope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "layer_id" ), layer->id(), true ) ); + scope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "layer" ), QVariant::fromValue( const_cast( layer ) ), true ) ); const QgsVectorLayer* vLayer = dynamic_cast< const QgsVectorLayer* >( layer ); if ( vLayer ) diff --git a/src/core/qgsexpressioncontext.h b/src/core/qgsexpressioncontext.h index 02462ae71bb..fa59d6a403e 100644 --- a/src/core/qgsexpressioncontext.h +++ b/src/core/qgsexpressioncontext.h @@ -581,7 +581,7 @@ class CORE_EXPORT QgsExpressionContextUtils /** Creates a new scope which contains variables and functions relating to a QgsMapLayer. * For instance, layer name, id and fields. */ - static QgsExpressionContextScope* layerScope( const QgsMapLayer *layer ); + static QgsExpressionContextScope* layerScope( const QgsMapLayer* layer ); /** Sets a layer context variable. This variable will be contained within scopes retrieved via * layerScope(). diff --git a/tests/src/core/testqgsexpression.cpp b/tests/src/core/testqgsexpression.cpp index 87b66047fd5..727aaa3fe94 100644 --- a/tests/src/core/testqgsexpression.cpp +++ b/tests/src/core/testqgsexpression.cpp @@ -1355,6 +1355,55 @@ class TestQgsExpression: public QObject QTest::newRow( "group by expression" ) << "sum(\"col1\", \"col1\" % 2)" << false << QVariant( 16 ); } + void selection() + { + QFETCH( QgsFeatureIds, selectedFeatures ); + QFETCH( QString, expression ); + QFETCH( QVariant, result ); + QFETCH( QgsFeature, feature ); + QFETCH( QgsVectorLayer*, layer ); + + QgsExpressionContext context; + if ( layer ) + context.appendScope( QgsExpressionContextUtils::layerScope( layer ) ); + + QgsFeatureIds backupSelection = mMemoryLayer->selectedFeaturesIds(); + context.setFeature( feature ); + + mMemoryLayer->selectByIds( selectedFeatures ); + + QgsExpression exp( expression ); + QCOMPARE( exp.parserErrorString(), QString( "" ) ); + exp.prepare( &context ); + QVariant res = exp.evaluate( &context ); + QCOMPARE( res, result ); + + mMemoryLayer->selectByIds( backupSelection ); + } + + void selection_data() + { + QTest::addColumn( "expression" ); + QTest::addColumn( "selectedFeatures" ); + QTest::addColumn( "feature" ); + QTest::addColumn( "layer" ); + QTest::addColumn( "result" ); + + QgsFeature firstFeature = mMemoryLayer->getFeature( 1 ); + QgsVectorLayer* noLayer = nullptr; + + QTest::newRow( "empty selection num_selected" ) << "num_selected()" << QgsFeatureIds() << firstFeature << mMemoryLayer << QVariant( 0 ); + QTest::newRow( "empty selection is_selected" ) << "is_selected()" << QgsFeatureIds() << firstFeature << mMemoryLayer << QVariant( false ); + QTest::newRow( "two_selected" ) << "num_selected()" << ( QgsFeatureIds() << 1 << 2 ) << firstFeature << mMemoryLayer << QVariant( 2 ); + QTest::newRow( "is_selected" ) << "is_selected()" << ( QgsFeatureIds() << 1 << 2 ) << firstFeature << mMemoryLayer << QVariant( true ); + QTest::newRow( "not_selected" ) << "is_selected()" << ( QgsFeatureIds() << 4 << 2 ) << firstFeature << mMemoryLayer << QVariant( false ); + QTest::newRow( "no layer num_selected" ) << "num_selected()" << ( QgsFeatureIds() << 4 << 2 ) << QgsFeature() << noLayer << QVariant( QVariant::LongLong ); + QTest::newRow( "no layer is_selected" ) << "is_selected()" << ( QgsFeatureIds() << 4 << 2 ) << QgsFeature() << noLayer << QVariant( QVariant::Bool ); + QTest::newRow( "no layer num_selected" ) << "num_selected()" << ( QgsFeatureIds() << 4 << 2 ) << QgsFeature() << noLayer << QVariant( QVariant::LongLong ); + QTest::newRow( "is_selected with params" ) << "is_selected('test', get_feature('test', 'col1', 10))" << ( QgsFeatureIds() << 4 << 2 ) << QgsFeature() << noLayer << QVariant( QVariant::Bool ); + QTest::newRow( "num_selected with params" ) << "num_selected('test')" << ( QgsFeatureIds() << 4 << 2 ) << QgsFeature() << noLayer << QVariant( 2 ); + } + void layerAggregates() { QgsExpressionContext context;