Skip to content

Commit 54a50cc

Browse files
committed
Fix Capitalize First Letter fails with curved labels (fix #14875)
Instead of using QFont's inbuilt capitalization support, which applies only on rendering and accordingly fails for curved labels which are drawn one character at a time, we now manually capitalize label text while registering features. The capitalize first method from Qt was reimplemented in QgsStringUtils (together with what I expect is better handling of unicode characters over the Qt method). This change also makes it possible to implement other capitalization methods not directly supported by Qt (cherry-picked from 15dd295)
1 parent 577913e commit 54a50cc

6 files changed

+186
-34
lines changed

python/core/qgsstringutils.sip

+19
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,25 @@ class QgsStringUtils
1010
#include <qgsstringutils.h>
1111
%End
1212
public:
13+
14+
15+
//! Capitalization options
16+
enum Capitalization
17+
{
18+
MixedCase, //!< Mixed case, ie no change
19+
AllUppercase, //!< Convert all characters to uppercase
20+
AllLowercase, //!< Convert all characters to lowercase
21+
ForceFirstLetterToCapital, //!< Convert just the first letter of each word to uppercase, leave the rest untouched
22+
};
23+
24+
/** Converts a string by applying capitalization rules to the string.
25+
* @param string input string
26+
* @param capitalization capitalization type to apply
27+
* @return capitalized string
28+
* @note added in QGIS 3.0
29+
*/
30+
static QString capitalize( const QString& string, Capitalization capitalization );
31+
1332
/** Returns the Levenshtein edit distance between two strings. This equates to the minimum
1433
* number of character edits (insertions, deletions or substitutions) required to change
1534
* one string to another.

src/core/qgspallabeling.cpp

+37-34
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
#include "qgspallabeling.h"
1919
#include "qgstextlabelfeature.h"
2020
#include "qgsunittypes.h"
21+
#include "qgsstringutils.h"
2122

2223
#include <list>
2324

@@ -2283,6 +2284,7 @@ void QgsPalLayerSettings::registerFeature( QgsFeature& f, QgsRenderContext &cont
22832284

22842285
// calculate rest of font attributes and store any data defined values
22852286
// this is done here for later use in making label backgrounds part of collision management (when implemented)
2287+
labelFont.setCapitalization( QFont::MixedCase ); // reset this - we don't use QFont's handling as it breaks with curved labels
22862288
parseTextStyle( labelFont, fontunits, context );
22872289
parseTextFormatting( context );
22882290
parseTextBuffer( context );
@@ -2316,6 +2318,41 @@ void QgsPalLayerSettings::registerFeature( QgsFeature& f, QgsRenderContext &cont
23162318
labelText = v.isNull() ? "" : v.toString();
23172319
}
23182320

2321+
// apply capitalization
2322+
QgsStringUtils::Capitalization capitalization = QgsStringUtils::MixedCase;
2323+
// maintain API - capitalization may have been set in textFont
2324+
if ( textFont.capitalization() != QFont::MixedCase )
2325+
{
2326+
capitalization = static_cast< QgsStringUtils::Capitalization >( textFont.capitalization() );
2327+
}
2328+
// data defined font capitalization?
2329+
if ( dataDefinedEvaluate( QgsPalLayerSettings::FontCase, exprVal, &context.expressionContext() ) )
2330+
{
2331+
QString fcase = exprVal.toString().trimmed();
2332+
QgsDebugMsgLevel( QString( "exprVal FontCase:%1" ).arg( fcase ), 4 );
2333+
2334+
if ( !fcase.isEmpty() )
2335+
{
2336+
if ( fcase.compare( "NoChange", Qt::CaseInsensitive ) == 0 )
2337+
{
2338+
capitalization = QgsStringUtils::MixedCase;
2339+
}
2340+
else if ( fcase.compare( "Upper", Qt::CaseInsensitive ) == 0 )
2341+
{
2342+
capitalization = QgsStringUtils::AllUppercase;
2343+
}
2344+
else if ( fcase.compare( "Lower", Qt::CaseInsensitive ) == 0 )
2345+
{
2346+
capitalization = QgsStringUtils::AllLowercase;
2347+
}
2348+
else if ( fcase.compare( "Capitalize", Qt::CaseInsensitive ) == 0 )
2349+
{
2350+
capitalization = QgsStringUtils::ForceFirstLetterToCapital;
2351+
}
2352+
}
2353+
}
2354+
labelText = QgsStringUtils::capitalize( labelText, capitalization );
2355+
23192356
// data defined format numbers?
23202357
bool formatnum = formatNumbers;
23212358
if ( dataDefinedEvaluate( QgsPalLayerSettings::NumFormat, exprVal, &context.expressionContext(), formatNumbers ) )
@@ -3356,7 +3393,6 @@ void QgsPalLayerSettings::parseTextStyle( QFont& labelFont,
33563393
// copy over existing font settings
33573394
//newFont = newFont.resolve( labelFont ); // should work, but let's be sure what's being copied
33583395
newFont.setPixelSize( labelFont.pixelSize() );
3359-
newFont.setCapitalization( labelFont.capitalization() );
33603396
newFont.setUnderline( labelFont.underline() );
33613397
newFont.setStrikeOut( labelFont.strikeOut() );
33623398
newFont.setWordSpacing( labelFont.wordSpacing() );
@@ -3393,39 +3429,6 @@ void QgsPalLayerSettings::parseTextStyle( QFont& labelFont,
33933429
}
33943430
labelFont.setLetterSpacing( QFont::AbsoluteSpacing, scaleToPixelContext( letterspace, context, fontunits, false, fontSizeMapUnitScale ) );
33953431

3396-
// data defined font capitalization?
3397-
QFont::Capitalization fontcaps = labelFont.capitalization();
3398-
if ( dataDefinedEvaluate( QgsPalLayerSettings::FontCase, exprVal, &context.expressionContext() ) )
3399-
{
3400-
QString fcase = exprVal.toString().trimmed();
3401-
QgsDebugMsgLevel( QString( "exprVal FontCase:%1" ).arg( fcase ), 4 );
3402-
3403-
if ( !fcase.isEmpty() )
3404-
{
3405-
if ( fcase.compare( "NoChange", Qt::CaseInsensitive ) == 0 )
3406-
{
3407-
fontcaps = QFont::MixedCase;
3408-
}
3409-
else if ( fcase.compare( "Upper", Qt::CaseInsensitive ) == 0 )
3410-
{
3411-
fontcaps = QFont::AllUppercase;
3412-
}
3413-
else if ( fcase.compare( "Lower", Qt::CaseInsensitive ) == 0 )
3414-
{
3415-
fontcaps = QFont::AllLowercase;
3416-
}
3417-
else if ( fcase.compare( "Capitalize", Qt::CaseInsensitive ) == 0 )
3418-
{
3419-
fontcaps = QFont::Capitalize;
3420-
}
3421-
3422-
if ( fontcaps != labelFont.capitalization() )
3423-
{
3424-
labelFont.setCapitalization( fontcaps );
3425-
}
3426-
}
3427-
}
3428-
34293432
// data defined strikeout font style?
34303433
if ( dataDefinedEvaluate( QgsPalLayerSettings::Strikeout, exprVal, &context.expressionContext(), labelFont.strikeOut() ) )
34313434
{

src/core/qgsstringutils.cpp

+50
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,56 @@
1515

1616
#include "qgsstringutils.h"
1717
#include <QVector>
18+
#include <QRegExp>
19+
#include <QStringList>
20+
#include <QTextBoundaryFinder>
21+
22+
QString QgsStringUtils::capitalize( const QString& string, QgsStringUtils::Capitalization capitalization )
23+
{
24+
if ( string.isEmpty() )
25+
return QString();
26+
27+
switch ( capitalization )
28+
{
29+
case MixedCase:
30+
return string;
31+
32+
case AllUppercase:
33+
return string.toUpper();
34+
35+
case AllLowercase:
36+
return string.toLower();
37+
38+
case ForceFirstLetterToCapital:
39+
{
40+
QString temp = string;
41+
42+
QTextBoundaryFinder wordSplitter( QTextBoundaryFinder::Word, string.constData(), string.length(), 0, 0 );
43+
QTextBoundaryFinder letterSplitter( QTextBoundaryFinder::Grapheme, string.constData(), string.length(), 0, 0 );
44+
45+
wordSplitter.setPosition( 0 );
46+
bool first = true;
47+
#if QT_VERSION >= 0x050000
48+
while (( first && wordSplitter.boundaryReasons() & QTextBoundaryFinder::StartOfItem )
49+
|| wordSplitter.toNextBoundary() >= 0 )
50+
#else
51+
while (( first && wordSplitter.boundaryReasons() & QTextBoundaryFinder::StartWord )
52+
|| wordSplitter.toNextBoundary() >= 0 )
53+
#endif
54+
{
55+
first = false;
56+
letterSplitter.setPosition( wordSplitter.position() );
57+
letterSplitter.toNextBoundary();
58+
QString substr = string.mid( wordSplitter.position(), letterSplitter.position() - wordSplitter.position() );
59+
temp.replace( wordSplitter.position(), substr.length(), substr.toUpper() );
60+
}
61+
return temp;
62+
}
63+
64+
}
65+
// no warnings
66+
return string;
67+
}
1868

1969
int QgsStringUtils::levenshteinDistance( const QString& string1, const QString& string2, bool caseSensitive )
2070
{

src/core/qgsstringutils.h

+18
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
***************************************************************************/
1515

1616
#include <QString>
17+
#include <QFont> // for enum values
1718

1819
#ifndef QGSSTRINGUTILS_H
1920
#define QGSSTRINGUTILS_H
@@ -28,6 +29,23 @@ class CORE_EXPORT QgsStringUtils
2829
{
2930
public:
3031

32+
//! Capitalization options
33+
enum Capitalization
34+
{
35+
MixedCase = QFont::MixedCase, //!< Mixed case, ie no change
36+
AllUppercase = QFont::AllUppercase, //!< Convert all characters to uppercase
37+
AllLowercase = QFont::AllLowercase, //!< Convert all characters to lowercase
38+
ForceFirstLetterToCapital = QFont::Capitalize, //!< Convert just the first letter of each word to uppercase, leave the rest untouched
39+
};
40+
41+
/** Converts a string by applying capitalization rules to the string.
42+
* @param string input string
43+
* @param capitalization capitalization type to apply
44+
* @return capitalized string
45+
* @note added in QGIS 3.0
46+
*/
47+
static QString capitalize( const QString& string, Capitalization capitalization );
48+
3149
/** Returns the Levenshtein edit distance between two strings. This equates to the minimum
3250
* number of character edits (insertions, deletions or substitutions) required to change
3351
* one string to another.

src/core/qgsvectorlayerlabelprovider.h

+2
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,8 @@ class CORE_EXPORT QgsVectorLayerLabelProvider : public QgsAbstractLabelProvider
118118

119119
//! List of generated
120120
QList<QgsLabelFeature*> mLabels;
121+
122+
friend class TestQgsLabelingEngineV2;
121123
};
122124

123125
#endif // QGSVECTORLAYERLABELPROVIDER_H

tests/src/core/testqgslabelingenginev2.cpp

+60
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ class TestQgsLabelingEngineV2 : public QObject
4343
void testRuleBased();
4444
void zOrder(); //test that labels are stacked correctly
4545
void testEncodeDecodePositionOrder();
46+
void testCapitalization();
4647

4748
private:
4849
QgsVectorLayer* vl;
@@ -413,6 +414,65 @@ void TestQgsLabelingEngineV2::testEncodeDecodePositionOrder()
413414
QCOMPARE( decoded, expected );
414415
}
415416

417+
void TestQgsLabelingEngineV2::testCapitalization()
418+
{
419+
QgsFeature f( vl->fields(), 1 );
420+
f.setGeometry( QgsGeometry::fromPoint( QgsPoint( 1, 2 ) ) );
421+
422+
// make a fake render context
423+
QSize size( 640, 480 );
424+
QgsMapSettings mapSettings;
425+
mapSettings.setOutputSize( size );
426+
mapSettings.setExtent( vl->extent() );
427+
mapSettings.setLayers( QStringList() << vl->id() );
428+
mapSettings.setOutputDpi( 96 );
429+
QgsRenderContext context = QgsRenderContext::fromMapSettings( mapSettings );
430+
QStringList attributes;
431+
QgsLabelingEngineV2 engine;
432+
engine.setMapSettings( mapSettings );
433+
434+
// no change
435+
QgsPalLayerSettings settings;
436+
QFont font = settings.textFont;
437+
font.setCapitalization( QFont::MixedCase );
438+
settings.textFont = font;
439+
settings.fieldName = QString( "'a teSt LABEL'" );
440+
settings.isExpression = true;
441+
442+
QgsVectorLayerLabelProvider* provider = new QgsVectorLayerLabelProvider( vl, "test", true, &settings );
443+
engine.addProvider( provider );
444+
provider->prepare( context, attributes );
445+
provider->registerFeature( f, context );
446+
QCOMPARE( provider->mLabels.at( 0 )->labelText(), QString( "a teSt LABEL" ) );
447+
448+
//uppercase
449+
font.setCapitalization( QFont::AllUppercase );
450+
settings.textFont = font;
451+
QgsVectorLayerLabelProvider* provider2 = new QgsVectorLayerLabelProvider( vl, "test2", true, &settings );
452+
engine.addProvider( provider2 );
453+
provider2->prepare( context, attributes );
454+
provider2->registerFeature( f, context );
455+
QCOMPARE( provider2->mLabels.at( 0 )->labelText(), QString( "A TEST LABEL" ) );
456+
457+
//lowercase
458+
font.setCapitalization( QFont::AllLowercase );
459+
settings.textFont = font;
460+
QgsVectorLayerLabelProvider* provider3 = new QgsVectorLayerLabelProvider( vl, "test3", true, &settings );
461+
engine.addProvider( provider3 );
462+
provider3->prepare( context, attributes );
463+
provider3->registerFeature( f, context );
464+
QCOMPARE( provider3->mLabels.at( 0 )->labelText(), QString( "a test label" ) );
465+
466+
//first letter uppercase
467+
font.setCapitalization( QFont::Capitalize );
468+
settings.textFont = font;
469+
QgsVectorLayerLabelProvider* provider4 = new QgsVectorLayerLabelProvider( vl, "test4", true, &settings );
470+
engine.addProvider( provider4 );
471+
provider4->prepare( context, attributes );
472+
provider4->registerFeature( f, context );
473+
QCOMPARE( provider4->mLabels.at( 0 )->labelText(), QString( "A TeSt LABEL" ) );
474+
}
475+
416476
bool TestQgsLabelingEngineV2::imageCheck( const QString& testName, QImage &image, int mismatchCount )
417477
{
418478
//draw background

0 commit comments

Comments
 (0)