Skip to content

Commit

Permalink
[feature] Support Small Caps style in labels/text renderer
Browse files Browse the repository at this point in the history
Adds two new capitalization styles for labels and text symbols:

- Small Caps: Renders lowercase characters as small caps
- All Small Caps: Renders all characters as small caps (regardless
of their original case)

Requires Qt 6.3+, or Qt 5.15 using KDE's fork and the cmake
HAS_KDE_QT5_SMALL_CAPS_FIX switch defined during build.
  • Loading branch information
nyalldawson committed Nov 8, 2021
1 parent a168bca commit dae69ac
Show file tree
Hide file tree
Showing 33 changed files with 176 additions and 99 deletions.
3 changes: 2 additions & 1 deletion CMakeLists.txt
Expand Up @@ -425,7 +425,8 @@ if(WITH_CORE)
else()
set(QT_MIN_VERSION 5.12.0)
set(QT_VERSION_BASE "Qt5")
set(WITH_QT5_KDE_FORK FALSE CACHE BOOL "Using KDE's Qt 5.15 fork")
set(HAS_KDE_QT5_PDF_TRANSFORM_FIX FALSE CACHE BOOL "Using KDE's Qt 5.15 fork with the PDF brush transform fix")
set(HAS_KDE_QT5_SMALL_CAPS_FIX FALSE CACHE BOOL "Using KDE's Qt 5.15 fork with the QFont::SmallCaps fix")
endif()

# Use Qt5SerialPort optionally for GPS
Expand Down
3 changes: 2 additions & 1 deletion cmake_templates/qgsconfig.h.in
Expand Up @@ -104,7 +104,8 @@

#cmakedefine ENABLE_TESTS

#cmakedefine WITH_QT5_KDE_FORK
#cmakedefine HAS_KDE_QT5_PDF_TRANSFORM_FIX
#cmakedefine HAS_KDE_QT5_SMALL_CAPS_FIX

#endif

29 changes: 29 additions & 0 deletions python/core/auto_additions/qgis.py
Expand Up @@ -1132,3 +1132,32 @@
Qgis.DashPatternSizeAdjustment.__doc__ = 'Dash pattern size adjustment options.\n\n.. versionadded:: 3.24\n\n' + '* ``ScaleBothDashAndGap``: ' + Qgis.DashPatternSizeAdjustment.ScaleBothDashAndGap.__doc__ + '\n' + '* ``ScaleDashOnly``: ' + Qgis.DashPatternSizeAdjustment.ScaleDashOnly.__doc__ + '\n' + '* ``ScaleGapOnly``: ' + Qgis.DashPatternSizeAdjustment.ScaleGapOnly.__doc__
# --
Qgis.DashPatternSizeAdjustment.baseClass = Qgis
QgsStringUtils.Capitalization = Qgis.Capitalization
# monkey patching scoped based enum
QgsStringUtils.MixedCase = Qgis.Capitalization.MixedCase
QgsStringUtils.MixedCase.is_monkey_patched = True
QgsStringUtils.MixedCase.__doc__ = "Mixed case, ie no change"
QgsStringUtils.AllUppercase = Qgis.Capitalization.AllUppercase
QgsStringUtils.AllUppercase.is_monkey_patched = True
QgsStringUtils.AllUppercase.__doc__ = "Convert all characters to uppercase"
QgsStringUtils.AllLowercase = Qgis.Capitalization.AllLowercase
QgsStringUtils.AllLowercase.is_monkey_patched = True
QgsStringUtils.AllLowercase.__doc__ = "Convert all characters to lowercase"
QgsStringUtils.ForceFirstLetterToCapital = Qgis.Capitalization.ForceFirstLetterToCapital
QgsStringUtils.ForceFirstLetterToCapital.is_monkey_patched = True
QgsStringUtils.ForceFirstLetterToCapital.__doc__ = "Convert just the first letter of each word to uppercase, leave the rest untouched"
QgsStringUtils.SmallCaps = Qgis.Capitalization.SmallCaps
QgsStringUtils.SmallCaps.is_monkey_patched = True
QgsStringUtils.SmallCaps.__doc__ = "Mixed case small caps (since QGIS 3.24)"
QgsStringUtils.TitleCase = Qgis.Capitalization.TitleCase
QgsStringUtils.TitleCase.is_monkey_patched = True
QgsStringUtils.TitleCase.__doc__ = "Simple title case conversion - does not fully grammatically parse the text and uses simple rules only. Note that this method does not convert any characters to lowercase, it only uppercases required letters. Callers must ensure that input strings are already lowercased."
QgsStringUtils.UpperCamelCase = Qgis.Capitalization.UpperCamelCase
QgsStringUtils.UpperCamelCase.is_monkey_patched = True
QgsStringUtils.UpperCamelCase.__doc__ = "Convert the string to upper camel case. Note that this method does not unaccent characters."
QgsStringUtils.AllSmallCaps = Qgis.Capitalization.AllSmallCaps
QgsStringUtils.AllSmallCaps.is_monkey_patched = True
QgsStringUtils.AllSmallCaps.__doc__ = "Force all characters to small caps (since QGIS 3.24)"
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__
# --
Qgis.Capitalization.baseClass = Qgis
14 changes: 14 additions & 0 deletions python/core/auto_generated/qgis.sip.in
Expand Up @@ -732,6 +732,20 @@ The development version
ScaleGapOnly,
};



enum class Capitalization
{
MixedCase,
AllUppercase,
AllLowercase,
ForceFirstLetterToCapital,
SmallCaps,
TitleCase,
UpperCamelCase,
AllSmallCaps,
};

static const double DEFAULT_SEARCH_RADIUS_MM;

static const float DEFAULT_MAPTOPIXEL_THRESHOLD;
Expand Down
12 changes: 1 addition & 11 deletions python/core/auto_generated/qgsstringutils.sip.in
Expand Up @@ -169,17 +169,7 @@ Utility functions for working with strings.
%End
public:

enum Capitalization
{
MixedCase,
AllUppercase,
AllLowercase,
ForceFirstLetterToCapital,
TitleCase,
UpperCamelCase,
};

static QString capitalize( const QString &string, Capitalization capitalization );
static QString capitalize( const QString &string, Qgis::Capitalization capitalization );
%Docstring
Converts a string by applying capitalization rules to the string.

Expand Down
Expand Up @@ -64,7 +64,7 @@ Returns ``True`` if the block is empty.
Returns the number of fragments in the block.
%End

void applyCapitalization( QgsStringUtils::Capitalization capitalization );
void applyCapitalization( Qgis::Capitalization capitalization );
%Docstring
Applies a ``capitalization`` style to the block's text.

Expand Down
Expand Up @@ -116,7 +116,7 @@ argument controls whether the lines should be wrapped to an ideal maximum of ``a
if ``False`` then the lines are wrapped to an ideal minimum length of ``autoWrapLength`` characters.
%End

void applyCapitalization( QgsStringUtils::Capitalization capitalization );
void applyCapitalization( Qgis::Capitalization capitalization );
%Docstring
Applies a ``capitalization`` style to the document's text.

Expand Down
4 changes: 2 additions & 2 deletions python/core/auto_generated/textrenderer/qgstextformat.sip.in
Expand Up @@ -402,7 +402,7 @@ Sets the ``orientation`` for the text.
.. versionadded:: 3.10
%End

QgsStringUtils::Capitalization capitalization() const;
Qgis::Capitalization capitalization() const;
%Docstring
Returns the text capitalization style.

Expand All @@ -411,7 +411,7 @@ Returns the text capitalization style.
.. versionadded:: 3.16
%End

void setCapitalization( QgsStringUtils::Capitalization capitalization );
void setCapitalization( Qgis::Capitalization capitalization );
%Docstring
Sets the text ``capitalization`` style.

Expand Down
Expand Up @@ -78,7 +78,7 @@ based on the resultant font metrics. Failure to do so will result in poor qualit
at small font sizes.
%End

void applyCapitalization( QgsStringUtils::Capitalization capitalization );
void applyCapitalization( Qgis::Capitalization capitalization );
%Docstring
Applies a ``capitalization`` style to the fragment's text.

Expand Down
2 changes: 1 addition & 1 deletion src/app/qgisapp.cpp
Expand Up @@ -9660,7 +9660,7 @@ bool QgisApp::uniqueLayoutTitle( QWidget *parent, QString &title, bool acceptEmp
layoutNames << l->name();
}

const QString windowTitle = tr( "Create %1" ).arg( QgsGui::higFlags() & QgsGui::HigDialogTitleIsTitleCase ? QgsStringUtils::capitalize( typeString, QgsStringUtils::TitleCase )
const QString windowTitle = tr( "Create %1" ).arg( QgsGui::higFlags() & QgsGui::HigDialogTitleIsTitleCase ? QgsStringUtils::capitalize( typeString, Qgis::Capitalization::TitleCase )
: typeString );

while ( !titleValid )
Expand Down
26 changes: 18 additions & 8 deletions src/core/labeling/qgspallabeling.cpp
Expand Up @@ -1897,11 +1897,11 @@ std::unique_ptr<QgsLabelFeature> QgsPalLayerSettings::registerFeatureWithDetails
}

// apply capitalization
QgsStringUtils::Capitalization capitalization = mFormat.capitalization();
Qgis::Capitalization capitalization = mFormat.capitalization();
// maintain API - capitalization may have been set in textFont
if ( capitalization == QgsStringUtils::MixedCase && mFormat.font().capitalization() != QFont::MixedCase )
if ( capitalization == Qgis::Capitalization::MixedCase && mFormat.font().capitalization() != QFont::MixedCase )
{
capitalization = static_cast< QgsStringUtils::Capitalization >( mFormat.font().capitalization() );
capitalization = static_cast< Qgis::Capitalization >( mFormat.font().capitalization() );
}
// data defined font capitalization?
if ( mDataDefinedProperties.isActive( QgsPalLayerSettings::FontCase ) )
Expand All @@ -1916,24 +1916,34 @@ std::unique_ptr<QgsLabelFeature> QgsPalLayerSettings::registerFeatureWithDetails
{
if ( fcase.compare( QLatin1String( "NoChange" ), Qt::CaseInsensitive ) == 0 )
{
capitalization = QgsStringUtils::MixedCase;
capitalization = Qgis::Capitalization::MixedCase;
}
else if ( fcase.compare( QLatin1String( "Upper" ), Qt::CaseInsensitive ) == 0 )
{
capitalization = QgsStringUtils::AllUppercase;
capitalization = Qgis::Capitalization::AllUppercase;
}
else if ( fcase.compare( QLatin1String( "Lower" ), Qt::CaseInsensitive ) == 0 )
{
capitalization = QgsStringUtils::AllLowercase;
capitalization = Qgis::Capitalization::AllLowercase;
}
else if ( fcase.compare( QLatin1String( "Capitalize" ), Qt::CaseInsensitive ) == 0 )
{
capitalization = QgsStringUtils::ForceFirstLetterToCapital;
capitalization = Qgis::Capitalization::ForceFirstLetterToCapital;
}
else if ( fcase.compare( QLatin1String( "Title" ), Qt::CaseInsensitive ) == 0 )
{
capitalization = QgsStringUtils::TitleCase;
capitalization = Qgis::Capitalization::TitleCase;
}
#if defined(HAS_KDE_QT5_SMALL_CAPS_FIX) || QT_VERSION >= QT_VERSION_CHECK(6, 3, 0)
else if ( fcase.compare( QLatin1String( "SmallCaps" ), Qt::CaseInsensitive ) == 0 )
{
capitalization = Qgis::Capitalization::SmallCaps;
}
else if ( fcase.compare( QLatin1String( "AllSmallCaps" ), Qt::CaseInsensitive ) == 0 )
{
capitalization = Qgis::Capitalization::AllSmallCaps;
}
#endif
}
}
}
Expand Down
12 changes: 6 additions & 6 deletions src/core/labeling/qgsvectorlayerlabeling.cpp
Expand Up @@ -294,18 +294,18 @@ void QgsAbstractVectorLayerLabeling::writeTextSymbolizer( QDomNode &parent, QgsP
}
else
{
QgsStringUtils::Capitalization capitalization = format.capitalization();
if ( capitalization == QgsStringUtils::MixedCase && font.capitalization() != QFont::MixedCase )
capitalization = static_cast< QgsStringUtils::Capitalization >( font.capitalization() );
if ( capitalization == QgsStringUtils::AllUppercase )
Qgis::Capitalization capitalization = format.capitalization();
if ( capitalization == Qgis::Capitalization::MixedCase && font.capitalization() != QFont::MixedCase )
capitalization = static_cast< Qgis::Capitalization >( font.capitalization() );
if ( capitalization == Qgis::Capitalization::AllUppercase )
{
appendSimpleFunction( doc, labelElement, QStringLiteral( "strToUpperCase" ), settings.fieldName );
}
else if ( capitalization == QgsStringUtils::AllLowercase )
else if ( capitalization == Qgis::Capitalization::AllLowercase )
{
appendSimpleFunction( doc, labelElement, QStringLiteral( "strToLowerCase" ), settings.fieldName );
}
else if ( capitalization == QgsStringUtils::ForceFirstLetterToCapital )
else if ( capitalization == Qgis::Capitalization::ForceFirstLetterToCapital )
{
appendSimpleFunction( doc, labelElement, QStringLiteral( "strCapitalize" ), settings.fieldName );
}
Expand Down
2 changes: 1 addition & 1 deletion src/core/layout/qgslayoutexporter.cpp
Expand Up @@ -1219,7 +1219,7 @@ void QgsLayoutExporter::preparePrintAsPdf( QgsLayout *layout, QPrinter &printer,
// May not work on Windows or non-X11 Linux. Works fine on Mac using QPrinter::NativeFormat
//printer.setFontEmbeddingEnabled( true );

#if defined(WITH_QT5_KDE_FORK) || QT_VERSION >= QT_VERSION_CHECK(6, 3, 0)
#if defined(HAS_KDE_QT5_PDF_TRANSFORM_FIX) || QT_VERSION >= QT_VERSION_CHECK(6, 3, 0)
// paint engine hack not required, fixed upstream
#else
QgsPaintEngineHack::fixEngineFlags( printer.paintEngine() );
Expand Down
2 changes: 1 addition & 1 deletion src/core/processing/models/qgsprocessingmodelalgorithm.cpp
Expand Up @@ -488,7 +488,7 @@ QStringList QgsProcessingModelAlgorithm::asPythonCode( const QgsProcessing::Pyth
n.replace( rx2, QString() );
if ( !capitalize )
n = n.replace( ' ', '_' );
return capitalize ? QgsStringUtils::capitalize( n, QgsStringUtils::UpperCamelCase ) : n;
return capitalize ? QgsStringUtils::capitalize( n, Qgis::Capitalization::UpperCamelCase ) : n;
};

auto uniqueSafeName = [ &safeName ]( const QString & name, bool capitalize, const QMap< QString, QString > &friendlyNames )->QString
Expand Down
23 changes: 23 additions & 0 deletions src/core/qgis.h
Expand Up @@ -1196,6 +1196,29 @@ class CORE_EXPORT Qgis
};
Q_ENUM( DashPatternSizeAdjustment )


// NOTE -- the hardcoded numbers here must match QFont::Capitalization!

/**
* String capitalization options.
*
* \note Prior to QGIS 3.24 this was available as QgsStringUtils::Capitalization
*
* \since QGIS 3.24
*/
enum class Capitalization SIP_MONKEYPATCH_SCOPEENUM_UNNEST( QgsStringUtils, Capitalization ) : int
{
MixedCase = 0, //!< Mixed case, ie no change
AllUppercase = 1, //!< Convert all characters to uppercase
AllLowercase = 2, //!< Convert all characters to lowercase
ForceFirstLetterToCapital = 4, //!< Convert just the first letter of each word to uppercase, leave the rest untouched
SmallCaps = 5, //!< Mixed case small caps (since QGIS 3.24)
TitleCase = 1004, //!< Simple title case conversion - does not fully grammatically parse the text and uses simple rules only. Note that this method does not convert any characters to lowercase, it only uppercases required letters. Callers must ensure that input strings are already lowercased.
UpperCamelCase = 1005, //!< Convert the string to upper camel case. Note that this method does not unaccent characters.
AllSmallCaps = 1006, //!< Force all characters to small caps (since QGIS 3.24)
};
Q_ENUM( Capitalization )

/**
* Identify search radius in mm
* \since QGIS 2.3
Expand Down
2 changes: 1 addition & 1 deletion src/core/qgsmaplayer.cpp
Expand Up @@ -953,7 +953,7 @@ QString QgsMapLayer::formatLayerName( const QString &name )
{
QString layerName( name );
layerName.replace( '_', ' ' );
layerName = QgsStringUtils::capitalize( layerName, QgsStringUtils::ForceFirstLetterToCapital );
layerName = QgsStringUtils::capitalize( layerName, Qgis::Capitalization::ForceFirstLetterToCapital );
return layerName;
}

Expand Down
4 changes: 2 additions & 2 deletions src/core/qgspaintenginehack.cpp
Expand Up @@ -21,7 +21,7 @@
// Hack to workaround Qt #5114 by disabling PatternTransform
void QgsPaintEngineHack::fixFlags()
{
#if defined(WITH_QT5_KDE_FORK) || QT_VERSION >= QT_VERSION_CHECK(6, 3, 0)
#if defined(HAS_KDE_QT5_PDF_TRANSFORM_FIX) || QT_VERSION >= QT_VERSION_CHECK(6, 3, 0)
// not required, fixed upstream
#else
gccaps = PaintEngineFeatures();
Expand Down Expand Up @@ -50,7 +50,7 @@ void QgsPaintEngineHack::fixFlags()

void QgsPaintEngineHack::fixEngineFlags( QPaintEngine *engine )
{
#if defined(WITH_QT5_KDE_FORK) || QT_VERSION >= QT_VERSION_CHECK(6, 3, 0)
#if defined(HAS_KDE_QT5_PDF_TRANSFORM_FIX) || QT_VERSION >= QT_VERSION_CHECK(6, 3, 0)
// not required, fixed upstream
( void )engine;
#else
Expand Down
20 changes: 11 additions & 9 deletions src/core/qgsstringutils.cpp
Expand Up @@ -21,23 +21,25 @@
#include <QRegularExpression>
#include <cstdlib> // for std::abs

QString QgsStringUtils::capitalize( const QString &string, QgsStringUtils::Capitalization capitalization )
QString QgsStringUtils::capitalize( const QString &string, Qgis::Capitalization capitalization )
{
if ( string.isEmpty() )
return QString();

switch ( capitalization )
{
case MixedCase:
case Qgis::Capitalization::MixedCase:
case Qgis::Capitalization::SmallCaps:
return string;

case AllUppercase:
case Qgis::Capitalization::AllUppercase:
return string.toUpper();

case AllLowercase:
case Qgis::Capitalization::AllLowercase:
case Qgis::Capitalization::AllSmallCaps:
return string.toLower();

case ForceFirstLetterToCapital:
case Qgis::Capitalization::ForceFirstLetterToCapital:
{
QString temp = string;

Expand All @@ -58,7 +60,7 @@ QString QgsStringUtils::capitalize( const QString &string, QgsStringUtils::Capit
return temp;
}

case TitleCase:
case Qgis::Capitalization::TitleCase:
{
// yes, this is MASSIVELY simplifying the problem!!

Expand Down Expand Up @@ -103,8 +105,8 @@ QString QgsStringUtils::capitalize( const QString &string, QgsStringUtils::Capit
return result;
}

case UpperCamelCase:
QString result = QgsStringUtils::capitalize( string.toLower(), QgsStringUtils::ForceFirstLetterToCapital ).simplified();
case Qgis::Capitalization::UpperCamelCase:
QString result = QgsStringUtils::capitalize( string.toLower(), Qgis::Capitalization::ForceFirstLetterToCapital ).simplified();
result.remove( ' ' );
return result;
}
Expand Down Expand Up @@ -669,7 +671,7 @@ QString QgsStringUtils::wordWrap( const QString &string, const int length, const
#if QT_VERSION < QT_VERSION_CHECK(5, 15, 2)
newstr.append( line.midRef( strCurrent ) );
#else
newstr.append( QStringView {line}.mid( strCurrent ) );
newstr.append( QStringView {line} .mid( strCurrent ) );
#endif
strCurrent = strLength;
}
Expand Down
14 changes: 1 addition & 13 deletions src/core/qgsstringutils.h
Expand Up @@ -20,7 +20,6 @@
#include <QRegularExpression>
#include <QList>
#include <QDomDocument>
#include <QFont> // for enum values

#ifndef QGSSTRINGUTILS_H
#define QGSSTRINGUTILS_H
Expand Down Expand Up @@ -185,25 +184,14 @@ 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
TitleCase = QFont::Capitalize + 1000, //!< Simple title case conversion - does not fully grammatically parse the text and uses simple rules only. Note that this method does not convert any characters to lowercase, it only uppercases required letters. Callers must ensure that input strings are already lowercased.
UpperCamelCase = QFont::Capitalize + 1001, //!< Convert the string to upper camel case. Note that this method does not unaccent characters.
};

/**
* Converts a string by applying capitalization rules to the string.
* \param string input string
* \param capitalization capitalization type to apply
* \returns capitalized string
* \since QGIS 3.0
*/
static QString capitalize( const QString &string, Capitalization capitalization );
static QString capitalize( const QString &string, Qgis::Capitalization capitalization );

/**
* Makes a raw string safe for inclusion as a HTML/XML string literal.
Expand Down

0 comments on commit dae69ac

Please sign in to comment.