[FEATURE] Allow symbol layers to be temporarily disabled

Adds a new checkbox at the bottom of each symbol layer's
properties which allows you to control whether the layer is
enabled or not.

Disabled layers are not drawn, but are saved and can be
enabled at a later stage.

This makes it easier to tweak symbol appearance without
having to totally delete a symbol layer.
This commit is contained in:
Nyall Dawson 2016-10-13 07:39:49 +10:00
parent 2a873b83ad
commit b8c2e68fc5
10 changed files with 301 additions and 7 deletions

View File

@ -67,6 +67,22 @@ class QgsSymbolLayer
virtual ~QgsSymbolLayer();
/**
* Returns true if symbol layer is enabled and will be drawn.
* @note added in QGIS 3.0
* @see setEnabled()
*/
bool enabled() const;
/**
* Sets whether symbol layer is enabled and should be drawn. Disabled
* layers are not drawn, but remain part of the symbol and can be re-enabled
* when desired.
* @note added in QGIS 3.0
* @see enabled()
*/
void setEnabled( bool enabled );
/**
* The fill color.
*/

View File

@ -396,7 +396,12 @@ void QgsSymbol::startRender( QgsRenderContext& context, const QgsFields& fields
mSymbolRenderContext->setExpressionContextScope( scope );
Q_FOREACH ( QgsSymbolLayer* layer, mLayers )
{
if ( !layer->enabled() )
continue;
layer->startRender( symbolContext );
}
}
void QgsSymbol::stopRender( QgsRenderContext& context )
@ -405,7 +410,12 @@ void QgsSymbol::stopRender( QgsRenderContext& context )
if ( mSymbolRenderContext )
{
Q_FOREACH ( QgsSymbolLayer* layer, mLayers )
{
if ( !layer->enabled() )
continue;
layer->stopRender( *mSymbolRenderContext );
}
}
delete mSymbolRenderContext;
@ -442,6 +452,9 @@ void QgsSymbol::drawPreviewIcon( QPainter* painter, QSize size, QgsRenderContext
Q_FOREACH ( QgsSymbolLayer* layer, mLayers )
{
if ( !layer->enabled() )
continue;
if ( mType == Fill && layer->type() == Line )
{
// line symbol layer would normally draw just a line
@ -587,6 +600,7 @@ QgsSymbolLayerList QgsSymbol::cloneLayers() const
QgsSymbolLayer* layer = ( *it )->clone();
layer->setLocked(( *it )->isLocked() );
layer->setRenderingPass(( *it )->renderingPass() );
layer->setEnabled(( *it )->enabled() );
lst.append( layer );
}
return lst;
@ -1417,7 +1431,7 @@ void QgsMarkerSymbol::renderPoint( QPointF point, const QgsFeature* f, QgsRender
if ( layerIdx != -1 )
{
QgsSymbolLayer* symbolLayer = mLayers.value( layerIdx );
if ( symbolLayer )
if ( symbolLayer && symbolLayer->enabled() )
{
if ( symbolLayer->type() == QgsSymbol::Marker )
{
@ -1432,6 +1446,9 @@ void QgsMarkerSymbol::renderPoint( QPointF point, const QgsFeature* f, QgsRender
Q_FOREACH ( QgsSymbolLayer* symbolLayer, mLayers )
{
if ( !symbolLayer->enabled() )
continue;
if ( symbolLayer->type() == QgsSymbol::Marker )
{
QgsMarkerSymbolLayer* markerLayer = static_cast<QgsMarkerSymbolLayer*>( symbolLayer );
@ -1625,7 +1642,7 @@ void QgsLineSymbol::renderPolyline( const QPolygonF& points, const QgsFeature* f
if ( layerIdx != -1 )
{
QgsSymbolLayer* symbolLayer = mLayers.value( layerIdx );
if ( symbolLayer )
if ( symbolLayer && symbolLayer->enabled() )
{
if ( symbolLayer->type() == QgsSymbol::Line )
{
@ -1640,6 +1657,9 @@ void QgsLineSymbol::renderPolyline( const QPolygonF& points, const QgsFeature* f
Q_FOREACH ( QgsSymbolLayer* symbolLayer, mLayers )
{
if ( !symbolLayer->enabled() )
continue;
if ( symbolLayer->type() == QgsSymbol::Line )
{
QgsLineSymbolLayer* lineLayer = static_cast<QgsLineSymbolLayer*>( symbolLayer );
@ -1704,7 +1724,7 @@ void QgsFillSymbol::renderPolygon( const QPolygonF& points, QList<QPolygonF>* ri
if ( layerIdx != -1 )
{
QgsSymbolLayer* symbolLayer = mLayers.value( layerIdx );
if ( symbolLayer )
if ( symbolLayer && symbolLayer->enabled() )
{
if ( symbolLayer->type() == Fill || symbolLayer->type() == Line )
renderPolygonUsingLayer( symbolLayer, points, rings, symbolContext );
@ -1716,6 +1736,9 @@ void QgsFillSymbol::renderPolygon( const QPolygonF& points, QList<QPolygonF>* ri
Q_FOREACH ( QgsSymbolLayer* symbolLayer, mLayers )
{
if ( !symbolLayer->enabled() )
continue;
if ( symbolLayer->type() == Fill || symbolLayer->type() == Line )
renderPolygonUsingLayer( symbolLayer, points, rings, symbolContext );
else

View File

@ -261,6 +261,7 @@ void QgsSymbolLayer::setPaintEffect( QgsPaintEffect *effect )
QgsSymbolLayer::QgsSymbolLayer( QgsSymbol::SymbolType type, bool locked )
: mType( type )
, mEnabled( true )
, mLocked( locked )
, mRenderingPass( 0 )
, mPaintEffect( nullptr )

View File

@ -53,6 +53,22 @@ class CORE_EXPORT QgsSymbolLayer
virtual ~QgsSymbolLayer();
/**
* Returns true if symbol layer is enabled and will be drawn.
* @note added in QGIS 3.0
* @see setEnabled()
*/
bool enabled() const { return mEnabled; }
/**
* Sets whether symbol layer is enabled and should be drawn. Disabled
* layers are not drawn, but remain part of the symbol and can be re-enabled
* when desired.
* @note added in QGIS 3.0
* @see enabled()
*/
void setEnabled( bool enabled ) { mEnabled = enabled; }
/**
* The fill color.
*/
@ -266,6 +282,10 @@ class CORE_EXPORT QgsSymbolLayer
QgsSymbolLayer( QgsSymbol::SymbolType type, bool locked = false );
QgsSymbol::SymbolType mType;
//! True if layer is enabled and should be drawn
bool mEnabled;
bool mLocked;
QColor mColor;
int mRenderingPass;

View File

@ -852,6 +852,7 @@ QgsSymbolLayer* QgsSymbolLayerUtils::loadSymbolLayer( QDomElement& element )
{
QString layerClass = element.attribute( "class" );
bool locked = element.attribute( "locked" ).toInt();
bool enabled = element.attribute( "enabled", "1" ).toInt();
int pass = element.attribute( "pass" ).toInt();
// parse properties
@ -863,6 +864,7 @@ QgsSymbolLayer* QgsSymbolLayerUtils::loadSymbolLayer( QDomElement& element )
{
layer->setLocked( locked );
layer->setRenderingPass( pass );
layer->setEnabled( enabled );
//restore layer effect
QDomElement effectElem = element.firstChildElement( "effect" );
@ -910,6 +912,7 @@ QDomElement QgsSymbolLayerUtils::saveSymbol( const QString& name, QgsSymbol* sym
QDomElement layerEl = doc.createElement( "layer" );
layerEl.setAttribute( "class", layer->layerType() );
layerEl.setAttribute( "enabled", layer->enabled() );
layerEl.setAttribute( "locked", layer->isLocked() );
layerEl.setAttribute( "pass", layer->renderingPass() );
saveProperties( layer->properties(), doc, layerEl );

View File

@ -112,6 +112,7 @@ QgsLayerPropertiesWidget::QgsLayerPropertiesWidget( QgsSymbolLayer* layer, const
// update layer type combo box
int idx = cboLayerType->findData( mLayer->layerType() );
cboLayerType->setCurrentIndex( idx );
mEnabledCheckBox->setChecked( mLayer->enabled() );
// set the corresponding widget
updateSymbolLayerWidget( layer );
connect( cboLayerType, SIGNAL( currentIndexChanged( int ) ), this, SLOT( layerTypeChanged() ) );
@ -236,3 +237,9 @@ void QgsLayerPropertiesWidget::reloadLayer()
{
emit changeLayer( mLayer );
}
void QgsLayerPropertiesWidget::on_mEnabledCheckBox_toggled( bool enabled )
{
mLayer->setEnabled( enabled );
emitSignalChanged();
}

View File

@ -82,6 +82,7 @@ class GUI_EXPORT QgsLayerPropertiesWidget : public QgsPanelWidget, private Ui::L
private slots:
void reloadLayer();
void on_mEnabledCheckBox_toggled( bool enabled );
private:

View File

@ -30,7 +30,7 @@
<number>3</number>
</property>
<item row="1" column="0">
<layout class="QHBoxLayout" name="horizontalLayout">
<layout class="QHBoxLayout" name="horizontalLayout" stretch="0,1">
<item>
<widget class="QLabel" name="label">
<property name="text">
@ -50,6 +50,9 @@
</item>
</layout>
</item>
<item row="4" column="0">
<widget class="QgsEffectStackCompactWidget" name="mEffectWidget" native="true"/>
</item>
<item row="2" column="0">
<widget class="QStackedWidget" name="stackedWidget">
<widget class="QWidget" name="pageDummy">
@ -69,7 +72,34 @@
</widget>
</item>
<item row="3" column="0">
<widget class="QgsEffectStackCompactWidget" name="mEffectWidget" native="true"/>
<layout class="QHBoxLayout" name="horizontalLayout_3">
<property name="spacing">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QCheckBox" name="mEnabledCheckBox">
<property name="text">
<string>Enable layer</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</widget>

View File

@ -29,7 +29,7 @@ import os
from qgis.PyQt.QtCore import pyqtWrapperType, Qt, QDir, QFile, QIODevice, QPointF
from qgis.PyQt.QtXml import QDomDocument
from qgis.PyQt.QtGui import QColor
from qgis.PyQt.QtGui import QColor, QImage, QPainter
from qgis.core import (QgsCentroidFillSymbolLayer,
QgsEllipseSymbolLayer,
@ -55,7 +55,17 @@ from qgis.core import (QgsCentroidFillSymbolLayer,
QgsShapeburstFillSymbolLayer,
QgsArrowSymbolLayer,
QgsSymbol,
QgsUnitTypes
QgsUnitTypes,
QgsFillSymbol,
QgsLineSymbol,
QgsMarkerSymbol,
QgsSymbolLayerUtils,
QgsMapSettings,
QgsGeometry,
QgsFeature,
QgsRenderContext,
QgsRenderChecker,
QgsRectangle
)
from qgis.testing import start_app, unittest
from utilities import unitTestDataPath
@ -79,6 +89,14 @@ class TestQgsSymbolLayer(unittest.TestCase):
returns NULL
"""
def setUp(self):
self.report = "<h1>Python QgsSymbolLayer Tests</h1>\n"
def tearDown(self):
report_file_path = "%s/qgistest.html" % QDir.tempPath()
with open(report_file_path, 'a') as report_file:
report_file.write(self.report)
def testBinding(self):
"""Test python bindings existence."""
mType = type(QgsSymbolLayer)
@ -270,6 +288,181 @@ class TestQgsSymbolLayer(unittest.TestCase):
mMessage = 'Expected "%s" got "%s"' % (mExpectedType, mType)
assert mExpectedType == mType, mMessage
def testGettersSetters(self):
""" test base class getters/setters """
layer = QgsSimpleFillSymbolLayer()
layer.setEnabled(False)
self.assertFalse(layer.enabled())
layer.setEnabled(True)
self.assertTrue(layer.enabled())
layer.setLocked(False)
self.assertFalse(layer.isLocked())
layer.setLocked(True)
self.assertTrue(layer.isLocked())
layer.setRenderingPass(5)
self.assertEqual(layer.renderingPass(), 5)
def testSaveRestore(self):
""" Test saving and restoring base symbol layer properties to xml"""
layer = QgsSimpleFillSymbolLayer()
layer.setEnabled(False)
layer.setLocked(True)
layer.setRenderingPass(5)
symbol = QgsFillSymbol()
symbol.changeSymbolLayer(0, layer)
doc = QDomDocument("testdoc")
elem = QgsSymbolLayerUtils.saveSymbol('test', symbol, doc)
restored_symbol = QgsSymbolLayerUtils.loadSymbol(elem)
restored_layer = restored_symbol.symbolLayer(0)
self.assertFalse(restored_layer.enabled())
self.assertTrue(restored_layer.isLocked())
self.assertEqual(restored_layer.renderingPass(), 5)
def testClone(self):
""" test that base symbol layer properties are cloned with layer """
layer = QgsSimpleFillSymbolLayer()
layer.setEnabled(False)
layer.setLocked(True)
layer.setRenderingPass(5)
symbol = QgsFillSymbol()
symbol.changeSymbolLayer(0, layer)
cloned_symbol = symbol.clone()
cloned_layer = cloned_symbol.symbolLayer(0)
self.assertFalse(cloned_layer.enabled())
self.assertTrue(cloned_layer.isLocked())
self.assertEqual(cloned_layer.renderingPass(), 5)
def imageCheck(self, name, reference_image, image):
self.report += "<h2>Render {}</h2>\n".format(name)
temp_dir = QDir.tempPath() + '/'
file_name = temp_dir + 'symbollayer_' + name + ".png"
image.save(file_name, "PNG")
checker = QgsRenderChecker()
checker.setControlPathPrefix("symbol_layer")
checker.setControlName("expected_" + reference_image)
checker.setRenderedImage(file_name)
checker.setColorTolerance(2)
result = checker.compareImages(name, 20)
self.report += checker.report()
print((self.report))
return result
def testRenderFillLayerDisabled(self):
""" test that rendering a fill symbol with disabled layer works"""
layer = QgsSimpleFillSymbolLayer()
layer.setEnabled(False)
symbol = QgsFillSymbol()
symbol.changeSymbolLayer(0, layer)
image = QImage(200, 200, QImage.Format_RGB32)
painter = QPainter()
ms = QgsMapSettings()
geom = QgsGeometry.fromWkt('Polygon ((0 0, 10 0, 10 10, 0 10, 0 0))')
f = QgsFeature()
f.setGeometry(geom)
extent = geom.geometry().boundingBox()
# buffer extent by 10%
extent = extent.buffer((extent.height() + extent.width()) / 20.0)
ms.setExtent(extent)
ms.setOutputSize(image.size())
context = QgsRenderContext.fromMapSettings(ms)
context.setPainter(painter)
context.setScaleFactor(96 / 25.4) # 96 DPI
painter.begin(image)
image.fill(QColor(255, 255, 255))
symbol.startRender(context)
symbol.renderFeature(f, context)
symbol.stopRender(context)
painter.end()
self.assertTrue(self.imageCheck('symbol_layer', 'symbollayer_disabled', image))
def testRenderLineLayerDisabled(self):
""" test that rendering a line symbol with disabled layer works"""
layer = QgsSimpleLineSymbolLayer()
layer.setEnabled(False)
symbol = QgsLineSymbol()
symbol.changeSymbolLayer(0, layer)
image = QImage(200, 200, QImage.Format_RGB32)
painter = QPainter()
ms = QgsMapSettings()
geom = QgsGeometry.fromWkt('LineString (0 0,3 4,4 3)')
f = QgsFeature()
f.setGeometry(geom)
extent = geom.geometry().boundingBox()
# buffer extent by 10%
extent = extent.buffer((extent.height() + extent.width()) / 20.0)
ms.setExtent(extent)
ms.setOutputSize(image.size())
context = QgsRenderContext.fromMapSettings(ms)
context.setPainter(painter)
context.setScaleFactor(96 / 25.4) # 96 DPI
painter.begin(image)
image.fill(QColor(255, 255, 255))
symbol.startRender(context)
symbol.renderFeature(f, context)
symbol.stopRender(context)
painter.end()
self.assertTrue(self.imageCheck('symbol_layer', 'symbollayer_disabled', image))
def testRenderMarkerLayerDisabled(self):
""" test that rendering a marker symbol with disabled layer works"""
layer = QgsSimpleMarkerSymbolLayer()
layer.setEnabled(False)
symbol = QgsMarkerSymbol()
symbol.changeSymbolLayer(0, layer)
image = QImage(200, 200, QImage.Format_RGB32)
painter = QPainter()
ms = QgsMapSettings()
geom = QgsGeometry.fromWkt('Point (1 2)')
f = QgsFeature()
f.setGeometry(geom)
extent = QgsRectangle(0, 0, 4, 4)
ms.setExtent(extent)
ms.setOutputSize(image.size())
context = QgsRenderContext.fromMapSettings(ms)
context.setPainter(painter)
context.setScaleFactor(96 / 25.4) # 96 DPI
painter.begin(image)
image.fill(QColor(255, 255, 255))
symbol.startRender(context)
symbol.renderFeature(f, context)
symbol.stopRender(context)
painter.end()
self.assertTrue(self.imageCheck('symbol_layer', 'symbollayer_disabled', image))
def testQgsSimpleFillSymbolLayer(self):
"""Create a new style from a .sld file and match test.
"""

Binary file not shown.

After

Width:  |  Height:  |  Size: 610 B