Always write/read legend attributes, add flags API

This commit is contained in:
Nyall Dawson 2025-08-11 12:15:31 +10:00
parent 3e8c2cdf14
commit 2a434da498
13 changed files with 326 additions and 10 deletions

View File

@ -29,6 +29,40 @@ Constructor for QgsMapLayerLegend
%End %End
Qgis::MapLayerLegendFlags flags() const;
%Docstring
Returns flags associated with the legend.
.. seealso:: :py:func:`setFlag`
.. seealso:: :py:func:`setFlags`
.. versionadded:: 4.0
%End
void setFlag( Qgis::MapLayerLegendFlag flag, bool on = true );
%Docstring
Enables or disables a particular ``flag`` (other flags are not
affected).
.. seealso:: :py:func:`flags`
.. seealso:: :py:func:`setFlags`
.. versionadded:: 4.0
%End
void setFlags( Qgis::MapLayerLegendFlags flags );
%Docstring
Sets the ``flags`` associated with the legend.
.. seealso:: :py:func:`setFlag`
.. seealso:: :py:func:`flags`
.. versionadded:: 4.0
%End
virtual void readXml( const QDomElement &elem, const QgsReadWriteContext &context ); virtual void readXml( const QDomElement &elem, const QgsReadWriteContext &context );
%Docstring %Docstring
Reads configuration from a DOM element previously written by Reads configuration from a DOM element previously written by
@ -80,6 +114,7 @@ Create new legend implementation for a point cloud ``layer``.
Emitted when existing items/nodes got invalid and should be replaced by Emitted when existing items/nodes got invalid and should be replaced by
new ones new ones
%End %End
}; };

View File

@ -29,6 +29,40 @@ Constructor for QgsMapLayerLegend
%End %End
Qgis::MapLayerLegendFlags flags() const;
%Docstring
Returns flags associated with the legend.
.. seealso:: :py:func:`setFlag`
.. seealso:: :py:func:`setFlags`
.. versionadded:: 4.0
%End
void setFlag( Qgis::MapLayerLegendFlag flag, bool on = true );
%Docstring
Enables or disables a particular ``flag`` (other flags are not
affected).
.. seealso:: :py:func:`flags`
.. seealso:: :py:func:`setFlags`
.. versionadded:: 4.0
%End
void setFlags( Qgis::MapLayerLegendFlags flags );
%Docstring
Sets the ``flags`` associated with the legend.
.. seealso:: :py:func:`setFlag`
.. seealso:: :py:func:`flags`
.. versionadded:: 4.0
%End
virtual void readXml( const QDomElement &elem, const QgsReadWriteContext &context ); virtual void readXml( const QDomElement &elem, const QgsReadWriteContext &context );
%Docstring %Docstring
Reads configuration from a DOM element previously written by Reads configuration from a DOM element previously written by
@ -80,6 +114,7 @@ Create new legend implementation for a point cloud ``layer``.
Emitted when existing items/nodes got invalid and should be replaced by Emitted when existing items/nodes got invalid and should be replaced by
new ones new ones
%End %End
}; };

View File

@ -1881,6 +1881,17 @@ bool QgsMeshLayer::readSymbology( const QDomNode &node, QString &errorMessage,
} }
} }
if ( categories.testFlag( Legend ) )
{
QgsReadWriteContextCategoryPopper p = context.enterCategory( tr( "Legend" ) );
const QDomElement legendElem = node.firstChildElement( QStringLiteral( "legend" ) );
if ( QgsMapLayerLegend *l = legend(); !legendElem.isNull() )
{
l->readXml( legendElem, context );
}
}
return true; return true;
} }
@ -1937,6 +1948,13 @@ bool QgsMeshLayer::writeSymbology( QDomNode &node, QDomDocument &doc, QString &e
node.appendChild( layerOpacityElem ); node.appendChild( layerOpacityElem );
} }
if ( categories.testFlag( Legend ) && legend() )
{
QDomElement legendElement = legend()->writeXml( doc, context );
if ( !legendElement.isNull() )
node.appendChild( legendElement );
}
return true; return true;
} }

View File

@ -232,6 +232,17 @@ bool QgsPointCloudLayer::readSymbology( const QDomNode &node, QString &errorMess
if ( categories.testFlag( CustomProperties ) ) if ( categories.testFlag( CustomProperties ) )
readCustomProperties( node, QStringLiteral( "variable" ) ); readCustomProperties( node, QStringLiteral( "variable" ) );
if ( categories.testFlag( Legend ) )
{
QgsReadWriteContextCategoryPopper p = context.enterCategory( tr( "Legend" ) );
const QDomElement legendElem = node.firstChildElement( QStringLiteral( "legend" ) );
if ( QgsMapLayerLegend *l = legend(); !legendElem.isNull() )
{
l->readXml( legendElem, context );
}
}
return true; return true;
} }
@ -321,6 +332,13 @@ bool QgsPointCloudLayer::writeSymbology( QDomNode &node, QDomDocument &doc, QStr
( void )writeStyle( node, doc, errorMessage, context, categories ); ( void )writeStyle( node, doc, errorMessage, context, categories );
if ( categories.testFlag( Legend ) && legend() )
{
QDomElement legendElement = legend()->writeXml( doc, context );
if ( !legendElement.isNull() )
node.appendChild( legendElement );
}
return true; return true;
} }

View File

@ -39,17 +39,20 @@ QgsMapLayerLegend::QgsMapLayerLegend( QObject *parent )
{ {
} }
void QgsMapLayerLegend::readXml( const QDomElement &elem, const QgsReadWriteContext &context ) void QgsMapLayerLegend::readXml( const QDomElement &elem, const QgsReadWriteContext & )
{ {
Q_UNUSED( elem ) mFlags = Qgis::MapLayerLegendFlags();
Q_UNUSED( context ) mFlags.setFlag( Qgis::MapLayerLegendFlag::ExcludeByDefault, elem.attribute( QStringLiteral( "showLabelLegend" ), QStringLiteral( "0" ) ).toInt() );
} }
QDomElement QgsMapLayerLegend::writeXml( QDomDocument &doc, const QgsReadWriteContext &context ) const QDomElement QgsMapLayerLegend::writeXml( QDomDocument &doc, const QgsReadWriteContext & ) const
{ {
Q_UNUSED( doc ) QDomElement elem = doc.createElement( QStringLiteral( "legend" ) );
Q_UNUSED( context )
return QDomElement(); if ( mFlags.testFlag( Qgis::MapLayerLegendFlag::ExcludeByDefault ) )
elem.setAttribute( QStringLiteral( "excludeByDefault" ), QStringLiteral( "1" ) );
return elem.attributes().isEmpty() ? QDomElement() : elem;
} }
QgsMapLayerLegend *QgsMapLayerLegend::defaultVectorLegend( QgsVectorLayer *vl ) QgsMapLayerLegend *QgsMapLayerLegend::defaultVectorLegend( QgsVectorLayer *vl )
@ -443,6 +446,8 @@ QList<QgsLayerTreeModelLegendNode *> QgsDefaultVectorLayerLegend::createLayerTre
void QgsDefaultVectorLayerLegend::readXml( const QDomElement &elem, const QgsReadWriteContext &context ) void QgsDefaultVectorLayerLegend::readXml( const QDomElement &elem, const QgsReadWriteContext &context )
{ {
QgsMapLayerLegend::readXml( elem, context );
mTextOnSymbolEnabled = false; mTextOnSymbolEnabled = false;
mTextOnSymbolTextFormat = QgsTextFormat(); mTextOnSymbolTextFormat = QgsTextFormat();
mTextOnSymbolContent.clear(); mTextOnSymbolContent.clear();
@ -467,7 +472,7 @@ void QgsDefaultVectorLayerLegend::readXml( const QDomElement &elem, const QgsRea
QDomElement QgsDefaultVectorLayerLegend::writeXml( QDomDocument &doc, const QgsReadWriteContext &context ) const QDomElement QgsDefaultVectorLayerLegend::writeXml( QDomDocument &doc, const QgsReadWriteContext &context ) const
{ {
QDomElement elem = doc.createElement( QStringLiteral( "legend" ) ); QDomElement elem = QgsMapLayerLegend::writeXml( doc, context );
elem.setAttribute( QStringLiteral( "type" ), QStringLiteral( "default-vector" ) ); elem.setAttribute( QStringLiteral( "type" ), QStringLiteral( "default-vector" ) );
elem.setAttribute( QStringLiteral( "showLabelLegend" ), mShowLabelLegend ); elem.setAttribute( QStringLiteral( "showLabelLegend" ), mShowLabelLegend );

View File

@ -53,6 +53,33 @@ class CORE_EXPORT QgsMapLayerLegend : public QObject
// TODO: type // TODO: type
/**
* Returns flags associated with the legend.
*
* \see setFlag()
* \see setFlags()
* \since QGIS 4.0
*/
Qgis::MapLayerLegendFlags flags() const { return mFlags; }
/**
* Enables or disables a particular \a flag (other flags are not affected).
*
* \see flags()
* \see setFlags()
* \since QGIS 4.0
*/
void setFlag( Qgis::MapLayerLegendFlag flag, bool on = true ) { mFlags.setFlag( flag, on ); }
/**
* Sets the \a flags associated with the legend.
*
* \see setFlag()
* \see flags()
* \since QGIS 4.0
*/
void setFlags( Qgis::MapLayerLegendFlags flags ) { mFlags = flags; }
/** /**
* Reads configuration from a DOM element previously written by writeXml() * Reads configuration from a DOM element previously written by writeXml()
* \since QGIS 3.2 * \since QGIS 3.2
@ -91,6 +118,10 @@ class CORE_EXPORT QgsMapLayerLegend : public QObject
signals: signals:
//! Emitted when existing items/nodes got invalid and should be replaced by new ones //! Emitted when existing items/nodes got invalid and should be replaced by new ones
void itemsChanged(); void itemsChanged();
private:
Qgis::MapLayerLegendFlags mFlags;
}; };

View File

@ -2268,6 +2268,17 @@ bool QgsRasterLayer::readSymbology( const QDomNode &layer_node, QString &errorMe
readRasterAttributeTableExternalPaths( layer_node, context ); readRasterAttributeTableExternalPaths( layer_node, context );
} }
if ( categories.testFlag( Legend ) )
{
QgsReadWriteContextCategoryPopper p = context.enterCategory( tr( "Legend" ) );
const QDomElement legendElem = layer_node.firstChildElement( QStringLiteral( "legend" ) );
if ( QgsMapLayerLegend *l = legend(); !legendElem.isNull() )
{
l->readXml( legendElem, context );
}
}
emit rendererChanged(); emit rendererChanged();
emitStyleChanged(); emitStyleChanged();
@ -2528,6 +2539,13 @@ bool QgsRasterLayer::writeSymbology( QDomNode &layer_node, QDomDocument &documen
layer_node.appendChild( blendModeElement ); layer_node.appendChild( blendModeElement );
} }
if ( categories.testFlag( Legend ) && legend() )
{
QDomElement legendElement = legend()->writeXml( document, context );
if ( !legendElement.isNull() )
layer_node.appendChild( legendElement );
}
return true; return true;
} }

View File

@ -28,7 +28,10 @@ QgsMapLayerStyleCategoriesModel::QgsMapLayerStyleCategoriesModel( Qgis::LayerTyp
break; break;
case Qgis::LayerType::VectorTile: case Qgis::LayerType::VectorTile:
mCategoryList << QgsMapLayer::StyleCategory::Symbology << QgsMapLayer::StyleCategory::Labeling << QgsMapLayer::StyleCategory::AllVisualStyleCategories << QgsMapLayer::StyleCategory::AllStyleCategories; mCategoryList << QgsMapLayer::StyleCategory::Symbology
<< QgsMapLayer::StyleCategory::Labeling
<< QgsMapLayer::StyleCategory::AllVisualStyleCategories
<< QgsMapLayer::StyleCategory::AllStyleCategories;
break; break;
case Qgis::LayerType::Raster: case Qgis::LayerType::Raster:
@ -42,6 +45,7 @@ QgsMapLayerStyleCategoriesModel::QgsMapLayerStyleCategoriesModel( Qgis::LayerTyp
<< QgsMapLayer::StyleCategory::Elevation << QgsMapLayer::StyleCategory::Elevation
<< QgsMapLayer::StyleCategory::AttributeTable << QgsMapLayer::StyleCategory::AttributeTable
<< QgsMapLayer::StyleCategory::Notes << QgsMapLayer::StyleCategory::Notes
<< QgsMapLayer::StyleCategory::Legend
<< QgsMapLayer::StyleCategory::AllVisualStyleCategories << QgsMapLayer::StyleCategory::AllVisualStyleCategories
<< QgsMapLayer::StyleCategory::AllStyleCategories; << QgsMapLayer::StyleCategory::AllStyleCategories;
break; break;

View File

@ -244,6 +244,7 @@ ADD_PYTHON_TEST(PyQgsPointCloudIndex test_qgspointcloudindex.py)
ADD_PYTHON_TEST(PyQgsPointCloudDataProvider test_qgspointcloudprovider.py) ADD_PYTHON_TEST(PyQgsPointCloudDataProvider test_qgspointcloudprovider.py)
ADD_PYTHON_TEST(PyQgsPointCloudElevationProperties test_qgspointcloudelevationproperties.py) ADD_PYTHON_TEST(PyQgsPointCloudElevationProperties test_qgspointcloudelevationproperties.py)
ADD_PYTHON_TEST(PyQgsPointCloudExtentRenderer test_qgspointcloudextentrenderer.py) ADD_PYTHON_TEST(PyQgsPointCloudExtentRenderer test_qgspointcloudextentrenderer.py)
ADD_PYTHON_TEST(PyQgsPointCloudLayer test_qgspointcloudlayer.py)
ADD_PYTHON_TEST(PyQgsPointCloudLayerProfileGenerator test_qgspointcloudlayerprofilegenerator.py) ADD_PYTHON_TEST(PyQgsPointCloudLayerProfileGenerator test_qgspointcloudlayerprofilegenerator.py)
ADD_PYTHON_TEST(PyQgsPointCloudRgbRenderer test_qgspointcloudrgbrenderer.py) ADD_PYTHON_TEST(PyQgsPointCloudRgbRenderer test_qgspointcloudrgbrenderer.py)
ADD_PYTHON_TEST(PyQgsPointClusterRenderer test_qgspointclusterrenderer.py) ADD_PYTHON_TEST(PyQgsPointClusterRenderer test_qgspointclusterrenderer.py)

View File

@ -6,8 +6,10 @@ the Free Software Foundation; either version 2 of the License, or
(at your option) any later version. (at your option) any later version.
""" """
from qgis.core import QgsMeshLayer, QgsMeshDatasetIndex from qgis.core import Qgis, QgsProject, QgsMeshLayer, QgsMeshDatasetIndex
import unittest import unittest
import tempfile
from qgis.testing import start_app, QgisTestCase from qgis.testing import start_app, QgisTestCase
start_app() start_app()
@ -63,6 +65,35 @@ class TestQgsMeshLayer(QgisTestCase):
layer.datasetGroupMetadata(QgsMeshDatasetIndex(4)).parentQuantityName() layer.datasetGroupMetadata(QgsMeshDatasetIndex(4)).parentQuantityName()
) )
def test_legend_settings(self):
ml = QgsMeshLayer(
self.get_test_data_path("mesh/netcdf_parent_quantity.nc").as_posix(),
"test",
"mdal",
)
self.assertTrue(ml.isValid())
self.assertFalse(ml.legend().flags() & Qgis.MapLayerLegendFlag.ExcludeByDefault)
ml.legend().setFlag(Qgis.MapLayerLegendFlag.ExcludeByDefault)
self.assertTrue(ml.legend().flags() & Qgis.MapLayerLegendFlag.ExcludeByDefault)
p = QgsProject()
p.addMapLayer(ml)
# test saving and restoring
with tempfile.TemporaryDirectory() as temp:
self.assertTrue(p.write(temp + "/test.qgs"))
p2 = QgsProject()
self.assertTrue(p2.read(temp + "/test.qgs"))
ml2 = list(p2.mapLayers().values())[0]
self.assertEqual(ml2.name(), ml.name())
self.assertTrue(
ml2.legend().flags() & Qgis.MapLayerLegendFlag.ExcludeByDefault
)
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()

View File

@ -0,0 +1,59 @@
"""QGIS Unit tests for QgsPointCloudLayer
.. note:: This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
"""
from qgis.core import QgsPointCloudLayer, QgsProviderRegistry, Qgis, QgsProject
import unittest
import tempfile
from qgis.testing import start_app, QgisTestCase
start_app()
class TestQgsPointCloudLayer(QgisTestCase):
@unittest.skipIf(
"ept" not in QgsProviderRegistry.instance().providerList(),
"EPT provider not available",
)
def test_legend_settings(self):
ml = QgsPointCloudLayer(
(
self.get_test_data_path("point_clouds")
/ "ept"
/ "sunshine-coast"
/ "ept.json"
).as_posix(),
"test",
"ept",
)
self.assertTrue(ml.isValid())
self.assertFalse(ml.legend().flags() & Qgis.MapLayerLegendFlag.ExcludeByDefault)
ml.legend().setFlag(Qgis.MapLayerLegendFlag.ExcludeByDefault)
self.assertTrue(ml.legend().flags() & Qgis.MapLayerLegendFlag.ExcludeByDefault)
p = QgsProject()
p.addMapLayer(ml)
# test saving and restoring
with tempfile.TemporaryDirectory() as temp:
self.assertTrue(p.write(temp + "/test.qgs"))
p2 = QgsProject()
self.assertTrue(p2.read(temp + "/test.qgs"))
ml2 = list(p2.mapLayers().values())[0]
self.assertEqual(ml2.name(), ml.name())
self.assertTrue(
ml2.legend().flags() & Qgis.MapLayerLegendFlag.ExcludeByDefault
)
if __name__ == "__main__":
unittest.main()

View File

@ -16,6 +16,7 @@ __copyright__ = "Copyright 2012, The QGIS Project"
import filecmp import filecmp
import os import os
from shutil import copyfile from shutil import copyfile
import tempfile
import numpy import numpy
import numpy as np import numpy as np
@ -1524,6 +1525,37 @@ class TestQgsRasterLayerTransformContext(QgisTestCase):
self.assertEqual(arrays.shape, (1, 4, 4)) self.assertEqual(arrays.shape, (1, 4, 4))
self.assertEqual(arrays[0].dtype, np.float64) self.assertEqual(arrays[0].dtype, np.float64)
def test_legend_settings(self):
rl = QgsRasterLayer(
os.path.join(
unitTestDataPath("raster"), "rnd_percentile_raster5_float64.tif"
),
"test",
"gdal",
)
self.assertTrue(rl.isValid())
self.assertFalse(rl.legend().flags() & Qgis.MapLayerLegendFlag.ExcludeByDefault)
rl.legend().setFlag(Qgis.MapLayerLegendFlag.ExcludeByDefault)
self.assertTrue(rl.legend().flags() & Qgis.MapLayerLegendFlag.ExcludeByDefault)
p = QgsProject()
p.addMapLayer(rl)
# test saving and restoring
with tempfile.TemporaryDirectory() as temp:
self.assertTrue(p.write(temp + "/test.qgs"))
p2 = QgsProject()
self.assertTrue(p2.read(temp + "/test.qgs"))
rl2 = list(p2.mapLayers().values())[0]
self.assertEqual(rl2.name(), rl.name())
self.assertTrue(
rl2.legend().flags() & Qgis.MapLayerLegendFlag.ExcludeByDefault
)
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()

View File

@ -5787,6 +5787,35 @@ class TestQgsVectorLayerTransformContext(QgisTestCase):
QgsFieldConstraints.ConstraintStrength.ConstraintStrengthNotSet, QgsFieldConstraints.ConstraintStrength.ConstraintStrengthNotSet,
) )
def test_legend_settings(self):
vl = QgsVectorLayer(
"Point?crs=epsg:3111&field=field_default:integer&field=field_dupe:integer&field=field_unset:integer&field=field_ratio:integer",
"test",
"memory",
)
self.assertTrue(vl.isValid())
self.assertFalse(vl.legend().flags() & Qgis.MapLayerLegendFlag.ExcludeByDefault)
vl.legend().setFlag(Qgis.MapLayerLegendFlag.ExcludeByDefault)
self.assertTrue(vl.legend().flags() & Qgis.MapLayerLegendFlag.ExcludeByDefault)
p = QgsProject()
p.addMapLayer(vl)
# test saving and restoring
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.assertTrue(
vl2.legend().flags() & Qgis.MapLayerLegendFlag.ExcludeByDefault
)
# TODO: # TODO:
# - fetch rect: feat with changed geometry: 1. in rect, 2. out of rect # - fetch rect: feat with changed geometry: 1. in rect, 2. out of rect