Merge pull request #6982 from elpaso/currentformfeature-expressions-4

[feature][need-docs] Current feature/current value form context expressions
This commit is contained in:
Alessandro Pasotti 2018-05-16 09:48:06 +02:00 committed by GitHub
commit a44eeae441
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 1072 additions and 98 deletions

View File

@ -178,6 +178,14 @@ which is determined at runtime.
.. versionadded:: 3.0
%End
QSet<QString> referencedFunctions() const;
%Docstring
Return a list of the names of all functions which are used in this expression.
.. versionadded:: 3.2
%End
QSet<int> referencedAttributeIndexes( const QgsFields &fields ) const;
%Docstring
Return a list of field name indexes obtained from the provided fields.

View File

@ -196,6 +196,11 @@ to evaluate child nodes.
virtual QSet<QString> referencedVariables() const = 0;
%Docstring
Return a set of all variables which are used in this expression.
%End
virtual QSet<QString> referencedFunctions() const = 0;
%Docstring
Return a set of all functions which are used in this expression.
%End
virtual bool needsGeometry() const = 0;

View File

@ -55,7 +55,7 @@ Returns the node the operator will operate upon.
virtual QSet<QString> referencedVariables() const;
virtual bool needsGeometry() const;
virtual QSet<QString> referencedFunctions() const;
virtual QgsExpressionNode *clone() const /Factory/;
@ -154,6 +154,8 @@ Returns the node to the right of the operator.
virtual QSet<QString> referencedVariables() const;
virtual QSet<QString> referencedFunctions() const;
virtual bool needsGeometry() const;
virtual QgsExpressionNode *clone() const /Factory/;
@ -224,7 +226,7 @@ Returns the list of nodes to search for matching values within.
virtual QSet<QString> referencedVariables() const;
virtual bool needsGeometry() const;
virtual QSet<QString> referencedFunctions() const;
virtual QgsExpressionNode *clone() const /Factory/;
@ -275,7 +277,8 @@ Returns a list of arguments specified for the function.
virtual QSet<QString> referencedVariables() const;
virtual bool needsGeometry() const;
virtual QSet<QString> referencedFunctions() const;
virtual QgsExpressionNode *clone() const /Factory/;
@ -323,7 +326,8 @@ The value of the literal.
virtual QSet<QString> referencedVariables() const;
virtual bool needsGeometry() const;
virtual QSet<QString> referencedFunctions() const;
virtual QgsExpressionNode *clone() const /Factory/;
@ -367,6 +371,8 @@ The name of the column.
virtual QSet<QString> referencedVariables() const;
virtual QSet<QString> referencedFunctions() const;
virtual bool needsGeometry() const;
@ -466,6 +472,9 @@ The ELSE expression used for the condition.
virtual QSet<QString> referencedVariables() const;
virtual QSet<QString> referencedFunctions() const;
virtual bool needsGeometry() const;
virtual QgsExpressionNode *clone() const /Factory/;

View File

@ -8,6 +8,7 @@
class QgsValueRelationFieldFormatter : QgsFieldFormatter
{
%Docstring
@ -56,25 +57,70 @@ Constructor for QgsValueRelationFieldFormatter.
virtual QVariant createCache( QgsVectorLayer *layer, int fieldIndex, const QVariantMap &config ) const;
static QgsValueRelationFieldFormatter::ValueRelationCache createCache( const QVariantMap &config );
static QStringList valueToStringList( const QVariant &value );
%Docstring
Utility to convert an array or a string representation of an array ``value`` to a string list
.. versionadded:: 3.2
%End
static QgsValueRelationFieldFormatter::ValueRelationCache createCache( const QVariantMap &config, const QgsFeature &formFeature = QgsFeature() );
%Docstring
Create a cache for a value relation field.
This can be used to keep the value map in the local memory
if doing multiple lookups in a loop.
:param config: The widget configuration
:param formFeature: The feature currently being edited with current attribute values
:return: A kvp list of values for the widget
.. versionadded:: 3.0
%End
static QStringList valueToStringList( const QVariant &value );
static bool expressionRequiresFormScope( const QString &expression );
%Docstring
Utility to convert an array or a string representation of and array ``value`` to a string list
Check if the ``expression`` requires a form scope (i.e. if it uses fields
or geometry of the currently edited feature).
:param value: The value to be converted
:param expression: The widget's filter expression
:return: A string list
:return: true if the expression requires a form scope
.. versionadded:: 3.2
%End
static QSet<QString> expressionFormAttributes( const QString &expression );
%Docstring
Return a list of attributes required by the form context ``expression``
:param expression: Form filter expression
:return: list of attributes required by the expression
.. versionadded:: 3.2
%End
static QSet<QString> expressionFormVariables( const QString &expression );
%Docstring
Return a list of variables required by the form context ``expression``
:param expression: Form filter expression
:return: list of variables required by the expression
.. versionadded:: 3.2
%End
static bool expressionIsUsable( const QString &expression, const QgsFeature &feature );
%Docstring
Check whether the ``feature`` has all values required by the ``expression``
:return: True if the expression can be used
.. versionadded:: 3.2
%End
};

View File

@ -763,6 +763,14 @@ Creates a new scope which contains variables and functions relating to the globa
For instance, QGIS version numbers and variables specified through QGIS options.
.. seealso:: :py:func:`setGlobalVariable`
%End
static QgsExpressionContextScope *formScope( const QgsFeature &formFeature = QgsFeature( ) ) /Factory/;
%Docstring
Creates a new scope which contains functions and variables from the current attribute form/table feature.
The variables and values in this scope will reflect the current state of the form/row being edited.
.. versionadded:: 3.2
%End
static void setGlobalVariable( const QString &name, const QVariant &value );

View File

@ -213,6 +213,7 @@ This will be disabled when the form is not editable.
.. versionadded:: 3.0
%End
signals:
void valueChanged( const QVariant &value );
@ -279,6 +280,34 @@ change the visual cue.
.. versionadded:: 2.16
%End
QgsFeature formFeature() const;
%Docstring
The feature currently being edited, in its current state
:return: the feature currently being edited, in its current state
.. versionadded:: 3.2
%End
void setFormFeature( const QgsFeature &feature );
%Docstring
Set the feature currently being edited to ``feature``
.. versionadded:: 3.2
%End
bool setFormFeatureAttribute( const QString &attributeName, const QVariant &attributeValue );
%Docstring
Update the feature currently being edited by changing its
attribute ``attributeName`` to ``attributeValue``
:return: bool true on success
.. versionadded:: 3.2
%End
};

View File

@ -180,6 +180,25 @@ QGIS forms
const QgsAttributeEditorContext *parentContext() const;
QgsFeature formFeature() const;
%Docstring
Return current feature from the currently edited form or table row
.. seealso:: :py:func:`setFormFeature`
.. versionadded:: 3.2
%End
void setFormFeature( const QgsFeature &feature );
%Docstring
Set current ``feature`` for the currently edited form or table row
.. seealso:: :py:func:`formFeature`
.. versionadded:: 3.2
%End
};
/************************************************************************

View File

@ -141,7 +141,8 @@ on all attribute widgets.
void attributeChanged( const QString &attribute, const QVariant &value ) /Deprecated/;
%Docstring
Notifies about changes of attributes
Notifies about changes of attributes, this signal is not emitted when the value is set
back to the original one.
:param attribute: The name of the attribute that changed.
:param value: The new value of the attribute.

View File

@ -0,0 +1,7 @@
{
"name": "current_value",
"type": "function",
"description": "Returns the current, unsaved value of a field in the form or table row currently being edited. This will differ from the feature's actual attribute values for features which are currently being edited or have not yet been added to a layer.",
"arguments": [ {"arg":"field_name","description":"a field name in the current form or table row"}],
"examples": [ { "expression":"current_value( 'FIELD_NAME' )","returns":"The current value of field 'FIELD_NAME'."} ]
}

View File

@ -276,6 +276,14 @@ QSet<QString> QgsExpression::referencedVariables() const
return d->mRootNode->referencedVariables();
}
QSet<QString> QgsExpression::referencedFunctions() const
{
if ( !d->mRootNode )
return QSet<QString>();
return d->mRootNode->referencedFunctions();
}
QSet<int> QgsExpression::referencedAttributeIndexes( const QgsFields &fields ) const
{
if ( !d->mRootNode )
@ -771,6 +779,10 @@ void QgsExpression::initVariableHelp()
//provider notification
sVariableHelpTexts.insert( QStringLiteral( "notification_message" ), QCoreApplication::translate( "notification_message", "Content of the notification message sent by the provider (available only for actions triggered by provider notifications)." ) );
//form context variable
sVariableHelpTexts.insert( QStringLiteral( "current_geometry" ), QCoreApplication::translate( "current_geometry", "Represents the geometry of the feature currently being edited in the form or the table row. Can be used for in a form/row context to filter the related features." ) );
sVariableHelpTexts.insert( QStringLiteral( "current_feature" ), QCoreApplication::translate( "current_feature", "Represents the feature currently being edited in the form or the table row. Can be used for in a form/row context to filter the related features." ) );
}
QString QgsExpression::variableHelpText( const QString &variableName )
@ -945,3 +957,14 @@ bool QgsExpression::isField() const
{
return d->mRootNode && d->mRootNode->nodeType() == QgsExpressionNode::ntColumnRef;
}
QList<const QgsExpressionNode *> QgsExpression::nodes() const
{
if ( !d->mRootNode )
return QList<const QgsExpressionNode *>();
return d->mRootNode->nodes();
}

View File

@ -29,6 +29,7 @@
#include "qgis.h"
#include "qgsunittypes.h"
#include "qgsinterval.h"
#include "qgsexpressionnode.h"
class QgsFeature;
class QgsGeometry;
@ -41,7 +42,6 @@ class QgsDistanceArea;
class QDomElement;
class QgsExpressionContext;
class QgsExpressionPrivate;
class QgsExpressionNode;
class QgsExpressionFunction;
/**
@ -258,6 +258,44 @@ class CORE_EXPORT QgsExpression
*/
QSet<QString> referencedVariables() const;
/**
* Return a list of the names of all functions which are used in this expression.
*
* \since QGIS 3.2
*/
QSet<QString> referencedFunctions() const;
#ifndef SIP_RUN
/**
* Return a list of all nodes which are used in this expression
*
* \note not available in Python bindings
* \since QGIS 3.2
*/
QList<const QgsExpressionNode *> nodes( ) const;
/**
* Return a list of all nodes of the given class which are used in this expression
*
* \note not available in Python bindings
* \since QGIS 3.2
*/
template <class T>
QList<const T *> findNodes( ) const
{
QList<const T *> lst;
const QList<const QgsExpressionNode *> allNodes( nodes() );
for ( const auto &node : allNodes )
{
const T *n = dynamic_cast<const T *>( node );
if ( n )
lst << n;
}
return lst;
}
#endif
/**
* Return a list of field name indexes obtained from the provided fields.
*

View File

@ -57,3 +57,4 @@ void QgsExpressionNode::cloneTo( QgsExpressionNode *target ) const
target->parserFirstColumn = parserFirstColumn;
target->parserFirstLine = parserFirstLine;
}

View File

@ -222,6 +222,19 @@ class CORE_EXPORT QgsExpressionNode SIP_ABSTRACT
*/
virtual QSet<QString> referencedVariables() const = 0;
/**
* Return a set of all functions which are used in this expression.
*/
virtual QSet<QString> referencedFunctions() const = 0;
/**
* Return a list of all nodes which are used in this expression.
*
* \note not available in Python bindings
* \since QGIS 3.2
*/
virtual QList<const QgsExpressionNode *> nodes( ) const = 0; SIP_SKIP
/**
* Abstract virtual method which returns if the geometry is required to evaluate
* this expression.

View File

@ -137,6 +137,19 @@ QSet<QString> QgsExpressionNodeUnaryOperator::referencedVariables() const
return mOperand->referencedVariables();
}
QSet<QString> QgsExpressionNodeUnaryOperator::referencedFunctions() const
{
return mOperand->referencedFunctions();
}
QList<const QgsExpressionNode *> QgsExpressionNodeUnaryOperator::nodes() const
{
QList<const QgsExpressionNode *> lst;
lst.append( this );
lst += mOperand->nodes();
return lst;
}
bool QgsExpressionNodeUnaryOperator::needsGeometry() const
{
return mOperand->needsGeometry();
@ -720,6 +733,19 @@ QSet<QString> QgsExpressionNodeBinaryOperator::referencedVariables() const
return mOpLeft->referencedVariables() + mOpRight->referencedVariables();
}
QSet<QString> QgsExpressionNodeBinaryOperator::referencedFunctions() const
{
return mOpLeft->referencedFunctions() + mOpRight->referencedFunctions();
}
QList<const QgsExpressionNode *> QgsExpressionNodeBinaryOperator::nodes() const
{
QList<const QgsExpressionNode *> lst;
lst << this;
lst += mOpLeft->nodes() + mOpRight->nodes();
return lst;
}
bool QgsExpressionNodeBinaryOperator::needsGeometry() const
{
return mOpLeft->needsGeometry() || mOpRight->needsGeometry();
@ -978,6 +1004,38 @@ QSet<QString> QgsExpressionNodeFunction::referencedVariables() const
}
}
QSet<QString> QgsExpressionNodeFunction::referencedFunctions() const
{
QgsExpressionFunction *fd = QgsExpression::QgsExpression::Functions()[mFnIndex];
QSet<QString> functions = QSet<QString>();
functions.insert( fd->name() );
if ( !mArgs )
return functions;
const QList< QgsExpressionNode * > nodeList = mArgs->list();
for ( QgsExpressionNode *n : nodeList )
{
functions.unite( n->referencedFunctions() );
}
return functions;
}
QList<const QgsExpressionNode *> QgsExpressionNodeFunction::nodes() const
{
QList<const QgsExpressionNode *> lst;
lst << this;
if ( !mArgs )
return lst;
const QList< QgsExpressionNode * > nodeList = mArgs->list();
for ( QgsExpressionNode *n : nodeList )
{
lst += n->nodes();
}
return lst;
}
bool QgsExpressionNodeFunction::needsGeometry() const
{
bool needs = QgsExpression::QgsExpression::Functions()[mFnIndex]->usesGeometry( this );
@ -1128,6 +1186,18 @@ QSet<QString> QgsExpressionNodeLiteral::referencedVariables() const
return QSet<QString>();
}
QSet<QString> QgsExpressionNodeLiteral::referencedFunctions() const
{
return QSet<QString>();
}
QList<const QgsExpressionNode *> QgsExpressionNodeLiteral::nodes() const
{
QList<const QgsExpressionNode *> lst;
lst << this;
return lst;
}
bool QgsExpressionNodeLiteral::needsGeometry() const
{
return false;
@ -1215,6 +1285,18 @@ QSet<QString> QgsExpressionNodeColumnRef::referencedVariables() const
return QSet<QString>();
}
QSet<QString> QgsExpressionNodeColumnRef::referencedFunctions() const
{
return QSet<QString>();
}
QList<const QgsExpressionNode *> QgsExpressionNodeColumnRef::nodes() const
{
QList<const QgsExpressionNode *> result;
result << this;
return result;
}
bool QgsExpressionNodeColumnRef::needsGeometry() const
{
return false;
@ -1338,6 +1420,35 @@ QSet<QString> QgsExpressionNodeCondition::referencedVariables() const
return lst;
}
QSet<QString> QgsExpressionNodeCondition::referencedFunctions() const
{
QSet<QString> lst;
for ( WhenThen *cond : mConditions )
{
lst += cond->mWhenExp->referencedFunctions() + cond->mThenExp->referencedFunctions();
}
if ( mElseExp )
lst += mElseExp->referencedFunctions();
return lst;
}
QList<const QgsExpressionNode *> QgsExpressionNodeCondition::nodes() const
{
QList<const QgsExpressionNode *> lst;
lst << this;
for ( WhenThen *cond : mConditions )
{
lst += cond->mWhenExp->nodes() + cond->mThenExp->nodes();
}
if ( mElseExp )
lst += mElseExp->nodes();
return lst;
}
bool QgsExpressionNodeCondition::needsGeometry() const
{
for ( WhenThen *cond : mConditions )
@ -1393,6 +1504,25 @@ QSet<QString> QgsExpressionNodeInOperator::referencedVariables() const
return lst;
}
QSet<QString> QgsExpressionNodeInOperator::referencedFunctions() const
{
QSet<QString> lst( mNode->referencedFunctions() );
const QList< QgsExpressionNode * > nodeList = mList->list();
for ( const QgsExpressionNode *n : nodeList )
lst.unite( n->referencedFunctions() );
return lst;
}
QList<const QgsExpressionNode *> QgsExpressionNodeInOperator::nodes() const
{
QList<const QgsExpressionNode *> lst;
lst << this;
const QList< QgsExpressionNode * > nodeList = mList->list();
for ( const QgsExpressionNode *n : nodeList )
lst += n->nodes();
return lst;
}
QgsExpressionNodeCondition::WhenThen::WhenThen( QgsExpressionNode *whenExp, QgsExpressionNode *thenExp )
: mWhenExp( whenExp )
, mThenExp( thenExp )
@ -1414,3 +1544,4 @@ QString QgsExpressionNodeBinaryOperator::text() const
{
return BINARY_OPERATOR_TEXT[mOp];
}

View File

@ -64,6 +64,8 @@ class CORE_EXPORT QgsExpressionNodeUnaryOperator : public QgsExpressionNode
QSet<QString> referencedColumns() const override;
QSet<QString> referencedVariables() const override;
QSet<QString> referencedFunctions() const override;
QList<const QgsExpressionNode *> nodes() const override; SIP_SKIP
bool needsGeometry() const override;
QgsExpressionNode *clone() const override SIP_FACTORY;
@ -162,6 +164,9 @@ class CORE_EXPORT QgsExpressionNodeBinaryOperator : public QgsExpressionNode
QSet<QString> referencedColumns() const override;
QSet<QString> referencedVariables() const override;
QSet<QString> referencedFunctions() const override;
QList<const QgsExpressionNode *> nodes( ) const override; SIP_SKIP
bool needsGeometry() const override;
QgsExpressionNode *clone() const override SIP_FACTORY;
bool isStatic( QgsExpression *parent, const QgsExpressionContext *context ) const override;
@ -241,6 +246,8 @@ class CORE_EXPORT QgsExpressionNodeInOperator : public QgsExpressionNode
QSet<QString> referencedColumns() const override;
QSet<QString> referencedVariables() const override;
QSet<QString> referencedFunctions() const override;
QList<const QgsExpressionNode *> nodes() const override; SIP_SKIP
bool needsGeometry() const override;
QgsExpressionNode *clone() const override SIP_FACTORY;
bool isStatic( QgsExpression *parent, const QgsExpressionContext *context ) const override;
@ -284,6 +291,9 @@ class CORE_EXPORT QgsExpressionNodeFunction : public QgsExpressionNode
QSet<QString> referencedColumns() const override;
QSet<QString> referencedVariables() const override;
QSet<QString> referencedFunctions() const override;
QList<const QgsExpressionNode *> nodes() const override; SIP_SKIP
bool needsGeometry() const override;
QgsExpressionNode *clone() const override SIP_FACTORY;
bool isStatic( QgsExpression *parent, const QgsExpressionContext *context ) const override;
@ -321,6 +331,9 @@ class CORE_EXPORT QgsExpressionNodeLiteral : public QgsExpressionNode
QSet<QString> referencedColumns() const override;
QSet<QString> referencedVariables() const override;
QSet<QString> referencedFunctions() const override;
QList<const QgsExpressionNode *> nodes() const override; SIP_SKIP
bool needsGeometry() const override;
QgsExpressionNode *clone() const override SIP_FACTORY;
bool isStatic( QgsExpression *parent, const QgsExpressionContext *context ) const override;
@ -356,6 +369,9 @@ class CORE_EXPORT QgsExpressionNodeColumnRef : public QgsExpressionNode
QSet<QString> referencedColumns() const override;
QSet<QString> referencedVariables() const override;
QSet<QString> referencedFunctions() const override;
QList<const QgsExpressionNode *> nodes( ) const override; SIP_SKIP
bool needsGeometry() const override;
QgsExpressionNode *clone() const override SIP_FACTORY;
@ -456,6 +472,10 @@ class CORE_EXPORT QgsExpressionNodeCondition : public QgsExpressionNode
QSet<QString> referencedColumns() const override;
QSet<QString> referencedVariables() const override;
QSet<QString> referencedFunctions() const override;
QList<const QgsExpressionNode *> nodes() const override; SIP_SKIP
bool needsGeometry() const override;
QgsExpressionNode *clone() const override SIP_FACTORY;
bool isStatic( QgsExpression *parent, const QgsExpressionContext *context ) const override;

View File

@ -18,6 +18,7 @@
#include "qgis.h"
#include "qgsproject.h"
#include "qgsvectorlayer.h"
#include "qgsexpressionnodeimpl.h"
#include <QSettings>
@ -99,7 +100,7 @@ QVariant QgsValueRelationFieldFormatter::createCache( QgsVectorLayer *layer, int
}
QgsValueRelationFieldFormatter::ValueRelationCache QgsValueRelationFieldFormatter::createCache( const QVariantMap &config )
QgsValueRelationFieldFormatter::ValueRelationCache QgsValueRelationFieldFormatter::createCache( const QVariantMap &config, const QgsFeature &formFeature )
{
ValueRelationCache cache;
@ -116,11 +117,19 @@ QgsValueRelationFieldFormatter::ValueRelationCache QgsValueRelationFieldFormatte
request.setFlags( QgsFeatureRequest::NoGeometry );
request.setSubsetOfAttributes( QgsAttributeList() << ki << vi );
if ( !config.value( QStringLiteral( "FilterExpression" ) ).toString().isEmpty() )
const QString expression = config.value( QStringLiteral( "FilterExpression" ) ).toString();
// Skip the filter and build a full cache if the form scope is required and the feature
// is not valid or the attributes required for the filter have no valid value
if ( ! expression.isEmpty() && ( ! expressionRequiresFormScope( expression )
|| expressionIsUsable( expression, formFeature ) ) )
{
QgsExpressionContext context( QgsExpressionContextUtils::globalProjectLayerScopes( layer ) );
if ( formFeature.isValid( ) )
context.appendScope( QgsExpressionContextUtils::formScope( formFeature ) );
request.setExpressionContext( context );
request.setFilterExpression( config.value( QStringLiteral( "FilterExpression" ) ).toString() );
request.setFilterExpression( expression );
}
QgsFeatureIterator fit = layer->getFeatures( request );
@ -162,3 +171,56 @@ QStringList QgsValueRelationFieldFormatter::valueToStringList( const QVariant &v
}
return checkList;
}
QSet<QString> QgsValueRelationFieldFormatter::expressionFormVariables( const QString &expression )
{
std::unique_ptr< QgsExpressionContextScope > scope( QgsExpressionContextUtils::formScope() );
QSet< QString > formVariables = scope->variableNames().toSet();
const QSet< QString > usedVariables = QgsExpression( expression ).referencedVariables();
formVariables.intersect( usedVariables );
return formVariables;
}
bool QgsValueRelationFieldFormatter::expressionRequiresFormScope( const QString &expression )
{
return !( expressionFormAttributes( expression ).isEmpty() && expressionFormVariables( expression ).isEmpty() );
}
QSet<QString> QgsValueRelationFieldFormatter::expressionFormAttributes( const QString &expression )
{
QSet<QString> attributes;
QgsExpression exp( expression );
std::unique_ptr< QgsExpressionContextScope > scope( QgsExpressionContextUtils::formScope() );
// List of form function names used in the expression
const QSet<QString> formFunctions( scope->functionNames()
.toSet()
.intersect( exp.referencedFunctions( ) ) );
const QList<const QgsExpressionNodeFunction *> expFunctions( exp.findNodes<QgsExpressionNodeFunction>() );
const QgsExpressionContext context;
for ( const auto &f : expFunctions )
{
QgsExpressionFunction *fd = QgsExpression::QgsExpression::Functions()[f->fnIndex()];
if ( formFunctions.contains( fd->name( ) ) )
{
for ( const auto &param : f->args( )->list() )
{
attributes.insert( param->eval( &exp, &context ).toString() );
}
}
}
return attributes;
}
bool QgsValueRelationFieldFormatter::expressionIsUsable( const QString &expression, const QgsFeature &feature )
{
const QSet<QString> attrs = expressionFormAttributes( expression );
for ( auto it = attrs.constBegin() ; it != attrs.constEnd(); it++ )
{
if ( ! feature.attribute( *it ).isValid() )
return false;
}
if ( ! expressionFormVariables( expression ).isEmpty() && feature.geometry().isEmpty( ) )
return false;
return true;
}

View File

@ -18,10 +18,13 @@
#include "qgis_core.h"
#include "qgsfieldformatter.h"
#include "qgsexpression.h"
#include "qgsexpressioncontext.h"
#include <QVector>
#include <QVariant>
/**
* \ingroup core
* Field formatter for a value relation field.
@ -63,22 +66,59 @@ class CORE_EXPORT QgsValueRelationFieldFormatter : public QgsFieldFormatter
QVariant createCache( QgsVectorLayer *layer, int fieldIndex, const QVariantMap &config ) const override;
/**
* Create a cache for a value relation field.
* This can be used to keep the value map in the local memory
* if doing multiple lookups in a loop.
*
* \since QGIS 3.0
*/
static QgsValueRelationFieldFormatter::ValueRelationCache createCache( const QVariantMap &config );
/**
* Utility to convert an array or a string representation of and array \a value to a string list
*
* \param value The value to be converted
* \return A string list
* Utility to convert an array or a string representation of an array \a value to a string list
* \since QGIS 3.2
*/
static QStringList valueToStringList( const QVariant &value );
/**
* Create a cache for a value relation field.
* This can be used to keep the value map in the local memory
* if doing multiple lookups in a loop.
* \param config The widget configuration
* \param formFeature The feature currently being edited with current attribute values
* \return A kvp list of values for the widget
*
* \since QGIS 3.0
*/
static QgsValueRelationFieldFormatter::ValueRelationCache createCache( const QVariantMap &config, const QgsFeature &formFeature = QgsFeature() );
/**
* Check if the \a expression requires a form scope (i.e. if it uses fields
* or geometry of the currently edited feature).
*
* \param expression The widget's filter expression
* \return true if the expression requires a form scope
* \since QGIS 3.2
*/
static bool expressionRequiresFormScope( const QString &expression );
/**
* Return a list of attributes required by the form context \a expression
*
* \param expression Form filter expression
* \return list of attributes required by the expression
* \since QGIS 3.2
*/
static QSet<QString> expressionFormAttributes( const QString &expression );
/**
* Return a list of variables required by the form context \a expression
*
* \param expression Form filter expression
* \return list of variables required by the expression
* \since QGIS 3.2
*/
static QSet<QString> expressionFormVariables( const QString &expression );
/**
* Check whether the \a feature has all values required by the \a expression
*
* \return True if the expression can be used
* \since QGIS 3.2
*/
static bool expressionIsUsable( const QString &expression, const QgsFeature &feature );
};
Q_DECLARE_METATYPE( QgsValueRelationFieldFormatter::ValueRelationCache )

View File

@ -736,6 +736,33 @@ class GetLayerVisibility : public QgsScopedExpressionFunction
};
class GetCurrentFormFieldValue : public QgsScopedExpressionFunction
{
public:
GetCurrentFormFieldValue( )
: QgsScopedExpressionFunction( QStringLiteral( "current_value" ), QgsExpressionFunction::ParameterList() << QStringLiteral( "field_name" ), QStringLiteral( "Form" ) )
{}
QVariant func( const QVariantList &values, const QgsExpressionContext *context, QgsExpression *, const QgsExpressionNodeFunction * ) override
{
QString fieldName( values.at( 0 ).toString() );
const QgsFeature feat( context->variable( QStringLiteral( "current_feature" ) ).value<QgsFeature>() );
if ( fieldName.isEmpty() || ! feat.isValid( ) )
{
return QVariant();
}
return feat.attribute( fieldName ) ;
}
QgsScopedExpressionFunction *clone() const override
{
return new GetCurrentFormFieldValue( );
}
};
class GetProcessingParameterValue : public QgsScopedExpressionFunction
{
public:
@ -762,6 +789,16 @@ class GetProcessingParameterValue : public QgsScopedExpressionFunction
///@endcond
QgsExpressionContextScope *QgsExpressionContextUtils::formScope( const QgsFeature &formFeature )
{
QgsExpressionContextScope *scope = new QgsExpressionContextScope( QObject::tr( "Form" ) );
scope->addFunction( QStringLiteral( "current_value" ), new GetCurrentFormFieldValue( ) );
scope->setVariable( QStringLiteral( "current_geometry" ), formFeature.geometry( ), true );
scope->setVariable( QStringLiteral( "current_feature" ), formFeature, true );
return scope;
}
QgsExpressionContextScope *QgsExpressionContextUtils::projectScope( const QgsProject *project )
{
QgsExpressionContextScope *scope = new QgsExpressionContextScope( QObject::tr( "Project" ) );
@ -900,6 +937,7 @@ QList<QgsExpressionContextScope *> QgsExpressionContextUtils::globalProjectLayer
return scopes;
}
void QgsExpressionContextUtils::setLayerVariable( QgsMapLayer *layer, const QString &name, const QVariant &value )
{
if ( !layer )
@ -1256,6 +1294,7 @@ void QgsExpressionContextUtils::registerContextFunctions()
QgsExpression::registerFunction( new GetLayoutItemVariables( nullptr ) );
QgsExpression::registerFunction( new GetLayerVisibility( QList<QgsMapLayer *>() ) );
QgsExpression::registerFunction( new GetProcessingParameterValue( QVariantMap() ) );
QgsExpression::registerFunction( new GetCurrentFormFieldValue( ) );
}
bool QgsScopedExpressionFunction::usesGeometry( const QgsExpressionNodeFunction *node ) const

View File

@ -739,6 +739,13 @@ class CORE_EXPORT QgsExpressionContextUtils
*/
static QgsExpressionContextScope *globalScope() SIP_FACTORY;
/**
* Creates a new scope which contains functions and variables from the current attribute form/table feature.
* The variables and values in this scope will reflect the current state of the form/row being edited.
* \since QGIS 3.2
*/
static QgsExpressionContextScope *formScope( const QgsFeature &formFeature = QgsFeature( ) ) SIP_FACTORY;
/**
* Sets a global context variable. This variable will be contained within scopes retrieved via
* globalScope().
@ -823,14 +830,14 @@ class CORE_EXPORT QgsExpressionContextUtils
static QList<QgsExpressionContextScope *> globalProjectLayerScopes( const QgsMapLayer *layer ) SIP_FACTORY;
/**
* Sets a layer context variable. This variable will be contained within scopes retrieved via
* layerScope().
* \param layer map layer
* \param name variable name
* \param value variable value
* \see setLayerVariables()
* \see layerScope()
*/
* Sets a layer context variable. This variable will be contained within scopes retrieved via
* layerScope().
* \param layer map layer
* \param name variable name
* \param value variable value
* \see setLayerVariables()
* \see layerScope()
*/
static void setLayerVariable( QgsMapLayer *layer, const QString &name, const QVariant &value );
/**

View File

@ -67,8 +67,12 @@ QWidget *QgsAttributeTableDelegate::createEditor( QWidget *parent, const QStyleO
return nullptr;
int fieldIdx = index.model()->data( index, QgsAttributeTableModel::FieldIndexRole ).toInt();
QgsAttributeEditorContext context( masterModel( index.model() )->editorContext(), QgsAttributeEditorContext::Popup );
// Update the editor form context with the feature being edited
QgsFeatureId fid( index.model()->data( index, QgsAttributeTableModel::FeatureIdRole ).toLongLong() );
context.setFormFeature( vl->getFeature( fid ) );
QgsEditorWidgetWrapper *eww = QgsGui::editorWidgetRegistry()->create( vl, fieldIdx, nullptr, parent, context );
QWidget *w = eww->widget();

View File

@ -346,7 +346,7 @@ class GUI_EXPORT QgsAttributeTableModel: public QAbstractTableModel
mutable QgsExpressionContext mExpressionContext;
/**
* Gets mFieldCount, mAttributes and mValueMaps
* Gets mFieldCount, mAttributes
*/
virtual void loadAttributes();

View File

@ -25,9 +25,9 @@
QgsEditorWidgetWrapper::QgsEditorWidgetWrapper( QgsVectorLayer *vl, int fieldIdx, QWidget *editor, QWidget *parent )
: QgsWidgetWrapper( vl, editor, parent )
, mFieldIdx( fieldIdx )
, mValidConstraint( true )
, mIsBlockingCommit( false )
, mFieldIdx( fieldIdx )
{
}
@ -68,7 +68,7 @@ void QgsEditorWidgetWrapper::setEnabled( bool enabled )
void QgsEditorWidgetWrapper::setFeature( const QgsFeature &feature )
{
mFeature = feature;
mFormFeature = feature;
setValue( feature.attribute( mFieldIdx ) );
}
@ -102,6 +102,11 @@ void QgsEditorWidgetWrapper::updateConstraintWidgetStatus()
}
}
bool QgsEditorWidgetWrapper::setFormFeatureAttribute( const QString &attributeName, const QVariant &attributeValue )
{
return mFormFeature.setAttribute( attributeName, attributeValue );
}
QgsEditorWidgetWrapper::ConstraintResult QgsEditorWidgetWrapper::constraintResult() const
{
return mConstraintResult;

View File

@ -213,6 +213,7 @@ class GUI_EXPORT QgsEditorWidgetWrapper : public QgsWidgetWrapper
*/
void setConstraintResultVisible( bool constraintResultVisible );
signals:
/**
@ -277,8 +278,44 @@ class GUI_EXPORT QgsEditorWidgetWrapper : public QgsWidgetWrapper
*/
virtual void updateConstraintWidgetStatus();
/**
* The feature currently being edited, in its current state
*
* \return the feature currently being edited, in its current state
* \since QGIS 3.2
*/
QgsFeature formFeature() const { return mFormFeature; }
/**
* Set the feature currently being edited to \a feature
*
* \since QGIS 3.2
*/
void setFormFeature( const QgsFeature &feature ) { mFormFeature = feature; }
/**
* Update the feature currently being edited by changing its
* attribute \a attributeName to \a attributeValue
*
* \return bool true on success
* \since QGIS 3.2
*/
bool setFormFeatureAttribute( const QString &attributeName, const QVariant &attributeValue );
private:
/**
* mFieldIdx the widget feature field id
*/
int mFieldIdx = -1;
/**
* The feature currently being edited, in its current state
*/
QgsFeature mFormFeature;
/**
* Boolean storing the current validity of the constraint for this widget.
*/
@ -296,8 +333,6 @@ class GUI_EXPORT QgsEditorWidgetWrapper : public QgsWidgetWrapper
//! The current constraint result
bool mConstraintResultVisible = false;
int mFieldIdx;
QgsFeature mFeature;
mutable QVariant mDefaultValue; // Cache default value, we don't want to retrieve different serial numbers if called repeatedly
};

View File

@ -92,6 +92,7 @@ void QgsValueRelationConfigDlg::editExpression()
return;
QgsExpressionContext context( QgsExpressionContextUtils::globalProjectLayerScopes( vl ) );
context << QgsExpressionContextUtils::formScope( );
QgsExpressionBuilderDialog dlg( vl, mFilterExpression->toPlainText(), this, QStringLiteral( "generic" ), context );
dlg.setWindowTitle( tr( "Edit Filter Expression" ) );

View File

@ -161,7 +161,6 @@ void QgsValueRelationSearchWidgetWrapper::onValueChanged()
}
else
{
QgsSettings settings;
setExpression( vl.isNull() ? QgsApplication::nullRepresentation() : vl.toString() );
emit valueChanged();
}

View File

@ -23,6 +23,8 @@
#include "qgsfilterlineedit.h"
#include "qgsfeatureiterator.h"
#include "qgsvaluerelationfieldformatter.h"
#include "qgsattributeform.h"
#include "qgsattributes.h"
#include <QHeaderView>
#include <QComboBox>
@ -70,7 +72,7 @@ QVariant QgsValueRelationWidgetWrapper::value() const
if ( mLineEdit )
{
Q_FOREACH ( const QgsValueRelationFieldFormatter::ValueRelationItem &item, mCache )
for ( const QgsValueRelationFieldFormatter::ValueRelationItem &item : qgis::as_const( mCache ) )
{
if ( item.value == mLineEdit->text() )
{
@ -85,6 +87,12 @@ QVariant QgsValueRelationWidgetWrapper::value() const
QWidget *QgsValueRelationWidgetWrapper::createWidget( QWidget *parent )
{
QgsAttributeForm *form = qobject_cast<QgsAttributeForm *>( parent );
if ( form )
connect( form, &QgsAttributeForm::widgetValueChanged, this, &QgsValueRelationWidgetWrapper::widgetValueChanged );
mExpression = config().value( QStringLiteral( "FilterExpression" ) ).toString();
if ( config( QStringLiteral( "AllowMulti" ) ).toBool() )
{
return new QTableWidget( parent );
@ -100,26 +108,18 @@ QWidget *QgsValueRelationWidgetWrapper::createWidget( QWidget *parent )
void QgsValueRelationWidgetWrapper::initWidget( QWidget *editor )
{
mCache = QgsValueRelationFieldFormatter::createCache( config() );
mComboBox = qobject_cast<QComboBox *>( editor );
mTableWidget = qobject_cast<QTableWidget *>( editor );
mLineEdit = qobject_cast<QLineEdit *>( editor );
// Read current initial form values from the editor context
setFeature( context().formFeature() );
if ( mComboBox )
{
if ( config( QStringLiteral( "AllowNull" ) ).toBool() )
{
mComboBox->addItem( tr( "(no selection)" ), QVariant( field().type() ) );
}
Q_FOREACH ( const QgsValueRelationFieldFormatter::ValueRelationItem &element, mCache )
{
mComboBox->addItem( element.value, element.key );
}
connect( mComboBox, static_cast<void ( QComboBox::* )( int )>( &QComboBox::currentIndexChanged ),
this, static_cast<void ( QgsEditorWidgetWrapper::* )()>( &QgsEditorWidgetWrapper::emitValueChanged ) );
this, static_cast<void ( QgsEditorWidgetWrapper::* )()>( &QgsEditorWidgetWrapper::emitValueChanged ), Qt::UniqueConnection );
}
else if ( mTableWidget )
{
@ -130,46 +130,11 @@ void QgsValueRelationWidgetWrapper::initWidget( QWidget *editor )
mTableWidget->setShowGrid( false );
mTableWidget->setEditTriggers( QAbstractItemView::NoEditTriggers );
mTableWidget->setSelectionMode( QAbstractItemView::NoSelection );
if ( mCache.size() > 0 )
mTableWidget->setRowCount( ( mCache.size() + config( QStringLiteral( "NofColumns" ) ).toInt() - 1 ) / config( QStringLiteral( "NofColumns" ) ).toInt() );
else
mTableWidget->setRowCount( 1 );
if ( config( QStringLiteral( "NofColumns" ) ).toInt() > 0 )
mTableWidget->setColumnCount( config( QStringLiteral( "NofColumns" ) ).toInt() );
else
mTableWidget->setColumnCount( 1 );
int row = 0, column = 0;
for ( const QgsValueRelationFieldFormatter::ValueRelationItem &element : qgis::as_const( mCache ) )
{
if ( column == config( QStringLiteral( "NofColumns" ) ).toInt() )
{
row++;
column = 0;
}
QTableWidgetItem *item = nullptr;
item = new QTableWidgetItem( element.value );
item->setData( Qt::UserRole, element.key );
mTableWidget->setItem( row, column, item );
column++;
}
connect( mTableWidget, &QTableWidget::itemChanged, this, static_cast<void ( QgsEditorWidgetWrapper::* )()>( &QgsEditorWidgetWrapper::emitValueChanged ) );
connect( mTableWidget, &QTableWidget::itemChanged, this, static_cast<void ( QgsEditorWidgetWrapper::* )()>( &QgsEditorWidgetWrapper::emitValueChanged ), Qt::UniqueConnection );
}
else if ( mLineEdit )
{
QStringList values;
values.reserve( mCache.size() );
Q_FOREACH ( const QgsValueRelationFieldFormatter::ValueRelationItem &i, mCache )
{
values << i.value;
}
QStringListModel *m = new QStringListModel( values, mLineEdit );
QCompleter *completer = new QCompleter( m, mLineEdit );
completer->setCaseSensitivity( Qt::CaseInsensitive );
mLineEdit->setCompleter( completer );
connect( mLineEdit, &QLineEdit::textChanged, this, [ = ]( const QString & value ) { emit valueChanged( value ); } );
connect( mLineEdit, &QLineEdit::textChanged, this, [ = ]( const QString & value ) { emit valueChanged( value ); }, Qt::UniqueConnection );
}
}
@ -184,6 +149,11 @@ void QgsValueRelationWidgetWrapper::setValue( const QVariant &value )
{
QStringList checkList( QgsValueRelationFieldFormatter::valueToStringList( value ) );
QTableWidgetItem *lastChangedItem = nullptr;
// This block is needed because item->setCheckState triggers dataChanged gets back to value()
// and iterate over all items again! This can be extremely slow on large items sets.
mTableWidget->blockSignals( true );
for ( int j = 0; j < mTableWidget->rowCount(); j++ )
{
for ( int i = 0; i < config( QStringLiteral( "NofColumns" ) ).toInt() ; ++i )
@ -192,9 +162,15 @@ void QgsValueRelationWidgetWrapper::setValue( const QVariant &value )
if ( item )
{
item->setCheckState( checkList.contains( item->data( Qt::UserRole ).toString() ) ? Qt::Checked : Qt::Unchecked );
lastChangedItem = item;
}
}
}
mTableWidget->blockSignals( false );
// let's trigger the signal now, once and for all
if ( lastChangedItem )
lastChangedItem->setCheckState( checkList.contains( lastChangedItem->data( Qt::UserRole ).toString() ) ? Qt::Checked : Qt::Unchecked );
}
else if ( mComboBox )
{
@ -202,7 +178,7 @@ void QgsValueRelationWidgetWrapper::setValue( const QVariant &value )
}
else if ( mLineEdit )
{
Q_FOREACH ( QgsValueRelationFieldFormatter::ValueRelationItem i, mCache )
for ( const QgsValueRelationFieldFormatter::ValueRelationItem &i : qgis::as_const( mCache ) )
{
if ( i.key == value )
{
@ -213,19 +189,126 @@ void QgsValueRelationWidgetWrapper::setValue( const QVariant &value )
}
}
void QgsValueRelationWidgetWrapper::widgetValueChanged( const QString &attribute, const QVariant &newValue, bool attributeChanged )
{
// Do nothing if the value has not changed
if ( attributeChanged )
{
setFormFeatureAttribute( attribute, newValue );
// Update combos if the value used in the filter expression has changed
if ( QgsValueRelationFieldFormatter::expressionRequiresFormScope( mExpression )
&& QgsValueRelationFieldFormatter::expressionFormAttributes( mExpression ).contains( attribute ) )
{
populate();
// Restore value
setValue( value( ) );
}
}
}
void QgsValueRelationWidgetWrapper::setFeature( const QgsFeature &feature )
{
setFormFeature( feature );
whileBlocking( this )->populate();
whileBlocking( this )->setValue( feature.attribute( fieldIdx() ) );
// A bit of logic to set the default value if AllowNull is false and this is a new feature
// Note that this needs to be here after the cache has been created/updated by populate()
// and signals unblocked (we want this to propagate to the feature itself)
if ( formFeature().isValid()
&& ! formFeature().attribute( fieldIdx() ).isValid()
&& mCache.size() > 0
&& ! config( QStringLiteral( "AllowNull" ) ).toBool( ) )
{
// This is deferred because at the time the feature is set in one widget it is not
// set in the next, which is typically the "down" in a drill-down
QTimer::singleShot( 0, [ = ]
{
setValue( mCache.at( 0 ).key );
} );
}
}
void QgsValueRelationWidgetWrapper::populate( )
{
// Initialize, note that signals are blocked, to avoid double signals on new features
if ( QgsValueRelationFieldFormatter::expressionRequiresFormScope( mExpression ) )
{
mCache = QgsValueRelationFieldFormatter::createCache( config( ), formFeature() );
}
else if ( mCache.isEmpty() )
{
mCache = QgsValueRelationFieldFormatter::createCache( config( ) );
}
if ( mComboBox )
{
mComboBox->clear();
if ( config( QStringLiteral( "AllowNull" ) ).toBool( ) )
{
whileBlocking( mComboBox )->addItem( tr( "(no selection)" ), QVariant( field().type( ) ) );
}
for ( const QgsValueRelationFieldFormatter::ValueRelationItem &element : qgis::as_const( mCache ) )
{
whileBlocking( mComboBox )->addItem( element.value, element.key );
}
}
else if ( mTableWidget )
{
if ( mCache.size() > 0 )
mTableWidget->setRowCount( ( mCache.size() + config( QStringLiteral( "NofColumns" ) ).toInt() - 1 ) / config( QStringLiteral( "NofColumns" ) ).toInt() );
else
mTableWidget->setRowCount( 1 );
if ( config( QStringLiteral( "NofColumns" ) ).toInt() > 0 )
mTableWidget->setColumnCount( config( QStringLiteral( "NofColumns" ) ).toInt() );
else
mTableWidget->setColumnCount( 1 );
whileBlocking( mTableWidget )->clear();
int row = 0, column = 0;
for ( const QgsValueRelationFieldFormatter::ValueRelationItem &element : qgis::as_const( mCache ) )
{
if ( column == config( QStringLiteral( "NofColumns" ) ).toInt() )
{
row++;
column = 0;
}
QTableWidgetItem *item = nullptr;
item = new QTableWidgetItem( element.value );
item->setData( Qt::UserRole, element.key );
whileBlocking( mTableWidget )->setItem( row, column, item );
column++;
}
}
else if ( mLineEdit )
{
QStringList values;
values.reserve( mCache.size() );
for ( const QgsValueRelationFieldFormatter::ValueRelationItem &i : qgis::as_const( mCache ) )
{
values << i.value;
}
QStringListModel *m = new QStringListModel( values, mLineEdit );
QCompleter *completer = new QCompleter( m, mLineEdit );
completer->setCaseSensitivity( Qt::CaseInsensitive );
mLineEdit->setCompleter( completer );
}
}
void QgsValueRelationWidgetWrapper::showIndeterminateState()
{
if ( mTableWidget )
{
mTableWidget->blockSignals( true );
for ( int j = 0; j < mTableWidget->rowCount(); j++ )
{
for ( int i = 0; i < config( QStringLiteral( "NofColumns" ) ).toInt(); ++i )
{
mTableWidget->item( j, i )->setCheckState( Qt::PartiallyChecked );
whileBlocking( mTableWidget )->item( j, i )->setCheckState( Qt::PartiallyChecked );
}
}
mTableWidget->blockSignals( false );
}
else if ( mComboBox )
{
@ -246,6 +329,7 @@ void QgsValueRelationWidgetWrapper::setEnabled( bool enabled )
if ( mTableWidget )
{
mTableWidget->blockSignals( true );
for ( int j = 0; j < mTableWidget->rowCount(); j++ )
{
for ( int i = 0; i < mTableWidget->columnCount(); ++i )
@ -260,6 +344,7 @@ void QgsValueRelationWidgetWrapper::setEnabled( bool enabled )
}
}
}
mTableWidget->blockSignals( false );
}
else
QgsEditorWidgetWrapper::setEnabled( enabled );

View File

@ -68,9 +68,39 @@ class GUI_EXPORT QgsValueRelationWidgetWrapper : public QgsEditorWidgetWrapper
bool valid() const override;
public slots:
void setValue( const QVariant &value ) override;
/**
* Will be called when a value in the current edited form or table row
* changes
*
* Update widget cache if the value is used in the filter expression and
* stores current field values to be used in expression form scope context
*
* \param attribute The name of the attribute that changed.
* \param newValue The new value of the attribute.
* \param attributeChanged If true, it corresponds to an actual change of the feature attribute
* \since QGIS 3.2.0
*/
void widgetValueChanged( const QString &attribute, const QVariant &newValue, bool attributeChanged );
/**
* Will be called when the feature changes
*
* Is forwarded to the slot setValue() and updates the widget cache if
* the filter expression context contains values from the current feature
*
* \param feature The new feature
*/
void setFeature( const QgsFeature &feature ) override;
private:
//! Set the values for the widgets, re-creates the cache when required
void populate( );
QComboBox *mComboBox = nullptr;
QTableWidget *mTableWidget = nullptr;
QLineEdit *mLineEdit = nullptr;
@ -79,6 +109,7 @@ class GUI_EXPORT QgsValueRelationWidgetWrapper : public QgsEditorWidgetWrapper
QgsVectorLayer *mLayer = nullptr;
bool mEnabled = true;
QString mExpression;
friend class QgsValueRelationWidgetFactory;
friend class TestQgsValueRelationWidgetWrapper;

View File

@ -63,6 +63,7 @@ class GUI_EXPORT QgsAttributeEditorContext
, mVectorLayerTools( parentContext.mVectorLayerTools )
, mMapCanvas( parentContext.mMapCanvas )
, mDistanceArea( parentContext.mDistanceArea )
, mFormFeature( parentContext.mFormFeature )
, mFormMode( formMode )
{
Q_ASSERT( parentContext.vectorLayerTools() );
@ -189,6 +190,21 @@ class GUI_EXPORT QgsAttributeEditorContext
inline const QgsAttributeEditorContext *parentContext() const { return mParentContext; }
/**
* Return current feature from the currently edited form or table row
* \see setFormFeature()
* \since QGIS 3.2
*/
QgsFeature formFeature() const { return mFormFeature; }
/**
* Set current \a feature for the currently edited form or table row
* \see formFeature()
* \since QGIS 3.2
*/
void setFormFeature( const QgsFeature &feature ) { mFormFeature = feature ; }
private:
const QgsAttributeEditorContext *mParentContext = nullptr;
QgsVectorLayer *mLayer = nullptr;
@ -197,6 +213,8 @@ class GUI_EXPORT QgsAttributeEditorContext
QgsDistanceArea mDistanceArea;
QgsRelation mRelation;
RelationMode mRelationMode = Undefined;
//! Store the values of the currently edited form or table row
QgsFeature mFormFeature;
FormMode mFormMode = Embed;
bool mAllowCustomUi = true;
};

View File

@ -690,6 +690,7 @@ QString QgsAttributeForm::createFilterExpression() const
return filter;
}
void QgsAttributeForm::onAttributeChanged( const QVariant &value )
{
QgsEditorWidgetWrapper *eww = qobject_cast<QgsEditorWidgetWrapper *>( sender() );

View File

@ -174,7 +174,8 @@ class GUI_EXPORT QgsAttributeForm : public QWidget
signals:
/**
* Notifies about changes of attributes
* Notifies about changes of attributes, this signal is not emitted when the value is set
* back to the original one.
*
* \param attribute The name of the attribute that changed.
* \param value The new value of the attribute.

View File

@ -1861,6 +1861,59 @@ class TestQgsExpression: public QObject
QCOMPARE( refVar, expectedVars );
}
void referenced_functions()
{
QSet<QString> expectedFunctions;
expectedFunctions << QStringLiteral( "current_value" )
<< QStringLiteral( "var" )
<< QStringLiteral( "intersects" )
<< QStringLiteral( "$geometry" )
<< QStringLiteral( "buffer" );
QgsExpression exp( QStringLiteral( "current_value( 'FIELD_NAME' ) = 'A_VALUE' AND intersects(buffer($geometry, 10), @current_geometry)" ) );
QCOMPARE( exp.hasParserError(), false );
QSet<QString> refVar = exp.referencedFunctions();
QCOMPARE( refVar, expectedFunctions );
}
void findNodes()
{
QSet<QString> expectedFunctions;
expectedFunctions << QStringLiteral( "current_value" )
<< QStringLiteral( "intersects" )
<< QStringLiteral( "var" )
<< QStringLiteral( "$geometry" )
<< QStringLiteral( "buffer" );
QgsExpression exp( QStringLiteral( "current_value( 'FIELD_NAME' ) = 'A_VALUE' AND intersects(buffer($geometry, 10), @current_geometry)" ) );
QList<const QgsExpressionNodeFunction *> functionNodes( exp.findNodes<QgsExpressionNodeFunction>() );
QCOMPARE( functionNodes.size(), 5 );
QgsExpressionFunction *fd;
QSet<QString> actualFunctions;
for ( const auto &f : functionNodes )
{
QCOMPARE( f->nodeType(), QgsExpressionNode::NodeType::ntFunction );
fd = QgsExpression::QgsExpression::Functions()[f->fnIndex()];
actualFunctions << fd->name();
}
QCOMPARE( actualFunctions, expectedFunctions );
QSet<QgsExpressionNodeBinaryOperator::BinaryOperator> expectedBinaryOps;
expectedBinaryOps << QgsExpressionNodeBinaryOperator::BinaryOperator::boAnd;
expectedBinaryOps << QgsExpressionNodeBinaryOperator::BinaryOperator::boEQ;
QList<const QgsExpressionNodeBinaryOperator *> binaryOpsNodes( exp.findNodes<QgsExpressionNodeBinaryOperator>() );
QCOMPARE( binaryOpsNodes.size(), 2 );
QSet<QgsExpressionNodeBinaryOperator::BinaryOperator> actualBinaryOps;
for ( const auto &f : binaryOpsNodes )
{
QCOMPARE( f->nodeType(), QgsExpressionNode::NodeType::ntBinaryOperator );
actualBinaryOps << f->op();
}
QCOMPARE( actualBinaryOps, expectedBinaryOps );
}
void referenced_columns_all_attributes()
{
QgsExpression exp( QStringLiteral( "attribute($currentfeature,'test')" ) );

View File

@ -24,6 +24,7 @@
#include "qgseditorwidgetwrapper.h"
#include <editorwidgets/qgsvaluerelationwidgetwrapper.h>
#include <QTableWidget>
#include <QComboBox>
#include "qgsgui.h"
class TestQgsValueRelationWidgetWrapper : public QObject
@ -39,6 +40,8 @@ class TestQgsValueRelationWidgetWrapper : public QObject
void cleanup(); // will be called after every testfunction.
void testScrollBarUnlocked();
void testDrillDown();
void testDrillDownMulti();
};
void TestQgsValueRelationWidgetWrapper::initTestCase()
@ -59,6 +62,7 @@ void TestQgsValueRelationWidgetWrapper::init()
void TestQgsValueRelationWidgetWrapper::cleanup()
{
}
void TestQgsValueRelationWidgetWrapper::testScrollBarUnlocked()
@ -110,5 +114,202 @@ void TestQgsValueRelationWidgetWrapper::testScrollBarUnlocked()
QCOMPARE( itemEnabled, true );
}
void TestQgsValueRelationWidgetWrapper::testDrillDown()
{
// create a vector layer
QgsVectorLayer vl1( QStringLiteral( "Polygon?crs=epsg:4326&field=pk:int&field=province:int&field=municipality:string" ), QStringLiteral( "vl1" ), QStringLiteral( "memory" ) );
QgsVectorLayer vl2( QStringLiteral( "Point?crs=epsg:4326&field=pk:int&field=fk_province:int&field=fk_municipality:int" ), QStringLiteral( "vl2" ), QStringLiteral( "memory" ) );
QgsProject::instance()->addMapLayer( &vl1, false, false );
QgsProject::instance()->addMapLayer( &vl2, false, false );
// insert some features
QgsFeature f1( vl1.fields() );
f1.setAttribute( QStringLiteral( "pk" ), 1 );
f1.setAttribute( QStringLiteral( "province" ), 123 );
f1.setAttribute( QStringLiteral( "municipality" ), QStringLiteral( "Some Place By The River" ) );
f1.setGeometry( QgsGeometry::fromWkt( QStringLiteral( "POLYGON(( 0 0, 0 1, 1 1, 1 0, 0 0 ))" ) ) );
QVERIFY( f1.isValid() );
QgsFeature f2( vl1.fields() );
f2.setAttribute( QStringLiteral( "pk" ), 2 );
f2.setAttribute( QStringLiteral( "province" ), 245 );
f2.setAttribute( QStringLiteral( "municipality" ), QStringLiteral( "Dreamland By The Clouds" ) );
f2.setGeometry( QgsGeometry::fromWkt( QStringLiteral( "POLYGON(( 1 0, 1 1, 2 1, 2 0, 1 0 ))" ) ) );
QVERIFY( f2.isValid() );
QVERIFY( vl1.dataProvider()->addFeatures( QgsFeatureList() << f1 << f2 ) );
QgsFeature f3( vl2.fields() );
f3.setAttribute( QStringLiteral( "fk_province" ), 123 );
f3.setAttribute( QStringLiteral( "fk_municipality" ), 1 );
f3.setGeometry( QgsGeometry::fromWkt( QStringLiteral( "POINT( 0.5 0.5)" ) ) );
QVERIFY( f3.isValid() );
QVERIFY( f3.geometry().isGeosValid() );
QVERIFY( vl2.dataProvider()->addFeature( f3 ) );
// build a value relation widget wrapper for municipality
QgsValueRelationWidgetWrapper w_municipality( &vl2, vl2.fields().indexOf( QStringLiteral( "fk_municipality" ) ), nullptr, nullptr );
QVariantMap cfg_municipality;
cfg_municipality.insert( QStringLiteral( "Layer" ), vl1.id() );
cfg_municipality.insert( QStringLiteral( "Key" ), QStringLiteral( "pk" ) );
cfg_municipality.insert( QStringLiteral( "Value" ), QStringLiteral( "municipality" ) );
cfg_municipality.insert( QStringLiteral( "AllowMulti" ), false );
cfg_municipality.insert( QStringLiteral( "NofColumns" ), 1 );
cfg_municipality.insert( QStringLiteral( "AllowNull" ), false );
cfg_municipality.insert( QStringLiteral( "OrderByValue" ), true );
cfg_municipality.insert( QStringLiteral( "FilterExpression" ), QStringLiteral( "\"province\" = current_value('fk_province')" ) );
cfg_municipality.insert( QStringLiteral( "UseCompleter" ), false );
w_municipality.setConfig( cfg_municipality );
w_municipality.widget();
w_municipality.setEnabled( true );
QCOMPARE( w_municipality.mCache.size(), 2 );
QCOMPARE( w_municipality.mComboBox->count(), 2 );
w_municipality.setFeature( f3 );
QCOMPARE( w_municipality.mCache.size(), 1 );
// Check first is selected
QCOMPARE( w_municipality.mComboBox->count(), 1 );
QCOMPARE( w_municipality.mComboBox->itemText( 0 ), QStringLiteral( "Some Place By The River" ) );
QCOMPARE( w_municipality.value().toString(), QStringLiteral( "1" ) );
// Filter by geometry
cfg_municipality[ QStringLiteral( "FilterExpression" ) ] = QStringLiteral( "contains(buffer(@current_geometry, 1 ), $geometry)" );
w_municipality.setConfig( cfg_municipality );
w_municipality.setFeature( f3 );
QCOMPARE( w_municipality.mComboBox->count(), 1 );
QCOMPARE( w_municipality.mComboBox->itemText( 0 ), QStringLiteral( "Some Place By The River" ) );
// Move the point to 1.5 0.5
f3.setGeometry( QgsGeometry::fromWkt( QStringLiteral( "POINT( 1.5 0.5)" ) ) );
w_municipality.setFeature( f3 );
QCOMPARE( w_municipality.mComboBox->count(), 1 );
QCOMPARE( w_municipality.mComboBox->itemText( 0 ), QStringLiteral( "Dreamland By The Clouds" ) );
// Enlarge the buffer
cfg_municipality[ QStringLiteral( "FilterExpression" ) ] = QStringLiteral( "contains(buffer(@current_geometry, 3 ), $geometry)" );
w_municipality.setConfig( cfg_municipality );
w_municipality.setFeature( f3 );
QCOMPARE( w_municipality.mComboBox->count(), 2 );
QCOMPARE( w_municipality.mComboBox->itemText( 0 ), QStringLiteral( "Dreamland By The Clouds" ) );
QCOMPARE( w_municipality.mComboBox->itemText( 1 ), QStringLiteral( "Some Place By The River" ) );
// Check with allow null
cfg_municipality[QStringLiteral( "AllowNull" )] = true;
w_municipality.setConfig( cfg_municipality );
w_municipality.setFeature( QgsFeature() );
// Check null is selected
QCOMPARE( w_municipality.mComboBox->count(), 3 );
QCOMPARE( w_municipality.mComboBox->itemText( 0 ), QStringLiteral( "(no selection)" ) );
QCOMPARE( w_municipality.value().toString(), QStringLiteral( "" ) );
// Check order by value false
cfg_municipality[QStringLiteral( "AllowNull" )] = false;
cfg_municipality[QStringLiteral( "OrderByValue" )] = false;
w_municipality.setConfig( cfg_municipality );
w_municipality.setFeature( f3 );
QCOMPARE( w_municipality.mComboBox->itemText( 1 ), QStringLiteral( "Dreamland By The Clouds" ) );
QCOMPARE( w_municipality.mComboBox->itemText( 0 ), QStringLiteral( "Some Place By The River" ) );
}
void TestQgsValueRelationWidgetWrapper::testDrillDownMulti()
{
// create a vector layer
QgsVectorLayer vl1( QStringLiteral( "Polygon?crs=epsg:4326&field=pk:int&field=province:int&field=municipality:string" ), QStringLiteral( "vl1" ), QStringLiteral( "memory" ) );
QgsVectorLayer vl2( QStringLiteral( "Point?crs=epsg:4326&field=pk:int&field=fk_province:int&field=fk_municipality:int" ), QStringLiteral( "vl2" ), QStringLiteral( "memory" ) );
QgsProject::instance()->addMapLayer( &vl1, false, false );
QgsProject::instance()->addMapLayer( &vl2, false, false );
// insert some features
QgsFeature f1( vl1.fields() );
f1.setAttribute( QStringLiteral( "pk" ), 1 );
f1.setAttribute( QStringLiteral( "province" ), 123 );
f1.setAttribute( QStringLiteral( "municipality" ), QStringLiteral( "Some Place By The River" ) );
f1.setGeometry( QgsGeometry::fromWkt( QStringLiteral( "POLYGON(( 0 0, 0 1, 1 1, 1 0, 0 0 ))" ) ) );
QVERIFY( f1.isValid() );
QgsFeature f2( vl1.fields() );
f2.setAttribute( QStringLiteral( "pk" ), 2 );
f2.setAttribute( QStringLiteral( "province" ), 245 );
f2.setAttribute( QStringLiteral( "municipality" ), QStringLiteral( "Dreamland By The Clouds" ) );
f2.setGeometry( QgsGeometry::fromWkt( QStringLiteral( "POLYGON(( 1 0, 1 1, 2 1, 2 0, 1 0 ))" ) ) );
QVERIFY( f2.isValid() );
QVERIFY( vl1.dataProvider()->addFeatures( QgsFeatureList() << f1 << f2 ) );
QgsFeature f3( vl2.fields() );
f3.setAttribute( QStringLiteral( "fk_province" ), 123 );
f3.setAttribute( QStringLiteral( "fk_municipality" ), QStringLiteral( "{1}" ) );
f3.setGeometry( QgsGeometry::fromWkt( QStringLiteral( "POINT( 0.5 0.5)" ) ) );
QVERIFY( f3.isValid() );
QVERIFY( f3.geometry().isGeosValid() );
QVERIFY( vl2.dataProvider()->addFeature( f3 ) );
// build a value relation widget wrapper for municipality
QgsValueRelationWidgetWrapper w_municipality( &vl2, vl2.fields().indexOf( QStringLiteral( "fk_municipality" ) ), nullptr, nullptr );
QVariantMap cfg_municipality;
cfg_municipality.insert( QStringLiteral( "Layer" ), vl1.id() );
cfg_municipality.insert( QStringLiteral( "Key" ), QStringLiteral( "pk" ) );
cfg_municipality.insert( QStringLiteral( "Value" ), QStringLiteral( "municipality" ) );
cfg_municipality.insert( QStringLiteral( "AllowMulti" ), true );
cfg_municipality.insert( QStringLiteral( "NofColumns" ), 1 );
cfg_municipality.insert( QStringLiteral( "AllowNull" ), false );
cfg_municipality.insert( QStringLiteral( "OrderByValue" ), true );
cfg_municipality.insert( QStringLiteral( "FilterExpression" ), QStringLiteral( "\"province\" = current_value('fk_province')" ) );
cfg_municipality.insert( QStringLiteral( "UseCompleter" ), false );
w_municipality.setConfig( cfg_municipality );
w_municipality.widget();
w_municipality.setEnabled( true );
QCOMPARE( w_municipality.mCache.size(), 2 );
QCOMPARE( w_municipality.mTableWidget->rowCount(), 2 );
w_municipality.setFeature( f3 );
QCOMPARE( w_municipality.mCache.size(), 1 );
QCOMPARE( w_municipality.mTableWidget->rowCount(), 1 );
QCOMPARE( w_municipality.mTableWidget->item( 0, 0 )->text(), QStringLiteral( "Some Place By The River" ) );
QCOMPARE( w_municipality.value().toString(), QStringLiteral( "{1}" ) );
// Filter by geometry
cfg_municipality[ QStringLiteral( "FilterExpression" ) ] = QStringLiteral( "contains(buffer(@current_geometry, 1 ), $geometry)" );
w_municipality.setConfig( cfg_municipality );
w_municipality.setFeature( f3 );
QCOMPARE( w_municipality.mTableWidget->rowCount(), 1 );
QCOMPARE( w_municipality.mTableWidget->item( 0, 0 )->text(), QStringLiteral( "Some Place By The River" ) );
// Move the point to 1.5 0.5
f3.setGeometry( QgsGeometry::fromWkt( QStringLiteral( "POINT( 1.5 0.5)" ) ) );
w_municipality.setFeature( f3 );
QCOMPARE( w_municipality.mTableWidget->rowCount(), 1 );
QCOMPARE( w_municipality.mTableWidget->item( 0, 0 )->text(), QStringLiteral( "Dreamland By The Clouds" ) );
// Enlarge the buffer
cfg_municipality[ QStringLiteral( "FilterExpression" ) ] = QStringLiteral( "contains(buffer(@current_geometry, 3 ), $geometry)" );
w_municipality.setConfig( cfg_municipality );
w_municipality.setFeature( f3 );
QCOMPARE( w_municipality.mTableWidget->rowCount(), 2 );
QCOMPARE( w_municipality.mTableWidget->item( 0, 0 )->text(), QStringLiteral( "Dreamland By The Clouds" ) );
QCOMPARE( w_municipality.mTableWidget->item( 0, 0 )->data( Qt::UserRole ).toString(), QStringLiteral( "2" ) );
QCOMPARE( w_municipality.mTableWidget->item( 1, 0 )->text(), QStringLiteral( "Some Place By The River" ) );
QCOMPARE( w_municipality.mTableWidget->item( 1, 0 )->data( Qt::UserRole ).toString(), QStringLiteral( "1" ) );
QCOMPARE( w_municipality.value().toString(), QStringLiteral( "{1}" ) );
QCOMPARE( w_municipality.mTableWidget->item( 0, 0 )->checkState(), Qt::Unchecked );
QCOMPARE( w_municipality.mTableWidget->item( 1, 0 )->checkState(), Qt::Checked );
w_municipality.setValue( QStringLiteral( "{1,2}" ) );
QCOMPARE( w_municipality.value().toString(), QStringLiteral( "{2,1}" ) );
QCOMPARE( w_municipality.mTableWidget->item( 0, 0 )->checkState(), Qt::Checked );
QCOMPARE( w_municipality.mTableWidget->item( 1, 0 )->checkState(), Qt::Checked );
// Check values are checked
f3.setAttribute( QStringLiteral( "fk_municipality" ), QStringLiteral( "{1,2}" ) );
w_municipality.setFeature( f3 );
QCOMPARE( w_municipality.mTableWidget->rowCount(), 2 );
QCOMPARE( w_municipality.mTableWidget->item( 0, 0 )->text(), QStringLiteral( "Dreamland By The Clouds" ) );
QCOMPARE( w_municipality.mTableWidget->item( 1, 0 )->text(), QStringLiteral( "Some Place By The River" ) );
QCOMPARE( w_municipality.mTableWidget->item( 0, 0 )->checkState(), Qt::Checked );
QCOMPARE( w_municipality.mTableWidget->item( 1, 0 )->checkState(), Qt::Checked );
QCOMPARE( w_municipality.value().toString(), QStringLiteral( "{2,1}" ) );
}
QGSTEST_MAIN( TestQgsValueRelationWidgetWrapper )
#include "testqgsvaluerelationwidgetwrapper.moc"

View File

@ -16,7 +16,8 @@ import qgis # NOQA
from qgis.core import (QgsFeature, QgsProject, QgsRelation, QgsVectorLayer,
QgsValueMapFieldFormatter, QgsValueRelationFieldFormatter,
QgsRelationReferenceFieldFormatter, QgsRangeFieldFormatter, QgsSettings)
QgsRelationReferenceFieldFormatter, QgsRangeFieldFormatter,
QgsSettings, QgsGeometry, QgsPointXY)
from qgis.PyQt.QtCore import QCoreApplication, QLocale
from qgis.testing import start_app, unittest
@ -133,6 +134,39 @@ class TestQgsValueRelationFieldFormatter(unittest.TestCase):
_test(['1', '2', '3'], ["1", "2", "3"])
_test('not an array', ['not an array'])
def test_expressionRequiresFormScope(self):
res = list(QgsValueRelationFieldFormatter.expressionFormAttributes("current_value('ONE') AND current_value('TWO')"))
res = sorted(res)
self.assertEqual(res, ['ONE', 'TWO'])
res = list(QgsValueRelationFieldFormatter.expressionFormVariables("@current_geometry"))
self.assertEqual(res, ['current_geometry'])
self.assertFalse(QgsValueRelationFieldFormatter.expressionRequiresFormScope(""))
self.assertTrue(QgsValueRelationFieldFormatter.expressionRequiresFormScope("current_value('TWO')"))
self.assertTrue(QgsValueRelationFieldFormatter.expressionRequiresFormScope("current_value ( 'TWO' )"))
self.assertTrue(QgsValueRelationFieldFormatter.expressionRequiresFormScope("@current_geometry"))
self.assertTrue(QgsValueRelationFieldFormatter.expressionIsUsable("", QgsFeature()))
self.assertFalse(QgsValueRelationFieldFormatter.expressionIsUsable("@current_geometry", QgsFeature()))
self.assertFalse(QgsValueRelationFieldFormatter.expressionIsUsable("current_value ( 'TWO' )", QgsFeature()))
layer = QgsVectorLayer("none?field=pkid:integer&field=decoded:string",
"layer", "memory")
self.assertTrue(layer.isValid())
QgsProject.instance().addMapLayer(layer)
f = QgsFeature(layer.fields())
f.setAttributes([1, 'value'])
point = QgsGeometry.fromPointXY(QgsPointXY(123, 456))
f.setGeometry(point)
self.assertTrue(QgsValueRelationFieldFormatter.expressionIsUsable("current_geometry", f))
self.assertFalse(QgsValueRelationFieldFormatter.expressionIsUsable("current_value ( 'TWO' )", f))
self.assertTrue(QgsValueRelationFieldFormatter.expressionIsUsable("current_value ( 'pkid' )", f))
self.assertTrue(QgsValueRelationFieldFormatter.expressionIsUsable("@current_geometry current_value ( 'pkid' )", f))
QgsProject.instance().removeMapLayer(layer.id())
class TestQgsRelationReferenceFieldFormatter(unittest.TestCase):