Skip to content

Commit 7a6220a

Browse files
committedMay 5, 2020
[FEATURE][layouts] Allow overriding the default symbol for a legend node
This allows users to (optionally!) customise the symbol appearance for a legend node, e.g. to tweak the colors or symbol sizes to better provide a "representative" patch symbol compared with how those corresponding features actually appear on the map. It's useful for exaggerating symbol widths, or for manually tweaking the colors of semi-transparent symbols so that the colors represent the actual appearance of the symbols when rendered on top of the map content. Or to tweak the marker interval/offset in marker lines so that the markers are nicely spaced in the legend patch. Fixes #14077
1 parent 8f7bb7f commit 7a6220a

File tree

9 files changed

+268
-5
lines changed

9 files changed

+268
-5
lines changed
 

‎python/core/auto_generated/layertree/qgslayertreemodellegendnode.sip.in

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -366,6 +366,32 @@ Sets the symbol patch ``shape`` to use when rendering the legend node symbol.
366366

367367
.. seealso:: :py:func:`patchShape`
368368

369+
.. versionadded:: 3.14
370+
%End
371+
372+
QgsSymbol *customSymbol() const;
373+
%Docstring
374+
Returns the node's custom symbol.
375+
376+
If a non-``None`` value is returned, then this symbol will be used for rendering
377+
the legend node instead of the default symbol().
378+
379+
.. seealso:: :py:func:`setCustomSymbol`
380+
381+
.. versionadded:: 3.14
382+
%End
383+
384+
void setCustomSymbol( QgsSymbol *symbol /Transfer/ );
385+
%Docstring
386+
Sets the node's custom ``symbol``.
387+
388+
If a non-``None`` value is set, then this symbol will be used for rendering
389+
the legend node instead of the default symbol().
390+
391+
Ownership of ``symbol`` is transferred.
392+
393+
.. seealso:: :py:func:`customSymbol`
394+
369395
.. versionadded:: 3.14
370396
%End
371397

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

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,32 @@ symbol width or height from :py:class:`QgsLegendSettings`.
135135

136136
.. seealso:: :py:func:`setLegendNodeSymbolSize`
137137

138+
.. versionadded:: 3.14
139+
%End
140+
141+
static void setLegendNodeCustomSymbol( QgsLayerTreeLayer *nodeLayer, int originalIndex, const QgsSymbol *symbol );
142+
%Docstring
143+
Sets a custom legend ``symbol`` size for the legend node belonging to ``nodeLayer`` at the specified ``originalIndex``.
144+
145+
If ``symbol`` is non-``None``, it will be used in place of the default symbol when rendering
146+
the legend node.
147+
148+
.. seealso:: :py:func:`legendNodeCustomSymbol`
149+
150+
.. versionadded:: 3.14
151+
%End
152+
153+
static QgsSymbol *legendNodeCustomSymbol( QgsLayerTreeLayer *nodeLayer, int originalIndex ) /Factory/;
154+
%Docstring
155+
Returns the custom legend symbol for the legend node belonging to ``nodeLayer`` at the specified ``originalIndex``.
156+
157+
If the symbol is non-``None``, it will be used in place of the default symbol when rendering
158+
the legend node.
159+
160+
Caller takes ownership of the returned symbol.
161+
162+
.. seealso:: :py:func:`setLegendNodeCustomSymbol`
163+
138164
.. versionadded:: 3.14
139165
%End
140166

‎src/core/layertree/qgslayertreemodellegendnode.cpp

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,16 @@ void QgsSymbolLegendNode::setPatchShape( const QgsLegendPatchShape &shape )
336336
mPatchShape = shape;
337337
}
338338

339+
QgsSymbol *QgsSymbolLegendNode::customSymbol() const
340+
{
341+
return mCustomSymbol.get();
342+
}
343+
344+
void QgsSymbolLegendNode::setCustomSymbol( QgsSymbol *symbol )
345+
{
346+
mCustomSymbol.reset( symbol );
347+
}
348+
339349
void QgsSymbolLegendNode::setSymbol( QgsSymbol *symbol )
340350
{
341351
if ( !symbol )
@@ -517,7 +527,7 @@ bool QgsSymbolLegendNode::setData( const QVariant &value, int role )
517527

518528
QSizeF QgsSymbolLegendNode::drawSymbol( const QgsLegendSettings &settings, ItemContext *ctx, double itemHeight ) const
519529
{
520-
QgsSymbol *s = mItem.symbol();
530+
QgsSymbol *s = mCustomSymbol ? mCustomSymbol.get() : mItem.symbol();
521531
if ( !s )
522532
{
523533
return QSizeF();
@@ -652,7 +662,7 @@ QSizeF QgsSymbolLegendNode::drawSymbol( const QgsLegendSettings &settings, ItemC
652662

653663
void QgsSymbolLegendNode::exportSymbolToJson( const QgsLegendSettings &settings, const QgsRenderContext &context, QJsonObject &json ) const
654664
{
655-
const QgsSymbol *s = mItem.symbol();
665+
const QgsSymbol *s = mCustomSymbol ? mCustomSymbol.get() : mItem.symbol();
656666
if ( !s )
657667
{
658668
return;

‎src/core/layertree/qgslayertreemodellegendnode.h

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -391,6 +391,30 @@ class CORE_EXPORT QgsSymbolLegendNode : public QgsLayerTreeModelLegendNode
391391
*/
392392
void setPatchShape( const QgsLegendPatchShape &shape );
393393

394+
/**
395+
* Returns the node's custom symbol.
396+
*
397+
* If a non-NULLPTR value is returned, then this symbol will be used for rendering
398+
* the legend node instead of the default symbol().
399+
*
400+
* \see setCustomSymbol()
401+
* \since QGIS 3.14
402+
*/
403+
QgsSymbol *customSymbol() const;
404+
405+
/**
406+
* Sets the node's custom \a symbol.
407+
*
408+
* If a non-NULLPTR value is set, then this symbol will be used for rendering
409+
* the legend node instead of the default symbol().
410+
*
411+
* Ownership of \a symbol is transferred.
412+
*
413+
* \see customSymbol()
414+
* \since QGIS 3.14
415+
*/
416+
void setCustomSymbol( QgsSymbol *symbol SIP_TRANSFER );
417+
394418
/**
395419
* Evaluates and returns the text label of the current node
396420
* \param context extra QgsExpressionContext to use for evaluating the expression
@@ -438,6 +462,8 @@ class CORE_EXPORT QgsSymbolLegendNode : public QgsLayerTreeModelLegendNode
438462
QString mTextOnSymbolLabel;
439463
QgsTextFormat mTextOnSymbolTextFormat;
440464

465+
std::unique_ptr< QgsSymbol > mCustomSymbol;
466+
441467
// ident the symbol icon to make it look like a tree structure
442468
static const int INDENT_SIZE = 20;
443469

‎src/core/qgsmaplayerlegend.cpp

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,37 @@ QSizeF QgsMapLayerLegendUtils::legendNodeSymbolSize( QgsLayerTreeLayer *nodeLaye
185185
return QgsSymbolLayerUtils::decodeSize( size );
186186
}
187187

188+
void QgsMapLayerLegendUtils::setLegendNodeCustomSymbol( QgsLayerTreeLayer *nodeLayer, int originalIndex, const QgsSymbol *symbol )
189+
{
190+
if ( symbol )
191+
{
192+
QDomDocument doc;
193+
QgsReadWriteContext rwContext;
194+
rwContext.setPathResolver( QgsProject::instance()->pathResolver() );
195+
QDomElement elem = QgsSymbolLayerUtils::saveSymbol( QStringLiteral( "custom symbol" ), symbol, doc, rwContext );
196+
doc.appendChild( elem );
197+
nodeLayer->setCustomProperty( "legend/custom-symbol-" + QString::number( originalIndex ), doc.toString() );
198+
}
199+
else
200+
nodeLayer->removeCustomProperty( "legend/custom-symbol-" + QString::number( originalIndex ) );
201+
}
202+
203+
QgsSymbol *QgsMapLayerLegendUtils::legendNodeCustomSymbol( QgsLayerTreeLayer *nodeLayer, int originalIndex )
204+
{
205+
const QString symbolDef = nodeLayer->customProperty( "legend/custom-symbol-" + QString::number( originalIndex ) ).toString();
206+
if ( symbolDef.isEmpty() )
207+
return nullptr;
208+
209+
QDomDocument doc;
210+
doc.setContent( symbolDef );
211+
const QDomElement elem = doc.documentElement();
212+
213+
QgsReadWriteContext rwContext;
214+
rwContext.setPathResolver( QgsProject::instance()->pathResolver() );
215+
216+
return QgsSymbolLayerUtils::loadSymbol( elem, rwContext );
217+
}
218+
188219
void QgsMapLayerLegendUtils::applyLayerNodeProperties( QgsLayerTreeLayer *nodeLayer, QList<QgsLayerTreeModelLegendNode *> &nodes )
189220
{
190221
// handle user labels
@@ -200,6 +231,8 @@ void QgsMapLayerLegendUtils::applyLayerNodeProperties( QgsLayerTreeLayer *nodeLa
200231
{
201232
const QgsLegendPatchShape shape = QgsMapLayerLegendUtils::legendNodePatchShape( nodeLayer, i );
202233
symbolNode->setPatchShape( shape );
234+
235+
symbolNode->setCustomSymbol( QgsMapLayerLegendUtils::legendNodeCustomSymbol( nodeLayer, i ) );
203236
}
204237

205238
const QSizeF userSize = QgsMapLayerLegendUtils::legendNodeSymbolSize( nodeLayer, i );

‎src/core/qgsmaplayerlegend.h

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ class QgsRasterLayer;
3030
class QgsReadWriteContext;
3131
class QgsVectorLayer;
3232
class QgsLegendPatchShape;
33+
class QgsSymbol;
3334

3435
#include "qgis_core.h"
3536

@@ -141,6 +142,30 @@ class CORE_EXPORT QgsMapLayerLegendUtils
141142
*/
142143
static QSizeF legendNodeSymbolSize( QgsLayerTreeLayer *nodeLayer, int originalIndex );
143144

145+
/**
146+
* Sets a custom legend \a symbol size for the legend node belonging to \a nodeLayer at the specified \a originalIndex.
147+
*
148+
* If \a symbol is non-NULLPTR, it will be used in place of the default symbol when rendering
149+
* the legend node.
150+
*
151+
* \see legendNodeCustomSymbol()
152+
* \since QGIS 3.14
153+
*/
154+
static void setLegendNodeCustomSymbol( QgsLayerTreeLayer *nodeLayer, int originalIndex, const QgsSymbol *symbol );
155+
156+
/**
157+
* Returns the custom legend symbol for the legend node belonging to \a nodeLayer at the specified \a originalIndex.
158+
*
159+
* If the symbol is non-NULLPTR, it will be used in place of the default symbol when rendering
160+
* the legend node.
161+
*
162+
* Caller takes ownership of the returned symbol.
163+
*
164+
* \see setLegendNodeCustomSymbol()
165+
* \since QGIS 3.14
166+
*/
167+
static QgsSymbol *legendNodeCustomSymbol( QgsLayerTreeLayer *nodeLayer, int originalIndex ) SIP_FACTORY;
168+
144169
//! update according to layer node's custom properties (order of items, user labels for items)
145170
static void applyLayerNodeProperties( QgsLayerTreeLayer *nodeLayer, QList<QgsLayerTreeModelLegendNode *> &nodes );
146171
};

‎src/gui/layout/qgslayoutlegendwidget.cpp

Lines changed: 79 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1446,6 +1446,7 @@ QgsLayoutLegendNodeWidget::QgsLayoutLegendNodeWidget( QgsLayoutItemLegend *legen
14461446
mHeightSpinBox->setVisible( mLegendNode || mLayer );
14471447
mPatchWidthLabel->setVisible( mLegendNode || mLayer );
14481448
mPatchHeightLabel->setVisible( mLegendNode || mLayer );
1449+
mCustomSymbolCheckBox->setVisible( mLegendNode || mLegend->model()->legendNodeEmbeddedInParent( mLayer ) );
14491450
if ( mLegendNode )
14501451
{
14511452
mWidthSpinBox->setValue( mLegendNode->userPatchSize().width() );
@@ -1457,18 +1458,47 @@ QgsLayoutLegendNodeWidget::QgsLayoutLegendNodeWidget( QgsLayoutItemLegend *legen
14571458
mHeightSpinBox->setValue( mLayer->patchSize().height() );
14581459
}
14591460

1461+
mCustomSymbolCheckBox->setChecked( false );
1462+
14601463
QgsLegendPatchShape patchShape;
14611464
if ( QgsSymbolLegendNode *symbolLegendNode = dynamic_cast< QgsSymbolLegendNode * >( mLegendNode ) )
14621465
{
14631466
patchShape = symbolLegendNode->patchShape();
1464-
if ( symbolLegendNode->symbol() )
1467+
1468+
std::unique_ptr< QgsSymbol > customSymbol( symbolLegendNode->customSymbol() ? symbolLegendNode->customSymbol()->clone() : nullptr );
1469+
mCustomSymbolCheckBox->setChecked( customSymbol.get() );
1470+
if ( customSymbol )
1471+
{
1472+
mPatchShapeButton->setPreviewSymbol( customSymbol->clone() );
1473+
mCustomSymbolButton->setSymbolType( customSymbol->type() );
1474+
mCustomSymbolButton->setSymbol( customSymbol.release() );
1475+
}
1476+
else if ( symbolLegendNode->symbol() )
1477+
{
14651478
mPatchShapeButton->setPreviewSymbol( symbolLegendNode->symbol()->clone() );
1479+
mCustomSymbolButton->setSymbolType( symbolLegendNode->symbol()->type() );
1480+
mCustomSymbolButton->setSymbol( symbolLegendNode->symbol()->clone() );
1481+
}
14661482
}
14671483
else if ( !mLegendNode && mLayer )
14681484
{
14691485
patchShape = mLayer->patchShape();
14701486
if ( QgsSymbolLegendNode *symbolLegendNode = dynamic_cast< QgsSymbolLegendNode * >( mLegend->model()->legendNodeEmbeddedInParent( mLayer ) ) )
1471-
mPatchShapeButton->setPreviewSymbol( symbolLegendNode->symbol()->clone() );
1487+
{
1488+
if ( QgsSymbol *customSymbol = symbolLegendNode->customSymbol() )
1489+
{
1490+
mCustomSymbolCheckBox->setChecked( true );
1491+
mPatchShapeButton->setPreviewSymbol( customSymbol->clone() );
1492+
mCustomSymbolButton->setSymbolType( customSymbol->type() );
1493+
mCustomSymbolButton->setSymbol( customSymbol->clone() );
1494+
}
1495+
else
1496+
{
1497+
mPatchShapeButton->setPreviewSymbol( symbolLegendNode->symbol()->clone() );
1498+
mCustomSymbolButton->setSymbolType( symbolLegendNode->symbol()->type() );
1499+
mCustomSymbolButton->setSymbol( symbolLegendNode->symbol()->clone() );
1500+
}
1501+
}
14721502
}
14731503

14741504
if ( mLayer && mLayer->layer() && mLayer->layer()->type() == QgsMapLayerType::VectorLayer )
@@ -1509,6 +1539,9 @@ QgsLayoutLegendNodeWidget::QgsLayoutLegendNodeWidget( QgsLayoutItemLegend *legen
15091539

15101540
connect( mWidthSpinBox, qgis::overload<double>::of( &QgsDoubleSpinBox::valueChanged ), this, &QgsLayoutLegendNodeWidget::sizeChanged );
15111541
connect( mHeightSpinBox, qgis::overload<double>::of( &QgsDoubleSpinBox::valueChanged ), this, &QgsLayoutLegendNodeWidget::sizeChanged );
1542+
1543+
connect( mCustomSymbolCheckBox, &QGroupBox::toggled, this, &QgsLayoutLegendNodeWidget::customSymbolChanged );
1544+
connect( mCustomSymbolButton, &QgsSymbolButton::changed, this, &QgsLayoutLegendNodeWidget::customSymbolChanged );
15121545
}
15131546

15141547
void QgsLayoutLegendNodeWidget::labelChanged()
@@ -1637,4 +1670,48 @@ void QgsLayoutLegendNodeWidget::sizeChanged( double )
16371670
mLegend->endCommand();
16381671
}
16391672

1673+
void QgsLayoutLegendNodeWidget::customSymbolChanged()
1674+
{
1675+
mLegend->beginCommand( tr( "Edit Legend Item" ) );
1676+
1677+
if ( mCustomSymbolCheckBox->isChecked() )
1678+
{
1679+
if ( mLegendNode )
1680+
{
1681+
QgsMapLayerLegendUtils::setLegendNodeCustomSymbol( mLayer, mOriginalLegendNodeIndex, mCustomSymbolButton->symbol() );
1682+
mLegend->model()->refreshLayerLegend( mLayer );
1683+
}
1684+
else if ( mLayer )
1685+
{
1686+
const QList<QgsLayerTreeModelLegendNode *> layerLegendNodes = mLegend->model()->layerLegendNodes( mLayer, false );
1687+
for ( QgsLayerTreeModelLegendNode *node : layerLegendNodes )
1688+
{
1689+
QgsMapLayerLegendUtils::setLegendNodeCustomSymbol( mLayer, _originalLegendNodeIndex( node ), mCustomSymbolButton->symbol() );
1690+
}
1691+
mLegend->model()->refreshLayerLegend( mLayer );
1692+
}
1693+
}
1694+
else
1695+
{
1696+
if ( mLegendNode )
1697+
{
1698+
QgsMapLayerLegendUtils::setLegendNodeCustomSymbol( mLayer, mOriginalLegendNodeIndex, nullptr );
1699+
mLegend->model()->refreshLayerLegend( mLayer );
1700+
}
1701+
else if ( mLayer )
1702+
{
1703+
const QList<QgsLayerTreeModelLegendNode *> layerLegendNodes = mLegend->model()->layerLegendNodes( mLayer, false );
1704+
for ( QgsLayerTreeModelLegendNode *node : layerLegendNodes )
1705+
{
1706+
QgsMapLayerLegendUtils::setLegendNodeCustomSymbol( mLayer, _originalLegendNodeIndex( node ), nullptr );
1707+
}
1708+
mLegend->model()->refreshLayerLegend( mLayer );
1709+
}
1710+
}
1711+
1712+
mLegend->adjustBoxSize();
1713+
mLegend->updateFilterByMap();
1714+
mLegend->endCommand();
1715+
}
1716+
16401717
///@endcond

‎src/gui/layout/qgslayoutlegendwidget.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,7 @@ class GUI_EXPORT QgsLayoutLegendNodeWidget: public QgsPanelWidget, private Ui::Q
194194
void patchChanged();
195195
void insertExpression();
196196
void sizeChanged( double );
197+
void customSymbolChanged();
197198

198199
private:
199200

‎src/ui/layout/qgslayoutlegendnodewidgetbase.ui

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
<x>0</x>
88
<y>0</y>
99
<width>351</width>
10-
<height>381</height>
10+
<height>456</height>
1111
</rect>
1212
</property>
1313
<property name="windowTitle">
@@ -114,6 +114,40 @@
114114
</layout>
115115
</widget>
116116
</item>
117+
<item row="3" column="0" colspan="2">
118+
<widget class="QGroupBox" name="mCustomSymbolCheckBox">
119+
<property name="title">
120+
<string>Custom Symbol</string>
121+
</property>
122+
<property name="checkable">
123+
<bool>true</bool>
124+
</property>
125+
<layout class="QVBoxLayout" name="verticalLayout">
126+
<item>
127+
<widget class="QgsSymbolButton" name="mCustomSymbolButton">
128+
<property name="enabled">
129+
<bool>true</bool>
130+
</property>
131+
<property name="sizePolicy">
132+
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
133+
<horstretch>0</horstretch>
134+
<verstretch>0</verstretch>
135+
</sizepolicy>
136+
</property>
137+
<property name="minimumSize">
138+
<size>
139+
<width>100</width>
140+
<height>0</height>
141+
</size>
142+
</property>
143+
<property name="text">
144+
<string/>
145+
</property>
146+
</widget>
147+
</item>
148+
</layout>
149+
</widget>
150+
</item>
117151
</layout>
118152
</widget>
119153
<customwidgets>
@@ -133,6 +167,11 @@
133167
<extends>QToolButton</extends>
134168
<header>qgslegendpatchshapebutton.h</header>
135169
</customwidget>
170+
<customwidget>
171+
<class>QgsSymbolButton</class>
172+
<extends>QToolButton</extends>
173+
<header>qgssymbolbutton.h</header>
174+
</customwidget>
136175
</customwidgets>
137176
<tabstops>
138177
<tabstop>mLabelEdit</tabstop>

0 commit comments

Comments
 (0)
Please sign in to comment.