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