Skip to content
Permalink
Browse files

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.
  • Loading branch information
nyalldawson committed Mar 9, 2018
1 parent d6eeabf commit 60afeadf4475e557c94b7f16d84fe9df7e8618f6
@@ -234,6 +234,38 @@ def __exit__(self, ex_type, ex_value, traceback):
QgsReadWriteContext.enterCategory = ReadWriteContextEnterCategory


# Python class to extend QgsProjectDirtyBlocker C++ class


class ProjectDirtyBlocker():
"""
Context manager used to block project setDirty calls.
Example:
project = QgsProject.instance()
with QgsProject.blockDirtying(project):
# do something
.. versionadded:: 3.2
"""

def __init__(self, project):
self.project = project
self.blocker = None

def __enter__(self):
self.blocker = QgsProjectDirtyBlocker(self.project)
return self.project

def __exit__(self, ex_type, ex_value, traceback):
del self.blocker
return True


# Inject the context manager into QgsProject class as a member
QgsProject.blockDirtying = ProjectDirtyBlocker


class QgsTaskWrapper(QgsTask):

def __init__(self, description, flags, function, on_finished, *args, **kwargs):
@@ -1189,6 +1189,7 @@ The snapping configuration for this project.
.. versionadded:: 3.0
%End


void setDirty( bool b = true );
%Docstring
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.

};

class QgsProjectDirtyBlocker
{
%Docstring
Temporarily blocks QgsProject "dirtying" for the lifetime of the object.

QgsProjectDirtyBlocker supports "stacked" blocking, so two QgsProjectDirtyBlockers created
for the same project will both need to be destroyed before the project can be dirtied again.

Note that QgsProjectDirtyBlocker only blocks calls which set the project as dirty - calls
which set the project as clean are not blocked.

Python scripts should not use QgsProjectDirtyBlocker directly. Instead, use :py:func:`QgsProject.blockDirtying()`
.. code-block:: python

project = QgsProject.instance()
with QgsProject.blockDirtying(project):
# do something

.. seealso:: :py:func:`QgsProject.setDirty`

.. versionadded:: 3.2
%End

%TypeHeaderCode
#include "qgsproject.h"
%End
public:

QgsProjectDirtyBlocker( QgsProject *project );
%Docstring
Constructor for QgsProjectDirtyBlocker.

This will block dirtying the specified ``project`` for the lifetime of this object.
%End

~QgsProjectDirtyBlocker();

};


/************************************************************************
* This file has been generated automatically from *
@@ -5066,6 +5066,7 @@ bool QgisApp::fileNew( bool promptToSaveFlag, bool forceBlank )

QgsSettings settings;

MAYBE_UNUSED QgsProjectDirtyBlocker dirtyBlocker( QgsProject::instance() );
closeProject();

QgsProject *prj = QgsProject::instance();
@@ -5472,7 +5473,7 @@ void QgisApp::fileOpen()
// open the selected project
addProject( fullPath );
}
} // QgisApp::fileOpen
}

void QgisApp::enableProjectMacros()
{
@@ -5488,6 +5489,8 @@ void QgisApp::enableProjectMacros()
*/
bool QgisApp::addProject( const QString &projectFile )
{
MAYBE_UNUSED QgsProjectDirtyBlocker dirtyBlocker( QgsProject::instance() );

// close the previous opened project if any
closeProject();

@@ -411,9 +411,15 @@ bool QgsProject::isDirty() const
return mDirty;
}

void QgsProject::setDirty( bool b )
void QgsProject::setDirty( const bool dirty )
{
mDirty = b;
if ( dirty && mDirtyBlockCount > 0 )
return;

if ( mDirty == dirty )
return;

mDirty = dirty;
emit isDirtyChanged( mDirty );
}

@@ -1136,6 +1136,8 @@ class CORE_EXPORT QgsProject : public QObject, public QgsExpressionContextGenera
*/
void setSnappingConfig( const QgsSnappingConfig &snappingConfig );

// TODO QGIS 4.0 - rename b to dirty

/**
* Flag the project as dirty (modified). If this flag is set, the user will
* 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
bool mEvaluateDefaultValues = false; // evaluate default values immediately
QgsCoordinateReferenceSystem mCrs;
bool mDirty = false; // project has been modified since it has been read or saved
int mDirtyBlockCount = 0;
bool mTrustLayerMetadata = false;

QgsCoordinateTransformContext mTransformContext;

friend class QgsProjectDirtyBlocker;
};

/**
* Temporarily blocks QgsProject "dirtying" for the lifetime of the object.
*
* QgsProjectDirtyBlocker supports "stacked" blocking, so two QgsProjectDirtyBlockers created
* for the same project will both need to be destroyed before the project can be dirtied again.
*
* Note that QgsProjectDirtyBlocker only blocks calls which set the project as dirty - calls
* which set the project as clean are not blocked.
*
* Python scripts should not use QgsProjectDirtyBlocker directly. Instead, use QgsProject.blockDirtying()
* \code{.py}
* project = QgsProject.instance()
* with QgsProject.blockDirtying(project):
* # do something
* \endcode
*
* \see QgsProject::setDirty()
*
* \ingroup core
* \since QGIS 3.2
*/
class CORE_EXPORT QgsProjectDirtyBlocker
{
public:

/**
* Constructor for QgsProjectDirtyBlocker.
*
* This will block dirtying the specified \a project for the lifetime of this object.
*/
QgsProjectDirtyBlocker( QgsProject *project )
: mProject( project )
{
mProject->mDirtyBlockCount++;
}

~QgsProjectDirtyBlocker()
{
mProject->mDirtyBlockCount--;
}

private:
QgsProject *mProject = nullptr;
};

/**
@@ -19,6 +19,7 @@
import qgis # NOQA

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

def testDirtyBlocker(self):
# first test manual QgsProjectDirtyBlocker construction
p = QgsProject()

dirty_spy = QSignalSpy(p.isDirtyChanged)
# ^ will do *whatever* it takes to discover the enemy's secret plans!

# simple checks
p.setDirty(True)
self.assertTrue(p.isDirty())
self.assertEqual(len(dirty_spy), 1)
self.assertEqual(dirty_spy[-1], [True])
p.setDirty(True) # already dirty
self.assertTrue(p.isDirty())
self.assertEqual(len(dirty_spy), 1)
p.setDirty(False)
self.assertFalse(p.isDirty())
self.assertEqual(len(dirty_spy), 2)
self.assertEqual(dirty_spy[-1], [False])
p.setDirty(True)
self.assertTrue(p.isDirty())
self.assertEqual(len(dirty_spy), 3)
self.assertEqual(dirty_spy[-1], [True])

# with a blocker
blocker = QgsProjectDirtyBlocker(p)
# blockers will allow cleaning projects
p.setDirty(False)
self.assertFalse(p.isDirty())
self.assertEqual(len(dirty_spy), 4)
self.assertEqual(dirty_spy[-1], [False])
# but not dirtying!
p.setDirty(True)
self.assertFalse(p.isDirty())
self.assertEqual(len(dirty_spy), 4)
self.assertEqual(dirty_spy[-1], [False])
# nested block
blocker2 = QgsProjectDirtyBlocker(p)
p.setDirty(True)
self.assertFalse(p.isDirty())
self.assertEqual(len(dirty_spy), 4)
self.assertEqual(dirty_spy[-1], [False])
del blocker2
p.setDirty(True)
self.assertFalse(p.isDirty())
self.assertEqual(len(dirty_spy), 4)
self.assertEqual(dirty_spy[-1], [False])
del blocker
p.setDirty(True)
self.assertTrue(p.isDirty())
self.assertEqual(len(dirty_spy), 5)
self.assertEqual(dirty_spy[-1], [True])

# using python context manager
with QgsProject.blockDirtying(p):
# cleaning allowed
p.setDirty(False)
self.assertFalse(p.isDirty())
self.assertEqual(len(dirty_spy), 6)
self.assertEqual(dirty_spy[-1], [False])
# but not dirtying!
p.setDirty(True)
self.assertFalse(p.isDirty())
self.assertEqual(len(dirty_spy), 6)
self.assertEqual(dirty_spy[-1], [False])

# unblocked
p.setDirty(True)
self.assertTrue(p.isDirty())
self.assertEqual(len(dirty_spy), 7)
self.assertEqual(dirty_spy[-1], [True])


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

0 comments on commit 60afead

Please sign in to comment.
You can’t perform that action at this time.