From 49b02bf5628dceeebbaaf16dc665db88911a436f Mon Sep 17 00:00:00 2001 From: Martin Dobias Date: Wed, 18 Apr 2018 21:52:56 +0200 Subject: [PATCH 1/3] [FEATURE] Legend: optional text on top of symbols for vector layers In some cases it is useful to add extra information to the symbols in the legend. This work allows definition of additional labels in vector layer properties > Legend tab. --- .../qgslayertreemodellegendnode.sip.in | 28 ++++ python/core/qgsmaplayerlegend.sip.in | 66 +++++++++ python/gui/qgstextformatwidget.sip.in | 7 + src/app/CMakeLists.txt | 2 + src/app/qgsvectorlayerlegendwidget.cpp | 140 ++++++++++++++++++ src/app/qgsvectorlayerlegendwidget.h | 58 ++++++++ src/app/qgsvectorlayerproperties.cpp | 2 + .../layertree/qgslayertreemodellegendnode.cpp | 29 ++++ .../layertree/qgslayertreemodellegendnode.h | 28 ++++ src/core/qgsmaplayerlegend.cpp | 69 ++++++++- src/core/qgsmaplayerlegend.h | 69 ++++++++- src/core/qgsvectorlayer.cpp | 14 +- src/gui/qgstextformatwidget.cpp | 5 + src/gui/qgstextformatwidget.h | 6 + src/ui/qgsvectorlayerpropertiesbase.ui | 9 ++ tests/src/core/testqgslegendrenderer.cpp | 41 +++++ .../expected_legend_text_on_symbol.png | Bin 0 -> 6907 bytes 17 files changed, 570 insertions(+), 3 deletions(-) create mode 100644 src/app/qgsvectorlayerlegendwidget.cpp create mode 100644 src/app/qgsvectorlayerlegendwidget.h create mode 100644 tests/testdata/control_images/legend/expected_legend_text_on_symbol/expected_legend_text_on_symbol.png 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..f223573de7e --- /dev/null +++ b/src/app/qgsvectorlayerlegendwidget.cpp @@ -0,0 +1,140 @@ +/*************************************************************************** + 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 "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 ); + + mTextOnSymbolGroupBox = new QgsCollapsibleGroupBox; + + QVBoxLayout *groupLayout = new QVBoxLayout; + groupLayout->addWidget( mLegendTreeView ); + groupLayout->addWidget( mTextOnSymbolFormatButton ); + + 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(); + QHash content = legend->textOnSymbolContent(); + + QStandardItemModel *model = new QStandardItemModel; + model->setColumnCount( 2 ); + model->setHorizontalHeaderLabels( QStringList() << tr( "Symbol" ) << tr( "Text" ) ); + + const QgsLegendSymbolList lst = layer->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(); +} diff --git a/src/app/qgsvectorlayerlegendwidget.h b/src/app/qgsvectorlayerlegendwidget.h new file mode 100644 index 00000000000..13f4076a1c4 --- /dev/null +++ b/src/app/qgsvectorlayerlegendwidget.h @@ -0,0 +1,58 @@ +/*************************************************************************** + 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 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 ); + + //! 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(); + + private: + QTreeView *mLegendTreeView = nullptr; + QPushButton *mTextOnSymbolFormatButton = nullptr; + QgsCollapsibleGroupBox *mTextOnSymbolGroupBox = nullptr; + QLabel *mTextOnSymbolLabel = nullptr; + + QgsVectorLayer *mLayer = nullptr; + QgsTextFormat mTextOnSymbolTextFormat; +}; + +#endif // QGSVECTORLAYERLEGENDWIDGET_H diff --git a/src/app/qgsvectorlayerproperties.cpp b/src/app/qgsvectorlayerproperties.cpp index 2b4c8864a7a..ed8654ec579 100644 --- a/src/app/qgsvectorlayerproperties.cpp +++ b/src/app/qgsvectorlayerproperties.cpp @@ -302,6 +302,7 @@ QgsVectorLayerProperties::QgsVectorLayerProperties( mDiagramFrame->setLayout( diagLayout ); // Legend tab + mLegendWidget->setLayer( mLayer ); mLegendConfigEmbeddedWidget->setLayer( mLayer ); // WMS Name as layer short name @@ -572,6 +573,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..3797a9a7a80 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,46 @@ 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 ); + 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 0000000000000000000000000000000000000000..03793cf7e8ea0af97d60e2ee900f759a3702a9d9 GIT binary patch literal 6907 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 literal 0 HcmV?d00001 From 00d8dbfb47523700db8b81145ac096e769604721 Mon Sep 17 00:00:00 2001 From: Martin Dobias Date: Thu, 19 Apr 2018 14:16:42 +0200 Subject: [PATCH 2/3] Make it possible to load "text on symbol" labels from expression --- src/app/qgsvectorlayerlegendwidget.cpp | 62 ++++++++++++++++++++++++-- src/app/qgsvectorlayerlegendwidget.h | 13 ++++++ src/app/qgsvectorlayerproperties.cpp | 1 + 3 files changed, 72 insertions(+), 4 deletions(-) diff --git a/src/app/qgsvectorlayerlegendwidget.cpp b/src/app/qgsvectorlayerlegendwidget.cpp index f223573de7e..fb23e7c5510 100644 --- a/src/app/qgsvectorlayerlegendwidget.cpp +++ b/src/app/qgsvectorlayerlegendwidget.cpp @@ -19,6 +19,8 @@ #include #include +#include "qgsexpressionbuilderdialog.h" +#include "qgsmapcanvas.h" #include "qgsmaplayerlegend.h" #include "qgsrenderer.h" #include "qgssymbollayerutils.h" @@ -32,14 +34,21 @@ QgsVectorLayerLegendWidget::QgsVectorLayerLegendWidget( QWidget *parent ) mLegendTreeView = new QTreeView; mLegendTreeView->setRootIsDecorated( false ); - mTextOnSymbolFormatButton = new QPushButton( tr( "Set Text Format..." ) ); + 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->addWidget( mTextOnSymbolFormatButton ); + groupLayout->addLayout( buttonsLayout ); mTextOnSymbolGroupBox->setTitle( tr( "Text on Symbols" ) ); mTextOnSymbolGroupBox->setCheckable( true ); @@ -62,13 +71,17 @@ void QgsVectorLayerLegendWidget::setLayer( QgsVectorLayer *layer ) mTextOnSymbolGroupBox->setChecked( legend->textOnSymbolEnabled() ); mTextOnSymbolTextFormat = legend->textOnSymbolTextFormat(); - QHash content = legend->textOnSymbolContent(); + 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 = layer->renderer()->legendSymbolItems(); + const QgsLegendSymbolList lst = mLayer->renderer()->legendSymbolItems(); for ( const QgsLegendSymbolItem &symbolItem : lst ) { if ( !symbolItem.symbol() ) @@ -138,3 +151,44 @@ void QgsVectorLayerLegendWidget::openTextFormatWidget() 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 index 13f4076a1c4..c6ea59e46d7 100644 --- a/src/app/qgsvectorlayerlegendwidget.h +++ b/src/app/qgsvectorlayerlegendwidget.h @@ -25,6 +25,7 @@ class QPushButton; class QTreeView; class QgsCollapsibleGroupBox; +class QgsMapCanvas; class QgsVectorLayer; /** @@ -36,6 +37,12 @@ class QgsVectorLayerLegendWidget : public QWidget 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 ); @@ -44,13 +51,19 @@ class QgsVectorLayerLegendWidget : public QWidget 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; }; diff --git a/src/app/qgsvectorlayerproperties.cpp b/src/app/qgsvectorlayerproperties.cpp index ed8654ec579..ca91fc7ea19 100644 --- a/src/app/qgsvectorlayerproperties.cpp +++ b/src/app/qgsvectorlayerproperties.cpp @@ -302,6 +302,7 @@ QgsVectorLayerProperties::QgsVectorLayerProperties( mDiagramFrame->setLayout( diagLayout ); // Legend tab + mLegendWidget->setMapCanvas( QgisApp::instance()->mapCanvas() ); mLegendWidget->setLayer( mLayer ); mLegendConfigEmbeddedWidget->setLayer( mLayer ); From f1a31d09f1a2c4a19a18799ffa358709e6ad60bf Mon Sep 17 00:00:00 2001 From: Martin Dobias Date: Thu, 19 Apr 2018 14:23:49 +0200 Subject: [PATCH 3/3] Use standard font for text on symbol --- tests/src/core/testqgslegendrenderer.cpp | 4 ++++ .../expected_legend_text_on_symbol.png | Bin 6907 -> 6892 bytes 2 files changed, 4 insertions(+) diff --git a/tests/src/core/testqgslegendrenderer.cpp b/tests/src/core/testqgslegendrenderer.cpp index 3797a9a7a80..8548345ad0f 100644 --- a/tests/src/core/testqgslegendrenderer.cpp +++ b/tests/src/core/testqgslegendrenderer.cpp @@ -809,6 +809,10 @@ void TestQgsLegendRenderer::testTextOnSymbol() 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(); 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 index 03793cf7e8ea0af97d60e2ee900f759a3702a9d9..5fc7feef4ee1f5a8a674c6a9909941cc4ac725df 100644 GIT binary patch literal 6892 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 XW6jUQj9KvY6{H}eB3=IU<=g)N{7XBM literal 6907 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