diff --git a/python/core/auto_generated/symbology/qgssymbollayerutils.sip.in b/python/core/auto_generated/symbology/qgssymbollayerutils.sip.in index 283732941ba..adbb16b3191 100644 --- a/python/core/auto_generated/symbology/qgssymbollayerutils.sip.in +++ b/python/core/auto_generated/symbology/qgssymbollayerutils.sip.in @@ -978,7 +978,7 @@ Encodes a reference to a parametric SVG into a path with parameters according to .. versionadded:: 3.0 %End - static QSet toSymbolLayerPointers( QgsFeatureRenderer *renderer, const QSet &symbolLayerIds ); + static QSet toSymbolLayerPointers( const QgsFeatureRenderer *renderer, const QSet &symbolLayerIds ); %Docstring Converts a set of symbol layer id to a set of pointers to actual symbol layers carried by the feature renderer. @@ -1030,6 +1030,15 @@ The method makes approximations and can modify ``angle`` in order to generate th :return: the size of the tile +.. versionadded:: 3.30 +%End + + static void fixOldSymbolLayerReferences( const QMap &mapLayers ); +%Docstring +:py:class:`QgsSymbolLayerReference` uses :py:class:`QgsSymbolLayer` unique uuid identifier since QGIS 3.30, instead of the symbol +key (rule for :py:class:`QgsRuleBasedRenderer` for instance) and index path, so this method migrates ``mapLayers`` old references +to new ones. + .. versionadded:: 3.30 %End diff --git a/src/core/project/qgsproject.cpp b/src/core/project/qgsproject.cpp index 2e790907f7f..cfa3dd69ac7 100644 --- a/src/core/project/qgsproject.cpp +++ b/src/core/project/qgsproject.cpp @@ -1946,6 +1946,14 @@ bool QgsProject::readProjectFile( const QString &filename, Qgis::ProjectReadFlag profile.switchTask( tr( "Resolving references" ) ); mRootGroup->resolveReferences( this ); + // we need to migrate old fashion designed QgsSymbolLayerReference to new ones + if ( QgsProjectVersion( 3, 28, 0 ) > mSaveVersion ) + { + Q_NOWARN_DEPRECATED_PUSH + QgsSymbolLayerUtils::fixOldSymbolLayerReferences( mapLayers() ); + Q_NOWARN_DEPRECATED_POP + } + if ( !layerTreeElem.isNull() ) { mRootGroup->readLayerOrderFromXml( layerTreeElem ); diff --git a/src/core/symbology/qgssymbollayerreference.cpp b/src/core/symbology/qgssymbollayerreference.cpp index f9a9354a163..d56be6bbffa 100644 --- a/src/core/symbology/qgssymbollayerreference.cpp +++ b/src/core/symbology/qgssymbollayerreference.cpp @@ -40,7 +40,7 @@ QgsSymbolLayerReferenceList stringToSymbolLayerReferenceList( const QString &str // TODO QGIS 4 : remove this if branch, keep only else part Q_NOWARN_DEPRECATED_PUSH - // old masked symbol layer format (before 3.28), we use unique id now! + // old masked symbol layer format (before 3.30), we use unique id now! // we load it the old fashion way and we will update the new one later when // the whole project is loaded diff --git a/src/core/symbology/qgssymbollayerutils.cpp b/src/core/symbology/qgssymbollayerutils.cpp index 1ffe57e19da..22a6c20c363 100644 --- a/src/core/symbology/qgssymbollayerutils.cpp +++ b/src/core/symbology/qgssymbollayerutils.cpp @@ -43,6 +43,7 @@ #include "qgssymbollayerreference.h" #include "qgsmarkersymbollayer.h" #include "qmath.h" +#include "qgsmasksymbollayer.h" #include #include @@ -4911,7 +4912,7 @@ double QgsSymbolLayerUtils::sizeInPixelsFromSldUom( const QString &uom, double s return size * scale; } -QSet QgsSymbolLayerUtils::toSymbolLayerPointers( QgsFeatureRenderer *renderer, const QSet &symbolLayerIds ) +QSet QgsSymbolLayerUtils::toSymbolLayerPointers( const QgsFeatureRenderer *renderer, const QSet &symbolLayerIds ) { class SymbolLayerVisitor : public QgsStyleEntityVisitorInterface { @@ -5355,5 +5356,92 @@ QSize QgsSymbolLayerUtils::tileSize( int width, int height, double &angleRad ) } return tileSize; - +} + +void QgsSymbolLayerUtils::fixOldSymbolLayerReferences( const QMap &mapLayers ) +{ + for ( QgsMapLayer *ml : mapLayers ) + { + QgsVectorLayer *vl = qobject_cast( ml ); + if ( !vl ) + continue; + + auto migrateOldReferences = [&mapLayers]( const QList &slRefs ) + { + QList newRefs; + for ( QgsSymbolLayerReference slRef : slRefs ) + { + const QgsVectorLayer *vlRef = qobject_cast( mapLayers[ slRef.layerId() ] ); + const QgsFeatureRenderer *renderer = vlRef ? vlRef->renderer() : nullptr; + QSet symbolLayers = renderer ? QgsSymbolLayerUtils::toSymbolLayerPointers( + renderer, QSet() << slRef.symbolLayerId() ) : QSet(); + const QString slId = symbolLayers.isEmpty() ? QString() : ( *symbolLayers.constBegin() )->id(); + newRefs << QgsSymbolLayerReference( slRef.layerId(), slId ); + } + + return newRefs; + }; + + if ( QgsAbstractVectorLayerLabeling *labeling = vl->labeling() ) + { + for ( QString provider : labeling->subProviders() ) + { + QgsPalLayerSettings settings = labeling->settings( provider ); + QgsTextFormat format = settings.format(); + QList newMaskedSymbolLayers = migrateOldReferences( format.mask().maskedSymbolLayers() ); + format.mask().setMaskedSymbolLayers( newMaskedSymbolLayers ); + settings.setFormat( format ); + labeling->setSettings( new QgsPalLayerSettings( settings ), provider ); + } + } + + if ( QgsFeatureRenderer *renderer = vl->renderer() ) + { + + class SymbolLayerVisitor : public QgsStyleEntityVisitorInterface + { + public: + bool visitEnter( const QgsStyleEntityVisitorInterface::Node &node ) override + { + return ( node.type == QgsStyleEntityVisitorInterface::NodeType::SymbolRule ); + } + + void visitSymbol( const QgsSymbol *symbol ) + { + for ( int idx = 0; idx < symbol->symbolLayerCount(); idx++ ) + { + const QgsSymbolLayer *sl = symbol->symbolLayer( idx ); + + // recurse over sub symbols + const QgsSymbol *subSymbol = const_cast( sl )->subSymbol(); + if ( subSymbol ) + visitSymbol( subSymbol ); + + if ( const QgsMaskMarkerSymbolLayer *maskLayer = dynamic_cast( sl ) ) + maskSymbolLayers << maskLayer; + } + } + + bool visit( const QgsStyleEntityVisitorInterface::StyleLeaf &leaf ) override + { + if ( leaf.entity && leaf.entity->type() == QgsStyle::SymbolEntity ) + { + auto symbolEntity = static_cast( leaf.entity ); + if ( symbolEntity->symbol() ) + visitSymbol( symbolEntity->symbol() ); + } + return true; + } + + QList maskSymbolLayers; + }; + + SymbolLayerVisitor visitor; + renderer->accept( &visitor ); + + for ( const QgsMaskMarkerSymbolLayer *maskSymbolLayer : visitor.maskSymbolLayers ) + // Ugly but there is no other proper way to get those layer in order to modify them + const_cast( maskSymbolLayer )->setMasks( migrateOldReferences( maskSymbolLayer->masks() ) ); + } + } } diff --git a/src/core/symbology/qgssymbollayerutils.h b/src/core/symbology/qgssymbollayerutils.h index 70f46dfeb59..5ec19a71cb8 100644 --- a/src/core/symbology/qgssymbollayerutils.h +++ b/src/core/symbology/qgssymbollayerutils.h @@ -884,7 +884,7 @@ class CORE_EXPORT QgsSymbolLayerUtils * Converts a set of symbol layer id to a set of pointers to actual symbol layers carried by the feature renderer. * \since QGIS 3.12 */ - static QSet toSymbolLayerPointers( QgsFeatureRenderer *renderer, const QSet &symbolLayerIds ); + static QSet toSymbolLayerPointers( const QgsFeatureRenderer *renderer, const QSet &symbolLayerIds ); /** * Calculates the frame rate (in frames per second) at which the given \a renderer must be redrawn. @@ -928,6 +928,14 @@ class CORE_EXPORT QgsSymbolLayerUtils */ static QSize tileSize( int width, int height, double &angleRad SIP_INOUT ); + /** + * QgsSymbolLayerReference uses QgsSymbolLayer unique uuid identifier since QGIS 3.30, instead of the symbol + * key (rule for QgsRuleBasedRenderer for instance) and index path, so this method migrates \a mapLayers old references + * to new ones. + * \since QGIS 3.30 + */ + Q_DECL_DEPRECATED static void fixOldSymbolLayerReferences( const QMap &mapLayers ); + ///@cond PRIVATE #ifndef SIP_RUN static QgsProperty rotateWholeSymbol( double additionalRotation, const QgsProperty &property ) diff --git a/tests/src/python/test_selective_masking.py b/tests/src/python/test_selective_masking.py index c0af44811f9..b4aa14b28e3 100644 --- a/tests/src/python/test_selective_masking.py +++ b/tests/src/python/test_selective_masking.py @@ -145,7 +145,7 @@ class TestSelectiveMasking(unittest.TestCase): cls.report != REPORT_TITLE): QDesktopServices.openUrl(QUrl("file:///{}".format(report_file_path))) - def get_symbollayer_ref(self, layer, ruleId, symbollayer_ids): + def get_symbollayer(self, layer, ruleId, symbollayer_ids): """ Returns the symbol layer according to given layer, ruleId (None if no rule) and the path to symbol layer id (for instance [0, 1]) @@ -164,6 +164,14 @@ class TestSelectiveMasking(unittest.TestCase): symbol = symbollayer.subSymbol() symbollayer = symbol.symbolLayer(symbollayer_ids[i]) + return symbollayer + + def get_symbollayer_ref(self, layer, ruleId, symbollayer_ids): + """ + Returns the symbol layer according to given layer, ruleId (None if no rule) and the path + to symbol layer id (for instance [0, 1]) + """ + symbollayer = self.get_symbollayer(layer, rule, symbollayer_ids) return QgsSymbolLayerReference(layer.id(), symbollayer.id()) def check_renderings(self, map_settings, control_name): @@ -265,7 +273,7 @@ class TestSelectiveMasking(unittest.TestCase): # simple ids mask_layer = QgsMaskMarkerSymbolLayer() mask_layer.setMasks([ - self.get_symbollayer_ref(self.lines_layer, "", [0]), + QgsSymbolLayerReference(self.lines_layer.id(), QgsSymbolLayerId("", [0])), QgsSymbolLayerReference(self.lines_layer2.id(), QgsSymbolLayerId("some_id", [1, 3, 5, 19])), QgsSymbolLayerReference(self.polys_layer.id(), QgsSymbolLayerId("some_other_id", [4, 5])), ]) @@ -273,8 +281,14 @@ class TestSelectiveMasking(unittest.TestCase): props = mask_layer.properties() mask_layer2 = QgsMaskMarkerSymbolLayer.create(props) + print(f"mask2={mask_layer2.masks()}") + print("mask={}".format([ + QgsSymbolLayerReference(self.lines_layer.id(), QgsSymbolLayerId("", [0])), + QgsSymbolLayerReference(self.lines_layer2.id(), QgsSymbolLayerId("some_id", [1, 3, 5, 19])), + QgsSymbolLayerReference(self.polys_layer.id(), QgsSymbolLayerId("some_other_id", [4, 5])), + ])) self.assertEqual(mask_layer2.masks(), [ - self.get_symbollayer_ref(self.lines_layer, "", [0]), + QgsSymbolLayerReference(self.lines_layer.id(), QgsSymbolLayerId("", [0])), QgsSymbolLayerReference(self.lines_layer2.id(), QgsSymbolLayerId("some_id", [1, 3, 5, 19])), QgsSymbolLayerReference(self.polys_layer.id(), QgsSymbolLayerId("some_other_id", [4, 5])), ]) @@ -313,6 +327,73 @@ class TestSelectiveMasking(unittest.TestCase): QgsSymbolLayerReference(self.polys_layer.id(), QgsSymbolLayerId("some other; id, lik;e, this", [4, 5])), ]) + def test_migrate_old_references(self): + """ + Since QGIS 3.30, QgsSymbolLayerReference has change its definition, so we test we can migrate + old reference to new ones + """ + + # test label mask + label_settings = self.polys_layer.labeling().settings() + fmt = label_settings.format() + # enable a mask + fmt.mask().setEnabled(True) + fmt.mask().setSize(4.0) + # and mask other symbol layers underneath + oldMaskRefs = [ + # the black part of roads + QgsSymbolLayerReference(self.lines_layer2.id(), QgsSymbolLayerId("", [1, 0])), + # the black jets + QgsSymbolLayerReference(self.points_layer.id(), QgsSymbolLayerId("B52", [0])), + QgsSymbolLayerReference(self.points_layer.id(), QgsSymbolLayerId("Jet", [0]))] + fmt.mask().setMaskedSymbolLayers(oldMaskRefs) + + label_settings.setFormat(fmt) + self.polys_layer.labeling().setSettings(label_settings) + + self.assertEqual([slRef.symbolLayerIdV2() for slRef in self.polys_layer.labeling().settings().format().mask().maskedSymbolLayers()], + ["", "", ""]) + self.assertEqual([slRef.symbolLayerId() for slRef in self.polys_layer.labeling().settings().format().mask().maskedSymbolLayers()], + [slRef.symbolLayerId() for slRef in oldMaskRefs]) + + QgsSymbolLayerUtils.fixOldSymbolLayerReferences(QgsProject.instance().mapLayers()) + + self.assertEqual([QUuid(slRef.symbolLayerIdV2()).isNull() for slRef in self.polys_layer.labeling().settings().format().mask().maskedSymbolLayers()], + [False, False, False]) + self.assertEqual([slRef.symbolLayerIdV2() for slRef in self.polys_layer.labeling().settings().format().mask().maskedSymbolLayers()], + [self.get_symbollayer(self.lines_layer2, "", [1, 0]).id(), + self.get_symbollayer(self.points_layer, "B52", [0]).id(), + self.get_symbollayer(self.points_layer, "Jet", [0]).id()]) + self.assertEqual([slRef.symbolLayerId() for slRef in self.polys_layer.labeling().settings().format().mask().maskedSymbolLayers()], + [QgsSymbolLayerId(), QgsSymbolLayerId(), QgsSymbolLayerId()]) + + # test symbol layer masks + p = QgsMarkerSymbol.createSimple({'color': '#fdbf6f', 'size': "7"}) + self.points_layer.setRenderer(QgsSingleSymbolRenderer(p)) + + circle_symbol = QgsMarkerSymbol.createSimple({'size': '10'}) + mask_layer = QgsMaskMarkerSymbolLayer() + mask_layer.setSubSymbol(circle_symbol) + oldMaskRefs = [QgsSymbolLayerReference(self.lines_layer2.id(), QgsSymbolLayerId("", [1, 0]))] + mask_layer.setMasks(oldMaskRefs) + + # add this mask layer to the point layer + self.points_layer.renderer().symbol().appendSymbolLayer(mask_layer) + + self.assertEqual([slRef.symbolLayerIdV2() for slRef in self.points_layer.renderer().symbol().symbolLayers()[1].masks()], + [""]) + self.assertEqual([slRef.symbolLayerId() for slRef in self.points_layer.renderer().symbol().symbolLayers()[1].masks()], + [slRef.symbolLayerId() for slRef in oldMaskRefs]) + + QgsSymbolLayerUtils.fixOldSymbolLayerReferences(QgsProject.instance().mapLayers()) + + self.assertEqual([QUuid(slRef.symbolLayerIdV2()).isNull() for slRef in self.points_layer.renderer().symbol().symbolLayers()[1].masks()], + [False]) + self.assertEqual([slRef.symbolLayerIdV2() for slRef in self.points_layer.renderer().symbol().symbolLayers()[1].masks()], + [self.get_symbollayer(self.lines_layer2, "", [1, 0]).id()]) + self.assertEqual([slRef.symbolLayerId() for slRef in self.points_layer.renderer().symbol().symbolLayers()[1].masks()], + [QgsSymbolLayerId()]) + def test_label_mask(self): # modify labeling settings label_settings = self.polys_layer.labeling().settings()