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