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.
This commit is contained in:
Nyall Dawson 2018-03-09 11:54:57 +10:00
parent d6eeabf69b
commit 60afeadf44
6 changed files with 207 additions and 3 deletions

View File

@ -234,6 +234,38 @@ class ReadWriteContextEnterCategory():
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):

View File

@ -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 *

View File

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

View File

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

View File

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

View File

@ -19,6 +19,7 @@ import os
import qgis # NOQA
from qgis.core import (QgsProject,
QgsProjectDirtyBlocker,
QgsApplication,
QgsUnitTypes,
QgsCoordinateReferenceSystem,
@ -930,6 +931,78 @@ class TestQgsProject(unittest.TestCase):
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()