Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,20 @@ Find custom node IDs. Searches recursively the whole sub-tree.
%Docstring
Find group node with specified name. Searches recursively the whole
sub-tree.
%End

QgsLayerTreeGroup *findGroupByPath( const QList<QPair<QString, int>> &path );
%Docstring
Find a group node by navigating a hierarchical path from this node.

Each element in the ``path`` is a pair of (group name, occurrence index)
where the occurrence index is the 0-based position among same-named
non-embedded sibling groups at each level. This allows unambiguous
identification of groups even when multiple groups share the same name.

Returns ``None`` if the path cannot be resolved.

.. versionadded:: 3.44
%End

QList<QgsLayerTreeGroup *> findGroups( bool recursive = false ) const;
Expand Down
17 changes: 17 additions & 0 deletions python/PyQt6/core/auto_generated/project/qgsproject.sip.in
Original file line number Diff line number Diff line change
Expand Up @@ -793,6 +793,23 @@ Create layer group instance defined in an arbitrary project file.

The optional ``flags`` argument can be used to control layer reading
behavior.
%End

std::unique_ptr< QgsLayerTreeGroup > createEmbeddedGroup( const QList<QPair<QString, int>> &groupPath, const QString &projectFilePath, const QStringList &invisibleLayers, Qgis::ProjectReadFlags flags = Qgis::ProjectReadFlags() );
%Docstring
Create layer group instance defined in an arbitrary project file,
identified by a hierarchical path.

The ``groupPath`` is a list of (group name, occurrence index) pairs that
describes the path from the root of the layer tree to the target group.
The occurrence index is the 0-based position among same-named
non-embedded sibling groups at each level. This allows unambiguous
identification of groups even when multiple groups share the same name.

The optional ``flags`` argument can be used to control layer reading
behavior.

.. versionadded:: 3.44
%End

void setTopologicalEditing( bool enabled );
Expand Down
3 changes: 3 additions & 0 deletions python/PyQt6/core/class_map.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6732,6 +6732,7 @@ QgsLayerTreeGroup.dump: src/core/layertree/qgslayertreegroup.h#L298
QgsLayerTreeGroup.findCustomNode: src/core/layertree/qgslayertreegroup.h#L188
QgsLayerTreeGroup.findCustomNodeIds: src/core/layertree/qgslayertreegroup.h#L260
QgsLayerTreeGroup.findGroup: src/core/layertree/qgslayertreegroup.h#L265
QgsLayerTreeGroup.findGroupByPath: src/core/layertree/qgslayertreegroup.h#L279
QgsLayerTreeGroup.findLayer: src/core/layertree/qgslayertreegroup.h#L169
QgsLayerTreeGroup.findLayer: src/core/layertree/qgslayertreegroup.h#L174
QgsLayerTreeGroup.findLayerIds: src/core/layertree/qgslayertreegroup.h#L255
Expand Down Expand Up @@ -14002,6 +14003,8 @@ QgsProject.cleared: src/core/project/qgsproject.h#L1863
QgsProject.commitChanges: src/core/project/qgsproject.h#L2408
QgsProject.count: src/core/project/qgsproject.h#L1229
QgsProject.createAttachedFile: src/core/project/qgsproject.h#L1623
QgsProject.createEmbeddedGroup: src/core/project/qgsproject.h#L765
QgsProject.createEmbeddedGroup: src/core/project/qgsproject.h#L781
QgsProject.createEmbeddedLayer: src/core/project/qgsproject.h#L756
QgsProject.createExpressionContext: src/core/project/qgsproject.h#L1154
QgsProject.createExpressionContextScope: src/core/project/qgsproject.h#L1155
Expand Down
18 changes: 17 additions & 1 deletion src/app/qgisapp.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13358,7 +13358,23 @@ void QgisApp::embedLayers()
QgsProjectLayerGroupDialog d( this );
if ( d.exec() == QDialog::Accepted && d.isValid() )
{
addEmbeddedItems( d.selectedProjectFile(), d.selectedGroups(), d.selectedLayerIds() );
const QString projectFile = d.selectedProjectFile();
const QList<QList<QPair<QString, int>>> groupPaths = d.selectedGroupPaths();
const QStringList layerIds = d.selectedLayerIds();

QgsCanvasRefreshBlocker refreshBlocker;

// embed groups using paths for accurate identification
for ( const QList<QPair<QString, int>> &path : groupPaths )
{
std::unique_ptr< QgsLayerTreeGroup > newGroup = QgsProject::instance()->createEmbeddedGroup( path, projectFile, QStringList() );
if ( newGroup )
QgsProject::instance()->layerTreeRoot()->addChildNode( newGroup.release() );
}

// embed layers
if ( !layerIds.isEmpty() )
addEmbeddedItems( projectFile, QStringList(), layerIds );
}
}

Expand Down
36 changes: 36 additions & 0 deletions src/app/qgsprojectlayergroupdialog.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,42 @@ QStringList QgsProjectLayerGroupDialog::selectedGroups() const
return groups;
}

QList<QList<QPair<QString, int>>> QgsProjectLayerGroupDialog::selectedGroupPaths() const
{
QList<QList<QPair<QString, int>>> paths;
const auto constSelectedIndexes = mTreeView->selectionModel()->selectedIndexes();
for ( const QModelIndex &index : constSelectedIndexes )
{
QgsLayerTreeNode *node = mTreeView->index2node( index );
if ( QgsLayerTree::isGroup( node ) )
{
QList<QPair<QString, int>> path;
QgsLayerTreeNode *current = node;
while ( current && current->parent() )
{
if ( QgsLayerTree::isGroup( current ) )
{
const QString name = QgsLayerTree::toGroup( current )->name();
// Count how many same-named siblings appear before this node
int occurrence = 0;
QgsLayerTreeNode *parent = current->parent();
for ( QgsLayerTreeNode *sibling : parent->children() )
{
if ( sibling == current )
break;
if ( QgsLayerTree::isGroup( sibling ) && QgsLayerTree::toGroup( sibling )->name() == name )
occurrence++;
}
path.prepend( qMakePair( name, occurrence ) );
}
current = current->parent();
}
paths.append( path );
}
}
return paths;
}

QStringList QgsProjectLayerGroupDialog::selectedLayerIds() const
{
QStringList layerIds;
Expand Down
13 changes: 13 additions & 0 deletions src/app/qgsprojectlayergroupdialog.h
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,19 @@ class APP_EXPORT QgsProjectLayerGroupDialog : public QDialog, private Ui::QgsPro
~QgsProjectLayerGroupDialog() override;

QStringList selectedGroups() const;

/**
* Returns the hierarchical paths of the selected groups.
*
* Each path is a list of (group name, occurrence index) pairs describing
* the navigation from the root of the layer tree to the selected group.
* The occurrence index is the 0-based position among same-named sibling
* groups at each level.
*
* \since QGIS 3.44
*/
QList<QList<QPair<QString, int>>> selectedGroupPaths() const;

QStringList selectedLayerIds() const;
QStringList selectedLayerNames() const;
QString selectedProjectFile() const;
Expand Down
35 changes: 35 additions & 0 deletions src/core/layertree/qgslayertreegroup.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -474,6 +474,41 @@ QgsLayerTreeGroup *QgsLayerTreeGroup::findGroup( const QString &name )
return nullptr;
}

QgsLayerTreeGroup *QgsLayerTreeGroup::findGroupByPath( const QList<QPair<QString, int>> &path )
{
if ( path.isEmpty() )
return nullptr;

QgsLayerTreeGroup *current = this;
for ( const auto &[name, occurrence] : path )
{
int matchCount = 0;
QgsLayerTreeGroup *found = nullptr;
for ( QgsLayerTreeNode *child : std::as_const( current->mChildren ) )
{
if ( QgsLayerTree::isGroup( child ) )
{
QgsLayerTreeGroup *childGroup = QgsLayerTree::toGroup( child );
if ( childGroup->customProperty( u"embedded"_s ).toInt() )
continue;
if ( childGroup->name() == name )
{
if ( matchCount == occurrence )
{
found = childGroup;
break;
}
matchCount++;
}
}
}
if ( !found )
return nullptr;
current = found;
}
return current;
}

QList<QgsLayerTreeGroup *> QgsLayerTreeGroup::findGroups( bool recursive ) const
{
QList<QgsLayerTreeGroup *> list;
Expand Down
14 changes: 14 additions & 0 deletions src/core/layertree/qgslayertreegroup.h
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,20 @@ class CORE_EXPORT QgsLayerTreeGroup : public QgsLayerTreeNode
*/
QgsLayerTreeGroup *findGroup( const QString &name );

/**
* Find a group node by navigating a hierarchical path from this node.
*
* Each element in the \a path is a pair of (group name, occurrence index) where
* the occurrence index is the 0-based position among same-named non-embedded
* sibling groups at each level. This allows unambiguous identification of groups
* even when multiple groups share the same name.
*
* Returns NULLPTR if the path cannot be resolved.
*
* \since QGIS 3.44
*/
QgsLayerTreeGroup *findGroupByPath( const QList<QPair<QString, int>> &path );

/**
* Find group layer nodes. Searches recursively the whole sub-tree, if recursive is set.
*/
Expand Down
76 changes: 76 additions & 0 deletions src/core/project/qgsproject.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4207,6 +4207,82 @@ std::unique_ptr<QgsLayerTreeGroup> QgsProject::createEmbeddedGroup( const QStrin
return newGroup;
}

std::unique_ptr<QgsLayerTreeGroup> QgsProject::createEmbeddedGroup( const QList<QPair<QString, int>> &groupPath, const QString &projectFilePath, const QStringList &invisibleLayers, Qgis::ProjectReadFlags flags )
{
QGIS_PROTECT_QOBJECT_THREAD_ACCESS

QString qgsProjectFile = projectFilePath;
QgsProjectArchive archive;
if ( projectFilePath.endsWith( ".qgz"_L1, Qt::CaseInsensitive ) )
{
archive.unzip( projectFilePath );
qgsProjectFile = archive.projectFile();
}

// open project file, get layer ids in group, add the layers
QFile projectFile( qgsProjectFile );
if ( !projectFile.open( QIODevice::ReadOnly ) )
{
return nullptr;
}

QDomDocument projectDocument;
if ( !projectDocument.setContent( &projectFile ) )
{
return nullptr;
}

QgsReadWriteContext context;
context.setPathResolver( pathResolver() );
context.setProjectTranslator( this );
context.setTransformContext( transformContext() );

auto root = std::make_unique< QgsLayerTreeGroup >();

QDomElement layerTreeElem = projectDocument.documentElement().firstChildElement( u"layer-tree-group"_s );
if ( !layerTreeElem.isNull() )
{
root->readChildrenFromXml( layerTreeElem, context );
}
else
{
QgsLayerTreeUtils::readOldLegend( root.get(), projectDocument.documentElement().firstChildElement( u"legend"_s ) );
}

QgsLayerTreeGroup *group = root->findGroupByPath( groupPath );
if ( !group || group->customProperty( u"embedded"_s ).toBool() )
{
// embedded groups cannot be embedded again
return nullptr;
}

// clone the group sub-tree (it is used already in a tree, we cannot just tear it off)
std::unique_ptr< QgsLayerTreeGroup > newGroup( QgsLayerTree::toGroup( group->clone() ) );
root.reset();

newGroup->setCustomProperty( u"embedded"_s, 1 );
newGroup->setCustomProperty( u"embedded_project"_s, projectFilePath );

// set "embedded" to all children + load embedded layers
mLayerTreeRegistryBridge->setEnabled( false );
initializeEmbeddedSubtree( projectFilePath, newGroup.get(), flags );
mLayerTreeRegistryBridge->setEnabled( true );

// consider the layers might be identify disabled in its project
const QStringList constFindLayerIds = newGroup->findLayerIds();
for ( const QString &layerId : constFindLayerIds )
{
QgsLayerTreeLayer *layer = newGroup->findLayer( layerId );
if ( layer )
{
layer->resolveReferences( this );
layer->setItemVisibilityChecked( !invisibleLayers.contains( layerId ) );
}
}

return newGroup;
}

void QgsProject::initializeEmbeddedSubtree( const QString &projectFilePath, QgsLayerTreeGroup *group, Qgis::ProjectReadFlags flags )
{
QGIS_PROTECT_QOBJECT_THREAD_ACCESS
Expand Down
16 changes: 16 additions & 0 deletions src/core/project/qgsproject.h
Original file line number Diff line number Diff line change
Expand Up @@ -764,6 +764,22 @@ class CORE_EXPORT QgsProject : public QObject, public QgsExpressionContextGenera
*/
std::unique_ptr< QgsLayerTreeGroup > createEmbeddedGroup( const QString &groupName, const QString &projectFilePath, const QStringList &invisibleLayers, Qgis::ProjectReadFlags flags = Qgis::ProjectReadFlags() );

/**
* Create layer group instance defined in an arbitrary project file,
* identified by a hierarchical path.
*
* The \a groupPath is a list of (group name, occurrence index) pairs that
* describes the path from the root of the layer tree to the target group.
* The occurrence index is the 0-based position among same-named non-embedded
* sibling groups at each level. This allows unambiguous identification of
* groups even when multiple groups share the same name.
*
* The optional \a flags argument can be used to control layer reading behavior.
*
* \since QGIS 3.44
*/
std::unique_ptr< QgsLayerTreeGroup > createEmbeddedGroup( const QList<QPair<QString, int>> &groupPath, const QString &projectFilePath, const QStringList &invisibleLayers, Qgis::ProjectReadFlags flags = Qgis::ProjectReadFlags() );

//! Convenience function to set topological editing
void setTopologicalEditing( bool enabled );

Expand Down
46 changes: 46 additions & 0 deletions tests/src/core/testqgslayertree.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ class TestQgsLayerTree : public QObject
void testLayerDeleted();
void testFindGroups();
void testFindNestedGroups();
void testFindGroupByPath();
void testCustomNodes();
void testCustomNodeOrderAndFinding();
void testCustomNodeDeleted();
Expand Down Expand Up @@ -835,6 +836,51 @@ void TestQgsLayerTree::testFindNestedGroups()
QVERIFY( all.contains( group3 ) );
}

void TestQgsLayerTree::testFindGroupByPath()
{
// root
// ├─ A
// │ ├─ B
// │ └─ B (second occurrence)
// └─ A (second occurrence)
// └─ C
const QgsProject project;
QgsLayerTreeGroup *a0 = project.layerTreeRoot()->addGroup( u"A"_s );
QgsLayerTreeGroup *b0 = a0->addGroup( u"B"_s );
QgsLayerTreeGroup *b1 = a0->addGroup( u"B"_s );
QgsLayerTreeGroup *a1 = project.layerTreeRoot()->addGroup( u"A"_s );
QgsLayerTreeGroup *c = a1->addGroup( u"C"_s );

// single-level path
QList<QPair<QString, int>> path;
path << qMakePair( u"A"_s, 0 );
QCOMPARE( project.layerTreeRoot()->findGroupByPath( path ), a0 );

path.clear();
path << qMakePair( u"A"_s, 1 );
QCOMPARE( project.layerTreeRoot()->findGroupByPath( path ), a1 );

// multi-level path with same-named groups
path.clear();
path << qMakePair( u"A"_s, 0 ) << qMakePair( u"B"_s, 1 );
QCOMPARE( project.layerTreeRoot()->findGroupByPath( path ), b1 );

path.clear();
path << qMakePair( u"A"_s, 1 ) << qMakePair( u"C"_s, 0 );
QCOMPARE( project.layerTreeRoot()->findGroupByPath( path ), c );

// invalid paths
QVERIFY( !project.layerTreeRoot()->findGroupByPath( {} ) );

path.clear();
path << qMakePair( u"X"_s, 0 );
QVERIFY( !project.layerTreeRoot()->findGroupByPath( path ) );

path.clear();
path << qMakePair( u"A"_s, 2 ); // only two A's
QVERIFY( !project.layerTreeRoot()->findGroupByPath( path ) );
}

void TestQgsLayerTree::testCustomNodes()
{
auto custom = std::make_unique< QgsLayerTreeCustomNode >( u"custom-id"_s );
Expand Down
Loading