Skip to content

Commit 835e6d2

Browse files
authored
Merge pull request #5062 from boundlessgeo/raster-widget-multiple-select
Allow multiple raster selection from GDAL source select widget
2 parents 50e8e1c + b947406 commit 835e6d2

File tree

5 files changed

+166
-42
lines changed

5 files changed

+166
-42
lines changed

python/gui/qgsfilewidget.sip

+14-3
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ class QgsFileWidget : QWidget
3030
enum StorageMode
3131
{
3232
GetFile,
33-
GetDirectory
33+
GetDirectory,
34+
GetMultipleFiles,
3435
};
3536

3637
enum RelativeStorage
@@ -47,10 +48,20 @@ class QgsFileWidget : QWidget
4748

4849
QString filePath();
4950
%Docstring
50-
Returns the current file path
51+
Returns the current file path(s)
52+
when multiple files are selected, they are quoted and separated
53+
by a single space (for example: '"/path/foo" "path/bar"')
54+
.. seealso:: filePaths
5155
:rtype: str
5256
%End
5357

58+
static QStringList splitFilePaths( const QString &path );
59+
%Docstring
60+
Split the the quoted and space separated ``path`` and returns a QString list
61+
.. seealso:: filePath
62+
:rtype: list of str
63+
%End
64+
5465
void setFilePath( QString path );
5566
%Docstring
5667
Sets the file path
@@ -72,7 +83,7 @@ returns the open file dialog title
7283
setDialogTitle defines the open file dialog title
7384
.. note::
7485

75-
if not defined, the title is "Select a file" or "Select a directory" depending on the configuration.
86+
if not defined, the title is "Select a file" or "Select a directory" or "Select one or more files" depending on the configuration.
7687
%End
7788

7889
QString filter() const;

src/gui/qgsfilewidget.cpp

+97-28
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,16 @@ QString QgsFileWidget::filePath()
7777
return mFilePath;
7878
}
7979

80+
QStringList QgsFileWidget::splitFilePaths( const QString &path )
81+
{
82+
QStringList paths;
83+
for ( auto pathsPart : path.split( QRegExp( "\"\\s+\"" ), QString::SkipEmptyParts ) )
84+
{
85+
paths.append( pathsPart.remove( QRegExp( "(^\\s*\")|(\"\\s*)" ) ) );
86+
}
87+
return paths;
88+
}
89+
8090
void QgsFileWidget::setFilePath( QString path )
8191
{
8292
if ( path == QgsApplication::nullRepresentation() )
@@ -86,6 +96,7 @@ void QgsFileWidget::setFilePath( QString path )
8696

8797
//will trigger textEdited slot
8898
mLineEdit->setValue( path );
99+
89100
}
90101

91102
void QgsFileWidget::setReadOnly( bool readOnly )
@@ -130,6 +141,15 @@ void QgsFileWidget::textEdited( const QString &path )
130141
{
131142
mFilePath = path;
132143
mLinkLabel->setText( toUrl( path ) );
144+
// Show tooltip if multiple files are selected
145+
if ( path.contains( QStringLiteral( "\" \"" ) ) )
146+
{
147+
mLineEdit->setToolTip( tr( "Selected files:<br><ul><li>%1</li></ul><br>" ).arg( splitFilePaths( path ).join( QStringLiteral( "</li><li>" ) ) ) );
148+
}
149+
else
150+
{
151+
mLineEdit->setToolTip( QString() );
152+
}
133153
emit fileChanged( mFilePath );
134154
}
135155

@@ -231,39 +251,75 @@ void QgsFileWidget::openFileDialog()
231251

232252
// Handle Storage
233253
QString fileName;
254+
QStringList fileNames;
234255
QString title;
235-
if ( mStorageMode == GetFile )
236-
{
237-
title = !mDialogTitle.isEmpty() ? mDialogTitle : tr( "Select a file" );
238-
fileName = QFileDialog::getOpenFileName( this, title, QFileInfo( oldPath ).absoluteFilePath(), mFilter );
239-
}
240-
else if ( mStorageMode == GetDirectory )
256+
257+
switch ( mStorageMode )
241258
{
242-
title = !mDialogTitle.isEmpty() ? mDialogTitle : tr( "Select a directory" );
243-
fileName = QFileDialog::getExistingDirectory( this, title, QFileInfo( oldPath ).absoluteFilePath(), QFileDialog::ShowDirsOnly );
259+
case GetFile:
260+
title = !mDialogTitle.isEmpty() ? mDialogTitle : tr( "Select a file" );
261+
fileName = QFileDialog::getOpenFileName( this, title, QFileInfo( oldPath ).absoluteFilePath(), mFilter );
262+
break;
263+
case GetMultipleFiles:
264+
title = !mDialogTitle.isEmpty() ? mDialogTitle : tr( "Select one ore more files" );
265+
fileNames = QFileDialog::getOpenFileNames( this, title, QFileInfo( oldPath ).absoluteFilePath(), mFilter );
266+
break;
267+
case GetDirectory:
268+
title = !mDialogTitle.isEmpty() ? mDialogTitle : tr( "Select a directory" );
269+
fileName = QFileDialog::getExistingDirectory( this, title, QFileInfo( oldPath ).absoluteFilePath(), QFileDialog::ShowDirsOnly );
270+
break;
244271
}
245272

246-
if ( fileName.isEmpty() )
273+
if ( fileName.isEmpty() && fileNames.isEmpty( ) )
247274
return;
248275

276+
if ( mStorageMode != GetMultipleFiles )
277+
{
278+
fileName = QDir::toNativeSeparators( QDir::cleanPath( QFileInfo( fileName ).absoluteFilePath() ) );
279+
}
280+
else
281+
{
282+
for ( int i = 0; i < fileNames.length(); i++ )
283+
{
284+
fileNames.replace( i, QDir::toNativeSeparators( QDir::cleanPath( QFileInfo( fileNames.at( i ) ).absoluteFilePath() ) ) ) ;
285+
}
286+
}
249287

250-
fileName = QDir::toNativeSeparators( QDir::cleanPath( QFileInfo( fileName ).absoluteFilePath() ) );
251288
// Store the last used path:
289+
switch ( mStorageMode )
290+
{
291+
case GetFile:
292+
settings.setValue( QStringLiteral( "UI/lastFileNameWidgetDir" ), QFileInfo( fileName ).absolutePath() );
293+
break;
294+
case GetDirectory:
295+
settings.setValue( QStringLiteral( "UI/lastFileNameWidgetDir" ), fileName );
296+
break;
297+
case GetMultipleFiles:
298+
settings.setValue( QStringLiteral( "UI/lastFileNameWidgetDir" ), QFileInfo( fileNames.first( ) ).absolutePath() );
299+
break;
300+
}
252301

253-
if ( mStorageMode == GetFile )
302+
// Handle relative Path storage
303+
if ( mStorageMode != GetMultipleFiles )
254304
{
255-
settings.setValue( QStringLiteral( "UI/lastFileNameWidgetDir" ), QFileInfo( fileName ).absolutePath() );
305+
fileName = relativePath( fileName, true );
306+
setFilePath( fileName );
256307
}
257-
else if ( mStorageMode == GetDirectory )
308+
else
258309
{
259-
settings.setValue( QStringLiteral( "UI/lastFileNameWidgetDir" ), fileName );
310+
for ( int i = 0; i < fileNames.length(); i++ )
311+
{
312+
fileNames.replace( i, relativePath( fileNames.at( i ), true ) );
313+
}
314+
if ( fileNames.length() > 1 )
315+
{
316+
setFilePath( QStringLiteral( "\"%1\"" ).arg( fileNames.join( "\" \"" ) ) );
317+
}
318+
else
319+
{
320+
setFilePath( fileNames.first( ) );
321+
}
260322
}
261-
262-
// Handle relative Path storage
263-
fileName = relativePath( fileName, true );
264-
265-
// Keep the new value
266-
setFilePath( fileName );
267323
}
268324

269325

@@ -327,7 +383,6 @@ QString QgsFileWidget::toUrl( const QString &path ) const
327383

328384

329385

330-
331386
///@cond PRIVATE
332387

333388

@@ -359,16 +414,30 @@ void QgsFileDropEdit::setFilters( const QString &filters )
359414

360415
QString QgsFileDropEdit::acceptableFilePath( QDropEvent *event ) const
361416
{
362-
QString path;
417+
QStringList paths;
363418
if ( event->mimeData()->hasUrls() )
364419
{
365-
QFileInfo file( event->mimeData()->urls().first().toLocalFile() );
366-
if ( ( mStorageMode == QgsFileWidget::GetFile && file.isFile() &&
367-
( mAcceptableExtensions.isEmpty() || mAcceptableExtensions.contains( file.suffix(), Qt::CaseInsensitive ) ) )
368-
|| ( mStorageMode == QgsFileWidget::GetDirectory && file.isDir() ) )
369-
path = file.filePath();
420+
Q_FOREACH ( const QUrl &url, event->mimeData()->urls() )
421+
{
422+
QFileInfo file( url.toLocalFile() );
423+
if ( ( mStorageMode != QgsFileWidget::GetDirectory && file.isFile() &&
424+
( mAcceptableExtensions.isEmpty() || mAcceptableExtensions.contains( file.suffix(), Qt::CaseInsensitive ) ) )
425+
|| ( mStorageMode == QgsFileWidget::GetDirectory && file.isDir() ) )
426+
paths.append( file.filePath() );
427+
}
428+
}
429+
if ( paths.size() > 1 )
430+
{
431+
return QStringLiteral( "\"%1\"" ).arg( paths.join( "\" \"" ) );
432+
}
433+
else if ( paths.size() == 1 )
434+
{
435+
return paths.first();
436+
}
437+
else
438+
{
439+
return QString();
370440
}
371-
return path;
372441
}
373442

374443
void QgsFileDropEdit::dragEnterEvent( QDragEnterEvent *event )

src/gui/qgsfilewidget.h

+17-7
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,9 @@ class GUI_EXPORT QgsFileWidget : public QWidget
6161
*/
6262
enum StorageMode
6363
{
64-
GetFile,
65-
GetDirectory
64+
GetFile, //! Select a single file
65+
GetDirectory, //! Select a directory
66+
GetMultipleFiles, //! Select multiple files
6667
};
6768

6869
/**
@@ -80,9 +81,20 @@ class GUI_EXPORT QgsFileWidget : public QWidget
8081
*/
8182
explicit QgsFileWidget( QWidget *parent SIP_TRANSFERTHIS = nullptr );
8283

83-
//! Returns the current file path
84+
/**
85+
* \brief Returns the current file path(s)
86+
* when multiple files are selected, they are quoted and separated
87+
* by a single space (for example: '"/path/foo" "path/bar"')
88+
* \see filePaths
89+
*/
8490
QString filePath();
8591

92+
/**
93+
* \brief Split the the quoted and space separated \a path and returns a QString list
94+
* \see filePath
95+
*/
96+
static QStringList splitFilePaths( const QString &path );
97+
8698
//! Sets the file path
8799
void setFilePath( QString path );
88100

@@ -94,7 +106,7 @@ class GUI_EXPORT QgsFileWidget : public QWidget
94106

95107
/**
96108
* \brief setDialogTitle defines the open file dialog title
97-
* \note if not defined, the title is "Select a file" or "Select a directory" depending on the configuration.
109+
* \note if not defined, the title is "Select a file" or "Select a directory" or "Select one or more files" depending on the configuration.
98110
*/
99111
void setDialogTitle( const QString &title );
100112

@@ -211,9 +223,7 @@ class GUI_EXPORT QgsFileDropEdit: public QgsFilterLineEdit
211223

212224
private:
213225

214-
/**
215-
Return file name if object meets drop criteria.
216-
*/
226+
//! Return file name if object meets drop criteria.
217227
QString acceptableFilePath( QDropEvent *event ) const;
218228

219229
QStringList mAcceptableExtensions;

src/providers/gdal/qgsgdalsourceselect.cpp

+5-2
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ QgsGdalSourceSelect::QgsGdalSourceSelect( QWidget *parent, Qt::WindowFlags fl, Q
2424
setupUi( this );
2525
setupButtons( buttonBox );
2626
mQgsFileWidget->setFilter( QgsProviderRegistry::instance()->fileRasterFilters() );
27+
mQgsFileWidget->setStorageMode( QgsFileWidget::GetMultipleFiles );
2728
connect( mQgsFileWidget, &QgsFileWidget::fileChanged, this, [ = ]( const QString & path )
2829
{
2930
mRasterPath = path;
@@ -38,8 +39,10 @@ QgsGdalSourceSelect::~QgsGdalSourceSelect()
3839

3940
void QgsGdalSourceSelect::addButtonClicked()
4041
{
41-
QFileInfo baseName( mRasterPath );
42-
emit addRasterLayer( mRasterPath, baseName.baseName(), QStringLiteral( "gdal" ) );
42+
Q_FOREACH ( const QString &path, QgsFileWidget::splitFilePaths( mRasterPath ) )
43+
{
44+
emit addRasterLayer( path, QFileInfo( path ).baseName(), QStringLiteral( "gdal" ) );
45+
}
4346
}
4447

4548
QGISEXTERN QgsGdalSourceSelect *selectWidget( QWidget *parent, Qt::WindowFlags fl, QgsProviderRegistry::WidgetMode widgetMode )

tests/src/gui/testqgsfilewidget.cpp

+33-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/***************************************************************************
2-
testqgsdoublespinbox.cpp
2+
testqgsfilewidget.cpp
33
--------------------------------------
44
Date : December 2014
55
Copyright : (C) 2014 Nyall Dawson
@@ -27,10 +27,11 @@ class TestQgsFileWidget: public QObject
2727
void cleanupTestCase(); // will be called after the last testfunction was executed.
2828
void init(); // will be called before each testfunction is executed.
2929
void cleanup(); // will be called after every testfunction.
30-
3130
void relativePath();
3231
void toUrl();
3332
void testDroppedFiles();
33+
void testMultipleFiles();
34+
void testSplitFilePaths();
3435

3536
};
3637

@@ -140,5 +141,35 @@ void TestQgsFileWidget::testDroppedFiles()
140141

141142
}
142143

144+
void TestQgsFileWidget::testMultipleFiles()
145+
{
146+
QgsFileWidget *w = new QgsFileWidget();
147+
w->setStorageMode( QgsFileWidget::GetMultipleFiles );
148+
149+
std::unique_ptr< QMimeData > mime( new QMimeData() );
150+
mime->setUrls( QList<QUrl>() << QUrl::fromLocalFile( TEST_DATA_DIR + QStringLiteral( "/bug5598.shp" ) )
151+
<< QUrl::fromLocalFile( TEST_DATA_DIR + QStringLiteral( "/bug5598.shp" ) ) );
152+
std::unique_ptr< QDropEvent > event( new QDropEvent( QPointF( 1, 1 ), Qt::CopyAction, mime.get(), Qt::LeftButton, Qt::NoModifier ) );
153+
154+
qobject_cast< QgsFileDropEdit * >( w->lineEdit() )->dropEvent( event.get() );
155+
QCOMPARE( w->lineEdit()->text(), QStringLiteral( "\"%1\" \"%1\"" ).arg( TEST_DATA_DIR + QStringLiteral( "/bug5598.shp" ) ) );
156+
}
157+
158+
159+
160+
void TestQgsFileWidget::testSplitFilePaths()
161+
{
162+
const QString path = QString( TEST_DATA_DIR + QStringLiteral( "/bug5598.shp" ) );
163+
QCOMPARE( QgsFileWidget::splitFilePaths( QStringLiteral( "\"%1\" \"%1\"" ).arg( path ) ), QStringList() << path << path );
164+
QCOMPARE( QgsFileWidget::splitFilePaths( QStringLiteral( "\"%1\" \"%1\"" ).arg( path ) ), QStringList() << path << path );
165+
QCOMPARE( QgsFileWidget::splitFilePaths( QStringLiteral( " \"%1\" \"%1\"" ).arg( path ) ), QStringList() << path << path );
166+
QCOMPARE( QgsFileWidget::splitFilePaths( QStringLiteral( " \"%1\" \"%1\" " ).arg( path ) ), QStringList() << path << path );
167+
QCOMPARE( QgsFileWidget::splitFilePaths( QStringLiteral( "\"%1\" \"%1\" " ).arg( path ) ), QStringList() << path << path );
168+
QCOMPARE( QgsFileWidget::splitFilePaths( path ), QStringList() << path );
169+
}
170+
171+
172+
173+
143174
QGSTEST_MAIN( TestQgsFileWidget )
144175
#include "testqgsfilewidget.moc"

0 commit comments

Comments
 (0)