From e106e06fc74e3cb743e75d9c4050847ce7e85608 Mon Sep 17 00:00:00 2001 From: Jean Felder Date: Wed, 5 Feb 2025 16:23:16 +0100 Subject: [PATCH 1/2] qgslayertreeregistrybridge: Add a layerInsertionPoint getter --- .../qgslayertreeregistrybridge.sip.in | 7 +++ .../qgslayertreeregistrybridge.sip.in | 7 +++ .../layertree/qgslayertreeregistrybridge.cpp | 6 ++ .../layertree/qgslayertreeregistrybridge.h | 6 ++ tests/src/python/CMakeLists.txt | 1 + .../python/test_qgslayertreeregistrybridge.py | 62 +++++++++++++++++++ 6 files changed, 89 insertions(+) create mode 100644 tests/src/python/test_qgslayertreeregistrybridge.py diff --git a/python/PyQt6/core/auto_generated/layertree/qgslayertreeregistrybridge.sip.in b/python/PyQt6/core/auto_generated/layertree/qgslayertreeregistrybridge.sip.in index 879a15e88df2..b614ba15fe46 100644 --- a/python/PyQt6/core/auto_generated/layertree/qgslayertreeregistrybridge.sip.in +++ b/python/PyQt6/core/auto_generated/layertree/qgslayertreeregistrybridge.sip.in @@ -66,6 +66,13 @@ Set where the new layers should be inserted - can be used to follow current sele By default it is root group with zero index. .. versionadded:: 3.10 +%End + + InsertionPoint layerInsertionPoint() const; +%Docstring +Returns the insertion point used to add layers to the tree + +.. versionadded:: 3.42 %End void setLayerInsertionMethod( Qgis::LayerTreeInsertionMethod method ); diff --git a/python/core/auto_generated/layertree/qgslayertreeregistrybridge.sip.in b/python/core/auto_generated/layertree/qgslayertreeregistrybridge.sip.in index 879a15e88df2..b614ba15fe46 100644 --- a/python/core/auto_generated/layertree/qgslayertreeregistrybridge.sip.in +++ b/python/core/auto_generated/layertree/qgslayertreeregistrybridge.sip.in @@ -66,6 +66,13 @@ Set where the new layers should be inserted - can be used to follow current sele By default it is root group with zero index. .. versionadded:: 3.10 +%End + + InsertionPoint layerInsertionPoint() const; +%Docstring +Returns the insertion point used to add layers to the tree + +.. versionadded:: 3.42 %End void setLayerInsertionMethod( Qgis::LayerTreeInsertionMethod method ); diff --git a/src/core/layertree/qgslayertreeregistrybridge.cpp b/src/core/layertree/qgslayertreeregistrybridge.cpp index 09967561fce4..8b07cc0be17e 100644 --- a/src/core/layertree/qgslayertreeregistrybridge.cpp +++ b/src/core/layertree/qgslayertreeregistrybridge.cpp @@ -49,6 +49,12 @@ void QgsLayerTreeRegistryBridge::setLayerInsertionPoint( const InsertionPoint &i mInsertionPointPosition = insertionPoint.position; } +QgsLayerTreeRegistryBridge::InsertionPoint QgsLayerTreeRegistryBridge::layerInsertionPoint() const +{ + QgsLayerTreeGroup *group = mInsertionPointGroup.isNull() ? mRoot : mInsertionPointGroup.data(); + return InsertionPoint( group, mInsertionPointPosition ); +} + void QgsLayerTreeRegistryBridge::layersAdded( const QList &layers ) { if ( !mEnabled ) diff --git a/src/core/layertree/qgslayertreeregistrybridge.h b/src/core/layertree/qgslayertreeregistrybridge.h index 191c12dd589e..3281a9a4331b 100644 --- a/src/core/layertree/qgslayertreeregistrybridge.h +++ b/src/core/layertree/qgslayertreeregistrybridge.h @@ -85,6 +85,12 @@ class CORE_EXPORT QgsLayerTreeRegistryBridge : public QObject */ void setLayerInsertionPoint( const InsertionPoint &insertionPoint ); + /** + * Returns the insertion point used to add layers to the tree + * \since QGIS 3.42 + */ + InsertionPoint layerInsertionPoint() const; + /** * Sets the insertion \a method used to add layers to the tree * \since QGIS 3.30 diff --git a/tests/src/python/CMakeLists.txt b/tests/src/python/CMakeLists.txt index be46cfd16125..1115a59495d6 100644 --- a/tests/src/python/CMakeLists.txt +++ b/tests/src/python/CMakeLists.txt @@ -127,6 +127,7 @@ ADD_PYTHON_TEST(PyQgsLayerMetadataProviderPython test_qgslayermetadataprovider_p ADD_PYTHON_TEST(PyQgsLayerMetadataProviderOgr test_qgslayermetadataprovider_ogr.py) ADD_PYTHON_TEST(PyQgsLayerTree test_qgslayertree.py) ADD_PYTHON_TEST(PyQgsLayerTreeFilterProxyModel test_qgslayertreefilterproxymodel.py) +ADD_PYTHON_TEST(PyQgsLayerTreeRegistryBridge test_qgslayertreeregistrybridge.py) ADD_PYTHON_TEST(PyQgsLayout test_qgslayout.py) ADD_PYTHON_TEST(PyQgsLayoutAlign test_qgslayoutaligner.py) ADD_PYTHON_TEST(PyQgsLayoutAtlas test_qgslayoutatlas.py) diff --git a/tests/src/python/test_qgslayertreeregistrybridge.py b/tests/src/python/test_qgslayertreeregistrybridge.py new file mode 100644 index 000000000000..611311634929 --- /dev/null +++ b/tests/src/python/test_qgslayertreeregistrybridge.py @@ -0,0 +1,62 @@ +"""QGIS Unit tests for QgsLayerTreeRegistryBridge + +.. 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. +""" + +__author__ = "Jean Felder" +__date__ = "05/02/2025" +__copyright__ = "Copyright 2025, The QGIS Project" + +from qgis.core import ( + QgsLayerTreeRegistryBridge, + QgsLayerTree, + QgsProject, + QgsLayerTreeGroup, +) + +import unittest +from qgis.testing import start_app, QgisTestCase + +start_app() + + +class TestQgsLayerTreeRegistryBridge(QgisTestCase): + + def test_constructor(self): + project = QgsProject() + root_group = QgsLayerTree() + bridge = QgsLayerTreeRegistryBridge(root_group, project) + + self.assertTrue(bridge.isEnabled()) + self.assertTrue(bridge.newLayersVisible()) + self.assertEqual(bridge.layerInsertionPoint().group, root_group) + self.assertEqual(bridge.layerInsertionPoint().position, 0) + + def test_insertion_point(self): + project = QgsProject() + root_group = QgsLayerTree() + bridge = QgsLayerTreeRegistryBridge(root_group, project) + + self.assertEqual(bridge.layerInsertionPoint().group, root_group) + self.assertEqual(bridge.layerInsertionPoint().position, 0) + + group_node = QgsLayerTreeGroup() + root_group.addChildNode(group_node) + bridge.setLayerInsertionPoint( + QgsLayerTreeRegistryBridge.InsertionPoint(group_node, 4) + ) + self.assertEqual(bridge.layerInsertionPoint().group, group_node) + self.assertEqual(bridge.layerInsertionPoint().position, 4) + + bridge.setLayerInsertionPoint( + QgsLayerTreeRegistryBridge.InsertionPoint(root_group, 0) + ) + self.assertEqual(bridge.layerInsertionPoint().group, root_group) + self.assertEqual(bridge.layerInsertionPoint().position, 0) + + +if __name__ == "__main__": + unittest.main() From 9b0c1af5f9a6e90dad45a002e17bfa47aad24f8c Mon Sep 17 00:00:00 2001 From: Jean Felder Date: Tue, 4 Feb 2025 15:49:28 +0100 Subject: [PATCH 2/2] postprocessing: Use QgsLayerTreeRegistryBridge to add layers When a profile tool is already opened, the output of a procssing is not added to its layer tree. This is because it relies on a `QgsLayerTreeRegistryBridge`. Indeed, `QgsLayerTreeRegistryBridge` listens to the `QgsProject::legendLayersAdded()` signal in order to update the elevation profile tree view. However this signal is not triggered by the current logic in `Postprocessing.py`because `QgsProject::addMaplayer` is called with `addToLegend` set to False. Then, the layer is added to the tree by calling `QgsLayerTreeGroup::insertChildNode`. This issue is fixed by creating a `QgsLayerTreeRegistryBridge::InsertionPoint` to set the insertion point and then calling `QgsProject::addMaplayer` with `addToLegend` set to True. --- .../plugins/processing/gui/Postprocessing.py | 32 +++++++++++++++---- 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/python/plugins/processing/gui/Postprocessing.py b/python/plugins/processing/gui/Postprocessing.py index d36ce1f34b53..f495c2af0091 100644 --- a/python/plugins/processing/gui/Postprocessing.py +++ b/python/plugins/processing/gui/Postprocessing.py @@ -35,6 +35,7 @@ QgsLayerTreeLayer, QgsLayerTreeGroup, QgsLayerTreeNode, + QgsLayerTreeRegistryBridge, ) from qgis.utils import iface @@ -237,8 +238,6 @@ def handleAlgorithmResults( # output layer already exists in the destination project owned_map_layer = context.temporaryLayerStore().takeMapLayer(layer) if owned_map_layer: - details.project.addMapLayer(owned_map_layer, False) - # we don't add the layer to the tree yet -- that's done # later, after we've sorted all added layers layer_tree_layer = create_layer_tree_layer(owned_map_layer, details) @@ -277,10 +276,15 @@ def handleAlgorithmResults( current_selected_node = iface.layerTreeView().currentNode() iface.layerTreeView().setUpdatesEnabled(False) + # store the current intersection point to restore it later + previous_insertion_point = ( + details.project.layerTreeRegistryBridge().layerInsertionPoint() + ) for group, layer_node in sorted_layer_tree_layers: layer_node.removeCustomProperty(SORT_ORDER_CUSTOM_PROPERTY) + insertion_point: Optional[QgsLayerTreeRegistryBridge.InsertionPoint] = None if group is not None: - group.insertChildNode(0, layer_node) + insertion_point = QgsLayerTreeRegistryBridge.InsertionPoint(group, 0) else: # no destination group for this layer, so should be placed # above the current layer @@ -289,16 +293,32 @@ def handleAlgorithmResults( current_node_index = current_node_group.children().index( current_selected_node ) - current_node_group.insertChildNode(current_node_index, layer_node) + insertion_point = QgsLayerTreeRegistryBridge.InsertionPoint( + current_node_group, current_node_index + ) elif isinstance(current_selected_node, QgsLayerTreeGroup): - current_selected_node.insertChildNode(0, layer_node) + insertion_point = QgsLayerTreeRegistryBridge.InsertionPoint( + current_selected_node, 0 + ) elif context.project(): - context.project().layerTreeRoot().insertChildNode(0, layer_node) + insertion_point = QgsLayerTreeRegistryBridge.InsertionPoint() + + if insertion_point: + details.project.layerTreeRegistryBridge().setLayerInsertionPoint( + insertion_point + ) + + details.project.addMapLayer(layer_node.layer()) if not have_set_active_layer and iface is not None: iface.setActiveLayer(layer_node.layer()) have_set_active_layer = True + # reset to the previous insertion point + details.project.layerTreeRegistryBridge().setLayerInsertionPoint( + previous_insertion_point + ) + # all layers have been added to the layer tree, so safe to call # postProcessors now for layer, details in layers_to_post_process: