Skip to content

Commit

Permalink
Visible status for embedded layers in embedded group
Browse files Browse the repository at this point in the history
- For project : check visible state for embedded layers inside an unchecked group, instead of putting all layers in embedded-invisible-layers
- For theme : Add an 'checked-group-node' to save group visible state independently to layers in it.
Fixes #33097

(cherry picked from commit 35eea1c)
  • Loading branch information
SebastienPeillet authored and nirvn committed Dec 6, 2019
1 parent 5a8dfa6 commit 1f818ad
Show file tree
Hide file tree
Showing 5 changed files with 184 additions and 4 deletions.
17 changes: 17 additions & 0 deletions python/core/auto_generated/qgsmapthemecollection.sip.in
Expand Up @@ -125,13 +125,30 @@ Group identifiers are built using group names, a sub-group name is prepended by
and a forward slash, e.g. "level1/level2"

.. versionadded:: 3.2
%End

QSet<QString> checkedGroupNodes() const;
%Docstring
Returns a set of group identifiers for group nodes that should have checked state (other group nodes should be unchecked).
The returned value is valid only when hasCheckedStateInfo() returns ``True``.
Group identifiers are built using group names, a sub-group name is prepended by parent group's identifier
and a forward slash, e.g. "level1/level2"

.. versionadded:: 3.10.1
%End

void setExpandedGroupNodes( const QSet<QString> &expandedGroupNodes );
%Docstring
Sets a set of group identifiers for group nodes that should have expanded state. See expandedGroupNodes().

.. versionadded:: 3.2
%End

void setCheckedGroupNodes( const QSet<QString> &checkedGroupNodes );
%Docstring
Sets a set of group identifiers for group nodes that should have checked state. See checkedGroupNodes().

.. versionadded:: 3.10.1
%End

};
Expand Down
5 changes: 4 additions & 1 deletion src/core/layertree/qgslayertreeutils.cpp
Expand Up @@ -369,7 +369,10 @@ QStringList QgsLayerTreeUtils::invisibleLayerList( QgsLayerTreeNode *node )
const auto constChildren = QgsLayerTree::toGroup( node )->children();
for ( QgsLayerTreeNode *child : constChildren )
{
list << invisibleLayerList( child );
if ( child->itemVisibilityChecked() == Qt::Unchecked )
{
list << invisibleLayerList( child );
}
}
}
else if ( QgsLayerTree::isLayer( node ) )
Expand Down
44 changes: 43 additions & 1 deletion src/core/qgsmapthemecollection.cpp
Expand Up @@ -87,11 +87,13 @@ void QgsMapThemeCollection::createThemeFromCurrentState( QgsLayerTreeGroup *pare
createThemeFromCurrentState( QgsLayerTree::toGroup( node ), model, rec );
if ( node->isExpanded() )
rec.mExpandedGroupNodes.insert( _groupId( node ) );
if ( node->itemVisibilityChecked() != Qt::Unchecked )
rec.mCheckedGroupNodes.insert( _groupId( node ) );
}
else if ( QgsLayerTree::isLayer( node ) )
{
QgsLayerTreeLayer *nodeLayer = QgsLayerTree::toLayer( node );
if ( nodeLayer->isVisible() )
if ( node->itemVisibilityChecked() != Qt::Unchecked )
rec.mLayerRecords << createThemeLayerRecord( nodeLayer, model );
}
}
Expand All @@ -101,6 +103,7 @@ QgsMapThemeCollection::MapThemeRecord QgsMapThemeCollection::createThemeFromCurr
{
QgsMapThemeCollection::MapThemeRecord rec;
rec.setHasExpandedStateInfo( true ); // all newly created theme records have expanded state info
rec.setHasCheckedStateInfo( true ); // all newly created theme records have checked state info
createThemeFromCurrentState( root, model, rec );
return rec;
}
Expand Down Expand Up @@ -182,6 +185,8 @@ void QgsMapThemeCollection::applyThemeToGroup( QgsLayerTreeGroup *parent, QgsLay
applyThemeToGroup( QgsLayerTree::toGroup( node ), model, rec );
if ( rec.hasExpandedStateInfo() )
node->setExpanded( rec.expandedGroupNodes().contains( _groupId( node ) ) );
if ( rec.hasCheckedStateInfo() )
node->setItemVisibilityChecked( rec.checkedGroupNodes().contains( _groupId( node ) ) );
}
else if ( QgsLayerTree::isLayer( node ) )
applyThemeToLayer( QgsLayerTree::toLayer( node ), model, rec );
Expand Down Expand Up @@ -431,6 +436,10 @@ void QgsMapThemeCollection::readXml( const QDomDocument &doc )
if ( visPresetElem.hasAttribute( QStringLiteral( "has-expanded-info" ) ) )
expandedStateInfo = visPresetElem.attribute( QStringLiteral( "has-expanded-info" ) ).toInt();

bool checkedStateInfo = false;
if ( visPresetElem.hasAttribute( QStringLiteral( "has-checked-group-info" ) ) )
checkedStateInfo = visPresetElem.attribute( QStringLiteral( "has-checked-group-info" ) ).toInt();

QString presetName = visPresetElem.attribute( QStringLiteral( "name" ) );
QDomElement visPresetLayerElem = visPresetElem.firstChildElement( QStringLiteral( "layer" ) );
while ( !visPresetLayerElem.isNull() )
Expand Down Expand Up @@ -510,10 +519,28 @@ void QgsMapThemeCollection::readXml( const QDomDocument &doc )
}
}

QSet<QString> checkedGroupNodes;
if ( checkedStateInfo )
{
// expanded state of legend nodes
QDomElement checkedGroupNodesElem = visPresetElem.firstChildElement( QStringLiteral( "checked-group-nodes" ) );
if ( !checkedGroupNodesElem.isNull() )
{
QDomElement checkedGroupNodeElem = checkedGroupNodesElem.firstChildElement( QStringLiteral( "checked-group-node" ) );
while ( !checkedGroupNodeElem.isNull() )
{
checkedGroupNodes << checkedGroupNodeElem.attribute( QStringLiteral( "id" ) );
checkedGroupNodeElem = checkedGroupNodeElem.nextSiblingElement( QStringLiteral( "checked-group-node" ) );
}
}
}

MapThemeRecord rec;
rec.setLayerRecords( layerRecords.values() );
rec.setHasExpandedStateInfo( expandedStateInfo );
rec.setExpandedGroupNodes( expandedGroupNodes );
rec.setHasCheckedStateInfo( checkedStateInfo );
rec.setCheckedGroupNodes( checkedGroupNodes );
mMapThemes.insert( presetName, rec );
emit mapThemeChanged( presetName );

Expand All @@ -539,6 +566,8 @@ void QgsMapThemeCollection::writeXml( QDomDocument &doc )
visPresetElem.setAttribute( QStringLiteral( "name" ), grpName );
if ( rec.hasExpandedStateInfo() )
visPresetElem.setAttribute( QStringLiteral( "has-expanded-info" ), QStringLiteral( "1" ) );
if ( rec.hasCheckedStateInfo() )
visPresetElem.setAttribute( QStringLiteral( "has-checked-group-info" ), QStringLiteral( "1" ) );
for ( const MapThemeLayerRecord &layerRec : qgis::as_const( rec.mLayerRecords ) )
{
if ( !layerRec.layer() )
Expand Down Expand Up @@ -579,6 +608,19 @@ void QgsMapThemeCollection::writeXml( QDomDocument &doc )
}
}

if ( rec.hasCheckedStateInfo() )
{
QDomElement checkedGroupElems = doc.createElement( QStringLiteral( "checked-group-nodes" ) );
const QSet<QString> checkedGroupNodes = rec.checkedGroupNodes();
for ( const QString &groupId : checkedGroupNodes )
{
QDomElement checkedGroupElem = doc.createElement( QStringLiteral( "checked-group-node" ) );
checkedGroupElem.setAttribute( QStringLiteral( "id" ), groupId );
checkedGroupElems.appendChild( checkedGroupElem );
}
visPresetElem.appendChild( checkedGroupElems );
}

if ( rec.hasExpandedStateInfo() )
{
QDomElement expandedGroupElems = doc.createElement( QStringLiteral( "expanded-group-nodes" ) );
Expand Down
42 changes: 40 additions & 2 deletions src/core/qgsmapthemecollection.h
Expand Up @@ -118,7 +118,8 @@ class CORE_EXPORT QgsMapThemeCollection : public QObject
bool operator==( const QgsMapThemeCollection::MapThemeRecord &other ) const
{
return validLayerRecords() == other.validLayerRecords() &&
mHasExpandedStateInfo == other.mHasExpandedStateInfo && mExpandedGroupNodes == other.mExpandedGroupNodes;
mHasExpandedStateInfo == other.mHasExpandedStateInfo &&
mExpandedGroupNodes == other.mExpandedGroupNodes && mCheckedGroupNodes == other.mCheckedGroupNodes;
}
bool operator!=( const QgsMapThemeCollection::MapThemeRecord &other ) const
{
Expand All @@ -139,7 +140,6 @@ class CORE_EXPORT QgsMapThemeCollection : public QObject

/**
* Returns set with only records for valid layers
* \note not available in Python bindings
*/
QHash<QgsMapLayer *, QgsMapThemeCollection::MapThemeLayerRecord> validLayerRecords() const SIP_SKIP;

Expand All @@ -150,12 +150,27 @@ class CORE_EXPORT QgsMapThemeCollection : public QObject
*/
bool hasExpandedStateInfo() const { return mHasExpandedStateInfo; }

/**
* Returns whether information about checked/unchecked state of groups has been recorded
* and thus whether checkedGroupNodes() is valid.
* \note Not available in Python bindings
* \since QGIS 3.10.1
*/
bool hasCheckedStateInfo() const { return mHasCheckedStateInfo; } SIP_SKIP;

/**
* Sets whether the map theme contains valid expanded/collapsed state of nodes
* \since QGIS 3.2
*/
void setHasExpandedStateInfo( bool hasInfo ) { mHasExpandedStateInfo = hasInfo; }

/**
* Sets whether the map theme contains valid checked/unchecked state of group nodes
* \note Not available in Python bindings
* \since QGIS 3.10.1
*/
void setHasCheckedStateInfo( bool hasInfo ) { mHasCheckedStateInfo = hasInfo; } SIP_SKIP;

/**
* Returns a set of group identifiers for group nodes that should have expanded state (other group nodes should be collapsed).
* The returned value is valid only when hasExpandedStateInfo() returns TRUE.
Expand All @@ -165,25 +180,48 @@ class CORE_EXPORT QgsMapThemeCollection : public QObject
*/
QSet<QString> expandedGroupNodes() const { return mExpandedGroupNodes; }

/**
* Returns a set of group identifiers for group nodes that should have checked state (other group nodes should be unchecked).
* The returned value is valid only when hasCheckedStateInfo() returns TRUE.
* Group identifiers are built using group names, a sub-group name is prepended by parent group's identifier
* and a forward slash, e.g. "level1/level2"
* \since QGIS 3.10.1
*/
QSet<QString> checkedGroupNodes() const { return mCheckedGroupNodes; }

/**
* Sets a set of group identifiers for group nodes that should have expanded state. See expandedGroupNodes().
* \since QGIS 3.2
*/
void setExpandedGroupNodes( const QSet<QString> &expandedGroupNodes ) { mExpandedGroupNodes = expandedGroupNodes; }

/**
* Sets a set of group identifiers for group nodes that should have checked state. See checkedGroupNodes().
* \since QGIS 3.10.1
*/
void setCheckedGroupNodes( const QSet<QString> &checkedGroupNodes ) { mCheckedGroupNodes = checkedGroupNodes; }

private:
//! Layer-specific records for the theme. Only visible layers are listed.
QList<MapThemeLayerRecord> mLayerRecords;

//! Whether the information about expanded/collapsed state of groups, layers and legend items has been stored
bool mHasExpandedStateInfo = false;
//! Whether the information about checked/unchecked state of groups, layers and legend items has been stored
bool mHasCheckedStateInfo = false;

/**
* Which groups should be expanded. Each group is identified by its name (sub-groups IDs are prepended with parent
* group and forward slash - e.g. "level1/level2/level3").
*/
QSet<QString> mExpandedGroupNodes;

/**
* Which groups should be checked. Each group is identified by its name (sub-groups IDs are prepended with parent
* group and forward slash - e.g. "level1/level2/level3").
*/
QSet<QString> mCheckedGroupNodes;

friend class QgsMapThemeCollection;
};

Expand Down
80 changes: 80 additions & 0 deletions tests/src/core/testqgsmapthemecollection.cpp
Expand Up @@ -34,6 +34,7 @@ class TestQgsMapThemeCollection : public QObject
void cleanupTestCase();// will be called after the last testfunction was executed.

void expandedState();
void checkedState();

private:
QgsVectorLayer *mPointsLayer = nullptr;
Expand Down Expand Up @@ -206,5 +207,84 @@ void TestQgsMapThemeCollection::expandedState()
QCOMPARE( recExpandedPoints2.expandedLegendItems.count(), 4 );
}

void TestQgsMapThemeCollection::checkedState()
{
QgsMapThemeCollection themes( mProject );

QStringList pointLayerRootLegendNodes;
const QList<QgsLayerTreeModelLegendNode *> legendNodes = mLayerTreeModel->layerLegendNodes( mNodeLayerPoints, true );
for ( QgsLayerTreeModelLegendNode *legendNode : legendNodes )
{
QString key = legendNode->data( QgsLayerTreeModelLegendNode::RuleKeyRole ).toString();
pointLayerRootLegendNodes << key;
}
QCOMPARE( pointLayerRootLegendNodes.count(), 4 );

// make two themes: 1. all checked, 2. all collapsed
// keep a layer checked inside group3 to check behavior

mNodeGroup1->setItemVisibilityChecked( false );
mNodeGroup2->setItemVisibilityChecked( false );
mNodeGroup3->setItemVisibilityChecked( false );
mNodeLayerLines->setItemVisibilityChecked( false );
mNodeLayerPolys->setItemVisibilityChecked( true );

themes.insert( "all-unchecked", QgsMapThemeCollection::createThemeFromCurrentState( mLayerTree, mLayerTreeModel ) );

mNodeGroup1->setItemVisibilityChecked( true );
mNodeGroup2->setItemVisibilityChecked( true );
mNodeGroup3->setItemVisibilityChecked( true );
// keep a layer checked inside a group to check behavior
mNodeLayerPolys->setItemVisibilityChecked( true );

themes.insert( "all-checked", QgsMapThemeCollection::createThemeFromCurrentState( mLayerTree, mLayerTreeModel ) );

// check theme data

QgsMapThemeCollection::MapThemeRecord recUnchecked = themes.mapThemeState( "all-unchecked" );
QVERIFY( recUnchecked.hasCheckedStateInfo() );
QCOMPARE( recUnchecked.checkedGroupNodes().count(), 0 );
QCOMPARE( mNodeLayerLines->itemVisibilityChecked(), false );
QCOMPARE( mNodeLayerPolys->itemVisibilityChecked(), true );

QgsMapThemeCollection::MapThemeRecord recChecked = themes.mapThemeState( "all-checked" );
QVERIFY( recChecked.hasCheckedStateInfo() );
QCOMPARE( recChecked.checkedGroupNodes().count(), 3 );
QCOMPARE( mNodeLayerLines->itemVisibilityChecked(), false );
QCOMPARE( mNodeLayerPolys->itemVisibilityChecked(), true );

// switch themes

themes.applyTheme( "all-unchecked", mLayerTree, mLayerTreeModel );
QCOMPARE( mNodeGroup1->itemVisibilityChecked(), false );
QCOMPARE( mNodeGroup3->itemVisibilityChecked(), false );
QCOMPARE( mNodeLayerLines->itemVisibilityChecked(), false );
QCOMPARE( mNodeLayerPolys->itemVisibilityChecked(), true );

themes.applyTheme( "all-checked", mLayerTree, mLayerTreeModel );
QCOMPARE( mNodeGroup1->itemVisibilityChecked(), true );
QCOMPARE( mNodeGroup3->itemVisibilityChecked(), true );
QCOMPARE( mNodeLayerLines->itemVisibilityChecked(), false );
QCOMPARE( mNodeLayerPolys->itemVisibilityChecked(), true );

// test read+write

QDomDocument doc;
QDomElement elemRoot = doc.createElement( "qgis" );
doc.appendChild( elemRoot );
themes.writeXml( doc );

QgsMapThemeCollection themes2( mProject );
themes2.readXml( doc );

QgsMapThemeCollection::MapThemeRecord recUnchecked2 = themes2.mapThemeState( "all-unchecked" );
QVERIFY( recUnchecked2.hasCheckedStateInfo() );
QCOMPARE( recUnchecked2.checkedGroupNodes().count(), 0 );

QgsMapThemeCollection::MapThemeRecord recChecked2 = themes2.mapThemeState( "all-checked" );
QVERIFY( recChecked2.hasCheckedStateInfo() );
QCOMPARE( recChecked2.checkedGroupNodes().count(), 3 );
}

QGSTEST_MAIN( TestQgsMapThemeCollection )
#include "testqgsmapthemecollection.moc"

0 comments on commit 1f818ad

Please sign in to comment.