Add API for custom preview generators in QgsExpressionBuilderWidget

In this mode, the widget will call a callback function to generate
a new QgsExpressionContext as the previewed object changes. This
can be used to provide custom preview values for different objects
(i.e. for objects which aren't vector layer features), such as raster
bands or other custom objects.
This commit is contained in:
Nyall Dawson 2024-03-21 14:46:37 +10:00 committed by Martin Dobias
parent aee2805234
commit d9a49f58fd
16 changed files with 675 additions and 21 deletions

View File

@ -159,6 +159,45 @@ preview result and to populate the list of available functions and variables.
Returns if the expression is valid
%End
void setCustomPreviewGenerator( const QString &label, const QList< QPair< QString, QVariant > > &choices, SIP_PYCALLABLE );
%Docstring
Sets the widget to run using a custom preview generator.
In this mode, the widget will call a callback function to generate a new :py:class:`QgsExpressionContext`
as the previewed object changes. This can be used to provide custom preview values for different
objects (i.e. for objects which aren't vector layer features).
:param label: The label to display for the combo box presenting choices of objects. This should be a representative name, eg "Band" if the widget is showing choices of raster layer bands
:param choices: A list of choices to present to the user. Each choice is a pair of a human-readable label and a QVariant representing the object to preview.
:param previewContextGenerator: A function which takes a QVariant representing the object to preview, and returns a :py:class:`QgsExpressionContext` to use for previewing the object.
.. versionadded:: 3.38
%End
%MethodCode
Py_XINCREF( a2 );
Py_BEGIN_ALLOW_THREADS
sipCpp->setCustomPreviewGenerator( *a0, *a1, [a2]( const QVariant &value )->QgsExpressionContext
{
QgsExpressionContext res;
SIP_BLOCK_THREADS
PyObject *s = sipCallMethod( NULL, a2, "D", &value, sipType_QVariant, NULL );
int state;
int sipIsError = 0;
QgsExpressionContext *t1 = reinterpret_cast<QgsExpressionContext *>( sipConvertToType( s, sipType_QgsExpressionContext, 0, SIP_NOT_NONE, &state, &sipIsError ) );
if ( sipIsError == 0 )
{
res = QgsExpressionContext( *t1 );
}
sipReleaseType( t1, sipType_QgsExpressionContext, state );
SIP_UNBLOCK_THREADS
return res;
} );
Py_END_ALLOW_THREADS
%End
void saveToRecent( const QString &collection = "generic" ) /Deprecated/;
%Docstring
Adds the current expression to the given ``collection``.

View File

@ -11,6 +11,7 @@
class QgsExpressionPreviewWidget : QWidget
{
%Docstring(signature="appended")
@ -34,6 +35,44 @@ Constructor
Sets the layer used in the preview
%End
void setCustomPreviewGenerator( const QString &label, const QList< QPair< QString, QVariant > > &choices, SIP_PYCALLABLE );
%Docstring
Sets the widget to run using a custom preview generator.
In this mode, the widget will call a callback function to generate a new :py:class:`QgsExpressionContext`
as the previewed object changes. This can be used to provide custom preview values for different
objects (i.e. for objects which aren't vector layer features).
:param label: The label to display for the combo box presenting choices of objects. This should be a representative name, eg "Band" if the widget is showing choices of raster layer bands
:param choices: A list of choices to present to the user. Each choice is a pair of a human-readable label and a QVariant representing the object to preview.
:param previewContextGenerator: A function which takes a QVariant representing the object to preview, and returns a :py:class:`QgsExpressionContext` to use for previewing the object.
.. versionadded:: 3.38
%End
%MethodCode
Py_XINCREF( a2 );
Py_BEGIN_ALLOW_THREADS
sipCpp->setCustomPreviewGenerator( *a0, *a1, [a2]( const QVariant &value )->QgsExpressionContext
{
QgsExpressionContext res;
SIP_BLOCK_THREADS
PyObject *s = sipCallMethod( NULL, a2, "D", &value, sipType_QVariant, NULL );
int state;
int sipIsError = 0;
QgsExpressionContext *t1 = reinterpret_cast<QgsExpressionContext *>( sipConvertToType( s, sipType_QgsExpressionContext, 0, SIP_NOT_NONE, &state, &sipIsError ) );
if ( sipIsError == 0 )
{
res = QgsExpressionContext( *t1 );
}
sipReleaseType( t1, sipType_QgsExpressionContext, state );
SIP_UNBLOCK_THREADS
return res;
} );
Py_END_ALLOW_THREADS
%End
void setExpressionText( const QString &expression );
%Docstring
Sets the expression
@ -80,6 +119,13 @@ Returns the root node of the expression
QList<QgsExpression::ParserError> parserErrors() const;
%Docstring
Returns the expression parser errors
%End
QString currentPreviewText() const;
%Docstring
Returns the current expression result preview text.
.. versionadded:: 3.38
%End
signals:

View File

@ -142,6 +142,44 @@ an expression context for the widget.
create an expression context when required.
%End
void setCustomPreviewGenerator( const QString &label, const QList< QPair< QString, QVariant > > &choices, SIP_PYCALLABLE );
%Docstring
Sets the widget to run using a custom preview generator.
In this mode, the widget will call a callback function to generate a new :py:class:`QgsExpressionContext`
as the previewed object changes. This can be used to provide custom preview values for different
objects (i.e. for objects which aren't vector layer features).
:param label: The label to display for the combo box presenting choices of objects. This should be a representative name, eg "Band" if the widget is showing choices of raster layer bands
:param choices: A list of choices to present to the user. Each choice is a pair of a human-readable label and a QVariant representing the object to preview.
:param previewContextGenerator: A function which takes a QVariant representing the object to preview, and returns a :py:class:`QgsExpressionContext` to use for previewing the object.
.. versionadded:: 3.38
%End
%MethodCode
Py_XINCREF( a2 );
Py_BEGIN_ALLOW_THREADS
sipCpp->setCustomPreviewGenerator( *a0, *a1, [a2]( const QVariant &value )->QgsExpressionContext
{
QgsExpressionContext res;
SIP_BLOCK_THREADS
PyObject *s = sipCallMethod( NULL, a2, "D", &value, sipType_QVariant, NULL );
int state;
int sipIsError = 0;
QgsExpressionContext *t1 = reinterpret_cast<QgsExpressionContext *>( sipConvertToType( s, sipType_QgsExpressionContext, 0, SIP_NOT_NONE, &state, &sipIsError ) );
if ( sipIsError == 0 )
{
res = QgsExpressionContext( *t1 );
}
sipReleaseType( t1, sipType_QgsExpressionContext, state );
SIP_UNBLOCK_THREADS
return res;
} );
Py_END_ALLOW_THREADS
%End
bool allowEvalErrors() const;
%Docstring
Allow accepting expressions with evaluation errors. This can be useful when we are not able to

View File

@ -159,6 +159,45 @@ preview result and to populate the list of available functions and variables.
Returns if the expression is valid
%End
void setCustomPreviewGenerator( const QString &label, const QList< QPair< QString, QVariant > > &choices, SIP_PYCALLABLE );
%Docstring
Sets the widget to run using a custom preview generator.
In this mode, the widget will call a callback function to generate a new :py:class:`QgsExpressionContext`
as the previewed object changes. This can be used to provide custom preview values for different
objects (i.e. for objects which aren't vector layer features).
:param label: The label to display for the combo box presenting choices of objects. This should be a representative name, eg "Band" if the widget is showing choices of raster layer bands
:param choices: A list of choices to present to the user. Each choice is a pair of a human-readable label and a QVariant representing the object to preview.
:param previewContextGenerator: A function which takes a QVariant representing the object to preview, and returns a :py:class:`QgsExpressionContext` to use for previewing the object.
.. versionadded:: 3.38
%End
%MethodCode
Py_XINCREF( a2 );
Py_BEGIN_ALLOW_THREADS
sipCpp->setCustomPreviewGenerator( *a0, *a1, [a2]( const QVariant &value )->QgsExpressionContext
{
QgsExpressionContext res;
SIP_BLOCK_THREADS
PyObject *s = sipCallMethod( NULL, a2, "D", &value, sipType_QVariant, NULL );
int state;
int sipIsError = 0;
QgsExpressionContext *t1 = reinterpret_cast<QgsExpressionContext *>( sipConvertToType( s, sipType_QgsExpressionContext, 0, SIP_NOT_NONE, &state, &sipIsError ) );
if ( sipIsError == 0 )
{
res = QgsExpressionContext( *t1 );
}
sipReleaseType( t1, sipType_QgsExpressionContext, state );
SIP_UNBLOCK_THREADS
return res;
} );
Py_END_ALLOW_THREADS
%End
void saveToRecent( const QString &collection = "generic" ) /Deprecated/;
%Docstring
Adds the current expression to the given ``collection``.

View File

@ -11,6 +11,7 @@
class QgsExpressionPreviewWidget : QWidget
{
%Docstring(signature="appended")
@ -34,6 +35,44 @@ Constructor
Sets the layer used in the preview
%End
void setCustomPreviewGenerator( const QString &label, const QList< QPair< QString, QVariant > > &choices, SIP_PYCALLABLE );
%Docstring
Sets the widget to run using a custom preview generator.
In this mode, the widget will call a callback function to generate a new :py:class:`QgsExpressionContext`
as the previewed object changes. This can be used to provide custom preview values for different
objects (i.e. for objects which aren't vector layer features).
:param label: The label to display for the combo box presenting choices of objects. This should be a representative name, eg "Band" if the widget is showing choices of raster layer bands
:param choices: A list of choices to present to the user. Each choice is a pair of a human-readable label and a QVariant representing the object to preview.
:param previewContextGenerator: A function which takes a QVariant representing the object to preview, and returns a :py:class:`QgsExpressionContext` to use for previewing the object.
.. versionadded:: 3.38
%End
%MethodCode
Py_XINCREF( a2 );
Py_BEGIN_ALLOW_THREADS
sipCpp->setCustomPreviewGenerator( *a0, *a1, [a2]( const QVariant &value )->QgsExpressionContext
{
QgsExpressionContext res;
SIP_BLOCK_THREADS
PyObject *s = sipCallMethod( NULL, a2, "D", &value, sipType_QVariant, NULL );
int state;
int sipIsError = 0;
QgsExpressionContext *t1 = reinterpret_cast<QgsExpressionContext *>( sipConvertToType( s, sipType_QgsExpressionContext, 0, SIP_NOT_NONE, &state, &sipIsError ) );
if ( sipIsError == 0 )
{
res = QgsExpressionContext( *t1 );
}
sipReleaseType( t1, sipType_QgsExpressionContext, state );
SIP_UNBLOCK_THREADS
return res;
} );
Py_END_ALLOW_THREADS
%End
void setExpressionText( const QString &expression );
%Docstring
Sets the expression
@ -80,6 +119,13 @@ Returns the root node of the expression
QList<QgsExpression::ParserError> parserErrors() const;
%Docstring
Returns the expression parser errors
%End
QString currentPreviewText() const;
%Docstring
Returns the current expression result preview text.
.. versionadded:: 3.38
%End
signals:

View File

@ -142,6 +142,44 @@ an expression context for the widget.
create an expression context when required.
%End
void setCustomPreviewGenerator( const QString &label, const QList< QPair< QString, QVariant > > &choices, SIP_PYCALLABLE );
%Docstring
Sets the widget to run using a custom preview generator.
In this mode, the widget will call a callback function to generate a new :py:class:`QgsExpressionContext`
as the previewed object changes. This can be used to provide custom preview values for different
objects (i.e. for objects which aren't vector layer features).
:param label: The label to display for the combo box presenting choices of objects. This should be a representative name, eg "Band" if the widget is showing choices of raster layer bands
:param choices: A list of choices to present to the user. Each choice is a pair of a human-readable label and a QVariant representing the object to preview.
:param previewContextGenerator: A function which takes a QVariant representing the object to preview, and returns a :py:class:`QgsExpressionContext` to use for previewing the object.
.. versionadded:: 3.38
%End
%MethodCode
Py_XINCREF( a2 );
Py_BEGIN_ALLOW_THREADS
sipCpp->setCustomPreviewGenerator( *a0, *a1, [a2]( const QVariant &value )->QgsExpressionContext
{
QgsExpressionContext res;
SIP_BLOCK_THREADS
PyObject *s = sipCallMethod( NULL, a2, "D", &value, sipType_QVariant, NULL );
int state;
int sipIsError = 0;
QgsExpressionContext *t1 = reinterpret_cast<QgsExpressionContext *>( sipConvertToType( s, sipType_QgsExpressionContext, 0, SIP_NOT_NONE, &state, &sipIsError ) );
if ( sipIsError == 0 )
{
res = QgsExpressionContext( *t1 );
}
sipReleaseType( t1, sipType_QgsExpressionContext, state );
SIP_UNBLOCK_THREADS
return res;
} );
Py_END_ALLOW_THREADS
%End
bool allowEvalErrors() const;
%Docstring
Allow accepting expressions with evaluation errors. This can be useful when we are not able to

View File

@ -147,7 +147,7 @@ QgsMeshLayerElevationProperties *QgsMeshLayerElevationProperties::clone() const
return res.release();
}
bool QgsMeshLayerElevationProperties::isVisibleInZRange( const QgsDoubleRange &, QgsMapLayer * ) const
bool QgsMeshLayerElevationProperties::isVisibleInZRange( const QgsDoubleRange &range, QgsMapLayer * ) const
{
switch ( mMode )
{

View File

@ -617,6 +617,11 @@ bool QgsExpressionBuilderWidget::isExpressionValid()
return mExpressionValid;
}
void QgsExpressionBuilderWidget::setCustomPreviewGenerator( const QString &label, const QList<QPair<QString, QVariant> > &choices, const std::function<QgsExpressionContext( const QVariant & )> &previewContextGenerator )
{
mExpressionPreviewWidget->setCustomPreviewGenerator( label, choices, previewContextGenerator );
}
void QgsExpressionBuilderWidget::saveToRecent( const QString &collection )
{
mExpressionTreeView->saveToRecent( expressionText(), collection );

View File

@ -151,6 +151,63 @@ class GUI_EXPORT QgsExpressionBuilderWidget : public QWidget, private Ui::QgsExp
//! Returns if the expression is valid
bool isExpressionValid();
#ifndef SIP_RUN
/**
* Sets the widget to run using a custom preview generator.
*
* In this mode, the widget will call a callback function to generate a new QgsExpressionContext
* as the previewed object changes. This can be used to provide custom preview values for different
* objects (i.e. for objects which aren't vector layer features).
*
* \param label The label to display for the combo box presenting choices of objects. This should be a representative name, eg "Band" if the widget is showing choices of raster layer bands
* \param choices A list of choices to present to the user. Each choice is a pair of a human-readable label and a QVariant representing the object to preview.
* \param previewContextGenerator A function which takes a QVariant representing the object to preview, and returns a QgsExpressionContext to use for previewing the object.
*
* \since QGIS 3.38
*/
void setCustomPreviewGenerator( const QString &label, const QList< QPair< QString, QVariant > > &choices, const std::function< QgsExpressionContext( const QVariant & ) > &previewContextGenerator );
#else
/**
* Sets the widget to run using a custom preview generator.
*
* In this mode, the widget will call a callback function to generate a new QgsExpressionContext
* as the previewed object changes. This can be used to provide custom preview values for different
* objects (i.e. for objects which aren't vector layer features).
*
* \param label The label to display for the combo box presenting choices of objects. This should be a representative name, eg "Band" if the widget is showing choices of raster layer bands
* \param choices A list of choices to present to the user. Each choice is a pair of a human-readable label and a QVariant representing the object to preview.
* \param previewContextGenerator A function which takes a QVariant representing the object to preview, and returns a QgsExpressionContext to use for previewing the object.
*
* \since QGIS 3.38
*/
void setCustomPreviewGenerator( const QString &label, const QList< QPair< QString, QVariant > > &choices, SIP_PYCALLABLE );
% MethodCode
Py_XINCREF( a2 );
Py_BEGIN_ALLOW_THREADS
sipCpp->setCustomPreviewGenerator( *a0, *a1, [a2]( const QVariant &value )->QgsExpressionContext
{
QgsExpressionContext res;
SIP_BLOCK_THREADS
PyObject *s = sipCallMethod( NULL, a2, "D", &value, sipType_QVariant, NULL );
int state;
int sipIsError = 0;
QgsExpressionContext *t1 = reinterpret_cast<QgsExpressionContext *>( sipConvertToType( s, sipType_QgsExpressionContext, 0, SIP_NOT_NONE, &state, &sipIsError ) );
if ( sipIsError == 0 )
{
res = QgsExpressionContext( *t1 );
}
sipReleaseType( t1, sipType_QgsExpressionContext, state );
SIP_UNBLOCK_THREADS
return res;
} );
Py_END_ALLOW_THREADS
% End
#endif
/**
* Adds the current expression to the given \a collection.
* By default it is saved to the collection "generic".

View File

@ -33,10 +33,23 @@ QgsExpressionPreviewWidget::QgsExpressionPreviewWidget( QWidget *parent )
mCopyPreviewAction = new QAction( QgsApplication::getThemeIcon( QStringLiteral( "/mActionEditCopy.svg" ) ), tr( "Copy Expression Value" ), this );
mPreviewLabel->addAction( mCopyPreviewAction );
mFeaturePickerWidget->setShowBrowserButtons( true );
mStackedWidget->setSizeMode( QgsStackedWidget::SizeMode::CurrentPageOnly );
mStackedWidget->setCurrentWidget( mPageFeaturePicker );
mCustomButtonNext->setEnabled( false );
mCustomButtonPrev->setEnabled( false );
connect( mFeaturePickerWidget, &QgsFeaturePickerWidget::featureChanged, this, &QgsExpressionPreviewWidget::setCurrentFeature );
connect( mCustomComboBox, qOverload< int >( &QComboBox::currentIndexChanged ), this, &QgsExpressionPreviewWidget::setCustomChoice );
connect( mPreviewLabel, &QLabel::linkActivated, this, &QgsExpressionPreviewWidget::linkActivated );
connect( mCopyPreviewAction, &QAction::triggered, this, &QgsExpressionPreviewWidget::copyFullExpressionValue );
connect( mCustomButtonPrev, &QToolButton::clicked, this, [this]
{
mCustomComboBox->setCurrentIndex( std::max( 0, mCustomComboBox->currentIndex() - 1 ) );
} );
connect( mCustomButtonNext, &QToolButton::clicked, this, [this]
{
mCustomComboBox->setCurrentIndex( std::min( mCustomComboBox->count() - 1, mCustomComboBox->currentIndex() + 1 ) );
} );
}
void QgsExpressionPreviewWidget::setLayer( QgsVectorLayer *layer )
@ -48,6 +61,21 @@ void QgsExpressionPreviewWidget::setLayer( QgsVectorLayer *layer )
}
}
void QgsExpressionPreviewWidget::setCustomPreviewGenerator( const QString &label, const QList<QPair<QString, QVariant> > &choices, const std::function< QgsExpressionContext( const QVariant & ) > &previewContextGenerator )
{
mCustomPreviewGeneratorFunction = previewContextGenerator;
mStackedWidget->setCurrentWidget( mPageCustomPicker );
mCustomLabel->setText( label );
mCustomComboBox->blockSignals( true );
mCustomComboBox->clear();
for ( const auto &choice : choices )
{
mCustomComboBox->addItem( choice.first, choice.second );
}
mCustomComboBox->blockSignals( false );
setCustomChoice( 0 );
}
void QgsExpressionPreviewWidget::setExpressionText( const QString &expression )
{
if ( expression != mExpressionText )
@ -198,6 +226,11 @@ bool QgsExpressionPreviewWidget::parserError() const
return mParserError;
}
QString QgsExpressionPreviewWidget::currentPreviewText() const
{
return mPreviewLabel->text();
}
void QgsExpressionPreviewWidget::setEvalError( bool evalError )
{
if ( evalError == mEvalError )
@ -220,3 +253,15 @@ void QgsExpressionPreviewWidget::copyFullExpressionValue()
QgsDebugMsgLevel( QStringLiteral( "set clipboard: %1" ).arg( copiedValue ), 4 );
clipboard->setText( copiedValue );
}
void QgsExpressionPreviewWidget::setCustomChoice( int )
{
const QVariant selectedValue = mCustomComboBox->currentData();
mCustomButtonPrev->setEnabled( mCustomComboBox->currentIndex() > 0 && mCustomComboBox->count() > 0 );
mCustomButtonNext->setEnabled( mCustomComboBox->currentIndex() < ( mCustomComboBox->count() - 1 ) && mCustomComboBox->count() > 0 );
mExpressionContext = mCustomPreviewGeneratorFunction( selectedValue );
refreshPreview();
}

View File

@ -25,6 +25,8 @@
#include "qgsexpression.h"
#include "qgsexpressioncontext.h"
#include <functional>
class QAction;
class QgsVectorLayer;
@ -44,6 +46,62 @@ class GUI_EXPORT QgsExpressionPreviewWidget : public QWidget, private Ui::QgsExp
//! Sets the layer used in the preview
void setLayer( QgsVectorLayer *layer );
#ifndef SIP_RUN
/**
* Sets the widget to run using a custom preview generator.
*
* In this mode, the widget will call a callback function to generate a new QgsExpressionContext
* as the previewed object changes. This can be used to provide custom preview values for different
* objects (i.e. for objects which aren't vector layer features).
*
* \param label The label to display for the combo box presenting choices of objects. This should be a representative name, eg "Band" if the widget is showing choices of raster layer bands
* \param choices A list of choices to present to the user. Each choice is a pair of a human-readable label and a QVariant representing the object to preview.
* \param previewContextGenerator A function which takes a QVariant representing the object to preview, and returns a QgsExpressionContext to use for previewing the object.
*
* \since QGIS 3.38
*/
void setCustomPreviewGenerator( const QString &label, const QList< QPair< QString, QVariant > > &choices, const std::function< QgsExpressionContext( const QVariant & ) > &previewContextGenerator );
#else
/**
* Sets the widget to run using a custom preview generator.
*
* In this mode, the widget will call a callback function to generate a new QgsExpressionContext
* as the previewed object changes. This can be used to provide custom preview values for different
* objects (i.e. for objects which aren't vector layer features).
*
* \param label The label to display for the combo box presenting choices of objects. This should be a representative name, eg "Band" if the widget is showing choices of raster layer bands
* \param choices A list of choices to present to the user. Each choice is a pair of a human-readable label and a QVariant representing the object to preview.
* \param previewContextGenerator A function which takes a QVariant representing the object to preview, and returns a QgsExpressionContext to use for previewing the object.
*
* \since QGIS 3.38
*/
void setCustomPreviewGenerator( const QString &label, const QList< QPair< QString, QVariant > > &choices, SIP_PYCALLABLE );
% MethodCode
Py_XINCREF( a2 );
Py_BEGIN_ALLOW_THREADS
sipCpp->setCustomPreviewGenerator( *a0, *a1, [a2]( const QVariant &value )->QgsExpressionContext
{
QgsExpressionContext res;
SIP_BLOCK_THREADS
PyObject *s = sipCallMethod( NULL, a2, "D", &value, sipType_QVariant, NULL );
int state;
int sipIsError = 0;
QgsExpressionContext *t1 = reinterpret_cast<QgsExpressionContext *>( sipConvertToType( s, sipType_QgsExpressionContext, 0, SIP_NOT_NONE, &state, &sipIsError ) );
if ( sipIsError == 0 )
{
res = QgsExpressionContext( *t1 );
}
sipReleaseType( t1, sipType_QgsExpressionContext, state );
SIP_UNBLOCK_THREADS
return res;
} );
Py_END_ALLOW_THREADS
% End
#endif
//! Sets the expression
void setExpressionText( const QString &expression );
@ -82,6 +140,13 @@ class GUI_EXPORT QgsExpressionPreviewWidget : public QWidget, private Ui::QgsExp
//! Returns the expression parser errors
QList<QgsExpression::ParserError> parserErrors() const {return mExpression.parserErrors();}
/**
* Returns the current expression result preview text.
*
* \since QGIS 3.38
*/
QString currentPreviewText() const;
signals:
/**
@ -117,6 +182,7 @@ class GUI_EXPORT QgsExpressionPreviewWidget : public QWidget, private Ui::QgsExp
void setEvalError( bool evalError );
void setParserError( bool parserError );
void copyFullExpressionValue();
void setCustomChoice( int );
private:
void setExpressionToolTip( const QString &toolTip );
@ -132,6 +198,8 @@ class GUI_EXPORT QgsExpressionPreviewWidget : public QWidget, private Ui::QgsExp
QString mExpressionText;
QgsExpression mExpression;
QAction *mCopyPreviewAction = nullptr;
std::function< QgsExpressionContext( const QVariant & ) > mCustomPreviewGeneratorFunction;
};
#endif // QGSEXPRESSIONPREVIEWWIDGET_H

View File

@ -162,6 +162,13 @@ void QgsFieldExpressionWidget::registerExpressionContextGenerator( const QgsExpr
mExpressionContextGenerator = generator;
}
void QgsFieldExpressionWidget::setCustomPreviewGenerator( const QString &label, const QList<QPair<QString, QVariant> > &choices, const std::function<QgsExpressionContext( const QVariant & )> &previewContextGenerator )
{
mCustomPreviewLabel = label;
mCustomChoices = choices;
mPreviewContextGenerator = previewContextGenerator;
}
void QgsFieldExpressionWidget::setLayer( QgsMapLayer *layer )
{
QgsVectorLayer *vl = qobject_cast< QgsVectorLayer * >( layer );
@ -243,6 +250,11 @@ void QgsFieldExpressionWidget::editExpression()
dlg.setWindowTitle( mExpressionDialogTitle );
dlg.setAllowEvalErrors( mAllowEvalErrors );
if ( !mCustomChoices.isEmpty() )
{
dlg.expressionBuilder()->setCustomPreviewGenerator( mCustomPreviewLabel, mCustomChoices, mPreviewContextGenerator );
}
if ( !vl )
dlg.expressionBuilder()->expressionTree()->loadFieldNames( mFieldProxyModel->sourceFieldModel()->fields() );

View File

@ -152,6 +152,62 @@ class GUI_EXPORT QgsFieldExpressionWidget : public QWidget
*/
void registerExpressionContextGenerator( const QgsExpressionContextGenerator *generator );
#ifndef SIP_RUN
/**
* Sets the widget to run using a custom preview generator.
*
* In this mode, the widget will call a callback function to generate a new QgsExpressionContext
* as the previewed object changes. This can be used to provide custom preview values for different
* objects (i.e. for objects which aren't vector layer features).
*
* \param label The label to display for the combo box presenting choices of objects. This should be a representative name, eg "Band" if the widget is showing choices of raster layer bands
* \param choices A list of choices to present to the user. Each choice is a pair of a human-readable label and a QVariant representing the object to preview.
* \param previewContextGenerator A function which takes a QVariant representing the object to preview, and returns a QgsExpressionContext to use for previewing the object.
*
* \since QGIS 3.38
*/
void setCustomPreviewGenerator( const QString &label, const QList< QPair< QString, QVariant > > &choices, const std::function< QgsExpressionContext( const QVariant & ) > &previewContextGenerator );
#else
/**
* Sets the widget to run using a custom preview generator.
*
* In this mode, the widget will call a callback function to generate a new QgsExpressionContext
* as the previewed object changes. This can be used to provide custom preview values for different
* objects (i.e. for objects which aren't vector layer features).
*
* \param label The label to display for the combo box presenting choices of objects. This should be a representative name, eg "Band" if the widget is showing choices of raster layer bands
* \param choices A list of choices to present to the user. Each choice is a pair of a human-readable label and a QVariant representing the object to preview.
* \param previewContextGenerator A function which takes a QVariant representing the object to preview, and returns a QgsExpressionContext to use for previewing the object.
*
* \since QGIS 3.38
*/
void setCustomPreviewGenerator( const QString &label, const QList< QPair< QString, QVariant > > &choices, SIP_PYCALLABLE );
% MethodCode
Py_XINCREF( a2 );
Py_BEGIN_ALLOW_THREADS
sipCpp->setCustomPreviewGenerator( *a0, *a1, [a2]( const QVariant &value )->QgsExpressionContext
{
QgsExpressionContext res;
SIP_BLOCK_THREADS
PyObject *s = sipCallMethod( NULL, a2, "D", &value, sipType_QVariant, NULL );
int state;
int sipIsError = 0;
QgsExpressionContext *t1 = reinterpret_cast<QgsExpressionContext *>( sipConvertToType( s, sipType_QgsExpressionContext, 0, SIP_NOT_NONE, &state, &sipIsError ) );
if ( sipIsError == 0 )
{
res = QgsExpressionContext( *t1 );
}
sipReleaseType( t1, sipType_QgsExpressionContext, state );
SIP_UNBLOCK_THREADS
return res;
} );
Py_END_ALLOW_THREADS
% End
#endif
/**
* Allow accepting expressions with evaluation errors. This can be useful when we are not able to
* provide an expression context of which we are sure it's completely populated.
@ -275,6 +331,10 @@ class GUI_EXPORT QgsFieldExpressionWidget : public QWidget
QString mBackupExpression;
bool mAllowEvalErrors = false;
QString mCustomPreviewLabel;
QList< QPair< QString, QVariant > > mCustomChoices;
std::function< QgsExpressionContext( const QVariant & ) > mPreviewContextGenerator;
friend class TestQgsFieldExpressionWidget;
};

View File

@ -7,7 +7,7 @@
<x>0</x>
<y>0</y>
<width>400</width>
<height>67</height>
<height>50</height>
</rect>
</property>
<property name="windowTitle">
@ -65,27 +65,112 @@
</widget>
</item>
<item row="0" column="0" colspan="2">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="label_2">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
<widget class="QgsStackedWidget" name="mStackedWidget">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Maximum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="currentIndex">
<number>1</number>
</property>
<widget class="QWidget" name="mPageFeaturePicker">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Maximum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<property name="leftMargin">
<number>0</number>
</property>
<property name="toolTip">
<string>Select the feature to use for the output preview</string>
<property name="topMargin">
<number>0</number>
</property>
<property name="text">
<string>Feature</string>
<property name="rightMargin">
<number>0</number>
</property>
</widget>
</item>
<item>
<widget class="QgsFeaturePickerWidget" name="mFeaturePickerWidget"/>
</item>
</layout>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="label_2">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="toolTip">
<string>Select the feature to use for the output preview</string>
</property>
<property name="text">
<string>Feature</string>
</property>
</widget>
</item>
<item>
<widget class="QgsFeaturePickerWidget" name="mFeaturePickerWidget"/>
</item>
</layout>
</widget>
<widget class="QWidget" name="mPageCustomPicker">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Maximum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<layout class="QHBoxLayout" name="horizontalLayout" stretch="0,1,0,0">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="mCustomLabel">
<property name="text">
<string>Result</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="mCustomComboBox"/>
</item>
<item>
<widget class="QToolButton" name="mCustomButtonPrev">
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset resource="../../images/images.qrc">
<normaloff>:/images/themes/default/mActionArrowLeft.svg</normaloff>:/images/themes/default/mActionArrowLeft.svg</iconset>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="mCustomButtonNext">
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset resource="../../images/images.qrc">
<normaloff>:/images/themes/default/mActionArrowRight.svg</normaloff>:/images/themes/default/mActionArrowRight.svg</iconset>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</widget>
@ -95,7 +180,15 @@
<extends>QComboBox</extends>
<header>qgsfeaturepickerwidget.h</header>
</customwidget>
<customwidget>
<class>QgsStackedWidget</class>
<extends>QStackedWidget</extends>
<header>qgsstackedwidget.h</header>
<container>1</container>
</customwidget>
</customwidgets>
<resources/>
<resources>
<include location="../../images/images.qrc"/>
</resources>
<connections/>
</ui>

View File

@ -68,6 +68,7 @@ ADD_PYTHON_TEST(PyQgsEllipsoidUtils test_qgsellipsoidutils.py)
ADD_PYTHON_TEST(PyQgsEmbeddedSymbolRenderer test_qgsembeddedsymbolrenderer.py)
ADD_PYTHON_TEST(PyQgsExifTools test_qgsexiftools.py)
ADD_PYTHON_TEST(PyQgsExpression test_qgsexpression.py)
ADD_PYTHON_TEST(PyQgsExpressionPreviewWidget test_qgsexpressionpreviewwidget.py)
ADD_PYTHON_TEST(PyQgsExternalStorageWebDav test_qgsexternalstorage_webdav.py)
ADD_PYTHON_TEST(PyQgsExternalStorageAwsS3 test_qgsexternalstorage_awss3.py)
ADD_PYTHON_TEST(PyQgsFeature test_qgsfeature.py)

View File

@ -0,0 +1,67 @@
"""QGIS Unit tests for QgsExpressionPreviewWidget
.. note:: This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
"""
from qgis.PyQt.QtWidgets import QToolButton
from qgis.gui import QgsExpressionPreviewWidget
from qgis.core import (
QgsExpressionContext,
QgsExpressionContextScope
)
import unittest
from qgis.testing import start_app, QgisTestCase
start_app()
class TestQgsExpressionPreviewWidget(QgisTestCase):
def test_custom_mode(self):
"""
Test using a custom preview generator with the widget
"""
def make_context(value):
res = QgsExpressionContext()
scope = QgsExpressionContextScope()
scope.setVariable('test', value)
scope.setVariable('test2', value * 2)
res.appendScope(scope)
return res
w = QgsExpressionPreviewWidget()
w.setCustomPreviewGenerator('Band',
[['Band 1', 1], ['Band 2', 2], ['Band 3', 3]],
make_context)
w.setExpressionText("@test * 5")
self.assertEqual(w.currentPreviewText(), '5')
w.setExpressionText("@test2 * 5")
self.assertEqual(w.currentPreviewText(), '10')
next_button = w.findChild(QToolButton, 'mCustomButtonNext')
prev_button = w.findChild(QToolButton, 'mCustomButtonPrev')
self.assertFalse(prev_button.isEnabled())
self.assertTrue(next_button.isEnabled())
next_button.click()
self.assertEqual(w.currentPreviewText(), '20')
self.assertTrue(prev_button.isEnabled())
self.assertTrue(next_button.isEnabled())
next_button.click()
self.assertEqual(w.currentPreviewText(), '30')
self.assertTrue(prev_button.isEnabled())
self.assertFalse(next_button.isEnabled())
prev_button.click()
self.assertEqual(w.currentPreviewText(), '20')
self.assertTrue(prev_button.isEnabled())
self.assertTrue(next_button.isEnabled())
prev_button.click()
self.assertEqual(w.currentPreviewText(), '10')
self.assertFalse(prev_button.isEnabled())
self.assertTrue(next_button.isEnabled())
if __name__ == '__main__':
unittest.main()