Skip to content
Permalink
Browse files
Merge pull request #7298 from nyalldawson/layout
[layouts] Save last used export folder in project
  • Loading branch information
m-kuhn committed Jun 22, 2018
2 parents c780d60 + c6b4404 commit 2718317
Show file tree
Hide file tree
Showing 9 changed files with 189 additions and 77 deletions.
@@ -75,6 +75,14 @@ for filenames with an '_' character.
.. warning::

This method strips slashes from the filename, so it is safe to call with file names only, not complete paths.
%End

static QString findClosestExistingPath( const QString &path );
%Docstring
Returns the top-most existing folder from ``path``. E.g. if ``path`` is "/home/user/projects/2018/P4343"
and "/home/user/projects" exists but no "2018" subfolder exists, then the function will return "/home/user/projects".

.. versionadded:: 3.2
%End
};

@@ -57,7 +57,7 @@ Returns the layout view utilized by the designer.
Returns the designer's message bar.
%End

virtual void selectItems( QList< QgsLayoutItem * > items ) = 0;
virtual void selectItems( const QList< QgsLayoutItem * > &items ) = 0;
%Docstring
Selects the specified ``items``.
%End

Large diffs are not rendered by default.

@@ -59,7 +59,7 @@ class QgsAppLayoutDesignerInterface : public QgsLayoutDesignerInterface
QgsMasterLayoutInterface *masterLayout() override;
QgsLayoutView *view() override;
QgsMessageBar *messageBar() override;
void selectItems( QList< QgsLayoutItem * > items ) override;
void selectItems( const QList< QgsLayoutItem * > &items ) override;

public slots:

@@ -132,7 +132,7 @@ class QgsLayoutDesignerDialog: public QMainWindow, private Ui::QgsLayoutDesigner
/**
* Selects the specified \a items.
*/
void selectItems( QList< QgsLayoutItem * > items );
void selectItems( const QList<QgsLayoutItem *> &items );

/**
* Returns the designer's message bar.
@@ -309,7 +309,7 @@ class QgsLayoutDesignerDialog: public QMainWindow, private Ui::QgsLayoutDesigner
void addPages();
void statusMessageReceived( const QString &message );
void dockVisibilityChanged( bool visible );
void undoRedoOccurredForItems( QSet< QString > itemUuids );
void undoRedoOccurredForItems( const QSet< QString > &itemUuids );
void saveAsTemplate();
void addItemsFromTemplate();
void duplicate();
@@ -502,6 +502,9 @@ class QgsLayoutDesignerDialog: public QMainWindow, private Ui::QgsLayoutDesigner
QString reportTypeString();
void updateActionNames( QgsMasterLayoutInterface::Type type );

QString defaultExportPath() const;
void setLastExportPath( const QString &path ) const;

};

#endif // QGSLAYOUTDESIGNERDIALOG_H
@@ -15,6 +15,9 @@
#include "qgsfileutils.h"
#include <QObject>
#include <QRegularExpression>
#include <QFileInfo>
#include <QDir>
#include <QSet>

QString QgsFileUtils::representFileSize( qint64 bytes )
{
@@ -29,7 +32,7 @@ QString QgsFileUtils::representFileSize( qint64 bytes )
unit = i.next();
bytes /= 1024.0;
}
return QString( "%1 %2" ).arg( QString::number( bytes ), unit );
return QStringLiteral( "%1 %2" ).arg( QString::number( bytes ), unit );
}

QStringList QgsFileUtils::extensionsFromFilter( const QString &filter )
@@ -86,8 +89,41 @@ QString QgsFileUtils::addExtensionFromFilter( const QString &fileName, const QSt

QString QgsFileUtils::stringToSafeFilename( const QString &string )
{
QRegularExpression rx( "[/\\\\\\?%\\*\\:\\|\"<>]" );
QRegularExpression rx( QStringLiteral( "[/\\\\\\?%\\*\\:\\|\"<>]" ) );
QString s = string;
s.replace( rx, QStringLiteral( "_" ) );
return s;
}

QString QgsFileUtils::findClosestExistingPath( const QString &path )
{
if ( path.isEmpty() )
return QString();

QDir currentPath;
QFileInfo fi( path );
if ( fi.isFile() )
currentPath = fi.dir();
else
currentPath = QDir( path );

QSet< QString > visited;
while ( !currentPath.exists() )
{
const QString parentPath = QDir::cleanPath( currentPath.absolutePath() + QStringLiteral( "/.." ) );
if ( visited.contains( parentPath ) )
return QString(); // break circular links

if ( parentPath.isEmpty() || parentPath == '.' )
return QString();
currentPath = QDir( parentPath );
visited << parentPath;
}

const QString res = QDir::cleanPath( currentPath.absolutePath() );

if ( res == QDir::currentPath() )
return QString(); // avoid default to binary folder if a filename alone is specified

return res == '.' ? QString() : res;
}
@@ -78,6 +78,14 @@ class CORE_EXPORT QgsFileUtils
* \warning This method strips slashes from the filename, so it is safe to call with file names only, not complete paths.
*/
static QString stringToSafeFilename( const QString &string );

/**
* Returns the top-most existing folder from \a path. E.g. if \a path is "/home/user/projects/2018/P4343"
* and "/home/user/projects" exists but no "2018" subfolder exists, then the function will return "/home/user/projects".
*
* \since QGIS 3.2
*/
static QString findClosestExistingPath( const QString &path );
};

#endif // QGSFILEUTILS_H
@@ -612,7 +612,7 @@ void QgsProject::clear()
// basically a debugging tool to dump property list values
void dump_( const QgsProjectPropertyKey &topQgsPropertyKey )
{
QgsDebugMsg( "current properties:" );
QgsDebugMsg( QStringLiteral( "current properties:" ) );
topQgsPropertyKey.dump();
}

@@ -660,13 +660,13 @@ void _getProperties( const QDomDocument &doc, QgsProjectPropertyKey &project_pro

if ( scopes.count() < 1 )
{
QgsDebugMsg( "empty ``properties'' XML tag ... bailing" );
QgsDebugMsg( QStringLiteral( "empty ``properties'' XML tag ... bailing" ) );
return;
}

if ( ! project_properties.readXml( propertiesElem ) )
{
QgsDebugMsg( "Project_properties.readXml() failed" );
QgsDebugMsg( QStringLiteral( "Project_properties.readXml() failed" ) );
}
}

@@ -683,23 +683,23 @@ static void _getTitle( const QDomDocument &doc, QString &title )

if ( !nl.count() )
{
QgsDebugMsg( "unable to find title element" );
QgsDebugMsg( QStringLiteral( "unable to find title element" ) );
return;
}

QDomNode titleNode = nl.item( 0 ); // there should only be one, so zeroth element OK

if ( !titleNode.hasChildNodes() ) // if not, then there's no actual text
{
QgsDebugMsg( "unable to find title element" );
QgsDebugMsg( QStringLiteral( "unable to find title element" ) );
return;
}

QDomNode titleTextNode = titleNode.firstChild(); // should only have one child

if ( !titleTextNode.isText() )
{
QgsDebugMsg( "unable to find title element" );
QgsDebugMsg( QStringLiteral( "unable to find title element" ) );
return;
}

@@ -715,7 +715,7 @@ QgsProjectVersion getVersion( const QDomDocument &doc )

if ( !nl.count() )
{
QgsDebugMsg( " unable to find qgis element in project file" );
QgsDebugMsg( QStringLiteral( " unable to find qgis element in project file" ) );
return QgsProjectVersion( 0, 0, 0, QString() );
}

@@ -839,7 +839,7 @@ bool QgsProject::addLayer( const QDomElement &layerElem, QList<QDomNode> &broken

if ( !mapLayer )
{
QgsDebugMsg( "Unable to create layer" );
QgsDebugMsg( QStringLiteral( "Unable to create layer" ) );

return false;
}
@@ -976,7 +976,7 @@ bool QgsProject::readProjectFile( const QString &filename )

// Shows a warning when an old project file is read.
emit oldProjectVersionWarning( fileVersion.text() );
QgsDebugMsg( "Emitting oldProjectVersionWarning(oldVersion)." );
QgsDebugMsg( QStringLiteral( "Emitting oldProjectVersionWarning(oldVersion)." ) );

projectFile.updateRevision( thisVersion );
}
@@ -1124,7 +1124,7 @@ bool QgsProject::readProjectFile( const QString &filename )
// review the integrity of the retrieved map layers
if ( !clean )
{
QgsDebugMsg( "Unable to get map layers from project file." );
QgsDebugMsg( QStringLiteral( "Unable to get map layers from project file." ) );

if ( !brokenNodes.isEmpty() )
{
@@ -1551,15 +1551,15 @@ bool QgsProject::writeProjectFile( const QString &filename )
qgisNode.appendChild( titleNode );

QDomElement transactionNode = doc->createElement( QStringLiteral( "autotransaction" ) );
transactionNode.setAttribute( QStringLiteral( "active" ), mAutoTransaction ? "1" : "0" );
transactionNode.setAttribute( QStringLiteral( "active" ), mAutoTransaction ? '1' : '0' );
qgisNode.appendChild( transactionNode );

QDomElement evaluateDefaultValuesNode = doc->createElement( QStringLiteral( "evaluateDefaultValues" ) );
evaluateDefaultValuesNode.setAttribute( QStringLiteral( "active" ), mEvaluateDefaultValues ? "1" : "0" );
evaluateDefaultValuesNode.setAttribute( QStringLiteral( "active" ), mEvaluateDefaultValues ? '1' : '0' );
qgisNode.appendChild( evaluateDefaultValuesNode );

QDomElement trustNode = doc->createElement( QStringLiteral( "trust" ) );
trustNode.setAttribute( QStringLiteral( "active" ), mTrustLayerMetadata ? "1" : "0" );
trustNode.setAttribute( QStringLiteral( "active" ), mTrustLayerMetadata ? '1' : '0' );
qgisNode.appendChild( trustNode );

QDomText titleText = doc->createTextNode( title() ); // XXX why have title TWICE?
@@ -1643,7 +1643,7 @@ bool QgsProject::writeProjectFile( const QString &filename )

dump_( mProperties );

QgsDebugMsg( QString( "there are %1 property scopes" ).arg( static_cast<int>( mProperties.count() ) ) );
QgsDebugMsg( QStringLiteral( "there are %1 property scopes" ).arg( static_cast<int>( mProperties.count() ) ) );

if ( !mProperties.isEmpty() ) // only worry about properties if we
// actually have any properties
@@ -2691,6 +2691,7 @@ QSet<QgsMapLayer *> QgsProject::requiredLayers() const
void QgsProject::setRequiredLayers( const QSet<QgsMapLayer *> &layers )
{
QStringList layerIds;
layerIds.reserve( layers.count() );
for ( QgsMapLayer *layer : layers )
{
layerIds << layer->id();
@@ -76,7 +76,7 @@ class GUI_EXPORT QgsLayoutDesignerInterface: public QObject
/**
* Selects the specified \a items.
*/
virtual void selectItems( QList< QgsLayoutItem * > items ) = 0;
virtual void selectItems( const QList< QgsLayoutItem * > &items ) = 0;

public slots:

@@ -14,6 +14,8 @@

import qgis # NOQA

import tempfile
import os
from qgis.core import QgsFileUtils
from qgis.testing import unittest

@@ -61,6 +63,37 @@ def testStringToSafeFilename(self):
QgsFileUtils.stringToSafeFilename('rendered map_final? rev (12-03-1017)_real/\\?%*:|"<>.tif'),
'rendered map_final_ rev (12-03-1017)_real__________.tif')

def testFindClosestExistingPath(self):
self.assertEqual(QgsFileUtils.findClosestExistingPath(''), '')
self.assertEqual(QgsFileUtils.findClosestExistingPath('.'), '')
self.assertEqual(QgsFileUtils.findClosestExistingPath('just_a_filename'), '')
self.assertEqual(QgsFileUtils.findClosestExistingPath('just_a_filename.txt'), '')
self.assertEqual(QgsFileUtils.findClosestExistingPath('a_very_unlikely_path_to_really_exist/because/no_one_would_have_a_folder_called/MapInfo is the bestest/'), '')
# sorry anyone not on linux!
self.assertEqual(QgsFileUtils.findClosestExistingPath('/usr/youve_been_hacked/by_the_l77t_krew'), '/usr')

base_path = tempfile.mkdtemp()
file = os.path.join(base_path, 'test.csv')
with open(file, 'wt') as f:
f.write('\n')

self.assertEqual(QgsFileUtils.findClosestExistingPath(os.path.join(base_path, 'a file name.bmp')), base_path) # non-existent file
self.assertEqual(QgsFileUtils.findClosestExistingPath(file), base_path) # real file!
self.assertEqual(QgsFileUtils.findClosestExistingPath(os.path.join(base_path, 'non/existent/subfolder')), base_path)

sub_folder1 = os.path.join(base_path, 'subfolder1')
os.mkdir(sub_folder1)
sub_folder2 = os.path.join(sub_folder1, 'subfolder2')
os.mkdir(sub_folder2)
bad_sub_folder = os.path.join(sub_folder2, 'nooo')
self.assertEqual(QgsFileUtils.findClosestExistingPath(bad_sub_folder), sub_folder2)
self.assertEqual(QgsFileUtils.findClosestExistingPath(sub_folder2), sub_folder2)
self.assertEqual(QgsFileUtils.findClosestExistingPath(sub_folder2 + '/.'), sub_folder2)
self.assertEqual(QgsFileUtils.findClosestExistingPath(sub_folder2 + '/..'), sub_folder1)
self.assertEqual(QgsFileUtils.findClosestExistingPath(sub_folder2 + '/../ddddddd'), sub_folder1)
self.assertEqual(QgsFileUtils.findClosestExistingPath(sub_folder2 + '/../subfolder2'), sub_folder2)
self.assertEqual(QgsFileUtils.findClosestExistingPath(sub_folder2 + '/../subfolder2/zxcv/asfdasd'), sub_folder2)


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

0 comments on commit 2718317

Please sign in to comment.