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
+
+
+
+ QgsDoubleSpinBox
+ QDoubleSpinBox
+
+
+
+ QgsUnitSelectionWidget
+ QWidget
+
+ 1
+
+
+ QgsCollapsibleGroupBox
+ QGroupBox
+
+ 1
+
+
+ QgsAlignmentComboBox
+ QComboBox
+
+
+
+
+
+
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 000000000000..f42da355a30c
Binary files /dev/null and b/tests/testdata/control_images/annotation_layer/expected_recttext_background_frame/expected_recttext_background_frame.png differ
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 000000000000..610ae2a4a91e
Binary files /dev/null and b/tests/testdata/control_images/annotation_layer/expected_recttext_background_frame/expected_recttext_background_frame_mask.png differ
diff --git a/tests/testdata/control_images/annotation_layer/expected_recttext_render/expected_recttext_render.png b/tests/testdata/control_images/annotation_layer/expected_recttext_render/expected_recttext_render.png
new file mode 100644
index 000000000000..5410dedd9fc1
Binary files /dev/null and b/tests/testdata/control_images/annotation_layer/expected_recttext_render/expected_recttext_render.png differ
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 000000000000..e7107cd4f46b
Binary files /dev/null and b/tests/testdata/control_images/annotation_layer/expected_recttext_render/expected_recttext_render_mask.png differ
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 000000000000..131cb7c6d5fb
Binary files /dev/null and b/tests/testdata/control_images/annotation_layer/expected_recttext_render_align/expected_recttext_render_align.png differ
diff --git a/tests/testdata/control_images/annotation_layer/expected_recttext_render_align/expected_recttext_render_align_mask.png b/tests/testdata/control_images/annotation_layer/expected_recttext_render_align/expected_recttext_render_align_mask.png
new file mode 100644
index 000000000000..d98fd66887e0
Binary files /dev/null and b/tests/testdata/control_images/annotation_layer/expected_recttext_render_align/expected_recttext_render_align_mask.png differ
diff --git a/tests/testdata/control_images/annotation_layer/expected_recttext_render_transform/expected_recttext_render_transform.png b/tests/testdata/control_images/annotation_layer/expected_recttext_render_transform/expected_recttext_render_transform.png
new file mode 100644
index 000000000000..1e1da602e0a6
Binary files /dev/null and b/tests/testdata/control_images/annotation_layer/expected_recttext_render_transform/expected_recttext_render_transform.png differ
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 000000000000..84fded069239
Binary files /dev/null and b/tests/testdata/control_images/annotation_layer/expected_recttext_render_transform/expected_recttext_render_transform_mask.png differ