Skip to content

Commit

Permalink
[feature] Use text renderer in layout legends
Browse files Browse the repository at this point in the history
This change allows use of the full text renderer capabilities
for legend titles, subtitles, and item text. It allows buffers,
shadows, font spacing control, and all over options (including
mixed HTML formatting!) which is permitted for use in text
formats.
  • Loading branch information
nyalldawson committed Nov 29, 2022
1 parent b4d6275 commit 34df6d1
Show file tree
Hide file tree
Showing 179 changed files with 523 additions and 360 deletions.
Expand Up @@ -169,6 +169,11 @@ Default implementation does nothing.
QgsLegendPatchShape patchShape;

QSizeF patchSize;

const QgsTextDocument *textDocument;

const QgsTextDocumentMetrics *textDocumentMetrics;

};

struct ItemMetrics
Expand Down
20 changes: 16 additions & 4 deletions python/core/auto_generated/layout/qgslayoutitemlegend.sip.in
Expand Up @@ -210,18 +210,24 @@ Returns legend style.
Sets the style of ``component`` to ``style`` for the legend.
%End

QFont styleFont( QgsLegendStyle::Style component ) const;
QFont styleFont( QgsLegendStyle::Style component ) const /Deprecated/;
%Docstring
Returns the font settings for a legend ``component``.

.. seealso:: :py:func:`setStyleFont`

.. deprecated::
use :py:func:`QgsLegendStyle.textFormat()` from :py:func:`~QgsLayoutItemLegend.style` instead.
%End

void setStyleFont( QgsLegendStyle::Style component, const QFont &font );
void setStyleFont( QgsLegendStyle::Style component, const QFont &font ) /Deprecated/;
%Docstring
Sets the style ``font`` for a legend ``component``.

.. seealso:: :py:func:`styleFont`

.. deprecated::
use :py:func:`QgsLegendStyle.setTextFormat()` from :py:func:`~QgsLayoutItemLegend.style` instead.
%End

void setStyleMargin( QgsLegendStyle::Style component, double margin );
Expand Down Expand Up @@ -276,18 +282,24 @@ Sets the legend column ``spacing``.
.. seealso:: :py:func:`columnSpace`
%End

QColor fontColor() const;
QColor fontColor() const /Deprecated/;
%Docstring
Returns the legend font color.

.. seealso:: :py:func:`setFontColor`

.. deprecated::
use :py:func:`QgsLegendStyle.setTextFormat()` from :py:func:`~QgsLayoutItemLegend.style` instead.
%End

void setFontColor( const QColor &color );
void setFontColor( const QColor &color ) /Deprecated/;
%Docstring
Sets the legend font ``color``.

.. seealso:: :py:func:`fontColor`

.. deprecated::
use :py:func:`QgsLegendStyle.setTextFormat()` from :py:func:`~QgsLayoutItemLegend.style` instead.
%End

double symbolWidth() const;
Expand Down
20 changes: 14 additions & 6 deletions python/core/auto_generated/qgslegendsettings.sip.in
Expand Up @@ -172,32 +172,39 @@ If ``False``, then then columns will be individually resized to their minimum po
.. seealso:: :py:func:`equalColumnWidth`
%End

QColor fontColor() const;
QColor fontColor() const /Deprecated/;
%Docstring
Returns the font color used for legend items.

.. seealso:: :py:func:`setFontColor`

.. deprecated::
Use :py:func:`QgsLegendStyle.textFormat()` instead.
%End

void setFontColor( const QColor &c );
void setFontColor( const QColor &c ) /Deprecated/;
%Docstring
Sets the font color used for legend items.

.. seealso:: :py:func:`fontColor`

.. deprecated::
Use :py:func:`QgsLegendStyle.textFormat()` instead.
%End

QColor layerFontColor() const;
QColor layerFontColor() const /Deprecated/;
%Docstring
Returns layer font color, defaults to :py:func:`~QgsLegendSettings.fontColor`

.. seealso:: :py:func:`setLayerFontColor`

.. seealso:: :py:func:`fontColor`

.. versionadded:: 3.4.7
.. deprecated::
Use :py:func:`QgsLegendStyle.textFormat()` instead.
%End

void setLayerFontColor( const QColor &fontColor );
void setLayerFontColor( const QColor &fontColor ) /Deprecated/;
%Docstring
Sets layer font color to ``fontColor``
Overrides :py:func:`~QgsLegendSettings.fontColor`
Expand All @@ -206,7 +213,8 @@ Overrides :py:func:`~QgsLegendSettings.fontColor`

.. seealso:: :py:func:`fontColor`

.. versionadded:: 3.4.7
.. deprecated::
Use :py:func:`QgsLegendStyle.textFormat()` instead.
%End

QSizeF symbolSize() const;
Expand Down
28 changes: 26 additions & 2 deletions python/core/auto_generated/qgslegendstyle.sip.in
Expand Up @@ -43,21 +43,45 @@ Contains detailed styling information relating to how a layout legend should be

QgsLegendStyle();

QFont font() const;
QFont font() const /Deprecated/;
%Docstring
Returns the font used for rendering this legend component.

.. seealso:: :py:func:`setFont`

.. deprecated::
use :py:func:`~QgsLegendStyle.textFormat` instead
%End

void setFont( const QFont &font );
void setFont( const QFont &font ) /Deprecated/;
%Docstring
Sets the ``font`` used for rendering this legend component.

.. seealso:: :py:func:`font`

.. deprecated::
use :py:func:`~QgsLegendStyle.setTextFormat` instead
%End

QgsTextFormat &textFormat();
%Docstring
Returns the text format used for rendering this legend component.

.. seealso:: :py:func:`setTextFormat`

.. versionadded:: 3.30
%End


void setTextFormat( const QgsTextFormat &format );
%Docstring
Sets the text ``format`` used for rendering this legend component.

.. seealso:: :py:func:`textFormat`

.. versionadded:: 3.30
%End

double margin( Side side );
%Docstring
Returns the margin (in mm) for the specified ``side`` of the component.
Expand Down
11 changes: 2 additions & 9 deletions src/core/layertree/qgscolorramplegendnode.cpp
Expand Up @@ -209,11 +209,7 @@ QSizeF QgsColorRampLegendNode::drawSymbol( const QgsLegendSettings &settings, It
context = tempRenderContext.get();
}

const QFont symbolLabelFont = settings.style( QgsLegendStyle::SymbolLabel ).font();
QgsTextFormat format = mSettings.textFormat().isValid() ? mSettings.textFormat() : QgsTextFormat::fromQFont( symbolLabelFont );
if ( !mSettings.textFormat().isValid() )
format.setColor( settings.fontColor() );

const QgsTextFormat format = mSettings.textFormat().isValid() ? mSettings.textFormat() : settings.style( QgsLegendStyle::SymbolLabel ).textFormat();
const QString minLabel = labelForMinimum();
const QString maxLabel = labelForMaximum();

Expand Down Expand Up @@ -413,10 +409,7 @@ QSizeF QgsColorRampLegendNode::drawSymbolText( const QgsLegendSettings &settings
context = tempRenderContext.get();
}

const QFont symbolLabelFont = settings.style( QgsLegendStyle::SymbolLabel ).font();
QgsTextFormat format = mSettings.textFormat().isValid() ? mSettings.textFormat() : QgsTextFormat::fromQFont( symbolLabelFont );
if ( !mSettings.textFormat().isValid() )
format.setColor( settings.fontColor() );
const QgsTextFormat format = mSettings.textFormat().isValid() ? mSettings.textFormat() : settings.style( QgsLegendStyle::SymbolLabel ).textFormat();

const QString minLabel = labelForMinimum();
const QString maxLabel = labelForMaximum();
Expand Down
102 changes: 55 additions & 47 deletions src/core/layertree/qgslayertreemodellegendnode.cpp
Expand Up @@ -37,9 +37,11 @@
#include "qgsmarkersymbol.h"
#include "qgsvariantutils.h"
#include "qgslayertreelayer.h"
#include "qgsgeometrygeneratorsymbollayer.h"
#include "qgstextdocument.h"
#include "qgstextdocumentmetrics.h"

#include <QBuffer>
#include <optional>

QgsLayerTreeModelLegendNode::QgsLayerTreeModelLegendNode( QgsLayerTreeLayer *nodeL, QObject *parent )
: QObject( parent )
Expand Down Expand Up @@ -84,9 +86,22 @@ void QgsLayerTreeModelLegendNode::setUserPatchSize( QSizeF size )

QgsLayerTreeModelLegendNode::ItemMetrics QgsLayerTreeModelLegendNode::draw( const QgsLegendSettings &settings, ItemContext *ctx )
{
const QFont symbolLabelFont = settings.style( QgsLegendStyle::SymbolLabel ).font();
const QgsTextFormat f = settings.style( QgsLegendStyle::SymbolLabel ).textFormat();

const QStringList lines = settings.evaluateItemText( data( Qt::DisplayRole ).toString(), ctx->context->expressionContext() );

const QgsTextDocument textDocument = f.allowHtmlFormatting() ? QgsTextDocument::fromHtml( lines ) : QgsTextDocument::fromPlainText( lines );
ctx->textDocument = &textDocument;

std::optional< QgsScopedRenderContextScaleToPixels > scaleToPx( *ctx->context );
const QgsTextDocumentMetrics textDocumentMetrics = QgsTextDocumentMetrics::calculateMetrics( textDocument, f, *ctx->context,
ctx->context->flags() & Qgis::RenderContextFlag::ApplyScalingWorkaroundForTextRendering ? QgsTextRenderer::FONT_WORKAROUND_SCALE : 1 );
ctx->textDocumentMetrics = &textDocumentMetrics;
scaleToPx.reset();

//const double textHeight = textDocumentMetrics.documentSize( Qgis::TextLayoutMode::Legend, Qgis::TextOrientation::Horizontal ).height() / ctx->context->scaleFactor();
const double textHeight = textDocumentMetrics.firstLineCapHeight() / ctx->context->scaleFactor();

const double textHeight = settings.fontHeightCharacterMM( symbolLabelFont, QChar( '0' ) );
// itemHeight here is not really item height, it is only for symbol
// vertical alignment purpose, i.e. OK take single line height
// if there are more lines, those run under the symbol
Expand All @@ -95,6 +110,9 @@ QgsLayerTreeModelLegendNode::ItemMetrics QgsLayerTreeModelLegendNode::draw( cons
ItemMetrics im;
im.symbolSize = drawSymbol( settings, ctx, itemHeight );
im.labelSize = drawSymbolText( settings, ctx, im.symbolSize );

ctx->textDocument = nullptr;
ctx->textDocumentMetrics = nullptr;
return im;
}

Expand Down Expand Up @@ -163,31 +181,38 @@ QJsonObject QgsLayerTreeModelLegendNode::exportSymbolToJson( const QgsLegendSett
return json;
}

QSizeF QgsLayerTreeModelLegendNode::drawSymbolText( const QgsLegendSettings &settings, ItemContext *ctx, QSizeF symbolSize ) const
QSizeF QgsLayerTreeModelLegendNode::drawSymbolText( const QgsLegendSettings &settings, ItemContext *ctx, QSizeF symbolSizeMM ) const
{
QSizeF labelSize( 0, 0 );
// we need a full render context here, so make one if we don't already have one
std::unique_ptr< QgsRenderContext > tempContext;
QgsRenderContext *context = ctx ? ctx->context : nullptr;
if ( !context )
{
tempContext.reset( new QgsRenderContext( QgsRenderContext::fromQPainter( ctx ? ctx->painter : nullptr ) ) );
context = tempContext.get();
}

const QFont symbolLabelFont = settings.style( QgsLegendStyle::SymbolLabel ).font();
const double textHeight = settings.fontHeightCharacterMM( symbolLabelFont, QChar( '0' ) );
const double textDescent = settings.fontDescentMillimeters( symbolLabelFont );
const QgsTextFormat format = settings.style( QgsLegendStyle::SymbolLabel ).textFormat();

const QgsExpressionContext tempContext;
const double dotsPerMM = ctx->context->scaleFactor();
QgsScopedRenderContextScaleToPixels scaleToPx( *context );

const QStringList lines = settings.evaluateItemText( data( Qt::DisplayRole ).toString(), ctx && ctx->context ? ctx->context->expressionContext() : tempContext );
// TODO when no metrics
// TODO --- line spacing!!!

labelSize.rheight() = lines.count() * textHeight + ( lines.count() - 1 ) * ( settings.lineSpacing() + textDescent );
const QSizeF documentSize = ctx->textDocumentMetrics->documentSize( Qgis::TextLayoutMode::RectangleCapHeightBased, Qgis::TextOrientation::Horizontal );
const QSizeF labelSizeMM( documentSize / dotsPerMM );

double labelXMin = 0.0;
double labelXMax = 0.0;
double labelY = 0.0;
if ( ctx && ctx->painter )
double labelYMM = 0.0;
if ( ctx && context->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 )
labelXMin = ctx->columnLeft + std::max( static_cast< double >( symbolSizeMM.width() ), ctx->maxSiblingSymbolWidth )
+ settings.style( QgsLegendStyle::Symbol ).margin( QgsLegendStyle::Right )
+ settings.style( QgsLegendStyle::SymbolLabel ).margin( QgsLegendStyle::Left );
labelXMax = ctx->columnRight;
Expand All @@ -198,50 +223,33 @@ QSizeF QgsLayerTreeModelLegendNode::drawSymbolText( const QgsLegendSettings &set
// 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 )
labelXMax = ctx->columnRight - std::max( static_cast< double >( symbolSizeMM.width() ), ctx->maxSiblingSymbolWidth )
- settings.style( QgsLegendStyle::Symbol ).margin( QgsLegendStyle::Right )
- settings.style( QgsLegendStyle::SymbolLabel ).margin( QgsLegendStyle::Left );
break;
}

labelY = ctx->top;
labelYMM = ctx->top;

// Vertical alignment of label with symbol
if ( labelSize.height() < symbolSize.height() )
labelY += symbolSize.height() / 2 - labelSize.height() / 2; // label centered with symbol

labelY += textHeight;
if ( labelSizeMM.height() < symbolSizeMM.height() )
labelYMM += ( symbolSizeMM.height() - labelSizeMM.height() ) / 2; // label centered with symbol
}

for ( QStringList::ConstIterator itemPart = lines.constBegin(); itemPart != lines.constEnd(); ++itemPart )
if ( context->painter() )
{
const double lineWidth = settings.textWidthMillimeters( symbolLabelFont, *itemPart );
labelSize.rwidth() = std::max( lineWidth, double( labelSize.width() ) );
Qgis::TextHorizontalAlignment halign = settings.style( QgsLegendStyle::SymbolLabel ).alignment() == Qt::AlignLeft ? Qgis::TextHorizontalAlignment::Left :
settings.style( QgsLegendStyle::SymbolLabel ).alignment() == Qt::AlignRight ? Qgis::TextHorizontalAlignment::Right : Qgis::TextHorizontalAlignment::Center;

if ( ctx && ctx->painter )
{
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;
}
QgsTextRenderer::drawDocument( QRectF( labelXMin * dotsPerMM, std::round( labelYMM * dotsPerMM ),
( labelXMax - labelXMin )* dotsPerMM,
std::max( symbolSizeMM.height(), labelSizeMM.height() ) * dotsPerMM ),
format, *ctx->textDocument, *ctx->textDocumentMetrics, *context, halign, Qgis::TextVerticalAlignment::Top,
0, Qgis::TextLayoutMode::RectangleCapHeightBased );
}

return labelSize;
return labelSizeMM;
}

void QgsLayerTreeModelLegendNode::checkAllItems()
Expand Down Expand Up @@ -1458,8 +1466,8 @@ QgsLayerTreeModelLegendNode::ItemMetrics QgsDataDefinedSizeLegendNode::draw( con
}

QgsDataDefinedSizeLegend ddsLegend( *mSettings );
ddsLegend.setFont( settings.style( QgsLegendStyle::SymbolLabel ).font() );
ddsLegend.setTextColor( settings.fontColor() );
ddsLegend.setFont( settings.style( QgsLegendStyle::SymbolLabel ).textFormat().toQFont() );
ddsLegend.setTextColor( settings.style( QgsLegendStyle::SymbolLabel ).textFormat().color() );

QSizeF contentSize;
double labelXOffset;
Expand Down
18 changes: 18 additions & 0 deletions src/core/layertree/qgslayertreemodellegendnode.h
Expand Up @@ -35,6 +35,9 @@ class QgsLegendSettings;
class QgsMapSettings;
class QgsSymbol;
class QgsRenderContext;
class QgsTextFormat;
class QgsTextDocument;
class QgsTextDocumentMetrics;

/**
* \ingroup core
Expand Down Expand Up @@ -232,6 +235,21 @@ class CORE_EXPORT QgsLayerTreeModelLegendNode : public QObject
* \since QGIS 3.14
*/
QSizeF patchSize;

/**
* Optional text document
*
* \since QGIS 3.30
*/
const QgsTextDocument *textDocument = nullptr;

/**
* Optional text document metrics.
*
* \since QGIS 3.30
*/
const QgsTextDocumentMetrics *textDocumentMetrics = nullptr;

};

struct ItemMetrics
Expand Down

0 comments on commit 34df6d1

Please sign in to comment.