From d47f09f34c4edc9b9bbb0e67e43f73f63f201adf Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Wed, 17 Jul 2024 09:48:17 +1000 Subject: [PATCH] [FEATURE] Text inside rectangle annotation item This adds a new annotation item type for rendering paragraphs of text inside a rectangle Options are present for: - Text format - Horizontal and vertical alignment - Margins - Background and frame symbol --- images/images.qrc | 1 + .../themes/default/mActionTextInsideRect.svg | 1 + .../annotations/qgsannotationitem.sip.in | 4 + .../qgsannotationrectangletextitem.sip.in | 244 +++++++++++ python/PyQt6/core/core_auto.sip | 1 + .../annotations/qgsannotationitem.sip.in | 4 + .../qgsannotationrectangletextitem.sip.in | 244 +++++++++++ python/core/core_auto.sip | 1 + src/core/CMakeLists.txt | 2 + src/core/annotations/qgsannotationitem.h | 4 + .../annotations/qgsannotationitemregistry.cpp | 3 + .../qgsannotationrectangletextitem.cpp | 378 ++++++++++++++++++ .../qgsannotationrectangletextitem.h | 245 ++++++++++++ .../qgsannotationitemguiregistry.cpp | 13 + .../qgsannotationitemwidget_impl.cpp | 179 +++++++++ .../qgsannotationitemwidget_impl.h | 34 ++ .../qgscreateannotationitemmaptool_impl.cpp | 98 +++++ .../qgscreateannotationitemmaptool_impl.h | 26 +- .../qgsannotationrectangulartextwidgetbase.ui | 366 +++++++++++++++++ tests/src/python/CMakeLists.txt | 1 + .../python/test_qgsannotationrecttextitem.py | 370 +++++++++++++++++ .../expected_recttext_background_frame.png | Bin 0 -> 1041 bytes ...xpected_recttext_background_frame_mask.png | Bin 0 -> 892 bytes .../expected_recttext_render.png | Bin 0 -> 1072 bytes .../expected_recttext_render_mask.png | Bin 0 -> 828 bytes .../expected_recttext_render_align.png | Bin 0 -> 1105 bytes .../expected_recttext_render_align_mask.png | Bin 0 -> 852 bytes .../expected_recttext_render_transform.png | Bin 0 -> 1057 bytes ...xpected_recttext_render_transform_mask.png | Bin 0 -> 843 bytes 29 files changed, 2218 insertions(+), 1 deletion(-) create mode 100644 images/themes/default/mActionTextInsideRect.svg create mode 100644 python/PyQt6/core/auto_generated/annotations/qgsannotationrectangletextitem.sip.in create mode 100644 python/core/auto_generated/annotations/qgsannotationrectangletextitem.sip.in create mode 100644 src/core/annotations/qgsannotationrectangletextitem.cpp create mode 100644 src/core/annotations/qgsannotationrectangletextitem.h create mode 100644 src/ui/annotations/qgsannotationrectangulartextwidgetbase.ui create mode 100644 tests/src/python/test_qgsannotationrecttextitem.py create mode 100644 tests/testdata/control_images/annotation_layer/expected_recttext_background_frame/expected_recttext_background_frame.png create mode 100644 tests/testdata/control_images/annotation_layer/expected_recttext_background_frame/expected_recttext_background_frame_mask.png create mode 100644 tests/testdata/control_images/annotation_layer/expected_recttext_render/expected_recttext_render.png create mode 100644 tests/testdata/control_images/annotation_layer/expected_recttext_render/expected_recttext_render_mask.png create mode 100644 tests/testdata/control_images/annotation_layer/expected_recttext_render_align/expected_recttext_render_align.png create mode 100644 tests/testdata/control_images/annotation_layer/expected_recttext_render_align/expected_recttext_render_align_mask.png create mode 100644 tests/testdata/control_images/annotation_layer/expected_recttext_render_transform/expected_recttext_render_transform.png create mode 100644 tests/testdata/control_images/annotation_layer/expected_recttext_render_transform/expected_recttext_render_transform_mask.png diff --git a/images/images.qrc b/images/images.qrc index 6dc35903e4f9..d0efab54cfb4 100644 --- a/images/images.qrc +++ b/images/images.qrc @@ -1005,6 +1005,7 @@ themes/default/mIconSearchRegex.svg themes/default/mActionReplace.svg themes/default/mIconCloud.svg + themes/default/mActionTextInsideRect.svg qgis_tips/symbol_levels.png diff --git a/images/themes/default/mActionTextInsideRect.svg b/images/themes/default/mActionTextInsideRect.svg new file mode 100644 index 000000000000..42a8c375132e --- /dev/null +++ b/images/themes/default/mActionTextInsideRect.svg @@ -0,0 +1 @@ + diff --git a/python/PyQt6/core/auto_generated/annotations/qgsannotationitem.sip.in b/python/PyQt6/core/auto_generated/annotations/qgsannotationitem.sip.in index cc6ec1cd1393..61e5d670ab41 100644 --- a/python/PyQt6/core/auto_generated/annotations/qgsannotationitem.sip.in +++ b/python/PyQt6/core/auto_generated/annotations/qgsannotationitem.sip.in @@ -42,6 +42,10 @@ Abstract base class for annotation items which are drawn with :py:class:`QgsAnno { sipType = sipType_QgsAnnotationLineTextItem; } + else if ( sipCpp->type() == QLatin1String( "recttext" ) ) + { + sipType = sipType_QgsAnnotationRectangleTextItem; + } else if ( sipCpp->type() == QLatin1String( "picture" ) ) { sipType = sipType_QgsAnnotationPictureItem; diff --git a/python/PyQt6/core/auto_generated/annotations/qgsannotationrectangletextitem.sip.in b/python/PyQt6/core/auto_generated/annotations/qgsannotationrectangletextitem.sip.in new file mode 100644 index 000000000000..c0dea93da3e2 --- /dev/null +++ b/python/PyQt6/core/auto_generated/annotations/qgsannotationrectangletextitem.sip.in @@ -0,0 +1,244 @@ +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/annotations/qgsannotationrectangletextitem.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ + + + + +class QgsAnnotationRectangleTextItem : QgsAnnotationItem +{ +%Docstring(signature="appended") +An annotation item which renders paragraphs of text within a rectangle. + +.. versionadded:: 3.40 +%End + +%TypeHeaderCode +#include "qgsannotationrectangletextitem.h" +%End + public: + + QgsAnnotationRectangleTextItem( const QString &text, const QgsRectangle &bounds ); +%Docstring +Constructor for QgsAnnotationRectangleTextItem, containing the specified ``text`` +within the specified ``bounds`` rectangle. +%End + ~QgsAnnotationRectangleTextItem(); + + virtual QString type() const; + + virtual Qgis::AnnotationItemFlags flags() const; + + virtual void render( QgsRenderContext &context, QgsFeedback *feedback ); + + virtual bool writeXml( QDomElement &element, QDomDocument &document, const QgsReadWriteContext &context ) const; + + virtual QList< QgsAnnotationItemNode > nodesV2( const QgsAnnotationItemEditContext &context ) const; + + virtual Qgis::AnnotationItemEditOperationResult applyEditV2( QgsAbstractAnnotationItemEditOperation *operation, const QgsAnnotationItemEditContext &context ); + + virtual QgsAnnotationItemEditOperationTransientResults *transientEditResultsV2( QgsAbstractAnnotationItemEditOperation *operation, const QgsAnnotationItemEditContext &context ) /Factory/; + + + static QgsAnnotationRectangleTextItem *create() /Factory/; +%Docstring +Creates a new rectangle text annotation item. +%End + + virtual bool readXml( const QDomElement &element, const QgsReadWriteContext &context ); + + virtual QgsAnnotationRectangleTextItem *clone() const /Factory/; + + virtual QgsRectangle boundingBox() const; + + + QgsRectangle bounds() const; +%Docstring +Returns the bounds of the text. + +The coordinate reference system for the bounds will be the parent layer's :py:func:`QgsAnnotationLayer.crs()`. + +.. seealso:: :py:func:`setBounds` +%End + + void setBounds( const QgsRectangle &bounds ); +%Docstring +Sets the ``bounds`` of the text. + +The coordinate reference system for the bounds will be the parent layer's :py:func:`QgsAnnotationLayer.crs()`. + +.. seealso:: :py:func:`bounds` +%End + + QString text() const; +%Docstring +Returns the text rendered by the item. + +.. seealso:: :py:func:`setText` +%End + + void setText( const QString &text ); +%Docstring +Sets the ``text`` rendered by the item. + +.. seealso:: :py:func:`text` +%End + + QgsTextFormat format() const; +%Docstring +Returns the text format used to render the text. + +.. seealso:: :py:func:`setFormat` +%End + + void setFormat( const QgsTextFormat &format ); +%Docstring +Sets the text ``format`` used to render the text. + +.. seealso:: :py:func:`format` +%End + + Qt::Alignment alignment() const; +%Docstring +Returns the text's alignment relative to the :py:func:`~QgsAnnotationRectangleTextItem.bounds` rectangle. + +.. seealso:: :py:func:`setAlignment` +%End + + void setAlignment( Qt::Alignment alignment ); +%Docstring +Sets the text's ``alignment`` relative to the :py:func:`~QgsAnnotationRectangleTextItem.bounds` rectangle. + +.. seealso:: :py:func:`alignment` +%End + + bool backgroundEnabled() const; +%Docstring +Returns ``True`` if the item's background should be rendered. + +.. seealso:: :py:func:`setBackgroundEnabled` + +.. seealso:: :py:func:`backgroundSymbol` +%End + + void setBackgroundEnabled( bool enabled ); +%Docstring +Sets whether the item's background should be rendered. + +.. seealso:: :py:func:`backgroundEnabled` + +.. seealso:: :py:func:`setBackgroundSymbol` +%End + + const QgsFillSymbol *backgroundSymbol() const; +%Docstring +Returns the symbol used to render the item's background. + +.. seealso:: :py:func:`backgroundEnabled` + +.. seealso:: :py:func:`setBackgroundSymbol` +%End + + void setBackgroundSymbol( QgsFillSymbol *symbol /Transfer/ ); +%Docstring +Sets the ``symbol`` used to render the item's background. + +The item takes ownership of the symbol. + +.. seealso:: :py:func:`backgroundSymbol` + +.. seealso:: :py:func:`setBackgroundEnabled` +%End + + bool frameEnabled() const; +%Docstring +Returns ``True`` if the item's frame should be rendered. + +.. seealso:: :py:func:`setFrameEnabled` + +.. seealso:: :py:func:`frameSymbol` +%End + + void setFrameEnabled( bool enabled ); +%Docstring +Sets whether the item's frame should be rendered. + +.. seealso:: :py:func:`frameEnabled` + +.. seealso:: :py:func:`setFrameSymbol` +%End + + const QgsFillSymbol *frameSymbol() const; +%Docstring +Returns the symbol used to render the item's frame. + +.. seealso:: :py:func:`frameEnabled` + +.. seealso:: :py:func:`setFrameSymbol` +%End + + void setFrameSymbol( QgsFillSymbol *symbol /Transfer/ ); +%Docstring +Sets the ``symbol`` used to render the item's frame. + +The item takes ownership of the symbol. + +.. seealso:: :py:func:`frameSymbol` + +.. seealso:: :py:func:`setBackgroundEnabled` +%End + + const QgsMargins &margins() const; +%Docstring +Returns the margins between the outside of the item's frame and the interior text. + +Units are retrieved via :py:func:`~QgsAnnotationRectangleTextItem.marginsUnit` + +.. seealso:: :py:func:`setMargins` + +.. seealso:: :py:func:`marginsUnit` +%End + + void setMargins( const QgsMargins &margins ); +%Docstring +Sets the ``margins`` between the outside of the item's frame and the interior text. + +Units are set via :py:func:`~QgsAnnotationRectangleTextItem.setMarginsUnit` + +.. seealso:: :py:func:`margins` + +.. seealso:: :py:func:`setMarginsUnit` +%End + + void setMarginsUnit( Qgis::RenderUnit unit ); +%Docstring +Sets the ``unit`` for the margins between the item's frame and the interior text. + +.. seealso:: :py:func:`margins` + +.. seealso:: :py:func:`marginsUnit` +%End + + Qgis::RenderUnit marginsUnit() const; +%Docstring +Returns the units for the margins between the item's frame and the interior text. + +.. seealso:: :py:func:`setMarginsUnit` + +.. seealso:: :py:func:`margins` +%End + + private: + QgsAnnotationRectangleTextItem( const QgsAnnotationRectangleTextItem &other ); +}; +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/annotations/qgsannotationrectangletextitem.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ diff --git a/python/PyQt6/core/core_auto.sip b/python/PyQt6/core/core_auto.sip index d5ccc57a6a7d..5d5d1a31b989 100644 --- a/python/PyQt6/core/core_auto.sip +++ b/python/PyQt6/core/core_auto.sip @@ -235,6 +235,7 @@ %Include auto_generated/annotations/qgsannotationpictureitem.sip %Include auto_generated/annotations/qgsannotationpointtextitem.sip %Include auto_generated/annotations/qgsannotationpolygonitem.sip +%Include auto_generated/annotations/qgsannotationrectangletextitem.sip %Include auto_generated/annotations/qgshtmlannotation.sip %Include auto_generated/annotations/qgsrenderedannotationitemdetails.sip %Include auto_generated/annotations/qgssvgannotation.sip diff --git a/python/core/auto_generated/annotations/qgsannotationitem.sip.in b/python/core/auto_generated/annotations/qgsannotationitem.sip.in index cc6ec1cd1393..61e5d670ab41 100644 --- a/python/core/auto_generated/annotations/qgsannotationitem.sip.in +++ b/python/core/auto_generated/annotations/qgsannotationitem.sip.in @@ -42,6 +42,10 @@ Abstract base class for annotation items which are drawn with :py:class:`QgsAnno { sipType = sipType_QgsAnnotationLineTextItem; } + else if ( sipCpp->type() == QLatin1String( "recttext" ) ) + { + sipType = sipType_QgsAnnotationRectangleTextItem; + } else if ( sipCpp->type() == QLatin1String( "picture" ) ) { sipType = sipType_QgsAnnotationPictureItem; diff --git a/python/core/auto_generated/annotations/qgsannotationrectangletextitem.sip.in b/python/core/auto_generated/annotations/qgsannotationrectangletextitem.sip.in new file mode 100644 index 000000000000..c0dea93da3e2 --- /dev/null +++ b/python/core/auto_generated/annotations/qgsannotationrectangletextitem.sip.in @@ -0,0 +1,244 @@ +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/annotations/qgsannotationrectangletextitem.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ + + + + +class QgsAnnotationRectangleTextItem : QgsAnnotationItem +{ +%Docstring(signature="appended") +An annotation item which renders paragraphs of text within a rectangle. + +.. versionadded:: 3.40 +%End + +%TypeHeaderCode +#include "qgsannotationrectangletextitem.h" +%End + public: + + QgsAnnotationRectangleTextItem( const QString &text, const QgsRectangle &bounds ); +%Docstring +Constructor for QgsAnnotationRectangleTextItem, containing the specified ``text`` +within the specified ``bounds`` rectangle. +%End + ~QgsAnnotationRectangleTextItem(); + + virtual QString type() const; + + virtual Qgis::AnnotationItemFlags flags() const; + + virtual void render( QgsRenderContext &context, QgsFeedback *feedback ); + + virtual bool writeXml( QDomElement &element, QDomDocument &document, const QgsReadWriteContext &context ) const; + + virtual QList< QgsAnnotationItemNode > nodesV2( const QgsAnnotationItemEditContext &context ) const; + + virtual Qgis::AnnotationItemEditOperationResult applyEditV2( QgsAbstractAnnotationItemEditOperation *operation, const QgsAnnotationItemEditContext &context ); + + virtual QgsAnnotationItemEditOperationTransientResults *transientEditResultsV2( QgsAbstractAnnotationItemEditOperation *operation, const QgsAnnotationItemEditContext &context ) /Factory/; + + + static QgsAnnotationRectangleTextItem *create() /Factory/; +%Docstring +Creates a new rectangle text annotation item. +%End + + virtual bool readXml( const QDomElement &element, const QgsReadWriteContext &context ); + + virtual QgsAnnotationRectangleTextItem *clone() const /Factory/; + + virtual QgsRectangle boundingBox() const; + + + QgsRectangle bounds() const; +%Docstring +Returns the bounds of the text. + +The coordinate reference system for the bounds will be the parent layer's :py:func:`QgsAnnotationLayer.crs()`. + +.. seealso:: :py:func:`setBounds` +%End + + void setBounds( const QgsRectangle &bounds ); +%Docstring +Sets the ``bounds`` of the text. + +The coordinate reference system for the bounds will be the parent layer's :py:func:`QgsAnnotationLayer.crs()`. + +.. seealso:: :py:func:`bounds` +%End + + QString text() const; +%Docstring +Returns the text rendered by the item. + +.. seealso:: :py:func:`setText` +%End + + void setText( const QString &text ); +%Docstring +Sets the ``text`` rendered by the item. + +.. seealso:: :py:func:`text` +%End + + QgsTextFormat format() const; +%Docstring +Returns the text format used to render the text. + +.. seealso:: :py:func:`setFormat` +%End + + void setFormat( const QgsTextFormat &format ); +%Docstring +Sets the text ``format`` used to render the text. + +.. seealso:: :py:func:`format` +%End + + Qt::Alignment alignment() const; +%Docstring +Returns the text's alignment relative to the :py:func:`~QgsAnnotationRectangleTextItem.bounds` rectangle. + +.. seealso:: :py:func:`setAlignment` +%End + + void setAlignment( Qt::Alignment alignment ); +%Docstring +Sets the text's ``alignment`` relative to the :py:func:`~QgsAnnotationRectangleTextItem.bounds` rectangle. + +.. seealso:: :py:func:`alignment` +%End + + bool backgroundEnabled() const; +%Docstring +Returns ``True`` if the item's background should be rendered. + +.. seealso:: :py:func:`setBackgroundEnabled` + +.. seealso:: :py:func:`backgroundSymbol` +%End + + void setBackgroundEnabled( bool enabled ); +%Docstring +Sets whether the item's background should be rendered. + +.. seealso:: :py:func:`backgroundEnabled` + +.. seealso:: :py:func:`setBackgroundSymbol` +%End + + const QgsFillSymbol *backgroundSymbol() const; +%Docstring +Returns the symbol used to render the item's background. + +.. seealso:: :py:func:`backgroundEnabled` + +.. seealso:: :py:func:`setBackgroundSymbol` +%End + + void setBackgroundSymbol( QgsFillSymbol *symbol /Transfer/ ); +%Docstring +Sets the ``symbol`` used to render the item's background. + +The item takes ownership of the symbol. + +.. seealso:: :py:func:`backgroundSymbol` + +.. seealso:: :py:func:`setBackgroundEnabled` +%End + + bool frameEnabled() const; +%Docstring +Returns ``True`` if the item's frame should be rendered. + +.. seealso:: :py:func:`setFrameEnabled` + +.. seealso:: :py:func:`frameSymbol` +%End + + void setFrameEnabled( bool enabled ); +%Docstring +Sets whether the item's frame should be rendered. + +.. seealso:: :py:func:`frameEnabled` + +.. seealso:: :py:func:`setFrameSymbol` +%End + + const QgsFillSymbol *frameSymbol() const; +%Docstring +Returns the symbol used to render the item's frame. + +.. seealso:: :py:func:`frameEnabled` + +.. seealso:: :py:func:`setFrameSymbol` +%End + + void setFrameSymbol( QgsFillSymbol *symbol /Transfer/ ); +%Docstring +Sets the ``symbol`` used to render the item's frame. + +The item takes ownership of the symbol. + +.. seealso:: :py:func:`frameSymbol` + +.. seealso:: :py:func:`setBackgroundEnabled` +%End + + const QgsMargins &margins() const; +%Docstring +Returns the margins between the outside of the item's frame and the interior text. + +Units are retrieved via :py:func:`~QgsAnnotationRectangleTextItem.marginsUnit` + +.. seealso:: :py:func:`setMargins` + +.. seealso:: :py:func:`marginsUnit` +%End + + void setMargins( const QgsMargins &margins ); +%Docstring +Sets the ``margins`` between the outside of the item's frame and the interior text. + +Units are set via :py:func:`~QgsAnnotationRectangleTextItem.setMarginsUnit` + +.. seealso:: :py:func:`margins` + +.. seealso:: :py:func:`setMarginsUnit` +%End + + void setMarginsUnit( Qgis::RenderUnit unit ); +%Docstring +Sets the ``unit`` for the margins between the item's frame and the interior text. + +.. seealso:: :py:func:`margins` + +.. seealso:: :py:func:`marginsUnit` +%End + + Qgis::RenderUnit marginsUnit() const; +%Docstring +Returns the units for the margins between the item's frame and the interior text. + +.. seealso:: :py:func:`setMarginsUnit` + +.. seealso:: :py:func:`margins` +%End + + private: + QgsAnnotationRectangleTextItem( const QgsAnnotationRectangleTextItem &other ); +}; +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/annotations/qgsannotationrectangletextitem.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ diff --git a/python/core/core_auto.sip b/python/core/core_auto.sip index d5ccc57a6a7d..5d5d1a31b989 100644 --- a/python/core/core_auto.sip +++ b/python/core/core_auto.sip @@ -235,6 +235,7 @@ %Include auto_generated/annotations/qgsannotationpictureitem.sip %Include auto_generated/annotations/qgsannotationpointtextitem.sip %Include auto_generated/annotations/qgsannotationpolygonitem.sip +%Include auto_generated/annotations/qgsannotationrectangletextitem.sip %Include auto_generated/annotations/qgshtmlannotation.sip %Include auto_generated/annotations/qgsrenderedannotationitemdetails.sip %Include auto_generated/annotations/qgssvgannotation.sip diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 56bf18d3e667..74db5413f719 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -223,6 +223,7 @@ set(QGIS_CORE_SRCS annotations/qgsannotationpictureitem.cpp annotations/qgsannotationpointtextitem.cpp annotations/qgsannotationpolygonitem.cpp + annotations/qgsannotationrectangletextitem.cpp annotations/qgshtmlannotation.cpp annotations/qgsrenderedannotationitemdetails.cpp annotations/qgssvgannotation.cpp @@ -1340,6 +1341,7 @@ set(QGIS_CORE_HDRS annotations/qgsannotationpictureitem.h annotations/qgsannotationpointtextitem.h annotations/qgsannotationpolygonitem.h + annotations/qgsannotationrectangletextitem.h annotations/qgsannotationregistry.h annotations/qgshtmlannotation.h annotations/qgsrenderedannotationitemdetails.h diff --git a/src/core/annotations/qgsannotationitem.h b/src/core/annotations/qgsannotationitem.h index 58f3241a3a29..f2d7892713e2 100644 --- a/src/core/annotations/qgsannotationitem.h +++ b/src/core/annotations/qgsannotationitem.h @@ -63,6 +63,10 @@ class CORE_EXPORT QgsAnnotationItem { sipType = sipType_QgsAnnotationLineTextItem; } + else if ( sipCpp->type() == QLatin1String( "recttext" ) ) + { + sipType = sipType_QgsAnnotationRectangleTextItem; + } else if ( sipCpp->type() == QLatin1String( "picture" ) ) { sipType = sipType_QgsAnnotationPictureItem; diff --git a/src/core/annotations/qgsannotationitemregistry.cpp b/src/core/annotations/qgsannotationitemregistry.cpp index c77436fdb2d0..7c3637624be1 100644 --- a/src/core/annotations/qgsannotationitemregistry.cpp +++ b/src/core/annotations/qgsannotationitemregistry.cpp @@ -21,6 +21,7 @@ #include "qgsannotationpolygonitem.h" #include "qgsannotationpointtextitem.h" #include "qgsannotationlinetextitem.h" +#include "qgsannotationrectangletextitem.h" #include "qgsannotationpictureitem.h" #include @@ -49,6 +50,8 @@ bool QgsAnnotationItemRegistry::populate() QgsAnnotationPointTextItem::create ) ); mMetadata.insert( QStringLiteral( "linetext" ), new QgsAnnotationItemMetadata( QStringLiteral( "linetext" ), QObject::tr( "Text along line" ), QObject::tr( "Text along lines" ), QgsAnnotationLineTextItem::create ) ); + mMetadata.insert( QStringLiteral( "recttext" ), new QgsAnnotationItemMetadata( QStringLiteral( "recttext" ), QObject::tr( "Text in rectangle" ), QObject::tr( "Text in rectangles" ), + QgsAnnotationRectangleTextItem::create ) ); mMetadata.insert( QStringLiteral( "picture" ), new QgsAnnotationItemMetadata( QStringLiteral( "picture" ), QObject::tr( "Picture" ), QObject::tr( "Pictures" ), QgsAnnotationPictureItem::create ) ); return true; diff --git a/src/core/annotations/qgsannotationrectangletextitem.cpp b/src/core/annotations/qgsannotationrectangletextitem.cpp new file mode 100644 index 000000000000..92cb151d5d34 --- /dev/null +++ b/src/core/annotations/qgsannotationrectangletextitem.cpp @@ -0,0 +1,378 @@ +/*************************************************************************** + qgsannotationrectangletextitem.cpp + ---------------- + begin : July 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 "qgsannotationrectangletextitem.h" +#include "qgsgeometry.h" +#include "qgsrendercontext.h" +#include "qgsannotationitemnode.h" +#include "qgsannotationitemeditoperation.h" +#include "qgsfillsymbol.h" +#include "qgssymbollayerutils.h" +#include "qgsfillsymbollayer.h" +#include "qgslinesymbollayer.h" +#include "qgstextrenderer.h" +#include "qgsunittypes.h" + +QgsAnnotationRectangleTextItem::QgsAnnotationRectangleTextItem( const QString &text, const QgsRectangle &bounds ) + : QgsAnnotationItem() + , mBounds( bounds ) + , mText( text ) +{ + mBackgroundSymbol = std::make_unique< QgsFillSymbol >( QgsSymbolLayerList { new QgsSimpleFillSymbolLayer( QColor( 255, 255, 255 ), Qt::BrushStyle::SolidPattern, QColor( 0, 0, 0 ), Qt::PenStyle::NoPen ) } ); + QgsSimpleLineSymbolLayer *borderSymbol = new QgsSimpleLineSymbolLayer( QColor( 0, 0, 0 ) ); + borderSymbol->setPenJoinStyle( Qt::MiterJoin ); + mFrameSymbol = std::make_unique< QgsFillSymbol >( QgsSymbolLayerList { borderSymbol } ); +} + +QgsAnnotationRectangleTextItem::~QgsAnnotationRectangleTextItem() = default; + +QString QgsAnnotationRectangleTextItem::type() const +{ + return QStringLiteral( "recttext" ); +} + +void QgsAnnotationRectangleTextItem::render( QgsRenderContext &context, QgsFeedback * ) +{ + QgsRectangle bounds = mBounds; + if ( context.coordinateTransform().isValid() ) + { + try + { + bounds = context.coordinateTransform().transformBoundingBox( mBounds ); + } + catch ( QgsCsException & ) + { + return; + } + } + + const QRectF painterBounds = context.mapToPixel().transformBounds( bounds.toRectF() ); + if ( painterBounds.width() < 1 || painterBounds.height() < 1 ) + return; + + if ( mDrawBackground && mBackgroundSymbol ) + { + mBackgroundSymbol->startRender( context ); + mBackgroundSymbol->renderPolygon( painterBounds, nullptr, nullptr, context ); + mBackgroundSymbol->stopRender( context ); + } + + const double marginLeft = context.convertToPainterUnits( mMargins.left(), mMarginUnit ); + const double marginTop = context.convertToPainterUnits( mMargins.top(), mMarginUnit ); + const double marginRight = context.convertToPainterUnits( mMargins.right(), mMarginUnit ); + const double marginBottom = context.convertToPainterUnits( mMargins.bottom(), mMarginUnit ); + + const QRectF innerRect( + painterBounds.left() + marginLeft, + painterBounds.top() + marginTop, + painterBounds.width() - marginLeft - marginRight, + painterBounds.height() - marginTop - marginBottom ); + + const QString displayText = QgsExpression::replaceExpressionText( mText, &context.expressionContext(), &context.distanceArea() ); + + const bool prevWorkaroundFlag = context.testFlag( Qgis::RenderContextFlag::ApplyScalingWorkaroundForTextRendering ); + context.setFlag( Qgis::RenderContextFlag::ApplyScalingWorkaroundForTextRendering, true ); + QgsTextRenderer::drawText( innerRect, 0, + QgsTextRenderer::convertQtHAlignment( mAlignment ), + displayText.split( '\n' ), context, mTextFormat, true, + QgsTextRenderer::convertQtVAlignment( mAlignment ), + Qgis::TextRendererFlag::WrapLines ); + context.setFlag( Qgis::RenderContextFlag::ApplyScalingWorkaroundForTextRendering, prevWorkaroundFlag ); + + if ( mDrawFrame && mFrameSymbol ) + { + mFrameSymbol->startRender( context ); + mFrameSymbol->renderPolygon( painterBounds, nullptr, nullptr, context ); + mFrameSymbol->stopRender( context ); + } +} + +bool QgsAnnotationRectangleTextItem::writeXml( QDomElement &element, QDomDocument &document, const QgsReadWriteContext &context ) const +{ + element.setAttribute( QStringLiteral( "text" ), mText ); + element.setAttribute( QStringLiteral( "alignment" ), QString::number( mAlignment ) ); + + QDomElement textFormatElem = document.createElement( QStringLiteral( "rectTextFormat" ) ); + textFormatElem.appendChild( mTextFormat.writeXml( document, context ) ); + element.appendChild( textFormatElem ); + + element.setAttribute( QStringLiteral( "margins" ), mMargins.toString() ); + element.setAttribute( QStringLiteral( "marginUnit" ), QgsUnitTypes::encodeUnit( mMarginUnit ) ); + + element.setAttribute( QStringLiteral( "xMin" ), qgsDoubleToString( mBounds.xMinimum() ) ); + element.setAttribute( QStringLiteral( "xMax" ), qgsDoubleToString( mBounds.xMaximum() ) ); + element.setAttribute( QStringLiteral( "yMin" ), qgsDoubleToString( mBounds.yMinimum() ) ); + element.setAttribute( QStringLiteral( "yMax" ), qgsDoubleToString( mBounds.yMaximum() ) ); + + element.setAttribute( QStringLiteral( "backgroundEnabled" ), mDrawBackground ? QStringLiteral( "1" ) : QStringLiteral( "0" ) ); + if ( mBackgroundSymbol ) + { + QDomElement backgroundElement = document.createElement( QStringLiteral( "backgroundSymbol" ) ); + backgroundElement.appendChild( QgsSymbolLayerUtils::saveSymbol( QStringLiteral( "backgroundSymbol" ), mBackgroundSymbol.get(), document, context ) ); + element.appendChild( backgroundElement ); + } + + element.setAttribute( QStringLiteral( "frameEnabled" ), mDrawFrame ? QStringLiteral( "1" ) : QStringLiteral( "0" ) ); + if ( mFrameSymbol ) + { + QDomElement frameElement = document.createElement( QStringLiteral( "frameSymbol" ) ); + frameElement.appendChild( QgsSymbolLayerUtils::saveSymbol( QStringLiteral( "frameSymbol" ), mFrameSymbol.get(), document, context ) ); + element.appendChild( frameElement ); + } + + writeCommonProperties( element, document, context ); + return true; +} + +QList QgsAnnotationRectangleTextItem::nodesV2( const QgsAnnotationItemEditContext & ) const +{ + return + { + QgsAnnotationItemNode( QgsVertexId( 0, 0, 0 ), QgsPointXY( mBounds.xMinimum(), mBounds.yMinimum() ), Qgis::AnnotationItemNodeType::VertexHandle ), + QgsAnnotationItemNode( QgsVertexId( 0, 0, 1 ), QgsPointXY( mBounds.xMaximum(), mBounds.yMinimum() ), Qgis::AnnotationItemNodeType::VertexHandle ), + QgsAnnotationItemNode( QgsVertexId( 0, 0, 2 ), QgsPointXY( mBounds.xMaximum(), mBounds.yMaximum() ), Qgis::AnnotationItemNodeType::VertexHandle ), + QgsAnnotationItemNode( QgsVertexId( 0, 0, 3 ), QgsPointXY( mBounds.xMinimum(), mBounds.yMaximum() ), Qgis::AnnotationItemNodeType::VertexHandle ), + }; +} + +Qgis::AnnotationItemEditOperationResult QgsAnnotationRectangleTextItem::applyEditV2( QgsAbstractAnnotationItemEditOperation *operation, const QgsAnnotationItemEditContext & ) +{ + switch ( operation->type() ) + { + case QgsAbstractAnnotationItemEditOperation::Type::MoveNode: + { + QgsAnnotationItemEditOperationMoveNode *moveOperation = dynamic_cast< QgsAnnotationItemEditOperationMoveNode * >( operation ); + switch ( moveOperation->nodeId().vertex ) + { + case 0: + mBounds = QgsRectangle( moveOperation->after().x(), + moveOperation->after().y(), + mBounds.xMaximum(), + mBounds.yMaximum() ); + break; + case 1: + mBounds = QgsRectangle( mBounds.xMinimum(), + moveOperation->after().y(), + moveOperation->after().x(), + mBounds.yMaximum() ); + break; + case 2: + mBounds = QgsRectangle( mBounds.xMinimum(), + mBounds.yMinimum(), + moveOperation->after().x(), + moveOperation->after().y() ); + break; + case 3: + mBounds = QgsRectangle( moveOperation->after().x(), + mBounds.yMinimum(), + mBounds.xMaximum(), + moveOperation->after().y() ); + break; + default: + break; + } + return Qgis::AnnotationItemEditOperationResult::Success; + } + + case QgsAbstractAnnotationItemEditOperation::Type::TranslateItem: + { + QgsAnnotationItemEditOperationTranslateItem *moveOperation = qgis::down_cast< QgsAnnotationItemEditOperationTranslateItem * >( operation ); + mBounds = QgsRectangle( mBounds.xMinimum() + moveOperation->translationX(), + mBounds.yMinimum() + moveOperation->translationY(), + mBounds.xMaximum() + moveOperation->translationX(), + mBounds.yMaximum() + moveOperation->translationY() ); + return Qgis::AnnotationItemEditOperationResult::Success; + } + + case QgsAbstractAnnotationItemEditOperation::Type::DeleteNode: + case QgsAbstractAnnotationItemEditOperation::Type::AddNode: + break; + } + return Qgis::AnnotationItemEditOperationResult::Invalid; +} + +QgsAnnotationItemEditOperationTransientResults *QgsAnnotationRectangleTextItem::transientEditResultsV2( QgsAbstractAnnotationItemEditOperation *operation, const QgsAnnotationItemEditContext & ) +{ + switch ( operation->type() ) + { + case QgsAbstractAnnotationItemEditOperation::Type::MoveNode: + { + QgsAnnotationItemEditOperationMoveNode *moveOperation = dynamic_cast< QgsAnnotationItemEditOperationMoveNode * >( operation ); + QgsRectangle modifiedBounds = mBounds; + switch ( moveOperation->nodeId().vertex ) + { + case 0: + modifiedBounds.setXMinimum( moveOperation->after().x() ); + modifiedBounds.setYMinimum( moveOperation->after().y() ); + break; + case 1: + modifiedBounds.setXMaximum( moveOperation->after().x() ); + modifiedBounds.setYMinimum( moveOperation->after().y() ); + break; + case 2: + modifiedBounds.setXMaximum( moveOperation->after().x() ); + modifiedBounds.setYMaximum( moveOperation->after().y() ); + break; + case 3: + modifiedBounds.setXMinimum( moveOperation->after().x() ); + modifiedBounds.setYMaximum( moveOperation->after().y() ); + break; + default: + break; + } + + return new QgsAnnotationItemEditOperationTransientResults( QgsGeometry::fromRect( modifiedBounds ) ); + } + + case QgsAbstractAnnotationItemEditOperation::Type::TranslateItem: + { + QgsAnnotationItemEditOperationTranslateItem *moveOperation = qgis::down_cast< QgsAnnotationItemEditOperationTranslateItem * >( operation ); + const QgsRectangle modifiedBounds( mBounds.xMinimum() + moveOperation->translationX(), + mBounds.yMinimum() + moveOperation->translationY(), + mBounds.xMaximum() + moveOperation->translationX(), + mBounds.yMaximum() + moveOperation->translationY() ); + return new QgsAnnotationItemEditOperationTransientResults( QgsGeometry::fromRect( modifiedBounds ) ); + } + + case QgsAbstractAnnotationItemEditOperation::Type::DeleteNode: + case QgsAbstractAnnotationItemEditOperation::Type::AddNode: + break; + } + return nullptr; +} + +QgsAnnotationRectangleTextItem *QgsAnnotationRectangleTextItem::create() +{ + return new QgsAnnotationRectangleTextItem( QString(), QgsRectangle() ); +} + +bool QgsAnnotationRectangleTextItem::readXml( const QDomElement &element, const QgsReadWriteContext &context ) +{ + mText = element.attribute( QStringLiteral( "text" ) ); + + const QDomElement textFormatElem = element.firstChildElement( QStringLiteral( "rectTextFormat" ) ); + if ( !textFormatElem.isNull() ) + { + const QDomNodeList textFormatNodeList = textFormatElem.elementsByTagName( QStringLiteral( "text-style" ) ); + const QDomElement textFormatElem = textFormatNodeList.at( 0 ).toElement(); + mTextFormat.readXml( textFormatElem, context ); + } + + mBounds.setXMinimum( element.attribute( QStringLiteral( "xMin" ) ).toDouble() ); + mBounds.setXMaximum( element.attribute( QStringLiteral( "xMax" ) ).toDouble() ); + mBounds.setYMinimum( element.attribute( QStringLiteral( "yMin" ) ).toDouble() ); + mBounds.setYMaximum( element.attribute( QStringLiteral( "yMax" ) ).toDouble() ); + + mMargins = QgsMargins::fromString( element.attribute( QStringLiteral( "margins" ) ) ); + mMarginUnit = QgsUnitTypes::decodeRenderUnit( element.attribute( QStringLiteral( "marginUnit" ), QgsUnitTypes::encodeUnit( Qgis::RenderUnit::Millimeters ) ) ); + + mAlignment = static_cast< Qt::Alignment >( element.attribute( QStringLiteral( "alignment" ) ).toInt() ); + + mDrawBackground = element.attribute( QStringLiteral( "backgroundEnabled" ), QStringLiteral( "1" ) ).toInt(); + const QDomElement backgroundSymbolElem = element.firstChildElement( QStringLiteral( "backgroundSymbol" ) ).firstChildElement(); + if ( !backgroundSymbolElem.isNull() ) + { + setBackgroundSymbol( QgsSymbolLayerUtils::loadSymbol< QgsFillSymbol >( backgroundSymbolElem, context ) ); + } + + mDrawFrame = element.attribute( QStringLiteral( "frameEnabled" ), QStringLiteral( "1" ) ).toInt(); + const QDomElement frameSymbolElem = element.firstChildElement( QStringLiteral( "frameSymbol" ) ).firstChildElement(); + if ( !frameSymbolElem.isNull() ) + { + setFrameSymbol( QgsSymbolLayerUtils::loadSymbol< QgsFillSymbol >( frameSymbolElem, context ) ); + } + + readCommonProperties( element, context ); + return true; +} + +QgsAnnotationRectangleTextItem *QgsAnnotationRectangleTextItem::clone() const +{ + std::unique_ptr< QgsAnnotationRectangleTextItem > item = std::make_unique< QgsAnnotationRectangleTextItem >( mText, mBounds ); + + item->setFormat( mTextFormat ); + item->setAlignment( mAlignment ); + + item->setBackgroundEnabled( mDrawBackground ); + if ( mBackgroundSymbol ) + item->setBackgroundSymbol( mBackgroundSymbol->clone() ); + + item->setFrameEnabled( mDrawFrame ); + if ( mFrameSymbol ) + item->setFrameSymbol( mFrameSymbol->clone() ); + + item->setMargins( mMargins ); + item->setMarginsUnit( mMarginUnit ); + + item->copyCommonProperties( this ); + return item.release(); +} + +QgsRectangle QgsAnnotationRectangleTextItem::boundingBox() const +{ + return mBounds; +} + +void QgsAnnotationRectangleTextItem::setBounds( const QgsRectangle &bounds ) +{ + mBounds = bounds; +} + +const QgsFillSymbol *QgsAnnotationRectangleTextItem::backgroundSymbol() const +{ + return mBackgroundSymbol.get(); +} + +void QgsAnnotationRectangleTextItem::setBackgroundSymbol( QgsFillSymbol *symbol ) +{ + mBackgroundSymbol.reset( symbol ); +} + +const QgsFillSymbol *QgsAnnotationRectangleTextItem::frameSymbol() const +{ + return mFrameSymbol.get(); +} + +void QgsAnnotationRectangleTextItem::setFrameSymbol( QgsFillSymbol *symbol ) +{ + mFrameSymbol.reset( symbol ); +} + +Qgis::AnnotationItemFlags QgsAnnotationRectangleTextItem::flags() const +{ + return Qgis::AnnotationItemFlag::SupportsReferenceScale; +} + +QgsTextFormat QgsAnnotationRectangleTextItem::format() const +{ + return mTextFormat; +} + +void QgsAnnotationRectangleTextItem::setFormat( const QgsTextFormat &format ) +{ + mTextFormat = format; +} + +Qt::Alignment QgsAnnotationRectangleTextItem::alignment() const +{ + return mAlignment; +} + +void QgsAnnotationRectangleTextItem::setAlignment( Qt::Alignment alignment ) +{ + mAlignment = alignment; +} diff --git a/src/core/annotations/qgsannotationrectangletextitem.h b/src/core/annotations/qgsannotationrectangletextitem.h new file mode 100644 index 000000000000..739157bfccd4 --- /dev/null +++ b/src/core/annotations/qgsannotationrectangletextitem.h @@ -0,0 +1,245 @@ +/*************************************************************************** + qgsannotationrectangletextitem.h + ---------------- + begin : July 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 QGSANNOTATIONRECTANGLETEXTITEM_H +#define QGSANNOTATIONRECTANGLETEXTITEM_H + +#include "qgis_core.h" +#include "qgis_sip.h" +#include "qgsannotationitem.h" +#include "qgstextformat.h" +#include "qgsmargins.h" + +/** + * \ingroup core + * \brief An annotation item which renders paragraphs of text within a rectangle. + * + * \since QGIS 3.40 + */ +class CORE_EXPORT QgsAnnotationRectangleTextItem : public QgsAnnotationItem +{ + public: + + /** + * Constructor for QgsAnnotationRectangleTextItem, containing the specified \a text + * within the specified \a bounds rectangle. + */ + QgsAnnotationRectangleTextItem( const QString &text, const QgsRectangle &bounds ); + ~QgsAnnotationRectangleTextItem() override; + + QString type() const override; + Qgis::AnnotationItemFlags flags() const override; + void render( QgsRenderContext &context, QgsFeedback *feedback ) override; + bool writeXml( QDomElement &element, QDomDocument &document, const QgsReadWriteContext &context ) const override; + QList< QgsAnnotationItemNode > nodesV2( const QgsAnnotationItemEditContext &context ) const override; + Qgis::AnnotationItemEditOperationResult applyEditV2( QgsAbstractAnnotationItemEditOperation *operation, const QgsAnnotationItemEditContext &context ) override; + QgsAnnotationItemEditOperationTransientResults *transientEditResultsV2( QgsAbstractAnnotationItemEditOperation *operation, const QgsAnnotationItemEditContext &context ) override SIP_FACTORY; + + /** + * Creates a new rectangle text annotation item. + */ + static QgsAnnotationRectangleTextItem *create() SIP_FACTORY; + + bool readXml( const QDomElement &element, const QgsReadWriteContext &context ) override; + QgsAnnotationRectangleTextItem *clone() const override SIP_FACTORY; + QgsRectangle boundingBox() const override; + + /** + * Returns the bounds of the text. + * + * The coordinate reference system for the bounds will be the parent layer's QgsAnnotationLayer::crs(). + * + * \see setBounds() + */ + QgsRectangle bounds() const { return mBounds; } + + /** + * Sets the \a bounds of the text. + * + * The coordinate reference system for the bounds will be the parent layer's QgsAnnotationLayer::crs(). + * + * \see bounds() + */ + void setBounds( const QgsRectangle &bounds ); + + /** + * Returns the text rendered by the item. + * + * \see setText() + */ + QString text() const { return mText; } + + /** + * Sets the \a text rendered by the item. + * + * \see text() + */ + void setText( const QString &text ) { mText = text; } + + /** + * Returns the text format used to render the text. + * + * \see setFormat() + */ + QgsTextFormat format() const; + + /** + * Sets the text \a format used to render the text. + * + * \see format() + */ + void setFormat( const QgsTextFormat &format ); + + /** + * Returns the text's alignment relative to the bounds() rectangle. + * + * \see setAlignment(). + */ + Qt::Alignment alignment() const; + + /** + * Sets the text's \a alignment relative to the bounds() rectangle. + * + * \see alignment(). + */ + void setAlignment( Qt::Alignment alignment ); + + /** + * Returns TRUE if the item's background should be rendered. + * + * \see setBackgroundEnabled() + * \see backgroundSymbol() + */ + bool backgroundEnabled() const { return mDrawBackground; } + + /** + * Sets whether the item's background should be rendered. + * + * \see backgroundEnabled() + * \see setBackgroundSymbol() + */ + void setBackgroundEnabled( bool enabled ) { mDrawBackground = enabled; } + + /** + * Returns the symbol used to render the item's background. + * + * \see backgroundEnabled() + * \see setBackgroundSymbol() + */ + const QgsFillSymbol *backgroundSymbol() const; + + /** + * Sets the \a symbol used to render the item's background. + * + * The item takes ownership of the symbol. + * + * \see backgroundSymbol() + * \see setBackgroundEnabled() + */ + void setBackgroundSymbol( QgsFillSymbol *symbol SIP_TRANSFER ); + + /** + * Returns TRUE if the item's frame should be rendered. + * + * \see setFrameEnabled() + * \see frameSymbol() + */ + bool frameEnabled() const { return mDrawFrame; } + + /** + * Sets whether the item's frame should be rendered. + * + * \see frameEnabled() + * \see setFrameSymbol() + */ + void setFrameEnabled( bool enabled ) { mDrawFrame = enabled; } + + /** + * Returns the symbol used to render the item's frame. + * + * \see frameEnabled() + * \see setFrameSymbol() + */ + const QgsFillSymbol *frameSymbol() const; + + /** + * Sets the \a symbol used to render the item's frame. + * + * The item takes ownership of the symbol. + * + * \see frameSymbol() + * \see setBackgroundEnabled() + */ + void setFrameSymbol( QgsFillSymbol *symbol SIP_TRANSFER ); + + /** + * Returns the margins between the outside of the item's frame and the interior text. + * + * Units are retrieved via marginsUnit() + * + * \see setMargins() + * \see marginsUnit() + */ + const QgsMargins &margins() const { return mMargins; } + + /** + * Sets the \a margins between the outside of the item's frame and the interior text. + * + * Units are set via setMarginsUnit() + * + * \see margins() + * \see setMarginsUnit() + */ + void setMargins( const QgsMargins &margins ) { mMargins = margins; } + + /** + * Sets the \a unit for the margins between the item's frame and the interior text. + * + * \see margins() + * \see marginsUnit() + */ + void setMarginsUnit( Qgis::RenderUnit unit ) { mMarginUnit = unit; } + + /** + * Returns the units for the margins between the item's frame and the interior text. + * + * \see setMarginsUnit() + * \see margins() + */ + Qgis::RenderUnit marginsUnit() const { return mMarginUnit; } + + private: + + QgsRectangle mBounds; + QString mText; + QgsTextFormat mTextFormat; + Qt::Alignment mAlignment = Qt::AlignLeft; + + bool mDrawBackground = true; + std::unique_ptr< QgsFillSymbol > mBackgroundSymbol; + bool mDrawFrame = true; + std::unique_ptr< QgsFillSymbol > mFrameSymbol; + + QgsMargins mMargins; + Qgis::RenderUnit mMarginUnit = Qgis::RenderUnit::Millimeters; + +#ifdef SIP_RUN + QgsAnnotationRectangleTextItem( const QgsAnnotationRectangleTextItem &other ); +#endif + +}; +#endif // QGSANNOTATIONRECTANGLETEXTITEM_H diff --git a/src/gui/annotations/qgsannotationitemguiregistry.cpp b/src/gui/annotations/qgsannotationitemguiregistry.cpp index 1516e610bea7..b3ba06f71a93 100644 --- a/src/gui/annotations/qgsannotationitemguiregistry.cpp +++ b/src/gui/annotations/qgsannotationitemguiregistry.cpp @@ -251,6 +251,19 @@ void QgsAnnotationItemGuiRegistry::addDefaultItems() return new QgsCreateLineTextItemMapTool( canvas, cadDockWidget ); } ) ); + addAnnotationItemGuiMetadata( new QgsAnnotationItemGuiMetadata( QStringLiteral( "recttext" ), + QObject::tr( "Text Annotation in Rectangle" ), + QgsApplication::getThemeIcon( QStringLiteral( "/mActionTextInsideRect.svg" ) ), + [ = ]( QgsAnnotationItem * item )->QgsAnnotationItemBaseWidget * + { + QgsAnnotationRectangleTextItemWidget *widget = new QgsAnnotationRectangleTextItemWidget( nullptr ); + widget->setItem( item ); + return widget; + }, QString(), Qgis::AnnotationItemGuiFlags(), nullptr, + [ = ]( QgsMapCanvas * canvas, QgsAdvancedDigitizingDockWidget * cadDockWidget )->QgsCreateAnnotationItemMapToolInterface * + { + return new QgsCreateRectangleTextItemMapTool( canvas, cadDockWidget ); + } ) ); addAnnotationItemGuiMetadata( new QgsAnnotationItemGuiMetadata( QStringLiteral( "picture" ), QObject::tr( "Picture" ), diff --git a/src/gui/annotations/qgsannotationitemwidget_impl.cpp b/src/gui/annotations/qgsannotationitemwidget_impl.cpp index 54e07f5c5098..d5ef953857b1 100644 --- a/src/gui/annotations/qgsannotationitemwidget_impl.cpp +++ b/src/gui/annotations/qgsannotationitemwidget_impl.cpp @@ -24,6 +24,7 @@ #include "qgsannotationmarkeritem.h" #include "qgsannotationpointtextitem.h" #include "qgsannotationlinetextitem.h" +#include "qgsannotationrectangletextitem.h" #include "qgsannotationpictureitem.h" #include "qgsexpressionbuilderdialog.h" #include "qgstextformatwidget.h" @@ -610,6 +611,184 @@ void QgsAnnotationLineTextItemWidget::mInsertExpressionButton_clicked() } + +// +// QgsAnnotationRectangleTextItemWidget +// + +QgsAnnotationRectangleTextItemWidget::QgsAnnotationRectangleTextItemWidget( QWidget *parent ) + : QgsAnnotationItemBaseWidget( parent ) +{ + setupUi( this ); + + mBackgroundSymbolButton->setSymbolType( Qgis::SymbolType::Fill ); + mBackgroundSymbolButton->setDialogTitle( tr( "Background" ) ); + mBackgroundSymbolButton->registerExpressionContextGenerator( this ); + mFrameSymbolButton->setSymbolType( Qgis::SymbolType::Fill ); + mFrameSymbolButton->setDialogTitle( tr( "Frame" ) ); + mFrameSymbolButton->registerExpressionContextGenerator( this ); + + mSpinBottomMargin->setClearValue( 0 ); + mSpinTopMargin->setClearValue( 0 ); + mSpinRightMargin->setClearValue( 0 ); + mSpinLeftMargin->setClearValue( 0 ); + mMarginUnitWidget->setUnits( QgsUnitTypes::RenderUnitList() << Qgis::RenderUnit::Millimeters << Qgis::RenderUnit::MetersInMapUnits << Qgis::RenderUnit::MapUnits << Qgis::RenderUnit::Pixels + << Qgis::RenderUnit::Points << Qgis::RenderUnit::Inches ); + + mTextFormatWidget = new QgsTextFormatWidget(); + QVBoxLayout *vLayout = new QVBoxLayout(); + vLayout->setContentsMargins( 0, 0, 0, 0 ); + vLayout->addWidget( mTextFormatWidget ); + mTextFormatWidgetContainer->setLayout( vLayout ); + + mTextEdit->setMaximumHeight( mTextEdit->fontMetrics().height() * 10 ); + + mAlignmentComboBox->setAvailableAlignments( Qt::AlignLeft | Qt::AlignHCenter | Qt::AlignRight | Qt::AlignJustify ); + mVerticalAlignmentComboBox->setAvailableAlignments( Qt::AlignTop | Qt::AlignVCenter | Qt::AlignBottom ); + + mTextFormatWidget->setDockMode( dockMode() ); + connect( mTextFormatWidget, &QgsTextFormatWidget::widgetChanged, this, &QgsAnnotationRectangleTextItemWidget::onWidgetChanged ); + connect( mTextEdit, &QPlainTextEdit::textChanged, this, &QgsAnnotationRectangleTextItemWidget::onWidgetChanged ); + connect( mInsertExpressionButton, &QPushButton::clicked, this, &QgsAnnotationRectangleTextItemWidget::mInsertExpressionButton_clicked ); + connect( mPropertiesWidget, &QgsAnnotationItemCommonPropertiesWidget::itemChanged, this, &QgsAnnotationRectangleTextItemWidget::onWidgetChanged ); + connect( mAlignmentComboBox, &QgsAlignmentComboBox::changed, this, &QgsAnnotationRectangleTextItemWidget::onWidgetChanged ); + connect( mVerticalAlignmentComboBox, &QgsAlignmentComboBox::changed, this, &QgsAnnotationRectangleTextItemWidget::onWidgetChanged ); + connect( mFrameCheckbox, &QGroupBox::toggled, this, &QgsAnnotationRectangleTextItemWidget::onWidgetChanged ); + connect( mBackgroundCheckbox, &QGroupBox::toggled, this, &QgsAnnotationRectangleTextItemWidget::onWidgetChanged ); + connect( mBackgroundSymbolButton, &QgsSymbolButton::changed, this, &QgsAnnotationRectangleTextItemWidget::onWidgetChanged ); + connect( mFrameSymbolButton, &QgsSymbolButton::changed, this, &QgsAnnotationRectangleTextItemWidget::onWidgetChanged ); + connect( mSpinTopMargin, qOverload< double >( &QgsDoubleSpinBox::valueChanged ), this, &QgsAnnotationRectangleTextItemWidget::onWidgetChanged ); + connect( mSpinRightMargin, qOverload< double >( &QgsDoubleSpinBox::valueChanged ), this, &QgsAnnotationRectangleTextItemWidget::onWidgetChanged ); + connect( mSpinLeftMargin, qOverload< double >( &QgsDoubleSpinBox::valueChanged ), this, &QgsAnnotationRectangleTextItemWidget::onWidgetChanged ); + connect( mSpinBottomMargin, qOverload< double >( &QgsDoubleSpinBox::valueChanged ), this, &QgsAnnotationRectangleTextItemWidget::onWidgetChanged ); + connect( mMarginUnitWidget, &QgsUnitSelectionWidget::changed, this, &QgsAnnotationRectangleTextItemWidget::onWidgetChanged ); +} + +QgsAnnotationItem *QgsAnnotationRectangleTextItemWidget::createItem() +{ + QgsAnnotationRectangleTextItem *newItem = mItem->clone(); + updateItem( newItem ); + return newItem; +} + +void QgsAnnotationRectangleTextItemWidget::updateItem( QgsAnnotationItem *item ) +{ + if ( QgsAnnotationRectangleTextItem *rectTextItem = dynamic_cast< QgsAnnotationRectangleTextItem * >( item ) ) + { + rectTextItem->setFormat( mTextFormatWidget->format() ); + rectTextItem->setText( mTextEdit->toPlainText() ); + rectTextItem->setAlignment( mAlignmentComboBox->currentAlignment() | mVerticalAlignmentComboBox->currentAlignment() ); + + rectTextItem->setBackgroundEnabled( mBackgroundCheckbox->isChecked() ); + rectTextItem->setFrameEnabled( mFrameCheckbox->isChecked() ); + rectTextItem->setBackgroundSymbol( mBackgroundSymbolButton->clonedSymbol< QgsFillSymbol >() ); + rectTextItem->setFrameSymbol( mFrameSymbolButton->clonedSymbol< QgsFillSymbol >() ); + + rectTextItem->setMargins( QgsMargins( mSpinLeftMargin->value(), + mSpinTopMargin->value(), + mSpinRightMargin->value(), + mSpinBottomMargin->value() ) ); + rectTextItem->setMarginsUnit( mMarginUnitWidget->unit() ); + + mPropertiesWidget->updateItem( rectTextItem ); + } +} + +void QgsAnnotationRectangleTextItemWidget::setDockMode( bool dockMode ) +{ + QgsAnnotationItemBaseWidget::setDockMode( dockMode ); + if ( mTextFormatWidget ) + mTextFormatWidget->setDockMode( dockMode ); +} + +void QgsAnnotationRectangleTextItemWidget::setContext( const QgsSymbolWidgetContext &context ) +{ + QgsAnnotationItemBaseWidget::setContext( context ); + if ( mTextFormatWidget ) + mTextFormatWidget->setContext( context ); + mBackgroundSymbolButton->setMapCanvas( context.mapCanvas() ); + mBackgroundSymbolButton->setMessageBar( context.messageBar() ); + mFrameSymbolButton->setMapCanvas( context.mapCanvas() ); + mFrameSymbolButton->setMessageBar( context.messageBar() ); + mPropertiesWidget->setContext( context ); +} + +QgsExpressionContext QgsAnnotationRectangleTextItemWidget::createExpressionContext() const +{ + QgsExpressionContext expressionContext; + if ( context().expressionContext() ) + expressionContext = *( context().expressionContext() ); + else + expressionContext = QgsProject::instance()->createExpressionContext(); + return expressionContext; +} + +void QgsAnnotationRectangleTextItemWidget::focusDefaultWidget() +{ + mTextEdit->selectAll(); + mTextEdit->setFocus(); +} + +QgsAnnotationRectangleTextItemWidget::~QgsAnnotationRectangleTextItemWidget() = default; + +bool QgsAnnotationRectangleTextItemWidget::setNewItem( QgsAnnotationItem *item ) +{ + QgsAnnotationRectangleTextItem *textItem = dynamic_cast< QgsAnnotationRectangleTextItem * >( item ); + if ( !textItem ) + return false; + + mItem.reset( textItem->clone() ); + + mBlockChangedSignal = true; + mTextFormatWidget->setFormat( mItem->format() ); + mTextEdit->setPlainText( mItem->text() ); + mAlignmentComboBox->setCurrentAlignment( mItem->alignment() & Qt::AlignHorizontal_Mask ); + mVerticalAlignmentComboBox->setCurrentAlignment( mItem->alignment() & Qt::AlignVertical_Mask ); + mPropertiesWidget->setItem( mItem.get() ); + + mBackgroundCheckbox->setChecked( textItem->backgroundEnabled() ); + if ( const QgsSymbol *symbol = textItem->backgroundSymbol() ) + mBackgroundSymbolButton->setSymbol( symbol->clone() ); + + mFrameCheckbox->setChecked( textItem->frameEnabled() ); + if ( const QgsSymbol *symbol = textItem->frameSymbol() ) + mFrameSymbolButton->setSymbol( symbol->clone() ); + + mMarginUnitWidget->setUnit( textItem->marginsUnit() ); + mSpinLeftMargin->setValue( textItem->margins().left() ); + mSpinTopMargin->setValue( textItem->margins().top() ); + mSpinRightMargin->setValue( textItem->margins().right() ); + mSpinBottomMargin->setValue( textItem->margins().bottom() ); + + mBlockChangedSignal = false; + + return true; +} + +void QgsAnnotationRectangleTextItemWidget::onWidgetChanged() +{ + if ( !mBlockChangedSignal ) + emit itemChanged(); +} + +void QgsAnnotationRectangleTextItemWidget::mInsertExpressionButton_clicked() +{ + QString expression = QgsExpressionFinder::findAndSelectActiveExpression( mTextEdit ); + + QgsExpressionBuilderDialog exprDlg( nullptr, expression, this, QStringLiteral( "generic" ), createExpressionContext() ); + + exprDlg.setWindowTitle( tr( "Insert Expression" ) ); + if ( exprDlg.exec() == QDialog::Accepted ) + { + expression = exprDlg.expressionText().trimmed(); + if ( !expression.isEmpty() ) + { + mTextEdit->insertPlainText( "[%" + expression + "%]" ); + } + } +} + + // // QgsAnnotationPictureItemWidget // diff --git a/src/gui/annotations/qgsannotationitemwidget_impl.h b/src/gui/annotations/qgsannotationitemwidget_impl.h index 44a9bcbee9c8..41c41e6455f2 100644 --- a/src/gui/annotations/qgsannotationitemwidget_impl.h +++ b/src/gui/annotations/qgsannotationitemwidget_impl.h @@ -25,6 +25,7 @@ #include "ui_qgsannotationpointtextwidgetbase.h" #include "ui_qgsannotationsymbolwidgetbase.h" #include "ui_qgsannotationlinetextwidgetbase.h" +#include "ui_qgsannotationrectangulartextwidgetbase.h" #include "ui_qgsannotationpicturewidgetbase.h" class QgsSymbolSelectorWidget; @@ -37,6 +38,7 @@ class QgsAnnotationMarkerItem; class QgsAnnotationPointTextItem; class QgsAnnotationLineTextItem; class QgsAnnotationPictureItem; +class QgsAnnotationRectangleTextItem; class QgsTextFormatWidget; #define SIP_NO_FILE @@ -141,6 +143,38 @@ class QgsAnnotationPointTextItemWidget : public QgsAnnotationItemBaseWidget, pri }; +class QgsAnnotationRectangleTextItemWidget : public QgsAnnotationItemBaseWidget, private Ui_QgsAnnotationRectangleTextWidgetBase, private QgsExpressionContextGenerator +{ + Q_OBJECT + + public: + QgsAnnotationRectangleTextItemWidget( QWidget *parent ); + ~QgsAnnotationRectangleTextItemWidget() override; + QgsAnnotationItem *createItem() override; + void updateItem( QgsAnnotationItem *item ) override; + void setDockMode( bool dockMode ) override; + void setContext( const QgsSymbolWidgetContext &context ) override; + QgsExpressionContext createExpressionContext() const override; + + public slots: + + void focusDefaultWidget() override; + + protected: + bool setNewItem( QgsAnnotationItem *item ) override; + + private slots: + + void onWidgetChanged(); + + private: + void mInsertExpressionButton_clicked(); + + QgsTextFormatWidget *mTextFormatWidget = nullptr; + bool mBlockChangedSignal = false; + std::unique_ptr< QgsAnnotationRectangleTextItem> mItem; +}; + class QgsAnnotationLineTextItemWidget : public QgsAnnotationItemBaseWidget, private Ui_QgsAnnotationLineTextWidgetBase { Q_OBJECT diff --git a/src/gui/annotations/qgscreateannotationitemmaptool_impl.cpp b/src/gui/annotations/qgscreateannotationitemmaptool_impl.cpp index e257be841cda..04459af3f032 100644 --- a/src/gui/annotations/qgscreateannotationitemmaptool_impl.cpp +++ b/src/gui/annotations/qgscreateannotationitemmaptool_impl.cpp @@ -20,6 +20,7 @@ #include "qgsannotationlineitem.h" #include "qgsannotationpolygonitem.h" #include "qgsannotationlinetextitem.h" +#include "qgsannotationrectangletextitem.h" #include "qgsannotationpictureitem.h" #include "qgsannotationlayer.h" #include "qgsstyle.h" @@ -376,6 +377,103 @@ QgsMapTool *QgsCreatePictureItemMapTool::mapTool() } + +// +// QgsCreateRectangleTextItemMapTool +// + +QgsCreateRectangleTextItemMapTool::QgsCreateRectangleTextItemMapTool( QgsMapCanvas *canvas, QgsAdvancedDigitizingDockWidget *cadDockWidget ) + : QgsMapToolAdvancedDigitizing( canvas, cadDockWidget ) + , mHandler( new QgsCreateAnnotationItemMapToolHandler( canvas, cadDockWidget, this ) ) +{ + setUseSnappingIndicator( true ); +} + +void QgsCreateRectangleTextItemMapTool::cadCanvasPressEvent( QgsMapMouseEvent *event ) +{ + if ( event->button() == Qt::RightButton && mRubberBand ) + { + mRubberBand.reset(); + cadDockWidget()->clearPoints(); + return; + } + + if ( event->button() != Qt::LeftButton ) + return; + + if ( !mRubberBand ) + { + mFirstPoint = event->snapPoint(); + mRect.setRect( mFirstPoint.x(), mFirstPoint.y(), mFirstPoint.x(), mFirstPoint.y() ); + + mRubberBand.reset( new QgsRubberBand( mCanvas, Qgis::GeometryType::Polygon ) ); + mRubberBand->setWidth( digitizingStrokeWidth() ); + QColor color = digitizingStrokeColor(); + + const double alphaScale = QgsSettingsRegistryCore::settingsDigitizingLineColorAlphaScale->value(); + color.setAlphaF( color.alphaF() * alphaScale ); + mRubberBand->setLineStyle( Qt::DotLine ); + mRubberBand->setStrokeColor( color ); + + const QColor fillColor = digitizingFillColor(); + mRubberBand->setFillColor( fillColor ); + } + else + { + mRubberBand.reset(); + + const QgsPointXY point1 = toLayerCoordinates( mHandler->targetLayer(), mFirstPoint ); + const QgsPointXY point2 = toLayerCoordinates( mHandler->targetLayer(), event->snapPoint() ); + + cadDockWidget()->clearPoints(); + + std::unique_ptr< QgsAnnotationRectangleTextItem > createdItem = std::make_unique< QgsAnnotationRectangleTextItem >( tr( "Text" ), QgsRectangle( point1, point2 ) ); + // newly created rect text items default to using symbology reference scale at the current map scale + createdItem->setUseSymbologyReferenceScale( true ); + createdItem->setSymbologyReferenceScale( canvas()->scale() ); + mHandler->pushCreatedItem( createdItem.release() ); + } +} + +void QgsCreateRectangleTextItemMapTool::cadCanvasMoveEvent( QgsMapMouseEvent *event ) +{ + if ( !mRubberBand ) + return; + + const QgsPointXY mapPoint = event->snapPoint(); + mRect.setBottomRight( mapPoint.toQPointF() ); + + mRubberBand->reset( Qgis::GeometryType::Polygon ); + mRubberBand->addPoint( mRect.bottomLeft(), false ); + mRubberBand->addPoint( mRect.bottomRight(), false ); + mRubberBand->addPoint( mRect.topRight(), false ); + mRubberBand->addPoint( mRect.topLeft(), true ); +} + +void QgsCreateRectangleTextItemMapTool::keyPressEvent( QKeyEvent *event ) +{ + if ( event->key() == Qt::Key_Escape ) + { + if ( mRubberBand ) + { + mRubberBand.reset(); + cadDockWidget()->clearPoints(); + event->ignore(); + } + } +} + +QgsCreateAnnotationItemMapToolHandler *QgsCreateRectangleTextItemMapTool::handler() +{ + return mHandler; +} + +QgsMapTool *QgsCreateRectangleTextItemMapTool::mapTool() +{ + return this; +} + + // // QgsCreateLineTextItemMapTool // diff --git a/src/gui/annotations/qgscreateannotationitemmaptool_impl.h b/src/gui/annotations/qgscreateannotationitemmaptool_impl.h index 2283736cd616..9f9544ad2f6e 100644 --- a/src/gui/annotations/qgscreateannotationitemmaptool_impl.h +++ b/src/gui/annotations/qgscreateannotationitemmaptool_impl.h @@ -102,6 +102,31 @@ class QgsCreatePolygonItemMapTool: public QgsMapToolCaptureAnnotationItem }; +class QgsCreateRectangleTextItemMapTool: public QgsMapToolAdvancedDigitizing, public QgsCreateAnnotationItemMapToolInterface +{ + Q_OBJECT + + public: + + QgsCreateRectangleTextItemMapTool( QgsMapCanvas *canvas, QgsAdvancedDigitizingDockWidget *cadDockWidget ); + + void cadCanvasPressEvent( QgsMapMouseEvent *event ) override; + void cadCanvasMoveEvent( QgsMapMouseEvent *event ) override; + void keyPressEvent( QKeyEvent *event ) override; + + QgsCreateAnnotationItemMapToolHandler *handler() override; + QgsMapTool *mapTool() override; + + private: + + QgsCreateAnnotationItemMapToolHandler *mHandler = nullptr; + + QRectF mRect; + QgsPointXY mFirstPoint; + QObjectUniquePtr< QgsRubberBand > mRubberBand; +}; + + class QgsCreatePictureItemMapTool: public QgsMapToolAdvancedDigitizing, public QgsCreateAnnotationItemMapToolInterface { Q_OBJECT @@ -129,7 +154,6 @@ class QgsCreatePictureItemMapTool: public QgsMapToolAdvancedDigitizing, public Q }; - class QgsCreateLineTextItemMapTool: public QgsMapToolCaptureAnnotationItem { Q_OBJECT diff --git a/src/ui/annotations/qgsannotationrectangulartextwidgetbase.ui b/src/ui/annotations/qgsannotationrectangulartextwidgetbase.ui new file mode 100644 index 000000000000..12da48554dae --- /dev/null +++ b/src/ui/annotations/qgsannotationrectangulartextwidgetbase.ui @@ -0,0 +1,366 @@ + + + QgsAnnotationRectangleTextWidgetBase + + + + 0 + 0 + 321 + 716 + + + + Rectangle Text Annotation + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + + + + Vertical alignment + + + + + + + Horizontal alignment + + + + + + + Background + + + true + + + + + + Symbol + + + + + + + + 0 + 0 + + + + Change… + + + + + + + + + + + + + Frame + + + true + + + + + + Symbol + + + + + + + + 0 + 0 + + + + Change… + + + + + + + + + + + 0 + 0 + + + + Insert/Edit Expression… + + + QToolButton::MenuButtonPopup + + + Qt::ToolButtonTextOnly + + + Qt::DownArrow + + + + + + + + 0 + 150 + + + + + + + + + + + Margins + + + + + + Bottom + + + + + + + + 1 + 0 + + + + + + + 6 + + + 0.000000000000000 + + + 10000000.000000000000000 + + + 0.200000000000000 + + + 1.000000000000000 + + + + + + + + 1 + 0 + + + + + + + 6 + + + 0.000000000000000 + + + 10000000.000000000000000 + + + 0.200000000000000 + + + 1.000000000000000 + + + + + + + Top + + + + + + + + 1 + 0 + + + + + + + 6 + + + 0.000000000000000 + + + 10000000.000000000000000 + + + 0.200000000000000 + + + 1.000000000000000 + + + + + + + + 1 + 0 + + + + + + + 6 + + + 0.000000000000000 + + + 10000000.000000000000000 + + + 0.200000000000000 + + + 1.000000000000000 + + + + + + + Units + + + + + + + Left + + + + + + + Right + + + + + + + + 10 + 0 + + + + Qt::StrongFocus + + + + + + + + + + + + + + QgsAnnotationItemCommonPropertiesWidget + QWidget +
qgsannotationitemcommonpropertieswidget.h
+ 1 +
+ + QgsSymbolButton + QToolButton +
qgssymbolbutton.h
+
+ + QgsDoubleSpinBox + QDoubleSpinBox +
qgsdoublespinbox.h
+
+ + QgsUnitSelectionWidget + QWidget +
qgsunitselectionwidget.h
+ 1 +
+ + QgsCollapsibleGroupBox + QGroupBox +
qgscollapsiblegroupbox.h
+ 1 +
+ + QgsAlignmentComboBox + QComboBox +
qgsalignmentcombobox.h
+
+
+ + +
diff --git a/tests/src/python/CMakeLists.txt b/tests/src/python/CMakeLists.txt index fe39fb474905..99ac4eed7e65 100644 --- a/tests/src/python/CMakeLists.txt +++ b/tests/src/python/CMakeLists.txt @@ -24,6 +24,7 @@ ADD_PYTHON_TEST(PyQgsAnnotationMarkerItem test_qgsannotationmarkeritem.py) ADD_PYTHON_TEST(PyQgsAnnotationPictureItem test_qgsannotationpictureitem.py) ADD_PYTHON_TEST(PyQgsAnnotationPointTextItem test_qgsannotationpointtextitem.py) ADD_PYTHON_TEST(PyQgsAnnotationPolygonItem test_qgsannotationpolygonitem.py) +ADD_PYTHON_TEST(PyQgsAnnotationRectangleTextItem test_qgsannotationrecttextitem.py) ADD_PYTHON_TEST(PyQgsApplication test_qgsapplication.py) ADD_PYTHON_TEST(PyQgsAttributeTableConfig test_qgsattributetableconfig.py) ADD_PYTHON_TEST(PyQgsAuthBasicMethod test_qgsauthbasicmethod.py) diff --git a/tests/src/python/test_qgsannotationrecttextitem.py b/tests/src/python/test_qgsannotationrecttextitem.py new file mode 100644 index 000000000000..dbc7f37d14b6 --- /dev/null +++ b/tests/src/python/test_qgsannotationrecttextitem.py @@ -0,0 +1,370 @@ +"""QGIS Unit tests for QgsAnnotationRectangleTextItem. + +From build dir, run: ctest -R QgsAnnotationRectangleTextItem -V + +.. note:: 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. +""" + +from qgis.PyQt.QtCore import Qt, QSize +from qgis.PyQt.QtGui import QColor, QImage, QPainter +from qgis.PyQt.QtXml import QDomDocument +from qgis.core import ( + Qgis, + QgsAnnotationItemEditOperationAddNode, + QgsAnnotationItemEditOperationDeleteNode, + QgsAnnotationItemEditOperationMoveNode, + QgsAnnotationItemEditOperationTranslateItem, + QgsAnnotationItemEditContext, + QgsAnnotationItemNode, + QgsAnnotationRectangleTextItem, + QgsCircularString, + QgsCoordinateReferenceSystem, + QgsCoordinateTransform, + QgsCurvePolygon, + QgsFillSymbol, + QgsLineString, + QgsMapSettings, + QgsPoint, + QgsPointXY, + QgsPolygon, + QgsProject, + QgsReadWriteContext, + QgsRectangle, + QgsRenderContext, + QgsVertexId, + QgsTextFormat, + QgsMargins +) +import unittest +from qgis.testing import start_app, QgisTestCase + +from utilities import getTestFont, unitTestDataPath + +start_app() +TEST_DATA_DIR = unitTestDataPath() + + +class TestQgsAnnotationRectangleTextItem(QgisTestCase): + + @classmethod + def control_path_prefix(cls): + return "annotation_layer" + + def testBasic(self): + item = QgsAnnotationRectangleTextItem('my text', QgsRectangle(10, 20, 30, 40)) + + self.assertEqual(item.text(), 'my text') + self.assertEqual(item.boundingBox().toString(3), '10.000,20.000 : 30.000,40.000') + + item.setBounds(QgsRectangle(100, 200, 300, 400)) + item.setZIndex(11) + item.setText('different text') + item.setBackgroundEnabled(True) + item.setFrameEnabled(True) + item.setAlignment(Qt.AlignmentFlag.AlignRight) + format = QgsTextFormat() + format.setSize(37) + item.setFormat(format) + item.setMargins(QgsMargins(1,2 ,3 ,4 )) + item.setMarginsUnit(Qgis.RenderUnit.Points) + + self.assertEqual(item.boundingBox().toString(3), '100.000,200.000 : 300.000,400.000') + self.assertEqual(item.text(), 'different text') + self.assertEqual(item.zIndex(), 11) + self.assertTrue(item.backgroundEnabled()) + self.assertTrue(item.frameEnabled()) + self.assertEqual(item.alignment(), Qt.AlignmentFlag.AlignRight) + self.assertEqual(item.format().size(), 37) + self.assertEqual(item.margins(), QgsMargins(1,2 ,3 ,4 )) + self.assertEqual(item.marginsUnit(), Qgis.RenderUnit.Points) + + item.setBackgroundSymbol(QgsFillSymbol.createSimple({'color': '200,100,100', 'outline_color': 'black'})) + item.setFrameSymbol(QgsFillSymbol.createSimple( + {'color': '100,200,250', 'outline_color': 'black'})) + self.assertEqual(item.backgroundSymbol()[0].color(), QColor(200, 100, 100)) + self.assertEqual(item.frameSymbol()[0].color(), + QColor(100, 200, 250)) + + def test_nodes(self): + """ + Test nodes for item + """ + item = QgsAnnotationRectangleTextItem('my text', QgsRectangle(10, 20, 30, 40)) + # nodes shouldn't form a closed ring + self.assertEqual(item.nodesV2(QgsAnnotationItemEditContext()), + [QgsAnnotationItemNode(QgsVertexId(0, 0, 0), QgsPointXY(10, 20), Qgis.AnnotationItemNodeType.VertexHandle), + QgsAnnotationItemNode(QgsVertexId(0, 0, 1), QgsPointXY(30, 20), Qgis.AnnotationItemNodeType.VertexHandle), + QgsAnnotationItemNode(QgsVertexId(0, 0, 2), QgsPointXY(30, 40), Qgis.AnnotationItemNodeType.VertexHandle), + QgsAnnotationItemNode(QgsVertexId(0, 0, 3), QgsPointXY(10, 40), Qgis.AnnotationItemNodeType.VertexHandle)]) + + def test_transform(self): + item = QgsAnnotationRectangleTextItem('my text', QgsRectangle(10, 20, 30, 40)) + + self.assertEqual(item.bounds().toString(3), '10.000,20.000 : 30.000,40.000') + + self.assertEqual(item.applyEditV2(QgsAnnotationItemEditOperationTranslateItem('', 100, 200), QgsAnnotationItemEditContext()), + Qgis.AnnotationItemEditOperationResult.Success) + self.assertEqual(item.bounds().toString(3), '110.000,220.000 : 130.000,240.000') + + def test_apply_move_node_edit(self): + item = QgsAnnotationRectangleTextItem('my text', QgsRectangle(10, 20, 30, 40)) + + self.assertEqual(item.bounds().toString(3), '10.000,20.000 : 30.000,40.000') + + self.assertEqual(item.applyEditV2(QgsAnnotationItemEditOperationMoveNode('', QgsVertexId(0, 0, 1), QgsPoint(30, 20), QgsPoint(17, 18)), + QgsAnnotationItemEditContext()), Qgis.AnnotationItemEditOperationResult.Success) + self.assertEqual(item.bounds().toString(3), '10.000,18.000 : 17.000,40.000') + self.assertEqual(item.applyEditV2(QgsAnnotationItemEditOperationMoveNode('', QgsVertexId(0, 0, 0), QgsPoint(10, 18), QgsPoint(5, 13)), + QgsAnnotationItemEditContext()), Qgis.AnnotationItemEditOperationResult.Success) + self.assertEqual(item.bounds().toString(3), '5.000,13.000 : 17.000,40.000') + self.assertEqual(item.applyEditV2(QgsAnnotationItemEditOperationMoveNode('', QgsVertexId(0, 0, 2), QgsPoint(17, 14), QgsPoint(18, 38)), + QgsAnnotationItemEditContext()), Qgis.AnnotationItemEditOperationResult.Success) + self.assertEqual(item.bounds().toString(3), '5.000,13.000 : 18.000,38.000') + self.assertEqual(item.applyEditV2(QgsAnnotationItemEditOperationMoveNode('', QgsVertexId(0, 0, 3), QgsPoint(5, 38), QgsPoint(2, 39)), + QgsAnnotationItemEditContext()), Qgis.AnnotationItemEditOperationResult.Success) + self.assertEqual(item.bounds().toString(3), '2.000,13.000 : 18.000,39.000') + + def test_apply_delete_node_edit(self): + item = QgsAnnotationRectangleTextItem('my text', QgsRectangle(10, 20, 30, 40)) + + self.assertEqual(item.applyEditV2(QgsAnnotationItemEditOperationDeleteNode('', QgsVertexId(0, 0, 1), QgsPoint(14, 13)), + QgsAnnotationItemEditContext()), Qgis.AnnotationItemEditOperationResult.Invalid) + + def test_apply_add_node_edit(self): + item = QgsAnnotationRectangleTextItem('my text', QgsRectangle(10, 20, 30, 40)) + + self.assertEqual(item.applyEditV2(QgsAnnotationItemEditOperationAddNode('', QgsPoint(15, 16)), + QgsAnnotationItemEditContext()), Qgis.AnnotationItemEditOperationResult.Invalid) + + def test_transient_move_operation(self): + item = QgsAnnotationRectangleTextItem('my text', QgsRectangle(10, 20, 30, 40)) + + self.assertEqual(item.bounds().toString(3), '10.000,20.000 : 30.000,40.000') + + res = item.transientEditResultsV2(QgsAnnotationItemEditOperationMoveNode('', QgsVertexId(0, 0, 1), QgsPoint(30, 20), QgsPoint(17, 18)), + QgsAnnotationItemEditContext() + ) + self.assertEqual(res.representativeGeometry().asWkt(), 'Polygon ((10 18, 17 18, 17 40, 10 40, 10 18))') + + def test_transient_translate_operation(self): + item = QgsAnnotationRectangleTextItem('my text', QgsRectangle(10, 20, 30, 40)) + + self.assertEqual(item.bounds().toString(3), '10.000,20.000 : 30.000,40.000') + + res = item.transientEditResultsV2(QgsAnnotationItemEditOperationTranslateItem('', 100, 200), + QgsAnnotationItemEditContext() + ) + self.assertEqual(res.representativeGeometry().asWkt(), 'Polygon ((110 220, 130 220, 130 240, 110 240, 110 220))') + + def testReadWriteXml(self): + doc = QDomDocument("testdoc") + elem = doc.createElement('test') + + item = QgsAnnotationRectangleTextItem('my text', QgsRectangle(10, 20, 30, 40)) + + item.setBackgroundEnabled(True) + item.setBackgroundSymbol(QgsFillSymbol.createSimple({'color': '200,100,100', 'outline_color': 'black'})) + item.setFrameEnabled(True) + item.setFrameSymbol(QgsFillSymbol.createSimple({'color': '100,200,150', 'outline_color': 'black'})) + item.setZIndex(11) + item.setAlignment(Qt.AlignmentFlag.AlignRight) + format = QgsTextFormat() + format.setSize(37) + item.setFormat(format) + item.setMargins(QgsMargins(1,2 ,3 ,4 )) + item.setMarginsUnit(Qgis.RenderUnit.Points) + + self.assertTrue(item.writeXml(elem, doc, QgsReadWriteContext())) + + s2 = QgsAnnotationRectangleTextItem.create() + self.assertTrue(s2.readXml(elem, QgsReadWriteContext())) + + self.assertEqual(s2.bounds().toString(3), '10.000,20.000 : 30.000,40.000') + self.assertEqual(s2.text(), 'my text') + self.assertEqual(s2.backgroundSymbol()[0].color(), QColor(200, 100, 100)) + self.assertEqual(s2.frameSymbol()[0].color(), + QColor(100, 200, 150)) + self.assertEqual(s2.zIndex(), 11) + self.assertTrue(s2.frameEnabled()) + self.assertTrue(s2.backgroundEnabled()) + self.assertEqual(s2.alignment(), Qt.AlignmentFlag.AlignRight) + self.assertEqual(s2.format().size(), 37) + self.assertEqual(s2.margins(), QgsMargins(1,2 ,3 ,4 )) + self.assertEqual(s2.marginsUnit(), Qgis.RenderUnit.Points) + + def testClone(self): + item = QgsAnnotationRectangleTextItem('my text', QgsRectangle(10, 20, 30, 40)) + + item.setBackgroundEnabled(True) + item.setBackgroundSymbol(QgsFillSymbol.createSimple({'color': '200,100,100', 'outline_color': 'black'})) + item.setFrameEnabled(True) + item.setFrameSymbol(QgsFillSymbol.createSimple({'color': '100,200,150', 'outline_color': 'black'})) + item.setZIndex(11) + item.setAlignment(Qt.AlignmentFlag.AlignRight) + format = QgsTextFormat() + format.setSize(37) + item.setFormat(format) + item.setMargins(QgsMargins(1,2 ,3 ,4 )) + item.setMarginsUnit(Qgis.RenderUnit.Points) + + s2 = item.clone() + self.assertEqual(s2.bounds().toString(3), '10.000,20.000 : 30.000,40.000') + self.assertEqual(s2.text(), 'my text') + self.assertEqual(s2.backgroundSymbol()[0].color(), QColor(200, 100, 100)) + self.assertEqual(s2.frameSymbol()[0].color(), + QColor(100, 200, 150)) + self.assertEqual(s2.zIndex(), 11) + self.assertTrue(s2.frameEnabled()) + self.assertTrue(s2.backgroundEnabled()) + self.assertEqual(s2.alignment(), Qt.AlignmentFlag.AlignRight) + self.assertEqual(s2.format().size(), 37) + self.assertEqual(s2.margins(), QgsMargins(1,2 ,3 ,4 )) + self.assertEqual(s2.marginsUnit(), Qgis.RenderUnit.Points) + + def testRender(self): + item = QgsAnnotationRectangleTextItem('my text', QgsRectangle(12, 13, 14, 15)) + item.setMargins(QgsMargins(1, 0.5, 1, 0)) + item.setFrameSymbol(QgsFillSymbol.createSimple( + {'color': '0,0,0,0', 'outline_color': 'black', 'outline_width': 2})) + + format = QgsTextFormat.fromQFont(getTestFont('Bold')) + format.setColor(QColor(255, 0, 0)) + format.setOpacity(150 / 255) + format.setSize(20) + item.setFormat(format) + + settings = QgsMapSettings() + settings.setDestinationCrs(QgsCoordinateReferenceSystem('EPSG:4326')) + settings.setExtent(QgsRectangle(10, 10, 18, 18)) + settings.setOutputSize(QSize(300, 300)) + + settings.setFlag(QgsMapSettings.Flag.Antialiasing, False) + + rc = QgsRenderContext.fromMapSettings(settings) + image = QImage(200, 200, QImage.Format.Format_ARGB32) + image.setDotsPerMeterX(int(96 / 25.4 * 1000)) + image.setDotsPerMeterY(int(96 / 25.4 * 1000)) + image.fill(QColor(255, 255, 255)) + painter = QPainter(image) + rc.setPainter(painter) + + try: + item.render(rc, None) + finally: + painter.end() + + self.assertTrue(self.image_check('recttext_render', 'recttext_render', image)) + + def testRenderAlignment(self): + item = QgsAnnotationRectangleTextItem('my text', QgsRectangle(12, 13, 14, 15)) + item.setMargins(QgsMargins(1, 0.5, 1, 0)) + item.setFrameSymbol(QgsFillSymbol.createSimple( + {'color': '0,0,0,0', 'outline_color': 'black', 'outline_width': 2})) + + format = QgsTextFormat.fromQFont(getTestFont('Bold')) + format.setColor(QColor(255, 0, 0)) + format.setOpacity(150 / 255) + format.setSize(20) + item.setFormat(format) + + item.setAlignment(Qt.AlignRight | Qt.AlignBottom) + + settings = QgsMapSettings() + settings.setDestinationCrs(QgsCoordinateReferenceSystem('EPSG:4326')) + settings.setExtent(QgsRectangle(10, 10, 18, 18)) + settings.setOutputSize(QSize(300, 300)) + + settings.setFlag(QgsMapSettings.Flag.Antialiasing, False) + + rc = QgsRenderContext.fromMapSettings(settings) + image = QImage(200, 200, QImage.Format.Format_ARGB32) + image.setDotsPerMeterX(int(96 / 25.4 * 1000)) + image.setDotsPerMeterY(int(96 / 25.4 * 1000)) + image.fill(QColor(255, 255, 255)) + painter = QPainter(image) + rc.setPainter(painter) + + try: + item.render(rc, None) + finally: + painter.end() + + self.assertTrue(self.image_check('recttext_render_align', 'recttext_render_align', image)) + + def testRenderWithTransform(self): + item = QgsAnnotationRectangleTextItem('my text', QgsRectangle(11.5, 13, 12, 13.5)) + + format = QgsTextFormat.fromQFont(getTestFont('Bold')) + format.setColor(QColor(255, 0, 0)) + format.setOpacity(150 / 255) + format.setSize(20) + item.setFormat(format) + item.setFrameSymbol(QgsFillSymbol.createSimple( + {'color': '0,0,0,0', 'outline_color': 'black', 'outline_width': 1})) + + settings = QgsMapSettings() + settings.setDestinationCrs(QgsCoordinateReferenceSystem('EPSG:3857')) + settings.setExtent(QgsRectangle(1250958, 1386945, 1420709, 1532518)) + settings.setOutputSize(QSize(300, 300)) + + settings.setFlag(QgsMapSettings.Flag.Antialiasing, False) + + rc = QgsRenderContext.fromMapSettings(settings) + rc.setCoordinateTransform(QgsCoordinateTransform(QgsCoordinateReferenceSystem('EPSG:4326'), settings.destinationCrs(), QgsProject.instance())) + image = QImage(200, 200, QImage.Format.Format_ARGB32) + image.setDotsPerMeterX(int(96 / 25.4 * 1000)) + image.setDotsPerMeterY(int(96 / 25.4 * 1000)) + image.fill(QColor(255, 255, 255)) + painter = QPainter(image) + rc.setPainter(painter) + + try: + item.render(rc, None) + finally: + painter.end() + + self.assertTrue(self.image_check('recttext_render_transform', 'recttext_render_transform', image)) + + def testRenderBackgroundFrame(self): + item = QgsAnnotationRectangleTextItem('my text', QgsRectangle(12, 13, 16, 15)) + + item.setFrameEnabled(True) + item.setBackgroundEnabled(True) + item.setBackgroundSymbol(QgsFillSymbol.createSimple({'color': '200,100,100', 'outline_color': 'black'})) + item.setFrameSymbol(QgsFillSymbol.createSimple( + {'color': '100,200,250,120', 'outline_color': 'black', 'outline_width': 2})) + + format = QgsTextFormat.fromQFont(getTestFont('Bold')) + format.setColor(QColor(255, 0, 0)) + format.setOpacity(150 / 255) + format.setSize(20) + item.setFormat(format) + + settings = QgsMapSettings() + settings.setDestinationCrs(QgsCoordinateReferenceSystem('EPSG:4326')) + settings.setExtent(QgsRectangle(10, 10, 18, 18)) + settings.setOutputSize(QSize(300, 300)) + + settings.setFlag(QgsMapSettings.Flag.Antialiasing, False) + + rc = QgsRenderContext.fromMapSettings(settings) + image = QImage(200, 200, QImage.Format.Format_ARGB32) + image.setDotsPerMeterX(int(96 / 25.4 * 1000)) + image.setDotsPerMeterY(int(96 / 25.4 * 1000)) + image.fill(QColor(255, 255, 255)) + painter = QPainter(image) + rc.setPainter(painter) + + try: + item.render(rc, None) + finally: + painter.end() + + self.assertTrue(self.image_check('recttext_background_frame', 'recttext_background_frame', image)) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/testdata/control_images/annotation_layer/expected_recttext_background_frame/expected_recttext_background_frame.png b/tests/testdata/control_images/annotation_layer/expected_recttext_background_frame/expected_recttext_background_frame.png new file mode 100644 index 0000000000000000000000000000000000000000..f42da355a30cfb7af6765119aee7a872cbd1cc08 GIT binary patch literal 1041 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k4M?tyST_$yaTa()7BevL9R^{>l=51EY%1ovz&x~Y}n zQ(x|Vs691uTK)mg{b%GDY>XQn1UOhm2~ZgPu&ZXMF0_zI<8S0#qriOR<35(6MvHs1 z9hxj)VKlum|HJY3^=IXn%F|dI6IdVG*VWYgd0##M_~R)XW0GHgjCNGuU}B}!}oR{5x^oRe(rko}B7Sz|3z4YCa+H~H`$}!(E?^u`01ooS+&f0#r zFy3yQJ-@mn^-tBEHLJ7rKIdh;4a+L=E;{=p-nuf&Cwp7z^S=Gxw)W*j@4svC&wGu6 zv}CRDKDqL@E>d${&gYaoimyHs#!{pny|1&{O8n;K2=^OFwigzwonJifd7S#C#o;XS z2e~!{FAhJpqgOOV-Oc}+{7><3S7vOh_~$+S%-m*!C!*o~>A!*gElc57KM?Kz;pY0A zVy|28WE)*ITlFq>&0h(RxijWh^#!jle-d|U*8Rz%De3S2ikF@~{{Q30!eE)`JI1@F zI>a40^IH!Xg1+bNtG-v8Owz3vw>vAXS39fx+W#f?zw7_np0@4LwO~&yuI70t6TL?K zgO~89n-Pv>k3`>BzG)OqdA>&OyXpj4;p2aACcSNuc($R5E{WPg=k%sCaz{2>$?$D% zKadh8z`@k003FVdQ&MBb@08JlurT_o{ literal 0 HcmV?d00001 diff --git a/tests/testdata/control_images/annotation_layer/expected_recttext_background_frame/expected_recttext_background_frame_mask.png b/tests/testdata/control_images/annotation_layer/expected_recttext_background_frame/expected_recttext_background_frame_mask.png new file mode 100644 index 0000000000000000000000000000000000000000..610ae2a4a91e811312f53e1b7d5c41d6f6c98ae9 GIT binary patch literal 892 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k4M?tyST_$yaTa()7BevLCxS5J3j5`m3=GVA zo-U3d6?5L+iuSwhz~FE(pj3YI|I}2~55Y^OSxvY3Goz(g#k;1ZLgv}Ai-%PvNy4$wf+aI`cOk(-xpL>4S#Wwq-tu6f> z_xk_RziVp$ZWTGc^#1)vJNWH;_Z}*HB9wUT=}E!kyT7GBVy+3&yco8PN#_uc$% z6W3_7QZjF$Uhl(8Pv&YqispFYS?jk$(y20af4>|pt%cj~)H1Ggt8a-l;X`v!~0{-9Be_e%sNz+sEbz3)o=+~WQ{`~XL+uv)y=xh{i+|wbcc6qPf-{m!J*@EIP`F~Ug#TC0hzqQ-$ zyHAg<`sG>u`)m3>?{;^8Rgw4ijGW~DcG3A4bndUSZ*)-D!PclCz`->5h!2cy!o5{b TC(9fFW+w(uS3j3^P6hxqvVy7<69dt+_SS^vMkZ`<9BZ~LTr1fOV> zKKTD@_fq9!lbG*hY~gO4lX{ME&vmo=eX_Ri&IC0s79LEWDdL}yDlDo8lK zJ9MIuqkQ9`*!O2-9H@|7UYA{8`*@oxfFWOV1w;66Q1f&M^0L^ZMz3qQxI<=6s^#eg0l{ zlF;$AWf6&=&3*Q6F3mD2->tem&-~sq)1!}eU5mSI;HeP4?TL=v*0n3IYTfhP-=%#1 z$D@d|983o}idd`XJY2pn^t#5TF2N^Uc7={_7uAY3zIiB|{yeH_)6=p7Te)Qs?JJwV zU9A?`|8LKi?1lX`#utPtluu-4)n2{B+8tb2QJ-Sm_~z-E_pb53B&IDmleW_H$B$2) zjr@0Z>zN9A?0DT>*RuPacCZd=Tq9x~%CZaGaqtRP*s*j-Xf!HJFnG26!Me03e>Em> zD(~MBZt&}$@Cly9`%k3rJX}{iyL_4To*O!F)yU@AGp;}O@^b)l>rDnA@O1TaS?83{ F1OP89f*Sw; literal 0 HcmV?d00001 diff --git a/tests/testdata/control_images/annotation_layer/expected_recttext_render/expected_recttext_render_mask.png b/tests/testdata/control_images/annotation_layer/expected_recttext_render/expected_recttext_render_mask.png new file mode 100644 index 0000000000000000000000000000000000000000..e7107cd4f46b1b364d082652cdcf2910d6c3d19a GIT binary patch literal 828 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k4M?tyST_$yaTa()7BevLCxS5J3j5`m3=B-~ zJY5_^D(1Ys>zMb-K*ZtVmS3w=9q;MM{)uGW?(12=@!TUnWJ%Qp?c177{i%`bxC<^n zf5UoUA}33eg967WL2MYzOX%6yV^~+id;If8oztb?lpd^jqrjdPZrQfl@_&!vvh@#L z(|+x`iD&{OpXcyEJaHH@;BKw z&N=aYcUyv-`UK%(%lbLs2r2*Uw4r&nT-k-Npo8kU#?r0pPY8TaLcdX7Dh}F&5{DD{O2h(Boq+{R1GZPqqz|+;wWt~$(69DR36z~85 literal 0 HcmV?d00001 diff --git a/tests/testdata/control_images/annotation_layer/expected_recttext_render_align/expected_recttext_render_align.png b/tests/testdata/control_images/annotation_layer/expected_recttext_render_align/expected_recttext_render_align.png new file mode 100644 index 0000000000000000000000000000000000000000..131cb7c6d5fb536a399aa6737a93e19daaa9ea13 GIT binary patch literal 1105 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k4M?tyST_$yaTa()7BevL9R^{>O41<>cc6*r@Tg*Q{|6c3EnDy8F-yceHo4|SG<1AYl^G2JE z?UVm_E|#}%Y`N(FF07|UhLy=tL4bwnVD3HEZw1oDy2}6O1onVLz`_KHJ&Oxcrl0;V zUtha-A20vh_4mwwUD&*R;=aEZ?#A7@zi;2W|G(cpm*4yCeD164P@nQ`e}8PO5)NK- z@k*1@^i$ERF>*WvK(S%Ub7-9IH$T()1H@9y}=(zQzL+~$?Hr_|n5R6db$ZI?p) z%rx185}^|>-m!kpaw(4A{`ul#6@htH4^|XvsS!rh@qfze z|5x)u!|ne6R&b~t6v<{O{`bjh^`W?h-ANuQS{3q_S8t3GVSx5b)W!EwA_eKhvYTVtNb? za%@t4za!v!Mb4K-&Xm{J>Xz-im?Rdw#(&1{ZLg|-i%)O-^Yq@_wU)tanb-0fH|@DS z%fBzSnDf*m(-?N83H1-BdQ6L%5v>1ZW&Gm<6C<*O%(m7%`nKr)1J>Oulea20w(Onv z1rZ>5pY=ZEL>>GAPd7qtdrfzmD$Bb!+8BO1dAI3e|9l5EiN7lKr{wx(-`*qq_F;(&(p{Crk}ol{_eef-~U}&U-_l>+Pc>ncFQt% z^~JHgnW}s3;GN%Z&lRtG_BUDe61ZK?tsOpOWH94t-Vz6v|o6E_;mp8Ie5r^|Eme#>8>yR_4OFrAS;6WLtI*7$%2 zJwgu#_vgL2JAc{dyVw8g)N8aPbh84Ze2$}8(e2&(l{*{V^-Vv#NadBgTUXoveqY(y zU$4LQC;SkdoIbxg{FGhs*Ef;t_uS2B(|_>A_z`=Q%jsj1Pki_M+W)g}k80J{hf`e$hYj@2H-*B|)fn19FpJn%#KkOHo@n62bCfDw; zc!JnDoAcex6fccCRIrXLD1>;|RB#@`qUtee4nF zbWq?a)qS@_Elr^L-Y3td@S~O6jXt|6N{xx?Z&9>SGZWCP#(r$fC^YmjzBtlWTqW;gt!? zCN<%D3#Oa1eyc54o3NELK?CTj3E`V|&FOR4rqGfYv&>QM|Ht>2P0IeK>;9GaqrkUa zeqIV|V z|9bV8lb*-km440g$AqOQ`(*z6bmgC~Y-O4p_I>*4@U=D+MnQmOleqfpv#Qn7wxL`T?6#Z-_=gYjH+Y*y<528-1nWb zo3HJi!W_q0ET57RJ-1w@K*{+04eb-l*V+kfDtQ`FoYQ;%VutpKYL-K%d$!5TBsmH| z{0a6fO^F{nzq92$E!z2JFDK6dE?`vWq|I8k*Zg7TVHTr9=Oj*Q{7zm`JRz`*>$u?= hE~aR@xeH;@58n3eFAO;(BvToHz|+;wWt~$(69BBtjr0Hj literal 0 HcmV?d00001 diff --git a/tests/testdata/control_images/annotation_layer/expected_recttext_render_transform/expected_recttext_render_transform_mask.png b/tests/testdata/control_images/annotation_layer/expected_recttext_render_transform/expected_recttext_render_transform_mask.png new file mode 100644 index 0000000000000000000000000000000000000000..84fded0692398bc0ea610864c60d3bf115075a6f GIT binary patch literal 843 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k4M?tyST_$yaTa()7BevLCxS5J3j5`m3=B+v zJzX3_D(1YsbJ6dSgG9r{T^}Adsr&O=`Pcxlnr*nFf zMYDs&Q^^#za{<2%Ym`e`Htw{47Lnw*;YgE$00&d0;q1?~8`&El@GvqrrCHJU`v3TonFg1eA024E(ocDhQ zeaK$$Yp1!~McaCvjcO0NuI=HI zKVe(lIDg->*Ppk=rR#6X|5td&{9k?ZZ|{0LrpAIFs$zfI21~5cI)(jV4f&WWwx#fX R88CM+c)I$ztaD0e0s!d_7>ED> literal 0 HcmV?d00001