Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Ensure context is available to builders from data defined buttons
  • Loading branch information
nyalldawson committed Aug 22, 2015
1 parent bfc8f56 commit a7d8519
Show file tree
Hide file tree
Showing 15 changed files with 203 additions and 53 deletions.
2 changes: 1 addition & 1 deletion python/core/qgsexpressioncontext.sip
Expand Up @@ -400,7 +400,7 @@ class QgsExpressionContextUtils
/** Creates a new scope which contains variables and functions relating to a QgsMapLayer.
* For instance, layer name, id and fields.
*/
static QgsExpressionContextScope* layerScope( QgsMapLayer* layer ) /Factory/;
static QgsExpressionContextScope* layerScope( const QgsMapLayer* layer ) /Factory/;

/** Helper function for creating an expression context which contains just a feature and fields
* collection. Generally this method should not be used as the created context does not include
Expand Down
12 changes: 12 additions & 0 deletions python/gui/qgsdatadefinedbutton.sip
Expand Up @@ -168,6 +168,18 @@ class QgsDataDefinedButton : QToolButton
*/
void clearCheckedWidgets();

//! Callback function for retrieving the expression context for the button
//typedef QgsExpressionContext( *ExpressionContextCallback )( const void* context );

/** Register callback function for retrieving the expression context for the button
* @param fnGetExpressionContext call back function, will be called when the data defined
* button requires the current expression context
* @param context context for callback function
* @note added in QGIS 2.12
* @note not available in Python bindings
*/
//void registerGetExpressionContextCallback( ExpressionContextCallback fnGetExpressionContext, const void* context );

/**
* Sets an assistant used to define the data defined object properties.
* Ownership of the assistant is transferred to the widget.
Expand Down
14 changes: 14 additions & 0 deletions src/app/composer/qgscomposerhtmlwidget.cpp
Expand Up @@ -482,13 +482,27 @@ QgsComposerItem::DataDefinedProperty QgsComposerHtmlWidget::ddPropertyForWidget(
return QgsComposerItem::NoProperty;
}

static QgsExpressionContext _getExpressionContext( const void* context )
{
const QgsComposerObject* composerObject = ( const QgsComposerObject* ) context;
if ( !composerObject )
{
return QgsExpressionContext();
}

QScopedPointer< QgsExpressionContext > expContext( composerObject->createExpressionContext() );
return QgsExpressionContext( *expContext );
}

void QgsComposerHtmlWidget::populateDataDefinedButtons()
{
QgsVectorLayer* vl = atlasCoverageLayer();

//block signals from data defined buttons
mUrlDDBtn->blockSignals( true );

mUrlDDBtn->registerGetExpressionContextCallback( &_getExpressionContext, mHtml );

//initialise buttons to use atlas coverage layer
mUrlDDBtn->init( vl, mHtml->dataDefinedProperty( QgsComposerItem::SourceUrl ),
QgsDataDefinedButton::AnyType, tr( "url string" ) );
Expand Down
38 changes: 21 additions & 17 deletions src/app/composer/qgscomposeritemwidget.cpp
Expand Up @@ -550,19 +550,27 @@ void QgsComposerItemWidget::setValuesForGuiNonPositionElements()
mExcludeFromPrintsCheckBox->blockSignals( false );
}

static QgsExpressionContext _getExpressionContext( const void* context )
{
const QgsComposerObject* composerObject = ( const QgsComposerObject* ) context;
if ( !composerObject )
{
return QgsExpressionContext();
}

QScopedPointer< QgsExpressionContext > expContext( composerObject->createExpressionContext() );
return QgsExpressionContext( *expContext );
}

void QgsComposerItemWidget::populateDataDefinedButtons()
{
QgsVectorLayer* vl = atlasCoverageLayer();

//block signals from data defined buttons
mXPositionDDBtn->blockSignals( true );
mYPositionDDBtn->blockSignals( true );
mWidthDDBtn->blockSignals( true );
mHeightDDBtn->blockSignals( true );
mItemRotationDDBtn->blockSignals( true );
mTransparencyDDBtn->blockSignals( true );
mBlendModeDDBtn->blockSignals( true );
mExcludePrintsDDBtn->blockSignals( true );
Q_FOREACH ( QgsDataDefinedButton* button, findChildren< QgsDataDefinedButton* >() )
{
button->blockSignals( true );
button->registerGetExpressionContextCallback( &_getExpressionContext, mItem );
}

//initialise buttons to use atlas coverage layer
mXPositionDDBtn->init( vl, mItem->dataDefinedProperty( QgsComposerObject::PositionX ),
Expand All @@ -583,14 +591,10 @@ void QgsComposerItemWidget::populateDataDefinedButtons()
QgsDataDefinedButton::String, QgsDataDefinedButton::boolDesc() );

//unblock signals from data defined buttons
mXPositionDDBtn->blockSignals( false );
mYPositionDDBtn->blockSignals( false );
mWidthDDBtn->blockSignals( false );
mHeightDDBtn->blockSignals( false );
mItemRotationDDBtn->blockSignals( false );
mTransparencyDDBtn->blockSignals( false );
mBlendModeDDBtn->blockSignals( false );
mExcludePrintsDDBtn->blockSignals( false );
Q_FOREACH ( QgsDataDefinedButton* button, findChildren< QgsDataDefinedButton* >() )
{
button->blockSignals( false );
}
}

QgsComposerObject::DataDefinedProperty QgsComposerItemWidget::ddPropertyForWidget( QgsDataDefinedButton* widget )
Expand Down
41 changes: 21 additions & 20 deletions src/app/composer/qgscomposermapwidget.cpp
Expand Up @@ -188,20 +188,27 @@ QgsComposerMapWidget::~QgsComposerMapWidget()
{
}

static QgsExpressionContext _getExpressionContext( const void* context )
{
const QgsComposerObject* composerObject = ( const QgsComposerObject* ) context;
if ( !composerObject )
{
return QgsExpressionContext();
}

QScopedPointer< QgsExpressionContext > expContext( composerObject->createExpressionContext() );
return QgsExpressionContext( *expContext );
}

void QgsComposerMapWidget::populateDataDefinedButtons()
{
QgsVectorLayer* vl = atlasCoverageLayer();

//block signals from data defined buttons
mScaleDDBtn->blockSignals( true );
mMapRotationDDBtn->blockSignals( true );
mXMinDDBtn->blockSignals( true );
mYMinDDBtn->blockSignals( true );
mXMaxDDBtn->blockSignals( true );
mYMaxDDBtn->blockSignals( true );
mAtlasMarginDDBtn->blockSignals( true );
mStylePresetsDDBtn->blockSignals( true );
mLayersDDBtn->blockSignals( true );
Q_FOREACH ( QgsDataDefinedButton* button, findChildren< QgsDataDefinedButton* >() )
{
button->blockSignals( true );
button->registerGetExpressionContextCallback( &_getExpressionContext, mComposerMap );
}

//initialise buttons to use atlas coverage layer
mScaleDDBtn->init( vl, mComposerMap->dataDefinedProperty( QgsComposerObject::MapScale ),
Expand All @@ -223,16 +230,10 @@ void QgsComposerMapWidget::populateDataDefinedButtons()
mLayersDDBtn->init( vl, mComposerMap->dataDefinedProperty( QgsComposerObject::MapLayers ),
QgsDataDefinedButton::String, tr( "list of map layer names separated by | characters" ) );

//unblock signals from data defined buttons
mScaleDDBtn->blockSignals( false );
mMapRotationDDBtn->blockSignals( false );
mXMinDDBtn->blockSignals( false );
mYMinDDBtn->blockSignals( false );
mXMaxDDBtn->blockSignals( false );
mYMaxDDBtn->blockSignals( false );
mAtlasMarginDDBtn->blockSignals( false );
mStylePresetsDDBtn->blockSignals( false );
mLayersDDBtn->blockSignals( false );
Q_FOREACH ( QgsDataDefinedButton* button, findChildren< QgsDataDefinedButton* >() )
{
button->blockSignals( false );
}
}

QgsComposerObject::DataDefinedProperty QgsComposerMapWidget::ddPropertyForWidget( QgsDataDefinedButton* widget )
Expand Down
14 changes: 14 additions & 0 deletions src/app/composer/qgscomposerpicturewidget.cpp
Expand Up @@ -609,13 +609,27 @@ QgsComposerObject::DataDefinedProperty QgsComposerPictureWidget::ddPropertyForWi
return QgsComposerObject::NoProperty;
}

static QgsExpressionContext _getExpressionContext( const void* context )
{
const QgsComposerObject* composerObject = ( const QgsComposerObject* ) context;
if ( !composerObject )
{
return QgsExpressionContext();
}

QScopedPointer< QgsExpressionContext > expContext( composerObject->createExpressionContext() );
return QgsExpressionContext( *expContext );
}

void QgsComposerPictureWidget::populateDataDefinedButtons()
{
QgsVectorLayer* vl = atlasCoverageLayer();

//block signals from data defined buttons
mSourceDDBtn->blockSignals( true );

mSourceDDBtn->registerGetExpressionContextCallback( &_getExpressionContext, mPicture );

//initialise buttons to use atlas coverage layer
mSourceDDBtn->init( vl, mPicture->dataDefinedProperty( QgsComposerObject::PictureSource ),
QgsDataDefinedButton::AnyType, QgsDataDefinedButton::anyStringDesc() );
Expand Down
31 changes: 21 additions & 10 deletions src/app/composer/qgscompositionwidget.cpp
Expand Up @@ -137,6 +137,18 @@ QgsCompositionWidget::~QgsCompositionWidget()

}

static QgsExpressionContext _getExpressionContext( const void* context )
{
const QgsComposition* composition = ( const QgsComposition* ) context;
if ( !composition )
{
return QgsExpressionContext();
}

QScopedPointer< QgsExpressionContext > expContext( composition->createExpressionContext() );
return QgsExpressionContext( *expContext );
}

void QgsCompositionWidget::populateDataDefinedButtons()
{
if ( !mComposition )
Expand All @@ -152,11 +164,11 @@ void QgsCompositionWidget::populateDataDefinedButtons()
vl = atlas->coverageLayer();
}

mPaperSizeDDBtn->blockSignals( true );
mPaperWidthDDBtn->blockSignals( true );
mPaperHeightDDBtn->blockSignals( true );
mNumPagesDDBtn->blockSignals( true );
mPaperOrientationDDBtn->blockSignals( true );
Q_FOREACH ( QgsDataDefinedButton* button, findChildren< QgsDataDefinedButton* >() )
{
button->blockSignals( true );
button->registerGetExpressionContextCallback( &_getExpressionContext, mComposition );
}

mPaperSizeDDBtn->init( vl, mComposition->dataDefinedProperty( QgsComposerObject::PresetPaperSize ),
QgsDataDefinedButton::String, QgsDataDefinedButton::paperSizeDesc() );
Expand All @@ -172,11 +184,10 @@ void QgsCompositionWidget::populateDataDefinedButtons()
//initial state of controls - disable related controls when dd buttons are active
mPaperSizeComboBox->setEnabled( !mPaperSizeDDBtn->isActive() );

mPaperSizeDDBtn->blockSignals( false );
mPaperWidthDDBtn->blockSignals( false );
mPaperHeightDDBtn->blockSignals( false );
mNumPagesDDBtn->blockSignals( false );
mPaperOrientationDDBtn->blockSignals( false );
Q_FOREACH ( QgsDataDefinedButton* button, findChildren< QgsDataDefinedButton* >() )
{
button->blockSignals( false );
}
}

void QgsCompositionWidget::variablesChanged()
Expand Down
19 changes: 19 additions & 0 deletions src/app/qgslabelinggui.cpp
Expand Up @@ -872,14 +872,33 @@ void QgsLabelingGui::setDataDefinedProperty( const QgsDataDefinedButton* ddBtn,
lyr.setDataDefinedProperty( p, map.value( "active" ).toInt(), map.value( "useexpr" ).toInt(), map.value( "expression" ), map.value( "field" ) );
}

static QgsExpressionContext _getExpressionContext( const void* context )
{
QgsExpressionContext expContext;
expContext << QgsExpressionContextUtils::globalScope()
<< QgsExpressionContextUtils::projectScope();

const QgsVectorLayer* layer = ( const QgsVectorLayer* ) context;
if ( layer )
expContext << QgsExpressionContextUtils::layerScope( layer );

return expContext;
}

void QgsLabelingGui::populateDataDefinedButtons( QgsPalLayerSettings& s )
{
Q_FOREACH ( QgsDataDefinedButton* button, findChildren< QgsDataDefinedButton* >() )
{
button->registerGetExpressionContextCallback( &_getExpressionContext, mLayer );
}

// don't register enable/disable siblings, since visual feedback from data defined buttons should be enough,
// and ability to edit layer-level setting should remain enabled regardless

QString trString = tr( "string " );

// text style

mFontDDBtn->init( mLayer, s.dataDefinedProperty( QgsPalLayerSettings::Family ),
QgsDataDefinedButton::String,
trString + tr( "[<b>family</b>|<b>family[foundry]</b>],<br>"
Expand Down
8 changes: 5 additions & 3 deletions src/core/qgsexpressioncontext.cpp
Expand Up @@ -527,7 +527,7 @@ void QgsExpressionContextUtils::setProjectVariables( const QgsStringMap &variabl
project->writeEntry( "Variables", "/variableValues", variableValues );
}

QgsExpressionContextScope* QgsExpressionContextUtils::layerScope( QgsMapLayer* layer )
QgsExpressionContextScope* QgsExpressionContextUtils::layerScope( const QgsMapLayer* layer )
{
QgsExpressionContextScope* scope = new QgsExpressionContextScope( QObject::tr( "Layer" ) );

Expand Down Expand Up @@ -566,11 +566,13 @@ QgsExpressionContextScope* QgsExpressionContextUtils::layerScope( QgsMapLayer* l
scope->addVariable( QgsExpressionContextScope::StaticVariable( "layer_crs", layer->crs().authid(), true ) );
scope->addVariable( QgsExpressionContextScope::StaticVariable( "layer_crsdefinition", layer->crs().toProj4(), true ) );

QgsGeometry* extentGeom = QgsGeometry::fromRect( layer->extent() );
//some methods we want aren't const
QgsMapLayer* nonConstLayer = const_cast< QgsMapLayer* >( layer );
QgsGeometry* extentGeom = QgsGeometry::fromRect( nonConstLayer->extent() );
scope->addVariable( QgsExpressionContextScope::StaticVariable( "layer_extent", QVariant::fromValue( *extentGeom ), true ) );
delete extentGeom;

QgsVectorLayer* vLayer = dynamic_cast< QgsVectorLayer* >( layer );
QgsVectorLayer* vLayer = dynamic_cast< QgsVectorLayer* >( nonConstLayer );
if ( vLayer )
{
scope->addVariable( QgsExpressionContextScope::StaticVariable( "layer_geometrytype", vLayer->type(), true ) );
Expand Down
2 changes: 1 addition & 1 deletion src/core/qgsexpressioncontext.h
Expand Up @@ -435,7 +435,7 @@ class CORE_EXPORT QgsExpressionContextUtils
/** Creates a new scope which contains variables and functions relating to a QgsMapLayer.
* For instance, layer name, id and fields.
*/
static QgsExpressionContextScope* layerScope( QgsMapLayer* layer );
static QgsExpressionContextScope* layerScope( const QgsMapLayer *layer );

/** Sets a layer context variable. This variable will be contained within scopes retrieved via
* layerScope().
Expand Down
12 changes: 11 additions & 1 deletion src/gui/qgsdatadefinedbutton.cpp
Expand Up @@ -41,6 +41,8 @@ QgsDataDefinedButton::QgsDataDefinedButton( QWidget* parent,
DataTypes datatypes,
QString description )
: QToolButton( parent )
, mExpressionContextCallback( 0 )
, mExpressionContextCallbackContext( 0 )
{
// set up static icons
if ( mIconDataDefine.isNull() )
Expand Down Expand Up @@ -452,7 +454,9 @@ void QgsDataDefinedButton::showAssistant()

void QgsDataDefinedButton::showExpressionDialog()
{
QgsExpressionBuilderDialog d( const_cast<QgsVectorLayer*>( mVectorLayer ), getExpression() );
QgsExpressionContext context = mExpressionContextCallback ? mExpressionContextCallback( mExpressionContextCallbackContext ) : QgsExpressionContext();

QgsExpressionBuilderDialog d( const_cast<QgsVectorLayer*>( mVectorLayer ), getExpression(), this, "generic", context );
if ( d.exec() == QDialog::Accepted )
{
QString newExp = d.expressionText();
Expand Down Expand Up @@ -632,6 +636,12 @@ QList<QWidget*> QgsDataDefinedButton::registeredCheckedWidgets()
return wdgtList;
}

void QgsDataDefinedButton::registerGetExpressionContextCallback( QgsDataDefinedButton::ExpressionContextCallback fnGetExpressionContext, const void *context )
{
mExpressionContextCallback = fnGetExpressionContext;
mExpressionContextCallbackContext = context;
}

void QgsDataDefinedButton::setAssistant( const QString& title, QgsDataDefinedAssistant *assistant )
{
mActionAssistant->setText( title.isEmpty() ? tr( "Assistant..." ) : title );
Expand Down
16 changes: 16 additions & 0 deletions src/gui/qgsdatadefinedbutton.h
Expand Up @@ -21,6 +21,7 @@
#include <QPointer>
#include <QToolButton>
#include <QScopedPointer>
#include "qgsexpressioncontext.h"

class QgsVectorLayer;
class QgsDataDefined;
Expand Down Expand Up @@ -194,6 +195,18 @@ class GUI_EXPORT QgsDataDefinedButton: public QToolButton
*/
void clearCheckedWidgets() { mCheckedWidgets.clear(); }

//! Callback function for retrieving the expression context for the button
typedef QgsExpressionContext( *ExpressionContextCallback )( const void* context );

/** Register callback function for retrieving the expression context for the button
* @param fnGetExpressionContext call back function, will be called when the data defined
* button requires the current expression context
* @param context context for callback function
* @note added in QGIS 2.12
* @note not available in Python bindings
*/
void registerGetExpressionContextCallback( ExpressionContextCallback fnGetExpressionContext, const void* context );

/**
* Sets an assistant used to define the data defined object properties.
* Ownership of the assistant is transferred to the widget.
Expand Down Expand Up @@ -332,6 +345,9 @@ class GUI_EXPORT QgsDataDefinedButton: public QToolButton
static QIcon mIconDataDefineExpressionOn;
static QIcon mIconDataDefineExpressionError;

ExpressionContextCallback mExpressionContextCallback;
const void* mExpressionContextCallbackContext;

private slots:
void aboutToShowMenu();
void menuActionTriggered( QAction* action );
Expand Down

5 comments on commit a7d8519

@m-kuhn
Copy link
Member

@m-kuhn m-kuhn commented on a7d8519 Dec 10, 2015

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nyalldawson I have trouble understanding why the callback is required. Does the context change through the lifetime of these buttons?

I wonder because in the geometry generator config widget there's an expression widget which does not have the same "on demand popup" semantics like the button and it would benefit from a setter approach.

@nyalldawson
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@m-kuhn

Does the context change through the lifetime of these buttons?

yes, it can do. An example would be adding/changing a layer variable from the layer properties window.

I'm not sure what the best approach would be for an embedded expression widget. I can't think of a signal based approach that would work cleanly (ideas welcome!).

Best I can think of would be if the function tree was updated when the widget is focused, but that seems very hacky....

@m-kuhn
Copy link
Member

@m-kuhn m-kuhn commented on a7d8519 Dec 10, 2015

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just thinking out loud:

Let's say each scope has a signal expressionContextChanged (app?, project, layer, symbol...)

Then the layer does connect( project, SIGNAL(expressionContextChanged), this, SIGNAL(expressionContextChanged)), the symbol does connect( layer, SIGNAL(expressionContextChanged), this, SIGNAL(expressionContextChanged))

The layer can add additional ones like connect( this, SIGNAL(fieldsChanged), this, SIGNAL(expressionContextChanged))

Each scope can be cached on the appropriate level (already done?) on first request after being marked dirty by the appropriate signal.

@nyalldawson
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was trying to avoid the expense of making scopes/contexts a QObject, if at all possible...

@m-kuhn
Copy link
Member

@m-kuhn m-kuhn commented on a7d8519 Dec 10, 2015

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, but as far as I can see, there's always an equivalent to a scope which is a QObject. QgsProject, QgsVectorLayer...

Please sign in to comment.