Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
[api] Move text wrapping handling logic from layout table code to Qgs…
…TextRenderer

Allows other users of QgsTextRenderer to take advantage of the
automatic line wrapping behaviour
  • Loading branch information
nyalldawson committed Nov 29, 2021
1 parent dcf0cfe commit fadfb47
Show file tree
Hide file tree
Showing 12 changed files with 204 additions and 127 deletions.
7 changes: 7 additions & 0 deletions python/core/auto_additions/qgis.py
Expand Up @@ -1169,6 +1169,13 @@
Qgis.Capitalization.__doc__ = 'String capitalization options.\n\n.. note::\n\n Prior to QGIS 3.24 this was available as :py:class:`QgsStringUtils`.Capitalization\n\n.. versionadded:: 3.24\n\n' + '* ``MixedCase``: ' + Qgis.Capitalization.MixedCase.__doc__ + '\n' + '* ``AllUppercase``: ' + Qgis.Capitalization.AllUppercase.__doc__ + '\n' + '* ``AllLowercase``: ' + Qgis.Capitalization.AllLowercase.__doc__ + '\n' + '* ``ForceFirstLetterToCapital``: ' + Qgis.Capitalization.ForceFirstLetterToCapital.__doc__ + '\n' + '* ``SmallCaps``: ' + Qgis.Capitalization.SmallCaps.__doc__ + '\n' + '* ``TitleCase``: ' + Qgis.Capitalization.TitleCase.__doc__ + '\n' + '* ``UpperCamelCase``: ' + Qgis.Capitalization.UpperCamelCase.__doc__ + '\n' + '* ``AllSmallCaps``: ' + Qgis.Capitalization.AllSmallCaps.__doc__
# --
Qgis.Capitalization.baseClass = Qgis
# monkey patching scoped based enum
Qgis.TextRendererFlag.WrapLines.__doc__ = "Automatically wrap long lines of text"
Qgis.TextRendererFlag.__doc__ = 'Flags which control the behavior of rendering text.\n\n.. versionadded:: 3.24\n\n' + '* ``WrapLines``: ' + Qgis.TextRendererFlag.WrapLines.__doc__
# --
Qgis.TextRendererFlag.baseClass = Qgis
Qgis.TextRendererFlags.baseClass = Qgis
TextRendererFlags = Qgis # dirty hack since SIP seems to introduce the flags in module
QgsCurve.Orientation = Qgis.AngularDirection
# monkey patching scoped based enum
QgsCurve.Clockwise = Qgis.AngularDirection.Clockwise
Expand Down
8 changes: 8 additions & 0 deletions python/core/auto_generated/qgis.sip.in
Expand Up @@ -750,6 +750,12 @@ The development version
AllSmallCaps,
};

enum class TextRendererFlag
{
WrapLines,
};
typedef QFlags<Qgis::TextRendererFlag> TextRendererFlags;


enum class AngularDirection
{
Expand Down Expand Up @@ -858,6 +864,8 @@ QFlags<Qgis::VectorLayerTypeFlag> operator|(Qgis::VectorLayerTypeFlag f1, QFlags

QFlags<Qgis::MarkerLinePlacement> operator|(Qgis::MarkerLinePlacement f1, QFlags<Qgis::MarkerLinePlacement> f2);

QFlags<Qgis::TextRendererFlag> operator|(Qgis::TextRendererFlag f1, QFlags<Qgis::TextRendererFlag> f2);




Expand Down
26 changes: 24 additions & 2 deletions python/core/auto_generated/textrenderer/qgstextrenderer.sip.in
Expand Up @@ -87,7 +87,8 @@ Calculates pixel size (considering output size should be in pixel or map units,

static void drawText( const QRectF &rect, double rotation, HAlignment alignment, const QStringList &textLines,
QgsRenderContext &context, const QgsTextFormat &format,
bool drawAsOutlines = true, VAlignment vAlignment = AlignTop );
bool drawAsOutlines = true, VAlignment vAlignment = AlignTop,
Qgis::TextRendererFlags flags = Qgis::TextRendererFlags() );
%Docstring
Draws text within a rectangle using the specified settings.

Expand All @@ -102,6 +103,7 @@ Draws text within a rectangle using the specified settings.
rendering and may result in side effects like misaligned text buffers. This setting is deprecated and has no effect
as of QGIS 3.4.3 and the text format should be set using :py:func:`QgsRenderContext.setTextRenderFormat()` instead.
:param vAlignment: vertical alignment (since QGIS 3.16)
:param flags: text rendering flags (since QGIS 3.24)
%End

static void drawText( QPointF point, double rotation, HAlignment alignment, const QStringList &textLines,
Expand Down Expand Up @@ -196,7 +198,7 @@ Returns the width of a text based on a given format.
%End

static double textHeight( const QgsRenderContext &context, const QgsTextFormat &format, const QStringList &textLines, DrawMode mode = Point,
QFontMetricsF *fontMetrics = 0 );
QFontMetricsF *fontMetrics = 0, Qgis::TextRendererFlags flags = Qgis::TextRendererFlags(), double maxLineWidth = 0 );
%Docstring
Returns the height of a text based on a given format.

Expand All @@ -205,6 +207,8 @@ Returns the height of a text based on a given format.
:param textLines: list of lines of text to calculate width from
:param mode: draw mode
:param fontMetrics: font metrics
:param flags: text renderer flags (since QGIS 3.24)
:param maxLineWidth: maximum line width, in painter units. Used when the Qgis.TextRendererFlag.WrapLines flag is used (since QGIS 3.24)
%End

static double textHeight( const QgsRenderContext &context, const QgsTextFormat &format, QChar character, bool includeEffects = false );
Expand All @@ -218,6 +222,24 @@ Returns the height of a character when rendered with the specified text ``format
returned height. If ``False``, then the returned size considers the character only.

.. versionadded:: 3.16
%End

static bool textRequiresWrapping( const QgsRenderContext &context, const QString &text, double width, const QgsTextFormat &format );
%Docstring
Returns ``True`` if the specified ``text`` requires line wrapping in order to fit within the specified ``width`` (in painter units).

.. seealso:: :py:func:`wrappedText`

.. versionadded:: 3.24
%End

static QStringList wrappedText( const QgsRenderContext &context, const QString &text, double width, const QgsTextFormat &format );
%Docstring
Wraps a ``text`` string to multiple lines, such that each individual line will fit within the specified ``width`` (in painter units).

.. seealso:: :py:func:`textRequiresWrapping`

.. versionadded:: 3.24
%End

static const double FONT_WORKAROUND_SCALE;
Expand Down
135 changes: 24 additions & 111 deletions src/core/layout/qgslayouttable.cpp
Expand Up @@ -470,13 +470,7 @@ void QgsLayoutTable::render( QgsLayoutItemRenderContext &context, const QRectF &

const QRectF textCell = QRectF( currentX, currentY + mCellMargin, mMaxColumnWidthMap[col], cellHeaderHeight - 2 * mCellMargin );

// disable text clipping to target text rectangle, because we manually clip to the full cell bounds below
// and it's ok if text overlaps into the margin (e.g. extenders or italicized text)
QStringList str = column.heading().split( '\n' );
if ( ( mWrapBehavior != TruncateText || column.width() > 0 ) && textRequiresWrapping( context.renderContext(), column.heading(), column.width(), headerFormat ) )
{
str = wrappedText( context.renderContext(), column.heading(), column.width(), headerFormat );
}
const QStringList str = column.heading().split( '\n' );

// scale to dots
{
Expand All @@ -485,7 +479,9 @@ void QgsLayoutTable::render( QgsLayoutItemRenderContext &context, const QRectF &
textCell.top() * context.renderContext().scaleFactor(),
textCell.width() * context.renderContext().scaleFactor(),
textCell.height() * context.renderContext().scaleFactor() ), 0,
headerAlign, str, context.renderContext(), headerFormat, true, QgsTextRenderer::AlignVCenter );
headerAlign, str, context.renderContext(), headerFormat, true, QgsTextRenderer::AlignVCenter,
mWrapBehavior == WrapText ? Qgis::TextRendererFlag::WrapLines : Qgis::TextRendererFlags()
);
}

currentX += mMaxColumnWidthMap[ col ];
Expand Down Expand Up @@ -514,6 +510,7 @@ void QgsLayoutTable::render( QgsLayoutItemRenderContext &context, const QRectF &

for ( const QgsLayoutTableColumn &column : std::as_const( mColumns ) )
{
( void )column;
const QRectF fullCell( currentX, currentY, mMaxColumnWidthMap[col] + 2 * mCellMargin, rowHeight );
//draw background
p->save();
Expand All @@ -527,19 +524,12 @@ void QgsLayoutTable::render( QgsLayoutItemRenderContext &context, const QRectF &

QVariant cellContents = mTableContents.at( row ).at( col );
const QString localizedString { QgsExpressionUtils::toLocalizedString( cellContents ) };
QStringList str = localizedString.split( '\n' );
const QStringList str = localizedString.split( '\n' );

QgsTextFormat cellFormat = textFormatForCell( row, col );
QgsExpressionContextScopePopper popper( context.renderContext().expressionContext(), scopeForCell( row, col ) );
cellFormat.updateDataDefinedProperties( context.renderContext() );

// disable text clipping to target text rectangle, because we manually clip to the full cell bounds below
// and it's ok if text overlaps into the margin (e.g. extenders or italicized text)
if ( ( mWrapBehavior != TruncateText || column.width() > 0 ) && textRequiresWrapping( context.renderContext(), localizedString, column.width(), cellFormat ) )
{
str = wrappedText( context.renderContext(), localizedString, column.width(), cellFormat );
}

p->save();
p->setClipRect( fullCell );
const QRectF textCell = QRectF( currentX, currentY + mCellMargin, mMaxColumnWidthMap[col], rowHeight - 2 * mCellMargin );
Expand All @@ -559,7 +549,8 @@ void QgsLayoutTable::render( QgsLayoutItemRenderContext &context, const QRectF &
textCell.width() * context.renderContext().scaleFactor(),
textCell.height() * context.renderContext().scaleFactor() ), 0,
QgsTextRenderer::convertQtHAlignment( horizontalAlignmentForCell( row, col ) ), str, context.renderContext(), cellFormat, true,
QgsTextRenderer::convertQtVAlignment( verticalAlignmentForCell( row, col ) ) );
QgsTextRenderer::convertQtVAlignment( verticalAlignmentForCell( row, col ) ),
mWrapBehavior == WrapText ? Qgis::TextRendererFlag::WrapLines : Qgis::TextRendererFlags() );
}
p->restore();

Expand Down Expand Up @@ -1189,16 +1180,16 @@ bool QgsLayoutTable::calculateMaxRowHeights()
{
heights[i] = 0;
}
else if ( textRequiresWrapping( context, col.heading(), mColumns.at( i ).width(), cellFormat ) )
{
//contents too wide for cell, need to wrap
heights[i] = QgsTextRenderer::textHeight( context, cellFormat, wrappedText( context, col.heading(), mColumns.at( i ).width(), cellFormat ), QgsTextRenderer::Rect )
/ context.convertToPainterUnits( 1, QgsUnitTypes::RenderMillimeters )
- headerDescentMm;
}
else
{
heights[i] = QgsTextRenderer::textHeight( context, cellFormat, QStringList() << col.heading(), QgsTextRenderer::Rect ) / context.convertToPainterUnits( 1, QgsUnitTypes::RenderMillimeters )
heights[i] = QgsTextRenderer::textHeight( context,
cellFormat,
QStringList() << col.heading(), QgsTextRenderer::Rect,
nullptr,
mWrapBehavior == WrapText ? Qgis::TextRendererFlag::WrapLines : Qgis::TextRendererFlags(),
context.convertToPainterUnits( mColumns.at( i ).width(), QgsUnitTypes::RenderMillimeters )
)
/ context.convertToPainterUnits( 1, QgsUnitTypes::RenderMillimeters )
- headerDescentMm;
}
i++;
Expand All @@ -1219,15 +1210,14 @@ bool QgsLayoutTable::calculateMaxRowHeights()
const double contentDescentMm = QgsTextRenderer::fontMetrics( context, cellFormat, QgsTextRenderer::FONT_WORKAROUND_SCALE ).descent() / QgsTextRenderer::FONT_WORKAROUND_SCALE / context.convertToPainterUnits( 1, QgsUnitTypes::RenderMillimeters );
const QString localizedString { QgsExpressionUtils::toLocalizedString( *colIt ) };

if ( textRequiresWrapping( context, localizedString, mColumns.at( i ).width(), cellFormat ) )
{
//contents too wide for cell, need to wrap
heights[ row * cols + i ] = QgsTextRenderer::textHeight( context, cellFormat, wrappedText( context, localizedString, mColumns.at( i ).width(), cellFormat ), QgsTextRenderer::Rect ) / context.convertToPainterUnits( 1, QgsUnitTypes::RenderMillimeters ) - contentDescentMm;
}
else
{
heights[ row * cols + i ] = QgsTextRenderer::textHeight( context, cellFormat, QStringList() << localizedString.split( '\n' ), QgsTextRenderer::Rect ) / context.convertToPainterUnits( 1, QgsUnitTypes::RenderMillimeters ) - contentDescentMm;
}
heights[ row * cols + i ] = QgsTextRenderer::textHeight( context,
cellFormat,
QStringList() << localizedString.split( '\n' ),
QgsTextRenderer::Rect,
nullptr,
mWrapBehavior == WrapText ? Qgis::TextRendererFlag::WrapLines : Qgis::TextRendererFlags(),
context.convertToPainterUnits( mColumns.at( i ).width(), QgsUnitTypes::RenderMillimeters )
) / context.convertToPainterUnits( 1, QgsUnitTypes::RenderMillimeters ) - contentDescentMm;

i++;
}
Expand Down Expand Up @@ -1375,83 +1365,6 @@ void QgsLayoutTable::drawHorizontalGridLines( QgsLayoutItemRenderContext &contex
painter->drawLine( QPointF( halfGridStrokeWidth, currentY ), QPointF( mTableSize.width() - halfGridStrokeWidth, currentY ) );
}

bool QgsLayoutTable::textRequiresWrapping( QgsRenderContext &context, const QString &text, double columnWidth, const QgsTextFormat &format ) const
{
if ( qgsDoubleNear( columnWidth, 0.0 ) || mWrapBehavior != WrapText )
return false;

const QStringList multiLineSplit = text.split( '\n' );
const double currentTextWidth = QgsTextRenderer::textWidth( context, format, multiLineSplit ) / context.convertToPainterUnits( 1, QgsUnitTypes::RenderMillimeters );
return currentTextWidth > columnWidth;
}

QStringList QgsLayoutTable::wrappedText( QgsRenderContext &context, const QString &value, double columnWidth, const QgsTextFormat &format ) const
{
QStringList lines = value.split( '\n' );
QStringList outLines;
const auto constLines = lines;
for ( const QString &line : constLines )
{
if ( textRequiresWrapping( context, line, columnWidth, format ) )
{
//first step is to identify words which must be on their own line (too long to fit)
QStringList words = line.split( ' ' );
QStringList linesToProcess;
QString wordsInCurrentLine;
const auto constWords = words;
for ( const QString &word : constWords )
{
if ( textRequiresWrapping( context, word, columnWidth, format ) )
{
//too long to fit
if ( !wordsInCurrentLine.isEmpty() )
linesToProcess << wordsInCurrentLine;
wordsInCurrentLine.clear();
linesToProcess << word;
}
else
{
if ( !wordsInCurrentLine.isEmpty() )
wordsInCurrentLine.append( ' ' );
wordsInCurrentLine.append( word );
}
}
if ( !wordsInCurrentLine.isEmpty() )
linesToProcess << wordsInCurrentLine;

const auto constLinesToProcess = linesToProcess;
for ( const QString &line : constLinesToProcess )
{
QString remainingText = line;
int lastPos = remainingText.lastIndexOf( ' ' );
while ( lastPos > -1 )
{
//check if remaining text is short enough to go in one line
if ( !textRequiresWrapping( context, remainingText, columnWidth, format ) )
{
break;
}

if ( !textRequiresWrapping( context, remainingText.left( lastPos ), columnWidth, format ) )
{
outLines << remainingText.left( lastPos );
remainingText = remainingText.mid( lastPos + 1 );
lastPos = 0;
}
lastPos = remainingText.lastIndexOf( ' ', lastPos - 1 );
}
outLines << remainingText;
}
}
else
{
outLines << line;
}
}

return outLines;
}

QColor QgsLayoutTable::backgroundColor( int row, int column ) const
{
QColor color = mBackgroundColor;
Expand Down
4 changes: 0 additions & 4 deletions src/core/layout/qgslayouttable.h
Expand Up @@ -771,10 +771,6 @@ class CORE_EXPORT QgsLayoutTable: public QgsLayoutMultiFrame
//! Initializes cell style map
void initStyles();

bool textRequiresWrapping( QgsRenderContext &context, const QString &text, double columnWidth, const QgsTextFormat &format ) const;

QStringList wrappedText( QgsRenderContext &context, const QString &value, double columnWidth, const QgsTextFormat &format ) const;

/**
* Returns the calculated background color for a row and column combination.
* \param row row number, where -1 is the header row, and 0 is the first body row
Expand Down
13 changes: 13 additions & 0 deletions src/core/qgis.h
Expand Up @@ -1223,6 +1223,18 @@ class CORE_EXPORT Qgis
};
Q_ENUM( Capitalization )

/**
* Flags which control the behavior of rendering text.
*
* \since QGIS 3.24
*/
enum class TextRendererFlag : int
{
WrapLines = 1 << 0, //!< Automatically wrap long lines of text
};
Q_ENUM( TextRendererFlag )
Q_DECLARE_FLAGS( TextRendererFlags, TextRendererFlag )
Q_FLAG( TextRendererFlags )

/**
* Angular directions.
Expand Down Expand Up @@ -1367,6 +1379,7 @@ Q_DECLARE_OPERATORS_FOR_FLAGS( Qgis::MapSettingsFlags )
Q_DECLARE_OPERATORS_FOR_FLAGS( Qgis::RenderContextFlags )
Q_DECLARE_OPERATORS_FOR_FLAGS( Qgis::VectorLayerTypeFlags )
Q_DECLARE_OPERATORS_FOR_FLAGS( Qgis::MarkerLinePlacements )
Q_DECLARE_OPERATORS_FOR_FLAGS( Qgis::TextRendererFlags )


// hack to workaround warnings when casting void pointers
Expand Down

0 comments on commit fadfb47

Please sign in to comment.