Skip to content

Commit 2718317

Browse files
authored
Merge pull request #7298 from nyalldawson/layout
[layouts] Save last used export folder in project
2 parents c780d60 + c6b4404 commit 2718317

File tree

9 files changed

+189
-77
lines changed

9 files changed

+189
-77
lines changed

python/core/auto_generated/qgsfileutils.sip.in

+8
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,14 @@ for filenames with an '_' character.
7575
.. warning::
7676

7777
This method strips slashes from the filename, so it is safe to call with file names only, not complete paths.
78+
%End
79+
80+
static QString findClosestExistingPath( const QString &path );
81+
%Docstring
82+
Returns the top-most existing folder from ``path``. E.g. if ``path`` is "/home/user/projects/2018/P4343"
83+
and "/home/user/projects" exists but no "2018" subfolder exists, then the function will return "/home/user/projects".
84+
85+
.. versionadded:: 3.2
7886
%End
7987
};
8088

python/gui/auto_generated/layout/qgslayoutdesignerinterface.sip.in

+1-1
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ Returns the layout view utilized by the designer.
5757
Returns the designer's message bar.
5858
%End
5959

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

src/app/layout/qgslayoutdesignerdialog.cpp

+79-56
Large diffs are not rendered by default.

src/app/layout/qgslayoutdesignerdialog.h

+6-3
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ class QgsAppLayoutDesignerInterface : public QgsLayoutDesignerInterface
5959
QgsMasterLayoutInterface *masterLayout() override;
6060
QgsLayoutView *view() override;
6161
QgsMessageBar *messageBar() override;
62-
void selectItems( QList< QgsLayoutItem * > items ) override;
62+
void selectItems( const QList< QgsLayoutItem * > &items ) override;
6363

6464
public slots:
6565

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

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

505+
QString defaultExportPath() const;
506+
void setLastExportPath( const QString &path ) const;
507+
505508
};
506509

507510
#endif // QGSLAYOUTDESIGNERDIALOG_H

src/core/qgsfileutils.cpp

+38-2
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@
1515
#include "qgsfileutils.h"
1616
#include <QObject>
1717
#include <QRegularExpression>
18+
#include <QFileInfo>
19+
#include <QDir>
20+
#include <QSet>
1821

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

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

8790
QString QgsFileUtils::stringToSafeFilename( const QString &string )
8891
{
89-
QRegularExpression rx( "[/\\\\\\?%\\*\\:\\|\"<>]" );
92+
QRegularExpression rx( QStringLiteral( "[/\\\\\\?%\\*\\:\\|\"<>]" ) );
9093
QString s = string;
9194
s.replace( rx, QStringLiteral( "_" ) );
9295
return s;
9396
}
97+
98+
QString QgsFileUtils::findClosestExistingPath( const QString &path )
99+
{
100+
if ( path.isEmpty() )
101+
return QString();
102+
103+
QDir currentPath;
104+
QFileInfo fi( path );
105+
if ( fi.isFile() )
106+
currentPath = fi.dir();
107+
else
108+
currentPath = QDir( path );
109+
110+
QSet< QString > visited;
111+
while ( !currentPath.exists() )
112+
{
113+
const QString parentPath = QDir::cleanPath( currentPath.absolutePath() + QStringLiteral( "/.." ) );
114+
if ( visited.contains( parentPath ) )
115+
return QString(); // break circular links
116+
117+
if ( parentPath.isEmpty() || parentPath == '.' )
118+
return QString();
119+
currentPath = QDir( parentPath );
120+
visited << parentPath;
121+
}
122+
123+
const QString res = QDir::cleanPath( currentPath.absolutePath() );
124+
125+
if ( res == QDir::currentPath() )
126+
return QString(); // avoid default to binary folder if a filename alone is specified
127+
128+
return res == '.' ? QString() : res;
129+
}

src/core/qgsfileutils.h

+8
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,14 @@ class CORE_EXPORT QgsFileUtils
7878
* \warning This method strips slashes from the filename, so it is safe to call with file names only, not complete paths.
7979
*/
8080
static QString stringToSafeFilename( const QString &string );
81+
82+
/**
83+
* Returns the top-most existing folder from \a path. E.g. if \a path is "/home/user/projects/2018/P4343"
84+
* and "/home/user/projects" exists but no "2018" subfolder exists, then the function will return "/home/user/projects".
85+
*
86+
* \since QGIS 3.2
87+
*/
88+
static QString findClosestExistingPath( const QString &path );
8189
};
8290

8391
#endif // QGSFILEUTILS_H

src/core/qgsproject.cpp

+15-14
Original file line numberDiff line numberDiff line change
@@ -612,7 +612,7 @@ void QgsProject::clear()
612612
// basically a debugging tool to dump property list values
613613
void dump_( const QgsProjectPropertyKey &topQgsPropertyKey )
614614
{
615-
QgsDebugMsg( "current properties:" );
615+
QgsDebugMsg( QStringLiteral( "current properties:" ) );
616616
topQgsPropertyKey.dump();
617617
}
618618

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

661661
if ( scopes.count() < 1 )
662662
{
663-
QgsDebugMsg( "empty ``properties'' XML tag ... bailing" );
663+
QgsDebugMsg( QStringLiteral( "empty ``properties'' XML tag ... bailing" ) );
664664
return;
665665
}
666666

667667
if ( ! project_properties.readXml( propertiesElem ) )
668668
{
669-
QgsDebugMsg( "Project_properties.readXml() failed" );
669+
QgsDebugMsg( QStringLiteral( "Project_properties.readXml() failed" ) );
670670
}
671671
}
672672

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

684684
if ( !nl.count() )
685685
{
686-
QgsDebugMsg( "unable to find title element" );
686+
QgsDebugMsg( QStringLiteral( "unable to find title element" ) );
687687
return;
688688
}
689689

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

692692
if ( !titleNode.hasChildNodes() ) // if not, then there's no actual text
693693
{
694-
QgsDebugMsg( "unable to find title element" );
694+
QgsDebugMsg( QStringLiteral( "unable to find title element" ) );
695695
return;
696696
}
697697

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

700700
if ( !titleTextNode.isText() )
701701
{
702-
QgsDebugMsg( "unable to find title element" );
702+
QgsDebugMsg( QStringLiteral( "unable to find title element" ) );
703703
return;
704704
}
705705

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

716716
if ( !nl.count() )
717717
{
718-
QgsDebugMsg( " unable to find qgis element in project file" );
718+
QgsDebugMsg( QStringLiteral( " unable to find qgis element in project file" ) );
719719
return QgsProjectVersion( 0, 0, 0, QString() );
720720
}
721721

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

840840
if ( !mapLayer )
841841
{
842-
QgsDebugMsg( "Unable to create layer" );
842+
QgsDebugMsg( QStringLiteral( "Unable to create layer" ) );
843843

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

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

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

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

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

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

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

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

16441644
dump_( mProperties );
16451645

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

16481648
if ( !mProperties.isEmpty() ) // only worry about properties if we
16491649
// actually have any properties
@@ -2691,6 +2691,7 @@ QSet<QgsMapLayer *> QgsProject::requiredLayers() const
26912691
void QgsProject::setRequiredLayers( const QSet<QgsMapLayer *> &layers )
26922692
{
26932693
QStringList layerIds;
2694+
layerIds.reserve( layers.count() );
26942695
for ( QgsMapLayer *layer : layers )
26952696
{
26962697
layerIds << layer->id();

src/gui/layout/qgslayoutdesignerinterface.h

+1-1
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ class GUI_EXPORT QgsLayoutDesignerInterface: public QObject
7676
/**
7777
* Selects the specified \a items.
7878
*/
79-
virtual void selectItems( QList< QgsLayoutItem * > items ) = 0;
79+
virtual void selectItems( const QList< QgsLayoutItem * > &items ) = 0;
8080

8181
public slots:
8282

tests/src/python/test_qgsfileutils.py

+33
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414

1515
import qgis # NOQA
1616

17+
import tempfile
18+
import os
1719
from qgis.core import QgsFileUtils
1820
from qgis.testing import unittest
1921

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

66+
def testFindClosestExistingPath(self):
67+
self.assertEqual(QgsFileUtils.findClosestExistingPath(''), '')
68+
self.assertEqual(QgsFileUtils.findClosestExistingPath('.'), '')
69+
self.assertEqual(QgsFileUtils.findClosestExistingPath('just_a_filename'), '')
70+
self.assertEqual(QgsFileUtils.findClosestExistingPath('just_a_filename.txt'), '')
71+
self.assertEqual(QgsFileUtils.findClosestExistingPath('a_very_unlikely_path_to_really_exist/because/no_one_would_have_a_folder_called/MapInfo is the bestest/'), '')
72+
# sorry anyone not on linux!
73+
self.assertEqual(QgsFileUtils.findClosestExistingPath('/usr/youve_been_hacked/by_the_l77t_krew'), '/usr')
74+
75+
base_path = tempfile.mkdtemp()
76+
file = os.path.join(base_path, 'test.csv')
77+
with open(file, 'wt') as f:
78+
f.write('\n')
79+
80+
self.assertEqual(QgsFileUtils.findClosestExistingPath(os.path.join(base_path, 'a file name.bmp')), base_path) # non-existent file
81+
self.assertEqual(QgsFileUtils.findClosestExistingPath(file), base_path) # real file!
82+
self.assertEqual(QgsFileUtils.findClosestExistingPath(os.path.join(base_path, 'non/existent/subfolder')), base_path)
83+
84+
sub_folder1 = os.path.join(base_path, 'subfolder1')
85+
os.mkdir(sub_folder1)
86+
sub_folder2 = os.path.join(sub_folder1, 'subfolder2')
87+
os.mkdir(sub_folder2)
88+
bad_sub_folder = os.path.join(sub_folder2, 'nooo')
89+
self.assertEqual(QgsFileUtils.findClosestExistingPath(bad_sub_folder), sub_folder2)
90+
self.assertEqual(QgsFileUtils.findClosestExistingPath(sub_folder2), sub_folder2)
91+
self.assertEqual(QgsFileUtils.findClosestExistingPath(sub_folder2 + '/.'), sub_folder2)
92+
self.assertEqual(QgsFileUtils.findClosestExistingPath(sub_folder2 + '/..'), sub_folder1)
93+
self.assertEqual(QgsFileUtils.findClosestExistingPath(sub_folder2 + '/../ddddddd'), sub_folder1)
94+
self.assertEqual(QgsFileUtils.findClosestExistingPath(sub_folder2 + '/../subfolder2'), sub_folder2)
95+
self.assertEqual(QgsFileUtils.findClosestExistingPath(sub_folder2 + '/../subfolder2/zxcv/asfdasd'), sub_folder2)
96+
6497

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

0 commit comments

Comments
 (0)