diff --git a/python/PyQt6/core/auto_generated/callouts/qgscallout.sip.in b/python/PyQt6/core/auto_generated/callouts/qgscallout.sip.in index 53aa58387ba1d..eb48af799d7c7 100644 --- a/python/PyQt6/core/auto_generated/callouts/qgscallout.sip.in +++ b/python/PyQt6/core/auto_generated/callouts/qgscallout.sip.in @@ -908,6 +908,35 @@ Sets the fill ``symbol`` used to render the callout. Ownership of ``symbol`` is transferred to the callout. .. seealso:: :py:func:`fillSymbol` +%End + + QgsMarkerSymbol *markerSymbol(); +%Docstring +Returns the marker symbol used to render the callout endpoint. + +May be ``None``, if no endpoint marker will be used. + +The marker will always be rendered below the fill symbol for the callout. + +Ownership is not transferred. + +.. seealso:: :py:func:`setMarkerSymbol` + +.. versionadded:: 3.40 +%End + + void setMarkerSymbol( QgsMarkerSymbol *symbol /Transfer/ ); +%Docstring +Sets the marker ``symbol`` used to render the callout endpoint. Ownership of ``symbol`` is +transferred to the callout. + +Set to ``None`` to disable the endpoint marker. + +The marker will always be rendered below the fill symbol for the callout. + +.. seealso:: :py:func:`markerSymbol` + +.. versionadded:: 3.40 %End double offsetFromAnchor() const; diff --git a/python/core/auto_generated/callouts/qgscallout.sip.in b/python/core/auto_generated/callouts/qgscallout.sip.in index 6ea45ba21b98f..d7e7063aa2fb2 100644 --- a/python/core/auto_generated/callouts/qgscallout.sip.in +++ b/python/core/auto_generated/callouts/qgscallout.sip.in @@ -908,6 +908,35 @@ Sets the fill ``symbol`` used to render the callout. Ownership of ``symbol`` is transferred to the callout. .. seealso:: :py:func:`fillSymbol` +%End + + QgsMarkerSymbol *markerSymbol(); +%Docstring +Returns the marker symbol used to render the callout endpoint. + +May be ``None``, if no endpoint marker will be used. + +The marker will always be rendered below the fill symbol for the callout. + +Ownership is not transferred. + +.. seealso:: :py:func:`setMarkerSymbol` + +.. versionadded:: 3.40 +%End + + void setMarkerSymbol( QgsMarkerSymbol *symbol /Transfer/ ); +%Docstring +Sets the marker ``symbol`` used to render the callout endpoint. Ownership of ``symbol`` is +transferred to the callout. + +Set to ``None`` to disable the endpoint marker. + +The marker will always be rendered below the fill symbol for the callout. + +.. seealso:: :py:func:`markerSymbol` + +.. versionadded:: 3.40 %End double offsetFromAnchor() const; diff --git a/src/core/callouts/qgscallout.cpp b/src/core/callouts/qgscallout.cpp index 9be6f9caabb51..249af6942c394 100644 --- a/src/core/callouts/qgscallout.cpp +++ b/src/core/callouts/qgscallout.cpp @@ -31,6 +31,7 @@ #include "qgspainting.h" #include "qgsfillsymbol.h" #include "qgslinesymbol.h" +#include "qgsmarkersymbol.h" #include "qgsunittypes.h" #include @@ -1042,6 +1043,7 @@ QgsBalloonCallout::~QgsBalloonCallout() = default; QgsBalloonCallout::QgsBalloonCallout( const QgsBalloonCallout &other ) : QgsCallout( other ) , mFillSymbol( other.mFillSymbol ? other.mFillSymbol->clone() : nullptr ) + , mMarkerSymbol( other.mMarkerSymbol ? other.mMarkerSymbol->clone() : nullptr ) , mOffsetFromAnchorDistance( other.mOffsetFromAnchorDistance ) , mOffsetFromAnchorUnit( other.mOffsetFromAnchorUnit ) , mOffsetFromAnchorScale( other.mOffsetFromAnchorScale ) @@ -1083,6 +1085,11 @@ QVariantMap QgsBalloonCallout::properties( const QgsReadWriteContext &context ) props[ QStringLiteral( "fillSymbol" ) ] = QgsSymbolLayerUtils::symbolProperties( mFillSymbol.get() ); } + if ( mMarkerSymbol ) + { + props[ QStringLiteral( "markerSymbol" ) ] = QgsSymbolLayerUtils::symbolProperties( mMarkerSymbol.get() ); + } + props[ QStringLiteral( "offsetFromAnchor" ) ] = mOffsetFromAnchorDistance; props[ QStringLiteral( "offsetFromAnchorUnit" ) ] = QgsUnitTypes::encodeUnit( mOffsetFromAnchorUnit ); props[ QStringLiteral( "offsetFromAnchorMapUnitScale" ) ] = QgsSymbolLayerUtils::encodeMapUnitScale( mOffsetFromAnchorScale ); @@ -1105,13 +1112,25 @@ void QgsBalloonCallout::readProperties( const QVariantMap &props, const QgsReadW { QgsCallout::readProperties( props, context ); - const QString fillSymbolDef = props.value( QStringLiteral( "fillSymbol" ) ).toString(); - QDomDocument doc( QStringLiteral( "symbol" ) ); - doc.setContent( fillSymbolDef ); - const QDomElement symbolElem = doc.firstChildElement( QStringLiteral( "symbol" ) ); - std::unique_ptr< QgsFillSymbol > fillSymbol( QgsSymbolLayerUtils::loadSymbol< QgsFillSymbol >( symbolElem, context ) ); - if ( fillSymbol ) - mFillSymbol = std::move( fillSymbol ); + { + const QString fillSymbolDef = props.value( QStringLiteral( "fillSymbol" ) ).toString(); + QDomDocument doc( QStringLiteral( "symbol" ) ); + doc.setContent( fillSymbolDef ); + const QDomElement symbolElem = doc.firstChildElement( QStringLiteral( "symbol" ) ); + std::unique_ptr< QgsFillSymbol > fillSymbol( QgsSymbolLayerUtils::loadSymbol< QgsFillSymbol >( symbolElem, context ) ); + if ( fillSymbol ) + mFillSymbol = std::move( fillSymbol ); + } + + { + const QString markerSymbolDef = props.value( QStringLiteral( "markerSymbol" ) ).toString(); + QDomDocument doc( QStringLiteral( "symbol" ) ); + doc.setContent( markerSymbolDef ); + const QDomElement symbolElem = doc.firstChildElement( QStringLiteral( "symbol" ) ); + std::unique_ptr< QgsMarkerSymbol > markerSymbol( QgsSymbolLayerUtils::loadSymbol< QgsMarkerSymbol >( symbolElem, context ) ); + if ( markerSymbol ) + mMarkerSymbol = std::move( markerSymbol ); + } mOffsetFromAnchorDistance = props.value( QStringLiteral( "offsetFromAnchor" ), 0 ).toDouble(); mOffsetFromAnchorUnit = QgsUnitTypes::decodeRenderUnit( props.value( QStringLiteral( "offsetFromAnchorUnit" ) ).toString() ); @@ -1134,6 +1153,8 @@ void QgsBalloonCallout::startRender( QgsRenderContext &context ) QgsCallout::startRender( context ); if ( mFillSymbol ) mFillSymbol->startRender( context ); + if ( mMarkerSymbol ) + mMarkerSymbol->startRender( context ); } void QgsBalloonCallout::stopRender( QgsRenderContext &context ) @@ -1141,6 +1162,8 @@ void QgsBalloonCallout::stopRender( QgsRenderContext &context ) QgsCallout::stopRender( context ); if ( mFillSymbol ) mFillSymbol->stopRender( context ); + if ( mMarkerSymbol ) + mMarkerSymbol->stopRender( context ); } QSet QgsBalloonCallout::referencedFields( const QgsRenderContext &context ) const @@ -1148,6 +1171,8 @@ QSet QgsBalloonCallout::referencedFields( const QgsRenderContext &conte QSet fields = QgsCallout::referencedFields( context ); if ( mFillSymbol ) fields.unite( mFillSymbol->usedAttributes( context ) ); + if ( mMarkerSymbol ) + fields.unite( mMarkerSymbol->usedAttributes( context ) ); return fields; } @@ -1161,11 +1186,30 @@ void QgsBalloonCallout::setFillSymbol( QgsFillSymbol *symbol ) mFillSymbol.reset( symbol ); } +QgsMarkerSymbol *QgsBalloonCallout::markerSymbol() +{ + return mMarkerSymbol.get(); +} + +void QgsBalloonCallout::setMarkerSymbol( QgsMarkerSymbol *symbol ) +{ + mMarkerSymbol.reset( symbol ); +} + void QgsBalloonCallout::draw( QgsRenderContext &context, const QRectF &rect, const double, const QgsGeometry &anchor, QgsCalloutContext &calloutContext ) { bool destinationIsPinned = false; QgsGeometry line = calloutLineToPart( QgsGeometry::fromRect( rect ), anchor.constGet(), context, calloutContext, destinationIsPinned ); + if ( mMarkerSymbol ) + { + if ( const QgsLineString *ls = qgsgeometry_cast< const QgsLineString * >( line.constGet() ) ) + { + QgsPoint anchorPoint = ls->endPoint(); + mMarkerSymbol->renderPoint( anchorPoint.toQPointF(), nullptr, context ); + } + } + double offsetFromAnchor = mOffsetFromAnchorDistance; if ( dataDefinedProperties().isActive( QgsCallout::Property::OffsetFromAnchor ) ) { diff --git a/src/core/callouts/qgscallout.h b/src/core/callouts/qgscallout.h index a15a0ad318d3d..c532ceaea67a4 100644 --- a/src/core/callouts/qgscallout.h +++ b/src/core/callouts/qgscallout.h @@ -33,6 +33,7 @@ #include class QgsLineSymbol; +class QgsMarkerSymbol; class QgsFillSymbol; class QgsGeometry; class QgsRenderContext; @@ -919,6 +920,33 @@ class CORE_EXPORT QgsBalloonCallout : public QgsCallout */ void setFillSymbol( QgsFillSymbol *symbol SIP_TRANSFER ); + /** + * Returns the marker symbol used to render the callout endpoint. + * + * May be NULLPTR, if no endpoint marker will be used. + * + * The marker will always be rendered below the fill symbol for the callout. + * + * Ownership is not transferred. + * + * \see setMarkerSymbol() + * \since QGIS 3.40 + */ + QgsMarkerSymbol *markerSymbol(); + + /** + * Sets the marker \a symbol used to render the callout endpoint. Ownership of \a symbol is + * transferred to the callout. + * + * Set to NULLPTR to disable the endpoint marker. + * + * The marker will always be rendered below the fill symbol for the callout. + * + * \see markerSymbol() + * \since QGIS 3.40 + */ + void setMarkerSymbol( QgsMarkerSymbol *symbol SIP_TRANSFER ); + /** * Returns the offset distance from the anchor point at which to start the line. Units are specified through offsetFromAnchorUnit(). * \see setOffsetFromAnchor() @@ -1125,6 +1153,7 @@ class CORE_EXPORT QgsBalloonCallout : public QgsCallout #endif std::unique_ptr< QgsFillSymbol > mFillSymbol; + std::unique_ptr< QgsMarkerSymbol > mMarkerSymbol; double mOffsetFromAnchorDistance = 0; Qgis::RenderUnit mOffsetFromAnchorUnit = Qgis::RenderUnit::Millimeters; diff --git a/src/gui/callouts/qgscalloutwidget.cpp b/src/gui/callouts/qgscalloutwidget.cpp index 874b8a1182236..3eb1d0b7c8634 100644 --- a/src/gui/callouts/qgscalloutwidget.cpp +++ b/src/gui/callouts/qgscalloutwidget.cpp @@ -23,6 +23,7 @@ #include "qgsauxiliarystorage.h" #include "qgslinesymbol.h" #include "qgsfillsymbol.h" +#include "qgsmarkersymbol.h" QgsExpressionContext QgsCalloutWidget::createExpressionContext() const { @@ -565,7 +566,14 @@ QgsBalloonCalloutWidget::QgsBalloonCalloutWidget( QgsMapLayer *vl, QWidget *pare mCalloutFillStyleButton->setDialogTitle( tr( "Balloon Symbol" ) ); mCalloutFillStyleButton->registerExpressionContextGenerator( this ); + mMarkerSymbolButton->setSymbolType( Qgis::SymbolType::Marker ); + mMarkerSymbolButton->setDialogTitle( tr( "Marker Symbol" ) ); + mMarkerSymbolButton->registerExpressionContextGenerator( this ); + mMarkerSymbolButton->setShowNull( true ); + mMarkerSymbolButton->setToNull(); + mCalloutFillStyleButton->setLayer( qobject_cast< QgsVectorLayer * >( vl ) ); + mMarkerSymbolButton->setLayer( qobject_cast< QgsVectorLayer * >( vl ) ); mOffsetFromAnchorUnitWidget->setUnits( QgsUnitTypes::RenderUnitList() << Qgis::RenderUnit::Millimeters << Qgis::RenderUnit::MetersInMapUnits << Qgis::RenderUnit::MapUnits << Qgis::RenderUnit::Pixels << Qgis::RenderUnit::Points << Qgis::RenderUnit::Inches ); mMarginUnitWidget->setUnits( QgsUnitTypes::RenderUnitList() << Qgis::RenderUnit::Millimeters << Qgis::RenderUnit::MetersInMapUnits << Qgis::RenderUnit::MapUnits << Qgis::RenderUnit::Pixels @@ -593,6 +601,7 @@ QgsBalloonCalloutWidget::QgsBalloonCalloutWidget( QgsMapLayer *vl, QWidget *pare connect( mAnchorPointComboBox, static_cast( &QComboBox::currentIndexChanged ), this, &QgsBalloonCalloutWidget::mAnchorPointComboBox_currentIndexChanged ); connect( mCalloutFillStyleButton, &QgsSymbolButton::changed, this, &QgsBalloonCalloutWidget::fillSymbolChanged ); + connect( mMarkerSymbolButton, &QgsSymbolButton::changed, this, &QgsBalloonCalloutWidget::markerSymbolChanged ); connect( mSpinBottomMargin, qOverload< double >( &QDoubleSpinBox::valueChanged ), this, [ = ]( double value ) { @@ -691,6 +700,10 @@ void QgsBalloonCalloutWidget::setCallout( const QgsCallout *callout ) whileBlocking( mCornerRadiusSpin )->setValue( mCallout->cornerRadius() ); whileBlocking( mCalloutFillStyleButton )->setSymbol( mCallout->fillSymbol()->clone() ); + if ( QgsMarkerSymbol *marker = mCallout->markerSymbol() ) + whileBlocking( mMarkerSymbolButton )->setSymbol( marker->clone() ); + else + whileBlocking( mMarkerSymbolButton )->setToNull(); whileBlocking( mAnchorPointComboBox )->setCurrentIndex( mAnchorPointComboBox->findData( static_cast< int >( callout->anchorPoint() ) ) ); @@ -742,6 +755,12 @@ void QgsBalloonCalloutWidget::fillSymbolChanged() emit changed(); } +void QgsBalloonCalloutWidget::markerSymbolChanged() +{ + mCallout->setMarkerSymbol( mMarkerSymbolButton->isNull() ? nullptr : mMarkerSymbolButton->clonedSymbol< QgsMarkerSymbol >() ); + emit changed(); +} + void QgsBalloonCalloutWidget::mAnchorPointComboBox_currentIndexChanged( int index ) { mCallout->setAnchorPoint( static_cast( mAnchorPointComboBox->itemData( index ).toInt() ) ); diff --git a/src/gui/callouts/qgscalloutwidget.h b/src/gui/callouts/qgscalloutwidget.h index d48b167bacc28..5875ce4f8f657 100644 --- a/src/gui/callouts/qgscalloutwidget.h +++ b/src/gui/callouts/qgscalloutwidget.h @@ -252,6 +252,7 @@ class GUI_EXPORT QgsBalloonCalloutWidget : public QgsCalloutWidget, private Ui:: void offsetFromAnchorUnitWidgetChanged(); void offsetFromAnchorChanged(); void fillSymbolChanged(); + void markerSymbolChanged(); void mAnchorPointComboBox_currentIndexChanged( int index ); void mCalloutBlendComboBox_currentIndexChanged( int index ); diff --git a/src/ui/callouts/widget_ballooncallout.ui b/src/ui/callouts/widget_ballooncallout.ui index c963d2758bd49..e02e1e04f09f9 100644 --- a/src/ui/callouts/widget_ballooncallout.ui +++ b/src/ui/callouts/widget_ballooncallout.ui @@ -7,7 +7,7 @@ 0 0 371 - 456 + 507 @@ -34,45 +34,32 @@ 0 - - - - - - - - + - - + + - Blend mode + Feature anchor point - - + + - Wedge width + - - - - - 0 - 0 - - + + - Symbol… + - - + + 10 @@ -80,14 +67,14 @@ - Qt::StrongFocus + Qt::FocusPolicy::StrongFocus - - + + - Fill style + Corner radius @@ -98,8 +85,8 @@ - - + + 1 @@ -123,43 +110,9 @@ - - - - - - - - - - - - 10 - 0 - - - - Qt::StrongFocus - - - - - - - - - - - - - - Feature anchor point - - - @@ -185,22 +138,55 @@ - - + + + + Blend mode + + + + + - - + + + + + 0 + 0 + + - Corner radius + Symbol… - - + + + + + + + + + + + + 10 + 0 + + + + Qt::FocusPolicy::StrongFocus + + + + + 1 @@ -224,26 +210,6 @@ - - - - - 10 - 0 - - - - Qt::StrongFocus - - - - - - - - - - @@ -259,7 +225,7 @@ - Qt::StrongFocus + Qt::FocusPolicy::StrongFocus @@ -429,7 +395,7 @@ - Qt::Horizontal + Qt::Orientation::Horizontal @@ -444,6 +410,60 @@ + + + + + + + + + + + Wedge width + + + + + + + + 10 + 0 + + + + Qt::FocusPolicy::StrongFocus + + + + + + + Fill style + + + + + + + End point marker + + + + + + + + 0 + 0 + + + + Symbol… + + + @@ -524,7 +544,7 @@ - Qt::Horizontal + Qt::Orientation::Horizontal @@ -552,7 +572,7 @@ - Qt::Vertical + Qt::Orientation::Vertical @@ -570,28 +590,28 @@ QDoubleSpinBox
qgsdoublespinbox.h
- - QgsCollapsibleGroupBox - QGroupBox -
qgscollapsiblegroupbox.h
- 1 -
QgsSymbolButton QToolButton
qgssymbolbutton.h
- - QgsPropertyOverrideButton - QToolButton -
qgspropertyoverridebutton.h
-
QgsUnitSelectionWidget QWidget
qgsunitselectionwidget.h
1
+ + QgsCollapsibleGroupBox + QGroupBox +
qgscollapsiblegroupbox.h
+ 1 +
+ + QgsPropertyOverrideButton + QToolButton +
qgspropertyoverridebutton.h
+
QgsBlendModeComboBox QWidget @@ -616,8 +636,8 @@ mOffsetFromAnchorUnitWidget mOffsetFromAnchorDDBtn mAnchorPointComboBox + mMarkerSymbolButton mAnchorPointDDBtn - mCalloutBlendComboBox mCalloutBlendModeDDBtn mDestXDDBtn mDestYDDBtn diff --git a/tests/src/core/testqgscallout.cpp b/tests/src/core/testqgscallout.cpp index 858cbe3595114..7b8d525cf957d 100644 --- a/tests/src/core/testqgscallout.cpp +++ b/tests/src/core/testqgscallout.cpp @@ -175,6 +175,7 @@ class TestQgsCallout: public QgsTest void balloonCalloutMargin(); void balloonCalloutWedgeWidth(); void balloonCalloutCornerRadius(); + void balloonCalloutMarkerSymbol(); void blendMode(); void calloutsBlend(); @@ -4146,6 +4147,70 @@ void TestQgsCallout::balloonCalloutCornerRadius() QGSVERIFYIMAGECHECK( "balloon_callout_corner_radius", "balloon_callout_corner_radius", img, QString(), 20, QSize( 0, 0 ), 2 ); } +void TestQgsCallout::balloonCalloutMarkerSymbol() +{ + const QSize size( 640, 480 ); + QgsMapSettings mapSettings; + mapSettings.setOutputSize( size ); + mapSettings.setExtent( vl->extent() ); + mapSettings.setLayers( QList() << vl ); + mapSettings.setOutputDpi( 96 ); + + // first render the map and labeling separately + + QgsMapRendererSequentialJob job( mapSettings ); + job.start(); + job.waitForFinished(); + + QImage img = job.renderedImage(); + + QPainter p( &img ); + QgsRenderContext context = QgsRenderContext::fromMapSettings( mapSettings ); + context.setPainter( &p ); + + QgsPalLayerSettings settings; + settings.fieldName = QStringLiteral( "Class" ); + settings.placement = Qgis::LabelPlacement::AroundPoint; + settings.dist = 7; + + QgsTextFormat format; + format.setFont( QgsFontUtils::getStandardTestFont( QStringLiteral( "Bold" ) ).family() ); + format.setSize( 12 ); + format.setNamedStyle( QStringLiteral( "Bold" ) ); + format.setColor( QColor( 200, 0, 200 ) ); + settings.setFormat( format ); + + QgsBalloonCallout *callout = new QgsBalloonCallout(); + callout->setEnabled( true ); + callout->setFillSymbol( QgsFillSymbol::createSimple( QVariantMap( { { "color", "#ffcccc"}, + { "outline-width", "1"} + } ) ) ); + callout->setOffsetFromAnchor( 1 ); + + QVariantMap props; + props[QStringLiteral( "name" )] = QStringLiteral( "circle" ); + props[QStringLiteral( "size" )] = 5; + props[QStringLiteral( "color" )] = QStringLiteral( "200,255,200" ); + props[QStringLiteral( "outline_style" )] = QStringLiteral( "no" ); + callout->setMarkerSymbol( + QgsMarkerSymbol::createSimple( props ) + ); + settings.setCallout( callout ); + + vl->setLabeling( new QgsVectorLayerSimpleLabeling( settings ) ); + vl->setLabelsEnabled( true ); + + QgsDefaultLabelingEngine engine; + engine.setMapSettings( mapSettings ); + engine.addProvider( new QgsVectorLayerLabelProvider( vl, QString(), true, &settings ) ); + //engine.setFlags( QgsLabelingEngine::RenderOutlineLabels | QgsLabelingEngine::DrawLabelRectOnly ); + engine.run( context ); + + p.end(); + + QGSVERIFYIMAGECHECK( "balloon_callout_render_marker_symbol", "balloon_callout_render_marker_symbol", img, QString(), 20, QSize( 0, 0 ), 2 ); +} + void TestQgsCallout::blendMode() { std::unique_ptr< QgsManhattanLineCallout > callout = std::make_unique< QgsManhattanLineCallout >(); diff --git a/tests/testdata/control_images/callouts/expected_balloon_callout_render_marker_symbol/expected_balloon_callout_render_marker_symbol.png b/tests/testdata/control_images/callouts/expected_balloon_callout_render_marker_symbol/expected_balloon_callout_render_marker_symbol.png new file mode 100644 index 0000000000000..e2518f35a64ea Binary files /dev/null and b/tests/testdata/control_images/callouts/expected_balloon_callout_render_marker_symbol/expected_balloon_callout_render_marker_symbol.png differ