Skip to content

Commit fadfb47

Browse files
committedNov 29, 2021
[api] Move text wrapping handling logic from layout table code to QgsTextRenderer
Allows other users of QgsTextRenderer to take advantage of the automatic line wrapping behaviour
1 parent dcf0cfe commit fadfb47

File tree

12 files changed

+204
-127
lines changed

12 files changed

+204
-127
lines changed
 

‎python/core/auto_additions/qgis.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1169,6 +1169,13 @@
11691169
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__
11701170
# --
11711171
Qgis.Capitalization.baseClass = Qgis
1172+
# monkey patching scoped based enum
1173+
Qgis.TextRendererFlag.WrapLines.__doc__ = "Automatically wrap long lines of text"
1174+
Qgis.TextRendererFlag.__doc__ = 'Flags which control the behavior of rendering text.\n\n.. versionadded:: 3.24\n\n' + '* ``WrapLines``: ' + Qgis.TextRendererFlag.WrapLines.__doc__
1175+
# --
1176+
Qgis.TextRendererFlag.baseClass = Qgis
1177+
Qgis.TextRendererFlags.baseClass = Qgis
1178+
TextRendererFlags = Qgis # dirty hack since SIP seems to introduce the flags in module
11721179
QgsCurve.Orientation = Qgis.AngularDirection
11731180
# monkey patching scoped based enum
11741181
QgsCurve.Clockwise = Qgis.AngularDirection.Clockwise

‎python/core/auto_generated/qgis.sip.in

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -750,6 +750,12 @@ The development version
750750
AllSmallCaps,
751751
};
752752

753+
enum class TextRendererFlag
754+
{
755+
WrapLines,
756+
};
757+
typedef QFlags<Qgis::TextRendererFlag> TextRendererFlags;
758+
753759

754760
enum class AngularDirection
755761
{
@@ -858,6 +864,8 @@ QFlags<Qgis::VectorLayerTypeFlag> operator|(Qgis::VectorLayerTypeFlag f1, QFlags
858864

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

867+
QFlags<Qgis::TextRendererFlag> operator|(Qgis::TextRendererFlag f1, QFlags<Qgis::TextRendererFlag> f2);
868+
861869

862870

863871

‎python/core/auto_generated/textrenderer/qgstextrenderer.sip.in

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,8 @@ Calculates pixel size (considering output size should be in pixel or map units,
8787

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

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

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

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

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

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

220224
.. versionadded:: 3.16
225+
%End
226+
227+
static bool textRequiresWrapping( const QgsRenderContext &context, const QString &text, double width, const QgsTextFormat &format );
228+
%Docstring
229+
Returns ``True`` if the specified ``text`` requires line wrapping in order to fit within the specified ``width`` (in painter units).
230+
231+
.. seealso:: :py:func:`wrappedText`
232+
233+
.. versionadded:: 3.24
234+
%End
235+
236+
static QStringList wrappedText( const QgsRenderContext &context, const QString &text, double width, const QgsTextFormat &format );
237+
%Docstring
238+
Wraps a ``text`` string to multiple lines, such that each individual line will fit within the specified ``width`` (in painter units).
239+
240+
.. seealso:: :py:func:`textRequiresWrapping`
241+
242+
.. versionadded:: 3.24
221243
%End
222244

223245
static const double FONT_WORKAROUND_SCALE;

‎src/core/layout/qgslayouttable.cpp

Lines changed: 24 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -470,13 +470,7 @@ void QgsLayoutTable::render( QgsLayoutItemRenderContext &context, const QRectF &
470470

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

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

481475
// scale to dots
482476
{
@@ -485,7 +479,9 @@ void QgsLayoutTable::render( QgsLayoutItemRenderContext &context, const QRectF &
485479
textCell.top() * context.renderContext().scaleFactor(),
486480
textCell.width() * context.renderContext().scaleFactor(),
487481
textCell.height() * context.renderContext().scaleFactor() ), 0,
488-
headerAlign, str, context.renderContext(), headerFormat, true, QgsTextRenderer::AlignVCenter );
482+
headerAlign, str, context.renderContext(), headerFormat, true, QgsTextRenderer::AlignVCenter,
483+
mWrapBehavior == WrapText ? Qgis::TextRendererFlag::WrapLines : Qgis::TextRendererFlags()
484+
);
489485
}
490486

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

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

528525
QVariant cellContents = mTableContents.at( row ).at( col );
529526
const QString localizedString { QgsExpressionUtils::toLocalizedString( cellContents ) };
530-
QStringList str = localizedString.split( '\n' );
527+
const QStringList str = localizedString.split( '\n' );
531528

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

536-
// disable text clipping to target text rectangle, because we manually clip to the full cell bounds below
537-
// and it's ok if text overlaps into the margin (e.g. extenders or italicized text)
538-
if ( ( mWrapBehavior != TruncateText || column.width() > 0 ) && textRequiresWrapping( context.renderContext(), localizedString, column.width(), cellFormat ) )
539-
{
540-
str = wrappedText( context.renderContext(), localizedString, column.width(), cellFormat );
541-
}
542-
543533
p->save();
544534
p->setClipRect( fullCell );
545535
const QRectF textCell = QRectF( currentX, currentY + mCellMargin, mMaxColumnWidthMap[col], rowHeight - 2 * mCellMargin );
@@ -559,7 +549,8 @@ void QgsLayoutTable::render( QgsLayoutItemRenderContext &context, const QRectF &
559549
textCell.width() * context.renderContext().scaleFactor(),
560550
textCell.height() * context.renderContext().scaleFactor() ), 0,
561551
QgsTextRenderer::convertQtHAlignment( horizontalAlignmentForCell( row, col ) ), str, context.renderContext(), cellFormat, true,
562-
QgsTextRenderer::convertQtVAlignment( verticalAlignmentForCell( row, col ) ) );
552+
QgsTextRenderer::convertQtVAlignment( verticalAlignmentForCell( row, col ) ),
553+
mWrapBehavior == WrapText ? Qgis::TextRendererFlag::WrapLines : Qgis::TextRendererFlags() );
563554
}
564555
p->restore();
565556

@@ -1189,16 +1180,16 @@ bool QgsLayoutTable::calculateMaxRowHeights()
11891180
{
11901181
heights[i] = 0;
11911182
}
1192-
else if ( textRequiresWrapping( context, col.heading(), mColumns.at( i ).width(), cellFormat ) )
1193-
{
1194-
//contents too wide for cell, need to wrap
1195-
heights[i] = QgsTextRenderer::textHeight( context, cellFormat, wrappedText( context, col.heading(), mColumns.at( i ).width(), cellFormat ), QgsTextRenderer::Rect )
1196-
/ context.convertToPainterUnits( 1, QgsUnitTypes::RenderMillimeters )
1197-
- headerDescentMm;
1198-
}
11991183
else
12001184
{
1201-
heights[i] = QgsTextRenderer::textHeight( context, cellFormat, QStringList() << col.heading(), QgsTextRenderer::Rect ) / context.convertToPainterUnits( 1, QgsUnitTypes::RenderMillimeters )
1185+
heights[i] = QgsTextRenderer::textHeight( context,
1186+
cellFormat,
1187+
QStringList() << col.heading(), QgsTextRenderer::Rect,
1188+
nullptr,
1189+
mWrapBehavior == WrapText ? Qgis::TextRendererFlag::WrapLines : Qgis::TextRendererFlags(),
1190+
context.convertToPainterUnits( mColumns.at( i ).width(), QgsUnitTypes::RenderMillimeters )
1191+
)
1192+
/ context.convertToPainterUnits( 1, QgsUnitTypes::RenderMillimeters )
12021193
- headerDescentMm;
12031194
}
12041195
i++;
@@ -1219,15 +1210,14 @@ bool QgsLayoutTable::calculateMaxRowHeights()
12191210
const double contentDescentMm = QgsTextRenderer::fontMetrics( context, cellFormat, QgsTextRenderer::FONT_WORKAROUND_SCALE ).descent() / QgsTextRenderer::FONT_WORKAROUND_SCALE / context.convertToPainterUnits( 1, QgsUnitTypes::RenderMillimeters );
12201211
const QString localizedString { QgsExpressionUtils::toLocalizedString( *colIt ) };
12211212

1222-
if ( textRequiresWrapping( context, localizedString, mColumns.at( i ).width(), cellFormat ) )
1223-
{
1224-
//contents too wide for cell, need to wrap
1225-
heights[ row * cols + i ] = QgsTextRenderer::textHeight( context, cellFormat, wrappedText( context, localizedString, mColumns.at( i ).width(), cellFormat ), QgsTextRenderer::Rect ) / context.convertToPainterUnits( 1, QgsUnitTypes::RenderMillimeters ) - contentDescentMm;
1226-
}
1227-
else
1228-
{
1229-
heights[ row * cols + i ] = QgsTextRenderer::textHeight( context, cellFormat, QStringList() << localizedString.split( '\n' ), QgsTextRenderer::Rect ) / context.convertToPainterUnits( 1, QgsUnitTypes::RenderMillimeters ) - contentDescentMm;
1230-
}
1213+
heights[ row * cols + i ] = QgsTextRenderer::textHeight( context,
1214+
cellFormat,
1215+
QStringList() << localizedString.split( '\n' ),
1216+
QgsTextRenderer::Rect,
1217+
nullptr,
1218+
mWrapBehavior == WrapText ? Qgis::TextRendererFlag::WrapLines : Qgis::TextRendererFlags(),
1219+
context.convertToPainterUnits( mColumns.at( i ).width(), QgsUnitTypes::RenderMillimeters )
1220+
) / context.convertToPainterUnits( 1, QgsUnitTypes::RenderMillimeters ) - contentDescentMm;
12311221

12321222
i++;
12331223
}
@@ -1375,83 +1365,6 @@ void QgsLayoutTable::drawHorizontalGridLines( QgsLayoutItemRenderContext &contex
13751365
painter->drawLine( QPointF( halfGridStrokeWidth, currentY ), QPointF( mTableSize.width() - halfGridStrokeWidth, currentY ) );
13761366
}
13771367

1378-
bool QgsLayoutTable::textRequiresWrapping( QgsRenderContext &context, const QString &text, double columnWidth, const QgsTextFormat &format ) const
1379-
{
1380-
if ( qgsDoubleNear( columnWidth, 0.0 ) || mWrapBehavior != WrapText )
1381-
return false;
1382-
1383-
const QStringList multiLineSplit = text.split( '\n' );
1384-
const double currentTextWidth = QgsTextRenderer::textWidth( context, format, multiLineSplit ) / context.convertToPainterUnits( 1, QgsUnitTypes::RenderMillimeters );
1385-
return currentTextWidth > columnWidth;
1386-
}
1387-
1388-
QStringList QgsLayoutTable::wrappedText( QgsRenderContext &context, const QString &value, double columnWidth, const QgsTextFormat &format ) const
1389-
{
1390-
QStringList lines = value.split( '\n' );
1391-
QStringList outLines;
1392-
const auto constLines = lines;
1393-
for ( const QString &line : constLines )
1394-
{
1395-
if ( textRequiresWrapping( context, line, columnWidth, format ) )
1396-
{
1397-
//first step is to identify words which must be on their own line (too long to fit)
1398-
QStringList words = line.split( ' ' );
1399-
QStringList linesToProcess;
1400-
QString wordsInCurrentLine;
1401-
const auto constWords = words;
1402-
for ( const QString &word : constWords )
1403-
{
1404-
if ( textRequiresWrapping( context, word, columnWidth, format ) )
1405-
{
1406-
//too long to fit
1407-
if ( !wordsInCurrentLine.isEmpty() )
1408-
linesToProcess << wordsInCurrentLine;
1409-
wordsInCurrentLine.clear();
1410-
linesToProcess << word;
1411-
}
1412-
else
1413-
{
1414-
if ( !wordsInCurrentLine.isEmpty() )
1415-
wordsInCurrentLine.append( ' ' );
1416-
wordsInCurrentLine.append( word );
1417-
}
1418-
}
1419-
if ( !wordsInCurrentLine.isEmpty() )
1420-
linesToProcess << wordsInCurrentLine;
1421-
1422-
const auto constLinesToProcess = linesToProcess;
1423-
for ( const QString &line : constLinesToProcess )
1424-
{
1425-
QString remainingText = line;
1426-
int lastPos = remainingText.lastIndexOf( ' ' );
1427-
while ( lastPos > -1 )
1428-
{
1429-
//check if remaining text is short enough to go in one line
1430-
if ( !textRequiresWrapping( context, remainingText, columnWidth, format ) )
1431-
{
1432-
break;
1433-
}
1434-
1435-
if ( !textRequiresWrapping( context, remainingText.left( lastPos ), columnWidth, format ) )
1436-
{
1437-
outLines << remainingText.left( lastPos );
1438-
remainingText = remainingText.mid( lastPos + 1 );
1439-
lastPos = 0;
1440-
}
1441-
lastPos = remainingText.lastIndexOf( ' ', lastPos - 1 );
1442-
}
1443-
outLines << remainingText;
1444-
}
1445-
}
1446-
else
1447-
{
1448-
outLines << line;
1449-
}
1450-
}
1451-
1452-
return outLines;
1453-
}
1454-
14551368
QColor QgsLayoutTable::backgroundColor( int row, int column ) const
14561369
{
14571370
QColor color = mBackgroundColor;

‎src/core/layout/qgslayouttable.h

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -771,10 +771,6 @@ class CORE_EXPORT QgsLayoutTable: public QgsLayoutMultiFrame
771771
//! Initializes cell style map
772772
void initStyles();
773773

774-
bool textRequiresWrapping( QgsRenderContext &context, const QString &text, double columnWidth, const QgsTextFormat &format ) const;
775-
776-
QStringList wrappedText( QgsRenderContext &context, const QString &value, double columnWidth, const QgsTextFormat &format ) const;
777-
778774
/**
779775
* Returns the calculated background color for a row and column combination.
780776
* \param row row number, where -1 is the header row, and 0 is the first body row

‎src/core/qgis.h

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1223,6 +1223,18 @@ class CORE_EXPORT Qgis
12231223
};
12241224
Q_ENUM( Capitalization )
12251225

1226+
/**
1227+
* Flags which control the behavior of rendering text.
1228+
*
1229+
* \since QGIS 3.24
1230+
*/
1231+
enum class TextRendererFlag : int
1232+
{
1233+
WrapLines = 1 << 0, //!< Automatically wrap long lines of text
1234+
};
1235+
Q_ENUM( TextRendererFlag )
1236+
Q_DECLARE_FLAGS( TextRendererFlags, TextRendererFlag )
1237+
Q_FLAG( TextRendererFlags )
12261238

12271239
/**
12281240
* Angular directions.
@@ -1367,6 +1379,7 @@ Q_DECLARE_OPERATORS_FOR_FLAGS( Qgis::MapSettingsFlags )
13671379
Q_DECLARE_OPERATORS_FOR_FLAGS( Qgis::RenderContextFlags )
13681380
Q_DECLARE_OPERATORS_FOR_FLAGS( Qgis::VectorLayerTypeFlags )
13691381
Q_DECLARE_OPERATORS_FOR_FLAGS( Qgis::MarkerLinePlacements )
1382+
Q_DECLARE_OPERATORS_FOR_FLAGS( Qgis::TextRendererFlags )
13701383

13711384

13721385
// hack to workaround warnings when casting void pointers

0 commit comments

Comments
 (0)