QGIS API Documentation 3.99.0-Master (d270888f95f)
Loading...
Searching...
No Matches
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 "qgsapplication.h"
20#include "qgsfileutils.h"
21#include "qgsfilterlineedit.h"
22#include "qgsfocuskeeper.h"
23#include "qgslogger.h"
24#include "qgsmimedatautils.h"
25#include "qgsproject.h"
26#include "qgssettings.h"
27
28#include <QDropEvent>
29#include <QGridLayout>
30#include <QLabel>
31#include <QLineEdit>
32#include <QRegularExpression>
33#include <QString>
34#include <QToolButton>
35#include <QUrl>
36
37#include "moc_qgsfilewidget.cpp"
38
39using namespace Qt::StringLiterals;
40
42 : QWidget( parent )
43{
44 mLayout = new QHBoxLayout();
45 mLayout->setContentsMargins( 0, 0, 0, 0 );
46
47 // If displaying a hyperlink, use a QLabel
48 mLinkLabel = new QLabel( this );
49 // Make Qt opens the link with the OS defined viewer
50 mLinkLabel->setOpenExternalLinks( true );
51 // Label should always be enabled to be able to open
52 // the link on read only mode.
53 mLinkLabel->setEnabled( true );
54 mLinkLabel->setSizePolicy( QSizePolicy::Expanding, QSizePolicy::Preferred );
55 mLinkLabel->setTextFormat( Qt::RichText );
56 mLinkLabel->hide(); // do not show by default
57 mLayout->addWidget( mLinkLabel );
58
59 // otherwise, use the traditional QLineEdit subclass
60 mLineEdit = new QgsFileDropEdit( this );
61 mLineEdit->setDragEnabled( true );
62 mLineEdit->setToolTip( tr( "Full path to the file(s), including name and extension" ) );
63 connect( mLineEdit, &QLineEdit::textChanged, this, &QgsFileWidget::textEdited );
64 connect( mLineEdit, &QgsFileDropEdit::fileDropped, this, &QgsFileWidget::fileDropped );
65 mLayout->addWidget( mLineEdit );
66
67 mLinkEditButton = new QToolButton( this );
68 mLinkEditButton->setIcon( QgsApplication::getThemeIcon( u"/mActionToggleEditing.svg"_s ) );
69 mLayout->addWidget( mLinkEditButton );
70 connect( mLinkEditButton, &QToolButton::clicked, this, &QgsFileWidget::editLink );
71 mLinkEditButton->hide(); // do not show by default
72
73 mFileWidgetButton = new QToolButton( this );
74 mFileWidgetButton->setText( QChar( 0x2026 ) );
75 mFileWidgetButton->setToolTip( tr( "Browse" ) );
76 connect( mFileWidgetButton, &QAbstractButton::clicked, this, &QgsFileWidget::openFileDialog );
77 mLayout->addWidget( mFileWidgetButton );
78
79 setLayout( mLayout );
80}
81
83{
84 return mFilePath;
85}
86
87QStringList QgsFileWidget::splitFilePaths( const QString &path )
88{
89 QStringList pathParts;
90 // Iterate over regular expression matches in the path instead of splitting the path on an expression.
91 // Splitting on an expression discards the string parts matching the expression.
92 // We want to split on spaces between double quotes without discarding double quotes around spaces.
93 // The decision whether to discard double quotes is made later, based on each isolated split path.
94 const thread_local QRegularExpression partSeparatorsRegex = QRegularExpression( u"(?:\")(\\s+)(?:\")"_s );
95 QRegularExpressionMatchIterator partSeparatorMatches = partSeparatorsRegex.globalMatch( path );
96 int substringStart = 0;
97 while ( partSeparatorMatches.hasNext() )
98 {
99 QRegularExpressionMatch match = partSeparatorMatches.next();
100 int substringEnd = match.capturedStart() + 1;
101 int substringLength = substringEnd - substringStart;
102 pathParts.append( path.mid( substringStart, substringLength ) );
103 substringStart = match.capturedEnd() - 1;
104 if ( !partSeparatorMatches.hasNext() )
105 {
106 pathParts.append( path.mid( substringStart ) );
107 }
108 }
109 if ( pathParts.length() == 0 )
110 {
111 pathParts.append( path );
112 }
113
114 QStringList paths;
115 const thread_local QRegularExpression doubleQuoteWrappedRegex( u"(?:^\\s*\")(.+)(?:\"\\s*$)"_s );
116 for ( const QString &pathsPart : pathParts )
117 {
118 QRegularExpressionMatch match = doubleQuoteWrappedRegex.match( pathsPart );
119 QString finalPath;
120 if ( match.hasMatch() )
121 {
122 finalPath = match.captured( 1 );
123 }
124 else
125 {
126 finalPath = pathsPart;
127 }
128 paths.append( finalPath );
129 }
130 return paths;
131}
132
133void QgsFileWidget::setFilePath( const QString &path )
134{
135 //will trigger textEdited slot
136 mLineEdit->setValue( path );
137}
138
139void QgsFileWidget::setReadOnly( bool readOnly )
140{
141 if ( mReadOnly == readOnly )
142 return;
143
144 mReadOnly = readOnly;
145
146 updateLayout();
147}
148
150{
151 return mDialogTitle;
152}
153
154void QgsFileWidget::setDialogTitle( const QString &title )
155{
156 mDialogTitle = title;
157}
158
160{
161 return mFilter;
162}
163
164void QgsFileWidget::setFilter( const QString &filters )
165{
166 mFilter = filters;
167 mLineEdit->setFilters( filters );
168}
169
170QFileDialog::Options QgsFileWidget::options() const
171{
172 return mOptions;
173}
174
175void QgsFileWidget::setOptions( QFileDialog::Options options )
176{
178}
179
184
186{
187 mButtonVisible = visible;
188 mFileWidgetButton->setVisible( visible );
189}
190
191bool QgsFileWidget::isMultiFiles( const QString &path )
192{
193 return path.contains( u"\" \""_s );
194}
195
196void QgsFileWidget::textEdited( const QString &path )
197{
198 mFilePath = path;
199 mLinkLabel->setText( toUrl( path ) );
200 // Show tooltip if multiple files are selected
201 if ( isMultiFiles( path ) )
202 {
203 mLineEdit->setToolTip( tr( "Selected files:<br><ul><li>%1</li></ul><br>" ).arg( splitFilePaths( path ).join( "</li><li>"_L1 ) ) );
204 }
205 else
206 {
207 mLineEdit->setToolTip( QString() );
208 }
209 emit fileChanged( mFilePath );
210}
211
212void QgsFileWidget::editLink()
213{
214 if ( !mUseLink || mReadOnly )
215 return;
216
218 updateLayout();
219}
220
221void QgsFileWidget::fileDropped( const QString &filePath )
222{
223 setSelectedFileNames( QStringList() << filePath );
224 mLineEdit->selectAll();
225 mLineEdit->setFocus( Qt::MouseFocusReason );
226}
227
229{
230 return mUseLink;
231}
232
234{
235 if ( mUseLink == useLink )
236 return;
237
239 updateLayout();
240}
241
243{
244 return mFullUrl;
245}
246
248{
250}
251
253{
254 return mDefaultRoot;
255}
256
261
266
272
277
282
287
289{
290 const bool linkVisible = mUseLink && !mIsLinkEdited;
291
292 mLineEdit->setVisible( !linkVisible );
293 mLinkLabel->setVisible( linkVisible );
294 mLinkEditButton->setVisible( mUseLink && !mReadOnly );
295
296 mFileWidgetButton->setEnabled( !mReadOnly );
297 mLineEdit->setEnabled( !mReadOnly );
298
299 mLinkEditButton->setIcon( linkVisible && !mReadOnly ? QgsApplication::getThemeIcon( u"/mActionToggleEditing.svg"_s ) : QgsApplication::getThemeIcon( u"/mActionSaveEdits.svg"_s ) );
300}
301
302void QgsFileWidget::openFileDialog()
303{
304 QgsSettings settings;
305 QString oldPath;
306
307 // if we use a relative path option, we need to obtain the full path
308 // first choice is the current file path, if one is entered
309 if ( !mFilePath.isEmpty() && ( QFile::exists( mFilePath ) || mStorageMode == SaveFile ) )
310 {
311 oldPath = relativePath( mFilePath, false );
312 }
313 // If we use fixed default path
314 // second choice is the default root
315 else if ( !mDefaultRoot.isEmpty() )
316 {
317 oldPath = QDir::cleanPath( mDefaultRoot );
318 }
319
320 // If there is no valid value, find a default path to use
321 QUrl url = QUrl::fromUserInput( oldPath );
322 if ( !url.isValid() )
323 {
324 QString defPath = QDir::cleanPath( QFileInfo( QgsProject::instance()->absoluteFilePath() ).path() );
325 if ( defPath.isEmpty() )
326 {
327 defPath = QDir::homePath();
328 }
329 oldPath = settings.value( u"UI/lastFileNameWidgetDir"_s, defPath ).toString();
330 }
331
332 // Handle Storage
333 QString fileName;
334 QStringList fileNames;
335 QString title;
336
337 {
338 QgsFocusKeeper focusKeeper;
339 switch ( mStorageMode )
340 {
341 case GetFile:
342 title = !mDialogTitle.isEmpty() ? mDialogTitle : tr( "Select a file" );
343 fileName = QFileDialog::getOpenFileName( this, title, QFileInfo( oldPath ).absoluteFilePath(), mFilter, &mSelectedFilter, mOptions );
344 break;
345 case GetMultipleFiles:
346 title = !mDialogTitle.isEmpty() ? mDialogTitle : tr( "Select one or more files" );
347 fileNames = QFileDialog::getOpenFileNames( this, title, QFileInfo( oldPath ).absoluteFilePath(), mFilter, &mSelectedFilter, mOptions );
348 break;
349 case GetDirectory:
350 title = !mDialogTitle.isEmpty() ? mDialogTitle : tr( "Select a directory" );
351 fileName = QFileDialog::getExistingDirectory( this, title, QFileInfo( oldPath ).absoluteFilePath(), mOptions );
352 break;
353 case SaveFile:
354 {
355 title = !mDialogTitle.isEmpty() ? mDialogTitle : tr( "Create or select a file" );
356 if ( !confirmOverwrite() )
357 {
358 fileName = QFileDialog::getSaveFileName( this, title, QFileInfo( oldPath ).absoluteFilePath(), mFilter, &mSelectedFilter, mOptions | QFileDialog::DontConfirmOverwrite );
359 }
360 else
361 {
362 fileName = QFileDialog::getSaveFileName( this, title, QFileInfo( oldPath ).absoluteFilePath(), mFilter, &mSelectedFilter, mOptions );
363 }
364
365 // make sure filename ends with filter. This isn't automatically done by
366 // getSaveFileName on some platforms (e.g. gnome)
368
369 // A bit of hack to solve https://github.com/qgis/QGIS/issues/54566
370 // to be able to select an existing File Geodatabase, we add in the filter
371 // the "gdb" file that is found in all File Geodatabase .gdb directory
372 // to allow the user to select it. We now need to remove this gdb file
373 // (which became gdb.gdb due to above logic) from the selected filename
374 if ( mFilter.contains( "(*.gdb *.GDB gdb)"_L1 ) && ( fileName.endsWith( "/gdb.gdb"_L1 ) || fileName.endsWith( "\\gdb.gdb"_L1 ) ) )
375 {
376 fileName.chop( static_cast<int>( strlen( "/gdb.gdb" ) ) );
377 }
378 }
379 break;
380 }
381 }
382
383 // return dialog focus on Mac
384 activateWindow();
385 raise();
386
387 if ( fileName.isEmpty() && fileNames.isEmpty() )
388 return;
389
391 fileNames << fileName;
392
393 for ( int i = 0; i < fileNames.length(); i++ )
394 {
395 fileNames.replace( i, QDir::toNativeSeparators( QDir::cleanPath( QFileInfo( fileNames.at( i ) ).absoluteFilePath() ) ) );
396 }
397
398 // Store the last used path:
399 switch ( mStorageMode )
400 {
401 case GetFile:
402 case SaveFile:
403 case GetMultipleFiles:
404 settings.setValue( u"UI/lastFileNameWidgetDir"_s, QFileInfo( fileNames.first() ).absolutePath() );
405 break;
406 case GetDirectory:
407 settings.setValue( u"UI/lastFileNameWidgetDir"_s, fileNames.first() );
408 break;
409 }
410
411 setSelectedFileNames( fileNames );
412}
413
414void QgsFileWidget::setSelectedFileNames( QStringList fileNames )
415{
416 Q_ASSERT( fileNames.count() );
417
418 // Handle relative Path storage
419 for ( int i = 0; i < fileNames.length(); i++ )
420 {
421 fileNames.replace( i, relativePath( fileNames.at( i ), true ) );
422 }
423
424 setFilePaths( fileNames );
425}
426
427void QgsFileWidget::setFilePaths( const QStringList &filePaths )
428{
430 {
431 setFilePath( filePaths.first() );
432 }
433 else
434 {
435 if ( filePaths.length() > 1 )
436 {
437 setFilePath( u"\"%1\""_s.arg( filePaths.join( "\" \""_L1 ) ) );
438 }
439 else
440 {
441 setFilePath( filePaths.first() );
442 }
443 }
444}
445
446QString QgsFileWidget::relativePath( const QString &filePath, bool removeRelative ) const
447{
448 QString RelativePath;
450 {
451 RelativePath = QDir::toNativeSeparators( QDir::cleanPath( QFileInfo( QgsProject::instance()->absoluteFilePath() ).path() ) );
452 }
453 else if ( mRelativeStorage == RelativeDefaultPath && !mDefaultRoot.isEmpty() )
454 {
455 RelativePath = QDir::toNativeSeparators( QDir::cleanPath( mDefaultRoot ) );
456 }
457
458 if ( !RelativePath.isEmpty() )
459 {
460 if ( removeRelative )
461 {
462 return QDir::cleanPath( QDir( RelativePath ).relativeFilePath( filePath ) );
463 }
464 else
465 {
466 return QDir::cleanPath( QDir( RelativePath ).filePath( filePath ) );
467 }
468 }
469
470 return filePath;
471}
472
474{
475 QSize size { mLineEdit->minimumSizeHint() };
476 const QSize btnSize { mFileWidgetButton->minimumSizeHint() };
477 size.setWidth( size.width() + btnSize.width() );
478 size.setHeight( std::max( size.height(), btnSize.height() ) );
479 return size;
480}
481
482
483QString QgsFileWidget::toUrl( const QString &path ) const
484{
485 QString rep;
486 if ( path.isEmpty() || path == QgsApplication::nullRepresentation() )
487 {
489 }
490
491 if ( isMultiFiles( path ) )
492 {
493 return u"<a>%1</a>"_s.arg( path );
494 }
495
496 QString urlStr = relativePath( path, false );
497 QUrl url = QUrl::fromUserInput( urlStr );
498 if ( !url.isValid() || !url.isLocalFile() )
499 {
500 QgsDebugMsgLevel( u"URL: %1 is not valid or not a local file!"_s.arg( path ), 2 );
501 rep = path;
502 }
503
504 QString pathStr = url.toString();
505 if ( mFullUrl )
506 {
507 rep = u"<a href=\"%1\">%2</a>"_s.arg( pathStr, path );
508 }
509 else
510 {
511 QString fileName = QFileInfo( urlStr ).fileName();
512 rep = u"<a href=\"%1\">%2</a>"_s.arg( pathStr, fileName );
513 }
514
515 return rep;
516}
517
518
520
521
522QgsFileDropEdit::QgsFileDropEdit( QWidget *parent )
523 : QgsHighlightableLineEdit( parent )
524{
525 setAcceptDrops( true );
526}
527
528void QgsFileDropEdit::setFilters( const QString &filters )
529{
530 mAcceptableExtensions.clear();
531
532 if ( filters.contains( u"*.*"_s ) )
533 return; // everything is allowed!
534
535 const thread_local QRegularExpression rx( u"\\*\\.(\\w+)"_s );
536 QRegularExpressionMatchIterator i = rx.globalMatch( filters );
537 while ( i.hasNext() )
538 {
539 QRegularExpressionMatch match = i.next();
540 if ( match.hasMatch() )
541 {
542 mAcceptableExtensions << match.captured( 1 ).toLower();
543 }
544 }
545}
546
547QStringList QgsFileDropEdit::acceptableFilePaths( QDropEvent *event ) const
548{
549 QStringList rawPaths;
550 QStringList paths;
551 if ( event->mimeData()->hasUrls() )
552 {
553 const QList<QUrl> urls = event->mimeData()->urls();
554 rawPaths.reserve( urls.count() );
555 for ( const QUrl &url : urls )
556 {
557 const QString local = url.toLocalFile();
558 if ( !rawPaths.contains( local ) )
559 rawPaths.append( local );
560 }
561 }
562
564 for ( const QgsMimeDataUtils::Uri &u : std::as_const( lst ) )
565 {
566 if ( !rawPaths.contains( u.uri ) )
567 rawPaths.append( u.uri );
568 }
569
570 if ( !event->mimeData()->text().isEmpty() && !rawPaths.contains( event->mimeData()->text() ) )
571 rawPaths.append( event->mimeData()->text() );
572
573 paths.reserve( rawPaths.count() );
574 for ( const QString &path : std::as_const( rawPaths ) )
575 {
576 QFileInfo file( path );
577 switch ( mStorageMode )
578 {
582 {
583 if ( file.isFile() && ( mAcceptableExtensions.isEmpty() || mAcceptableExtensions.contains( file.suffix(), Qt::CaseInsensitive ) ) )
584 paths.append( file.filePath() );
585
586 break;
587 }
588
590 {
591 if ( file.isDir() )
592 paths.append( file.filePath() );
593 else if ( file.isFile() )
594 {
595 // folder mode, but a file dropped. So get folder name from file
596 paths.append( file.absolutePath() );
597 }
598
599 break;
600 }
601 }
602 }
603
604 return paths;
605}
606
607QString QgsFileDropEdit::acceptableFilePath( QDropEvent *event ) const
608{
609 const QStringList paths = acceptableFilePaths( event );
610 if ( paths.size() > 1 )
611 {
612 return u"\"%1\""_s.arg( paths.join( "\" \""_L1 ) );
613 }
614 else if ( paths.size() == 1 )
615 {
616 return paths.first();
617 }
618 else
619 {
620 return QString();
621 }
622}
623
624void QgsFileDropEdit::dragEnterEvent( QDragEnterEvent *event )
625{
626 QString filePath = acceptableFilePath( event );
627 if ( !filePath.isEmpty() )
628 {
629 event->acceptProposedAction();
630 setHighlighted( true );
631 }
632 else
633 {
634 event->ignore();
635 }
636}
637
638void QgsFileDropEdit::dragLeaveEvent( QDragLeaveEvent *event )
639{
640 QgsFilterLineEdit::dragLeaveEvent( event );
641 event->accept();
642 setHighlighted( false );
643}
644
645void QgsFileDropEdit::dropEvent( QDropEvent *event )
646{
647 QString filePath = acceptableFilePath( event );
648 if ( !filePath.isEmpty() )
649 {
650 event->acceptProposedAction();
651 emit fileDropped( filePath );
652 }
653
654 setHighlighted( false );
655}
656
static QString nullRepresentation()
Returns the string 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.
@ GetMultipleFiles
Select multiple files.
@ GetFile
Select a single file.
@ GetDirectory
Select a directory.
@ SaveFile
Select a single new or pre-existing file.
StorageMode mStorageMode
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
QFileDialog::Options options
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
RelativeStorage
The RelativeStorage enum determines if path is absolute, relative to the current project path or rela...
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 filePath() const
Returns the current file path(s).
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
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
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
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...
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.
Stores settings for use within QGIS.
Definition qgssettings.h:68
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:63