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