mirror of
https://github.com/qgis/QGIS.git
synced 2025-10-15 00:02:52 -04:00
[api] Add text-along-line annotation item type
This annotation item renders curved text along a linestring
This commit is contained in:
parent
b9c0fc46b5
commit
cd3a1bf237
@ -38,6 +38,10 @@ Abstract base class for annotation items which are drawn with :py:class:`QgsAnno
|
||||
{
|
||||
sipType = sipType_QgsAnnotationPointTextItem;
|
||||
}
|
||||
else if ( sipCpp->type() == QLatin1String( "linetext" ) )
|
||||
{
|
||||
sipType = sipType_QgsAnnotationLineTextItem;
|
||||
}
|
||||
else
|
||||
{
|
||||
sipType = 0;
|
||||
|
@ -25,7 +25,7 @@ An annotation item which renders a line symbol along a line geometry.
|
||||
|
||||
QgsAnnotationLineItem( QgsCurve *curve /Transfer/ );
|
||||
%Docstring
|
||||
Constructor for QgsAnnotationLineItem, with the specified ``linestring``.
|
||||
Constructor for QgsAnnotationLineItem, with the specified ``curve``.
|
||||
%End
|
||||
~QgsAnnotationLineItem();
|
||||
|
||||
|
@ -0,0 +1,118 @@
|
||||
/************************************************************************
|
||||
* This file has been generated automatically from *
|
||||
* *
|
||||
* src/core/annotations/qgsannotationlinetextitem.h *
|
||||
* *
|
||||
* Do not edit manually ! Edit header and run scripts/sipify.pl again *
|
||||
************************************************************************/
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class QgsAnnotationLineTextItem : QgsAnnotationItem
|
||||
{
|
||||
%Docstring(signature="appended")
|
||||
An annotation item which renders text along a line geometry.
|
||||
|
||||
.. versionadded:: 3.32
|
||||
%End
|
||||
|
||||
%TypeHeaderCode
|
||||
#include "qgsannotationlinetextitem.h"
|
||||
%End
|
||||
public:
|
||||
|
||||
QgsAnnotationLineTextItem( const QString &text, QgsCurve *curve /Transfer/ );
|
||||
%Docstring
|
||||
Constructor for QgsAnnotationLineTextItem, with the specified ``curve`` and ``text``.
|
||||
%End
|
||||
~QgsAnnotationLineTextItem();
|
||||
|
||||
virtual Qgis::AnnotationItemFlags flags() const;
|
||||
|
||||
virtual QString type() const;
|
||||
|
||||
virtual void render( QgsRenderContext &context, QgsFeedback *feedback );
|
||||
|
||||
virtual bool writeXml( QDomElement &element, QDomDocument &document, const QgsReadWriteContext &context ) const;
|
||||
|
||||
virtual QList< QgsAnnotationItemNode > nodes() const;
|
||||
|
||||
virtual Qgis::AnnotationItemEditOperationResult applyEdit( QgsAbstractAnnotationItemEditOperation *operation );
|
||||
|
||||
virtual QgsAnnotationItemEditOperationTransientResults *transientEditResults( QgsAbstractAnnotationItemEditOperation *operation ) /Factory/;
|
||||
|
||||
|
||||
static QgsAnnotationLineTextItem *create() /Factory/;
|
||||
%Docstring
|
||||
Creates a new linestring annotation item.
|
||||
%End
|
||||
|
||||
virtual bool readXml( const QDomElement &element, const QgsReadWriteContext &context );
|
||||
|
||||
virtual QgsRectangle boundingBox() const;
|
||||
|
||||
virtual QgsRectangle boundingBox( QgsRenderContext &context ) const;
|
||||
|
||||
|
||||
virtual QgsAnnotationLineTextItem *clone() /Factory/;
|
||||
|
||||
|
||||
const QgsCurve *geometry() const;
|
||||
%Docstring
|
||||
Returns the geometry of the item.
|
||||
|
||||
The coordinate reference system for the line will be the parent layer's :py:func:`QgsAnnotationLayer.crs()`.
|
||||
|
||||
.. seealso:: :py:func:`setGeometry`
|
||||
%End
|
||||
|
||||
void setGeometry( QgsCurve *geometry /Transfer/ );
|
||||
%Docstring
|
||||
Sets the ``geometry`` of the item. Ownership of ``geometry`` is transferred.
|
||||
|
||||
The coordinate reference system for the line will be the parent layer's :py:func:`QgsAnnotationLayer.crs()`.
|
||||
|
||||
.. seealso:: :py:func:`geometry`
|
||||
%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
|
||||
|
||||
private:
|
||||
QgsAnnotationLineTextItem( const QgsAnnotationLineTextItem &other );
|
||||
};
|
||||
|
||||
/************************************************************************
|
||||
* This file has been generated automatically from *
|
||||
* *
|
||||
* src/core/annotations/qgsannotationlinetextitem.h *
|
||||
* *
|
||||
* Do not edit manually ! Edit header and run scripts/sipify.pl again *
|
||||
************************************************************************/
|
@ -227,6 +227,7 @@
|
||||
%Include auto_generated/annotations/qgsannotationitemregistry.sip
|
||||
%Include auto_generated/annotations/qgsannotationlayer.sip
|
||||
%Include auto_generated/annotations/qgsannotationlineitem.sip
|
||||
%Include auto_generated/annotations/qgsannotationlinetextitem.sip
|
||||
%Include auto_generated/annotations/qgsannotationmarkeritem.sip
|
||||
%Include auto_generated/annotations/qgsannotationmanager.sip
|
||||
%Include auto_generated/annotations/qgsannotationpointtextitem.sip
|
||||
|
@ -207,6 +207,7 @@ set(QGIS_CORE_SRCS
|
||||
annotations/qgsannotationlayer.cpp
|
||||
annotations/qgsannotationlayerrenderer.cpp
|
||||
annotations/qgsannotationlineitem.cpp
|
||||
annotations/qgsannotationlinetextitem.cpp
|
||||
annotations/qgsannotationmarkeritem.cpp
|
||||
annotations/qgsannotationmanager.cpp
|
||||
annotations/qgsannotationpointtextitem.cpp
|
||||
@ -1269,6 +1270,7 @@ set(QGIS_CORE_HDRS
|
||||
annotations/qgsannotationlayer.h
|
||||
annotations/qgsannotationlayerrenderer.h
|
||||
annotations/qgsannotationlineitem.h
|
||||
annotations/qgsannotationlinetextitem.h
|
||||
annotations/qgsannotationmarkeritem.h
|
||||
annotations/qgsannotationmanager.h
|
||||
annotations/qgsannotationpointtextitem.h
|
||||
|
@ -20,8 +20,6 @@
|
||||
#include "qgis_core.h"
|
||||
#include "qgis_sip.h"
|
||||
#include "qgscoordinatereferencesystem.h"
|
||||
#include "qgslinestring.h"
|
||||
#include "qgspolygon.h"
|
||||
|
||||
class QgsFeedback;
|
||||
class QgsMarkerSymbol;
|
||||
@ -60,6 +58,10 @@ class CORE_EXPORT QgsAnnotationItem
|
||||
{
|
||||
sipType = sipType_QgsAnnotationPointTextItem;
|
||||
}
|
||||
else if ( sipCpp->type() == QLatin1String( "linetext" ) )
|
||||
{
|
||||
sipType = sipType_QgsAnnotationLineTextItem;
|
||||
}
|
||||
else
|
||||
{
|
||||
sipType = 0;
|
||||
|
@ -20,6 +20,7 @@
|
||||
#include "qgsannotationlineitem.h"
|
||||
#include "qgsannotationpolygonitem.h"
|
||||
#include "qgsannotationpointtextitem.h"
|
||||
#include "qgsannotationlinetextitem.h"
|
||||
#include <QDomElement>
|
||||
|
||||
QgsAnnotationItemRegistry::QgsAnnotationItemRegistry( QObject *parent )
|
||||
@ -45,6 +46,8 @@ bool QgsAnnotationItemRegistry::populate()
|
||||
QgsAnnotationPolygonItem::create ) );
|
||||
mMetadata.insert( QStringLiteral( "pointtext" ), new QgsAnnotationItemMetadata( QStringLiteral( "pointtext" ), QObject::tr( "Text at point" ), QObject::tr( "Text at points" ),
|
||||
QgsAnnotationPointTextItem::create ) );
|
||||
mMetadata.insert( QStringLiteral( "linetext" ), new QgsAnnotationItemMetadata( QStringLiteral( "linetext" ), QObject::tr( "Text along line" ), QObject::tr( "Text along lines" ),
|
||||
QgsAnnotationLineTextItem::create ) );
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -21,6 +21,8 @@
|
||||
#include "qgslinesymbol.h"
|
||||
#include "qgsannotationitemnode.h"
|
||||
#include "qgsannotationitemeditoperation.h"
|
||||
#include "qgscurve.h"
|
||||
#include "qgslinestring.h"
|
||||
|
||||
QgsAnnotationLineItem::QgsAnnotationLineItem( QgsCurve *curve )
|
||||
: QgsAnnotationItem()
|
||||
@ -201,6 +203,8 @@ QgsAnnotationLineItem *QgsAnnotationLineItem::clone()
|
||||
return item.release();
|
||||
}
|
||||
|
||||
void QgsAnnotationLineItem::setGeometry( QgsCurve *geometry ) { mCurve.reset( geometry ); }
|
||||
|
||||
const QgsLineSymbol *QgsAnnotationLineItem::symbol() const
|
||||
{
|
||||
return mSymbol.get();
|
||||
|
@ -35,7 +35,7 @@ class CORE_EXPORT QgsAnnotationLineItem : public QgsAnnotationItem
|
||||
public:
|
||||
|
||||
/**
|
||||
* Constructor for QgsAnnotationLineItem, with the specified \a linestring.
|
||||
* Constructor for QgsAnnotationLineItem, with the specified \a curve.
|
||||
*/
|
||||
QgsAnnotationLineItem( QgsCurve *curve SIP_TRANSFER );
|
||||
~QgsAnnotationLineItem() override;
|
||||
@ -73,7 +73,7 @@ class CORE_EXPORT QgsAnnotationLineItem : public QgsAnnotationItem
|
||||
*
|
||||
* \see geometry()
|
||||
*/
|
||||
void setGeometry( QgsCurve *geometry SIP_TRANSFER ) { mCurve.reset( geometry ); }
|
||||
void setGeometry( QgsCurve *geometry SIP_TRANSFER );
|
||||
|
||||
/**
|
||||
* Returns the symbol used to render the item.
|
||||
|
250
src/core/annotations/qgsannotationlinetextitem.cpp
Normal file
250
src/core/annotations/qgsannotationlinetextitem.cpp
Normal file
@ -0,0 +1,250 @@
|
||||
/***************************************************************************
|
||||
qgsannotationlinetextitem.cpp
|
||||
----------------
|
||||
begin : March 2023
|
||||
copyright : (C) 2023 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 "qgsannotationlinetextitem.h"
|
||||
#include "qgsannotationitemnode.h"
|
||||
#include "qgsannotationitemeditoperation.h"
|
||||
#include "qgsrendercontext.h"
|
||||
#include "qgscurve.h"
|
||||
#include "qgslinestring.h"
|
||||
#include "qgstextrenderer.h"
|
||||
|
||||
QgsAnnotationLineTextItem::QgsAnnotationLineTextItem( const QString &text, QgsCurve *curve )
|
||||
: QgsAnnotationItem()
|
||||
, mText( text )
|
||||
, mCurve( curve )
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
Qgis::AnnotationItemFlags QgsAnnotationLineTextItem::flags() const
|
||||
{
|
||||
// in truth this should depend on whether the text format is scale dependent or not!
|
||||
return Qgis::AnnotationItemFlag::ScaleDependentBoundingBox;
|
||||
}
|
||||
|
||||
QgsAnnotationLineTextItem::~QgsAnnotationLineTextItem() = default;
|
||||
|
||||
QString QgsAnnotationLineTextItem::type() const
|
||||
{
|
||||
return QStringLiteral( "linetext" );
|
||||
}
|
||||
|
||||
void QgsAnnotationLineTextItem::render( QgsRenderContext &context, QgsFeedback * )
|
||||
{
|
||||
// TODO -- expose as an option!
|
||||
QgsGeometry smoothed( mCurve->clone() );
|
||||
smoothed = smoothed.smooth( );
|
||||
|
||||
QPolygonF pts = smoothed.asQPolygonF();
|
||||
|
||||
//transform the QPolygonF to screen coordinates
|
||||
if ( context.coordinateTransform().isValid() )
|
||||
{
|
||||
try
|
||||
{
|
||||
context.coordinateTransform().transformPolygon( pts );
|
||||
}
|
||||
catch ( QgsCsException & )
|
||||
{
|
||||
// we don't abort the rendering here, instead we remove any invalid points and just plot those which ARE valid
|
||||
}
|
||||
}
|
||||
|
||||
// remove non-finite points, e.g. infinite or NaN points caused by reprojecting errors
|
||||
pts.erase( std::remove_if( pts.begin(), pts.end(),
|
||||
[]( const QPointF point )
|
||||
{
|
||||
return !std::isfinite( point.x() ) || !std::isfinite( point.y() );
|
||||
} ), pts.end() );
|
||||
|
||||
QPointF *ptr = pts.data();
|
||||
for ( int i = 0; i < pts.size(); ++i, ++ptr )
|
||||
{
|
||||
context.mapToPixel().transformInPlace( ptr->rx(), ptr->ry() );
|
||||
}
|
||||
|
||||
const QString displayText = QgsExpression::replaceExpressionText( mText, &context.expressionContext(), &context.distanceArea() );
|
||||
QgsTextRenderer::drawTextOnLine( pts, displayText, context, mTextFormat );
|
||||
}
|
||||
|
||||
bool QgsAnnotationLineTextItem::writeXml( QDomElement &element, QDomDocument &document, const QgsReadWriteContext &context ) const
|
||||
{
|
||||
element.setAttribute( QStringLiteral( "wkt" ), mCurve->asWkt() );
|
||||
element.setAttribute( QStringLiteral( "text" ), mText );
|
||||
QDomElement textFormatElem = document.createElement( QStringLiteral( "lineTextFormat" ) );
|
||||
textFormatElem.appendChild( mTextFormat.writeXml( document, context ) );
|
||||
element.appendChild( textFormatElem );
|
||||
|
||||
writeCommonProperties( element, document, context );
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
QList<QgsAnnotationItemNode> QgsAnnotationLineTextItem::nodes() const
|
||||
{
|
||||
QList< QgsAnnotationItemNode > res;
|
||||
int i = 0;
|
||||
for ( auto it = mCurve->vertices_begin(); it != mCurve->vertices_end(); ++it, ++i )
|
||||
{
|
||||
res.append( QgsAnnotationItemNode( it.vertexId(), QgsPointXY( ( *it ).x(), ( *it ).y() ), Qgis::AnnotationItemNodeType::VertexHandle ) );
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
Qgis::AnnotationItemEditOperationResult QgsAnnotationLineTextItem::applyEdit( QgsAbstractAnnotationItemEditOperation *operation )
|
||||
{
|
||||
switch ( operation->type() )
|
||||
{
|
||||
case QgsAbstractAnnotationItemEditOperation::Type::MoveNode:
|
||||
{
|
||||
QgsAnnotationItemEditOperationMoveNode *moveOperation = qgis::down_cast< QgsAnnotationItemEditOperationMoveNode * >( operation );
|
||||
if ( mCurve->moveVertex( moveOperation->nodeId(), QgsPoint( moveOperation->after() ) ) )
|
||||
return Qgis::AnnotationItemEditOperationResult::Success;
|
||||
break;
|
||||
}
|
||||
|
||||
case QgsAbstractAnnotationItemEditOperation::Type::DeleteNode:
|
||||
{
|
||||
QgsAnnotationItemEditOperationDeleteNode *deleteOperation = qgis::down_cast< QgsAnnotationItemEditOperationDeleteNode * >( operation );
|
||||
if ( mCurve->deleteVertex( deleteOperation->nodeId() ) )
|
||||
return mCurve->isEmpty() ? Qgis::AnnotationItemEditOperationResult::ItemCleared : Qgis::AnnotationItemEditOperationResult::Success;
|
||||
break;
|
||||
}
|
||||
|
||||
case QgsAbstractAnnotationItemEditOperation::Type::AddNode:
|
||||
{
|
||||
QgsAnnotationItemEditOperationAddNode *addOperation = qgis::down_cast< QgsAnnotationItemEditOperationAddNode * >( operation );
|
||||
|
||||
QgsPoint segmentPoint;
|
||||
QgsVertexId endOfSegmentVertex;
|
||||
mCurve->closestSegment( addOperation->point(), segmentPoint, endOfSegmentVertex );
|
||||
if ( mCurve->insertVertex( endOfSegmentVertex, segmentPoint ) )
|
||||
return Qgis::AnnotationItemEditOperationResult::Success;
|
||||
break;
|
||||
}
|
||||
|
||||
case QgsAbstractAnnotationItemEditOperation::Type::TranslateItem:
|
||||
{
|
||||
QgsAnnotationItemEditOperationTranslateItem *moveOperation = qgis::down_cast< QgsAnnotationItemEditOperationTranslateItem * >( operation );
|
||||
const QTransform transform = QTransform::fromTranslate( moveOperation->translationX(), moveOperation->translationY() );
|
||||
mCurve->transform( transform );
|
||||
return Qgis::AnnotationItemEditOperationResult::Success;
|
||||
}
|
||||
}
|
||||
|
||||
return Qgis::AnnotationItemEditOperationResult::Invalid;
|
||||
}
|
||||
|
||||
QgsAnnotationItemEditOperationTransientResults *QgsAnnotationLineTextItem::transientEditResults( QgsAbstractAnnotationItemEditOperation *operation )
|
||||
{
|
||||
switch ( operation->type() )
|
||||
{
|
||||
case QgsAbstractAnnotationItemEditOperation::Type::MoveNode:
|
||||
{
|
||||
QgsAnnotationItemEditOperationMoveNode *moveOperation = dynamic_cast< QgsAnnotationItemEditOperationMoveNode * >( operation );
|
||||
std::unique_ptr< QgsCurve > modifiedCurve( mCurve->clone() );
|
||||
if ( modifiedCurve->moveVertex( moveOperation->nodeId(), QgsPoint( moveOperation->after() ) ) )
|
||||
{
|
||||
return new QgsAnnotationItemEditOperationTransientResults( QgsGeometry( std::move( modifiedCurve ) ) );
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case QgsAbstractAnnotationItemEditOperation::Type::TranslateItem:
|
||||
{
|
||||
QgsAnnotationItemEditOperationTranslateItem *moveOperation = qgis::down_cast< QgsAnnotationItemEditOperationTranslateItem * >( operation );
|
||||
const QTransform transform = QTransform::fromTranslate( moveOperation->translationX(), moveOperation->translationY() );
|
||||
std::unique_ptr< QgsCurve > modifiedCurve( mCurve->clone() );
|
||||
modifiedCurve->transform( transform );
|
||||
return new QgsAnnotationItemEditOperationTransientResults( QgsGeometry( std::move( modifiedCurve ) ) );
|
||||
}
|
||||
|
||||
case QgsAbstractAnnotationItemEditOperation::Type::DeleteNode:
|
||||
case QgsAbstractAnnotationItemEditOperation::Type::AddNode:
|
||||
break;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
QgsAnnotationLineTextItem *QgsAnnotationLineTextItem::create()
|
||||
{
|
||||
return new QgsAnnotationLineTextItem( QString(), new QgsLineString() );
|
||||
}
|
||||
|
||||
bool QgsAnnotationLineTextItem::readXml( const QDomElement &element, const QgsReadWriteContext &context )
|
||||
{
|
||||
const QString wkt = element.attribute( QStringLiteral( "wkt" ) );
|
||||
const QgsGeometry geometry = QgsGeometry::fromWkt( wkt );
|
||||
if ( const QgsCurve *curve = qgsgeometry_cast< const QgsCurve * >( geometry.constGet() ) )
|
||||
mCurve.reset( curve->clone() );
|
||||
|
||||
mText = element.attribute( QStringLiteral( "text" ) );
|
||||
const QDomElement textFormatElem = element.firstChildElement( QStringLiteral( "lineTextFormat" ) );
|
||||
if ( !textFormatElem.isNull() )
|
||||
{
|
||||
const QDomNodeList textFormatNodeList = textFormatElem.elementsByTagName( QStringLiteral( "text-style" ) );
|
||||
const QDomElement textFormatElem = textFormatNodeList.at( 0 ).toElement();
|
||||
mTextFormat.readXml( textFormatElem, context );
|
||||
}
|
||||
|
||||
readCommonProperties( element, context );
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
QgsRectangle QgsAnnotationLineTextItem::boundingBox() const
|
||||
{
|
||||
return mCurve->boundingBox();
|
||||
}
|
||||
|
||||
QgsRectangle QgsAnnotationLineTextItem::boundingBox( QgsRenderContext &context ) const
|
||||
{
|
||||
const QString displayText = QgsExpression::replaceExpressionText( mText, &context.expressionContext(), &context.distanceArea() );
|
||||
|
||||
const double heightInPixels = QgsTextRenderer::textHeight( context, mTextFormat, { displayText} );
|
||||
|
||||
// text size has already been calculated using any symbology reference scale factor above -- we need
|
||||
// to temporarily remove the reference scale here or we'll be undoing the scaling
|
||||
QgsScopedRenderContextReferenceScaleOverride resetScaleFactor( context, -1.0 );
|
||||
const double heightInMapUnits = context.convertToMapUnits( heightInPixels, Qgis::RenderUnit::Pixels );
|
||||
|
||||
return mCurve->boundingBox().buffered( heightInMapUnits );
|
||||
}
|
||||
|
||||
QgsAnnotationLineTextItem *QgsAnnotationLineTextItem::clone()
|
||||
{
|
||||
std::unique_ptr< QgsAnnotationLineTextItem > item = std::make_unique< QgsAnnotationLineTextItem >( mText, mCurve->clone() );
|
||||
item->setFormat( mTextFormat );
|
||||
item->copyCommonProperties( this );
|
||||
return item.release();
|
||||
}
|
||||
|
||||
void QgsAnnotationLineTextItem::setGeometry( QgsCurve *geometry )
|
||||
{
|
||||
mCurve.reset( geometry );
|
||||
}
|
||||
|
||||
QgsTextFormat QgsAnnotationLineTextItem::format() const
|
||||
{
|
||||
return mTextFormat;
|
||||
}
|
||||
|
||||
void QgsAnnotationLineTextItem::setFormat( const QgsTextFormat &format )
|
||||
{
|
||||
mTextFormat = format;
|
||||
}
|
121
src/core/annotations/qgsannotationlinetextitem.h
Normal file
121
src/core/annotations/qgsannotationlinetextitem.h
Normal file
@ -0,0 +1,121 @@
|
||||
/***************************************************************************
|
||||
qgsannotationlinetextitem.h
|
||||
----------------
|
||||
begin : March 2023
|
||||
copyright : (C) 2023 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 QGSANNOTATIONLINETEXTITEM_H
|
||||
#define QGSANNOTATIONLINETEXTITEM_H
|
||||
|
||||
#include "qgis_core.h"
|
||||
#include "qgis_sip.h"
|
||||
#include "qgsannotationitem.h"
|
||||
#include "qgstextformat.h"
|
||||
|
||||
class QgsCurve;
|
||||
|
||||
/**
|
||||
* \ingroup core
|
||||
* \brief An annotation item which renders text along a line geometry.
|
||||
*
|
||||
* \since QGIS 3.32
|
||||
*/
|
||||
class CORE_EXPORT QgsAnnotationLineTextItem : public QgsAnnotationItem
|
||||
{
|
||||
public:
|
||||
|
||||
/**
|
||||
* Constructor for QgsAnnotationLineTextItem, with the specified \a curve and \a text.
|
||||
*/
|
||||
QgsAnnotationLineTextItem( const QString &text, QgsCurve *curve SIP_TRANSFER );
|
||||
~QgsAnnotationLineTextItem() override;
|
||||
|
||||
Qgis::AnnotationItemFlags flags() const override;
|
||||
QString type() const override;
|
||||
void render( QgsRenderContext &context, QgsFeedback *feedback ) override;
|
||||
bool writeXml( QDomElement &element, QDomDocument &document, const QgsReadWriteContext &context ) const override;
|
||||
QList< QgsAnnotationItemNode > nodes() const override;
|
||||
Qgis::AnnotationItemEditOperationResult applyEdit( QgsAbstractAnnotationItemEditOperation *operation ) override;
|
||||
QgsAnnotationItemEditOperationTransientResults *transientEditResults( QgsAbstractAnnotationItemEditOperation *operation ) override SIP_FACTORY;
|
||||
|
||||
/**
|
||||
* Creates a new linestring annotation item.
|
||||
*/
|
||||
static QgsAnnotationLineTextItem *create() SIP_FACTORY;
|
||||
|
||||
bool readXml( const QDomElement &element, const QgsReadWriteContext &context ) override;
|
||||
QgsRectangle boundingBox() const override;
|
||||
QgsRectangle boundingBox( QgsRenderContext &context ) const override;
|
||||
|
||||
QgsAnnotationLineTextItem *clone() override SIP_FACTORY;
|
||||
|
||||
/**
|
||||
* Returns the geometry of the item.
|
||||
*
|
||||
* The coordinate reference system for the line will be the parent layer's QgsAnnotationLayer::crs().
|
||||
*
|
||||
* \see setGeometry()
|
||||
*/
|
||||
const QgsCurve *geometry() const { return mCurve.get(); }
|
||||
|
||||
/**
|
||||
* Sets the \a geometry of the item. Ownership of \a geometry is transferred.
|
||||
*
|
||||
* The coordinate reference system for the line will be the parent layer's QgsAnnotationLayer::crs().
|
||||
*
|
||||
* \see geometry()
|
||||
*/
|
||||
void setGeometry( QgsCurve *geometry SIP_TRANSFER );
|
||||
|
||||
/**
|
||||
* 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 );
|
||||
|
||||
private:
|
||||
|
||||
QString mText;
|
||||
std::unique_ptr< QgsCurve > mCurve;
|
||||
QgsTextFormat mTextFormat;
|
||||
|
||||
#ifdef SIP_RUN
|
||||
QgsAnnotationLineTextItem( const QgsAnnotationLineTextItem &other );
|
||||
#endif
|
||||
|
||||
};
|
||||
|
||||
#endif // QGSANNOTATIONLINETEXTITEM_H
|
@ -18,7 +18,9 @@
|
||||
#include "qgsannotationpolygonitem.h"
|
||||
#include "qgssymbol.h"
|
||||
#include "qgssymbollayerutils.h"
|
||||
#include "qgssurface.h"
|
||||
#include "qgscurvepolygon.h"
|
||||
#include "qgscurve.h"
|
||||
#include "qgspolygon.h"
|
||||
#include "qgsfillsymbol.h"
|
||||
#include "qgsannotationitemnode.h"
|
||||
#include "qgsannotationitemeditoperation.h"
|
||||
@ -230,6 +232,11 @@ QgsRectangle QgsAnnotationPolygonItem::boundingBox() const
|
||||
return mPolygon->boundingBox();
|
||||
}
|
||||
|
||||
void QgsAnnotationPolygonItem::setGeometry( QgsCurvePolygon *geometry )
|
||||
{
|
||||
mPolygon.reset( geometry );
|
||||
}
|
||||
|
||||
const QgsFillSymbol *QgsAnnotationPolygonItem::symbol() const
|
||||
{
|
||||
return mSymbol.get();
|
||||
|
@ -22,6 +22,7 @@
|
||||
#include "qgis_sip.h"
|
||||
#include "qgsannotationitem.h"
|
||||
|
||||
class QgsCurvePolygon;
|
||||
|
||||
/**
|
||||
* \ingroup core
|
||||
@ -71,7 +72,7 @@ class CORE_EXPORT QgsAnnotationPolygonItem : public QgsAnnotationItem
|
||||
*
|
||||
* \see geometry()
|
||||
*/
|
||||
void setGeometry( QgsCurvePolygon *geometry SIP_TRANSFER ) { mPolygon.reset( geometry ); }
|
||||
void setGeometry( QgsCurvePolygon *geometry SIP_TRANSFER );
|
||||
|
||||
/**
|
||||
* Returns the symbol used to render the item.
|
||||
|
@ -28,6 +28,7 @@
|
||||
#include "qgsadvanceddigitizingdockwidget.h"
|
||||
#include "qgsapplication.h"
|
||||
#include "qgsrecentstylehandler.h"
|
||||
#include "qgscurvepolygon.h"
|
||||
|
||||
///@cond PRIVATE
|
||||
|
||||
|
@ -23,6 +23,7 @@ ADD_PYTHON_TEST(PyQgsAnnotationItemEditOperation test_qgsannotationitemeditopera
|
||||
ADD_PYTHON_TEST(PyQgsAnnotationItemNode test_qgsannotationitemnode.py)
|
||||
ADD_PYTHON_TEST(PyQgsAnnotationLayer test_qgsannotationlayer.py)
|
||||
ADD_PYTHON_TEST(PyQgsAnnotationLineItem test_qgsannotationlineitem.py)
|
||||
ADD_PYTHON_TEST(PyQgsAnnotationLineTextItem test_qgsannotationlinetextitem.py)
|
||||
ADD_PYTHON_TEST(PyQgsAnnotationMarkerItem test_qgsannotationmarkeritem.py)
|
||||
ADD_PYTHON_TEST(PyQgsAnnotationPointTextItem test_qgsannotationpointtextitem.py)
|
||||
ADD_PYTHON_TEST(PyQgsAnnotationPolygonItem test_qgsannotationpolygonitem.py)
|
||||
|
288
tests/src/python/test_qgsannotationlinetextitem.py
Normal file
288
tests/src/python/test_qgsannotationlinetextitem.py
Normal file
@ -0,0 +1,288 @@
|
||||
"""QGIS Unit tests for QgsAnnotationLineTextItem.
|
||||
|
||||
From build dir, run: ctest -R PyQgsAnnotationlINETextItem -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.
|
||||
"""
|
||||
__author__ = '(C) 2020 by Nyall Dawson'
|
||||
__date__ = '10/08/2020'
|
||||
__copyright__ = 'Copyright 2020, The QGIS Project'
|
||||
|
||||
import qgis # NOQA
|
||||
from qgis.PyQt.QtCore import QDir, QSize, Qt
|
||||
from qgis.PyQt.QtGui import QColor, QImage, QPainter
|
||||
from qgis.PyQt.QtXml import QDomDocument
|
||||
from qgis.core import (
|
||||
Qgis,
|
||||
QgsAnnotationItemEditOperationAddNode,
|
||||
QgsAnnotationItemEditOperationDeleteNode,
|
||||
QgsAnnotationItemEditOperationMoveNode,
|
||||
QgsAnnotationItemEditOperationTranslateItem,
|
||||
QgsAnnotationItemNode,
|
||||
QgsAnnotationLineTextItem,
|
||||
QgsCoordinateReferenceSystem,
|
||||
QgsCoordinateTransform,
|
||||
QgsMapSettings,
|
||||
QgsPoint,
|
||||
QgsPointXY,
|
||||
QgsProject,
|
||||
QgsReadWriteContext,
|
||||
QgsRectangle,
|
||||
QgsRenderChecker,
|
||||
QgsRenderContext,
|
||||
QgsTextFormat,
|
||||
QgsVertexId,
|
||||
QgsLineString,
|
||||
)
|
||||
from qgis.testing import start_app, unittest
|
||||
|
||||
from utilities import getTestFont, unitTestDataPath
|
||||
|
||||
start_app()
|
||||
TEST_DATA_DIR = unitTestDataPath()
|
||||
|
||||
|
||||
class TestQgsAnnotationLineTextItem(unittest.TestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.report = "<h1>Python QgsAnnotationLineTextItem Tests</h1>\n"
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
report_file_path = f"{QDir.tempPath()}/qgistest.html"
|
||||
with open(report_file_path, 'a') as report_file:
|
||||
report_file.write(cls.report)
|
||||
|
||||
def testBasic(self):
|
||||
item = QgsAnnotationLineTextItem('my text', QgsLineString(((12, 13), (13, 13.1), (14, 13))))
|
||||
|
||||
self.assertEqual(item.text(), 'my text')
|
||||
self.assertEqual(item.geometry().asWkt(1), 'LineString (12 13, 13 13.1, 14 13)')
|
||||
|
||||
item.setText('tttttt')
|
||||
item.setGeometry(QgsLineString(((12, 13), (13, 13.1))))
|
||||
item.setZIndex(11)
|
||||
|
||||
format = QgsTextFormat()
|
||||
format.setSize(37)
|
||||
item.setFormat(format)
|
||||
|
||||
self.assertEqual(item.text(), 'tttttt')
|
||||
self.assertEqual(item.geometry().asWkt(1), 'LineString (12 13, 13 13.1)')
|
||||
self.assertEqual(item.zIndex(), 11)
|
||||
self.assertEqual(item.format().size(), 37)
|
||||
|
||||
def test_nodes(self):
|
||||
"""
|
||||
Test nodes for item
|
||||
"""
|
||||
item = QgsAnnotationLineTextItem('my text', QgsLineString(((12, 13), (13, 13.1), (14, 13))))
|
||||
self.assertEqual(item.nodes(), [
|
||||
QgsAnnotationItemNode(QgsVertexId(0, 0, 0), QgsPointXY(12, 13),
|
||||
Qgis.AnnotationItemNodeType.VertexHandle),
|
||||
QgsAnnotationItemNode(QgsVertexId(0, 0, 1), QgsPointXY(13, 13.1),
|
||||
Qgis.AnnotationItemNodeType.VertexHandle),
|
||||
QgsAnnotationItemNode(QgsVertexId(0, 0, 2), QgsPointXY(14, 13),
|
||||
Qgis.AnnotationItemNodeType.VertexHandle)
|
||||
])
|
||||
|
||||
def test_transform(self):
|
||||
item = QgsAnnotationLineTextItem('my text', QgsLineString(((12, 13), (13, 13.1), (14, 13))))
|
||||
self.assertEqual(item.geometry().asWkt(1), 'LineString (12 13, 13 13.1, 14 13)')
|
||||
|
||||
self.assertEqual(item.applyEdit(QgsAnnotationItemEditOperationTranslateItem('', 100, 200)), Qgis.AnnotationItemEditOperationResult.Success)
|
||||
self.assertEqual(item.geometry().asWkt(1), 'LineString (112 213, 113 213.1, 114 213)')
|
||||
|
||||
def test_apply_move_node_edit(self):
|
||||
item = QgsAnnotationLineTextItem('my text', QgsLineString(((12, 13), (13, 13.1), (14, 13))))
|
||||
self.assertEqual(item.geometry().asWkt(1), 'LineString (12 13, 13 13.1, 14 13)')
|
||||
|
||||
self.assertEqual(item.applyEdit(QgsAnnotationItemEditOperationMoveNode('', QgsVertexId(0, 0, 1), QgsPoint(13, 13.1), QgsPoint(17, 18))), Qgis.AnnotationItemEditOperationResult.Success)
|
||||
self.assertEqual(item.geometry().asWkt(1), 'LineString (12 13, 17 18, 14 13)')
|
||||
|
||||
def test_transient_move_operation(self):
|
||||
item = QgsAnnotationLineTextItem('my text', QgsLineString(((12, 13), (13, 13.1), (14, 13))))
|
||||
self.assertEqual(item.geometry().asWkt(1), 'LineString (12 13, 13 13.1, 14 13)')
|
||||
|
||||
res = item.transientEditResults(QgsAnnotationItemEditOperationMoveNode('', QgsVertexId(0, 0, 1), QgsPoint(13, 13.1), QgsPoint(17, 18)))
|
||||
self.assertEqual(res.representativeGeometry().asWkt(1), 'LineString (12 13, 17 18, 14 13)')
|
||||
|
||||
def test_transient_translate_operation(self):
|
||||
item = QgsAnnotationLineTextItem('my text', QgsLineString(((12, 13), (13, 13.1), (14, 13))))
|
||||
self.assertEqual(item.geometry().asWkt(1), 'LineString (12 13, 13 13.1, 14 13)')
|
||||
|
||||
res = item.transientEditResults(QgsAnnotationItemEditOperationTranslateItem('', 100, 200))
|
||||
self.assertEqual(res.representativeGeometry().asWkt(1), 'LineString (112 213, 113 213.1, 114 213)')
|
||||
|
||||
def test_apply_delete_node_edit(self):
|
||||
item = QgsAnnotationLineTextItem('my text', QgsLineString(((12, 13), (13, 13.1), (14, 13))))
|
||||
self.assertEqual(item.geometry().asWkt(1), 'LineString (12 13, 13 13.1, 14 13)')
|
||||
|
||||
self.assertEqual(item.applyEdit(QgsAnnotationItemEditOperationDeleteNode('', QgsVertexId(0, 0, 1), QgsPoint(13, 13.1))), Qgis.AnnotationItemEditOperationResult.Success)
|
||||
self.assertEqual(item.geometry().asWkt(1),
|
||||
'LineString (12 13, 14 13)')
|
||||
|
||||
self.assertEqual(item.applyEdit(QgsAnnotationItemEditOperationDeleteNode('', QgsVertexId(0, 0, 1), QgsPoint(14, 13))), Qgis.AnnotationItemEditOperationResult.ItemCleared)
|
||||
|
||||
def test_apply_add_node_edit(self):
|
||||
item = QgsAnnotationLineTextItem('my text', QgsLineString(((12, 13), (13, 13.1), (14, 13))))
|
||||
self.assertEqual(item.applyEdit(QgsAnnotationItemEditOperationAddNode('', QgsPoint(12.5, 12.8))), Qgis.AnnotationItemEditOperationResult.Success)
|
||||
self.assertEqual(item.geometry().asWkt(1),
|
||||
'LineString (12 13, 12.5 13, 13 13.1, 14 13)')
|
||||
|
||||
def testReadWriteXml(self):
|
||||
doc = QDomDocument("testdoc")
|
||||
elem = doc.createElement('test')
|
||||
|
||||
item = QgsAnnotationLineTextItem('my text', QgsLineString(((12, 13), (13, 13.1), (14, 13))))
|
||||
item.setZIndex(11)
|
||||
format = QgsTextFormat()
|
||||
format.setSize(37)
|
||||
item.setFormat(format)
|
||||
item.setUseSymbologyReferenceScale(True)
|
||||
item.setSymbologyReferenceScale(5000)
|
||||
|
||||
self.assertTrue(item.writeXml(elem, doc, QgsReadWriteContext()))
|
||||
|
||||
s2 = QgsAnnotationLineTextItem.create()
|
||||
self.assertTrue(s2.readXml(elem, QgsReadWriteContext()))
|
||||
self.assertEqual(s2.text(), 'my text')
|
||||
self.assertEqual(s2.geometry().asWkt(1), 'LineString (12 13, 13 13.1, 14 13)')
|
||||
self.assertEqual(s2.zIndex(), 11)
|
||||
self.assertEqual(s2.format().size(), 37)
|
||||
self.assertTrue(s2.useSymbologyReferenceScale())
|
||||
self.assertEqual(s2.symbologyReferenceScale(), 5000)
|
||||
|
||||
def testClone(self):
|
||||
item = QgsAnnotationLineTextItem('my text', QgsLineString(((12, 13), (13, 13.1), (14, 13))))
|
||||
item.setZIndex(11)
|
||||
format = QgsTextFormat()
|
||||
format.setSize(37)
|
||||
item.setFormat(format)
|
||||
item.setUseSymbologyReferenceScale(True)
|
||||
item.setSymbologyReferenceScale(5000)
|
||||
|
||||
item2 = item.clone()
|
||||
self.assertEqual(item2.text(), 'my text')
|
||||
self.assertEqual(item2.geometry().asWkt(1), 'LineString (12 13, 13 13.1, 14 13)')
|
||||
self.assertEqual(item2.zIndex(), 11)
|
||||
self.assertEqual(item2.format().size(), 37)
|
||||
self.assertTrue(item2.useSymbologyReferenceScale())
|
||||
self.assertEqual(item2.symbologyReferenceScale(), 5000)
|
||||
|
||||
def testRenderLine(self):
|
||||
item = QgsAnnotationLineTextItem('my text', QgsLineString(((12, 13), (13, 13.1), (14, 12))))
|
||||
|
||||
format = QgsTextFormat.fromQFont(getTestFont('Bold'))
|
||||
format.setColor(QColor(255, 0, 0))
|
||||
format.setOpacity(150 / 255)
|
||||
format.setSize(55)
|
||||
item.setFormat(format)
|
||||
|
||||
settings = QgsMapSettings()
|
||||
settings.setDestinationCrs(QgsCoordinateReferenceSystem('EPSG:4326'))
|
||||
settings.setExtent(QgsRectangle(11.9, 11.9, 14.5, 14))
|
||||
settings.setOutputSize(QSize(600, 300))
|
||||
|
||||
settings.setFlag(QgsMapSettings.Antialiasing, False)
|
||||
|
||||
rc = QgsRenderContext.fromMapSettings(settings)
|
||||
image = QImage(600, 300, QImage.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.imageCheck('linetext_item', 'linetext_item', image))
|
||||
|
||||
def testRenderLineTextExpression(self):
|
||||
item = QgsAnnotationLineTextItem('[% 1 + 1.5 %]', QgsLineString(((12, 13), (13, 13.1), (14, 12))))
|
||||
|
||||
format = QgsTextFormat.fromQFont(getTestFont('Bold'))
|
||||
format.setColor(QColor(255, 0, 0))
|
||||
format.setOpacity(150 / 255)
|
||||
format.setSize(55)
|
||||
item.setFormat(format)
|
||||
|
||||
settings = QgsMapSettings()
|
||||
settings.setDestinationCrs(QgsCoordinateReferenceSystem('EPSG:4326'))
|
||||
settings.setExtent(QgsRectangle(11.9, 11.9, 14.5, 14))
|
||||
settings.setOutputSize(QSize(600, 300))
|
||||
|
||||
settings.setFlag(QgsMapSettings.Antialiasing, False)
|
||||
|
||||
rc = QgsRenderContext.fromMapSettings(settings)
|
||||
image = QImage(600, 300, QImage.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.imageCheck('linetext_item_expression', 'linetext_item_expression', image))
|
||||
|
||||
def testRenderWithTransform(self):
|
||||
item = QgsAnnotationLineTextItem('my text', QgsLineString(
|
||||
((12, 13), (13, 13.1), (14, 12))))
|
||||
|
||||
format = QgsTextFormat.fromQFont(getTestFont('Bold'))
|
||||
format.setColor(QColor(255, 0, 0))
|
||||
format.setOpacity(150 / 255)
|
||||
format.setSize(55)
|
||||
item.setFormat(format)
|
||||
|
||||
settings = QgsMapSettings()
|
||||
settings.setDestinationCrs(QgsCoordinateReferenceSystem('EPSG:3857'))
|
||||
settings.setExtent(QgsRectangle(1291958, 1386945, 1420709, 1532518))
|
||||
settings.setOutputSize(QSize(600, 300))
|
||||
|
||||
settings.setFlag(QgsMapSettings.Antialiasing, False)
|
||||
|
||||
rc = QgsRenderContext.fromMapSettings(settings)
|
||||
rc.setCoordinateTransform(QgsCoordinateTransform(QgsCoordinateReferenceSystem('EPSG:4326'), settings.destinationCrs(), QgsProject.instance()))
|
||||
image = QImage(600, 300, QImage.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.imageCheck('linetext_item_transform', 'linetext_item_transform', image))
|
||||
|
||||
def imageCheck(self, name, reference_image, image):
|
||||
TestQgsAnnotationLineTextItem.report += f"<h2>Render {name}</h2>\n"
|
||||
temp_dir = QDir.tempPath() + '/'
|
||||
file_name = temp_dir + 'patch_' + name + ".png"
|
||||
image.save(file_name, "PNG")
|
||||
checker = QgsRenderChecker()
|
||||
checker.setControlPathPrefix("annotation_layer")
|
||||
checker.setControlName("expected_" + reference_image)
|
||||
checker.setRenderedImage(file_name)
|
||||
checker.setColorTolerance(2)
|
||||
result = checker.compareImages(name, 20)
|
||||
TestQgsAnnotationLineTextItem.report += checker.report()
|
||||
return result
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
BIN
tests/testdata/control_images/annotation_layer/expected_linetext_item/expected_linetext_item.png
vendored
Normal file
BIN
tests/testdata/control_images/annotation_layer/expected_linetext_item/expected_linetext_item.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.9 KiB |
Binary file not shown.
After Width: | Height: | Size: 2.0 KiB |
Binary file not shown.
After Width: | Height: | Size: 2.8 KiB |
Loading…
x
Reference in New Issue
Block a user