Skip to content

Commit 3400199

Browse files
committed
[FEATURE] Map themes: store also expanded/collapsed state of nodes
Each map theme will also record which layers, groups and legend items are expanded, so when a map theme is selected, the expanded/collapsed states get applied in the layer tree.
1 parent 29b080f commit 3400199

7 files changed

+426
-2
lines changed

python/core/qgsmapthemecollection.sip.in

+36
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,10 @@ Set the map layer for this record
5959
QString currentStyle;
6060
bool usingLegendItems;
6161
QSet<QString> checkedLegendItems;
62+
63+
QSet<QString> expandedLegendItems;
64+
65+
bool expandedLayerNode;
6266
};
6367

6468
class MapThemeRecord
@@ -98,6 +102,38 @@ Add a new record for a layer.
98102
%End
99103

100104

105+
bool hasExpandedStateInfo() const;
106+
%Docstring
107+
Returns whether information about expanded/collapsed state of nodes has been recorded
108+
and thus whether expandedGroupNodes() and expandedLegendItems + expandedLayerNode from layer records are valid.
109+
110+
.. versionadded:: 3.2
111+
%End
112+
113+
void setHasExpandedStateInfo( bool hasInfo );
114+
%Docstring
115+
Sets whether the map theme contains valid expanded/collapsed state of nodes
116+
117+
.. versionadded:: 3.2
118+
%End
119+
120+
QSet<QString> expandedGroupNodes() const;
121+
%Docstring
122+
Returns a set of group identifiers for group nodes that should have expanded state (other group nodes should be collapsed).
123+
The returned value is valid only when hasExpandedStateInfo() returns true.
124+
Group identifiers are built using group names, a sub-group name is prepended by parent group's identifier
125+
and a forward slash, e.g. "level1/level2"
126+
127+
.. versionadded:: 3.2
128+
%End
129+
130+
void setExpandedGroupNodes( const QSet<QString> &expandedGroupNodes );
131+
%Docstring
132+
Sets a set of group identifiers for group nodes that should have expanded state. See expandedGroupNodes().
133+
134+
.. versionadded:: 3.2
135+
%End
136+
101137
};
102138

103139
QgsMapThemeCollection( QgsProject *project = 0 );

src/core/qgsmapthemecollection.cpp

+105
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ QgsMapThemeCollection::MapThemeLayerRecord QgsMapThemeCollection::createThemeLay
3737
MapThemeLayerRecord layerRec( nodeLayer->layer() );
3838
layerRec.usingCurrentStyle = true;
3939
layerRec.currentStyle = nodeLayer->layer()->styleManager()->currentStyle();
40+
layerRec.expandedLayerNode = nodeLayer->isExpanded();
41+
layerRec.expandedLegendItems = nodeLayer->customProperty( QStringLiteral( "expandedLegendNodes" ) ).toStringList().toSet();
4042

4143
// get checked legend items
4244
bool hasCheckableItems = false;
@@ -63,12 +65,27 @@ QgsMapThemeCollection::MapThemeLayerRecord QgsMapThemeCollection::createThemeLay
6365
return layerRec;
6466
}
6567

68+
static QString _groupId( QgsLayerTreeNode *node )
69+
{
70+
QStringList lst;
71+
while ( node->parent() )
72+
{
73+
lst.prepend( node->name() );
74+
node = node->parent();
75+
}
76+
return lst.join( '/' );
77+
}
78+
6679
void QgsMapThemeCollection::createThemeFromCurrentState( QgsLayerTreeGroup *parent, QgsLayerTreeModel *model, QgsMapThemeCollection::MapThemeRecord &rec )
6780
{
6881
Q_FOREACH ( QgsLayerTreeNode *node, parent->children() )
6982
{
7083
if ( QgsLayerTree::isGroup( node ) )
84+
{
7185
createThemeFromCurrentState( QgsLayerTree::toGroup( node ), model, rec );
86+
if ( node->isExpanded() )
87+
rec.mExpandedGroupNodes.insert( _groupId( node ) );
88+
}
7289
else if ( QgsLayerTree::isLayer( node ) )
7390
{
7491
QgsLayerTreeLayer *nodeLayer = QgsLayerTree::toLayer( node );
@@ -81,6 +98,7 @@ void QgsMapThemeCollection::createThemeFromCurrentState( QgsLayerTreeGroup *pare
8198
QgsMapThemeCollection::MapThemeRecord QgsMapThemeCollection::createThemeFromCurrentState( QgsLayerTreeGroup *root, QgsLayerTreeModel *model )
8299
{
83100
QgsMapThemeCollection::MapThemeRecord rec;
101+
rec.setHasExpandedStateInfo( true ); // all newly created theme records have expanded state info
84102
createThemeFromCurrentState( root, model, rec );
85103
return rec;
86104
}
@@ -140,6 +158,13 @@ void QgsMapThemeCollection::applyThemeToLayer( QgsLayerTreeLayer *nodeLayer, Qgs
140158
legendNode->setData( Qt::Checked, Qt::CheckStateRole );
141159
}
142160
}
161+
162+
// apply expanded/collapsed state to the layer and its legend nodes
163+
if ( rec.hasExpandedStateInfo() )
164+
{
165+
nodeLayer->setExpanded( layerRec.expandedLayerNode );
166+
nodeLayer->setCustomProperty( QStringLiteral( "expandedLegendNodes" ), QStringList( layerRec.expandedLegendItems.toList() ) );
167+
}
143168
}
144169

145170

@@ -148,7 +173,11 @@ void QgsMapThemeCollection::applyThemeToGroup( QgsLayerTreeGroup *parent, QgsLay
148173
Q_FOREACH ( QgsLayerTreeNode *node, parent->children() )
149174
{
150175
if ( QgsLayerTree::isGroup( node ) )
176+
{
151177
applyThemeToGroup( QgsLayerTree::toGroup( node ), model, rec );
178+
if ( rec.hasExpandedStateInfo() )
179+
node->setExpanded( rec.expandedGroupNodes().contains( _groupId( node ) ) );
180+
}
152181
else if ( QgsLayerTree::isLayer( node ) )
153182
applyThemeToLayer( QgsLayerTree::toLayer( node ), model, rec );
154183
}
@@ -385,6 +414,10 @@ void QgsMapThemeCollection::readXml( const QDomDocument &doc )
385414
{
386415
QHash<QString, MapThemeLayerRecord> layerRecords; // key = layer ID
387416

417+
bool expandedStateInfo = false;
418+
if ( visPresetElem.hasAttribute( QStringLiteral( "has-expanded-info" ) ) )
419+
expandedStateInfo = visPresetElem.attribute( QStringLiteral( "has-expanded-info" ) ).toInt();
420+
388421
QString presetName = visPresetElem.attribute( QStringLiteral( "name" ) );
389422
QDomElement visPresetLayerElem = visPresetElem.firstChildElement( QStringLiteral( "layer" ) );
390423
while ( !visPresetLayerElem.isNull() )
@@ -399,6 +432,9 @@ void QgsMapThemeCollection::readXml( const QDomDocument &doc )
399432
layerRecords[layerID].usingCurrentStyle = true;
400433
layerRecords[layerID].currentStyle = visPresetLayerElem.attribute( QStringLiteral( "style" ) );
401434
}
435+
436+
if ( visPresetLayerElem.hasAttribute( QStringLiteral( "expanded" ) ) )
437+
layerRecords[layerID].expandedLayerNode = visPresetLayerElem.attribute( QStringLiteral( "expanded" ) ).toInt();
402438
}
403439
visPresetLayerElem = visPresetLayerElem.nextSiblingElement( QStringLiteral( "layer" ) );
404440
}
@@ -424,8 +460,47 @@ void QgsMapThemeCollection::readXml( const QDomDocument &doc )
424460
checkedLegendNodesElem = checkedLegendNodesElem.nextSiblingElement( QStringLiteral( "checked-legend-nodes" ) );
425461
}
426462

463+
QSet<QString> expandedGroupNodes;
464+
if ( expandedStateInfo )
465+
{
466+
// expanded state of legend nodes
467+
QDomElement expandedLegendNodesElem = visPresetElem.firstChildElement( QStringLiteral( "expanded-legend-nodes" ) );
468+
while ( !expandedLegendNodesElem.isNull() )
469+
{
470+
QSet<QString> expandedLegendNodes;
471+
472+
QDomElement expandedLegendNodeElem = expandedLegendNodesElem.firstChildElement( QStringLiteral( "expanded-legend-node" ) );
473+
while ( !expandedLegendNodeElem.isNull() )
474+
{
475+
expandedLegendNodes << expandedLegendNodeElem.attribute( QStringLiteral( "id" ) );
476+
expandedLegendNodeElem = expandedLegendNodeElem.nextSiblingElement( QStringLiteral( "expanded-legend-node" ) );
477+
}
478+
479+
QString layerID = expandedLegendNodesElem.attribute( QStringLiteral( "id" ) );
480+
if ( mProject->mapLayer( layerID ) ) // only use valid IDs
481+
{
482+
layerRecords[layerID].expandedLegendItems = expandedLegendNodes;
483+
}
484+
expandedLegendNodesElem = expandedLegendNodesElem.nextSiblingElement( QStringLiteral( "expanded-legend-nodes" ) );
485+
}
486+
487+
// expanded state of group nodes
488+
QDomElement expandedGroupNodesElem = visPresetElem.firstChildElement( QStringLiteral( "expanded-group-nodes" ) );
489+
if ( !expandedGroupNodesElem.isNull() )
490+
{
491+
QDomElement expandedGroupNodeElem = expandedGroupNodesElem.firstChildElement( QStringLiteral( "expanded-group-node" ) );
492+
while ( !expandedGroupNodeElem.isNull() )
493+
{
494+
expandedGroupNodes << expandedGroupNodeElem.attribute( QStringLiteral( "id" ) );
495+
expandedGroupNodeElem = expandedGroupNodeElem.nextSiblingElement( QStringLiteral( "expanded-group-node" ) );
496+
}
497+
}
498+
}
499+
427500
MapThemeRecord rec;
428501
rec.setLayerRecords( layerRecords.values() );
502+
rec.setHasExpandedStateInfo( expandedStateInfo );
503+
rec.setExpandedGroupNodes( expandedGroupNodes );
429504
mMapThemes.insert( presetName, rec );
430505
emit mapThemeChanged( presetName );
431506

@@ -446,6 +521,8 @@ void QgsMapThemeCollection::writeXml( QDomDocument &doc )
446521
const MapThemeRecord &rec = it.value();
447522
QDomElement visPresetElem = doc.createElement( QStringLiteral( "visibility-preset" ) );
448523
visPresetElem.setAttribute( QStringLiteral( "name" ), grpName );
524+
if ( rec.hasExpandedStateInfo() )
525+
visPresetElem.setAttribute( QStringLiteral( "has-expanded-info" ), QStringLiteral( "1" ) );
449526
Q_FOREACH ( const MapThemeLayerRecord &layerRec, rec.mLayerRecords )
450527
{
451528
if ( !layerRec.layer() )
@@ -469,6 +546,34 @@ void QgsMapThemeCollection::writeXml( QDomDocument &doc )
469546
}
470547
visPresetElem.appendChild( checkedLegendNodesElem );
471548
}
549+
550+
if ( rec.hasExpandedStateInfo() )
551+
{
552+
layerElem.setAttribute( QStringLiteral( "expanded" ), layerRec.expandedLayerNode ? QStringLiteral( "1" ) : QStringLiteral( "0" ) );
553+
554+
QDomElement expandedLegendNodesElem = doc.createElement( QStringLiteral( "expanded-legend-nodes" ) );
555+
expandedLegendNodesElem.setAttribute( QStringLiteral( "id" ), layerID );
556+
for ( const QString &expandedLegendNode : qgis::as_const( layerRec.expandedLegendItems ) )
557+
{
558+
QDomElement expandedLegendNodeElem = doc.createElement( QStringLiteral( "expanded-legend-node" ) );
559+
expandedLegendNodeElem.setAttribute( QStringLiteral( "id" ), expandedLegendNode );
560+
expandedLegendNodesElem.appendChild( expandedLegendNodeElem );
561+
}
562+
visPresetElem.appendChild( expandedLegendNodesElem );
563+
}
564+
}
565+
566+
if ( rec.hasExpandedStateInfo() )
567+
{
568+
QDomElement expandedGroupElems = doc.createElement( QStringLiteral( "expanded-group-nodes" ) );
569+
const QSet<QString> expandedGroupNodes = rec.expandedGroupNodes();
570+
for ( const QString &groupId : expandedGroupNodes )
571+
{
572+
QDomElement expandedGroupElem = doc.createElement( QStringLiteral( "expanded-group-node" ) );
573+
expandedGroupElem.setAttribute( QStringLiteral( "id" ), groupId );
574+
expandedGroupElems.appendChild( expandedGroupElem );
575+
}
576+
visPresetElem.appendChild( expandedGroupElems );
472577
}
473578

474579
visPresetsElem.appendChild( visPresetElem );

src/core/qgsmapthemecollection.h

+54-2
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,8 @@ class CORE_EXPORT QgsMapThemeCollection : public QObject
6565
{
6666
return mLayer == other.mLayer &&
6767
usingCurrentStyle == other.usingCurrentStyle && currentStyle == other.currentStyle &&
68-
usingLegendItems == other.usingLegendItems && checkedLegendItems == other.checkedLegendItems;
68+
usingLegendItems == other.usingLegendItems && checkedLegendItems == other.checkedLegendItems &&
69+
expandedLegendItems == other.expandedLegendItems && expandedLayerNode == other.expandedLayerNode;
6970
}
7071
bool operator!=( const QgsMapThemeCollection::MapThemeLayerRecord &other ) const
7172
{
@@ -86,6 +87,19 @@ class CORE_EXPORT QgsMapThemeCollection : public QObject
8687
bool usingLegendItems = false;
8788
//! Rule keys of check legend items in layer tree model
8889
QSet<QString> checkedLegendItems;
90+
91+
/**
92+
* Rule keys of expanded legend items in layer tree view.
93+
* \since QGIS 3.2
94+
*/
95+
QSet<QString> expandedLegendItems;
96+
97+
/**
98+
* Whether the layer's tree node is expanded
99+
* (only to be applied if the parent MapThemeRecord has the information about expanded nodes stored)
100+
* \since QGIS 3.2
101+
*/
102+
bool expandedLayerNode = false;
89103
private:
90104
//! Weak pointer to the layer
91105
QgsWeakMapLayerPointer mLayer;
@@ -103,7 +117,8 @@ class CORE_EXPORT QgsMapThemeCollection : public QObject
103117

104118
bool operator==( const QgsMapThemeCollection::MapThemeRecord &other ) const
105119
{
106-
return validLayerRecords() == other.validLayerRecords();
120+
return validLayerRecords() == other.validLayerRecords() &&
121+
mHasExpandedStateInfo == other.mHasExpandedStateInfo && mExpandedGroupNodes == other.mExpandedGroupNodes;
107122
}
108123
bool operator!=( const QgsMapThemeCollection::MapThemeRecord &other ) const
109124
{
@@ -128,10 +143,47 @@ class CORE_EXPORT QgsMapThemeCollection : public QObject
128143
*/
129144
QHash<QgsMapLayer *, QgsMapThemeCollection::MapThemeLayerRecord> validLayerRecords() const SIP_SKIP;
130145

146+
/**
147+
* Returns whether information about expanded/collapsed state of nodes has been recorded
148+
* and thus whether expandedGroupNodes() and expandedLegendItems + expandedLayerNode from layer records are valid.
149+
* \since QGIS 3.2
150+
*/
151+
bool hasExpandedStateInfo() const { return mHasExpandedStateInfo; }
152+
153+
/**
154+
* Sets whether the map theme contains valid expanded/collapsed state of nodes
155+
* \since QGIS 3.2
156+
*/
157+
void setHasExpandedStateInfo( bool hasInfo ) { mHasExpandedStateInfo = hasInfo; }
158+
159+
/**
160+
* Returns a set of group identifiers for group nodes that should have expanded state (other group nodes should be collapsed).
161+
* The returned value is valid only when hasExpandedStateInfo() returns true.
162+
* Group identifiers are built using group names, a sub-group name is prepended by parent group's identifier
163+
* and a forward slash, e.g. "level1/level2"
164+
* \since QGIS 3.2
165+
*/
166+
QSet<QString> expandedGroupNodes() const { return mExpandedGroupNodes; }
167+
168+
/**
169+
* Sets a set of group identifiers for group nodes that should have expanded state. See expandedGroupNodes().
170+
* \since QGIS 3.2
171+
*/
172+
void setExpandedGroupNodes( const QSet<QString> &expandedGroupNodes ) { mExpandedGroupNodes = expandedGroupNodes; }
173+
131174
private:
132175
//! Layer-specific records for the theme. Only visible layers are listed.
133176
QList<MapThemeLayerRecord> mLayerRecords;
134177

178+
//! Whether the information about expanded/collapsed state of groups, layers and legend items has been stored
179+
bool mHasExpandedStateInfo = false;
180+
181+
/**
182+
* Which groups should be expanded. Each group is identified by its name (sub-groups IDs are prepended with parent
183+
* group and forward slash - e.g. "level1/level2/level3").
184+
*/
185+
QSet<QString> mExpandedGroupNodes;
186+
135187
friend class QgsMapThemeCollection;
136188
};
137189

src/gui/layertree/qgslayertreeview.cpp

+17
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ void QgsLayerTreeView::setModel( QAbstractItemModel *model )
6969
QTreeView::setModel( model );
7070

7171
connect( layerTreeModel()->rootGroup(), &QgsLayerTreeNode::expandedChanged, this, &QgsLayerTreeView::onExpandedChanged );
72+
connect( layerTreeModel()->rootGroup(), &QgsLayerTreeNode::customPropertyChanged, this, &QgsLayerTreeView::onCustomPropertyChanged );
7273

7374
connect( selectionModel(), &QItemSelectionModel::currentChanged, this, &QgsLayerTreeView::onCurrentChanged );
7475

@@ -244,6 +245,22 @@ void QgsLayerTreeView::onExpandedChanged( QgsLayerTreeNode *node, bool expanded
244245
setExpanded( idx, expanded );
245246
}
246247

248+
void QgsLayerTreeView::onCustomPropertyChanged( QgsLayerTreeNode *node, const QString &key )
249+
{
250+
if ( key != QStringLiteral( "expandedLegendNodes" ) || !QgsLayerTree::isLayer( node ) )
251+
return;
252+
253+
QSet<QString> expandedLegendNodes = node->customProperty( QStringLiteral( "expandedLegendNodes" ) ).toStringList().toSet();
254+
255+
const QList<QgsLayerTreeModelLegendNode *> legendNodes = layerTreeModel()->layerLegendNodes( QgsLayerTree::toLayer( node ), true );
256+
for ( QgsLayerTreeModelLegendNode *legendNode : legendNodes )
257+
{
258+
QString key = legendNode->data( QgsLayerTreeModelLegendNode::RuleKeyRole ).toString();
259+
if ( !key.isEmpty() )
260+
setExpanded( layerTreeModel()->legendNode2index( legendNode ), expandedLegendNodes.contains( key ) );
261+
}
262+
}
263+
247264
void QgsLayerTreeView::onModelReset()
248265
{
249266
updateExpandedStateFromNode( layerTreeModel()->rootGroup() );

src/gui/layertree/qgslayertreeview.h

+3
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,9 @@ class GUI_EXPORT QgsLayerTreeView : public QTreeView
192192
void onExpandedChanged( QgsLayerTreeNode *node, bool expanded );
193193
void onModelReset();
194194

195+
private slots:
196+
void onCustomPropertyChanged( QgsLayerTreeNode *node, const QString &key );
197+
195198
protected:
196199
//! helper class with default actions. Lazily initialized.
197200
QgsLayerTreeViewDefaultActions *mDefaultActions = nullptr;

tests/src/core/CMakeLists.txt

+1
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ SET(TESTS
141141
testqgsmaprotation.cpp
142142
testqgsmapsettings.cpp
143143
testqgsmapsettingsutils.cpp
144+
testqgsmapthemecollection.cpp
144145
testqgsmaptopixelgeometrysimplifier.cpp
145146
testqgsmaptopixel.cpp
146147
testqgsmarkerlinesymbol.cpp

0 commit comments

Comments
 (0)