Skip to content

Commit 60afead

Browse files
committedMar 10, 2018
Add QgsProjectDirtyBlocker and QgsProject.blockDirtying to prevent
project dirtying for the lifetime of an object Python code can then call: project = QgsProject.instance() with QgsProject.blockDirtying(project): # do something Use QgsProjectDirtyBlocker to prevent projects being marked as dirty while creating a new project or while loading an existing project -- avoids the titlebar temporarily showing the project state as unsaved while it is being loaded.
1 parent d6eeabf commit 60afead

File tree

6 files changed

+207
-3
lines changed

6 files changed

+207
-3
lines changed
 

‎python/core/__init__.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,38 @@ def __exit__(self, ex_type, ex_value, traceback):
234234
QgsReadWriteContext.enterCategory = ReadWriteContextEnterCategory
235235

236236

237+
# Python class to extend QgsProjectDirtyBlocker C++ class
238+
239+
240+
class ProjectDirtyBlocker():
241+
"""
242+
Context manager used to block project setDirty calls.
243+
244+
Example:
245+
project = QgsProject.instance()
246+
with QgsProject.blockDirtying(project):
247+
# do something
248+
249+
.. versionadded:: 3.2
250+
"""
251+
252+
def __init__(self, project):
253+
self.project = project
254+
self.blocker = None
255+
256+
def __enter__(self):
257+
self.blocker = QgsProjectDirtyBlocker(self.project)
258+
return self.project
259+
260+
def __exit__(self, ex_type, ex_value, traceback):
261+
del self.blocker
262+
return True
263+
264+
265+
# Inject the context manager into QgsProject class as a member
266+
QgsProject.blockDirtying = ProjectDirtyBlocker
267+
268+
237269
class QgsTaskWrapper(QgsTask):
238270

239271
def __init__(self, description, flags, function, on_finished, *args, **kwargs):

‎python/core/qgsproject.sip.in

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1189,6 +1189,7 @@ The snapping configuration for this project.
11891189
.. versionadded:: 3.0
11901190
%End
11911191

1192+
11921193
void setDirty( bool b = true );
11931194
%Docstring
11941195
Flag the project as dirty (modified). If this flag is set, the user will
@@ -1217,6 +1218,45 @@ home path will be automatically determined from the project's file path.
12171218

12181219
};
12191220

1221+
class QgsProjectDirtyBlocker
1222+
{
1223+
%Docstring
1224+
Temporarily blocks QgsProject "dirtying" for the lifetime of the object.
1225+
1226+
QgsProjectDirtyBlocker supports "stacked" blocking, so two QgsProjectDirtyBlockers created
1227+
for the same project will both need to be destroyed before the project can be dirtied again.
1228+
1229+
Note that QgsProjectDirtyBlocker only blocks calls which set the project as dirty - calls
1230+
which set the project as clean are not blocked.
1231+
1232+
Python scripts should not use QgsProjectDirtyBlocker directly. Instead, use :py:func:`QgsProject.blockDirtying()`
1233+
.. code-block:: python
1234+
1235+
project = QgsProject.instance()
1236+
with QgsProject.blockDirtying(project):
1237+
# do something
1238+
1239+
.. seealso:: :py:func:`QgsProject.setDirty`
1240+
1241+
.. versionadded:: 3.2
1242+
%End
1243+
1244+
%TypeHeaderCode
1245+
#include "qgsproject.h"
1246+
%End
1247+
public:
1248+
1249+
QgsProjectDirtyBlocker( QgsProject *project );
1250+
%Docstring
1251+
Constructor for QgsProjectDirtyBlocker.
1252+
1253+
This will block dirtying the specified ``project`` for the lifetime of this object.
1254+
%End
1255+
1256+
~QgsProjectDirtyBlocker();
1257+
1258+
};
1259+
12201260

12211261
/************************************************************************
12221262
* This file has been generated automatically from *

‎src/app/qgisapp.cpp

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5066,6 +5066,7 @@ bool QgisApp::fileNew( bool promptToSaveFlag, bool forceBlank )
50665066

50675067
QgsSettings settings;
50685068

5069+
MAYBE_UNUSED QgsProjectDirtyBlocker dirtyBlocker( QgsProject::instance() );
50695070
closeProject();
50705071

50715072
QgsProject *prj = QgsProject::instance();
@@ -5472,7 +5473,7 @@ void QgisApp::fileOpen()
54725473
// open the selected project
54735474
addProject( fullPath );
54745475
}
5475-
} // QgisApp::fileOpen
5476+
}
54765477

54775478
void QgisApp::enableProjectMacros()
54785479
{
@@ -5488,6 +5489,8 @@ void QgisApp::enableProjectMacros()
54885489
*/
54895490
bool QgisApp::addProject( const QString &projectFile )
54905491
{
5492+
MAYBE_UNUSED QgsProjectDirtyBlocker dirtyBlocker( QgsProject::instance() );
5493+
54915494
// close the previous opened project if any
54925495
closeProject();
54935496

‎src/core/qgsproject.cpp

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -411,9 +411,15 @@ bool QgsProject::isDirty() const
411411
return mDirty;
412412
}
413413

414-
void QgsProject::setDirty( bool b )
414+
void QgsProject::setDirty( const bool dirty )
415415
{
416-
mDirty = b;
416+
if ( dirty && mDirtyBlockCount > 0 )
417+
return;
418+
419+
if ( mDirty == dirty )
420+
return;
421+
422+
mDirty = dirty;
417423
emit isDirtyChanged( mDirty );
418424
}
419425

‎src/core/qgsproject.h

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1136,6 +1136,8 @@ class CORE_EXPORT QgsProject : public QObject, public QgsExpressionContextGenera
11361136
*/
11371137
void setSnappingConfig( const QgsSnappingConfig &snappingConfig );
11381138

1139+
// TODO QGIS 4.0 - rename b to dirty
1140+
11391141
/**
11401142
* Flag the project as dirty (modified). If this flag is set, the user will
11411143
* be asked to save changes to the project before closing the current project.
@@ -1262,9 +1264,57 @@ class CORE_EXPORT QgsProject : public QObject, public QgsExpressionContextGenera
12621264
bool mEvaluateDefaultValues = false; // evaluate default values immediately
12631265
QgsCoordinateReferenceSystem mCrs;
12641266
bool mDirty = false; // project has been modified since it has been read or saved
1267+
int mDirtyBlockCount = 0;
12651268
bool mTrustLayerMetadata = false;
12661269

12671270
QgsCoordinateTransformContext mTransformContext;
1271+
1272+
friend class QgsProjectDirtyBlocker;
1273+
};
1274+
1275+
/**
1276+
* Temporarily blocks QgsProject "dirtying" for the lifetime of the object.
1277+
*
1278+
* QgsProjectDirtyBlocker supports "stacked" blocking, so two QgsProjectDirtyBlockers created
1279+
* for the same project will both need to be destroyed before the project can be dirtied again.
1280+
*
1281+
* Note that QgsProjectDirtyBlocker only blocks calls which set the project as dirty - calls
1282+
* which set the project as clean are not blocked.
1283+
*
1284+
* Python scripts should not use QgsProjectDirtyBlocker directly. Instead, use QgsProject.blockDirtying()
1285+
* \code{.py}
1286+
* project = QgsProject.instance()
1287+
* with QgsProject.blockDirtying(project):
1288+
* # do something
1289+
* \endcode
1290+
*
1291+
* \see QgsProject::setDirty()
1292+
*
1293+
* \ingroup core
1294+
* \since QGIS 3.2
1295+
*/
1296+
class CORE_EXPORT QgsProjectDirtyBlocker
1297+
{
1298+
public:
1299+
1300+
/**
1301+
* Constructor for QgsProjectDirtyBlocker.
1302+
*
1303+
* This will block dirtying the specified \a project for the lifetime of this object.
1304+
*/
1305+
QgsProjectDirtyBlocker( QgsProject *project )
1306+
: mProject( project )
1307+
{
1308+
mProject->mDirtyBlockCount++;
1309+
}
1310+
1311+
~QgsProjectDirtyBlocker()
1312+
{
1313+
mProject->mDirtyBlockCount--;
1314+
}
1315+
1316+
private:
1317+
QgsProject *mProject = nullptr;
12681318
};
12691319

12701320
/**

‎tests/src/python/test_qgsproject.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import qgis # NOQA
2020

2121
from qgis.core import (QgsProject,
22+
QgsProjectDirtyBlocker,
2223
QgsApplication,
2324
QgsUnitTypes,
2425
QgsCoordinateReferenceSystem,
@@ -930,6 +931,78 @@ def testHomePath(self):
930931
scope = QgsExpressionContextUtils.projectScope(p)
931932
self.assertEqual(scope.variable('project_home'), '../home')
932933

934+
def testDirtyBlocker(self):
935+
# first test manual QgsProjectDirtyBlocker construction
936+
p = QgsProject()
937+
938+
dirty_spy = QSignalSpy(p.isDirtyChanged)
939+
# ^ will do *whatever* it takes to discover the enemy's secret plans!
940+
941+
# simple checks
942+
p.setDirty(True)
943+
self.assertTrue(p.isDirty())
944+
self.assertEqual(len(dirty_spy), 1)
945+
self.assertEqual(dirty_spy[-1], [True])
946+
p.setDirty(True) # already dirty
947+
self.assertTrue(p.isDirty())
948+
self.assertEqual(len(dirty_spy), 1)
949+
p.setDirty(False)
950+
self.assertFalse(p.isDirty())
951+
self.assertEqual(len(dirty_spy), 2)
952+
self.assertEqual(dirty_spy[-1], [False])
953+
p.setDirty(True)
954+
self.assertTrue(p.isDirty())
955+
self.assertEqual(len(dirty_spy), 3)
956+
self.assertEqual(dirty_spy[-1], [True])
957+
958+
# with a blocker
959+
blocker = QgsProjectDirtyBlocker(p)
960+
# blockers will allow cleaning projects
961+
p.setDirty(False)
962+
self.assertFalse(p.isDirty())
963+
self.assertEqual(len(dirty_spy), 4)
964+
self.assertEqual(dirty_spy[-1], [False])
965+
# but not dirtying!
966+
p.setDirty(True)
967+
self.assertFalse(p.isDirty())
968+
self.assertEqual(len(dirty_spy), 4)
969+
self.assertEqual(dirty_spy[-1], [False])
970+
# nested block
971+
blocker2 = QgsProjectDirtyBlocker(p)
972+
p.setDirty(True)
973+
self.assertFalse(p.isDirty())
974+
self.assertEqual(len(dirty_spy), 4)
975+
self.assertEqual(dirty_spy[-1], [False])
976+
del blocker2
977+
p.setDirty(True)
978+
self.assertFalse(p.isDirty())
979+
self.assertEqual(len(dirty_spy), 4)
980+
self.assertEqual(dirty_spy[-1], [False])
981+
del blocker
982+
p.setDirty(True)
983+
self.assertTrue(p.isDirty())
984+
self.assertEqual(len(dirty_spy), 5)
985+
self.assertEqual(dirty_spy[-1], [True])
986+
987+
# using python context manager
988+
with QgsProject.blockDirtying(p):
989+
# cleaning allowed
990+
p.setDirty(False)
991+
self.assertFalse(p.isDirty())
992+
self.assertEqual(len(dirty_spy), 6)
993+
self.assertEqual(dirty_spy[-1], [False])
994+
# but not dirtying!
995+
p.setDirty(True)
996+
self.assertFalse(p.isDirty())
997+
self.assertEqual(len(dirty_spy), 6)
998+
self.assertEqual(dirty_spy[-1], [False])
999+
1000+
# unblocked
1001+
p.setDirty(True)
1002+
self.assertTrue(p.isDirty())
1003+
self.assertEqual(len(dirty_spy), 7)
1004+
self.assertEqual(dirty_spy[-1], [True])
1005+
9331006

9341007
if __name__ == '__main__':
9351008
unittest.main()

0 commit comments

Comments
 (0)