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
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 );
%Docstring
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
new ones
%End
};

View File

@ -29,6 +29,40 @@ Constructor for QgsMapLayerLegend
%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 );
%Docstring
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
new ones
%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;
}
@ -1937,6 +1948,13 @@ bool QgsMeshLayer::writeSymbology( QDomNode &node, QDomDocument &doc, QString &e
node.appendChild( layerOpacityElem );
}
if ( categories.testFlag( Legend ) && legend() )
{
QDomElement legendElement = legend()->writeXml( doc, context );
if ( !legendElement.isNull() )
node.appendChild( legendElement );
}
return true;
}

View File

@ -232,6 +232,17 @@ bool QgsPointCloudLayer::readSymbology( const QDomNode &node, QString &errorMess
if ( categories.testFlag( CustomProperties ) )
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;
}
@ -321,6 +332,13 @@ bool QgsPointCloudLayer::writeSymbology( QDomNode &node, QDomDocument &doc, QStr
( 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;
}

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 )
Q_UNUSED( context )
mFlags = Qgis::MapLayerLegendFlags();
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 )
Q_UNUSED( context )
return QDomElement();
QDomElement elem = doc.createElement( QStringLiteral( "legend" ) );
if ( mFlags.testFlag( Qgis::MapLayerLegendFlag::ExcludeByDefault ) )
elem.setAttribute( QStringLiteral( "excludeByDefault" ), QStringLiteral( "1" ) );
return elem.attributes().isEmpty() ? QDomElement() : elem;
}
QgsMapLayerLegend *QgsMapLayerLegend::defaultVectorLegend( QgsVectorLayer *vl )
@ -443,6 +446,8 @@ QList<QgsLayerTreeModelLegendNode *> QgsDefaultVectorLayerLegend::createLayerTre
void QgsDefaultVectorLayerLegend::readXml( const QDomElement &elem, const QgsReadWriteContext &context )
{
QgsMapLayerLegend::readXml( elem, context );
mTextOnSymbolEnabled = false;
mTextOnSymbolTextFormat = QgsTextFormat();
mTextOnSymbolContent.clear();
@ -467,7 +472,7 @@ void QgsDefaultVectorLayerLegend::readXml( const QDomElement &elem, const QgsRea
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( "showLabelLegend" ), mShowLabelLegend );

View File

@ -53,6 +53,33 @@ class CORE_EXPORT QgsMapLayerLegend : public QObject
// 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()
* \since QGIS 3.2
@ -91,6 +118,10 @@ class CORE_EXPORT QgsMapLayerLegend : public QObject
signals:
//! Emitted when existing items/nodes got invalid and should be replaced by new ones
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 );
}
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();
emitStyleChanged();
@ -2528,6 +2539,13 @@ bool QgsRasterLayer::writeSymbology( QDomNode &layer_node, QDomDocument &documen
layer_node.appendChild( blendModeElement );
}
if ( categories.testFlag( Legend ) && legend() )
{
QDomElement legendElement = legend()->writeXml( document, context );
if ( !legendElement.isNull() )
layer_node.appendChild( legendElement );
}
return true;
}

View File

@ -28,7 +28,10 @@ QgsMapLayerStyleCategoriesModel::QgsMapLayerStyleCategoriesModel( Qgis::LayerTyp
break;
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;
case Qgis::LayerType::Raster:
@ -42,6 +45,7 @@ QgsMapLayerStyleCategoriesModel::QgsMapLayerStyleCategoriesModel( Qgis::LayerTyp
<< QgsMapLayer::StyleCategory::Elevation
<< QgsMapLayer::StyleCategory::AttributeTable
<< QgsMapLayer::StyleCategory::Notes
<< QgsMapLayer::StyleCategory::Legend
<< QgsMapLayer::StyleCategory::AllVisualStyleCategories
<< QgsMapLayer::StyleCategory::AllStyleCategories;
break;

View File

@ -244,6 +244,7 @@ ADD_PYTHON_TEST(PyQgsPointCloudIndex test_qgspointcloudindex.py)
ADD_PYTHON_TEST(PyQgsPointCloudDataProvider test_qgspointcloudprovider.py)
ADD_PYTHON_TEST(PyQgsPointCloudElevationProperties test_qgspointcloudelevationproperties.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(PyQgsPointCloudRgbRenderer test_qgspointcloudrgbrenderer.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.
"""
from qgis.core import QgsMeshLayer, QgsMeshDatasetIndex
from qgis.core import Qgis, QgsProject, QgsMeshLayer, QgsMeshDatasetIndex
import unittest
import tempfile
from qgis.testing import start_app, QgisTestCase
start_app()
@ -63,6 +65,35 @@ class TestQgsMeshLayer(QgisTestCase):
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__":
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 os
from shutil import copyfile
import tempfile
import numpy
import numpy as np
@ -1524,6 +1525,37 @@ class TestQgsRasterLayerTransformContext(QgisTestCase):
self.assertEqual(arrays.shape, (1, 4, 4))
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__":
unittest.main()

View File

@ -5787,6 +5787,35 @@ class TestQgsVectorLayerTransformContext(QgisTestCase):
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:
# - fetch rect: feat with changed geometry: 1. in rect, 2. out of rect