QGIS API Documentation 3.34.0-Prizren (ffbdd678812)
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 <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
448{
449 QSize size { mLineEdit->minimumSizeHint() };
450 const QSize btnSize { mFileWidgetButton->minimumSizeHint() };
451 size.setWidth( size.width() + btnSize.width() );
452 size.setHeight( std::max( size.height(), btnSize.height() ) );
453 return size;
454}
455
456
457QString QgsFileWidget::toUrl( const QString &path ) const
458{
459 QString rep;
460 if ( path.isEmpty() || path == QgsApplication::nullRepresentation() )
461 {
463 }
464
465 if ( isMultiFiles( path ) )
466 {
467 return QStringLiteral( "<a>%1</a>" ).arg( path );
468 }
469
470 QString urlStr = relativePath( path, false );
471 QUrl url = QUrl::fromUserInput( urlStr );
472 if ( !url.isValid() || !url.isLocalFile() )
473 {
474 QgsDebugMsgLevel( QStringLiteral( "URL: %1 is not valid or not a local file!" ).arg( path ), 2 );
475 rep = path;
476 }
477
478 QString pathStr = url.toString();
479 if ( mFullUrl )
480 {
481 rep = QStringLiteral( "<a href=\"%1\">%2</a>" ).arg( pathStr, path );
482 }
483 else
484 {
485 QString fileName = QFileInfo( urlStr ).fileName();
486 rep = QStringLiteral( "<a href=\"%1\">%2</a>" ).arg( pathStr, fileName );
487 }
488
489 return rep;
490}
491
492
494
495
496QgsFileDropEdit::QgsFileDropEdit( QWidget *parent )
497 : QgsHighlightableLineEdit( parent )
498{
499 setAcceptDrops( true );
500}
501
502void QgsFileDropEdit::setFilters( const QString &filters )
503{
504 mAcceptableExtensions.clear();
505
506 if ( filters.contains( QStringLiteral( "*.*" ) ) )
507 return; // everything is allowed!
508
509 const thread_local QRegularExpression rx( QStringLiteral( "\\*\\.(\\w+)" ) );
510 QRegularExpressionMatchIterator i = rx.globalMatch( filters );
511 while ( i.hasNext() )
512 {
513 QRegularExpressionMatch match = i.next();
514 if ( match.hasMatch() )
515 {
516 mAcceptableExtensions << match.captured( 1 ).toLower();
517 }
518 }
519}
520
521QStringList QgsFileDropEdit::acceptableFilePaths( QDropEvent *event ) const
522{
523 QStringList rawPaths;
524 QStringList paths;
525 if ( event->mimeData()->hasUrls() )
526 {
527 const QList< QUrl > urls = event->mimeData()->urls();
528 rawPaths.reserve( urls.count() );
529 for ( const QUrl &url : urls )
530 {
531 const QString local = url.toLocalFile();
532 if ( !rawPaths.contains( local ) )
533 rawPaths.append( local );
534 }
535 }
536
538 for ( const QgsMimeDataUtils::Uri &u : std::as_const( lst ) )
539 {
540 if ( !rawPaths.contains( u.uri ) )
541 rawPaths.append( u.uri );
542 }
543
544 if ( !event->mimeData()->text().isEmpty() && !rawPaths.contains( event->mimeData()->text() ) )
545 rawPaths.append( event->mimeData()->text() );
546
547 paths.reserve( rawPaths.count() );
548 for ( const QString &path : std::as_const( rawPaths ) )
549 {
550 QFileInfo file( path );
551 switch ( mStorageMode )
552 {
556 {
557 if ( file.isFile() && ( mAcceptableExtensions.isEmpty() || mAcceptableExtensions.contains( file.suffix(), Qt::CaseInsensitive ) ) )
558 paths.append( file.filePath() );
559
560 break;
561 }
562
564 {
565 if ( file.isDir() )
566 paths.append( file.filePath() );
567 else if ( file.isFile() )
568 {
569 // folder mode, but a file dropped. So get folder name from file
570 paths.append( file.absolutePath() );
571 }
572
573 break;
574 }
575 }
576 }
577
578 return paths;
579}
580
581QString QgsFileDropEdit::acceptableFilePath( QDropEvent *event ) const
582{
583 const QStringList paths = acceptableFilePaths( event );
584 if ( paths.size() > 1 )
585 {
586 return QStringLiteral( "\"%1\"" ).arg( paths.join( QLatin1String( "\" \"" ) ) );
587 }
588 else if ( paths.size() == 1 )
589 {
590 return paths.first();
591 }
592 else
593 {
594 return QString();
595 }
596}
597
598void QgsFileDropEdit::dragEnterEvent( QDragEnterEvent *event )
599{
600 QString filePath = acceptableFilePath( event );
601 if ( !filePath.isEmpty() )
602 {
603 event->acceptProposedAction();
604 setHighlighted( true );
605 }
606 else
607 {
608 event->ignore();
609 }
610}
611
612void QgsFileDropEdit::dragLeaveEvent( QDragLeaveEvent *event )
613{
614 QgsFilterLineEdit::dragLeaveEvent( event );
615 event->accept();
616 setHighlighted( false );
617}
618
619void QgsFileDropEdit::dropEvent( QDropEvent *event )
620{
621 QString filePath = acceptableFilePath( event );
622 if ( !filePath.isEmpty() )
623 {
624 event->acceptProposedAction();
625 emit fileDropped( filePath );
626 }
627
628 setHighlighted( false );
629}
630
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
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:63
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