diff --git a/python/core/layertree/qgslayertreemodellegendnode.sip.in b/python/core/layertree/qgslayertreemodellegendnode.sip.in index 2510b472d69..c1db2707b1d 100644 --- a/python/core/layertree/qgslayertreemodellegendnode.sip.in +++ b/python/core/layertree/qgslayertreemodellegendnode.sip.in @@ -231,6 +231,34 @@ to the associated vector layer's renderer. .. seealso:: :py:func:`symbol` .. versionadded:: 2.14 +%End + + QString textOnSymbolLabel() const; +%Docstring +Returns label of text to be shown on top of the symbol. + +.. versionadded:: 3.2 +%End + + void setTextOnSymbolLabel( const QString &label ); +%Docstring +Sets label of text to be shown on top of the symbol. + +.. versionadded:: 3.2 +%End + + QgsTextFormat textOnSymbolTextFormat() const; +%Docstring +Returns text format of the label to be shown on top of the symbol. + +.. versionadded:: 3.2 +%End + + void setTextOnSymbolTextFormat( const QgsTextFormat &format ); +%Docstring +Sets format of text to be shown on top of the symbol. + +.. versionadded:: 3.2 %End public slots: diff --git a/python/core/qgsmaplayerlegend.sip.in b/python/core/qgsmaplayerlegend.sip.in index 1cb1d37eda6..873e8440368 100644 --- a/python/core/qgsmaplayerlegend.sip.in +++ b/python/core/qgsmaplayerlegend.sip.in @@ -11,6 +11,7 @@ + class QgsMapLayerLegend : QObject { %Docstring @@ -31,6 +32,20 @@ Constructor for QgsMapLayerLegend %End + virtual void readXml( const QDomElement &elem, const QgsReadWriteContext &context ); +%Docstring +Reads configuration from a DOM element previously written by writeXml() + +.. versionadded:: 3.2 +%End + + virtual QDomElement writeXml( QDomDocument &doc, const QgsReadWriteContext &context ) const; +%Docstring +Writes configuration to a DOM element, to be used later with readXml() + +.. versionadded:: 3.2 +%End + virtual QList createLayerTreeModelLegendNodes( QgsLayerTreeLayer *nodeLayer ) = 0 /Factory/; %Docstring Return list of legend nodes to be used for a particular layer tree layer node. @@ -84,6 +99,7 @@ update according to layer node's custom properties (order of items, user labels + class QgsDefaultVectorLayerLegend : QgsMapLayerLegend { %Docstring @@ -98,8 +114,58 @@ Default legend implementation for vector layers public: explicit QgsDefaultVectorLayerLegend( QgsVectorLayer *vl ); + bool textOnSymbolEnabled() const; +%Docstring +Returns whether the "text on symbol" functionality is enabled. When enabled, legend symbols +may have extra text rendered on top. The content of labels and their style is controlled +by textOnSymbolContent() and textOnSymbolTextFormat(). + +.. versionadded:: 3.2 +%End + + void setTextOnSymbolEnabled( bool enabled ); +%Docstring +Sets whether the "text on symbol" functionality is enabled. When enabled, legend symbols +may have extra text rendered on top. The content of labels and their style is controlled +by textOnSymbolContent() and textOnSymbolTextFormat(). + +.. versionadded:: 3.2 +%End + + QgsTextFormat textOnSymbolTextFormat() const; +%Docstring +Returns text format of symbol labels for "text on symbol" functionality. + +.. versionadded:: 3.2 +%End + + void setTextOnSymbolTextFormat( const QgsTextFormat &format ); +%Docstring +Sets text format of symbol labels for "text on symbol" functionality. + +.. versionadded:: 3.2 +%End + + QHash textOnSymbolContent() const; +%Docstring +Returns per-symbol content of labels for "text on symbol" functionality. In the passed dictionary +the keys are rule keys of legend items, the values are labels to be shown. + +.. versionadded:: 3.2 +%End + + void setTextOnSymbolContent( const QHash &content ); +%Docstring +Sets per-symbol content of labels for "text on symbol" functionality. In the passed dictionary +the keys are rule keys of legend items, the values are labels to be shown. + +.. versionadded:: 3.2 +%End + virtual QList createLayerTreeModelLegendNodes( QgsLayerTreeLayer *nodeLayer ) /Factory/; + virtual void readXml( const QDomElement &elem, const QgsReadWriteContext &context ); + virtual QDomElement writeXml( QDomDocument &doc, const QgsReadWriteContext &context ) const; }; diff --git a/python/gui/qgstextformatwidget.sip.in b/python/gui/qgstextformatwidget.sip.in index 55ad6c2dcf7..604517a712d 100644 --- a/python/gui/qgstextformatwidget.sip.in +++ b/python/gui/qgstextformatwidget.sip.in @@ -48,6 +48,13 @@ Constructor for QgsTextFormatWidget. QgsTextFormat format() const; %Docstring Returns the current formatting settings defined by the widget. +%End + + void setFormat( const QgsTextFormat &format ); +%Docstring +Sets the current formatting settings + +.. versionadded:: 3.2 %End public slots: diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt index 346f51deec4..84b021e8670 100755 --- a/src/app/CMakeLists.txt +++ b/src/app/CMakeLists.txt @@ -133,6 +133,7 @@ SET(QGIS_APP_SRCS qgstextannotationdialog.cpp qgssvgannotationdialog.cpp qgsundowidget.cpp + qgsvectorlayerlegendwidget.cpp qgsvectorlayerproperties.cpp qgsmapthemes.cpp qgshandlebadlayers.cpp @@ -361,6 +362,7 @@ SET (QGIS_APP_MOC_HDRS qgssvgannotationdialog.h qgstextannotationdialog.h qgsundowidget.h + qgsvectorlayerlegendwidget.h qgsvectorlayerproperties.h qgsmapthemes.h qgshandlebadlayers.h diff --git a/src/app/qgsvectorlayerlegendwidget.cpp b/src/app/qgsvectorlayerlegendwidget.cpp new file mode 100644 index 00000000000..fb23e7c5510 --- /dev/null +++ b/src/app/qgsvectorlayerlegendwidget.cpp @@ -0,0 +1,194 @@ +/*************************************************************************** + qgsvectorlayerlegendwidget.cpp + --------------------- + Date : April 2018 + Copyright : (C) 2018 by Martin Dobias + Email : wonder dot sk at gmail dot com + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#include "qgsvectorlayerlegendwidget.h" + +#include +#include +#include + +#include "qgsexpressionbuilderdialog.h" +#include "qgsmapcanvas.h" +#include "qgsmaplayerlegend.h" +#include "qgsrenderer.h" +#include "qgssymbollayerutils.h" +#include "qgstextformatwidget.h" +#include "qgsvectorlayer.h" + + +QgsVectorLayerLegendWidget::QgsVectorLayerLegendWidget( QWidget *parent ) + : QWidget( parent ) +{ + mLegendTreeView = new QTreeView; + mLegendTreeView->setRootIsDecorated( false ); + + mTextOnSymbolFormatButton = new QPushButton( tr( "Set Text Format…" ) ); + connect( mTextOnSymbolFormatButton, &QPushButton::clicked, this, &QgsVectorLayerLegendWidget::openTextFormatWidget ); + + mTextOnSymbolFromExpressionButton = new QPushButton( tr( "Set Labels from Expression…" ) ); + connect( mTextOnSymbolFromExpressionButton, &QPushButton::clicked, this, &QgsVectorLayerLegendWidget::labelsFromExpression ); + + mTextOnSymbolGroupBox = new QgsCollapsibleGroupBox; + + QHBoxLayout *buttonsLayout = new QHBoxLayout; + buttonsLayout->addWidget( mTextOnSymbolFormatButton ); + buttonsLayout->addWidget( mTextOnSymbolFromExpressionButton ); + + QVBoxLayout *groupLayout = new QVBoxLayout; + groupLayout->addWidget( mLegendTreeView ); + groupLayout->addLayout( buttonsLayout ); + + mTextOnSymbolGroupBox->setTitle( tr( "Text on Symbols" ) ); + mTextOnSymbolGroupBox->setCheckable( true ); + mTextOnSymbolGroupBox->setLayout( groupLayout ); + mTextOnSymbolGroupBox->setCollapsed( true ); + + QVBoxLayout *layout = new QVBoxLayout; + layout->addWidget( mTextOnSymbolGroupBox ); + setLayout( layout ); +} + + +void QgsVectorLayerLegendWidget::setLayer( QgsVectorLayer *layer ) +{ + mLayer = layer; + + QgsDefaultVectorLayerLegend *legend = qobject_cast( layer->legend() ); + if ( !legend ) + return; + + mTextOnSymbolGroupBox->setChecked( legend->textOnSymbolEnabled() ); + mTextOnSymbolTextFormat = legend->textOnSymbolTextFormat(); + populateLegendTreeView( legend->textOnSymbolContent() ); +} + + +void QgsVectorLayerLegendWidget::populateLegendTreeView( const QHash &content ) +{ + QStandardItemModel *model = new QStandardItemModel; + model->setColumnCount( 2 ); + model->setHorizontalHeaderLabels( QStringList() << tr( "Symbol" ) << tr( "Text" ) ); + + const QgsLegendSymbolList lst = mLayer->renderer()->legendSymbolItems(); + for ( const QgsLegendSymbolItem &symbolItem : lst ) + { + if ( !symbolItem.symbol() ) + continue; + + QgsRenderContext context; + QSize iconSize( 16, 16 ); + QIcon icon = QgsSymbolLayerUtils::symbolPreviewPixmap( symbolItem.symbol(), iconSize, 0, &context ); + + QStandardItem *item1 = new QStandardItem( icon, symbolItem.label() ); + item1->setEditable( false ); + QStandardItem *item2 = new QStandardItem; + if ( symbolItem.ruleKey().isEmpty() ) + { + item1->setEnabled( false ); + item2->setEnabled( true ); + } + else + { + item1->setData( symbolItem.ruleKey() ); + if ( content.contains( symbolItem.ruleKey() ) ) + item2->setText( content.value( symbolItem.ruleKey() ) ); + } + model->appendRow( QList() << item1 << item2 ); + } + mLegendTreeView->setModel( model ); + mLegendTreeView->resizeColumnToContents( 0 ); +} + + +void QgsVectorLayerLegendWidget::applyToLayer() +{ + QgsDefaultVectorLayerLegend *legend = new QgsDefaultVectorLayerLegend( mLayer ); + legend->setTextOnSymbolEnabled( mTextOnSymbolGroupBox->isChecked() ); + legend->setTextOnSymbolTextFormat( mTextOnSymbolTextFormat ); + + QHash content; + if ( QStandardItemModel *model = qobject_cast( mLegendTreeView->model() ) ) + { + for ( int i = 0; i < model->rowCount(); ++i ) + { + QString ruleKey = model->item( i, 0 )->data().toString(); + QString label = model->item( i, 1 )->text(); + if ( !label.isEmpty() ) + content[ruleKey] = label; + } + } + legend->setTextOnSymbolContent( content ); + + mLayer->setLegend( legend ); +} + + +void QgsVectorLayerLegendWidget::openTextFormatWidget() +{ + QgsTextFormatWidget *textOnSymbolFormatWidget = new QgsTextFormatWidget( mTextOnSymbolTextFormat ); + QDialogButtonBox *dialogButtonBox = new QDialogButtonBox( QDialogButtonBox::Ok | QDialogButtonBox::Cancel ); + QVBoxLayout *layout = new QVBoxLayout; + layout->addWidget( textOnSymbolFormatWidget ); + layout->addWidget( dialogButtonBox ); + QDialog dlg; + connect( dialogButtonBox, &QDialogButtonBox::accepted, &dlg, &QDialog::accept ); + connect( dialogButtonBox, &QDialogButtonBox::rejected, &dlg, &QDialog::reject ); + dlg.setLayout( layout ); + if ( !dlg.exec() ) + return; + + mTextOnSymbolTextFormat = textOnSymbolFormatWidget->format(); +} + + +void QgsVectorLayerLegendWidget::labelsFromExpression() +{ + QHash content; + QgsRenderContext context( QgsRenderContext::fromMapSettings( mCanvas->mapSettings() ) ); + + QgsExpressionBuilderDialog dlgExpression( mLayer ); + dlgExpression.setExpressionContext( context.expressionContext() ); + if ( !dlgExpression.exec() ) + return; + + QgsExpression expr( dlgExpression.expressionText() ); + expr.prepare( &context.expressionContext() ); + + std::unique_ptr< QgsFeatureRenderer > r( mLayer->renderer()->clone() ); + + QgsFeature f; + QgsFeatureRequest request; + request.setSubsetOfAttributes( r->usedAttributes( context ), mLayer->fields() ); + QgsFeatureIterator fi = mLayer->getFeatures(); + + r->startRender( context, mLayer->fields() ); + while ( fi.nextFeature( f ) ) + { + context.expressionContext().setFeature( f ); + const QSet keys = r->legendKeysForFeature( f, context ); + for ( const QString &key : keys ) + { + if ( content.contains( key ) ) + continue; + + QString label = expr.evaluate( &context.expressionContext() ).toString(); + if ( !label.isEmpty() ) + content[key] = label; + } + } + r->stopRender( context ); + + populateLegendTreeView( content ); +} diff --git a/src/app/qgsvectorlayerlegendwidget.h b/src/app/qgsvectorlayerlegendwidget.h new file mode 100644 index 00000000000..c6ea59e46d7 --- /dev/null +++ b/src/app/qgsvectorlayerlegendwidget.h @@ -0,0 +1,71 @@ +/*************************************************************************** + qgsvectorlayerlegendwidget.h + --------------------- + Date : April 2018 + Copyright : (C) 2018 by Martin Dobias + Email : wonder dot sk at gmail dot com + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#ifndef QGSVECTORLAYERLEGENDWIDGET_H +#define QGSVECTORLAYERLEGENDWIDGET_H + +#include + +#include "qgstextrenderer.h" + +class QLabel; +class QPushButton; +class QTreeView; + +class QgsCollapsibleGroupBox; +class QgsMapCanvas; +class QgsVectorLayer; + +/** + * A widget for configuration of options specific to vector layer's legend. + */ +class QgsVectorLayerLegendWidget : public QWidget +{ + Q_OBJECT + public: + explicit QgsVectorLayerLegendWidget( QWidget *parent = nullptr ); + + //! Sets pointer to map canvas + void setMapCanvas( QgsMapCanvas *canvas ) { mCanvas = canvas; } + + //! Returns pointer to map canvas + QgsMapCanvas *mapCanvas() const { return mCanvas; } + + //! Initialize widget with a map layer + void setLayer( QgsVectorLayer *layer ); + + //! Store changes made in the widget to the layer + void applyToLayer(); + + private slots: + void openTextFormatWidget(); + void labelsFromExpression(); + + private: + void populateLegendTreeView( const QHash &content ); + + private: + QTreeView *mLegendTreeView = nullptr; + QPushButton *mTextOnSymbolFormatButton = nullptr; + QPushButton *mTextOnSymbolFromExpressionButton = nullptr; + QgsCollapsibleGroupBox *mTextOnSymbolGroupBox = nullptr; + QLabel *mTextOnSymbolLabel = nullptr; + + QgsMapCanvas *mCanvas = nullptr; + QgsVectorLayer *mLayer = nullptr; + QgsTextFormat mTextOnSymbolTextFormat; +}; + +#endif // QGSVECTORLAYERLEGENDWIDGET_H diff --git a/src/app/qgsvectorlayerproperties.cpp b/src/app/qgsvectorlayerproperties.cpp index 2b4c8864a7a..ca91fc7ea19 100644 --- a/src/app/qgsvectorlayerproperties.cpp +++ b/src/app/qgsvectorlayerproperties.cpp @@ -302,6 +302,8 @@ QgsVectorLayerProperties::QgsVectorLayerProperties( mDiagramFrame->setLayout( diagLayout ); // Legend tab + mLegendWidget->setMapCanvas( QgisApp::instance()->mapCanvas() ); + mLegendWidget->setLayer( mLayer ); mLegendConfigEmbeddedWidget->setLayer( mLayer ); // WMS Name as layer short name @@ -572,6 +574,7 @@ void QgsVectorLayerProperties::apply() } // apply legend settings + mLegendWidget->applyToLayer(); mLegendConfigEmbeddedWidget->applyToLayer(); // save metadata diff --git a/src/core/layertree/qgslayertreemodellegendnode.cpp b/src/core/layertree/qgslayertreemodellegendnode.cpp index bea2c227697..bb63138b4bd 100644 --- a/src/core/layertree/qgslayertreemodellegendnode.cpp +++ b/src/core/layertree/qgslayertreemodellegendnode.cpp @@ -178,6 +178,15 @@ QSize QgsSymbolLegendNode::minimumIconSize( QgsRenderContext *context ) const true ).size(); } + if ( !mTextOnSymbolLabel.isEmpty() && context ) + { + double w = QgsTextRenderer::textWidth( *context, mTextOnSymbolTextFormat, QStringList() << mTextOnSymbolLabel ); + double h = QgsTextRenderer::textHeight( *context, mTextOnSymbolTextFormat, QStringList() << mTextOnSymbolLabel, QgsTextRenderer::Point ); + int wInt = ceil( w ), hInt = ceil( h ); + if ( wInt > minSz.width() ) minSz.setWidth( wInt ); + if ( hInt > minSz.height() ) minSz.setHeight( hInt ); + } + if ( mItem.level() != 0 && !( model() && model()->testFlag( QgsLayerTreeModel::ShowLegendAsTree ) ) ) minSz.setWidth( mItem.level() * INDENT_SIZE + minSz.width() ); @@ -271,6 +280,17 @@ QVariant QgsSymbolLegendNode::data( int role ) const { std::unique_ptr context( createTemporaryRenderContext() ); pix = QgsSymbolLayerUtils::symbolPreviewPixmap( mItem.symbol(), mIconSize, 0, context.get() ); + + if ( !mTextOnSymbolLabel.isEmpty() && context ) + { + QPainter painter( &pix ); + painter.setRenderHint( QPainter::Antialiasing ); + context->setPainter( &painter ); + QFontMetricsF fm( mTextOnSymbolTextFormat.scaledFont( *context.get() ) ); + qreal yBaselineVCenter = ( mIconSize.height() + fm.ascent() - fm.descent() ) / 2; + QgsTextRenderer::drawText( QPointF( mIconSize.width() / 2, yBaselineVCenter ), 0, QgsTextRenderer::AlignCenter, + QStringList() << mTextOnSymbolLabel, *context.get(), mTextOnSymbolTextFormat ); + } } else { @@ -418,6 +438,15 @@ QSizeF QgsSymbolLegendNode::drawSymbol( const QgsLegendSettings &settings, ItemC { s->drawPreviewIcon( p, QSize( width * dotsPerMM, height * dotsPerMM ), &context ); } + + if ( !mTextOnSymbolLabel.isEmpty() ) + { + QFontMetricsF fm( mTextOnSymbolTextFormat.scaledFont( context ) ); + qreal yBaselineVCenter = ( height * dotsPerMM + fm.ascent() - fm.descent() ) / 2; + QgsTextRenderer::drawText( QPointF( width * dotsPerMM / 2, yBaselineVCenter ), 0, QgsTextRenderer::AlignCenter, + QStringList() << mTextOnSymbolLabel, context, mTextOnSymbolTextFormat ); + } + p->restore(); } diff --git a/src/core/layertree/qgslayertreemodellegendnode.h b/src/core/layertree/qgslayertreemodellegendnode.h index cebe7762e16..3dc2b86f5b2 100644 --- a/src/core/layertree/qgslayertreemodellegendnode.h +++ b/src/core/layertree/qgslayertreemodellegendnode.h @@ -144,6 +144,7 @@ class CORE_EXPORT QgsLayerTreeModelLegendNode : public QObject }; #include "qgslegendsymbolitem.h" +#include "qgstextrenderer.h" /** * \ingroup core @@ -221,6 +222,30 @@ class CORE_EXPORT QgsSymbolLegendNode : public QgsLayerTreeModelLegendNode */ void setSymbol( QgsSymbol *symbol ); + /** + * Returns label of text to be shown on top of the symbol. + * \since QGIS 3.2 + */ + QString textOnSymbolLabel() const { return mTextOnSymbolLabel; } + + /** + * Sets label of text to be shown on top of the symbol. + * \since QGIS 3.2 + */ + void setTextOnSymbolLabel( const QString &label ) { mTextOnSymbolLabel = label; } + + /** + * Returns text format of the label to be shown on top of the symbol. + * \since QGIS 3.2 + */ + QgsTextFormat textOnSymbolTextFormat() const { return mTextOnSymbolTextFormat; } + + /** + * Sets format of text to be shown on top of the symbol. + * \since QGIS 3.2 + */ + void setTextOnSymbolTextFormat( const QgsTextFormat &format ) { mTextOnSymbolTextFormat = format; } + public slots: /** @@ -247,6 +272,9 @@ class CORE_EXPORT QgsSymbolLegendNode : public QgsLayerTreeModelLegendNode bool mSymbolUsesMapUnits; QSize mIconSize; + QString mTextOnSymbolLabel; + QgsTextFormat mTextOnSymbolTextFormat; + // ident the symbol icon to make it look like a tree structure static const int INDENT_SIZE = 20; diff --git a/src/core/qgsmaplayerlegend.cpp b/src/core/qgsmaplayerlegend.cpp index 6a940475fdd..b09af0c2372 100644 --- a/src/core/qgsmaplayerlegend.cpp +++ b/src/core/qgsmaplayerlegend.cpp @@ -30,6 +30,19 @@ QgsMapLayerLegend::QgsMapLayerLegend( QObject *parent ) { } +void QgsMapLayerLegend::readXml( const QDomElement &elem, const QgsReadWriteContext &context ) +{ + Q_UNUSED( elem ); + Q_UNUSED( context ); +} + +QDomElement QgsMapLayerLegend::writeXml( QDomDocument &doc, const QgsReadWriteContext &context ) const +{ + Q_UNUSED( doc ); + Q_UNUSED( context ); + return QDomElement(); +} + QgsMapLayerLegend *QgsMapLayerLegend::defaultVectorLegend( QgsVectorLayer *vl ) { return new QgsDefaultVectorLayerLegend( vl ); @@ -199,7 +212,15 @@ QList QgsDefaultVectorLayerLegend::createLayerTre if ( i.dataDefinedSizeLegendSettings() ) nodes << new QgsDataDefinedSizeLegendNode( nodeLayer, *i.dataDefinedSizeLegendSettings() ); else - nodes << new QgsSymbolLegendNode( nodeLayer, i ); + { + QgsSymbolLegendNode *legendNode = new QgsSymbolLegendNode( nodeLayer, i ); + if ( mTextOnSymbolEnabled && mTextOnSymbolContent.contains( i.ruleKey() ) ) + { + legendNode->setTextOnSymbolLabel( mTextOnSymbolContent.value( i.ruleKey() ) ); + legendNode->setTextOnSymbolTextFormat( mTextOnSymbolTextFormat ); + } + nodes << legendNode; + } } if ( nodes.count() == 1 && nodes[0]->data( Qt::EditRole ).toString().isEmpty() ) @@ -218,6 +239,52 @@ QList QgsDefaultVectorLayerLegend::createLayerTre return nodes; } +void QgsDefaultVectorLayerLegend::readXml( const QDomElement &elem, const QgsReadWriteContext &context ) +{ + mTextOnSymbolEnabled = false; + mTextOnSymbolTextFormat = QgsTextFormat(); + mTextOnSymbolContent.clear(); + + QDomElement tosElem = elem.firstChildElement( QStringLiteral( "text-on-symbol" ) ); + if ( !tosElem.isNull() ) + { + mTextOnSymbolEnabled = true; + QDomElement tosFormatElem = tosElem.firstChildElement( QStringLiteral( "text-style" ) ); + mTextOnSymbolTextFormat.readXml( tosFormatElem, context ); + QDomElement tosContentElem = tosElem.firstChildElement( QStringLiteral( "content" ) ); + QDomElement tosContentItemElem = tosContentElem.firstChildElement( QStringLiteral( "item" ) ); + while ( !tosContentItemElem.isNull() ) + { + mTextOnSymbolContent.insert( tosContentItemElem.attribute( QStringLiteral( "key" ) ), tosContentItemElem.attribute( QStringLiteral( "value" ) ) ); + tosContentItemElem = tosContentItemElem.nextSiblingElement( QStringLiteral( "item" ) ); + } + } +} + +QDomElement QgsDefaultVectorLayerLegend::writeXml( QDomDocument &doc, const QgsReadWriteContext &context ) const +{ + QDomElement elem = doc.createElement( QStringLiteral( "legend" ) ); + elem.setAttribute( QStringLiteral( "type" ), QStringLiteral( "default-vector" ) ); + + if ( mTextOnSymbolEnabled ) + { + QDomElement tosElem = doc.createElement( QStringLiteral( "text-on-symbol" ) ); + QDomElement tosFormatElem = mTextOnSymbolTextFormat.writeXml( doc, context ); + tosElem.appendChild( tosFormatElem ); + QDomElement tosContentElem = doc.createElement( QStringLiteral( "content" ) ); + for ( auto it = mTextOnSymbolContent.constBegin(); it != mTextOnSymbolContent.constEnd(); ++it ) + { + QDomElement tosContentItemElem = doc.createElement( QStringLiteral( "item" ) ); + tosContentItemElem.setAttribute( QStringLiteral( "key" ), it.key() ); + tosContentItemElem.setAttribute( QStringLiteral( "value" ), it.value() ); + tosContentElem.appendChild( tosContentItemElem ); + } + tosElem.appendChild( tosContentElem ); + elem.appendChild( tosElem ); + } + + return elem; +} // ------------------------------------------------------------------------- diff --git a/src/core/qgsmaplayerlegend.h b/src/core/qgsmaplayerlegend.h index 9ef2bb9f357..f07c843c1c8 100644 --- a/src/core/qgsmaplayerlegend.h +++ b/src/core/qgsmaplayerlegend.h @@ -19,10 +19,14 @@ #include #include "qgis.h" +class QDomDocument; +class QDomElement; + class QgsLayerTreeLayer; class QgsLayerTreeModelLegendNode; class QgsPluginLayer; class QgsRasterLayer; +class QgsReadWriteContext; class QgsVectorLayer; #include "qgis_core.h" @@ -43,7 +47,19 @@ class CORE_EXPORT QgsMapLayerLegend : public QObject //! Constructor for QgsMapLayerLegend explicit QgsMapLayerLegend( QObject *parent SIP_TRANSFERTHIS = nullptr ); - // TODO: type, load/save settings + // TODO: type + + /** + * Reads configuration from a DOM element previously written by writeXml() + * \since QGIS 3.2 + */ + virtual void readXml( const QDomElement &elem, const QgsReadWriteContext &context ); + + /** + * Writes configuration to a DOM element, to be used later with readXml() + * \since QGIS 3.2 + */ + virtual QDomElement writeXml( QDomDocument &doc, const QgsReadWriteContext &context ) const; /** * Return list of legend nodes to be used for a particular layer tree layer node. @@ -89,6 +105,8 @@ class CORE_EXPORT QgsMapLayerLegendUtils #include +#include "qgstextrenderer.h" + /** * \ingroup core * Default legend implementation for vector layers @@ -101,10 +119,59 @@ class CORE_EXPORT QgsDefaultVectorLayerLegend : public QgsMapLayerLegend public: explicit QgsDefaultVectorLayerLegend( QgsVectorLayer *vl ); + /** + * Returns whether the "text on symbol" functionality is enabled. When enabled, legend symbols + * may have extra text rendered on top. The content of labels and their style is controlled + * by textOnSymbolContent() and textOnSymbolTextFormat(). + * \since QGIS 3.2 + */ + bool textOnSymbolEnabled() const { return mTextOnSymbolEnabled; } + + /** + * Sets whether the "text on symbol" functionality is enabled. When enabled, legend symbols + * may have extra text rendered on top. The content of labels and their style is controlled + * by textOnSymbolContent() and textOnSymbolTextFormat(). + * \since QGIS 3.2 + */ + void setTextOnSymbolEnabled( bool enabled ) { mTextOnSymbolEnabled = enabled; } + + /** + * Returns text format of symbol labels for "text on symbol" functionality. + * \since QGIS 3.2 + */ + QgsTextFormat textOnSymbolTextFormat() const { return mTextOnSymbolTextFormat; } + + /** + * Sets text format of symbol labels for "text on symbol" functionality. + * \since QGIS 3.2 + */ + void setTextOnSymbolTextFormat( const QgsTextFormat &format ) { mTextOnSymbolTextFormat = format; } + + /** + * Returns per-symbol content of labels for "text on symbol" functionality. In the passed dictionary + * the keys are rule keys of legend items, the values are labels to be shown. + * \since QGIS 3.2 + */ + QHash textOnSymbolContent() const { return mTextOnSymbolContent; } + + /** + * Sets per-symbol content of labels for "text on symbol" functionality. In the passed dictionary + * the keys are rule keys of legend items, the values are labels to be shown. + * \since QGIS 3.2 + */ + void setTextOnSymbolContent( const QHash &content ) { mTextOnSymbolContent = content; } + QList createLayerTreeModelLegendNodes( QgsLayerTreeLayer *nodeLayer ) SIP_FACTORY override; + virtual void readXml( const QDomElement &elem, const QgsReadWriteContext &context ) override; + virtual QDomElement writeXml( QDomDocument &doc, const QgsReadWriteContext &context ) const override; private: QgsVectorLayer *mLayer = nullptr; + + // text on symbol + bool mTextOnSymbolEnabled = false; + QgsTextFormat mTextOnSymbolTextFormat; + QHash mTextOnSymbolContent; }; diff --git a/src/core/qgsvectorlayer.cpp b/src/core/qgsvectorlayer.cpp index c8c3dd778a8..64690774743 100644 --- a/src/core/qgsvectorlayer.cpp +++ b/src/core/qgsvectorlayer.cpp @@ -1423,7 +1423,11 @@ bool QgsVectorLayer::readXml( const QDomNode &layer_node, QgsReadWriteContext &c } setDependencies( sources ); - setLegend( QgsMapLayerLegend::defaultVectorLegend( this ) ); + QgsMapLayerLegend *legend = QgsMapLayerLegend::defaultVectorLegend( this ); + QDomElement legendElem = layer_node.firstChildElement( QStringLiteral( "legend" ) ); + if ( !legendElem.isNull() ) + legend->readXml( legendElem, context ); + setLegend( legend ); // read extent if ( mReadExtentFromXml ) @@ -1688,6 +1692,14 @@ bool QgsVectorLayer::writeXml( QDomNode &layer_node, } layer_node.appendChild( dataDependenciesElement ); + // legend + if ( legend() ) + { + QDomElement legendElement = legend()->writeXml( document, context ); + if ( !legendElement.isNull() ) + layer_node.appendChild( legendElement ); + } + // save expression fields mExpressionFieldBuffer->writeXml( layer_node, document ); diff --git a/src/gui/qgstextformatwidget.cpp b/src/gui/qgstextformatwidget.cpp index 9005a400817..9ebdfecb604 100644 --- a/src/gui/qgstextformatwidget.cpp +++ b/src/gui/qgstextformatwidget.cpp @@ -828,6 +828,11 @@ QgsTextFormat QgsTextFormatWidget::format() const return format; } +void QgsTextFormatWidget::setFormat( const QgsTextFormat &format ) +{ + updateWidgetForFormat( format ); +} + void QgsTextFormatWidget::optionsStackedWidget_CurrentChanged( int indx ) { mLabelingOptionsListWidget->blockSignals( true ); diff --git a/src/gui/qgstextformatwidget.h b/src/gui/qgstextformatwidget.h index ce050e39f21..c481715b462 100644 --- a/src/gui/qgstextformatwidget.h +++ b/src/gui/qgstextformatwidget.h @@ -68,6 +68,12 @@ class GUI_EXPORT QgsTextFormatWidget : public QWidget, protected Ui::QgsTextForm */ QgsTextFormat format() const; + /** + * Sets the current formatting settings + * \since QGIS 3.2 + */ + void setFormat( const QgsTextFormat &format ); + public slots: /** diff --git a/src/ui/qgsvectorlayerpropertiesbase.ui b/src/ui/qgsvectorlayerpropertiesbase.ui index e162d68c7d6..2802bc5ac8c 100644 --- a/src/ui/qgsvectorlayerpropertiesbase.ui +++ b/src/ui/qgsvectorlayerpropertiesbase.ui @@ -1878,6 +1878,9 @@ border-radius: 2px; 0 + + + Embedded widgets in legend @@ -2416,6 +2419,12 @@ border-radius: 2px; QWidget
qgscodeeditorhtml.h
+ + QgsVectorLayerLegendWidget + QWidget +
qgsvectorlayerlegendwidget.h
+ 1 +
mSearchLineEdit diff --git a/tests/src/core/testqgslegendrenderer.cpp b/tests/src/core/testqgslegendrenderer.cpp index 68e55c742b7..8548345ad0f 100644 --- a/tests/src/core/testqgslegendrenderer.cpp +++ b/tests/src/core/testqgslegendrenderer.cpp @@ -122,6 +122,7 @@ class TestQgsLegendRenderer : public QObject void testDiagramAttributeLegend(); void testDiagramSizeLegend(); void testDataDefinedSizeCollapsed(); + void testTextOnSymbol(); private: QgsLayerTree *mRoot = nullptr; @@ -783,6 +784,50 @@ void TestQgsLegendRenderer::testDataDefinedSizeCollapsed() delete root; } +void TestQgsLegendRenderer::testTextOnSymbol() +{ + QString testName = QStringLiteral( "legend_text_on_symbol" ); + + QgsVectorLayer *vl = new QgsVectorLayer( QStringLiteral( "Polygon" ), QStringLiteral( "Polygon Layer" ), QStringLiteral( "memory" ) ); + + QgsCategoryList cats; + QgsFillSymbol *sym_1 = new QgsFillSymbol(); + sym_1->setColor( Qt::red ); + cats << QgsRendererCategory( 1, sym_1, QStringLiteral( "Red" ) ); + QgsFillSymbol *sym_2 = new QgsFillSymbol(); + sym_2->setColor( Qt::green ); + cats << QgsRendererCategory( 2, sym_2, QStringLiteral( "Green" ) ); + QgsFillSymbol *sym_3 = new QgsFillSymbol(); + sym_3->setColor( Qt::blue ); + cats << QgsRendererCategory( 3, sym_3, QStringLiteral( "Blue" ) ); + QgsCategorizedSymbolRenderer *r = new QgsCategorizedSymbolRenderer( QStringLiteral( "test_attr" ), cats ); + vl->setRenderer( r ); + + QgsDefaultVectorLayerLegend *legend = new QgsDefaultVectorLayerLegend( vl ); + legend->setTextOnSymbolEnabled( true ); + QHash content; + content["0"] = "Rd"; + content["2"] = "Bl"; + legend->setTextOnSymbolContent( content ); + QgsTextFormat textFormat; + textFormat.setFont( QgsFontUtils::getStandardTestFont( QStringLiteral( "Roman" ) ) ); + textFormat.setSize( 9 ); + legend->setTextOnSymbolTextFormat( textFormat ); + vl->setLegend( legend ); + + QgsLayerTree *root = new QgsLayerTree(); + root->addLayer( vl ); + + QgsLayerTreeModel legendModel( root ); + + QgsLegendSettings settings; + _setStandardTestFont( settings ); + _renderLegend( testName, &legendModel, settings ); + QVERIFY( _verifyImage( testName, mReport ) ); + + delete root; +} + QGSTEST_MAIN( TestQgsLegendRenderer ) #include "testqgslegendrenderer.moc" diff --git a/tests/testdata/control_images/legend/expected_legend_text_on_symbol/expected_legend_text_on_symbol.png b/tests/testdata/control_images/legend/expected_legend_text_on_symbol/expected_legend_text_on_symbol.png new file mode 100644 index 00000000000..5fc7feef4ee Binary files /dev/null and b/tests/testdata/control_images/legend/expected_legend_text_on_symbol/expected_legend_text_on_symbol.png differ