QGIS API Documentation  3.27.0-Master (597e8eebd4)
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 : [email protected]
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 
36 QgsFileWidget::QgsFileWidget( QWidget *parent )
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 
82 QStringList 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 
103 void QgsFileWidget::setFilePath( const QString &path )
104 {
105  //will trigger textEdited slot
106  mLineEdit->setValue( path );
107 }
108 
109 void 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 
124 void QgsFileWidget::setDialogTitle( const QString &title )
125 {
126  mDialogTitle = title;
127 }
128 
129 QString QgsFileWidget::filter() const
130 {
131  return mFilter;
132 }
133 
134 void QgsFileWidget::setFilter( const QString &filters )
135 {
136  mFilter = filters;
137  mLineEdit->setFilters( filters );
138 }
139 
140 QFileDialog::Options QgsFileWidget::options() const
141 {
142  return mOptions;
143 }
144 
145 void QgsFileWidget::setOptions( QFileDialog::Options options )
146 {
147  mOptions = options;
148 }
149 
151 {
152  return mButtonVisible;
153 }
154 
156 {
157  mButtonVisible = visible;
158  mFileWidgetButton->setVisible( visible );
159 }
160 
161 bool QgsFileWidget::isMultiFiles( const QString &path )
162 {
163  return path.contains( QStringLiteral( "\" \"" ) );
164 }
165 
166 void 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 
182 void QgsFileWidget::editLink()
183 {
184  if ( !mUseLink || mReadOnly )
185  return;
186 
188  updateLayout();
189 }
190 
191 void 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 
203 void QgsFileWidget::setUseLink( bool useLink )
204 {
205  if ( mUseLink == useLink )
206  return;
207 
208  mUseLink = useLink;
209  updateLayout();
210 }
211 
213 {
214  return mFullUrl;
215 }
216 
217 void QgsFileWidget::setFullUrl( bool fullUrl )
218 {
219  mFullUrl = fullUrl;
220 }
221 
223 {
224  return mDefaultRoot;
225 }
226 
227 void QgsFileWidget::setDefaultRoot( const QString &defaultRoot )
228 {
230 }
231 
233 {
234  return mStorageMode;
235 }
236 
238 {
240  mLineEdit->setStorageMode( storageMode );
241 }
242 
244 {
245  return mRelativeStorage;
246 }
247 
249 {
251 }
252 
254 {
255  return mLineEdit;
256 }
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 
274 void 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  if ( fileName.isEmpty() && fileNames.isEmpty( ) )
346  return;
347 
349  fileNames << fileName;
350 
351  for ( int i = 0; i < fileNames.length(); i++ )
352  {
353  fileNames.replace( i, QDir::toNativeSeparators( QDir::cleanPath( QFileInfo( fileNames.at( i ) ).absoluteFilePath() ) ) );
354  }
355 
356  // Store the last used path:
357  switch ( mStorageMode )
358  {
359  case GetFile:
360  case SaveFile:
361  case GetMultipleFiles:
362  settings.setValue( QStringLiteral( "UI/lastFileNameWidgetDir" ), QFileInfo( fileNames.first() ).absolutePath() );
363  break;
364  case GetDirectory:
365  settings.setValue( QStringLiteral( "UI/lastFileNameWidgetDir" ), fileNames.first() );
366  break;
367  }
368 
369  setSelectedFileNames( fileNames );
370 }
371 
372 void QgsFileWidget::setSelectedFileNames( QStringList fileNames )
373 {
374  Q_ASSERT( fileNames.count() );
375 
376  // Handle relative Path storage
377  for ( int i = 0; i < fileNames.length(); i++ )
378  {
379  fileNames.replace( i, relativePath( fileNames.at( i ), true ) );
380  }
381 
382  setFilePaths( fileNames );
383 }
384 
385 void QgsFileWidget::setFilePaths( const QStringList &filePaths )
386 {
388  {
389  setFilePath( filePaths.first() );
390  }
391  else
392  {
393  if ( filePaths.length() > 1 )
394  {
395  setFilePath( QStringLiteral( "\"%1\"" ).arg( filePaths.join( QLatin1String( "\" \"" ) ) ) );
396  }
397  else
398  {
399  setFilePath( filePaths.first( ) );
400  }
401  }
402 }
403 
404 QString QgsFileWidget::relativePath( const QString &filePath, bool removeRelative ) const
405 {
406  QString RelativePath;
408  {
409  RelativePath = QDir::toNativeSeparators( QDir::cleanPath( QFileInfo( QgsProject::instance()->absoluteFilePath() ).path() ) );
410  }
411  else if ( mRelativeStorage == RelativeDefaultPath && !mDefaultRoot.isEmpty() )
412  {
413  RelativePath = QDir::toNativeSeparators( QDir::cleanPath( mDefaultRoot ) );
414  }
415 
416  if ( !RelativePath.isEmpty() )
417  {
418  if ( removeRelative )
419  {
420  return QDir::cleanPath( QDir( RelativePath ).relativeFilePath( filePath ) );
421  }
422  else
423  {
424  return QDir::cleanPath( QDir( RelativePath ).filePath( filePath ) );
425  }
426  }
427 
428  return filePath;
429 }
430 
431 
432 QString QgsFileWidget::toUrl( const QString &path ) const
433 {
434  QString rep;
435  if ( path.isEmpty() )
436  {
438  }
439 
440  if ( isMultiFiles( path ) )
441  {
442  return QStringLiteral( "<a>%1</a>" ).arg( path );
443  }
444 
445  QString urlStr = relativePath( path, false );
446  QUrl url = QUrl::fromUserInput( urlStr );
447  if ( !url.isValid() || !url.isLocalFile() )
448  {
449  QgsDebugMsgLevel( QStringLiteral( "URL: %1 is not valid or not a local file!" ).arg( path ), 2 );
450  rep = path;
451  }
452 
453  QString pathStr = url.toString();
454  if ( mFullUrl )
455  {
456  rep = QStringLiteral( "<a href=\"%1\">%2</a>" ).arg( pathStr, path );
457  }
458  else
459  {
460  QString fileName = QFileInfo( urlStr ).fileName();
461  rep = QStringLiteral( "<a href=\"%1\">%2</a>" ).arg( pathStr, fileName );
462  }
463 
464  return rep;
465 }
466 
467 
469 
470 
471 QgsFileDropEdit::QgsFileDropEdit( QWidget *parent )
472  : QgsHighlightableLineEdit( parent )
473 {
474  setAcceptDrops( true );
475 }
476 
477 void QgsFileDropEdit::setFilters( const QString &filters )
478 {
479  mAcceptableExtensions.clear();
480 
481  if ( filters.contains( QStringLiteral( "*.*" ) ) )
482  return; // everything is allowed!
483 
484  QRegularExpression rx( QStringLiteral( "\\*\\.(\\w+)" ) );
485  QRegularExpressionMatchIterator i = rx.globalMatch( filters );
486  while ( i.hasNext() )
487  {
488  QRegularExpressionMatch match = i.next();
489  if ( match.hasMatch() )
490  {
491  mAcceptableExtensions << match.captured( 1 ).toLower();
492  }
493  }
494 }
495 
496 QStringList QgsFileDropEdit::acceptableFilePaths( QDropEvent *event ) const
497 {
498  QStringList rawPaths;
499  QStringList paths;
500  if ( event->mimeData()->hasUrls() )
501  {
502  const QList< QUrl > urls = event->mimeData()->urls();
503  rawPaths.reserve( urls.count() );
504  for ( const QUrl &url : urls )
505  {
506  const QString local = url.toLocalFile();
507  if ( !rawPaths.contains( local ) )
508  rawPaths.append( local );
509  }
510  }
511 
513  for ( const QgsMimeDataUtils::Uri &u : std::as_const( lst ) )
514  {
515  if ( !rawPaths.contains( u.uri ) )
516  rawPaths.append( u.uri );
517  }
518 
519  if ( !event->mimeData()->text().isEmpty() && !rawPaths.contains( event->mimeData()->text() ) )
520  rawPaths.append( event->mimeData()->text() );
521 
522  paths.reserve( rawPaths.count() );
523  for ( const QString &path : std::as_const( rawPaths ) )
524  {
525  QFileInfo file( path );
526  switch ( mStorageMode )
527  {
531  {
532  if ( file.isFile() && ( mAcceptableExtensions.isEmpty() || mAcceptableExtensions.contains( file.suffix(), Qt::CaseInsensitive ) ) )
533  paths.append( file.filePath() );
534 
535  break;
536  }
537 
539  {
540  if ( file.isDir() )
541  paths.append( file.filePath() );
542  else if ( file.isFile() )
543  {
544  // folder mode, but a file dropped. So get folder name from file
545  paths.append( file.absolutePath() );
546  }
547 
548  break;
549  }
550  }
551  }
552 
553  return paths;
554 }
555 
556 QString QgsFileDropEdit::acceptableFilePath( QDropEvent *event ) const
557 {
558  const QStringList paths = acceptableFilePaths( event );
559  if ( paths.size() > 1 )
560  {
561  return QStringLiteral( "\"%1\"" ).arg( paths.join( QLatin1String( "\" \"" ) ) );
562  }
563  else if ( paths.size() == 1 )
564  {
565  return paths.first();
566  }
567  else
568  {
569  return QString();
570  }
571 }
572 
573 void QgsFileDropEdit::dragEnterEvent( QDragEnterEvent *event )
574 {
575  QString filePath = acceptableFilePath( event );
576  if ( !filePath.isEmpty() )
577  {
578  event->acceptProposedAction();
579  setHighlighted( true );
580  }
581  else
582  {
583  event->ignore();
584  }
585 }
586 
587 void QgsFileDropEdit::dragLeaveEvent( QDragLeaveEvent *event )
588 {
589  QgsFilterLineEdit::dragLeaveEvent( event );
590  event->accept();
591  setHighlighted( false );
592 }
593 
594 void QgsFileDropEdit::dropEvent( QDropEvent *event )
595 {
596  QString filePath = acceptableFilePath( event );
597  if ( !filePath.isEmpty() )
598  {
599  event->acceptProposedAction();
600  emit fileDropped( filePath );
601  }
602 
603  setHighlighted( false );
604 }
605 
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.
Definition: qgsfilewidget.h:67
@ GetMultipleFiles
Select multiple files.
Definition: qgsfilewidget.h:70
@ GetFile
Select a single file.
Definition: qgsfilewidget.h:68
@ GetDirectory
Select a directory.
Definition: qgsfilewidget.h:69
@ SaveFile
Select a single new or pre-existing file.
Definition: qgsfilewidget.h:71
StorageMode mStorageMode
QString mFilePath
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
Definition: qgsfilewidget.h:51
QString filter
Definition: qgsfilewidget.h:55
QFileDialog::Options options
Definition: qgsfilewidget.h:59
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
Definition: qgsfilewidget.h:54
RelativeStorage
The RelativeStorage enum determines if path is absolute, relative to the current project path or rela...
Definition: qgsfilewidget.h:79
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
Definition: qgsfilewidget.h:58
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
Definition: qgsfilewidget.h:57
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
Definition: qgsfilewidget.h:56
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.
Definition: qgsproject.cpp:479
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