QGIS API Documentation 3.28.14-Firenze (exported)
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
7 email : denis.rouzaud@gmail.com
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
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
236
242
247
252
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 // A bit of hack to solve https://github.com/qgis/QGIS/issues/54566
342 // to be able to select an existing File Geodatabase, we add in the filter
343 // the "gdb" file that is found in all File Geodatabase .gdb directory
344 // to allow the user to select it. We now need to remove this gdb file
345 // (which became gdb.gdb due to above logic) from the selected filename
346 if ( mFilter.contains( QLatin1String( "(*.gdb *.GDB gdb)" ) ) &&
347 ( fileName.endsWith( QLatin1String( "/gdb.gdb" ) ) ||
348 fileName.endsWith( QLatin1String( "\\gdb.gdb" ) ) ) )
349 {
350 fileName.chop( static_cast<int>( strlen( "/gdb.gdb" ) ) );
351 }
352 }
353 break;
354 }
355 }
356
357 // return dialog focus on Mac
358 activateWindow();
359 raise();
360
361 if ( fileName.isEmpty() && fileNames.isEmpty( ) )
362 return;
363
365 fileNames << fileName;
366
367 for ( int i = 0; i < fileNames.length(); i++ )
368 {
369 fileNames.replace( i, QDir::toNativeSeparators( QDir::cleanPath( QFileInfo( fileNames.at( i ) ).absoluteFilePath() ) ) );
370 }
371
372 // Store the last used path:
373 switch ( mStorageMode )
374 {
375 case GetFile:
376 case SaveFile:
377 case GetMultipleFiles:
378 settings.setValue( QStringLiteral( "UI/lastFileNameWidgetDir" ), QFileInfo( fileNames.first() ).absolutePath() );
379 break;
380 case GetDirectory:
381 settings.setValue( QStringLiteral( "UI/lastFileNameWidgetDir" ), fileNames.first() );
382 break;
383 }
384
385 setSelectedFileNames( fileNames );
386}
387
388void QgsFileWidget::setSelectedFileNames( QStringList fileNames )
389{
390 Q_ASSERT( fileNames.count() );
391
392 // Handle relative Path storage
393 for ( int i = 0; i < fileNames.length(); i++ )
394 {
395 fileNames.replace( i, relativePath( fileNames.at( i ), true ) );
396 }
397
398 setFilePaths( fileNames );
399}
400
401void QgsFileWidget::setFilePaths( const QStringList &filePaths )
402{
404 {
405 setFilePath( filePaths.first() );
406 }
407 else
408 {
409 if ( filePaths.length() > 1 )
410 {
411 setFilePath( QStringLiteral( "\"%1\"" ).arg( filePaths.join( QLatin1String( "\" \"" ) ) ) );
412 }
413 else
414 {
415 setFilePath( filePaths.first( ) );
416 }
417 }
418}
419
420QString QgsFileWidget::relativePath( const QString &filePath, bool removeRelative ) const
421{
422 QString RelativePath;
424 {
425 RelativePath = QDir::toNativeSeparators( QDir::cleanPath( QFileInfo( QgsProject::instance()->absoluteFilePath() ).path() ) );
426 }
427 else if ( mRelativeStorage == RelativeDefaultPath && !mDefaultRoot.isEmpty() )
428 {
429 RelativePath = QDir::toNativeSeparators( QDir::cleanPath( mDefaultRoot ) );
430 }
431
432 if ( !RelativePath.isEmpty() )
433 {
434 if ( removeRelative )
435 {
436 return QDir::cleanPath( QDir( RelativePath ).relativeFilePath( filePath ) );
437 }
438 else
439 {
440 return QDir::cleanPath( QDir( RelativePath ).filePath( filePath ) );
441 }
442 }
443
444 return filePath;
445}
446
447
448QString QgsFileWidget::toUrl( const QString &path ) const
449{
450 QString rep;
451 if ( path.isEmpty() || path == QgsApplication::nullRepresentation() )
452 {
454 }
455
456 if ( isMultiFiles( path ) )
457 {
458 return QStringLiteral( "<a>%1</a>" ).arg( path );
459 }
460
461 QString urlStr = relativePath( path, false );
462 QUrl url = QUrl::fromUserInput( urlStr );
463 if ( !url.isValid() || !url.isLocalFile() )
464 {
465 QgsDebugMsgLevel( QStringLiteral( "URL: %1 is not valid or not a local file!" ).arg( path ), 2 );
466 rep = path;
467 }
468
469 QString pathStr = url.toString();
470 if ( mFullUrl )
471 {
472 rep = QStringLiteral( "<a href=\"%1\">%2</a>" ).arg( pathStr, path );
473 }
474 else
475 {
476 QString fileName = QFileInfo( urlStr ).fileName();
477 rep = QStringLiteral( "<a href=\"%1\">%2</a>" ).arg( pathStr, fileName );
478 }
479
480 return rep;
481}
482
483
485
486
487QgsFileDropEdit::QgsFileDropEdit( QWidget *parent )
488 : QgsHighlightableLineEdit( parent )
489{
490 setAcceptDrops( true );
491}
492
493void QgsFileDropEdit::setFilters( const QString &filters )
494{
495 mAcceptableExtensions.clear();
496
497 if ( filters.contains( QStringLiteral( "*.*" ) ) )
498 return; // everything is allowed!
499
500 QRegularExpression rx( QStringLiteral( "\\*\\.(\\w+)" ) );
501 QRegularExpressionMatchIterator i = rx.globalMatch( filters );
502 while ( i.hasNext() )
503 {
504 QRegularExpressionMatch match = i.next();
505 if ( match.hasMatch() )
506 {
507 mAcceptableExtensions << match.captured( 1 ).toLower();
508 }
509 }
510}
511
512QStringList QgsFileDropEdit::acceptableFilePaths( QDropEvent *event ) const
513{
514 QStringList rawPaths;
515 QStringList paths;
516 if ( event->mimeData()->hasUrls() )
517 {
518 const QList< QUrl > urls = event->mimeData()->urls();
519 rawPaths.reserve( urls.count() );
520 for ( const QUrl &url : urls )
521 {
522 const QString local = url.toLocalFile();
523 if ( !rawPaths.contains( local ) )
524 rawPaths.append( local );
525 }
526 }
527
529 for ( const QgsMimeDataUtils::Uri &u : std::as_const( lst ) )
530 {
531 if ( !rawPaths.contains( u.uri ) )
532 rawPaths.append( u.uri );
533 }
534
535 if ( !event->mimeData()->text().isEmpty() && !rawPaths.contains( event->mimeData()->text() ) )
536 rawPaths.append( event->mimeData()->text() );
537
538 paths.reserve( rawPaths.count() );
539 for ( const QString &path : std::as_const( rawPaths ) )
540 {
541 QFileInfo file( path );
542 switch ( mStorageMode )
543 {
547 {
548 if ( file.isFile() && ( mAcceptableExtensions.isEmpty() || mAcceptableExtensions.contains( file.suffix(), Qt::CaseInsensitive ) ) )
549 paths.append( file.filePath() );
550
551 break;
552 }
553
555 {
556 if ( file.isDir() )
557 paths.append( file.filePath() );
558 else if ( file.isFile() )
559 {
560 // folder mode, but a file dropped. So get folder name from file
561 paths.append( file.absolutePath() );
562 }
563
564 break;
565 }
566 }
567 }
568
569 return paths;
570}
571
572QString QgsFileDropEdit::acceptableFilePath( QDropEvent *event ) const
573{
574 const QStringList paths = acceptableFilePaths( event );
575 if ( paths.size() > 1 )
576 {
577 return QStringLiteral( "\"%1\"" ).arg( paths.join( QLatin1String( "\" \"" ) ) );
578 }
579 else if ( paths.size() == 1 )
580 {
581 return paths.first();
582 }
583 else
584 {
585 return QString();
586 }
587}
588
589void QgsFileDropEdit::dragEnterEvent( QDragEnterEvent *event )
590{
591 QString filePath = acceptableFilePath( event );
592 if ( !filePath.isEmpty() )
593 {
594 event->acceptProposedAction();
595 setHighlighted( true );
596 }
597 else
598 {
599 event->ignore();
600 }
601}
602
603void QgsFileDropEdit::dragLeaveEvent( QDragLeaveEvent *event )
604{
605 QgsFilterLineEdit::dragLeaveEvent( event );
606 event->accept();
607 setHighlighted( false );
608}
609
610void QgsFileDropEdit::dropEvent( QDropEvent *event )
611{
612 QString filePath = acceptableFilePath( event );
613 if ( !filePath.isEmpty() )
614 {
615 event->acceptProposedAction();
616 emit fileDropped( filePath );
617 }
618
619 setHighlighted( false );
620}
621
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.
@ 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
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: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