Merge pull request #38659 from suricactus/relref_filter_expr

Add support for filter expressions in relation reference widget
This commit is contained in:
Matthias Kuhn 2020-09-10 06:46:44 +02:00 committed by GitHub
commit a853f2643d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 192 additions and 38 deletions

View File

@ -136,6 +136,16 @@ Set if filters are chained.
Chained filters restrict the option of subsequent filters based on the selection of a previous filter.
:param chainFilters: If chaining should be enabled
%End
QString filterExpression() const;
%Docstring
Returns the currently set filter expression.
%End
void setFilterExpression( const QString &filterExpression );
%Docstring
If not empty, will be used as filter expression.
Only if this evaluates to ``True``, the value will be shown.
%End
QgsFeature referencedFeature() const;

View File

@ -21,6 +21,7 @@
#include "qgsrelationmanager.h"
#include "qgsvectorlayer.h"
#include "qgsexpressionbuilderdialog.h"
#include "qgsexpressioncontextutils.h"
QgsRelationReferenceConfigDlg::QgsRelationReferenceConfigDlg( QgsVectorLayer *vl, int fieldIdx, QWidget *parent )
: QgsEditorConfigWidget( vl, fieldIdx, parent )
@ -59,6 +60,34 @@ QgsRelationReferenceConfigDlg::QgsRelationReferenceConfigDlg( QgsVectorLayer *vl
connect( mFilterFieldsList, &QListWidget::itemChanged, this, &QgsEditorConfigWidget::changed );
connect( mCbxChainFilters, &QAbstractButton::toggled, this, &QgsEditorConfigWidget::changed );
connect( mExpressionWidget, static_cast<void ( QgsFieldExpressionWidget::* )( const QString & )>( &QgsFieldExpressionWidget::fieldChanged ), this, &QgsEditorConfigWidget::changed );
connect( mEditExpression, &QAbstractButton::clicked, this, &QgsRelationReferenceConfigDlg::mEditExpression_clicked );
connect( mFilterExpression, &QTextEdit::textChanged, this, &QgsEditorConfigWidget::changed );
}
void QgsRelationReferenceConfigDlg::mEditExpression_clicked()
{
QgsVectorLayer *vl = qobject_cast<QgsVectorLayer *>( layer() );
if ( !vl )
return;
QgsExpressionContext context( QgsExpressionContextUtils::globalProjectLayerScopes( vl ) );
context << QgsExpressionContextUtils::formScope( );
context << QgsExpressionContextUtils::parentFormScope( );
context.setHighlightedFunctions( QStringList() << QStringLiteral( "current_value" ) << QStringLiteral( "current_parent_value" ) );
context.setHighlightedVariables( QStringList() << QStringLiteral( "current_geometry" )
<< QStringLiteral( "current_feature" )
<< QStringLiteral( "form_mode" )
<< QStringLiteral( "current_parent_geometry" )
<< QStringLiteral( "current_parent_feature" ) );
QgsExpressionBuilderDialog dlg( vl, mFilterExpression->toPlainText(), this, QStringLiteral( "generic" ), context );
dlg.setWindowTitle( tr( "Edit Filter Expression" ) );
if ( dlg.exec() == QDialog::Accepted )
{
mFilterExpression->setPlainText( dlg.expressionBuilder()->expressionText() );
}
}
void QgsRelationReferenceConfigDlg::setConfig( const QVariantMap &config )
@ -77,6 +106,7 @@ void QgsRelationReferenceConfigDlg::setConfig( const QVariantMap &config )
mCbxMapIdentification->setChecked( config.value( QStringLiteral( "MapIdentification" ), false ).toBool() );
mCbxAllowAddFeatures->setChecked( config.value( QStringLiteral( "AllowAddFeatures" ), false ).toBool() );
mCbxReadOnly->setChecked( config.value( QStringLiteral( "ReadOnly" ), false ).toBool() );
mFilterExpression->setPlainText( config.value( QStringLiteral( "FilterExpression" ) ).toString() );
if ( config.contains( QStringLiteral( "FilterFields" ) ) )
{
@ -149,6 +179,7 @@ QVariantMap QgsRelationReferenceConfigDlg::config()
myConfig.insert( QStringLiteral( "FilterFields" ), filterFields );
myConfig.insert( QStringLiteral( "ChainFilters" ), mCbxChainFilters->isChecked() );
myConfig.insert( QStringLiteral( "FilterExpression" ), mFilterExpression->toPlainText() );
}
if ( mReferencedLayer )

View File

@ -51,6 +51,11 @@ class GUI_EXPORT QgsRelationReferenceConfigDlg : public QgsEditorConfigWidget, p
void relationChanged( int idx );
void mAddFilterButton_clicked();
void mRemoveFilterButton_clicked();
/**
* Opens an expression dialog and sets its value as filter expression for the relation reference.
*/
void mEditExpression_clicked();
};
#endif // QGSRELATIONREFERENCECONFIGDLGBASE_H

View File

@ -217,6 +217,7 @@ void QgsRelationReferenceSearchWidgetWrapper::initWidget( QWidget *editor )
{
mWidget->setFilterFields( config( QStringLiteral( "FilterFields" ) ).toStringList() );
mWidget->setChainFilters( config( QStringLiteral( "ChainFilters" ) ).toBool() );
mWidget->setFilterExpression( config( QStringLiteral( "FilterExpression" ) ).toString() );
}
QgsRelation relation = QgsProject::instance()->relationManager()->relation( config( QStringLiteral( "Relation" ) ).toString() );

View File

@ -189,6 +189,7 @@ void QgsRelationReferenceWidget::setRelation( const QgsRelation &relation, bool
mRelation = relation;
mReferencingLayer = relation.referencingLayer();
mReferencedLayer = relation.referencedLayer();
const QList<QgsRelation::FieldPair> fieldPairs = relation.fieldPairs();
for ( const QgsRelation::FieldPair &fieldPair : fieldPairs )
{
@ -199,6 +200,7 @@ void QgsRelationReferenceWidget::setRelation( const QgsRelation &relation, bool
mComboBox->setAllowNull( mAllowNull );
mComboBox->setSourceLayer( mReferencedLayer );
mComboBox->setIdentifierFields( mReferencedFields );
mComboBox->setFilterExpression( mFilterExpression );
}
mAttributeEditorFrame->setObjectName( QStringLiteral( "referencing/" ) + relation.name() );
@ -487,6 +489,11 @@ void QgsRelationReferenceWidget::setChainFilters( bool chainFilters )
mChainFilters = chainFilters;
}
void QgsRelationReferenceWidget::setFilterExpression( const QString &expression )
{
mFilterExpression = expression;
}
void QgsRelationReferenceWidget::showEvent( QShowEvent *e )
{
Q_UNUSED( e )
@ -542,7 +549,9 @@ void QgsRelationReferenceWidget::init()
QVariant nullValue = QgsApplication::nullRepresentation();
QgsFeature ft;
QgsFeatureIterator fit = mReferencedLayer->getFeatures();
QgsFeatureIterator fit = mFilterExpression.isEmpty()
? mReferencedLayer->getFeatures()
: mReferencedLayer->getFeatures( mFilterExpression );
while ( fit.nextFeature( ft ) )
{
const int count = std::min( mFilterComboBoxes.count(), mFilterFields.count() );
@ -574,6 +583,9 @@ void QgsRelationReferenceWidget::init()
mComboBox->setAllowNull( mAllowNull );
mComboBox->setIdentifierFields( mReferencedFields );
if ( ! mFilterExpression.isEmpty() )
mComboBox->setFilterExpression( mFilterExpression );
QVariant nullValue = QgsApplication::nullRepresentation();
if ( mChainFilters && mFeature.isValid() )
@ -851,7 +863,7 @@ void QgsRelationReferenceWidget::filterChanged()
QgsFeature f;
QgsFeatureIds featureIds;
QString filterExpression;
QString filterExpression = mFilterExpression;
// comboboxes have to be disabled before building filters
if ( mChainFilters )
@ -907,9 +919,16 @@ void QgsRelationReferenceWidget::filterChanged()
{
QMap<QString, QString> filtersAttrs = filters;
filtersAttrs[fieldName] = QgsExpression::createFieldEqualityExpression( fieldName, txt );
QString expression = filtersAttrs.values().join( QStringLiteral( " AND " ) );
QgsAttributeList subset = attrs;
QString expression = filterExpression;
if ( ! filterExpression.isEmpty() && ! filtersAttrs.values().isEmpty() )
expression += QStringLiteral( " AND " );
expression += filtersAttrs.isEmpty() ? QString() : QStringLiteral( " ( " );
expression += filtersAttrs.values().join( QStringLiteral( " AND " ) );
expression += filtersAttrs.isEmpty() ? QString() : QStringLiteral( " ) " );
subset << mReferencedLayer->fields().lookupField( fieldName );
QgsFeatureIterator it( mReferencedLayer->getFeatures( QgsFeatureRequest().setFilterExpression( expression ).setSubsetOfAttributes( subset ) ) );
@ -938,7 +957,14 @@ void QgsRelationReferenceWidget::filterChanged()
}
}
}
filterExpression = filters.values().join( QStringLiteral( " AND " ) );
if ( ! filterExpression.isEmpty() && ! filters.values().isEmpty() )
filterExpression += QStringLiteral( " AND " );
filterExpression += filters.isEmpty() ? QString() : QStringLiteral( " ( " );
filterExpression += filters.values().join( QStringLiteral( " AND " ) );
filterExpression += filters.isEmpty() ? QString() : QStringLiteral( " ) " );
mComboBox->setFilterExpression( filterExpression );
}

View File

@ -157,6 +157,17 @@ class GUI_EXPORT QgsRelationReferenceWidget : public QWidget
*/
void setChainFilters( bool chainFilters );
/**
* Returns the currently set filter expression.
*/
QString filterExpression() const { return mFilterExpression; };
/**
* If not empty, will be used as filter expression.
* Only if this evaluates to TRUE, the value will be shown.
*/
void setFilterExpression( const QString &filterExpression );
/**
* Returns the related feature (from the referenced layer)
* if no feature is related, it returns an invalid feature
@ -313,6 +324,7 @@ class GUI_EXPORT QgsRelationReferenceWidget : public QWidget
QgsVectorLayer *mReferencingLayer = nullptr;
QgsFeatureListComboBox *mComboBox = nullptr;
QList<QComboBox *> mFilterComboBoxes;
QString mFilterExpression;
QWidget *mWindowWidget = nullptr;
bool mShown = false;
QgsRelation mRelation;

View File

@ -18,6 +18,7 @@
#include "qgsproject.h"
#include "qgsrelationmanager.h"
#include "qgsrelationreferencewidget.h"
#include "qgsattributeform.h"
QgsRelationReferenceWidgetWrapper::QgsRelationReferenceWidgetWrapper( QgsVectorLayer *vl, int fieldIdx, QWidget *editor, QgsMapCanvas *canvas, QgsMessageBar *messageBar, QWidget *parent )
: QgsEditorWidgetWrapper( vl, fieldIdx, editor, parent )
@ -62,6 +63,7 @@ void QgsRelationReferenceWidgetWrapper::initWidget( QWidget *editor )
{
mWidget->setFilterFields( config( QStringLiteral( "FilterFields" ) ).toStringList() );
mWidget->setChainFilters( config( QStringLiteral( "ChainFilters" ) ).toBool() );
mWidget->setFilterExpression( config( QStringLiteral( "FilterExpression" ) ).toString() );
}
mWidget->setAllowAddFeatures( config( QStringLiteral( "AllowAddFeatures" ), false ).toBool() );

View File

@ -73,6 +73,8 @@ class GUI_EXPORT QgsRelationReferenceWidgetWrapper : public QgsEditorWidgetWrapp
private:
void updateValues( const QVariant &val, const QVariantList &additionalValues = QVariantList() ) override;
QString mExpression;
QgsRelationReferenceWidget *mWidget = nullptr;
QgsMapCanvas *mCanvas = nullptr;
QgsMessageBar *mMessageBar = nullptr;

View File

@ -139,6 +139,7 @@ QWidget *QgsValueRelationWidgetWrapper::createWidget( QWidget *parent )
{
return new QgsFilterLineEdit( parent );
}
else
{
return new QComboBox( parent );
}

View File

@ -99,6 +99,33 @@
<bool>false</bool>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="2" column="1">
<widget class="QToolButton" name="mAddFilterButton">
<property name="text">
<string>…</string>
</property>
<property name="icon">
<iconset resource="../../../images/images.qrc">
<normaloff>:/images/themes/default/symbologyAdd.svg</normaloff>:/images/themes/default/symbologyAdd.svg</iconset>
</property>
</widget>
</item>
<item row="1" column="0" rowspan="4">
<widget class="QListWidget" name="mAvailableFieldsList"/>
</item>
<item row="1" column="2" rowspan="4">
<widget class="QListWidget" name="mFilterFieldsList"/>
</item>
<item row="5" column="0" colspan="3">
<widget class="QCheckBox" name="mCbxChainFilters">
<property name="toolTip">
<string>When activated, the filters will restrict the choices of fields to options that are </string>
</property>
<property name="text">
<string>Chain filters</string>
</property>
</widget>
</item>
<item row="4" column="1">
<spacer name="verticalSpacer_2">
<property name="orientation">
@ -112,28 +139,6 @@
</property>
</spacer>
</item>
<item row="2" column="1">
<widget class="QToolButton" name="mAddFilterButton">
<property name="text">
<string>…</string>
</property>
<property name="icon">
<iconset>
<normaloff>:/images/themes/default/symbologyAdd.svg</normaloff>:/images/themes/default/symbologyAdd.svg</iconset>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QToolButton" name="mRemoveFilterButton">
<property name="text">
<string>…</string>
</property>
<property name="icon">
<iconset resource="../../../images/images.qrc">
<normaloff>:/images/themes/default/symbologyRemove.svg</normaloff>:/images/themes/default/symbologyRemove.svg</iconset>
</property>
</widget>
</item>
<item row="1" column="1">
<spacer name="verticalSpacer">
<property name="orientation">
@ -147,22 +152,55 @@
</property>
</spacer>
</item>
<item row="1" column="2" rowspan="4">
<widget class="QListWidget" name="mFilterFieldsList"/>
</item>
<item row="1" column="0" rowspan="4">
<widget class="QListWidget" name="mAvailableFieldsList"/>
</item>
<item row="5" column="0" colspan="3">
<widget class="QCheckBox" name="mCbxChainFilters">
<property name="toolTip">
<string>When activated, the filters will restrict the choices of fields to options that are </string>
<item row="6" column="0" colspan="3">
<layout class="QHBoxLayout" name="horizontalLayout">
<property name="topMargin">
<number>10</number>
</property>
<item>
<widget class="QLabel" name="label_3">
<property name="text">
<string>Filter expression</string>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="mEditExpression">
<property name="icon">
<iconset resource="../../../images/images.qrc">
<normaloff>:/images/themes/default/mIconExpression.svg</normaloff>:/images/themes/default/mIconExpression.svg</iconset>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item row="3" column="1">
<widget class="QToolButton" name="mRemoveFilterButton">
<property name="text">
<string>Chain filters</string>
<string>…</string>
</property>
<property name="icon">
<iconset resource="../../../images/images.qrc">
<normaloff>:/images/themes/default/symbologyRemove.svg</normaloff>:/images/themes/default/symbologyRemove.svg</iconset>
</property>
</widget>
</item>
<item row="7" column="0" colspan="3">
<widget class="QTextEdit" name="mFilterExpression"/>
</item>
</layout>
</widget>
</item>

View File

@ -66,6 +66,7 @@ class TestQgsRelationReferenceWidget : public QObject
void testAddEntry();
void testAddEntryNoGeom();
void testDependencies(); // Test relation datasource, id etc. config storage
void testSetFilterExpression();
private:
std::unique_ptr<QgsVectorLayer> mLayer1;
@ -709,5 +710,30 @@ void TestQgsRelationReferenceWidget::testDependencies()
}
void TestQgsRelationReferenceWidget::testSetFilterExpression()
{
// init a relation reference widget
QStringList filterFields = { "material", "diameter", "raccord" };
QWidget parentWidget;
QgsRelationReferenceWidget w( &parentWidget );
QEventLoop loop;
connect( qobject_cast<QgsFeatureFilterModel *>( w.mComboBox->model() ), &QgsFeatureFilterModel::filterJobCompleted, &loop, &QEventLoop::quit );
w.setChainFilters( true );
w.setFilterFields( filterFields );
w.setRelation( *mRelation, true );
w.setFilterExpression( QStringLiteral( " \"material\" = 'iron' " ) );
w.init();
loop.exec();
QStringList items = getComboBoxItems( w.mComboBox );
QCOMPARE( w.mComboBox->currentText(), QStringLiteral( "NULL" ) );
// in case there is no filter, the number of filtered features will be 4
QCOMPARE( w.mComboBox->count(), 3 );
}
QGSTEST_MAIN( TestQgsRelationReferenceWidget )
#include "testqgsrelationreferencewidget.moc"