Skip to content
Permalink
Browse files

[FEATURE] Mutually exclusive layer tree groups (only one child may be…

… 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 50d4e720a78d3fd2396f9bfe6bba3f4a9ebe6d53
@@ -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 );
};
@@ -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 );
@@ -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 );
@@ -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 ) );

@@ -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* ) ) );
}
@@ -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* ) ) );
}
@@ -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();
}

@@ -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();
}

@@ -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 );
@@ -217,6 +258,8 @@ QgsLayerTreeGroup* QgsLayerTreeGroup::readXML( QDomElement& element )

groupNode->readChildrenFromXML( element );

groupNode->setIsMutuallyExclusive( isMutuallyExclusive, mutuallyExclusiveChildIndex );

return groupNode;
}

@@ -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 );

@@ -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
@@ -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()
@@ -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 )
@@ -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;
}
@@ -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;
};


@@ -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();
@@ -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

This comment has been minimized.

Copy link
Collaborator

@nyalldawson nyalldawson replied Sep 21, 2015

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

@gioman

This comment has been minimized.

Copy link
Contributor

@gioman gioman replied Sep 21, 2015

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

@nirvn

This comment has been minimized.

Copy link
Contributor

@nirvn nirvn replied Sep 22, 2015

@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

This comment has been minimized.

Copy link

@vincentschut vincentschut replied Oct 30, 2015

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

This comment has been minimized.

Copy link
Contributor

@gioman gioman replied Oct 31, 2015

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.
You can’t perform that action at this time.