QGIS API Documentation 3.28.11-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 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() || path == QgsApplication::nullRepresentation() )
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.
@ 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