From da79b6f0e6e911b806b4a1d242416da704cf1b8f Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Thu, 27 Jun 2019 13:32:17 +1000 Subject: [PATCH] [FEATURE][labels] New text "background" setting for marker symbol backgrounds Allows for rendering a marker symbol in the background of labels (complementing the existing shapes/SVG choices). This allows use of QGIS marker symbology as a background symbol behind labels (e.g. for highway shield labels) --- .../auto_generated/qgstextrenderer.sip.in | 31 +- .../auto_generated/qgstextformatwidget.sip.in | 5 +- src/core/qgspallabeling.cpp | 13 + src/core/qgstextrenderer.cpp | 503 +++++----- src/core/qgstextrenderer.h | 22 +- src/core/qgstextrenderer_p.h | 16 +- src/core/qgsvectorlayerlabeling.cpp | 11 + src/core/qgsvectorlayerlabelprovider.cpp | 2 +- src/gui/qgslabelinggui.cpp | 1 + src/gui/qgslabelinggui.h | 2 +- src/gui/qgssymbolbutton.cpp | 1 + src/gui/qgstextformatwidget.cpp | 111 ++- src/gui/qgstextformatwidget.h | 5 +- src/ui/qgstextformatwidgetbase.ui | 877 +++++++++--------- tests/src/python/test_qgstextformatwidget.py | 8 +- tests/src/python/test_qgstextrenderer.py | 78 +- .../background_marker_buffer_mapunits.png | Bin 0 -> 5380 bytes .../background_marker_buffer_mm.png | Bin 0 -> 5819 bytes .../background_marker_buffer_pixels.png | Bin 0 -> 5242 bytes .../background_marker_fixed_mapunits.png | Bin 0 -> 7809 bytes .../background_marker_fixed_mm.png | Bin 0 -> 5973 bytes .../background_marker_fixed_pixels.png | Bin 0 -> 4079 bytes 22 files changed, 967 insertions(+), 719 deletions(-) create mode 100644 tests/testdata/control_images/text_renderer/background_marker_buffer_mapunits/background_marker_buffer_mapunits.png create mode 100644 tests/testdata/control_images/text_renderer/background_marker_buffer_mm/background_marker_buffer_mm.png create mode 100644 tests/testdata/control_images/text_renderer/background_marker_buffer_pixels/background_marker_buffer_pixels.png create mode 100644 tests/testdata/control_images/text_renderer/background_marker_fixed_mapunits/background_marker_fixed_mapunits.png create mode 100644 tests/testdata/control_images/text_renderer/background_marker_fixed_mm/background_marker_fixed_mm.png create mode 100644 tests/testdata/control_images/text_renderer/background_marker_fixed_pixels/background_marker_fixed_pixels.png diff --git a/python/core/auto_generated/qgstextrenderer.sip.in b/python/core/auto_generated/qgstextrenderer.sip.in index 7eca16738eb..c59ae46e268 100644 --- a/python/core/auto_generated/qgstextrenderer.sip.in +++ b/python/core/auto_generated/qgstextrenderer.sip.in @@ -268,7 +268,8 @@ Container for settings relating to a text background object. ShapeSquare, ShapeEllipse, ShapeCircle, - ShapeSVG + ShapeSVG, + ShapeMarkerSymbol, }; enum SizeType @@ -344,6 +345,34 @@ QgsTextBackgroundSettings.ShapeSVG. The path must be absolute. :param file: Absolute SVG file path .. seealso:: :py:func:`svgFile` +%End + + QgsMarkerSymbol *markerSymbol() const; +%Docstring +Returns the marker symbol to be rendered in the background. Ownership remains with +the background settings. + +.. note:: + + This is only used when the type() is QgsTextBackgroundSettings.ShapeMarkerSymbol. + +.. seealso:: :py:func:`setMarkerSymbol` + +.. versionadded:: 3.10 +%End + + void setMarkerSymbol( QgsMarkerSymbol *symbol /Transfer/ ); +%Docstring +Sets the current marker ``symbol`` for the background shape. Ownership is transferred +to the background settings. + +.. note:: + + This is only used when the type() is QgsTextBackgroundSettings.ShapeMarkerSymbol. + +.. seealso:: :py:func:`markerSymbol` + +.. versionadded:: 3.10 %End SizeType sizeType() const; diff --git a/python/gui/auto_generated/qgstextformatwidget.sip.in b/python/gui/auto_generated/qgstextformatwidget.sip.in index 3080eff9b24..f8ab230657f 100644 --- a/python/gui/auto_generated/qgstextformatwidget.sip.in +++ b/python/gui/auto_generated/qgstextformatwidget.sip.in @@ -12,7 +12,7 @@ -class QgsTextFormatWidget : QWidget, protected Ui::QgsTextFormatWidgetBase +class QgsTextFormatWidget : QWidget, QgsExpressionContextGenerator, protected Ui::QgsTextFormatWidgetBase { %Docstring A widget for customizing text formatting settings. @@ -111,6 +111,9 @@ Controls whether data defined alignment buttons are enabled. :param enable: set to ``True`` to enable alignment controls %End + virtual QgsExpressionContext createExpressionContext() const; + + protected slots: diff --git a/src/core/qgspallabeling.cpp b/src/core/qgspallabeling.cpp index e06591704b0..f05981f168a 100644 --- a/src/core/qgspallabeling.cpp +++ b/src/core/qgspallabeling.cpp @@ -2865,6 +2865,10 @@ void QgsPalLayerSettings::parseShapeBackground( QgsRenderContext &context ) { shpkind = QgsTextBackgroundSettings::ShapeSVG; } + else if ( skind.compare( QLatin1String( "marker" ), Qt::CaseInsensitive ) == 0 ) + { + shpkind = QgsTextBackgroundSettings::ShapeMarkerSymbol; + } shapeKind = shpkind; dataDefinedValues.insert( QgsPalLayerSettings::ShapeKind, QVariant( static_cast< int >( shpkind ) ) ); } @@ -2930,7 +2934,16 @@ void QgsPalLayerSettings::parseShapeBackground( QgsRenderContext &context ) { skip = true; } + if ( shapeKind == QgsTextBackgroundSettings::ShapeMarkerSymbol + && ( !background.markerSymbol() + || ( background.markerSymbol() + && shpSizeType == QgsTextBackgroundSettings::SizeFixed + && ddShpSizeX == 0.0 ) ) ) + { + skip = true; + } if ( shapeKind != QgsTextBackgroundSettings::ShapeSVG + && shapeKind != QgsTextBackgroundSettings::ShapeMarkerSymbol && shpSizeType == QgsTextBackgroundSettings::SizeFixed && ( ddShpSizeX == 0.0 || ddShpSizeY == 0.0 ) ) { diff --git a/src/core/qgstextrenderer.cpp b/src/core/qgstextrenderer.cpp index fe978491385..ed35901a05e 100644 --- a/src/core/qgstextrenderer.cpp +++ b/src/core/qgstextrenderer.cpp @@ -177,13 +177,12 @@ void QgsTextBufferSettings::setBlendMode( QPainter::CompositionMode mode ) QgsPaintEffect *QgsTextBufferSettings::paintEffect() const { - return d->paintEffect; + return d->paintEffect.get(); } void QgsTextBufferSettings::setPaintEffect( QgsPaintEffect *effect ) { - delete d->paintEffect; - d->paintEffect = effect; + d->paintEffect.reset( effect ); } void QgsTextBufferSettings::readFromLayer( QgsVectorLayer *layer ) @@ -337,7 +336,7 @@ QDomElement QgsTextBufferSettings::writeXml( QDomDocument &doc ) const textBufferElem.setAttribute( QStringLiteral( "bufferOpacity" ), d->opacity ); textBufferElem.setAttribute( QStringLiteral( "bufferJoinStyle" ), static_cast< unsigned int >( d->joinStyle ) ); textBufferElem.setAttribute( QStringLiteral( "bufferBlendMode" ), QgsPainting::getBlendModeEnum( d->blendMode ) ); - if ( d->paintEffect && !QgsPaintEffectRegistry::isDefaultStack( d->paintEffect ) ) + if ( d->paintEffect && !QgsPaintEffectRegistry::isDefaultStack( d->paintEffect.get() ) ) d->paintEffect->saveProperties( doc, textBufferElem ); return textBufferElem; } @@ -399,6 +398,16 @@ void QgsTextBackgroundSettings::setSvgFile( const QString &file ) d->svgFile = file; } +QgsMarkerSymbol *QgsTextBackgroundSettings::markerSymbol() const +{ + return d->markerSymbol.get(); +} + +void QgsTextBackgroundSettings::setMarkerSymbol( QgsMarkerSymbol *symbol ) +{ + d->markerSymbol.reset( symbol ); +} + QgsTextBackgroundSettings::SizeType QgsTextBackgroundSettings::sizeType() const { return d->sizeType; @@ -601,13 +610,12 @@ void QgsTextBackgroundSettings::setJoinStyle( Qt::PenJoinStyle style ) QgsPaintEffect *QgsTextBackgroundSettings::paintEffect() const { - return d->paintEffect; + return d->paintEffect.get(); } void QgsTextBackgroundSettings::setPaintEffect( QgsPaintEffect *effect ) { - delete d->paintEffect; - d->paintEffect = effect; + d->paintEffect.reset( effect ); } void QgsTextBackgroundSettings::readFromLayer( QgsVectorLayer *layer ) @@ -861,6 +869,12 @@ void QgsTextBackgroundSettings::readXml( const QDomElement &elem, const QgsReadW setPaintEffect( QgsApplication::paintEffectRegistry()->createEffect( effectElem ) ); else setPaintEffect( nullptr ); + + const QDomElement symbolElem = backgroundElem.firstChildElement( QStringLiteral( "symbol" ) ); + if ( !symbolElem.isNull() ) + setMarkerSymbol( QgsSymbolLayerUtils::loadSymbol< QgsMarkerSymbol >( symbolElem, context ) ); + else + setMarkerSymbol( nullptr ); } QDomElement QgsTextBackgroundSettings::writeXml( QDomDocument &doc, const QgsReadWriteContext &context ) const @@ -892,8 +906,12 @@ QDomElement QgsTextBackgroundSettings::writeXml( QDomDocument &doc, const QgsRea backgroundElem.setAttribute( QStringLiteral( "shapeJoinStyle" ), static_cast< unsigned int >( d->joinStyle ) ); backgroundElem.setAttribute( QStringLiteral( "shapeOpacity" ), d->opacity ); backgroundElem.setAttribute( QStringLiteral( "shapeBlendMode" ), QgsPainting::getBlendModeEnum( d->blendMode ) ); - if ( d->paintEffect && !QgsPaintEffectRegistry::isDefaultStack( d->paintEffect ) ) + if ( d->paintEffect && !QgsPaintEffectRegistry::isDefaultStack( d->paintEffect.get() ) ) d->paintEffect->saveProperties( doc, backgroundElem ); + + if ( d->markerSymbol ) + backgroundElem.appendChild( QgsSymbolLayerUtils::saveSymbol( QStringLiteral( "marker" ), d->markerSymbol.get(), doc, context ) ); + return backgroundElem; } @@ -1857,7 +1875,7 @@ QgsTextFormat QgsTextRenderer::updateShadowPosition( const QgsTextFormat &format return format; QgsTextFormat tmpFormat = format; - if ( tmpFormat.background().enabled() ) + if ( tmpFormat.background().enabled() && tmpFormat.background().type() != QgsTextBackgroundSettings::ShapeMarkerSymbol ) // background shadow not compatible with marker symbol backgrounds { tmpFormat.shadow().setShadowPlacement( QgsTextShadowSettings::ShadowShape ); } @@ -2198,264 +2216,291 @@ void QgsTextRenderer::drawBackground( QgsRenderContext &context, QgsTextRenderer // TODO: the following label-buffered generated shapes and SVG symbols should be moved into marker symbology classes - if ( background.type() == QgsTextBackgroundSettings::ShapeSVG ) + switch ( background.type() ) { - // all calculations done in shapeSizeUnits, which are then passed to symbology class for painting - - if ( background.svgFile().isEmpty() ) - return; - - double sizeOut = 0.0; - // only one size used for SVG sizing/scaling (no use of shapeSize.y() or Y field in gui) - if ( background.sizeType() == QgsTextBackgroundSettings::SizeFixed ) + case QgsTextBackgroundSettings::ShapeSVG: + case QgsTextBackgroundSettings::ShapeMarkerSymbol: { - sizeOut = context.convertToPainterUnits( background.size().width(), background.sizeUnit(), background.sizeMapUnitScale() ); - } - else if ( background.sizeType() == QgsTextBackgroundSettings::SizeBuffer ) - { - sizeOut = std::max( component.size.width(), component.size.height() ); - double bufferSize = context.convertToPainterUnits( background.size().width(), background.sizeUnit(), background.sizeMapUnitScale() ); + // all calculations done in shapeSizeUnits, which are then passed to symbology class for painting - // add buffer - sizeOut += bufferSize * 2; - } + if ( background.type() == QgsTextBackgroundSettings::ShapeSVG && background.svgFile().isEmpty() ) + return; - // don't bother rendering symbols smaller than 1x1 pixels in size - // TODO: add option to not show any svgs under/over a certain size - if ( sizeOut < 1.0 ) - return; + if ( background.type() == QgsTextBackgroundSettings::ShapeMarkerSymbol && !background.markerSymbol() ) + return; - QgsStringMap map; // for SVG symbology marker - map[QStringLiteral( "name" )] = background.svgFile().trimmed(); - map[QStringLiteral( "size" )] = QString::number( sizeOut ); - map[QStringLiteral( "size_unit" )] = QgsUnitTypes::encodeUnit( QgsUnitTypes::RenderPixels ); - map[QStringLiteral( "angle" )] = QString::number( 0.0 ); // angle is handled by this local painter + double sizeOut = 0.0; + // only one size used for SVG sizing/scaling (no use of shapeSize.y() or Y field in gui) + if ( background.sizeType() == QgsTextBackgroundSettings::SizeFixed ) + { + sizeOut = context.convertToPainterUnits( background.size().width(), background.sizeUnit(), background.sizeMapUnitScale() ); + } + else if ( background.sizeType() == QgsTextBackgroundSettings::SizeBuffer ) + { + sizeOut = std::max( component.size.width(), component.size.height() ); + double bufferSize = context.convertToPainterUnits( background.size().width(), background.sizeUnit(), background.sizeMapUnitScale() ); - // offset is handled by this local painter - // TODO: see why the marker renderer doesn't seem to translate offset *after* applying rotation - //map["offset"] = QgsSymbolLayerUtils::encodePoint( tmpLyr.shapeOffset ); - //map["offset_unit"] = QgsUnitTypes::encodeUnit( - // tmpLyr.shapeOffsetUnits == QgsPalLayerSettings::MapUnits ? QgsUnitTypes::MapUnit : QgsUnitTypes::MM ); + // add buffer + sizeOut += bufferSize * 2; + } - map[QStringLiteral( "fill" )] = background.fillColor().name(); - map[QStringLiteral( "outline" )] = background.strokeColor().name(); - map[QStringLiteral( "outline-width" )] = QString::number( background.strokeWidth() ); - map[QStringLiteral( "outline_width_unit" )] = QgsUnitTypes::encodeUnit( background.strokeWidthUnit() ); + // don't bother rendering symbols smaller than 1x1 pixels in size + // TODO: add option to not show any svgs under/over a certain size + if ( sizeOut < 1.0 ) + return; - if ( format.shadow().enabled() && format.shadow().shadowPlacement() == QgsTextShadowSettings::ShadowShape ) - { - QgsTextShadowSettings shadow = format.shadow(); - // configure SVG shadow specs - QgsStringMap shdwmap( map ); - shdwmap[QStringLiteral( "fill" )] = shadow.color().name(); - shdwmap[QStringLiteral( "outline" )] = shadow.color().name(); - shdwmap[QStringLiteral( "size" )] = QString::number( sizeOut ); + std::unique_ptr< QgsMarkerSymbol > renderedSymbol; + if ( background.type() == QgsTextBackgroundSettings::ShapeSVG ) + { + QgsStringMap map; // for SVG symbology marker + map[QStringLiteral( "name" )] = background.svgFile().trimmed(); + map[QStringLiteral( "size" )] = QString::number( sizeOut ); + map[QStringLiteral( "size_unit" )] = QgsUnitTypes::encodeUnit( QgsUnitTypes::RenderPixels ); + map[QStringLiteral( "angle" )] = QString::number( 0.0 ); // angle is handled by this local painter - // store SVG's drawing in QPicture for drop shadow call - QPicture svgPict; - QPainter svgp; - svgp.begin( &svgPict ); + // offset is handled by this local painter + // TODO: see why the marker renderer doesn't seem to translate offset *after* applying rotation + //map["offset"] = QgsSymbolLayerUtils::encodePoint( tmpLyr.shapeOffset ); + //map["offset_unit"] = QgsUnitTypes::encodeUnit( + // tmpLyr.shapeOffsetUnits == QgsPalLayerSettings::MapUnits ? QgsUnitTypes::MapUnit : QgsUnitTypes::MM ); - // draw shadow symbol + map[QStringLiteral( "fill" )] = background.fillColor().name(); + map[QStringLiteral( "outline" )] = background.strokeColor().name(); + map[QStringLiteral( "outline-width" )] = QString::number( background.strokeWidth() ); + map[QStringLiteral( "outline_width_unit" )] = QgsUnitTypes::encodeUnit( background.strokeWidthUnit() ); - // clone current render context map unit/mm conversion factors, but not - // other map canvas parameters, then substitute this painter for use in symbology painting - // NOTE: this is because the shadow needs to be scaled correctly for output to map canvas, - // but will be created relative to the SVG's computed size, not the current map canvas - QgsRenderContext shdwContext; - shdwContext.setMapToPixel( context.mapToPixel() ); - shdwContext.setScaleFactor( context.scaleFactor() ); - shdwContext.setPainter( &svgp ); + if ( format.shadow().enabled() && format.shadow().shadowPlacement() == QgsTextShadowSettings::ShadowShape ) + { + QgsTextShadowSettings shadow = format.shadow(); + // configure SVG shadow specs + QgsStringMap shdwmap( map ); + shdwmap[QStringLiteral( "fill" )] = shadow.color().name(); + shdwmap[QStringLiteral( "outline" )] = shadow.color().name(); + shdwmap[QStringLiteral( "size" )] = QString::number( sizeOut ); - QgsSymbolLayer *symShdwL = QgsSvgMarkerSymbolLayer::create( shdwmap ); - QgsSvgMarkerSymbolLayer *svgShdwM = static_cast( symShdwL ); - QgsSymbolRenderContext svgShdwContext( shdwContext, QgsUnitTypes::RenderUnknownUnit, background.opacity() ); + // store SVG's drawing in QPicture for drop shadow call + QPicture svgPict; + QPainter svgp; + svgp.begin( &svgPict ); - svgShdwM->renderPoint( QPointF( sizeOut / 2, -sizeOut / 2 ), svgShdwContext ); - svgp.end(); + // draw shadow symbol - component.picture = svgPict; - // TODO: when SVG symbol's stroke width/units is fixed in QgsSvgCache, adjust for it here - component.pictureBuffer = 0.0; + // clone current render context map unit/mm conversion factors, but not + // other map canvas parameters, then substitute this painter for use in symbology painting + // NOTE: this is because the shadow needs to be scaled correctly for output to map canvas, + // but will be created relative to the SVG's computed size, not the current map canvas + QgsRenderContext shdwContext; + shdwContext.setMapToPixel( context.mapToPixel() ); + shdwContext.setScaleFactor( context.scaleFactor() ); + shdwContext.setPainter( &svgp ); - component.size = QSizeF( sizeOut, sizeOut ); - component.offset = QPointF( 0.0, 0.0 ); + QgsSymbolLayer *symShdwL = QgsSvgMarkerSymbolLayer::create( shdwmap ); + QgsSvgMarkerSymbolLayer *svgShdwM = static_cast( symShdwL ); + QgsSymbolRenderContext svgShdwContext( shdwContext, QgsUnitTypes::RenderUnknownUnit, background.opacity() ); - // rotate about origin center of SVG + svgShdwM->renderPoint( QPointF( sizeOut / 2, -sizeOut / 2 ), svgShdwContext ); + svgp.end(); + + component.picture = svgPict; + // TODO: when SVG symbol's stroke width/units is fixed in QgsSvgCache, adjust for it here + component.pictureBuffer = 0.0; + + component.size = QSizeF( sizeOut, sizeOut ); + component.offset = QPointF( 0.0, 0.0 ); + + // rotate about origin center of SVG + p->save(); + p->translate( component.center.x(), component.center.y() ); + p->rotate( component.rotation ); + double xoff = context.convertToPainterUnits( background.offset().x(), background.offsetUnit(), background.offsetMapUnitScale() ); + double yoff = context.convertToPainterUnits( background.offset().y(), background.offsetUnit(), background.offsetMapUnitScale() ); + p->translate( QPointF( xoff, yoff ) ); + p->rotate( component.rotationOffset ); + p->translate( -sizeOut / 2, sizeOut / 2 ); + if ( context.flags() & QgsRenderContext::Antialiasing ) + { + p->setRenderHint( QPainter::Antialiasing ); + } + + drawShadow( context, component, format ); + p->restore(); + + delete svgShdwM; + svgShdwM = nullptr; + } + renderedSymbol.reset( ); + + QgsSymbolLayer *symL = QgsSvgMarkerSymbolLayer::create( map ); + renderedSymbol.reset( new QgsMarkerSymbol( QgsSymbolLayerList() << symL ) ); + } + else + { + renderedSymbol.reset( background.markerSymbol()->clone() ); + renderedSymbol->setSize( sizeOut ); + renderedSymbol->setSizeUnit( QgsUnitTypes::RenderPixels ); + } + + renderedSymbol->setOpacity( background.opacity() ); + + // draw the actual symbol p->save(); + if ( context.useAdvancedEffects() ) + { + p->setCompositionMode( background.blendMode() ); + } + if ( context.flags() & QgsRenderContext::Antialiasing ) + { + p->setRenderHint( QPainter::Antialiasing ); + } p->translate( component.center.x(), component.center.y() ); p->rotate( component.rotation ); double xoff = context.convertToPainterUnits( background.offset().x(), background.offsetUnit(), background.offsetMapUnitScale() ); double yoff = context.convertToPainterUnits( background.offset().y(), background.offsetUnit(), background.offsetMapUnitScale() ); p->translate( QPointF( xoff, yoff ) ); p->rotate( component.rotationOffset ); - p->translate( -sizeOut / 2, sizeOut / 2 ); + + const QgsFeature f = context.expressionContext().feature(); + renderedSymbol->startRender( context, context.expressionContext().fields() ); + renderedSymbol->renderPoint( QPointF( 0, 0 ), &f, context ); + renderedSymbol->stopRender( context ); + p->setCompositionMode( QPainter::CompositionMode_SourceOver ); // just to be sure + p->restore(); + + break; + } + + case QgsTextBackgroundSettings::ShapeRectangle: + case QgsTextBackgroundSettings::ShapeCircle: + case QgsTextBackgroundSettings::ShapeSquare: + case QgsTextBackgroundSettings::ShapeEllipse: + { + double w = component.size.width(); + double h = component.size.height(); + + if ( background.sizeType() == QgsTextBackgroundSettings::SizeFixed ) + { + w = context.convertToPainterUnits( background.size().width(), background.sizeUnit(), + background.sizeMapUnitScale() ); + h = context.convertToPainterUnits( background.size().height(), background.sizeUnit(), + background.sizeMapUnitScale() ); + } + else if ( background.sizeType() == QgsTextBackgroundSettings::SizeBuffer ) + { + if ( background.type() == QgsTextBackgroundSettings::ShapeSquare ) + { + if ( w > h ) + h = w; + else if ( h > w ) + w = h; + } + else if ( background.type() == QgsTextBackgroundSettings::ShapeCircle ) + { + // start with label bound by circle + h = std::sqrt( std::pow( w, 2 ) + std::pow( h, 2 ) ); + w = h; + } + else if ( background.type() == QgsTextBackgroundSettings::ShapeEllipse ) + { + // start with label bound by ellipse + h = h * M_SQRT1_2 * 2; + w = w * M_SQRT1_2 * 2; + } + + double bufferWidth = context.convertToPainterUnits( background.size().width(), background.sizeUnit(), + background.sizeMapUnitScale() ); + double bufferHeight = context.convertToPainterUnits( background.size().height(), background.sizeUnit(), + background.sizeMapUnitScale() ); + + w += bufferWidth * 2; + h += bufferHeight * 2; + } + + // offsets match those of symbology: -x = left, -y = up + QRectF rect( -w / 2.0, - h / 2.0, w, h ); + + if ( rect.isNull() ) + return; + + p->save(); if ( context.flags() & QgsRenderContext::Antialiasing ) { p->setRenderHint( QPainter::Antialiasing ); } + p->translate( QPointF( component.center.x(), component.center.y() ) ); + p->rotate( component.rotation ); + double xoff = context.convertToPainterUnits( background.offset().x(), background.offsetUnit(), background.offsetMapUnitScale() ); + double yoff = context.convertToPainterUnits( background.offset().y(), background.offsetUnit(), background.offsetMapUnitScale() ); + p->translate( QPointF( xoff, yoff ) ); + p->rotate( component.rotationOffset ); - drawShadow( context, component, format ); - p->restore(); + double penSize = context.convertToPainterUnits( background.strokeWidth(), background.strokeWidthUnit(), background.strokeWidthMapUnitScale() ); - delete svgShdwM; - svgShdwM = nullptr; - } - - // draw the actual symbol - QgsSymbolLayer *symL = QgsSvgMarkerSymbolLayer::create( map ); - QgsSvgMarkerSymbolLayer *svgM = static_cast( symL ); - QgsSymbolRenderContext svgContext( context, QgsUnitTypes::RenderUnknownUnit, background.opacity() ); - - p->save(); - if ( context.useAdvancedEffects() ) - { - p->setCompositionMode( background.blendMode() ); - } - if ( context.flags() & QgsRenderContext::Antialiasing ) - { - p->setRenderHint( QPainter::Antialiasing ); - } - p->translate( component.center.x(), component.center.y() ); - p->rotate( component.rotation ); - double xoff = context.convertToPainterUnits( background.offset().x(), background.offsetUnit(), background.offsetMapUnitScale() ); - double yoff = context.convertToPainterUnits( background.offset().y(), background.offsetUnit(), background.offsetMapUnitScale() ); - p->translate( QPointF( xoff, yoff ) ); - p->rotate( component.rotationOffset ); - svgM->renderPoint( QPointF( 0, 0 ), svgContext ); - p->setCompositionMode( QPainter::CompositionMode_SourceOver ); // just to be sure - p->restore(); - - delete svgM; - svgM = nullptr; - - } - else // Generated Shapes - { - double w = component.size.width(); - double h = component.size.height(); - - if ( background.sizeType() == QgsTextBackgroundSettings::SizeFixed ) - { - w = context.convertToPainterUnits( background.size().width(), background.sizeUnit(), - background.sizeMapUnitScale() ); - h = context.convertToPainterUnits( background.size().height(), background.sizeUnit(), - background.sizeMapUnitScale() ); - } - else if ( background.sizeType() == QgsTextBackgroundSettings::SizeBuffer ) - { - if ( background.type() == QgsTextBackgroundSettings::ShapeSquare ) + QPen pen; + if ( background.strokeWidth() > 0 ) { - if ( w > h ) - h = w; - else if ( h > w ) - w = h; - } - else if ( background.type() == QgsTextBackgroundSettings::ShapeCircle ) - { - // start with label bound by circle - h = std::sqrt( std::pow( w, 2 ) + std::pow( h, 2 ) ); - w = h; - } - else if ( background.type() == QgsTextBackgroundSettings::ShapeEllipse ) - { - // start with label bound by ellipse - h = h * M_SQRT1_2 * 2; - w = w * M_SQRT1_2 * 2; - } - - double bufferWidth = context.convertToPainterUnits( background.size().width(), background.sizeUnit(), - background.sizeMapUnitScale() ); - double bufferHeight = context.convertToPainterUnits( background.size().height(), background.sizeUnit(), - background.sizeMapUnitScale() ); - - w += bufferWidth * 2; - h += bufferHeight * 2; - } - - // offsets match those of symbology: -x = left, -y = up - QRectF rect( -w / 2.0, - h / 2.0, w, h ); - - if ( rect.isNull() ) - return; - - p->save(); - if ( context.flags() & QgsRenderContext::Antialiasing ) - { - p->setRenderHint( QPainter::Antialiasing ); - } - p->translate( QPointF( component.center.x(), component.center.y() ) ); - p->rotate( component.rotation ); - double xoff = context.convertToPainterUnits( background.offset().x(), background.offsetUnit(), background.offsetMapUnitScale() ); - double yoff = context.convertToPainterUnits( background.offset().y(), background.offsetUnit(), background.offsetMapUnitScale() ); - p->translate( QPointF( xoff, yoff ) ); - p->rotate( component.rotationOffset ); - - double penSize = context.convertToPainterUnits( background.strokeWidth(), background.strokeWidthUnit(), background.strokeWidthMapUnitScale() ); - - QPen pen; - if ( background.strokeWidth() > 0 ) - { - pen.setColor( background.strokeColor() ); - pen.setWidthF( penSize ); - if ( background.type() == QgsTextBackgroundSettings::ShapeRectangle ) - pen.setJoinStyle( background.joinStyle() ); - } - else - { - pen = Qt::NoPen; - } - - // store painting in QPicture for shadow drawing - QPicture shapePict; - QPainter shapep; - shapep.begin( &shapePict ); - shapep.setPen( pen ); - shapep.setBrush( background.fillColor() ); - - if ( background.type() == QgsTextBackgroundSettings::ShapeRectangle - || background.type() == QgsTextBackgroundSettings::ShapeSquare ) - { - if ( background.radiiUnit() == QgsUnitTypes::RenderPercentage ) - { - shapep.drawRoundedRect( rect, background.radii().width(), background.radii().height(), Qt::RelativeSize ); + pen.setColor( background.strokeColor() ); + pen.setWidthF( penSize ); + if ( background.type() == QgsTextBackgroundSettings::ShapeRectangle ) + pen.setJoinStyle( background.joinStyle() ); } else { - double xRadius = context.convertToPainterUnits( background.radii().width(), background.radiiUnit(), background.radiiMapUnitScale() ); - double yRadius = context.convertToPainterUnits( background.radii().height(), background.radiiUnit(), background.radiiMapUnitScale() ); - shapep.drawRoundedRect( rect, xRadius, yRadius ); + pen = Qt::NoPen; } - } - else if ( background.type() == QgsTextBackgroundSettings::ShapeEllipse - || background.type() == QgsTextBackgroundSettings::ShapeCircle ) - { - shapep.drawEllipse( rect ); - } - shapep.end(); - if ( format.shadow().enabled() && format.shadow().shadowPlacement() == QgsTextShadowSettings::ShadowShape ) - { - component.picture = shapePict; - component.pictureBuffer = penSize / 2.0; + // store painting in QPicture for shadow drawing + QPicture shapePict; + QPainter shapep; + shapep.begin( &shapePict ); + shapep.setPen( pen ); + shapep.setBrush( background.fillColor() ); - component.size = rect.size(); - component.offset = QPointF( rect.width() / 2, -rect.height() / 2 ); - drawShadow( context, component, format ); + if ( background.type() == QgsTextBackgroundSettings::ShapeRectangle + || background.type() == QgsTextBackgroundSettings::ShapeSquare ) + { + if ( background.radiiUnit() == QgsUnitTypes::RenderPercentage ) + { + shapep.drawRoundedRect( rect, background.radii().width(), background.radii().height(), Qt::RelativeSize ); + } + else + { + double xRadius = context.convertToPainterUnits( background.radii().width(), background.radiiUnit(), background.radiiMapUnitScale() ); + double yRadius = context.convertToPainterUnits( background.radii().height(), background.radiiUnit(), background.radiiMapUnitScale() ); + shapep.drawRoundedRect( rect, xRadius, yRadius ); + } + } + else if ( background.type() == QgsTextBackgroundSettings::ShapeEllipse + || background.type() == QgsTextBackgroundSettings::ShapeCircle ) + { + shapep.drawEllipse( rect ); + } + shapep.end(); + + if ( format.shadow().enabled() && format.shadow().shadowPlacement() == QgsTextShadowSettings::ShadowShape ) + { + component.picture = shapePict; + component.pictureBuffer = penSize / 2.0; + + component.size = rect.size(); + component.offset = QPointF( rect.width() / 2, -rect.height() / 2 ); + drawShadow( context, component, format ); + } + + p->setOpacity( background.opacity() ); + if ( context.useAdvancedEffects() ) + { + p->setCompositionMode( background.blendMode() ); + } + + // scale for any print output or image saving @ specific dpi + p->scale( component.dpiRatio, component.dpiRatio ); + _fixQPictureDPI( p ); + p->drawPicture( 0, 0, shapePict ); + p->restore(); + break; } - - p->setOpacity( background.opacity() ); - if ( context.useAdvancedEffects() ) - { - p->setCompositionMode( background.blendMode() ); - } - - // scale for any print output or image saving @ specific dpi - p->scale( component.dpiRatio, component.dpiRatio ); - _fixQPictureDPI( p ); - p->drawPicture( 0, 0, shapePict ); - p->restore(); } + if ( background.paintEffect() && background.paintEffect()->enabled() ) { background.paintEffect()->end( context ); diff --git a/src/core/qgstextrenderer.h b/src/core/qgstextrenderer.h index 902ff0d70d5..8e1b5892183 100644 --- a/src/core/qgstextrenderer.h +++ b/src/core/qgstextrenderer.h @@ -34,6 +34,7 @@ class QgsTextShadowSettingsPrivate; class QgsTextSettingsPrivate; class QgsVectorLayer; class QgsPaintEffect; +class QgsMarkerSymbol; /** * \class QgsTextBufferSettings @@ -252,7 +253,8 @@ class CORE_EXPORT QgsTextBackgroundSettings ShapeSquare, //!< Square - buffered sizes only ShapeEllipse, //!< Ellipse ShapeCircle, //!< Circle - ShapeSVG //!< SVG file + ShapeSVG, //!< SVG file + ShapeMarkerSymbol, //!< Marker symbol }; /** @@ -327,6 +329,24 @@ class CORE_EXPORT QgsTextBackgroundSettings */ void setSvgFile( const QString &file ); + /** + * Returns the marker symbol to be rendered in the background. Ownership remains with + * the background settings. + * \note This is only used when the type() is QgsTextBackgroundSettings::ShapeMarkerSymbol. + * \see setMarkerSymbol() + * \since QGIS 3.10 + */ + QgsMarkerSymbol *markerSymbol() const; + + /** + * Sets the current marker \a symbol for the background shape. Ownership is transferred + * to the background settings. + * \note This is only used when the type() is QgsTextBackgroundSettings::ShapeMarkerSymbol. + * \see markerSymbol() + * \since QGIS 3.10 + */ + void setMarkerSymbol( QgsMarkerSymbol *symbol SIP_TRANSFER ); + /** * Returns the method used to determine the size of the background shape (e.g., fixed size or buffer * around text). diff --git a/src/core/qgstextrenderer_p.h b/src/core/qgstextrenderer_p.h index 0315e007d0b..034589981a5 100644 --- a/src/core/qgstextrenderer_p.h +++ b/src/core/qgstextrenderer_p.h @@ -64,11 +64,6 @@ class QgsTextBufferSettingsPrivate : public QSharedData { } - ~QgsTextBufferSettingsPrivate() - { - delete paintEffect; - } - bool enabled = false; double size = 1; QgsUnitTypes::RenderUnit sizeUnit = QgsUnitTypes::RenderMillimeters; @@ -78,7 +73,7 @@ class QgsTextBufferSettingsPrivate : public QSharedData bool fillBufferInterior = false; Qt::PenJoinStyle joinStyle = Qt::RoundJoin; QPainter::CompositionMode blendMode = QPainter::CompositionMode_SourceOver; - QgsPaintEffect *paintEffect = nullptr; + std::unique_ptr< QgsPaintEffect > paintEffect; }; @@ -121,14 +116,10 @@ class QgsTextBackgroundSettingsPrivate : public QSharedData , strokeWidthMapUnitScale( other.strokeWidthMapUnitScale ) , joinStyle( other.joinStyle ) , paintEffect( other.paintEffect ? other.paintEffect->clone() : nullptr ) + , markerSymbol( other.markerSymbol ? other.markerSymbol->clone() : nullptr ) { } - ~QgsTextBackgroundSettingsPrivate() - { - delete paintEffect; - } - bool enabled = false; QgsTextBackgroundSettings::ShapeType type = QgsTextBackgroundSettings::ShapeRectangle; QString svgFile; //!< Absolute path to SVG file @@ -152,7 +143,8 @@ class QgsTextBackgroundSettingsPrivate : public QSharedData QgsUnitTypes::RenderUnit strokeWidthUnits = QgsUnitTypes::RenderMillimeters; QgsMapUnitScale strokeWidthMapUnitScale; Qt::PenJoinStyle joinStyle = Qt::BevelJoin; - QgsPaintEffect *paintEffect = nullptr; + std::unique_ptr< QgsPaintEffect > paintEffect; + std::unique_ptr< QgsMarkerSymbol > markerSymbol; }; diff --git a/src/core/qgsvectorlayerlabeling.cpp b/src/core/qgsvectorlayerlabeling.cpp index ea3b62a8cb8..9920fcad9eb 100644 --- a/src/core/qgsvectorlayerlabeling.cpp +++ b/src/core/qgsvectorlayerlabeling.cpp @@ -165,6 +165,16 @@ std::unique_ptr backgroundToMarkerLayer( const QgsTextBack layer.reset( svg ); break; } + case QgsTextBackgroundSettings::ShapeMarkerSymbol: + { + // just grab the first layer and hope for the best + if ( settings.markerSymbol() && settings.markerSymbol()->symbolLayerCount() > 0 ) + { + layer.reset( static_cast< QgsMarkerSymbolLayer * >( settings.markerSymbol()->symbolLayer( 0 )->clone() ) ); + break; + } + FALLTHROUGH // not set, just go with the default + } case QgsTextBackgroundSettings::ShapeCircle: case QgsTextBackgroundSettings::ShapeEllipse: case QgsTextBackgroundSettings::ShapeRectangle: @@ -184,6 +194,7 @@ std::unique_ptr backgroundToMarkerLayer( const QgsTextBack shape = QgsSimpleMarkerSymbolLayerBase::Square; break; case QgsTextBackgroundSettings::ShapeSVG: + case QgsTextBackgroundSettings::ShapeMarkerSymbol: break; } diff --git a/src/core/qgsvectorlayerlabelprovider.cpp b/src/core/qgsvectorlayerlabelprovider.cpp index 1ea6bf031eb..b57cfb4032a 100644 --- a/src/core/qgsvectorlayerlabelprovider.cpp +++ b/src/core/qgsvectorlayerlabelprovider.cpp @@ -339,7 +339,7 @@ void QgsVectorLayerLabelProvider::drawLabel( QgsRenderContext &context, pal::Lab { QgsTextFormat format = tmpLyr.format(); - if ( tmpLyr.format().background().enabled() ) + if ( tmpLyr.format().background().enabled() && tmpLyr.format().background().type() != QgsTextBackgroundSettings::ShapeMarkerSymbol ) // background shadows not compatible with marker symbol backgrounds { format.shadow().setShadowPlacement( QgsTextShadowSettings::ShadowShape ); } diff --git a/src/gui/qgslabelinggui.cpp b/src/gui/qgslabelinggui.cpp index 0c20dc8eeb3..ecf35a15638 100644 --- a/src/gui/qgslabelinggui.cpp +++ b/src/gui/qgslabelinggui.cpp @@ -126,6 +126,7 @@ void QgsLabelingGui::setLayer( QgsMapLayer *mapLayer ) mLayer = layer; mTextFormatsListWidget->setLayerType( mLayer ? mLayer->geometryType() : mGeomType ); + mBackgroundSymbolButton->setLayer( mLayer ); // load labeling settings from layer updateGeometryTypeBasedWidgets(); diff --git a/src/gui/qgslabelinggui.h b/src/gui/qgslabelinggui.h index d4a9dad1499..8e93401029c 100644 --- a/src/gui/qgslabelinggui.h +++ b/src/gui/qgslabelinggui.h @@ -27,7 +27,7 @@ ///@cond PRIVATE -class GUI_EXPORT QgsLabelingGui : public QgsTextFormatWidget, private QgsExpressionContextGenerator +class GUI_EXPORT QgsLabelingGui : public QgsTextFormatWidget { Q_OBJECT diff --git a/src/gui/qgssymbolbutton.cpp b/src/gui/qgssymbolbutton.cpp index 89c5cd25513..3b78581f9dd 100644 --- a/src/gui/qgssymbolbutton.cpp +++ b/src/gui/qgssymbolbutton.cpp @@ -110,6 +110,7 @@ void QgsSymbolButton::showSettingsDialog() if ( panel && panel->dockMode() ) { QgsSymbolSelectorWidget *d = new QgsSymbolSelectorWidget( newSymbol, QgsStyle::defaultStyle(), mLayer, nullptr ); + d->setPanelTitle( mDialogTitle ); d->setContext( symbolContext ); connect( d, &QgsPanelWidget::widgetChanged, this, &QgsSymbolButton::updateSymbolFromWidget ); connect( d, &QgsPanelWidget::panelAccepted, this, &QgsSymbolButton::cleanUpSymbolSelector ); diff --git a/src/gui/qgstextformatwidget.cpp b/src/gui/qgstextformatwidget.cpp index 26e882791ab..8d3ce36fed6 100644 --- a/src/gui/qgstextformatwidget.cpp +++ b/src/gui/qgstextformatwidget.cpp @@ -29,6 +29,7 @@ #include "qgseffectstack.h" #include "qgspainteffectregistry.h" #include "qgsstylesavedialog.h" +#include "qgsexpressioncontextutils.h" #include #include @@ -160,6 +161,20 @@ void QgsTextFormatWidget::initWidget() mOffsetTypeComboBox->addItem( tr( "From point" ), QgsPalLayerSettings::FromPoint ); mOffsetTypeComboBox->addItem( tr( "From symbol bounds" ), QgsPalLayerSettings::FromSymbolBounds ); + mShapeTypeCmbBx->addItem( tr( "Rectangle" ), QgsTextBackgroundSettings::ShapeRectangle ); + mShapeTypeCmbBx->addItem( tr( "Square" ), QgsTextBackgroundSettings::ShapeSquare ); + mShapeTypeCmbBx->addItem( tr( "Ellipse" ), QgsTextBackgroundSettings::ShapeEllipse ); + mShapeTypeCmbBx->addItem( tr( "Circle" ), QgsTextBackgroundSettings::ShapeCircle ); + mShapeTypeCmbBx->addItem( tr( "SVG" ), QgsTextBackgroundSettings::ShapeSVG ); + mShapeTypeCmbBx->addItem( tr( "Marker Symbol" ), QgsTextBackgroundSettings::ShapeMarkerSymbol ); + + updateAvailableShadowPositions(); + + mBackgroundSymbolButton->setSymbolType( QgsSymbol::Marker ); + mBackgroundSymbolButton->setDialogTitle( tr( "Background Symbol" ) ); + mBackgroundSymbolButton->registerExpressionContextGenerator( this ); + mBackgroundSymbolButton->setMapCanvas( mMapCanvas ); + mCharDlg = new QgsCharacterSelectorDialog( this ); mRefFont = lblFontPreview->font(); @@ -522,7 +537,8 @@ void QgsTextFormatWidget::initWidget() << mGeometryGeneratorGroupBox << mGeometryGenerator << mGeometryGeneratorType - << mLinePlacementFlagsDDBtn; + << mLinePlacementFlagsDDBtn + << mBackgroundSymbolButton; connectValueChanged( widgets, SLOT( updatePreview() ) ); connect( mQuadrantBtnGrp, static_cast( &QButtonGroup::buttonClicked ), this, &QgsTextFormatWidget::updatePreview ); @@ -610,6 +626,10 @@ void QgsTextFormatWidget::connectValueChanged( const QList &widgets, { connect( w, SIGNAL( changed() ), this, slot ); } + else if ( QgsSymbolButton *w = qobject_cast( widget ) ) + { + connect( w, SIGNAL( changed() ), this, slot ); + } else if ( QgsFieldExpressionWidget *w = qobject_cast< QgsFieldExpressionWidget *>( widget ) ) { connect( w, SIGNAL( fieldChanged( QString ) ), this, slot ); @@ -732,8 +752,9 @@ void QgsTextFormatWidget::updateWidgetForFormat( const QgsTextFormat &format ) // shape background mShapeDrawChkBx->setChecked( background.enabled() ); mShapeTypeCmbBx->blockSignals( true ); - mShapeTypeCmbBx->setCurrentIndex( background.type() ); + mShapeTypeCmbBx->setCurrentIndex( mShapeTypeCmbBx->findData( background.type() ) ); mShapeTypeCmbBx->blockSignals( false ); + updateAvailableShadowPositions(); mShapeSVGPathLineEdit->setText( background.svgFile() ); mShapeSizeCmbBx->setCurrentIndex( background.sizeType() ); @@ -776,9 +797,11 @@ void QgsTextFormatWidget::updateWidgetForFormat( const QgsTextFormat &format ) } mBackgroundEffectWidget->setPaintEffect( mBackgroundEffect.get() ); + mBackgroundSymbolButton->setSymbol( background.markerSymbol() ? background.markerSymbol()->clone() : QgsSymbol::defaultSymbol( QgsWkbTypes::PointGeometry ) ); + // drop shadow mShadowDrawChkBx->setChecked( shadow.enabled() ); - mShadowUnderCmbBx->setCurrentIndex( shadow.shadowPlacement() ); + mShadowUnderCmbBx->setCurrentIndex( mShadowUnderCmbBx->findData( shadow.shadowPlacement() ) ); mShadowOffsetAngleSpnBx->setValue( shadow.offsetAngle() ); mShadowOffsetSpnBx->setValue( shadow.offsetDistance() ); mShadowOffsetUnitWidget->setUnit( shadow.offsetUnit() ); @@ -842,7 +865,7 @@ QgsTextFormat QgsTextFormatWidget::format() const // shape background QgsTextBackgroundSettings background; background.setEnabled( mShapeDrawChkBx->isChecked() ); - background.setType( ( QgsTextBackgroundSettings::ShapeType )mShapeTypeCmbBx->currentIndex() ); + background.setType( static_cast< QgsTextBackgroundSettings::ShapeType >( mShapeTypeCmbBx->currentData().toInt() ) ); background.setSvgFile( mShapeSVGPathLineEdit->text() ); background.setSizeType( ( QgsTextBackgroundSettings::SizeType )mShapeSizeCmbBx->currentIndex() ); background.setSize( QSizeF( mShapeSizeXSpnBx->value(), mShapeSizeYSpnBx->value() ) ); @@ -869,12 +892,13 @@ QgsTextFormat QgsTextFormatWidget::format() const background.setPaintEffect( mBackgroundEffect->clone() ); else background.setPaintEffect( nullptr ); + background.setMarkerSymbol( mBackgroundSymbolButton->clonedSymbol< QgsMarkerSymbol >() ); format.setBackground( background ); // drop shadow QgsTextShadowSettings shadow; shadow.setEnabled( mShadowDrawChkBx->isChecked() ); - shadow.setShadowPlacement( ( QgsTextShadowSettings::ShadowPlacement )mShadowUnderCmbBx->currentIndex() ); + shadow.setShadowPlacement( static_cast< QgsTextShadowSettings::ShadowPlacement >( mShadowUnderCmbBx->currentData().toInt() ) ); shadow.setOffsetAngle( mShadowOffsetAngleSpnBx->value() ); shadow.setOffsetDistance( mShadowOffsetSpnBx->value() ); shadow.setOffsetUnit( mShadowOffsetUnitWidget->unit() ); @@ -1232,22 +1256,25 @@ void QgsTextFormatWidget::mCoordYDDBtn_activated( bool active ) } } -void QgsTextFormatWidget::mShapeTypeCmbBx_currentIndexChanged( int index ) +void QgsTextFormatWidget::mShapeTypeCmbBx_currentIndexChanged( int ) { // shape background - bool isRect = ( ( QgsTextBackgroundSettings::ShapeType )index == QgsTextBackgroundSettings::ShapeRectangle - || ( QgsTextBackgroundSettings::ShapeType )index == QgsTextBackgroundSettings::ShapeSquare ); - bool isSVG = ( ( QgsTextBackgroundSettings::ShapeType )index == QgsTextBackgroundSettings::ShapeSVG ); + QgsTextBackgroundSettings::ShapeType type = static_cast< QgsTextBackgroundSettings::ShapeType >( mShapeTypeCmbBx->currentData().toInt() ); + const bool isRect = type == QgsTextBackgroundSettings::ShapeRectangle || type == QgsTextBackgroundSettings::ShapeSquare; + const bool isSVG = type == QgsTextBackgroundSettings::ShapeSVG; + const bool isMarker = type == QgsTextBackgroundSettings::ShapeMarkerSymbol; showBackgroundPenStyle( isRect ); showBackgroundRadius( isRect ); mShapeSVGPathFrame->setVisible( isSVG ); + mBackgroundSymbolButton->setVisible( isMarker ); + // symbology SVG renderer only supports size^2 scaling, so we only use the x size spinbox - mShapeSizeYLabel->setVisible( !isSVG ); - mShapeSizeYSpnBx->setVisible( !isSVG ); - mShapeSizeYDDBtn->setVisible( !isSVG && mWidgetMode == Labeling ); - mShapeSizeXLabel->setText( tr( "Size%1" ).arg( !isSVG ? tr( " X" ) : QString() ) ); + mShapeSizeYLabel->setVisible( !isSVG && !isMarker ); + mShapeSizeYSpnBx->setVisible( !isSVG && !isMarker ); + mShapeSizeYDDBtn->setVisible( !isSVG && !isMarker && mWidgetMode == Labeling ); + mShapeSizeXLabel->setText( tr( "Size%1" ).arg( !isSVG && !isMarker ? tr( " X" ) : QString() ) ); // SVG parameter setting doesn't support color's alpha component yet mShapeFillColorBtn->setAllowOpacity( !isSVG ); @@ -1263,21 +1290,23 @@ void QgsTextFormatWidget::mShapeTypeCmbBx_currentIndexChanged( int index ) } else { - mShapeFillColorLabel->setEnabled( true ); - mShapeFillColorBtn->setEnabled( true ); - mShapeFillColorDDBtn->setEnabled( true ); - mShapeStrokeColorLabel->setEnabled( true ); - mShapeStrokeColorBtn->setEnabled( true ); - mShapeStrokeColorDDBtn->setEnabled( true ); - mShapeStrokeWidthLabel->setEnabled( true ); - mShapeStrokeWidthSpnBx->setEnabled( true ); - mShapeStrokeWidthDDBtn->setEnabled( true ); + mShapeFillColorLabel->setEnabled( !isMarker ); + mShapeFillColorBtn->setEnabled( !isMarker ); + mShapeFillColorDDBtn->setEnabled( !isMarker ); + mShapeStrokeColorLabel->setEnabled( !isMarker ); + mShapeStrokeColorBtn->setEnabled( !isMarker ); + mShapeStrokeColorDDBtn->setEnabled( !isMarker ); + mShapeStrokeWidthLabel->setEnabled( !isMarker ); + mShapeStrokeWidthSpnBx->setEnabled( !isMarker ); + mShapeStrokeWidthDDBtn->setEnabled( !isMarker ); } // TODO: fix overriding SVG symbol's stroke width units in QgsSvgCache // currently broken, fall back to symbol units only - mShapeStrokeWidthUnitWidget->setVisible( !isSVG ); + mShapeStrokeWidthUnitWidget->setVisible( !isSVG && !isMarker ); mShapeSVGUnitsLabel->setVisible( isSVG ); - mShapeStrokeUnitsDDBtn->setEnabled( !isSVG ); + mShapeStrokeUnitsDDBtn->setEnabled( !isSVG && !isMarker ); + + updateAvailableShadowPositions(); } void QgsTextFormatWidget::mShapeSVGPathLineEdit_textChanged( const QString &text ) @@ -1376,6 +1405,28 @@ void QgsTextFormatWidget::updateSvgWidgets( const QString &svgPath ) mShapeSVGUnitsLabel->setEnabled( validSVG && strokeWidthParam ); } +void QgsTextFormatWidget::updateAvailableShadowPositions() +{ + if ( mShadowUnderCmbBx->count() == 0 + || ( mShadowUnderCmbBx->findData( QgsTextShadowSettings::ShadowShape ) > -1 && mShapeTypeCmbBx->currentData().toInt() == QgsTextBackgroundSettings::ShapeMarkerSymbol ) + || ( mShadowUnderCmbBx->findData( QgsTextShadowSettings::ShadowShape ) == -1 && mShapeTypeCmbBx->currentData().toInt() != QgsTextBackgroundSettings::ShapeMarkerSymbol ) ) + { + // showing invalid choices, have to rebuild the list + QgsTextShadowSettings::ShadowPlacement currentPlacement = static_cast< QgsTextShadowSettings::ShadowPlacement >( mShadowUnderCmbBx->currentData().toInt() ); + mShadowUnderCmbBx->clear(); + + mShadowUnderCmbBx->addItem( tr( "Lowest label component" ), QgsTextShadowSettings::ShadowLowest ); + mShadowUnderCmbBx->addItem( tr( "Text" ), QgsTextShadowSettings::ShadowText ); + mShadowUnderCmbBx->addItem( tr( "Buffer" ), QgsTextShadowSettings::ShadowBuffer ); + if ( mShapeTypeCmbBx->currentData().toInt() != QgsTextBackgroundSettings::ShapeMarkerSymbol ) + mShadowUnderCmbBx->addItem( tr( "Background" ), QgsTextShadowSettings::ShadowShape ); // not supported for marker symbol background shapes + + mShadowUnderCmbBx->setCurrentIndex( mShadowUnderCmbBx->findData( currentPlacement ) ); + if ( mShadowUnderCmbBx->currentIndex() == -1 ) + mShadowUnderCmbBx->setCurrentIndex( 0 ); + } +} + void QgsTextFormatWidget::setFormatFromStyle( const QString &name, QgsStyle::StyleEntity type ) { switch ( type ) @@ -1587,6 +1638,18 @@ void QgsTextFormatWidget::enableDataDefinedAlignment( bool enable ) mCoordAlignmentFrame->setEnabled( enable ); } +QgsExpressionContext QgsTextFormatWidget::createExpressionContext() const +{ + QgsExpressionContext expContext; + expContext << QgsExpressionContextUtils::globalScope() + << QgsExpressionContextUtils::projectScope( QgsProject::instance() ) + << QgsExpressionContextUtils::atlasScope( nullptr ); + if ( mMapCanvas ) + expContext << QgsExpressionContextUtils::mapSettingsScope( mMapCanvas->mapSettings() ); + + return expContext; +} + // // QgsTextFormatDialog diff --git a/src/gui/qgstextformatwidget.h b/src/gui/qgstextformatwidget.h index af9e69a174e..cd6167b43d8 100644 --- a/src/gui/qgstextformatwidget.h +++ b/src/gui/qgstextformatwidget.h @@ -46,7 +46,7 @@ class QgsCharacterSelectorDialog; * \since QGIS 3.0 */ -class GUI_EXPORT QgsTextFormatWidget : public QWidget, protected Ui::QgsTextFormatWidgetBase +class GUI_EXPORT QgsTextFormatWidget : public QWidget, public QgsExpressionContextGenerator, protected Ui::QgsTextFormatWidgetBase { Q_OBJECT Q_PROPERTY( QgsTextFormat format READ format ) @@ -122,6 +122,8 @@ class GUI_EXPORT QgsTextFormatWidget : public QWidget, protected Ui::QgsTextForm */ void enableDataDefinedAlignment( bool enable ); + QgsExpressionContext createExpressionContext() const override; + //! Text substitution list QgsStringReplacementCollection mSubstitutions; //! Quadrant button group @@ -222,6 +224,7 @@ class GUI_EXPORT QgsTextFormatWidget : public QWidget, protected Ui::QgsTextForm void updatePreview(); void scrollPreview(); void updateSvgWidgets( const QString &svgPath ); + void updateAvailableShadowPositions(); }; diff --git a/src/ui/qgstextformatwidgetbase.ui b/src/ui/qgstextformatwidgetbase.ui index 3224369a330..5a911d3410a 100644 --- a/src/ui/qgstextformatwidgetbase.ui +++ b/src/ui/qgstextformatwidgetbase.ui @@ -112,7 +112,7 @@ 0 0 - 811 + 807 300 @@ -591,7 +591,7 @@ - 0 + 4 @@ -620,8 +620,8 @@ 0 0 - 771 - 889 + 770 + 866 @@ -1203,8 +1203,8 @@ font-style: italic; 0 0 - 771 - 872 + 770 + 847 @@ -2078,8 +2078,8 @@ font-style: italic; 0 0 - 224 - 233 + 770 + 866 @@ -2424,8 +2424,8 @@ font-style: italic; 0 0 - 368 - 572 + 770 + 866 @@ -2494,6 +2494,162 @@ font-style: italic; 0 + + + + + + + + + + + Stroke width + + + + + + + Radius X,Y + + + + + + + Load symbol parameters + + + + + + + + 200 + 0 + + + + + + + + + + + + + + + + 0 + 0 + + + + + 85 + 0 + + + + Fill color + + + + + + + + + + + + + + + 0 + 0 + + + + + + + + + Buffer + + + + + Fixed + + + + + + + + Rotation + + + + + + + + + + + + + + + + Qt::StrongFocus + + + + + + + symbol units + + + + + + + + + + 150 + 0 + + + + + + + + false + + + true + + + ˚ + + + -360.000000000000000 + + + 360.000000000000000 + + + @@ -2529,14 +2685,121 @@ font-style: italic; - - + + - Radius X,Y + - + + + + + + + 0 + 0 + + + + 4 + + + 999999999.990000009536743 + + + 0.100000000000000 + + + + + + + + 0 + 0 + + + + 4 + + + 999999999.990000009536743 + + + 0.100000000000000 + + + + + + + + + + 0 + 0 + + + + Pen join style + + + + + + + + + + + + + + Qt::StrongFocus + + + + + + + Size type + + + + + + + + + + + + + + + + + + + + + + + + Shape + + + + + + + + + + + @@ -2554,7 +2817,14 @@ font-style: italic; - + + + + + + + + @@ -2608,106 +2878,21 @@ font-style: italic; - - - - - - Qt::StrongFocus - - - - - - - symbol units - - - - - - - + + - + Offset X,Y - - + + - + Size X - - - - - - - - - - - - 0 - 0 - - - - Blend mode - - - - - - - - - - - - - - Stroke width - - - - - - - - - - - - - - false - - - true - - - ˚ - - - -360.000000000000000 - - - 360.000000000000000 - - - - - - - - - - - + @@ -2729,97 +2914,24 @@ font-style: italic; - - + + + + + 0 + 0 + + + - - + + - - - - - Buffer - - - - - Fixed - - - - - - - - - - - - - - - Size Y - - - - - - - true - - - Opacity - - - - - - - Load symbol parameters - - - - - - - Shape - - - - - - - Size X - - - - - - - - - - - - - - Offset X,Y - - - - - - - - - - - + @@ -2838,26 +2950,6 @@ font-style: italic; - - - - Size type - - - - - - - - 0 - 0 - - - - Pen join style - - - @@ -2871,143 +2963,35 @@ font-style: italic; - - - - - - - - - - - - 150 - 0 - - - - - - - - - - - 4 - - - -9999999.000000000000000 - - - 9999999.000000000000000 - - - 0.100000000000000 - - - false - - - - - - - - 0 - 0 - - - - - 85 - 0 - - - - Fill color - - - - - - - - - - - - - - Rotation - - - - - - - - - - - - - - - - - 0 - 0 - - - - 4 - - - 999999999.990000009536743 - - - 0.100000000000000 - - - - - - - - 0 - 0 - - - - 4 - - - 999999999.990000009536743 - - - 0.100000000000000 - - - - - - - - - - - - - + - - + + + + + + + + + + + + 0 + 0 + + + + Blend mode + + + + + @@ -3020,6 +3004,47 @@ font-style: italic; + + + + Qt::StrongFocus + + + + + + + true + + + Opacity + + + + + + + + 20 + 20 + + + + + + + + Size Y + + + + + + + Qt::StrongFocus + + + @@ -3039,47 +3064,39 @@ font-style: italic; - - + + - - - - - 20 - 20 - - - - - - - - - 0 - 0 - - - - - - - - - 0 - 0 - - - - - - - Qt::StrongFocus + + + + + + 4 + + + -9999999.000000000000000 + + + 9999999.000000000000000 + + + 0.100000000000000 + + + false + + + + + + + @@ -3090,53 +3107,24 @@ font-style: italic; - - - - Qt::StrongFocus + + + + - - - - Qt::StrongFocus + + + + + 0 + 0 + - - - - - - - 200 - 0 - + + Background Symbol… - - - Rectangle - - - - - Square - - - - - Ellipse - - - - - Circle - - - - - SVG - - @@ -3197,8 +3185,8 @@ font-style: italic; 0 0 - 259 - 350 + 770 + 866 @@ -3414,26 +3402,6 @@ font-style: italic; 0 - - - Lowest label component - - - - - Text - - - - - Buffer - - - - - Background - - @@ -3658,8 +3626,8 @@ font-style: italic; 0 0 - 288 - 852 + 335 + 884 @@ -5366,8 +5334,8 @@ font-style: italic; 0 0 - 287 - 656 + 302 + 674 @@ -6393,6 +6361,11 @@ font-style: italic;
qgsstyleitemslistwidget.h
1 + + QgsSymbolButton + QToolButton +
qgssymbolbutton.h
+
mFieldExpressionWidget @@ -6477,6 +6450,7 @@ font-style: italic; mShapeSVGPathLineEdit mShapeSVGSelectorBtn mShapeSVGPathDDBtn + mBackgroundSymbolButton mShapeSizeCmbBx mShapeSizeTypeDDBtn mShapeSizeXSpnBx @@ -6635,9 +6609,20 @@ font-style: italic; mGeometryGeneratorGroupBox mGeometryGeneratorExpressionButton mGeometryGeneratorType + mFontWordSpacingSpinBox + mFontCaseDDBtn + mFontLetterSpacingSpinBox + comboBlendMode + mToolButtonConfigureSubstitutes + mFontBlendModeDDBtn + mFontCapitalsComboBox + mFontLetterSpacingDDBtn + mCheckBoxSubstituteText + mFontWordSpacingDDBtn + diff --git a/tests/src/python/test_qgstextformatwidget.py b/tests/src/python/test_qgstextformatwidget.py index 0f0308bc4da..73a22c0766b 100644 --- a/tests/src/python/test_qgstextformatwidget.py +++ b/tests/src/python/test_qgstextformatwidget.py @@ -18,7 +18,8 @@ from qgis.core import (QgsTextBufferSettings, QgsTextFormat, QgsUnitTypes, QgsMapUnitScale, - QgsBlurEffect) + QgsBlurEffect, + QgsMarkerSymbol) from qgis.gui import (QgsTextFormatWidget, QgsTextFormatDialog) from qgis.PyQt.QtGui import (QColor, QPainter) from qgis.PyQt.QtCore import (Qt, QSizeF, QPointF) @@ -84,6 +85,11 @@ class PyQgsTextFormatWidget(unittest.TestCase): s.setStrokeWidthUnit(QgsUnitTypes.RenderMapUnits) s.setStrokeWidthMapUnitScale(QgsMapUnitScale(QgsMapUnitScale(25, 26))) s.setPaintEffect(QgsBlurEffect.create({'blur_level': '6.0', 'blur_unit': QgsUnitTypes.encodeUnit(QgsUnitTypes.RenderMillimeters), 'enabled': '1'})) + + marker = QgsMarkerSymbol() + marker.setColor(QColor(100, 112, 134)) + s.setMarkerSymbol(marker) + return s def checkBackgroundSettings(self, s): diff --git a/tests/src/python/test_qgstextrenderer.py b/tests/src/python/test_qgstextrenderer.py index f3eeca82009..8f899e95b8a 100644 --- a/tests/src/python/test_qgstextrenderer.py +++ b/tests/src/python/test_qgstextrenderer.py @@ -26,7 +26,8 @@ from qgis.core import (QgsTextBufferSettings, QgsRenderContext, QgsRectangle, QgsRenderChecker, - QgsBlurEffect) + QgsBlurEffect, + QgsMarkerSymbol) from qgis.PyQt.QtGui import (QColor, QPainter, QFont, QImage, QBrush, QPen, QFontMetricsF) from qgis.PyQt.QtCore import (Qt, QSizeF, QPointF, QRectF, QDir, QSize) from qgis.PyQt.QtXml import QDomDocument @@ -135,6 +136,11 @@ class PyQgsTextRenderer(unittest.TestCase): s.setStrokeWidth(7) s.setStrokeWidthUnit(QgsUnitTypes.RenderPoints) s.setStrokeWidthMapUnitScale(QgsMapUnitScale(QgsMapUnitScale(25, 26))) + + marker = QgsMarkerSymbol() + marker.setColor(QColor(100, 112, 134)) + s.setMarkerSymbol(marker) + return s def checkBackgroundSettings(self, s): @@ -162,6 +168,7 @@ class PyQgsTextRenderer(unittest.TestCase): self.assertEqual(s.strokeWidth(), 7) self.assertEqual(s.strokeWidthUnit(), QgsUnitTypes.RenderPoints) self.assertEqual(s.strokeWidthMapUnitScale(), QgsMapUnitScale(25, 26)) + self.assertEqual(s.markerSymbol().color().name(), '#647086') def testBackgroundGettersSetters(self): s = self.createBackgroundSettings() @@ -929,6 +936,75 @@ class PyQgsTextRenderer(unittest.TestCase): assert self.checkRender(format, 'background_svg_buffer_mm', QgsTextRenderer.Background, rect=QRectF(100, 100, 100, 100)) + def testDrawBackgroundMarkerFixedPixels(self): + format = QgsTextFormat() + format.setFont(getTestFont('bold')) + format.background().setEnabled(True) + format.background().setMarkerSymbol(QgsMarkerSymbol.createSimple({'color': '#ffffff', 'size': '3', 'outline_color': 'red', 'outline_width': '3'})) + format.background().setType(QgsTextBackgroundSettings.ShapeMarkerSymbol) + format.background().setSize(QSizeF(60, 80)) + format.background().setSizeType(QgsTextBackgroundSettings.SizeFixed) + format.background().setSizeUnit(QgsUnitTypes.RenderPixels) + assert self.checkRender(format, 'background_marker_fixed_pixels', QgsTextRenderer.Background) + + def testDrawBackgroundMarkerFixedMapUnits(self): + format = QgsTextFormat() + format.setFont(getTestFont('bold')) + format.background().setEnabled(True) + format.background().setMarkerSymbol(QgsMarkerSymbol.createSimple({'color': '#ffffff', 'size': '3', 'outline_color': 'red', 'outline_width': '3'})) + format.background().setType(QgsTextBackgroundSettings.ShapeMarkerSymbol) + format.background().setSize(QSizeF(20, 20)) + format.background().setSizeType(QgsTextBackgroundSettings.SizeFixed) + format.background().setSizeUnit(QgsUnitTypes.RenderMapUnits) + assert self.checkRender(format, 'background_marker_fixed_mapunits', QgsTextRenderer.Background) + + def testDrawBackgroundMarkerFixedMM(self): + format = QgsTextFormat() + format.setFont(getTestFont('bold')) + format.background().setEnabled(True) + format.background().setMarkerSymbol(QgsMarkerSymbol.createSimple({'color': '#ffffff', 'size': '3', 'outline_color': 'red', 'outline_width': '3'})) + format.background().setType(QgsTextBackgroundSettings.ShapeMarkerSymbol) + format.background().setSize(QSizeF(30, 30)) + format.background().setSizeType(QgsTextBackgroundSettings.SizeFixed) + format.background().setSizeUnit(QgsUnitTypes.RenderMillimeters) + assert self.checkRender(format, 'background_marker_fixed_mm', QgsTextRenderer.Background) + + def testDrawBackgroundMarkerBufferPixels(self): + format = QgsTextFormat() + format.setFont(getTestFont('bold')) + format.background().setEnabled(True) + format.background().setMarkerSymbol(QgsMarkerSymbol.createSimple({'color': '#ffffff', 'size': '3', 'outline_color': 'red', 'outline_width': '3'})) + format.background().setType(QgsTextBackgroundSettings.ShapeMarkerSymbol) + format.background().setSize(QSizeF(30, 30)) + format.background().setSizeType(QgsTextBackgroundSettings.SizeBuffer) + format.background().setSizeUnit(QgsUnitTypes.RenderPixels) + assert self.checkRender(format, 'background_marker_buffer_pixels', QgsTextRenderer.Background, + rect=QRectF(100, 100, 100, 100)) + + def testDrawBackgroundMarkerBufferMapUnits(self): + format = QgsTextFormat() + format.setFont(getTestFont('bold')) + format.background().setEnabled(True) + format.background().setMarkerSymbol(QgsMarkerSymbol.createSimple({'color': '#ffffff', 'size': '3', 'outline_color': 'red', 'outline_width': '3'})) + format.background().setType(QgsTextBackgroundSettings.ShapeMarkerSymbol) + format.background().setSize(QSizeF(4, 4)) + format.background().setSizeType(QgsTextBackgroundSettings.SizeBuffer) + format.background().setSizeUnit(QgsUnitTypes.RenderMapUnits) + assert self.checkRender(format, 'background_marker_buffer_mapunits', QgsTextRenderer.Background, + rect=QRectF(100, 100, 100, 100)) + + def testDrawBackgroundMarkerBufferMM(self): + format = QgsTextFormat() + format.setFont(getTestFont('bold')) + format.background().setEnabled(True) + format.background().setMarkerSymbol(QgsMarkerSymbol.createSimple({'color': '#ffffff', 'size': '3', 'outline_color': 'red', 'outline_width': '3'})) + format.background().setType(QgsTextBackgroundSettings.ShapeMarkerSymbol) + format.background().setSize(QSizeF(10, 10)) + format.background().setSizeType(QgsTextBackgroundSettings.SizeBuffer) + format.background().setSizeUnit(QgsUnitTypes.RenderMillimeters) + assert self.checkRender(format, 'background_marker_buffer_mm', QgsTextRenderer.Background, + rect=QRectF(100, 100, 100, 100)) + def testDrawBackgroundRotationFixed(self): format = QgsTextFormat() format.setFont(getTestFont('bold')) diff --git a/tests/testdata/control_images/text_renderer/background_marker_buffer_mapunits/background_marker_buffer_mapunits.png b/tests/testdata/control_images/text_renderer/background_marker_buffer_mapunits/background_marker_buffer_mapunits.png new file mode 100644 index 0000000000000000000000000000000000000000..98ed54c4129ef2ec3a323a35ba5d0953795f6334 GIT binary patch literal 5380 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 literal 0 HcmV?d00001 diff --git a/tests/testdata/control_images/text_renderer/background_marker_buffer_pixels/background_marker_buffer_pixels.png b/tests/testdata/control_images/text_renderer/background_marker_buffer_pixels/background_marker_buffer_pixels.png new file mode 100644 index 0000000000000000000000000000000000000000..b79ffa6a0879e587b824ad072b078cf1e0e28d47 GIT binary patch literal 5242 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 Q{7eMtY8h%)Jg|-YFEeBm_y7O^ literal 0 HcmV?d00001 diff --git a/tests/testdata/control_images/text_renderer/background_marker_fixed_mapunits/background_marker_fixed_mapunits.png b/tests/testdata/control_images/text_renderer/background_marker_fixed_mapunits/background_marker_fixed_mapunits.png new file mode 100644 index 0000000000000000000000000000000000000000..c2cd16844c261ca9eae1383f76dca3f1321f2d1c GIT binary patch literal 7809 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 gF9!aPV}L*c^`5A8hFw1ONa4 literal 0 HcmV?d00001 diff --git a/tests/testdata/control_images/text_renderer/background_marker_fixed_pixels/background_marker_fixed_pixels.png b/tests/testdata/control_images/text_renderer/background_marker_fixed_pixels/background_marker_fixed_pixels.png new file mode 100644 index 0000000000000000000000000000000000000000..8a6cf2eb0e797ab2ba7917e3dbb390320f703510 GIT binary patch literal 4079 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 Q4_?UXs@)arW#8EU0xvps1ONa4 literal 0 HcmV?d00001