QGIS API Documentation 3.28.0-Firenze (ed3ad0430f)
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#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
86 const QStringList pathParts = path.split( QRegExp( "\"\\s+\"" ), QString::SkipEmptyParts );
87#else
88 const thread_local QRegularExpression partsRegex = QRegularExpression( QStringLiteral( "\"\\s+\"" ) );
89 const QStringList pathParts = path.split( partsRegex, Qt::SkipEmptyParts );
90#endif
91
92 const thread_local QRegularExpression cleanRe( QStringLiteral( "(^\\s*\")|(\"\\s*)" ) );
93 paths.reserve( pathParts.size() );
94 for ( const QString &pathsPart : pathParts )
95 {
96 QString cleaned = pathsPart;
97 cleaned.remove( cleanRe );
98 paths.append( cleaned );
99 }
100 return paths;
101}
102
103void QgsFileWidget::setFilePath( const QString &path )
104{
105 //will trigger textEdited slot
106 mLineEdit->setValue( path );
107}
108
109void QgsFileWidget::setReadOnly( bool readOnly )
110{
111 if ( mReadOnly == readOnly )
112 return;
113
114 mReadOnly = readOnly;
115
116 updateLayout();
117}
118
120{
121 return mDialogTitle;
122}
123
124void QgsFileWidget::setDialogTitle( const QString &title )
125{
126 mDialogTitle = title;
127}
128
130{
131 return mFilter;
132}
133
134void QgsFileWidget::setFilter( const QString &filters )
135{
136 mFilter = filters;
137 mLineEdit->setFilters( filters );
138}
139
140QFileDialog::Options QgsFileWidget::options() const
141{
142 return mOptions;
143}
144
145void QgsFileWidget::setOptions( QFileDialog::Options options )
146{
148}
149
151{
152 return mButtonVisible;
153}
154
156{
157 mButtonVisible = visible;
158 mFileWidgetButton->setVisible( visible );
159}
160
161bool QgsFileWidget::isMultiFiles( const QString &path )
162{
163 return path.contains( QStringLiteral( "\" \"" ) );
164}
165
166void QgsFileWidget::textEdited( const QString &path )
167{
168 mFilePath = path;
169 mLinkLabel->setText( toUrl( path ) );
170 // Show tooltip if multiple files are selected
171 if ( isMultiFiles( path ) )
172 {
173 mLineEdit->setToolTip( tr( "Selected files:<br><ul><li>%1</li></ul><br>" ).arg( splitFilePaths( path ).join( QLatin1String( "</li><li>" ) ) ) );
174 }
175 else
176 {
177 mLineEdit->setToolTip( QString() );
178 }
179 emit fileChanged( mFilePath );
180}
181
182void QgsFileWidget::editLink()
183{
184 if ( !mUseLink || mReadOnly )
185 return;
186
188 updateLayout();
189}
190
191void QgsFileWidget::fileDropped( const QString &filePath )
192{
193 setSelectedFileNames( QStringList() << filePath );
194 mLineEdit->selectAll();
195 mLineEdit->setFocus( Qt::MouseFocusReason );
196}
197
199{
200 return mUseLink;
201}
202
203void QgsFileWidget::setUseLink( bool useLink )
204{
205 if ( mUseLink == useLink )
206 return;
207
209 updateLayout();
210}
211
213{
214 return mFullUrl;
215}
216
217void QgsFileWidget::setFullUrl( bool fullUrl )
218{
220}
221
223{
224 return mDefaultRoot;
225}
226
227void QgsFileWidget::setDefaultRoot( const QString &defaultRoot )
228{
230}
231
233{
234 return mStorageMode;
235}
236
238{
240 mLineEdit->setStorageMode( storageMode );
241}
242
244{
245 return mRelativeStorage;
246}
247
249{
251}
252
254{
255 return mLineEdit;
256}
257
259{
260 const bool linkVisible = mUseLink && !mIsLinkEdited;
261
262 mLineEdit->setVisible( !linkVisible );
263 mLinkLabel->setVisible( linkVisible );
264 mLinkEditButton->setVisible( mUseLink && !mReadOnly );
265
266 mFileWidgetButton->setEnabled( !mReadOnly );
267 mLineEdit->setEnabled( !mReadOnly );
268
269 mLinkEditButton->setIcon( linkVisible && !mReadOnly ?
270 QgsApplication::getThemeIcon( QStringLiteral( "/mActionToggleEditing.svg" ) ) :
271 QgsApplication::getThemeIcon( QStringLiteral( "/mActionSaveEdits.svg" ) ) );
272}
273
274void QgsFileWidget::openFileDialog()
275{
276 QgsSettings settings;
277 QString oldPath;
278
279 // if we use a relative path option, we need to obtain the full path
280 // first choice is the current file path, if one is entered
281 if ( !mFilePath.isEmpty() && ( QFile::exists( mFilePath ) || mStorageMode == SaveFile ) )
282 {
283 oldPath = relativePath( mFilePath, false );
284 }
285 // If we use fixed default path
286 // second choice is the default root
287 else if ( !mDefaultRoot.isEmpty() )
288 {
289 oldPath = QDir::cleanPath( mDefaultRoot );
290 }
291
292 // If there is no valid value, find a default path to use
293 QUrl url = QUrl::fromUserInput( oldPath );
294 if ( !url.isValid() )
295 {
296 QString defPath = QDir::cleanPath( QFileInfo( QgsProject::instance()->absoluteFilePath() ).path() );
297 if ( defPath.isEmpty() )
298 {
299 defPath = QDir::homePath();
300 }
301 oldPath = settings.value( QStringLiteral( "UI/lastFileNameWidgetDir" ), defPath ).toString();
302 }
303
304 // Handle Storage
305 QString fileName;
306 QStringList fileNames;
307 QString title;
308
309 {
310 QgsFocusKeeper focusKeeper;
311 switch ( mStorageMode )
312 {
313 case GetFile:
314 title = !mDialogTitle.isEmpty() ? mDialogTitle : tr( "Select a file" );
315 fileName = QFileDialog::getOpenFileName( this, title, QFileInfo( oldPath ).absoluteFilePath(), mFilter, &mSelectedFilter, mOptions );
316 break;
317 case GetMultipleFiles:
318 title = !mDialogTitle.isEmpty() ? mDialogTitle : tr( "Select one or more files" );
319 fileNames = QFileDialog::getOpenFileNames( this, title, QFileInfo( oldPath ).absoluteFilePath(), mFilter, &mSelectedFilter, mOptions );
320 break;
321 case GetDirectory:
322 title = !mDialogTitle.isEmpty() ? mDialogTitle : tr( "Select a directory" );
323 fileName = QFileDialog::getExistingDirectory( this, title, QFileInfo( oldPath ).absoluteFilePath(), mOptions );
324 break;
325 case SaveFile:
326 {
327 title = !mDialogTitle.isEmpty() ? mDialogTitle : tr( "Create or select a file" );
328 if ( !confirmOverwrite() )
329 {
330 fileName = QFileDialog::getSaveFileName( this, title, QFileInfo( oldPath ).absoluteFilePath(), mFilter, &mSelectedFilter, mOptions | QFileDialog::DontConfirmOverwrite );
331 }
332 else
333 {
334 fileName = QFileDialog::getSaveFileName( this, title, QFileInfo( oldPath ).absoluteFilePath(), mFilter, &mSelectedFilter, mOptions );
335 }
336
337 // make sure filename ends with filter. This isn't automatically done by
338 // getSaveFileName on some platforms (e.g. gnome)
340 }
341 break;
342 }
343 }
344
345 // return dialog focus on Mac
346 activateWindow();
347 raise();
348
349 if ( fileName.isEmpty() && fileNames.isEmpty( ) )
350 return;
351
353 fileNames << fileName;
354
355 for ( int i = 0; i < fileNames.length(); i++ )
356 {
357 fileNames.replace( i, QDir::toNativeSeparators( QDir::cleanPath( QFileInfo( fileNames.at( i ) ).absoluteFilePath() ) ) );
358 }
359
360 // Store the last used path:
361 switch ( mStorageMode )
362 {
363 case GetFile:
364 case SaveFile:
365 case GetMultipleFiles:
366 settings.setValue( QStringLiteral( "UI/lastFileNameWidgetDir" ), QFileInfo( fileNames.first() ).absolutePath() );
367 break;
368 case GetDirectory:
369 settings.setValue( QStringLiteral( "UI/lastFileNameWidgetDir" ), fileNames.first() );
370 break;
371 }
372
373 setSelectedFileNames( fileNames );
374}
375
376void QgsFileWidget::setSelectedFileNames( QStringList fileNames )
377{
378 Q_ASSERT( fileNames.count() );
379
380 // Handle relative Path storage
381 for ( int i = 0; i < fileNames.length(); i++ )
382 {
383 fileNames.replace( i, relativePath( fileNames.at( i ), true ) );
384 }
385
386 setFilePaths( fileNames );
387}
388
389void QgsFileWidget::setFilePaths( const QStringList &filePaths )
390{
392 {
393 setFilePath( filePaths.first() );
394 }
395 else
396 {
397 if ( filePaths.length() > 1 )
398 {
399 setFilePath( QStringLiteral( "\"%1\"" ).arg( filePaths.join( QLatin1String( "\" \"" ) ) ) );
400 }
401 else
402 {
403 setFilePath( filePaths.first( ) );
404 }
405 }
406}
407
408QString QgsFileWidget::relativePath( const QString &filePath, bool removeRelative ) const
409{
410 QString RelativePath;
412 {
413 RelativePath = QDir::toNativeSeparators( QDir::cleanPath( QFileInfo( QgsProject::instance()->absoluteFilePath() ).path() ) );
414 }
415 else if ( mRelativeStorage == RelativeDefaultPath && !mDefaultRoot.isEmpty() )
416 {
417 RelativePath = QDir::toNativeSeparators( QDir::cleanPath( mDefaultRoot ) );
418 }
419
420 if ( !RelativePath.isEmpty() )
421 {
422 if ( removeRelative )
423 {
424 return QDir::cleanPath( QDir( RelativePath ).relativeFilePath( filePath ) );
425 }
426 else
427 {
428 return QDir::cleanPath( QDir( RelativePath ).filePath( filePath ) );
429 }
430 }
431
432 return filePath;
433}
434
435
436QString QgsFileWidget::toUrl( const QString &path ) const
437{
438 QString rep;
439 if ( path.isEmpty() )
440 {
442 }
443
444 if ( isMultiFiles( path ) )
445 {
446 return QStringLiteral( "<a>%1</a>" ).arg( path );
447 }
448
449 QString urlStr = relativePath( path, false );
450 QUrl url = QUrl::fromUserInput( urlStr );
451 if ( !url.isValid() || !url.isLocalFile() )
452 {
453 QgsDebugMsgLevel( QStringLiteral( "URL: %1 is not valid or not a local file!" ).arg( path ), 2 );
454 rep = path;
455 }
456
457 QString pathStr = url.toString();
458 if ( mFullUrl )
459 {
460 rep = QStringLiteral( "<a href=\"%1\">%2</a>" ).arg( pathStr, path );
461 }
462 else
463 {
464 QString fileName = QFileInfo( urlStr ).fileName();
465 rep = QStringLiteral( "<a href=\"%1\">%2</a>" ).arg( pathStr, fileName );
466 }
467
468 return rep;
469}
470
471
473
474
475QgsFileDropEdit::QgsFileDropEdit( QWidget *parent )
476 : QgsHighlightableLineEdit( parent )
477{
478 setAcceptDrops( true );
479}
480
481void QgsFileDropEdit::setFilters( const QString &filters )
482{
483 mAcceptableExtensions.clear();
484
485 if ( filters.contains( QStringLiteral( "*.*" ) ) )
486 return; // everything is allowed!
487
488 QRegularExpression rx( QStringLiteral( "\\*\\.(\\w+)" ) );
489 QRegularExpressionMatchIterator i = rx.globalMatch( filters );
490 while ( i.hasNext() )
491 {
492 QRegularExpressionMatch match = i.next();
493 if ( match.hasMatch() )
494 {
495 mAcceptableExtensions << match.captured( 1 ).toLower();
496 }
497 }
498}
499
500QStringList QgsFileDropEdit::acceptableFilePaths( QDropEvent *event ) const
501{
502 QStringList rawPaths;
503 QStringList paths;
504 if ( event->mimeData()->hasUrls() )
505 {
506 const QList< QUrl > urls = event->mimeData()->urls();
507 rawPaths.reserve( urls.count() );
508 for ( const QUrl &url : urls )
509 {
510 const QString local = url.toLocalFile();
511 if ( !rawPaths.contains( local ) )
512 rawPaths.append( local );
513 }
514 }
515
517 for ( const QgsMimeDataUtils::Uri &u : std::as_const( lst ) )
518 {
519 if ( !rawPaths.contains( u.uri ) )
520 rawPaths.append( u.uri );
521 }
522
523 if ( !event->mimeData()->text().isEmpty() && !rawPaths.contains( event->mimeData()->text() ) )
524 rawPaths.append( event->mimeData()->text() );
525
526 paths.reserve( rawPaths.count() );
527 for ( const QString &path : std::as_const( rawPaths ) )
528 {
529 QFileInfo file( path );
530 switch ( mStorageMode )
531 {
535 {
536 if ( file.isFile() && ( mAcceptableExtensions.isEmpty() || mAcceptableExtensions.contains( file.suffix(), Qt::CaseInsensitive ) ) )
537 paths.append( file.filePath() );
538
539 break;
540 }
541
543 {
544 if ( file.isDir() )
545 paths.append( file.filePath() );
546 else if ( file.isFile() )
547 {
548 // folder mode, but a file dropped. So get folder name from file
549 paths.append( file.absolutePath() );
550 }
551
552 break;
553 }
554 }
555 }
556
557 return paths;
558}
559
560QString QgsFileDropEdit::acceptableFilePath( QDropEvent *event ) const
561{
562 const QStringList paths = acceptableFilePaths( event );
563 if ( paths.size() > 1 )
564 {
565 return QStringLiteral( "\"%1\"" ).arg( paths.join( QLatin1String( "\" \"" ) ) );
566 }
567 else if ( paths.size() == 1 )
568 {
569 return paths.first();
570 }
571 else
572 {
573 return QString();
574 }
575}
576
577void QgsFileDropEdit::dragEnterEvent( QDragEnterEvent *event )
578{
579 QString filePath = acceptableFilePath( event );
580 if ( !filePath.isEmpty() )
581 {
582 event->acceptProposedAction();
583 setHighlighted( true );
584 }
585 else
586 {
587 event->ignore();
588 }
589}
590
591void QgsFileDropEdit::dragLeaveEvent( QDragLeaveEvent *event )
592{
593 QgsFilterLineEdit::dragLeaveEvent( event );
594 event->accept();
595 setHighlighted( false );
596}
597
598void QgsFileDropEdit::dropEvent( QDropEvent *event )
599{
600 QString filePath = acceptableFilePath( event );
601 if ( !filePath.isEmpty() )
602 {
603 event->acceptProposedAction();
604 emit fileDropped( filePath );
605 }
606
607 setHighlighted( false );
608}
609
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
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:477
This class is a composition of two QSettings instances:
Definition: qgssettings.h:62
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