QGIS API Documentation 3.37.0-Master (fdefdf9c27f)
qgsfilewidget.cpp
Go to the documentation of this file.
1/***************************************************************************
2 qgsfilewidget.cpp
3
4 ---------------------
5 begin : 17.12.2015
6 copyright : (C) 2015 by Denis Rouzaud
8 ***************************************************************************
9 * *
10 * This program is free software; you can redistribute it and/or modify *
11 * it under the terms of the GNU General Public License as published by *
12 * the Free Software Foundation; either version 2 of the License, or *
13 * (at your option) any later version. *
14 * *
15 ***************************************************************************/
16
17#include "qgsfilewidget.h"
18
19#include <QLineEdit>
20#include <QToolButton>
21#include <QLabel>
22#include <QGridLayout>
23#include <QUrl>
24#include <QDropEvent>
25#include <QRegularExpression>
26
27#include "qgssettings.h"
28#include "qgsfilterlineedit.h"
29#include "qgsfocuskeeper.h"
30#include "qgslogger.h"
31#include "qgsproject.h"
32#include "qgsapplication.h"
33#include "qgsfileutils.h"
34#include "qgsmimedatautils.h"
35
37 : QWidget( parent )
38{
39 mLayout = new QHBoxLayout();
40 mLayout->setContentsMargins( 0, 0, 0, 0 );
41
42 // If displaying a hyperlink, use a QLabel
43 mLinkLabel = new QLabel( this );
44 // Make Qt opens the link with the OS defined viewer
45 mLinkLabel->setOpenExternalLinks( true );
46 // Label should always be enabled to be able to open
47 // the link on read only mode.
48 mLinkLabel->setEnabled( true );
49 mLinkLabel->setSizePolicy( QSizePolicy::Expanding, QSizePolicy::Preferred );
50 mLinkLabel->setTextFormat( Qt::RichText );
51 mLinkLabel->hide(); // do not show by default
52 mLayout->addWidget( mLinkLabel );
53
54 // otherwise, use the traditional QLineEdit subclass
55 mLineEdit = new QgsFileDropEdit( this );
56 mLineEdit->setDragEnabled( true );
57 mLineEdit->setToolTip( tr( "Full path to the file(s), including name and extension" ) );
58 connect( mLineEdit, &QLineEdit::textChanged, this, &QgsFileWidget::textEdited );
59 connect( mLineEdit, &QgsFileDropEdit::fileDropped, this, &QgsFileWidget::fileDropped );
60 mLayout->addWidget( mLineEdit );
61
62 mLinkEditButton = new QToolButton( this );
63 mLinkEditButton->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "/mActionToggleEditing.svg" ) ) );
64 mLayout->addWidget( mLinkEditButton );
65 connect( mLinkEditButton, &QToolButton::clicked, this, &QgsFileWidget::editLink );
66 mLinkEditButton->hide(); // do not show by default
67
68 mFileWidgetButton = new QToolButton( this );
69 mFileWidgetButton->setText( QChar( 0x2026 ) );
70 mFileWidgetButton->setToolTip( tr( "Browse" ) );
71 connect( mFileWidgetButton, &QAbstractButton::clicked, this, &QgsFileWidget::openFileDialog );
72 mLayout->addWidget( mFileWidgetButton );
73
74 setLayout( mLayout );
75}
76
78{
79 return mFilePath;
80}
81
82QStringList QgsFileWidget::splitFilePaths( const QString &path )
83{
84 QStringList paths;
85 const thread_local QRegularExpression partsRegex = QRegularExpression( QStringLiteral( "\"\\s+\"" ) );
86 const QStringList pathParts = path.split( partsRegex, Qt::SkipEmptyParts );
87
88 const thread_local QRegularExpression cleanRe( QStringLiteral( "(^\\s*\")|(\"\\s*)" ) );
89 paths.reserve( pathParts.size() );
90 for ( const QString &pathsPart : pathParts )
91 {
92 QString cleaned = pathsPart;
93 cleaned.remove( cleanRe );
94 paths.append( cleaned );
95 }
96 return paths;
97}
98
99void QgsFileWidget::setFilePath( const QString &path )
100{
101 //will trigger textEdited slot
102 mLineEdit->setValue( path );
103}
104
105void QgsFileWidget::setReadOnly( bool readOnly )
106{
107 if ( mReadOnly == readOnly )
108 return;
109
110 mReadOnly = readOnly;
111
112 updateLayout();
113}
114
116{
117 return mDialogTitle;
118}
119
120void QgsFileWidget::setDialogTitle( const QString &title )
121{
122 mDialogTitle = title;
123}
124
126{
127 return mFilter;
128}
129
130void QgsFileWidget::setFilter( const QString &filters )
131{
132 mFilter = filters;
133 mLineEdit->setFilters( filters );
134}
135
136QFileDialog::Options QgsFileWidget::options() const
137{
138 return mOptions;
139}
140
141void QgsFileWidget::setOptions( QFileDialog::Options options )
142{
144}
145
147{
148 return mButtonVisible;
149}
150
152{
153 mButtonVisible = visible;
154 mFileWidgetButton->setVisible( visible );
155}
156
157bool QgsFileWidget::isMultiFiles( const QString &path )
158{
159 return path.contains( QStringLiteral( "\" \"" ) );
160}
161
162void QgsFileWidget::textEdited( const QString &path )
163{
164 mFilePath = path;
165 mLinkLabel->setText( toUrl( path ) );
166 // Show tooltip if multiple files are selected
167 if ( isMultiFiles( path ) )
168 {
169 mLineEdit->setToolTip( tr( "Selected files:<br><ul><li>%1</li></ul><br>" ).arg( splitFilePaths( path ).join( QLatin1String( "</li><li>" ) ) ) );
170 }
171 else
172 {
173 mLineEdit->setToolTip( QString() );
174 }
175 emit fileChanged( mFilePath );
176}
177
178void QgsFileWidget::editLink()
179{
180 if ( !mUseLink || mReadOnly )
181 return;
182
184 updateLayout();
185}
186
187void QgsFileWidget::fileDropped( const QString &filePath )
188{
189 setSelectedFileNames( QStringList() << filePath );
190 mLineEdit->selectAll();
191 mLineEdit->setFocus( Qt::MouseFocusReason );
192}
193
195{
196 return mUseLink;
197}
198
199void QgsFileWidget::setUseLink( bool useLink )
200{
201 if ( mUseLink == useLink )
202 return;
203
205 updateLayout();
206}
207
209{
210 return mFullUrl;
211}
212
213void QgsFileWidget::setFullUrl( bool fullUrl )
214{
216}
217
219{
220 return mDefaultRoot;
221}
222
223void QgsFileWidget::setDefaultRoot( const QString &defaultRoot )
224{
226}
227
229{
230 return mStorageMode;
231}
232
234{
236 mLineEdit->setStorageMode( storageMode );
237}
238
240{
241 return mRelativeStorage;
242}
243
245{
247}
248
250{
251 return mLineEdit;
252}
253
255{
256 const bool linkVisible = mUseLink && !mIsLinkEdited;
257
258 mLineEdit->setVisible( !linkVisible );
259 mLinkLabel->setVisible( linkVisible );
260 mLinkEditButton->setVisible( mUseLink && !mReadOnly );
261
262 mFileWidgetButton->setEnabled( !mReadOnly );
263 mLineEdit->setEnabled( !mReadOnly );
264
265 mLinkEditButton->setIcon( linkVisible && !mReadOnly ?
266 QgsApplication::getThemeIcon( QStringLiteral( "/mActionToggleEditing.svg" ) ) :
267 QgsApplication::getThemeIcon( QStringLiteral( "/mActionSaveEdits.svg" ) ) );
268}
269
270void QgsFileWidget::openFileDialog()
271{
272 QgsSettings settings;
273 QString oldPath;
274
275 // if we use a relative path option, we need to obtain the full path
276 // first choice is the current file path, if one is entered
277 if ( !mFilePath.isEmpty() && ( QFile::exists( mFilePath ) || mStorageMode == SaveFile ) )
278 {
279 oldPath = relativePath( mFilePath, false );
280 }
281 // If we use fixed default path
282 // second choice is the default root
283 else if ( !mDefaultRoot.isEmpty() )
284 {
285 oldPath = QDir::cleanPath( mDefaultRoot );
286 }
287
288 // If there is no valid value, find a default path to use
289 QUrl url = QUrl::fromUserInput( oldPath );
290 if ( !url.isValid() )
291 {
292 QString defPath = QDir::cleanPath( QFileInfo( QgsProject::instance()->absoluteFilePath() ).path() );
293 if ( defPath.isEmpty() )
294 {
295 defPath = QDir::homePath();
296 }
297 oldPath = settings.value( QStringLiteral( "UI/lastFileNameWidgetDir" ), defPath ).toString();
298 }
299
300 // Handle Storage
301 QString fileName;
302 QStringList fileNames;
303 QString title;
304
305 {
306 QgsFocusKeeper focusKeeper;
307 switch ( mStorageMode )
308 {
309 case GetFile:
310 title = !mDialogTitle.isEmpty() ? mDialogTitle : tr( "Select a file" );
311 fileName = QFileDialog::getOpenFileName( this, title, QFileInfo( oldPath ).absoluteFilePath(), mFilter, &mSelectedFilter, mOptions );
312 break;
313 case GetMultipleFiles:
314 title = !mDialogTitle.isEmpty() ? mDialogTitle : tr( "Select one or more files" );
315 fileNames = QFileDialog::getOpenFileNames( this, title, QFileInfo( oldPath ).absoluteFilePath(), mFilter, &mSelectedFilter, mOptions );
316 break;
317 case GetDirectory:
318 title = !mDialogTitle.isEmpty() ? mDialogTitle : tr( "Select a directory" );
319 fileName = QFileDialog::getExistingDirectory( this, title, QFileInfo( oldPath ).absoluteFilePath(), mOptions );
320 break;
321 case SaveFile:
322 {
323 title = !mDialogTitle.isEmpty() ? mDialogTitle : tr( "Create or select a file" );
324 if ( !confirmOverwrite() )
325 {
326 fileName = QFileDialog::getSaveFileName( this, title, QFileInfo( oldPath ).absoluteFilePath(), mFilter, &mSelectedFilter, mOptions | QFileDialog::DontConfirmOverwrite );
327 }
328 else
329 {
330 fileName = QFileDialog::getSaveFileName( this, title, QFileInfo( oldPath ).absoluteFilePath(), mFilter, &mSelectedFilter, mOptions );
331 }
332
333 // make sure filename ends with filter. This isn't automatically done by
334 // getSaveFileName on some platforms (e.g. gnome)
336
337 // A bit of hack to solve https://github.com/qgis/QGIS/issues/54566
338 // to be able to select an existing File Geodatabase, we add in the filter
339 // the "gdb" file that is found in all File Geodatabase .gdb directory
340 // to allow the user to select it. We now need to remove this gdb file
341 // (which became gdb.gdb due to above logic) from the selected filename
342 if ( mFilter.contains( QLatin1String( "(*.gdb *.GDB gdb)" ) ) &&
343 ( fileName.endsWith( QLatin1String( "/gdb.gdb" ) ) ||
344 fileName.endsWith( QLatin1String( "\\gdb.gdb" ) ) ) )
345 {
346 fileName.chop( static_cast<int>( strlen( "/gdb.gdb" ) ) );
347 }
348 }
349 break;
350 }
351 }
352
353 // return dialog focus on Mac
354 activateWindow();
355 raise();
356
357 if ( fileName.isEmpty() && fileNames.isEmpty( ) )
358 return;
359
361 fileNames << fileName;
362
363 for ( int i = 0; i < fileNames.length(); i++ )
364 {
365 fileNames.replace( i, QDir::toNativeSeparators( QDir::cleanPath( QFileInfo( fileNames.at( i ) ).absoluteFilePath() ) ) );
366 }
367
368 // Store the last used path:
369 switch ( mStorageMode )
370 {
371 case GetFile:
372 case SaveFile:
373 case GetMultipleFiles:
374 settings.setValue( QStringLiteral( "UI/lastFileNameWidgetDir" ), QFileInfo( fileNames.first() ).absolutePath() );
375 break;
376 case GetDirectory:
377 settings.setValue( QStringLiteral( "UI/lastFileNameWidgetDir" ), fileNames.first() );
378 break;
379 }
380
381 setSelectedFileNames( fileNames );
382}
383
384void QgsFileWidget::setSelectedFileNames( QStringList fileNames )
385{
386 Q_ASSERT( fileNames.count() );
387
388 // Handle relative Path storage
389 for ( int i = 0; i < fileNames.length(); i++ )
390 {
391 fileNames.replace( i, relativePath( fileNames.at( i ), true ) );
392 }
393
394 setFilePaths( fileNames );
395}
396
397void QgsFileWidget::setFilePaths( const QStringList &filePaths )
398{
400 {
401 setFilePath( filePaths.first() );
402 }
403 else
404 {
405 if ( filePaths.length() > 1 )
406 {
407 setFilePath( QStringLiteral( "\"%1\"" ).arg( filePaths.join( QLatin1String( "\" \"" ) ) ) );
408 }
409 else
410 {
411 setFilePath( filePaths.first( ) );
412 }
413 }
414}
415
416QString QgsFileWidget::relativePath( const QString &filePath, bool removeRelative ) const
417{
418 QString RelativePath;
420 {
421 RelativePath = QDir::toNativeSeparators( QDir::cleanPath( QFileInfo( QgsProject::instance()->absoluteFilePath() ).path() ) );
422 }
423 else if ( mRelativeStorage == RelativeDefaultPath && !mDefaultRoot.isEmpty() )
424 {
425 RelativePath = QDir::toNativeSeparators( QDir::cleanPath( mDefaultRoot ) );
426 }
427
428 if ( !RelativePath.isEmpty() )
429 {
430 if ( removeRelative )
431 {
432 return QDir::cleanPath( QDir( RelativePath ).relativeFilePath( filePath ) );
433 }
434 else
435 {
436 return QDir::cleanPath( QDir( RelativePath ).filePath( filePath ) );
437 }
438 }
439
440 return filePath;
441}
442
444{
445 QSize size { mLineEdit->minimumSizeHint() };
446 const QSize btnSize { mFileWidgetButton->minimumSizeHint() };
447 size.setWidth( size.width() + btnSize.width() );
448 size.setHeight( std::max( size.height(), btnSize.height() ) );
449 return size;
450}
451
452
453QString QgsFileWidget::toUrl( const QString &path ) const
454{
455 QString rep;
456 if ( path.isEmpty() || path == QgsApplication::nullRepresentation() )
457 {
459 }
460
461 if ( isMultiFiles( path ) )
462 {
463 return QStringLiteral( "<a>%1</a>" ).arg( path );
464 }
465
466 QString urlStr = relativePath( path, false );
467 QUrl url = QUrl::fromUserInput( urlStr );
468 if ( !url.isValid() || !url.isLocalFile() )
469 {
470 QgsDebugMsgLevel( QStringLiteral( "URL: %1 is not valid or not a local file!" ).arg( path ), 2 );
471 rep = path;
472 }
473
474 QString pathStr = url.toString();
475 if ( mFullUrl )
476 {
477 rep = QStringLiteral( "<a href=\"%1\">%2</a>" ).arg( pathStr, path );
478 }
479 else
480 {
481 QString fileName = QFileInfo( urlStr ).fileName();
482 rep = QStringLiteral( "<a href=\"%1\">%2</a>" ).arg( pathStr, fileName );
483 }
484
485 return rep;
486}
487
488
490
491
492QgsFileDropEdit::QgsFileDropEdit( QWidget *parent )
493 : QgsHighlightableLineEdit( parent )
494{
495 setAcceptDrops( true );
496}
497
498void QgsFileDropEdit::setFilters( const QString &filters )
499{
500 mAcceptableExtensions.clear();
501
502 if ( filters.contains( QStringLiteral( "*.*" ) ) )
503 return; // everything is allowed!
504
505 const thread_local QRegularExpression rx( QStringLiteral( "\\*\\.(\\w+)" ) );
506 QRegularExpressionMatchIterator i = rx.globalMatch( filters );
507 while ( i.hasNext() )
508 {
509 QRegularExpressionMatch match = i.next();
510 if ( match.hasMatch() )
511 {
512 mAcceptableExtensions << match.captured( 1 ).toLower();
513 }
514 }
515}
516
517QStringList QgsFileDropEdit::acceptableFilePaths( QDropEvent *event ) const
518{
519 QStringList rawPaths;
520 QStringList paths;
521 if ( event->mimeData()->hasUrls() )
522 {
523 const QList< QUrl > urls = event->mimeData()->urls();
524 rawPaths.reserve( urls.count() );
525 for ( const QUrl &url : urls )
526 {
527 const QString local = url.toLocalFile();
528 if ( !rawPaths.contains( local ) )
529 rawPaths.append( local );
530 }
531 }
532
534 for ( const QgsMimeDataUtils::Uri &u : std::as_const( lst ) )
535 {
536 if ( !rawPaths.contains( u.uri ) )
537 rawPaths.append( u.uri );
538 }
539
540 if ( !event->mimeData()->text().isEmpty() && !rawPaths.contains( event->mimeData()->text() ) )
541 rawPaths.append( event->mimeData()->text() );
542
543 paths.reserve( rawPaths.count() );
544 for ( const QString &path : std::as_const( rawPaths ) )
545 {
546 QFileInfo file( path );
547 switch ( mStorageMode )
548 {
552 {
553 if ( file.isFile() && ( mAcceptableExtensions.isEmpty() || mAcceptableExtensions.contains( file.suffix(), Qt::CaseInsensitive ) ) )
554 paths.append( file.filePath() );
555
556 break;
557 }
558
560 {
561 if ( file.isDir() )
562 paths.append( file.filePath() );
563 else if ( file.isFile() )
564 {
565 // folder mode, but a file dropped. So get folder name from file
566 paths.append( file.absolutePath() );
567 }
568
569 break;
570 }
571 }
572 }
573
574 return paths;
575}
576
577QString QgsFileDropEdit::acceptableFilePath( QDropEvent *event ) const
578{
579 const QStringList paths = acceptableFilePaths( event );
580 if ( paths.size() > 1 )
581 {
582 return QStringLiteral( "\"%1\"" ).arg( paths.join( QLatin1String( "\" \"" ) ) );
583 }
584 else if ( paths.size() == 1 )
585 {
586 return paths.first();
587 }
588 else
589 {
590 return QString();
591 }
592}
593
594void QgsFileDropEdit::dragEnterEvent( QDragEnterEvent *event )
595{
596 QString filePath = acceptableFilePath( event );
597 if ( !filePath.isEmpty() )
598 {
599 event->acceptProposedAction();
600 setHighlighted( true );
601 }
602 else
603 {
604 event->ignore();
605 }
606}
607
608void QgsFileDropEdit::dragLeaveEvent( QDragLeaveEvent *event )
609{
610 QgsFilterLineEdit::dragLeaveEvent( event );
611 event->accept();
612 setHighlighted( false );
613}
614
615void QgsFileDropEdit::dropEvent( QDropEvent *event )
616{
617 QString filePath = acceptableFilePath( event );
618 if ( !filePath.isEmpty() )
619 {
620 event->acceptProposedAction();
621 emit fileDropped( filePath );
622 }
623
624 setHighlighted( false );
625}
626
static QString nullRepresentation()
This string is used to represent the value NULL throughout QGIS.
static QIcon getThemeIcon(const QString &name, const QColor &fillColor=QColor(), const QColor &strokeColor=QColor())
Helper to get a theme icon.
static QString addExtensionFromFilter(const QString &fileName, const QString &filter)
Ensures that a fileName ends with an extension from the specified filter string.
QString relativePath(const QString &filePath, bool removeRelative) const
Returns a filePath with relative path options applied (or not) !
StorageMode
The StorageMode enum determines if the file picker should pick files or directories.
Definition: qgsfilewidget.h:67
@ GetMultipleFiles
Select multiple files.
Definition: qgsfilewidget.h:70
@ GetFile
Select a single file.
Definition: qgsfilewidget.h:68
@ GetDirectory
Select a directory.
Definition: qgsfilewidget.h:69
@ SaveFile
Select a single new or pre-existing file.
Definition: qgsfilewidget.h:71
StorageMode mStorageMode
QString mFilePath
QString filePath()
Returns the current file path(s).
void setRelativeStorage(QgsFileWidget::RelativeStorage relativeStorage)
Sets whether the relative path is with respect to the project path or the default path.
void setOptions(QFileDialog::Options options)
Set additional options used for QFileDialog.
void fileChanged(const QString &path)
Emitted whenever the current file or directory path is changed.
bool confirmOverwrite() const
Returns whether a confirmation will be shown when overwriting an existing file.
QString mSelectedFilter
bool fileWidgetButtonVisible
Definition: qgsfilewidget.h:51
QString filter
Definition: qgsfilewidget.h:55
QFileDialog::Options options
Definition: qgsfilewidget.h:59
static bool isMultiFiles(const QString &path)
Returns true if path is a multifiles.
void setFullUrl(bool fullUrl)
Sets whether links shown use the full path.
QString dialogTitle
Definition: qgsfilewidget.h:54
RelativeStorage
The RelativeStorage enum determines if path is absolute, relative to the current project path or rela...
Definition: qgsfilewidget.h:79
QgsFileWidget(QWidget *parent=nullptr)
QgsFileWidget creates a widget for selecting a file or a folder.
void setStorageMode(QgsFileWidget::StorageMode storageMode)
Sets the widget's storage mode (i.e.
void setUseLink(bool useLink)
Sets whether the file path will be shown as a link.
void setFilePaths(const QStringList &filePaths)
Update filePath according to filePaths list.
void setDefaultRoot(const QString &defaultRoot)
Returns the default root path used as the first shown location when picking a file and used if the Re...
QHBoxLayout * mLayout
RelativeStorage mRelativeStorage
QFileDialog::Options mOptions
QString toUrl(const QString &path) const
returns a HTML code with a link to the given file path
QString mDefaultRoot
void setDialogTitle(const QString &title)
Sets the title to use for the open file dialog.
RelativeStorage relativeStorage
Definition: qgsfilewidget.h:58
QgsFilterLineEdit * lineEdit()
Returns a pointer to the widget's line edit, which can be used to customize the appearance and behavi...
static QStringList splitFilePaths(const QString &path)
Split the the quoted and space separated path and returns a list of strings.
void setFileWidgetButtonVisible(bool visible)
Sets whether the tool button is visible.
virtual void setSelectedFileNames(QStringList fileNames)
Called whenever user select fileNames from dialog.
QgsFileDropEdit * mLineEdit
QToolButton * mLinkEditButton
void setFilter(const QString &filter)
setFilter sets the filter used by the model to filters.
QToolButton * mFileWidgetButton
QLabel * mLinkLabel
StorageMode storageMode
Definition: qgsfilewidget.h:57
virtual void setReadOnly(bool readOnly)
Sets whether the widget should be read only.
void setFilePath(const QString &path)
Sets the current file path.
QString mDialogTitle
QString defaultRoot
Definition: qgsfilewidget.h:56
QSize minimumSizeHint() const override
virtual void updateLayout()
Update buttons visibility.
QLineEdit subclass with built in support for clearing the widget's value and handling custom null val...
Trick to keep a widget focused and avoid QT crashes.
A QgsFilterLineEdit subclass with the ability to "highlight" the edges of the widget.
QList< QgsMimeDataUtils::Uri > UriList
static UriList decodeUriList(const QMimeData *data)
static QgsProject * instance()
Returns the QgsProject singleton instance.
Definition: qgsproject.cpp:481
This class is a composition of two QSettings instances:
Definition: qgssettings.h:64
QVariant value(const QString &key, const QVariant &defaultValue=QVariant(), Section section=NoSection) const
Returns the value for setting key.
void setValue(const QString &key, const QVariant &value, QgsSettings::Section section=QgsSettings::NoSection)
Sets the value of setting key to value.
#define QgsDebugMsgLevel(str, level)
Definition: qgslogger.h:39