Skip to content

Commit d70b60d

Browse files
committedJan 18, 2019
[FEATURE][layouts] Allow expressions to be embedded inside legend item text
This feature allows for expressions to be embedded directly inside legend item text (e.g. group, subgroup and item text). The expressions are evaluated at render time, with full knowledge of the legend's expression context (so can utilise variables from the layout/layout item/ etc) There's no UI for this yet (that will come in 3.8), but expressions are entered using the standard [% 1 + 2 %] format. E.g. a legend item text of My layer (rendered at 1:[% @map_scale %]) will show in the output as My layer (renderer at 1:1000)
1 parent b3d5f27 commit d70b60d

File tree

9 files changed

+170
-9
lines changed

9 files changed

+170
-9
lines changed
 

‎python/core/auto_generated/qgslegendsettings.sip.in

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,16 @@ The ``scale`` value indicates the scale denominator, e.g. 1000.0 for a 1:1000 ma
198198
void setDpi( int dpi );
199199

200200

201+
202+
QStringList evaluateItemText( const QString &stringToSplt, const QgsExpressionContext &context ) const;
203+
%Docstring
204+
Returns the actual text to render for a legend item, split into separate lines.
205+
206+
:param ctx: Context for rendering - may be null if only doing layout without actual rendering
207+
208+
.. versionadded:: 3.6
209+
%End
210+
201211
QStringList splitStringForWrapping( const QString &stringToSplt ) const;
202212
%Docstring
203213
Splits a string using the wrap char taking into account handling empty

‎src/core/layertree/qgslayertreemodellegendnode.cpp

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,9 @@ QSizeF QgsLayerTreeModelLegendNode::drawSymbolText( const QgsLegendSettings &set
9292
double textHeight = settings.fontHeightCharacterMM( symbolLabelFont, QChar( '0' ) );
9393
double textDescent = settings.fontDescentMillimeters( symbolLabelFont );
9494

95-
const QStringList lines = settings.splitStringForWrapping( data( Qt::DisplayRole ).toString() );
95+
QgsExpressionContext tempContext;
96+
97+
const QStringList lines = settings.evaluateItemText( data( Qt::DisplayRole ).toString(), ctx && ctx->context ? ctx->context->expressionContext() : tempContext );
9698

9799
labelSize.rheight() = lines.count() * textHeight + ( lines.count() - 1 ) * ( settings.lineSpacing() + textDescent );
98100

‎src/core/layout/qgslayoutitemlegend.cpp

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -831,7 +831,6 @@ QgsExpressionContext QgsLayoutItemLegend::createExpressionContext() const
831831
context.appendScope( mMap->createExpressionContext().popScope() );
832832
}
833833

834-
835834
QgsExpressionContextScope *scope = new QgsExpressionContextScope( tr( "Legend Settings" ) );
836835

837836
scope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "legend_title" ), title(), true ) );

‎src/core/qgslegendrenderer.cpp

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -566,7 +566,8 @@ QSizeF QgsLegendRenderer::drawLayerTitleInternal( QgsLayerTreeLayer *nodeLayer,
566566
QModelIndex idx = mLegendModel->node2index( nodeLayer );
567567

568568
//Let the user omit the layer title item by having an empty layer title string
569-
if ( mLegendModel->data( idx, Qt::DisplayRole ).toString().isEmpty() ) return size;
569+
if ( mLegendModel->data( idx, Qt::DisplayRole ).toString().isEmpty() )
570+
return size;
570571

571572
double y = point.y();
572573

@@ -577,8 +578,11 @@ QSizeF QgsLegendRenderer::drawLayerTitleInternal( QgsLayerTreeLayer *nodeLayer,
577578

578579
QFont layerFont = mSettings.style( nodeLegendStyle( nodeLayer ) ).font();
579580

580-
QStringList lines = mSettings.splitStringForWrapping( mLegendModel->data( idx, Qt::DisplayRole ).toString() );
581-
for ( QStringList::Iterator layerItemPart = lines.begin(); layerItemPart != lines.end(); ++layerItemPart )
581+
QgsExpressionContext tempContext;
582+
583+
const QStringList lines = mSettings.evaluateItemText( mLegendModel->data( idx, Qt::DisplayRole ).toString(),
584+
context ? context->expressionContext() : tempContext );
585+
for ( QStringList::ConstIterator layerItemPart = lines.constBegin(); layerItemPart != lines.constEnd(); ++layerItemPart )
582586
{
583587
y += mSettings.fontAscentMillimeters( layerFont );
584588
if ( context && context->painter() )
@@ -617,8 +621,11 @@ QSizeF QgsLegendRenderer::drawGroupTitleInternal( QgsLayerTreeGroup *nodeGroup,
617621

618622
QFont groupFont = mSettings.style( nodeLegendStyle( nodeGroup ) ).font();
619623

620-
QStringList lines = mSettings.splitStringForWrapping( mLegendModel->data( idx, Qt::DisplayRole ).toString() );
621-
for ( QStringList::Iterator groupPart = lines.begin(); groupPart != lines.end(); ++groupPart )
624+
QgsExpressionContext tempContext;
625+
626+
const QStringList lines = mSettings.evaluateItemText( mLegendModel->data( idx, Qt::DisplayRole ).toString(),
627+
context ? context->expressionContext() : tempContext );
628+
for ( QStringList::ConstIterator groupPart = lines.constBegin(); groupPart != lines.constEnd(); ++groupPart )
622629
{
623630
y += mSettings.fontAscentMillimeters( groupFont );
624631
if ( context && context->painter() )

‎src/core/qgslegendsettings.cpp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
***************************************************************************/
1515

1616
#include "qgslegendsettings.h"
17+
#include "qgsexpressioncontext.h"
18+
#include "qgsexpression.h"
1719

1820
#include <QPainter>
1921

@@ -35,6 +37,12 @@ QgsLegendSettings::QgsLegendSettings()
3537
rstyle( QgsLegendStyle::SymbolLabel ).rfont().setPointSizeF( 12.0 );
3638
}
3739

40+
QStringList QgsLegendSettings::evaluateItemText( const QString &stringToSplt, const QgsExpressionContext &context ) const
41+
{
42+
const QString textToRender = QgsExpression::replaceExpressionText( stringToSplt, &context );
43+
return splitStringForWrapping( textToRender );
44+
}
45+
3846
QStringList QgsLegendSettings::splitStringForWrapping( const QString &stringToSplt ) const
3947
{
4048
QStringList list;

‎src/core/qgslegendsettings.h

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ class QRectF;
2525

2626
#include "qgslegendstyle.h"
2727

28+
class QgsExpressionContext;
2829

2930
/**
3031
* \ingroup core
@@ -182,6 +183,20 @@ class CORE_EXPORT QgsLegendSettings
182183

183184
// utility functions
184185

186+
/**
187+
* Splits a string using the wrap char taking into account handling empty
188+
* wrap char which means no wrapping
189+
*/
190+
191+
/**
192+
* Returns the actual text to render for a legend item, split into separate lines.
193+
*
194+
* \param ctx Context for rendering - may be null if only doing layout without actual rendering
195+
*
196+
* \since QGIS 3.6
197+
*/
198+
QStringList evaluateItemText( const QString &stringToSplt, const QgsExpressionContext &context ) const;
199+
185200
/**
186201
* Splits a string using the wrap char taking into account handling empty
187202
* wrap char which means no wrapping

‎tests/src/core/testqgslayertree.cpp

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
#include <qgslayertreemodel.h>
2828
#include <qgslayertreemodellegendnode.h>
2929
#include <qgslayertreeutils.h>
30+
#include "qgslegendsettings.h"
3031

3132
class TestQgsLayerTree : public QObject
3233
{
@@ -53,6 +54,7 @@ class TestQgsLayerTree : public QObject
5354
void testFindGroups();
5455
void testUtilsCollectMapLayers();
5556
void testUtilsCountMapLayers();
57+
void testSymbolText();
5658

5759
private:
5860

@@ -707,6 +709,62 @@ void TestQgsLayerTree::testUtilsCountMapLayers()
707709
QCOMPARE( QgsLayerTreeUtils::countMapLayerInTree( &root, vl ), 2 );
708710
}
709711

712+
void TestQgsLayerTree::testSymbolText()
713+
{
714+
//new memory layer
715+
QgsVectorLayer *vl = new QgsVectorLayer( QStringLiteral( "Point?field=col1:integer" ), QStringLiteral( "vl" ), QStringLiteral( "memory" ) );
716+
QVERIFY( vl->isValid() );
717+
718+
QgsProject project;
719+
project.addMapLayer( vl );
720+
721+
//create a categorized renderer for layer
722+
QgsCategorizedSymbolRenderer *renderer = new QgsCategorizedSymbolRenderer();
723+
renderer->setClassAttribute( QStringLiteral( "col1" ) );
724+
renderer->setSourceSymbol( QgsSymbol::defaultSymbol( QgsWkbTypes::PointGeometry ) );
725+
renderer->addCategory( QgsRendererCategory( "a", QgsSymbol::defaultSymbol( QgsWkbTypes::PointGeometry ), QStringLiteral( "a [% 1 + 2 %]" ) ) );
726+
renderer->addCategory( QgsRendererCategory( "b", QgsSymbol::defaultSymbol( QgsWkbTypes::PointGeometry ), QStringLiteral( "b,c" ) ) );
727+
renderer->addCategory( QgsRendererCategory( "c", QgsSymbol::defaultSymbol( QgsWkbTypes::PointGeometry ), QStringLiteral( "c" ) ) );
728+
vl->setRenderer( renderer );
729+
730+
//create legend with symbology nodes for categorized renderer
731+
QgsLayerTree *root = new QgsLayerTree();
732+
QgsLayerTreeLayer *n = new QgsLayerTreeLayer( vl );
733+
root->addChildNode( n );
734+
QgsLayerTreeModel *m = new QgsLayerTreeModel( root, nullptr );
735+
m->refreshLayerLegend( n );
736+
737+
QList<QgsLayerTreeModelLegendNode *> nodes = m->layerLegendNodes( n );
738+
QCOMPARE( nodes.length(), 3 );
739+
740+
QgsLegendSettings settings;
741+
settings.setWrapChar( QStringLiteral( "," ) );
742+
QCOMPARE( nodes.at( 0 )->data( Qt::DisplayRole ).toString(), QStringLiteral( "a [% 1 + 2 %]" ) );
743+
QCOMPARE( nodes.at( 1 )->data( Qt::DisplayRole ).toString(), QStringLiteral( "b,c" ) );
744+
QCOMPARE( nodes.at( 2 )->data( Qt::DisplayRole ).toString(), QStringLiteral( "c" ) );
745+
nodes.at( 2 )->setUserLabel( QStringLiteral( "[% 2+3 %] x [% 3+4 %]" ) );
746+
QCOMPARE( nodes.at( 2 )->data( Qt::DisplayRole ).toString(), QStringLiteral( "[% 2+3 %] x [% 3+4 %]" ) );
747+
748+
QgsExpressionContext context;
749+
QCOMPARE( settings.evaluateItemText( nodes.at( 0 )->data( Qt::DisplayRole ).toString(), context ), QStringList() << QStringLiteral( "a 3" ) );
750+
QCOMPARE( settings.evaluateItemText( nodes.at( 1 )->data( Qt::DisplayRole ).toString(), context ), QStringList() << QStringLiteral( "b" ) << QStringLiteral( "c" ) );
751+
QCOMPARE( settings.evaluateItemText( nodes.at( 2 )->data( Qt::DisplayRole ).toString(), context ), QStringList() << QStringLiteral( "5 x 7" ) );
752+
753+
// split string should happen after expression evaluation
754+
QgsExpressionContextScope *scope = new QgsExpressionContextScope();
755+
scope->setVariable( QStringLiteral( "bbbb" ), QStringLiteral( "aaaa,bbbb,cccc" ) );
756+
context.appendScope( scope );
757+
nodes.at( 2 )->setUserLabel( QStringLiteral( "[% @bbbb %],[% 3+4 %]" ) );
758+
QCOMPARE( settings.evaluateItemText( nodes.at( 2 )->data( Qt::DisplayRole ).toString(), context ), QStringList() << QStringLiteral( "aaaa" )
759+
<< QStringLiteral( "bbbb" )
760+
<< QStringLiteral( "cccc" )
761+
<< QStringLiteral( "7" ) );
762+
763+
//cleanup
764+
delete m;
765+
delete root;
766+
}
767+
710768

711769
QGSTEST_MAIN( TestQgsLayerTree )
712770
#include "testqgslayertree.moc"

‎tests/src/python/test_qgslayoutlegend.py

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@
1515
from qgis.PyQt.QtCore import QRectF
1616
from qgis.PyQt.QtGui import QColor
1717

18-
from qgis.core import (QgsLayoutItemLegend,
18+
from qgis.core import (QgsPrintLayout,
19+
QgsLayoutItemLegend,
1920
QgsLayoutItemMap,
2021
QgsLayout,
2122
QgsMapSettings,
@@ -30,7 +31,10 @@
3031
QgsLayoutItem,
3132
QgsLayoutPoint,
3233
QgsLayoutSize,
33-
QgsExpression)
34+
QgsExpression,
35+
QgsMapLayerLegendUtils,
36+
QgsLegendStyle,
37+
QgsFontUtils)
3438
from qgis.testing import (start_app,
3539
unittest
3640
)
@@ -305,6 +309,64 @@ def testLegendScopeVariables(self):
305309
exp6 = QgsExpression("@map_scale")
306310
self.assertAlmostEqual(exp6.evaluate(expc2), 15000, 2)
307311

312+
def testExpressionInText(self):
313+
"""Test expressions embedded in legend node text"""
314+
315+
point_path = os.path.join(TEST_DATA_DIR, 'points.shp')
316+
point_layer = QgsVectorLayer(point_path, 'points', 'ogr')
317+
318+
layout = QgsPrintLayout(QgsProject.instance())
319+
layout.setName('LAYOUT')
320+
layout.initializeDefaults()
321+
322+
map = QgsLayoutItemMap(layout)
323+
map.attemptSetSceneRect(QRectF(20, 20, 80, 80))
324+
map.setFrameEnabled(True)
325+
map.setLayers([point_layer])
326+
layout.addLayoutItem(map)
327+
map.setExtent(point_layer.extent())
328+
329+
legend = QgsLayoutItemLegend(layout)
330+
legend.setTitle("Legend")
331+
legend.attemptSetSceneRect(QRectF(120, 20, 100, 100))
332+
legend.setFrameEnabled(True)
333+
legend.setFrameStrokeWidth(QgsLayoutMeasurement(2))
334+
legend.setBackgroundColor(QColor(200, 200, 200))
335+
legend.setTitle('')
336+
legend.setLegendFilterByMapEnabled(False)
337+
legend.setStyleFont(QgsLegendStyle.Title, QgsFontUtils.getStandardTestFont('Bold', 16))
338+
legend.setStyleFont(QgsLegendStyle.Group, QgsFontUtils.getStandardTestFont('Bold', 16))
339+
legend.setStyleFont(QgsLegendStyle.Subgroup, QgsFontUtils.getStandardTestFont('Bold', 16))
340+
legend.setStyleFont(QgsLegendStyle.Symbol, QgsFontUtils.getStandardTestFont('Bold', 16))
341+
legend.setStyleFont(QgsLegendStyle.SymbolLabel, QgsFontUtils.getStandardTestFont('Bold', 16))
342+
343+
# disable auto resizing
344+
legend.setResizeToContents(False)
345+
legend.setAutoUpdateModel(False)
346+
347+
QgsProject.instance().addMapLayers([point_layer])
348+
s = QgsMapSettings()
349+
s.setLayers([point_layer])
350+
351+
group = legend.model().rootGroup().addGroup("Group [% 1 + 5 %] [% @layout_name %]")
352+
layer_tree_layer = group.addLayer(point_layer)
353+
layer_tree_layer.setCustomProperty("legend/title-label", 'bbbb [% 1+2 %] xx [% @layout_name %]')
354+
QgsMapLayerLegendUtils.setLegendNodeUserLabel(layer_tree_layer, 0, 'xxxx')
355+
legend.model().refreshLayerLegend(layer_tree_layer)
356+
legend.model().layerLegendNodes(layer_tree_layer)[0].setUserLabel('bbbb [% 1+2 %] xx [% @layout_name %]')
357+
358+
layout.addLayoutItem(legend)
359+
legend.setLinkedMap(map)
360+
361+
map.setExtent(QgsRectangle(-102.51, 41.16, -102.36, 41.30))
362+
363+
checker = QgsLayoutChecker(
364+
'composer_legend_expressions', layout)
365+
checker.setControlPathPrefix("composer_legend")
366+
result, message = checker.testLayout()
367+
self.assertTrue(result, message)
368+
369+
QgsProject.instance().removeMapLayers([point_layer.id()])
308370

309371
if __name__ == '__main__':
310372
unittest.main()

0 commit comments

Comments
 (0)
Please sign in to comment.