mirror of
https://github.com/qgis/QGIS.git
synced 2025-06-19 00:02:48 -04:00
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
This commit is contained in:
parent
0658640fa0
commit
15dd29564c
@ -122,6 +122,25 @@ class QgsStringUtils
|
||||
#include <qgsstringutils.h>
|
||||
%End
|
||||
public:
|
||||
|
||||
|
||||
//! Capitalization options
|
||||
enum Capitalization
|
||||
{
|
||||
MixedCase, //!< Mixed case, ie no change
|
||||
AllUppercase, //!< Convert all characters to uppercase
|
||||
AllLowercase, //!< Convert all characters to lowercase
|
||||
ForceFirstLetterToCapital, //!< Convert just the first letter of each word to uppercase, leave the rest untouched
|
||||
};
|
||||
|
||||
/** Converts a string by applying capitalization rules to the string.
|
||||
* @param string input string
|
||||
* @param capitalization capitalization type to apply
|
||||
* @return capitalized string
|
||||
* @note added in QGIS 3.0
|
||||
*/
|
||||
static QString capitalize( const QString& string, Capitalization capitalization );
|
||||
|
||||
/** Returns the Levenshtein edit distance between two strings. This equates to the minimum
|
||||
* number of character edits (insertions, deletions or substitutions) required to change
|
||||
* one string to another.
|
||||
|
@ -2302,6 +2302,7 @@ void QgsPalLayerSettings::registerFeature( QgsFeature& f, QgsRenderContext &cont
|
||||
|
||||
// calculate rest of font attributes and store any data defined values
|
||||
// this is done here for later use in making label backgrounds part of collision management (when implemented)
|
||||
labelFont.setCapitalization( QFont::MixedCase ); // reset this - we don't use QFont's handling as it breaks with curved labels
|
||||
parseTextStyle( labelFont, fontunits, context );
|
||||
parseTextFormatting( context );
|
||||
parseTextBuffer( context );
|
||||
@ -2340,6 +2341,41 @@ void QgsPalLayerSettings::registerFeature( QgsFeature& f, QgsRenderContext &cont
|
||||
labelText = substitutions.process( labelText );
|
||||
}
|
||||
|
||||
// apply capitalization
|
||||
QgsStringUtils::Capitalization capitalization = QgsStringUtils::MixedCase;
|
||||
// maintain API - capitalization may have been set in textFont
|
||||
if ( textFont.capitalization() != QFont::MixedCase )
|
||||
{
|
||||
capitalization = static_cast< QgsStringUtils::Capitalization >( textFont.capitalization() );
|
||||
}
|
||||
// data defined font capitalization?
|
||||
if ( dataDefinedEvaluate( QgsPalLayerSettings::FontCase, exprVal, &context.expressionContext() ) )
|
||||
{
|
||||
QString fcase = exprVal.toString().trimmed();
|
||||
QgsDebugMsgLevel( QString( "exprVal FontCase:%1" ).arg( fcase ), 4 );
|
||||
|
||||
if ( !fcase.isEmpty() )
|
||||
{
|
||||
if ( fcase.compare( "NoChange", Qt::CaseInsensitive ) == 0 )
|
||||
{
|
||||
capitalization = QgsStringUtils::MixedCase;
|
||||
}
|
||||
else if ( fcase.compare( "Upper", Qt::CaseInsensitive ) == 0 )
|
||||
{
|
||||
capitalization = QgsStringUtils::AllUppercase;
|
||||
}
|
||||
else if ( fcase.compare( "Lower", Qt::CaseInsensitive ) == 0 )
|
||||
{
|
||||
capitalization = QgsStringUtils::AllLowercase;
|
||||
}
|
||||
else if ( fcase.compare( "Capitalize", Qt::CaseInsensitive ) == 0 )
|
||||
{
|
||||
capitalization = QgsStringUtils::ForceFirstLetterToCapital;
|
||||
}
|
||||
}
|
||||
}
|
||||
labelText = QgsStringUtils::capitalize( labelText, capitalization );
|
||||
|
||||
// data defined format numbers?
|
||||
bool formatnum = formatNumbers;
|
||||
if ( dataDefinedEvaluate( QgsPalLayerSettings::NumFormat, exprVal, &context.expressionContext(), formatNumbers ) )
|
||||
@ -3358,7 +3394,6 @@ void QgsPalLayerSettings::parseTextStyle( QFont& labelFont,
|
||||
// copy over existing font settings
|
||||
//newFont = newFont.resolve( labelFont ); // should work, but let's be sure what's being copied
|
||||
newFont.setPixelSize( labelFont.pixelSize() );
|
||||
newFont.setCapitalization( labelFont.capitalization() );
|
||||
newFont.setUnderline( labelFont.underline() );
|
||||
newFont.setStrikeOut( labelFont.strikeOut() );
|
||||
newFont.setWordSpacing( labelFont.wordSpacing() );
|
||||
@ -3395,39 +3430,6 @@ void QgsPalLayerSettings::parseTextStyle( QFont& labelFont,
|
||||
}
|
||||
labelFont.setLetterSpacing( QFont::AbsoluteSpacing, scaleToPixelContext( letterspace, context, fontunits, false, fontSizeMapUnitScale ) );
|
||||
|
||||
// data defined font capitalization?
|
||||
QFont::Capitalization fontcaps = labelFont.capitalization();
|
||||
if ( dataDefinedEvaluate( QgsPalLayerSettings::FontCase, exprVal, &context.expressionContext() ) )
|
||||
{
|
||||
QString fcase = exprVal.toString().trimmed();
|
||||
QgsDebugMsgLevel( QString( "exprVal FontCase:%1" ).arg( fcase ), 4 );
|
||||
|
||||
if ( !fcase.isEmpty() )
|
||||
{
|
||||
if ( fcase.compare( "NoChange", Qt::CaseInsensitive ) == 0 )
|
||||
{
|
||||
fontcaps = QFont::MixedCase;
|
||||
}
|
||||
else if ( fcase.compare( "Upper", Qt::CaseInsensitive ) == 0 )
|
||||
{
|
||||
fontcaps = QFont::AllUppercase;
|
||||
}
|
||||
else if ( fcase.compare( "Lower", Qt::CaseInsensitive ) == 0 )
|
||||
{
|
||||
fontcaps = QFont::AllLowercase;
|
||||
}
|
||||
else if ( fcase.compare( "Capitalize", Qt::CaseInsensitive ) == 0 )
|
||||
{
|
||||
fontcaps = QFont::Capitalize;
|
||||
}
|
||||
|
||||
if ( fontcaps != labelFont.capitalization() )
|
||||
{
|
||||
labelFont.setCapitalization( fontcaps );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// data defined strikeout font style?
|
||||
if ( dataDefinedEvaluate( QgsPalLayerSettings::Strikeout, exprVal, &context.expressionContext(), labelFont.strikeOut() ) )
|
||||
{
|
||||
|
@ -17,6 +17,55 @@
|
||||
#include <QVector>
|
||||
#include <QRegExp>
|
||||
#include <QTextDocument> // for Qt::escape
|
||||
#include <QStringList>
|
||||
#include <QTextBoundaryFinder>
|
||||
|
||||
QString QgsStringUtils::capitalize( const QString& string, QgsStringUtils::Capitalization capitalization )
|
||||
{
|
||||
if ( string.isEmpty() )
|
||||
return QString();
|
||||
|
||||
switch ( capitalization )
|
||||
{
|
||||
case MixedCase:
|
||||
return string;
|
||||
|
||||
case AllUppercase:
|
||||
return string.toUpper();
|
||||
|
||||
case AllLowercase:
|
||||
return string.toLower();
|
||||
|
||||
case ForceFirstLetterToCapital:
|
||||
{
|
||||
QString temp = string;
|
||||
|
||||
QTextBoundaryFinder wordSplitter( QTextBoundaryFinder::Word, string.constData(), string.length(), 0, 0 );
|
||||
QTextBoundaryFinder letterSplitter( QTextBoundaryFinder::Grapheme, string.constData(), string.length(), 0, 0 );
|
||||
|
||||
wordSplitter.setPosition( 0 );
|
||||
bool first = true;
|
||||
#if QT_VERSION >= 0x050000
|
||||
while (( first && wordSplitter.boundaryReasons() & QTextBoundaryFinder::StartOfItem )
|
||||
|| wordSplitter.toNextBoundary() >= 0 )
|
||||
#else
|
||||
while (( first && wordSplitter.boundaryReasons() & QTextBoundaryFinder::StartWord )
|
||||
|| wordSplitter.toNextBoundary() >= 0 )
|
||||
#endif
|
||||
{
|
||||
first = false;
|
||||
letterSplitter.setPosition( wordSplitter.position() );
|
||||
letterSplitter.toNextBoundary();
|
||||
QString substr = string.mid( wordSplitter.position(), letterSplitter.position() - wordSplitter.position() );
|
||||
temp.replace( wordSplitter.position(), substr.length(), substr.toUpper() );
|
||||
}
|
||||
return temp;
|
||||
}
|
||||
|
||||
}
|
||||
// no warnings
|
||||
return string;
|
||||
}
|
||||
|
||||
int QgsStringUtils::levenshteinDistance( const QString& string1, const QString& string2, bool caseSensitive )
|
||||
{
|
||||
|
@ -17,6 +17,7 @@
|
||||
#include <QRegExp>
|
||||
#include <QList>
|
||||
#include <QDomDocument>
|
||||
#include <QFont> // for enum values
|
||||
#include "qgis.h"
|
||||
|
||||
#ifndef QGSSTRINGUTILS_H
|
||||
@ -166,6 +167,23 @@ class CORE_EXPORT QgsStringUtils
|
||||
{
|
||||
public:
|
||||
|
||||
//! Capitalization options
|
||||
enum Capitalization
|
||||
{
|
||||
MixedCase = QFont::MixedCase, //!< Mixed case, ie no change
|
||||
AllUppercase = QFont::AllUppercase, //!< Convert all characters to uppercase
|
||||
AllLowercase = QFont::AllLowercase, //!< Convert all characters to lowercase
|
||||
ForceFirstLetterToCapital = QFont::Capitalize, //!< Convert just the first letter of each word to uppercase, leave the rest untouched
|
||||
};
|
||||
|
||||
/** Converts a string by applying capitalization rules to the string.
|
||||
* @param string input string
|
||||
* @param capitalization capitalization type to apply
|
||||
* @return capitalized string
|
||||
* @note added in QGIS 3.0
|
||||
*/
|
||||
static QString capitalize( const QString& string, Capitalization capitalization );
|
||||
|
||||
/** Returns the Levenshtein edit distance between two strings. This equates to the minimum
|
||||
* number of character edits (insertions, deletions or substitutions) required to change
|
||||
* one string to another.
|
||||
|
@ -44,6 +44,7 @@ class TestQgsLabelingEngine : public QObject
|
||||
void zOrder(); //test that labels are stacked correctly
|
||||
void testEncodeDecodePositionOrder();
|
||||
void testSubstitutions();
|
||||
void testCapitalization();
|
||||
|
||||
private:
|
||||
QgsVectorLayer* vl;
|
||||
@ -52,6 +53,7 @@ class TestQgsLabelingEngine : public QObject
|
||||
|
||||
void setDefaultLabelParams( QgsVectorLayer* layer );
|
||||
bool imageCheck( const QString& testName, QImage &image, int mismatchCount );
|
||||
|
||||
};
|
||||
|
||||
void TestQgsLabelingEngine::initTestCase()
|
||||
@ -454,6 +456,65 @@ void TestQgsLabelingEngine::testSubstitutions()
|
||||
QCOMPARE( provider2->mLabels.at( 0 )->labelText(), QString( "bb label" ) );
|
||||
}
|
||||
|
||||
void TestQgsLabelingEngine::testCapitalization()
|
||||
{
|
||||
QgsFeature f( vl->fields(), 1 );
|
||||
f.setGeometry( QgsGeometry::fromPoint( QgsPoint( 1, 2 ) ) );
|
||||
|
||||
// make a fake render context
|
||||
QSize size( 640, 480 );
|
||||
QgsMapSettings mapSettings;
|
||||
mapSettings.setOutputSize( size );
|
||||
mapSettings.setExtent( vl->extent() );
|
||||
mapSettings.setLayers( QStringList() << vl->id() );
|
||||
mapSettings.setOutputDpi( 96 );
|
||||
QgsRenderContext context = QgsRenderContext::fromMapSettings( mapSettings );
|
||||
QStringList attributes;
|
||||
QgsLabelingEngine engine;
|
||||
engine.setMapSettings( mapSettings );
|
||||
|
||||
// no change
|
||||
QgsPalLayerSettings settings;
|
||||
QFont font = settings.textFont;
|
||||
font.setCapitalization( QFont::MixedCase );
|
||||
settings.textFont = font;
|
||||
settings.fieldName = QString( "'a teSt LABEL'" );
|
||||
settings.isExpression = true;
|
||||
|
||||
QgsVectorLayerLabelProvider* provider = new QgsVectorLayerLabelProvider( vl, "test", true, &settings );
|
||||
engine.addProvider( provider );
|
||||
provider->prepare( context, attributes );
|
||||
provider->registerFeature( f, context );
|
||||
QCOMPARE( provider->mLabels.at( 0 )->labelText(), QString( "a teSt LABEL" ) );
|
||||
|
||||
//uppercase
|
||||
font.setCapitalization( QFont::AllUppercase );
|
||||
settings.textFont = font;
|
||||
QgsVectorLayerLabelProvider* provider2 = new QgsVectorLayerLabelProvider( vl, "test2", true, &settings );
|
||||
engine.addProvider( provider2 );
|
||||
provider2->prepare( context, attributes );
|
||||
provider2->registerFeature( f, context );
|
||||
QCOMPARE( provider2->mLabels.at( 0 )->labelText(), QString( "A TEST LABEL" ) );
|
||||
|
||||
//lowercase
|
||||
font.setCapitalization( QFont::AllLowercase );
|
||||
settings.textFont = font;
|
||||
QgsVectorLayerLabelProvider* provider3 = new QgsVectorLayerLabelProvider( vl, "test3", true, &settings );
|
||||
engine.addProvider( provider3 );
|
||||
provider3->prepare( context, attributes );
|
||||
provider3->registerFeature( f, context );
|
||||
QCOMPARE( provider3->mLabels.at( 0 )->labelText(), QString( "a test label" ) );
|
||||
|
||||
//first letter uppercase
|
||||
font.setCapitalization( QFont::Capitalize );
|
||||
settings.textFont = font;
|
||||
QgsVectorLayerLabelProvider* provider4 = new QgsVectorLayerLabelProvider( vl, "test4", true, &settings );
|
||||
engine.addProvider( provider4 );
|
||||
provider4->prepare( context, attributes );
|
||||
provider4->registerFeature( f, context );
|
||||
QCOMPARE( provider4->mLabels.at( 0 )->labelText(), QString( "A TeSt LABEL" ) );
|
||||
}
|
||||
|
||||
bool TestQgsLabelingEngine::imageCheck( const QString& testName, QImage &image, int mismatchCount )
|
||||
{
|
||||
//draw background
|
||||
|
@ -133,5 +133,49 @@ class PyQgsStringReplacementCollection(unittest.TestCase):
|
||||
self.assertEqual(c2.replacements(), c.replacements())
|
||||
|
||||
|
||||
class PyQgsStringUtils(unittest.TestCase):
|
||||
|
||||
def testMixed(self):
|
||||
""" test mixed capitalization - ie, no change! """
|
||||
self.assertFalse(QgsStringUtils.capitalize(None, QgsStringUtils.MixedCase))
|
||||
self.assertEqual(QgsStringUtils.capitalize('', QgsStringUtils.MixedCase), '')
|
||||
self.assertEqual(QgsStringUtils.capitalize('testing 123', QgsStringUtils.MixedCase), 'testing 123')
|
||||
self.assertEqual(QgsStringUtils.capitalize(' tESTinG 123 ', QgsStringUtils.MixedCase), ' tESTinG 123 ')
|
||||
self.assertEqual(QgsStringUtils.capitalize(' TESTING ABC', QgsStringUtils.MixedCase), ' TESTING ABC')
|
||||
|
||||
def testUpperCase(self):
|
||||
""" test uppercase """
|
||||
self.assertFalse(QgsStringUtils.capitalize(None, QgsStringUtils.AllUppercase))
|
||||
self.assertEqual(QgsStringUtils.capitalize('', QgsStringUtils.AllUppercase), '')
|
||||
self.assertEqual(QgsStringUtils.capitalize('testing 123', QgsStringUtils.AllUppercase), 'TESTING 123')
|
||||
self.assertEqual(QgsStringUtils.capitalize(' tESTinG abc ', QgsStringUtils.AllUppercase), ' TESTING ABC ')
|
||||
self.assertEqual(QgsStringUtils.capitalize(' TESTING ABC', QgsStringUtils.AllUppercase), ' TESTING ABC')
|
||||
|
||||
def testLowerCase(self):
|
||||
""" test lowercase """
|
||||
self.assertFalse(QgsStringUtils.capitalize(None, QgsStringUtils.AllLowercase))
|
||||
self.assertEqual(QgsStringUtils.capitalize('', QgsStringUtils.AllLowercase), '')
|
||||
self.assertEqual(QgsStringUtils.capitalize('testing 123', QgsStringUtils.AllLowercase), 'testing 123')
|
||||
self.assertEqual(QgsStringUtils.capitalize(' tESTinG abc ', QgsStringUtils.AllLowercase),
|
||||
' testing abc ')
|
||||
self.assertEqual(QgsStringUtils.capitalize(' TESTING ABC', QgsStringUtils.AllLowercase), ' testing abc')
|
||||
|
||||
def testCapitalizeFirst(self):
|
||||
""" test capitalize first """
|
||||
self.assertFalse(QgsStringUtils.capitalize(None, QgsStringUtils.ForceFirstLetterToCapital))
|
||||
self.assertEqual(QgsStringUtils.capitalize('', QgsStringUtils.ForceFirstLetterToCapital), '')
|
||||
self.assertEqual(QgsStringUtils.capitalize('testing 123', QgsStringUtils.ForceFirstLetterToCapital), 'Testing 123')
|
||||
self.assertEqual(QgsStringUtils.capitalize('testing', QgsStringUtils.ForceFirstLetterToCapital),
|
||||
'Testing')
|
||||
self.assertEqual(QgsStringUtils.capitalize('Testing', QgsStringUtils.ForceFirstLetterToCapital),
|
||||
'Testing')
|
||||
self.assertEqual(QgsStringUtils.capitalize('TESTING', QgsStringUtils.ForceFirstLetterToCapital),
|
||||
'TESTING')
|
||||
self.assertEqual(QgsStringUtils.capitalize(' tESTinG abc ', QgsStringUtils.ForceFirstLetterToCapital),
|
||||
' TESTinG Abc ')
|
||||
self.assertEqual(QgsStringUtils.capitalize(' TESTING ABC', QgsStringUtils.ForceFirstLetterToCapital), ' TESTING ABC')
|
||||
self.assertEqual(QgsStringUtils.capitalize(' testing abc', QgsStringUtils.ForceFirstLetterToCapital),
|
||||
' Testing Abc')
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
Loading…
x
Reference in New Issue
Block a user