Skip to content

Commit

Permalink
[needs-docs][labeling] Fix substitutions don't play well with wrapped…
Browse files Browse the repository at this point in the history
… 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 234985b
Show file tree
Hide file tree
Showing 10 changed files with 263 additions and 114 deletions.
18 changes: 12 additions & 6 deletions python/core/auto_generated/qgspallabeling.sip.in
Expand Up @@ -159,6 +159,7 @@ class QgsPalLayerSettings

// text formatting
MultiLineWrapChar,
AutoWrapLength,
MultiLineHeight,
MultiLineAlignment,
DirSymbDraw,
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion python/core/auto_generated/qgsstringutils.sip.in
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions src/app/qgslabelinggui.cpp
Expand Up @@ -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 );

Expand Down Expand Up @@ -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();

Expand Down Expand Up @@ -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 );

Expand Down
50 changes: 46 additions & 4 deletions src/core/qgspallabeling.cpp
Expand Up @@ -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 ) },
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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( "<" ) );
Expand Down Expand Up @@ -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 );
Expand Down Expand Up @@ -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;
Expand All @@ -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();
Expand Down Expand Up @@ -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 );

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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() );

Expand Down Expand Up @@ -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' ) );
}
Expand All @@ -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;
}

Expand Down Expand Up @@ -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 ) )
Expand Down
36 changes: 31 additions & 5 deletions src/core/qgspallabeling.h
Expand Up @@ -269,6 +269,7 @@ class CORE_EXPORT QgsPalLayerSettings

// text formatting
MultiLineWrapChar = 31,
AutoWrapLength = 101,
MultiLineHeight = 32,
MultiLineAlignment = 33,
DirSymbDraw = 34,
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/core/qgsstringutils.h
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/core/qgsvectorlayerlabelprovider.cpp
Expand Up @@ -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 )
Expand Down
3 changes: 3 additions & 0 deletions src/gui/qgstextformatwidget.cpp
Expand Up @@ -456,10 +456,13 @@ void QgsTextFormatWidget::initWidget()
<< mShapeTypeDDBtn
<< mShowLabelDDBtn
<< mWrapCharDDBtn
<< mAutoWrapLengthDDBtn
<< mZIndexDDBtn
<< mZIndexSpinBox
<< spinBufferSize
<< wrapCharacterEdit
<< mAutoWrapLengthSpinBox
<< mAutoWrapTypeComboBox
<< mCentroidRadioVisible
<< mCentroidRadioWhole
<< mDirectSymbRadioBtnAbove
Expand Down

0 comments on commit 234985b

Please sign in to comment.