diff --git a/images/images.qrc b/images/images.qrc
index b17e1a98e44..253cc9eadb4 100644
--- a/images/images.qrc
+++ b/images/images.qrc
@@ -177,6 +177,7 @@
themes/default/mActionDistributeTop.svg
themes/default/mActionDistributeVCenter.svg
themes/default/mActionDistributeVSpace.svg
+ themes/default/mActionAddExpression.svg
themes/default/mActionAddLayer.svg
themes/default/mActionAddMeshLayer.svg
themes/default/mActionAddAllToOverview.svg
diff --git a/images/themes/default/mActionAddExpression.svg b/images/themes/default/mActionAddExpression.svg
new file mode 100644
index 00000000000..b1ea2c68c21
--- /dev/null
+++ b/images/themes/default/mActionAddExpression.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/python/core/auto_generated/layertree/qgslayertreelayer.sip.in b/python/core/auto_generated/layertree/qgslayertreelayer.sip.in
index 6bf20f5523a..589f5da5068 100644
--- a/python/core/auto_generated/layertree/qgslayertreelayer.sip.in
+++ b/python/core/auto_generated/layertree/qgslayertreelayer.sip.in
@@ -122,6 +122,20 @@ Also resolves textual references to layers from the project (calls resolveRefere
Resolves reference to layer from stored layer ID (if it has not been resolved already)
.. versionadded:: 3.0
+%End
+
+ void setLabelExpression( const QString &expression );
+%Docstring
+set the expression to evaluate
+
+.. versionadded:: 3.10
+%End
+
+ QString labelExpression() const;
+%Docstring
+Returns the expression member of the LayerTreeNode
+
+.. versionadded:: 3.10
%End
signals:
diff --git a/python/core/auto_generated/layertree/qgslayertreemodellegendnode.sip.in b/python/core/auto_generated/layertree/qgslayertreemodellegendnode.sip.in
index 4f59c513d94..0bd3fdccad7 100644
--- a/python/core/auto_generated/layertree/qgslayertreemodellegendnode.sip.in
+++ b/python/core/auto_generated/layertree/qgslayertreemodellegendnode.sip.in
@@ -12,6 +12,7 @@
+
class QgsLayerTreeModelLegendNode : QObject
{
%Docstring
@@ -26,6 +27,12 @@ and customized look.
%TypeHeaderCode
#include "qgslayertreemodellegendnode.h"
+%End
+%ConvertToSubClassCode
+ if ( qobject_cast ( sipCpp ) )
+ sipType = sipType_QgsSymbolLegendNode;
+ else
+ sipType = 0;
%End
public:
@@ -309,6 +316,23 @@ Returns text format of the label to be shown on top of the symbol.
Sets format of text to be shown on top of the symbol.
.. versionadded:: 3.2
+%End
+
+ QString symbolLabel() const;
+%Docstring
+Label of the symbol, user defined label will be used, otherwise will default to the label made by QGIS.
+
+.. versionadded:: 3.10
+%End
+
+ QString evaluateLabel( const QgsExpressionContext &context = QgsExpressionContext(), const QString &label = QString() );
+%Docstring
+Evaluates and returns the text label of the current node
+
+:param context: extra QgsExpressionContext to use for evaluating the expression
+:param label: text to evaluate instead of the layer layertree string
+
+.. versionadded:: 3.10
%End
public slots:
diff --git a/python/core/auto_generated/layout/qgslayoutitemlegend.sip.in b/python/core/auto_generated/layout/qgslayoutitemlegend.sip.in
index 0a647eeb38e..467d3229e44 100644
--- a/python/core/auto_generated/layout/qgslayoutitemlegend.sip.in
+++ b/python/core/auto_generated/layout/qgslayoutitemlegend.sip.in
@@ -24,9 +24,14 @@ Overrides some functionality of QgsLayerTreeModel to better fit the needs of lay
#include "qgslayoutitemlegend.h"
%End
public:
- QgsLegendModel( QgsLayerTree *rootNode, QObject *parent /TransferThis/ = 0 );
+ QgsLegendModel( QgsLayerTree *rootNode, QObject *parent /TransferThis/ = 0, QgsLayoutItemLegend *layout = 0 );
%Docstring
Construct the model based on the given layer tree
+%End
+
+ QgsLegendModel( QgsLayerTree *rootNode, QgsLayoutItemLegend *layout );
+%Docstring
+Alternative constructor.
%End
virtual QVariant data( const QModelIndex &index, int role ) const;
@@ -34,6 +39,16 @@ Construct the model based on the given layer tree
virtual Qt::ItemFlags flags( const QModelIndex &index ) const;
+
+ signals:
+
+ void refreshLegend();
+%Docstring
+Emitted to refresh the legend.
+
+.. versionadded:: 3.10
+%End
+
};
@@ -513,7 +528,6 @@ Returns the legend's renderer settings object.
virtual QgsExpressionContext createExpressionContext() const;
-
public slots:
virtual void refresh();
diff --git a/python/core/auto_generated/qgsvectorlayer.sip.in b/python/core/auto_generated/qgsvectorlayer.sip.in
index 2a0a28cac86..e3a5077b046 100644
--- a/python/core/auto_generated/qgsvectorlayer.sip.in
+++ b/python/core/auto_generated/qgsvectorlayer.sip.in
@@ -2279,7 +2279,6 @@ Configuration and logic to apply automatically on any edit happening on this lay
-
public slots:
void select( QgsFeatureId featureId );
@@ -2653,6 +2652,7 @@ Emitted when the feature count for symbols on this layer has been recalculated.
.. versionadded:: 3.0
%End
+
protected:
virtual void setExtent( const QgsRectangle &rect ) ${SIP_FINAL};
diff --git a/python/core/auto_generated/qgsvectorlayerfeaturecounter.sip.in b/python/core/auto_generated/qgsvectorlayerfeaturecounter.sip.in
index 7e6354cf05f..ccfc8a4bfcf 100644
--- a/python/core/auto_generated/qgsvectorlayerfeaturecounter.sip.in
+++ b/python/core/auto_generated/qgsvectorlayerfeaturecounter.sip.in
@@ -29,16 +29,29 @@ QgsVectorLayer.countSymbolFeatures() and connect to the signal
Create a new feature counter for ``layer``.
%End
+
virtual bool run();
+%Docstring
+Calculates the feature count and Ids per symbol
+%End
long featureCount( const QString &legendKey ) const;
%Docstring
-Gets the feature count for a particular ``legendKey``.
+Returns the feature count for a particular ``legendKey``.
If the key has not been found, -1 will be returned.
%End
+
+ QgsFeatureIds featureIds( const QString &symbolkey ) const;
+%Docstring
+Returns the feature Ids for a particular ``legendKey``.
+If the key has not been found an empty QSet will be returned.
+
+.. versionadded:: 3.10
+%End
+
signals:
void symbolsCounted();
diff --git a/src/app/layout/qgslayoutlegendwidget.cpp b/src/app/layout/qgslayoutlegendwidget.cpp
index a8b0b56c97a..f786d903f47 100644
--- a/src/app/layout/qgslayoutlegendwidget.cpp
+++ b/src/app/layout/qgslayoutlegendwidget.cpp
@@ -38,6 +38,7 @@
#include "qgslayoutitemlegend.h"
#include "qgslayoutmeasurementconverter.h"
#include "qgsunittypes.h"
+#include "qgsexpressionbuilderdialog.h"
#include
#include
@@ -104,6 +105,7 @@ QgsLayoutLegendWidget::QgsLayoutLegendWidget( QgsLayoutItemLegend *legend )
connect( mEditPushButton, &QToolButton::clicked, this, &QgsLayoutLegendWidget::mEditPushButton_clicked );
connect( mCountToolButton, &QToolButton::clicked, this, &QgsLayoutLegendWidget::mCountToolButton_clicked );
connect( mExpressionFilterButton, &QgsLegendFilterButton::toggled, this, &QgsLayoutLegendWidget::mExpressionFilterButton_toggled );
+ connect( mLayerExpressionButton, &QToolButton::clicked, this, &QgsLayoutLegendWidget::mLayerExpressionButton_clicked );
connect( mFilterByMapToolButton, &QToolButton::toggled, this, &QgsLayoutLegendWidget::mFilterByMapToolButton_toggled );
connect( mUpdateAllPushButton, &QToolButton::clicked, this, &QgsLayoutLegendWidget::mUpdateAllPushButton_clicked );
connect( mAddGroupToolButton, &QToolButton::clicked, this, &QgsLayoutLegendWidget::mAddGroupToolButton_clicked );
@@ -136,6 +138,7 @@ QgsLayoutLegendWidget::QgsLayoutLegendWidget( QgsLayoutItemLegend *legend )
mMoveUpToolButton->setIcon( QIcon( QgsApplication::iconPath( "mActionArrowUp.svg" ) ) );
mMoveDownToolButton->setIcon( QIcon( QgsApplication::iconPath( "mActionArrowDown.svg" ) ) );
mCountToolButton->setIcon( QIcon( QgsApplication::iconPath( "mActionSum.svg" ) ) );
+ mLayerExpressionButton->setIcon( QIcon( QgsApplication::iconPath( "mActionAddExpression.svg" ) ) );
mFontColorButton->setColorDialogTitle( tr( "Select Font Color" ) );
mFontColorButton->setContext( QStringLiteral( "composer" ) );
@@ -994,6 +997,45 @@ void QgsLayoutLegendWidget::mExpressionFilterButton_toggled( bool checked )
mLegend->endCommand();
}
+void QgsLayoutLegendWidget::mLayerExpressionButton_clicked()
+{
+
+ if ( !mLegend )
+ {
+ return;
+ }
+
+ QModelIndex currentIndex = mItemTreeView->currentIndex();
+ if ( !currentIndex.isValid() )
+ return;
+
+ QgsLayerTreeNode *currentNode = mItemTreeView->currentNode();
+ if ( !QgsLayerTree::isLayer( currentNode ) )
+ return;
+
+ QgsLayerTreeLayer *layerNode = qobject_cast( currentNode );
+ QgsVectorLayer *vl = qobject_cast( layerNode->layer() );
+
+ if ( !vl )
+ return;
+
+ QString currentExpression;
+ if ( layerNode->labelExpression().isEmpty() )
+ currentExpression = QStringLiteral( "@symbol_label" );
+ else
+ currentExpression = layerNode->labelExpression();
+ QgsExpressionContext legendContext = mLegend->createExpressionContext();
+ legendContext.appendScope( vl->createExpressionContextScope() );
+ QgsExpressionBuilderDialog expressiondialog( vl, currentExpression, nullptr, "generic", legendContext );
+ if ( expressiondialog.exec() )
+ layerNode->setLabelExpression( expressiondialog.expressionText() );
+
+ mLegend->beginCommand( tr( "Update Legend" ) );
+ mLegend->updateLegend();
+ mLegend->adjustBoxSize();
+ mLegend->endCommand();
+}
+
void QgsLayoutLegendWidget::mUpdateAllPushButton_clicked()
{
updateLegend();
@@ -1105,12 +1147,27 @@ void QgsLayoutLegendWidget::selectedChanged( const QModelIndex ¤t, const Q
Q_UNUSED( current )
Q_UNUSED( previous )
+ mLayerExpressionButton->setEnabled( false );
+
if ( mLegend && mLegend->autoUpdateModel() )
+ {
+ QgsLayerTreeNode *currentNode = mItemTreeView->currentNode();
+ if ( !QgsLayerTree::isLayer( currentNode ) )
+ return;
+
+ QgsLayerTreeLayer *currentLayerNode = QgsLayerTree::toLayer( currentNode );
+ QgsVectorLayer *vl = qobject_cast( currentLayerNode->layer() );
+ if ( !vl )
+ return;
+
+ mLayerExpressionButton->setEnabled( true );
return;
+ }
mCountToolButton->setChecked( false );
mCountToolButton->setEnabled( false );
+
mExpressionFilterButton->blockSignals( true );
mExpressionFilterButton->setChecked( false );
mExpressionFilterButton->setEnabled( false );
@@ -1127,6 +1184,7 @@ void QgsLayoutLegendWidget::selectedChanged( const QModelIndex ¤t, const Q
mCountToolButton->setChecked( currentNode->customProperty( QStringLiteral( "showFeatureCount" ), 0 ).toInt() );
mCountToolButton->setEnabled( true );
+ mLayerExpressionButton->setEnabled( true );
bool exprEnabled;
QString expr = QgsLayerTreeUtils::legendFilterByExpression( *qobject_cast( currentNode ), &exprEnabled );
diff --git a/src/app/layout/qgslayoutlegendwidget.h b/src/app/layout/qgslayoutlegendwidget.h
index 6ff900753bd..49b8e0176f2 100644
--- a/src/app/layout/qgslayoutlegendwidget.h
+++ b/src/app/layout/qgslayoutlegendwidget.h
@@ -85,6 +85,7 @@ class QgsLayoutLegendWidget: public QgsLayoutItemBaseWidget, private Ui::QgsLayo
void resetLayerNodeToDefaults();
void mUpdateAllPushButton_clicked();
void mAddGroupToolButton_clicked();
+ void mLayerExpressionButton_clicked();
void mFilterLegendByAtlasCheckBox_toggled( bool checked );
diff --git a/src/core/expression/qgsexpression.cpp b/src/core/expression/qgsexpression.cpp
index 0e05a583cf5..06f2b581249 100644
--- a/src/core/expression/qgsexpression.cpp
+++ b/src/core/expression/qgsexpression.cpp
@@ -830,6 +830,10 @@ void QgsExpression::initVariableHelp()
sVariableHelpTexts.insert( QStringLiteral( "symbol_color" ), QCoreApplication::translate( "symbol_color", "Color of symbol used to render the feature." ) );
sVariableHelpTexts.insert( QStringLiteral( "symbol_angle" ), QCoreApplication::translate( "symbol_angle", "Angle of symbol used to render the feature (valid for marker symbols only)." ) );
+ sVariableHelpTexts.insert( QStringLiteral( "symbol_label" ), QCoreApplication::translate( "symbol_label", "Label of the symbol, user defined label will be used, otherwise will default to the label made by QGIS." ) );
+ sVariableHelpTexts.insert( QStringLiteral( "symbol_id" ), QCoreApplication::translate( "symbol_id", "Id of the symbol." ) );
+ sVariableHelpTexts.insert( QStringLiteral( "symbol_count" ), QCoreApplication::translate( "symbol_count", "Total number of features defined by this symbol." ) );
+
//cluster variables
sVariableHelpTexts.insert( QStringLiteral( "cluster_color" ), QCoreApplication::translate( "cluster_color", "Color of symbols within a cluster, or NULL if symbols have mixed colors." ) );
sVariableHelpTexts.insert( QStringLiteral( "cluster_size" ), QCoreApplication::translate( "cluster_size", "Number of symbols contained within a cluster." ) );
diff --git a/src/core/layertree/qgslayertreelayer.cpp b/src/core/layertree/qgslayertreelayer.cpp
index 4acc7df7d5b..659a3c41a07 100644
--- a/src/core/layertree/qgslayertreelayer.cpp
+++ b/src/core/layertree/qgslayertreelayer.cpp
@@ -188,3 +188,9 @@ void QgsLayerTreeLayer::layerNameChanged()
Q_ASSERT( mRef );
emit nameChanged( this, mRef->name() );
}
+
+void QgsLayerTreeLayer::setLabelExpression( const QString &expression )
+{
+ mLabelExpression = expression;
+}
+
diff --git a/src/core/layertree/qgslayertreelayer.h b/src/core/layertree/qgslayertreelayer.h
index 2cca4172c05..d4186228269 100644
--- a/src/core/layertree/qgslayertreelayer.h
+++ b/src/core/layertree/qgslayertreelayer.h
@@ -128,6 +128,20 @@ class CORE_EXPORT QgsLayerTreeLayer : public QgsLayerTreeNode
*/
void resolveReferences( const QgsProject *project, bool looseMatching = false ) override;
+ /**
+ * set the expression to evaluate
+ *
+ * \since QGIS 3.10
+ */
+ void setLabelExpression( const QString &expression );
+
+ /**
+ * Returns the expression member of the LayerTreeNode
+ *
+ * \since QGIS 3.10
+ */
+ QString labelExpression() const { return mLabelExpression; }
+
signals:
/**
@@ -148,6 +162,8 @@ class CORE_EXPORT QgsLayerTreeLayer : public QgsLayerTreeNode
QgsMapLayerRef mRef;
//! Layer name - only used if layer does not exist or if mUseLayerName is false
QString mLayerName;
+ //! Expression to evaluate in the legend
+ QString mLabelExpression;
//!
bool mUseLayerName = true;
diff --git a/src/core/layertree/qgslayertreemodellegendnode.cpp b/src/core/layertree/qgslayertreemodellegendnode.cpp
index d19d215c846..69ee3f99c44 100644
--- a/src/core/layertree/qgslayertreemodellegendnode.cpp
+++ b/src/core/layertree/qgslayertreemodellegendnode.cpp
@@ -29,6 +29,11 @@
#include "qgsvectorlayer.h"
#include "qgsrasterrenderer.h"
#include "qgsexpressioncontextutils.h"
+#include "qgsfeatureid.h"
+#include "qgslayoutitem.h"
+#include "qgsvectorlayerfeaturecounter.h"
+#include "qgsexpression.h"
+
QgsLayerTreeModelLegendNode::QgsLayerTreeModelLegendNode( QgsLayerTreeLayer *nodeL, QObject *parent )
: QObject( parent )
@@ -284,6 +289,20 @@ const QgsSymbol *QgsSymbolLegendNode::symbol() const
return mItem.symbol();
}
+QString QgsSymbolLegendNode::symbolLabel() const
+{
+ QString label;
+ if ( mEmbeddedInParent )
+ {
+ QVariant legendlabel = mLayerNode->customProperty( QStringLiteral( "legend/title-label" ) );
+ QString layerName = legendlabel.isNull() ? mLayerNode->name() : legendlabel.toString();
+ label = mUserLabel.isEmpty() ? layerName : mUserLabel;
+ }
+ else
+ label = mUserLabel.isEmpty() ? mItem.label() : mUserLabel;
+ return label;
+}
+
void QgsSymbolLegendNode::setSymbol( QgsSymbol *symbol )
{
if ( !symbol )
@@ -648,31 +667,68 @@ void QgsSymbolLegendNode::updateLabel()
bool showFeatureCount = mLayerNode->customProperty( QStringLiteral( "showFeatureCount" ), 0 ).toBool();
QgsVectorLayer *vl = qobject_cast( mLayerNode->layer() );
+ mLabel = symbolLabel();
- if ( mEmbeddedInParent )
+ if ( showFeatureCount && vl )
{
- QString layerName = mLayerNode->name();
- if ( !mLayerNode->customProperty( QStringLiteral( "legend/title-label" ) ).isNull() )
- layerName = mLayerNode->customProperty( QStringLiteral( "legend/title-label" ) ).toString();
-
- mLabel = mUserLabel.isEmpty() ? layerName : mUserLabel;
- if ( showFeatureCount && vl && vl->featureCount() >= 0 )
- mLabel += QStringLiteral( " [%1]" ).arg( vl->featureCount() );
- }
- else
- {
- mLabel = mUserLabel.isEmpty() ? mItem.label() : mUserLabel;
- if ( showFeatureCount && vl )
- {
- qlonglong count = vl->featureCount( mItem.ruleKey() );
- mLabel += QStringLiteral( " [%1]" ).arg( count != -1 ? QLocale().toString( count ) : tr( "N/A" ) );
- }
+ qlonglong count = mEmbeddedInParent ? vl->featureCount() : vl->featureCount( mItem.ruleKey() ) ;
+ mLabel += QStringLiteral( " [%1]" ).arg( count != -1 ? QLocale().toString( count ) : tr( "N/A" ) );
}
emit dataChanged();
}
+QString QgsSymbolLegendNode::evaluateLabel( const QgsExpressionContext &context, const QString &label )
+{
+ if ( !mLayerNode )
+ return QString();
+ QgsVectorLayer *vl = qobject_cast( mLayerNode->layer() );
+
+ if ( vl )
+ {
+ QgsExpressionContext contextCopy = QgsExpressionContext( context );
+ QgsExpressionContextScope *symbolScope = createSymbolScope();
+ contextCopy.appendScope( symbolScope );
+ contextCopy.appendScope( vl->createExpressionContextScope() );
+
+ if ( label.isEmpty() )
+ {
+ if ( ! mLayerNode->labelExpression().isEmpty() )
+ mLabel = QgsExpression::replaceExpressionText( "[%" + mLayerNode->labelExpression() + "%]", &contextCopy );
+ else if ( mLabel.contains( "[%" ) )
+ {
+ const QString symLabel = symbolLabel();
+ mLabel = QgsExpression::replaceExpressionText( symLabel, &contextCopy );
+ }
+ return mLabel;
+ }
+ else
+ {
+ QString eLabel;
+ if ( ! mLayerNode->labelExpression().isEmpty() )
+ eLabel = QgsExpression::replaceExpressionText( label + "[%" + mLayerNode->labelExpression() + "%]", &contextCopy );
+ else if ( label.contains( "[%" ) )
+ eLabel = QgsExpression::replaceExpressionText( label, &contextCopy );
+ return eLabel;
+ }
+ }
+ return mLabel;
+}
+
+QgsExpressionContextScope *QgsSymbolLegendNode::createSymbolScope() const
+{
+ QgsVectorLayer *vl = qobject_cast( mLayerNode->layer() );
+
+ QgsExpressionContextScope *scope = new QgsExpressionContextScope( tr( "Symbol scope" ) );
+ scope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "symbol_label" ), symbolLabel().remove( "[%" ).remove( "%]" ), true ) );
+ scope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "symbol_id" ), mItem.ruleKey(), true ) );
+ if ( vl )
+ {
+ scope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "symbol_count" ), QVariant::fromValue( vl->featureCount( mItem.ruleKey() ) ), true ) );
+ }
+ return scope;
+}
// -------------------------------------------------------------------------
diff --git a/src/core/layertree/qgslayertreemodellegendnode.h b/src/core/layertree/qgslayertreemodellegendnode.h
index 5008893273b..32a05931e7b 100644
--- a/src/core/layertree/qgslayertreemodellegendnode.h
+++ b/src/core/layertree/qgslayertreemodellegendnode.h
@@ -27,6 +27,7 @@
#include "qgis_sip.h"
#include "qgsrasterdataprovider.h" // for QgsImageFetcher dtor visibility
+#include "qgsexpressioncontext.h"
class QgsLayerTreeLayer;
class QgsLayerTreeModel;
@@ -48,6 +49,14 @@ class QgsRenderContext;
class CORE_EXPORT QgsLayerTreeModelLegendNode : public QObject
{
Q_OBJECT
+#ifdef SIP_RUN
+ SIP_CONVERT_TO_SUBCLASS_CODE
+ if ( qobject_cast ( sipCpp ) )
+ sipType = sipType_QgsSymbolLegendNode;
+ else
+ sipType = 0;
+ SIP_END
+#endif
public:
enum LegendNodeRoles
@@ -228,6 +237,7 @@ class CORE_EXPORT QgsSymbolLegendNode : public QgsLayerTreeModelLegendNode
{
Q_OBJECT
+
public:
/**
@@ -319,6 +329,20 @@ class CORE_EXPORT QgsSymbolLegendNode : public QgsLayerTreeModelLegendNode
*/
void setTextOnSymbolTextFormat( const QgsTextFormat &format ) { mTextOnSymbolTextFormat = format; }
+ /**
+ * Label of the symbol, user defined label will be used, otherwise will default to the label made by QGIS.
+ * \since QGIS 3.10
+ */
+ QString symbolLabel() const;
+
+ /**
+ * Evaluates and returns the text label of the current node
+ * \param context extra QgsExpressionContext to use for evaluating the expression
+ * \param label text to evaluate instead of the layer layertree string
+ * \since QGIS 3.10
+ */
+ QString evaluateLabel( const QgsExpressionContext &context = QgsExpressionContext(), const QString &label = QString() );
+
public slots:
/**
@@ -361,6 +385,12 @@ class CORE_EXPORT QgsSymbolLegendNode : public QgsLayerTreeModelLegendNode
// ident the symbol icon to make it look like a tree structure
static const int INDENT_SIZE = 20;
+ /**
+ * Create an expressionContextScope containing symbol related variables
+ * \since QGIS 3.10
+ */
+ QgsExpressionContextScope *createSymbolScope() const SIP_FACTORY;
+
/**
* Sets all items belonging to the same layer as this node to the same check state.
* \param state check state
diff --git a/src/core/layout/qgslayoutitemlegend.cpp b/src/core/layout/qgslayoutitemlegend.cpp
index 8769b6a0114..ee4ff825acf 100644
--- a/src/core/layout/qgslayoutitemlegend.cpp
+++ b/src/core/layout/qgslayoutitemlegend.cpp
@@ -34,10 +34,11 @@
#include
#include
#include
+#include "qgsexpressioncontext.h"
QgsLayoutItemLegend::QgsLayoutItemLegend( QgsLayout *layout )
: QgsLayoutItem( layout )
- , mLegendModel( new QgsLegendModel( layout->project()->layerTreeRoot() ) )
+ , mLegendModel( new QgsLegendModel( layout->project()->layerTreeRoot(), this ) )
{
#if 0 //no longer required?
connect( &layout->atlasComposition(), &QgsAtlasComposition::renderEnded, this, &QgsLayoutItemLegend::onAtlasEnded );
@@ -55,6 +56,7 @@ QgsLayoutItemLegend::QgsLayoutItemLegend( QgsLayout *layout )
invalidateCache();
update();
} );
+ connect( mLegendModel.get(), &QgsLegendModel::refreshLegend, this, &QgsLayoutItemLegend::refresh );
}
QgsLayoutItemLegend *QgsLayoutItemLegend::create( QgsLayout *layout )
@@ -225,6 +227,7 @@ void QgsLayoutItemLegend::setCustomLayerTree( QgsLayerTree *rootGroup )
mCustomLayerTree.reset( rootGroup );
}
+
void QgsLayoutItemLegend::setAutoUpdateModel( bool autoUpdate )
{
if ( autoUpdate == autoUpdateModel() )
@@ -697,6 +700,7 @@ void QgsLayoutItemLegend::setLinkedMap( QgsLayoutItemMap *map )
}
updateFilterByMap();
+
}
void QgsLayoutItemLegend::invalidateCurrentMap()
@@ -760,9 +764,8 @@ void QgsLayoutItemLegend::mapLayerStyleOverridesChanged()
else
{
mLegendModel->setLayerStyleOverrides( mMap->layerStyleOverrides() );
-
- const auto constFindLayers = mLegendModel->rootGroup()->findLayers();
- for ( QgsLayerTreeLayer *nodeLayer : constFindLayers )
+ const QList< QgsLayerTreeLayer * > layers = mLegendModel->rootGroup()->findLayers();
+ for ( QgsLayerTreeLayer *nodeLayer : layers )
mLegendModel->refreshLayerLegend( nodeLayer );
}
@@ -846,11 +849,8 @@ QgsExpressionContext QgsLayoutItemLegend::createExpressionContext() const
// We only want the last scope from the map's expression context, as this contains
// the map specific variables. We don't want the rest of the map's context, because that
// will contain duplicate global, project, layout, etc scopes.
-
if ( mMap )
- {
context.appendScope( mMap->createExpressionContext().popScope() );
- }
QgsExpressionContextScope *scope = new QgsExpressionContextScope( tr( "Legend Settings" ) );
@@ -862,16 +862,26 @@ QgsExpressionContext QgsLayoutItemLegend::createExpressionContext() const
scope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "legend_filter_out_atlas" ), legendFilterOutAtlas(), true ) );
context.appendScope( scope );
-
return context;
}
+
// -------------------------------------------------------------------------
#include "qgslayertreemodellegendnode.h"
#include "qgsvectorlayer.h"
+#include "qgsmaplayerlegend.h"
-QgsLegendModel::QgsLegendModel( QgsLayerTree *rootNode, QObject *parent )
+QgsLegendModel::QgsLegendModel( QgsLayerTree *rootNode, QObject *parent, QgsLayoutItemLegend *layout )
: QgsLayerTreeModel( rootNode, parent )
+ , mLayoutLegend( layout )
+{
+ setFlag( QgsLayerTreeModel::AllowLegendChangeState, false );
+ setFlag( QgsLayerTreeModel::AllowNodeReorder, true );
+}
+
+QgsLegendModel::QgsLegendModel( QgsLayerTree *rootNode, QgsLayoutItemLegend *layout )
+ : QgsLayerTreeModel( rootNode )
+ , mLayoutLegend( layout )
{
setFlag( QgsLayerTreeModel::AllowLegendChangeState, false );
setFlag( QgsLayerTreeModel::AllowNodeReorder, true );
@@ -880,22 +890,62 @@ QgsLegendModel::QgsLegendModel( QgsLayerTree *rootNode, QObject *parent )
QVariant QgsLegendModel::data( const QModelIndex &index, int role ) const
{
// handle custom layer node labels
- if ( QgsLayerTreeNode *node = index2node( index ) )
- {
- if ( QgsLayerTree::isLayer( node ) && ( role == Qt::DisplayRole || role == Qt::EditRole ) && !node->customProperty( QStringLiteral( "legend/title-label" ) ).isNull() )
- {
- QgsLayerTreeLayer *nodeLayer = QgsLayerTree::toLayer( node );
- QString name = node->customProperty( QStringLiteral( "legend/title-label" ) ).toString();
- if ( nodeLayer->customProperty( QStringLiteral( "showFeatureCount" ), 0 ).toInt() && role == Qt::DisplayRole )
- {
- QgsVectorLayer *vlayer = qobject_cast( nodeLayer->layer() );
- if ( vlayer && vlayer->featureCount() >= 0 )
- name += QStringLiteral( " [%1]" ).arg( vlayer->featureCount() );
- }
- return name;
- }
- }
+ QgsLayerTreeNode *node = index2node( index );
+ QgsLayerTreeLayer *nodeLayer = QgsLayerTree::isLayer( node ) ? QgsLayerTree::toLayer( node ) : nullptr;
+ if ( nodeLayer && ( role == Qt::DisplayRole || role == Qt::EditRole ) )
+ {
+ QString name;
+ QgsVectorLayer *vlayer = qobject_cast( nodeLayer->layer() );
+
+ //finding the first label that is stored
+ name = nodeLayer->customProperty( QStringLiteral( "legend/title-label" ) ).toString();
+ if ( name.isEmpty() )
+ name = nodeLayer->name();
+ if ( name.isEmpty() )
+ name = node->customProperty( QStringLiteral( "legend/title-label" ) ).toString();
+ if ( name.isEmpty() )
+ name = node->name();
+ if ( nodeLayer->customProperty( QStringLiteral( "showFeatureCount" ), 0 ).toInt() )
+ {
+ if ( vlayer && vlayer->featureCount() >= 0 )
+ {
+ name += QStringLiteral( " [%1]" ).arg( vlayer->featureCount() );
+ return name;
+ }
+ }
+
+ bool evaluate = vlayer ? !nodeLayer->labelExpression().isEmpty() : false;
+
+ if ( evaluate || name.contains( "[%" ) )
+ {
+ QgsExpressionContext expressionContext;
+ if ( vlayer )
+ {
+ connect( vlayer, &QgsVectorLayer::symbolFeatureCountMapChanged, this, &QgsLegendModel::forceRefresh, Qt::UniqueConnection );
+ // counting is done here to ensure that a valid vector layer needs to be evaluated, count is used to validate previous count or update the count if invalidated
+ vlayer->countSymbolFeatures();
+ }
+
+ if ( mLayoutLegend )
+ expressionContext = mLayoutLegend->createExpressionContext();
+ else
+ expressionContext = QgsExpressionContext();
+
+ const QList legendnodes = layerLegendNodes( nodeLayer, false );
+ if ( legendnodes.count() > 1 ) // evaluate all existing legend nodes but leave the name for the legend evaluator
+ {
+ for ( QgsLayerTreeModelLegendNode *treenode : legendnodes )
+ {
+ if ( QgsSymbolLegendNode *symnode = qobject_cast( treenode ) )
+ symnode->evaluateLabel( expressionContext );
+ }
+ }
+ else if ( QgsSymbolLegendNode *symnode = qobject_cast( legendnodes.first() ) )
+ name = symnode->evaluateLabel( expressionContext, name );
+ }
+ return name;
+ }
return QgsLayerTreeModel::data( index, role );
}
@@ -907,3 +957,22 @@ Qt::ItemFlags QgsLegendModel::flags( const QModelIndex &index ) const
return QgsLayerTreeModel::flags( index );
}
+
+QList QgsLegendModel::layerLegendNodes( QgsLayerTreeLayer *nodeLayer, bool skipNodeEmbeddedInParent ) const
+{
+ if ( !mLegend.contains( nodeLayer ) )
+ return QList();
+
+ const LayerLegendData &data = mLegend[nodeLayer];
+ QList lst( data.activeNodes );
+ if ( !skipNodeEmbeddedInParent && data.embeddedNodeInParent )
+ lst.prepend( data.embeddedNodeInParent );
+ return lst;
+}
+
+void QgsLegendModel::forceRefresh()
+{
+ emit refreshLegend();
+}
+
+
diff --git a/src/core/layout/qgslayoutitemlegend.h b/src/core/layout/qgslayoutitemlegend.h
index 9db70ac87c3..23ca4f26165 100644
--- a/src/core/layout/qgslayoutitemlegend.h
+++ b/src/core/layout/qgslayoutitemlegend.h
@@ -24,11 +24,13 @@
#include "qgslayertreemodel.h"
#include "qgslegendsettings.h"
#include "qgslayertreegroup.h"
+#include "qgsexpressioncontext.h"
class QgsLayerTreeModel;
class QgsSymbol;
class QgsLayoutItemMap;
class QgsLegendRenderer;
+class QgsLayoutItemLegend;
/**
* \ingroup core
@@ -44,11 +46,48 @@ class CORE_EXPORT QgsLegendModel : public QgsLayerTreeModel
public:
//! Construct the model based on the given layer tree
- QgsLegendModel( QgsLayerTree *rootNode, QObject *parent SIP_TRANSFERTHIS = nullptr );
+ QgsLegendModel( QgsLayerTree *rootNode, QObject *parent SIP_TRANSFERTHIS = nullptr, QgsLayoutItemLegend *layout = nullptr );
+
+ //! Alternative constructor.
+ QgsLegendModel( QgsLayerTree *rootNode, QgsLayoutItemLegend *layout );
QVariant data( const QModelIndex &index, int role ) const override;
Qt::ItemFlags flags( const QModelIndex &index ) const override;
+
+ signals:
+
+ /**
+ * Emitted to refresh the legend.
+ * \since QGIS 3.10
+ */
+ void refreshLegend();
+
+ private slots:
+
+ /**
+ * Handle incoming signal to refresh the legend.
+ * \since QGIS 3.10
+ */
+ void forceRefresh();
+
+ private:
+
+ /**
+ * Returns filtered list of active legend nodes attached to a particular layer node
+ * (by default it returns also legend node embedded in parent layer node (if any) unless skipNodeEmbeddedInParent is true)
+ * \note Parameter skipNodeEmbeddedInParent added in QGIS 2.18
+ * \see layerOriginalLegendNodes()
+ * \since QGIS 3.10
+ */
+ QList layerLegendNodes( QgsLayerTreeLayer *nodeLayer, bool skipNodeEmbeddedInParent = false ) const;
+
+ /**
+ * Pointer to the QgsLayoutItemLegend class that made the model.
+ * \since QGIS 3.10
+ */
+ QgsLayoutItemLegend *mLayoutLegend = nullptr;
+
};
@@ -458,7 +497,6 @@ class CORE_EXPORT QgsLayoutItemLegend : public QgsLayoutItem
QgsExpressionContext createExpressionContext() const override;
-
public slots:
void refresh() override;
@@ -486,6 +524,7 @@ class CORE_EXPORT QgsLayoutItemLegend : public QgsLayoutItem
void nodeCustomPropertyChanged( QgsLayerTreeNode *node, const QString &key );
+
private:
QgsLayoutItemLegend() = delete;
diff --git a/src/core/layout/qgslayoutitemmap.cpp b/src/core/layout/qgslayoutitemmap.cpp
index 8acce7270b6..7543f709021 100644
--- a/src/core/layout/qgslayoutitemmap.cpp
+++ b/src/core/layout/qgslayoutitemmap.cpp
@@ -2243,3 +2243,4 @@ QgsRectangle QgsLayoutItemMap::computeAtlasRectangle()
return g.boundingBox();
}
}
+
diff --git a/src/core/qgsvectordataprovider.h b/src/core/qgsvectordataprovider.h
index b78fcb7952c..f2ea9369208 100644
--- a/src/core/qgsvectordataprovider.h
+++ b/src/core/qgsvectordataprovider.h
@@ -32,6 +32,7 @@ class QTextCodec;
#include "qgsrelation.h"
#include "qgsfeaturesink.h"
#include "qgsfeaturesource.h"
+#include "qgsfeaturerequest.h"
typedef QList QgsAttributeList SIP_SKIP;
typedef QSet QgsAttributeIds SIP_SKIP;
@@ -43,7 +44,6 @@ class QgsFeedback;
class QgsFeatureRenderer;
class QgsAbstractVectorLayerLabeling;
-#include "qgsfeaturerequest.h"
/**
* \ingroup core
diff --git a/src/core/qgsvectorlayer.h b/src/core/qgsvectorlayer.h
index c957c848902..e32a79fa608 100644
--- a/src/core/qgsvectorlayer.h
+++ b/src/core/qgsvectorlayer.h
@@ -27,7 +27,7 @@
#include
#include
-#include "qgis_sip.h"
+#include "qgis.h"
#include "qgsmaplayer.h"
#include "qgsfeature.h"
#include "qgsfeaturerequest.h"
@@ -41,6 +41,7 @@
#include "qgsfeatureiterator.h"
#include "qgsexpressioncontextgenerator.h"
#include "qgsexpressioncontextscopegenerator.h"
+#include "qgsexpressioncontext.h"
class QPainter;
class QImage;
diff --git a/src/core/qgsvectorlayerfeaturecounter.cpp b/src/core/qgsvectorlayerfeaturecounter.cpp
index 3890b4fe8f8..1b2ec7e5946 100644
--- a/src/core/qgsvectorlayerfeaturecounter.cpp
+++ b/src/core/qgsvectorlayerfeaturecounter.cpp
@@ -15,6 +15,7 @@
#include "qgsvectorlayerfeaturecounter.h"
#include "qgsvectorlayer.h"
+#include "qgsfeatureid.h"
QgsVectorLayerFeatureCounter::QgsVectorLayerFeatureCounter( QgsVectorLayer *layer, const QgsExpressionContext &context )
: QgsTask( tr( "Counting features in %1" ).arg( layer->name() ), QgsTask::CanCancel )
@@ -31,12 +32,15 @@ QgsVectorLayerFeatureCounter::QgsVectorLayerFeatureCounter( QgsVectorLayer *laye
bool QgsVectorLayerFeatureCounter::run()
{
+ mSymbolFeatureCountMap.clear();
+ mSymbolFeatureIdMap.clear();
QgsLegendSymbolList symbolList = mRenderer->legendSymbolItems();
QgsLegendSymbolList::const_iterator symbolIt = symbolList.constBegin();
for ( ; symbolIt != symbolList.constEnd(); ++symbolIt )
{
mSymbolFeatureCountMap.insert( symbolIt->label(), 0 );
+ mSymbolFeatureIdMap.insert( symbolIt->label(), QgsFeatureIds() );
}
// If there are no features to be counted, we can spare us the trouble
@@ -65,11 +69,12 @@ bool QgsVectorLayerFeatureCounter::run()
while ( fit.nextFeature( f ) )
{
renderContext.expressionContext().setFeature( f );
- QSet featureKeyList = mRenderer->legendKeysForFeature( f, renderContext );
- const auto constFeatureKeyList = featureKeyList;
- for ( const QString &key : constFeatureKeyList )
+
+ const QSet featureKeyList = mRenderer->legendKeysForFeature( f, renderContext );
+ for ( const QString &key : featureKeyList )
{
mSymbolFeatureCountMap[key] += 1;
+ mSymbolFeatureIdMap[key].insert( f.id() );
}
++featuresCounted;
@@ -88,9 +93,7 @@ bool QgsVectorLayerFeatureCounter::run()
}
mRenderer->stopRender( renderContext );
}
-
setProgress( 100 );
-
emit symbolsCounted();
return true;
}
@@ -104,3 +107,13 @@ long QgsVectorLayerFeatureCounter::featureCount( const QString &legendKey ) cons
{
return mSymbolFeatureCountMap.value( legendKey, -1 );
}
+
+QHash QgsVectorLayerFeatureCounter::symbolFeatureIdMap() const
+{
+ return mSymbolFeatureIdMap;
+}
+
+QgsFeatureIds QgsVectorLayerFeatureCounter::featureIds( const QString &symbolkey ) const
+{
+ return mSymbolFeatureIdMap.value( symbolkey, QgsFeatureIds() );
+}
diff --git a/src/core/qgsvectorlayerfeaturecounter.h b/src/core/qgsvectorlayerfeaturecounter.h
index ebeaf903ba7..d137e0e823d 100644
--- a/src/core/qgsvectorlayerfeaturecounter.h
+++ b/src/core/qgsvectorlayerfeaturecounter.h
@@ -18,6 +18,7 @@
#include "qgsvectorlayerfeatureiterator.h"
#include "qgsrenderer.h"
#include "qgstaskmanager.h"
+#include "qgsfeatureid.h"
/**
* \ingroup core
@@ -40,10 +41,14 @@ class CORE_EXPORT QgsVectorLayerFeatureCounter : public QgsTask
*/
QgsVectorLayerFeatureCounter( QgsVectorLayer *layer, const QgsExpressionContext &context = QgsExpressionContext() );
+
+ /**
+ * Calculates the feature count and Ids per symbol
+ */
bool run() override;
/**
- * Gets the count for each symbol. Only valid after the symbolsCounted()
+ * Returns the count for each symbol. Only valid after the symbolsCounted()
* signal has been emitted.
*
* \note Not available in Python bindings.
@@ -51,11 +56,29 @@ class CORE_EXPORT QgsVectorLayerFeatureCounter : public QgsTask
QHash symbolFeatureCountMap() const SIP_SKIP;
/**
- * Gets the feature count for a particular \a legendKey.
+ * Returns the feature count for a particular \a legendKey.
* If the key has not been found, -1 will be returned.
*/
long featureCount( const QString &legendKey ) const;
+ /**
+ * Returns the QgsFeatureIds for each symbol. Only valid after the symbolsCounted()
+ * signal has been emitted.
+ *
+ * \see symbolFeatureCountMap
+ * \note Not available in Python bindings.
+ * \since QGIS 3.10
+ */
+ QHash symbolFeatureIdMap() const SIP_SKIP;
+
+ /**
+ * Returns the feature Ids for a particular \a legendKey.
+ * If the key has not been found an empty QSet will be returned.
+ *
+ * \since QGIS 3.10
+ */
+ QgsFeatureIds featureIds( const QString &symbolkey ) const;
+
signals:
/**
@@ -68,6 +91,7 @@ class CORE_EXPORT QgsVectorLayerFeatureCounter : public QgsTask
std::unique_ptr mRenderer;
QgsExpressionContext mExpressionContext;
QHash mSymbolFeatureCountMap;
+ QHash mSymbolFeatureIdMap;
int mFeatureCount;
};
diff --git a/src/ui/layout/qgslayoutlegendwidgetbase.ui b/src/ui/layout/qgslayoutlegendwidgetbase.ui
index 8c8cf3a7fd1..e0d30b38947 100644
--- a/src/ui/layout/qgslayoutlegendwidgetbase.ui
+++ b/src/ui/layout/qgslayoutlegendwidgetbase.ui
@@ -401,6 +401,23 @@
+ -
+
+
+ Add an expression to the vector layer and each child symbol's label
+
+
+
+ :/images/themes/default/mActionAddExpression.svg:/images/themes/default/mActionAddExpression.svg
+
+
+
+ 20
+ 20
+
+
+
+
-
diff --git a/tests/src/python/test_qgslayoutlegend.py b/tests/src/python/test_qgslayoutlegend.py
index 21bd4441a86..66344ffb63a 100644
--- a/tests/src/python/test_qgslayoutlegend.py
+++ b/tests/src/python/test_qgslayoutlegend.py
@@ -32,14 +32,15 @@ from qgis.core import (QgsPrintLayout,
QgsExpression,
QgsMapLayerLegendUtils,
QgsLegendStyle,
- QgsFontUtils)
+ QgsFontUtils,
+ QgsApplication)
from qgis.testing import (start_app,
unittest
)
from utilities import unitTestDataPath
from qgslayoutchecker import QgsLayoutChecker
import os
-
+from time import sleep
from test_qgslayoutitem import LayoutItemTestCase
start_app()
@@ -66,6 +67,7 @@ class TestQgsLayoutItemLegend(unittest.TestCase, LayoutItemTestCase):
point_path = os.path.join(TEST_DATA_DIR, 'points.shp')
point_layer = QgsVectorLayer(point_path, 'points', 'ogr')
+ QgsProject.instance().clear()
QgsProject.instance().addMapLayers([point_layer])
marker_symbol = QgsMarkerSymbol.createSimple({'color': '#ff0000', 'outline_style': 'no', 'size': '5', 'size_unit': 'MapUnit'})
@@ -123,7 +125,6 @@ class TestQgsLayoutItemLegend(unittest.TestCase, LayoutItemTestCase):
def testResizeWithMapContent(self):
"""Test test legend resizes to match map content"""
-
point_path = os.path.join(TEST_DATA_DIR, 'points.shp')
point_layer = QgsVectorLayer(point_path, 'points', 'ogr')
QgsProject.instance().addMapLayers([point_layer])
@@ -164,7 +165,6 @@ class TestQgsLayoutItemLegend(unittest.TestCase, LayoutItemTestCase):
def testResizeDisabled(self):
"""Test that test legend does not resize if auto size is disabled"""
-
point_path = os.path.join(TEST_DATA_DIR, 'points.shp')
point_layer = QgsVectorLayer(point_path, 'points', 'ogr')
QgsProject.instance().addMapLayers([point_layer])
@@ -209,7 +209,6 @@ class TestQgsLayoutItemLegend(unittest.TestCase, LayoutItemTestCase):
def testResizeDisabledCrop(self):
"""Test that if legend resizing is disabled, and legend is too small, then content is cropped"""
-
point_path = os.path.join(TEST_DATA_DIR, 'points.shp')
point_layer = QgsVectorLayer(point_path, 'points', 'ogr')
QgsProject.instance().addMapLayers([point_layer])
@@ -322,10 +321,8 @@ class TestQgsLayoutItemLegend(unittest.TestCase, LayoutItemTestCase):
def testExpressionInText(self):
"""Test expressions embedded in legend node text"""
-
point_path = os.path.join(TEST_DATA_DIR, 'points.shp')
point_layer = QgsVectorLayer(point_path, 'points', 'ogr')
-
layout = QgsPrintLayout(QgsProject.instance())
layout.setName('LAYOUT')
layout.initializeDefaults()
@@ -378,6 +375,121 @@ class TestQgsLayoutItemLegend(unittest.TestCase, LayoutItemTestCase):
QgsProject.instance().removeMapLayers([point_layer.id()])
+ def testSymbolExpressions(self):
+ "Test expressions embedded in legend node text"
+ QgsProject.instance().clear()
+ point_path = os.path.join(TEST_DATA_DIR, 'points.shp')
+ point_layer = QgsVectorLayer(point_path, 'points', 'ogr')
+
+ layout = QgsPrintLayout(QgsProject.instance())
+ layout.initializeDefaults()
+
+ map = QgsLayoutItemMap(layout)
+ map.setLayers([point_layer])
+ layout.addLayoutItem(map)
+ map.setExtent(point_layer.extent())
+
+ legend = QgsLayoutItemLegend(layout)
+
+ layer = QgsProject.instance().addMapLayer(point_layer)
+ legendlayer = legend.model().rootGroup().addLayer(point_layer)
+
+ counterTask = point_layer.countSymbolFeatures()
+ counterTask.waitForFinished()
+ TM = QgsApplication.taskManager()
+ actask = TM.activeTasks()
+ print(TM.tasks(), actask)
+ count = actask[0]
+ legend.model().refreshLayerLegend(legendlayer)
+ legendnodes = legend.model().layerLegendNodes(legendlayer)
+ legendnodes[0].setUserLabel('[% @symbol_id %]')
+ legendnodes[1].setUserLabel('[% @symbol_count %]')
+ legendnodes[2].setUserLabel('[% sum("Pilots") %]')
+ label1 = legendnodes[0].evaluateLabel()
+ label2 = legendnodes[1].evaluateLabel()
+ label3 = legendnodes[2].evaluateLabel()
+ count.waitForFinished()
+ self.assertEqual(label1, '0')
+ #self.assertEqual(label2, '5')
+ #self.assertEqual(label3, '12')
+
+ legendlayer.setLabelExpression("Concat(@symbol_label, @symbol_id)")
+
+ label1 = legendnodes[0].evaluateLabel()
+ label2 = legendnodes[1].evaluateLabel()
+ label3 = legendnodes[2].evaluateLabel()
+
+ self.assertEqual(label1, ' @symbol_id 0')
+ #self.assertEqual(label2, '@symbol_count 1')
+ #self.assertEqual(label3, 'sum("Pilots") 2')
+
+ QgsProject.instance().clear()
+
+ def testSymbolExpressionRender(self):
+ """Test expressions embedded in legend node text"""
+ point_path = os.path.join(TEST_DATA_DIR, 'points.shp')
+ point_layer = QgsVectorLayer(point_path, 'points', 'ogr')
+ layout = QgsPrintLayout(QgsProject.instance())
+ layout.setName('LAYOUT')
+ layout.initializeDefaults()
+
+ map = QgsLayoutItemMap(layout)
+ map.attemptSetSceneRect(QRectF(20, 20, 80, 80))
+ map.setFrameEnabled(True)
+ map.setLayers([point_layer])
+ layout.addLayoutItem(map)
+ map.setExtent(point_layer.extent())
+
+ legend = QgsLayoutItemLegend(layout)
+ legend.setTitle("Legend")
+ legend.attemptSetSceneRect(QRectF(120, 20, 100, 100))
+ legend.setFrameEnabled(True)
+ legend.setFrameStrokeWidth(QgsLayoutMeasurement(2))
+ legend.setBackgroundColor(QColor(200, 200, 200))
+ legend.setTitle('')
+ legend.setLegendFilterByMapEnabled(False)
+ legend.setStyleFont(QgsLegendStyle.Title, QgsFontUtils.getStandardTestFont('Bold', 16))
+ legend.setStyleFont(QgsLegendStyle.Group, QgsFontUtils.getStandardTestFont('Bold', 16))
+ legend.setStyleFont(QgsLegendStyle.Subgroup, QgsFontUtils.getStandardTestFont('Bold', 16))
+ legend.setStyleFont(QgsLegendStyle.Symbol, QgsFontUtils.getStandardTestFont('Bold', 16))
+ legend.setStyleFont(QgsLegendStyle.SymbolLabel, QgsFontUtils.getStandardTestFont('Bold', 16))
+
+ legend.setAutoUpdateModel(False)
+
+ QgsProject.instance().addMapLayers([point_layer])
+ s = QgsMapSettings()
+ s.setLayers([point_layer])
+
+ group = legend.model().rootGroup().addGroup("Group [% 1 + 5 %] [% @layout_name %]")
+ layer_tree_layer = group.addLayer(point_layer)
+ counterTask = point_layer.countSymbolFeatures()
+ counterTask.waitForFinished() # does this even work?
+ layer_tree_layer.setCustomProperty("legend/title-label", 'bbbb [% 1+2 %] xx [% @layout_name %] [% @layer_name %]')
+ QgsMapLayerLegendUtils.setLegendNodeUserLabel(layer_tree_layer, 0, 'xxxx')
+ legend.model().refreshLayerLegend(layer_tree_layer)
+ layer_tree_layer.setLabelExpression('Concat(@symbol_id, @symbol_label, count("Class"))')
+ legend.model().layerLegendNodes(layer_tree_layer)[0].setUserLabel(' sym 1')
+ legend.model().layerLegendNodes(layer_tree_layer)[1].setUserLabel('[%@symbol_count %]')
+ legend.model().layerLegendNodes(layer_tree_layer)[2].setUserLabel('[% count("Class") %]')
+ layout.addLayoutItem(legend)
+ legend.setLinkedMap(map)
+ legend.updateLegend()
+ print(layer_tree_layer.labelExpression())
+ TM = QgsApplication.taskManager()
+ actask = TM.activeTasks()
+ print(TM.tasks(), actask)
+ count = actask[0]
+ count.waitForFinished()
+ map.setExtent(QgsRectangle(-102.51, 41.16, -102.36, 41.30))
+ checker = QgsLayoutChecker(
+ 'composer_legend_symbol_expression', layout)
+ checker.setControlPathPrefix("composer_legend")
+ sleep(4)
+ result, message = checker.testLayout()
+ self.assertTrue(result, message)
+
+ QgsProject.instance().removeMapLayers([point_layer.id()])
+
if __name__ == '__main__':
unittest.main()
diff --git a/tests/testdata/control_images/composer_legend/expected_composer_legend_symbol_expression/expected_composer_legend_symbol_expression.png b/tests/testdata/control_images/composer_legend/expected_composer_legend_symbol_expression/expected_composer_legend_symbol_expression.png
new file mode 100644
index 00000000000..9c86c7ee6c4
Binary files /dev/null and b/tests/testdata/control_images/composer_legend/expected_composer_legend_symbol_expression/expected_composer_legend_symbol_expression.png differ