Skip to content

Commit

Permalink
[FEATURE] Map themes: store also expanded/collapsed state of nodes
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
wonder-sk committed Apr 11, 2018
1 parent 29b080f commit 3400199
Show file tree
Hide file tree
Showing 7 changed files with 426 additions and 2 deletions.
36 changes: 36 additions & 0 deletions python/core/qgsmapthemecollection.sip.in
Expand Up @@ -59,6 +59,10 @@ Set the map layer for this record
QString currentStyle;
bool usingLegendItems;
QSet<QString> checkedLegendItems;

QSet<QString> expandedLegendItems;

bool expandedLayerNode;
};

class MapThemeRecord
Expand Down Expand Up @@ -98,6 +102,38 @@ Add a new record for a layer.
%End


bool hasExpandedStateInfo() const;
%Docstring
Returns whether information about expanded/collapsed state of nodes has been recorded
and thus whether expandedGroupNodes() and expandedLegendItems + expandedLayerNode from layer records are valid.

.. versionadded:: 3.2
%End

void setHasExpandedStateInfo( bool hasInfo );
%Docstring
Sets whether the map theme contains valid expanded/collapsed state of nodes

.. versionadded:: 3.2
%End

QSet<QString> expandedGroupNodes() const;
%Docstring
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.
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.2
%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

};

QgsMapThemeCollection( QgsProject *project = 0 );
Expand Down
105 changes: 105 additions & 0 deletions src/core/qgsmapthemecollection.cpp
Expand Up @@ -37,6 +37,8 @@ QgsMapThemeCollection::MapThemeLayerRecord QgsMapThemeCollection::createThemeLay
MapThemeLayerRecord layerRec( nodeLayer->layer() );
layerRec.usingCurrentStyle = true;
layerRec.currentStyle = nodeLayer->layer()->styleManager()->currentStyle();
layerRec.expandedLayerNode = nodeLayer->isExpanded();
layerRec.expandedLegendItems = nodeLayer->customProperty( QStringLiteral( "expandedLegendNodes" ) ).toStringList().toSet();

// get checked legend items
bool hasCheckableItems = false;
Expand All @@ -63,12 +65,27 @@ QgsMapThemeCollection::MapThemeLayerRecord QgsMapThemeCollection::createThemeLay
return layerRec;
}

static QString _groupId( QgsLayerTreeNode *node )
{
QStringList lst;
while ( node->parent() )
{
lst.prepend( node->name() );
node = node->parent();
}
return lst.join( '/' );
}

void QgsMapThemeCollection::createThemeFromCurrentState( QgsLayerTreeGroup *parent, QgsLayerTreeModel *model, QgsMapThemeCollection::MapThemeRecord &rec )
{
Q_FOREACH ( QgsLayerTreeNode *node, parent->children() )
{
if ( QgsLayerTree::isGroup( node ) )
{
createThemeFromCurrentState( QgsLayerTree::toGroup( node ), model, rec );
if ( node->isExpanded() )
rec.mExpandedGroupNodes.insert( _groupId( node ) );
}
else if ( QgsLayerTree::isLayer( node ) )
{
QgsLayerTreeLayer *nodeLayer = QgsLayerTree::toLayer( node );
Expand All @@ -81,6 +98,7 @@ void QgsMapThemeCollection::createThemeFromCurrentState( QgsLayerTreeGroup *pare
QgsMapThemeCollection::MapThemeRecord QgsMapThemeCollection::createThemeFromCurrentState( QgsLayerTreeGroup *root, QgsLayerTreeModel *model )
{
QgsMapThemeCollection::MapThemeRecord rec;
rec.setHasExpandedStateInfo( true ); // all newly created theme records have expanded state info
createThemeFromCurrentState( root, model, rec );
return rec;
}
Expand Down Expand Up @@ -140,6 +158,13 @@ void QgsMapThemeCollection::applyThemeToLayer( QgsLayerTreeLayer *nodeLayer, Qgs
legendNode->setData( Qt::Checked, Qt::CheckStateRole );
}
}

// apply expanded/collapsed state to the layer and its legend nodes
if ( rec.hasExpandedStateInfo() )
{
nodeLayer->setExpanded( layerRec.expandedLayerNode );
nodeLayer->setCustomProperty( QStringLiteral( "expandedLegendNodes" ), QStringList( layerRec.expandedLegendItems.toList() ) );
}
}


Expand All @@ -148,7 +173,11 @@ void QgsMapThemeCollection::applyThemeToGroup( QgsLayerTreeGroup *parent, QgsLay
Q_FOREACH ( QgsLayerTreeNode *node, parent->children() )
{
if ( QgsLayerTree::isGroup( node ) )
{
applyThemeToGroup( QgsLayerTree::toGroup( node ), model, rec );
if ( rec.hasExpandedStateInfo() )
node->setExpanded( rec.expandedGroupNodes().contains( _groupId( node ) ) );
}
else if ( QgsLayerTree::isLayer( node ) )
applyThemeToLayer( QgsLayerTree::toLayer( node ), model, rec );
}
Expand Down Expand Up @@ -385,6 +414,10 @@ void QgsMapThemeCollection::readXml( const QDomDocument &doc )
{
QHash<QString, MapThemeLayerRecord> layerRecords; // key = layer ID

bool expandedStateInfo = false;
if ( visPresetElem.hasAttribute( QStringLiteral( "has-expanded-info" ) ) )
expandedStateInfo = visPresetElem.attribute( QStringLiteral( "has-expanded-info" ) ).toInt();

QString presetName = visPresetElem.attribute( QStringLiteral( "name" ) );
QDomElement visPresetLayerElem = visPresetElem.firstChildElement( QStringLiteral( "layer" ) );
while ( !visPresetLayerElem.isNull() )
Expand All @@ -399,6 +432,9 @@ void QgsMapThemeCollection::readXml( const QDomDocument &doc )
layerRecords[layerID].usingCurrentStyle = true;
layerRecords[layerID].currentStyle = visPresetLayerElem.attribute( QStringLiteral( "style" ) );
}

if ( visPresetLayerElem.hasAttribute( QStringLiteral( "expanded" ) ) )
layerRecords[layerID].expandedLayerNode = visPresetLayerElem.attribute( QStringLiteral( "expanded" ) ).toInt();
}
visPresetLayerElem = visPresetLayerElem.nextSiblingElement( QStringLiteral( "layer" ) );
}
Expand All @@ -424,8 +460,47 @@ void QgsMapThemeCollection::readXml( const QDomDocument &doc )
checkedLegendNodesElem = checkedLegendNodesElem.nextSiblingElement( QStringLiteral( "checked-legend-nodes" ) );
}

QSet<QString> expandedGroupNodes;
if ( expandedStateInfo )
{
// expanded state of legend nodes
QDomElement expandedLegendNodesElem = visPresetElem.firstChildElement( QStringLiteral( "expanded-legend-nodes" ) );
while ( !expandedLegendNodesElem.isNull() )
{
QSet<QString> expandedLegendNodes;

QDomElement expandedLegendNodeElem = expandedLegendNodesElem.firstChildElement( QStringLiteral( "expanded-legend-node" ) );
while ( !expandedLegendNodeElem.isNull() )
{
expandedLegendNodes << expandedLegendNodeElem.attribute( QStringLiteral( "id" ) );
expandedLegendNodeElem = expandedLegendNodeElem.nextSiblingElement( QStringLiteral( "expanded-legend-node" ) );
}

QString layerID = expandedLegendNodesElem.attribute( QStringLiteral( "id" ) );
if ( mProject->mapLayer( layerID ) ) // only use valid IDs
{
layerRecords[layerID].expandedLegendItems = expandedLegendNodes;
}
expandedLegendNodesElem = expandedLegendNodesElem.nextSiblingElement( QStringLiteral( "expanded-legend-nodes" ) );
}

// expanded state of group nodes
QDomElement expandedGroupNodesElem = visPresetElem.firstChildElement( QStringLiteral( "expanded-group-nodes" ) );
if ( !expandedGroupNodesElem.isNull() )
{
QDomElement expandedGroupNodeElem = expandedGroupNodesElem.firstChildElement( QStringLiteral( "expanded-group-node" ) );
while ( !expandedGroupNodeElem.isNull() )
{
expandedGroupNodes << expandedGroupNodeElem.attribute( QStringLiteral( "id" ) );
expandedGroupNodeElem = expandedGroupNodeElem.nextSiblingElement( QStringLiteral( "expanded-group-node" ) );
}
}
}

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

Expand All @@ -446,6 +521,8 @@ void QgsMapThemeCollection::writeXml( QDomDocument &doc )
const MapThemeRecord &rec = it.value();
QDomElement visPresetElem = doc.createElement( QStringLiteral( "visibility-preset" ) );
visPresetElem.setAttribute( QStringLiteral( "name" ), grpName );
if ( rec.hasExpandedStateInfo() )
visPresetElem.setAttribute( QStringLiteral( "has-expanded-info" ), QStringLiteral( "1" ) );
Q_FOREACH ( const MapThemeLayerRecord &layerRec, rec.mLayerRecords )
{
if ( !layerRec.layer() )
Expand All @@ -469,6 +546,34 @@ void QgsMapThemeCollection::writeXml( QDomDocument &doc )
}
visPresetElem.appendChild( checkedLegendNodesElem );
}

if ( rec.hasExpandedStateInfo() )
{
layerElem.setAttribute( QStringLiteral( "expanded" ), layerRec.expandedLayerNode ? QStringLiteral( "1" ) : QStringLiteral( "0" ) );

QDomElement expandedLegendNodesElem = doc.createElement( QStringLiteral( "expanded-legend-nodes" ) );
expandedLegendNodesElem.setAttribute( QStringLiteral( "id" ), layerID );
for ( const QString &expandedLegendNode : qgis::as_const( layerRec.expandedLegendItems ) )
{
QDomElement expandedLegendNodeElem = doc.createElement( QStringLiteral( "expanded-legend-node" ) );
expandedLegendNodeElem.setAttribute( QStringLiteral( "id" ), expandedLegendNode );
expandedLegendNodesElem.appendChild( expandedLegendNodeElem );
}
visPresetElem.appendChild( expandedLegendNodesElem );
}
}

if ( rec.hasExpandedStateInfo() )
{
QDomElement expandedGroupElems = doc.createElement( QStringLiteral( "expanded-group-nodes" ) );
const QSet<QString> expandedGroupNodes = rec.expandedGroupNodes();
for ( const QString &groupId : expandedGroupNodes )
{
QDomElement expandedGroupElem = doc.createElement( QStringLiteral( "expanded-group-node" ) );
expandedGroupElem.setAttribute( QStringLiteral( "id" ), groupId );
expandedGroupElems.appendChild( expandedGroupElem );
}
visPresetElem.appendChild( expandedGroupElems );
}

visPresetsElem.appendChild( visPresetElem );
Expand Down
56 changes: 54 additions & 2 deletions src/core/qgsmapthemecollection.h
Expand Up @@ -65,7 +65,8 @@ class CORE_EXPORT QgsMapThemeCollection : public QObject
{
return mLayer == other.mLayer &&
usingCurrentStyle == other.usingCurrentStyle && currentStyle == other.currentStyle &&
usingLegendItems == other.usingLegendItems && checkedLegendItems == other.checkedLegendItems;
usingLegendItems == other.usingLegendItems && checkedLegendItems == other.checkedLegendItems &&
expandedLegendItems == other.expandedLegendItems && expandedLayerNode == other.expandedLayerNode;
}
bool operator!=( const QgsMapThemeCollection::MapThemeLayerRecord &other ) const
{
Expand All @@ -86,6 +87,19 @@ class CORE_EXPORT QgsMapThemeCollection : public QObject
bool usingLegendItems = false;
//! Rule keys of check legend items in layer tree model
QSet<QString> checkedLegendItems;

/**
* Rule keys of expanded legend items in layer tree view.
* \since QGIS 3.2
*/
QSet<QString> expandedLegendItems;

/**
* Whether the layer's tree node is expanded
* (only to be applied if the parent MapThemeRecord has the information about expanded nodes stored)
* \since QGIS 3.2
*/
bool expandedLayerNode = false;
private:
//! Weak pointer to the layer
QgsWeakMapLayerPointer mLayer;
Expand All @@ -103,7 +117,8 @@ class CORE_EXPORT QgsMapThemeCollection : public QObject

bool operator==( const QgsMapThemeCollection::MapThemeRecord &other ) const
{
return validLayerRecords() == other.validLayerRecords();
return validLayerRecords() == other.validLayerRecords() &&
mHasExpandedStateInfo == other.mHasExpandedStateInfo && mExpandedGroupNodes == other.mExpandedGroupNodes;
}
bool operator!=( const QgsMapThemeCollection::MapThemeRecord &other ) const
{
Expand All @@ -128,10 +143,47 @@ class CORE_EXPORT QgsMapThemeCollection : public QObject
*/
QHash<QgsMapLayer *, QgsMapThemeCollection::MapThemeLayerRecord> validLayerRecords() const SIP_SKIP;

/**
* Returns whether information about expanded/collapsed state of nodes has been recorded
* and thus whether expandedGroupNodes() and expandedLegendItems + expandedLayerNode from layer records are valid.
* \since QGIS 3.2
*/
bool hasExpandedStateInfo() const { return mHasExpandedStateInfo; }

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

/**
* 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.
* 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.2
*/
QSet<QString> expandedGroupNodes() const { return mExpandedGroupNodes; }

/**
* 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; }

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;

/**
* 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;

friend class QgsMapThemeCollection;
};

Expand Down
17 changes: 17 additions & 0 deletions src/gui/layertree/qgslayertreeview.cpp
Expand Up @@ -69,6 +69,7 @@ void QgsLayerTreeView::setModel( QAbstractItemModel *model )
QTreeView::setModel( model );

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

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

Expand Down Expand Up @@ -244,6 +245,22 @@ void QgsLayerTreeView::onExpandedChanged( QgsLayerTreeNode *node, bool expanded
setExpanded( idx, expanded );
}

void QgsLayerTreeView::onCustomPropertyChanged( QgsLayerTreeNode *node, const QString &key )
{
if ( key != QStringLiteral( "expandedLegendNodes" ) || !QgsLayerTree::isLayer( node ) )
return;

QSet<QString> expandedLegendNodes = node->customProperty( QStringLiteral( "expandedLegendNodes" ) ).toStringList().toSet();

const QList<QgsLayerTreeModelLegendNode *> legendNodes = layerTreeModel()->layerLegendNodes( QgsLayerTree::toLayer( node ), true );
for ( QgsLayerTreeModelLegendNode *legendNode : legendNodes )
{
QString key = legendNode->data( QgsLayerTreeModelLegendNode::RuleKeyRole ).toString();
if ( !key.isEmpty() )
setExpanded( layerTreeModel()->legendNode2index( legendNode ), expandedLegendNodes.contains( key ) );
}
}

void QgsLayerTreeView::onModelReset()
{
updateExpandedStateFromNode( layerTreeModel()->rootGroup() );
Expand Down
3 changes: 3 additions & 0 deletions src/gui/layertree/qgslayertreeview.h
Expand Up @@ -192,6 +192,9 @@ class GUI_EXPORT QgsLayerTreeView : public QTreeView
void onExpandedChanged( QgsLayerTreeNode *node, bool expanded );
void onModelReset();

private slots:
void onCustomPropertyChanged( QgsLayerTreeNode *node, const QString &key );

protected:
//! helper class with default actions. Lazily initialized.
QgsLayerTreeViewDefaultActions *mDefaultActions = nullptr;
Expand Down
1 change: 1 addition & 0 deletions tests/src/core/CMakeLists.txt
Expand Up @@ -141,6 +141,7 @@ SET(TESTS
testqgsmaprotation.cpp
testqgsmapsettings.cpp
testqgsmapsettingsutils.cpp
testqgsmapthemecollection.cpp
testqgsmaptopixelgeometrysimplifier.cpp
testqgsmaptopixel.cpp
testqgsmarkerlinesymbol.cpp
Expand Down

0 comments on commit 3400199

Please sign in to comment.