diff --git a/python/core/auto_generated/symbology/qgsstyle.sip.in b/python/core/auto_generated/symbology/qgsstyle.sip.in index 877b8c3ee41..9eaaabaf83e 100644 --- a/python/core/auto_generated/symbology/qgsstyle.sip.in +++ b/python/core/auto_generated/symbology/qgsstyle.sip.in @@ -96,6 +96,7 @@ Constructor for QgsStyle. TextFormatEntity, LabelSettingsEntity, LegendPatchShapeEntity, + Symbol3DEntity, }; bool addEntity( const QString &name, const QgsStyleEntityInterface *entity, bool update = false ); @@ -186,6 +187,21 @@ Returns ``True`` if the operation was successful. Adding legend patch shapes with the name of existing ones replaces them. .. versionadded:: 3.14 +%End + + bool addSymbol3D( const QString &name, QgsAbstract3DSymbol *symbol /Transfer/, bool update = false ); +%Docstring +Adds a 3d ``symbol`` with the specified ``name`` to the style. Ownership of ``symbol`` is transferred. + +If ``update`` is set to ``True``, the style database will be automatically updated with the new legend patch shape. + +Returns ``True`` if the operation was successful. + +.. note:: + + Adding 3d symbols with the name of existing ones replaces them. + +.. versionadded:: 3.16 %End int addTag( const QString &tagName ); @@ -311,6 +327,29 @@ with the specified ``name``, or QgsSymbol.Hybrid if a matching legend patch shape is not present. .. versionadded:: 3.14 +%End + + QgsAbstract3DSymbol *symbol3D( const QString &name ) const /Factory/; +%Docstring +Returns a new copy of the 3D symbol with the specified ``name``. + +.. versionadded:: 3.16 +%End + + int symbol3DCount() const; +%Docstring +Returns count of 3D symbols in the style. + +.. versionadded:: 3.16 +%End + + QString symbol3DType( const QString &name ) const; +%Docstring +Returns the symbol type corresponding to the 3d symbol +with the specified ``name``, or an empty string +if a matching 3d symbol is not present. + +.. versionadded:: 3.16 %End QgsWkbTypes::GeometryType labelSettingsLayerType( const QString &name ) const; @@ -663,6 +702,34 @@ Returns the default patch geometry for the given symbol ``type`` and ``size`` as .. seealso:: :py:func:`defaultPatch` .. versionadded:: 3.14 +%End + + bool saveSymbol3D( const QString &name, QgsAbstract3DSymbol *symbol /Transfer/, bool favorite, const QStringList &tags ); +%Docstring +Adds a 3d ``symbol`` to the database. + +:param name: is the name of the 3d symbol +:param symbol: 3d symbol to save. Ownership is transferred. +:param favorite: is a boolean value to specify whether the 3d symbol should be added to favorites +:param tags: is a list of tags that are associated with the 3d symbol + +:return: returns the success state of the save operation + +.. versionadded:: 3.16 +%End + + bool renameSymbol3D( const QString &oldName, const QString &newName ); +%Docstring +Changes a 3d symbol's name. + +.. versionadded:: 3.16 +%End + + QStringList symbol3DNames() const; +%Docstring +Returns a list of names of 3d symbols in the style. + +.. versionadded:: 3.16 %End bool createDatabase( const QString &filename ); @@ -1241,6 +1308,36 @@ Returns the entity's legend patch shape. }; +class QgsStyleSymbol3DEntity : QgsStyleEntityInterface +{ +%Docstring +A 3d symbol entity for QgsStyle databases. + +.. versionadded:: 3.16 +%End + +%TypeHeaderCode +#include "qgsstyle.h" +%End + public: + + QgsStyleSymbol3DEntity( const QgsAbstract3DSymbol *symbol ); +%Docstring +Constructor for QgsStyleSymbol3DEntity, with the specified ``symbol``. + +Ownership of ``symbol`` is NOT transferred. +%End + + virtual QgsStyle::StyleEntity type() const; + + + const QgsAbstract3DSymbol *symbol() const; +%Docstring +Returns the entity's symbol. +%End + +}; + /************************************************************************ * This file has been generated automatically from * * * diff --git a/src/core/symbology/qgsstyle.cpp b/src/core/symbology/qgsstyle.cpp index 1294b8e03d1..c5f312db004 100644 --- a/src/core/symbology/qgsstyle.cpp +++ b/src/core/symbology/qgsstyle.cpp @@ -29,6 +29,8 @@ #include "qgslinesymbollayer.h" #include "qgsfillsymbollayer.h" #include "qgsruntimeprofiler.h" +#include "qgsabstract3dsymbol.h" +#include "qgs3dsymbolregistry.h" #include #include @@ -54,6 +56,17 @@ enum LegendPatchTable LegendPatchTableFavoriteId, //!< Legend patch is favorite flag }; +/** + * Columns available in the 3d symbol table. + */ +enum Symbol3DTable +{ + Symbol3DTableId, //!< 3d symbol ID + Symbol3DTableName, //!< 3d symbol name + Symbol3DTableXML, //!< 3d symbol definition (as XML) + Symbol3DTableFavoriteId, //!< 3d symbol is favorite flag +}; + QgsStyle *QgsStyle::sDefaultStyle = nullptr; @@ -100,6 +113,9 @@ bool QgsStyle::addEntity( const QString &name, const QgsStyleEntityInterface *en case LegendPatchShapeEntity: return addLegendPatchShape( name, static_cast< const QgsStyleLegendPatchShapeEntity * >( entity )->shape(), update ); + case Symbol3DEntity: + return addSymbol3D( name, static_cast< const QgsStyleSymbol3DEntity * >( entity )->symbol()->clone(), update ); + case TagEntity: case SmartgroupEntity: break; @@ -147,10 +163,12 @@ void QgsStyle::clear() { qDeleteAll( mSymbols ); qDeleteAll( mColorRamps ); + qDeleteAll( m3dSymbols ); mSymbols.clear(); mColorRamps.clear(); mTextFormats.clear(); + m3dSymbols.clear(); mCachedTags.clear(); mCachedFavorites.clear(); @@ -238,6 +256,9 @@ bool QgsStyle::renameEntity( QgsStyle::StyleEntity type, const QString &oldName, case LegendPatchShapeEntity: return renameLegendPatchShape( oldName, newName ); + case Symbol3DEntity: + return renameSymbol3D( oldName, newName ); + case TagEntity: case SmartgroupEntity: return false; @@ -354,6 +375,27 @@ bool QgsStyle::addLegendPatchShape( const QString &name, const QgsLegendPatchSha return true; } +bool QgsStyle::addSymbol3D( const QString &name, QgsAbstract3DSymbol *symbol, bool update ) +{ + // delete previous symbol (if any) + if ( m3dSymbols.contains( name ) ) + { + // TODO remove groups and tags? + delete m3dSymbols.take( name ); + m3dSymbols.insert( name, symbol ); + if ( update ) + updateSymbol( Symbol3DEntity, name ); + } + else + { + m3dSymbols.insert( name, symbol ); + if ( update ) + saveSymbol3D( name, symbol, false, QStringList() ); + } + + return true; +} + bool QgsStyle::saveColorRamp( const QString &name, QgsColorRamp *ramp, bool favorite, const QStringList &tags ) { // insert it into the database @@ -483,6 +525,11 @@ void QgsStyle::createTables() "name TEXT UNIQUE,"\ "xml TEXT,"\ "favorite INTEGER);"\ + "CREATE TABLE symbol3d("\ + "id INTEGER PRIMARY KEY,"\ + "name TEXT UNIQUE,"\ + "xml TEXT,"\ + "favorite INTEGER);"\ "CREATE TABLE tag("\ "id INTEGER PRIMARY KEY,"\ "name TEXT);"\ @@ -501,6 +548,9 @@ void QgsStyle::createTables() "CREATE TABLE lpstagmap("\ "tag_id INTEGER NOT NULL,"\ "legendpatchshape_id INTEGER);"\ + "CREATE TABLE symbol3dtagmap("\ + "tag_id INTEGER NOT NULL,"\ + "symbol3d_id INTEGER);"\ "CREATE TABLE smartgroup("\ "id INTEGER PRIMARY KEY,"\ "name TEXT,"\ @@ -567,6 +617,21 @@ bool QgsStyle::load( const QString &filename ) "legendpatchshape_id INTEGER);" ); runEmptyQuery( query ); } + // make sure 3d symbol table exists + query = QgsSqlite3Mprintf( "SELECT name FROM sqlite_master WHERE name='symbol3d'" ); + statement = mCurrentDB.prepare( query, rc ); + if ( rc != SQLITE_OK || sqlite3_step( statement.get() ) != SQLITE_ROW ) + { + query = QgsSqlite3Mprintf( "CREATE TABLE symbol3d("\ + "id INTEGER PRIMARY KEY,"\ + "name TEXT UNIQUE,"\ + "xml TEXT,"\ + "favorite INTEGER);"\ + "CREATE TABLE symbol3dtagmap("\ + "tag_id INTEGER NOT NULL,"\ + "symbol3d_id INTEGER);" ); + runEmptyQuery( query ); + } // Make sure there are no Null fields in parenting symbols and groups query = QgsSqlite3Mprintf( "UPDATE symbol SET favorite=0 WHERE favorite IS NULL;" @@ -574,6 +639,7 @@ bool QgsStyle::load( const QString &filename ) "UPDATE textformat SET favorite=0 WHERE favorite IS NULL;" "UPDATE labelsettings SET favorite=0 WHERE favorite IS NULL;" "UPDATE legendpatchshapes SET favorite=0 WHERE favorite IS NULL;" + "UPDATE symbol3d SET favorite=0 WHERE favorite IS NULL;" ); runEmptyQuery( query ); @@ -690,6 +756,38 @@ bool QgsStyle::load( const QString &filename ) } } + { + QgsScopedRuntimeProfile profile( tr( "Load 3d symbols shapes" ) ); + query = QgsSqlite3Mprintf( "SELECT * FROM symbol3d" ); + statement = mCurrentDB.prepare( query, rc ); + while ( rc == SQLITE_OK && sqlite3_step( statement.get() ) == SQLITE_ROW ) + { + QDomDocument doc; + const QString settingsName = statement.columnAsText( Symbol3DTableName ); + QgsScopedRuntimeProfile profile( settingsName ); + const QString xmlstring = statement.columnAsText( Symbol3DTableXML ); + if ( !doc.setContent( xmlstring ) ) + { + QgsDebugMsg( "Cannot open 3d symbol " + settingsName ); + continue; + } + QDomElement settingsElement = doc.documentElement(); + + const QString symbolType = settingsElement.attribute( QStringLiteral( "type" ) ); + std::unique_ptr< QgsAbstract3DSymbol > symbol( QgsApplication::symbol3DRegistry()->createSymbol( symbolType ) ); + if ( symbol ) + { + symbol->readXml( settingsElement, QgsReadWriteContext() ); + m3dSymbols.insert( settingsName, symbol.release() ); + } + else + { + QgsDebugMsg( "Cannot open 3d symbol " + settingsName ); + continue; + } + } + } + mFileName = filename; return true; } @@ -1082,6 +1180,74 @@ QList > QgsStyle::defaultPatchAsQPolygonF( QgsSymbol::SymbolTyp return res; } +bool QgsStyle::saveSymbol3D( const QString &name, QgsAbstract3DSymbol *symbol, bool favorite, const QStringList &tags ) +{ + // insert it into the database + QDomDocument doc( QStringLiteral( "dummy" ) ); + QDomElement elem = doc.createElement( QStringLiteral( "symbol" ) ); + elem.setAttribute( QStringLiteral( "type" ), symbol->type() ); + symbol->writeXml( elem, QgsReadWriteContext() ); + + QByteArray xmlArray; + QTextStream stream( &xmlArray ); + stream.setCodec( "UTF-8" ); + elem.save( stream, 4 ); + auto query = QgsSqlite3Mprintf( "INSERT INTO symbol3d VALUES (NULL, '%q', '%q', %d);", + name.toUtf8().constData(), xmlArray.constData(), ( favorite ? 1 : 0 ) ); + if ( !runEmptyQuery( query ) ) + { + QgsDebugMsg( QStringLiteral( "Couldn't insert 3d symbol into the database!" ) ); + return false; + } + + mCachedFavorites[ Symbol3DEntity ].insert( name, favorite ); + + tagSymbol( Symbol3DEntity, name, tags ); + + emit entityAdded( Symbol3DEntity, name ); + + return true; +} + +bool QgsStyle::renameSymbol3D( const QString &oldName, const QString &newName ) +{ + if ( m3dSymbols.contains( newName ) ) + { + QgsDebugMsg( QStringLiteral( "3d symbol of new name already exists." ) ); + return false; + } + + if ( !m3dSymbols.contains( oldName ) ) + return false; + QgsAbstract3DSymbol *symbol = m3dSymbols.take( oldName ); + + m3dSymbols.insert( newName, symbol ); + mCachedTags[Symbol3DEntity ].remove( oldName ); + mCachedFavorites[ Symbol3DEntity ].remove( oldName ); + + int labelSettingsId = 0; + sqlite3_statement_unique_ptr statement; + auto query = QgsSqlite3Mprintf( "SELECT id FROM symbol3d WHERE name='%q'", oldName.toUtf8().constData() ); + int nErr; + statement = mCurrentDB.prepare( query, nErr ); + if ( nErr == SQLITE_OK && sqlite3_step( statement.get() ) == SQLITE_ROW ) + { + labelSettingsId = sqlite3_column_int( statement.get(), 0 ); + } + const bool result = rename( Symbol3DEntity, labelSettingsId, newName ); + if ( result ) + { + emit entityRenamed( Symbol3DEntity, oldName, newName ); + } + + return result; +} + +QStringList QgsStyle::symbol3DNames() const +{ + return m3dSymbols.keys(); +} + QStringList QgsStyle::symbolsOfFavorite( StyleEntity type ) const { if ( !mCurrentDB ) @@ -1300,6 +1466,15 @@ bool QgsStyle::removeEntityByName( QgsStyle::StyleEntity type, const QString &na break; } + case QgsStyle::Symbol3DEntity: + { + std::unique_ptr< QgsAbstract3DSymbol > symbol( m3dSymbols.take( name ) ); + if ( !symbol ) + return false; + + break; + } + case QgsStyle::ColorrampEntity: { std::unique_ptr< QgsColorRamp > ramp( mColorRamps.take( name ) ); @@ -1935,6 +2110,24 @@ QgsSymbol::SymbolType QgsStyle::legendPatchShapeSymbolType( const QString &name return mLegendPatchShapes.value( name ).symbolType(); } +QgsAbstract3DSymbol *QgsStyle::symbol3D( const QString &name ) const +{ + return m3dSymbols.contains( name ) ? m3dSymbols.value( name )->clone() : nullptr; +} + +int QgsStyle::symbol3DCount() const +{ + return m3dSymbols.count(); +} + +QString QgsStyle::symbol3DType( const QString &name ) const +{ + if ( !m3dSymbols.contains( name ) ) + return QString(); + + return m3dSymbols.value( name )->type(); +} + QgsWkbTypes::GeometryType QgsStyle::labelSettingsLayerType( const QString &name ) const { if ( !mLabelSettings.contains( name ) ) @@ -2011,6 +2204,9 @@ QStringList QgsStyle::allNames( QgsStyle::StyleEntity type ) const case LegendPatchShapeEntity: return legendPatchShapeNames(); + case Symbol3DEntity: + return symbol3DNames(); + case TagEntity: return tags(); @@ -2299,6 +2495,8 @@ bool QgsStyle::exportXml( const QString &filename ) const QStringList favoriteSymbols = symbolsOfFavorite( SymbolEntity ); const QStringList favoriteColorramps = symbolsOfFavorite( ColorrampEntity ); const QStringList favoriteTextFormats = symbolsOfFavorite( TextFormatEntity ); + const QStringList favoriteLegendShapes = symbolsOfFavorite( LegendPatchShapeEntity ); + const QStringList favorite3DSymbols = symbolsOfFavorite( Symbol3DEntity ); // save symbols and attach tags QDomElement symbolsElem = QgsSymbolLayerUtils::saveSymbols( mSymbols, QStringLiteral( "symbols" ), doc, QgsReadWriteContext() ); @@ -2390,18 +2588,41 @@ bool QgsStyle::exportXml( const QString &filename ) { legendPatchShapeEl.setAttribute( QStringLiteral( "tags" ), tags.join( ',' ) ); } - if ( favoriteTextFormats.contains( it.key() ) ) + if ( favoriteLegendShapes.contains( it.key() ) ) { legendPatchShapeEl.setAttribute( QStringLiteral( "favorite" ), QStringLiteral( "1" ) ); } legendPatchShapesElem.appendChild( legendPatchShapeEl ); } + // save symbols and attach tags + QDomElement symbols3DElem = doc.createElement( QStringLiteral( "symbols3d" ) ); + for ( auto it = m3dSymbols.constBegin(); it != m3dSymbols.constEnd(); ++it ) + { + QDomElement symbolEl = doc.createElement( QStringLiteral( "symbol3d" ) ); + symbolEl.setAttribute( QStringLiteral( "name" ), it.key() ); + QDomElement defEl = doc.createElement( QStringLiteral( "definition" ) ); + defEl.setAttribute( QStringLiteral( "type" ), it.value()->type() ); + it.value()->writeXml( defEl, QgsReadWriteContext() ); + symbolEl.appendChild( defEl ); + QStringList tags = tagsOfSymbol( Symbol3DEntity, it.key() ); + if ( tags.count() > 0 ) + { + symbolEl.setAttribute( QStringLiteral( "tags" ), tags.join( ',' ) ); + } + if ( favorite3DSymbols.contains( it.key() ) ) + { + symbolEl.setAttribute( QStringLiteral( "favorite" ), QStringLiteral( "1" ) ); + } + symbols3DElem.appendChild( symbolEl ); + } + root.appendChild( symbolsElem ); root.appendChild( rampsElem ); root.appendChild( textFormatsElem ); root.appendChild( labelSettingsElem ); root.appendChild( legendPatchShapesElem ); + root.appendChild( symbols3DElem ); // save QFile f( filename ); @@ -2702,6 +2923,56 @@ bool QgsStyle::importXml( const QString &filename, int sinceVersion ) } } + // load 3d symbols + if ( version == STYLE_CURRENT_VERSION ) + { + const QDomElement symbols3DElement = docEl.firstChildElement( QStringLiteral( "symbols3d" ) ); + e = symbols3DElement.firstChildElement(); + while ( !e.isNull() ) + { + const int entityAddedVersion = e.attribute( QStringLiteral( "addedVersion" ) ).toInt(); + if ( entityAddedVersion != 0 && sinceVersion != -1 && entityAddedVersion <= sinceVersion ) + { + // skip the symbol, should already be present + continue; + } + + if ( e.tagName() == QLatin1String( "symbol3d" ) ) + { + QString name = e.attribute( QStringLiteral( "name" ) ); + QStringList tags; + if ( e.hasAttribute( QStringLiteral( "tags" ) ) ) + { + tags = e.attribute( QStringLiteral( "tags" ) ).split( ',' ); + } + bool favorite = false; + if ( e.hasAttribute( QStringLiteral( "favorite" ) ) && e.attribute( QStringLiteral( "favorite" ) ) == QStringLiteral( "1" ) ) + { + favorite = true; + } + + const QDomElement symbolElem = e.firstChildElement(); + const QString type = symbolElem.attribute( QStringLiteral( "type" ) ); + std::unique_ptr< QgsAbstract3DSymbol > sym( QgsApplication::symbol3DRegistry()->createSymbol( type ) ); + if ( sym ) + { + sym->readXml( symbolElem, QgsReadWriteContext() ); + QgsAbstract3DSymbol *newSym = sym.get(); + addSymbol3D( name, sym.release() ); + if ( mCurrentDB ) + { + saveSymbol3D( name, newSym, favorite, tags ); + } + } + } + else + { + QgsDebugMsg( "unknown tag: " + e.tagName() ); + } + e = e.nextSiblingElement(); + } + } + query = QgsSqlite3Mprintf( "COMMIT TRANSACTION;" ); runEmptyQuery( query ); @@ -2762,6 +3033,29 @@ bool QgsStyle::updateSymbol( StyleEntity type, const QString &name ) break; } + case Symbol3DEntity: + { + // check if it is an existing symbol + if ( !symbol3DNames().contains( name ) ) + { + QgsDebugMsg( QStringLiteral( "Update request received for unavailable symbol" ) ); + return false; + } + + symEl = doc.createElement( QStringLiteral( "symbol" ) ); + symEl.setAttribute( QStringLiteral( "type" ), m3dSymbols.value( name )->type() ); + m3dSymbols.value( name )->writeXml( symEl, QgsReadWriteContext() ); + if ( symEl.isNull() ) + { + QgsDebugMsg( QStringLiteral( "Couldn't convert symbol to valid XML!" ) ); + return false; + } + symEl.save( stream, 4 ); + query = QgsSqlite3Mprintf( "UPDATE symbol3d SET xml='%q' WHERE name='%q';", + xmlArray.constData(), name.toUtf8().constData() ); + break; + } + case ColorrampEntity: { if ( !colorRampNames().contains( name ) ) @@ -2879,6 +3173,7 @@ bool QgsStyle::updateSymbol( StyleEntity type, const QString &name ) case LegendPatchShapeEntity: case TagEntity: case SmartgroupEntity: + case Symbol3DEntity: break; } emit entityChanged( type, name ); @@ -2953,6 +3248,9 @@ QString QgsStyle::entityTableName( QgsStyle::StyleEntity type ) case LegendPatchShapeEntity: return QStringLiteral( "legendpatchshapes" ); + case Symbol3DEntity: + return QStringLiteral( "symbol3d" ); + case TagEntity: return QStringLiteral( "tag" ); @@ -2981,6 +3279,9 @@ QString QgsStyle::tagmapTableName( QgsStyle::StyleEntity type ) case LegendPatchShapeEntity: return QStringLiteral( "lpstagmap" ); + case Symbol3DEntity: + return QStringLiteral( "symbol3dtagmap" ); + case TagEntity: case SmartgroupEntity: break; @@ -3007,6 +3308,9 @@ QString QgsStyle::tagmapEntityIdFieldName( QgsStyle::StyleEntity type ) case LegendPatchShapeEntity: return QStringLiteral( "legendpatchshape_id" ); + case Symbol3DEntity: + return QStringLiteral( "symbol3d_id" ); + case TagEntity: case SmartgroupEntity: break; @@ -3038,3 +3342,8 @@ QgsStyle::StyleEntity QgsStyleLegendPatchShapeEntity::type() const { return QgsStyle::LegendPatchShapeEntity; } + +QgsStyle::StyleEntity QgsStyleSymbol3DEntity::type() const +{ + return QgsStyle::Symbol3DEntity; +} diff --git a/src/core/symbology/qgsstyle.h b/src/core/symbology/qgsstyle.h index ce3f2405565..7fc32c80e79 100644 --- a/src/core/symbology/qgsstyle.h +++ b/src/core/symbology/qgsstyle.h @@ -184,6 +184,7 @@ class CORE_EXPORT QgsStyle : public QObject TextFormatEntity, //!< Text formats LabelSettingsEntity, //!< Label settings LegendPatchShapeEntity, //!< Legend patch shape (since QGIS 3.14) + Symbol3DEntity, //!< 3D symbol entity (since QGIS 3.14) }; /** @@ -256,6 +257,18 @@ class CORE_EXPORT QgsStyle : public QObject */ bool addLegendPatchShape( const QString &name, const QgsLegendPatchShape &shape, bool update = false ); + /** + * Adds a 3d \a symbol with the specified \a name to the style. Ownership of \a symbol is transferred. + * + * If \a update is set to TRUE, the style database will be automatically updated with the new legend patch shape. + * + * Returns TRUE if the operation was successful. + * + * \note Adding 3d symbols with the name of existing ones replaces them. + * \since QGIS 3.16 + */ + bool addSymbol3D( const QString &name, QgsAbstract3DSymbol *symbol SIP_TRANSFER, bool update = false ); + /** * Adds a new tag and returns the tag's id * @@ -378,6 +391,28 @@ class CORE_EXPORT QgsStyle : public QObject */ QgsSymbol::SymbolType legendPatchShapeSymbolType( const QString &name ) const; + /** + * Returns a new copy of the 3D symbol with the specified \a name. + * + * \since QGIS 3.16 + */ + QgsAbstract3DSymbol *symbol3D( const QString &name ) const SIP_FACTORY; + + /** + * Returns count of 3D symbols in the style. + * \since QGIS 3.16 + */ + int symbol3DCount() const; + + /** + * Returns the symbol type corresponding to the 3d symbol + * with the specified \a name, or an empty string + * if a matching 3d symbol is not present. + * + * \since QGIS 3.16 + */ + QString symbol3DType( const QString &name ) const; + /** * Returns the layer geometry type corresponding to the label settings * with the specified \a name, or QgsWkbTypes::UnknownGeometry @@ -688,6 +723,32 @@ class CORE_EXPORT QgsStyle : public QObject */ QList< QList< QPolygonF > > defaultPatchAsQPolygonF( QgsSymbol::SymbolType type, QSizeF size ) const; + /** + * Adds a 3d \a symbol to the database. + * + * \param name is the name of the 3d symbol + * \param symbol 3d symbol to save. Ownership is transferred. + * \param favorite is a boolean value to specify whether the 3d symbol should be added to favorites + * \param tags is a list of tags that are associated with the 3d symbol + * \returns returns the success state of the save operation + * + * \since QGIS 3.16 + */ + bool saveSymbol3D( const QString &name, QgsAbstract3DSymbol *symbol SIP_TRANSFER, bool favorite, const QStringList &tags ); + + /** + * Changes a 3d symbol's name. + * + * \since QGIS 3.16 + */ + bool renameSymbol3D( const QString &oldName, const QString &newName ); + + /** + * Returns a list of names of 3d symbols in the style. + * \since QGIS 3.16 + */ + QStringList symbol3DNames() const; + /** * Creates an on-disk database * @@ -1006,6 +1067,7 @@ class CORE_EXPORT QgsStyle : public QObject QgsTextFormatMap mTextFormats; QgsLabelSettingsMap mLabelSettings; QMap mLegendPatchShapes; + QMap m3dSymbols; QHash< QgsStyle::StyleEntity, QHash< QString, QStringList > > mCachedTags; QHash< QgsStyle::StyleEntity, QHash< QString, bool > > mCachedFavorites; @@ -1278,4 +1340,35 @@ class CORE_EXPORT QgsStyleLegendPatchShapeEntity : public QgsStyleEntityInterfac QgsLegendPatchShape mShape; }; +/** + * \class QgsStyleSymbol3DEntity + * \ingroup core + * A 3d symbol entity for QgsStyle databases. + * \since QGIS 3.16 + */ +class CORE_EXPORT QgsStyleSymbol3DEntity : public QgsStyleEntityInterface +{ + public: + + /** + * Constructor for QgsStyleSymbol3DEntity, with the specified \a symbol. + * + * Ownership of \a symbol is NOT transferred. + */ + QgsStyleSymbol3DEntity( const QgsAbstract3DSymbol *symbol ) + : mSymbol( symbol ) + {} + + QgsStyle::StyleEntity type() const override; + + /** + * Returns the entity's symbol. + */ + const QgsAbstract3DSymbol *symbol() const { return mSymbol; } + + private: + + const QgsAbstract3DSymbol *mSymbol = nullptr; +}; + #endif diff --git a/src/core/symbology/qgsstylemodel.cpp b/src/core/symbology/qgsstylemodel.cpp index 4d41ac6a08d..2dd06a30531 100644 --- a/src/core/symbology/qgsstylemodel.cpp +++ b/src/core/symbology/qgsstylemodel.cpp @@ -175,6 +175,7 @@ QVariant QgsStyleModel::data( const QModelIndex &index, int role ) const case QgsStyle::ColorrampEntity: case QgsStyle::TagEntity: case QgsStyle::SmartgroupEntity: + case QgsStyle::Symbol3DEntity: break; } return tooltip; @@ -317,6 +318,7 @@ QVariant QgsStyleModel::data( const QModelIndex &index, int role ) const case QgsStyle::TagEntity: case QgsStyle::SmartgroupEntity: + case QgsStyle::Symbol3DEntity: return QVariant(); } break; @@ -354,6 +356,7 @@ QVariant QgsStyleModel::data( const QModelIndex &index, int role ) const case QgsStyle::SmartgroupEntity: case QgsStyle::LabelSettingsEntity: case QgsStyle::TextFormatEntity: + case QgsStyle::Symbol3DEntity: return QVariant(); } return QVariant(); diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index e470b1a6990..b6084295931 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -1256,6 +1256,7 @@ INCLUDE_DIRECTORIES( ${CMAKE_SOURCE_DIR}/src/gui/vectortile ${CMAKE_SOURCE_DIR}/src/gui/tableeditor ${CMAKE_SOURCE_DIR}/src/core + ${CMAKE_SOURCE_DIR}/src/core/3d ${CMAKE_SOURCE_DIR}/src/core/annotations ${CMAKE_SOURCE_DIR}/src/core/auth ${CMAKE_SOURCE_DIR}/src/core/callouts diff --git a/src/gui/labeling/qgslabelinggui.cpp b/src/gui/labeling/qgslabelinggui.cpp index 80ee470ba5b..ff216acfba6 100644 --- a/src/gui/labeling/qgslabelinggui.cpp +++ b/src/gui/labeling/qgslabelinggui.cpp @@ -681,6 +681,7 @@ void QgsLabelingGui::setFormatFromStyle( const QString &name, QgsStyle::StyleEnt case QgsStyle::SmartgroupEntity: case QgsStyle::TextFormatEntity: case QgsStyle::LegendPatchShapeEntity: + case QgsStyle::Symbol3DEntity: { QgsTextFormatWidget::setFormatFromStyle( name, type ); return; @@ -781,6 +782,7 @@ void QgsLabelingGui::saveFormat() case QgsStyle::TagEntity: case QgsStyle::SmartgroupEntity: case QgsStyle::LegendPatchShapeEntity: + case QgsStyle::Symbol3DEntity: break; } } diff --git a/src/gui/qgsstyleitemslistwidget.cpp b/src/gui/qgsstyleitemslistwidget.cpp index f73398a10ad..5a67137de47 100644 --- a/src/gui/qgsstyleitemslistwidget.cpp +++ b/src/gui/qgsstyleitemslistwidget.cpp @@ -204,6 +204,13 @@ void QgsStyleItemsListWidget::setEntityType( QgsStyle::StyleEntity type ) groupsCombo->setItemText( allGroup, tr( "All Legend Patch Shapes" ) ); break; + case QgsStyle::Symbol3DEntity: + btnSaveSymbol->setText( tr( "Save 3D Symbol…" ) ); + btnSaveSymbol->setToolTip( tr( "Save 3D symbol to styles" ) ); + if ( allGroup >= 0 ) + groupsCombo->setItemText( allGroup, tr( "All 3D Symbols" ) ); + break; + case QgsStyle::TagEntity: case QgsStyle::SmartgroupEntity: break; @@ -330,6 +337,10 @@ void QgsStyleItemsListWidget::populateGroups() allText = tr( "All Legend Patch Shapes" ); break; + case QgsStyle::Symbol3DEntity: + allText = tr( "All 3D Symbols" ); + break; + case QgsStyle::TagEntity: case QgsStyle::SmartgroupEntity: break; diff --git a/src/gui/qgstextformatwidget.cpp b/src/gui/qgstextformatwidget.cpp index 24670031749..3dd5f9c2570 100644 --- a/src/gui/qgstextformatwidget.cpp +++ b/src/gui/qgstextformatwidget.cpp @@ -1814,6 +1814,7 @@ void QgsTextFormatWidget::setFormatFromStyle( const QString &name, QgsStyle::Sty case QgsStyle::TagEntity: case QgsStyle::SmartgroupEntity: case QgsStyle::LegendPatchShapeEntity: + case QgsStyle::Symbol3DEntity: return; case QgsStyle::TextFormatEntity: diff --git a/src/gui/symbology/qgsstylemanagerdialog.cpp b/src/gui/symbology/qgsstylemanagerdialog.cpp index 00b7f2a1fad..cde27b97bdc 100644 --- a/src/gui/symbology/qgsstylemanagerdialog.cpp +++ b/src/gui/symbology/qgsstylemanagerdialog.cpp @@ -34,6 +34,7 @@ #include "qgstextformatwidget.h" #include "qgslabelinggui.h" #include "qgslegendpatchshapewidget.h" +#include "qgsabstract3dsymbol.h" #include #include #include @@ -614,6 +615,7 @@ void QgsStyleManagerDialog::copyItem() case QgsStyle::ColorrampEntity: case QgsStyle::LegendPatchShapeEntity: + case QgsStyle::Symbol3DEntity: case QgsStyle::TagEntity: case QgsStyle::SmartgroupEntity: return; @@ -1014,6 +1016,59 @@ int QgsStyleManagerDialog::copyItems( const QList symbol( src->symbol3D( details.name ) ); + if ( !symbol ) + continue; + + const bool hasDuplicateName = dst->symbol3DNames().contains( details.name ); + bool overwriteThis = false; + if ( isImport ) + addItemToFavorites = favoriteSymbols.contains( details.name ); + + if ( hasDuplicateName && prompt ) + { + cursorOverride.reset(); + int res = QMessageBox::warning( parentWidget, isImport ? tr( "Import 3D Symbol" ) : tr( "Export 3D Symbol" ), + tr( "A 3D symbol with the name “%1” already exists.\nOverwrite?" ) + .arg( details.name ), + QMessageBox::Yes | QMessageBox::YesToAll | QMessageBox::No | QMessageBox::NoToAll | QMessageBox::Cancel ); + cursorOverride = qgis::make_unique< QgsTemporaryCursorOverride >( Qt::WaitCursor ); + switch ( res ) + { + case QMessageBox::Cancel: + return count; + + case QMessageBox::No: + continue; + + case QMessageBox::Yes: + overwriteThis = true; + break; + + case QMessageBox::YesToAll: + prompt = false; + overwriteAll = true; + break; + + case QMessageBox::NoToAll: + prompt = false; + overwriteAll = false; + break; + } + } + + if ( !hasDuplicateName || overwriteAll || overwriteThis ) + { + QgsAbstract3DSymbol *newSymbol = symbol.get(); + dst->addSymbol3D( details.name, symbol.release() ); + dst->saveSymbol3D( details.name, newSymbol, addItemToFavorites, symbolTags ); + count++; + } + break; + } + case QgsStyle::TagEntity: case QgsStyle::SmartgroupEntity: break; diff --git a/src/gui/symbology/qgsstylesavedialog.cpp b/src/gui/symbology/qgsstylesavedialog.cpp index 4d853f27e30..ca1b0b2c43b 100644 --- a/src/gui/symbology/qgsstylesavedialog.cpp +++ b/src/gui/symbology/qgsstylesavedialog.cpp @@ -65,6 +65,11 @@ QgsStyleSaveDialog::QgsStyleSaveDialog( QWidget *parent, QgsStyle::StyleEntity t possibleEntities << QgsStyle::LegendPatchShapeEntity; break; + case QgsStyle::Symbol3DEntity: + this->setWindowTitle( tr( "Save New 3D Symbol" ) ); + possibleEntities << QgsStyle::Symbol3DEntity; + break; + case QgsStyle::TagEntity: case QgsStyle::SmartgroupEntity: break; @@ -101,6 +106,10 @@ QgsStyleSaveDialog::QgsStyleSaveDialog( QWidget *parent, QgsStyle::StyleEntity t mComboSaveAs->addItem( QgsApplication::getThemeIcon( QStringLiteral( "legend.svg" ) ), tr( "Legend Patch Shape" ), e ); break; + case QgsStyle::Symbol3DEntity: + mComboSaveAs->addItem( QgsApplication::getThemeIcon( QStringLiteral( "3d.svg" ) ), tr( "3D Symbol" ), e ); + break; + case QgsStyle::TagEntity: case QgsStyle::SmartgroupEntity: break; diff --git a/tests/src/core/CMakeLists.txt b/tests/src/core/CMakeLists.txt index 6ae35c6ee5c..6ddf382fd11 100644 --- a/tests/src/core/CMakeLists.txt +++ b/tests/src/core/CMakeLists.txt @@ -7,6 +7,7 @@ INCLUDE_DIRECTORIES(${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_SOURCE_DIR}/external/kdbush/include ${CMAKE_SOURCE_DIR}/external/nlohmann ${CMAKE_SOURCE_DIR}/src/core + ${CMAKE_SOURCE_DIR}/src/core/3d ${CMAKE_SOURCE_DIR}/src/core/annotations ${CMAKE_SOURCE_DIR}/src/core/auth ${CMAKE_SOURCE_DIR}/src/core/callouts diff --git a/tests/src/core/testqgsstyle.cpp b/tests/src/core/testqgsstyle.cpp index ac79250aab5..2b001483722 100644 --- a/tests/src/core/testqgsstyle.cpp +++ b/tests/src/core/testqgsstyle.cpp @@ -50,6 +50,8 @@ #include "qgslayertreelayer.h" #include "qgslayertreeutils.h" #include "qgsmaplayerlegend.h" +#include "qgsabstract3dsymbol.h" +#include "qgs3dsymbolregistry.h" /** * \ingroup UnitTests @@ -86,6 +88,7 @@ class TestStyle : public QObject void testCreateTextFormats(); void testCreateLabelSettings(); void testCreateLegendPatchShapes(); + void testCreate3dSymbol(); void testLoadColorRamps(); void testSaveLoad(); void testFavorites(); @@ -96,6 +99,21 @@ class TestStyle : public QObject }; + +class Dummy3DSymbol : public QgsAbstract3DSymbol +{ + public: + static QgsAbstract3DSymbol *create() { return new Dummy3DSymbol; } + QString type() const override { return QStringLiteral( "dummy" ); } + QgsAbstract3DSymbol *clone() const override { Dummy3DSymbol *res = new Dummy3DSymbol(); res->id = id; return res; } + void readXml( const QDomElement &elem, const QgsReadWriteContext & ) override { id = elem.attribute( QStringLiteral( "id" ) ); } + void writeXml( QDomElement &elem, const QgsReadWriteContext & ) const override { elem.setAttribute( QStringLiteral( "id" ), id ); } + + QString id; + +}; + + TestStyle::TestStyle() = default; // slots @@ -123,6 +141,9 @@ void TestStyle::initTestCase() QgsCptCityArchive::initArchives(); mReport += QLatin1String( "

Style Tests

\n" ); + + QgsApplication::symbol3DRegistry()->addSymbolType( new Qgs3DSymbolMetadata( QStringLiteral( "dummy" ), QObject::tr( "Dummy" ), + &Dummy3DSymbol::create, nullptr, nullptr ) ); } void TestStyle::cleanupTestCase() @@ -417,6 +438,68 @@ void TestStyle::testCreateLegendPatchShapes() QVERIFY( mStyle->legendPatchShapeNames().contains( QStringLiteral( "test_settings2" ) ) ); } +void TestStyle::testCreate3dSymbol() +{ + QVERIFY( mStyle->symbol3DNames().isEmpty() ); + QCOMPARE( mStyle->symbol3DCount(), 0 ); + // non existent settings, should be default + QVERIFY( !mStyle->symbol3D( QString( "blah" ) ) ); + + QSignalSpy spy( mStyle, &QgsStyle::entityAdded ); + QSignalSpy spyChanged( mStyle, &QgsStyle::entityChanged ); + // add symbol + Dummy3DSymbol symbol; + symbol.id = QStringLiteral( "xxx" ); + QVERIFY( mStyle->addSymbol3D( "test_settings", symbol.clone(), true ) ); + QCOMPARE( spy.count(), 1 ); + QCOMPARE( spyChanged.count(), 0 ); + + QVERIFY( mStyle->symbol3DNames().contains( QStringLiteral( "test_settings" ) ) ); + QCOMPARE( mStyle->symbol3DCount(), 1 ); + std::unique_ptr< Dummy3DSymbol > retrieved( dynamic_cast< Dummy3DSymbol * >( mStyle->symbol3D( QStringLiteral( "test_settings" ) ) ) ); + QCOMPARE( retrieved->id, QStringLiteral( "xxx" ) ); + symbol.id = QStringLiteral( "yyy" ); + QVERIFY( mStyle->addSymbol3D( "test_settings", symbol.clone(), true ) ); + QVERIFY( mStyle->symbol3DNames().contains( QStringLiteral( "test_settings" ) ) ); + QCOMPARE( mStyle->symbol3DCount(), 1 ); + retrieved.reset( dynamic_cast< Dummy3DSymbol * >( mStyle->symbol3D( QStringLiteral( "test_settings" ) ) ) ); + QCOMPARE( retrieved->id, QStringLiteral( "yyy" ) ); + QCOMPARE( spy.count(), 1 ); + QCOMPARE( spyChanged.count(), 1 ); + + symbol.id = QStringLiteral( "zzz" ); + QVERIFY( mStyle->addSymbol3D( "test_format2", symbol.clone(), true ) ); + QVERIFY( mStyle->symbol3DNames().contains( QStringLiteral( "test_format2" ) ) ); + QCOMPARE( mStyle->symbol3DCount(), 2 ); + retrieved.reset( dynamic_cast< Dummy3DSymbol * >( mStyle->symbol3D( QStringLiteral( "test_settings" ) ) ) ); + QCOMPARE( retrieved->id, QStringLiteral( "yyy" ) ); + retrieved.reset( dynamic_cast< Dummy3DSymbol * >( mStyle->symbol3D( QStringLiteral( "test_format2" ) ) ) ); + QCOMPARE( retrieved->id, QStringLiteral( "zzz" ) ); + QCOMPARE( spy.count(), 2 ); + QCOMPARE( spyChanged.count(), 1 ); + + // save and restore + QVERIFY( mStyle->exportXml( QDir::tempPath() + "/text_style.xml" ) ); + + QgsStyle style2; + QVERIFY( style2.importXml( QDir::tempPath() + "/text_style.xml" ) ); + + QVERIFY( style2.symbol3DNames().contains( QStringLiteral( "test_settings" ) ) ); + QVERIFY( style2.symbol3DNames().contains( QStringLiteral( "test_format2" ) ) ); + QCOMPARE( style2.symbol3DCount(), 2 ); + retrieved.reset( dynamic_cast< Dummy3DSymbol * >( style2.symbol3D( QStringLiteral( "test_settings" ) ) ) ); + QCOMPARE( retrieved->id, QStringLiteral( "yyy" ) ); + retrieved.reset( dynamic_cast< Dummy3DSymbol * >( style2.symbol3D( QStringLiteral( "test_format2" ) ) ) ); + QCOMPARE( retrieved->id, QStringLiteral( "zzz" ) ); + + QCOMPARE( mStyle->allNames( QgsStyle::Symbol3DEntity ), QStringList() << QStringLiteral( "test_format2" ) + << QStringLiteral( "test_settings" ) ); + + QgsStyleSymbol3DEntity entity( &symbol ); + QVERIFY( mStyle->addEntity( "test_settings2", &entity, true ) ); + QVERIFY( mStyle->symbol3DNames().contains( QStringLiteral( "test_settings2" ) ) ); +} + void TestStyle::testLoadColorRamps() { QStringList colorRamps = mStyle->colorRampNames(); @@ -499,6 +582,7 @@ void TestStyle::testFavorites() QVERIFY( !mStyle->isFavorite( QgsStyle::LabelSettingsEntity, QStringLiteral( "AaaaaaaaaA" ) ) ); QVERIFY( !mStyle->isFavorite( QgsStyle::ColorrampEntity, QStringLiteral( "AaaaaaaaaA" ) ) ); QVERIFY( !mStyle->isFavorite( QgsStyle::LegendPatchShapeEntity, QStringLiteral( "AaaaaaaaaA" ) ) ); + QVERIFY( !mStyle->isFavorite( QgsStyle::Symbol3DEntity, QStringLiteral( "AaaaaaaaaA" ) ) ); // add some symbols to favorites std::unique_ptr< QgsMarkerSymbol > sym1( QgsMarkerSymbol::createSimple( QgsStringMap() ) ); @@ -652,6 +736,32 @@ void TestStyle::testFavorites() favorites = mStyle->symbolsOfFavorite( QgsStyle::LegendPatchShapeEntity ); QCOMPARE( favorites.count(), 0 ); QVERIFY( !mStyle->isFavorite( QgsStyle::LegendPatchShapeEntity, QStringLiteral( "settings_1" ) ) ); + + // symbol 3d + Dummy3DSymbol symbol3d1; + QVERIFY( mStyle->addSymbol3D( QStringLiteral( "settings_1" ), symbol3d1.clone(), true ) ); + favorites = mStyle->symbolsOfFavorite( QgsStyle::Symbol3DEntity ); + QCOMPARE( favorites.count(), 0 ); + QVERIFY( !mStyle->isFavorite( QgsStyle::Symbol3DEntity, QStringLiteral( "settings_1" ) ) ); + + mStyle->addFavorite( QgsStyle::Symbol3DEntity, QStringLiteral( "settings_1" ) ); + QCOMPARE( favoriteChangedSpy.count(), 11 ); + QCOMPARE( favoriteChangedSpy.at( 10 ).at( 0 ).toInt(), static_cast< int >( QgsStyle::Symbol3DEntity ) ); + QCOMPARE( favoriteChangedSpy.at( 10 ).at( 1 ).toString(), QStringLiteral( "settings_1" ) ); + QCOMPARE( favoriteChangedSpy.at( 10 ).at( 2 ).toBool(), true ); + favorites = mStyle->symbolsOfFavorite( QgsStyle::Symbol3DEntity ); + QCOMPARE( favorites.count(), 1 ); + QVERIFY( favorites.contains( QStringLiteral( "settings_1" ) ) ); + QVERIFY( mStyle->isFavorite( QgsStyle::Symbol3DEntity, QStringLiteral( "settings_1" ) ) ); + + mStyle->removeFavorite( QgsStyle::Symbol3DEntity, QStringLiteral( "settings_1" ) ); + QCOMPARE( favoriteChangedSpy.count(), 12 ); + QCOMPARE( favoriteChangedSpy.at( 11 ).at( 0 ).toInt(), static_cast< int >( QgsStyle::Symbol3DEntity ) ); + QCOMPARE( favoriteChangedSpy.at( 11 ).at( 1 ).toString(), QStringLiteral( "settings_1" ) ); + QCOMPARE( favoriteChangedSpy.at( 11 ).at( 2 ).toBool(), false ); + favorites = mStyle->symbolsOfFavorite( QgsStyle::Symbol3DEntity ); + QCOMPARE( favorites.count(), 0 ); + QVERIFY( !mStyle->isFavorite( QgsStyle::Symbol3DEntity, QStringLiteral( "settings_1" ) ) ); } void TestStyle::testTags() @@ -1090,6 +1200,69 @@ void TestStyle::testTags() QCOMPARE( tagsChangedSpy.at( 34 ).at( 0 ).toInt(), static_cast< int >( QgsStyle::LegendPatchShapeEntity ) ); QCOMPARE( tagsChangedSpy.at( 34 ).at( 1 ).toString(), QStringLiteral( "shape1" ) ); QCOMPARE( tagsChangedSpy.at( 34 ).at( 2 ).toStringList(), QStringList() ); + + + // 3d symbols + // tag format + Dummy3DSymbol symbol3d1; + QVERIFY( mStyle->addSymbol3D( "3dsymbol1", symbol3d1.clone(), true ) ); + Dummy3DSymbol symbol3d2; + QVERIFY( mStyle->addSymbol3D( "3dsymbol2", symbol3d2.clone(), true ) ); + + QVERIFY( mStyle->tagSymbol( QgsStyle::Symbol3DEntity, "3dsymbol1", QStringList() << "blue" << "starry" ) ); + QCOMPARE( tagsChangedSpy.count(), 38 ); + QCOMPARE( tagsChangedSpy.at( 37 ).at( 0 ).toInt(), static_cast< int>( QgsStyle::Symbol3DEntity ) ); + QCOMPARE( tagsChangedSpy.at( 37 ).at( 1 ).toString(), QStringLiteral( "3dsymbol1" ) ); + QCOMPARE( tagsChangedSpy.at( 37 ).at( 2 ).toStringList(), QStringList() << QStringLiteral( "blue" ) << QStringLiteral( "starry" ) ); + + QVERIFY( mStyle->tagSymbol( QgsStyle::Symbol3DEntity, "3dsymbol2", QStringList() << "red" << "circle" ) ); + QCOMPARE( tagsChangedSpy.count(), 39 ); + QCOMPARE( tagsChangedSpy.at( 38 ).at( 0 ).toInt(), static_cast< int>( QgsStyle::Symbol3DEntity ) ); + QCOMPARE( tagsChangedSpy.at( 38 ).at( 1 ).toString(), QStringLiteral( "3dsymbol2" ) ); + QCOMPARE( tagsChangedSpy.at( 38 ).at( 2 ).toStringList(), QStringList() << QStringLiteral( "red" ) << QStringLiteral( "circle" ) ); + + //bad format name + QVERIFY( !mStyle->tagSymbol( QgsStyle::Symbol3DEntity, "no patch", QStringList() << "red" << "circle" ) ); + QCOMPARE( tagsChangedSpy.count(), 39 ); + //tag which hasn't been added yet + QVERIFY( mStyle->tagSymbol( QgsStyle::Symbol3DEntity, "3dsymbol2", QStringList() << "red patch" ) ); + QCOMPARE( tagsChangedSpy.count(), 40 ); + QCOMPARE( tagsChangedSpy.at( 39 ).at( 0 ).toInt(), static_cast< int>( QgsStyle::Symbol3DEntity ) ); + QCOMPARE( tagsChangedSpy.at( 39 ).at( 1 ).toString(), QStringLiteral( "3dsymbol2" ) ); + QCOMPARE( tagsChangedSpy.at( 39 ).at( 2 ).toStringList(), QStringList() << QStringLiteral( "red" ) << QStringLiteral( "circle" ) << QStringLiteral( "red patch" ) ); + + tags = mStyle->tags(); + QVERIFY( tags.contains( QStringLiteral( "red patch" ) ) ); + + //check that tags have been applied + tags = mStyle->tagsOfSymbol( QgsStyle::Symbol3DEntity, QStringLiteral( "3dsymbol1" ) ); + QCOMPARE( tags.count(), 2 ); + QVERIFY( tags.contains( "blue" ) ); + QVERIFY( tags.contains( "starry" ) ); + tags = mStyle->tagsOfSymbol( QgsStyle::Symbol3DEntity, QStringLiteral( "3dsymbol2" ) ); + QCOMPARE( tags.count(), 3 ); + QVERIFY( tags.contains( "red" ) ); + QVERIFY( tags.contains( "circle" ) ); + QVERIFY( tags.contains( "red patch" ) ); + + //remove a tag, including a non-present tag + QVERIFY( mStyle->detagSymbol( QgsStyle::Symbol3DEntity, "3dsymbol1", QStringList() << "bad" << "blue" ) ); + tags = mStyle->tagsOfSymbol( QgsStyle::Symbol3DEntity, QStringLiteral( "3dsymbol1" ) ); + QCOMPARE( tags.count(), 1 ); + QVERIFY( tags.contains( "starry" ) ); + QCOMPARE( tagsChangedSpy.count(), 41 ); + QCOMPARE( tagsChangedSpy.at( 40 ).at( 0 ).toInt(), static_cast< int >( QgsStyle::Symbol3DEntity ) ); + QCOMPARE( tagsChangedSpy.at( 40 ).at( 1 ).toString(), QStringLiteral( "3dsymbol1" ) ); + QCOMPARE( tagsChangedSpy.at( 40 ).at( 2 ).toStringList(), QStringList() << QStringLiteral( "starry" ) ); + + // completely detag symbol + QVERIFY( mStyle->detagSymbol( QgsStyle::Symbol3DEntity, QStringLiteral( "3dsymbol1" ) ) ); + tags = mStyle->tagsOfSymbol( QgsStyle::Symbol3DEntity, QStringLiteral( "3dsymbol1" ) ); + QCOMPARE( tags.count(), 0 ); + QCOMPARE( tagsChangedSpy.count(), 42 ); + QCOMPARE( tagsChangedSpy.at( 41 ).at( 0 ).toInt(), static_cast< int >( QgsStyle::Symbol3DEntity ) ); + QCOMPARE( tagsChangedSpy.at( 41 ).at( 1 ).toString(), QStringLiteral( "3dsymbol1" ) ); + QCOMPARE( tagsChangedSpy.at( 41 ).at( 2 ).toStringList(), QStringList() ); } void TestStyle::testSmartGroup() @@ -1125,6 +1298,11 @@ void TestStyle::testSmartGroup() QgsLegendPatchShape shape2; QVERIFY( style.addLegendPatchShape( "different shp bbb", shape2, true ) ); + Dummy3DSymbol symbol3d1; + QVERIFY( style.addSymbol3D( "symbol3D a", symbol3d1.clone(), true ) ); + Dummy3DSymbol symbol3d2; + QVERIFY( style.addSymbol3D( "different symbol3D bbb", symbol3d2.clone(), true ) ); + QVERIFY( style.smartgroupNames().empty() ); QVERIFY( style.smartgroup( 5 ).isEmpty() ); QCOMPARE( style.smartgroupId( QStringLiteral( "no exist" ) ), 0 ); @@ -1144,6 +1322,7 @@ void TestStyle::testSmartGroup() QCOMPARE( style.symbolsOfSmartgroup( QgsStyle::TextFormatEntity, 1 ), QStringList() << QStringLiteral( "format a" ) ); QCOMPARE( style.symbolsOfSmartgroup( QgsStyle::LabelSettingsEntity, 1 ), QStringList() << QStringLiteral( "settings a" ) ); QCOMPARE( style.symbolsOfSmartgroup( QgsStyle::LegendPatchShapeEntity, 1 ), QStringList() << QStringLiteral( "shp a" ) ); + QCOMPARE( style.symbolsOfSmartgroup( QgsStyle::Symbol3DEntity, 1 ), QStringList() << QStringLiteral( "symbol3D a" ) ); res = style.addSmartgroup( QStringLiteral( "tag" ), QStringLiteral( "OR" ), QStringList(), QStringList(), QStringList() << "c", QStringList() << "a" ); QCOMPARE( res, 2 ); @@ -1158,6 +1337,7 @@ void TestStyle::testSmartGroup() QCOMPARE( style.symbolsOfSmartgroup( QgsStyle::TextFormatEntity, 2 ), QStringList() << QStringLiteral( "different text bbb" ) ); QCOMPARE( style.symbolsOfSmartgroup( QgsStyle::LabelSettingsEntity, 2 ), QStringList() << QStringLiteral( "different l bbb" ) ); QCOMPARE( style.symbolsOfSmartgroup( QgsStyle::LegendPatchShapeEntity, 2 ), QStringList() << QStringLiteral( "different shp bbb" ) ); + QCOMPARE( style.symbolsOfSmartgroup( QgsStyle::Symbol3DEntity, 2 ), QStringList() << QStringLiteral( "different symbol3D bbb" ) ); // tag some symbols style.tagSymbol( QgsStyle::SymbolEntity, "symbolA", QStringList() << "red" << "blue" ); @@ -1170,6 +1350,8 @@ void TestStyle::testSmartGroup() style.tagSymbol( QgsStyle::LabelSettingsEntity, "different l bbb", QStringList() << "blue" << "red" ); style.tagSymbol( QgsStyle::LegendPatchShapeEntity, "shp a", QStringList() << "blue" ); style.tagSymbol( QgsStyle::LegendPatchShapeEntity, "different shp bbb", QStringList() << "blue" << "red" ); + style.tagSymbol( QgsStyle::Symbol3DEntity, "symbol3D a", QStringList() << "blue" ); + style.tagSymbol( QgsStyle::Symbol3DEntity, "different symbol3D bbb", QStringList() << "blue" << "red" ); // adding tags modifies groups! QCOMPARE( groupModifiedSpy.count(), 4 ); @@ -1187,6 +1369,7 @@ void TestStyle::testSmartGroup() QCOMPARE( style.symbolsOfSmartgroup( QgsStyle::TextFormatEntity, 3 ), QStringList() << QStringLiteral( "format a" ) ); QCOMPARE( style.symbolsOfSmartgroup( QgsStyle::LabelSettingsEntity, 3 ), QStringList() << QStringLiteral( "settings a" ) ); QCOMPARE( style.symbolsOfSmartgroup( QgsStyle::LegendPatchShapeEntity, 3 ), QStringList() << QStringLiteral( "shp a" ) ); + QCOMPARE( style.symbolsOfSmartgroup( QgsStyle::Symbol3DEntity, 3 ), QStringList() << QStringLiteral( "symbol3D a" ) ); res = style.addSmartgroup( QStringLiteral( "combined" ), QStringLiteral( "AND" ), QStringList() << "blue", QStringList(), QStringList(), QStringList() << "a" ); QCOMPARE( res, 4 ); @@ -1201,6 +1384,7 @@ void TestStyle::testSmartGroup() QCOMPARE( style.symbolsOfSmartgroup( QgsStyle::TextFormatEntity, 4 ), QStringList() << QStringLiteral( "different text bbb" ) ); QCOMPARE( style.symbolsOfSmartgroup( QgsStyle::LabelSettingsEntity, 4 ), QStringList() << QStringLiteral( "different l bbb" ) ); QCOMPARE( style.symbolsOfSmartgroup( QgsStyle::LegendPatchShapeEntity, 4 ), QStringList() << QStringLiteral( "different shp bbb" ) ); + QCOMPARE( style.symbolsOfSmartgroup( QgsStyle::Symbol3DEntity, 4 ), QStringList() << QStringLiteral( "different symbol3D bbb" ) ); style.remove( QgsStyle::SmartgroupEntity, 1 ); QCOMPARE( style.smartgroupNames(), QStringList() << QStringLiteral( "tag" ) << QStringLiteral( "tags" ) << QStringLiteral( "combined" ) ); @@ -1265,6 +1449,10 @@ class TestVisitor : public QgsStyleEntityVisitorInterface mFound << QStringLiteral( "patch: %1 %2 %3" ).arg( entity.description, entity.identifier, static_cast< const QgsStyleLegendPatchShapeEntity * >( entity.entity )->shape().geometry().asWkt() ); break; + case QgsStyle::Symbol3DEntity: + mFound << QStringLiteral( "symbol 3d: %1 %2 %3" ).arg( entity.description, entity.identifier, static_cast< const QgsStyleSymbol3DEntity * >( entity.entity )->symbol()->type() ); + break; + case QgsStyle::TagEntity: case QgsStyle::SmartgroupEntity: break;