Skip to content

Commit 2b0a2bf

Browse files
committedMar 21, 2021
[feature] Add new "balloon" (speech bubble) callout style
1 parent abad549 commit 2b0a2bf

File tree

8 files changed

+1259
-0
lines changed

8 files changed

+1259
-0
lines changed
 

‎python/core/auto_generated/callouts/qgscallout.sip.in

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ relevant symbology elements to render them.
3838
{
3939
sipType = sipType_QgsCurvedLineCallout;
4040
}
41+
else if ( sipCpp->type() == "balloon" && dynamic_cast<QgsBalloonCallout *>( sipCpp ) != NULL )
42+
{
43+
sipType = sipType_QgsBalloonCallout;
44+
}
4145
else
4246
{
4347
sipType = 0;
@@ -59,6 +63,7 @@ relevant symbology elements to render them.
5963
DestinationY,
6064
Curvature,
6165
Orientation,
66+
Margins,
6267
};
6368

6469
enum DrawOrder
@@ -819,6 +824,180 @@ Sets the callout line's curve ``orientation``.
819824
};
820825

821826

827+
class QgsBalloonCallout : QgsCallout
828+
{
829+
%Docstring
830+
A cartoon talking bubble callout style.
831+
832+
.. versionadded:: 3.20
833+
%End
834+
835+
%TypeHeaderCode
836+
#include "qgscallout.h"
837+
%End
838+
public:
839+
840+
QgsBalloonCallout();
841+
~QgsBalloonCallout();
842+
843+
844+
static QgsCallout *create( const QVariantMap &properties = QVariantMap(), const QgsReadWriteContext &context = QgsReadWriteContext() ) /Factory/;
845+
%Docstring
846+
Creates a new QgsBalloonCallout, using the settings
847+
serialized in the ``properties`` map (corresponding to the output from
848+
:py:func:`QgsBalloonCallout.properties()` ).
849+
%End
850+
851+
virtual QString type() const;
852+
853+
virtual QgsBalloonCallout *clone() const;
854+
855+
virtual QVariantMap properties( const QgsReadWriteContext &context ) const;
856+
857+
virtual void readProperties( const QVariantMap &props, const QgsReadWriteContext &context );
858+
859+
virtual void startRender( QgsRenderContext &context );
860+
861+
virtual void stopRender( QgsRenderContext &context );
862+
863+
virtual QSet< QString > referencedFields( const QgsRenderContext &context ) const;
864+
865+
866+
QgsFillSymbol *fillSymbol();
867+
%Docstring
868+
Returns the fill symbol used to render the callout.
869+
870+
Ownership is not transferred.
871+
872+
.. seealso:: :py:func:`setFillSymbol`
873+
%End
874+
875+
void setFillSymbol( QgsFillSymbol *symbol /Transfer/ );
876+
%Docstring
877+
Sets the fill ``symbol`` used to render the callout. Ownership of ``symbol`` is
878+
transferred to the callout.
879+
880+
.. seealso:: :py:func:`fillSymbol`
881+
%End
882+
883+
double offsetFromAnchor() const;
884+
%Docstring
885+
Returns the offset distance from the anchor point at which to start the line. Units are specified through :py:func:`~QgsBalloonCallout.offsetFromAnchorUnit`.
886+
887+
.. seealso:: :py:func:`setOffsetFromAnchor`
888+
889+
.. seealso:: :py:func:`offsetFromAnchorUnit`
890+
%End
891+
892+
void setOffsetFromAnchor( double distance );
893+
%Docstring
894+
Sets the offset ``distance`` from the anchor point at which to start the line. Units are specified through :py:func:`~QgsBalloonCallout.setOffsetFromAnchorUnit`.
895+
896+
.. seealso:: :py:func:`offsetFromAnchor`
897+
898+
.. seealso:: :py:func:`setOffsetFromAnchorUnit`
899+
%End
900+
901+
void setOffsetFromAnchorUnit( QgsUnitTypes::RenderUnit unit );
902+
%Docstring
903+
Sets the ``unit`` for the offset from anchor distance.
904+
905+
.. seealso:: :py:func:`offsetFromAnchor`
906+
907+
.. seealso:: :py:func:`setOffsetFromAnchor`
908+
%End
909+
910+
QgsUnitTypes::RenderUnit offsetFromAnchorUnit() const;
911+
%Docstring
912+
Returns the units for the offset from anchor point.
913+
914+
.. seealso:: :py:func:`setOffsetFromAnchorUnit`
915+
916+
.. seealso:: :py:func:`offsetFromAnchor`
917+
%End
918+
919+
void setOffsetFromAnchorMapUnitScale( const QgsMapUnitScale &scale );
920+
%Docstring
921+
Sets the map unit ``scale`` for the offset from anchor.
922+
923+
.. seealso:: :py:func:`offsetFromAnchorMapUnitScale`
924+
925+
.. seealso:: :py:func:`setOffsetFromAnchorUnit`
926+
927+
.. seealso:: :py:func:`setOffsetFromAnchor`
928+
%End
929+
930+
const QgsMapUnitScale &offsetFromAnchorMapUnitScale() const;
931+
%Docstring
932+
Returns the map unit scale for the offset from anchor.
933+
934+
.. seealso:: :py:func:`setOffsetFromAnchorMapUnitScale`
935+
936+
.. seealso:: :py:func:`offsetFromAnchorUnit`
937+
938+
.. seealso:: :py:func:`offsetFromAnchor`
939+
%End
940+
941+
const QgsMargins &margins() const;
942+
%Docstring
943+
Returns the margins between the outside of the callout frame and the label's bounding rectangle.
944+
945+
Units are retrieved via :py:func:`~QgsBalloonCallout.marginsUnit`
946+
947+
.. note::
948+
949+
Negative margins are acceptable.
950+
951+
.. seealso:: :py:func:`setMargins`
952+
953+
.. seealso:: :py:func:`marginsUnit`
954+
%End
955+
956+
void setMargins( const QgsMargins &margins );
957+
%Docstring
958+
Sets the ``margins`` between the outside of the callout frame and the label's bounding rectangle.
959+
960+
Units are set via :py:func:`~QgsBalloonCallout.setMarginsUnit`
961+
962+
.. note::
963+
964+
Negative margins are acceptable.
965+
966+
.. seealso:: :py:func:`margins`
967+
968+
.. seealso:: :py:func:`setMarginsUnit`
969+
%End
970+
971+
void setMarginsUnit( QgsUnitTypes::RenderUnit unit );
972+
%Docstring
973+
Sets the ``unit`` for the margins between the outside of the callout frame and the label's bounding rectangle.
974+
975+
.. seealso:: :py:func:`margins`
976+
977+
.. seealso:: :py:func:`marginsUnit`
978+
%End
979+
980+
QgsUnitTypes::RenderUnit marginsUnit() const;
981+
%Docstring
982+
Returns the units for the margins between the outside of the callout frame and the label's bounding rectangle.
983+
984+
.. seealso:: :py:func:`setMarginsUnit`
985+
986+
.. seealso:: :py:func:`margins`
987+
%End
988+
989+
protected:
990+
virtual void draw( QgsRenderContext &context, const QRectF &bodyBoundingBox, const double angle, const QgsGeometry &anchor, QgsCallout::QgsCalloutContext &calloutContext );
991+
992+
993+
private:
994+
QgsBalloonCallout( const QgsBalloonCallout &other );
995+
QgsBalloonCallout &operator=( const QgsBalloonCallout & );
996+
};
997+
998+
999+
1000+
8221001
/************************************************************************
8231002
* This file has been generated automatically from *
8241003
* *

‎src/core/callouts/qgscallout.cpp

Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,15 @@
1919
#include "qgsrendercontext.h"
2020
#include "qgssymbol.h"
2121
#include "qgslinesymbollayer.h"
22+
#include "qgsfillsymbollayer.h"
2223
#include "qgssymbollayerutils.h"
2324
#include "qgsxmlutils.h"
2425
#include "qgslinestring.h"
2526
#include "qgslogger.h"
2627
#include "qgsgeos.h"
2728
#include "qgsgeometryutils.h"
2829
#include "qgscircularstring.h"
30+
#include "qgsshapegenerator.h"
2931
#include <QPainter>
3032
#include <mutex>
3133

@@ -57,6 +59,9 @@ void QgsCallout::initPropertyDefinitions()
5759
{
5860
QgsCallout::Orientation, QgsPropertyDefinition( "Orientation", QgsPropertyDefinition::DataTypeString, QObject::tr( "Callout curve orientation" ), QObject::tr( "string " ) + "[<b>auto</b>|<b>clockwise</b>|<b>counterclockwise</b>]", origin )
5961
},
62+
{
63+
QgsCallout::Margins, QgsPropertyDefinition( "Margins", QgsPropertyDefinition::DataTypeString, QObject::tr( "Margins" ), QObject::tr( "string of four doubles '<b>top,right,bottom,left</b>' or array of doubles <b>[top, right, bottom, left]</b>" ) )
64+
},
6065
};
6166
}
6267

@@ -993,3 +998,244 @@ void QgsCurvedLineCallout::setCurvature( double curvature )
993998
{
994999
mCurvature = curvature;
9951000
}
1001+
1002+
1003+
1004+
//
1005+
// QgsBalloonCallout
1006+
//
1007+
1008+
QgsBalloonCallout::QgsBalloonCallout()
1009+
{
1010+
mFillSymbol = std::make_unique< QgsFillSymbol >( QgsSymbolLayerList() << new QgsSimpleFillSymbolLayer( QColor( 255, 200, 60 ) ) );
1011+
}
1012+
1013+
QgsBalloonCallout::~QgsBalloonCallout() = default;
1014+
1015+
QgsBalloonCallout::QgsBalloonCallout( const QgsBalloonCallout &other )
1016+
: QgsCallout( other )
1017+
, mFillSymbol( other.mFillSymbol ? other.mFillSymbol->clone() : nullptr )
1018+
, mOffsetFromAnchorDistance( other.mOffsetFromAnchorDistance )
1019+
, mOffsetFromAnchorUnit( other.mOffsetFromAnchorUnit )
1020+
, mOffsetFromAnchorScale( other.mOffsetFromAnchorScale )
1021+
, mMargins( other.mMargins )
1022+
, mMarginUnit( other.mMarginUnit )
1023+
{
1024+
1025+
}
1026+
1027+
QgsCallout *QgsBalloonCallout::create( const QVariantMap &properties, const QgsReadWriteContext &context )
1028+
{
1029+
std::unique_ptr< QgsBalloonCallout > callout = std::make_unique< QgsBalloonCallout >();
1030+
callout->readProperties( properties, context );
1031+
return callout.release();
1032+
}
1033+
1034+
QString QgsBalloonCallout::type() const
1035+
{
1036+
return QStringLiteral( "balloon" );
1037+
}
1038+
1039+
QgsBalloonCallout *QgsBalloonCallout::clone() const
1040+
{
1041+
return new QgsBalloonCallout( *this );
1042+
}
1043+
1044+
QVariantMap QgsBalloonCallout::properties( const QgsReadWriteContext &context ) const
1045+
{
1046+
QVariantMap props = QgsCallout::properties( context );
1047+
1048+
if ( mFillSymbol )
1049+
{
1050+
props[ QStringLiteral( "fillSymbol" ) ] = QgsSymbolLayerUtils::symbolProperties( mFillSymbol.get() );
1051+
}
1052+
1053+
props[ QStringLiteral( "offsetFromAnchor" ) ] = mOffsetFromAnchorDistance;
1054+
props[ QStringLiteral( "offsetFromAnchorUnit" ) ] = QgsUnitTypes::encodeUnit( mOffsetFromAnchorUnit );
1055+
props[ QStringLiteral( "offsetFromAnchorMapUnitScale" ) ] = QgsSymbolLayerUtils::encodeMapUnitScale( mOffsetFromAnchorScale );
1056+
1057+
props[ QStringLiteral( "margins" ) ] = mMargins.toString();
1058+
props[ QStringLiteral( "marginsUnit" ) ] = QgsUnitTypes::encodeUnit( mMarginUnit );
1059+
1060+
return props;
1061+
}
1062+
1063+
void QgsBalloonCallout::readProperties( const QVariantMap &props, const QgsReadWriteContext &context )
1064+
{
1065+
QgsCallout::readProperties( props, context );
1066+
1067+
const QString fillSymbolDef = props.value( QStringLiteral( "fillSymbol" ) ).toString();
1068+
QDomDocument doc( QStringLiteral( "symbol" ) );
1069+
doc.setContent( fillSymbolDef );
1070+
QDomElement symbolElem = doc.firstChildElement( QStringLiteral( "symbol" ) );
1071+
std::unique_ptr< QgsFillSymbol > fillSymbol( QgsSymbolLayerUtils::loadSymbol< QgsFillSymbol >( symbolElem, context ) );
1072+
if ( fillSymbol )
1073+
mFillSymbol = std::move( fillSymbol );
1074+
1075+
mOffsetFromAnchorDistance = props.value( QStringLiteral( "offsetFromAnchor" ), 0 ).toDouble();
1076+
mOffsetFromAnchorUnit = QgsUnitTypes::decodeRenderUnit( props.value( QStringLiteral( "offsetFromAnchorUnit" ) ).toString() );
1077+
mOffsetFromAnchorScale = QgsSymbolLayerUtils::decodeMapUnitScale( props.value( QStringLiteral( "offsetFromAnchorMapUnitScale" ) ).toString() );
1078+
1079+
mMargins = QgsMargins::fromString( props.value( QStringLiteral( "margins" ) ).toString() );
1080+
mMarginUnit = QgsUnitTypes::decodeRenderUnit( props.value( QStringLiteral( "marginsUnit" ) ).toString() );
1081+
}
1082+
1083+
void QgsBalloonCallout::startRender( QgsRenderContext &context )
1084+
{
1085+
QgsCallout::startRender( context );
1086+
if ( mFillSymbol )
1087+
mFillSymbol->startRender( context );
1088+
}
1089+
1090+
void QgsBalloonCallout::stopRender( QgsRenderContext &context )
1091+
{
1092+
QgsCallout::stopRender( context );
1093+
if ( mFillSymbol )
1094+
mFillSymbol->stopRender( context );
1095+
}
1096+
1097+
QSet<QString> QgsBalloonCallout::referencedFields( const QgsRenderContext &context ) const
1098+
{
1099+
QSet<QString> fields = QgsCallout::referencedFields( context );
1100+
if ( mFillSymbol )
1101+
fields.unite( mFillSymbol->usedAttributes( context ) );
1102+
return fields;
1103+
}
1104+
1105+
QgsFillSymbol *QgsBalloonCallout::fillSymbol()
1106+
{
1107+
return mFillSymbol.get();
1108+
}
1109+
1110+
void QgsBalloonCallout::setFillSymbol( QgsFillSymbol *symbol )
1111+
{
1112+
mFillSymbol.reset( symbol );
1113+
}
1114+
1115+
void QgsBalloonCallout::draw( QgsRenderContext &context, const QRectF &rect, const double, const QgsGeometry &anchor, QgsCalloutContext &calloutContext )
1116+
{
1117+
bool destinationIsPinned = false;
1118+
QgsGeometry line = calloutLineToPart( QgsGeometry::fromRect( rect ), anchor.constGet(), context, calloutContext, destinationIsPinned );
1119+
1120+
double offsetFromAnchor = mOffsetFromAnchorDistance;
1121+
if ( dataDefinedProperties().isActive( QgsCallout::OffsetFromAnchor ) )
1122+
{
1123+
context.expressionContext().setOriginalValueVariable( offsetFromAnchor );
1124+
offsetFromAnchor = dataDefinedProperties().valueAsDouble( QgsCallout::OffsetFromAnchor, context.expressionContext(), offsetFromAnchor );
1125+
}
1126+
const double offsetFromAnchorPixels = context.convertToPainterUnits( offsetFromAnchor, mOffsetFromAnchorUnit, mOffsetFromAnchorScale );
1127+
1128+
if ( offsetFromAnchorPixels > 0 )
1129+
{
1130+
if ( const QgsLineString *ls = qgsgeometry_cast< const QgsLineString * >( line.constGet() ) )
1131+
{
1132+
line = QgsGeometry( ls->curveSubstring( 0, ls->length() - offsetFromAnchorPixels ) );
1133+
}
1134+
}
1135+
1136+
QgsPointXY destination;
1137+
QgsPointXY origin;
1138+
if ( const QgsLineString *ls = qgsgeometry_cast< const QgsLineString * >( line.constGet() ) )
1139+
{
1140+
origin = ls->startPoint();
1141+
destination = ls->endPoint();
1142+
}
1143+
else
1144+
{
1145+
destination = QgsPointXY( rect.center() );
1146+
}
1147+
1148+
const QPolygonF points = getPoints( context, destination, rect );
1149+
if ( points.empty() )
1150+
return;
1151+
1152+
if ( !origin.isEmpty() )
1153+
{
1154+
QgsCalloutPosition position;
1155+
position.setOrigin( context.mapToPixel().toMapCoordinates( origin.x(), origin.y() ).toQPointF() );
1156+
position.setOriginIsPinned( false );
1157+
position.setDestination( context.mapToPixel().toMapCoordinates( destination.x(), destination.y() ).toQPointF() );
1158+
position.setDestinationIsPinned( destinationIsPinned );
1159+
calloutContext.addCalloutPosition( position );
1160+
}
1161+
1162+
mFillSymbol->renderPolygon( points, nullptr, nullptr, context );
1163+
}
1164+
1165+
QPolygonF QgsBalloonCallout::getPoints( QgsRenderContext &context, QgsPointXY origin, QRectF rect ) const
1166+
{
1167+
double segmentPointWidth = context.convertToPainterUnits( 2.64, QgsUnitTypes::RenderMillimeters );
1168+
1169+
double left = mMargins.left();
1170+
double right = mMargins.right();
1171+
double top = mMargins.top();
1172+
double bottom = mMargins.bottom();
1173+
1174+
if ( dataDefinedProperties().isActive( QgsCallout::Margins ) )
1175+
{
1176+
const QVariant value = dataDefinedProperties().value( QgsCallout::Margins, context.expressionContext() );
1177+
if ( !value.isNull() )
1178+
{
1179+
if ( value.type() == QVariant::List )
1180+
{
1181+
const QVariantList list = value.toList();
1182+
if ( list.size() == 4 )
1183+
{
1184+
bool topOk = false;
1185+
bool rightOk = false;
1186+
bool bottomOk = false;
1187+
bool leftOk = false;
1188+
double evaluatedTop = list.at( 0 ).toDouble( &topOk );
1189+
double evaluatedRight = list.at( 1 ).toDouble( &rightOk );
1190+
double evaluatedBottom = list.at( 2 ).toDouble( &bottomOk );
1191+
double evaluatedLeft = list.at( 3 ).toDouble( &leftOk );
1192+
if ( topOk && rightOk && bottomOk && leftOk )
1193+
{
1194+
left = evaluatedLeft;
1195+
top = evaluatedTop;
1196+
right = evaluatedRight;
1197+
bottom = evaluatedBottom;
1198+
}
1199+
}
1200+
}
1201+
else
1202+
{
1203+
const QStringList list = value.toString().trimmed().split( ',' );
1204+
if ( list.count() == 4 )
1205+
{
1206+
bool topOk = false;
1207+
bool rightOk = false;
1208+
bool bottomOk = false;
1209+
bool leftOk = false;
1210+
double evaluatedTop = list.at( 0 ).toDouble( &topOk );
1211+
double evaluatedRight = list.at( 1 ).toDouble( &rightOk );
1212+
double evaluatedBottom = list.at( 2 ).toDouble( &bottomOk );
1213+
double evaluatedLeft = list.at( 3 ).toDouble( &leftOk );
1214+
if ( topOk && rightOk && bottomOk && leftOk )
1215+
{
1216+
left = evaluatedLeft;
1217+
top = evaluatedTop;
1218+
right = evaluatedRight;
1219+
bottom = evaluatedBottom;
1220+
}
1221+
}
1222+
}
1223+
}
1224+
}
1225+
1226+
const double marginLeft = context.convertToPainterUnits( left, mMarginUnit );
1227+
const double marginRight = context.convertToPainterUnits( right, mMarginUnit );
1228+
const double marginTop = context.convertToPainterUnits( top, mMarginUnit );
1229+
const double marginBottom = context.convertToPainterUnits( bottom, mMarginUnit );
1230+
1231+
const QRectF expandedRect( rect.left() - marginLeft, rect.top() + marginBottom,
1232+
rect.width() + marginLeft + marginRight,
1233+
rect.height() - marginTop - marginBottom );
1234+
1235+
// IMPORTANT -- check for degenerate height is >=0, because QRectF are not normalized and we are using painter
1236+
// coordinates with descending vertical axis!
1237+
if ( expandedRect.width() <= 0 || expandedRect.height() >= 0 )
1238+
return QPolygonF();
1239+
1240+
return QgsShapeGenerator::createBalloon( origin, expandedRect, segmentPointWidth );
1241+
}

‎src/core/callouts/qgscallout.h

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,13 @@
2424
#include "qgspropertycollection.h"
2525
#include "qgsmapunitscale.h"
2626
#include "qgscalloutposition.h"
27+
#include "qgsmargins.h"
2728
#include <QString>
2829
#include <QRectF>
2930
#include <memory>
3031

3132
class QgsLineSymbol;
33+
class QgsFillSymbol;
3234
class QgsGeometry;
3335
class QgsRenderContext;
3436

@@ -61,6 +63,10 @@ class CORE_EXPORT QgsCallout
6163
{
6264
sipType = sipType_QgsCurvedLineCallout;
6365
}
66+
else if ( sipCpp->type() == "balloon" && dynamic_cast<QgsBalloonCallout *>( sipCpp ) != NULL )
67+
{
68+
sipType = sipType_QgsBalloonCallout;
69+
}
6470
else
6571
{
6672
sipType = 0;
@@ -85,6 +91,7 @@ class CORE_EXPORT QgsCallout
8591
DestinationY, //!< Y-coordinate of callout destination (feature anchor) (since QGIS 3.20)
8692
Curvature, //!< Curvature of curved line callouts (since QGIS 3.20)
8793
Orientation, //!< Orientation of curved line callouts (since QGIS 3.20)
94+
Margins, //!< Margin from text (since QGIS 3.20)
8895
};
8996

9097
//! Options for draw order (stacking) of callouts
@@ -844,5 +851,169 @@ class CORE_EXPORT QgsCurvedLineCallout : public QgsSimpleLineCallout
844851
double mCurvature = 0.1;
845852
};
846853

854+
855+
/**
856+
* \ingroup core
857+
* \brief A cartoon talking bubble callout style.
858+
*
859+
* \since QGIS 3.20
860+
*/
861+
class CORE_EXPORT QgsBalloonCallout : public QgsCallout
862+
{
863+
public:
864+
865+
QgsBalloonCallout();
866+
~QgsBalloonCallout() override;
867+
868+
#ifndef SIP_RUN
869+
870+
/**
871+
* Copy constructor.
872+
*/
873+
QgsBalloonCallout( const QgsBalloonCallout &other );
874+
QgsBalloonCallout &operator=( const QgsBalloonCallout & ) = delete;
875+
#endif
876+
877+
/**
878+
* Creates a new QgsBalloonCallout, using the settings
879+
* serialized in the \a properties map (corresponding to the output from
880+
* QgsBalloonCallout::properties() ).
881+
*/
882+
static QgsCallout *create( const QVariantMap &properties = QVariantMap(), const QgsReadWriteContext &context = QgsReadWriteContext() ) SIP_FACTORY;
883+
884+
QString type() const override;
885+
QgsBalloonCallout *clone() const override;
886+
QVariantMap properties( const QgsReadWriteContext &context ) const override;
887+
void readProperties( const QVariantMap &props, const QgsReadWriteContext &context ) override;
888+
void startRender( QgsRenderContext &context ) override;
889+
void stopRender( QgsRenderContext &context ) override;
890+
QSet< QString > referencedFields( const QgsRenderContext &context ) const override;
891+
892+
/**
893+
* Returns the fill symbol used to render the callout.
894+
*
895+
* Ownership is not transferred.
896+
*
897+
* \see setFillSymbol()
898+
*/
899+
QgsFillSymbol *fillSymbol();
900+
901+
/**
902+
* Sets the fill \a symbol used to render the callout. Ownership of \a symbol is
903+
* transferred to the callout.
904+
*
905+
* \see fillSymbol()
906+
*/
907+
void setFillSymbol( QgsFillSymbol *symbol SIP_TRANSFER );
908+
909+
/**
910+
* Returns the offset distance from the anchor point at which to start the line. Units are specified through offsetFromAnchorUnit().
911+
* \see setOffsetFromAnchor()
912+
* \see offsetFromAnchorUnit()
913+
*/
914+
double offsetFromAnchor() const { return mOffsetFromAnchorDistance; }
915+
916+
/**
917+
* Sets the offset \a distance from the anchor point at which to start the line. Units are specified through setOffsetFromAnchorUnit().
918+
* \see offsetFromAnchor()
919+
* \see setOffsetFromAnchorUnit()
920+
*/
921+
void setOffsetFromAnchor( double distance ) { mOffsetFromAnchorDistance = distance; }
922+
923+
/**
924+
* Sets the \a unit for the offset from anchor distance.
925+
* \see offsetFromAnchor()
926+
* \see setOffsetFromAnchor()
927+
*/
928+
void setOffsetFromAnchorUnit( QgsUnitTypes::RenderUnit unit ) { mOffsetFromAnchorUnit = unit; }
929+
930+
/**
931+
* Returns the units for the offset from anchor point.
932+
* \see setOffsetFromAnchorUnit()
933+
* \see offsetFromAnchor()
934+
*/
935+
QgsUnitTypes::RenderUnit offsetFromAnchorUnit() const { return mOffsetFromAnchorUnit; }
936+
937+
/**
938+
* Sets the map unit \a scale for the offset from anchor.
939+
* \see offsetFromAnchorMapUnitScale()
940+
* \see setOffsetFromAnchorUnit()
941+
* \see setOffsetFromAnchor()
942+
*/
943+
void setOffsetFromAnchorMapUnitScale( const QgsMapUnitScale &scale ) { mOffsetFromAnchorScale = scale; }
944+
945+
/**
946+
* Returns the map unit scale for the offset from anchor.
947+
* \see setOffsetFromAnchorMapUnitScale()
948+
* \see offsetFromAnchorUnit()
949+
* \see offsetFromAnchor()
950+
*/
951+
const QgsMapUnitScale &offsetFromAnchorMapUnitScale() const { return mOffsetFromAnchorScale; }
952+
953+
/**
954+
* Returns the margins between the outside of the callout frame and the label's bounding rectangle.
955+
*
956+
* Units are retrieved via marginsUnit()
957+
*
958+
* \note Negative margins are acceptable.
959+
*
960+
* \see setMargins()
961+
* \see marginsUnit()
962+
*/
963+
const QgsMargins &margins() const { return mMargins; }
964+
965+
/**
966+
* Sets the \a margins between the outside of the callout frame and the label's bounding rectangle.
967+
*
968+
* Units are set via setMarginsUnit()
969+
*
970+
* \note Negative margins are acceptable.
971+
*
972+
* \see margins()
973+
* \see setMarginsUnit()
974+
*/
975+
void setMargins( const QgsMargins &margins ) { mMargins = margins; }
976+
977+
/**
978+
* Sets the \a unit for the margins between the outside of the callout frame and the label's bounding rectangle.
979+
*
980+
* \see margins()
981+
* \see marginsUnit()
982+
*/
983+
void setMarginsUnit( QgsUnitTypes::RenderUnit unit ) { mMarginUnit = unit; }
984+
985+
/**
986+
* Returns the units for the margins between the outside of the callout frame and the label's bounding rectangle.
987+
*
988+
* \see setMarginsUnit()
989+
* \see margins()
990+
*/
991+
QgsUnitTypes::RenderUnit marginsUnit() const { return mMarginUnit; }
992+
993+
protected:
994+
void draw( QgsRenderContext &context, const QRectF &bodyBoundingBox, const double angle, const QgsGeometry &anchor, QgsCallout::QgsCalloutContext &calloutContext ) override;
995+
996+
private:
997+
998+
QPolygonF getPoints( QgsRenderContext &context, QgsPointXY origin, QRectF rect ) const;
999+
1000+
#ifdef SIP_RUN
1001+
QgsBalloonCallout( const QgsBalloonCallout &other );
1002+
QgsBalloonCallout &operator=( const QgsBalloonCallout & );
1003+
#endif
1004+
1005+
std::unique_ptr< QgsFillSymbol > mFillSymbol;
1006+
1007+
double mOffsetFromAnchorDistance = 0;
1008+
QgsUnitTypes::RenderUnit mOffsetFromAnchorUnit = QgsUnitTypes::RenderMillimeters;
1009+
QgsMapUnitScale mOffsetFromAnchorScale;
1010+
1011+
QgsMargins mMargins;
1012+
QgsUnitTypes::RenderUnit mMarginUnit = QgsUnitTypes::RenderMillimeters;
1013+
1014+
};
1015+
1016+
1017+
8471018
#endif // QGSCALLOUT_H
8481019

‎src/core/callouts/qgscalloutsregistry.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ QgsCalloutRegistry::QgsCalloutRegistry()
5252
addCalloutType( new QgsCalloutMetadata( QStringLiteral( "simple" ), QObject::tr( "Simple lines" ), QgsApplication::getThemeIcon( QStringLiteral( "labelingCalloutSimple.svg" ) ), QgsSimpleLineCallout::create ) );
5353
addCalloutType( new QgsCalloutMetadata( QStringLiteral( "manhattan" ), QObject::tr( "Manhattan lines" ), QgsApplication::getThemeIcon( QStringLiteral( "labelingCalloutManhattan.svg" ) ), QgsManhattanLineCallout::create ) );
5454
addCalloutType( new QgsCalloutMetadata( QStringLiteral( "curved" ), QObject::tr( "Curved lines" ), QgsApplication::getThemeIcon( QStringLiteral( "labelingCalloutSimple.svg" ) ), QgsCurvedLineCallout::create ) );
55+
addCalloutType( new QgsCalloutMetadata( QStringLiteral( "balloon" ), QObject::tr( "Balloons" ), QgsApplication::getThemeIcon( QStringLiteral( "labelingCalloutManhattan.svg" ) ), QgsBalloonCallout::create ) );
5556
}
5657

5758
QgsCalloutRegistry::~QgsCalloutRegistry()

‎src/gui/callouts/qgscalloutwidget.cpp

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -520,5 +520,152 @@ void QgsCurvedLineCalloutWidget::drawToAllPartsToggled( bool active )
520520
}
521521

522522

523+
//
524+
// QgsBalloonCalloutWidget
525+
//
526+
527+
QgsBalloonCalloutWidget::QgsBalloonCalloutWidget( QgsVectorLayer *vl, QWidget *parent )
528+
: QgsCalloutWidget( parent, vl )
529+
{
530+
setupUi( this );
531+
532+
// Callout options - to move to custom widgets when multiple callout styles exist
533+
mCalloutFillStyleButton->setSymbolType( QgsSymbol::Fill );
534+
mCalloutFillStyleButton->setDialogTitle( tr( "Balloon Symbol" ) );
535+
mCalloutFillStyleButton->registerExpressionContextGenerator( this );
536+
537+
mCalloutFillStyleButton->setLayer( vl );
538+
mOffsetFromAnchorUnitWidget->setUnits( QgsUnitTypes::RenderUnitList() << QgsUnitTypes::RenderMillimeters << QgsUnitTypes::RenderMetersInMapUnits << QgsUnitTypes::RenderMapUnits << QgsUnitTypes::RenderPixels
539+
<< QgsUnitTypes::RenderPoints << QgsUnitTypes::RenderInches );
540+
mMarginUnitWidget->setUnits( QgsUnitTypes::RenderUnitList() << QgsUnitTypes::RenderMillimeters << QgsUnitTypes::RenderMetersInMapUnits << QgsUnitTypes::RenderMapUnits << QgsUnitTypes::RenderPixels
541+
<< QgsUnitTypes::RenderPoints << QgsUnitTypes::RenderInches );
542+
543+
mSpinBottomMargin->setClearValue( 0 );
544+
mSpinTopMargin->setClearValue( 0 );
545+
mSpinRightMargin->setClearValue( 0 );
546+
mSpinLeftMargin->setClearValue( 0 );
547+
548+
connect( mOffsetFromAnchorUnitWidget, &QgsUnitSelectionWidget::changed, this, &QgsBalloonCalloutWidget::offsetFromAnchorUnitWidgetChanged );
549+
connect( mOffsetFromAnchorSpin, static_cast < void ( QDoubleSpinBox::* )( double ) > ( &QDoubleSpinBox::valueChanged ), this, &QgsBalloonCalloutWidget::offsetFromAnchorChanged );
550+
551+
// Anchor point options
552+
mAnchorPointComboBox->addItem( tr( "Pole of Inaccessibility" ), static_cast< int >( QgsCallout::PoleOfInaccessibility ) );
553+
mAnchorPointComboBox->addItem( tr( "Point on Exterior" ), static_cast< int >( QgsCallout::PointOnExterior ) );
554+
mAnchorPointComboBox->addItem( tr( "Point on Surface" ), static_cast< int >( QgsCallout::PointOnSurface ) );
555+
mAnchorPointComboBox->addItem( tr( "Centroid" ), static_cast< int >( QgsCallout::Centroid ) );
556+
connect( mAnchorPointComboBox, static_cast<void ( QComboBox::* )( int )>( &QComboBox::currentIndexChanged ), this, &QgsBalloonCalloutWidget::mAnchorPointComboBox_currentIndexChanged );
557+
558+
connect( mCalloutFillStyleButton, &QgsSymbolButton::changed, this, &QgsBalloonCalloutWidget::fillSymbolChanged );
559+
560+
connect( mSpinBottomMargin, qOverload< double >( &QDoubleSpinBox::valueChanged ), this, [ = ]( double value )
561+
{
562+
QgsMargins margins = mCallout->margins();
563+
margins.setBottom( value );
564+
mCallout->setMargins( margins );
565+
emit changed();
566+
} );
567+
connect( mSpinTopMargin, qOverload< double >( &QDoubleSpinBox::valueChanged ), this, [ = ]( double value )
568+
{
569+
QgsMargins margins = mCallout->margins();
570+
margins.setTop( value );
571+
mCallout->setMargins( margins );
572+
emit changed();
573+
} );
574+
connect( mSpinLeftMargin, qOverload< double >( &QDoubleSpinBox::valueChanged ), this, [ = ]( double value )
575+
{
576+
QgsMargins margins = mCallout->margins();
577+
margins.setLeft( value );
578+
mCallout->setMargins( margins );
579+
emit changed();
580+
} );
581+
connect( mSpinRightMargin, qOverload< double >( &QDoubleSpinBox::valueChanged ), this, [ = ]( double value )
582+
{
583+
QgsMargins margins = mCallout->margins();
584+
margins.setRight( value );
585+
mCallout->setMargins( margins );
586+
emit changed();
587+
} );
588+
connect( mMarginUnitWidget, &QgsUnitSelectionWidget::changed, this, [ = ]
589+
{
590+
mCallout->setMarginsUnit( mMarginUnitWidget->unit() );
591+
emit changed();
592+
} );
593+
594+
}
595+
596+
void QgsBalloonCalloutWidget::setCallout( QgsCallout *callout )
597+
{
598+
if ( !callout )
599+
return;
600+
601+
mCallout.reset( dynamic_cast<QgsBalloonCallout *>( callout->clone() ) );
602+
if ( !mCallout )
603+
return;
604+
605+
mOffsetFromAnchorUnitWidget->blockSignals( true );
606+
mOffsetFromAnchorUnitWidget->setUnit( mCallout->offsetFromAnchorUnit() );
607+
mOffsetFromAnchorUnitWidget->setMapUnitScale( mCallout->offsetFromAnchorMapUnitScale() );
608+
mOffsetFromAnchorUnitWidget->blockSignals( false );
609+
whileBlocking( mOffsetFromAnchorSpin )->setValue( mCallout->offsetFromAnchor() );
610+
611+
whileBlocking( mSpinBottomMargin )->setValue( mCallout->margins().bottom() );
612+
whileBlocking( mSpinTopMargin )->setValue( mCallout->margins().top() );
613+
whileBlocking( mSpinLeftMargin )->setValue( mCallout->margins().left() );
614+
whileBlocking( mSpinRightMargin )->setValue( mCallout->margins().right() );
615+
whileBlocking( mMarginUnitWidget )->setUnit( mCallout->marginsUnit() );
616+
617+
whileBlocking( mCalloutFillStyleButton )->setSymbol( mCallout->fillSymbol()->clone() );
618+
619+
whileBlocking( mAnchorPointComboBox )->setCurrentIndex( mAnchorPointComboBox->findData( static_cast< int >( callout->anchorPoint() ) ) );
620+
621+
registerDataDefinedButton( mOffsetFromAnchorDDBtn, QgsCallout::OffsetFromAnchor );
622+
registerDataDefinedButton( mAnchorPointDDBtn, QgsCallout::AnchorPointPosition );
623+
624+
registerDataDefinedButton( mDestXDDBtn, QgsCallout::DestinationX );
625+
registerDataDefinedButton( mDestYDDBtn, QgsCallout::DestinationY );
626+
registerDataDefinedButton( mMarginsDDBtn, QgsCallout::Margins );
627+
}
628+
629+
void QgsBalloonCalloutWidget::setGeometryType( QgsWkbTypes::GeometryType type )
630+
{
631+
bool isPolygon = type == QgsWkbTypes::PolygonGeometry;
632+
mAnchorPointLbl->setEnabled( isPolygon );
633+
mAnchorPointLbl->setVisible( isPolygon );
634+
mAnchorPointComboBox->setEnabled( isPolygon );
635+
mAnchorPointComboBox->setVisible( isPolygon );
636+
mAnchorPointDDBtn->setEnabled( isPolygon );
637+
mAnchorPointDDBtn->setVisible( isPolygon );
638+
}
639+
640+
QgsCallout *QgsBalloonCalloutWidget::callout()
641+
{
642+
return mCallout.get();
643+
}
644+
645+
void QgsBalloonCalloutWidget::offsetFromAnchorUnitWidgetChanged()
646+
{
647+
mCallout->setOffsetFromAnchorUnit( mOffsetFromAnchorUnitWidget->unit() );
648+
mCallout->setOffsetFromAnchorMapUnitScale( mOffsetFromAnchorUnitWidget->getMapUnitScale() );
649+
emit changed();
650+
}
651+
652+
void QgsBalloonCalloutWidget::offsetFromAnchorChanged()
653+
{
654+
mCallout->setOffsetFromAnchor( mOffsetFromAnchorSpin->value() );
655+
emit changed();
656+
}
657+
658+
void QgsBalloonCalloutWidget::fillSymbolChanged()
659+
{
660+
mCallout->setFillSymbol( mCalloutFillStyleButton->clonedSymbol< QgsFillSymbol >() );
661+
emit changed();
662+
}
663+
664+
void QgsBalloonCalloutWidget::mAnchorPointComboBox_currentIndexChanged( int index )
665+
{
666+
mCallout->setAnchorPoint( static_cast<QgsCallout::AnchorPoint>( mAnchorPointComboBox->itemData( index ).toInt() ) );
667+
emit changed();
668+
}
669+
523670
///@endcond
524671

‎src/gui/callouts/qgscalloutwidget.h

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,40 @@ class GUI_EXPORT QgsCurvedLineCalloutWidget : public QgsCalloutWidget, private U
215215
};
216216

217217

218+
///////////
219+
220+
#include "ui_widget_ballooncallout.h"
221+
222+
class QgsBalloonCallout;
223+
224+
class GUI_EXPORT QgsBalloonCalloutWidget : public QgsCalloutWidget, private Ui::WidgetBalloonCallout
225+
{
226+
Q_OBJECT
227+
228+
public:
229+
230+
QgsBalloonCalloutWidget( QgsVectorLayer *vl, QWidget *parent SIP_TRANSFERTHIS = nullptr );
231+
232+
static QgsCalloutWidget *create( QgsVectorLayer *vl ) SIP_FACTORY { return new QgsBalloonCalloutWidget( vl ); }
233+
234+
void setCallout( QgsCallout *callout ) override;
235+
236+
QgsCallout *callout() override;
237+
238+
void setGeometryType( QgsWkbTypes::GeometryType type ) override;
239+
240+
private slots:
241+
242+
void offsetFromAnchorUnitWidgetChanged();
243+
void offsetFromAnchorChanged();
244+
void fillSymbolChanged();
245+
void mAnchorPointComboBox_currentIndexChanged( int index );
246+
247+
private:
248+
std::unique_ptr< QgsBalloonCallout > mCallout;
249+
250+
};
251+
218252
#endif
219253
///@endcond
220254

‎src/gui/labeling/qgslabelinggui.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ void QgsLabelingGui::initCalloutWidgets()
8585
_initCalloutWidgetFunction( QStringLiteral( "simple" ), QgsSimpleLineCalloutWidget::create );
8686
_initCalloutWidgetFunction( QStringLiteral( "manhattan" ), QgsManhattanLineCalloutWidget::create );
8787
_initCalloutWidgetFunction( QStringLiteral( "curved" ), QgsCurvedLineCalloutWidget::create );
88+
_initCalloutWidgetFunction( QStringLiteral( "balloon" ), QgsBalloonCalloutWidget::create );
8889
}
8990

9091
void QgsLabelingGui::updateCalloutWidget( QgsCallout *callout )

‎src/ui/callouts/widget_ballooncallout.ui

Lines changed: 480 additions & 0 deletions
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)
Please sign in to comment.