From 1fac3af50645f922c3e7d961942ab41218b79b63 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Tue, 29 Oct 2024 09:02:43 +1000 Subject: [PATCH 1/7] Move all QgsTextFormat tests to correct file --- tests/src/python/test_qgstextformat.py | 1444 +++++++++++++++++++++- tests/src/python/test_qgstextrenderer.py | 1424 +-------------------- 2 files changed, 1443 insertions(+), 1425 deletions(-) diff --git a/tests/src/python/test_qgstextformat.py b/tests/src/python/test_qgstextformat.py index b4f4fe6ab354..6570970bec3e 100644 --- a/tests/src/python/test_qgstextformat.py +++ b/tests/src/python/test_qgstextformat.py @@ -9,14 +9,36 @@ __date__ = '2024-10-20' __copyright__ = 'Copyright 2024, The QGIS Project' -from qgis.PyQt.QtGui import QFont +from qgis.PyQt.QtCore import ( + Qt, + QPointF, + QSizeF, + QT_VERSION_STR +) +from qgis.PyQt.QtGui import ( + QFont, + QColor, + QPainter +) from qgis.PyQt.QtXml import ( QDomDocument, - QDomElement, ) from qgis.core import ( + Qgis, + QgsMapUnitScale, QgsTextFormat, + QgsProperty, + QgsPalLayerSettings, QgsReadWriteContext, + QgsTextBufferSettings, + QgsTextMaskSettings, + QgsSymbolLayerReference, + QgsTextBackgroundSettings, + QgsMarkerSymbol, + QgsTextShadowSettings, + QgsRenderContext, + QgsFontUtils, + QgsVectorLayer, ) import unittest from qgis.testing import start_app, QgisTestCase @@ -25,8 +47,971 @@ start_app() +def createEmptyLayer(): + layer = QgsVectorLayer("Point", "addfeat", "memory") + assert layer.isValid() + return layer + + class PyQgsTextFormat(QgisTestCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + QgsFontUtils.loadStandardTestFonts(['Bold', 'Oblique']) + + def testValid(self): + t = QgsTextFormat() + self.assertFalse(t.isValid()) + + tt = QgsTextFormat(t) + self.assertFalse(tt.isValid()) + + t.setValid() + self.assertTrue(t.isValid()) + tt = QgsTextFormat(t) + self.assertTrue(tt.isValid()) + + doc = QDomDocument() + elem = t.writeXml(doc, QgsReadWriteContext()) + parent = doc.createElement("settings") + parent.appendChild(elem) + t3 = QgsTextFormat() + t3.readXml(parent, QgsReadWriteContext()) + self.assertTrue(t3.isValid()) + + t = QgsTextFormat() + t.buffer().setEnabled(True) + self.assertTrue(t.isValid()) + + t = QgsTextFormat() + t.background().setEnabled(True) + self.assertTrue(t.isValid()) + + t = QgsTextFormat() + t.shadow().setEnabled(True) + self.assertTrue(t.isValid()) + + t = QgsTextFormat() + t.mask().setEnabled(True) + self.assertTrue(t.isValid()) + + t = QgsTextFormat() + t.font() + self.assertFalse(t.isValid()) + t.setFont(QFont()) + self.assertTrue(t.isValid()) + + t = QgsTextFormat() + t.setNamedStyle('Bold') + self.assertTrue(t.isValid()) + + t = QgsTextFormat() + t.setSize(20) + self.assertTrue(t.isValid()) + + t = QgsTextFormat() + t.setSizeUnit(Qgis.RenderUnit.Pixels) + self.assertTrue(t.isValid()) + + t = QgsTextFormat() + t.setSizeMapUnitScale(QgsMapUnitScale(5, 10)) + self.assertTrue(t.isValid()) + + t = QgsTextFormat() + t.setColor(QColor(255, 0, 0)) + self.assertTrue(t.isValid()) + + t = QgsTextFormat() + t.setOpacity(0.2) + self.assertTrue(t.isValid()) + + t = QgsTextFormat() + t.setBlendMode(QPainter.CompositionMode.CompositionMode_Darken) + self.assertTrue(t.isValid()) + + t = QgsTextFormat() + t.setLineHeight(20) + self.assertTrue(t.isValid()) + + t = QgsTextFormat() + t.setLineHeightUnit(Qgis.RenderUnit.Points) + self.assertTrue(t.isValid()) + + t = QgsTextFormat() + t.setTabStopDistance(3) + self.assertTrue(t.isValid()) + + t = QgsTextFormat() + t.setTabStopDistanceUnit(Qgis.RenderUnit.Points) + self.assertTrue(t.isValid()) + + t = QgsTextFormat() + t.setTabStopDistanceMapUnitScale(QgsMapUnitScale(5, 10)) + self.assertTrue(t.isValid()) + + t = QgsTextFormat() + t.setOrientation(QgsTextFormat.TextOrientation.VerticalOrientation) + self.assertTrue(t.isValid()) + + t = QgsTextFormat() + t.setCapitalization(Qgis.Capitalization.TitleCase) + self.assertTrue(t.isValid()) + + t = QgsTextFormat() + t.setAllowHtmlFormatting(True) + self.assertTrue(t.isValid()) + + t = QgsTextFormat() + t.setPreviewBackgroundColor(QColor(255, 0, 0)) + self.assertTrue(t.isValid()) + + t = QgsTextFormat() + t.setFamilies(['Arial', 'Comic Sans']) + self.assertTrue(t.isValid()) + + t = QgsTextFormat() + t.setStretchFactor(110) + self.assertTrue(t.isValid()) + + t = QgsTextFormat() + t.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.Bold, QgsProperty.fromValue(True)) + self.assertTrue(t.isValid()) + + t = QgsTextFormat() + t.setForcedBold(True) + self.assertTrue(t.isValid()) + + t = QgsTextFormat() + t.setForcedItalic(True) + self.assertTrue(t.isValid()) + + def createBufferSettings(self): + s = QgsTextBufferSettings() + s.setEnabled(True) + s.setSize(5) + s.setSizeUnit(Qgis.RenderUnit.Points) + s.setSizeMapUnitScale(QgsMapUnitScale(1, 2)) + s.setColor(QColor(255, 0, 0)) + s.setFillBufferInterior(True) + s.setOpacity(0.5) + s.setJoinStyle(Qt.PenJoinStyle.RoundJoin) + s.setBlendMode(QPainter.CompositionMode.CompositionMode_DestinationAtop) + return s + + def testBufferEquality(self): + s = self.createBufferSettings() + s2 = self.createBufferSettings() + self.assertEqual(s, s2) + + s.setEnabled(False) + self.assertNotEqual(s, s2) + s = self.createBufferSettings() + + s.setSize(15) + self.assertNotEqual(s, s2) + s = self.createBufferSettings() + + s.setSizeUnit(Qgis.RenderUnit.Pixels) + self.assertNotEqual(s, s2) + s = self.createBufferSettings() + + s.setSizeMapUnitScale(QgsMapUnitScale(11, 12)) + self.assertNotEqual(s, s2) + s = self.createBufferSettings() + + s.setColor(QColor(255, 255, 0)) + self.assertNotEqual(s, s2) + s = self.createBufferSettings() + + s.setFillBufferInterior(False) + self.assertNotEqual(s, s2) + s = self.createBufferSettings() + + s.setOpacity(0.6) + self.assertNotEqual(s, s2) + s = self.createBufferSettings() + + s.setJoinStyle(Qt.PenJoinStyle.MiterJoin) + self.assertNotEqual(s, s2) + s = self.createBufferSettings() + + s.setBlendMode(QPainter.CompositionMode.CompositionMode_Darken) + self.assertNotEqual(s, s2) + + def checkBufferSettings(self, s): + """ test QgsTextBufferSettings """ + self.assertTrue(s.enabled()) + self.assertEqual(s.size(), 5) + self.assertEqual(s.sizeUnit(), Qgis.RenderUnit.Points) + self.assertEqual(s.sizeMapUnitScale(), QgsMapUnitScale(1, 2)) + self.assertEqual(s.color(), QColor(255, 0, 0)) + self.assertTrue(s.fillBufferInterior()) + self.assertEqual(s.opacity(), 0.5) + self.assertEqual(s.joinStyle(), Qt.PenJoinStyle.RoundJoin) + self.assertEqual(s.blendMode(), QPainter.CompositionMode.CompositionMode_DestinationAtop) + + def testBufferGettersSetters(self): + s = self.createBufferSettings() + self.checkBufferSettings(s) + + # some other checks + s.setEnabled(False) + self.assertFalse(s.enabled()) + s.setEnabled(True) + self.assertTrue(s.enabled()) + s.setFillBufferInterior(False) + self.assertFalse(s.fillBufferInterior()) + s.setFillBufferInterior(True) + self.assertTrue(s.fillBufferInterior()) + + def testBufferReadWriteXml(self): + """test saving and restoring state of a buffer to xml""" + doc = QDomDocument("testdoc") + s = self.createBufferSettings() + elem = s.writeXml(doc) + parent = doc.createElement("settings") + parent.appendChild(elem) + t = QgsTextBufferSettings() + t.readXml(parent) + self.checkBufferSettings(t) + + def testBufferCopy(self): + s = self.createBufferSettings() + s2 = s + self.checkBufferSettings(s2) + s3 = QgsTextBufferSettings(s) + self.checkBufferSettings(s3) + + def createMaskSettings(self): + s = QgsTextMaskSettings() + s.setEnabled(True) + s.setType(QgsTextMaskSettings.MaskType.MaskBuffer) + s.setSize(5) + s.setSizeUnit(Qgis.RenderUnit.Points) + s.setSizeMapUnitScale(QgsMapUnitScale(1, 2)) + s.setOpacity(0.5) + s.setJoinStyle(Qt.PenJoinStyle.RoundJoin) + s.setMaskedSymbolLayers([QgsSymbolLayerReference("layerid1", "symbol1"), + QgsSymbolLayerReference("layerid2", "symbol2")]) + return s + + def testMaskEquality(self): + s = self.createMaskSettings() + s2 = self.createMaskSettings() + self.assertEqual(s, s2) + + s.setEnabled(False) + self.assertNotEqual(s, s2) + s = self.createMaskSettings() + + s.setSize(15) + self.assertNotEqual(s, s2) + s = self.createMaskSettings() + + s.setSizeUnit(Qgis.RenderUnit.Pixels) + self.assertNotEqual(s, s2) + s = self.createMaskSettings() + + s.setSizeMapUnitScale(QgsMapUnitScale(11, 12)) + self.assertNotEqual(s, s2) + s = self.createMaskSettings() + + s.setOpacity(0.6) + self.assertNotEqual(s, s2) + s = self.createMaskSettings() + + s.setJoinStyle(Qt.PenJoinStyle.MiterJoin) + self.assertNotEqual(s, s2) + s = self.createMaskSettings() + + s.setMaskedSymbolLayers([QgsSymbolLayerReference("layerid11", "symbol1"), + QgsSymbolLayerReference("layerid21", "symbol2")]) + self.assertNotEqual(s, s2) + + def checkMaskSettings(self, s): + """ test QgsTextMaskSettings """ + self.assertTrue(s.enabled()) + self.assertEqual(s.type(), QgsTextMaskSettings.MaskType.MaskBuffer) + self.assertEqual(s.size(), 5) + self.assertEqual(s.sizeUnit(), Qgis.RenderUnit.Points) + self.assertEqual(s.sizeMapUnitScale(), QgsMapUnitScale(1, 2)) + self.assertEqual(s.opacity(), 0.5) + self.assertEqual(s.joinStyle(), Qt.PenJoinStyle.RoundJoin) + self.assertEqual(s.maskedSymbolLayers(), [QgsSymbolLayerReference("layerid1", "symbol1"), + QgsSymbolLayerReference("layerid2", "symbol2")]) + + def testMaskGettersSetters(self): + s = self.createMaskSettings() + self.checkMaskSettings(s) + + # some other checks + s.setEnabled(False) + self.assertFalse(s.enabled()) + + def testMaskReadWriteXml(self): + """test saving and restoring state of a mask to xml""" + doc = QDomDocument("testdoc") + s = self.createMaskSettings() + elem = s.writeXml(doc) + parent = doc.createElement("settings") + parent.appendChild(elem) + t = QgsTextMaskSettings() + t.readXml(parent) + self.checkMaskSettings(t) + + def testMaskCopy(self): + s = self.createMaskSettings() + s2 = s + self.checkMaskSettings(s2) + s3 = QgsTextMaskSettings(s) + self.checkMaskSettings(s3) + + def createBackgroundSettings(self): + s = QgsTextBackgroundSettings() + s.setEnabled(True) + s.setType(QgsTextBackgroundSettings.ShapeType.ShapeEllipse) + s.setSvgFile('svg.svg') + s.setSizeType(QgsTextBackgroundSettings.SizeType.SizePercent) + s.setSize(QSizeF(1, 2)) + s.setSizeUnit(Qgis.RenderUnit.Points) + s.setSizeMapUnitScale(QgsMapUnitScale(1, 2)) + s.setRotationType(QgsTextBackgroundSettings.RotationType.RotationFixed) + s.setRotation(45) + s.setOffset(QPointF(3, 4)) + s.setOffsetUnit(Qgis.RenderUnit.MapUnits) + s.setOffsetMapUnitScale(QgsMapUnitScale(5, 6)) + s.setRadii(QSizeF(11, 12)) + s.setRadiiUnit(Qgis.RenderUnit.Percentage) + s.setRadiiMapUnitScale(QgsMapUnitScale(15, 16)) + s.setFillColor(QColor(255, 0, 0)) + s.setStrokeColor(QColor(0, 255, 0)) + s.setOpacity(0.5) + s.setJoinStyle(Qt.PenJoinStyle.RoundJoin) + s.setBlendMode(QPainter.CompositionMode.CompositionMode_DestinationAtop) + s.setStrokeWidth(7) + s.setStrokeWidthUnit(Qgis.RenderUnit.Points) + s.setStrokeWidthMapUnitScale(QgsMapUnitScale(QgsMapUnitScale(25, 26))) + + marker = QgsMarkerSymbol() + marker.setColor(QColor(100, 112, 134)) + s.setMarkerSymbol(marker) + + return s + + def testBackgroundEquality(self): + s = self.createBackgroundSettings() + s2 = self.createBackgroundSettings() + self.assertEqual(s, s2) + + s.setEnabled(False) + self.assertNotEqual(s, s2) + s = self.createBackgroundSettings() + + s.setType(QgsTextBackgroundSettings.ShapeType.ShapeRectangle) + self.assertNotEqual(s, s2) + s = self.createBackgroundSettings() + + s.setSvgFile('svg2.svg') + self.assertNotEqual(s, s2) + s = self.createBackgroundSettings() + + s.setSizeType(QgsTextBackgroundSettings.SizeType.SizeFixed) + self.assertNotEqual(s, s2) + s = self.createBackgroundSettings() + + s.setSize(QSizeF(1, 22)) + self.assertNotEqual(s, s2) + s = self.createBackgroundSettings() + + s.setSizeUnit(Qgis.RenderUnit.Pixels) + self.assertNotEqual(s, s2) + s = self.createBackgroundSettings() + + s.setSizeMapUnitScale(QgsMapUnitScale(11, 22)) + self.assertNotEqual(s, s2) + s = self.createBackgroundSettings() + + s.setRotationType(QgsTextBackgroundSettings.RotationType.RotationSync) + self.assertNotEqual(s, s2) + s = self.createBackgroundSettings() + + s.setRotation(145) + self.assertNotEqual(s, s2) + s = self.createBackgroundSettings() + + s.setOffset(QPointF(31, 41)) + self.assertNotEqual(s, s2) + s = self.createBackgroundSettings() + + s.setOffsetUnit(Qgis.RenderUnit.Pixels) + self.assertNotEqual(s, s2) + s = self.createBackgroundSettings() + + s.setOffsetMapUnitScale(QgsMapUnitScale(15, 16)) + self.assertNotEqual(s, s2) + s = self.createBackgroundSettings() + + s.setRadii(QSizeF(111, 112)) + self.assertNotEqual(s, s2) + s = self.createBackgroundSettings() + + s.setRadiiUnit(Qgis.RenderUnit.Points) + self.assertNotEqual(s, s2) + s = self.createBackgroundSettings() + + s.setRadiiMapUnitScale(QgsMapUnitScale(151, 161)) + self.assertNotEqual(s, s2) + s = self.createBackgroundSettings() + + s.setFillColor(QColor(255, 255, 0)) + self.assertNotEqual(s, s2) + s = self.createBackgroundSettings() + + s.setStrokeColor(QColor(0, 255, 255)) + self.assertNotEqual(s, s2) + s = self.createBackgroundSettings() + + s.setOpacity(0.6) + self.assertNotEqual(s, s2) + s = self.createBackgroundSettings() + + s.setJoinStyle(Qt.PenJoinStyle.MiterJoin) + self.assertNotEqual(s, s2) + s = self.createBackgroundSettings() + + s.setBlendMode(QPainter.CompositionMode.CompositionMode_Darken) + self.assertNotEqual(s, s2) + s = self.createBackgroundSettings() + + s.setStrokeWidth(17) + self.assertNotEqual(s, s2) + s = self.createBackgroundSettings() + + s.setStrokeWidthUnit(Qgis.RenderUnit.Pixels) + self.assertNotEqual(s, s2) + s = self.createBackgroundSettings() + + s.setStrokeWidthMapUnitScale(QgsMapUnitScale(QgsMapUnitScale(251, 261))) + self.assertNotEqual(s, s2) + + def checkBackgroundSettings(self, s): + """ test QgsTextBackgroundSettings """ + self.assertTrue(s.enabled()) + self.assertEqual(s.type(), QgsTextBackgroundSettings.ShapeType.ShapeEllipse) + self.assertEqual(s.svgFile(), 'svg.svg') + self.assertEqual(s.sizeType(), QgsTextBackgroundSettings.SizeType.SizePercent) + self.assertEqual(s.size(), QSizeF(1, 2)) + self.assertEqual(s.sizeUnit(), Qgis.RenderUnit.Points) + self.assertEqual(s.sizeMapUnitScale(), QgsMapUnitScale(1, 2)) + self.assertEqual(s.rotationType(), QgsTextBackgroundSettings.RotationType.RotationFixed) + self.assertEqual(s.rotation(), 45) + self.assertEqual(s.offset(), QPointF(3, 4)) + self.assertEqual(s.offsetUnit(), Qgis.RenderUnit.MapUnits) + self.assertEqual(s.offsetMapUnitScale(), QgsMapUnitScale(5, 6)) + self.assertEqual(s.radii(), QSizeF(11, 12)) + self.assertEqual(s.radiiUnit(), Qgis.RenderUnit.Percentage) + self.assertEqual(s.radiiMapUnitScale(), QgsMapUnitScale(15, 16)) + self.assertEqual(s.fillColor(), QColor(255, 0, 0)) + self.assertEqual(s.strokeColor(), QColor(0, 255, 0)) + self.assertEqual(s.opacity(), 0.5) + self.assertEqual(s.joinStyle(), Qt.PenJoinStyle.RoundJoin) + self.assertEqual(s.blendMode(), QPainter.CompositionMode.CompositionMode_DestinationAtop) + self.assertEqual(s.strokeWidth(), 7) + self.assertEqual(s.strokeWidthUnit(), Qgis.RenderUnit.Points) + self.assertEqual(s.strokeWidthMapUnitScale(), QgsMapUnitScale(25, 26)) + self.assertEqual(s.markerSymbol().color().name(), '#647086') + + def testBackgroundGettersSetters(self): + s = self.createBackgroundSettings() + self.checkBackgroundSettings(s) + + # some other checks + s.setEnabled(False) + self.assertFalse(s.enabled()) + s.setEnabled(True) + self.assertTrue(s.enabled()) + + def testBackgroundCopy(self): + s = self.createBackgroundSettings() + s2 = s + self.checkBackgroundSettings(s2) + s3 = QgsTextBackgroundSettings(s) + self.checkBackgroundSettings(s3) + + def testBackgroundReadWriteXml(self): + """test saving and restoring state of a background to xml""" + doc = QDomDocument("testdoc") + s = self.createBackgroundSettings() + elem = s.writeXml(doc, QgsReadWriteContext()) + parent = doc.createElement("settings") + parent.appendChild(elem) + t = QgsTextBackgroundSettings() + t.readXml(parent, QgsReadWriteContext()) + self.checkBackgroundSettings(t) + + def createShadowSettings(self): + s = QgsTextShadowSettings() + s.setEnabled(True) + s.setShadowPlacement(QgsTextShadowSettings.ShadowPlacement.ShadowBuffer) + s.setOffsetAngle(45) + s.setOffsetDistance(75) + s.setOffsetUnit(Qgis.RenderUnit.MapUnits) + s.setOffsetMapUnitScale(QgsMapUnitScale(5, 6)) + s.setOffsetGlobal(True) + s.setBlurRadius(11) + s.setBlurRadiusUnit(Qgis.RenderUnit.Percentage) + s.setBlurRadiusMapUnitScale(QgsMapUnitScale(15, 16)) + s.setBlurAlphaOnly(True) + s.setColor(QColor(255, 0, 0)) + s.setOpacity(0.5) + s.setScale(123) + s.setBlendMode(QPainter.CompositionMode.CompositionMode_DestinationAtop) + return s + + def testShadowEquality(self): + s = self.createShadowSettings() + s2 = self.createShadowSettings() + self.assertEqual(s, s2) + + s.setEnabled(False) + self.assertNotEqual(s, s2) + s = self.createShadowSettings() + + s.setShadowPlacement(QgsTextShadowSettings.ShadowPlacement.ShadowText) + self.assertNotEqual(s, s2) + s = self.createShadowSettings() + + s.setOffsetAngle(145) + self.assertNotEqual(s, s2) + s = self.createShadowSettings() + + s.setOffsetDistance(175) + self.assertNotEqual(s, s2) + s = self.createShadowSettings() + + s.setOffsetUnit(Qgis.RenderUnit.Pixels) + self.assertNotEqual(s, s2) + s = self.createShadowSettings() + + s.setOffsetMapUnitScale(QgsMapUnitScale(15, 16)) + self.assertNotEqual(s, s2) + s = self.createShadowSettings() + + s.setOffsetGlobal(False) + self.assertNotEqual(s, s2) + s = self.createShadowSettings() + + s.setBlurRadius(21) + self.assertNotEqual(s, s2) + s = self.createShadowSettings() + + s.setBlurRadiusUnit(Qgis.RenderUnit.Points) + self.assertNotEqual(s, s2) + s = self.createShadowSettings() + + s.setBlurRadiusMapUnitScale(QgsMapUnitScale(115, 116)) + self.assertNotEqual(s, s2) + s = self.createShadowSettings() + + s.setBlurAlphaOnly(False) + self.assertNotEqual(s, s2) + s = self.createShadowSettings() + + s.setColor(QColor(255, 255, 0)) + self.assertNotEqual(s, s2) + s = self.createShadowSettings() + + s.setOpacity(0.6) + self.assertNotEqual(s, s2) + s = self.createShadowSettings() + + s.setScale(23) + self.assertNotEqual(s, s2) + s = self.createShadowSettings() + + s.setBlendMode(QPainter.CompositionMode.CompositionMode_Darken) + self.assertNotEqual(s, s2) + + def checkShadowSettings(self, s): + """ test QgsTextShadowSettings """ + self.assertTrue(s.enabled()) + self.assertEqual(s.shadowPlacement(), QgsTextShadowSettings.ShadowPlacement.ShadowBuffer) + self.assertEqual(s.offsetAngle(), 45) + self.assertEqual(s.offsetDistance(), 75) + self.assertEqual(s.offsetUnit(), Qgis.RenderUnit.MapUnits) + self.assertEqual(s.offsetMapUnitScale(), QgsMapUnitScale(5, 6)) + self.assertTrue(s.offsetGlobal()) + self.assertEqual(s.blurRadius(), 11) + self.assertEqual(s.blurRadiusUnit(), Qgis.RenderUnit.Percentage) + self.assertEqual(s.blurRadiusMapUnitScale(), QgsMapUnitScale(15, 16)) + self.assertTrue(s.blurAlphaOnly()) + self.assertEqual(s.color(), QColor(255, 0, 0)) + self.assertEqual(s.opacity(), 0.5) + self.assertEqual(s.scale(), 123) + self.assertEqual(s.blendMode(), QPainter.CompositionMode.CompositionMode_DestinationAtop) + + def testShadowGettersSetters(self): + s = self.createShadowSettings() + self.checkShadowSettings(s) + + # some other checks + s.setEnabled(False) + self.assertFalse(s.enabled()) + s.setEnabled(True) + self.assertTrue(s.enabled()) + s.setOffsetGlobal(False) + self.assertFalse(s.offsetGlobal()) + s.setOffsetGlobal(True) + self.assertTrue(s.offsetGlobal()) + s.setBlurAlphaOnly(False) + self.assertFalse(s.blurAlphaOnly()) + s.setBlurAlphaOnly(True) + self.assertTrue(s.blurAlphaOnly()) + + def testShadowCopy(self): + s = self.createShadowSettings() + s2 = s + self.checkShadowSettings(s2) + s3 = QgsTextShadowSettings(s) + self.checkShadowSettings(s3) + + def testShadowReadWriteXml(self): + """test saving and restoring state of a shadow to xml""" + doc = QDomDocument("testdoc") + s = self.createShadowSettings() + elem = s.writeXml(doc) + parent = doc.createElement("settings") + parent.appendChild(elem) + t = QgsTextShadowSettings() + t.readXml(parent) + self.checkShadowSettings(t) + + def createFormatSettings(self): + s = QgsTextFormat() + s.buffer().setEnabled(True) + s.buffer().setSize(25) + s.mask().setEnabled(True) + s.mask().setSize(32) + s.background().setEnabled(True) + s.background().setSvgFile('test.svg') + s.shadow().setEnabled(True) + s.shadow().setOffsetAngle(223) + font = getTestFont() + font.setKerning(False) + s.setFont(font) + s.setCapitalization(Qgis.Capitalization.TitleCase) + s.setNamedStyle('Italic') + s.setFamilies(['Arial', 'Comic Sans']) + s.setSize(5) + s.setSizeUnit(Qgis.RenderUnit.Points) + s.setSizeMapUnitScale(QgsMapUnitScale(1, 2)) + s.setColor(QColor(255, 0, 0)) + s.setOpacity(0.5) + s.setBlendMode(QPainter.CompositionMode.CompositionMode_DestinationAtop) + s.setLineHeight(5) + s.setLineHeightUnit(Qgis.RenderUnit.Inches) + s.setPreviewBackgroundColor(QColor(100, 150, 200)) + s.setOrientation(QgsTextFormat.TextOrientation.VerticalOrientation) + s.setAllowHtmlFormatting(True) + s.setForcedBold(True) + s.setForcedItalic(True) + + s.setTabStopDistance(4.5) + s.setTabStopDistanceUnit(Qgis.RenderUnit.RenderInches) + s.setTabStopDistanceMapUnitScale(QgsMapUnitScale(11, 12)) + + s.setStretchFactor(110) + + s.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.Bold, QgsProperty.fromExpression('1>2')) + return s + + def testFormatEquality(self): + s = self.createFormatSettings() + s2 = self.createFormatSettings() + self.assertEqual(s, s2) + + s.buffer().setEnabled(False) + self.assertNotEqual(s, s2) + s = self.createFormatSettings() + + s.buffer().setSize(12) + self.assertNotEqual(s, s2) + s = self.createFormatSettings() + + s.mask().setEnabled(False) + self.assertNotEqual(s, s2) + s = self.createFormatSettings() + + s.mask().setSize(12) + self.assertNotEqual(s, s2) + s = self.createFormatSettings() + + s.background().setEnabled(False) + self.assertNotEqual(s, s2) + s = self.createFormatSettings() + + s.background().setSvgFile('test2.svg') + self.assertNotEqual(s, s2) + s = self.createFormatSettings() + + s.shadow().setEnabled(False) + self.assertNotEqual(s, s2) + s = self.createFormatSettings() + + s.shadow().setOffsetAngle(123) + self.assertNotEqual(s, s2) + s = self.createFormatSettings() + + font = getTestFont() + font.setKerning(True) + s.setFont(font) + self.assertNotEqual(s, s2) + s = self.createFormatSettings() + + s.setNamedStyle('Bold') + self.assertNotEqual(s, s2) + s = self.createFormatSettings() + + s.setSize(15) + self.assertNotEqual(s, s2) + s = self.createFormatSettings() + + s.setSizeUnit(Qgis.RenderUnit.Pixels) + self.assertNotEqual(s, s2) + s = self.createFormatSettings() + + s.setSizeMapUnitScale(QgsMapUnitScale(11, 12)) + self.assertNotEqual(s, s2) + s = self.createFormatSettings() + + s.setColor(QColor(255, 255, 0)) + self.assertNotEqual(s, s2) + s = self.createFormatSettings() + + s.setOpacity(0.6) + self.assertNotEqual(s, s2) + s = self.createFormatSettings() + + s.setBlendMode(QPainter.CompositionMode.CompositionMode_Darken) + self.assertNotEqual(s, s2) + s = self.createFormatSettings() + + s.setLineHeight(15) + self.assertNotEqual(s, s2) + s = self.createFormatSettings() + + s.setLineHeightUnit(Qgis.RenderUnit.Points) + self.assertNotEqual(s, s2) + s = self.createFormatSettings() + + s.setPreviewBackgroundColor(QColor(100, 250, 200)) + self.assertNotEqual(s, s2) + s = self.createFormatSettings() + + s.setOrientation(QgsTextFormat.TextOrientation.HorizontalOrientation) + self.assertNotEqual(s, s2) + s = self.createFormatSettings() + + s.setAllowHtmlFormatting(False) + self.assertNotEqual(s, s2) + s = self.createFormatSettings() + + s.setForcedBold(False) + self.assertNotEqual(s, s2) + s = self.createFormatSettings() + + s.setForcedItalic(False) + self.assertNotEqual(s, s2) + s = self.createFormatSettings() + + s.setCapitalization(Qgis.Capitalization.ForceFirstLetterToCapital) + self.assertNotEqual(s, s2) + s = self.createFormatSettings() + + s.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.Bold, QgsProperty.fromExpression('1>3')) + self.assertNotEqual(s, s2) + + s = self.createFormatSettings() + s.setFamilies(['Times New Roman']) + self.assertNotEqual(s, s2) + + s = self.createFormatSettings() + s.setStretchFactor(120) + self.assertNotEqual(s, s2) + + s = self.createFormatSettings() + s.setTabStopDistance(120) + self.assertNotEqual(s, s2) + + s = self.createFormatSettings() + s.setTabStopDistanceUnit(Qgis.RenderUnit.Points) + self.assertNotEqual(s, s2) + + s = self.createFormatSettings() + s.setTabStopDistanceMapUnitScale( + QgsMapUnitScale(111, 122)) + self.assertNotEqual(s, s2) + + def checkTextFormat(self, s): + """ test QgsTextFormat """ + self.assertTrue(s.buffer().enabled()) + self.assertEqual(s.buffer().size(), 25) + self.assertTrue(s.mask().enabled()) + self.assertEqual(s.mask().size(), 32) + self.assertTrue(s.background().enabled()) + self.assertEqual(s.background().svgFile(), 'test.svg') + self.assertTrue(s.shadow().enabled()) + self.assertEqual(s.shadow().offsetAngle(), 223) + self.assertEqual(s.font().family(), 'QGIS Vera Sans') + self.assertEqual(s.families(), ['Arial', 'Comic Sans']) + self.assertFalse(s.font().kerning()) + self.assertEqual(s.namedStyle(), 'Italic') + self.assertEqual(s.size(), 5) + self.assertEqual(s.sizeUnit(), Qgis.RenderUnit.Points) + self.assertEqual(s.sizeMapUnitScale(), QgsMapUnitScale(1, 2)) + self.assertEqual(s.color(), QColor(255, 0, 0)) + self.assertEqual(s.opacity(), 0.5) + self.assertEqual(s.blendMode(), QPainter.CompositionMode.CompositionMode_DestinationAtop) + self.assertEqual(s.lineHeight(), 5) + self.assertEqual(s.lineHeightUnit(), Qgis.RenderUnit.Inches) + self.assertEqual(s.previewBackgroundColor().name(), '#6496c8') + self.assertEqual(s.orientation(), QgsTextFormat.TextOrientation.VerticalOrientation) + self.assertEqual(s.capitalization(), Qgis.Capitalization.TitleCase) + self.assertTrue(s.allowHtmlFormatting()) + self.assertEqual(s.dataDefinedProperties().property(QgsPalLayerSettings.Property.Bold).expressionString(), '1>2') + self.assertTrue(s.forcedBold()) + self.assertTrue(s.forcedItalic()) + self.assertEqual(s.tabStopDistance(), 4.5) + self.assertEqual(s.tabStopDistanceUnit(), Qgis.RenderUnit.Inches) + self.assertEqual(s.tabStopDistanceMapUnitScale(), QgsMapUnitScale(11, 12)) + + if int(QT_VERSION_STR.split('.')[0]) > 6 or ( + int(QT_VERSION_STR.split('.')[0]) == 6 and int(QT_VERSION_STR.split('.')[1]) >= 3): + self.assertEqual(s.stretchFactor(), 110) + + def testFormatGettersSetters(self): + s = self.createFormatSettings() + self.checkTextFormat(s) + + def testFormatCopy(self): + s = self.createFormatSettings() + s2 = s + self.checkTextFormat(s2) + s3 = QgsTextFormat(s) + self.checkTextFormat(s3) + + def testFormatReadWriteXml(self): + """test saving and restoring state of a shadow to xml""" + doc = QDomDocument("testdoc") + s = self.createFormatSettings() + elem = s.writeXml(doc, QgsReadWriteContext()) + parent = doc.createElement("settings") + parent.appendChild(elem) + t = QgsTextFormat() + t.readXml(parent, QgsReadWriteContext()) + self.checkTextFormat(t) + + def testFormatToFromMimeData(self): + """Test converting format to and from mime data""" + s = self.createFormatSettings() + md = s.toMimeData() + from_mime, ok = QgsTextFormat.fromMimeData(None) + self.assertFalse(ok) + from_mime, ok = QgsTextFormat.fromMimeData(md) + self.assertTrue(ok) + self.checkTextFormat(from_mime) + + def testRestoreUsingFamilyList(self): + format = QgsTextFormat() + + doc = QDomDocument("testdoc") + elem = format.writeXml(doc, QgsReadWriteContext()) + parent = doc.createElement("settings") + parent.appendChild(elem) + doc.appendChild(parent) + + # swap out font name in xml to one which doesn't exist on system + xml = doc.toString() + xml = xml.replace(QFont().family(), 'NOT A REAL FONT') + doc = QDomDocument("testdoc") + doc.setContent(xml) + parent = doc.firstChildElement('settings') + + t = QgsTextFormat() + t.readXml(parent, QgsReadWriteContext()) + # should be default font + self.assertEqual(t.font().family(), QFont().family()) + + format.setFamilies(['not real', 'still not real', getTestFont().family()]) + + doc = QDomDocument("testdoc") + elem = format.writeXml(doc, QgsReadWriteContext()) + parent = doc.createElement("settings") + parent.appendChild(elem) + doc.appendChild(parent) + + # swap out font name in xml to one which doesn't exist on system + xml = doc.toString() + xml = xml.replace(QFont().family(), 'NOT A REAL FONT') + doc = QDomDocument("testdoc") + doc.setContent(xml) + parent = doc.firstChildElement('settings') + + t = QgsTextFormat() + t.readXml(parent, QgsReadWriteContext()) + self.assertEqual(t.families(), ['not real', 'still not real', getTestFont().family()]) + # should have skipped the missing fonts and fallen back to the test font family entry, NOT the default application font! + self.assertEqual(t.font().family(), getTestFont().family()) + + def testMultiplyOpacity(self): + + s = self.createFormatSettings() + old_opacity = s.opacity() + old_buffer_opacity = s.buffer().opacity() + old_shadow_opacity = s.shadow().opacity() + old_mask_opacity = s.mask().opacity() + + s.multiplyOpacity(0.5) + + self.assertEqual(s.opacity(), old_opacity * 0.5) + self.assertEqual(s.buffer().opacity(), old_buffer_opacity * 0.5) + self.assertEqual(s.shadow().opacity(), old_shadow_opacity * 0.5) + self.assertEqual(s.mask().opacity(), old_mask_opacity * 0.5) + + s.multiplyOpacity(2.0) + self.checkTextFormat(s) + + def testContainsAdvancedEffects(self): + t = QgsTextFormat() + self.assertFalse(t.containsAdvancedEffects()) + t.setBlendMode(QPainter.CompositionMode.CompositionMode_DestinationAtop) + self.assertTrue(t.containsAdvancedEffects()) + + t = QgsTextFormat() + t.buffer().setBlendMode(QPainter.CompositionMode.CompositionMode_DestinationAtop) + self.assertFalse(t.containsAdvancedEffects()) + t.buffer().setEnabled(True) + self.assertTrue(t.containsAdvancedEffects()) + t.buffer().setBlendMode(QPainter.CompositionMode.CompositionMode_SourceOver) + self.assertFalse(t.containsAdvancedEffects()) + + t = QgsTextFormat() + t.background().setBlendMode(QPainter.CompositionMode.CompositionMode_DestinationAtop) + self.assertFalse(t.containsAdvancedEffects()) + t.background().setEnabled(True) + self.assertTrue(t.containsAdvancedEffects()) + t.background().setBlendMode(QPainter.CompositionMode.CompositionMode_SourceOver) + self.assertFalse(t.containsAdvancedEffects()) + + t = QgsTextFormat() + t.shadow().setBlendMode(QPainter.CompositionMode.CompositionMode_DestinationAtop) + self.assertFalse(t.containsAdvancedEffects()) + t.shadow().setEnabled(True) + self.assertTrue(t.containsAdvancedEffects()) + t.shadow().setBlendMode(QPainter.CompositionMode.CompositionMode_SourceOver) + self.assertFalse(t.containsAdvancedEffects()) + def testRestoringAndSavingMissingFont(self): # test that a missing font on text format load will still save with the same missing font unless manually changed document = QDomDocument() @@ -50,6 +1035,461 @@ def testRestoringAndSavingMissingFont(self): element = text_format.writeXml(document, context) self.assertEqual(element.attribute("fontFamily"), "QGIS Vera Sans") + def testDataDefinedBufferSettings(self): + f = QgsTextFormat() + context = QgsRenderContext() + + # buffer enabled + f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.BufferDraw, QgsProperty.fromExpression('1')) + f.updateDataDefinedProperties(context) + self.assertTrue(f.buffer().enabled()) + f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.BufferDraw, QgsProperty.fromExpression('0')) + context = QgsRenderContext() + f.updateDataDefinedProperties(context) + self.assertFalse(f.buffer().enabled()) + + # buffer size + f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.BufferSize, QgsProperty.fromExpression('7.8')) + f.updateDataDefinedProperties(context) + self.assertEqual(f.buffer().size(), 7.8) + f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.BufferUnit, QgsProperty.fromExpression("'pixel'")) + f.updateDataDefinedProperties(context) + self.assertEqual(f.buffer().sizeUnit(), Qgis.RenderUnit.Pixels) + + # buffer opacity + f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.BufferOpacity, QgsProperty.fromExpression('37')) + f.updateDataDefinedProperties(context) + self.assertEqual(f.buffer().opacity(), 0.37) + + # blend mode + f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.BufferBlendMode, QgsProperty.fromExpression("'burn'")) + f.updateDataDefinedProperties(context) + self.assertEqual(f.buffer().blendMode(), QPainter.CompositionMode.CompositionMode_ColorBurn) + + # join style + f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.BufferJoinStyle, + QgsProperty.fromExpression("'miter'")) + f.updateDataDefinedProperties(context) + self.assertEqual(f.buffer().joinStyle(), Qt.PenJoinStyle.MiterJoin) + + # color + f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.BufferColor, QgsProperty.fromExpression("'#ff0088'")) + f.updateDataDefinedProperties(context) + self.assertEqual(f.buffer().color().name(), '#ff0088') + + def testDataDefinedMaskSettings(self): + f = QgsTextFormat() + context = QgsRenderContext() + + # mask enabled + f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.MaskEnabled, QgsProperty.fromExpression('1')) + f.updateDataDefinedProperties(context) + self.assertTrue(f.mask().enabled()) + f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.MaskEnabled, QgsProperty.fromExpression('0')) + context = QgsRenderContext() + f.updateDataDefinedProperties(context) + self.assertFalse(f.mask().enabled()) + + # mask buffer size + f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.MaskBufferSize, QgsProperty.fromExpression('7.8')) + f.updateDataDefinedProperties(context) + self.assertEqual(f.mask().size(), 7.8) + f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.MaskBufferUnit, QgsProperty.fromExpression("'pixel'")) + f.updateDataDefinedProperties(context) + self.assertEqual(f.mask().sizeUnit(), Qgis.RenderUnit.Pixels) + + # mask opacity + f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.MaskOpacity, QgsProperty.fromExpression('37')) + f.updateDataDefinedProperties(context) + self.assertEqual(f.mask().opacity(), 0.37) + + # join style + f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.MaskJoinStyle, QgsProperty.fromExpression("'miter'")) + f.updateDataDefinedProperties(context) + self.assertEqual(f.mask().joinStyle(), Qt.PenJoinStyle.MiterJoin) + + def testDataDefinedBackgroundSettings(self): + f = QgsTextFormat() + context = QgsRenderContext() + + # background enabled + f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.ShapeDraw, QgsProperty.fromExpression('1')) + f.updateDataDefinedProperties(context) + self.assertTrue(f.background().enabled()) + f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.ShapeDraw, QgsProperty.fromExpression('0')) + context = QgsRenderContext() + f.updateDataDefinedProperties(context) + self.assertFalse(f.background().enabled()) + + # background size + f.background().setSize(QSizeF(13, 14)) + f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.ShapeSizeX, QgsProperty.fromExpression('7.8')) + f.updateDataDefinedProperties(context) + self.assertEqual(f.background().size().width(), 7.8) + self.assertEqual(f.background().size().height(), 14) + f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.ShapeSizeY, QgsProperty.fromExpression('17.8')) + f.updateDataDefinedProperties(context) + self.assertEqual(f.background().size().width(), 7.8) + self.assertEqual(f.background().size().height(), 17.8) + + f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.ShapeSizeUnits, QgsProperty.fromExpression("'pixel'")) + f.updateDataDefinedProperties(context) + self.assertEqual(f.background().sizeUnit(), Qgis.RenderUnit.Pixels) + + # shape kind + f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.ShapeKind, QgsProperty.fromExpression("'square'")) + f.updateDataDefinedProperties(context) + self.assertEqual(f.background().type(), QgsTextBackgroundSettings.ShapeType.ShapeSquare) + f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.ShapeKind, QgsProperty.fromExpression("'ellipse'")) + f.updateDataDefinedProperties(context) + self.assertEqual(f.background().type(), QgsTextBackgroundSettings.ShapeType.ShapeEllipse) + f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.ShapeKind, QgsProperty.fromExpression("'circle'")) + f.updateDataDefinedProperties(context) + self.assertEqual(f.background().type(), QgsTextBackgroundSettings.ShapeType.ShapeCircle) + f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.ShapeKind, QgsProperty.fromExpression("'svg'")) + f.updateDataDefinedProperties(context) + self.assertEqual(f.background().type(), QgsTextBackgroundSettings.ShapeType.ShapeSVG) + f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.ShapeKind, QgsProperty.fromExpression("'marker'")) + f.updateDataDefinedProperties(context) + self.assertEqual(f.background().type(), QgsTextBackgroundSettings.ShapeType.ShapeMarkerSymbol) + f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.ShapeKind, QgsProperty.fromExpression("'rect'")) + f.updateDataDefinedProperties(context) + self.assertEqual(f.background().type(), QgsTextBackgroundSettings.ShapeType.ShapeRectangle) + + # size type + f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.ShapeSizeType, QgsProperty.fromExpression("'fixed'")) + f.updateDataDefinedProperties(context) + self.assertEqual(f.background().sizeType(), QgsTextBackgroundSettings.SizeType.SizeFixed) + f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.ShapeSizeType, QgsProperty.fromExpression("'buffer'")) + f.updateDataDefinedProperties(context) + self.assertEqual(f.background().sizeType(), QgsTextBackgroundSettings.SizeType.SizeBuffer) + + # svg path + f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.ShapeSVGFile, QgsProperty.fromExpression("'my.svg'")) + f.updateDataDefinedProperties(context) + self.assertEqual(f.background().svgFile(), 'my.svg') + + # shape rotation + f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.ShapeRotation, QgsProperty.fromExpression('67')) + f.updateDataDefinedProperties(context) + self.assertEqual(f.background().rotation(), 67) + f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.ShapeRotationType, + QgsProperty.fromExpression("'offset'")) + f.updateDataDefinedProperties(context) + self.assertEqual(f.background().rotationType(), QgsTextBackgroundSettings.RotationType.RotationOffset) + f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.ShapeRotationType, + QgsProperty.fromExpression("'fixed'")) + f.updateDataDefinedProperties(context) + self.assertEqual(f.background().rotationType(), QgsTextBackgroundSettings.RotationType.RotationFixed) + f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.ShapeRotationType, + QgsProperty.fromExpression("'sync'")) + f.updateDataDefinedProperties(context) + self.assertEqual(f.background().rotationType(), QgsTextBackgroundSettings.RotationType.RotationSync) + + # shape offset + f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.ShapeOffset, QgsProperty.fromExpression("'7,9'")) + f.updateDataDefinedProperties(context) + self.assertEqual(f.background().offset(), QPointF(7, 9)) + f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.ShapeOffsetUnits, + QgsProperty.fromExpression("'pixel'")) + f.updateDataDefinedProperties(context) + self.assertEqual(f.background().offsetUnit(), Qgis.RenderUnit.Pixels) + + # shape radii + f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.ShapeRadii, QgsProperty.fromExpression("'18,19'")) + f.updateDataDefinedProperties(context) + self.assertEqual(f.background().radii(), QSizeF(18, 19)) + f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.ShapeRadiiUnits, + QgsProperty.fromExpression("'pixel'")) + f.updateDataDefinedProperties(context) + self.assertEqual(f.background().radiiUnit(), Qgis.RenderUnit.Pixels) + + # shape opacity + f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.ShapeOpacity, QgsProperty.fromExpression('37')) + f.updateDataDefinedProperties(context) + self.assertEqual(f.background().opacity(), 0.37) + + # color + f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.ShapeFillColor, + QgsProperty.fromExpression("'#ff0088'")) + f.updateDataDefinedProperties(context) + self.assertEqual(f.background().fillColor().name(), '#ff0088') + f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.ShapeStrokeColor, + QgsProperty.fromExpression("'#8800ff'")) + f.updateDataDefinedProperties(context) + self.assertEqual(f.background().strokeColor().name(), '#8800ff') + + # stroke width + f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.ShapeStrokeWidth, QgsProperty.fromExpression('88')) + f.updateDataDefinedProperties(context) + self.assertEqual(f.background().strokeWidth(), 88) + f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.ShapeStrokeWidthUnits, + QgsProperty.fromExpression("'pixel'")) + f.updateDataDefinedProperties(context) + self.assertEqual(f.background().strokeWidthUnit(), Qgis.RenderUnit.Pixels) + + # blend mode + f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.ShapeBlendMode, QgsProperty.fromExpression("'burn'")) + f.updateDataDefinedProperties(context) + self.assertEqual(f.background().blendMode(), QPainter.CompositionMode.CompositionMode_ColorBurn) + + # join style + f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.ShapeJoinStyle, QgsProperty.fromExpression("'miter'")) + f.updateDataDefinedProperties(context) + self.assertEqual(f.background().joinStyle(), Qt.PenJoinStyle.MiterJoin) + + def testDataDefinedShadowSettings(self): + f = QgsTextFormat() + context = QgsRenderContext() + + # shadow enabled + f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.ShadowDraw, QgsProperty.fromExpression('1')) + f.updateDataDefinedProperties(context) + self.assertTrue(f.shadow().enabled()) + f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.ShadowDraw, QgsProperty.fromExpression('0')) + context = QgsRenderContext() + f.updateDataDefinedProperties(context) + self.assertFalse(f.shadow().enabled()) + + # placement type + f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.ShadowUnder, QgsProperty.fromExpression("'text'")) + f.updateDataDefinedProperties(context) + self.assertEqual(f.shadow().shadowPlacement(), QgsTextShadowSettings.ShadowPlacement.ShadowText) + f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.ShadowUnder, QgsProperty.fromExpression("'buffer'")) + f.updateDataDefinedProperties(context) + self.assertEqual(f.shadow().shadowPlacement(), QgsTextShadowSettings.ShadowPlacement.ShadowBuffer) + f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.ShadowUnder, + QgsProperty.fromExpression("'background'")) + f.updateDataDefinedProperties(context) + self.assertEqual(f.shadow().shadowPlacement(), QgsTextShadowSettings.ShadowPlacement.ShadowShape) + f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.ShadowUnder, QgsProperty.fromExpression("'svg'")) + f.updateDataDefinedProperties(context) + self.assertEqual(f.shadow().shadowPlacement(), QgsTextShadowSettings.ShadowPlacement.ShadowLowest) + f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.ShadowUnder, QgsProperty.fromExpression("'lowest'")) + + # offset angle + f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.ShadowOffsetAngle, QgsProperty.fromExpression('67')) + f.updateDataDefinedProperties(context) + self.assertEqual(f.shadow().offsetAngle(), 67) + + # offset distance + f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.ShadowOffsetDist, QgsProperty.fromExpression('38')) + f.updateDataDefinedProperties(context) + self.assertEqual(f.shadow().offsetDistance(), 38) + f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.ShadowOffsetUnits, + QgsProperty.fromExpression("'pixel'")) + f.updateDataDefinedProperties(context) + self.assertEqual(f.shadow().offsetUnit(), Qgis.RenderUnit.Pixels) + + # radius + f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.ShadowRadius, QgsProperty.fromExpression('58')) + f.updateDataDefinedProperties(context) + self.assertEqual(f.shadow().blurRadius(), 58) + f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.ShadowRadiusUnits, + QgsProperty.fromExpression("'pixel'")) + f.updateDataDefinedProperties(context) + self.assertEqual(f.shadow().blurRadiusUnit(), Qgis.RenderUnit.Pixels) + + # opacity + f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.ShadowOpacity, QgsProperty.fromExpression('37')) + f.updateDataDefinedProperties(context) + self.assertEqual(f.shadow().opacity(), 0.37) + + # color + f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.ShadowColor, QgsProperty.fromExpression("'#ff0088'")) + f.updateDataDefinedProperties(context) + self.assertEqual(f.shadow().color().name(), '#ff0088') + + # blend mode + f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.ShadowBlendMode, QgsProperty.fromExpression("'burn'")) + f.updateDataDefinedProperties(context) + self.assertEqual(f.shadow().blendMode(), QPainter.CompositionMode.CompositionMode_ColorBurn) + + def testDataDefinedFormatSettings(self): + f = QgsTextFormat() + font = f.font() + font.setUnderline(True) + font.setStrikeOut(True) + font.setWordSpacing(5.7) + font.setLetterSpacing(QFont.SpacingType.AbsoluteSpacing, 3.3) + f.setFont(font) + context = QgsRenderContext() + + # family + f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.Family, QgsProperty.fromExpression( + f"'{QgsFontUtils.getStandardTestFont().family()}'")) + f.updateDataDefinedProperties(context) + self.assertEqual(f.font().family(), QgsFontUtils.getStandardTestFont().family()) + + # style + f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.FontStyle, QgsProperty.fromExpression("'Bold'")) + f.updateDataDefinedProperties(context) + self.assertEqual(f.font().styleName(), 'Bold') + f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.FontStyle, QgsProperty.fromExpression("'Roman'")) + f.updateDataDefinedProperties(context) + self.assertEqual(f.font().styleName(), 'Roman') + f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.Bold, QgsProperty.fromExpression("1")) + f.updateDataDefinedProperties(context) + self.assertTrue(f.font().bold()) + self.assertFalse(f.font().italic()) + f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.Bold, QgsProperty.fromExpression("0")) + f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.Italic, QgsProperty.fromExpression("1")) + f.updateDataDefinedProperties(context) + self.assertFalse(f.font().bold()) + self.assertTrue(f.font().italic()) + self.assertTrue(f.font().underline()) + self.assertTrue(f.font().strikeOut()) + self.assertAlmostEqual(f.font().wordSpacing(), 5.7, 1) + self.assertAlmostEqual(f.font().letterSpacing(), 3.3, 1) + + # underline + f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.Underline, QgsProperty.fromExpression("0")) + f.updateDataDefinedProperties(context) + self.assertFalse(f.font().underline()) + f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.Underline, QgsProperty.fromExpression("1")) + f.updateDataDefinedProperties(context) + self.assertTrue(f.font().underline()) + + # strikeout + f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.Strikeout, QgsProperty.fromExpression("0")) + f.updateDataDefinedProperties(context) + self.assertFalse(f.font().strikeOut()) + f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.Strikeout, QgsProperty.fromExpression("1")) + f.updateDataDefinedProperties(context) + self.assertTrue(f.font().strikeOut()) + + # color + f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.Color, QgsProperty.fromExpression("'#ff0088'")) + f.updateDataDefinedProperties(context) + self.assertEqual(f.color().name(), '#ff0088') + + # size + f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.Size, QgsProperty.fromExpression('38')) + f.updateDataDefinedProperties(context) + self.assertEqual(f.size(), 38) + f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.FontSizeUnit, QgsProperty.fromExpression("'pixel'")) + f.updateDataDefinedProperties(context) + self.assertEqual(f.sizeUnit(), Qgis.RenderUnit.Pixels) + + # opacity + f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.FontOpacity, QgsProperty.fromExpression('37')) + f.updateDataDefinedProperties(context) + self.assertEqual(f.opacity(), 0.37) + + # letter spacing + f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.FontLetterSpacing, QgsProperty.fromExpression('58')) + f.updateDataDefinedProperties(context) + self.assertEqual(f.font().letterSpacing(), 58) + + # word spacing + f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.FontWordSpacing, QgsProperty.fromExpression('8')) + f.updateDataDefinedProperties(context) + self.assertEqual(f.font().wordSpacing(), 8) + + # blend mode + f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.FontBlendMode, QgsProperty.fromExpression("'burn'")) + f.updateDataDefinedProperties(context) + self.assertEqual(f.blendMode(), QPainter.CompositionMode.CompositionMode_ColorBurn) + + if int(QT_VERSION_STR.split('.')[0]) > 6 or ( + int(QT_VERSION_STR.split('.')[0]) == 6 and int(QT_VERSION_STR.split('.')[1]) >= 3): + # stretch + f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.FontStretchFactor, QgsProperty.fromExpression("135")) + f.updateDataDefinedProperties(context) + self.assertEqual(f.stretchFactor(), 135) + + f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.TabStopDistance, QgsProperty.fromExpression("15")) + f.updateDataDefinedProperties(context) + self.assertEqual(f.tabStopDistance(), 15) + + def testFontFoundFromLayer(self): + layer = createEmptyLayer() + layer.setCustomProperty('labeling/fontFamily', 'asdasd') + f = QgsTextFormat() + f.readFromLayer(layer) + self.assertFalse(f.fontFound()) + + font = getTestFont() + layer.setCustomProperty('labeling/fontFamily', font.family()) + f.readFromLayer(layer) + self.assertTrue(f.fontFound()) + + def testFontFoundFromXml(self): + doc = QDomDocument("testdoc") + f = QgsTextFormat() + elem = f.writeXml(doc, QgsReadWriteContext()) + elem.setAttribute('fontFamily', 'asdfasdfsadf') + parent = doc.createElement("parent") + parent.appendChild(elem) + + f.readXml(parent, QgsReadWriteContext()) + self.assertFalse(f.fontFound()) + + font = getTestFont() + elem.setAttribute('fontFamily', font.family()) + f.readXml(parent, QgsReadWriteContext()) + self.assertTrue(f.fontFound()) + + def testFromQFont(self): + qfont = getTestFont() + qfont.setPointSizeF(16.5) + qfont.setLetterSpacing(QFont.SpacingType.AbsoluteSpacing, 3) + + format = QgsTextFormat.fromQFont(qfont) + self.assertEqual(format.font().family(), qfont.family()) + self.assertEqual(format.font().letterSpacing(), 3.0) + self.assertEqual(format.size(), 16.5) + self.assertEqual(format.sizeUnit(), Qgis.RenderUnit.Points) + + qfont.setPixelSize(12) + format = QgsTextFormat.fromQFont(qfont) + self.assertEqual(format.size(), 12.0) + self.assertEqual(format.sizeUnit(), Qgis.RenderUnit.Pixels) + + def testToQFont(self): + s = QgsTextFormat() + f = getTestFont() + f.setLetterSpacing(QFont.SpacingType.AbsoluteSpacing, 3) + s.setFont(f) + s.setNamedStyle('Italic') + s.setSize(5.5) + s.setSizeUnit(Qgis.RenderUnit.Points) + + qfont = s.toQFont() + self.assertEqual(qfont.family(), f.family()) + self.assertEqual(qfont.pointSizeF(), 5.5) + self.assertEqual(qfont.letterSpacing(), 3.0) + + s.setSize(5) + s.setSizeUnit(Qgis.RenderUnit.Pixels) + qfont = s.toQFont() + self.assertEqual(qfont.pixelSize(), 5) + + s.setSize(5) + s.setSizeUnit(Qgis.RenderUnit.Millimeters) + qfont = s.toQFont() + self.assertAlmostEqual(qfont.pointSizeF(), 14.17, 2) + + s.setSizeUnit(Qgis.RenderUnit.Inches) + qfont = s.toQFont() + self.assertAlmostEqual(qfont.pointSizeF(), 360.0, 2) + + self.assertFalse(qfont.bold()) + s.setForcedBold(True) + qfont = s.toQFont() + self.assertTrue(qfont.bold()) + + self.assertFalse(qfont.italic()) + s.setForcedItalic(True) + qfont = s.toQFont() + self.assertTrue(qfont.italic()) + + if int(QT_VERSION_STR.split('.')[0]) > 6 or ( + int(QT_VERSION_STR.split('.')[0]) == 6 and int(QT_VERSION_STR.split('.')[1]) >= 3): + s.setStretchFactor(115) + qfont = s.toQFont() + self.assertEqual(qfont.stretch(), 115) + if __name__ == '__main__': unittest.main() diff --git a/tests/src/python/test_qgstextrenderer.py b/tests/src/python/test_qgstextrenderer.py index 2f4ac84819da..d6d98c275df8 100644 --- a/tests/src/python/test_qgstextrenderer.py +++ b/tests/src/python/test_qgstextrenderer.py @@ -25,39 +25,30 @@ from qgis.PyQt.QtGui import ( QBrush, QColor, - QFont, QImage, QPainter, QPen, QPolygonF ) -from qgis.PyQt.QtXml import QDomDocument from qgis.core import ( Qgis, QgsBlurEffect, QgsFillSymbol, QgsFontUtils, QgsMapSettings, - QgsMapUnitScale, QgsMarkerSymbol, QgsPalLayerSettings, QgsProperty, - QgsReadWriteContext, QgsRectangle, QgsRenderContext, QgsSimpleFillSymbolLayer, - QgsStringUtils, - QgsSymbolLayerReference, QgsTextBackgroundSettings, - QgsTextBufferSettings, QgsTextDocument, QgsTextDocumentMetrics, QgsTextFormat, - QgsTextMaskSettings, QgsTextRenderer, QgsTextShadowSettings, - QgsUnitTypes, - QgsVectorLayer, + QgsUnitTypes ) import unittest from qgis.testing import start_app, QgisTestCase @@ -67,12 +58,6 @@ start_app() -def createEmptyLayer(): - layer = QgsVectorLayer("Point", "addfeat", "memory") - assert layer.isValid() - return layer - - class PyQgsTextRenderer(QgisTestCase): @classmethod @@ -84,132 +69,6 @@ def setUpClass(cls): def control_path_prefix(cls): return 'text_renderer' - def testValid(self): - t = QgsTextFormat() - self.assertFalse(t.isValid()) - - tt = QgsTextFormat(t) - self.assertFalse(tt.isValid()) - - t.setValid() - self.assertTrue(t.isValid()) - tt = QgsTextFormat(t) - self.assertTrue(tt.isValid()) - - doc = QDomDocument() - elem = t.writeXml(doc, QgsReadWriteContext()) - parent = doc.createElement("settings") - parent.appendChild(elem) - t3 = QgsTextFormat() - t3.readXml(parent, QgsReadWriteContext()) - self.assertTrue(t3.isValid()) - - t = QgsTextFormat() - t.buffer().setEnabled(True) - self.assertTrue(t.isValid()) - - t = QgsTextFormat() - t.background().setEnabled(True) - self.assertTrue(t.isValid()) - - t = QgsTextFormat() - t.shadow().setEnabled(True) - self.assertTrue(t.isValid()) - - t = QgsTextFormat() - t.mask().setEnabled(True) - self.assertTrue(t.isValid()) - - t = QgsTextFormat() - t.font() - self.assertFalse(t.isValid()) - t.setFont(QFont()) - self.assertTrue(t.isValid()) - - t = QgsTextFormat() - t.setNamedStyle('Bold') - self.assertTrue(t.isValid()) - - t = QgsTextFormat() - t.setSize(20) - self.assertTrue(t.isValid()) - - t = QgsTextFormat() - t.setSizeUnit(QgsUnitTypes.RenderUnit.RenderPixels) - self.assertTrue(t.isValid()) - - t = QgsTextFormat() - t.setSizeMapUnitScale(QgsMapUnitScale(5, 10)) - self.assertTrue(t.isValid()) - - t = QgsTextFormat() - t.setColor(QColor(255, 0, 0)) - self.assertTrue(t.isValid()) - - t = QgsTextFormat() - t.setOpacity(0.2) - self.assertTrue(t.isValid()) - - t = QgsTextFormat() - t.setBlendMode(QPainter.CompositionMode.CompositionMode_Darken) - self.assertTrue(t.isValid()) - - t = QgsTextFormat() - t.setLineHeight(20) - self.assertTrue(t.isValid()) - - t = QgsTextFormat() - t.setLineHeightUnit(QgsUnitTypes.RenderUnit.RenderPoints) - self.assertTrue(t.isValid()) - - t = QgsTextFormat() - t.setTabStopDistance(3) - self.assertTrue(t.isValid()) - - t = QgsTextFormat() - t.setTabStopDistanceUnit(Qgis.RenderUnit.Points) - self.assertTrue(t.isValid()) - - t = QgsTextFormat() - t.setTabStopDistanceMapUnitScale(QgsMapUnitScale(5, 10)) - self.assertTrue(t.isValid()) - - t = QgsTextFormat() - t.setOrientation(QgsTextFormat.TextOrientation.VerticalOrientation) - self.assertTrue(t.isValid()) - - t = QgsTextFormat() - t.setCapitalization(QgsStringUtils.Capitalization.TitleCase) - self.assertTrue(t.isValid()) - - t = QgsTextFormat() - t.setAllowHtmlFormatting(True) - self.assertTrue(t.isValid()) - - t = QgsTextFormat() - t.setPreviewBackgroundColor(QColor(255, 0, 0)) - self.assertTrue(t.isValid()) - - t = QgsTextFormat() - t.setFamilies(['Arial', 'Comic Sans']) - self.assertTrue(t.isValid()) - - t = QgsTextFormat() - t.setStretchFactor(110) - self.assertTrue(t.isValid()) - - t = QgsTextFormat() - t.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.Bold, QgsProperty.fromValue(True)) - self.assertTrue(t.isValid()) - - t = QgsTextFormat() - t.setForcedBold(True) - self.assertTrue(t.isValid()) - - t = QgsTextFormat() - t.setForcedItalic(True) - self.assertTrue(t.isValid()) - def testAlignmentConversion(self): self.assertEqual(QgsTextRenderer.convertQtHAlignment(Qt.AlignmentFlag.AlignLeft), QgsTextRenderer.HAlignment.AlignLeft) self.assertEqual(QgsTextRenderer.convertQtHAlignment(Qt.AlignmentFlag.AlignRight), QgsTextRenderer.HAlignment.AlignRight) @@ -224,1287 +83,6 @@ def testAlignmentConversion(self): # note supported, should fallback to bottom self.assertEqual(QgsTextRenderer.convertQtVAlignment(Qt.AlignmentFlag.AlignBaseline), QgsTextRenderer.VAlignment.AlignBottom) - def createBufferSettings(self): - s = QgsTextBufferSettings() - s.setEnabled(True) - s.setSize(5) - s.setSizeUnit(QgsUnitTypes.RenderUnit.RenderPoints) - s.setSizeMapUnitScale(QgsMapUnitScale(1, 2)) - s.setColor(QColor(255, 0, 0)) - s.setFillBufferInterior(True) - s.setOpacity(0.5) - s.setJoinStyle(Qt.PenJoinStyle.RoundJoin) - s.setBlendMode(QPainter.CompositionMode.CompositionMode_DestinationAtop) - return s - - def testBufferEquality(self): - s = self.createBufferSettings() - s2 = self.createBufferSettings() - self.assertEqual(s, s2) - - s.setEnabled(False) - self.assertNotEqual(s, s2) - s = self.createBufferSettings() - - s.setSize(15) - self.assertNotEqual(s, s2) - s = self.createBufferSettings() - - s.setSizeUnit(QgsUnitTypes.RenderUnit.RenderPixels) - self.assertNotEqual(s, s2) - s = self.createBufferSettings() - - s.setSizeMapUnitScale(QgsMapUnitScale(11, 12)) - self.assertNotEqual(s, s2) - s = self.createBufferSettings() - - s.setColor(QColor(255, 255, 0)) - self.assertNotEqual(s, s2) - s = self.createBufferSettings() - - s.setFillBufferInterior(False) - self.assertNotEqual(s, s2) - s = self.createBufferSettings() - - s.setOpacity(0.6) - self.assertNotEqual(s, s2) - s = self.createBufferSettings() - - s.setJoinStyle(Qt.PenJoinStyle.MiterJoin) - self.assertNotEqual(s, s2) - s = self.createBufferSettings() - - s.setBlendMode(QPainter.CompositionMode.CompositionMode_Darken) - self.assertNotEqual(s, s2) - - def checkBufferSettings(self, s): - """ test QgsTextBufferSettings """ - self.assertTrue(s.enabled()) - self.assertEqual(s.size(), 5) - self.assertEqual(s.sizeUnit(), QgsUnitTypes.RenderUnit.RenderPoints) - self.assertEqual(s.sizeMapUnitScale(), QgsMapUnitScale(1, 2)) - self.assertEqual(s.color(), QColor(255, 0, 0)) - self.assertTrue(s.fillBufferInterior()) - self.assertEqual(s.opacity(), 0.5) - self.assertEqual(s.joinStyle(), Qt.PenJoinStyle.RoundJoin) - self.assertEqual(s.blendMode(), QPainter.CompositionMode.CompositionMode_DestinationAtop) - - def testBufferGettersSetters(self): - s = self.createBufferSettings() - self.checkBufferSettings(s) - - # some other checks - s.setEnabled(False) - self.assertFalse(s.enabled()) - s.setEnabled(True) - self.assertTrue(s.enabled()) - s.setFillBufferInterior(False) - self.assertFalse(s.fillBufferInterior()) - s.setFillBufferInterior(True) - self.assertTrue(s.fillBufferInterior()) - - def testBufferReadWriteXml(self): - """test saving and restoring state of a buffer to xml""" - doc = QDomDocument("testdoc") - s = self.createBufferSettings() - elem = s.writeXml(doc) - parent = doc.createElement("settings") - parent.appendChild(elem) - t = QgsTextBufferSettings() - t.readXml(parent) - self.checkBufferSettings(t) - - def testBufferCopy(self): - s = self.createBufferSettings() - s2 = s - self.checkBufferSettings(s2) - s3 = QgsTextBufferSettings(s) - self.checkBufferSettings(s3) - - def createMaskSettings(self): - s = QgsTextMaskSettings() - s.setEnabled(True) - s.setType(QgsTextMaskSettings.MaskType.MaskBuffer) - s.setSize(5) - s.setSizeUnit(QgsUnitTypes.RenderUnit.RenderPoints) - s.setSizeMapUnitScale(QgsMapUnitScale(1, 2)) - s.setOpacity(0.5) - s.setJoinStyle(Qt.PenJoinStyle.RoundJoin) - s.setMaskedSymbolLayers([QgsSymbolLayerReference("layerid1", "symbol1"), - QgsSymbolLayerReference("layerid2", "symbol2")]) - return s - - def testMaskEquality(self): - s = self.createMaskSettings() - s2 = self.createMaskSettings() - self.assertEqual(s, s2) - - s.setEnabled(False) - self.assertNotEqual(s, s2) - s = self.createMaskSettings() - - s.setSize(15) - self.assertNotEqual(s, s2) - s = self.createMaskSettings() - - s.setSizeUnit(QgsUnitTypes.RenderUnit.RenderPixels) - self.assertNotEqual(s, s2) - s = self.createMaskSettings() - - s.setSizeMapUnitScale(QgsMapUnitScale(11, 12)) - self.assertNotEqual(s, s2) - s = self.createMaskSettings() - - s.setOpacity(0.6) - self.assertNotEqual(s, s2) - s = self.createMaskSettings() - - s.setJoinStyle(Qt.PenJoinStyle.MiterJoin) - self.assertNotEqual(s, s2) - s = self.createMaskSettings() - - s.setMaskedSymbolLayers([QgsSymbolLayerReference("layerid11", "symbol1"), - QgsSymbolLayerReference("layerid21", "symbol2")]) - self.assertNotEqual(s, s2) - - def checkMaskSettings(self, s): - """ test QgsTextMaskSettings """ - self.assertTrue(s.enabled()) - self.assertEqual(s.type(), QgsTextMaskSettings.MaskType.MaskBuffer) - self.assertEqual(s.size(), 5) - self.assertEqual(s.sizeUnit(), QgsUnitTypes.RenderUnit.RenderPoints) - self.assertEqual(s.sizeMapUnitScale(), QgsMapUnitScale(1, 2)) - self.assertEqual(s.opacity(), 0.5) - self.assertEqual(s.joinStyle(), Qt.PenJoinStyle.RoundJoin) - self.assertEqual(s.maskedSymbolLayers(), [QgsSymbolLayerReference("layerid1", "symbol1"), - QgsSymbolLayerReference("layerid2", "symbol2")]) - - def testMaskGettersSetters(self): - s = self.createMaskSettings() - self.checkMaskSettings(s) - - # some other checks - s.setEnabled(False) - self.assertFalse(s.enabled()) - - def testMaskReadWriteXml(self): - """test saving and restoring state of a mask to xml""" - doc = QDomDocument("testdoc") - s = self.createMaskSettings() - elem = s.writeXml(doc) - parent = doc.createElement("settings") - parent.appendChild(elem) - t = QgsTextMaskSettings() - t.readXml(parent) - self.checkMaskSettings(t) - - def testMaskCopy(self): - s = self.createMaskSettings() - s2 = s - self.checkMaskSettings(s2) - s3 = QgsTextMaskSettings(s) - self.checkMaskSettings(s3) - - def createBackgroundSettings(self): - s = QgsTextBackgroundSettings() - s.setEnabled(True) - s.setType(QgsTextBackgroundSettings.ShapeType.ShapeEllipse) - s.setSvgFile('svg.svg') - s.setSizeType(QgsTextBackgroundSettings.SizeType.SizePercent) - s.setSize(QSizeF(1, 2)) - s.setSizeUnit(QgsUnitTypes.RenderUnit.RenderPoints) - s.setSizeMapUnitScale(QgsMapUnitScale(1, 2)) - s.setRotationType(QgsTextBackgroundSettings.RotationType.RotationFixed) - s.setRotation(45) - s.setOffset(QPointF(3, 4)) - s.setOffsetUnit(QgsUnitTypes.RenderUnit.RenderMapUnits) - s.setOffsetMapUnitScale(QgsMapUnitScale(5, 6)) - s.setRadii(QSizeF(11, 12)) - s.setRadiiUnit(QgsUnitTypes.RenderUnit.RenderPercentage) - s.setRadiiMapUnitScale(QgsMapUnitScale(15, 16)) - s.setFillColor(QColor(255, 0, 0)) - s.setStrokeColor(QColor(0, 255, 0)) - s.setOpacity(0.5) - s.setJoinStyle(Qt.PenJoinStyle.RoundJoin) - s.setBlendMode(QPainter.CompositionMode.CompositionMode_DestinationAtop) - s.setStrokeWidth(7) - s.setStrokeWidthUnit(QgsUnitTypes.RenderUnit.RenderPoints) - s.setStrokeWidthMapUnitScale(QgsMapUnitScale(QgsMapUnitScale(25, 26))) - - marker = QgsMarkerSymbol() - marker.setColor(QColor(100, 112, 134)) - s.setMarkerSymbol(marker) - - return s - - def testBackgroundEquality(self): - s = self.createBackgroundSettings() - s2 = self.createBackgroundSettings() - self.assertEqual(s, s2) - - s.setEnabled(False) - self.assertNotEqual(s, s2) - s = self.createBackgroundSettings() - - s.setType(QgsTextBackgroundSettings.ShapeType.ShapeRectangle) - self.assertNotEqual(s, s2) - s = self.createBackgroundSettings() - - s.setSvgFile('svg2.svg') - self.assertNotEqual(s, s2) - s = self.createBackgroundSettings() - - s.setSizeType(QgsTextBackgroundSettings.SizeType.SizeFixed) - self.assertNotEqual(s, s2) - s = self.createBackgroundSettings() - - s.setSize(QSizeF(1, 22)) - self.assertNotEqual(s, s2) - s = self.createBackgroundSettings() - - s.setSizeUnit(QgsUnitTypes.RenderUnit.RenderPixels) - self.assertNotEqual(s, s2) - s = self.createBackgroundSettings() - - s.setSizeMapUnitScale(QgsMapUnitScale(11, 22)) - self.assertNotEqual(s, s2) - s = self.createBackgroundSettings() - - s.setRotationType(QgsTextBackgroundSettings.RotationType.RotationSync) - self.assertNotEqual(s, s2) - s = self.createBackgroundSettings() - - s.setRotation(145) - self.assertNotEqual(s, s2) - s = self.createBackgroundSettings() - - s.setOffset(QPointF(31, 41)) - self.assertNotEqual(s, s2) - s = self.createBackgroundSettings() - - s.setOffsetUnit(QgsUnitTypes.RenderUnit.RenderPixels) - self.assertNotEqual(s, s2) - s = self.createBackgroundSettings() - - s.setOffsetMapUnitScale(QgsMapUnitScale(15, 16)) - self.assertNotEqual(s, s2) - s = self.createBackgroundSettings() - - s.setRadii(QSizeF(111, 112)) - self.assertNotEqual(s, s2) - s = self.createBackgroundSettings() - - s.setRadiiUnit(QgsUnitTypes.RenderUnit.RenderPoints) - self.assertNotEqual(s, s2) - s = self.createBackgroundSettings() - - s.setRadiiMapUnitScale(QgsMapUnitScale(151, 161)) - self.assertNotEqual(s, s2) - s = self.createBackgroundSettings() - - s.setFillColor(QColor(255, 255, 0)) - self.assertNotEqual(s, s2) - s = self.createBackgroundSettings() - - s.setStrokeColor(QColor(0, 255, 255)) - self.assertNotEqual(s, s2) - s = self.createBackgroundSettings() - - s.setOpacity(0.6) - self.assertNotEqual(s, s2) - s = self.createBackgroundSettings() - - s.setJoinStyle(Qt.PenJoinStyle.MiterJoin) - self.assertNotEqual(s, s2) - s = self.createBackgroundSettings() - - s.setBlendMode(QPainter.CompositionMode.CompositionMode_Darken) - self.assertNotEqual(s, s2) - s = self.createBackgroundSettings() - - s.setStrokeWidth(17) - self.assertNotEqual(s, s2) - s = self.createBackgroundSettings() - - s.setStrokeWidthUnit(QgsUnitTypes.RenderUnit.RenderPixels) - self.assertNotEqual(s, s2) - s = self.createBackgroundSettings() - - s.setStrokeWidthMapUnitScale(QgsMapUnitScale(QgsMapUnitScale(251, 261))) - self.assertNotEqual(s, s2) - - def checkBackgroundSettings(self, s): - """ test QgsTextBackgroundSettings """ - self.assertTrue(s.enabled()) - self.assertEqual(s.type(), QgsTextBackgroundSettings.ShapeType.ShapeEllipse) - self.assertEqual(s.svgFile(), 'svg.svg') - self.assertEqual(s.sizeType(), QgsTextBackgroundSettings.SizeType.SizePercent) - self.assertEqual(s.size(), QSizeF(1, 2)) - self.assertEqual(s.sizeUnit(), QgsUnitTypes.RenderUnit.RenderPoints) - self.assertEqual(s.sizeMapUnitScale(), QgsMapUnitScale(1, 2)) - self.assertEqual(s.rotationType(), QgsTextBackgroundSettings.RotationType.RotationFixed) - self.assertEqual(s.rotation(), 45) - self.assertEqual(s.offset(), QPointF(3, 4)) - self.assertEqual(s.offsetUnit(), QgsUnitTypes.RenderUnit.RenderMapUnits) - self.assertEqual(s.offsetMapUnitScale(), QgsMapUnitScale(5, 6)) - self.assertEqual(s.radii(), QSizeF(11, 12)) - self.assertEqual(s.radiiUnit(), QgsUnitTypes.RenderUnit.RenderPercentage) - self.assertEqual(s.radiiMapUnitScale(), QgsMapUnitScale(15, 16)) - self.assertEqual(s.fillColor(), QColor(255, 0, 0)) - self.assertEqual(s.strokeColor(), QColor(0, 255, 0)) - self.assertEqual(s.opacity(), 0.5) - self.assertEqual(s.joinStyle(), Qt.PenJoinStyle.RoundJoin) - self.assertEqual(s.blendMode(), QPainter.CompositionMode.CompositionMode_DestinationAtop) - self.assertEqual(s.strokeWidth(), 7) - self.assertEqual(s.strokeWidthUnit(), QgsUnitTypes.RenderUnit.RenderPoints) - self.assertEqual(s.strokeWidthMapUnitScale(), QgsMapUnitScale(25, 26)) - self.assertEqual(s.markerSymbol().color().name(), '#647086') - - def testBackgroundGettersSetters(self): - s = self.createBackgroundSettings() - self.checkBackgroundSettings(s) - - # some other checks - s.setEnabled(False) - self.assertFalse(s.enabled()) - s.setEnabled(True) - self.assertTrue(s.enabled()) - - def testBackgroundCopy(self): - s = self.createBackgroundSettings() - s2 = s - self.checkBackgroundSettings(s2) - s3 = QgsTextBackgroundSettings(s) - self.checkBackgroundSettings(s3) - - def testBackgroundReadWriteXml(self): - """test saving and restoring state of a background to xml""" - doc = QDomDocument("testdoc") - s = self.createBackgroundSettings() - elem = s.writeXml(doc, QgsReadWriteContext()) - parent = doc.createElement("settings") - parent.appendChild(elem) - t = QgsTextBackgroundSettings() - t.readXml(parent, QgsReadWriteContext()) - self.checkBackgroundSettings(t) - - def createShadowSettings(self): - s = QgsTextShadowSettings() - s.setEnabled(True) - s.setShadowPlacement(QgsTextShadowSettings.ShadowPlacement.ShadowBuffer) - s.setOffsetAngle(45) - s.setOffsetDistance(75) - s.setOffsetUnit(QgsUnitTypes.RenderUnit.RenderMapUnits) - s.setOffsetMapUnitScale(QgsMapUnitScale(5, 6)) - s.setOffsetGlobal(True) - s.setBlurRadius(11) - s.setBlurRadiusUnit(QgsUnitTypes.RenderUnit.RenderPercentage) - s.setBlurRadiusMapUnitScale(QgsMapUnitScale(15, 16)) - s.setBlurAlphaOnly(True) - s.setColor(QColor(255, 0, 0)) - s.setOpacity(0.5) - s.setScale(123) - s.setBlendMode(QPainter.CompositionMode.CompositionMode_DestinationAtop) - return s - - def testShadowEquality(self): - s = self.createShadowSettings() - s2 = self.createShadowSettings() - self.assertEqual(s, s2) - - s.setEnabled(False) - self.assertNotEqual(s, s2) - s = self.createShadowSettings() - - s.setShadowPlacement(QgsTextShadowSettings.ShadowPlacement.ShadowText) - self.assertNotEqual(s, s2) - s = self.createShadowSettings() - - s.setOffsetAngle(145) - self.assertNotEqual(s, s2) - s = self.createShadowSettings() - - s.setOffsetDistance(175) - self.assertNotEqual(s, s2) - s = self.createShadowSettings() - - s.setOffsetUnit(QgsUnitTypes.RenderUnit.RenderPixels) - self.assertNotEqual(s, s2) - s = self.createShadowSettings() - - s.setOffsetMapUnitScale(QgsMapUnitScale(15, 16)) - self.assertNotEqual(s, s2) - s = self.createShadowSettings() - - s.setOffsetGlobal(False) - self.assertNotEqual(s, s2) - s = self.createShadowSettings() - - s.setBlurRadius(21) - self.assertNotEqual(s, s2) - s = self.createShadowSettings() - - s.setBlurRadiusUnit(QgsUnitTypes.RenderUnit.RenderPoints) - self.assertNotEqual(s, s2) - s = self.createShadowSettings() - - s.setBlurRadiusMapUnitScale(QgsMapUnitScale(115, 116)) - self.assertNotEqual(s, s2) - s = self.createShadowSettings() - - s.setBlurAlphaOnly(False) - self.assertNotEqual(s, s2) - s = self.createShadowSettings() - - s.setColor(QColor(255, 255, 0)) - self.assertNotEqual(s, s2) - s = self.createShadowSettings() - - s.setOpacity(0.6) - self.assertNotEqual(s, s2) - s = self.createShadowSettings() - - s.setScale(23) - self.assertNotEqual(s, s2) - s = self.createShadowSettings() - - s.setBlendMode(QPainter.CompositionMode.CompositionMode_Darken) - self.assertNotEqual(s, s2) - - def checkShadowSettings(self, s): - """ test QgsTextShadowSettings """ - self.assertTrue(s.enabled()) - self.assertEqual(s.shadowPlacement(), QgsTextShadowSettings.ShadowPlacement.ShadowBuffer) - self.assertEqual(s.offsetAngle(), 45) - self.assertEqual(s.offsetDistance(), 75) - self.assertEqual(s.offsetUnit(), QgsUnitTypes.RenderUnit.RenderMapUnits) - self.assertEqual(s.offsetMapUnitScale(), QgsMapUnitScale(5, 6)) - self.assertTrue(s.offsetGlobal()) - self.assertEqual(s.blurRadius(), 11) - self.assertEqual(s.blurRadiusUnit(), QgsUnitTypes.RenderUnit.RenderPercentage) - self.assertEqual(s.blurRadiusMapUnitScale(), QgsMapUnitScale(15, 16)) - self.assertTrue(s.blurAlphaOnly()) - self.assertEqual(s.color(), QColor(255, 0, 0)) - self.assertEqual(s.opacity(), 0.5) - self.assertEqual(s.scale(), 123) - self.assertEqual(s.blendMode(), QPainter.CompositionMode.CompositionMode_DestinationAtop) - - def testShadowGettersSetters(self): - s = self.createShadowSettings() - self.checkShadowSettings(s) - - # some other checks - s.setEnabled(False) - self.assertFalse(s.enabled()) - s.setEnabled(True) - self.assertTrue(s.enabled()) - s.setOffsetGlobal(False) - self.assertFalse(s.offsetGlobal()) - s.setOffsetGlobal(True) - self.assertTrue(s.offsetGlobal()) - s.setBlurAlphaOnly(False) - self.assertFalse(s.blurAlphaOnly()) - s.setBlurAlphaOnly(True) - self.assertTrue(s.blurAlphaOnly()) - - def testShadowCopy(self): - s = self.createShadowSettings() - s2 = s - self.checkShadowSettings(s2) - s3 = QgsTextShadowSettings(s) - self.checkShadowSettings(s3) - - def testShadowReadWriteXml(self): - """test saving and restoring state of a shadow to xml""" - doc = QDomDocument("testdoc") - s = self.createShadowSettings() - elem = s.writeXml(doc) - parent = doc.createElement("settings") - parent.appendChild(elem) - t = QgsTextShadowSettings() - t.readXml(parent) - self.checkShadowSettings(t) - - def createFormatSettings(self): - s = QgsTextFormat() - s.buffer().setEnabled(True) - s.buffer().setSize(25) - s.mask().setEnabled(True) - s.mask().setSize(32) - s.background().setEnabled(True) - s.background().setSvgFile('test.svg') - s.shadow().setEnabled(True) - s.shadow().setOffsetAngle(223) - font = getTestFont() - font.setKerning(False) - s.setFont(font) - s.setCapitalization(QgsStringUtils.Capitalization.TitleCase) - s.setNamedStyle('Italic') - s.setFamilies(['Arial', 'Comic Sans']) - s.setSize(5) - s.setSizeUnit(QgsUnitTypes.RenderUnit.RenderPoints) - s.setSizeMapUnitScale(QgsMapUnitScale(1, 2)) - s.setColor(QColor(255, 0, 0)) - s.setOpacity(0.5) - s.setBlendMode(QPainter.CompositionMode.CompositionMode_DestinationAtop) - s.setLineHeight(5) - s.setLineHeightUnit(QgsUnitTypes.RenderUnit.RenderInches) - s.setPreviewBackgroundColor(QColor(100, 150, 200)) - s.setOrientation(QgsTextFormat.TextOrientation.VerticalOrientation) - s.setAllowHtmlFormatting(True) - s.setForcedBold(True) - s.setForcedItalic(True) - - s.setTabStopDistance(4.5) - s.setTabStopDistanceUnit(Qgis.RenderUnit.RenderInches) - s.setTabStopDistanceMapUnitScale(QgsMapUnitScale(11, 12)) - - s.setStretchFactor(110) - - s.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.Bold, QgsProperty.fromExpression('1>2')) - return s - - def testFormatEquality(self): - s = self.createFormatSettings() - s2 = self.createFormatSettings() - self.assertEqual(s, s2) - - s.buffer().setEnabled(False) - self.assertNotEqual(s, s2) - s = self.createFormatSettings() - - s.buffer().setSize(12) - self.assertNotEqual(s, s2) - s = self.createFormatSettings() - - s.mask().setEnabled(False) - self.assertNotEqual(s, s2) - s = self.createFormatSettings() - - s.mask().setSize(12) - self.assertNotEqual(s, s2) - s = self.createFormatSettings() - - s.background().setEnabled(False) - self.assertNotEqual(s, s2) - s = self.createFormatSettings() - - s.background().setSvgFile('test2.svg') - self.assertNotEqual(s, s2) - s = self.createFormatSettings() - - s.shadow().setEnabled(False) - self.assertNotEqual(s, s2) - s = self.createFormatSettings() - - s.shadow().setOffsetAngle(123) - self.assertNotEqual(s, s2) - s = self.createFormatSettings() - - font = getTestFont() - font.setKerning(True) - s.setFont(font) - self.assertNotEqual(s, s2) - s = self.createFormatSettings() - - s.setNamedStyle('Bold') - self.assertNotEqual(s, s2) - s = self.createFormatSettings() - - s.setSize(15) - self.assertNotEqual(s, s2) - s = self.createFormatSettings() - - s.setSizeUnit(QgsUnitTypes.RenderUnit.RenderPixels) - self.assertNotEqual(s, s2) - s = self.createFormatSettings() - - s.setSizeMapUnitScale(QgsMapUnitScale(11, 12)) - self.assertNotEqual(s, s2) - s = self.createFormatSettings() - - s.setColor(QColor(255, 255, 0)) - self.assertNotEqual(s, s2) - s = self.createFormatSettings() - - s.setOpacity(0.6) - self.assertNotEqual(s, s2) - s = self.createFormatSettings() - - s.setBlendMode(QPainter.CompositionMode.CompositionMode_Darken) - self.assertNotEqual(s, s2) - s = self.createFormatSettings() - - s.setLineHeight(15) - self.assertNotEqual(s, s2) - s = self.createFormatSettings() - - s.setLineHeightUnit(QgsUnitTypes.RenderUnit.RenderPoints) - self.assertNotEqual(s, s2) - s = self.createFormatSettings() - - s.setPreviewBackgroundColor(QColor(100, 250, 200)) - self.assertNotEqual(s, s2) - s = self.createFormatSettings() - - s.setOrientation(QgsTextFormat.TextOrientation.HorizontalOrientation) - self.assertNotEqual(s, s2) - s = self.createFormatSettings() - - s.setAllowHtmlFormatting(False) - self.assertNotEqual(s, s2) - s = self.createFormatSettings() - - s.setForcedBold(False) - self.assertNotEqual(s, s2) - s = self.createFormatSettings() - - s.setForcedItalic(False) - self.assertNotEqual(s, s2) - s = self.createFormatSettings() - - s.setCapitalization(QgsStringUtils.Capitalization.ForceFirstLetterToCapital) - self.assertNotEqual(s, s2) - s = self.createFormatSettings() - - s.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.Bold, QgsProperty.fromExpression('1>3')) - self.assertNotEqual(s, s2) - - s = self.createFormatSettings() - s.setFamilies(['Times New Roman']) - self.assertNotEqual(s, s2) - - s = self.createFormatSettings() - s.setStretchFactor(120) - self.assertNotEqual(s, s2) - - s = self.createFormatSettings() - s.setTabStopDistance(120) - self.assertNotEqual(s, s2) - - s = self.createFormatSettings() - s.setTabStopDistanceUnit(Qgis.RenderUnit.Points) - self.assertNotEqual(s, s2) - - s = self.createFormatSettings() - s.setTabStopDistanceMapUnitScale( - QgsMapUnitScale(111, 122)) - self.assertNotEqual(s, s2) - - def checkTextFormat(self, s): - """ test QgsTextFormat """ - self.assertTrue(s.buffer().enabled()) - self.assertEqual(s.buffer().size(), 25) - self.assertTrue(s.mask().enabled()) - self.assertEqual(s.mask().size(), 32) - self.assertTrue(s.background().enabled()) - self.assertEqual(s.background().svgFile(), 'test.svg') - self.assertTrue(s.shadow().enabled()) - self.assertEqual(s.shadow().offsetAngle(), 223) - self.assertEqual(s.font().family(), 'QGIS Vera Sans') - self.assertEqual(s.families(), ['Arial', 'Comic Sans']) - self.assertFalse(s.font().kerning()) - self.assertEqual(s.namedStyle(), 'Italic') - self.assertEqual(s.size(), 5) - self.assertEqual(s.sizeUnit(), QgsUnitTypes.RenderUnit.RenderPoints) - self.assertEqual(s.sizeMapUnitScale(), QgsMapUnitScale(1, 2)) - self.assertEqual(s.color(), QColor(255, 0, 0)) - self.assertEqual(s.opacity(), 0.5) - self.assertEqual(s.blendMode(), QPainter.CompositionMode.CompositionMode_DestinationAtop) - self.assertEqual(s.lineHeight(), 5) - self.assertEqual(s.lineHeightUnit(), QgsUnitTypes.RenderUnit.RenderInches) - self.assertEqual(s.previewBackgroundColor().name(), '#6496c8') - self.assertEqual(s.orientation(), QgsTextFormat.TextOrientation.VerticalOrientation) - self.assertEqual(s.capitalization(), QgsStringUtils.Capitalization.TitleCase) - self.assertTrue(s.allowHtmlFormatting()) - self.assertEqual(s.dataDefinedProperties().property(QgsPalLayerSettings.Property.Bold).expressionString(), '1>2') - self.assertTrue(s.forcedBold()) - self.assertTrue(s.forcedItalic()) - self.assertEqual(s.tabStopDistance(), 4.5) - self.assertEqual(s.tabStopDistanceUnit(), Qgis.RenderUnit.Inches) - self.assertEqual(s.tabStopDistanceMapUnitScale(), QgsMapUnitScale(11, 12)) - - if int(QT_VERSION_STR.split('.')[0]) > 6 or ( - int(QT_VERSION_STR.split('.')[0]) == 6 and int(QT_VERSION_STR.split('.')[1]) >= 3): - self.assertEqual(s.stretchFactor(), 110) - - def testFormatGettersSetters(self): - s = self.createFormatSettings() - self.checkTextFormat(s) - - def testFormatCopy(self): - s = self.createFormatSettings() - s2 = s - self.checkTextFormat(s2) - s3 = QgsTextFormat(s) - self.checkTextFormat(s3) - - def testFormatReadWriteXml(self): - """test saving and restoring state of a shadow to xml""" - doc = QDomDocument("testdoc") - s = self.createFormatSettings() - elem = s.writeXml(doc, QgsReadWriteContext()) - parent = doc.createElement("settings") - parent.appendChild(elem) - t = QgsTextFormat() - t.readXml(parent, QgsReadWriteContext()) - self.checkTextFormat(t) - - def testFormatToFromMimeData(self): - """Test converting format to and from mime data""" - s = self.createFormatSettings() - md = s.toMimeData() - from_mime, ok = QgsTextFormat.fromMimeData(None) - self.assertFalse(ok) - from_mime, ok = QgsTextFormat.fromMimeData(md) - self.assertTrue(ok) - self.checkTextFormat(from_mime) - - def testRestoreUsingFamilyList(self): - format = QgsTextFormat() - - doc = QDomDocument("testdoc") - elem = format.writeXml(doc, QgsReadWriteContext()) - parent = doc.createElement("settings") - parent.appendChild(elem) - doc.appendChild(parent) - - # swap out font name in xml to one which doesn't exist on system - xml = doc.toString() - xml = xml.replace(QFont().family(), 'NOT A REAL FONT') - doc = QDomDocument("testdoc") - doc.setContent(xml) - parent = doc.firstChildElement('settings') - - t = QgsTextFormat() - t.readXml(parent, QgsReadWriteContext()) - # should be default font - self.assertEqual(t.font().family(), QFont().family()) - - format.setFamilies(['not real', 'still not real', getTestFont().family()]) - - doc = QDomDocument("testdoc") - elem = format.writeXml(doc, QgsReadWriteContext()) - parent = doc.createElement("settings") - parent.appendChild(elem) - doc.appendChild(parent) - - # swap out font name in xml to one which doesn't exist on system - xml = doc.toString() - xml = xml.replace(QFont().family(), 'NOT A REAL FONT') - doc = QDomDocument("testdoc") - doc.setContent(xml) - parent = doc.firstChildElement('settings') - - t = QgsTextFormat() - t.readXml(parent, QgsReadWriteContext()) - self.assertEqual(t.families(), ['not real', 'still not real', getTestFont().family()]) - # should have skipped the missing fonts and fallen back to the test font family entry, NOT the default application font! - self.assertEqual(t.font().family(), getTestFont().family()) - - def testMultiplyOpacity(self): - - s = self.createFormatSettings() - old_opacity = s.opacity() - old_buffer_opacity = s.buffer().opacity() - old_shadow_opacity = s.shadow().opacity() - old_mask_opacity = s.mask().opacity() - - s.multiplyOpacity(0.5) - - self.assertEqual(s.opacity(), old_opacity * 0.5) - self.assertEqual(s.buffer().opacity(), old_buffer_opacity * 0.5) - self.assertEqual(s.shadow().opacity(), old_shadow_opacity * 0.5) - self.assertEqual(s.mask().opacity(), old_mask_opacity * 0.5) - - s.multiplyOpacity(2.0) - self.checkTextFormat(s) - - def containsAdvancedEffects(self): - t = QgsTextFormat() - self.assertFalse(t.containsAdvancedEffects()) - t.setBlendMode(QPainter.CompositionMode.CompositionMode_DestinationAtop) - self.assertTrue(t.containsAdvancedEffects()) - - t = QgsTextFormat() - t.buffer().setBlendMode(QPainter.CompositionMode.CompositionMode_DestinationAtop) - self.assertFalse(t.containsAdvancedEffects()) - t.buffer().setEnabled(True) - self.assertTrue(t.containsAdvancedEffects()) - t.buffer().setBlendMode(QPainter.CompositionMode.CompositionMode_SourceOver) - self.assertFalse(t.containsAdvancedEffects()) - - t = QgsTextFormat() - t.background().setBlendMode(QPainter.CompositionMode.CompositionMode_DestinationAtop) - self.assertFalse(t.containsAdvancedEffects()) - t.background().setEnabled(True) - self.assertTrue(t.containsAdvancedEffects()) - t.background().setBlendMode(QPainter.CompositionMode.CompositionMode_SourceOver) - self.assertFalse(t.containsAdvancedEffects()) - - t = QgsTextFormat() - t.shadow().setBlendMode(QPainter.CompositionMode.CompositionMode_DestinationAtop) - self.assertFalse(t.containsAdvancedEffects()) - t.shadow().setEnabled(True) - self.assertTrue(t.containsAdvancedEffects()) - t.shadow().setBlendMode(QPainter.CompositionMode.CompositionMode_SourceOver) - self.assertFalse(t.containsAdvancedEffects()) - - def testDataDefinedBufferSettings(self): - f = QgsTextFormat() - context = QgsRenderContext() - - # buffer enabled - f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.BufferDraw, QgsProperty.fromExpression('1')) - f.updateDataDefinedProperties(context) - self.assertTrue(f.buffer().enabled()) - f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.BufferDraw, QgsProperty.fromExpression('0')) - context = QgsRenderContext() - f.updateDataDefinedProperties(context) - self.assertFalse(f.buffer().enabled()) - - # buffer size - f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.BufferSize, QgsProperty.fromExpression('7.8')) - f.updateDataDefinedProperties(context) - self.assertEqual(f.buffer().size(), 7.8) - f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.BufferUnit, QgsProperty.fromExpression("'pixel'")) - f.updateDataDefinedProperties(context) - self.assertEqual(f.buffer().sizeUnit(), QgsUnitTypes.RenderUnit.RenderPixels) - - # buffer opacity - f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.BufferOpacity, QgsProperty.fromExpression('37')) - f.updateDataDefinedProperties(context) - self.assertEqual(f.buffer().opacity(), 0.37) - - # blend mode - f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.BufferBlendMode, QgsProperty.fromExpression("'burn'")) - f.updateDataDefinedProperties(context) - self.assertEqual(f.buffer().blendMode(), QPainter.CompositionMode.CompositionMode_ColorBurn) - - # join style - f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.BufferJoinStyle, - QgsProperty.fromExpression("'miter'")) - f.updateDataDefinedProperties(context) - self.assertEqual(f.buffer().joinStyle(), Qt.PenJoinStyle.MiterJoin) - - # color - f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.BufferColor, QgsProperty.fromExpression("'#ff0088'")) - f.updateDataDefinedProperties(context) - self.assertEqual(f.buffer().color().name(), '#ff0088') - - def testDataDefinedMaskSettings(self): - f = QgsTextFormat() - context = QgsRenderContext() - - # mask enabled - f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.MaskEnabled, QgsProperty.fromExpression('1')) - f.updateDataDefinedProperties(context) - self.assertTrue(f.mask().enabled()) - f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.MaskEnabled, QgsProperty.fromExpression('0')) - context = QgsRenderContext() - f.updateDataDefinedProperties(context) - self.assertFalse(f.mask().enabled()) - - # mask buffer size - f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.MaskBufferSize, QgsProperty.fromExpression('7.8')) - f.updateDataDefinedProperties(context) - self.assertEqual(f.mask().size(), 7.8) - f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.MaskBufferUnit, QgsProperty.fromExpression("'pixel'")) - f.updateDataDefinedProperties(context) - self.assertEqual(f.mask().sizeUnit(), QgsUnitTypes.RenderUnit.RenderPixels) - - # mask opacity - f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.MaskOpacity, QgsProperty.fromExpression('37')) - f.updateDataDefinedProperties(context) - self.assertEqual(f.mask().opacity(), 0.37) - - # join style - f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.MaskJoinStyle, QgsProperty.fromExpression("'miter'")) - f.updateDataDefinedProperties(context) - self.assertEqual(f.mask().joinStyle(), Qt.PenJoinStyle.MiterJoin) - - def testDataDefinedBackgroundSettings(self): - f = QgsTextFormat() - context = QgsRenderContext() - - # background enabled - f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.ShapeDraw, QgsProperty.fromExpression('1')) - f.updateDataDefinedProperties(context) - self.assertTrue(f.background().enabled()) - f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.ShapeDraw, QgsProperty.fromExpression('0')) - context = QgsRenderContext() - f.updateDataDefinedProperties(context) - self.assertFalse(f.background().enabled()) - - # background size - f.background().setSize(QSizeF(13, 14)) - f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.ShapeSizeX, QgsProperty.fromExpression('7.8')) - f.updateDataDefinedProperties(context) - self.assertEqual(f.background().size().width(), 7.8) - self.assertEqual(f.background().size().height(), 14) - f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.ShapeSizeY, QgsProperty.fromExpression('17.8')) - f.updateDataDefinedProperties(context) - self.assertEqual(f.background().size().width(), 7.8) - self.assertEqual(f.background().size().height(), 17.8) - - f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.ShapeSizeUnits, QgsProperty.fromExpression("'pixel'")) - f.updateDataDefinedProperties(context) - self.assertEqual(f.background().sizeUnit(), QgsUnitTypes.RenderUnit.RenderPixels) - - # shape kind - f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.ShapeKind, QgsProperty.fromExpression("'square'")) - f.updateDataDefinedProperties(context) - self.assertEqual(f.background().type(), QgsTextBackgroundSettings.ShapeType.ShapeSquare) - f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.ShapeKind, QgsProperty.fromExpression("'ellipse'")) - f.updateDataDefinedProperties(context) - self.assertEqual(f.background().type(), QgsTextBackgroundSettings.ShapeType.ShapeEllipse) - f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.ShapeKind, QgsProperty.fromExpression("'circle'")) - f.updateDataDefinedProperties(context) - self.assertEqual(f.background().type(), QgsTextBackgroundSettings.ShapeType.ShapeCircle) - f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.ShapeKind, QgsProperty.fromExpression("'svg'")) - f.updateDataDefinedProperties(context) - self.assertEqual(f.background().type(), QgsTextBackgroundSettings.ShapeType.ShapeSVG) - f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.ShapeKind, QgsProperty.fromExpression("'marker'")) - f.updateDataDefinedProperties(context) - self.assertEqual(f.background().type(), QgsTextBackgroundSettings.ShapeType.ShapeMarkerSymbol) - f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.ShapeKind, QgsProperty.fromExpression("'rect'")) - f.updateDataDefinedProperties(context) - self.assertEqual(f.background().type(), QgsTextBackgroundSettings.ShapeType.ShapeRectangle) - - # size type - f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.ShapeSizeType, QgsProperty.fromExpression("'fixed'")) - f.updateDataDefinedProperties(context) - self.assertEqual(f.background().sizeType(), QgsTextBackgroundSettings.SizeType.SizeFixed) - f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.ShapeSizeType, QgsProperty.fromExpression("'buffer'")) - f.updateDataDefinedProperties(context) - self.assertEqual(f.background().sizeType(), QgsTextBackgroundSettings.SizeType.SizeBuffer) - - # svg path - f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.ShapeSVGFile, QgsProperty.fromExpression("'my.svg'")) - f.updateDataDefinedProperties(context) - self.assertEqual(f.background().svgFile(), 'my.svg') - - # shape rotation - f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.ShapeRotation, QgsProperty.fromExpression('67')) - f.updateDataDefinedProperties(context) - self.assertEqual(f.background().rotation(), 67) - f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.ShapeRotationType, - QgsProperty.fromExpression("'offset'")) - f.updateDataDefinedProperties(context) - self.assertEqual(f.background().rotationType(), QgsTextBackgroundSettings.RotationType.RotationOffset) - f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.ShapeRotationType, - QgsProperty.fromExpression("'fixed'")) - f.updateDataDefinedProperties(context) - self.assertEqual(f.background().rotationType(), QgsTextBackgroundSettings.RotationType.RotationFixed) - f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.ShapeRotationType, - QgsProperty.fromExpression("'sync'")) - f.updateDataDefinedProperties(context) - self.assertEqual(f.background().rotationType(), QgsTextBackgroundSettings.RotationType.RotationSync) - - # shape offset - f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.ShapeOffset, QgsProperty.fromExpression("'7,9'")) - f.updateDataDefinedProperties(context) - self.assertEqual(f.background().offset(), QPointF(7, 9)) - f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.ShapeOffsetUnits, - QgsProperty.fromExpression("'pixel'")) - f.updateDataDefinedProperties(context) - self.assertEqual(f.background().offsetUnit(), QgsUnitTypes.RenderUnit.RenderPixels) - - # shape radii - f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.ShapeRadii, QgsProperty.fromExpression("'18,19'")) - f.updateDataDefinedProperties(context) - self.assertEqual(f.background().radii(), QSizeF(18, 19)) - f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.ShapeRadiiUnits, - QgsProperty.fromExpression("'pixel'")) - f.updateDataDefinedProperties(context) - self.assertEqual(f.background().radiiUnit(), QgsUnitTypes.RenderUnit.RenderPixels) - - # shape opacity - f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.ShapeOpacity, QgsProperty.fromExpression('37')) - f.updateDataDefinedProperties(context) - self.assertEqual(f.background().opacity(), 0.37) - - # color - f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.ShapeFillColor, - QgsProperty.fromExpression("'#ff0088'")) - f.updateDataDefinedProperties(context) - self.assertEqual(f.background().fillColor().name(), '#ff0088') - f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.ShapeStrokeColor, - QgsProperty.fromExpression("'#8800ff'")) - f.updateDataDefinedProperties(context) - self.assertEqual(f.background().strokeColor().name(), '#8800ff') - - # stroke width - f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.ShapeStrokeWidth, QgsProperty.fromExpression('88')) - f.updateDataDefinedProperties(context) - self.assertEqual(f.background().strokeWidth(), 88) - f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.ShapeStrokeWidthUnits, - QgsProperty.fromExpression("'pixel'")) - f.updateDataDefinedProperties(context) - self.assertEqual(f.background().strokeWidthUnit(), QgsUnitTypes.RenderUnit.RenderPixels) - - # blend mode - f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.ShapeBlendMode, QgsProperty.fromExpression("'burn'")) - f.updateDataDefinedProperties(context) - self.assertEqual(f.background().blendMode(), QPainter.CompositionMode.CompositionMode_ColorBurn) - - # join style - f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.ShapeJoinStyle, QgsProperty.fromExpression("'miter'")) - f.updateDataDefinedProperties(context) - self.assertEqual(f.background().joinStyle(), Qt.PenJoinStyle.MiterJoin) - - def testDataDefinedShadowSettings(self): - f = QgsTextFormat() - context = QgsRenderContext() - - # shadow enabled - f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.ShadowDraw, QgsProperty.fromExpression('1')) - f.updateDataDefinedProperties(context) - self.assertTrue(f.shadow().enabled()) - f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.ShadowDraw, QgsProperty.fromExpression('0')) - context = QgsRenderContext() - f.updateDataDefinedProperties(context) - self.assertFalse(f.shadow().enabled()) - - # placement type - f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.ShadowUnder, QgsProperty.fromExpression("'text'")) - f.updateDataDefinedProperties(context) - self.assertEqual(f.shadow().shadowPlacement(), QgsTextShadowSettings.ShadowPlacement.ShadowText) - f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.ShadowUnder, QgsProperty.fromExpression("'buffer'")) - f.updateDataDefinedProperties(context) - self.assertEqual(f.shadow().shadowPlacement(), QgsTextShadowSettings.ShadowPlacement.ShadowBuffer) - f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.ShadowUnder, - QgsProperty.fromExpression("'background'")) - f.updateDataDefinedProperties(context) - self.assertEqual(f.shadow().shadowPlacement(), QgsTextShadowSettings.ShadowPlacement.ShadowShape) - f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.ShadowUnder, QgsProperty.fromExpression("'svg'")) - f.updateDataDefinedProperties(context) - self.assertEqual(f.shadow().shadowPlacement(), QgsTextShadowSettings.ShadowPlacement.ShadowLowest) - f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.ShadowUnder, QgsProperty.fromExpression("'lowest'")) - - # offset angle - f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.ShadowOffsetAngle, QgsProperty.fromExpression('67')) - f.updateDataDefinedProperties(context) - self.assertEqual(f.shadow().offsetAngle(), 67) - - # offset distance - f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.ShadowOffsetDist, QgsProperty.fromExpression('38')) - f.updateDataDefinedProperties(context) - self.assertEqual(f.shadow().offsetDistance(), 38) - f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.ShadowOffsetUnits, - QgsProperty.fromExpression("'pixel'")) - f.updateDataDefinedProperties(context) - self.assertEqual(f.shadow().offsetUnit(), QgsUnitTypes.RenderUnit.RenderPixels) - - # radius - f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.ShadowRadius, QgsProperty.fromExpression('58')) - f.updateDataDefinedProperties(context) - self.assertEqual(f.shadow().blurRadius(), 58) - f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.ShadowRadiusUnits, - QgsProperty.fromExpression("'pixel'")) - f.updateDataDefinedProperties(context) - self.assertEqual(f.shadow().blurRadiusUnit(), QgsUnitTypes.RenderUnit.RenderPixels) - - # opacity - f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.ShadowOpacity, QgsProperty.fromExpression('37')) - f.updateDataDefinedProperties(context) - self.assertEqual(f.shadow().opacity(), 0.37) - - # color - f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.ShadowColor, QgsProperty.fromExpression("'#ff0088'")) - f.updateDataDefinedProperties(context) - self.assertEqual(f.shadow().color().name(), '#ff0088') - - # blend mode - f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.ShadowBlendMode, QgsProperty.fromExpression("'burn'")) - f.updateDataDefinedProperties(context) - self.assertEqual(f.shadow().blendMode(), QPainter.CompositionMode.CompositionMode_ColorBurn) - - def testDataDefinedFormatSettings(self): - f = QgsTextFormat() - font = f.font() - font.setUnderline(True) - font.setStrikeOut(True) - font.setWordSpacing(5.7) - font.setLetterSpacing(QFont.SpacingType.AbsoluteSpacing, 3.3) - f.setFont(font) - context = QgsRenderContext() - - # family - f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.Family, QgsProperty.fromExpression( - f"'{QgsFontUtils.getStandardTestFont().family()}'")) - f.updateDataDefinedProperties(context) - self.assertEqual(f.font().family(), QgsFontUtils.getStandardTestFont().family()) - - # style - f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.FontStyle, QgsProperty.fromExpression("'Bold'")) - f.updateDataDefinedProperties(context) - self.assertEqual(f.font().styleName(), 'Bold') - f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.FontStyle, QgsProperty.fromExpression("'Roman'")) - f.updateDataDefinedProperties(context) - self.assertEqual(f.font().styleName(), 'Roman') - f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.Bold, QgsProperty.fromExpression("1")) - f.updateDataDefinedProperties(context) - self.assertTrue(f.font().bold()) - self.assertFalse(f.font().italic()) - f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.Bold, QgsProperty.fromExpression("0")) - f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.Italic, QgsProperty.fromExpression("1")) - f.updateDataDefinedProperties(context) - self.assertFalse(f.font().bold()) - self.assertTrue(f.font().italic()) - self.assertTrue(f.font().underline()) - self.assertTrue(f.font().strikeOut()) - self.assertAlmostEqual(f.font().wordSpacing(), 5.7, 1) - self.assertAlmostEqual(f.font().letterSpacing(), 3.3, 1) - - # underline - f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.Underline, QgsProperty.fromExpression("0")) - f.updateDataDefinedProperties(context) - self.assertFalse(f.font().underline()) - f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.Underline, QgsProperty.fromExpression("1")) - f.updateDataDefinedProperties(context) - self.assertTrue(f.font().underline()) - - # strikeout - f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.Strikeout, QgsProperty.fromExpression("0")) - f.updateDataDefinedProperties(context) - self.assertFalse(f.font().strikeOut()) - f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.Strikeout, QgsProperty.fromExpression("1")) - f.updateDataDefinedProperties(context) - self.assertTrue(f.font().strikeOut()) - - # color - f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.Color, QgsProperty.fromExpression("'#ff0088'")) - f.updateDataDefinedProperties(context) - self.assertEqual(f.color().name(), '#ff0088') - - # size - f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.Size, QgsProperty.fromExpression('38')) - f.updateDataDefinedProperties(context) - self.assertEqual(f.size(), 38) - f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.FontSizeUnit, QgsProperty.fromExpression("'pixel'")) - f.updateDataDefinedProperties(context) - self.assertEqual(f.sizeUnit(), QgsUnitTypes.RenderUnit.RenderPixels) - - # opacity - f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.FontOpacity, QgsProperty.fromExpression('37')) - f.updateDataDefinedProperties(context) - self.assertEqual(f.opacity(), 0.37) - - # letter spacing - f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.FontLetterSpacing, QgsProperty.fromExpression('58')) - f.updateDataDefinedProperties(context) - self.assertEqual(f.font().letterSpacing(), 58) - - # word spacing - f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.FontWordSpacing, QgsProperty.fromExpression('8')) - f.updateDataDefinedProperties(context) - self.assertEqual(f.font().wordSpacing(), 8) - - # blend mode - f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.FontBlendMode, QgsProperty.fromExpression("'burn'")) - f.updateDataDefinedProperties(context) - self.assertEqual(f.blendMode(), QPainter.CompositionMode.CompositionMode_ColorBurn) - - if int(QT_VERSION_STR.split('.')[0]) > 6 or ( - int(QT_VERSION_STR.split('.')[0]) == 6 and int(QT_VERSION_STR.split('.')[1]) >= 3): - # stretch - f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.FontStretchFactor, QgsProperty.fromExpression("135")) - f.updateDataDefinedProperties(context) - self.assertEqual(f.stretchFactor(), 135) - - f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.TabStopDistance, QgsProperty.fromExpression("15")) - f.updateDataDefinedProperties(context) - self.assertEqual(f.tabStopDistance(), 15) - - def testFontFoundFromLayer(self): - layer = createEmptyLayer() - layer.setCustomProperty('labeling/fontFamily', 'asdasd') - f = QgsTextFormat() - f.readFromLayer(layer) - self.assertFalse(f.fontFound()) - - font = getTestFont() - layer.setCustomProperty('labeling/fontFamily', font.family()) - f.readFromLayer(layer) - self.assertTrue(f.fontFound()) - - def testFontFoundFromXml(self): - doc = QDomDocument("testdoc") - f = QgsTextFormat() - elem = f.writeXml(doc, QgsReadWriteContext()) - elem.setAttribute('fontFamily', 'asdfasdfsadf') - parent = doc.createElement("parent") - parent.appendChild(elem) - - f.readXml(parent, QgsReadWriteContext()) - self.assertFalse(f.fontFound()) - - font = getTestFont() - elem.setAttribute('fontFamily', font.family()) - f.readXml(parent, QgsReadWriteContext()) - self.assertTrue(f.fontFound()) - - def testFromQFont(self): - qfont = getTestFont() - qfont.setPointSizeF(16.5) - qfont.setLetterSpacing(QFont.SpacingType.AbsoluteSpacing, 3) - - format = QgsTextFormat.fromQFont(qfont) - self.assertEqual(format.font().family(), qfont.family()) - self.assertEqual(format.font().letterSpacing(), 3.0) - self.assertEqual(format.size(), 16.5) - self.assertEqual(format.sizeUnit(), QgsUnitTypes.RenderUnit.RenderPoints) - - qfont.setPixelSize(12) - format = QgsTextFormat.fromQFont(qfont) - self.assertEqual(format.size(), 12.0) - self.assertEqual(format.sizeUnit(), QgsUnitTypes.RenderUnit.RenderPixels) - - def testToQFont(self): - s = QgsTextFormat() - f = getTestFont() - f.setLetterSpacing(QFont.SpacingType.AbsoluteSpacing, 3) - s.setFont(f) - s.setNamedStyle('Italic') - s.setSize(5.5) - s.setSizeUnit(QgsUnitTypes.RenderUnit.RenderPoints) - - qfont = s.toQFont() - self.assertEqual(qfont.family(), f.family()) - self.assertEqual(qfont.pointSizeF(), 5.5) - self.assertEqual(qfont.letterSpacing(), 3.0) - - s.setSize(5) - s.setSizeUnit(QgsUnitTypes.RenderUnit.RenderPixels) - qfont = s.toQFont() - self.assertEqual(qfont.pixelSize(), 5) - - s.setSize(5) - s.setSizeUnit(QgsUnitTypes.RenderUnit.RenderMillimeters) - qfont = s.toQFont() - self.assertAlmostEqual(qfont.pointSizeF(), 14.17, 2) - - s.setSizeUnit(QgsUnitTypes.RenderUnit.RenderInches) - qfont = s.toQFont() - self.assertAlmostEqual(qfont.pointSizeF(), 360.0, 2) - - self.assertFalse(qfont.bold()) - s.setForcedBold(True) - qfont = s.toQFont() - self.assertTrue(qfont.bold()) - - self.assertFalse(qfont.italic()) - s.setForcedItalic(True) - qfont = s.toQFont() - self.assertTrue(qfont.italic()) - - if int(QT_VERSION_STR.split('.')[0]) > 6 or ( - int(QT_VERSION_STR.split('.')[0]) == 6 and int(QT_VERSION_STR.split('.')[1]) >= 3): - s.setStretchFactor(115) - qfont = s.toQFont() - self.assertEqual(qfont.stretch(), 115) - def testFontMetrics(self): """ Test calculating font metrics from scaled text formats From 4f35816225788e6539b3f52759472ecf2da7f508 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Tue, 29 Oct 2024 08:42:59 +1000 Subject: [PATCH 2/7] [api] Add tab positions list for QgsTextFormat Allows setting a list of custom tab stop distances, instead of just a single distance --- .../core/auto_additions/qgstextformat.py | 4 + .../textrenderer/qgstextformat.sip.in | 102 +++++++++++++++++- python/core/auto_additions/qgstextformat.py | 4 + .../textrenderer/qgstextformat.sip.in | 102 +++++++++++++++++- src/core/labeling/qgspallabeling.cpp | 2 +- .../textrenderer/qgstextdocumentmetrics.cpp | 31 +++++- src/core/textrenderer/qgstextformat.cpp | 67 +++++++++++- src/core/textrenderer/qgstextformat.h | 90 +++++++++++++++- src/core/textrenderer/qgstextrenderer_p.h | 2 + tests/src/python/test_qgstextformat.py | 25 +++++ tests/src/python/test_qgstextrenderer.py | 78 ++++++++++++++ .../text_tab_positions_fixed_size.png | Bin 0 -> 6912 bytes .../text_tab_positions_fixed_size_mask.png | Bin 0 -> 6574 bytes .../text_tab_positions_fixed_size_html.png | Bin 0 -> 6916 bytes ...ext_tab_positions_fixed_size_html_mask.png | Bin 0 -> 6574 bytes ...ext_tab_positions_fixed_size_more_tabs.png | Bin 0 -> 7685 bytes ...ab_positions_fixed_size_more_tabs_mask.png | Bin 0 -> 7375 bytes .../text_tab_positions_percentage.png | Bin 0 -> 6755 bytes .../text_tab_positions_percentage_mask.png | Bin 0 -> 6632 bytes .../text_tab_positions_percentage_html.png | Bin 0 -> 7016 bytes ...ext_tab_positions_percentage_html_mask.png | Bin 0 -> 6750 bytes 21 files changed, 501 insertions(+), 6 deletions(-) create mode 100644 tests/testdata/control_images/text_renderer/text_tab_positions_fixed_size/text_tab_positions_fixed_size.png create mode 100644 tests/testdata/control_images/text_renderer/text_tab_positions_fixed_size/text_tab_positions_fixed_size_mask.png create mode 100644 tests/testdata/control_images/text_renderer/text_tab_positions_fixed_size_html/text_tab_positions_fixed_size_html.png create mode 100644 tests/testdata/control_images/text_renderer/text_tab_positions_fixed_size_html/text_tab_positions_fixed_size_html_mask.png create mode 100644 tests/testdata/control_images/text_renderer/text_tab_positions_fixed_size_more_tabs/text_tab_positions_fixed_size_more_tabs.png create mode 100644 tests/testdata/control_images/text_renderer/text_tab_positions_fixed_size_more_tabs/text_tab_positions_fixed_size_more_tabs_mask.png create mode 100644 tests/testdata/control_images/text_renderer/text_tab_positions_percentage/text_tab_positions_percentage.png create mode 100644 tests/testdata/control_images/text_renderer/text_tab_positions_percentage/text_tab_positions_percentage_mask.png create mode 100644 tests/testdata/control_images/text_renderer/text_tab_positions_percentage_html/text_tab_positions_percentage_html.png create mode 100644 tests/testdata/control_images/text_renderer/text_tab_positions_percentage_html/text_tab_positions_percentage_html_mask.png diff --git a/python/PyQt6/core/auto_additions/qgstextformat.py b/python/PyQt6/core/auto_additions/qgstextformat.py index 18b2c645a848..288ab6fddfbc 100644 --- a/python/PyQt6/core/auto_additions/qgstextformat.py +++ b/python/PyQt6/core/auto_additions/qgstextformat.py @@ -6,3 +6,7 @@ QgsTextFormat.__group__ = ['textrenderer'] except NameError: pass +try: + QgsTextFormat.Tab.__group__ = ['textrenderer'] +except NameError: + pass diff --git a/python/PyQt6/core/auto_generated/textrenderer/qgstextformat.sip.in b/python/PyQt6/core/auto_generated/textrenderer/qgstextformat.sip.in index 735505ca8c77..f279e0b89100 100644 --- a/python/PyQt6/core/auto_generated/textrenderer/qgstextformat.sip.in +++ b/python/PyQt6/core/auto_generated/textrenderer/qgstextformat.sip.in @@ -506,6 +506,12 @@ Sets the ``unit`` for the line height for text. %Docstring Returns the distance for tab stops. +.. note:: + + This value will be ignored if :py:func:`~QgsTextFormat.tabPositions` is non-empty. + +.. seealso:: :py:func:`tabPositions` + .. seealso:: :py:func:`tabStopDistanceUnit` .. seealso:: :py:func:`setTabStopDistance` @@ -515,13 +521,103 @@ Returns the distance for tab stops. void setTabStopDistance( double distance ); %Docstring -Sets the ``distance`` for tab stops. The units are specified using :py:func:`~QgsTextFormat.setTabStopDistanceUnit`. +Sets the ``distance`` for tab stops. + +The units are specified using :py:func:`~QgsTextFormat.setTabStopDistanceUnit`. + +.. note:: + + This value will be ignored if :py:func:`~QgsTextFormat.tabPositions` is non-empty. + +.. seealso:: :py:func:`tabPositions` .. seealso:: :py:func:`tabStopDistance` .. seealso:: :py:func:`setTabStopDistanceUnit` .. versionadded:: 3.38 +%End + + class Tab +{ +%Docstring(signature="appended") +Defines a tab position for a text format. + +.. versionadded:: 3.42 +%End + +%TypeHeaderCode +#include "qgstextformat.h" +%End + public: + + explicit Tab( double position ); +%Docstring +Constructor for a Tab at the specified ``position``. +%End + + void setPosition( double position ); +%Docstring +Sets the tab position. + +.. seealso:: :py:func:`position` +%End + + double position() const; +%Docstring +Returns the tab position. + +.. seealso:: :py:func:`setPosition` +%End + + bool operator==( const QgsTextFormat::Tab &other ) const; + + SIP_PYOBJECT __repr__(); +%MethodCode + const QString str = QStringLiteral( "" ).arg( sipCpp->position() ); + sipRes = PyUnicode_FromString( str.toUtf8().constData() ); +%End + + }; + + QList< QgsTextFormat::Tab > tabPositions() const; +%Docstring +Returns the list of tab positions for tab stops. + +The units are specified using :py:func:`~QgsTextFormat.tabStopDistanceUnit`. + +.. note:: + + If non-empty, this list overrides any distance defined by :py:func:`~QgsTextFormat.tabStopDistance`. + +.. seealso:: :py:func:`setTabPositions` + +.. seealso:: :py:func:`tabStopDistance` + +.. seealso:: :py:func:`tabStopDistanceUnit` + +.. seealso:: :py:func:`setTabStopDistance` + +.. versionadded:: 3.42 +%End + + void setTabPositions( const QList< QgsTextFormat::Tab > &positions ); +%Docstring +Sets the list of tab ``positions`` for tab stops. + +The units are specified using :py:func:`~QgsTextFormat.setTabStopDistanceUnit`. + +.. note:: + + If non-empty, this list overrides any distance defined by :py:func:`~QgsTextFormat.setTabStopDistance`. + +.. seealso:: :py:func:`tabPositions` + +.. seealso:: :py:func:`setTabStopDistance` + +.. seealso:: :py:func:`setTabStopDistanceUnit` + +.. versionadded:: 3.42 %End Qgis::RenderUnit tabStopDistanceUnit() const; @@ -530,6 +626,8 @@ Returns the units for the tab stop distance. .. seealso:: :py:func:`tabStopDistance` +.. seealso:: :py:func:`tabPositions` + .. seealso:: :py:func:`setTabStopDistanceUnit` .. versionadded:: 3.38 @@ -541,6 +639,8 @@ Sets the ``unit`` used for the tab stop distance. .. seealso:: :py:func:`setTabStopDistance` +.. seealso:: :py:func:`setTabPositions` + .. seealso:: :py:func:`tabStopDistanceUnit` .. versionadded:: 3.38 diff --git a/python/core/auto_additions/qgstextformat.py b/python/core/auto_additions/qgstextformat.py index 18b2c645a848..288ab6fddfbc 100644 --- a/python/core/auto_additions/qgstextformat.py +++ b/python/core/auto_additions/qgstextformat.py @@ -6,3 +6,7 @@ QgsTextFormat.__group__ = ['textrenderer'] except NameError: pass +try: + QgsTextFormat.Tab.__group__ = ['textrenderer'] +except NameError: + pass diff --git a/python/core/auto_generated/textrenderer/qgstextformat.sip.in b/python/core/auto_generated/textrenderer/qgstextformat.sip.in index 735505ca8c77..f279e0b89100 100644 --- a/python/core/auto_generated/textrenderer/qgstextformat.sip.in +++ b/python/core/auto_generated/textrenderer/qgstextformat.sip.in @@ -506,6 +506,12 @@ Sets the ``unit`` for the line height for text. %Docstring Returns the distance for tab stops. +.. note:: + + This value will be ignored if :py:func:`~QgsTextFormat.tabPositions` is non-empty. + +.. seealso:: :py:func:`tabPositions` + .. seealso:: :py:func:`tabStopDistanceUnit` .. seealso:: :py:func:`setTabStopDistance` @@ -515,13 +521,103 @@ Returns the distance for tab stops. void setTabStopDistance( double distance ); %Docstring -Sets the ``distance`` for tab stops. The units are specified using :py:func:`~QgsTextFormat.setTabStopDistanceUnit`. +Sets the ``distance`` for tab stops. + +The units are specified using :py:func:`~QgsTextFormat.setTabStopDistanceUnit`. + +.. note:: + + This value will be ignored if :py:func:`~QgsTextFormat.tabPositions` is non-empty. + +.. seealso:: :py:func:`tabPositions` .. seealso:: :py:func:`tabStopDistance` .. seealso:: :py:func:`setTabStopDistanceUnit` .. versionadded:: 3.38 +%End + + class Tab +{ +%Docstring(signature="appended") +Defines a tab position for a text format. + +.. versionadded:: 3.42 +%End + +%TypeHeaderCode +#include "qgstextformat.h" +%End + public: + + explicit Tab( double position ); +%Docstring +Constructor for a Tab at the specified ``position``. +%End + + void setPosition( double position ); +%Docstring +Sets the tab position. + +.. seealso:: :py:func:`position` +%End + + double position() const; +%Docstring +Returns the tab position. + +.. seealso:: :py:func:`setPosition` +%End + + bool operator==( const QgsTextFormat::Tab &other ) const; + + SIP_PYOBJECT __repr__(); +%MethodCode + const QString str = QStringLiteral( "" ).arg( sipCpp->position() ); + sipRes = PyUnicode_FromString( str.toUtf8().constData() ); +%End + + }; + + QList< QgsTextFormat::Tab > tabPositions() const; +%Docstring +Returns the list of tab positions for tab stops. + +The units are specified using :py:func:`~QgsTextFormat.tabStopDistanceUnit`. + +.. note:: + + If non-empty, this list overrides any distance defined by :py:func:`~QgsTextFormat.tabStopDistance`. + +.. seealso:: :py:func:`setTabPositions` + +.. seealso:: :py:func:`tabStopDistance` + +.. seealso:: :py:func:`tabStopDistanceUnit` + +.. seealso:: :py:func:`setTabStopDistance` + +.. versionadded:: 3.42 +%End + + void setTabPositions( const QList< QgsTextFormat::Tab > &positions ); +%Docstring +Sets the list of tab ``positions`` for tab stops. + +The units are specified using :py:func:`~QgsTextFormat.setTabStopDistanceUnit`. + +.. note:: + + If non-empty, this list overrides any distance defined by :py:func:`~QgsTextFormat.setTabStopDistance`. + +.. seealso:: :py:func:`tabPositions` + +.. seealso:: :py:func:`setTabStopDistance` + +.. seealso:: :py:func:`setTabStopDistanceUnit` + +.. versionadded:: 3.42 %End Qgis::RenderUnit tabStopDistanceUnit() const; @@ -530,6 +626,8 @@ Returns the units for the tab stop distance. .. seealso:: :py:func:`tabStopDistance` +.. seealso:: :py:func:`tabPositions` + .. seealso:: :py:func:`setTabStopDistanceUnit` .. versionadded:: 3.38 @@ -541,6 +639,8 @@ Sets the ``unit`` used for the tab stop distance. .. seealso:: :py:func:`setTabStopDistance` +.. seealso:: :py:func:`setTabPositions` + .. seealso:: :py:func:`tabStopDistanceUnit` .. versionadded:: 3.38 diff --git a/src/core/labeling/qgspallabeling.cpp b/src/core/labeling/qgspallabeling.cpp index c2d74937806b..99d99f8cd4a4 100644 --- a/src/core/labeling/qgspallabeling.cpp +++ b/src/core/labeling/qgspallabeling.cpp @@ -128,7 +128,7 @@ void QgsPalLayerSettings::initPropertyDefinitions() { static_cast< int >( QgsPalLayerSettings::Property::AutoWrapLength ), QgsPropertyDefinition( "AutoWrapLength", QObject::tr( "Automatic word wrap line length" ), QgsPropertyDefinition::IntegerPositive, origin ) }, { static_cast< int >( QgsPalLayerSettings::Property::MultiLineHeight ), QgsPropertyDefinition( "MultiLineHeight", QObject::tr( "Line height" ), QgsPropertyDefinition::DoublePositive, origin ) }, { static_cast< int >( QgsPalLayerSettings::Property::MultiLineAlignment ), QgsPropertyDefinition( "MultiLineAlignment", QgsPropertyDefinition::DataTypeString, QObject::tr( "Line alignment" ), QObject::tr( "string " ) + "[Left|Center|Right|Follow]", origin ) }, - { static_cast< int >( QgsPalLayerSettings::Property::TabStopDistance ), QgsPropertyDefinition( "TabStopDistance", QObject::tr( "Tab stop distance" ), QgsPropertyDefinition::DoublePositive, origin ) }, + { static_cast< int >( QgsPalLayerSettings::Property::TabStopDistance ), QgsPropertyDefinition( "TabStopDistance", QgsPropertyDefinition::DataTypeNumeric, QObject::tr( "Tab stop distance(s)" ), QObject::tr( "Numeric distance or array of distances" ), origin ) }, { static_cast< int >( QgsPalLayerSettings::Property::TextOrientation ), QgsPropertyDefinition( "TextOrientation", QgsPropertyDefinition::DataTypeString, QObject::tr( "Text orientation" ), QObject::tr( "string " ) + "[horizontal|vertical]", origin ) }, { static_cast< int >( QgsPalLayerSettings::Property::DirSymbDraw ), QgsPropertyDefinition( "DirSymbDraw", QObject::tr( "Draw direction symbol" ), QgsPropertyDefinition::Boolean, origin ) }, { static_cast< int >( QgsPalLayerSettings::Property::DirSymbLeft ), QgsPropertyDefinition( "DirSymbLeft", QObject::tr( "Left direction symbol" ), QgsPropertyDefinition::String, origin ) }, diff --git a/src/core/textrenderer/qgstextdocumentmetrics.cpp b/src/core/textrenderer/qgstextdocumentmetrics.cpp index f5b195aa897c..021d1dc66613 100644 --- a/src/core/textrenderer/qgstextdocumentmetrics.cpp +++ b/src/core/textrenderer/qgstextdocumentmetrics.cpp @@ -33,6 +33,7 @@ constexpr double SUBSCRIPT_VERTICAL_BASELINE_ADJUSTMENT_FACTOR = 1.0 / 6.0; struct DocumentMetrics { double tabStopDistancePainterUnits = 0; + QList< double > tabStopDistancesPainterUnits; double width = 0; double heightLabelMode = 0; double heightPointRectMode = 0; @@ -220,7 +221,24 @@ void QgsTextDocumentMetrics::processFragment( QgsTextDocumentMetrics &res, const if ( fragment.isTab() ) { // special handling for tab characters - const double nextTabStop = ( std::floor( thisBlockMetrics.blockXMax / documentMetrics.tabStopDistancePainterUnits ) + 1 ) * documentMetrics.tabStopDistancePainterUnits; + double nextTabStop = 0; + if ( !documentMetrics.tabStopDistancesPainterUnits.isEmpty() ) + { + // if we don't find a tab stop before the current length of line, we just ignore the tab character entirely + nextTabStop = thisBlockMetrics.blockXMax; + for ( const double tabStop : std::as_const( documentMetrics.tabStopDistancesPainterUnits ) ) + { + if ( tabStop >= thisBlockMetrics.blockXMax ) + { + nextTabStop = tabStop; + break; + } + } + } + else + { + nextTabStop = ( std::floor( thisBlockMetrics.blockXMax / documentMetrics.tabStopDistancePainterUnits ) + 1 ) * documentMetrics.tabStopDistancePainterUnits; + } const double fragmentWidth = nextTabStop - thisBlockMetrics.blockXMax; thisBlockMetrics.blockWidth += fragmentWidth; @@ -538,6 +556,17 @@ QgsTextDocumentMetrics QgsTextDocumentMetrics::calculateMetrics( const QgsTextDo ? format.tabStopDistance() * font.pixelSize() / scaleFactor : context.convertToPainterUnits( format.tabStopDistance(), format.tabStopDistanceUnit(), format.tabStopDistanceMapUnitScale() ); + const QList< QgsTextFormat::Tab > tabPositions = format.tabPositions(); + documentMetrics.tabStopDistancesPainterUnits.reserve( tabPositions.size() ); + for ( const QgsTextFormat::Tab &tab : tabPositions ) + { + documentMetrics.tabStopDistancesPainterUnits.append( + format.tabStopDistanceUnit() == Qgis::RenderUnit::Percentage + ? tab.position() * font.pixelSize() / scaleFactor + : context.convertToPainterUnits( tab.position(), format.tabStopDistanceUnit(), format.tabStopDistanceMapUnitScale() ) + ); + } + documentMetrics.blockSize = document.size(); res.mDocument.reserve( documentMetrics.blockSize ); res.mFragmentFonts.reserve( documentMetrics.blockSize ); diff --git a/src/core/textrenderer/qgstextformat.cpp b/src/core/textrenderer/qgstextformat.cpp index 09664c7ca8aa..b70979d9e3ff 100644 --- a/src/core/textrenderer/qgstextformat.cpp +++ b/src/core/textrenderer/qgstextformat.cpp @@ -87,6 +87,7 @@ bool QgsTextFormat::operator==( const QgsTextFormat &other ) const || d->forcedItalic != other.forcedItalic() || d->capitalization != other.capitalization() || d->tabStopDistance != other.tabStopDistance() + || d->tabPositions != other.tabPositions() || d->tabStopDistanceUnits != other.tabStopDistanceUnit() || d->tabStopDistanceMapUnitScale != other.tabStopDistanceMapUnitScale() || mBufferSettings != other.mBufferSettings @@ -389,6 +390,17 @@ void QgsTextFormat::setTabStopDistance( double distance ) d->tabStopDistance = distance; } +QList QgsTextFormat::tabPositions() const +{ + return d->tabPositions; +} + +void QgsTextFormat::setTabPositions( const QList &positions ) +{ + d->isValid = true; + d->tabPositions = positions; +} + Qgis::RenderUnit QgsTextFormat::tabStopDistanceUnit() const { return d->tabStopDistanceUnits; @@ -689,6 +701,19 @@ void QgsTextFormat::readXml( const QDomElement &elem, const QgsReadWriteContext d->tabStopDistanceUnits = QgsUnitTypes::decodeRenderUnit( textStyleElem.attribute( QStringLiteral( "tabStopDistanceUnit" ), QStringLiteral( "Point" ) ), &ok ); d->tabStopDistanceMapUnitScale = QgsSymbolLayerUtils::decodeMapUnitScale( textStyleElem.attribute( QStringLiteral( "tabStopDistanceMapUnitScale" ) ) ); + QList< Tab > tabPositions; + { + const QDomElement tabPositionsElem = textStyleElem.firstChildElement( QStringLiteral( "tabPositions" ) ); + const QDomNodeList tabNodes = tabPositionsElem.childNodes(); + tabPositions.reserve( tabNodes.size() ); + for ( int i = 0; i < tabNodes.count(); ++i ) + { + const QDomElement tabElem = tabNodes.at( i ).toElement(); + tabPositions << Tab( tabElem.attribute( QStringLiteral( "position" ) ).toDouble() ); + } + } + d->tabPositions = tabPositions; + if ( textStyleElem.hasAttribute( QStringLiteral( "capitalization" ) ) ) d->capitalization = static_cast< Qgis::Capitalization >( textStyleElem.attribute( QStringLiteral( "capitalization" ), QString::number( static_cast< int >( Qgis::Capitalization::MixedCase ) ) ).toInt() ); else @@ -792,6 +817,18 @@ QDomElement QgsTextFormat::writeXml( QDomDocument &doc, const QgsReadWriteContex textStyleElem.setAttribute( QStringLiteral( "tabStopDistanceUnit" ), QgsUnitTypes::encodeUnit( d->tabStopDistanceUnits ) ); textStyleElem.setAttribute( QStringLiteral( "tabStopDistanceMapUnitScale" ), QgsSymbolLayerUtils::encodeMapUnitScale( d->tabStopDistanceMapUnitScale ) ); + if ( !d->tabPositions.empty() ) + { + QDomElement tabPositionsElem = doc.createElement( QStringLiteral( "tabPositions" ) ); + for ( const Tab &tab : std::as_const( d->tabPositions ) ) + { + QDomElement tabElem = doc.createElement( QStringLiteral( "tab" ) ); + tabElem.setAttribute( QStringLiteral( "position" ), tab.position() ); + tabPositionsElem.appendChild( tabElem ); + } + textStyleElem.appendChild( tabPositionsElem ); + } + textStyleElem.setAttribute( QStringLiteral( "allowHtml" ), d->allowHtmlFormatting ? QStringLiteral( "1" ) : QStringLiteral( "0" ) ); textStyleElem.setAttribute( QStringLiteral( "capitalization" ), QString::number( static_cast< int >( d->capitalization ) ) ); @@ -1144,7 +1181,31 @@ void QgsTextFormat::updateDataDefinedProperties( QgsRenderContext &context ) const QVariant val = d->mDataDefinedProperties.value( QgsPalLayerSettings::Property::TabStopDistance, context.expressionContext(), d->tabStopDistance ); if ( !QgsVariantUtils::isNull( val ) ) { - d->tabStopDistance = val.toDouble(); + if ( val.userType() == QMetaType::Type::QVariantList ) + { + const QVariantList parts = val.toList(); + d->tabPositions.clear(); + d->tabPositions.reserve( parts.size() ); + for ( const QVariant &part : parts ) + { + d->tabPositions.append( Tab( part.toDouble() ) ); + } + } + else if ( val.userType() == QMetaType::Type::QStringList ) + { + const QStringList parts = val.toStringList(); + d->tabPositions.clear(); + d->tabPositions.reserve( parts.size() ); + for ( const QString &part : parts ) + { + d->tabPositions.append( Tab( part.toDouble() ) ); + } + } + else + { + d->tabPositions.clear(); + d->tabStopDistance = val.toDouble(); + } } } @@ -1320,3 +1381,7 @@ QString QgsTextFormat::asCSS( double pointToPixelMultiplier ) const return css; } + +QgsTextFormat::Tab::Tab( double position ) + : mPosition( position ) +{} diff --git a/src/core/textrenderer/qgstextformat.h b/src/core/textrenderer/qgstextformat.h index fb9813eae312..9daf4a8950b4 100644 --- a/src/core/textrenderer/qgstextformat.h +++ b/src/core/textrenderer/qgstextformat.h @@ -462,6 +462,9 @@ class CORE_EXPORT QgsTextFormat /** * Returns the distance for tab stops. * + * \note This value will be ignored if tabPositions() is non-empty. + * + * \see tabPositions() * \see tabStopDistanceUnit() * \see setTabStopDistance() * @@ -470,8 +473,13 @@ class CORE_EXPORT QgsTextFormat double tabStopDistance() const; /** - * Sets the \a distance for tab stops. The units are specified using setTabStopDistanceUnit(). + * Sets the \a distance for tab stops. + * + * The units are specified using setTabStopDistanceUnit(). + * + * \note This value will be ignored if tabPositions() is non-empty. * + * \see tabPositions() * \see tabStopDistance() * \see setTabStopDistanceUnit() * @@ -479,10 +487,89 @@ class CORE_EXPORT QgsTextFormat */ void setTabStopDistance( double distance ); + /** + * \ingroup core + * \brief Defines a tab position for a text format. + * \since QGIS 3.42 + */ + class CORE_EXPORT Tab + { + public: + + /** + * Constructor for a Tab at the specified \a position. + */ + explicit Tab( double position ); + + /** + * Sets the tab position. + * + * \see position() + */ + void setPosition( double position ) { mPosition = position; } + + /** + * Returns the tab position. + * + * \see setPosition() + */ + double position() const { return mPosition; } + + bool operator==( const QgsTextFormat::Tab &other ) const + { + return qgsDoubleNear( mPosition, other.mPosition ); + } + +#ifdef SIP_RUN + SIP_PYOBJECT __repr__(); + % MethodCode + const QString str = QStringLiteral( "" ).arg( sipCpp->position() ); + sipRes = PyUnicode_FromString( str.toUtf8().constData() ); + % End +#endif + + private: + + double mPosition = 0; + + }; + + /** + * Returns the list of tab positions for tab stops. + * + * The units are specified using tabStopDistanceUnit(). + * + * \note If non-empty, this list overrides any distance defined by tabStopDistance(). + * + * \see setTabPositions() + * \see tabStopDistance() + * \see tabStopDistanceUnit() + * \see setTabStopDistance() + * + * \since QGIS 3.42 + */ + QList< QgsTextFormat::Tab > tabPositions() const; + + /** + * Sets the list of tab \a positions for tab stops. + * + * The units are specified using setTabStopDistanceUnit(). + * + * \note If non-empty, this list overrides any distance defined by setTabStopDistance(). + * + * \see tabPositions() + * \see setTabStopDistance() + * \see setTabStopDistanceUnit() + * + * \since QGIS 3.42 + */ + void setTabPositions( const QList< QgsTextFormat::Tab > &positions ); + /** * Returns the units for the tab stop distance. * * \see tabStopDistance() + * \see tabPositions() * \see setTabStopDistanceUnit() * * \since QGIS 3.38 @@ -493,6 +580,7 @@ class CORE_EXPORT QgsTextFormat * Sets the \a unit used for the tab stop distance. * * \see setTabStopDistance() + * \see setTabPositions() * \see tabStopDistanceUnit() * * \since QGIS 3.38 diff --git a/src/core/textrenderer/qgstextrenderer_p.h b/src/core/textrenderer/qgstextrenderer_p.h index 91169f01f66f..3aa35b4dc435 100644 --- a/src/core/textrenderer/qgstextrenderer_p.h +++ b/src/core/textrenderer/qgstextrenderer_p.h @@ -283,6 +283,7 @@ class QgsTextSettingsPrivate : public QSharedData , allowHtmlFormatting( other.allowHtmlFormatting ) , capitalization( other.capitalization ) , tabStopDistance( other.tabStopDistance ) + , tabPositions( other.tabPositions ) , tabStopDistanceUnits( other.tabStopDistanceUnits ) , tabStopDistanceMapUnitScale( other.tabStopDistanceMapUnitScale ) , mDataDefinedProperties( other.mDataDefinedProperties ) @@ -311,6 +312,7 @@ class QgsTextSettingsPrivate : public QSharedData Qgis::Capitalization capitalization = Qgis::Capitalization::MixedCase; double tabStopDistance = 6.0; + QList< QgsTextFormat::Tab > tabPositions; Qgis::RenderUnit tabStopDistanceUnits = Qgis::RenderUnit::Percentage; QgsMapUnitScale tabStopDistanceMapUnitScale; diff --git a/tests/src/python/test_qgstextformat.py b/tests/src/python/test_qgstextformat.py index 6570970bec3e..f8bc489025d3 100644 --- a/tests/src/python/test_qgstextformat.py +++ b/tests/src/python/test_qgstextformat.py @@ -142,6 +142,10 @@ def testValid(self): t.setTabStopDistance(3) self.assertTrue(t.isValid()) + t = QgsTextFormat() + t.setTabPositions([QgsTextFormat.Tab(4), QgsTextFormat.Tab(8)]) + self.assertTrue(t.isValid()) + t = QgsTextFormat() t.setTabStopDistanceUnit(Qgis.RenderUnit.Points) self.assertTrue(t.isValid()) @@ -186,6 +190,12 @@ def testValid(self): t.setForcedItalic(True) self.assertTrue(t.isValid()) + def test_tab(self): + pos = QgsTextFormat.Tab(4) + self.assertEqual(pos, QgsTextFormat.Tab(4)) + self.assertNotEqual(pos, QgsTextFormat.Tab(14)) + self.assertEqual(str(pos), '') + def createBufferSettings(self): s = QgsTextBufferSettings() s.setEnabled(True) @@ -718,6 +728,7 @@ def createFormatSettings(self): s.setForcedItalic(True) s.setTabStopDistance(4.5) + s.setTabPositions([QgsTextFormat.Tab(5), QgsTextFormat.Tab(17)]) s.setTabStopDistanceUnit(Qgis.RenderUnit.RenderInches) s.setTabStopDistanceMapUnitScale(QgsMapUnitScale(11, 12)) @@ -844,6 +855,10 @@ def testFormatEquality(self): s.setTabStopDistance(120) self.assertNotEqual(s, s2) + s = self.createFormatSettings() + s.setTabPositions([QgsTextFormat.Tab(11), QgsTextFormat.Tab(13)]) + self.assertNotEqual(s, s2) + s = self.createFormatSettings() s.setTabStopDistanceUnit(Qgis.RenderUnit.Points) self.assertNotEqual(s, s2) @@ -883,6 +898,7 @@ def checkTextFormat(self, s): self.assertTrue(s.forcedBold()) self.assertTrue(s.forcedItalic()) self.assertEqual(s.tabStopDistance(), 4.5) + self.assertEqual(s.tabPositions(), [QgsTextFormat.Tab(5), QgsTextFormat.Tab(17)]) self.assertEqual(s.tabStopDistanceUnit(), Qgis.RenderUnit.Inches) self.assertEqual(s.tabStopDistanceMapUnitScale(), QgsMapUnitScale(11, 12)) @@ -1401,6 +1417,15 @@ def testDataDefinedFormatSettings(self): f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.TabStopDistance, QgsProperty.fromExpression("15")) f.updateDataDefinedProperties(context) self.assertEqual(f.tabStopDistance(), 15) + self.assertFalse(f.tabPositions()) + + f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.TabStopDistance, QgsProperty.fromValue([11, 14])) + f.updateDataDefinedProperties(context) + self.assertEqual(f.tabPositions(), [QgsTextFormat.Tab(11), QgsTextFormat.Tab(14)]) + + f.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.TabStopDistance, QgsProperty.fromValue(["13.5", "15.8"])) + f.updateDataDefinedProperties(context) + self.assertEqual(f.tabPositions(), [QgsTextFormat.Tab(13.5), QgsTextFormat.Tab(15.8)]) def testFontFoundFromLayer(self): layer = createEmptyLayer() diff --git a/tests/src/python/test_qgstextrenderer.py b/tests/src/python/test_qgstextrenderer.py index d6d98c275df8..81ea4c4ebb51 100644 --- a/tests/src/python/test_qgstextrenderer.py +++ b/tests/src/python/test_qgstextrenderer.py @@ -2171,6 +2171,27 @@ def testDrawTextRectWordWrapTab(self): format.setAllowHtmlFormatting(True) format.setSizeUnit(QgsUnitTypes.RenderUnit.RenderPoints) format.setTabStopDistance(5) + painter = QPainter() + ms = QgsMapSettings() + ms.setExtent(QgsRectangle(0, 0, 50, 50)) + context = QgsRenderContext.fromMapSettings(ms) + context.setPainter(painter) + context.setScaleFactor(96 / 25.4) # 96 DPI + context.setFlag(QgsRenderContext.Flag.ApplyScalingWorkaroundForTextRendering, True) + + self.assertTrue( + self.checkRender(format, 'tab_wrapping', text=['this\ttab\tshould\twrap'], + alignment=QgsTextRenderer.HAlignment.AlignLeft, rect=QRectF(50, 130, 350, 100), + flags=Qgis.TextRendererFlag.WrapLines) + ) + + def testDrawTextRectWordWrapTabPositions(self): + format = QgsTextFormat() + format.setFont(getTestFont('bold')) + format.setSize(30) + format.setAllowHtmlFormatting(True) + format.setSizeUnit(QgsUnitTypes.RenderUnit.RenderPoints) + format.setTabPositions([QgsTextFormat.Tab(5), QgsTextFormat.Tab(8)]) format.setTabStopDistance(5) painter = QPainter() ms = QgsMapSettings() @@ -2334,6 +2355,17 @@ def testDrawTabPercent(self): 'text_tab_percentage', text=['with\ttabs', 'a\tb'])) + def testDrawTabPositionsPercent(self): + format = QgsTextFormat() + format.setFont(getTestFont('bold')) + format.setSize(20) + format.setSizeUnit(QgsUnitTypes.RenderUnit.RenderPoints) + format.setTabPositions([QgsTextFormat.Tab(3.1), QgsTextFormat.Tab(8)]) + format.setTabStopDistanceUnit(Qgis.RenderUnit.Percentage) + self.assertTrue(self.checkRender(format, + 'text_tab_positions_percentage', + text=['with\tmany\ttabs', 'a\tb\tc'])) + def testDrawTabFixedSize(self): format = QgsTextFormat() format.setFont(getTestFont('bold')) @@ -2345,6 +2377,28 @@ def testDrawTabFixedSize(self): 'text_tab_fixed_size', text=['with\ttabs', 'a\tb'])) + def testDrawTabPositionsFixedSize(self): + format = QgsTextFormat() + format.setFont(getTestFont('bold')) + format.setSize(20) + format.setSizeUnit(QgsUnitTypes.RenderUnit.RenderPoints) + format.setTabPositions([QgsTextFormat.Tab(20), QgsTextFormat.Tab(50)]) + format.setTabStopDistanceUnit(Qgis.RenderUnit.Millimeters) + self.assertTrue(self.checkRender(format, + 'text_tab_positions_fixed_size', + text=['with\tmany\ttabs', 'a\tb\tc'])) + + def testDrawTabPositionsFixedSizeMoreTabsThanPositions(self): + format = QgsTextFormat() + format.setFont(getTestFont('bold')) + format.setSize(20) + format.setSizeUnit(QgsUnitTypes.RenderUnit.RenderPoints) + format.setTabPositions([QgsTextFormat.Tab(20), QgsTextFormat.Tab(50)]) + format.setTabStopDistanceUnit(Qgis.RenderUnit.Millimeters) + self.assertTrue(self.checkRender(format, + 'text_tab_positions_fixed_size_more_tabs', + text=['with\tmany\ttabs', 'a\tb\tc\td\te'])) + def testHtmlFormatting(self): format = QgsTextFormat() format.setFont(getTestFont('bold')) @@ -2368,6 +2422,18 @@ def testHtmlTabPercent(self): 'text_tab_percentage_html', text=['with\ttabs', ' a\tb'])) + def testHtmlTabPositionsPercent(self): + format = QgsTextFormat() + format.setFont(getTestFont('bold')) + format.setSize(20) + format.setSizeUnit(QgsUnitTypes.RenderUnit.RenderPoints) + format.setTabPositions([QgsTextFormat.Tab(3), QgsTextFormat.Tab(8)]) + format.setTabStopDistanceUnit(Qgis.RenderUnit.Percentage) + format.setAllowHtmlFormatting(True) + self.assertTrue(self.checkRender(format, + 'text_tab_positions_percentage_html', + text=['with\tmany\ttabs', ' a\tb\tc'])) + def testHtmlTabFixedSize(self): format = QgsTextFormat() format.setFont(getTestFont('bold')) @@ -2380,6 +2446,18 @@ def testHtmlTabFixedSize(self): 'text_tab_fixed_size_html', text=['with\ttabs', ' a\tb'])) + def testHtmlTabPositionsFixedSize(self): + format = QgsTextFormat() + format.setFont(getTestFont('bold')) + format.setSize(20) + format.setSizeUnit(QgsUnitTypes.RenderUnit.RenderPoints) + format.setTabPositions([QgsTextFormat.Tab(25), QgsTextFormat.Tab(60)]) + format.setTabStopDistanceUnit(Qgis.RenderUnit.Millimeters) + format.setAllowHtmlFormatting(True) + self.assertTrue(self.checkRender(format, + 'text_tab_positions_fixed_size_html', + text=['with\tmany\ttabs', ' a\tb\tc'])) + def testHtmlFormattingBuffer(self): """ Test drawing HTML with buffer diff --git a/tests/testdata/control_images/text_renderer/text_tab_positions_fixed_size/text_tab_positions_fixed_size.png b/tests/testdata/control_images/text_renderer/text_tab_positions_fixed_size/text_tab_positions_fixed_size.png new file mode 100644 index 0000000000000000000000000000000000000000..26a2676d781186eeeeb23c832bd89c841a41f1e0 GIT binary patch literal 6912 zcmeI1hf`B)x5nY9pdz405D_8A4^gBlQl#b}3I`D)U3wFcUL=4-f`Sz3inJ3T8ajkd z=m9|xK?71l0-=a>LI@!Q5<>1icfLR3-kCGA*X)^{%zk&)e%7;o>&=@-CIz)}G7kBzUaq>6diXiDD0yv!t{LAJU7neZqKf#?TDW_EPDAdzg1FLrLP;~BOb+I^D6N4t<1n`^6xJ{Sb%;k|f zccP2YbHoQ%X^EH7BEfHF)NXfyJNK5p1@@l3(?O;mZos}%#d#cGbRiFhl3gtCzWI&o z_bEceF|H4tCogc_`ZxY>WaaQj$i6PIn4XCeGPe;2OnJ;q;#3HtL=neE`E}S@U|62@P1O3{E*4qdi zPfYiu-H82JIoTZCavHM0!O9Fh#3LZC9YVwsiQ>o(MEKJh4ejuqWF56syp`W`+QDwa zoAr=>iB}mCaI1Vr+Wy0gwe`p40gt}65FpZt_0(7cT7%Lsh+`Rr7ZcH5|Mj!NL`?iL zB@$}vCzczuw<}obls0`3X`=7VhtGn4jJdA0ZIQ-(jvkeBgBmnfeVC@IxvCh&C-Hpj zND(~SdW21)1_`8^Lg)s9K%ZfT8BF_S+ zJuUsmoO77FvTj=l#?ojk6AZVE-{PjJkpsOZZ0*dN z* z^9}fELM?@s#$OFu?g^wG9gCZc6v0Y{QqF>P# zc3$~~y`x9OZWE43aShU){TKiS!SQVV@UQBk<)QklA8*WUBbrhU7AiLz-_J?FTicL7 zNw>?wdUN=$yFs>CHtmy&90xUWD}EQ@O<5fe+r_;)=hnf7wPXB!Y8$?_=fPbAelaSu zybTD`)?GT@lb`3`d&5g}E zy}2se3UW#Ql-P6TkIpT2{CIfSdLiI7=9GlG`v>YQ9XrJHD>e@9s<}FByd;OIs!vKu zAsk`KQT7fK^(lDrFm|>z3=GuCA^N+~o9n-$7`^1f&H=a#|GirDSTQ7OC95iNCwQFj zdX;1yQ_SjzLM_J?R31}#L%sRfcX>4%1FKmITX3&pl(dta&GsU1fThBV0|&pH5&^a3 z&gVm#h|T1^L{ta`)VXjlsLL*r9O387LP=gZ;=Fwo|@YGW|bXDRF_fj(8Z%`K$ScIZBHI41-&h*5i+=)*cJ zZo}0kaz4pWXn}s@a*zN`u<`|aWTNoFx4{P$GmEV)IsdHP$?D%3ZF5|?0kp{JirI5+ zm^(6wqoWOjQyE9xL`{wRGY1Tcm4c~f)B$sBfF`1O2a^DeJC<(Wwtvb*q*+RmR#P%EZV@YTF+2v$`lyU=aPO8)dZ!=N;4$5x+dxuGK_~ zFCL5Q*Y~e#|HrBazxmj?Q@K(uxIJH%N!$Li_N=i@c-db4ke!`~x;!o}$c*Qwt&d{2 zQd+;%n++)zXoasZExTB!-;{Ttnk=h2G#)FIGtYP*yF_9MiiWOit~WojwaD4^8D~(i zZL1~e0kgJGaFS1hH(hM1+|oW-ku{c!oXkAWqrya%Ju1h5zD*D%0~Uas5-Z<&vc_8r zaNleZ?;oiqS%q#q#pg`!j9(v!jxo=)^mv9d1vmx`3pHuo!U2wV@3?+)s1NCj^UWuD zn&yi>Q4=;=pV}Y>J5TYqLVBL~rTYw*sJN)j1wCO|6HmmMAS_uJzr}CXA|4w8>Daaq z3OK03Kr=uSR)~@oD?nN!t#=c5p2=q)T1jWZg`mtN*=Pfk87!5(?-ZIPB>dQZ#8=4= z<{32HEg8YzS@$um{Ovh(+{u%m=kptoi8a96V%57&$ol30cFZea9Rme?d@aXGrpA4O zIu8vArnRlS<_M5~yEV<3ei7Yw#YJy5?KA*&_an_K^G6)T<$mZd z6*l`%B87#|>eQErRp$1=jp0@vZoVykzdVZ8J=^2sHas!h-THWK<(XzknJTOA-QdSn z?u-&IPhoqpF4pS{D=X`apclVI(@Vp~Ii(iqk?Q(ks9cnEdlU?(J5e<^VJ${y`Go4b zvHXv^C9kEj``ywa5A^_ECRXG%8qqs8d|o=M-20S1LJYA9dK>Ys4nWb2Bm_#@ffZK{ zBQdF$!fN)2>Mmsh#R`FKVz|Bi+I*r^dvf+M$|U zsd|EKSVSZ^CZf(@$+m*coUQZgtD6-d&_k^j>*?DXn5ce{8ZC1Ui@1H*kwbRM6k7Hy zEq+n=q?<|tdspV!&oEQ4miao{^wMu0q}^{(_%){lz*gzs%Vo<&Fg7a{l=^ykfm32A zC@*r4bVx64l1=;x-d^8u2OJL0=JBS?o326L)}%ScRBXP!-mecFm1Bh-Iy0?dUAEQu z%TTP#OP|U?{Dq9v%i$j3X{n@RO2gNeo)i&9Q;5?KE;&r$nxk5Gb348PgeDF<9=f4c z_GYV2705X7jaPaGAQCmdt@h^1AnFZU+rM zmU(`zCYP!8F)DILitXb3+{P=|HzXn$Meyxp6Yv`ciN?;oIrHbv``Ax-_P#zPsAaiW zF_YZ*Ybu6n+75KrUCe2KJ63zk>8*1D2kcR)BphzGmU;31Gpp_5stAHf(@*JjF^ikw8v1qn4Y?P|a zF)wAMV4*_~zA=K;)I=Z>#@f#Yojq69NUcW5BcArS&ByPBgs=A9TXHXMk7%UwiH`SR z-2Jk@x90^N{3JwgqI+g!dHUuzw?<}bN||5km*bolG_2oeH*D)ik?o7(t)obuy~phW zgf1oMhS?*37u=HnjvE>toWi(ZtSSZI*ys=t`ee|_3)K*W6TF^zeSy@pJ>jA9pJP7f zM(BUqXnlr9O>j7rbSUZH1&P{IeeL=~{N=^G_!EDg ziT-c!kX&vk%Ia~BjuayABBob<{e&brcbgG&_XUyY!<|Y1K+ei3pTGS3Z)anix&Ip~ z(Zo)9o|Ps*Q|$nD4QFrP5`D00r@<;ChGdQ?49sm#>WD%=IlVm z#=u)p^QL{*`b=F8>d<~THJ_Q<>fSSRf*D|~A8GMETb#K^n?g28aW3VDsTVdDeXW9u zrG?IF_r&p}<&A5)CReG|wk}qwTd`aRqCw#B%2d8^=KhdPogpVuzW%Dj42$THiA>XQ z?~WSHIM1+PFALu{1;~ayNA$}-#&V?bzn7uT&k7;_RX@7`w`bPr8T6x$wUWeOzc)Uz!`&IBtmDUJJ)S-2 zp?FBVH8HbhgBu8cydi3kI%Uq^T<020{LzJx_%+wZky@tQ;8`SIOo* zF@}hk--nApkH-}{7(oBC@w2t@*XwB6Y@;MT<7*ix=^=UTM(5)EH_dAvs(%l?ip1EK zJ1I?h_VyNnqbi4h=6V(MW=mM7Csa)OWgkOzjy?JyvT9t>vBZk5TZ^`GwEev`^k6H6 z;#mNxSagkT#SdJxVjL}G8QuMi0SG;KZPzf`vKiPDAvW4=Gy%g9W^MVTel6OqLfYww zVJFu)(VAtXI?*iZWTiP+SB1*1$_(Hgul)pJ1fe|s}^NP%fXmf}<9>aHKEvE-F3 zI8+{O_e@u1(fVcm>P(Gg*}kqMqbc_?ME*!;rgeN?vISI?_QDL<=aH=u=nB&we@UxF zXJiyq7&iw#!{cS-Z|KOcX^1|>YcgqJ1g{%JUqe&f-7I4!U&w~q*9+59hM#_n5E}`; zN6S_BClt-|kam$*}WDRW70d+3H6 zr>Rf>)EGUk_FUL7q0T?E=_&yBz!-2QG>bE*M{4+VC9nd;dyXZsQCR-qnYnF7EBm z2g8d{9db_&pKxA%_HFhub{|I@5cfmMgC?rdkUz&q3Z-x}#pDWp2s%OeiAw_Yt* ug6=qXg6==(f8*aA{HuZg*BYR5XsK$~l_oPz0e@+68QwR!SAEwx`hNh@XGi7$ literal 0 HcmV?d00001 diff --git a/tests/testdata/control_images/text_renderer/text_tab_positions_fixed_size/text_tab_positions_fixed_size_mask.png b/tests/testdata/control_images/text_renderer/text_tab_positions_fixed_size/text_tab_positions_fixed_size_mask.png new file mode 100644 index 0000000000000000000000000000000000000000..aed696fe4edd5b45bc5fd7e3832aabd9fd9bdcf1 GIT binary patch literal 6574 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 literal 0 HcmV?d00001 diff --git a/tests/testdata/control_images/text_renderer/text_tab_positions_fixed_size_html/text_tab_positions_fixed_size_html.png b/tests/testdata/control_images/text_renderer/text_tab_positions_fixed_size_html/text_tab_positions_fixed_size_html.png new file mode 100644 index 0000000000000000000000000000000000000000..03da4461392d001490be21d8d3d1dd5a4ba6aec2 GIT binary patch literal 6916 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 B-ah~U literal 0 HcmV?d00001 diff --git a/tests/testdata/control_images/text_renderer/text_tab_positions_fixed_size_html/text_tab_positions_fixed_size_html_mask.png b/tests/testdata/control_images/text_renderer/text_tab_positions_fixed_size_html/text_tab_positions_fixed_size_html_mask.png new file mode 100644 index 0000000000000000000000000000000000000000..91746640ac60ae28e553d2f16bac14666f4dabf7 GIT binary patch literal 6574 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 TTn7HvJ)y4yeN_6_i_rfAv-|W* literal 0 HcmV?d00001 diff --git a/tests/testdata/control_images/text_renderer/text_tab_positions_fixed_size_more_tabs/text_tab_positions_fixed_size_more_tabs_mask.png b/tests/testdata/control_images/text_renderer/text_tab_positions_fixed_size_more_tabs/text_tab_positions_fixed_size_more_tabs_mask.png new file mode 100644 index 0000000000000000000000000000000000000000..e1ba3f869a2f13256a6ffd98d28939d819be488c GIT binary patch literal 7375 zcmeHs`8U+>-~XgiN!lZOlFFWCWEU#g-_~qn9THJ2!M_7xW-gHs$YJlD->x@?6@d_lc+w5iD-N;G=0|Cf z$EQw{76WkAjnPd?CGQ81sHTXRi3*u?TMDV)Q2rLBGcTx4V8XYbZ=EE}N(e{EAw+d3 zH;RrY-TUpepI?l&c7gmjdJuM^(slj|^e}(-Do4F>w=4FwbM?U1>K)FVm_tYF_d@PY z(SxFQK_KzRckw}N&lo`<@_Y6}jP65rLEapZhCoj8?SZ^7z6*gk>^}mzEAa0_|Cdrw zJ+Rk^xxBob>MepJttY_p3kp2fqSXr@ttD(5LWgRpdtv!6U!J*m>LHFqCX*lcO}V+2 z6YfH^-xceje#Zv8v^8q$-Pv&X!dSu(mH#w2$e%2H{=8OyVP@ux5%R7l6t7`mpnUM) z!3@-|u$v3x#PJ3-Z?sP2GRPcV&dXadcBN@hL)DWh9$_L+y1QG!EteK2d+Y30-dTK@$*i?zT;<& zh97y0JT@=;$l_2Z5g{R8zI}V_+j>(`F5B$&YV4gm9t}e^zJ(6*=aP(G%rO1bPG484 z@u|OAo%Bp6+I(_q3LYIj;Mf&P+;obIixbBNx#WNC>kC~Unk5H|q==-P$%o%i70K=H z?neBuj7%@FH}wk%u@0u?XJ;S1SU|Ru&-06nwEx0r2o4F+`7!YG=Zl;9=RW!T1y(>} z7S|IRIy#htf`W=!LlnwcJc9bebA|n#)g;)N)zf2z+E}if3X;vaaezO$m?0@DdW@n0 z2k(>5`^UEmdyYT(%4v{=gyK)1u3pc471!C>*-&5q>xuJ)6RyJ+3v)w_OKXe1eJjf> z^fnDwyEpm$@Bvnoj#556t#2yFyiBG%@SoK09S>%SdFF81JjKG(v*cdquVK9XXa*Dt z1t)KuVW&KJa85M+OIKIQy-x3L|3+NS5+lMzu=7Ys-_$y7EGW2qwMZsAbt3$-I8teX zy^eS9QWc~|T2e-q?kJI~Hqi+Q2_(iL{^Zr^>01;|y&~CKE~mJ}#B|O<9?s!#s;SNa z0Tn?oo#%K`S67iig=?2DDPt&+h$dmV?Ky!(NV%Lvo}06C!BqPMDU=$D`uMzl#2M$%p-dZ?xQhEoqML)#a-A`w9=H2+O#+i0$N*_@H| z&-Lb{q+O(m;$lhebbl#Qt#H|WOjWRN{7LFyg-r?Gw|+3g&_;o}`OsQWHrQkZ$A?0G zcl11)9dO;jO~2wfxV)j@AJKF-TvL-QNW<^NH4Gu(rEQi};@ZoZaRe=YYb&wv*|Xrd zrmf`}`x|mODh?SDt%{M6k&n$}kxe|1#*x;@ia_CSd3h&irl-TcR$~vOrPkEew&ou_ z3hE(aVL(R4*Voq#Q?cn67})J-kC^PvIj^RxOKb9XsvWv#e1^m`l{s@*${^S^ay0bT zhubFUh-PpWz9b{Wb>~_PmCT4ZCUF0@t|WFccPgAUrhi&QB&)Smn<|9#^D`B=Z#*9^ zeI!gT#6ihkE+-qjE6@CDd3m{b$uF=GXqDpXM{X{jp2Fk?`p2~64It8;ZdG%VKZ)Y4 z)u*Nk3JRLZ9?QOef3I`ohoz;4J$$>IBd>xkID0+A4Ei+hmWKR&<10J+s4FQcX%{!v z*YC7RrNc0OL`DWnSSB|+4h~CP>kLv3$ zwMH}ZCvXV{p5(rhme$r^-o3l~wYT>zUr1Ec_oL-WM(-8}@rcu6VmT`-(Fg7u+f_RF z-MK@+7mFQs2VEUR(8%fWXRIww_=u#O5ZH@aqNmX#v`O*Y&BTj&<~UBZgM7xH>FMb# zKedaXCHa!>g5>T5PSH45(iaas%Fg$4ZaG}HNT8=K6a!m%;fz{c$#-(Z){`oSnNKHdS zw*QvH(t10cy*@luQdk)7aoW4B$V>W27J~sNg;v{MFq6p+8^ANEkrU8-GDG%QT54d? zZw?n87+8t7!qn8X|CtKaMpHb&pd^5j>wMf<;dIKc;c(%HSE+|lTC{Wo z4Y}8DbEUOPw6l5mVFgU_RHP;%Iwr;fjV9{FEt@57%v5+N#DH_B^EewB08S>@j`8OQ z++1BB16HuGuuy|S<(fmskvzhHm2=w}fQ6qwr}{Oz6c!Z?dAn>y!&JMH1o_`tsg^?PDe5&5E`O+04*Gw|1%#eaXS8f5tY;W z_N}~xgal6X`|NC$xK5N=et!PP21ODlY>9qjeUT&;L*QZUE{mT6Jv`a-v~6p+Cs75) zW)e#_#!CyXXG#uiNoA7Yw{Dq98YHwRz(e-(y|!|UsavrU?&&&hDw93TA5>JHYkrOz zp&Mv`a%nFT45oz)a|5+v?~HZXRyxSjpl}T}!BXg{7qDncs}XWPuHEOlt0iv@@KBMZ;Ys!qPRZZLrTwUFt5gNL2{sR+I8r~)kG{mAilgYHAvOyf6 zInzG*#N(-_c6}=c1R2y>gQNzF%Q~XzAtGHEs3+lT5g8V2e94M}GfDmAqp~9KvCm$; zTIlbZkllvcyvC0{m##aH#_Emb6VSym@wWsQ&17Zn8^1p~0+hx|{Dl(&#hM4OEL=Ej zB~npY`JZuC0boNQ1b$dFbTqq9ydQ&gUe1F4Xz@Pz@Y8VnMp&h1Z}lusM(mcaA?LQX zkUaM65DuqF<%M1530SP4Go=i7@SfD3V89wo%Xsu?ZWi@)bV-j^G|r0IyFNKt&V1%1 zd+aa7HUPN{1$_t906)`nyc!BL%LNLBqNc5V5xfJbM2SrdQ#oTad~+f@V6)%+j#a6S zwW(g!m`dDMXZ4q@pYM04ktQB@`@L>%rm_<{6HlgJ^I+@f=!l=M<8Ex|#di_pVY>B% zcR_A#F|6V!0FQWFRYdEY=rfl}2k~*6m>AAMO$vjJs#mnIq~vB~2m#%Y8_!wL&fnpn zn(?s9itoK_|9<~s%B$B;ZARb>_QkM*P_GL5@E1lzv94m^<=KlT1gxG~!j@k}WJ>Bs zY}x`G3fev59}oZ!4z}Ffh_`}1jh>$dxC`h2bbr33+*x&XbtQt@yScdZv_woP)*-PN0kT!LPL9djlo+jL6fER zgj%=Njg7aKHa2ayiyj=7LXMlq@M$Dm* zJe8man)>=mCMG5-3>>G*zvm{K%~sRWI-g|Z<*Y(pYHxqL>~t=t8&4qo+2nE>E z!p2yfHas`MKxOAyU0nr04E+53dQZFX_UN`L|EY`Du3Zx+REYv9X|7z51G?Dd=FKdC zc+M}@6FT(uCGSColG}?O!6wsMXw9J(F)_`6RW4aI)?(XMgBy94%HrLEfodjjAjlGu%DSQf8WbD_A&5 zlb2c5-604>Pvb>d(RA}x_k19nlZ=djIt38}5-lEgn>fcIbf{w^o|a$q@bJh$iHV8P zR$uGHEx(yiTR(pMcm#X0N58RCP0-|6+PzY!qkNuw!5t2F6OjHV4J**_J~VtyP0jZo z(G!S+?Xc3eHd?rslX5}OZk^KpH=veN;VwAxmNT~WhvjCB)1eQ>FQvgA#~yydx6`*^ zz-Dr7^&|@b<*xUkx9p$BOnxoYN6#E<66V`8cs?a1<+WX<86Xxj+21WoAprI15v_Rr z?Ic24(!F;EN7GE{p!|x_Octv^g~(gVD>%Rv+r>BMYfxeX$emt|?YJHNSKZ`V0#&>0=C6XBCaewxU}*8b(PN2R z#ElAyo#Lrac*OtrBs+T|N00N(zh`mC52O+=ytFj~p5nyAtKd%UK<=9#^)Mo=RD{-h zbyow2Z$5lPdJl2TI50Pf~`d#y=4py2x$J0cj(TO=X% zBtu<=I#uT^3X~=2ZlrtQGure=uyp~{4SX`XH6FPqGupv(xY&^OOt=RX8 zPJa!+aeY1%tpFr9NVq#OakT%tmb&`cnW&A{rEF0s(T6z|xr(Q1M58u^a# z3V*9pAl+C5HCWXb(@*r7aLpD~h==q|U-yaWfGts=#YB?1B^HHudaU;){ zi)Qy|dGryZW*+Ms7{KE7baZB`aH%AgKe$Dx73C7Ews|aKz{%#h4kNDCpY}WP`J1Gs zN>&FKv$_qE++FWWlFc*M&Zxt95*K2Hu7BIs($SHpVmtu8rVqEHH<_NG-)~<}Uz(GB z(>E3AKFo)ODHlZc#Ipz-x(#|GCur;Y_gqUWEA2^Cv@%!Sc5pQ3W#q2yIDS)*YG;9G1A)=dE|0=On~#?dOk?O+bI!73f%%HY8P z>j`7*t#KGrMMO~NZ+EY>i!f#RE8%h&emqQ@>_M#uaD;J-{n+GUU3%rhiC63^2N@CCRLD>; zv~A)_<3PRSifE9A<6>+A?O|}7gyo*(_Y!&MP6%j@uCAU1FGn2;(?!lbac@KR{B;(1 zENqKk{Po5p518)X&zl}jFhHs?rK^>d7q!7tUweAyr!_@)`$b1P@)C*F<=K{UZ~$>+ zssMj-#ym+?5Czf}b3&%f?uG$(u2CWnWhiYlZ0jpl1JG2ZW9`FH$sOJY6P>@m%^xZc zGzUI)ZvU(PH*1@T)P_CbOijrZ8n{pk3#)=m(nN-}Enr+DFTgI&M;M&@YKW|aF-XdVxwDd3S=-J#6B6>WU zBHgJ<|B|P$#362Po-7c7DUz2)c9DE8X#+R{A{Y$NFhDAi0ipq!`y+06J;4BsA~3G4 z-Xc40W+}L?Hnl~kJa*J}77U3nZ6`K16I&w~|9~`iPq7>DcsVdn0Eci4G7hRCi0@I7 zdh~(*3=BK)$$T~?2J@m#-!HGtys(5NH(Qq&j4tck9GIENG#p!vI1i9Q&5B}>u=Xb- zf8}j1*tVWBvz*_@rRUfnqb?pNj)xMt3vKvu78=ucMBu(_homQC2TEPRKmZglPz)Gi zbX3&meNqY1;G&Bs#Qzu$kaOKz!rl*@D(?29&kj^CtxuImwIp)qYuuH|oevH(zE|3- zUApvZh~aAby0OuI3^gU;Y~=$cae%I$>BjU9h(lFXr9rk>3@m!HdJ0u{4G24f&1L(Y zu-n0bfLkt7rUBtupyHX&8H=}h)jyqK$VxNu&|0&RJM-0*RVod$PlsH>CK z3K{WQMu{D~Z;UIp$%ku~2SXLW+O4>Q$+3FMQ+*|12Jxvuagn>ZqgS_iQ-*-MQQMmw zi8Bqr`T@g^2M?b*wAxbDS792o8U}#b@f`WnqgY*z0T>_ygp@V>m4o#(Z6+;UU zS_mN|kzOK1g-}umy@lRF3E>{sz3cwDf9}tF*P0)DoweTco^$rQ_w(-O+3$I6ZfYcS zQv9TVfPm1wySFU_1dhDK(hZ1M7K6crx(ey9^9S+T?EcJ}{ zg1ftydcM|fxR(9vQ8_unvqi>!XLYN;`PmB>qE-)hfqh?YZ1_DT~KF@a}ccd7(l-TKG=PsPB=YyOC(=PfoyzKc4P&e8?dFOC*ZtQ@(~C@vnb%ao0v zVc4f<#nlvUeq@}!ZeqW+{)yM5gTN!mA5dKnMu3SocQ*zrPK^%Oz*ym5@e0i?1tY|D za-4b;3e#s>O{*}~jZZqkjnDm(5Y#EkcY}ld+PcI>tL9$G2{$*Iv0l9=KccKda;%uLnG<|W&Hhr^8eq-}u3ElC5g093^q*2xN`R036NazV}UQEnU zd3kx0c!SKMDW#4XpkS$lU~~ShD>5MQI5GCZs88Nupy=Vq4cFi z$)lst{L_-yh_xnFGCx>VwwYCXYu3p{&@6A-vK1>Bj*DHUvpBV2gXC%hnx;|d3%$py@fZvqFqA>R8P=d^Ki+#AT{{X4Ti z;lg)9fmw(Vl=V_43}(Ka=;#~?vkB-ZCMsL}d6)6+A& zJzB9533Bd<@*iL@rZR}KTv#A}dNnt(BClJ)SEhXhx-o)30K-JU1^MqjrKZ*95NaU` z)FK5Cc7AvmQ@eKWubb9~rzmohP)pfU`BG2$tBwe~Kcf%hqgMjk>iCtM`RWD&&nUSj z6VYG{>Fe$08x`Nw%DPBj03xHi-V;?fJx?<27~08))ZdX+!@=U)=1kfXn(MOc85 zglQ9Wl&RT0d(99_{ig$n)~cG&iHgJR5a+VBmIoUiEB0&t$|Cl)(L;52I7Hsfi%LAr zM_ns|QQwSIJT{_Vq!`IK61~@JBl0u%rWmKN(_UhIM76ruNLoo$la}`$x&u@!)7HD~ zTkiWM`kY5n&OW~Y@$PTrrA4bQ!#Wt%ji%nWUr)~ zs_r@DHq#@W>%iJHQOwtDdfYR&IrWL4o4a<1DD|Is+`Y3c@m>k07p9`BUav3*+5wxhBDl#(xJmx5pom0R}{;W-%cVhV}s;FG|a6Tnp>2tNtOE3W>RaUR$r zIIRMTJrCu@N4**p(Miod9j3PE?O@pu=_G#`O_Rvm)W%PW$aF$&j6>8n5d9wJ@Nn-{ z5zywWtGoR|^%Cg8wolI`C5>xKhWgNz^N|NHJMO$F7~KG}Vdei&7em?oKcZ^oKAg2 z3W@m%I<%+2$aK-pvg-|XT#}Z^D#$2CxF&`XUpgXgE}#R6kU5GQ69^$l*#@e+ptCyc zIz-PGGm{YJM?!Diibm4##|X&?_Hf9sEy{Ah*J!Kts23_YoT5s2+FdyPj`r9r&%iPuQHs_{5|QFM+M8pW95CS zz4}HsP|#EywOw|t5y?fARex!nzcd3y{pGrXYok*fgI%}7Q?ka6lMoD{3wk);jnp06 zrFW{)+JA>w(BNN#`-+Y!V&U?rP%wW{yD)5OmX$sITGMXF53w(@@p!6&YOw?%>x)<$ zJKP_ZraGZg4;!9YAl?Pc_SY7eQ6ufW`- zxh&1MrOvQI+dCT!AtP57Q1(?mZI1m)qs2d_-Ws`6^6bUVBP0C^?O%pN7v zmdPDIZkC6l8Oyq$ zdf+BEY@x|G@9vlOug-=lPxmA$tm*xQk@15sL^4jJl^}KyZqBMk#Mb6AqVyMk`ubXd zIw!0UDVzj-3ATQx;9sCZso#lj=At=Za7v9b#Bcy+q*NV^eE)r28JgJoLJWu~MbDpOyzi&H_jR zU8}NB^QeM{si{c`YvLR27{d$u;v+AeGG8b&`EL-te=?bYrMt~z%Y4yBPJ$hs(~}!c?!0)WF=d~ z=&eDdS*XFovK<5V=YO^Tv?!U^NnWc4Aok74f#B!EZk(1D{FmM4Vo9#bAZIlR+bl1-dn%uJKR)c%B6 zDieKj@?!;&=#PCr)>o~w+G~6uaR!z8P>5T3Sqjtt377)|;ZyIk3GSu!5@MoKp;vAIx*}3%?pqfjpedSponbAT_7)y`DTV|>m2P+DdUN9n zyvHVSv+?a7+x*P4L2P)I(Tlvm9#kpj-g?3 z7G-WYJ^LzvLL?(6{Kn1F=7P<4E_QP#2BeJ!v+cZ)AXv`+H0A2_+WY+8aLBI6fLYKm zIr7c~w&mfa&_toreu_J~311x;*5L6Gzk3b-*q2VejURjo9y1CwM@RzxLa6QpuYEEu z7Kiv6RBg7%wCH%`f7e#Lqp)RlG7cN`(Vj7^&KYRcj6K(ch+QlHot=MG^ChkV$uRBd zZ3}+#FCPD$L>Lr?ELnz!8v@+ear@sI7ah(Y6oi<<^r|Nr@tq=`6zjo3-A)~-sR-N< z$z6?VbzmS5_e47+r(UHPRla;Vhn5n`X$T$o*0-g~Pld4d^mO;PBGxhIz_ug%Yuzsj z|Hc>fynK}Q(ihV*H2*|Jwa!ieqb`ZnxcP@C+e$m=Hx0rn^ruQJM;u1nd*~DBn{(WL ze`aHUP(iy9(fSPmQY_H*EMm8|^Z;I3*EH4a_edYqb^11_j7-zf zxCV*RuV0t8eJZu8=L$fTELWC#asyx<6HVAX-b6{-4hey(CWjbI3k6Y+19Wx4 z3}Iu8okMa^5fB3Wf%a3?zF#wQFGVSZH*(b~4fBPQbR^3aL{4OWD+!Dv@N%ypLPZ9t zQX5CUKKj*PVYr@zrQB5@fNA6F)+ugI0 z-g@mH5U4s-kFBXWNpDnt%%Oj@!4H(_{`%ezwgs#tal>&$b43)`+*o4vP3xfEp1TBb z@sDJ#wJfcO`~Ew>16LJiW!#Z>JyAV6&5FXA+R>$)v7N8x)&r;Bg zW62NTW0OIhL(O^c25!y`MBmJ@j>{w8$NR+}g#g0ucAi##O4|6s_&kf`wYD!j6R_4c z&XtnaS?14jn}s46R-Y%WZvd<1jFSPqkBMlyuRL4sfnp~lo-{KtzE61jCL?EZY;odS z7o#>SnSVS%#aZ}N`m7_yshx`C6nlAj@T`06v-}yM>Kj|$GyBCFwLxX0C01IIuBy%Y zqVKX#i-p|Oye7Y4(e)ZiIR!589c#-vFDbSx3fw&#ax-{sKv_mCG-%FIYjnv$^u~OW zT!m@M%tJjps4T3%Bz3q|Uvy$>VTMJ@*~af+#e=bPdw(Vy<&zkKmnQVu(9Z?h#@?KM zcHyT}|7rIdCmjzWS2Ly>AkrevE?z@!JDMiRC#-~%e-@%V@96x$Y}wX5)6=Zr&dfflo+jSQ>pNB+U~k7r$$hiTQKzr1=~6aU zfLa^>tRW8+{7ys1yJq)l&@*Ebe|6SonQ;WA7F2l3OPt^2E{rIS^6IAd7oVGtlIF6{!|jkOq3y-|BRXl{0?C zj$os}#QB8qre8Vxq~}{?%~qva^xowDQ5m`$SX1GSi5l73T4gKzQE9CBqqoPl##(XI z>|(4`c0=%|t|kAmYduR~)-;hk4-?Y53T)DH*iwIvi&#PtCNc}l}a9?_qD1gAqXwL@znb4HOu7?`E${n^d*OF zCjd>MJ&Ca(n}e!3$w@5B2<(RcVC62A*mSTbJZMv^@?9?+KK51tjb$-__PL*cQwVln zEiEA9=5A0<#XY~;U&MVhiN9;&k$_>>Bqmm{XkXtnF(rUzO78o0d3kgUkJr1>%i@hP zKMvI|sC_Orf|(?sjW2NNabjl2=Y3IkU&q_+u3tf=u90CFqhr@^cC5EZS$A)Sz~RK1 zii8XYAt8MDwkr_h&Sn?;ryK;}UkO~;v=Fv8B#BvnJbY!0C@QM=QSMV^P<&QF)QaQO zR&&{E=etK~3Bn6(B(xtwljz*V=FgRVrm+l3TF8EY`ETP>S_sLuV=#6ERL)h>=sdDh`2^%luda!;n3= zV+Z8<{{4`6v43~|i;IvwrU+>*cRZeSMUKJ=Zh_M2bgUq3&+gr-`+sg>GU_&3Ut45Z zm#U^)HV;)12-d})e~e_0XB|_y*|g`@_~qQPFa8ZpLgTK>jZ|NcUgs8PBpXuY3RVB@x8G{p+So7Y3+L-l#8fn zZ%kF_A}&pv%vM{uutKQxrZj;nC7 z{=U>4(&nN`5(+!m=HUFBiJ;zUJsq8I#^U5!C|YVtz+>PW8cy#`?5?e;`F0rn)G;U) zHK6Z{!PreDCH$!JJ|l5XqI>^XZW{%iUrp4Hn=i!gH6(78TP zB>ZxkSl7`3d+PY(tkRFntloZoJ2x+{a5MFEOGNGPOSaI&#Du~#H8ea94j_6Wg%*fF zdKMUAgS*}AYOB1WqIH8i$MCNGp3eadxf1*K8BrTBbny0`MhvKRr~uk9%VuY=SblD9 zZU+YROPwnEKuy1}!(L%LG&ApsitGS8@26|Ix#j_9Y_M8O5u;%yhf*Tv5B?(hpgrx1 zTp@Mu?t}}%wlY8CEgWxt>|mOKbn;hkZ*PSyg#mp|;2AbQHYurqJUurtF%c4Ns+V)0 zeN!d9hQF~fhlDYsw}K18YEu5^MJH!-ubRb#)p?oK>Zb2B}=XL*tfT6Goa`DhFQ# zq#?!w_bRL!5qtFQh5?wnyU#Jw44QKLSt}-o9U9y-&lgV&R|W{0{c@$n?)donzU7%u z2fKk<1!y~Z2L#w0Nxd83u=M;!UfGFiPonWMyTSYco6Xh*U_9K$Xijx~$p%YHT2N}-gsQV4d54?UI@1uLz zPydK69KNuj=-C;k)qeXB zGWo0;ytY?sVG5WYTK(deFON4@t8roGilBPeuiJYPqtvo|jhr7;d7C+)7(|~N+r`!6 zd@`<@HZW7e47hTfk-c_aR*4m|ohZiSO_o4dB^1WdT%7nW$w{I4uqb+Ite6dA?&;HG zXw6<73F_n^a-PwNo0^>GWuw||ezaH5Q0P6F^464!$M? zzH)Qq^Bqq04sxw(5w)#s8em^FgJjrgo12Ey z0M#utjb5Ak@GdspLgheCUfw}^<1}laoN~AKPjKaw27I0LYR6Kv`O(z7*=4Ma!`Brx zDrxNHxnaG-N=o4vOMTo;@KepSw$jac4Af#ZN`Ne{`K52y6}POOtEVblGzqd70doWy zLI?f-l9ZA{^;+lVmM@2;FowcfkXf$K8L_@L;{(bvVv&l!b$XS>rMa;!R0Q+ha(7jw z_tKUR zBpBMZitevoKkL`YuqwBtBrx8}VCd#e-2SwKl8I)~h&Xsn?ov(}D@WH-Rr-;2X^)Ns zQ}*53+FH7X;?HD+O2$Phe(lKkp zXae|hd$BvWA6u(2nFK*kQpW6x&6vI&m2{x%@~22UbDR(v+>$>xKX0t@cj#4m;}m!O z0a~VGpGbc(P_~7k>j8+|EzFn*MxYjUh!S#gMfdu|hQ)TPN@2_En)K~knL`riqvc16 z5(8)i)Cj9`pkoFAl^If2RyM_@!o#ZoJvr~1(vMc=MC*U7aPHPom6x>9%#hCZnO=N0AdlN;t7`f%nxpO{Jj>p@|l^$N8aoDdvC9z zOwJDq-CaFALQrlkkx$lYU$|%%@{TI3BJ1IYSfL|fpF9v#?^2J>P9o#d!p`Kaw*T!^ z@wkr1YP#Z9b-GUGXkLMx?)cqzUR#Ugplz&s{ippa}K7 z$xJw5V75G&2DdgY$hviF$6~Ffj1FeEIhc zKL0K7Cd0!<11DY%ESi)y4f-;6rW?R7RRM}}%~Atex_#HZmy8oN+Fl)xfocHrdDSf| zW_=nz%dA`Hy20?^5zLRtRRGY;2)E-hkBCIGia~!MEda7s59i(t8gpDw@+y78mw_m#Ah~Q8$36I(cJlaCy1*pR>pIii%KJH3jnW@_n0|?GjQ_ z(Vje#kCUQVlqzr{%Sh~8$u>{r*tV7#<$=~i0vQVt0sY$uz7BAgX)skIgVYCxT;fy_ z67AWNADt$i*j*Ta7;e-8lSK$C^RxEyDq?ceS`{7myBM3WqYR3U1dwEcR`U?~b#PIy=@m5GAar=IU_xgT3OAOAKx=C2f9&abMfQiu%tIrA~E5N>07`n1~CCKGNFNAaEJ={2^r zu##hRY40BZ#7FLf0^^|Ai= z(rdr%8|38V9s&mI;(0e=8RmGs(4h4j`xTns+8{T%|)6cvR$dE?jKyfQO017yjhNsIi*0WMf^5}HYW zLw#2^h7o~zMxI5CBb}Z(RpgV4eu@&L%WD4r*8h9P)a?oE4akpQ^Y086tE;O|4OMxl zV4r<%AIIq_f7D9?S%3=P+U)Ce-HijDVwvnEbOK$Ze`H?;jqLd@5YFXU5!S>qnI1E!w^BDdlNm0p7Pibs#KH?pI{ zwx^z+jCj)#%Wo9?T=Hh{HKh0QyAt*^UXep7nUIG8jK2s43pni792#!2)&WuuN(q~$ zjZ%W7P=ov9_3#1f2 z2DH^1yyZ8FeYiHdUX*qA*@Yem9( zl`gjS{EbGNMdwHqQHQg_n31={@BO3I(1 z2}ZpbdQgZzBKQ}KAFbpiJ|?2Jh8 z4uF3EKne$}NRXJ^4i6W@9P3h*fq{W-P6j?%5gs?!b~=~j)2eHpPXzG;EGZDevV5&z z_zVDLbg>GaOsd8S0@gWL9BAKPgode2d-h35kt;7%BiVQF-rb%x(3p4b>{_46!-9m> zN_p?!nY!f%Po?Qdpt09uVvt}-!4xMP4SBc%mM=*=Zb`i5CMP!p2MlNV;I%VhTiipxzeaoBcX=&K*C$i`%DlsHkgbl)6HVHEgnH zw(M>9-8s8Fc*%>jxw%;}o)R|rzpI+gt`Nwt+YEe6`QM#?i|{WJ{_TSQybCh7c1ola UAsS3Ya1ijbvam-G&R)L#Ki<*{n*aa+ literal 0 HcmV?d00001 diff --git a/tests/testdata/control_images/text_renderer/text_tab_positions_percentage_html/text_tab_positions_percentage_html.png b/tests/testdata/control_images/text_renderer/text_tab_positions_percentage_html/text_tab_positions_percentage_html.png new file mode 100644 index 0000000000000000000000000000000000000000..550ae3b3dbc36a10c3117c3c9c654de092904311 GIT binary patch literal 7016 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 CxdQY6 literal 0 HcmV?d00001 diff --git a/tests/testdata/control_images/text_renderer/text_tab_positions_percentage_html/text_tab_positions_percentage_html_mask.png b/tests/testdata/control_images/text_renderer/text_tab_positions_percentage_html/text_tab_positions_percentage_html_mask.png new file mode 100644 index 0000000000000000000000000000000000000000..d36ce84cce952840839abe5200ab623bde2eafa6 GIT binary patch literal 6750 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 literal 0 HcmV?d00001 From 5261b9c8adaa3fe918fb4334b6c4916e5a8b8acb Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Tue, 29 Oct 2024 12:45:01 +1000 Subject: [PATCH 3/7] Add GUI to edit tab stop distances --- .../auto_additions/qgstabpositionwidget.py | 11 + .../labeling/qgstabpositionwidget.sip.in | 92 +++ .../auto_generated/qgstextformatwidget.sip.in | 1 + python/PyQt6/gui/gui_auto.sip | 1 + .../auto_additions/qgstabpositionwidget.py | 11 + .../labeling/qgstabpositionwidget.sip.in | 92 +++ .../auto_generated/qgstextformatwidget.sip.in | 1 + python/gui/gui_auto.sip | 1 + src/gui/CMakeLists.txt | 2 + src/gui/labeling/qgstabpositionwidget.cpp | 123 ++++ src/gui/labeling/qgstabpositionwidget.h | 102 ++++ src/gui/qgstextformatwidget.cpp | 35 ++ src/gui/qgstextformatwidget.h | 4 + src/ui/qgstabpositionwidgetbase.ui | 87 +++ src/ui/qgstextformatwidgetbase.ui | 552 +++++++++--------- 15 files changed, 851 insertions(+), 264 deletions(-) create mode 100644 python/PyQt6/gui/auto_additions/qgstabpositionwidget.py create mode 100644 python/PyQt6/gui/auto_generated/labeling/qgstabpositionwidget.sip.in create mode 100644 python/gui/auto_additions/qgstabpositionwidget.py create mode 100644 python/gui/auto_generated/labeling/qgstabpositionwidget.sip.in create mode 100644 src/gui/labeling/qgstabpositionwidget.cpp create mode 100644 src/gui/labeling/qgstabpositionwidget.h create mode 100644 src/ui/qgstabpositionwidgetbase.ui diff --git a/python/PyQt6/gui/auto_additions/qgstabpositionwidget.py b/python/PyQt6/gui/auto_additions/qgstabpositionwidget.py new file mode 100644 index 000000000000..68595e701204 --- /dev/null +++ b/python/PyQt6/gui/auto_additions/qgstabpositionwidget.py @@ -0,0 +1,11 @@ +# The following has been generated automatically from src/gui/labeling/qgstabpositionwidget.h +try: + QgsTabPositionWidget.__attribute_docs__ = {'positionsChanged': 'Emitted when positions are changed in the widget.\n'} + QgsTabPositionWidget.__signal_arguments__ = {'positionsChanged': ['positions: List[QgsTextFormat.Tab]']} + QgsTabPositionWidget.__group__ = ['labeling'] +except NameError: + pass +try: + QgsTabPositionDialog.__group__ = ['labeling'] +except NameError: + pass diff --git a/python/PyQt6/gui/auto_generated/labeling/qgstabpositionwidget.sip.in b/python/PyQt6/gui/auto_generated/labeling/qgstabpositionwidget.sip.in new file mode 100644 index 000000000000..0c9f7f8473c1 --- /dev/null +++ b/python/PyQt6/gui/auto_generated/labeling/qgstabpositionwidget.sip.in @@ -0,0 +1,92 @@ +/************************************************************************ + * This file has been generated automatically from * + * * + * src/gui/labeling/qgstabpositionwidget.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.py again * + ************************************************************************/ + + + + + +class QgsTabPositionWidget: QgsPanelWidget +{ +%Docstring(signature="appended") +A widget for configuring :py:class:`QgsTextFormat` tab positions. + +.. versionadded:: 3.42 +%End + +%TypeHeaderCode +#include "qgstabpositionwidget.h" +%End + public: + + QgsTabPositionWidget( QWidget *parent /TransferThis/ = 0 ); +%Docstring +Constructor for QgsTabPositionWidget, with the specified ``parent`` widget +%End + + void setPositions( const QList< QgsTextFormat::Tab > &positions ); +%Docstring +Sets the tab ``positions`` to show in the widget. + +.. seealso:: :py:func:`positions` +%End + + QList< QgsTextFormat::Tab > positions() const; +%Docstring +Returns the tab positions defined in the widget. + +.. seealso:: :py:func:`setPositions` +%End + + signals: + + void positionsChanged( const QList< QgsTextFormat::Tab > &positions ); +%Docstring +Emitted when positions are changed in the widget. +%End + +}; + +class QgsTabPositionDialog : QDialog +{ +%Docstring(signature="appended") +A dialog to enter a custom dash space pattern for lines +%End + +%TypeHeaderCode +#include "qgstabpositionwidget.h" +%End + public: + + QgsTabPositionDialog( QWidget *parent /TransferThis/ = 0, Qt::WindowFlags f = Qt::WindowFlags() ); +%Docstring +Constructor for QgsTabPositionDialog +%End + + void setPositions( const QList< QgsTextFormat::Tab > &positions ); +%Docstring +Sets the tab ``positions`` to show in the dialog. + +.. seealso:: :py:func:`positions` +%End + + QList< QgsTextFormat::Tab > positions() const; +%Docstring +Returns the tab positions defined in the dialog. + +.. seealso:: :py:func:`setPositions` +%End + +}; + +/************************************************************************ + * This file has been generated automatically from * + * * + * src/gui/labeling/qgstabpositionwidget.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.py again * + ************************************************************************/ diff --git a/python/PyQt6/gui/auto_generated/qgstextformatwidget.sip.in b/python/PyQt6/gui/auto_generated/qgstextformatwidget.sip.in index 178f702dc62c..33de4a0ff9c1 100644 --- a/python/PyQt6/gui/auto_generated/qgstextformatwidget.sip.in +++ b/python/PyQt6/gui/auto_generated/qgstextformatwidget.sip.in @@ -166,6 +166,7 @@ when registering labels for the labeling settings currently defined by the widge + protected slots: void updateLinePlacementOptions(); diff --git a/python/PyQt6/gui/gui_auto.sip b/python/PyQt6/gui/gui_auto.sip index da7e61dabd1a..aecff104876f 100644 --- a/python/PyQt6/gui/gui_auto.sip +++ b/python/PyQt6/gui/gui_auto.sip @@ -364,6 +364,7 @@ %Include auto_generated/labeling/qgslabellineanchorwidget.sip %Include auto_generated/labeling/qgslabelobstaclesettingswidget.sip %Include auto_generated/labeling/qgslabelsettingswidgetbase.sip +%Include auto_generated/labeling/qgstabpositionwidget.sip %Include auto_generated/layertree/qgscustomlayerorderwidget.sip %Include auto_generated/layertree/qgslayertreeembeddedconfigwidget.sip %Include auto_generated/layertree/qgslayertreeembeddedwidgetregistry.sip diff --git a/python/gui/auto_additions/qgstabpositionwidget.py b/python/gui/auto_additions/qgstabpositionwidget.py new file mode 100644 index 000000000000..68595e701204 --- /dev/null +++ b/python/gui/auto_additions/qgstabpositionwidget.py @@ -0,0 +1,11 @@ +# The following has been generated automatically from src/gui/labeling/qgstabpositionwidget.h +try: + QgsTabPositionWidget.__attribute_docs__ = {'positionsChanged': 'Emitted when positions are changed in the widget.\n'} + QgsTabPositionWidget.__signal_arguments__ = {'positionsChanged': ['positions: List[QgsTextFormat.Tab]']} + QgsTabPositionWidget.__group__ = ['labeling'] +except NameError: + pass +try: + QgsTabPositionDialog.__group__ = ['labeling'] +except NameError: + pass diff --git a/python/gui/auto_generated/labeling/qgstabpositionwidget.sip.in b/python/gui/auto_generated/labeling/qgstabpositionwidget.sip.in new file mode 100644 index 000000000000..0c9f7f8473c1 --- /dev/null +++ b/python/gui/auto_generated/labeling/qgstabpositionwidget.sip.in @@ -0,0 +1,92 @@ +/************************************************************************ + * This file has been generated automatically from * + * * + * src/gui/labeling/qgstabpositionwidget.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.py again * + ************************************************************************/ + + + + + +class QgsTabPositionWidget: QgsPanelWidget +{ +%Docstring(signature="appended") +A widget for configuring :py:class:`QgsTextFormat` tab positions. + +.. versionadded:: 3.42 +%End + +%TypeHeaderCode +#include "qgstabpositionwidget.h" +%End + public: + + QgsTabPositionWidget( QWidget *parent /TransferThis/ = 0 ); +%Docstring +Constructor for QgsTabPositionWidget, with the specified ``parent`` widget +%End + + void setPositions( const QList< QgsTextFormat::Tab > &positions ); +%Docstring +Sets the tab ``positions`` to show in the widget. + +.. seealso:: :py:func:`positions` +%End + + QList< QgsTextFormat::Tab > positions() const; +%Docstring +Returns the tab positions defined in the widget. + +.. seealso:: :py:func:`setPositions` +%End + + signals: + + void positionsChanged( const QList< QgsTextFormat::Tab > &positions ); +%Docstring +Emitted when positions are changed in the widget. +%End + +}; + +class QgsTabPositionDialog : QDialog +{ +%Docstring(signature="appended") +A dialog to enter a custom dash space pattern for lines +%End + +%TypeHeaderCode +#include "qgstabpositionwidget.h" +%End + public: + + QgsTabPositionDialog( QWidget *parent /TransferThis/ = 0, Qt::WindowFlags f = Qt::WindowFlags() ); +%Docstring +Constructor for QgsTabPositionDialog +%End + + void setPositions( const QList< QgsTextFormat::Tab > &positions ); +%Docstring +Sets the tab ``positions`` to show in the dialog. + +.. seealso:: :py:func:`positions` +%End + + QList< QgsTextFormat::Tab > positions() const; +%Docstring +Returns the tab positions defined in the dialog. + +.. seealso:: :py:func:`setPositions` +%End + +}; + +/************************************************************************ + * This file has been generated automatically from * + * * + * src/gui/labeling/qgstabpositionwidget.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.py again * + ************************************************************************/ diff --git a/python/gui/auto_generated/qgstextformatwidget.sip.in b/python/gui/auto_generated/qgstextformatwidget.sip.in index 9b5cd9c36c23..1df50d557cd2 100644 --- a/python/gui/auto_generated/qgstextformatwidget.sip.in +++ b/python/gui/auto_generated/qgstextformatwidget.sip.in @@ -166,6 +166,7 @@ when registering labels for the labeling settings currently defined by the widge + protected slots: void updateLinePlacementOptions(); diff --git a/python/gui/gui_auto.sip b/python/gui/gui_auto.sip index da7e61dabd1a..aecff104876f 100644 --- a/python/gui/gui_auto.sip +++ b/python/gui/gui_auto.sip @@ -364,6 +364,7 @@ %Include auto_generated/labeling/qgslabellineanchorwidget.sip %Include auto_generated/labeling/qgslabelobstaclesettingswidget.sip %Include auto_generated/labeling/qgslabelsettingswidgetbase.sip +%Include auto_generated/labeling/qgstabpositionwidget.sip %Include auto_generated/layertree/qgscustomlayerorderwidget.sip %Include auto_generated/layertree/qgslayertreeembeddedconfigwidget.sip %Include auto_generated/layertree/qgslayertreeembeddedwidgetregistry.sip diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index d0fab41a0f74..2c6d41bb70fc 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -267,6 +267,7 @@ set(QGIS_GUI_SRCS labeling/qgslabelobstaclesettingswidget.cpp labeling/qgslabelsettingswidgetbase.cpp labeling/qgsrulebasedlabelingwidget.cpp + labeling/qgstabpositionwidget.cpp layertree/qgscustomlayerorderwidget.cpp layertree/qgslayertreeembeddedconfigwidget.cpp @@ -1245,6 +1246,7 @@ set(QGIS_GUI_HDRS labeling/qgslabelobstaclesettingswidget.h labeling/qgslabelsettingswidgetbase.h labeling/qgsrulebasedlabelingwidget.h + labeling/qgstabpositionwidget.h layertree/qgscustomlayerorderwidget.h layertree/qgslayertreeembeddedconfigwidget.h diff --git a/src/gui/labeling/qgstabpositionwidget.cpp b/src/gui/labeling/qgstabpositionwidget.cpp new file mode 100644 index 000000000000..ef3ef14592ad --- /dev/null +++ b/src/gui/labeling/qgstabpositionwidget.cpp @@ -0,0 +1,123 @@ +/*************************************************************************** + qgstabpositionwidget.h + --------------------- + begin : October 2024 + copyright : (C) 2024 by Nyall Dawson + email : nyall dot dawson at gmail dot com + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#include "qgstabpositionwidget.h" +#include "qgsapplication.h" +#include "qgsdoublevalidator.h" + +#include + +QgsTabPositionWidget::QgsTabPositionWidget( QWidget *parent ) + : QgsPanelWidget( parent ) +{ + setupUi( this ); + + mAddButton->setIcon( QgsApplication::getThemeIcon( "symbologyAdd.svg" ) ); + mRemoveButton->setIcon( QgsApplication::getThemeIcon( "symbologyRemove.svg" ) ); + + connect( mAddButton, &QPushButton::clicked, this, &QgsTabPositionWidget::mAddButton_clicked ); + connect( mRemoveButton, &QPushButton::clicked, this, &QgsTabPositionWidget::mRemoveButton_clicked ); + connect( mTabPositionTreeWidget, &QTreeWidget::itemChanged, this, &QgsTabPositionWidget::emitPositionsChanged ); +} + +void QgsTabPositionWidget::setPositions( const QList &positions ) +{ + mTabPositionTreeWidget->clear(); + for ( const QgsTextFormat::Tab &tab : positions ) + { + QTreeWidgetItem *entry = new QTreeWidgetItem(); + entry->setFlags( Qt::ItemIsSelectable | Qt::ItemIsEditable | Qt::ItemIsEnabled ); + entry->setText( 0, QLocale().toString( tab.position() ) ); + entry->setData( 0, Qt::EditRole, tab.position() ); + mTabPositionTreeWidget->addTopLevelItem( entry ); + } +} + +QList QgsTabPositionWidget::positions() const +{ + QList result; + const int nTopLevelItems = mTabPositionTreeWidget->topLevelItemCount(); + result.reserve( nTopLevelItems ); + for ( int i = 0; i < nTopLevelItems; ++i ) + { + if ( QTreeWidgetItem *currentItem = mTabPositionTreeWidget->topLevelItem( i ) ) + { + result << QgsTextFormat::Tab( QgsDoubleValidator::toDouble( currentItem->text( 0 ) ) ); + } + } + + std::sort( result.begin(), result.end(), []( const QgsTextFormat::Tab & a, const QgsTextFormat::Tab & b ) + { + return a.position() < b.position(); + } ); + + return result; +} + +void QgsTabPositionWidget::mAddButton_clicked() +{ + const QList< QgsTextFormat::Tab > currentPositions = positions(); + double newPosition = 6; + if ( !currentPositions.empty() ) + { + newPosition = currentPositions.last().position() + 6; + } + + QTreeWidgetItem *entry = new QTreeWidgetItem(); + entry->setFlags( Qt::ItemIsSelectable | Qt::ItemIsEditable | Qt::ItemIsEnabled ); + entry->setText( 0, QLocale().toString( newPosition ) ); + entry->setData( 0, Qt::EditRole, newPosition ); + mTabPositionTreeWidget->addTopLevelItem( entry ); + emitPositionsChanged(); +} + +void QgsTabPositionWidget::mRemoveButton_clicked() +{ + if ( QTreeWidgetItem *currentItem = mTabPositionTreeWidget->currentItem() ) + { + mTabPositionTreeWidget->takeTopLevelItem( mTabPositionTreeWidget->indexOfTopLevelItem( currentItem ) ); + } + emitPositionsChanged(); +} + +void QgsTabPositionWidget::emitPositionsChanged() +{ + emit positionsChanged( positions() ); +} + + +QgsTabPositionDialog::QgsTabPositionDialog( QWidget *parent, Qt::WindowFlags f ) + : QDialog( parent, f ) +{ + QVBoxLayout *vLayout = new QVBoxLayout(); + mWidget = new QgsTabPositionWidget(); + vLayout->addWidget( mWidget ); + QDialogButtonBox *bbox = new QDialogButtonBox( QDialogButtonBox::Ok | QDialogButtonBox::Cancel, Qt::Horizontal ); + connect( bbox, &QDialogButtonBox::accepted, this, &QgsTabPositionDialog::accept ); + connect( bbox, &QDialogButtonBox::rejected, this, &QgsTabPositionDialog::reject ); + vLayout->addWidget( bbox ); + setLayout( vLayout ); + setWindowTitle( tr( "Tab Positions" ) ); +} + +void QgsTabPositionDialog::setPositions( const QList &positions ) +{ + mWidget->setPositions( positions ); +} + +QList QgsTabPositionDialog::positions() const +{ + return mWidget->positions(); +} diff --git a/src/gui/labeling/qgstabpositionwidget.h b/src/gui/labeling/qgstabpositionwidget.h new file mode 100644 index 000000000000..863bb73e551e --- /dev/null +++ b/src/gui/labeling/qgstabpositionwidget.h @@ -0,0 +1,102 @@ +/*************************************************************************** + qgstabpositionwidget.h + --------------------- + begin : October 2024 + copyright : (C) 2024 by Nyall Dawson + email : nyall dot dawson at gmail dot com + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#ifndef QGSTABPOSITIONWIDGET_H +#define QGSTABPOSITIONWIDGET_H + +#include "ui_qgstabpositionwidgetbase.h" + +#include "qgis_gui.h" +#include "qgis_sip.h" +#include "qgspanelwidget.h" +#include "qgstextformat.h" + +#include + +/** + * \ingroup gui + * \brief A widget for configuring QgsTextFormat tab positions. + * \since QGIS 3.42 +*/ +class GUI_EXPORT QgsTabPositionWidget: public QgsPanelWidget, private Ui::QgsTabPositionWidgetBase +{ + Q_OBJECT + public: + + //! Constructor for QgsTabPositionWidget, with the specified \a parent widget + QgsTabPositionWidget( QWidget *parent SIP_TRANSFERTHIS = nullptr ); + + /** + * Sets the tab \a positions to show in the widget. + * + * \see positions() + */ + void setPositions( const QList< QgsTextFormat::Tab > &positions ); + + /** + * Returns the tab positions defined in the widget. + * + * \see setPositions() + */ + QList< QgsTextFormat::Tab > positions() const; + + signals: + + /** + * Emitted when positions are changed in the widget. + */ + void positionsChanged( const QList< QgsTextFormat::Tab > &positions ); + + private slots: + void mAddButton_clicked(); + void mRemoveButton_clicked(); + + void emitPositionsChanged(); + +}; + +/** + * \ingroup gui + * \brief A dialog to enter a custom dash space pattern for lines +*/ +class GUI_EXPORT QgsTabPositionDialog : public QDialog +{ + Q_OBJECT + public: + + //! Constructor for QgsTabPositionDialog + QgsTabPositionDialog( QWidget *parent SIP_TRANSFERTHIS = nullptr, Qt::WindowFlags f = Qt::WindowFlags() ); + + /** + * Sets the tab \a positions to show in the dialog. + * + * \see positions() + */ + void setPositions( const QList< QgsTextFormat::Tab > &positions ); + + /** + * Returns the tab positions defined in the dialog. + * + * \see setPositions() + */ + QList< QgsTextFormat::Tab > positions() const; + + private: + + QgsTabPositionWidget *mWidget = nullptr; + +}; + +#endif // QGSTABPOSITIONWIDGET_H diff --git a/src/gui/qgstextformatwidget.cpp b/src/gui/qgstextformatwidget.cpp index 36ddf6c60cd6..bf14c1b0bcc8 100644 --- a/src/gui/qgstextformatwidget.cpp +++ b/src/gui/qgstextformatwidget.cpp @@ -45,6 +45,7 @@ #include "qgsconfig.h" #include "qgsprojectstylesettings.h" #include "qgsprojectviewsettings.h" +#include "qgstabpositionwidget.h" #include #include @@ -110,6 +111,7 @@ void QgsTextFormatWidget::initWidget() connect( mToolButtonConfigureSubstitutes, &QToolButton::clicked, this, &QgsTextFormatWidget::mToolButtonConfigureSubstitutes_clicked ); connect( mKerningCheckBox, &QCheckBox::toggled, this, &QgsTextFormatWidget::kerningToggled ); connect( mComboOverlapHandling, qOverload< int >( &QComboBox::currentIndexChanged ), this, &QgsTextFormatWidget::overlapModeChanged ); + connect( mTabStopsButton, &QToolButton::clicked, this, &QgsTextFormatWidget::configureTabStops ); const int iconSize = QgsGuiUtils::scaleIconSize( 20 ); mOptionsTab->setIconSize( QSize( iconSize, iconSize ) ); @@ -929,6 +931,9 @@ void QgsTextFormatWidget::updateWidgetForFormat( const QgsTextFormat &format ) mDataDefinedProperties = format.dataDefinedProperties(); } + mTabPositions = format.tabPositions(); + mTabStopDistanceSpin->setEnabled( mTabPositions.empty() ); + // buffer mBufferDrawChkBx->setChecked( buffer.enabled() ); mBufferFrame->setEnabled( buffer.enabled() ); @@ -1128,6 +1133,7 @@ QgsTextFormat QgsTextFormatWidget::format( bool includeDataDefinedProperties ) c format.setTabStopDistance( mTabDistanceUnitWidget->unit() == Qgis::RenderUnit::Percentage ? ( mTabStopDistanceSpin->value() / 100 ) : mTabStopDistanceSpin->value() ); format.setTabStopDistanceUnit( mTabDistanceUnitWidget->unit() ); format.setTabStopDistanceMapUnitScale( mTabDistanceUnitWidget->getMapUnitScale() ); + format.setTabPositions( mTabPositions ); // buffer QgsTextBufferSettings buffer; @@ -2162,6 +2168,35 @@ void QgsTextFormatWidget::mToolButtonConfigureSubstitutes_clicked() } } +void QgsTextFormatWidget::configureTabStops() +{ + QgsPanelWidget *panel = QgsPanelWidget::findParentPanel( this ); + if ( panel && panel->dockMode() ) + { + QgsTabPositionWidget *widget = new QgsTabPositionWidget( panel ); + widget->setPanelTitle( tr( "Tab Positions" ) ); + widget->setPositions( mTabPositions ); + connect( widget, &QgsTabPositionWidget::positionsChanged, this, [ = ]( const QList< QgsTextFormat::Tab > &positions ) + { + mTabPositions = positions; + mTabStopDistanceSpin->setEnabled( mTabPositions.empty() ); + emit widgetChanged(); + } ); + panel->openPanel( widget ); + } + else + { + QgsTabPositionDialog dlg( this ); + dlg.setPositions( mTabPositions ); + if ( dlg.exec() == QDialog::Accepted ) + { + mTabPositions = dlg.positions(); + mTabStopDistanceSpin->setEnabled( mTabPositions.empty() ); + emit widgetChanged(); + } + } +} + void QgsTextFormatWidget::showBackgroundRadius( bool show ) { mShapeRadiusLabel->setVisible( show ); diff --git a/src/gui/qgstextformatwidget.h b/src/gui/qgstextformatwidget.h index 897b68945177..cea41c79609e 100644 --- a/src/gui/qgstextformatwidget.h +++ b/src/gui/qgstextformatwidget.h @@ -170,6 +170,9 @@ class GUI_EXPORT QgsTextFormatWidget : public QWidget, public QgsExpressionConte //! Text substitution list QgsStringReplacementCollection mSubstitutions; + //! Tab positions + QList< QgsTextFormat::Tab > mTabPositions; + //! Quadrant button group QButtonGroup *mQuadrantBtnGrp = nullptr; //! Symbol direction button group @@ -299,6 +302,7 @@ class GUI_EXPORT QgsTextFormatWidget : public QWidget, public QgsExpressionConte void mDirectSymbRightToolBtn_clicked(); void chkLineOrientationDependent_toggled( bool active ); void mToolButtonConfigureSubstitutes_clicked(); + void configureTabStops(); void collapseSample( bool collapse ); void changeTextColor( const QColor &color ); void changeBufferColor( const QColor &color ); diff --git a/src/ui/qgstabpositionwidgetbase.ui b/src/ui/qgstabpositionwidgetbase.ui new file mode 100644 index 000000000000..e158f3749c20 --- /dev/null +++ b/src/ui/qgstabpositionwidgetbase.ui @@ -0,0 +1,87 @@ + + + QgsTabPositionWidgetBase + + + + 0 + 0 + 194 + 277 + + + + Tab Positions + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + true + + + 1 + + + + Dash + + + + + + + + + + + + + + + + + + + + + + + + Qt::Vertical + + + + 20 + 245 + + + + + + + + + + + QgsPanelWidget + QWidget +
qgspanelwidget.h
+ 1 +
+
+ + +
diff --git a/src/ui/qgstextformatwidgetbase.ui b/src/ui/qgstextformatwidgetbase.ui index 536a6ed4535a..aa7f39ad91e1 100644 --- a/src/ui/qgstextformatwidgetbase.ui +++ b/src/ui/qgstextformatwidgetbase.ui @@ -625,7 +625,7 @@ - 0 + 1 @@ -654,8 +654,8 @@ 0 0 - 305 - 307 + 485 + 410 @@ -1217,8 +1217,8 @@ font-style: italic; 0 0 - 347 - 720 + 471 + 755 @@ -1228,34 +1228,41 @@ font-style: italic; 6 - - + + - Blend mode + Spacing - - - - - + + - Spacing + … - - + + … - - + + + + If enabled, the label text will automatically be modified using a preset list of substitutes + + + Apply label text substitutes + + + + + - + 0 @@ -1263,12 +1270,12 @@ font-style: italic; - word + letter - + 0 @@ -1297,17 +1304,27 @@ font-style: italic; - - - - If enabled, the label text will automatically be modified using a preset list of substitutes + + + + … + + + + + + + + 0 + 0 + - Apply label text substitutes + Type case - + false @@ -1320,159 +1337,17 @@ font-style: italic; - - - - … - - - - - - - … - - - - - - - 6 - - - - - Qt::Horizontal - - - - 0 - 20 - - - - - - - - - 0 - 0 - - - - Formatted numbers - - - - - - - … - - - - - - - QFrame::NoFrame - - - QFrame::Raised - - - - 20 - - - 0 - - - 0 - - - 0 - - - - - Decimal places - - - - - - - - 0 - 0 - - - - 20 - - - - - - - … - - - - - - - Qt::LeftToRight - - - Show plus sign - - - - - - - … - - - - - - - - - - - - … - - - - - + + … - - - - Qt::Vertical - - - - 20 - 0 - - - - - - + + - … + Enable kerning @@ -1498,7 +1373,30 @@ font-style: italic; - + + + + + + + … + + + + + + + Qt::Vertical + + + + 20 + 0 + + + + + 6 @@ -1710,36 +1608,10 @@ font-style: italic; - - - - - - - Stretch - - - - - - - % - - - 1 - - - 4000 - - - 100 - - - - - + + - + 0 @@ -1747,12 +1619,12 @@ font-style: italic; - letter + word - + 0 @@ -1781,7 +1653,141 @@ font-style: italic; - + + + + % + + + 1 + + + 4000 + + + 100 + + + + + + + Stretch + + + + + + + 6 + + + + + Qt::Horizontal + + + + 0 + 20 + + + + + + + + + 0 + 0 + + + + Formatted numbers + + + + + + + … + + + + + + + QFrame::NoFrame + + + QFrame::Raised + + + + 20 + + + 0 + + + 0 + + + 0 + + + + + Decimal places + + + + + + + + 0 + 0 + + + + 20 + + + + + + + … + + + + + + + Qt::LeftToRight + + + Show plus sign + + + + + + + … + + + + + + + + + + + + @@ -2081,39 +2087,85 @@ font-style: italic; - - - - - 0 - 0 - - + + - Type case + … - - + + - Enable kerning + Blend mode - + Text orientation - - - - Tab distance + + + + 0 - + + + + + 0 + 0 + + + + Space in pixels or map units, relative to size unit choice + + + 4 + + + -1000.000000000000000 + + + 999999999.000000000000000 + + + 0.100000000000000 + + + true + + + + + + + Qt::StrongFocus + + + + + + + Configure tab stops + + + ... + + + + :/images/themes/default/mActionOptions.svg:/images/themes/default/mActionOptions.svg + + + true + + + + @@ -2122,38 +2174,10 @@ font-style: italic; - - - - - 0 - 0 - - - - Space in pixels or map units, relative to size unit choice - - - 4 - - - -1000.000000000000000 - - - 999999999.000000000000000 - - - 0.100000000000000 - - - true - - - - - - - Qt::StrongFocus + + + + Tab distance @@ -2190,8 +2214,8 @@ font-style: italic; 0 0 - 325 - 346 + 273 + 299 @@ -4142,9 +4166,9 @@ font-style: italic; 0 - -229 - 471 - 1786 + 0 + 435 + 1784 @@ -5513,8 +5537,7 @@ font-style: italic; - - + @@ -6965,6 +6988,7 @@ This limit is inclusive, that means the label will be displayed on this scale.mFontStretchDDBtn mTabStopDistanceSpin mTabDistanceUnitWidget + mTabStopsButton mTabDistanceDDBtn mKerningCheckBox mTextOrientationComboBox From b40fd69bc8bc64e29ac29c40b797e72bedfcec7d Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Tue, 29 Oct 2024 13:19:34 +1000 Subject: [PATCH 4/7] Make sure curved labels respect tab positions --- src/core/labeling/qgspallabeling.cpp | 2 +- src/core/labeling/qgstextlabelfeature.cpp | 53 +++++++- src/core/labeling/qgstextlabelfeature.h | 2 +- tests/src/core/testqgslabelingengine.cpp | 116 ++++++++++++++++++ .../expected_curved_tab_positions.png | Bin 0 -> 12437 bytes .../expected_curved_tabs.png | Bin 0 -> 12409 bytes 6 files changed, 169 insertions(+), 4 deletions(-) create mode 100644 tests/testdata/control_images/labelingengine/expected_curved_tab_positions/expected_curved_tab_positions.png create mode 100644 tests/testdata/control_images/labelingengine/expected_curved_tabs/expected_curved_tabs.png diff --git a/src/core/labeling/qgspallabeling.cpp b/src/core/labeling/qgspallabeling.cpp index 99d99f8cd4a4..d0f7b3a05420 100644 --- a/src/core/labeling/qgspallabeling.cpp +++ b/src/core/labeling/qgspallabeling.cpp @@ -2967,7 +2967,7 @@ std::unique_ptr QgsPalLayerSettings::registerFeatureWithDetails case Qgis::LabelPlacement::Curved: case Qgis::LabelPlacement::PerimeterCurved: - labelFeature->setTextMetrics( QgsTextLabelFeature::calculateTextMetrics( xform, context, labelFont, labelFontMetrics, labelFont.letterSpacing(), labelFont.wordSpacing(), labelText, evaluatedFormat.allowHtmlFormatting() ? &doc : nullptr, evaluatedFormat.allowHtmlFormatting() ? &documentMetrics : nullptr ) ); + labelFeature->setTextMetrics( QgsTextLabelFeature::calculateTextMetrics( xform, context, evaluatedFormat, labelFont, labelFontMetrics, labelFont.letterSpacing(), labelFont.wordSpacing(), labelText, evaluatedFormat.allowHtmlFormatting() ? &doc : nullptr, evaluatedFormat.allowHtmlFormatting() ? &documentMetrics : nullptr ) ); break; } diff --git a/src/core/labeling/qgstextlabelfeature.cpp b/src/core/labeling/qgstextlabelfeature.cpp index f7be1e399914..ef985e0e87c9 100644 --- a/src/core/labeling/qgstextlabelfeature.cpp +++ b/src/core/labeling/qgstextlabelfeature.cpp @@ -21,7 +21,7 @@ #include "qgstextfragment.h" #include "qgstextblock.h" #include "qgstextrenderer.h" - +#include "qgsrendercontext.h" QgsTextLabelFeature::QgsTextLabelFeature( QgsFeatureId id, geos::unique_ptr geometry, QSizeF size ) : QgsLabelFeature( id, std::move( geometry ), size ) @@ -49,8 +49,25 @@ bool QgsTextLabelFeature::hasCharacterFormat( int partId ) const return mTextMetrics.has_value() && partId < mTextMetrics->graphemeFormatCount(); } -QgsPrecalculatedTextMetrics QgsTextLabelFeature::calculateTextMetrics( const QgsMapToPixel *xform, const QgsRenderContext &context, const QFont &baseFont, const QFontMetricsF &fontMetrics, double letterSpacing, double wordSpacing, const QString &text, QgsTextDocument *document, QgsTextDocumentMetrics * ) +QgsPrecalculatedTextMetrics QgsTextLabelFeature::calculateTextMetrics( const QgsMapToPixel *xform, const QgsRenderContext &context, const QgsTextFormat &format, const QFont &baseFont, const QFontMetricsF &fontMetrics, double letterSpacing, double wordSpacing, const QString &text, QgsTextDocument *document, QgsTextDocumentMetrics * ) { + const double tabStopDistancePainterUnits = format.tabStopDistanceUnit() == Qgis::RenderUnit::Percentage + ? format.tabStopDistance() * baseFont.pixelSize() + : context.convertToPainterUnits( format.tabStopDistance(), format.tabStopDistanceUnit(), format.tabStopDistanceMapUnitScale() ); + + const QList< QgsTextFormat::Tab > tabPositions = format.tabPositions(); + QList< double > tabStopDistancesPainterUnits; + tabStopDistancesPainterUnits.reserve( tabPositions.size() ); + for ( const QgsTextFormat::Tab &tab : tabPositions ) + { + tabStopDistancesPainterUnits.append( + format.tabStopDistanceUnit() == Qgis::RenderUnit::Percentage + ? tab.position() * baseFont.pixelSize() + : context.convertToPainterUnits( tab.position(), format.tabStopDistanceUnit(), format.tabStopDistanceMapUnitScale() ) + ); + } + + // create label info! const double mapScale = xform->mapUnitsPerPixel(); QStringList graphemes; @@ -83,6 +100,7 @@ QgsPrecalculatedTextMetrics QgsTextLabelFeature::calculateTextMetrics( const Qgs QFont previousNonSuperSubScriptFont; + double currentWidth = 0; for ( int i = 0; i < graphemes.count(); i++ ) { // reconstruct how Qt creates word spacing, then adjust per individual stored character @@ -137,6 +155,35 @@ QgsPrecalculatedTextMetrics QgsTextLabelFeature::calculateTextMetrics( const Qgs characterDescent = graphemeFontMetrics.descent(); characterHeight = graphemeFontMetrics.height(); } + else if ( graphemes[i] == '\t' ) + { + double nextTabStop = 0; + if ( !tabStopDistancesPainterUnits.empty() ) + { + // if we don't find a tab stop before the current length of line, we just ignore the tab character entirely + nextTabStop = currentWidth; + for ( const double tabStop : std::as_const( tabStopDistancesPainterUnits ) ) + { + if ( tabStop >= currentWidth ) + { + nextTabStop = tabStop; + break; + } + } + } + else + { + nextTabStop = ( std::floor( currentWidth / tabStopDistancePainterUnits ) + 1 ) * tabStopDistancePainterUnits; + } + + const double thisTabWidth = nextTabStop - currentWidth; + + graphemeFirstCharHorizontalAdvance = thisTabWidth; + graphemeFirstCharHorizontalAdvanceWithLetterSpacing = thisTabWidth; + graphemeHorizontalAdvance = thisTabWidth; + characterDescent = fontMetrics.descent(); + characterHeight = fontMetrics.height(); + } else { graphemeFirstCharHorizontalAdvance = fontMetrics.horizontalAdvance( QString( graphemes[i].at( 0 ) ) ); @@ -167,6 +214,8 @@ QgsPrecalculatedTextMetrics QgsTextLabelFeature::calculateTextMetrics( const Qgs characterWidths[i] = mapScale * charWidth; characterHeights[i] = mapScale * characterHeight; characterDescents[i] = mapScale * characterDescent; + + currentWidth += charWidth; } QgsPrecalculatedTextMetrics res( graphemes, std::move( characterWidths ), std::move( characterHeights ), std::move( characterDescents ) ); diff --git a/src/core/labeling/qgstextlabelfeature.h b/src/core/labeling/qgstextlabelfeature.h index 3a813d8ed64b..e5fb5ffab4dd 100644 --- a/src/core/labeling/qgstextlabelfeature.h +++ b/src/core/labeling/qgstextlabelfeature.h @@ -98,7 +98,7 @@ class CORE_EXPORT QgsTextLabelFeature : public QgsLabelFeature * * \since QGIS 3.20 */ - static QgsPrecalculatedTextMetrics calculateTextMetrics( const QgsMapToPixel *xform, const QgsRenderContext &context, const QFont &baseFont, const QFontMetricsF &fontMetrics, double letterSpacing, + static QgsPrecalculatedTextMetrics calculateTextMetrics( const QgsMapToPixel *xform, const QgsRenderContext &context, const QgsTextFormat &format, const QFont &baseFont, const QFontMetricsF &fontMetrics, double letterSpacing, double wordSpacing, const QString &text = QString(), QgsTextDocument *document = nullptr, QgsTextDocumentMetrics *metrics = nullptr ); /** diff --git a/tests/src/core/testqgslabelingengine.cpp b/tests/src/core/testqgslabelingengine.cpp index 885a4b561df2..1260d3d34f37 100644 --- a/tests/src/core/testqgslabelingengine.cpp +++ b/tests/src/core/testqgslabelingengine.cpp @@ -75,6 +75,8 @@ class TestQgsLabelingEngine : public QgsTest void testCurvedPerimeterLabelsHtmlFormatting(); void testCurvedLabelsHtmlSuperSubscript(); void testCurvedLabelsHtmlWordSpacing(); + void testCurvedLabelsTabs(); + void testCurvedLabelsTabPositions(); void testPointLabelTabs(); void testPointLabelTabsHtml(); void testPointLabelHtmlFormatting(); @@ -2071,6 +2073,120 @@ void TestQgsLabelingEngine::testCurvedLabelsHtmlWordSpacing() QVERIFY( imageCheck( QStringLiteral( "curved_html_wordspacing" ), img, 20 ) ); } +void TestQgsLabelingEngine::testCurvedLabelsTabs() +{ + // test curved label rendering with tab characters + QgsPalLayerSettings settings; + setDefaultLabelParams( settings ); + + QgsTextFormat format = settings.format(); + format.setSize( 30 ); + format.setColor( QColor( 0, 0, 0 ) ); + format.setTabStopDistance( 28 ); + format.setTabStopDistanceUnit( Qgis::RenderUnit::Millimeters ); + settings.setFormat( format ); + + settings.fieldName = QStringLiteral( "'test of\ttab\ttext'" ); + settings.isExpression = true; + settings.placement = Qgis::LabelPlacement::Curved; + settings.labelPerPart = false; + settings.lineSettings().setLineAnchorPercent( 0.5 ); + settings.lineSettings().setAnchorType( QgsLabelLineSettings::AnchorType::Strict ); + settings.lineSettings().setPlacementFlags( Qgis::LabelLinePlacementFlag::AboveLine | Qgis::LabelLinePlacementFlag::MapOrientation ); + settings.lineSettings().setAnchorTextPoint( QgsLabelLineSettings::AnchorTextPoint::CenterOfText ); + + std::unique_ptr< QgsVectorLayer> vl2( new QgsVectorLayer( QStringLiteral( "LineString?crs=epsg:3946&field=id:integer" ), QStringLiteral( "vl" ), QStringLiteral( "memory" ) ) ); + vl2->setRenderer( new QgsNullSymbolRenderer() ); + + QgsFeature f; + f.setAttributes( QgsAttributes() << 1 ); + f.setGeometry( QgsGeometry::fromWkt( QStringLiteral( "LineString (190000 5000010, 190100 5000000, 190200 5000000)" ) ) ); + QVERIFY( vl2->dataProvider()->addFeature( f ) ); + + vl2->setLabeling( new QgsVectorLayerSimpleLabeling( settings ) ); // TODO: this should not be necessary! + vl2->setLabelsEnabled( true ); + + // make a fake render context + const QSize size( 640, 480 ); + QgsMapSettings mapSettings; + mapSettings.setLabelingEngineSettings( createLabelEngineSettings() ); + mapSettings.setDestinationCrs( vl2->crs() ); + + mapSettings.setOutputSize( size ); + mapSettings.setExtent( f.geometry().boundingBox() ); + mapSettings.setLayers( QList() << vl2.get() ); + mapSettings.setOutputDpi( 96 ); + + QgsLabelingEngineSettings engineSettings = mapSettings.labelingEngineSettings(); + engineSettings.setFlag( Qgis::LabelingFlag::UsePartialCandidates, false ); + engineSettings.setFlag( Qgis::LabelingFlag::DrawCandidates, true ); + mapSettings.setLabelingEngineSettings( engineSettings ); + + QgsMapRendererSequentialJob job( mapSettings ); + job.start(); + job.waitForFinished(); + + QImage img = job.renderedImage(); + QVERIFY( imageCheck( QStringLiteral( "curved_tabs" ), img, 20 ) ); +} + +void TestQgsLabelingEngine::testCurvedLabelsTabPositions() +{ + // test curved label rendering with tab characters + QgsPalLayerSettings settings; + setDefaultLabelParams( settings ); + + QgsTextFormat format = settings.format(); + format.setSize( 30 ); + format.setColor( QColor( 0, 0, 0 ) ); + format.setTabPositions( {QgsTextFormat::Tab( 55 ), QgsTextFormat::Tab( 82 )} ); + format.setTabStopDistanceUnit( Qgis::RenderUnit::Millimeters ); + settings.setFormat( format ); + + settings.fieldName = QStringLiteral( "'test of\ttab\ttext'" ); + settings.isExpression = true; + settings.placement = Qgis::LabelPlacement::Curved; + settings.labelPerPart = false; + settings.lineSettings().setLineAnchorPercent( 0.5 ); + settings.lineSettings().setAnchorType( QgsLabelLineSettings::AnchorType::Strict ); + settings.lineSettings().setPlacementFlags( Qgis::LabelLinePlacementFlag::AboveLine | Qgis::LabelLinePlacementFlag::MapOrientation ); + settings.lineSettings().setAnchorTextPoint( QgsLabelLineSettings::AnchorTextPoint::CenterOfText ); + + std::unique_ptr< QgsVectorLayer> vl2( new QgsVectorLayer( QStringLiteral( "LineString?crs=epsg:3946&field=id:integer" ), QStringLiteral( "vl" ), QStringLiteral( "memory" ) ) ); + vl2->setRenderer( new QgsNullSymbolRenderer() ); + + QgsFeature f; + f.setAttributes( QgsAttributes() << 1 ); + f.setGeometry( QgsGeometry::fromWkt( QStringLiteral( "LineString (190000 5000010, 190100 5000000, 190200 5000000)" ) ) ); + QVERIFY( vl2->dataProvider()->addFeature( f ) ); + + vl2->setLabeling( new QgsVectorLayerSimpleLabeling( settings ) ); // TODO: this should not be necessary! + vl2->setLabelsEnabled( true ); + + // make a fake render context + const QSize size( 640, 480 ); + QgsMapSettings mapSettings; + mapSettings.setLabelingEngineSettings( createLabelEngineSettings() ); + mapSettings.setDestinationCrs( vl2->crs() ); + + mapSettings.setOutputSize( size ); + mapSettings.setExtent( f.geometry().boundingBox() ); + mapSettings.setLayers( QList() << vl2.get() ); + mapSettings.setOutputDpi( 96 ); + + QgsLabelingEngineSettings engineSettings = mapSettings.labelingEngineSettings(); + engineSettings.setFlag( Qgis::LabelingFlag::UsePartialCandidates, false ); + engineSettings.setFlag( Qgis::LabelingFlag::DrawCandidates, true ); + mapSettings.setLabelingEngineSettings( engineSettings ); + + QgsMapRendererSequentialJob job( mapSettings ); + job.start(); + job.waitForFinished(); + + QImage img = job.renderedImage(); + QVERIFY( imageCheck( QStringLiteral( "curved_tab_positions" ), img, 20 ) ); +} + void TestQgsLabelingEngine::testCurvedLabelsHtmlFormatting() { // test line label rendering with HTML formatting diff --git a/tests/testdata/control_images/labelingengine/expected_curved_tab_positions/expected_curved_tab_positions.png b/tests/testdata/control_images/labelingengine/expected_curved_tab_positions/expected_curved_tab_positions.png new file mode 100644 index 0000000000000000000000000000000000000000..6f0be0fe438afc2320ec346c7212384f337109cf GIT binary patch literal 12437 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 F{|8i}OzHpt literal 0 HcmV?d00001 From 7086b625bcae4784ebe92ca6ec4de85c2c423c59 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Tue, 29 Oct 2024 14:18:52 +1000 Subject: [PATCH 5/7] Add missing since --- .../gui/auto_generated/labeling/qgstabpositionwidget.sip.in | 2 ++ python/gui/auto_generated/labeling/qgstabpositionwidget.sip.in | 2 ++ src/gui/labeling/qgstabpositionwidget.h | 1 + 3 files changed, 5 insertions(+) diff --git a/python/PyQt6/gui/auto_generated/labeling/qgstabpositionwidget.sip.in b/python/PyQt6/gui/auto_generated/labeling/qgstabpositionwidget.sip.in index 0c9f7f8473c1..d44fdbd15fe2 100644 --- a/python/PyQt6/gui/auto_generated/labeling/qgstabpositionwidget.sip.in +++ b/python/PyQt6/gui/auto_generated/labeling/qgstabpositionwidget.sip.in @@ -55,6 +55,8 @@ class QgsTabPositionDialog : QDialog { %Docstring(signature="appended") A dialog to enter a custom dash space pattern for lines + +.. versionadded:: 3.42 %End %TypeHeaderCode diff --git a/python/gui/auto_generated/labeling/qgstabpositionwidget.sip.in b/python/gui/auto_generated/labeling/qgstabpositionwidget.sip.in index 0c9f7f8473c1..d44fdbd15fe2 100644 --- a/python/gui/auto_generated/labeling/qgstabpositionwidget.sip.in +++ b/python/gui/auto_generated/labeling/qgstabpositionwidget.sip.in @@ -55,6 +55,8 @@ class QgsTabPositionDialog : QDialog { %Docstring(signature="appended") A dialog to enter a custom dash space pattern for lines + +.. versionadded:: 3.42 %End %TypeHeaderCode diff --git a/src/gui/labeling/qgstabpositionwidget.h b/src/gui/labeling/qgstabpositionwidget.h index 863bb73e551e..c09a3683ce57 100644 --- a/src/gui/labeling/qgstabpositionwidget.h +++ b/src/gui/labeling/qgstabpositionwidget.h @@ -70,6 +70,7 @@ class GUI_EXPORT QgsTabPositionWidget: public QgsPanelWidget, private Ui::QgsTab /** * \ingroup gui * \brief A dialog to enter a custom dash space pattern for lines + * \since QGIS 3.42 */ class GUI_EXPORT QgsTabPositionDialog : public QDialog { From 7681f36f120929f13655182d4966c318b7b63999 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Tue, 29 Oct 2024 19:26:19 +1000 Subject: [PATCH 6/7] Test masks --- tests/src/python/test_qgstextrenderer.py | 11 +++++++++++ .../text_tab_positions_fixed_size_mask.png | Bin 6574 -> 6798 bytes ...ext_tab_positions_fixed_size_html_mask.png | Bin 6574 -> 6792 bytes ...ext_tab_positions_fixed_size_long_text.png | Bin 0 -> 6838 bytes ...ab_positions_fixed_size_long_text_mask.png | Bin 0 -> 6780 bytes ...ab_positions_fixed_size_more_tabs_mask.png | Bin 7375 -> 7585 bytes .../text_tab_positions_percentage_mask.png | Bin 6632 -> 6845 bytes ...ext_tab_positions_percentage_html_mask.png | Bin 6750 -> 6938 bytes 8 files changed, 11 insertions(+) create mode 100644 tests/testdata/control_images/text_renderer/text_tab_positions_fixed_size_long_text/text_tab_positions_fixed_size_long_text.png create mode 100644 tests/testdata/control_images/text_renderer/text_tab_positions_fixed_size_long_text/text_tab_positions_fixed_size_long_text_mask.png diff --git a/tests/src/python/test_qgstextrenderer.py b/tests/src/python/test_qgstextrenderer.py index 81ea4c4ebb51..6d774ef0f98a 100644 --- a/tests/src/python/test_qgstextrenderer.py +++ b/tests/src/python/test_qgstextrenderer.py @@ -2399,6 +2399,17 @@ def testDrawTabPositionsFixedSizeMoreTabsThanPositions(self): 'text_tab_positions_fixed_size_more_tabs', text=['with\tmany\ttabs', 'a\tb\tc\td\te'])) + def testDrawTabPositionsFixedSizeLongText(self): + format = QgsTextFormat() + format.setFont(getTestFont('bold')) + format.setSize(20) + format.setSizeUnit(QgsUnitTypes.RenderUnit.RenderPoints) + format.setTabPositions([QgsTextFormat.Tab(15), QgsTextFormat.Tab(50)]) + format.setTabStopDistanceUnit(Qgis.RenderUnit.Millimeters) + self.assertTrue(self.checkRender(format, + 'text_tab_positions_fixed_size_long_text', + text=['with long\ttext\t2', 'a\tb\tc'])) + def testHtmlFormatting(self): format = QgsTextFormat() format.setFont(getTestFont('bold')) diff --git a/tests/testdata/control_images/text_renderer/text_tab_positions_fixed_size/text_tab_positions_fixed_size_mask.png b/tests/testdata/control_images/text_renderer/text_tab_positions_fixed_size/text_tab_positions_fixed_size_mask.png index aed696fe4edd5b45bc5fd7e3832aabd9fd9bdcf1..ed1ce8c5c6644e0881d713e46b09bac24f2ab68a 100644 GIT binary patch literal 6798 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 Kue{>%$NvEPj*UP7 literal 6574 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 diff --git a/tests/testdata/control_images/text_renderer/text_tab_positions_fixed_size_html/text_tab_positions_fixed_size_html_mask.png b/tests/testdata/control_images/text_renderer/text_tab_positions_fixed_size_html/text_tab_positions_fixed_size_html_mask.png index 91746640ac60ae28e553d2f16bac14666f4dabf7..7cd98d7211f7bed3c26926f2ccb907ec6cc07f60 100644 GIT binary patch literal 6792 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 ZR6^O+j^}+ri literal 6574 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 literal 0 HcmV?d00001 diff --git a/tests/testdata/control_images/text_renderer/text_tab_positions_fixed_size_long_text/text_tab_positions_fixed_size_long_text_mask.png b/tests/testdata/control_images/text_renderer/text_tab_positions_fixed_size_long_text/text_tab_positions_fixed_size_long_text_mask.png new file mode 100644 index 0000000000000000000000000000000000000000..7b79ce64a512930c5727af0eb4ba09420809d86c GIT binary patch literal 6780 zcmeI0iBr=1yT`F>WgXLYEfsS~&D=9J_gu=d+%j`F6)P9qcNFTBrpJWL)YOzZxgNzW zL2-e~l+e^VrlhE3xTJ_2Bs9e3ew?{?<~R30xHE?t2If2PU7qLjyx#BkC)3H{;=aAd z_d+0$eV6QPTp$pU$J=kQUEs)FvcX62u_w~bBL)JI^xuAqR9RF8Lm%SO?=Lt zu#b?L6EtuUMyA-4E@jFW2(VH75ii2^mZ-%Mwf)MLdk^1fKlRBcORG(7O>R>1Gn{NG zWh-U-`pynzT=rp|C$rz)$Gg}zYF_^=xZ+~kkKz_-JUsKXivGmz6irh4jqJYpDXyaY zfcU-xjiQjd|Fs;8*a?BC9ug6ORPEmnxx4q@FaM8=kl5b2<40+j!Rd5=eFv#5TMs`! z)Kp(TOz%5)ozqG6_X#(#wzdxNaAU?2z4NVJo)DyLe%N$`h)#RKlaf6C8V+GL(-20I z5A*W!l!)W;ia8nUa~4~TCFt$|QBl=*-@d)DQ9N+!Xx2e7(f9gSsdGG+YF8VZGEdwe zK^{$wjh~;r7sia` z-s3}Q)5Q^_#(tBS*+=7K6q%hg@cf)oO-&7&N6hRFP*iNf4Jxb{QFVDf{{;8;;X}sZy=gtmX;yFfq;r`S6&2Iut&RHH+FGN{ zmEjwH=b)jV@3@wlA#QnTNsC`LE(ssUDk|m&W%^6I)C`xo)le4}{!&s?Qc`kJ&kN%n z%zTg{m?=ODw|cT=2vWbmg{C6izjIqPw6xlg$=K3T0*;(vr&sutf|1WMMLE>saKzFU zCpEhh`}bogL#+)fMz(`5S+vbw)`T{Ds;lt`pmp@UM)8Kr%RoxXi6zVYl?tijdS zY=*WUX`H*ZYllc^N=nLL-&K{|I-0djEs10vnwaRGiYT!_O~xA^%yb8pZ1h$>*r?^* z`mI&YQ?*N{Hr99|K2?&pR#*}2(~(a;7}%mQxHw;m`gH+Vghr?BSGja8Y_>@hSN=c+zJg1b-}q|V#5sg zi`l8?J<%qwM>DP}X!bpsnF-pn>;6IUU8|jo4t0LzF2P~8Lt(M8u22hP7~|bLZ|_)9y%Gl#1FyU}XHW>R$Y4Q1-BCiUL2+^MlfIcUCpDw==pG6M-a=+@BA!d^ zy8mQw4zB(ErIX#7P$&~|5L8D)TidYoj3C7~pz#?S>FeiL;HrL-3TtZYVl6J#dILNi zJ$e)din=t>PID)b$$nNZodU0|;PGdvGtn*@N97b`jvb3KBvu}Gti`{$GreIUm+mwIaJS;UHPUzF zKxn!E_cU&E!aLzdcW0-RDkYH^M2UmSy1Cv6HrtUNJMQ38G2{W z4jaKe@&OvgN)ZW5<2k5J9`<4EzOg<~^qS!?Y~8!5hW7!w@yhStf_1BV_1cx8Fw?4q zVs(O^co+A3?6cx3=u-+aDlFO^1LGyfw^SVt}H znRI9u#s&qI!!5xwHBJ6$N9-xV4SoIk?BmCeUnl=Ld2kO`kb>q|C5wp6JM*Q!&L;GR zORl+8!|3W=`d4YV`ee3GZIH)B(oXvlI(tM*J-p9A-lO%J(1#pNT$C;X8vh+!e~#6i zP&#qTBR{YU5SO{Ot2cXaE@tC!5fwI;o*WhByleMv6XWpV^MGi_jvs$gPg?6WuqbiJ z@|QM4Z1LpmFn_t#prz_Y{HxZoe0W)<{?h0fU)Zl+cE>}0YH!rO8ad^?;epICRy%_q zx*?pMtx(RrM2RjTsg=g=PO2rES?suH892n+P-!uAg<>w&c@q_}sVjx7Ex|PP&@m{~ z{L{u~YM|=jy_WzAD;=?8vyu`Lm#bYDMia$E=HWQUs%YYP;x~k8X%D%$;B?+)wBw1Q zgimoms8T0=owW+&9H%EHd?>f@Q;}b}OG7&1v?|$MEmA=JrG_m z)-s9g4sec7NC-+z?PNb4oa+X^zJFkn7;|wlb=8MHb6f8iT~8kQ>m|U%L|p2Xp;%Me zityC^JNoLM?|k8_k-WUT%uvlD=?%&5tV3mjw6dg&Z`Y8D;oLhLimNZi2@p?j?=ac$ za%ht;vY?qjP$|69Ltw9i*?E+meHMMzRetl%1PKX)O@BZibE|fhwRVe%iP6*1Igw*m z)`)kiWUi@}fO|zEeO;=bvb%|!0rX`0aA5A`a#&m(KFSzKhT7qS*g;h=xBNz9U0nqX z)U0V==Ek?(7LQRIEI++-=T2dfYr%n=Qhq541zJAQDe_It&B{dYf%s>6gBDxffXn5s z>X|eaT>RoNHaDCVSX2=f5n-=!G-g8JT55r$!^NV<+en(axk8w9`jJciG10q4Ae0LzoA^R~dF4Adk#3#cOs`Y~QVdM!Uv%6M zYc5_8&Z4!UdV|L$VJRu&$@Ap}E8es1jE!(6Bi+AsNhwe|J+J>34gj!xC>TKV=Acj^ z@AA2oJ;&wb;IgHv#r?)hm0WHVWhU7a%EHrp=`kDizMUbquRiw#w{$&?)jR8V!B7?S z=?qCu_SD+88_W4Y)Rh&2xVX5{thR>6hvnrape`K2{8t_E!spMM z*@vci0vHTd76tj3mbMdMhA>q%KOcNdP7c2>VkG3OuJ|iUNgeE4Y67`-Wp{MESHLJ{f!YCZFQooIkyk);~)&eQ<(EV5Ptw&+LLDW z{%j=jmzL_vhUR;mdcYh8GVTva z?>~^!!a#cYCe0j4TKif3tl|`l#X>A4fVl|@3bJh)t#!|z`E_pQ7WlvJBUgs<$_UOY zdUeT%zVT(vCnMv-{oRtVkdWS^MUv*fEbIKpT3|Z-11C*59ZJ`(U27tdw1z|C7#!UN zVSI^e4-cdG!x>3tL_MlngEsYd_|~%=ToKvW7U-=pb^{kD-t`!!7mT<#-uB9^vVh+9 zoG#&4VyAo%eSZMIIfx)w!jW56Sy((289$slrMY`uANm+AA<8SXG< z56)ww=x=h+5?Ca}H=8aUfO;-&P52F{-`Zyk^2@J{*<3yRCb})d z9oXF_KFe=w?6{0fjG3cW!Rsef7>!mk|D&SVFv628=#TJ5u{x5B6P9E#Y0ziV?~JE_ zt?O8wM0{Tr%8^sz{|24y#cr+75iCxc7#gYqroWr|r?FlJJt>L$WetRL89Y;tk_2|Y zxIe!kOG>-<{rd-C?ccMs1f45$sm`DFH(mT_1c<;_6PUmV^R}8oY7#0?%kU6~o(r&D=CQ8ufC(C1`@cMf&p}=GaehxXd z6G|f&)+i@b=3@B^*I6JI<@-z5q2vJ%wu6SRK{0zRIl-8?&p$JRFJx>@#&-fYs|lpa zOGQ?~Yd*G(MY}iuPsR4bp_2f-n~qFLBNEP1hd6G zz{F_-I5%~4jF+G7_Ky+T2-aBjee#?C?53=P_(14-ZsaRkc5&wp5y9vnQl%oecMglv zl7yN*1{tN=tp+}Lt=bhGCnxEpBONPOn$aCVxch;p85e2%(0N%8Sg zjZMG%^96999FnjO4s1Mjye-Zi{kA;f6)|d_BYOX>R_(;+U_((UF;QTHbE{pd zjnEA`XU-_ysPoPt5{Y#;eqoq4AGO~etX}UQPU=#*yLI=3! zTIz1bz2{oEu6iI8OH1K&>(%$nDB8=bvf4}c4!!Wi^?v!{%KT}s-KjhuJ4Ub_33dY9 z1vv;rC{+MDU!)*NKy^eaCU7S4nhCtwt4Tk9IMYTBJ&Z_AozR8xyQTa-0PH=&V6rI8 z14H^j0RdJ>Zk8X&)TUgMfzn+S8SCWmdeKE-uv6yS=@AdGt0mGq{N0 zcoK@4doL@amP<7vk#)tbFPA?7>l*0S)4aSRNn0DMa2i!doR}&kZ12qc7MlGEY^5jx zLIIB(hy*iErCt&YB#@NA_Q&$3kYf*wn$^p1znalZ8o$K(+2S=*C?lcr4fq*lAhsen zK6S#RWISGZeRGu@(H+oeHn>{jxA8~diITsm`d3+tqp#&O`x+aP1Gn=CJUsj|=&g5q zd_T7Z*sKVWpaN_Yl(Z=g&GM4K))4SB^U{m~nBFp-vFSF66tb@+baJ<@*IZ9pSuFG> zM%&i;v;*-J8kmDT?2BOI5AlM9`e#ZDM9tJ>CHmO(mU4hHQ>YXuUudX3NPVg^(H`4b zexAdL(1`HJ@8FMk`S{E^01uqR%r*?;&CbqFGcgwVH+9q)!pT)-pbH?rgIFLVBeOhU z!dWCLopivg1)kV1mO*3TuL5F5b0#~l@el1%J+i&Cs`Gy+{684~4=(?Exf85E1zH1~x!0VE&gH4^)&ENkUU3P9> literal 0 HcmV?d00001 diff --git a/tests/testdata/control_images/text_renderer/text_tab_positions_fixed_size_more_tabs/text_tab_positions_fixed_size_more_tabs_mask.png b/tests/testdata/control_images/text_renderer/text_tab_positions_fixed_size_more_tabs/text_tab_positions_fixed_size_more_tabs_mask.png index e1ba3f869a2f13256a6ffd98d28939d819be488c..2a740fff381f5d3b30d41d3874d15e0cbac79fd6 100644 GIT binary patch literal 7585 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 literal 7375 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 UAsS3Ya1ijbvam-G&R)L#Ki<*{n*aa+ diff --git a/tests/testdata/control_images/text_renderer/text_tab_positions_percentage_html/text_tab_positions_percentage_html_mask.png b/tests/testdata/control_images/text_renderer/text_tab_positions_percentage_html/text_tab_positions_percentage_html_mask.png index d36ce84cce952840839abe5200ab623bde2eafa6..fec70fd5b7e76c13bfc15cb9c7b82c81cf150e81 100644 GIT binary patch literal 6938 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 aBFSq-uZ3gYyYqn`tM&st_uAwrl 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 From e1f060f3036ac0a99d6fb85aea20e176440b129c Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Thu, 31 Oct 2024 10:04:06 +1000 Subject: [PATCH 7/7] Show units in tab position widget --- .../labeling/qgstabpositionwidget.sip.in | 10 ++++++++++ .../labeling/qgstabpositionwidget.sip.in | 10 ++++++++++ src/gui/labeling/qgstabpositionwidget.cpp | 15 +++++++++++++++ src/gui/labeling/qgstabpositionwidget.h | 10 ++++++++++ src/gui/qgstextformatwidget.cpp | 2 ++ src/ui/qgstabpositionwidgetbase.ui | 4 ++-- 6 files changed, 49 insertions(+), 2 deletions(-) diff --git a/python/PyQt6/gui/auto_generated/labeling/qgstabpositionwidget.sip.in b/python/PyQt6/gui/auto_generated/labeling/qgstabpositionwidget.sip.in index d44fdbd15fe2..028b6dc4d762 100644 --- a/python/PyQt6/gui/auto_generated/labeling/qgstabpositionwidget.sip.in +++ b/python/PyQt6/gui/auto_generated/labeling/qgstabpositionwidget.sip.in @@ -40,6 +40,11 @@ Sets the tab ``positions`` to show in the widget. Returns the tab positions defined in the widget. .. seealso:: :py:func:`setPositions` +%End + + void setUnit( Qgis::RenderUnit unit ); +%Docstring +Sets the unit type used for the tab positions (used to update interface labels). %End signals: @@ -81,6 +86,11 @@ Sets the tab ``positions`` to show in the dialog. Returns the tab positions defined in the dialog. .. seealso:: :py:func:`setPositions` +%End + + void setUnit( Qgis::RenderUnit unit ); +%Docstring +Sets the unit type used for the tab positions (used to update interface labels). %End }; diff --git a/python/gui/auto_generated/labeling/qgstabpositionwidget.sip.in b/python/gui/auto_generated/labeling/qgstabpositionwidget.sip.in index d44fdbd15fe2..028b6dc4d762 100644 --- a/python/gui/auto_generated/labeling/qgstabpositionwidget.sip.in +++ b/python/gui/auto_generated/labeling/qgstabpositionwidget.sip.in @@ -40,6 +40,11 @@ Sets the tab ``positions`` to show in the widget. Returns the tab positions defined in the widget. .. seealso:: :py:func:`setPositions` +%End + + void setUnit( Qgis::RenderUnit unit ); +%Docstring +Sets the unit type used for the tab positions (used to update interface labels). %End signals: @@ -81,6 +86,11 @@ Sets the tab ``positions`` to show in the dialog. Returns the tab positions defined in the dialog. .. seealso:: :py:func:`setPositions` +%End + + void setUnit( Qgis::RenderUnit unit ); +%Docstring +Sets the unit type used for the tab positions (used to update interface labels). %End }; diff --git a/src/gui/labeling/qgstabpositionwidget.cpp b/src/gui/labeling/qgstabpositionwidget.cpp index ef3ef14592ad..647113eea403 100644 --- a/src/gui/labeling/qgstabpositionwidget.cpp +++ b/src/gui/labeling/qgstabpositionwidget.cpp @@ -14,8 +14,10 @@ ***************************************************************************/ #include "qgstabpositionwidget.h" +#include "moc_qgstabpositionwidget.cpp" #include "qgsapplication.h" #include "qgsdoublevalidator.h" +#include "qgsunittypes.h" #include @@ -27,6 +29,8 @@ QgsTabPositionWidget::QgsTabPositionWidget( QWidget *parent ) mAddButton->setIcon( QgsApplication::getThemeIcon( "symbologyAdd.svg" ) ); mRemoveButton->setIcon( QgsApplication::getThemeIcon( "symbologyRemove.svg" ) ); + setUnit( Qgis::RenderUnit::Millimeters ); + connect( mAddButton, &QPushButton::clicked, this, &QgsTabPositionWidget::mAddButton_clicked ); connect( mRemoveButton, &QPushButton::clicked, this, &QgsTabPositionWidget::mRemoveButton_clicked ); connect( mTabPositionTreeWidget, &QTreeWidget::itemChanged, this, &QgsTabPositionWidget::emitPositionsChanged ); @@ -66,6 +70,12 @@ QList QgsTabPositionWidget::positions() const return result; } +void QgsTabPositionWidget::setUnit( Qgis::RenderUnit unit ) +{ + QTreeWidgetItem *headerItem = mTabPositionTreeWidget->headerItem(); + headerItem->setText( 0, QStringLiteral( "%1 (%2)" ).arg( tr( "Position" ), QgsUnitTypes::toAbbreviatedString( unit ) ) ); +} + void QgsTabPositionWidget::mAddButton_clicked() { const QList< QgsTextFormat::Tab > currentPositions = positions(); @@ -121,3 +131,8 @@ QList QgsTabPositionDialog::positions() const { return mWidget->positions(); } + +void QgsTabPositionDialog::setUnit( Qgis::RenderUnit unit ) +{ + mWidget->setUnit( unit ); +} diff --git a/src/gui/labeling/qgstabpositionwidget.h b/src/gui/labeling/qgstabpositionwidget.h index c09a3683ce57..b26ec299eb6e 100644 --- a/src/gui/labeling/qgstabpositionwidget.h +++ b/src/gui/labeling/qgstabpositionwidget.h @@ -52,6 +52,11 @@ class GUI_EXPORT QgsTabPositionWidget: public QgsPanelWidget, private Ui::QgsTab */ QList< QgsTextFormat::Tab > positions() const; + /** + * Sets the unit type used for the tab positions (used to update interface labels). + */ + void setUnit( Qgis::RenderUnit unit ); + signals: /** @@ -94,6 +99,11 @@ class GUI_EXPORT QgsTabPositionDialog : public QDialog */ QList< QgsTextFormat::Tab > positions() const; + /** + * Sets the unit type used for the tab positions (used to update interface labels). + */ + void setUnit( Qgis::RenderUnit unit ); + private: QgsTabPositionWidget *mWidget = nullptr; diff --git a/src/gui/qgstextformatwidget.cpp b/src/gui/qgstextformatwidget.cpp index bf14c1b0bcc8..8796e86354c5 100644 --- a/src/gui/qgstextformatwidget.cpp +++ b/src/gui/qgstextformatwidget.cpp @@ -2176,6 +2176,7 @@ void QgsTextFormatWidget::configureTabStops() QgsTabPositionWidget *widget = new QgsTabPositionWidget( panel ); widget->setPanelTitle( tr( "Tab Positions" ) ); widget->setPositions( mTabPositions ); + widget->setUnit( mTabDistanceUnitWidget->unit() ); connect( widget, &QgsTabPositionWidget::positionsChanged, this, [ = ]( const QList< QgsTextFormat::Tab > &positions ) { mTabPositions = positions; @@ -2188,6 +2189,7 @@ void QgsTextFormatWidget::configureTabStops() { QgsTabPositionDialog dlg( this ); dlg.setPositions( mTabPositions ); + dlg.setUnit( mTabDistanceUnitWidget->unit() ); if ( dlg.exec() == QDialog::Accepted ) { mTabPositions = dlg.positions(); diff --git a/src/ui/qgstabpositionwidgetbase.ui b/src/ui/qgstabpositionwidgetbase.ui index e158f3749c20..601ecaff642f 100644 --- a/src/ui/qgstabpositionwidgetbase.ui +++ b/src/ui/qgstabpositionwidgetbase.ui @@ -29,14 +29,14 @@ - true + false 1 - Dash + Position