Skip to content

Commit

Permalink
Add API to set specific symbol to render for selected features in
Browse files Browse the repository at this point in the history
a vector layer

If set, then that symbol will be used for selected features. Allows
eg selected lines to be rendered in a thicker line style vs
non-selected lines for improved visibility.
  • Loading branch information
nyalldawson committed Jul 28, 2023
1 parent 024e2db commit f54cba7
Show file tree
Hide file tree
Showing 8 changed files with 202 additions and 15 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@




class QgsVectorLayerSelectionProperties : QgsMapLayerSelectionProperties
{
%Docstring(signature="appended")
Expand All @@ -28,6 +29,7 @@ Implementation of layer selection properties for vector layers.
%Docstring
Constructor for QgsVectorLayerSelectionProperties, with the specified ``parent`` object.
%End
~QgsVectorLayerSelectionProperties();

virtual QDomElement writeXml( QDomElement &element, QDomDocument &doc, const QgsReadWriteContext &context );

Expand All @@ -54,6 +56,25 @@ An invalid ``color`` indicates that the default (i.e. project level) selection
color should be used instead.

.. seealso:: :py:func:`selectionColor`
%End

QgsSymbol *selectionSymbol() const;
%Docstring
Returns the symbol used to render selected features in the layer.

May be ``None`` if the default symbol should be used instead.

.. seealso:: :py:func:`setSelectionSymbol`
%End

void setSelectionSymbol( QgsSymbol *symbol /Transfer/ );
%Docstring
Sets the ``symbol`` used to render selected features in the layer.

Ownership of ``symbol`` is transferred to the plot. If ``symbol`` is ``None`` then
the default symbol will be used instead.

.. seealso:: :py:func:`selectionSymbol`
%End

};
Expand Down
81 changes: 68 additions & 13 deletions src/core/vector/qgsvectorlayerrenderer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ QgsVectorLayerRenderer::QgsVectorLayerRenderer( QgsVectorLayer *layer, QgsRender
if ( layerSelectionColor.isValid() )
context.setSelectionColor( layerSelectionColor );

if ( QgsSymbol *selectionSymbol = qobject_cast< QgsVectorLayerSelectionProperties * >( layer->selectionProperties() )->selectionSymbol() )
mSelectionSymbol.reset( selectionSymbol->clone() );

if ( !mainRenderer )
return;

Expand Down Expand Up @@ -450,6 +453,9 @@ void QgsVectorLayerRenderer::drawRenderer( QgsFeatureRenderer *renderer, QgsFeat
clipEngine->prepareGeometry();
}

if ( mSelectionSymbol && isMainRenderer )
mSelectionSymbol->startRender( context, mFields );

QgsFeature fet;
while ( fit.nextFeature( fet ) )
{
Expand All @@ -473,14 +479,24 @@ void QgsVectorLayerRenderer::drawRenderer( QgsFeatureRenderer *renderer, QgsFeat
if ( ! mNoSetLayerExpressionContext )
context.expressionContext().setFeature( fet );

bool sel = isMainRenderer && context.showSelection() && mSelectedFeatureIds.contains( fet.id() );
bool drawMarker = isMainRenderer && ( mDrawVertexMarkers && context.drawEditingInformation() && ( !mVertexMarkerOnlyForSelection || sel ) );
const bool featureIsSelected = isMainRenderer && context.showSelection() && mSelectedFeatureIds.contains( fet.id() );
bool drawMarker = isMainRenderer && ( mDrawVertexMarkers && context.drawEditingInformation() && ( !mVertexMarkerOnlyForSelection || featureIsSelected ) );

// render feature
bool rendered = false;
if ( !context.testFlag( Qgis::RenderContextFlag::SkipSymbolRendering ) )
{
rendered = renderer->renderFeature( fet, context, -1, sel, drawMarker );
if ( featureIsSelected && mSelectionSymbol )
{
// note: here we pass "false" for the selected argument, as we don't want to change
// the user's defined selection symbol colors or settings in any way
mSelectionSymbol->renderFeature( fet, context, -1, false, drawMarker );
rendered = renderer->willRenderFeature( fet, context );
}
else
{
rendered = renderer->renderFeature( fet, context, -1, featureIsSelected, drawMarker );
}
}
else
{
Expand Down Expand Up @@ -542,6 +558,9 @@ void QgsVectorLayerRenderer::drawRenderer( QgsFeatureRenderer *renderer, QgsFeat

delete context.expressionContext().popScope();

if ( mSelectionSymbol && isMainRenderer )
mSelectionSymbol->stopRender( context );

stopRenderer( renderer, nullptr );
}

Expand Down Expand Up @@ -625,7 +644,7 @@ void QgsVectorLayerRenderer::drawRendererLevels( QgsFeatureRenderer *renderer, Q
if ( renderer->orderByEnabled() )
{
QVector<QVariant> currentValues;
for ( auto const idx : orderByAttributeIdx )
for ( const int idx : std::as_const( orderByAttributeIdx ) )
{
currentValues.push_back( fet.attribute( idx ) );
}
Expand All @@ -644,11 +663,13 @@ void QgsVectorLayerRenderer::drawRendererLevels( QgsFeatureRenderer *renderer, Q

if ( !context.testFlag( Qgis::RenderContextFlag::SkipSymbolRendering ) )
{
if ( !features.back().contains( sym ) )
QHash<QgsSymbol *, QList<QgsFeature> > &featuresBack = features.back();
auto featuresBackIt = featuresBack.find( sym );
if ( featuresBackIt == featuresBack.end() )
{
features.back().insert( sym, QList<QgsFeature>() );
featuresBackIt = featuresBack.insert( sym, QList<QgsFeature>() );
}
features.back()[sym].append( fet );
featuresBackIt->append( fet );
}

// new labeling engine
Expand Down Expand Up @@ -713,7 +734,7 @@ void QgsVectorLayerRenderer::drawRendererLevels( QgsFeatureRenderer *renderer, Q
context.setFeatureClipGeometry( mClipFeatureGeom );

// 2. draw features in correct order
for ( auto &featureLists : features )
for ( const QHash< QgsSymbol *, QList<QgsFeature> > &featureLists : features )
{
for ( int l = 0; l < levels.count(); l++ )
{
Expand All @@ -728,24 +749,27 @@ void QgsVectorLayerRenderer::drawRendererLevels( QgsFeatureRenderer *renderer, Q
}
const int layer = item.layer();
const QList<QgsFeature> &lst = featureLists[item.symbol()];
for ( auto fit = lst.begin(); fit != lst.end(); ++fit )
for ( const QgsFeature &feature : lst )
{
if ( context.renderingStopped() )
{
stopRenderer( renderer, selRenderer );
return;
}

const bool sel = isMainRenderer && context.showSelection() && mSelectedFeatureIds.contains( fit->id() );
const bool featureIsSelected = isMainRenderer && context.showSelection() && mSelectedFeatureIds.contains( feature.id() );
if ( featureIsSelected && mSelectionSymbol )
continue; // defer rendering of selected symbols

// maybe vertex markers should be drawn only during the last pass...
const bool drawMarker = isMainRenderer && ( mDrawVertexMarkers && context.drawEditingInformation() && ( !mVertexMarkerOnlyForSelection || sel ) );
const bool drawMarker = isMainRenderer && ( mDrawVertexMarkers && context.drawEditingInformation() && ( !mVertexMarkerOnlyForSelection || featureIsSelected ) );

if ( ! mNoSetLayerExpressionContext )
context.expressionContext().setFeature( *fit );
context.expressionContext().setFeature( feature );

try
{
renderer->renderFeature( *fit, context, layer, sel, drawMarker );
renderer->renderFeature( feature, context, layer, featureIsSelected, drawMarker );

// as soon as first feature is rendered, we can start showing layer updates.
// but if we are blocking render updates (so that a previously cached image is being shown), we wait
Expand All @@ -766,6 +790,37 @@ void QgsVectorLayerRenderer::drawRendererLevels( QgsFeatureRenderer *renderer, Q
}
}

if ( mSelectionSymbol && !mSelectedFeatureIds.empty() && isMainRenderer && context.showSelection() )
{
mSelectionSymbol->startRender( context, mFields );

for ( const QHash< QgsSymbol *, QList<QgsFeature> > &featureLists : features )
{
for ( auto it = featureLists.constBegin(); it != featureLists.constEnd(); ++it )
{
const QList<QgsFeature> &lst = it.value();
for ( const QgsFeature &feature : lst )
{
if ( context.renderingStopped() )
{
break;
}

const bool featureIsSelected = mSelectedFeatureIds.contains( feature.id() );
if ( !featureIsSelected )
continue;

const bool drawMarker = mDrawVertexMarkers && context.drawEditingInformation();
// note: here we pass "false" for the selected argument, as we don't want to change
// the user's defined selection symbol colors or settings in any way
mSelectionSymbol->renderFeature( feature, context, -1, false, drawMarker );
}
}
}

mSelectionSymbol->stopRender( context );
}

stopRenderer( renderer, selRenderer );
}

Expand Down
2 changes: 2 additions & 0 deletions src/core/vector/qgsvectorlayerrenderer.h
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,8 @@ class QgsVectorLayerRenderer : public QgsMapLayerRenderer

bool mNoSetLayerExpressionContext = false;

std::unique_ptr< QgsSymbol > mSelectionSymbol;

};


Expand Down
26 changes: 26 additions & 0 deletions src/core/vector/qgsvectorlayerselectionproperties.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,29 @@

#include "qgsvectorlayerselectionproperties.h"
#include "qgscolorutils.h"
#include "qgssymbollayerutils.h"

QgsVectorLayerSelectionProperties::QgsVectorLayerSelectionProperties( QObject *parent )
: QgsMapLayerSelectionProperties( parent )
{
}

QgsVectorLayerSelectionProperties::~QgsVectorLayerSelectionProperties() = default;


QDomElement QgsVectorLayerSelectionProperties::writeXml( QDomElement &parentElement, QDomDocument &document, const QgsReadWriteContext &context )
{
QDomElement element = document.createElement( QStringLiteral( "selection" ) );

QgsColorUtils::writeXml( mSelectionColor, QStringLiteral( "selectionColor" ), document, element, context );

if ( mSelectionSymbol )
{
QDomElement selectionSymbolElement = document.createElement( QStringLiteral( "selectionSymbol" ) );
selectionSymbolElement.appendChild( QgsSymbolLayerUtils::saveSymbol( QString(), mSelectionSymbol.get(), document, context ) );
element.appendChild( selectionSymbolElement );
}

parentElement.appendChild( element );
return element;
}
Expand All @@ -41,13 +51,19 @@ bool QgsVectorLayerSelectionProperties::readXml( const QDomElement &element, con
return false;

mSelectionColor = QgsColorUtils::readXml( selectionElement, QStringLiteral( "selectionColor" ), context );

{
const QDomElement selectionSymbolElement = selectionElement.firstChildElement( QStringLiteral( "selectionSymbol" ) ).firstChildElement( QStringLiteral( "symbol" ) );
mSelectionSymbol.reset( QgsSymbolLayerUtils::loadSymbol( selectionSymbolElement, context ) );
}
return true;
}

QgsVectorLayerSelectionProperties *QgsVectorLayerSelectionProperties::clone() const
{
std::unique_ptr< QgsVectorLayerSelectionProperties > res = std::make_unique< QgsVectorLayerSelectionProperties >( nullptr );
res->mSelectionColor = mSelectionColor;
res->mSelectionSymbol.reset( mSelectionSymbol ? mSelectionSymbol->clone() : nullptr );
return res.release();
}

Expand All @@ -60,3 +76,13 @@ void QgsVectorLayerSelectionProperties::setSelectionColor( const QColor &color )
{
mSelectionColor = color;
}

QgsSymbol *QgsVectorLayerSelectionProperties::selectionSymbol() const
{
return mSelectionSymbol.get();
}

void QgsVectorLayerSelectionProperties::setSelectionSymbol( QgsSymbol *symbol )
{
mSelectionSymbol.reset( symbol );
}
24 changes: 23 additions & 1 deletion src/core/vector/qgsvectorlayerselectionproperties.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@

#include <QColor>

class QgsSymbol;

/**
* \class QgsVectorLayerSelectionProperties
* \ingroup core
Expand All @@ -43,6 +45,7 @@ class CORE_EXPORT QgsVectorLayerSelectionProperties : public QgsMapLayerSelectio
* Constructor for QgsVectorLayerSelectionProperties, with the specified \a parent object.
*/
QgsVectorLayerSelectionProperties( QObject *parent SIP_TRANSFERTHIS = nullptr );
~QgsVectorLayerSelectionProperties() override;

QDomElement writeXml( QDomElement &element, QDomDocument &doc, const QgsReadWriteContext &context ) override;
bool readXml( const QDomElement &element, const QgsReadWriteContext &context ) override;
Expand All @@ -68,10 +71,29 @@ class CORE_EXPORT QgsVectorLayerSelectionProperties : public QgsMapLayerSelectio
*/
void setSelectionColor( const QColor &color );

/**
* Returns the symbol used to render selected features in the layer.
*
* May be NULLPTR if the default symbol should be used instead.
*
* \see setSelectionSymbol()
*/
QgsSymbol *selectionSymbol() const;

/**
* Sets the \a symbol used to render selected features in the layer.
*
* Ownership of \a symbol is transferred to the plot. If \a symbol is NULLPTR then
* the default symbol will be used instead.
*
* \see selectionSymbol()
*/
void setSelectionSymbol( QgsSymbol *symbol SIP_TRANSFER );

private:

QColor mSelectionColor;

std::unique_ptr< QgsSymbol > mSelectionSymbol;
};

#endif // QGSVECTORLAYERSELECTIONPROPERTIES_H
22 changes: 21 additions & 1 deletion tests/src/python/test_qgsvectorlayer.py
Original file line number Diff line number Diff line change
Expand Up @@ -4537,6 +4537,7 @@ def test_selection_properties(self):
self.assertTrue(vl.isValid())

self.assertFalse(vl.selectionProperties().selectionColor().isValid())
self.assertFalse(vl.selectionProperties().selectionSymbol())
vl.selectionProperties().setSelectionColor(
QColor(255, 0, 0)
)
Expand All @@ -4546,7 +4547,7 @@ def test_selection_properties(self):
p = QgsProject()
p.addMapLayer(vl)

# test saving and restoring split policies
# test saving and restoring
with tempfile.TemporaryDirectory() as temp:
self.assertTrue(p.write(temp + '/test.qgs'))

Expand All @@ -4559,6 +4560,25 @@ def test_selection_properties(self):
self.assertEqual(vl2.selectionProperties().selectionColor(),
QColor(255, 0, 0))

selected_symbol = QgsMarkerSymbol()
selected_symbol.setColor(QColor(25, 26, 27))
vl.selectionProperties().setSelectionSymbol(
selected_symbol
)

with tempfile.TemporaryDirectory() as temp:
self.assertTrue(p.write(temp + '/test.qgs'))

p2 = QgsProject()
self.assertTrue(p2.read(temp + '/test.qgs'))

vl2 = list(p2.mapLayers().values())[0]
self.assertEqual(vl2.name(), vl.name())

self.assertEqual(vl2.selectionProperties().selectionSymbol().color(),
QColor(25, 26, 27))


# TODO:
# - fetch rect: feat with changed geometry: 1. in rect, 2. out of rect
# - more join tests
Expand Down
Loading

0 comments on commit f54cba7

Please sign in to comment.