Skip to content

Commit

Permalink
Merge pull request #6782 from wonder-sk/required-layers
Browse files Browse the repository at this point in the history
Mark layers as required in the project
  • Loading branch information
wonder-sk authored Apr 13, 2018
2 parents b662b44 + 7e022f0 commit 322bc78
Show file tree
Hide file tree
Showing 10 changed files with 205 additions and 6 deletions.
20 changes: 20 additions & 0 deletions python/core/qgsproject.sip.in
Original file line number Diff line number Diff line change
Expand Up @@ -952,6 +952,26 @@ Sets the project's ``metadata`` store.
.. seealso:: :py:func:`metadata`

.. seealso:: :py:func:`metadataChanged`
%End

QSet<QgsMapLayer *> requiredLayers() const;
%Docstring
Returns a set of map layers that are required in the project and therefore they should not get
removed from the project. The set of layers may be configured by users in project properties.
and it is mainly a hint for the user interface to protect users from removing layers that important
in the project. The removeMapLayer(), removeMapLayers() calls do not block removal of layers listed here.

.. versionadded:: 3.2
%End

void setRequiredLayers( const QSet<QgsMapLayer *> &layers );
%Docstring
Configures a set of map layers that are required in the project and therefore they should not get
removed from the project. The set of layers may be configured by users in project properties.
and it is mainly a hint for the user interface to protect users from removing layers that important
in the project. The removeMapLayer(), removeMapLayers() calls do not block removal of layers listed here.

.. versionadded:: 3.2
%End

signals:
Expand Down
31 changes: 28 additions & 3 deletions src/app/qgisapp.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9298,15 +9298,17 @@ void QgisApp::removeLayer()
return;
}

Q_FOREACH ( QgsMapLayer *layer, mLayerTreeView->selectedLayers() )
const QList<QgsMapLayer *> selectedLayers = mLayerTreeView->selectedLayers();

for ( QgsMapLayer *layer : selectedLayers )
{
QgsVectorLayer *vlayer = qobject_cast<QgsVectorLayer *>( layer );
if ( vlayer && vlayer->isEditable() && !toggleEditing( vlayer, true ) )
return;
}

QStringList activeTaskDescriptions;
Q_FOREACH ( QgsMapLayer *layer, mLayerTreeView->selectedLayers() )
for ( QgsMapLayer *layer : selectedLayers )
{
QList< QgsTask * > tasks = QgsApplication::taskManager()->tasksDependentOnLayer( layer );
if ( !tasks.isEmpty() )
Expand All @@ -9318,6 +9320,16 @@ void QgisApp::removeLayer()
}
}

// extra check for required layers
// In theory it should not be needed because the remove action should be disabled
// if there are required layers in the selection...
const QSet<QgsMapLayer *> requiredLayers = QgsProject::instance()->requiredLayers();
for ( QgsMapLayer *layer : selectedLayers )
{
if ( requiredLayers.contains( layer ) )
return;
}

if ( !activeTaskDescriptions.isEmpty() )
{
QMessageBox::warning( this, tr( "Active Tasks" ),
Expand Down Expand Up @@ -11573,7 +11585,7 @@ void QgisApp::selectionChanged( QgsMapLayer *layer )

void QgisApp::legendLayerSelectionChanged()
{
QList<QgsLayerTreeLayer *> selectedLayers = mLayerTreeView ? mLayerTreeView->selectedLayerNodes() : QList<QgsLayerTreeLayer *>();
const QList<QgsLayerTreeLayer *> selectedLayers = mLayerTreeView ? mLayerTreeView->selectedLayerNodes() : QList<QgsLayerTreeLayer *>();

mActionDuplicateLayer->setEnabled( !selectedLayers.isEmpty() );
mActionSetLayerScaleVisibility->setEnabled( !selectedLayers.isEmpty() );
Expand All @@ -11599,6 +11611,19 @@ void QgisApp::legendLayerSelectionChanged()
mLegendExpressionFilterButton->setChecked( exprEnabled );
}
}

// remove action - check for required layers
bool removeEnabled = true;
const QSet<QgsMapLayer *> requiredLayers = QgsProject::instance()->requiredLayers();
for ( QgsLayerTreeLayer *nodeLayer : selectedLayers )
{
if ( requiredLayers.contains( nodeLayer->layer() ) )
{
removeEnabled = false;
break;
}
}
mActionRemoveLayer->setEnabled( removeEnabled );
}

void QgisApp::layerEditStateChanged()
Expand Down
18 changes: 16 additions & 2 deletions src/app/qgsapplayertreeviewmenuprovider.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,8 @@ QMenu *QgsAppLayerTreeViewMenuProvider::createContextMenu()

menu->addSeparator();
menu->addAction( actions->actionAddGroup( menu ) );
menu->addAction( QgsApplication::getThemeIcon( QStringLiteral( "/mActionRemoveLayer.svg" ) ), tr( "&Remove Group…" ), QgisApp::instance(), SLOT( removeLayer() ) );
QAction *removeAction = menu->addAction( QgsApplication::getThemeIcon( QStringLiteral( "/mActionRemoveLayer.svg" ) ), tr( "&Remove Group…" ), QgisApp::instance(), SLOT( removeLayer() ) );
removeAction->setEnabled( removeActionEnabled() );
menu->addSeparator();

menu->addAction( QgsApplication::getThemeIcon( QStringLiteral( "/mActionSetCRS.png" ) ),
Expand Down Expand Up @@ -171,7 +172,8 @@ QMenu *QgsAppLayerTreeViewMenuProvider::createContextMenu()

// duplicate layer
QAction *duplicateLayersAction = menu->addAction( QgsApplication::getThemeIcon( QStringLiteral( "/mActionDuplicateLayer.svg" ) ), tr( "&Duplicate Layer" ), QgisApp::instance(), SLOT( duplicateLayers() ) );
menu->addAction( QgsApplication::getThemeIcon( QStringLiteral( "/mActionRemoveLayer.svg" ) ), tr( "&Remove Layer…" ), QgisApp::instance(), SLOT( removeLayer() ) );
QAction *removeAction = menu->addAction( QgsApplication::getThemeIcon( QStringLiteral( "/mActionRemoveLayer.svg" ) ), tr( "&Remove Layer…" ), QgisApp::instance(), SLOT( removeLayer() ) );
removeAction->setEnabled( removeActionEnabled() );

menu->addSeparator();

Expand Down Expand Up @@ -708,3 +710,15 @@ void QgsAppLayerTreeViewMenuProvider::setSymbolLegendNodeColor( const QColor &co
layer->emitStyleChanged();
}
}

bool QgsAppLayerTreeViewMenuProvider::removeActionEnabled()
{
const QList<QgsLayerTreeLayer *> selectedLayers = mView->selectedLayerNodes();
const QSet<QgsMapLayer *> requiredLayers = QgsProject::instance()->requiredLayers();
for ( QgsLayerTreeLayer *nodeLayer : selectedLayers )
{
if ( requiredLayers.contains( nodeLayer->layer() ) )
return false;
}
return true;
}
3 changes: 3 additions & 0 deletions src/app/qgsapplayertreeviewmenuprovider.h
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ class QgsAppLayerTreeViewMenuProvider : public QObject, public QgsLayerTreeViewM
void setVectorSymbolColor( const QColor &color );
void editSymbolLegendNodeSymbol();
void setSymbolLegendNodeColor( const QColor &color );

private:
bool removeActionEnabled();
};

#endif // QGSAPPLAYERTREEVIEWMENUPROVIDER_H
37 changes: 37 additions & 0 deletions src/app/qgsprojectproperties.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -831,6 +831,7 @@ QgsProjectProperties::QgsProjectProperties( QgsMapCanvas *mapCanvas, QWidget *pa
connect( titleEdit, &QLineEdit::textChanged, mMetadataWidget, &QgsMetadataWidget::setTitle );

projectionSelectorInitialized();
populateRequiredLayers();
restoreOptionsBaseUi();
restoreState();
}
Expand Down Expand Up @@ -1283,6 +1284,8 @@ void QgsProjectProperties::apply()
canvas->refresh();
}
QgisApp::instance()->mapOverviewCanvas()->refresh();

applyRequiredLayers();
}

void QgsProjectProperties::showProjectionsTab()
Expand Down Expand Up @@ -2122,3 +2125,37 @@ void QgsProjectProperties::showHelp()
}
QgsHelp::openHelp( link );
}

void QgsProjectProperties::populateRequiredLayers()
{
const QSet<QgsMapLayer *> requiredLayers = QgsProject::instance()->requiredLayers();
QStandardItemModel *model = new QStandardItemModel( mViewRequiredLayers );
QList<QgsLayerTreeLayer *> layers = QgsProject::instance()->layerTreeRoot()->findLayers();
std::sort( layers.begin(), layers.end(), []( QgsLayerTreeLayer * layer1, QgsLayerTreeLayer * layer2 ) { return layer1->name() < layer2->name(); } );
for ( const QgsLayerTreeLayer *l : layers )
{
QStandardItem *item = new QStandardItem( l->name() );
item->setCheckable( true );
item->setCheckState( requiredLayers.contains( l->layer() ) ? Qt::Checked : Qt::Unchecked );
item->setData( l->layerId() );
model->appendRow( item );
}

mViewRequiredLayers->setModel( model );
}

void QgsProjectProperties::applyRequiredLayers()
{
QSet<QgsMapLayer *> requiredLayers;
QAbstractItemModel *model = mViewRequiredLayers->model();
for ( int i = 0; i < model->rowCount(); ++i )
{
if ( model->data( model->index( i, 0 ), Qt::CheckStateRole ).toInt() == Qt::Checked )
{
QString layerId = model->data( model->index( i, 0 ), Qt::UserRole + 1 ).toString();
if ( QgsMapLayer *layer = QgsProject::instance()->mapLayer( layerId ) )
requiredLayers << layer;
}
}
QgsProject::instance()->setRequiredLayers( requiredLayers );
}
3 changes: 3 additions & 0 deletions src/app/qgsprojectproperties.h
Original file line number Diff line number Diff line change
Expand Up @@ -224,4 +224,7 @@ class APP_EXPORT QgsProjectProperties : public QgsOptionsDialogBase, private Ui:
void updateGuiForMapUnits();

void showHelp();

void populateRequiredLayers();
void applyRequiredLayers();
};
22 changes: 22 additions & 0 deletions src/core/qgsproject.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2660,3 +2660,25 @@ void QgsProject::setMetadata( const QgsProjectMetadata &metadata )

setDirty( true );
}

QSet<QgsMapLayer *> QgsProject::requiredLayers() const
{
QSet<QgsMapLayer *> layers;
const QStringList lst = readListEntry( QStringLiteral( "RequiredLayers" ), QStringLiteral( "Layers" ) );
for ( const QString &layerId : lst )
{
if ( QgsMapLayer *layer = mapLayer( layerId ) )
layers.insert( layer );
}
return layers;
}

void QgsProject::setRequiredLayers( const QSet<QgsMapLayer *> &layers )
{
QStringList layerIds;
for ( QgsMapLayer *layer : layers )
{
layerIds << layer->id();
}
writeEntry( QStringLiteral( "RequiredLayers" ), QStringLiteral( "Layers" ), layerIds );
}
18 changes: 18 additions & 0 deletions src/core/qgsproject.h
Original file line number Diff line number Diff line change
Expand Up @@ -928,6 +928,24 @@ class CORE_EXPORT QgsProject : public QObject, public QgsExpressionContextGenera
*/
void setMetadata( const QgsProjectMetadata &metadata );

/**
* Returns a set of map layers that are required in the project and therefore they should not get
* removed from the project. The set of layers may be configured by users in project properties.
* and it is mainly a hint for the user interface to protect users from removing layers that important
* in the project. The removeMapLayer(), removeMapLayers() calls do not block removal of layers listed here.
* \since QGIS 3.2
*/
QSet<QgsMapLayer *> requiredLayers() const;

/**
* Configures a set of map layers that are required in the project and therefore they should not get
* removed from the project. The set of layers may be configured by users in project properties.
* and it is mainly a hint for the user interface to protect users from removing layers that important
* in the project. The removeMapLayer(), removeMapLayers() calls do not block removal of layers listed here.
* \since QGIS 3.2
*/
void setRequiredLayers( const QSet<QgsMapLayer *> &layers );

signals:
//! emitted when project is being read
void readProject( const QDomDocument & );
Expand Down
24 changes: 23 additions & 1 deletion src/ui/qgsprojectpropertiesbase.ui
Original file line number Diff line number Diff line change
Expand Up @@ -1443,7 +1443,7 @@
</property>
</widget>
</item>
<item row="2" column="0">
<item row="3" column="0">
<widget class="QCheckBox" name="mTrustProjectCheckBox">
<property name="toolTip">
<string>Speed up project loading by skipping data checks. Useful in qgis server context or project with huge database views or materialized views.</string>
Expand All @@ -1453,6 +1453,28 @@
</property>
</widget>
</item>
<item row="4" column="0" colspan="2">
<widget class="QgsCollapsibleGroupBox" name="groupBox_5">
<property name="title">
<string>Required layers</string>
</property>
<layout class="QGridLayout" name="gridLayout_19">
<item row="0" column="0" colspan="2">
<widget class="QLabel" name="label_31">
<property name="text">
<string>Checked layers in this list are protected from inadvertent removal from the project.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item row="2" column="0" colspan="2">
<widget class="QListView" name="mViewRequiredLayers"/>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="mTabRelations">
Expand Down
35 changes: 35 additions & 0 deletions tests/src/core/testqgsproject.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ class TestQgsProject : public QObject
void testPathResolverSvg();
void testProjectUnits();
void variablesChanged();
void testRequiredLayers();
};

void TestQgsProject::init()
Expand Down Expand Up @@ -347,6 +348,40 @@ void TestQgsProject::variablesChanged()
delete prj;
}

void TestQgsProject::testRequiredLayers()
{
QString dataDir( TEST_DATA_DIR ); //defined in CmakeLists.txt
QString layerPath = dataDir + "/points.shp";
QgsVectorLayer *layer1 = new QgsVectorLayer( layerPath, QStringLiteral( "points 1" ), QStringLiteral( "ogr" ) );
QgsVectorLayer *layer2 = new QgsVectorLayer( layerPath, QStringLiteral( "points 2" ), QStringLiteral( "ogr" ) );

QgsProject prj;
prj.addMapLayer( layer1 );
prj.addMapLayer( layer2 );

QSet<QgsMapLayer *> reqLayers;
reqLayers << layer2;
prj.setRequiredLayers( reqLayers );

QSet<QgsMapLayer *> reqLayersReturned = prj.requiredLayers();
QCOMPARE( reqLayersReturned.count(), 1 );
QCOMPARE( *reqLayersReturned.constBegin(), layer2 );

QTemporaryFile f;
QVERIFY( f.open() );
f.close();
prj.setFileName( f.fileName() );
prj.write();

// test reading required layers back
QgsProject prj2;
prj2.setFileName( f.fileName() );
QVERIFY( prj2.read() );
QSet<QgsMapLayer *> reqLayersReturned2 = prj2.requiredLayers();
QCOMPARE( reqLayersReturned2.count(), 1 );
QCOMPARE( ( *reqLayersReturned.constBegin() )->name(), QString( "points 2" ) );
}


QGSTEST_MAIN( TestQgsProject )
#include "testqgsproject.moc"

0 comments on commit 322bc78

Please sign in to comment.