Skip to content

Commit

Permalink
[FEATURE] Mutually exclusive layer tree groups (only one child may be…
Browse files Browse the repository at this point in the history
… checked at a time)

The feature can be toggled individually for groups - in layer tree view context menu.

This code has been funded by Tuscany Region (Italy) - SITA (CIG: 63526840AE) and commissioned to Gis3W s.a.s.
  • Loading branch information
wonder-sk committed Sep 21, 2015
1 parent ac5f068 commit 50d4e72
Show file tree
Hide file tree
Showing 9 changed files with 451 additions and 10 deletions.
9 changes: 9 additions & 0 deletions python/core/layertree/qgslayertreegroup.sip
Expand Up @@ -73,6 +73,15 @@ class QgsLayerTreeGroup : QgsLayerTreeNode
//! Set check state of the group node - will also update children
void setVisible( Qt::CheckState state );

//! Return whether the group is mutually exclusive (only one child can be checked at a time)
//! @note added in 2.12
bool isMutuallyExclusive() const;
//! Set whether the group is mutually exclusive (only one child can be checked at a time).
//! The initial child index determines which child should be initially checked. The default value
//! of -1 will determine automatically (either first one currently checked or none)
//! @note added in 2.12
void setIsMutuallyExclusive( bool enabled, int initialChildIndex = -1 );

private:
QgsLayerTreeGroup( const QgsLayerTreeGroup& other );
};
6 changes: 6 additions & 0 deletions python/gui/layertree/qgslayertreeviewdefaultactions.sip
Expand Up @@ -26,6 +26,9 @@ class QgsLayerTreeViewDefaultActions : QObject

QAction* actionMakeTopLevel( QObject* parent = 0 ) /Factory/;
QAction* actionGroupSelected( QObject* parent = 0 ) /Factory/;
//! Action to enable/disable mutually exclusive flag of a group (only one child node may be checked)
//! @note added in 2.12
QAction* actionMutuallyExclusiveGroup( QObject* parent = 0 ) /Factory/;

void zoomToLayer( QgsMapCanvas* canvas );
void zoomToGroup( QgsMapCanvas* canvas );
Expand All @@ -42,6 +45,9 @@ class QgsLayerTreeViewDefaultActions : QObject
void zoomToGroup();
void makeTopLevel();
void groupSelected();
//! Slot to enable/disable mutually exclusive group flag
//! @note added in 2.12
void mutuallyExclusiveGroup();

protected:
void zoomToLayers( QgsMapCanvas* canvas, const QList<QgsMapLayer*>& layers );
Expand Down
2 changes: 2 additions & 0 deletions src/app/qgsapplayertreeviewmenuprovider.cpp
Expand Up @@ -53,6 +53,8 @@ QMenu* QgsAppLayerTreeViewMenuProvider::createContextMenu()

menu->addAction( actions->actionRenameGroupOrLayer( menu ) );

menu->addAction( actions->actionMutuallyExclusiveGroup( menu ) );

if ( mView->selectedNodes( true ).count() >= 2 )
menu->addAction( actions->actionGroupSelected( menu ) );

Expand Down
181 changes: 171 additions & 10 deletions src/core/layertree/qgslayertreegroup.cpp
Expand Up @@ -29,6 +29,8 @@ QgsLayerTreeGroup::QgsLayerTreeGroup( const QString& name, Qt::CheckState checke
, mName( name )
, mChecked( checked )
, mChangingChildVisibility( false )
, mMutuallyExclusive( false )
, mMutuallyExclusiveChildIndex( -1 )
{
connect( this, SIGNAL( visibilityChanged( QgsLayerTreeNode*, Qt::CheckState ) ), this, SLOT( nodeVisibilityChanged( QgsLayerTreeNode* ) ) );
}
Expand All @@ -38,6 +40,8 @@ QgsLayerTreeGroup::QgsLayerTreeGroup( const QgsLayerTreeGroup& other )
, mName( other.mName )
, mChecked( other.mChecked )
, mChangingChildVisibility( false )
, mMutuallyExclusive( false )
, mMutuallyExclusiveChildIndex( -1 )
{
connect( this, SIGNAL( visibilityChanged( QgsLayerTreeNode*, Qt::CheckState ) ), this, SLOT( nodeVisibilityChanged( QgsLayerTreeNode* ) ) );
}
Expand Down Expand Up @@ -86,9 +90,31 @@ void QgsLayerTreeGroup::insertChildNode( int index, QgsLayerTreeNode* node )

void QgsLayerTreeGroup::insertChildNodes( int index, QList<QgsLayerTreeNode*> nodes )
{
QgsLayerTreeNode* meChild = 0;
if ( mMutuallyExclusive && mMutuallyExclusiveChildIndex >= 0 && mMutuallyExclusiveChildIndex < mChildren.count() )
meChild = mChildren[mMutuallyExclusiveChildIndex];

// low-level insert
insertChildrenPrivate( index, nodes );

if ( mMutuallyExclusive )
{
if ( meChild )
{
// the child could have change its index - or the new children may have been also set as visible
mMutuallyExclusiveChildIndex = mChildren.indexOf( meChild );
}
else if ( mChecked == Qt::Checked )
{
// we have not picked a child index yet, but we should pick one now
// ... so pick the first one from the newly added
if ( index == -1 )
index = mChildren.count() - nodes.count(); // get real insertion index
mMutuallyExclusiveChildIndex = index;
}
updateChildVisibilityMutuallyExclusive();
}

updateVisibilityFromChildren();
}

Expand Down Expand Up @@ -122,8 +148,21 @@ void QgsLayerTreeGroup::removeLayer( QgsMapLayer* layer )

void QgsLayerTreeGroup::removeChildren( int from, int count )
{
QgsLayerTreeNode* meChild = 0;
if ( mMutuallyExclusive && mMutuallyExclusiveChildIndex >= 0 && mMutuallyExclusiveChildIndex < mChildren.count() )
meChild = mChildren[mMutuallyExclusiveChildIndex];

removeChildrenPrivate( from, count );

if ( meChild )
{
// the child could have change its index - or may have been removed completely
mMutuallyExclusiveChildIndex = mChildren.indexOf( meChild );
// we need to uncheck this group
if ( mMutuallyExclusiveChildIndex == -1 )
setVisible( Qt::Unchecked );
}

updateVisibilityFromChildren();
}

Expand Down Expand Up @@ -209,6 +248,8 @@ QgsLayerTreeGroup* QgsLayerTreeGroup::readXML( QDomElement& element )
QString name = element.attribute( "name" );
bool isExpanded = ( element.attribute( "expanded", "1" ) == "1" );
Qt::CheckState checked = QgsLayerTreeUtils::checkStateFromXml( element.attribute( "checked" ) );
bool isMutuallyExclusive = element.attribute( "mutually-exclusive", "0" ) == "1";
int mutuallyExclusiveChildIndex = element.attribute( "mutually-exclusive-child", "-1" ).toInt();

QgsLayerTreeGroup* groupNode = new QgsLayerTreeGroup( name, checked );
groupNode->setExpanded( isExpanded );
Expand All @@ -217,6 +258,8 @@ QgsLayerTreeGroup* QgsLayerTreeGroup::readXML( QDomElement& element )

groupNode->readChildrenFromXML( element );

groupNode->setIsMutuallyExclusive( isMutuallyExclusive, mutuallyExclusiveChildIndex );

return groupNode;
}

Expand All @@ -227,6 +270,11 @@ void QgsLayerTreeGroup::writeXML( QDomElement& parentElement )
elem.setAttribute( "name", mName );
elem.setAttribute( "expanded", mExpanded ? "1" : "0" );
elem.setAttribute( "checked", QgsLayerTreeUtils::checkStateToXml( mChecked ) );
if ( mMutuallyExclusive )
{
elem.setAttribute( "mutually-exclusive", "1" );
elem.setAttribute( "mutually-exclusive-child", mMutuallyExclusiveChildIndex );
}

writeCommonXML( elem );

Expand Down Expand Up @@ -276,21 +324,81 @@ void QgsLayerTreeGroup::setVisible( Qt::CheckState state )
mChecked = state;
emit visibilityChanged( this, state );

if ( mChecked == Qt::Unchecked || mChecked == Qt::Checked )
if ( mMutuallyExclusive )
{
if ( mMutuallyExclusiveChildIndex < 0 || mMutuallyExclusiveChildIndex >= mChildren.count() )
mMutuallyExclusiveChildIndex = 0; // just choose the first one if we have lost the active one
updateChildVisibilityMutuallyExclusive();
}
else if ( mChecked == Qt::Unchecked || mChecked == Qt::Checked )
{
updateChildVisibility();
}
}

void QgsLayerTreeGroup::updateChildVisibility()
{
mChangingChildVisibility = true; // guard against running again setVisible() triggered from children

// update children to have the correct visibility
Q_FOREACH ( QgsLayerTreeNode* child, mChildren )
{
if ( QgsLayerTree::isGroup( child ) )
QgsLayerTree::toGroup( child )->setVisible( mChecked );
else if ( QgsLayerTree::isLayer( child ) )
QgsLayerTree::toLayer( child )->setVisible( mChecked );
}

mChangingChildVisibility = false;
}


static bool _nodeIsChecked( QgsLayerTreeNode* node )
{
Qt::CheckState state;
if ( QgsLayerTree::isGroup( node ) )
state = QgsLayerTree::toGroup( node )->isVisible();
else if ( QgsLayerTree::isLayer( node ) )
state = QgsLayerTree::toLayer( node )->isVisible();
else
return false;

return state == Qt::Checked || state == Qt::PartiallyChecked;
}


bool QgsLayerTreeGroup::isMutuallyExclusive() const
{
return mMutuallyExclusive;
}

void QgsLayerTreeGroup::setIsMutuallyExclusive( bool enabled, int initialChildIndex )
{
mMutuallyExclusive = enabled;
mMutuallyExclusiveChildIndex = initialChildIndex;

if ( !enabled )
{
mChangingChildVisibility = true; // guard against running again setVisible() triggered from children
updateVisibilityFromChildren();
return;
}

// update children to have the correct visibility
if ( mMutuallyExclusiveChildIndex < 0 || mMutuallyExclusiveChildIndex >= mChildren.count() )
{
// try to use first checked index
int index = 0;
Q_FOREACH ( QgsLayerTreeNode* child, mChildren )
{
if ( QgsLayerTree::isGroup( child ) )
QgsLayerTree::toGroup( child )->setVisible( mChecked );
else if ( QgsLayerTree::isLayer( child ) )
QgsLayerTree::toLayer( child )->setVisible( mChecked );
if ( _nodeIsChecked( child ) )
{
mMutuallyExclusiveChildIndex = index;
break;
}
index++;
}

mChangingChildVisibility = false;
}

updateChildVisibilityMutuallyExclusive();
}

QStringList QgsLayerTreeGroup::findLayerIds() const
Expand All @@ -313,10 +421,30 @@ void QgsLayerTreeGroup::layerDestroyed()
//removeLayer( layer );
}


void QgsLayerTreeGroup::nodeVisibilityChanged( QgsLayerTreeNode* node )
{
if ( mChildren.indexOf( node ) != -1 )
int childIndex = mChildren.indexOf( node );
if ( childIndex == -1 )
return; // not a direct child - ignore

if ( mMutuallyExclusive )
{
if ( _nodeIsChecked( node ) )
mMutuallyExclusiveChildIndex = childIndex;

// we need to update this node's check status in two cases:
// 1. it was unchecked and a child node got checked
// 2. it was checked and the only checked child got unchecked
updateVisibilityFromChildren();

// we also need to make sure there is only one child node checked
updateChildVisibilityMutuallyExclusive();
}
else
{
updateVisibilityFromChildren();
}
}

void QgsLayerTreeGroup::updateVisibilityFromChildren()
Expand All @@ -327,6 +455,19 @@ void QgsLayerTreeGroup::updateVisibilityFromChildren()
if ( mChildren.count() == 0 )
return;

if ( mMutuallyExclusive )
{
// if in mutually exclusive mode, our check state depends only on the check state of the chosen child index

if ( mMutuallyExclusiveChildIndex < 0 || mMutuallyExclusiveChildIndex >= mChildren.count() )
return;

Qt::CheckState meChildState = _nodeIsChecked( mChildren[mMutuallyExclusiveChildIndex] ) ? Qt::Checked : Qt::Unchecked;

setVisible( meChildState );
return;
}

bool hasVisible = false, hasHidden = false;

Q_FOREACH ( QgsLayerTreeNode* child, mChildren )
Expand Down Expand Up @@ -356,3 +497,23 @@ void QgsLayerTreeGroup::updateVisibilityFromChildren()
setVisible( newState );
}

void QgsLayerTreeGroup::updateChildVisibilityMutuallyExclusive()
{
if ( mChildren.isEmpty() )
return;

mChangingChildVisibility = true; // guard against running again setVisible() triggered from children

int index = 0;
Q_FOREACH ( QgsLayerTreeNode* child, mChildren )
{
Qt::CheckState checked = ( index == mMutuallyExclusiveChildIndex ? mChecked : Qt::Unchecked );
if ( QgsLayerTree::isGroup( child ) )
QgsLayerTree::toGroup( child )->setVisible( checked );
else if ( QgsLayerTree::isLayer( child ) )
QgsLayerTree::toLayer( child )->setVisible( checked );
++index;
}

mChangingChildVisibility = false;
}
20 changes: 20 additions & 0 deletions src/core/layertree/qgslayertreegroup.h
Expand Up @@ -94,18 +94,38 @@ class CORE_EXPORT QgsLayerTreeGroup : public QgsLayerTreeNode
//! Set check state of the group node - will also update children
void setVisible( Qt::CheckState state );

//! Return whether the group is mutually exclusive (only one child can be checked at a time)
//! @note added in 2.12
bool isMutuallyExclusive() const;
//! Set whether the group is mutually exclusive (only one child can be checked at a time).
//! The initial child index determines which child should be initially checked. The default value
//! of -1 will determine automatically (either first one currently checked or none)
//! @note added in 2.12
void setIsMutuallyExclusive( bool enabled, int initialChildIndex = -1 );

protected slots:
void layerDestroyed();
void nodeVisibilityChanged( QgsLayerTreeNode* node );

protected:
//! Set check state of this group from its children
void updateVisibilityFromChildren();
//! Set check state of children (when this group's check state changes) - if not mutually exclusive
void updateChildVisibility();
//! Set check state of children - if mutually exclusive
void updateChildVisibilityMutuallyExclusive();

protected:
QString mName;
Qt::CheckState mChecked;

bool mChangingChildVisibility;

//! Whether the group is mutually exclusive (i.e. only one child can be checked at a time)
bool mMutuallyExclusive;
//! Keeps track which child has been most recently selected
//! (so if the whole group is unchecked and checked again, we know which child to check)
int mMutuallyExclusiveChildIndex;
};


Expand Down
22 changes: 22 additions & 0 deletions src/gui/layertree/qgslayertreeviewdefaultactions.cpp
Expand Up @@ -110,6 +110,19 @@ QAction* QgsLayerTreeViewDefaultActions::actionGroupSelected( QObject* parent )
return a;
}

QAction* QgsLayerTreeViewDefaultActions::actionMutuallyExclusiveGroup( QObject* parent )
{
QgsLayerTreeNode* node = mView->currentNode();
if ( !node || !QgsLayerTree::isGroup( node ) )
return 0;

QAction* a = new QAction( tr( "Mutually Exclusive Group" ), parent );
a->setCheckable( true );
a->setChecked( QgsLayerTree::toGroup( node )->isMutuallyExclusive() );
connect( a, SIGNAL( triggered() ), this, SLOT( mutuallyExclusiveGroup() ) );
return a;
}

void QgsLayerTreeViewDefaultActions::addGroup()
{
QgsLayerTreeGroup* group = mView->currentGroupNode();
Expand Down Expand Up @@ -289,3 +302,12 @@ void QgsLayerTreeViewDefaultActions::groupSelected()

mView->setCurrentIndex( mView->layerTreeModel()->node2index( newGroup ) );
}

void QgsLayerTreeViewDefaultActions::mutuallyExclusiveGroup()
{
QgsLayerTreeNode* node = mView->currentNode();
if ( !node || !QgsLayerTree::isGroup( node ) )
return;

QgsLayerTree::toGroup( node )->setIsMutuallyExclusive( !QgsLayerTree::toGroup( node )->isMutuallyExclusive() );
}

5 comments on commit 50d4e72

@nyalldawson
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@wonder-sk heck yeah!!! Thanks!!

@gioman
Copy link
Contributor

@gioman gioman commented on 50d4e72 Sep 21, 2015

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@wonder-sk @nyalldawson agree, very important feature! Thanks to all that made this possible.

@nirvn
Copy link
Contributor

@nirvn nirvn commented on 50d4e72 Sep 22, 2015

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@wonder-sk brilliant work, thanks.

The only thing I feel could be improved is to add a visual indication that a group is set to be mutually exclusive.

How hard would it be to change the layers' check boxes into radio buttons? That would make it utterly clear, and would be a nice visual touch.

Otherwise, a simpler option would be to have a different group icon.

@vincentschut
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this, very handy!
However, is it just me, or is this not saved in the project file? When I open a project with layer groups which I had previously set as mutually exclusive, the mutually exclusiveness is gone...

@gioman
Copy link
Contributor

@gioman gioman commented on 50d4e72 Oct 31, 2015

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

However, is it just me, or is this not saved in the project file? When I open a project with layer groups which I had previously set as mutually exclusive, the mutually exclusiveness is gone...

confirmed http://hub.qgis.org/issues/13723

Please sign in to comment.