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