Skip to content
Permalink
Browse files

[needs-docs][labeling] Fix substitutions don't play well with wrapped…

… labels

Fixes an issue identified in the upcoming QGIS Map Design 2nd ed
(spoiler alert!) where it's impossible to utilise label text
substitutions if you also want to use word wrapping.

This isn't possible to directly fix, because we need to evaluate
the full label expression (including the word wrapping component)
in order to actually HAVE text to substitute into.

So, a new setting has been added to the label formatting tab
allowing users to directly set an auto-wrapping line ideal
line size. This is applied AFTER label text evaluation, substitutions,
and the 'wrap text on' character, so it can play correctly well with
all these other settings. This also has the nice side-effect
of making auto label text wrapping more accessible to new
users/those unfamiliar with the wordwrap expression function.

Fixes #20007, and cleans up a chapter of QMD 2ed ;)
  • Loading branch information
nyalldawson committed Oct 4, 2018
1 parent dce8673 commit 234985b59d67e0741763ae32f6913b4c45f01ec4
@@ -159,6 +159,7 @@ class QgsPalLayerSettings

// text formatting
MultiLineWrapChar,
AutoWrapLength,
MultiLineHeight,
MultiLineAlignment,
DirSymbDraw,
@@ -283,6 +284,10 @@ Returns the QgsExpression for this label settings. May be None if isExpression i

QString wrapChar;

int autoWrapLength;

bool useMaxLineLengthForAutoWrap;

MultiLineAlign multilineAlign;

bool addDirectionSymbol;
@@ -559,15 +564,16 @@ Checks whether a geometry requires preparation before registration with PAL
.. versionadded:: 2.9
%End

static QStringList splitToLines( const QString &text, const QString &wrapCharacter );
static QStringList splitToLines( const QString &text, const QString &wrapCharacter, int autoWrapLength = 0, bool useMaxLineLengthWhenAutoWrapping = true );
%Docstring
Splits a text string to a list of separate lines, using a specified wrap character.
Splits a ``text`` string to a list of separate lines, using a specified wrap character (``wrapCharacter``).
The text string will be split on either newline characters or the wrap character.

:param text: text string to split
:param wrapCharacter: additional character to wrap on

:return: list of text split to lines
Since QGIS 3.4 the ``autoWrapLength`` argument can be used to specify an ideal length of line to automatically
wrap text to (automatic wrapping is disabled if ``autoWrapLength`` is 0). This automatic wrapping is performed
after processing wrapping using ``wrapCharacter``. When auto wrapping is enabled, the ``useMaxLineLengthWhenAutoWrapping``
argument controls whether the lines should be wrapped to an ideal maximum of ``autoWrapLength`` characters, or
if false then the lines are wrapped to an ideal minimum length of ``autoWrapLength`` characters.

.. versionadded:: 2.9
%End
@@ -264,7 +264,7 @@ links.

static QString wordWrap( const QString &string, int length, bool useMaxLineLength = true, const QString &customDelimiter = QString() );
%Docstring
Automatically wraps a \string by inserting new line characters at appropriate locations in the string.
Automatically wraps a ``string`` by inserting new line characters at appropriate locations in the string.

The ``length`` argument specifies either the minimum or maximum length of lines desired, depending
on whether ``useMaxLineLength`` is true. If ``useMaxLineLength`` is true, then the string will be wrapped
@@ -247,6 +247,8 @@ void QgsLabelingGui::setLayer( QgsMapLayer *mapLayer )
mMaxCharAngleOutDSpinBox->setValue( std::fabs( lyr.maxCurvedCharAngleOut ) );

wrapCharacterEdit->setText( lyr.wrapChar );
mAutoWrapLengthSpinBox->setValue( lyr.autoWrapLength );
mAutoWrapTypeComboBox->setCurrentIndex( lyr.useMaxLineLengthForAutoWrap ? 0 : 1 );
mFontMultiLineAlignComboBox->setCurrentIndex( ( unsigned int ) lyr.multilineAlign );
chkPreserveRotation->setChecked( lyr.preserveRotation );

@@ -435,6 +437,8 @@ QgsPalLayerSettings QgsLabelingGui::layerSettings()
lyr.fontMinPixelSize = mFontMinPixelSpinBox->value();
lyr.fontMaxPixelSize = mFontMaxPixelSpinBox->value();
lyr.wrapChar = wrapCharacterEdit->text();
lyr.autoWrapLength = mAutoWrapLengthSpinBox->value();
lyr.useMaxLineLengthForAutoWrap = mAutoWrapTypeComboBox->currentIndex() == 0;
lyr.multilineAlign = ( QgsPalLayerSettings::MultiLineAlign ) mFontMultiLineAlignComboBox->currentIndex();
lyr.preserveRotation = chkPreserveRotation->isChecked();

@@ -465,6 +469,7 @@ void QgsLabelingGui::populateDataDefinedButtons()

// text formatting
registerDataDefinedButton( mWrapCharDDBtn, QgsPalLayerSettings::MultiLineWrapChar );
registerDataDefinedButton( mAutoWrapLengthDDBtn, QgsPalLayerSettings::AutoWrapLength );
registerDataDefinedButton( mFontLineHeightDDBtn, QgsPalLayerSettings::MultiLineHeight );
registerDataDefinedButton( mFontMultiLineAlignDDBtn, QgsPalLayerSettings::MultiLineAlignment );

@@ -124,6 +124,7 @@ void QgsPalLayerSettings::initPropertyDefinitions()
{ QgsPalLayerSettings::FontWordSpacing, QgsPropertyDefinition( "FontWordSpacing", QObject::tr( "Word spacing" ), QgsPropertyDefinition::Double, origin ) },
{ QgsPalLayerSettings::FontBlendMode, QgsPropertyDefinition( "FontBlendMode", QObject::tr( "Text blend mode" ), QgsPropertyDefinition::BlendMode, origin ) },
{ QgsPalLayerSettings::MultiLineWrapChar, QgsPropertyDefinition( "MultiLineWrapChar", QObject::tr( "Wrap character" ), QgsPropertyDefinition::String, origin ) },
{ QgsPalLayerSettings::AutoWrapLength, QgsPropertyDefinition( "AutoWrapLength", QObject::tr( "Automatic word wrap line length" ), QgsPropertyDefinition::IntegerPositive, origin ) },
{ QgsPalLayerSettings::MultiLineHeight, QgsPropertyDefinition( "MultiLineHeight", QObject::tr( "Line height" ), QgsPropertyDefinition::DoublePositive, origin ) },
{ QgsPalLayerSettings::MultiLineAlignment, QgsPropertyDefinition( "MultiLineAlignment", QgsPropertyDefinition::DataTypeString, QObject::tr( "Line alignment" ), QObject::tr( "string " ) + "[<b>Left</b>|<b>Center</b>|<b>Right</b>|<b>Follow</b>]", origin ) },
{ QgsPalLayerSettings::DirSymbDraw, QgsPropertyDefinition( "DirSymbDraw", QObject::tr( "Draw direction symbol" ), QgsPropertyDefinition::Boolean, origin ) },
@@ -321,6 +322,8 @@ QgsPalLayerSettings &QgsPalLayerSettings::operator=( const QgsPalLayerSettings &

// text formatting
wrapChar = s.wrapChar;
autoWrapLength = s.autoWrapLength;
useMaxLineLengthForAutoWrap = s.useMaxLineLengthForAutoWrap;
multilineAlign = s.multilineAlign;
addDirectionSymbol = s.addDirectionSymbol;
leftDirectionSymbol = s.leftDirectionSymbol;
@@ -532,6 +535,9 @@ void QgsPalLayerSettings::readFromLayerCustomProperties( QgsVectorLayer *layer )

// text formatting
wrapChar = layer->customProperty( QStringLiteral( "labeling/wrapChar" ) ).toString();
autoWrapLength = layer->customProperty( QStringLiteral( "labeling/autoWrapLength" ) ).toInt();
useMaxLineLengthForAutoWrap = layer->customProperty( QStringLiteral( "labeling/useMaxLineLengthForAutoWrap" ), QStringLiteral( "1" ) ).toBool();

multilineAlign = static_cast< MultiLineAlign >( layer->customProperty( QStringLiteral( "labeling/multilineAlign" ), QVariant( MultiFollowPlacement ) ).toUInt() );
addDirectionSymbol = layer->customProperty( QStringLiteral( "labeling/addDirectionSymbol" ) ).toBool();
leftDirectionSymbol = layer->customProperty( QStringLiteral( "labeling/leftDirectionSymbol" ), QVariant( "<" ) ).toString();
@@ -739,6 +745,8 @@ void QgsPalLayerSettings::readXml( QDomElement &elem, const QgsReadWriteContext
// text formatting
QDomElement textFormatElem = elem.firstChildElement( QStringLiteral( "text-format" ) );
wrapChar = textFormatElem.attribute( QStringLiteral( "wrapChar" ) );
autoWrapLength = textFormatElem.attribute( QStringLiteral( "autoWrapLength" ), QStringLiteral( "0" ) ).toInt();
useMaxLineLengthForAutoWrap = textFormatElem.attribute( QStringLiteral( "useMaxLineLengthForAutoWrap" ), QStringLiteral( "1" ) ).toInt();
multilineAlign = static_cast< MultiLineAlign >( textFormatElem.attribute( QStringLiteral( "multilineAlign" ), QString::number( MultiFollowPlacement ) ).toUInt() );
addDirectionSymbol = textFormatElem.attribute( QStringLiteral( "addDirectionSymbol" ) ).toInt();
leftDirectionSymbol = textFormatElem.attribute( QStringLiteral( "leftDirectionSymbol" ), QStringLiteral( "<" ) );
@@ -953,6 +961,8 @@ QDomElement QgsPalLayerSettings::writeXml( QDomDocument &doc, const QgsReadWrite
// text formatting
QDomElement textFormatElem = doc.createElement( QStringLiteral( "text-format" ) );
textFormatElem.setAttribute( QStringLiteral( "wrapChar" ), wrapChar );
textFormatElem.setAttribute( QStringLiteral( "autoWrapLength" ), autoWrapLength );
textFormatElem.setAttribute( QStringLiteral( "useMaxLineLengthForAutoWrap" ), useMaxLineLengthForAutoWrap );
textFormatElem.setAttribute( QStringLiteral( "multilineAlign" ), static_cast< unsigned int >( multilineAlign ) );
textFormatElem.setAttribute( QStringLiteral( "addDirectionSymbol" ), addDirectionSymbol );
textFormatElem.setAttribute( QStringLiteral( "leftDirectionSymbol" ), leftDirectionSymbol );
@@ -1046,6 +1056,7 @@ void QgsPalLayerSettings::calculateLabelSize( const QFontMetricsF *fm, QString t
QgsRenderContext *rc = context ? context : scopedRc.get();

QString wrapchr = wrapChar;
int evalAutoWrapLength = autoWrapLength;
double multilineH = mFormat.lineHeight();

bool addDirSymb = addDirectionSymbol;
@@ -1060,6 +1071,11 @@ void QgsPalLayerSettings::calculateLabelSize( const QFontMetricsF *fm, QString t
wrapchr = dataDefinedValues.value( QgsPalLayerSettings::MultiLineWrapChar ).toString();
}

if ( dataDefinedValues.contains( QgsPalLayerSettings::AutoWrapLength ) )
{
evalAutoWrapLength = dataDefinedValues.value( QgsPalLayerSettings::AutoWrapLength, evalAutoWrapLength ).toInt();
}

if ( dataDefinedValues.contains( QgsPalLayerSettings::MultiLineHeight ) )
{
multilineH = dataDefinedValues.value( QgsPalLayerSettings::MultiLineHeight ).toDouble();
@@ -1095,6 +1111,9 @@ void QgsPalLayerSettings::calculateLabelSize( const QFontMetricsF *fm, QString t
rc->expressionContext().setOriginalValueVariable( wrapChar );
wrapchr = mDataDefinedProperties.value( QgsPalLayerSettings::MultiLineWrapChar, rc->expressionContext(), wrapchr ).toString();

rc->expressionContext().setOriginalValueVariable( evalAutoWrapLength );
evalAutoWrapLength = mDataDefinedProperties.value( QgsPalLayerSettings::AutoWrapLength, rc->expressionContext(), evalAutoWrapLength ).toInt();

rc->expressionContext().setOriginalValueVariable( multilineH );
multilineH = mDataDefinedProperties.valueAsDouble( QgsPalLayerSettings::MultiLineHeight, rc->expressionContext(), multilineH );

@@ -1140,7 +1159,7 @@ void QgsPalLayerSettings::calculateLabelSize( const QFontMetricsF *fm, QString t
}

double w = 0.0, h = 0.0;
QStringList multiLineSplit = QgsPalLabeling::splitToLines( text, wrapchr );
QStringList multiLineSplit = QgsPalLabeling::splitToLines( text, wrapchr, evalAutoWrapLength, useMaxLineLengthForAutoWrap );
int lines = multiLineSplit.size();

double labelHeight = fm->ascent() + fm->descent(); // ignore +1 for baseline
@@ -2422,6 +2441,12 @@ void QgsPalLayerSettings::parseTextFormatting( QgsRenderContext &context )
wrapchr = exprVal.toString();
}

int evalAutoWrapLength = autoWrapLength;
if ( dataDefinedValEval( DDInt, QgsPalLayerSettings::AutoWrapLength, exprVal, context.expressionContext(), evalAutoWrapLength ) )
{
evalAutoWrapLength = exprVal.toInt();
}

// data defined multiline height?
dataDefinedValEval( DDDouble, QgsPalLayerSettings::MultiLineHeight, exprVal, context.expressionContext() );

@@ -2844,13 +2869,14 @@ bool QgsPalLabeling::geometryRequiresPreparation( const QgsGeometry &geometry, Q
return false;
}

QStringList QgsPalLabeling::splitToLines( const QString &text, const QString &wrapCharacter )
QStringList QgsPalLabeling::splitToLines( const QString &text, const QString &wrapCharacter, const int autoWrapLength, const bool useMaxLineLengthWhenAutoWrapping )
{
QStringList multiLineSplit;
if ( !wrapCharacter.isEmpty() && wrapCharacter != QLatin1String( "\n" ) )
{
//wrap on both the wrapchr and new line characters
Q_FOREACH ( const QString &line, text.split( wrapCharacter ) )
const QStringList lines = text.split( wrapCharacter );
for ( const QString &line : lines )
{
multiLineSplit.append( line.split( '\n' ) );
}
@@ -2860,6 +2886,17 @@ QStringList QgsPalLabeling::splitToLines( const QString &text, const QString &wr
multiLineSplit = text.split( '\n' );
}

// apply auto wrapping to each manually created line
if ( autoWrapLength != 0 )
{
QStringList autoWrappedLines;
autoWrappedLines.reserve( multiLineSplit.count() );
for ( const QString &line : qgis::as_const( multiLineSplit ) )
{
autoWrappedLines.append( QgsStringUtils::wordWrap( line, autoWrapLength, useMaxLineLengthWhenAutoWrapping ).split( '\n' ) );
}
multiLineSplit = autoWrappedLines;
}
return multiLineSplit;
}

@@ -3044,7 +3081,12 @@ void QgsPalLabeling::dataDefinedTextFormatting( QgsPalLayerSettings &tmpLyr,
tmpLyr.wrapChar = ddValues.value( QgsPalLayerSettings::MultiLineWrapChar ).toString();
}

if ( !tmpLyr.wrapChar.isEmpty() || tmpLyr.getLabelExpression()->expression().contains( QLatin1String( "wordwrap" ) ) )
if ( ddValues.contains( QgsPalLayerSettings::AutoWrapLength ) )
{
tmpLyr.autoWrapLength = ddValues.value( QgsPalLayerSettings::AutoWrapLength ).toInt();
}

if ( !tmpLyr.wrapChar.isEmpty() || tmpLyr.getLabelExpression()->expression().contains( QLatin1String( "wordwrap" ) ) || tmpLyr.autoWrapLength > 0 )
{

if ( ddValues.contains( QgsPalLayerSettings::MultiLineHeight ) )
@@ -269,6 +269,7 @@ class CORE_EXPORT QgsPalLayerSettings

// text formatting
MultiLineWrapChar = 31,
AutoWrapLength = 101,
MultiLineHeight = 32,
MultiLineAlignment = 33,
DirSymbDraw = 34,
@@ -417,6 +418,27 @@ class CORE_EXPORT QgsPalLayerSettings
*/
QString wrapChar;

/**
* If non-zero, indicates that label text should be automatically wrapped to (ideally) the specified
* number of characters. If zero, auto wrapping is disabled.
*
* \see useMaxLineLengthForAutoWrap
* \since QGIS 3.4
*/
int autoWrapLength = 0;

/**
* If true, indicates that when auto wrapping label text the autoWrapLength length indicates the maximum
* ideal length of text lines. If false, then autoWrapLength indicates the ideal minimum length of text
* lines.
*
* If autoWrapLength is 0 then this value has no effect.
*
* \see autoWrapLength
* \since QGIS 3.4
*/
bool useMaxLineLengthForAutoWrap = true;

//! Horizontal alignment of multi-line labels.
MultiLineAlign multilineAlign;

@@ -986,14 +1008,18 @@ class CORE_EXPORT QgsPalLabeling
static bool geometryRequiresPreparation( const QgsGeometry &geometry, QgsRenderContext &context, const QgsCoordinateTransform &ct, const QgsGeometry &clipGeometry = QgsGeometry() );

/**
* Splits a text string to a list of separate lines, using a specified wrap character.
* Splits a \a text string to a list of separate lines, using a specified wrap character (\a wrapCharacter).
* The text string will be split on either newline characters or the wrap character.
* \param text text string to split
* \param wrapCharacter additional character to wrap on
* \returns list of text split to lines
*
* Since QGIS 3.4 the \a autoWrapLength argument can be used to specify an ideal length of line to automatically
* wrap text to (automatic wrapping is disabled if \a autoWrapLength is 0). This automatic wrapping is performed
* after processing wrapping using \a wrapCharacter. When auto wrapping is enabled, the \a useMaxLineLengthWhenAutoWrapping
* argument controls whether the lines should be wrapped to an ideal maximum of \a autoWrapLength characters, or
* if false then the lines are wrapped to an ideal minimum length of \a autoWrapLength characters.
*
* \since QGIS 2.9
*/
static QStringList splitToLines( const QString &text, const QString &wrapCharacter );
static QStringList splitToLines( const QString &text, const QString &wrapCharacter, int autoWrapLength = 0, bool useMaxLineLengthWhenAutoWrapping = true );

/**
* Splits a text string to a list of graphemes, which are the smallest allowable character
@@ -262,7 +262,7 @@ class CORE_EXPORT QgsStringUtils
static QString insertLinks( const QString &string, bool *foundLinks = nullptr );

/**
* Automatically wraps a \string by inserting new line characters at appropriate locations in the string.
* Automatically wraps a \a string by inserting new line characters at appropriate locations in the string.
*
* The \a length argument specifies either the minimum or maximum length of lines desired, depending
* on whether \a useMaxLineLength is true. If \a useMaxLineLength is true, then the string will be wrapped
@@ -612,7 +612,7 @@ void QgsVectorLayerLabelProvider::drawLabelPrivate( pal::LabelPosition *label, Q
}

//QgsDebugMsgLevel( "drawLabel " + txt, 4 );
QStringList multiLineList = QgsPalLabeling::splitToLines( txt, tmpLyr.wrapChar );
QStringList multiLineList = QgsPalLabeling::splitToLines( txt, tmpLyr.wrapChar, tmpLyr.autoWrapLength, tmpLyr.useMaxLineLengthForAutoWrap );

QgsTextRenderer::HAlignment hAlign = QgsTextRenderer::AlignLeft;
if ( tmpLyr.multilineAlign == QgsPalLayerSettings::MultiCenter )
@@ -456,10 +456,13 @@ void QgsTextFormatWidget::initWidget()
<< mShapeTypeDDBtn
<< mShowLabelDDBtn
<< mWrapCharDDBtn
<< mAutoWrapLengthDDBtn
<< mZIndexDDBtn
<< mZIndexSpinBox
<< spinBufferSize
<< wrapCharacterEdit
<< mAutoWrapLengthSpinBox
<< mAutoWrapTypeComboBox
<< mCentroidRadioVisible
<< mCentroidRadioWhole
<< mDirectSymbRadioBtnAbove

0 comments on commit 234985b

Please sign in to comment.
You can’t perform that action at this time.