Skip to content
Permalink
Browse files

[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
  • Loading branch information
nyalldawson committed May 4, 2020
1 parent 8f7bb7f commit 7a6220a77fda01845af37c7aa0a32410a6cd27ec
@@ -366,6 +366,32 @@ Sets the symbol patch ``shape`` to use when rendering the legend node symbol.

.. seealso:: :py:func:`patchShape`

.. versionadded:: 3.14
%End

QgsSymbol *customSymbol() const;
%Docstring
Returns the node's custom symbol.

If a non-``None`` value is returned, then this symbol will be used for rendering
the legend node instead of the default symbol().

.. seealso:: :py:func:`setCustomSymbol`

.. versionadded:: 3.14
%End

void setCustomSymbol( QgsSymbol *symbol /Transfer/ );
%Docstring
Sets the node's custom ``symbol``.

If a non-``None`` value is set, then this symbol will be used for rendering
the legend node instead of the default symbol().

Ownership of ``symbol`` is transferred.

.. seealso:: :py:func:`customSymbol`

.. versionadded:: 3.14
%End

@@ -135,6 +135,32 @@ symbol width or height from :py:class:`QgsLegendSettings`.

.. seealso:: :py:func:`setLegendNodeSymbolSize`

.. versionadded:: 3.14
%End

static void setLegendNodeCustomSymbol( QgsLayerTreeLayer *nodeLayer, int originalIndex, const QgsSymbol *symbol );
%Docstring
Sets a custom legend ``symbol`` size for the legend node belonging to ``nodeLayer`` at the specified ``originalIndex``.

If ``symbol`` is non-``None``, it will be used in place of the default symbol when rendering
the legend node.

.. seealso:: :py:func:`legendNodeCustomSymbol`

.. versionadded:: 3.14
%End

static QgsSymbol *legendNodeCustomSymbol( QgsLayerTreeLayer *nodeLayer, int originalIndex ) /Factory/;
%Docstring
Returns the custom legend symbol for the legend node belonging to ``nodeLayer`` at the specified ``originalIndex``.

If the symbol is non-``None``, it will be used in place of the default symbol when rendering
the legend node.

Caller takes ownership of the returned symbol.

.. seealso:: :py:func:`setLegendNodeCustomSymbol`

.. versionadded:: 3.14
%End

@@ -336,6 +336,16 @@ void QgsSymbolLegendNode::setPatchShape( const QgsLegendPatchShape &shape )
mPatchShape = shape;
}

QgsSymbol *QgsSymbolLegendNode::customSymbol() const
{
return mCustomSymbol.get();
}

void QgsSymbolLegendNode::setCustomSymbol( QgsSymbol *symbol )
{
mCustomSymbol.reset( symbol );
}

void QgsSymbolLegendNode::setSymbol( QgsSymbol *symbol )
{
if ( !symbol )
@@ -517,7 +527,7 @@ bool QgsSymbolLegendNode::setData( const QVariant &value, int role )

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

void QgsSymbolLegendNode::exportSymbolToJson( const QgsLegendSettings &settings, const QgsRenderContext &context, QJsonObject &json ) const
{
const QgsSymbol *s = mItem.symbol();
const QgsSymbol *s = mCustomSymbol ? mCustomSymbol.get() : mItem.symbol();
if ( !s )
{
return;
@@ -391,6 +391,30 @@ class CORE_EXPORT QgsSymbolLegendNode : public QgsLayerTreeModelLegendNode
*/
void setPatchShape( const QgsLegendPatchShape &shape );

/**
* Returns the node's custom symbol.
*
* If a non-NULLPTR value is returned, then this symbol will be used for rendering
* the legend node instead of the default symbol().
*
* \see setCustomSymbol()
* \since QGIS 3.14
*/
QgsSymbol *customSymbol() const;

/**
* Sets the node's custom \a symbol.
*
* If a non-NULLPTR value is set, then this symbol will be used for rendering
* the legend node instead of the default symbol().
*
* Ownership of \a symbol is transferred.
*
* \see customSymbol()
* \since QGIS 3.14
*/
void setCustomSymbol( QgsSymbol *symbol SIP_TRANSFER );

/**
* Evaluates and returns the text label of the current node
* \param context extra QgsExpressionContext to use for evaluating the expression
@@ -438,6 +462,8 @@ class CORE_EXPORT QgsSymbolLegendNode : public QgsLayerTreeModelLegendNode
QString mTextOnSymbolLabel;
QgsTextFormat mTextOnSymbolTextFormat;

std::unique_ptr< QgsSymbol > mCustomSymbol;

// ident the symbol icon to make it look like a tree structure
static const int INDENT_SIZE = 20;

@@ -185,6 +185,37 @@ QSizeF QgsMapLayerLegendUtils::legendNodeSymbolSize( QgsLayerTreeLayer *nodeLaye
return QgsSymbolLayerUtils::decodeSize( size );
}

void QgsMapLayerLegendUtils::setLegendNodeCustomSymbol( QgsLayerTreeLayer *nodeLayer, int originalIndex, const QgsSymbol *symbol )
{
if ( symbol )
{
QDomDocument doc;
QgsReadWriteContext rwContext;
rwContext.setPathResolver( QgsProject::instance()->pathResolver() );
QDomElement elem = QgsSymbolLayerUtils::saveSymbol( QStringLiteral( "custom symbol" ), symbol, doc, rwContext );
doc.appendChild( elem );
nodeLayer->setCustomProperty( "legend/custom-symbol-" + QString::number( originalIndex ), doc.toString() );
}
else
nodeLayer->removeCustomProperty( "legend/custom-symbol-" + QString::number( originalIndex ) );
}

QgsSymbol *QgsMapLayerLegendUtils::legendNodeCustomSymbol( QgsLayerTreeLayer *nodeLayer, int originalIndex )
{
const QString symbolDef = nodeLayer->customProperty( "legend/custom-symbol-" + QString::number( originalIndex ) ).toString();
if ( symbolDef.isEmpty() )
return nullptr;

QDomDocument doc;
doc.setContent( symbolDef );
const QDomElement elem = doc.documentElement();

QgsReadWriteContext rwContext;
rwContext.setPathResolver( QgsProject::instance()->pathResolver() );

return QgsSymbolLayerUtils::loadSymbol( elem, rwContext );
}

void QgsMapLayerLegendUtils::applyLayerNodeProperties( QgsLayerTreeLayer *nodeLayer, QList<QgsLayerTreeModelLegendNode *> &nodes )
{
// handle user labels
@@ -200,6 +231,8 @@ void QgsMapLayerLegendUtils::applyLayerNodeProperties( QgsLayerTreeLayer *nodeLa
{
const QgsLegendPatchShape shape = QgsMapLayerLegendUtils::legendNodePatchShape( nodeLayer, i );
symbolNode->setPatchShape( shape );

symbolNode->setCustomSymbol( QgsMapLayerLegendUtils::legendNodeCustomSymbol( nodeLayer, i ) );
}

const QSizeF userSize = QgsMapLayerLegendUtils::legendNodeSymbolSize( nodeLayer, i );
@@ -30,6 +30,7 @@ class QgsRasterLayer;
class QgsReadWriteContext;
class QgsVectorLayer;
class QgsLegendPatchShape;
class QgsSymbol;

#include "qgis_core.h"

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

/**
* Sets a custom legend \a symbol size for the legend node belonging to \a nodeLayer at the specified \a originalIndex.
*
* If \a symbol is non-NULLPTR, it will be used in place of the default symbol when rendering
* the legend node.
*
* \see legendNodeCustomSymbol()
* \since QGIS 3.14
*/
static void setLegendNodeCustomSymbol( QgsLayerTreeLayer *nodeLayer, int originalIndex, const QgsSymbol *symbol );

/**
* Returns the custom legend symbol for the legend node belonging to \a nodeLayer at the specified \a originalIndex.
*
* If the symbol is non-NULLPTR, it will be used in place of the default symbol when rendering
* the legend node.
*
* Caller takes ownership of the returned symbol.
*
* \see setLegendNodeCustomSymbol()
* \since QGIS 3.14
*/
static QgsSymbol *legendNodeCustomSymbol( QgsLayerTreeLayer *nodeLayer, int originalIndex ) SIP_FACTORY;

//! update according to layer node's custom properties (order of items, user labels for items)
static void applyLayerNodeProperties( QgsLayerTreeLayer *nodeLayer, QList<QgsLayerTreeModelLegendNode *> &nodes );
};
@@ -1446,6 +1446,7 @@ QgsLayoutLegendNodeWidget::QgsLayoutLegendNodeWidget( QgsLayoutItemLegend *legen
mHeightSpinBox->setVisible( mLegendNode || mLayer );
mPatchWidthLabel->setVisible( mLegendNode || mLayer );
mPatchHeightLabel->setVisible( mLegendNode || mLayer );
mCustomSymbolCheckBox->setVisible( mLegendNode || mLegend->model()->legendNodeEmbeddedInParent( mLayer ) );
if ( mLegendNode )
{
mWidthSpinBox->setValue( mLegendNode->userPatchSize().width() );
@@ -1457,18 +1458,47 @@ QgsLayoutLegendNodeWidget::QgsLayoutLegendNodeWidget( QgsLayoutItemLegend *legen
mHeightSpinBox->setValue( mLayer->patchSize().height() );
}

mCustomSymbolCheckBox->setChecked( false );

QgsLegendPatchShape patchShape;
if ( QgsSymbolLegendNode *symbolLegendNode = dynamic_cast< QgsSymbolLegendNode * >( mLegendNode ) )
{
patchShape = symbolLegendNode->patchShape();
if ( symbolLegendNode->symbol() )

std::unique_ptr< QgsSymbol > customSymbol( symbolLegendNode->customSymbol() ? symbolLegendNode->customSymbol()->clone() : nullptr );
mCustomSymbolCheckBox->setChecked( customSymbol.get() );
if ( customSymbol )
{
mPatchShapeButton->setPreviewSymbol( customSymbol->clone() );
mCustomSymbolButton->setSymbolType( customSymbol->type() );
mCustomSymbolButton->setSymbol( customSymbol.release() );
}
else if ( symbolLegendNode->symbol() )
{
mPatchShapeButton->setPreviewSymbol( symbolLegendNode->symbol()->clone() );
mCustomSymbolButton->setSymbolType( symbolLegendNode->symbol()->type() );
mCustomSymbolButton->setSymbol( symbolLegendNode->symbol()->clone() );
}
}
else if ( !mLegendNode && mLayer )
{
patchShape = mLayer->patchShape();
if ( QgsSymbolLegendNode *symbolLegendNode = dynamic_cast< QgsSymbolLegendNode * >( mLegend->model()->legendNodeEmbeddedInParent( mLayer ) ) )
mPatchShapeButton->setPreviewSymbol( symbolLegendNode->symbol()->clone() );
{
if ( QgsSymbol *customSymbol = symbolLegendNode->customSymbol() )
{
mCustomSymbolCheckBox->setChecked( true );
mPatchShapeButton->setPreviewSymbol( customSymbol->clone() );
mCustomSymbolButton->setSymbolType( customSymbol->type() );
mCustomSymbolButton->setSymbol( customSymbol->clone() );
}
else
{
mPatchShapeButton->setPreviewSymbol( symbolLegendNode->symbol()->clone() );
mCustomSymbolButton->setSymbolType( symbolLegendNode->symbol()->type() );
mCustomSymbolButton->setSymbol( symbolLegendNode->symbol()->clone() );
}
}
}

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

connect( mWidthSpinBox, qgis::overload<double>::of( &QgsDoubleSpinBox::valueChanged ), this, &QgsLayoutLegendNodeWidget::sizeChanged );
connect( mHeightSpinBox, qgis::overload<double>::of( &QgsDoubleSpinBox::valueChanged ), this, &QgsLayoutLegendNodeWidget::sizeChanged );

connect( mCustomSymbolCheckBox, &QGroupBox::toggled, this, &QgsLayoutLegendNodeWidget::customSymbolChanged );
connect( mCustomSymbolButton, &QgsSymbolButton::changed, this, &QgsLayoutLegendNodeWidget::customSymbolChanged );
}

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

void QgsLayoutLegendNodeWidget::customSymbolChanged()
{
mLegend->beginCommand( tr( "Edit Legend Item" ) );

if ( mCustomSymbolCheckBox->isChecked() )
{
if ( mLegendNode )
{
QgsMapLayerLegendUtils::setLegendNodeCustomSymbol( mLayer, mOriginalLegendNodeIndex, mCustomSymbolButton->symbol() );
mLegend->model()->refreshLayerLegend( mLayer );
}
else if ( mLayer )
{
const QList<QgsLayerTreeModelLegendNode *> layerLegendNodes = mLegend->model()->layerLegendNodes( mLayer, false );
for ( QgsLayerTreeModelLegendNode *node : layerLegendNodes )
{
QgsMapLayerLegendUtils::setLegendNodeCustomSymbol( mLayer, _originalLegendNodeIndex( node ), mCustomSymbolButton->symbol() );
}
mLegend->model()->refreshLayerLegend( mLayer );
}
}
else
{
if ( mLegendNode )
{
QgsMapLayerLegendUtils::setLegendNodeCustomSymbol( mLayer, mOriginalLegendNodeIndex, nullptr );
mLegend->model()->refreshLayerLegend( mLayer );
}
else if ( mLayer )
{
const QList<QgsLayerTreeModelLegendNode *> layerLegendNodes = mLegend->model()->layerLegendNodes( mLayer, false );
for ( QgsLayerTreeModelLegendNode *node : layerLegendNodes )
{
QgsMapLayerLegendUtils::setLegendNodeCustomSymbol( mLayer, _originalLegendNodeIndex( node ), nullptr );
}
mLegend->model()->refreshLayerLegend( mLayer );
}
}

mLegend->adjustBoxSize();
mLegend->updateFilterByMap();
mLegend->endCommand();
}

///@endcond
@@ -194,6 +194,7 @@ class GUI_EXPORT QgsLayoutLegendNodeWidget: public QgsPanelWidget, private Ui::Q
void patchChanged();
void insertExpression();
void sizeChanged( double );
void customSymbolChanged();

private:

0 comments on commit 7a6220a

Please sign in to comment.
You can’t perform that action at this time.