[FEATURE] Allow overwriting the project home path

This allows the project home path (which is used by the browser
to create the 'Project Home' item) to be set by users for a
project, instead of always matching the location where the project
is saved.

This allows users to set the project home to a folder which contains
data and other content, and is especially useful for organisations
where qgis projects are not stored in the root folder of a organisational
'project'.

Project home paths can also be set to relative paths, in which
case they will be relative to the project saved location.

The path can be set through the Project Properties dialog, or
by right-clicking on the Project Home browser item and
selecting 'set project home'

Sponsored by SMEC/SJ
This commit is contained in:
Nyall Dawson 2018-03-07 15:30:23 +10:00
parent d09a34c900
commit 4e5c08e2b5
9 changed files with 377 additions and 70 deletions

View File

@ -442,9 +442,34 @@ Sets the default area measurement units for the project.
QString homePath() const;
%Docstring
Return project's home path
Returns the project's home path. This will either be a manually set home path
(see presetHomePath()) or the path containing the project file itself.
:return: home path of project (or null QString if not set) *
This method always returns the absolute path to the project's home. See
presetHomePath() to retrieve any manual project home path override (e.g.
relative home paths).
.. seealso:: :py:func:`setPresetHomePath`
.. seealso:: :py:func:`presetHomePath`
.. seealso:: :py:func:`homePathChanged`
%End
QString presetHomePath() const;
%Docstring
Returns any manual project home path setting, or an empty string if not set.
This path may be a relative path. See homePath() to retrieve a path which is always
an absolute path.
.. seealso:: :py:func:`homePath`
.. seealso:: :py:func:`setPresetHomePath`
.. seealso:: :py:func:`homePathChanged`
.. versionadded:: 3.2
%End
QgsRelationManager *relationManager() const;
@ -937,7 +962,13 @@ Emitted when the file name of the project changes
void homePathChanged();
%Docstring
Emitted when the home path of the project changes
Emitted when the home path of the project changes.
.. seealso:: :py:func:`setPresetHomePath`
.. seealso:: :py:func:`homePath`
.. seealso:: :py:func:`presetHomePath`
%End
void snappingConfigChanged( const QgsSnappingConfig &config );
@ -1170,6 +1201,20 @@ be asked to save changes to the project before closing the current project.
promoted to public slot in 2.16
%End
void setPresetHomePath( const QString &path );
%Docstring
Sets the project's home ``path``. If an empty path is specified than the
home path will be automatically determined from the project's file path.
.. versionadded:: 3.2
.. seealso:: :py:func:`presetHomePath`
.. seealso:: :py:func:`homePath`
.. seealso:: :py:func:`homePathChanged`
%End
};

View File

@ -204,6 +204,41 @@ QgsProjectProperties::QgsProjectProperties( QgsMapCanvas *mapCanvas, QWidget *pa
mAutoTransaction->setChecked( QgsProject::instance()->autoTransaction() );
title( QgsProject::instance()->title() );
mProjectFileLineEdit->setText( QDir::toNativeSeparators( QgsProject::instance()->fileName() ) );
mProjectHomeLineEdit->setShowClearButton( true );
mProjectHomeLineEdit->setText( QDir::toNativeSeparators( QgsProject::instance()->presetHomePath() ) );
connect( mButtonSetProjectHome, &QToolButton::clicked, this, [ = ]
{
auto getAbsoluteHome = [this]()->QString
{
QString currentHome = QDir::fromNativeSeparators( mProjectHomeLineEdit->text() );
if ( !currentHome.isEmpty() )
{
QFileInfo homeInfo( currentHome );
if ( !homeInfo.isRelative() )
return currentHome;
}
QFileInfo pfi( QgsProject::instance()->fileName() );
if ( !pfi.exists() )
return QDir::homePath();
if ( !currentHome.isEmpty() )
{
// path is relative to project file
return QDir::cleanPath( pfi.path() + '/' + currentHome );
}
else
{
return pfi.canonicalPath();
}
};
QString newDir = QFileDialog::getExistingDirectory( this, tr( "Select Project Home Path" ), getAbsoluteHome() );
if ( ! newDir.isNull() )
{
mProjectHomeLineEdit->setText( QDir::toNativeSeparators( newDir ) );
}
} );
connect( mButtonOpenProjectFolder, &QToolButton::clicked, this, [ = ]
{
@ -833,6 +868,7 @@ void QgsProjectProperties::apply()
// Set the project title
QgsProject::instance()->setTitle( title() );
QgsProject::instance()->setPresetHomePath( QDir::fromNativeSeparators( mProjectHomeLineEdit->text() ) );
QgsProject::instance()->setAutoTransaction( mAutoTransaction->isChecked() );
QgsProject::instance()->setEvaluateDefaultValues( mEvaluateDefaultValues->isChecked() );
QgsProject::instance()->setTrustLayerMetadata( mTrustProjectCheckBox->isChecked() );

View File

@ -177,7 +177,8 @@ void QgsBrowserModel::initialize()
if ( ! mInitialized )
{
connect( QgsProject::instance(), &QgsProject::readProject, this, &QgsBrowserModel::updateProjectHome );
connect( QgsProject::instance(), &QgsProject::writeProject, this, &QgsBrowserModel::updateProjectHome );
connect( QgsProject::instance(), &QgsProject::projectSaved, this, &QgsBrowserModel::updateProjectHome );
connect( QgsProject::instance(), &QgsProject::homePathChanged, this, &QgsBrowserModel::updateProjectHome );
addRootItems();
mInitialized = true;
}

View File

@ -28,6 +28,7 @@
#include <QVector>
#include <QStyle>
#include <QDesktopServices>
#include <QFileDialog>
#include "qgis.h"
#include "qgsdataitem.h"
@ -40,6 +41,7 @@
#include "qgsconfig.h"
#include "qgssettings.h"
#include "qgsanimatedicon.h"
#include "qgsproject.h"
// use GDAL VSI mechanism
#define CPL_SUPRESS_CPLUSPLUS //#spellok
@ -1614,6 +1616,28 @@ QVariant QgsProjectHomeItem::sortKey() const
return QStringLiteral( " 1" );
}
QList<QAction *> QgsProjectHomeItem::actions( QWidget *parent )
{
QList<QAction *> lst = QgsDirectoryItem::actions( parent );
QAction *separator = new QAction( parent );
separator->setSeparator( true );
lst.append( separator );
QAction *setHome = new QAction( tr( "Set Project Home…" ), parent );
connect( setHome, &QAction::triggered, this, [ = ]
{
QString oldHome = QgsProject::instance()->homePath();
QString newPath = QFileDialog::getExistingDirectory( parent, tr( "Select Project Home Directory" ), oldHome );
if ( !newPath.isEmpty() )
{
QgsProject::instance()->setPresetHomePath( newPath );
}
} );
lst << setHome;
return lst;
}
QgsFavoriteItem::QgsFavoriteItem( QgsFavoritesItem *parent, const QString &name, const QString &dirPath, const QString &path )
: QgsDirectoryItem( parent, name, dirPath, path )
, mFavorites( parent )

View File

@ -745,6 +745,9 @@ class CORE_EXPORT QgsProjectHomeItem : public QgsDirectoryItem
QIcon icon() override;
QVariant sortKey() const override;
QList<QAction *> actions( QWidget *parent ) override;
};
/**

View File

@ -417,6 +417,17 @@ void QgsProject::setDirty( bool b )
emit isDirtyChanged( mDirty );
}
void QgsProject::setPresetHomePath( const QString &path )
{
if ( path == mHomePath )
return;
mHomePath = path;
emit homePathChanged();
setDirty( true );
}
void QgsProject::setFileName( const QString &name )
{
if ( name == mFile.fileName() )
@ -488,6 +499,7 @@ void QgsProject::clear()
mFile.setFileName( QString() );
mProperties.clearKeys();
mTitle.clear();
mHomePath.clear();
mAutoTransaction = false;
mEvaluateDefaultValues = false;
mDirty = false;
@ -895,6 +907,19 @@ bool QgsProject::readProjectFile( const QString &filename )
// now get project title
_getTitle( *doc, mTitle );
QDomNodeList homePathNl = doc->elementsByTagName( QStringLiteral( "homePath" ) );
if ( homePathNl.count() > 0 )
{
QDomElement homePathElement = homePathNl.at( 0 ).toElement();
QString homePath = homePathElement.attribute( QStringLiteral( "path" ) );
if ( !homePath.isEmpty() )
setPresetHomePath( homePath );
}
else
{
emit homePathChanged();
}
QgsReadWriteContext context;
context.setPathResolver( pathResolver() );
@ -1370,6 +1395,10 @@ bool QgsProject::writeProjectFile( const QString &filename )
doc->appendChild( qgisNode );
QDomElement homePathNode = doc->createElement( QStringLiteral( "homePath" ) );
homePathNode.setAttribute( QStringLiteral( "path" ), mHomePath );
qgisNode.appendChild( homePathNode );
// title
QDomElement titleNode = doc->createElement( QStringLiteral( "title" ) );
qgisNode.appendChild( titleNode );
@ -2077,11 +2106,31 @@ void QgsProject::setAreaUnits( QgsUnitTypes::AreaUnit unit )
QString QgsProject::homePath() const
{
if ( !mHomePath.isEmpty() )
{
QFileInfo homeInfo( mHomePath );
if ( !homeInfo.isRelative() )
return mHomePath;
}
QFileInfo pfi( fileName() );
if ( !pfi.exists() )
return QString();
return mHomePath;
return pfi.canonicalPath();
if ( !mHomePath.isEmpty() )
{
// path is relative to project file
return QDir::cleanPath( pfi.path() + '/' + mHomePath );
}
else
{
return pfi.canonicalPath();
}
}
QString QgsProject::presetHomePath() const
{
return mHomePath;
}
QgsRelationManager *QgsProject::relationManager() const

View File

@ -85,7 +85,7 @@ class CORE_EXPORT QgsProject : public QObject, public QgsExpressionContextGenera
Q_OBJECT
Q_PROPERTY( QStringList nonIdentifiableLayers READ nonIdentifiableLayers WRITE setNonIdentifiableLayers NOTIFY nonIdentifiableLayersChanged )
Q_PROPERTY( QString fileName READ fileName WRITE setFileName NOTIFY fileNameChanged )
Q_PROPERTY( QString homePath READ homePath NOTIFY homePathChanged )
Q_PROPERTY( QString homePath READ homePath WRITE setPresetHomePath NOTIFY homePathChanged )
Q_PROPERTY( QgsCoordinateReferenceSystem crs READ crs WRITE setCrs NOTIFY crsChanged )
Q_PROPERTY( QString ellipsoid READ ellipsoid WRITE setEllipsoid NOTIFY ellipsoidChanged )
Q_PROPERTY( QgsMapThemeCollection *mapThemeCollection READ mapThemeCollection NOTIFY mapThemeCollectionChanged )
@ -423,10 +423,33 @@ class CORE_EXPORT QgsProject : public QObject, public QgsExpressionContextGenera
void setAreaUnits( QgsUnitTypes::AreaUnit unit );
/**
* Return project's home path
\returns home path of project (or null QString if not set) */
* Returns the project's home path. This will either be a manually set home path
* (see presetHomePath()) or the path containing the project file itself.
*
* This method always returns the absolute path to the project's home. See
* presetHomePath() to retrieve any manual project home path override (e.g.
* relative home paths).
*
* \see setPresetHomePath()
* \see presetHomePath()
* \see homePathChanged()
*/
QString homePath() const;
/**
* Returns any manual project home path setting, or an empty string if not set.
*
* This path may be a relative path. See homePath() to retrieve a path which is always
* an absolute path.
*
* \see homePath()
* \see setPresetHomePath()
* \see homePathChanged()
*
* \since QGIS 3.2
*/
QString presetHomePath() const;
QgsRelationManager *relationManager() const;
/**
@ -905,7 +928,12 @@ class CORE_EXPORT QgsProject : public QObject, public QgsExpressionContextGenera
//! Emitted when the file name of the project changes
void fileNameChanged();
//! Emitted when the home path of the project changes
/**
* Emitted when the home path of the project changes.
* \see setPresetHomePath()
* \see homePath()
* \see presetHomePath()
*/
void homePathChanged();
//! emitted whenever the configuration for snapping has changed
@ -1117,6 +1145,16 @@ class CORE_EXPORT QgsProject : public QObject, public QgsExpressionContextGenera
*/
void setDirty( bool b = true );
/**
* Sets the project's home \a path. If an empty path is specified than the
* home path will be automatically determined from the project's file path.
* \since QGIS 3.2
* \see presetHomePath()
* \see homePath()
* \see homePathChanged()
*/
void setPresetHomePath( const QString &path );
private slots:
void onMapLayersAdded( const QList<QgsMapLayer *> &layers );
void onMapLayersRemoved( const QList<QgsMapLayer *> &layers );
@ -1212,6 +1250,12 @@ class CORE_EXPORT QgsProject : public QObject, public QgsExpressionContextGenera
std::unique_ptr<QgsAuxiliaryStorage> mAuxiliaryStorage;
QFile mFile; // current physical project file
/**
* Manual override for project home path - if empty, home path is automatically
* created based on file name.
*/
QString mHomePath;
mutable QgsProjectPropertyKey mProperties; // property hierarchy, TODO: this shouldn't be mutable
QString mTitle; // project title
bool mAutoTransaction = false; // transaction grouped editing

View File

@ -284,11 +284,27 @@
<property name="title">
<string>General settings</string>
</property>
<property name="syncGroup">
<property name="syncGroup" stdset="0">
<string notr="true">projgeneral</string>
</property>
<layout class="QGridLayout" name="gridLayout_26">
<item row="3" column="0">
<item row="1" column="0">
<widget class="QLabel" name="label_30">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Project home</string>
</property>
<property name="buddy">
<cstring>titleEdit</cstring>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="label_3">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Preferred">
@ -304,7 +320,7 @@
</property>
</widget>
</item>
<item row="3" column="1">
<item row="4" column="1">
<widget class="QComboBox" name="cbxAbsolutePath">
<item>
<property name="text">
@ -318,8 +334,8 @@
</item>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label_4">
<item row="3" column="0">
<widget class="QLabel" name="textLabel1">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Preferred">
<horstretch>0</horstretch>
@ -327,56 +343,10 @@
</sizepolicy>
</property>
<property name="text">
<string>Project file</string>
<string>Selection color</string>
</property>
<property name="buddy">
<cstring>titleEdit</cstring>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_2">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Project title</string>
</property>
</widget>
</item>
<item row="3" column="2" colspan="2">
<spacer>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item row="4" column="0" colspan="4">
<widget class="QCheckBox" name="mMapTileRenderingCheckBox">
<property name="toolTip">
<string>Checking this setting avoids visible edge artifacts when rendering this project as separate map tiles. Rendering performance will be degraded.</string>
</property>
<property name="text">
<string>Avoid artifacts when project is rendered as map tiles (degrades performance)</string>
</property>
</widget>
</item>
<item row="1" column="1" colspan="3">
<widget class="QLineEdit" name="titleEdit">
<property name="toolTip">
<string>Descriptive project name</string>
</property>
<property name="text">
<string>Default project title</string>
<cstring>pbnSelectionColor</cstring>
</property>
</widget>
</item>
@ -408,8 +378,18 @@
</item>
</layout>
</item>
<item row="2" column="0">
<widget class="QLabel" name="textLabel1">
<item row="5" column="0" colspan="4">
<widget class="QCheckBox" name="mMapTileRenderingCheckBox">
<property name="toolTip">
<string>Checking this setting avoids visible edge artifacts when rendering this project as separate map tiles. Rendering performance will be degraded.</string>
</property>
<property name="text">
<string>Avoid artifacts when project is rendered as map tiles (degrades performance)</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label_4">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Preferred">
<horstretch>0</horstretch>
@ -417,14 +397,37 @@
</sizepolicy>
</property>
<property name="text">
<string>Selection color</string>
<string>Project file</string>
</property>
<property name="buddy">
<cstring>pbnSelectionColor</cstring>
<cstring>titleEdit</cstring>
</property>
</widget>
</item>
<item row="2" column="1" colspan="3">
<widget class="QLineEdit" name="titleEdit">
<property name="toolTip">
<string>Descriptive project name</string>
</property>
<property name="text">
<string>Default project title</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_2">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Project title</string>
</property>
</widget>
</item>
<item row="3" column="1" colspan="3">
<layout class="QHBoxLayout" name="horizontalLayout_5">
<item>
<widget class="QgsColorButton" name="pbnSelectionColor">
@ -495,6 +498,49 @@
</item>
</layout>
</item>
<item row="4" column="2" colspan="2">
<spacer>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item row="1" column="1" colspan="3">
<layout class="QHBoxLayout" name="horizontalLayout_14">
<item>
<widget class="QgsFilterLineEdit" name="mProjectHomeLineEdit">
<property name="toolTip">
<string>Project home path. Leave blank to use the current project file location.</string>
</property>
<property name="readOnly">
<bool>false</bool>
</property>
<property name="clearButtonEnabled">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="mButtonSetProjectHome">
<property name="toolTip">
<string>Set the project home path</string>
</property>
<property name="text">
<string>…</string>
</property>
<property name="autoRaise">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
@ -503,7 +549,7 @@
<property name="title">
<string>Measurements</string>
</property>
<property name="syncGroup">
<property name="syncGroup" stdset="0">
<string notr="true">projgeneral</string>
</property>
<layout class="QGridLayout" name="gridLayoutMeasureTool">
@ -572,7 +618,7 @@
<property name="title">
<string>Coordinate display</string>
</property>
<property name="syncGroup">
<property name="syncGroup" stdset="0">
<string notr="true">projgeneral</string>
</property>
<layout class="QGridLayout" name="gridLayout_18">
@ -662,7 +708,7 @@
<property name="checked">
<bool>false</bool>
</property>
<property name="syncGroup">
<property name="syncGroup" stdset="0">
<string notr="true">projgeneral</string>
</property>
<layout class="QGridLayout" name="gridLayout_7">
@ -2710,6 +2756,8 @@
<tabstop>scrollArea_2</tabstop>
<tabstop>mProjectFileLineEdit</tabstop>
<tabstop>mButtonOpenProjectFolder</tabstop>
<tabstop>mProjectHomeLineEdit</tabstop>
<tabstop>mButtonSetProjectHome</tabstop>
<tabstop>titleEdit</tabstop>
<tabstop>pbnSelectionColor</tabstop>
<tabstop>pbnCanvasColor</tabstop>

View File

@ -857,6 +857,63 @@ class TestQgsProject(unittest.TestCase):
self.assertTrue('source="./points.shp"' in content)
self.assertTrue('source="./landsat_4326.tif"' in content)
def testHomePath(self):
p = QgsProject()
path_changed_spy = QSignalSpy(p.homePathChanged)
self.assertFalse(p.homePath())
self.assertFalse(p.presetHomePath())
# simulate save file
tmp_dir = QTemporaryDir()
tmp_file = "{}/project.qgs".format(tmp_dir.path())
with open(tmp_file, 'w') as f:
pass
p.setFileName(tmp_file)
# home path should be file path
self.assertEqual(p.homePath(), tmp_dir.path())
self.assertFalse(p.presetHomePath())
self.assertEqual(len(path_changed_spy), 1)
# manually override home path
p.setPresetHomePath('/tmp/my_path')
self.assertEqual(p.homePath(), '/tmp/my_path')
self.assertEqual(p.presetHomePath(), '/tmp/my_path')
self.assertEqual(len(path_changed_spy), 2)
# no extra signal if path is unchanged
p.setPresetHomePath('/tmp/my_path')
self.assertEqual(p.homePath(), '/tmp/my_path')
self.assertEqual(p.presetHomePath(), '/tmp/my_path')
self.assertEqual(len(path_changed_spy), 2)
# setting file name should not affect home path is manually set
tmp_file_2 = "{}/project/project2.qgs".format(tmp_dir.path())
os.mkdir(tmp_dir.path() + '/project')
with open(tmp_file_2, 'w') as f:
pass
p.setFileName(tmp_file_2)
self.assertEqual(p.homePath(), '/tmp/my_path')
self.assertEqual(p.presetHomePath(), '/tmp/my_path')
self.assertEqual(len(path_changed_spy), 2)
# clear manual path
p.setPresetHomePath('')
self.assertEqual(p.homePath(), tmp_dir.path() + '/project')
self.assertFalse(p.presetHomePath())
self.assertEqual(len(path_changed_spy), 3)
# relative path
p.setPresetHomePath('../home')
self.assertEqual(p.homePath(), tmp_dir.path() + '/home')
self.assertEqual(p.presetHomePath(), '../home')
self.assertEqual(len(path_changed_spy), 4)
# relative path, no filename
p.setFileName('')
self.assertEqual(p.homePath(), '../home')
self.assertEqual(p.presetHomePath(), '../home')
if __name__ == '__main__':
unittest.main()