diff --git a/images/images.qrc b/images/images.qrc index 997e1fc88f2..43595ac9aea 100644 --- a/images/images.qrc +++ b/images/images.qrc @@ -765,6 +765,8 @@ themes/default/mIconAlignJustify.svg themes/default/mIconAlignLeft.svg themes/default/mIconAlignRight.svg + themes/default/mIconArrangeSymbolsLeft.svg + themes/default/mIconArrangeSymbolsRight.svg qgis_tips/symbol_levels.png diff --git a/images/themes/default/mIconArrangeSymbolsLeft.svg b/images/themes/default/mIconArrangeSymbolsLeft.svg new file mode 100644 index 00000000000..2d5799c46f7 --- /dev/null +++ b/images/themes/default/mIconArrangeSymbolsLeft.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/themes/default/mIconArrangeSymbolsRight.svg b/images/themes/default/mIconArrangeSymbolsRight.svg new file mode 100644 index 00000000000..400c3d1041f --- /dev/null +++ b/images/themes/default/mIconArrangeSymbolsRight.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/python/core/auto_generated/layertree/qgslayertreemodellegendnode.sip.in b/python/core/auto_generated/layertree/qgslayertreemodellegendnode.sip.in index dabaa89f18a..4f59c513d94 100644 --- a/python/core/auto_generated/layertree/qgslayertreemodellegendnode.sip.in +++ b/python/core/auto_generated/layertree/qgslayertreemodellegendnode.sip.in @@ -76,10 +76,23 @@ Default implementation does nothing. * struct ItemContext { + ItemContext(); + QgsRenderContext *context; QPainter *painter; - QPointF point; - double labelXOffset; + + QPointF point; + + double labelXOffset; + + double top; + + double columnLeft; + + double columnRight; + + double maxSiblingSymbolWidth; + }; struct ItemMetrics diff --git a/python/core/auto_generated/layout/qgslayoutitemlegend.sip.in b/python/core/auto_generated/layout/qgslayoutitemlegend.sip.in index 89abc9e25dc..30d7ed9686b 100644 --- a/python/core/auto_generated/layout/qgslayoutitemlegend.sip.in +++ b/python/core/auto_generated/layout/qgslayoutitemlegend.sip.in @@ -279,6 +279,28 @@ Returns the legend symbol width. Sets the legend symbol ``width``. .. seealso:: :py:func:`symbolWidth` +%End + + void setSymbolAlignment( Qt::AlignmentFlag alignment ); +%Docstring +Sets the ``alignment`` for placement of legend symbols. + +Only Qt.AlignLeft or Qt.AlignRight are supported values. + +.. seealso:: :py:func:`symbolAlignment` + +.. versionadded:: 3.10.0 +%End + + Qt::AlignmentFlag symbolAlignment() const; +%Docstring +Returns the alignment for placement of legend symbols. + +Only Qt.AlignLeft or Qt.AlignRight are supported values. + +.. seealso:: :py:func:`setSymbolAlignment` + +.. versionadded:: 3.10.0 %End double symbolHeight() const; diff --git a/python/core/auto_generated/qgslegendsettings.sip.in b/python/core/auto_generated/qgslegendsettings.sip.in index 7247813d597..3997c0efc61 100644 --- a/python/core/auto_generated/qgslegendsettings.sip.in +++ b/python/core/auto_generated/qgslegendsettings.sip.in @@ -32,18 +32,14 @@ in QgsLegendModel class. Qt::AlignmentFlag titleAlignment() const; %Docstring -Returns the alignment of the legend title - -:return: Qt.AlignmentFlag for the legend title +Returns the alignment of the legend title. .. seealso:: :py:func:`setTitleAlignment` %End void setTitleAlignment( Qt::AlignmentFlag alignment ); %Docstring -Sets the alignment of the legend title - -:param alignment: Text alignment for drawing the legend title +Sets the ``alignment`` of the legend title. .. seealso:: :py:func:`titleAlignment` %End @@ -102,6 +98,28 @@ Overrides fontColor() QSizeF symbolSize() const; void setSymbolSize( QSizeF s ); + void setSymbolAlignment( Qt::AlignmentFlag alignment ); +%Docstring +Sets the ``alignment`` for placement of legend symbols. + +Only Qt.AlignLeft or Qt.AlignRight are supported values. + +.. seealso:: :py:func:`symbolAlignment` + +.. versionadded:: 3.10.0 +%End + + Qt::AlignmentFlag symbolAlignment() const; +%Docstring +Returns the alignment for placement of legend symbols. + +Only Qt.AlignLeft or Qt.AlignRight are supported values. + +.. seealso:: :py:func:`setSymbolAlignment` + +.. versionadded:: 3.10.0 +%End + bool drawRasterStroke() const; %Docstring Returns whether a stroke will be drawn around raster symbol items. diff --git a/python/core/auto_generated/qgslegendstyle.sip.in b/python/core/auto_generated/qgslegendstyle.sip.in index 0375f260f59..3b950262b3c 100644 --- a/python/core/auto_generated/qgslegendstyle.sip.in +++ b/python/core/auto_generated/qgslegendstyle.sip.in @@ -58,6 +58,24 @@ The font for this style. void setMargin( double margin ); %Docstring Sets all margins +%End + + Qt::Alignment alignment() const; +%Docstring +Returns the alignment for the legend component. + +.. seealso:: :py:func:`setAlignment` + +.. versionadded:: 3.10 +%End + + void setAlignment( Qt::Alignment alignment ); +%Docstring +Sets the alignment for the legend component. + +.. seealso:: :py:func:`alignment` + +.. versionadded:: 3.10 %End void writeXml( const QString &name, QDomElement &elem, QDomDocument &doc ) const; diff --git a/python/gui/auto_generated/qgsalignmentcombobox.sip.in b/python/gui/auto_generated/qgsalignmentcombobox.sip.in index 1006b083c55..429a5685cca 100644 --- a/python/gui/auto_generated/qgsalignmentcombobox.sip.in +++ b/python/gui/auto_generated/qgsalignmentcombobox.sip.in @@ -49,6 +49,18 @@ Sets the current ``alignment`` choice. .. seealso:: :py:func:`currentAlignment` %End + void customiseAlignmentDisplay( Qt::Alignment alignment, const QString &text = QString(), const QIcon &icon = QIcon() ); +%Docstring +Sets the ``text`` and ``icon`` to use for a particular ``alignment`` option, +replacing the default text or icon. + +If ``text`` or ``icon`` is not specified, they will not be changed from the default. + +.. note:: + + This must be called after first filtering the available alignment options via setAvailableAlignments(). +%End + signals: void changed(); diff --git a/src/app/layout/qgslayoutapputils.cpp b/src/app/layout/qgslayoutapputils.cpp index 19e761221fc..db734a19b6e 100644 --- a/src/app/layout/qgslayoutapputils.cpp +++ b/src/app/layout/qgslayoutapputils.cpp @@ -195,6 +195,16 @@ void QgsLayoutAppUtils::registerGuiForKnownItemTypes() // try to find a good map to link the legend with by default legend->setLinkedMap( findSensibleDefaultLinkedMapItem( legend ) ); + if ( QApplication::isRightToLeft() ) + { + // for right-to-left locales, use an appropriate default layout + legend->setSymbolAlignment( Qt::AlignRight ); + legend->rstyle( QgsLegendStyle::Group ).setAlignment( Qt::AlignRight ); + legend->rstyle( QgsLegendStyle::Subgroup ).setAlignment( Qt::AlignRight ); + legend->rstyle( QgsLegendStyle::SymbolLabel ).setAlignment( Qt::AlignRight ); + legend->setTitleAlignment( Qt::AlignRight ); + } + legend->updateLegend(); } ); diff --git a/src/app/layout/qgslayoutlegendwidget.cpp b/src/app/layout/qgslayoutlegendwidget.cpp index e98907aff58..b9ba7269886 100644 --- a/src/app/layout/qgslayoutlegendwidget.cpp +++ b/src/app/layout/qgslayoutlegendwidget.cpp @@ -70,7 +70,10 @@ QgsLayoutLegendWidget::QgsLayoutLegendWidget( QgsLayoutItemLegend *legend ) setupUi( this ); connect( mWrapCharLineEdit, &QLineEdit::textChanged, this, &QgsLayoutLegendWidget::mWrapCharLineEdit_textChanged ); connect( mTitleLineEdit, &QLineEdit::textChanged, this, &QgsLayoutLegendWidget::mTitleLineEdit_textChanged ); - connect( mTitleAlignCombo, static_cast( &QComboBox::currentIndexChanged ), this, &QgsLayoutLegendWidget::mTitleAlignCombo_currentIndexChanged ); + connect( mTitleAlignCombo, &QgsAlignmentComboBox::changed, this, &QgsLayoutLegendWidget::titleAlignmentChanged ); + connect( mGroupAlignCombo, &QgsAlignmentComboBox::changed, this, &QgsLayoutLegendWidget::groupAlignmentChanged ); + connect( mSubgroupAlignCombo, &QgsAlignmentComboBox::changed, this, &QgsLayoutLegendWidget::subgroupAlignmentChanged ); + connect( mItemAlignCombo, &QgsAlignmentComboBox::changed, this, &QgsLayoutLegendWidget::itemAlignmentChanged ); connect( mColumnCountSpinBox, static_cast < void ( QSpinBox::* )( int ) > ( &QSpinBox::valueChanged ), this, &QgsLayoutLegendWidget::mColumnCountSpinBox_valueChanged ); connect( mSplitLayerCheckBox, &QCheckBox::toggled, this, &QgsLayoutLegendWidget::mSplitLayerCheckBox_toggled ); connect( mEqualColumnWidthCheckBox, &QCheckBox::toggled, this, &QgsLayoutLegendWidget::mEqualColumnWidthCheckBox_toggled ); @@ -111,6 +114,16 @@ QgsLayoutLegendWidget::QgsLayoutLegendWidget( QgsLayoutItemLegend *legend ) mLayerFontButton->setMode( QgsFontButton::ModeQFont ); mItemFontButton->setMode( QgsFontButton::ModeQFont ); + mTitleAlignCombo->setAvailableAlignments( Qt::AlignLeft | Qt::AlignHCenter | Qt::AlignRight ); + mGroupAlignCombo->setAvailableAlignments( Qt::AlignLeft | Qt::AlignHCenter | Qt::AlignRight ); + mSubgroupAlignCombo->setAvailableAlignments( Qt::AlignLeft | Qt::AlignHCenter | Qt::AlignRight ); + mItemAlignCombo->setAvailableAlignments( Qt::AlignLeft | Qt::AlignHCenter | Qt::AlignRight ); + + mArrangementCombo->setAvailableAlignments( Qt::AlignLeft | Qt::AlignRight ); + connect( mArrangementCombo, &QgsAlignmentComboBox::changed, this, &QgsLayoutLegendWidget::arrangementChanged ); + mArrangementCombo->customiseAlignmentDisplay( Qt::AlignLeft, tr( "Symbols on Left" ), QgsApplication::getThemeIcon( QStringLiteral( "/mIconArrangeSymbolsLeft.svg" ) ) ); + mArrangementCombo->customiseAlignmentDisplay( Qt::AlignRight, tr( "Symbols on Right" ), QgsApplication::getThemeIcon( QStringLiteral( "/mIconArrangeSymbolsRight.svg" ) ) ); + // setup icons mAddToolButton->setIcon( QIcon( QgsApplication::iconPath( "symbologyAdd.svg" ) ) ); mEditPushButton->setIcon( QIcon( QgsApplication::iconPath( "symbologyEdit.svg" ) ) ); @@ -168,11 +181,13 @@ void QgsLayoutLegendWidget::setGuiElements() return; } - int alignment = mLegend->titleAlignment() == Qt::AlignLeft ? 0 : mLegend->titleAlignment() == Qt::AlignHCenter ? 1 : 2; - blockAllSignals( true ); mTitleLineEdit->setText( mLegend->title() ); - mTitleAlignCombo->setCurrentIndex( alignment ); + whileBlocking( mTitleAlignCombo )->setCurrentAlignment( mLegend->titleAlignment() ); + whileBlocking( mGroupAlignCombo )->setCurrentAlignment( mLegend->style( QgsLegendStyle::Group ).alignment() ); + whileBlocking( mSubgroupAlignCombo )->setCurrentAlignment( mLegend->style( QgsLegendStyle::Subgroup ).alignment() ); + whileBlocking( mItemAlignCombo )->setCurrentAlignment( mLegend->style( QgsLegendStyle::SymbolLabel ).alignment() ); + whileBlocking( mArrangementCombo )->setCurrentAlignment( mLegend->symbolAlignment() ); mFilterByMapToolButton->setChecked( mLegend->legendFilterByMapEnabled() ); mColumnCountSpinBox->setValue( mLegend->columnCount() ); mSplitLayerCheckBox->setChecked( mLegend->splitLayer() ); @@ -240,11 +255,11 @@ void QgsLayoutLegendWidget::mTitleLineEdit_textChanged( const QString &text ) } } -void QgsLayoutLegendWidget::mTitleAlignCombo_currentIndexChanged( int index ) +void QgsLayoutLegendWidget::titleAlignmentChanged() { if ( mLegend ) { - Qt::AlignmentFlag alignment = index == 0 ? Qt::AlignLeft : index == 1 ? Qt::AlignHCenter : Qt::AlignRight; + Qt::AlignmentFlag alignment = static_cast< Qt::AlignmentFlag >( static_cast< int >( mTitleAlignCombo->currentAlignment() & Qt::AlignHorizontal_Mask ) ); mLegend->beginCommand( tr( "Change Title Alignment" ) ); mLegend->setTitleAlignment( alignment ); mLegend->update(); @@ -252,6 +267,51 @@ void QgsLayoutLegendWidget::mTitleAlignCombo_currentIndexChanged( int index ) } } +void QgsLayoutLegendWidget::groupAlignmentChanged() +{ + if ( mLegend ) + { + mLegend->beginCommand( tr( "Change Group Alignment" ) ); + mLegend->rstyle( QgsLegendStyle::Group ).setAlignment( mGroupAlignCombo->currentAlignment() ); + mLegend->update(); + mLegend->endCommand(); + } +} + +void QgsLayoutLegendWidget::subgroupAlignmentChanged() +{ + if ( mLegend ) + { + mLegend->beginCommand( tr( "Change Subgroup Alignment" ) ); + mLegend->rstyle( QgsLegendStyle::Subgroup ).setAlignment( mSubgroupAlignCombo->currentAlignment() ); + mLegend->update(); + mLegend->endCommand(); + } +} + +void QgsLayoutLegendWidget::itemAlignmentChanged() +{ + if ( mLegend ) + { + mLegend->beginCommand( tr( "Change Item Alignment" ) ); + mLegend->rstyle( QgsLegendStyle::SymbolLabel ).setAlignment( mItemAlignCombo->currentAlignment() ); + mLegend->update(); + mLegend->endCommand(); + } +} + +void QgsLayoutLegendWidget::arrangementChanged() +{ + if ( mLegend ) + { + Qt::AlignmentFlag alignment = static_cast< Qt::AlignmentFlag >( static_cast< int >( mArrangementCombo->currentAlignment() & Qt::AlignHorizontal_Mask ) ); + mLegend->beginCommand( tr( "Change Legend Arrangement" ) ); + mLegend->setSymbolAlignment( alignment ); + mLegend->update(); + mLegend->endCommand(); + } +} + void QgsLayoutLegendWidget::mColumnCountSpinBox_valueChanged( int c ) { if ( mLegend ) @@ -1053,7 +1113,7 @@ void QgsLayoutLegendWidget::setCurrentNodeStyleFromAction() if ( !a || !mItemTreeView->currentNode() ) return; - QgsLegendRenderer::setNodeLegendStyle( mItemTreeView->currentNode(), ( QgsLegendStyle::Style ) a->data().toInt() ); + QgsLegendRenderer::setNodeLegendStyle( mItemTreeView->currentNode(), static_cast< QgsLegendStyle::Style >( a->data().toInt() ) ); mLegend->updateFilterByMap(); } @@ -1175,7 +1235,7 @@ QMenu *QgsLayoutLegendMenuProvider::createContextMenu() QAction *action = menu->addAction( QgsLegendStyle::styleLabel( style ), mWidget, &QgsLayoutLegendWidget::setCurrentNodeStyleFromAction ); action->setCheckable( true ); action->setChecked( currentStyle == style ); - action->setData( ( int ) style ); + action->setData( static_cast< int >( style ) ); } return menu; diff --git a/src/app/layout/qgslayoutlegendwidget.h b/src/app/layout/qgslayoutlegendwidget.h index 460959cdbe2..125eb906fdf 100644 --- a/src/app/layout/qgslayoutlegendwidget.h +++ b/src/app/layout/qgslayoutlegendwidget.h @@ -49,7 +49,6 @@ class QgsLayoutLegendWidget: public QgsLayoutItemBaseWidget, private Ui::QgsLayo void mWrapCharLineEdit_textChanged( const QString &text ); void mTitleLineEdit_textChanged( const QString &text ); - void mTitleAlignCombo_currentIndexChanged( int index ); void mColumnCountSpinBox_valueChanged( int c ); void mSplitLayerCheckBox_toggled( bool checked ); void mEqualColumnWidthCheckBox_toggled( bool checked ); @@ -108,6 +107,12 @@ class QgsLayoutLegendWidget: public QgsLayoutItemBaseWidget, private Ui::QgsLayo void layerFontChanged(); void itemFontChanged(); + void titleAlignmentChanged(); + void groupAlignmentChanged(); + void subgroupAlignmentChanged(); + void itemAlignmentChanged(); + void arrangementChanged(); + private: QgsLayoutLegendWidget() = delete; void blockAllSignals( bool b ); diff --git a/src/core/layertree/qgslayertreemodellegendnode.cpp b/src/core/layertree/qgslayertreemodellegendnode.cpp index 790ec9fb1da..d19d215c846 100644 --- a/src/core/layertree/qgslayertreemodellegendnode.cpp +++ b/src/core/layertree/qgslayertreemodellegendnode.cpp @@ -84,8 +84,27 @@ QSizeF QgsLayerTreeModelLegendNode::drawSymbol( const QgsLegendSettings &setting return QSizeF(); if ( ctx && ctx->painter ) - symbolIcon.paint( ctx->painter, ctx->point.x(), ctx->point.y() + ( itemHeight - settings.symbolSize().height() ) / 2, - settings.symbolSize().width(), settings.symbolSize().height() ); + { + switch ( settings.symbolAlignment() ) + { + case Qt::AlignLeft: + default: + symbolIcon.paint( ctx->painter, + static_cast< int >( ctx->columnLeft ), + static_cast< int >( ctx->top + ( itemHeight - settings.symbolSize().height() ) / 2 ), + static_cast< int >( settings.symbolSize().width() ), + static_cast< int >( settings.symbolSize().height() ) ); + break; + + case Qt::AlignRight: + symbolIcon.paint( ctx->painter, + static_cast< int >( ctx->columnRight - settings.symbolSize().width() ), + static_cast< int >( ctx->top + ( itemHeight - settings.symbolSize().height() ) / 2 ), + static_cast< int >( settings.symbolSize().width() ), + static_cast< int >( settings.symbolSize().height() ) ); + break; + } + } return settings.symbolSize(); } @@ -117,13 +136,34 @@ QSizeF QgsLayerTreeModelLegendNode::drawSymbolText( const QgsLegendSettings &set labelSize.rheight() = lines.count() * textHeight + ( lines.count() - 1 ) * ( settings.lineSpacing() + textDescent ); - double labelX = 0.0, labelY = 0.0; + double labelXMin = 0.0; + double labelXMax = 0.0; + double labelY = 0.0; if ( ctx && ctx->painter ) { ctx->painter->setPen( settings.fontColor() ); + switch ( settings.symbolAlignment() ) + { + case Qt::AlignLeft: + default: + labelXMin = ctx->columnLeft + std::max( static_cast< double >( symbolSize.width() ), ctx->maxSiblingSymbolWidth ) + + settings.style( QgsLegendStyle::Symbol ).margin( QgsLegendStyle::Right ) + + settings.style( QgsLegendStyle::SymbolLabel ).margin( QgsLegendStyle::Left ); + labelXMax = ctx->columnRight; + break; - labelX = ctx->point.x() + std::max( static_cast< double >( symbolSize.width() ), ctx->labelXOffset ); - labelY = ctx->point.y(); + case Qt::AlignRight: + labelXMin = ctx->columnLeft; + // NOTE -- while the below calculations use the flipped margins from the style, that's only done because + // those are the only margins we expose and use for now! (and we expose them as generic margins, not side-specific + // ones) TODO when/if we expose other margin settings, these should be reversed... + labelXMax = ctx->columnRight - std::max( static_cast< double >( symbolSize.width() ), ctx->maxSiblingSymbolWidth ) + - settings.style( QgsLegendStyle::Symbol ).margin( QgsLegendStyle::Right ) + - settings.style( QgsLegendStyle::SymbolLabel ).margin( QgsLegendStyle::Left ); + break; + } + + labelY = ctx->top; // Vertical alignment of label with symbol if ( labelSize.height() < symbolSize.height() ) @@ -134,11 +174,27 @@ QSizeF QgsLayerTreeModelLegendNode::drawSymbolText( const QgsLegendSettings &set for ( QStringList::ConstIterator itemPart = lines.constBegin(); itemPart != lines.constEnd(); ++itemPart ) { - labelSize.rwidth() = std::max( settings.textWidthMillimeters( symbolLabelFont, *itemPart ), double( labelSize.width() ) ); + const double lineWidth = settings.textWidthMillimeters( symbolLabelFont, *itemPart ); + labelSize.rwidth() = std::max( lineWidth, double( labelSize.width() ) ); if ( ctx && ctx->painter ) { - settings.drawText( ctx->painter, labelX, labelY, *itemPart, symbolLabelFont ); + switch ( settings.style( QgsLegendStyle::SymbolLabel ).alignment() ) + { + case Qt::AlignLeft: + default: + settings.drawText( ctx->painter, labelXMin, labelY, *itemPart, symbolLabelFont ); + break; + + case Qt::AlignRight: + settings.drawText( ctx->painter, labelXMax - lineWidth, labelY, *itemPart, symbolLabelFont ); + break; + + case Qt::AlignHCenter: + settings.drawText( ctx->painter, labelXMin + ( labelXMax - labelXMin - lineWidth ) / 2.0, labelY, *itemPart, symbolLabelFont ); + break; + } + if ( itemPart != ( lines.end() - 1 ) ) labelY += textDescent + settings.lineSpacing() + textHeight; } @@ -462,8 +518,7 @@ QSizeF QgsSymbolLegendNode::drawSymbol( const QgsLegendSettings &settings, ItemC if ( ctx && ctx->painter ) { - double currentXPosition = ctx->point.x(); - double currentYCoord = ctx->point.y() + ( itemHeight - settings.symbolSize().height() ) / 2; + double currentYCoord = ctx->top + ( itemHeight - settings.symbolSize().height() ) / 2; QPainter *p = ctx->painter; //setup painter scaling to dots so that raster symbology is drawn to scale @@ -475,7 +530,18 @@ QSizeF QgsSymbolLegendNode::drawSymbol( const QgsLegendSettings &settings, ItemC p->save(); p->setRenderHint( QPainter::Antialiasing ); - p->translate( currentXPosition + widthOffset, currentYCoord + heightOffset ); + + switch ( settings.symbolAlignment() ) + { + case Qt::AlignLeft: + default: + p->translate( ctx->columnLeft + widthOffset, currentYCoord + heightOffset ); + break; + case Qt::AlignRight: + p->translate( ctx->columnRight - widthOffset - width, currentYCoord + heightOffset ); + break; + } + p->scale( 1.0 / dotsPerMM, 1.0 / dotsPerMM ); if ( opacity != 255 && settings.useAdvancedEffects() ) { @@ -659,8 +725,19 @@ QSizeF QgsImageLegendNode::drawSymbol( const QgsLegendSettings &settings, ItemCo if ( ctx && ctx->painter ) { - ctx->painter->drawImage( QRectF( ctx->point.x(), ctx->point.y(), settings.wmsLegendSize().width(), settings.wmsLegendSize().height() ), - mImage, QRectF( 0, 0, mImage.width(), mImage.height() ) ); + switch ( settings.symbolAlignment() ) + { + case Qt::AlignLeft: + default: + ctx->painter->drawImage( QRectF( ctx->columnLeft, ctx->top, settings.wmsLegendSize().width(), settings.wmsLegendSize().height() ), + mImage, QRectF( 0, 0, mImage.width(), mImage.height() ) ); + break; + + case Qt::AlignRight: + ctx->painter->drawImage( QRectF( ctx->columnRight - settings.wmsLegendSize().width(), ctx->top, settings.wmsLegendSize().width(), settings.wmsLegendSize().height() ), + mImage, QRectF( 0, 0, mImage.width(), mImage.height() ) ); + break; + } } return settings.wmsLegendSize(); } @@ -724,8 +801,19 @@ QSizeF QgsRasterSymbolLegendNode::drawSymbol( const QgsLegendSettings &settings, ctx->painter->setPen( Qt::NoPen ); } - ctx->painter->drawRect( QRectF( ctx->point.x(), ctx->point.y() + ( itemHeight - settings.symbolSize().height() ) / 2, - settings.symbolSize().width(), settings.symbolSize().height() ) ); + switch ( settings.symbolAlignment() ) + { + case Qt::AlignLeft: + default: + ctx->painter->drawRect( QRectF( ctx->columnLeft, ctx->top + ( itemHeight - settings.symbolSize().height() ) / 2, + settings.symbolSize().width(), settings.symbolSize().height() ) ); + break; + + case Qt::AlignRight: + ctx->painter->drawRect( QRectF( ctx->columnRight - settings.symbolSize().width(), ctx->top + ( itemHeight - settings.symbolSize().height() ) / 2, + settings.symbolSize().width(), settings.symbolSize().height() ) ); + break; + } } return settings.symbolSize(); } @@ -829,9 +917,27 @@ QSizeF QgsWmsLegendNode::drawSymbol( const QgsLegendSettings &settings, ItemCont if ( ctx && ctx->painter ) { - ctx->painter->drawImage( QRectF( ctx->point, settings.wmsLegendSize() ), - mImage, - QRectF( QPointF( 0, 0 ), mImage.size() ) ); + switch ( settings.symbolAlignment() ) + { + case Qt::AlignLeft: + default: + ctx->painter->drawImage( QRectF( ctx->columnLeft, + ctx->top, + settings.wmsLegendSize().width(), + settings.wmsLegendSize().height() ), + mImage, + QRectF( QPointF( 0, 0 ), mImage.size() ) ); + break; + + case Qt::AlignRight: + ctx->painter->drawImage( QRectF( ctx->columnRight - settings.wmsLegendSize().width(), + ctx->top, + settings.wmsLegendSize().width(), + settings.wmsLegendSize().height() ), + mImage, + QRectF( QPointF( 0, 0 ), mImage.size() ) ); + break; + } } return settings.wmsLegendSize(); } @@ -958,7 +1064,7 @@ QgsLayerTreeModelLegendNode::ItemMetrics QgsDataDefinedSizeLegendNode::draw( con context.setPainter( ctx->painter ); ctx->painter->save(); ctx->painter->setRenderHint( QPainter::Antialiasing ); - ctx->painter->translate( ctx->point ); + ctx->painter->translate( ctx->columnLeft, ctx->top ); ctx->painter->scale( 1 / context.scaleFactor(), 1 / context.scaleFactor() ); } diff --git a/src/core/layertree/qgslayertreemodellegendnode.h b/src/core/layertree/qgslayertreemodellegendnode.h index 9a049d293d7..5008893273b 100644 --- a/src/core/layertree/qgslayertreemodellegendnode.h +++ b/src/core/layertree/qgslayertreemodellegendnode.h @@ -86,14 +86,56 @@ class CORE_EXPORT QgsLayerTreeModelLegendNode : public QObject struct ItemContext { + Q_NOWARN_DEPRECATED_PUSH //because of deprecated members + ItemContext() = default; + Q_NOWARN_DEPRECATED_POP + //! Render context, if available QgsRenderContext *context = nullptr; //! Painter QPainter *painter = nullptr; - //! Top-left corner of the legend item - QPointF point; - //! offset from the left side where label should start - double labelXOffset; + + /** + * Top-left corner of the legend item. + * \deprecated Use top, columnLeft, columnRight instead. + */ + Q_DECL_DEPRECATED QPointF point; + + /** + * Offset from the left side where label should start. + * \deprecated use columnLeft, columnRight instead. + */ + Q_DECL_DEPRECATED double labelXOffset = 0.0; + + /** + * Top y-position of legend item. + * \since QGIS 3.10 + */ + double top = 0.0; + + /** + * Left side of current legend column. This should be used when determining + * where to render legend item content, correctly respecting the symbol and text + * alignment from the legend settings. + * \since QGIS 3.10 + */ + double columnLeft = 0.0; + + /** + * Right side of current legend column. This should be used when determining + * where to render legend item content, correctly respecting the symbol and text + * alignment from the legend settings. + * \since QGIS 3.10 + */ + double columnRight = 0.0; + + /** + * Largest symbol width, considering all other sibling legend components associated with + * the current component. + * \since QGIS 3.10 + */ + double maxSiblingSymbolWidth = 0.0; + }; struct ItemMetrics diff --git a/src/core/layout/qgslayoutitemlegend.cpp b/src/core/layout/qgslayoutitemlegend.cpp index f516f5eb272..8769b6a0114 100644 --- a/src/core/layout/qgslayoutitemlegend.cpp +++ b/src/core/layout/qgslayoutitemlegend.cpp @@ -139,6 +139,7 @@ void QgsLayoutItemLegend::paint( QPainter *painter, const QStyleOptionGraphicsIt attemptResize( newSize ); } } + QgsLayoutItem::paint( painter, itemStyle, pWidget ); } @@ -366,6 +367,16 @@ void QgsLayoutItemLegend::setSymbolWidth( double w ) mSettings.setSymbolSize( QSizeF( w, mSettings.symbolSize().height() ) ); } +void QgsLayoutItemLegend::setSymbolAlignment( Qt::AlignmentFlag alignment ) +{ + mSettings.setSymbolAlignment( alignment ); +} + +Qt::AlignmentFlag QgsLayoutItemLegend::symbolAlignment() const +{ + return mSettings.symbolAlignment(); +} + double QgsLayoutItemLegend::symbolHeight() const { return mSettings.symbolSize().height(); @@ -488,6 +499,10 @@ bool QgsLayoutItemLegend::writePropertiesToElement( QDomElement &legendElem, QDo legendElem.setAttribute( QStringLiteral( "symbolWidth" ), QString::number( mSettings.symbolSize().width() ) ); legendElem.setAttribute( QStringLiteral( "symbolHeight" ), QString::number( mSettings.symbolSize().height() ) ); + + legendElem.setAttribute( QStringLiteral( "symbolAlignment" ), mSettings.symbolAlignment() ); + + legendElem.setAttribute( QStringLiteral( "symbolAlignment" ), mSettings.symbolAlignment() ); legendElem.setAttribute( QStringLiteral( "lineSpacing" ), QString::number( mSettings.lineSpacing() ) ); legendElem.setAttribute( QStringLiteral( "rasterBorder" ), mSettings.drawRasterStroke() ); @@ -577,6 +592,8 @@ bool QgsLayoutItemLegend::readPropertiesFromElement( const QDomElement &itemElem mSettings.setColumnSpace( itemElem.attribute( QStringLiteral( "columnSpace" ), QStringLiteral( "2.0" ) ).toDouble() ); mSettings.setSymbolSize( QSizeF( itemElem.attribute( QStringLiteral( "symbolWidth" ), QStringLiteral( "7.0" ) ).toDouble(), itemElem.attribute( QStringLiteral( "symbolHeight" ), QStringLiteral( "14.0" ) ).toDouble() ) ); + mSettings.setSymbolAlignment( static_cast< Qt::AlignmentFlag >( itemElem.attribute( QStringLiteral( "symbolAlignment" ), QString::number( Qt::AlignLeft ) ).toInt() ) ); + mSettings.setWmsLegendSize( QSizeF( itemElem.attribute( QStringLiteral( "wmsLegendWidth" ), QStringLiteral( "50" ) ).toDouble(), itemElem.attribute( QStringLiteral( "wmsLegendHeight" ), QStringLiteral( "25" ) ).toDouble() ) ); mSettings.setLineSpacing( itemElem.attribute( QStringLiteral( "lineSpacing" ), QStringLiteral( "1.0" ) ).toDouble() ); @@ -776,7 +793,7 @@ void QgsLayoutItemLegend::doUpdateFilterByMap() if ( mMap && ( mLegendFilterByMap || filterByExpression || mInAtlas ) ) { - int dpi = mLayout->renderContext().dpi(); + double dpi = mLayout->renderContext().dpi(); QgsRectangle requestRectangle = mMap->requestedExtent(); diff --git a/src/core/layout/qgslayoutitemlegend.h b/src/core/layout/qgslayoutitemlegend.h index c6a74fb2a55..8c42ff31fbf 100644 --- a/src/core/layout/qgslayoutitemlegend.h +++ b/src/core/layout/qgslayoutitemlegend.h @@ -266,6 +266,26 @@ class CORE_EXPORT QgsLayoutItemLegend : public QgsLayoutItem */ void setSymbolWidth( double width ); + /** + * Sets the \a alignment for placement of legend symbols. + * + * Only Qt::AlignLeft or Qt::AlignRight are supported values. + * + * \see symbolAlignment() + * \since QGIS 3.10.0 + */ + void setSymbolAlignment( Qt::AlignmentFlag alignment ); + + /** + * Returns the alignment for placement of legend symbols. + * + * Only Qt::AlignLeft or Qt::AlignRight are supported values. + * + * \see setSymbolAlignment() + * \since QGIS 3.10.0 + */ + Qt::AlignmentFlag symbolAlignment() const; + /** * Returns the legend symbol height. * \see setSymbolHeight() diff --git a/src/core/qgslegendrenderer.cpp b/src/core/qgslegendrenderer.cpp index 5d91b4a576f..9ea4c717874 100644 --- a/src/core/qgslegendrenderer.cpp +++ b/src/core/qgslegendrenderer.cpp @@ -129,19 +129,38 @@ QSizeF QgsLegendRenderer::paintAndDetermineSizeInternal( QgsRenderContext *conte if ( !rootGroup ) return size; - QList atomList = createAtomList( rootGroup, mSettings.splitLayer() ); + QList atomList = createComponentGroupList( rootGroup, mSettings.splitLayer() ); setColumns( atomList ); - qreal maxColumnWidth = 0; - if ( mSettings.equalColumnWidth() ) + // another iteration -- this one is required to calculate the maximum item width for each + // column. Unfortunately, we can't trust the atom widths at this stage, as they are minimal widths + // only. When actually rendering a symbol node, the text is aligned according to the WIDEST + // symbol in a column. So that means we can't possibly determine the exact size of legend components + // until now. BUUUUUUUUUUUUT. Because everything sucks, we can't even start the actual render of items + // at the same time we calculate this -- legend items REQUIRE the REAL width of the columns in order to + // correctly align right or center-aligned symbols/text. Bah -- A triple iteration it is! + QMap< int, double > maxColumnWidths; + qreal maxEqualColumnWidth = 0; + // temporarily remove painter from context -- we don't need to actually draw anything yet. But we DO need + // to send the full render context so that an expression context is available during the size calculation + QPainter *prevPainter = context ? context->painter() : nullptr; + if ( context ) + context->setPainter( nullptr ); + for ( const LegendComponentGroup &atom : qgis::as_const( atomList ) ) { - const auto constAtomList = atomList; - for ( const Atom &atom : constAtomList ) + const QSizeF actualSize = drawGroup( atom, context, ColumnContext() ); + if ( mSettings.equalColumnWidth() ) { - maxColumnWidth = std::max( atom.size.width(), maxColumnWidth ); + maxEqualColumnWidth = std::max( actualSize.width(), maxEqualColumnWidth ); + } + else + { + maxColumnWidths[ atom.column ] = std::max( actualSize.width(), maxColumnWidths.value( atom.column, 0 ) ); } } + if ( context ) + context->setPainter( prevPainter ); //calculate size of title QSizeF titleSize = drawTitle(); @@ -149,48 +168,46 @@ QSizeF QgsLegendRenderer::paintAndDetermineSizeInternal( QgsRenderContext *conte titleSize.rwidth() += mSettings.boxSpace() * 2.0; double columnTop = mSettings.boxSpace() + titleSize.height() + mSettings.style( QgsLegendStyle::Title ).margin( QgsLegendStyle::Bottom ); - QPointF point( mSettings.boxSpace(), columnTop ); bool firstInColumn = true; double columnMaxHeight = 0; qreal columnWidth = 0; - int column = 0; - const auto constAtomList = atomList; - for ( const Atom &atom : constAtomList ) + int column = -1; + ColumnContext columnContext; + columnContext.left = mSettings.boxSpace(); + columnContext.right = mLegendSize.width() - mSettings.boxSpace(); + double currentY = columnTop; + + for ( const LegendComponentGroup &atom : qgis::as_const( atomList ) ) { if ( atom.column > column ) { // Switch to next column - if ( mSettings.equalColumnWidth() ) - { - point.rx() += mSettings.columnSpace() + maxColumnWidth; - } - else - { - point.rx() += mSettings.columnSpace() + columnWidth; - } - point.ry() = columnTop; - columnWidth = 0; + columnContext.left = atom.column > 0 ? columnContext.right + mSettings.columnSpace() : mSettings.boxSpace(); + columnWidth = mSettings.equalColumnWidth() ? maxEqualColumnWidth : maxColumnWidths.value( atom.column ); + columnContext.right = columnContext.left + columnWidth; + currentY = columnTop; column++; firstInColumn = true; } if ( !firstInColumn ) { - point.ry() += spaceAboveAtom( atom ); + currentY += spaceAboveGroup( atom ); } - QSizeF atomSize = context ? drawAtom( atom, context, point ) - : drawAtom( atom, painter, point ); - columnWidth = std::max( atomSize.width(), columnWidth ); + if ( context ) + drawGroup( atom, context, columnContext, currentY ); + else if ( painter ) + drawGroup( atom, columnContext, painter, currentY ); - point.ry() += atom.size.height(); - columnMaxHeight = std::max( point.y() - columnTop, columnMaxHeight ); + currentY += atom.size.height(); + columnMaxHeight = std::max( currentY - columnTop, columnMaxHeight ); firstInColumn = false; } - point.rx() += columnWidth + mSettings.boxSpace(); + const double totalWidth = columnContext.right + mSettings.boxSpace(); size.rheight() = columnTop + columnMaxHeight + mSettings.boxSpace(); - size.rwidth() = point.x(); + size.rwidth() = totalWidth; if ( !mSettings.title().isEmpty() ) { size.rwidth() = std::max( titleSize.width(), size.width() ); @@ -207,32 +224,38 @@ QSizeF QgsLegendRenderer::paintAndDetermineSizeInternal( QgsRenderContext *conte // Now we have set the correct total item width and can draw the title centered if ( !mSettings.title().isEmpty() ) { - if ( mSettings.titleAlignment() == Qt::AlignLeft ) - { - point.rx() = mSettings.boxSpace(); - } - else if ( mSettings.titleAlignment() == Qt::AlignHCenter ) - { - point.rx() = size.width() / 2; - } - else - { - point.rx() = size.width() - mSettings.boxSpace(); - } - point.ry() = mSettings.boxSpace(); if ( context ) - drawTitle( context, point, mSettings.titleAlignment(), size.width() ); + drawTitle( context, mSettings.boxSpace(), mSettings.titleAlignment(), size.width() ); else - drawTitle( painter, point, mSettings.titleAlignment(), size.width() ); + drawTitle( painter, mSettings.boxSpace(), mSettings.titleAlignment(), size.width() ); } return size; } - -QList QgsLegendRenderer::createAtomList( QgsLayerTreeGroup *parentGroup, bool splitLayer ) +void QgsLegendRenderer::widthAndOffsetForTitleText( const Qt::AlignmentFlag halignment, const double legendWidth, double &textBoxWidth, double &textBoxLeft ) { - QList atoms; + switch ( halignment ) + { + default: + textBoxLeft = mSettings.boxSpace(); + textBoxWidth = legendWidth - 2 * mSettings.boxSpace(); + break; + + case Qt::AlignHCenter: + { + // not sure on this logic, I just moved it -- don't blame me for it being totally obscure! + const double centerX = legendWidth / 2; + textBoxWidth = ( std::min( static_cast< double >( centerX ), legendWidth - centerX ) - mSettings.boxSpace() ) * 2.0; + textBoxLeft = centerX - textBoxWidth / 2.; + break; + } + } +} + +QList QgsLegendRenderer::createComponentGroupList( QgsLayerTreeGroup *parentGroup, bool splitLayer ) +{ + QList atoms; if ( !parentGroup ) return atoms; @@ -244,29 +267,29 @@ QList QgsLegendRenderer::createAtomList( QgsLayerTreeGr QgsLayerTreeGroup *nodeGroup = QgsLayerTree::toGroup( node ); // Group subitems - QList groupAtoms = createAtomList( nodeGroup, splitLayer ); + QList groupAtoms = createComponentGroupList( nodeGroup, splitLayer ); bool hasSubItems = !groupAtoms.empty(); if ( nodeLegendStyle( nodeGroup ) != QgsLegendStyle::Hidden ) { - Nucleon nucleon; + LegendComponent nucleon; nucleon.item = node; nucleon.size = drawGroupTitle( nodeGroup ); if ( !groupAtoms.isEmpty() ) { // Add internal space between this group title and the next nucleon - groupAtoms[0].size.rheight() += spaceAboveAtom( groupAtoms[0] ); + groupAtoms[0].size.rheight() += spaceAboveGroup( groupAtoms[0] ); // Prepend this group title to the first atom - groupAtoms[0].nucleons.prepend( nucleon ); + groupAtoms[0].components.prepend( nucleon ); groupAtoms[0].size.rheight() += nucleon.size.height(); groupAtoms[0].size.rwidth() = std::max( nucleon.size.width(), groupAtoms[0].size.width() ); } else { // no subitems, append new atom - Atom atom; - atom.nucleons.append( nucleon ); + LegendComponentGroup atom; + atom.components.append( nucleon ); atom.size.rwidth() += nucleon.size.width(); atom.size.rheight() += nucleon.size.height(); atom.size.rwidth() = std::max( nucleon.size.width(), atom.size.width() ); @@ -284,14 +307,14 @@ QList QgsLegendRenderer::createAtomList( QgsLayerTreeGr { QgsLayerTreeLayer *nodeLayer = QgsLayerTree::toLayer( node ); - Atom atom; + LegendComponentGroup atom; if ( nodeLegendStyle( nodeLayer ) != QgsLegendStyle::Hidden ) { - Nucleon nucleon; + LegendComponent nucleon; nucleon.item = node; nucleon.size = drawLayerTitle( nodeLayer ); - atom.nucleons.append( nucleon ); + atom.components.append( nucleon ); atom.size.rwidth() = nucleon.size.width(); atom.size.rheight() = nucleon.size.height(); } @@ -304,13 +327,13 @@ QList QgsLegendRenderer::createAtomList( QgsLayerTreeGr if ( legendNodes.isEmpty() && mLegendModel->legendFilterMapSettings() ) continue; - QList layerAtoms; + QList layerAtoms; for ( int j = 0; j < legendNodes.count(); j++ ) { QgsLayerTreeModelLegendNode *legendNode = legendNodes.at( j ); - Nucleon symbolNucleon = drawSymbolItem( legendNode ); + LegendComponent symbolNucleon = drawSymbolItem( legendNode ); if ( !mSettings.splitLayer() || j == 0 ) { @@ -318,18 +341,18 @@ QList QgsLegendRenderer::createAtomList( QgsLayerTreeGr // the width is not correct at this moment, we must align all symbol labels atom.size.rwidth() = std::max( symbolNucleon.size.width(), atom.size.width() ); // Add symbol space only if there is already title or another item above - if ( !atom.nucleons.isEmpty() ) + if ( !atom.components.isEmpty() ) { // TODO: for now we keep Symbol and SymbolLabel Top margin in sync atom.size.rheight() += mSettings.style( QgsLegendStyle::Symbol ).margin( QgsLegendStyle::Top ); } atom.size.rheight() += symbolNucleon.size.height(); - atom.nucleons.append( symbolNucleon ); + atom.components.append( symbolNucleon ); } else { - Atom symbolAtom; - symbolAtom.nucleons.append( symbolNucleon ); + LegendComponentGroup symbolAtom; + symbolAtom.components.append( symbolNucleon ); symbolAtom.size.rwidth() = symbolNucleon.size.width(); symbolAtom.size.rheight() = symbolNucleon.size.height(); layerAtoms.append( symbolAtom ); @@ -344,7 +367,7 @@ QList QgsLegendRenderer::createAtomList( QgsLayerTreeGr } -void QgsLegendRenderer::setColumns( QList &atomList ) +void QgsLegendRenderer::setColumns( QList &atomList ) { if ( mSettings.columnCount() == 0 ) return; @@ -352,9 +375,9 @@ void QgsLegendRenderer::setColumns( QList &atomList ) double totalHeight = 0; qreal maxAtomHeight = 0; const auto constAtomList = atomList; - for ( const Atom &atom : constAtomList ) + for ( const LegendComponentGroup &atom : constAtomList ) { - totalHeight += spaceAboveAtom( atom ); + totalHeight += spaceAboveGroup( atom ); totalHeight += atom.size.height(); maxAtomHeight = std::max( atom.size.height(), maxAtomHeight ); } @@ -375,10 +398,10 @@ void QgsLegendRenderer::setColumns( QList &atomList ) // Recalc average height for remaining columns including current double avgColumnHeight = ( totalHeight - closedColumnsHeight ) / ( mSettings.columnCount() - currentColumn ); - Atom atom = atomList.at( i ); + LegendComponentGroup atom = atomList.at( i ); double currentHeight = currentColumnHeight; if ( currentColumnAtomCount > 0 ) - currentHeight += spaceAboveAtom( atom ); + currentHeight += spaceAboveGroup( atom ); currentHeight += atom.size.height(); bool canCreateNewColumn = ( currentColumnAtomCount > 0 ) // do not leave empty column @@ -414,39 +437,40 @@ void QgsLegendRenderer::setColumns( QList &atomList ) QMap maxSymbolWidth; for ( int i = 0; i < atomList.size(); i++ ) { - Atom &atom = atomList[i]; - for ( int j = 0; j < atom.nucleons.size(); j++ ) + LegendComponentGroup &atom = atomList[i]; + for ( int j = 0; j < atom.components.size(); j++ ) { - if ( QgsLayerTreeModelLegendNode *legendNode = qobject_cast( atom.nucleons.at( j ).item ) ) + if ( QgsLayerTreeModelLegendNode *legendNode = qobject_cast( atom.components.at( j ).item ) ) { QString key = QStringLiteral( "%1-%2" ).arg( reinterpret_cast< qulonglong >( legendNode->layerNode() ) ).arg( atom.column ); - maxSymbolWidth[key] = std::max( atom.nucleons.at( j ).symbolSize.width(), maxSymbolWidth[key] ); + maxSymbolWidth[key] = std::max( atom.components.at( j ).symbolSize.width(), maxSymbolWidth[key] ); } } } for ( int i = 0; i < atomList.size(); i++ ) { - Atom &atom = atomList[i]; - for ( int j = 0; j < atom.nucleons.size(); j++ ) + LegendComponentGroup &atom = atomList[i]; + for ( int j = 0; j < atom.components.size(); j++ ) { - if ( QgsLayerTreeModelLegendNode *legendNode = qobject_cast( atom.nucleons.at( j ).item ) ) + if ( QgsLayerTreeModelLegendNode *legendNode = qobject_cast( atom.components.at( j ).item ) ) { QString key = QStringLiteral( "%1-%2" ).arg( reinterpret_cast< qulonglong >( legendNode->layerNode() ) ).arg( atom.column ); double space = mSettings.style( QgsLegendStyle::Symbol ).margin( QgsLegendStyle::Right ) + mSettings.style( QgsLegendStyle::SymbolLabel ).margin( QgsLegendStyle::Left ); - atom.nucleons[j].labelXOffset = maxSymbolWidth[key] + space; - atom.nucleons[j].size.rwidth() = maxSymbolWidth[key] + space + atom.nucleons.at( j ).labelSize.width(); + atom.components[j].labelXOffset = maxSymbolWidth[key] + space; + atom.components[j].maxSiblingSymbolWidth = maxSymbolWidth[key]; + atom.components[j].size.rwidth() = maxSymbolWidth[key] + space + atom.components.at( j ).labelSize.width(); } } } } -QSizeF QgsLegendRenderer::drawTitle( QPainter *painter, QPointF point, Qt::AlignmentFlag halignment, double legendWidth ) +QSizeF QgsLegendRenderer::drawTitle( QPainter *painter, double top, Qt::AlignmentFlag halignment, double legendWidth ) { - return drawTitleInternal( nullptr, painter, point, halignment, legendWidth ); + return drawTitleInternal( nullptr, painter, top, halignment, legendWidth ); } -QSizeF QgsLegendRenderer::drawTitleInternal( QgsRenderContext *context, QPainter *painter, QPointF point, Qt::AlignmentFlag halignment, double legendWidth ) +QSizeF QgsLegendRenderer::drawTitleInternal( QgsRenderContext *context, QPainter *painter, const double top, Qt::AlignmentFlag halignment, double legendWidth ) { QSizeF size( 0, 0 ); if ( mSettings.title().isEmpty() ) @@ -455,7 +479,7 @@ QSizeF QgsLegendRenderer::drawTitleInternal( QgsRenderContext *context, QPainter } QStringList lines = mSettings.splitStringForWrapping( mSettings.title() ); - double y = point.y(); + double y = top; if ( context && context->painter() ) { @@ -469,22 +493,7 @@ QSizeF QgsLegendRenderer::drawTitleInternal( QgsRenderContext *context, QPainter //calculate width and left pos of rectangle to draw text into double textBoxWidth; double textBoxLeft; - switch ( halignment ) - { - case Qt::AlignHCenter: - textBoxWidth = ( std::min( static_cast< double >( point.x() ), legendWidth - point.x() ) - mSettings.boxSpace() ) * 2.0; - textBoxLeft = point.x() - textBoxWidth / 2.; - break; - case Qt::AlignRight: - textBoxLeft = mSettings.boxSpace(); - textBoxWidth = point.x() - mSettings.boxSpace(); - break; - case Qt::AlignLeft: - default: - textBoxLeft = point.x(); - textBoxWidth = legendWidth - point.x() - mSettings.boxSpace(); - break; - } + widthAndOffsetForTitleText( halignment, legendWidth, textBoxWidth, textBoxLeft ); QFont titleFont = mSettings.style( QgsLegendStyle::Title ).font(); @@ -515,17 +524,17 @@ QSizeF QgsLegendRenderer::drawTitleInternal( QgsRenderContext *context, QPainter y += mSettings.lineSpacing(); } } - size.rheight() = y - point.y(); + size.rheight() = y - top; return size; } -double QgsLegendRenderer::spaceAboveAtom( const Atom &atom ) +double QgsLegendRenderer::spaceAboveGroup( const LegendComponentGroup &atom ) { - if ( atom.nucleons.isEmpty() ) return 0; + if ( atom.components.isEmpty() ) return 0; - Nucleon nucleon = atom.nucleons.first(); + LegendComponent nucleon = atom.components.first(); if ( QgsLayerTreeGroup *nodeGroup = qobject_cast( nucleon.item ) ) { @@ -546,16 +555,17 @@ double QgsLegendRenderer::spaceAboveAtom( const Atom &atom ) // Draw atom and expand its size (using actual nucleons labelXOffset) -QSizeF QgsLegendRenderer::drawAtom( const Atom &atom, QPainter *painter, QPointF point ) +QSizeF QgsLegendRenderer::drawGroup( const LegendComponentGroup &atom, const ColumnContext &columnContext, QPainter *painter, double top ) { - return drawAtomInternal( atom, nullptr, painter, point ); + return drawGroupInternal( atom, nullptr, columnContext, painter, top ); } -QSizeF QgsLegendRenderer::drawAtomInternal( const Atom &atom, QgsRenderContext *context, QPainter *painter, QPointF point ) +QSizeF QgsLegendRenderer::drawGroupInternal( const LegendComponentGroup &atom, QgsRenderContext *context, const ColumnContext &columnContext, QPainter *painter, const double top ) { bool first = true; QSizeF size = QSizeF( atom.size ); - for ( const Nucleon &nucleon : qgis::as_const( atom.nucleons ) ) + double currentY = top; + for ( const LegendComponent &nucleon : qgis::as_const( atom.components ) ) { if ( QgsLayerTreeGroup *groupItem = qobject_cast( nucleon.item ) ) { @@ -564,12 +574,12 @@ QSizeF QgsLegendRenderer::drawAtomInternal( const Atom &atom, QgsRenderContext * { if ( !first ) { - point.ry() += mSettings.style( s ).margin( QgsLegendStyle::Top ); + currentY += mSettings.style( s ).margin( QgsLegendStyle::Top ); } if ( context ) - drawGroupTitle( groupItem, context, point ); + drawGroupTitle( groupItem, context, columnContext, currentY ); else - drawGroupTitle( groupItem, painter, point ); + drawGroupTitle( groupItem, columnContext, painter, currentY ); } } else if ( QgsLayerTreeLayer *layerItem = qobject_cast( nucleon.item ) ) @@ -579,38 +589,38 @@ QSizeF QgsLegendRenderer::drawAtomInternal( const Atom &atom, QgsRenderContext * { if ( !first ) { - point.ry() += mSettings.style( s ).margin( QgsLegendStyle::Top ); + currentY += mSettings.style( s ).margin( QgsLegendStyle::Top ); } if ( context ) - drawLayerTitle( layerItem, context, point ); + drawLayerTitle( layerItem, context, columnContext, currentY ); else - drawLayerTitle( layerItem, painter, point ); + drawLayerTitle( layerItem, columnContext, painter, currentY ); } } else if ( QgsLayerTreeModelLegendNode *legendNode = qobject_cast( nucleon.item ) ) { if ( !first ) { - point.ry() += mSettings.style( QgsLegendStyle::Symbol ).margin( QgsLegendStyle::Top ); + currentY += mSettings.style( QgsLegendStyle::Symbol ).margin( QgsLegendStyle::Top ); } - Nucleon symbolNucleon = context ? drawSymbolItem( legendNode, context, point, nucleon.labelXOffset ) - : drawSymbolItem( legendNode, painter, point, nucleon.labelXOffset ); + LegendComponent symbolNucleon = context ? drawSymbolItem( legendNode, context, columnContext, currentY, nucleon.maxSiblingSymbolWidth ) + : drawSymbolItem( legendNode, columnContext, painter, currentY, nucleon.maxSiblingSymbolWidth ); // expand width, it may be wider because of labelXOffset size.rwidth() = std::max( symbolNucleon.size.width(), size.width() ); } - point.ry() += nucleon.size.height(); + currentY += nucleon.size.height(); first = false; } return size; } -QgsLegendRenderer::Nucleon QgsLegendRenderer::drawSymbolItem( QgsLayerTreeModelLegendNode *symbolItem, QPainter *painter, QPointF point, double labelXOffset ) +QgsLegendRenderer::LegendComponent QgsLegendRenderer::drawSymbolItem( QgsLayerTreeModelLegendNode *symbolItem, const ColumnContext &columnContext, QPainter *painter, double top, double maxSiblingSymbolWidth ) { - return drawSymbolItemInternal( symbolItem, nullptr, painter, point, labelXOffset ); + return drawSymbolItemInternal( symbolItem, columnContext, nullptr, painter, top, maxSiblingSymbolWidth ); } -QgsLegendRenderer::Nucleon QgsLegendRenderer::drawSymbolItemInternal( QgsLayerTreeModelLegendNode *symbolItem, QgsRenderContext *context, QPainter *painter, QPointF point, double labelXOffset ) +QgsLegendRenderer::LegendComponent QgsLegendRenderer::drawSymbolItemInternal( QgsLayerTreeModelLegendNode *symbolItem, const ColumnContext &columnContext, QgsRenderContext *context, QPainter *painter, double top, double maxSiblingSymbolWidth ) { QgsLayerTreeModelLegendNode::ItemContext ctx; ctx.context = context; @@ -624,8 +634,15 @@ QgsLegendRenderer::Nucleon QgsLegendRenderer::drawSymbolItemInternal( QgsLayerTr } ctx.painter = context ? context->painter() : painter; - ctx.point = point; - ctx.labelXOffset = labelXOffset; + Q_NOWARN_DEPRECATED_PUSH + ctx.point = QPointF( columnContext.left, top ); + ctx.labelXOffset = maxSiblingSymbolWidth; + Q_NOWARN_DEPRECATED_POP + + ctx.top = top; + ctx.columnLeft = columnContext.left; + ctx.columnRight = columnContext.right; + ctx.maxSiblingSymbolWidth = maxSiblingSymbolWidth; QgsLayerTreeModelLegendNode::ItemMetrics im = symbolItem->draw( mSettings, context ? &ctx : ( painter ? &ctx : nullptr ) ); @@ -633,23 +650,30 @@ QgsLegendRenderer::Nucleon QgsLegendRenderer::drawSymbolItemInternal( QgsLayerTr if ( layerScope ) delete context->expressionContext().popScope(); - Nucleon nucleon; + LegendComponent nucleon; nucleon.item = symbolItem; nucleon.symbolSize = im.symbolSize; nucleon.labelSize = im.labelSize; //QgsDebugMsg( QStringLiteral( "symbol height = %1 label height = %2").arg( symbolSize.height()).arg( labelSize.height() )); - double width = std::max( static_cast< double >( im.symbolSize.width() ), labelXOffset ) + im.labelSize.width(); + // NOTE -- we hard code left/right margins below, because those are the only ones exposed for use currently. + // ideally we could (should?) expose all these margins as settings, and then adapt the below to respect the current symbol/text alignment + // and consider the correct margin sides... + double width = std::max( static_cast< double >( im.symbolSize.width() ), maxSiblingSymbolWidth ) + + mSettings.style( QgsLegendStyle::Symbol ).margin( QgsLegendStyle::Right ) + + mSettings.style( QgsLegendStyle::SymbolLabel ).margin( QgsLegendStyle::Left ) + + im.labelSize.width(); + double height = std::max( im.symbolSize.height(), im.labelSize.height() ); nucleon.size = QSizeF( width, height ); return nucleon; } -QSizeF QgsLegendRenderer::drawLayerTitle( QgsLayerTreeLayer *nodeLayer, QPainter *painter, QPointF point ) +QSizeF QgsLegendRenderer::drawLayerTitle( QgsLayerTreeLayer *nodeLayer, const ColumnContext &columnContext, QPainter *painter, double top ) { - return drawLayerTitleInternal( nodeLayer, nullptr, painter, point ); + return drawLayerTitleInternal( nodeLayer, columnContext, nullptr, painter, top ); } -QSizeF QgsLegendRenderer::drawLayerTitleInternal( QgsLayerTreeLayer *nodeLayer, QgsRenderContext *context, QPainter *painter, QPointF point ) +QSizeF QgsLegendRenderer::drawLayerTitleInternal( QgsLayerTreeLayer *nodeLayer, const ColumnContext &columnContext, QgsRenderContext *context, QPainter *painter, const double top ) { QSizeF size( 0, 0 ); QModelIndex idx = mLegendModel->node2index( nodeLayer ); @@ -658,7 +682,7 @@ QSizeF QgsLegendRenderer::drawLayerTitleInternal( QgsLayerTreeLayer *nodeLayer, if ( mLegendModel->data( idx, Qt::DisplayRole ).toString().isEmpty() ) return size; - double y = point.y(); + double y = top; if ( context && context->painter() ) context->painter()->setPen( mSettings.layerFontColor() ); @@ -678,21 +702,32 @@ QSizeF QgsLegendRenderer::drawLayerTitleInternal( QgsLayerTreeLayer *nodeLayer, const QStringList lines = mSettings.evaluateItemText( mLegendModel->data( idx, Qt::DisplayRole ).toString(), context ? context->expressionContext() : tempContext ); + int i = 0; for ( QStringList::ConstIterator layerItemPart = lines.constBegin(); layerItemPart != lines.constEnd(); ++layerItemPart ) { y += mSettings.fontAscentMillimeters( layerFont ); - if ( context && context->painter() ) - mSettings.drawText( context->painter(), point.x(), y, *layerItemPart, layerFont ); - if ( painter ) - mSettings.drawText( painter, point.x(), y, *layerItemPart, layerFont ); + if ( QPainter *destPainter = context && context->painter() ? context->painter() : painter ) + { + double x = columnContext.left; + if ( mSettings.style( nodeLegendStyle( nodeLayer ) ).alignment() != Qt::AlignLeft ) + { + const double labelWidth = mSettings.textWidthMillimeters( layerFont, *layerItemPart ); + if ( mSettings.style( nodeLegendStyle( nodeLayer ) ).alignment() == Qt::AlignRight ) + x = columnContext.right - labelWidth; + else if ( mSettings.style( nodeLegendStyle( nodeLayer ) ).alignment() == Qt::AlignHCenter ) + x = columnContext.left + ( columnContext.right - columnContext.left - labelWidth ) / 2; + } + mSettings.drawText( destPainter, x, y, *layerItemPart, layerFont ); + } qreal width = mSettings.textWidthMillimeters( layerFont, *layerItemPart ); size.rwidth() = std::max( width, size.width() ); if ( layerItemPart != ( lines.end() - 1 ) ) { y += mSettings.lineSpacing(); } + i++; } - size.rheight() = y - point.y(); + size.rheight() = y - top; size.rheight() += mSettings.style( nodeLegendStyle( nodeLayer ) ).margin( QgsLegendStyle::Side::Bottom ); if ( layerScope ) @@ -701,17 +736,17 @@ QSizeF QgsLegendRenderer::drawLayerTitleInternal( QgsLayerTreeLayer *nodeLayer, return size; } -QSizeF QgsLegendRenderer::drawGroupTitle( QgsLayerTreeGroup *nodeGroup, QPainter *painter, QPointF point ) +QSizeF QgsLegendRenderer::drawGroupTitle( QgsLayerTreeGroup *nodeGroup, const ColumnContext &columnContext, QPainter *painter, double top ) { - return drawGroupTitleInternal( nodeGroup, nullptr, painter, point ); + return drawGroupTitleInternal( nodeGroup, columnContext, nullptr, painter, top ); } -QSizeF QgsLegendRenderer::drawGroupTitleInternal( QgsLayerTreeGroup *nodeGroup, QgsRenderContext *context, QPainter *painter, QPointF point ) +QSizeF QgsLegendRenderer::drawGroupTitleInternal( QgsLayerTreeGroup *nodeGroup, const ColumnContext &columnContext, QgsRenderContext *context, QPainter *painter, const double top ) { QSizeF size( 0, 0 ); QModelIndex idx = mLegendModel->node2index( nodeGroup ); - double y = point.y(); + double y = top; if ( context && context->painter() ) context->painter()->setPen( mSettings.fontColor() ); @@ -727,10 +762,20 @@ QSizeF QgsLegendRenderer::drawGroupTitleInternal( QgsLayerTreeGroup *nodeGroup, for ( QStringList::ConstIterator groupPart = lines.constBegin(); groupPart != lines.constEnd(); ++groupPart ) { y += mSettings.fontAscentMillimeters( groupFont ); - if ( context && context->painter() ) - mSettings.drawText( context->painter(), point.x(), y, *groupPart, groupFont ); - else if ( painter ) - mSettings.drawText( painter, point.x(), y, *groupPart, groupFont ); + + if ( QPainter *destPainter = context && context->painter() ? context->painter() : painter ) + { + double x = columnContext.left; + if ( mSettings.style( nodeLegendStyle( nodeGroup ) ).alignment() != Qt::AlignLeft ) + { + const double labelWidth = mSettings.textWidthMillimeters( groupFont, *groupPart ); + if ( mSettings.style( nodeLegendStyle( nodeGroup ) ).alignment() == Qt::AlignRight ) + x = columnContext.right - labelWidth; + else if ( mSettings.style( nodeLegendStyle( nodeGroup ) ).alignment() == Qt::AlignHCenter ) + x = columnContext.left + ( columnContext.right - columnContext.left - labelWidth ) / 2; + } + mSettings.drawText( destPainter, x, y, *groupPart, groupFont ); + } qreal width = mSettings.textWidthMillimeters( groupFont, *groupPart ); size.rwidth() = std::max( width, size.width() ); if ( groupPart != ( lines.end() - 1 ) ) @@ -738,7 +783,7 @@ QSizeF QgsLegendRenderer::drawGroupTitleInternal( QgsLayerTreeGroup *nodeGroup, y += mSettings.lineSpacing(); } } - size.rheight() = y - point.y(); + size.rheight() = y - top; return size; } @@ -794,29 +839,29 @@ void QgsLegendRenderer::setNodeLegendStyle( QgsLayerTreeNode *node, QgsLegendSty node->removeCustomProperty( QStringLiteral( "legend/title-style" ) ); } -QSizeF QgsLegendRenderer::drawTitle( QgsRenderContext *rendercontext, QPointF point, Qt::AlignmentFlag halignment, double legendWidth ) +QSizeF QgsLegendRenderer::drawTitle( QgsRenderContext *rendercontext, double top, Qt::AlignmentFlag halignment, double legendWidth ) { - return drawTitleInternal( rendercontext, nullptr, point, halignment, legendWidth ); + return drawTitleInternal( rendercontext, nullptr, top, halignment, legendWidth ); } -QSizeF QgsLegendRenderer::drawAtom( const Atom &atom, QgsRenderContext *rendercontext, QPointF point ) +QSizeF QgsLegendRenderer::drawGroup( const LegendComponentGroup &atom, QgsRenderContext *rendercontext, const ColumnContext &columnContext, double top ) { - return drawAtomInternal( atom, rendercontext, nullptr, point ); + return drawGroupInternal( atom, rendercontext, columnContext, nullptr, top ); } -QgsLegendRenderer::Nucleon QgsLegendRenderer::drawSymbolItem( QgsLayerTreeModelLegendNode *symbolItem, QgsRenderContext *rendercontext, QPointF point, double labelXOffset ) +QgsLegendRenderer::LegendComponent QgsLegendRenderer::drawSymbolItem( QgsLayerTreeModelLegendNode *symbolItem, QgsRenderContext *rendercontext, const ColumnContext &columnContext, double top, double maxSiblingSymbolWidth ) { - return drawSymbolItemInternal( symbolItem, rendercontext, nullptr, point, labelXOffset ); + return drawSymbolItemInternal( symbolItem, columnContext, rendercontext, nullptr, top, maxSiblingSymbolWidth ); } -QSizeF QgsLegendRenderer::drawLayerTitle( QgsLayerTreeLayer *nodeLayer, QgsRenderContext *rendercontext, QPointF point ) +QSizeF QgsLegendRenderer::drawLayerTitle( QgsLayerTreeLayer *nodeLayer, QgsRenderContext *rendercontext, const ColumnContext &columnContext, double top ) { - return drawLayerTitleInternal( nodeLayer, rendercontext, nullptr, point ); + return drawLayerTitleInternal( nodeLayer, columnContext, rendercontext, nullptr, top ); } -QSizeF QgsLegendRenderer::drawGroupTitle( QgsLayerTreeGroup *nodeGroup, QgsRenderContext *rendercontext, QPointF point ) +QSizeF QgsLegendRenderer::drawGroupTitle( QgsLayerTreeGroup *nodeGroup, QgsRenderContext *rendercontext, const ColumnContext &columnContext, double top ) { - return drawGroupTitleInternal( nodeGroup, rendercontext, nullptr, point ); + return drawGroupTitleInternal( nodeGroup, columnContext, rendercontext, nullptr, top ); } void QgsLegendRenderer::drawLegend( QgsRenderContext &context ) diff --git a/src/core/qgslegendrenderer.h b/src/core/qgslegendrenderer.h index 6b3934f8b42..bb6d81ab536 100644 --- a/src/core/qgslegendrenderer.h +++ b/src/core/qgslegendrenderer.h @@ -119,17 +119,17 @@ class CORE_EXPORT QgsLegendRenderer #ifndef SIP_RUN /** - * A legend Nucleon is either a group title, a layer title or a layer child item. + * A legend component is either a group title, a layer title or a layer child item. * - * E.g. a layer title nucleon is just the layer's title, it does not - * include all of that layer's subitems. Similarly, a group's title nucleon is just + * E.g. a layer title component is just the layer's title, it does not + * include all of that layer's subitems. Similarly, a group's title component is just * the group title, and does not include the actual content of that group. */ - class Nucleon + class LegendComponent { public: - //! Constructor for Nuclean - Nucleon() = default; + + LegendComponent() = default; QObject *item = nullptr; @@ -139,7 +139,7 @@ class CORE_EXPORT QgsLegendRenderer //! Label size, not including any preset padding space around the label QSizeF labelSize; - //! Nucleon size + //! Component size QSizeF size; /** @@ -149,12 +149,18 @@ class CORE_EXPORT QgsLegendRenderer * within the same legend column. */ double labelXOffset = 0.0; + + /** + * Largest symbol width, considering all other sibling components associated with + * this component. + */ + double maxSiblingSymbolWidth = 0.0; }; /** - * An Atom is indivisible set of legend Nucleons (i.e. it is indivisible into more columns). + * An component group is an indivisible set of legend components (i.e. it is indivisible into more columns). * - * An Atom may consist of one or more Nucleon(s), depending on the layer splitting mode: + * A group may consist of one or more component(s), depending on the layer splitting mode: * * 1) no layer split: [group_title ...] layer_title layer_item [layer_item ...] * 2) layer split: [group_title ...] layer_title layer_item @@ -165,20 +171,38 @@ class CORE_EXPORT QgsLegendRenderer * and it would not be logical to leave a group or layer title at the bottom of a column, * separated from the actual content of that group or layer. */ - class Atom + class LegendComponentGroup { public: - //! List of child Nucleons belonging to this Atom. - QList nucleons; + //! List of child components belonging to this group. + QList components; - //! Atom size, including internal spacing between Nucleons, but excluding any padding space around the Atom itself. + //! Group size, including internal spacing between components, but excluding any padding space around the group itself. QSizeF size = QSizeF( 0, 0 ); //! Corresponding column index int column = 0; }; + /** + * Contains contextual information about the current column being rendered + */ + class ColumnContext + { + public: + + ColumnContext() + : left( 0 ) + , right( 0 ) + {} + + //! Left edge of column + double left = 0; + //! Right edge of column + double right = 0; + }; + /** * Draws the legend and returns the actual size of the legend. * @@ -188,15 +212,15 @@ class CORE_EXPORT QgsLegendRenderer QSizeF paintAndDetermineSize( QPainter *painter = nullptr ); /** - * Returns a list of Atoms for the specified \a parentGroup, respecting the current layer's + * Returns a list of component groups for the specified \a parentGroup, respecting the current layer's * splitting settings. */ - QList createAtomList( QgsLayerTreeGroup *parentGroup, bool splitLayer ); + QList createComponentGroupList( QgsLayerTreeGroup *parentGroup, bool splitLayer ); /** - * Divides a list of Atoms into columns, and sets the column index for each atom in the list. + * Divides a list of component groups into columns, and sets the column index for each group in the list. */ - void setColumns( QList &atomList ); + void setColumns( QList &groupList ); /** * Draws a title in the legend using the title font and the specified alignment settings. @@ -205,25 +229,25 @@ class CORE_EXPORT QgsLegendRenderer * * If \a painter is NULLPTR, no painting will be attempted, but the required size will still be calculated and returned. */ - QSizeF drawTitle( QPainter *painter = nullptr, QPointF point = QPointF(), Qt::AlignmentFlag halignment = Qt::AlignLeft, double legendWidth = 0 ); + QSizeF drawTitle( QPainter *painter = nullptr, double top = 0, Qt::AlignmentFlag halignment = Qt::AlignLeft, double legendWidth = 0 ); /** - * Returns the calculated padding space required above the given \a atom. + * Returns the calculated padding space required above the given component \a group. */ - double spaceAboveAtom( const Atom &atom ); + double spaceAboveGroup( const LegendComponentGroup &group ); /** - * Draws an \a atom and return its actual size. + * Draws a component \a group and return its actual size. * - * The \a atom is drawn with the space above it, so that the first atoms in column are all + * The \a group is drawn with the space above it, so that the first groups in a column are all * aligned to the same line regardless of their style top spacing. */ - QSizeF drawAtom( const Atom &atom, QPainter *painter = nullptr, QPointF point = QPointF() ); + QSizeF drawGroup( const LegendComponentGroup &group, const ColumnContext &columnContext, QPainter *painter = nullptr, double top = 0 ); /** * Draws the symbol of a given symbol QgsLayerTreeModelLegendNode. */ - Nucleon drawSymbolItem( QgsLayerTreeModelLegendNode *symbolItem, QPainter *painter = nullptr, QPointF point = QPointF(), double labelXOffset = 0 ); + LegendComponent drawSymbolItem( QgsLayerTreeModelLegendNode *symbolItem, const ColumnContext &columnContext = ColumnContext(), QPainter *painter = nullptr, double top = 0, double maxSiblingSymbolWidth = 0 ); /** * Draws the title of a layer, given a QgsLayerTreeLayer, and a destination \a painter. @@ -232,13 +256,13 @@ class CORE_EXPORT QgsLegendRenderer * * The \a painter may be NULLPTR, in which case on the size is calculated and no painting is attempted. */ - QSizeF drawLayerTitle( QgsLayerTreeLayer *nodeLayer, QPainter *painter = nullptr, QPointF point = QPointF() ); + QSizeF drawLayerTitle( QgsLayerTreeLayer *nodeLayer, const ColumnContext &columnContext = ColumnContext(), QPainter *painter = nullptr, double top = 0 ); /** * Draws a group item. * Returns the size of the title. */ - QSizeF drawGroupTitle( QgsLayerTreeGroup *nodeGroup, QPainter *painter = nullptr, QPointF point = QPointF() ); + QSizeF drawGroupTitle( QgsLayerTreeGroup *nodeGroup, const ColumnContext &columnContext = ColumnContext(), QPainter *painter = nullptr, double top = 0 ); /** * Renders a group item in a \a json object. @@ -262,22 +286,22 @@ class CORE_EXPORT QgsLegendRenderer * * If \a context is NULLPTR, no painting will be attempted, but the required size will still be calculated and returned. */ - QSizeF drawTitle( QgsRenderContext *context, QPointF point = QPointF(), Qt::AlignmentFlag halignment = Qt::AlignLeft, double legendWidth = 0 ); + QSizeF drawTitle( QgsRenderContext *context, double top, Qt::AlignmentFlag halignment = Qt::AlignLeft, double legendWidth = 0 ); /** - * Draws an \a atom and return its actual size, using the specified render \a context. + * Draws an \a group and return its actual size, using the specified render \a context. * - * The \a atom is drawn with the space above it, so that the first atoms in column are all + * The \a group is drawn with the space above it, so that the first groups in a column are all * aligned to the same line regardless of their style top spacing. * * If \a context is NULLPTR, no painting will be attempted, but the required size will still be calculated and returned. */ - QSizeF drawAtom( const Atom &atom, QgsRenderContext *rendercontext, QPointF point = QPointF() ); + QSizeF drawGroup( const LegendComponentGroup &group, QgsRenderContext *rendercontext, const ColumnContext &columnContext, double top = 0 ); /** * Draws the symbol of a given symbol QgsLayerTreeModelLegendNode, using the specified render \a context. */ - Nucleon drawSymbolItem( QgsLayerTreeModelLegendNode *symbolItem, QgsRenderContext *context, QPointF point = QPointF(), double labelXOffset = 0 ); + LegendComponent drawSymbolItem( QgsLayerTreeModelLegendNode *symbolItem, QgsRenderContext *context, const ColumnContext &columnContext, double top, double maxSiblingSymbolWidth = 0 ); /** * Draws the title of a layer, given a QgsLayerTreeLayer, and a destination render \a context. @@ -286,21 +310,20 @@ class CORE_EXPORT QgsLegendRenderer * * The \a context may be NULLPTR, in which case on the size is calculated and no painting is attempted. */ - QSizeF drawLayerTitle( QgsLayerTreeLayer *nodeLayer, QgsRenderContext *context, QPointF point = QPointF() ); + QSizeF drawLayerTitle( QgsLayerTreeLayer *nodeLayer, QgsRenderContext *context, const ColumnContext &columnContext, double top = 0 ); /** * Draws a group's title, using the specified render \a context. * * Returns the size of the title. */ - QSizeF drawGroupTitle( QgsLayerTreeGroup *nodeGroup, QgsRenderContext *context, QPointF point = QPointF() ); + QSizeF drawGroupTitle( QgsLayerTreeGroup *nodeGroup, QgsRenderContext *context, const ColumnContext &columnContext = ColumnContext(), double top = 0 ); /** * Returns the style of the given \a node. */ QgsLegendStyle::Style nodeLegendStyle( QgsLayerTreeNode *node ); - private: QgsLayerTreeModel *mLegendModel = nullptr; QgsLegendSettings mSettings; @@ -308,12 +331,14 @@ class CORE_EXPORT QgsLegendRenderer QSizeF mLegendSize; #endif - QSizeF drawTitleInternal( QgsRenderContext *context, QPainter *painter, QPointF point, Qt::AlignmentFlag halignment, double legendWidth ); - QSizeF drawAtomInternal( const Atom &atom, QgsRenderContext *context, QPainter *painter, QPointF point ); - QgsLegendRenderer::Nucleon drawSymbolItemInternal( QgsLayerTreeModelLegendNode *symbolItem, QgsRenderContext *context, QPainter *painter, QPointF point, double labelXOffset ); - QSizeF drawLayerTitleInternal( QgsLayerTreeLayer *nodeLayer, QgsRenderContext *context, QPainter *painter, QPointF point ); - QSizeF drawGroupTitleInternal( QgsLayerTreeGroup *nodeGroup, QgsRenderContext *context, QPainter *painter, QPointF point ); + QSizeF drawTitleInternal( QgsRenderContext *context, QPainter *painter, double top, Qt::AlignmentFlag halignment, double legendWidth ); + QSizeF drawGroupInternal( const LegendComponentGroup &group, QgsRenderContext *context, const ColumnContext &columnContext, QPainter *painter, double top ); + QgsLegendRenderer::LegendComponent drawSymbolItemInternal( QgsLayerTreeModelLegendNode *symbolItem, const ColumnContext &columnContext, QgsRenderContext *context, QPainter *painter, double top, double maxSiblingSymbolWidth ); + QSizeF drawLayerTitleInternal( QgsLayerTreeLayer *nodeLayer, const ColumnContext &columnContext, QgsRenderContext *context, QPainter *painter, double top ); + QSizeF drawGroupTitleInternal( QgsLayerTreeGroup *nodeGroup, const ColumnContext &columnContext, QgsRenderContext *context, QPainter *painter, double top ); QSizeF paintAndDetermineSizeInternal( QgsRenderContext *context, QPainter *painter ); + + void widthAndOffsetForTitleText( const Qt::AlignmentFlag halignment, double legendWidth, double &width, double &offset ); }; #endif // QGSLEGENDRENDERER_H diff --git a/src/core/qgslegendsettings.h b/src/core/qgslegendsettings.h index e39f2a326cd..8b578a85df4 100644 --- a/src/core/qgslegendsettings.h +++ b/src/core/qgslegendsettings.h @@ -44,16 +44,14 @@ class CORE_EXPORT QgsLegendSettings QString title() const { return mTitle; } /** - * Returns the alignment of the legend title - * \returns Qt::AlignmentFlag for the legend title - * \see setTitleAlignment + * Returns the alignment of the legend title. + * \see setTitleAlignment() */ Qt::AlignmentFlag titleAlignment() const { return mTitleAlignment; } /** - * Sets the alignment of the legend title - * \param alignment Text alignment for drawing the legend title - * \see titleAlignment + * Sets the \a alignment of the legend title. + * \see titleAlignment() */ void setTitleAlignment( Qt::AlignmentFlag alignment ) { mTitleAlignment = alignment; } @@ -109,6 +107,26 @@ class CORE_EXPORT QgsLegendSettings QSizeF symbolSize() const {return mSymbolSize;} void setSymbolSize( QSizeF s ) {mSymbolSize = s;} + /** + * Sets the \a alignment for placement of legend symbols. + * + * Only Qt::AlignLeft or Qt::AlignRight are supported values. + * + * \see symbolAlignment() + * \since QGIS 3.10.0 + */ + void setSymbolAlignment( Qt::AlignmentFlag alignment ) { mSymbolAlignment = alignment; } + + /** + * Returns the alignment for placement of legend symbols. + * + * Only Qt::AlignLeft or Qt::AlignRight are supported values. + * + * \see setSymbolAlignment() + * \since QGIS 3.10.0 + */ + Qt::AlignmentFlag symbolAlignment() const { return mSymbolAlignment; } + /** * Returns whether a stroke will be drawn around raster symbol items. * \see setDrawRasterStroke() @@ -327,6 +345,9 @@ class CORE_EXPORT QgsLegendSettings //! Font color for layers, overrides font color QColor mLayerFontColor; + + //! Symbol alignment + Qt::AlignmentFlag mSymbolAlignment = Qt::AlignLeft; }; diff --git a/src/core/qgslegendstyle.cpp b/src/core/qgslegendstyle.cpp index b844d4d4c0a..3d4c5d94bb3 100644 --- a/src/core/qgslegendstyle.cpp +++ b/src/core/qgslegendstyle.cpp @@ -48,16 +48,22 @@ void QgsLegendStyle::setMargin( double margin ) void QgsLegendStyle::writeXml( const QString &name, QDomElement &elem, QDomDocument &doc ) const { - if ( elem.isNull() ) return; + if ( elem.isNull() ) + return; QDomElement styleElem = doc.createElement( QStringLiteral( "style" ) ); styleElem.setAttribute( QStringLiteral( "name" ), name ); + styleElem.setAttribute( QStringLiteral( "alignment" ), QString::number( mAlignment ) ); - if ( !qgsDoubleNear( mMarginMap[Top], 0.0 ) ) styleElem.setAttribute( QStringLiteral( "marginTop" ), QString::number( mMarginMap[Top] ) ); - if ( !qgsDoubleNear( mMarginMap[Bottom], 0.0 ) ) styleElem.setAttribute( QStringLiteral( "marginBottom" ), QString::number( mMarginMap[Bottom] ) ); - if ( !qgsDoubleNear( mMarginMap[Left], 0.0 ) ) styleElem.setAttribute( QStringLiteral( "marginLeft" ), QString::number( mMarginMap[Left] ) ); - if ( !qgsDoubleNear( mMarginMap[Right], 0.0 ) ) styleElem.setAttribute( QStringLiteral( "marginRight" ), QString::number( mMarginMap[Right] ) ); + if ( !qgsDoubleNear( mMarginMap[Top], 0.0 ) ) + styleElem.setAttribute( QStringLiteral( "marginTop" ), QString::number( mMarginMap[Top] ) ); + if ( !qgsDoubleNear( mMarginMap[Bottom], 0.0 ) ) + styleElem.setAttribute( QStringLiteral( "marginBottom" ), QString::number( mMarginMap[Bottom] ) ); + if ( !qgsDoubleNear( mMarginMap[Left], 0.0 ) ) + styleElem.setAttribute( QStringLiteral( "marginLeft" ), QString::number( mMarginMap[Left] ) ); + if ( !qgsDoubleNear( mMarginMap[Right], 0.0 ) ) + styleElem.setAttribute( QStringLiteral( "marginRight" ), QString::number( mMarginMap[Right] ) ); styleElem.appendChild( QgsFontUtils::toXmlElement( mFont, doc, QStringLiteral( "styleFont" ) ) ); @@ -78,6 +84,8 @@ void QgsLegendStyle::readXml( const QDomElement &elem, const QDomDocument &doc ) mMarginMap[Bottom] = elem.attribute( QStringLiteral( "marginBottom" ), QStringLiteral( "0" ) ).toDouble(); mMarginMap[Left] = elem.attribute( QStringLiteral( "marginLeft" ), QStringLiteral( "0" ) ).toDouble(); mMarginMap[Right] = elem.attribute( QStringLiteral( "marginRight" ), QStringLiteral( "0" ) ).toDouble(); + + mAlignment = static_cast< Qt::Alignment >( elem.attribute( QStringLiteral( "alignment" ), QString::number( Qt::AlignLeft ) ).toInt() ); } QString QgsLegendStyle::styleName( Style s ) diff --git a/src/core/qgslegendstyle.h b/src/core/qgslegendstyle.h index 853f8265043..7758c17ae45 100644 --- a/src/core/qgslegendstyle.h +++ b/src/core/qgslegendstyle.h @@ -80,6 +80,22 @@ class CORE_EXPORT QgsLegendStyle //! Sets all margins void setMargin( double margin ); + /** + * Returns the alignment for the legend component. + * + * \see setAlignment() + * \since QGIS 3.10 + */ + Qt::Alignment alignment() const { return mAlignment; } + + /** + * Sets the alignment for the legend component. + * + * \see alignment() + * \since QGIS 3.10 + */ + void setAlignment( Qt::Alignment alignment ) { mAlignment = alignment; } + void writeXml( const QString &name, QDomElement &elem, QDomDocument &doc ) const; void readXml( const QDomElement &elem, const QDomDocument &doc ); @@ -97,6 +113,8 @@ class CORE_EXPORT QgsLegendStyle QFont mFont; //! Space around element QMap mMarginMap; + + Qt::Alignment mAlignment = Qt::AlignLeft; }; #endif diff --git a/src/gui/qgsalignmentcombobox.cpp b/src/gui/qgsalignmentcombobox.cpp index 17dde47c50f..35a582ba76d 100644 --- a/src/gui/qgsalignmentcombobox.cpp +++ b/src/gui/qgsalignmentcombobox.cpp @@ -48,6 +48,18 @@ void QgsAlignmentComboBox::setCurrentAlignment( Qt::Alignment alignment ) setCurrentIndex( index ); } +void QgsAlignmentComboBox::customiseAlignmentDisplay( Qt::Alignment alignment, const QString &text, const QIcon &icon ) +{ + const int index = findData( QVariant( alignment ) ); + if ( index >= 0 ) + { + if ( !text.isEmpty() ) + setItemText( index, text ); + if ( !icon.isNull() ) + setItemIcon( index, icon ); + } +} + void QgsAlignmentComboBox::populate() { Qt::Alignment prevAlign = currentAlignment(); diff --git a/src/gui/qgsalignmentcombobox.h b/src/gui/qgsalignmentcombobox.h index 91d0638ebcc..d66e63bf7c7 100644 --- a/src/gui/qgsalignmentcombobox.h +++ b/src/gui/qgsalignmentcombobox.h @@ -61,6 +61,16 @@ class GUI_EXPORT QgsAlignmentComboBox : public QComboBox */ void setCurrentAlignment( Qt::Alignment alignment ); + /** + * Sets the \a text and \a icon to use for a particular \a alignment option, + * replacing the default text or icon. + * + * If \a text or \a icon is not specified, they will not be changed from the default. + * + * \note This must be called after first filtering the available alignment options via setAvailableAlignments(). + */ + void customiseAlignmentDisplay( Qt::Alignment alignment, const QString &text = QString(), const QIcon &icon = QIcon() ); + signals: /** diff --git a/src/server/services/wms/qgswmsrenderer.cpp b/src/server/services/wms/qgswmsrenderer.cpp index 76633b2783d..bf7d7ca6369 100644 --- a/src/server/services/wms/qgswmsrenderer.cpp +++ b/src/server/services/wms/qgswmsrenderer.cpp @@ -167,8 +167,6 @@ namespace QgsWms QgsLegendSettings settings = legendSettings(); QgsLayerTreeModelLegendNode::ItemContext ctx; ctx.painter = painter.get(); - ctx.labelXOffset = 0; - ctx.point = QPointF(); nodeModel.drawSymbol( settings, &ctx, size.height() / dpmm ); painter->end(); diff --git a/src/ui/layout/qgslayoutlegendwidgetbase.ui b/src/ui/layout/qgslayoutlegendwidgetbase.ui index 1b0bbbf8197..f9cec60afec 100644 --- a/src/ui/layout/qgslayoutlegendwidgetbase.ui +++ b/src/ui/layout/qgslayoutlegendwidgetbase.ui @@ -6,7 +6,7 @@ 0 0 - 393 + 323 995 @@ -64,8 +64,8 @@ 0 0 - 377 - 1662 + 312 + 1459 @@ -84,6 +84,44 @@ false + + + + + + + Wrap text on + + + + + + + true + + + + + + + + + + + + + + Resize to fit contents + + + + + + + Map + + + @@ -94,70 +132,16 @@ - - - - Title alignment - - - - - - - Map - - - - - - - Resize to fit contents - - - - - - - Wrap text on - - - - - - - - - - - - - - - - - Left - - - - - Center - - - - - Right - - - - - - - - true + + + + + + Arrangement @@ -255,7 +239,7 @@ - + :/images/themes/default/mActionArrowDown.svg:/images/themes/default/mActionArrowDown.svg @@ -272,7 +256,7 @@ - + :/images/themes/default/mActionArrowUp.svg:/images/themes/default/mActionArrowUp.svg @@ -292,7 +276,7 @@ - + :/images/themes/default/mActionAddGroup.svg:/images/themes/default/mActionAddGroup.svg @@ -309,7 +293,7 @@ - + :/images/themes/default/symbologyAdd.svg:/images/themes/default/symbologyAdd.svg @@ -326,7 +310,7 @@ - + :/images/themes/default/symbologyRemove.svg:/images/themes/default/symbologyRemove.svg @@ -343,7 +327,7 @@ - + :/images/themes/default/symbologyEdit.svg:/images/themes/default/symbologyEdit.svg @@ -366,7 +350,7 @@ - + :/images/themes/default/mActionSum.svg:/images/themes/default/mActionSum.svg @@ -386,7 +370,7 @@ Filter Legend by Map Content - + :/images/themes/default/mActionFilter2.svg:/images/themes/default/mActionFilter2.svg @@ -406,7 +390,7 @@ - + :/images/themes/default/mIconExpressionFilter.svg:/images/themes/default/mIconExpressionFilter.svg @@ -451,7 +435,7 @@ Qt::StrongFocus - Fonts + Fonts and Text Formatting composeritem @@ -459,34 +443,32 @@ true - - - - - - 0 - 0 - - + + + - Title font + <b>Group Headings</b> - - - - - 0 - 0 - - + + - Group font + Font - + + + + + + + <b>Subgroup Headings</b> + + + + @@ -499,7 +481,10 @@ - + + + + @@ -512,7 +497,41 @@ - + + + + <b>Item Labels</b> + + + + + + + Alignment + + + + + + + + 0 + 0 + + + + Title font + + + + + + + Alignment + + + + @@ -555,6 +574,67 @@ + + + + Font + + + + + + + Alignment + + + + + + + Font + + + + + + + Font + + + + + + + <b>Legend Title</b> + + + + + + + + + + + 0 + 0 + + + + Group font + + + + + + + + + + Alignment + + + @@ -1102,15 +1182,20 @@ QToolButton
qgslegendfilterbutton.h
+ + QgsAlignmentComboBox + QComboBox +
qgsalignmentcombobox.h
+
scrollArea mMainPropertiesColGroupBox mTitleLineEdit mLegendTitleDDBtn - mTitleAlignCombo mMapComboBox mWrapCharLineEdit + mArrangementCombo mCheckboxResizeContents mLegendItemColGroupBox mCheckBoxAutoUpdate @@ -1128,13 +1213,17 @@ mFilterLegendByAtlasCheckBox mFontsColGroupBox mTitleFontButton + mTitleAlignCombo mGroupFontButton + mGroupAlignCombo mLayerFontButton + mSubgroupAlignCombo mItemFontButton + mItemAlignCombo mFontColorButton mColumnsColGroupBox - mColumnsDDBtn mColumnCountSpinBox + mColumnsDDBtn mEqualColumnWidthCheckBox mSplitLayerCheckBox mSymbolsColGroupBox @@ -1156,35 +1245,6 @@ mColumnSpaceSpinBox mLineSpacingSpinBox - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + diff --git a/tests/src/core/testqgslegendrenderer.cpp b/tests/src/core/testqgslegendrenderer.cpp index e0f8d31ccc5..e8c6df29fd7 100644 --- a/tests/src/core/testqgslegendrenderer.cpp +++ b/tests/src/core/testqgslegendrenderer.cpp @@ -124,6 +124,13 @@ class TestQgsLegendRenderer : public QObject void testBasic(); void testBigMarker(); + + void testRightAlignText(); + void testCenterAlignText(); + void testLeftAlignTextRightAlignSymbol(); + void testCenterAlignTextRightAlignSymbol(); + void testRightAlignTextRightAlignSymbol(); + void testMapUnits(); void testTallSymbol(); void testLineSpacing(); @@ -321,6 +328,124 @@ void TestQgsLegendRenderer::testBigMarker() QVERIFY( _verifyImage( testName, mReport ) ); } +void TestQgsLegendRenderer::testCenterAlignText() +{ + QgsMarkerSymbol *sym = new QgsMarkerSymbol(); + sym->setColor( Qt::red ); + sym->setSize( sym->size() * 6 ); + QgsCategorizedSymbolRenderer *catRenderer = dynamic_cast( mVL3->renderer() ); + QVERIFY( catRenderer ); + catRenderer->updateCategorySymbol( 0, sym ); + + QgsLayerTreeModel legendModel( mRoot ); + QgsLegendSettings settings; + settings.rstyle( QgsLegendStyle::Group ).setAlignment( Qt::AlignHCenter ); + settings.rstyle( QgsLegendStyle::Subgroup ).setAlignment( Qt::AlignHCenter ); + settings.rstyle( QgsLegendStyle::SymbolLabel ).setAlignment( Qt::AlignHCenter ); + _setStandardTestFont( settings ); + _renderLegend( QStringLiteral( "legend_center_align_text" ), &legendModel, settings ); + QVERIFY( _verifyImage( QStringLiteral( "legend_center_align_text" ), mReport ) ); + + settings.setColumnCount( 2 ); + _renderLegend( QStringLiteral( "legend_two_cols_center_align_text" ), &legendModel, settings ); + QVERIFY( _verifyImage( QStringLiteral( "legend_two_cols_center_align_text" ), mReport ) ); +} + +void TestQgsLegendRenderer::testLeftAlignTextRightAlignSymbol() +{ + QgsMarkerSymbol *sym = new QgsMarkerSymbol(); + sym->setColor( Qt::red ); + sym->setSize( sym->size() * 6 ); + QgsCategorizedSymbolRenderer *catRenderer = dynamic_cast( mVL3->renderer() ); + QVERIFY( catRenderer ); + catRenderer->updateCategorySymbol( 0, sym ); + + QgsLayerTreeModel legendModel( mRoot ); + QgsLegendSettings settings; + settings.rstyle( QgsLegendStyle::Group ).setAlignment( Qt::AlignLeft ); + settings.rstyle( QgsLegendStyle::Subgroup ).setAlignment( Qt::AlignLeft ); + settings.rstyle( QgsLegendStyle::SymbolLabel ).setAlignment( Qt::AlignLeft ); + settings.setSymbolAlignment( Qt::AlignRight ); + _setStandardTestFont( settings ); + _renderLegend( QStringLiteral( "legend_right_symbol_left_align_text" ), &legendModel, settings ); + QVERIFY( _verifyImage( QStringLiteral( "legend_right_symbol_left_align_text" ), mReport ) ); + + settings.setColumnCount( 2 ); + _renderLegend( QStringLiteral( "legend_two_cols_right_align_symbol_left_align_text" ), &legendModel, settings ); + QVERIFY( _verifyImage( QStringLiteral( "legend_two_cols_right_align_symbol_left_align_text" ), mReport ) ); +} + +void TestQgsLegendRenderer::testCenterAlignTextRightAlignSymbol() +{ + QgsMarkerSymbol *sym = new QgsMarkerSymbol(); + sym->setColor( Qt::red ); + sym->setSize( sym->size() * 6 ); + QgsCategorizedSymbolRenderer *catRenderer = dynamic_cast( mVL3->renderer() ); + QVERIFY( catRenderer ); + catRenderer->updateCategorySymbol( 0, sym ); + + QgsLayerTreeModel legendModel( mRoot ); + QgsLegendSettings settings; + settings.rstyle( QgsLegendStyle::Group ).setAlignment( Qt::AlignHCenter ); + settings.rstyle( QgsLegendStyle::Subgroup ).setAlignment( Qt::AlignHCenter ); + settings.rstyle( QgsLegendStyle::SymbolLabel ).setAlignment( Qt::AlignHCenter ); + settings.setSymbolAlignment( Qt::AlignRight ); + _setStandardTestFont( settings ); + _renderLegend( QStringLiteral( "legend_right_symbol_center_align_text" ), &legendModel, settings ); + QVERIFY( _verifyImage( QStringLiteral( "legend_right_symbol_center_align_text" ), mReport ) ); + + settings.setColumnCount( 2 ); + _renderLegend( QStringLiteral( "legend_two_cols_right_align_symbol_center_align_text" ), &legendModel, settings ); + QVERIFY( _verifyImage( QStringLiteral( "legend_two_cols_right_align_symbol_center_align_text" ), mReport ) ); +} + +void TestQgsLegendRenderer::testRightAlignTextRightAlignSymbol() +{ + QgsMarkerSymbol *sym = new QgsMarkerSymbol(); + sym->setColor( Qt::red ); + sym->setSize( sym->size() * 6 ); + QgsCategorizedSymbolRenderer *catRenderer = dynamic_cast( mVL3->renderer() ); + QVERIFY( catRenderer ); + catRenderer->updateCategorySymbol( 0, sym ); + + QgsLayerTreeModel legendModel( mRoot ); + QgsLegendSettings settings; + settings.rstyle( QgsLegendStyle::Group ).setAlignment( Qt::AlignRight ); + settings.rstyle( QgsLegendStyle::Subgroup ).setAlignment( Qt::AlignRight ); + settings.rstyle( QgsLegendStyle::SymbolLabel ).setAlignment( Qt::AlignRight ); + settings.setSymbolAlignment( Qt::AlignRight ); + _setStandardTestFont( settings ); + _renderLegend( QStringLiteral( "legend_right_symbol_right_align_text" ), &legendModel, settings ); + QVERIFY( _verifyImage( QStringLiteral( "legend_right_symbol_right_align_text" ), mReport ) ); + + settings.setColumnCount( 2 ); + _renderLegend( QStringLiteral( "legend_two_cols_right_align_symbol_right_align_text" ), &legendModel, settings ); + QVERIFY( _verifyImage( QStringLiteral( "legend_two_cols_right_align_symbol_right_align_text" ), mReport ) ); +} + +void TestQgsLegendRenderer::testRightAlignText() +{ + QgsMarkerSymbol *sym = new QgsMarkerSymbol(); + sym->setColor( Qt::red ); + sym->setSize( sym->size() * 6 ); + QgsCategorizedSymbolRenderer *catRenderer = dynamic_cast( mVL3->renderer() ); + QVERIFY( catRenderer ); + catRenderer->updateCategorySymbol( 0, sym ); + + QgsLayerTreeModel legendModel( mRoot ); + QgsLegendSettings settings; + settings.rstyle( QgsLegendStyle::Group ).setAlignment( Qt::AlignRight ); + settings.rstyle( QgsLegendStyle::Subgroup ).setAlignment( Qt::AlignRight ); + settings.rstyle( QgsLegendStyle::SymbolLabel ).setAlignment( Qt::AlignRight ); + _setStandardTestFont( settings ); + _renderLegend( QStringLiteral( "legend_right_align_text" ), &legendModel, settings ); + QVERIFY( _verifyImage( QStringLiteral( "legend_right_align_text" ), mReport ) ); + + settings.setColumnCount( 2 ); + _renderLegend( QStringLiteral( "legend_two_cols_right_align_text" ), &legendModel, settings ); + QVERIFY( _verifyImage( QStringLiteral( "legend_two_cols_right_align_text" ), mReport ) ); +} + void TestQgsLegendRenderer::testMapUnits() { QString testName = QStringLiteral( "legend_mapunits" ); diff --git a/tests/src/python/test_qgslayoutlegend.py b/tests/src/python/test_qgslayoutlegend.py index 47f77f2e293..21bd4441a86 100644 --- a/tests/src/python/test_qgslayoutlegend.py +++ b/tests/src/python/test_qgslayoutlegend.py @@ -10,7 +10,7 @@ __author__ = '(C) 2017 by Nyall Dawson' __date__ = '24/10/2017' __copyright__ = 'Copyright 2017, The QGIS Project' -from qgis.PyQt.QtCore import QRectF +from qgis.PyQt.QtCore import QRectF, QDir from qgis.PyQt.QtGui import QColor from qgis.core import (QgsPrintLayout, @@ -52,8 +52,17 @@ class TestQgsLayoutItemLegend(unittest.TestCase, LayoutItemTestCase): def setUpClass(cls): cls.item_class = QgsLayoutItemLegend + def setUp(self): + self.report = "

Python QgsLayoutItemLegend Tests

\n" + + def tearDown(self): + report_file_path = "%s/qgistest.html" % QDir.tempPath() + with open(report_file_path, 'a') as report_file: + report_file.write(self.report) + def testInitialSizeSymbolMapUnits(self): """Test initial size of legend with a symbol size in map units""" + QgsProject.instance().removeAllMapLayers() point_path = os.path.join(TEST_DATA_DIR, 'points.shp') point_layer = QgsVectorLayer(point_path, 'points', 'ogr') @@ -89,6 +98,7 @@ class TestQgsLayoutItemLegend(unittest.TestCase, LayoutItemTestCase): 'composer_legend_mapunits', layout) checker.setControlPathPrefix("composer_legend") result, message = checker.testLayout() + self.report += checker.report() self.assertTrue(result, message) # resize with non-top-left reference point @@ -147,6 +157,7 @@ class TestQgsLayoutItemLegend(unittest.TestCase, LayoutItemTestCase): 'composer_legend_size_content', layout) checker.setControlPathPrefix("composer_legend") result, message = checker.testLayout() + self.report += checker.report() self.assertTrue(result, message) QgsProject.instance().removeMapLayers([point_layer.id()]) @@ -191,6 +202,7 @@ class TestQgsLayoutItemLegend(unittest.TestCase, LayoutItemTestCase): 'composer_legend_noresize', layout) checker.setControlPathPrefix("composer_legend") result, message = checker.testLayout() + self.report += checker.report() self.assertTrue(result, message) QgsProject.instance().removeMapLayers([point_layer.id()]) @@ -235,6 +247,7 @@ class TestQgsLayoutItemLegend(unittest.TestCase, LayoutItemTestCase): 'composer_legend_noresize_crop', layout) checker.setControlPathPrefix("composer_legend") result, message = checker.testLayout() + self.report += checker.report() self.assertTrue(result, message) QgsProject.instance().removeMapLayers([point_layer.id()]) @@ -360,6 +373,7 @@ class TestQgsLayoutItemLegend(unittest.TestCase, LayoutItemTestCase): 'composer_legend_expressions', layout) checker.setControlPathPrefix("composer_legend") result, message = checker.testLayout() + self.report += checker.report() self.assertTrue(result, message) QgsProject.instance().removeMapLayers([point_layer.id()]) diff --git a/tests/testdata/control_images/legend/expected_legend_center_align_text/expected_legend_center_align_text.png b/tests/testdata/control_images/legend/expected_legend_center_align_text/expected_legend_center_align_text.png new file mode 100644 index 00000000000..628426cdcaa Binary files /dev/null and b/tests/testdata/control_images/legend/expected_legend_center_align_text/expected_legend_center_align_text.png differ diff --git a/tests/testdata/control_images/legend/expected_legend_right_align_text/expected_legend_right_align_text.png b/tests/testdata/control_images/legend/expected_legend_right_align_text/expected_legend_right_align_text.png new file mode 100644 index 00000000000..0f8f617dc22 Binary files /dev/null and b/tests/testdata/control_images/legend/expected_legend_right_align_text/expected_legend_right_align_text.png differ diff --git a/tests/testdata/control_images/legend/expected_legend_right_symbol_center_align_text/expected_legend_right_symbol_center_align_text.png b/tests/testdata/control_images/legend/expected_legend_right_symbol_center_align_text/expected_legend_right_symbol_center_align_text.png new file mode 100644 index 00000000000..fdec437db75 Binary files /dev/null and b/tests/testdata/control_images/legend/expected_legend_right_symbol_center_align_text/expected_legend_right_symbol_center_align_text.png differ diff --git a/tests/testdata/control_images/legend/expected_legend_right_symbol_left_align_text/expected_legend_right_symbol_left_align_text.png b/tests/testdata/control_images/legend/expected_legend_right_symbol_left_align_text/expected_legend_right_symbol_left_align_text.png new file mode 100644 index 00000000000..79104a369bf Binary files /dev/null and b/tests/testdata/control_images/legend/expected_legend_right_symbol_left_align_text/expected_legend_right_symbol_left_align_text.png differ diff --git a/tests/testdata/control_images/legend/expected_legend_right_symbol_right_align_text/expected_legend_right_symbol_right_align_text.png b/tests/testdata/control_images/legend/expected_legend_right_symbol_right_align_text/expected_legend_right_symbol_right_align_text.png new file mode 100644 index 00000000000..14177fb2a94 Binary files /dev/null and b/tests/testdata/control_images/legend/expected_legend_right_symbol_right_align_text/expected_legend_right_symbol_right_align_text.png differ diff --git a/tests/testdata/control_images/legend/expected_legend_two_cols_center_align_text/expected_legend_two_cols_center_align_text.png b/tests/testdata/control_images/legend/expected_legend_two_cols_center_align_text/expected_legend_two_cols_center_align_text.png new file mode 100644 index 00000000000..f5dec9d3543 Binary files /dev/null and b/tests/testdata/control_images/legend/expected_legend_two_cols_center_align_text/expected_legend_two_cols_center_align_text.png differ diff --git a/tests/testdata/control_images/legend/expected_legend_two_cols_right_align_symbol_center_align_text/expected_legend_two_cols_right_align_symbol_center_align_text.png b/tests/testdata/control_images/legend/expected_legend_two_cols_right_align_symbol_center_align_text/expected_legend_two_cols_right_align_symbol_center_align_text.png new file mode 100644 index 00000000000..be79fce0db7 Binary files /dev/null and b/tests/testdata/control_images/legend/expected_legend_two_cols_right_align_symbol_center_align_text/expected_legend_two_cols_right_align_symbol_center_align_text.png differ diff --git a/tests/testdata/control_images/legend/expected_legend_two_cols_right_align_symbol_left_align_text/expected_legend_two_cols_right_align_symbol_left_align_text.png b/tests/testdata/control_images/legend/expected_legend_two_cols_right_align_symbol_left_align_text/expected_legend_two_cols_right_align_symbol_left_align_text.png new file mode 100644 index 00000000000..daeb63e6797 Binary files /dev/null and b/tests/testdata/control_images/legend/expected_legend_two_cols_right_align_symbol_left_align_text/expected_legend_two_cols_right_align_symbol_left_align_text.png differ diff --git a/tests/testdata/control_images/legend/expected_legend_two_cols_right_align_symbol_right_align_text/expected_legend_two_cols_right_align_symbol_right_align_text.png b/tests/testdata/control_images/legend/expected_legend_two_cols_right_align_symbol_right_align_text/expected_legend_two_cols_right_align_symbol_right_align_text.png new file mode 100644 index 00000000000..c0a4df504c7 Binary files /dev/null and b/tests/testdata/control_images/legend/expected_legend_two_cols_right_align_symbol_right_align_text/expected_legend_two_cols_right_align_symbol_right_align_text.png differ diff --git a/tests/testdata/control_images/legend/expected_legend_two_cols_right_align_text/expected_legend_two_cols_right_align_text.png b/tests/testdata/control_images/legend/expected_legend_two_cols_right_align_text/expected_legend_two_cols_right_align_text.png new file mode 100644 index 00000000000..43af3251356 Binary files /dev/null and b/tests/testdata/control_images/legend/expected_legend_two_cols_right_align_text/expected_legend_two_cols_right_align_text.png differ