Skip to content

Commit

Permalink
allow to save/read enum/flags as map layer properties (#44329)
Browse files Browse the repository at this point in the history
  • Loading branch information
3nids committed Jul 23, 2021
1 parent b031910 commit eaa4b54
Show file tree
Hide file tree
Showing 3 changed files with 219 additions and 0 deletions.
2 changes: 2 additions & 0 deletions python/core/auto_generated/qgsmaplayer.sip.in
Expand Up @@ -696,6 +696,8 @@ Read all custom properties from layer. Properties are stored in a map and saved
.. versionadded:: 3.14
%End



void removeCustomProperty( const QString &key );
%Docstring
Remove a custom property from layer. Properties are stored in a map and saved in project file.
Expand Down
160 changes: 160 additions & 0 deletions src/core/qgsmaplayer.h
Expand Up @@ -40,6 +40,7 @@
#include "qgsreadwritecontext.h"
#include "qgsdataprovider.h"
#include "qgis.h"
#include "qgslogger.h"

class QgsAbstract3DRenderer;
class QgsDataProvider;
Expand Down Expand Up @@ -675,6 +676,165 @@ class CORE_EXPORT QgsMapLayer : public QObject
*/
const QgsObjectCustomProperties &customProperties() const;

#ifndef SIP_RUN

/**
* Returns the property value for a property based on an enum.
* This forces the output to be a valid and existing entry of the enum.
* Hence if the property value is incorrect, the given default value is returned.
* This tries first with property as a string (as the enum) and then as an integer value.
* \note The enum needs to be declared with Q_ENUM, and flags with Q_FLAG (not Q_FLAGS).
* \see setCustomEnumProperty
* \see customFlagProperty
* \since QGIS 3.22
*/
template <class T>
T customEnumProperty( const QString &key, const T &defaultValue )
{
QMetaEnum metaEnum = QMetaEnum::fromType<T>();
Q_ASSERT( metaEnum.isValid() );
if ( !metaEnum.isValid() )
{
QgsDebugMsg( QStringLiteral( "Invalid metaenum. Enum probably misses Q_ENUM or Q_FLAG declaration." ) );
}

T v;
bool ok = false;

if ( metaEnum.isValid() )
{
// read as string
QByteArray ba = customProperty( key, metaEnum.valueToKey( static_cast<int>( defaultValue ) ) ).toString().toUtf8();
const char *vs = ba.data();
v = static_cast<T>( metaEnum.keyToValue( vs, &ok ) );
if ( ok )
return v;
}

// if failed, try to read as int (old behavior)
// this code shall be removed later
// then the method could be marked as const
v = static_cast<T>( customProperty( key, static_cast<int>( defaultValue ) ).toInt( &ok ) );
if ( metaEnum.isValid() )
{
if ( !ok || !metaEnum.valueToKey( static_cast<int>( v ) ) )
{
v = defaultValue;
}
else
{
// found property as an integer
// convert the property to the new form (string)
setCustomEnumProperty( key, v );
}
}

return v;
}

/**
* Set the value of a property based on an enum.
* The property will be saved as string.
* \note The enum needs to be declared with Q_ENUM, and flags with Q_FLAG (not Q_FLAGS).
* \see customEnumProperty
* \see setCustomFlagProperty
* \since QGIS 3.22
*/
template <class T>
void setCustomEnumProperty( const QString &key, const T &value )
{
QMetaEnum metaEnum = QMetaEnum::fromType<T>();
Q_ASSERT( metaEnum.isValid() );
if ( metaEnum.isValid() )
{
setCustomProperty( key, metaEnum.valueToKey( static_cast<int>( value ) ) );
}
else
{
QgsDebugMsg( QStringLiteral( "Invalid metaenum. Enum probably misses Q_ENUM or Q_FLAG declaration." ) );
}
}

/**
* Returns the property value for a property based on a flag.
* This forces the output to be a valid and existing entry of the flag.
* Hence if the property value is incorrect, the given default value is returned.
* This tries first with property as a string (using a byte array) and then as an integer value.
* \note The flag needs to be declared with Q_FLAG (not Q_FLAGS).
* \note for Python bindings, a custom implementation is achieved in Python directly.
* \see setCustomFlagProperty
* \see customEnumProperty
* \since QGIS 3.22
*/
template <class T>
T customFlagProperty( const QString &key, const T &defaultValue )
{
QMetaEnum metaEnum = QMetaEnum::fromType<T>();
Q_ASSERT( metaEnum.isValid() );
if ( !metaEnum.isValid() )
{
QgsDebugMsg( QStringLiteral( "Invalid metaenum. Enum probably misses Q_ENUM or Q_FLAG declaration." ) );
}

T v = defaultValue;
bool ok = false;

if ( metaEnum.isValid() )
{
// read as string
QByteArray ba = customProperty( key, metaEnum.valueToKeys( defaultValue ) ).toString().toUtf8();
const char *vs = ba.data();
v = static_cast<T>( metaEnum.keysToValue( vs, &ok ) );
}
if ( !ok )
{
// if failed, try to read as int (old behavior)
// this code shall be removed later
// then the method could be marked as const
v = T( customProperty( key, static_cast<int>( defaultValue ) ).toInt( &ok ) );
if ( metaEnum.isValid() )
{
if ( !ok || metaEnum.valueToKeys( static_cast<int>( v ) ).isEmpty() )
{
v = defaultValue;
}
else
{
// found property as an integer
// convert the property to the new form (string)
setCustomFlagProperty( key, v );
}
}
}

return v;
}

/**
* Set the value of a property based on a flag.
* The property will be saved as string.
* \note The flag needs to be declared with Q_FLAG (not Q_FLAGS).
* \see customFlagProperty
* \see customEnumProperty
* \since QGIS 3.22
*/
template <class T>
void setCustomFlagProperty( const QString &key, const T &value )
{
QMetaEnum metaEnum = QMetaEnum::fromType<T>();
Q_ASSERT( metaEnum.isValid() );
if ( metaEnum.isValid() )
{
setCustomProperty( key, metaEnum.valueToKeys( value ) );
}
else
{
QgsDebugMsg( QStringLiteral( "Invalid metaenum. Enum probably misses Q_ENUM or Q_FLAG declaration." ) );
}
}
#endif


/**
* Remove a custom property from layer. Properties are stored in a map and saved in project file.
* \see setCustomProperty()
Expand Down
57 changes: 57 additions & 0 deletions tests/src/core/testqgsmaplayer.cpp
Expand Up @@ -28,6 +28,7 @@
#include <qgsproviderregistry.h>
#include "qgsvectorlayerref.h"
#include "qgsmaplayerlistutils.h"
#include "qgsmaplayerproxymodel.h"

/**
* \ingroup UnitTests
Expand Down Expand Up @@ -64,6 +65,8 @@ class TestQgsMapLayer : public QObject

void notify();

void customEnumFlagProperties();

private:
QgsVectorLayer *mpLayer = nullptr;
};
Expand Down Expand Up @@ -387,5 +390,59 @@ void TestQgsMapLayer::notify()
QCOMPARE( spyDataChanged.count(), 2 );
}

void TestQgsMapLayer::customEnumFlagProperties()
{
std::unique_ptr<QgsVectorLayer> ml = std::make_unique<QgsVectorLayer>( QStringLiteral( "Point" ), QStringLiteral( "name" ), QStringLiteral( "memory" ) );

// assign to inexisting property
ml->setCustomProperty( QStringLiteral( "my_property_for_units" ), -1 );
ml->setCustomProperty( QStringLiteral( "my_property_for_units_as_string" ), QStringLiteral( "myString" ) );
// just to be sure it really doesn't exist
QVERIFY( static_cast<int>( QgsUnitTypes::LayoutMeters ) != -1 );

// standard method returns invalid property
int v1 = ml->customProperty( QStringLiteral( "my_property_for_units" ), QgsUnitTypes::LayoutMeters ).toInt();
QCOMPARE( v1, -1 );

// enum method returns default property if current property is incorrect
QgsUnitTypes::LayoutUnit v2 = ml->customEnumProperty( QStringLiteral( "my_property_for_units" ), QgsUnitTypes::LayoutMeters );
QCOMPARE( v2, QgsUnitTypes::LayoutMeters );
QgsUnitTypes::LayoutUnit v2s = ml->customEnumProperty( QStringLiteral( "my_property_for_units_as_string" ), QgsUnitTypes::LayoutMeters );
QCOMPARE( v2s, QgsUnitTypes::LayoutMeters );

// test a different property than default
ml->setCustomEnumProperty( QStringLiteral( "my_property_for_units" ), QgsUnitTypes::LayoutCentimeters );
QgsUnitTypes::LayoutUnit v3 = ml->customEnumProperty( QStringLiteral( "my_property_for_units" ), QgsUnitTypes::LayoutMeters );
QCOMPARE( v3, QgsUnitTypes::LayoutCentimeters );
ml->setCustomEnumProperty( QStringLiteral( "my_property_for_units" ), QgsUnitTypes::LayoutCentimeters );
// auto conversion of old ml (int to str)
QCOMPARE( ml->customProperty( "my_property_for_units" ).toString(), QStringLiteral( "LayoutCentimeters" ) );
QgsUnitTypes::LayoutUnit v3s = ml->customEnumProperty( QStringLiteral( "my_property_for_units" ), QgsUnitTypes::LayoutMeters );
QCOMPARE( v3s, QgsUnitTypes::LayoutCentimeters );
QString v3ss = ml->customProperty( QStringLiteral( "my_property_for_units" ), QStringLiteral( "myDummyValue" ) ).toString();
QCOMPARE( v3ss, QStringLiteral( "LayoutCentimeters" ) );

// Flags
QgsMapLayerProxyModel::Filters pointAndLine = QgsMapLayerProxyModel::Filters( QgsMapLayerProxyModel::PointLayer | QgsMapLayerProxyModel::LineLayer );
QgsMapLayerProxyModel::Filters pointAndPolygon = QgsMapLayerProxyModel::Filters( QgsMapLayerProxyModel::PointLayer | QgsMapLayerProxyModel::PolygonLayer );
ml->setCustomProperty( QStringLiteral( "my_property_for_a_flag" ), 1e8 ); // invalid
// this should be switched to customFlagProperty (see https://stackoverflow.com/questions/68485843/how-to-test-if-a-value-is-valid-for-a-qflags)
QgsMapLayerProxyModel::Filters v4 = ml->customEnumProperty( QStringLiteral( "my_property_for_a_flag" ), pointAndLine );
QCOMPARE( v4, pointAndLine );

ml->setCustomProperty( QStringLiteral( "my_property_for_a_flag" ), static_cast<int>( pointAndPolygon ) );
QgsMapLayerProxyModel::Filters v5 = ml->customFlagProperty( QStringLiteral( "my_property_for_a_flag" ), pointAndLine );
QCOMPARE( v5, pointAndPolygon );
// auto conversion of old property (int to str)
QCOMPARE( ml->customProperty( "my_property_for_a_flag" ).toString(), QStringLiteral( "PointLayer|PolygonLayer" ) );

ml->setCustomFlagProperty( QStringLiteral( "my_property_for_a_flag_as_string" ), pointAndPolygon );
QgsMapLayerProxyModel::Filters v5s = ml->customFlagProperty( QStringLiteral( "my_property_for_a_flag_as_string" ), pointAndLine );
QCOMPARE( v5s, pointAndPolygon );
QString v5ss = ml->customProperty( QStringLiteral( "my_property_for_a_flag_as_string" ), QStringLiteral( "myDummyString" ) ).toString();
QCOMPARE( v5ss, QStringLiteral( "PointLayer|PolygonLayer" ) );
}


QGSTEST_MAIN( TestQgsMapLayer )
#include "testqgsmaplayer.moc"

0 comments on commit eaa4b54

Please sign in to comment.