QGIS API Documentation 3.41.0-Master (3440c17df1d)
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 ?
267 QgsApplication::getThemeIcon( QStringLiteral( "/mActionToggleEditing.svg" ) ) :
268 QgsApplication::getThemeIcon( QStringLiteral( "/mActionSaveEdits.svg" ) ) );
269}
270
271void QgsFileWidget::openFileDialog()
272{
273 QgsSettings settings;
274 QString oldPath;
275
276 // if we use a relative path option, we need to obtain the full path
277 // first choice is the current file path, if one is entered
278 if ( !mFilePath.isEmpty() && ( QFile::exists( mFilePath ) || mStorageMode == SaveFile ) )
279 {
280 oldPath = relativePath( mFilePath, false );
281 }
282 // If we use fixed default path
283 // second choice is the default root
284 else if ( !mDefaultRoot.isEmpty() )
285 {
286 oldPath = QDir::cleanPath( mDefaultRoot );
287 }
288
289 // If there is no valid value, find a default path to use
290 QUrl url = QUrl::fromUserInput( oldPath );
291 if ( !url.isValid() )
292 {
293 QString defPath = QDir::cleanPath( QFileInfo( QgsProject::instance()->absoluteFilePath() ).path() );
294 if ( defPath.isEmpty() )
295 {
296 defPath = QDir::homePath();
297 }
298 oldPath = settings.value( QStringLiteral( "UI/lastFileNameWidgetDir" ), defPath ).toString();
299 }
300
301 // Handle Storage
302 QString fileName;
303 QStringList fileNames;
304 QString title;
305
306 {
307 QgsFocusKeeper focusKeeper;
308 switch ( mStorageMode )
309 {
310 case GetFile:
311 title = !mDialogTitle.isEmpty() ? mDialogTitle : tr( "Select a file" );
312 fileName = QFileDialog::getOpenFileName( this, title, QFileInfo( oldPath ).absoluteFilePath(), mFilter, &mSelectedFilter, mOptions );
313 break;
314 case GetMultipleFiles:
315 title = !mDialogTitle.isEmpty() ? mDialogTitle : tr( "Select one or more files" );
316 fileNames = QFileDialog::getOpenFileNames( this, title, QFileInfo( oldPath ).absoluteFilePath(), mFilter, &mSelectedFilter, mOptions );
317 break;
318 case GetDirectory:
319 title = !mDialogTitle.isEmpty() ? mDialogTitle : tr( "Select a directory" );
320 fileName = QFileDialog::getExistingDirectory( this, title, QFileInfo( oldPath ).absoluteFilePath(), mOptions );
321 break;
322 case SaveFile:
323 {
324 title = !mDialogTitle.isEmpty() ? mDialogTitle : tr( "Create or select a file" );
325 if ( !confirmOverwrite() )
326 {
327 fileName = QFileDialog::getSaveFileName( this, title, QFileInfo( oldPath ).absoluteFilePath(), mFilter, &mSelectedFilter, mOptions | QFileDialog::DontConfirmOverwrite );
328 }
329 else
330 {
331 fileName = QFileDialog::getSaveFileName( this, title, QFileInfo( oldPath ).absoluteFilePath(), mFilter, &mSelectedFilter, mOptions );
332 }
333
334 // make sure filename ends with filter. This isn't automatically done by
335 // getSaveFileName on some platforms (e.g. gnome)
337
338 // A bit of hack to solve https://github.com/qgis/QGIS/issues/54566
339 // to be able to select an existing File Geodatabase, we add in the filter
340 // the "gdb" file that is found in all File Geodatabase .gdb directory
341 // to allow the user to select it. We now need to remove this gdb file
342 // (which became gdb.gdb due to above logic) from the selected filename
343 if ( mFilter.contains( QLatin1String( "(*.gdb *.GDB gdb)" ) ) &&
344 ( fileName.endsWith( QLatin1String( "/gdb.gdb" ) ) ||
345 fileName.endsWith( QLatin1String( "\\gdb.gdb" ) ) ) )
346 {
347 fileName.chop( static_cast<int>( strlen( "/gdb.gdb" ) ) );
348 }
349 }
350 break;
351 }
352 }
353
354 // return dialog focus on Mac
355 activateWindow();
356 raise();
357
358 if ( fileName.isEmpty() && fileNames.isEmpty( ) )
359 return;
360
362 fileNames << fileName;
363
364 for ( int i = 0; i < fileNames.length(); i++ )
365 {
366 fileNames.replace( i, QDir::toNativeSeparators( QDir::cleanPath( QFileInfo( fileNames.at( i ) ).absoluteFilePath() ) ) );
367 }
368
369 // Store the last used path:
370 switch ( mStorageMode )
371 {
372 case GetFile:
373 case SaveFile:
374 case GetMultipleFiles:
375 settings.setValue( QStringLiteral( "UI/lastFileNameWidgetDir" ), QFileInfo( fileNames.first() ).absolutePath() );
376 break;
377 case GetDirectory:
378 settings.setValue( QStringLiteral( "UI/lastFileNameWidgetDir" ), fileNames.first() );
379 break;
380 }
381
382 setSelectedFileNames( fileNames );
383}
384
385void QgsFileWidget::setSelectedFileNames( QStringList fileNames )
386{
387 Q_ASSERT( fileNames.count() );
388
389 // Handle relative Path storage
390 for ( int i = 0; i < fileNames.length(); i++ )
391 {
392 fileNames.replace( i, relativePath( fileNames.at( i ), true ) );
393 }
394
395 setFilePaths( fileNames );
396}
397
398void QgsFileWidget::setFilePaths( const QStringList &filePaths )
399{
401 {
402 setFilePath( filePaths.first() );
403 }
404 else
405 {
406 if ( filePaths.length() > 1 )
407 {
408 setFilePath( QStringLiteral( "\"%1\"" ).arg( filePaths.join( QLatin1String( "\" \"" ) ) ) );
409 }
410 else
411 {
412 setFilePath( filePaths.first( ) );
413 }
414 }
415}
416
417QString QgsFileWidget::relativePath( const QString &filePath, bool removeRelative ) const
418{
419 QString RelativePath;
421 {
422 RelativePath = QDir::toNativeSeparators( QDir::cleanPath( QFileInfo( QgsProject::instance()->absoluteFilePath() ).path() ) );
423 }
424 else if ( mRelativeStorage == RelativeDefaultPath && !mDefaultRoot.isEmpty() )
425 {
426 RelativePath = QDir::toNativeSeparators( QDir::cleanPath( mDefaultRoot ) );
427 }
428
429 if ( !RelativePath.isEmpty() )
430 {
431 if ( removeRelative )
432 {
433 return QDir::cleanPath( QDir( RelativePath ).relativeFilePath( filePath ) );
434 }
435 else
436 {
437 return QDir::cleanPath( QDir( RelativePath ).filePath( filePath ) );
438 }
439 }
440
441 return filePath;
442}
443
445{
446 QSize size { mLineEdit->minimumSizeHint() };
447 const QSize btnSize { mFileWidgetButton->minimumSizeHint() };
448 size.setWidth( size.width() + btnSize.width() );
449 size.setHeight( std::max( size.height(), btnSize.height() ) );
450 return size;
451}
452
453
454QString QgsFileWidget::toUrl( const QString &path ) const
455{
456 QString rep;
457 if ( path.isEmpty() || path == QgsApplication::nullRepresentation() )
458 {
460 }
461
462 if ( isMultiFiles( path ) )
463 {
464 return QStringLiteral( "<a>%1</a>" ).arg( path );
465 }
466
467 QString urlStr = relativePath( path, false );
468 QUrl url = QUrl::fromUserInput( urlStr );
469 if ( !url.isValid() || !url.isLocalFile() )
470 {
471 QgsDebugMsgLevel( QStringLiteral( "URL: %1 is not valid or not a local file!" ).arg( path ), 2 );
472 rep = path;
473 }
474
475 QString pathStr = url.toString();
476 if ( mFullUrl )
477 {
478 rep = QStringLiteral( "<a href=\"%1\">%2</a>" ).arg( pathStr, path );
479 }
480 else
481 {
482 QString fileName = QFileInfo( urlStr ).fileName();
483 rep = QStringLiteral( "<a href=\"%1\">%2</a>" ).arg( pathStr, fileName );
484 }
485
486 return rep;
487}
488
489
491
492
493QgsFileDropEdit::QgsFileDropEdit( QWidget *parent )
494 : QgsHighlightableLineEdit( parent )
495{
496 setAcceptDrops( true );
497}
498
499void QgsFileDropEdit::setFilters( const QString &filters )
500{
501 mAcceptableExtensions.clear();
502
503 if ( filters.contains( QStringLiteral( "*.*" ) ) )
504 return; // everything is allowed!
505
506 const thread_local QRegularExpression rx( QStringLiteral( "\\*\\.(\\w+)" ) );
507 QRegularExpressionMatchIterator i = rx.globalMatch( filters );
508 while ( i.hasNext() )
509 {
510 QRegularExpressionMatch match = i.next();
511 if ( match.hasMatch() )
512 {
513 mAcceptableExtensions << match.captured( 1 ).toLower();
514 }
515 }
516}
517
518QStringList QgsFileDropEdit::acceptableFilePaths( QDropEvent *event ) const
519{
520 QStringList rawPaths;
521 QStringList paths;
522 if ( event->mimeData()->hasUrls() )
523 {
524 const QList< QUrl > urls = event->mimeData()->urls();
525 rawPaths.reserve( urls.count() );
526 for ( const QUrl &url : urls )
527 {
528 const QString local = url.toLocalFile();
529 if ( !rawPaths.contains( local ) )
530 rawPaths.append( local );
531 }
532 }
533
535 for ( const QgsMimeDataUtils::Uri &u : std::as_const( lst ) )
536 {
537 if ( !rawPaths.contains( u.uri ) )
538 rawPaths.append( u.uri );
539 }
540
541 if ( !event->mimeData()->text().isEmpty() && !rawPaths.contains( event->mimeData()->text() ) )
542 rawPaths.append( event->mimeData()->text() );
543
544 paths.reserve( rawPaths.count() );
545 for ( const QString &path : std::as_const( rawPaths ) )
546 {
547 QFileInfo file( path );
548 switch ( mStorageMode )
549 {
553 {
554 if ( file.isFile() && ( mAcceptableExtensions.isEmpty() || mAcceptableExtensions.contains( file.suffix(), Qt::CaseInsensitive ) ) )
555 paths.append( file.filePath() );
556
557 break;
558 }
559
561 {
562 if ( file.isDir() )
563 paths.append( file.filePath() );
564 else if ( file.isFile() )
565 {
566 // folder mode, but a file dropped. So get folder name from file
567 paths.append( file.absolutePath() );
568 }
569
570 break;
571 }
572 }
573 }
574
575 return paths;
576}
577
578QString QgsFileDropEdit::acceptableFilePath( QDropEvent *event ) const
579{
580 const QStringList paths = acceptableFilePaths( event );
581 if ( paths.size() > 1 )
582 {
583 return QStringLiteral( "\"%1\"" ).arg( paths.join( QLatin1String( "\" \"" ) ) );
584 }
585 else if ( paths.size() == 1 )
586 {
587 return paths.first();
588 }
589 else
590 {
591 return QString();
592 }
593}
594
595void QgsFileDropEdit::dragEnterEvent( QDragEnterEvent *event )
596{
597 QString filePath = acceptableFilePath( event );
598 if ( !filePath.isEmpty() )
599 {
600 event->acceptProposedAction();
601 setHighlighted( true );
602 }
603 else
604 {
605 event->ignore();
606 }
607}
608
609void QgsFileDropEdit::dragLeaveEvent( QDragLeaveEvent *event )
610{
611 QgsFilterLineEdit::dragLeaveEvent( event );
612 event->accept();
613 setHighlighted( false );
614}
615
616void QgsFileDropEdit::dropEvent( QDropEvent *event )
617{
618 QString filePath = acceptableFilePath( event );
619 if ( !filePath.isEmpty() )
620 {
621 event->acceptProposedAction();
622 emit fileDropped( filePath );
623 }
624
625 setHighlighted( false );
626}
627
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