QGIS API Documentation  3.22.4-Białowieża (ce8e65e95e)
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  setBackgroundRole( QPalette::Window );
40  setAutoFillBackground( true );
41 
42  mLayout = new QHBoxLayout();
43  mLayout->setContentsMargins( 0, 0, 0, 0 );
44 
45  // If displaying a hyperlink, use a QLabel
46  mLinkLabel = new QLabel( this );
47  // Make Qt opens the link with the OS defined viewer
48  mLinkLabel->setOpenExternalLinks( true );
49  // Label should always be enabled to be able to open
50  // the link on read only mode.
51  mLinkLabel->setEnabled( true );
52  mLinkLabel->setSizePolicy( QSizePolicy::Expanding, QSizePolicy::Preferred );
53  mLinkLabel->setTextFormat( Qt::RichText );
54  mLinkLabel->hide(); // do not show by default
55  mLayout->addWidget( mLinkLabel );
56 
57  // otherwise, use the traditional QLineEdit subclass
58  mLineEdit = new QgsFileDropEdit( this );
59  mLineEdit->setDragEnabled( true );
60  mLineEdit->setToolTip( tr( "Full path to the file(s), including name and extension" ) );
61  connect( mLineEdit, &QLineEdit::textChanged, this, &QgsFileWidget::textEdited );
62  connect( mLineEdit, &QgsFileDropEdit::fileDropped, this, &QgsFileWidget::fileDropped );
63  mLayout->addWidget( mLineEdit );
64 
65  mLinkEditButton = new QToolButton( this );
66  mLinkEditButton->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "/mActionToggleEditing.svg" ) ) );
67  mLayout->addWidget( mLinkEditButton );
68  connect( mLinkEditButton, &QToolButton::clicked, this, &QgsFileWidget::editLink );
69  mLinkEditButton->hide(); // do not show by default
70 
71  mFileWidgetButton = new QToolButton( this );
72  mFileWidgetButton->setText( QChar( 0x2026 ) );
73  mFileWidgetButton->setToolTip( tr( "Browse" ) );
74  connect( mFileWidgetButton, &QAbstractButton::clicked, this, &QgsFileWidget::openFileDialog );
75  mLayout->addWidget( mFileWidgetButton );
76 
77  setLayout( mLayout );
78 }
79 
81 {
82  return mFilePath;
83 }
84 
85 QStringList QgsFileWidget::splitFilePaths( const QString &path )
86 {
87  QStringList paths;
88 #if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
89  const QStringList pathParts = path.split( QRegExp( "\"\\s+\"" ), QString::SkipEmptyParts );
90 #else
91  const thread_local QRegularExpression partsRegex = QRegularExpression( QStringLiteral( "\"\\s+\"" ) );
92  const QStringList pathParts = path.split( partsRegex, Qt::SkipEmptyParts );
93 #endif
94 
95  const thread_local QRegularExpression cleanRe( QStringLiteral( "(^\\s*\")|(\"\\s*)" ) );
96  paths.reserve( pathParts.size() );
97  for ( const QString &pathsPart : pathParts )
98  {
99  QString cleaned = pathsPart;
100  cleaned.remove( cleanRe );
101  paths.append( cleaned );
102  }
103  return paths;
104 }
105 
106 void QgsFileWidget::setFilePath( const QString &path )
107 {
108  //will trigger textEdited slot
109  mLineEdit->setValue( path );
110 }
111 
112 void QgsFileWidget::setReadOnly( bool readOnly )
113 {
114  if ( mReadOnly == readOnly )
115  return;
116 
117  mReadOnly = readOnly;
118 
119  updateLayout();
120 }
121 
123 {
124  return mDialogTitle;
125 }
126 
127 void QgsFileWidget::setDialogTitle( const QString &title )
128 {
129  mDialogTitle = title;
130 }
131 
132 QString QgsFileWidget::filter() const
133 {
134  return mFilter;
135 }
136 
137 void QgsFileWidget::setFilter( const QString &filters )
138 {
139  mFilter = filters;
140  mLineEdit->setFilters( filters );
141 }
142 
143 QFileDialog::Options QgsFileWidget::options() const
144 {
145  return mOptions;
146 }
147 
148 void QgsFileWidget::setOptions( QFileDialog::Options options )
149 {
150  mOptions = options;
151 }
152 
154 {
155  return mButtonVisible;
156 }
157 
159 {
160  mButtonVisible = visible;
161  mFileWidgetButton->setVisible( visible );
162 }
163 
164 bool QgsFileWidget::isMultiFiles( const QString &path )
165 {
166  return path.contains( QStringLiteral( "\" \"" ) );
167 }
168 
169 void QgsFileWidget::textEdited( const QString &path )
170 {
171  mFilePath = path;
172  mLinkLabel->setText( toUrl( path ) );
173  // Show tooltip if multiple files are selected
174  if ( isMultiFiles( path ) )
175  {
176  mLineEdit->setToolTip( tr( "Selected files:<br><ul><li>%1</li></ul><br>" ).arg( splitFilePaths( path ).join( QLatin1String( "</li><li>" ) ) ) );
177  }
178  else
179  {
180  mLineEdit->setToolTip( QString() );
181  }
182  emit fileChanged( mFilePath );
183 }
184 
185 void QgsFileWidget::editLink()
186 {
187  if ( !mUseLink || mReadOnly )
188  return;
189 
191  updateLayout();
192 }
193 
194 void QgsFileWidget::fileDropped( const QString &filePath )
195 {
196  setSelectedFileNames( QStringList() << filePath );
197  mLineEdit->selectAll();
198  mLineEdit->setFocus( Qt::MouseFocusReason );
199 }
200 
202 {
203  return mUseLink;
204 }
205 
206 void QgsFileWidget::setUseLink( bool useLink )
207 {
208  if ( mUseLink == useLink )
209  return;
210 
211  mUseLink = useLink;
212  updateLayout();
213 }
214 
216 {
217  return mFullUrl;
218 }
219 
220 void QgsFileWidget::setFullUrl( bool fullUrl )
221 {
222  mFullUrl = fullUrl;
223 }
224 
226 {
227  return mDefaultRoot;
228 }
229 
230 void QgsFileWidget::setDefaultRoot( const QString &defaultRoot )
231 {
233 }
234 
236 {
237  return mStorageMode;
238 }
239 
241 {
243  mLineEdit->setStorageMode( storageMode );
244 }
245 
247 {
248  return mRelativeStorage;
249 }
250 
252 {
254 }
255 
257 {
258  return mLineEdit;
259 }
260 
262 {
263  const bool linkVisible = mUseLink && !mIsLinkEdited;
264 
265  mLineEdit->setVisible( !linkVisible );
266  mLinkLabel->setVisible( linkVisible );
267  mLinkEditButton->setVisible( mUseLink && !mReadOnly );
268 
269  mFileWidgetButton->setEnabled( !mReadOnly );
270  mLineEdit->setEnabled( !mReadOnly );
271 
272  mLinkEditButton->setIcon( linkVisible && !mReadOnly ?
273  QgsApplication::getThemeIcon( QStringLiteral( "/mActionToggleEditing.svg" ) ) :
274  QgsApplication::getThemeIcon( QStringLiteral( "/mActionSaveEdits.svg" ) ) );
275 }
276 
277 void QgsFileWidget::openFileDialog()
278 {
279  QgsSettings settings;
280  QString oldPath;
281 
282  // if we use a relative path option, we need to obtain the full path
283  // first choice is the current file path, if one is entered
284  if ( !mFilePath.isEmpty() && QFile::exists( mFilePath ) )
285  {
286  oldPath = relativePath( mFilePath, false );
287  }
288  // If we use fixed default path
289  // second choice is the default root
290  else if ( !mDefaultRoot.isEmpty() )
291  {
292  oldPath = QDir::cleanPath( mDefaultRoot );
293  }
294 
295  // If there is no valid value, find a default path to use
296  QUrl url = QUrl::fromUserInput( oldPath );
297  if ( !url.isValid() )
298  {
299  QString defPath = QDir::cleanPath( QFileInfo( QgsProject::instance()->absoluteFilePath() ).path() );
300  if ( defPath.isEmpty() )
301  {
302  defPath = QDir::homePath();
303  }
304  oldPath = settings.value( QStringLiteral( "UI/lastFileNameWidgetDir" ), defPath ).toString();
305  }
306 
307  // Handle Storage
308  QString fileName;
309  QStringList fileNames;
310  QString title;
311 
312  {
313  QgsFocusKeeper focusKeeper;
314  switch ( mStorageMode )
315  {
316  case GetFile:
317  title = !mDialogTitle.isEmpty() ? mDialogTitle : tr( "Select a file" );
318  fileName = QFileDialog::getOpenFileName( this, title, QFileInfo( oldPath ).absoluteFilePath(), mFilter, &mSelectedFilter, mOptions );
319  break;
320  case GetMultipleFiles:
321  title = !mDialogTitle.isEmpty() ? mDialogTitle : tr( "Select one or more files" );
322  fileNames = QFileDialog::getOpenFileNames( this, title, QFileInfo( oldPath ).absoluteFilePath(), mFilter, &mSelectedFilter, mOptions );
323  break;
324  case GetDirectory:
325  title = !mDialogTitle.isEmpty() ? mDialogTitle : tr( "Select a directory" );
326  fileName = QFileDialog::getExistingDirectory( this, title, QFileInfo( oldPath ).absoluteFilePath(), mOptions | QFileDialog::ShowDirsOnly );
327  break;
328  case SaveFile:
329  {
330  title = !mDialogTitle.isEmpty() ? mDialogTitle : tr( "Create or select a file" );
331  if ( !confirmOverwrite() )
332  {
333  fileName = QFileDialog::getSaveFileName( this, title, QFileInfo( oldPath ).absoluteFilePath(), mFilter, &mSelectedFilter, mOptions | QFileDialog::DontConfirmOverwrite );
334  }
335  else
336  {
337  fileName = QFileDialog::getSaveFileName( this, title, QFileInfo( oldPath ).absoluteFilePath(), mFilter, &mSelectedFilter, mOptions );
338  }
339 
340  // make sure filename ends with filter. This isn't automatically done by
341  // getSaveFileName on some platforms (e.g. gnome)
343  }
344  break;
345  }
346  }
347 
348  if ( fileName.isEmpty() && fileNames.isEmpty( ) )
349  return;
350 
352  fileNames << fileName;
353 
354  for ( int i = 0; i < fileNames.length(); i++ )
355  {
356  fileNames.replace( i, QDir::toNativeSeparators( QDir::cleanPath( QFileInfo( fileNames.at( i ) ).absoluteFilePath() ) ) );
357  }
358 
359  // Store the last used path:
360  switch ( mStorageMode )
361  {
362  case GetFile:
363  case SaveFile:
364  case GetMultipleFiles:
365  settings.setValue( QStringLiteral( "UI/lastFileNameWidgetDir" ), QFileInfo( fileNames.first() ).absolutePath() );
366  break;
367  case GetDirectory:
368  settings.setValue( QStringLiteral( "UI/lastFileNameWidgetDir" ), fileNames.first() );
369  break;
370  }
371 
372  setSelectedFileNames( fileNames );
373 }
374 
375 void QgsFileWidget::setSelectedFileNames( QStringList fileNames )
376 {
377  Q_ASSERT( fileNames.count() );
378 
379  // Handle relative Path storage
380  for ( int i = 0; i < fileNames.length(); i++ )
381  {
382  fileNames.replace( i, relativePath( fileNames.at( i ), true ) );
383  }
384 
385  setFilePaths( fileNames );
386 }
387 
388 void QgsFileWidget::setFilePaths( const QStringList &filePaths )
389 {
391  {
392  setFilePath( filePaths.first() );
393  }
394  else
395  {
396  if ( filePaths.length() > 1 )
397  {
398  setFilePath( QStringLiteral( "\"%1\"" ).arg( filePaths.join( QLatin1String( "\" \"" ) ) ) );
399  }
400  else
401  {
402  setFilePath( filePaths.first( ) );
403  }
404  }
405 }
406 
407 QString QgsFileWidget::relativePath( const QString &filePath, bool removeRelative ) const
408 {
409  QString RelativePath;
411  {
412  RelativePath = QDir::toNativeSeparators( QDir::cleanPath( QFileInfo( QgsProject::instance()->absoluteFilePath() ).path() ) );
413  }
414  else if ( mRelativeStorage == RelativeDefaultPath && !mDefaultRoot.isEmpty() )
415  {
416  RelativePath = QDir::toNativeSeparators( QDir::cleanPath( mDefaultRoot ) );
417  }
418 
419  if ( !RelativePath.isEmpty() )
420  {
421  if ( removeRelative )
422  {
423  return QDir::cleanPath( QDir( RelativePath ).relativeFilePath( filePath ) );
424  }
425  else
426  {
427  return QDir::cleanPath( QDir( RelativePath ).filePath( filePath ) );
428  }
429  }
430 
431  return filePath;
432 }
433 
434 
435 QString QgsFileWidget::toUrl( const QString &path ) const
436 {
437  QString rep;
438  if ( path.isEmpty() )
439  {
441  }
442 
443  if ( isMultiFiles( path ) )
444  {
445  return QStringLiteral( "<a>%1</a>" ).arg( path );
446  }
447 
448  QString urlStr = relativePath( path, false );
449  QUrl url = QUrl::fromUserInput( urlStr );
450  if ( !url.isValid() || !url.isLocalFile() )
451  {
452  QgsDebugMsgLevel( QStringLiteral( "URL: %1 is not valid or not a local file!" ).arg( path ), 2 );
453  rep = path;
454  }
455 
456  QString pathStr = url.toString();
457  if ( mFullUrl )
458  {
459  rep = QStringLiteral( "<a href=\"%1\">%2</a>" ).arg( pathStr, path );
460  }
461  else
462  {
463  QString fileName = QFileInfo( urlStr ).fileName();
464  rep = QStringLiteral( "<a href=\"%1\">%2</a>" ).arg( pathStr, fileName );
465  }
466 
467  return rep;
468 }
469 
470 
472 
473 
474 QgsFileDropEdit::QgsFileDropEdit( QWidget *parent )
475  : QgsHighlightableLineEdit( parent )
476 {
477  setAcceptDrops( true );
478 }
479 
480 void QgsFileDropEdit::setFilters( const QString &filters )
481 {
482  mAcceptableExtensions.clear();
483 
484  if ( filters.contains( QStringLiteral( "*.*" ) ) )
485  return; // everything is allowed!
486 
487  QRegularExpression rx( QStringLiteral( "\\*\\.(\\w+)" ) );
488  QRegularExpressionMatchIterator i = rx.globalMatch( filters );
489  while ( i.hasNext() )
490  {
491  QRegularExpressionMatch match = i.next();
492  if ( match.hasMatch() )
493  {
494  mAcceptableExtensions << match.captured( 1 ).toLower();
495  }
496  }
497 }
498 
499 QStringList QgsFileDropEdit::acceptableFilePaths( QDropEvent *event ) const
500 {
501  QStringList rawPaths;
502  QStringList paths;
503  if ( event->mimeData()->hasUrls() )
504  {
505  const QList< QUrl > urls = event->mimeData()->urls();
506  rawPaths.reserve( urls.count() );
507  for ( const QUrl &url : urls )
508  {
509  const QString local = url.toLocalFile();
510  if ( !rawPaths.contains( local ) )
511  rawPaths.append( local );
512  }
513  }
514 
516  for ( const QgsMimeDataUtils::Uri &u : std::as_const( lst ) )
517  {
518  if ( !rawPaths.contains( u.uri ) )
519  rawPaths.append( u.uri );
520  }
521 
522  if ( !event->mimeData()->text().isEmpty() && !rawPaths.contains( event->mimeData()->text() ) )
523  rawPaths.append( event->mimeData()->text() );
524 
525  paths.reserve( rawPaths.count() );
526  for ( const QString &path : std::as_const( rawPaths ) )
527  {
528  QFileInfo file( path );
529  switch ( mStorageMode )
530  {
534  {
535  if ( file.isFile() && ( mAcceptableExtensions.isEmpty() || mAcceptableExtensions.contains( file.suffix(), Qt::CaseInsensitive ) ) )
536  paths.append( file.filePath() );
537 
538  break;
539  }
540 
542  {
543  if ( file.isDir() )
544  paths.append( file.filePath() );
545  else if ( file.isFile() )
546  {
547  // folder mode, but a file dropped. So get folder name from file
548  paths.append( file.absolutePath() );
549  }
550 
551  break;
552  }
553  }
554  }
555 
556  return paths;
557 }
558 
559 QString QgsFileDropEdit::acceptableFilePath( QDropEvent *event ) const
560 {
561  const QStringList paths = acceptableFilePaths( event );
562  if ( paths.size() > 1 )
563  {
564  return QStringLiteral( "\"%1\"" ).arg( paths.join( QLatin1String( "\" \"" ) ) );
565  }
566  else if ( paths.size() == 1 )
567  {
568  return paths.first();
569  }
570  else
571  {
572  return QString();
573  }
574 }
575 
576 void QgsFileDropEdit::dragEnterEvent( QDragEnterEvent *event )
577 {
578  QString filePath = acceptableFilePath( event );
579  if ( !filePath.isEmpty() )
580  {
581  event->acceptProposedAction();
582  setHighlighted( true );
583  }
584  else
585  {
586  event->ignore();
587  }
588 }
589 
590 void QgsFileDropEdit::dragLeaveEvent( QDragLeaveEvent *event )
591 {
592  QgsFilterLineEdit::dragLeaveEvent( event );
593  event->accept();
594  setHighlighted( false );
595 }
596 
597 void QgsFileDropEdit::dropEvent( QDropEvent *event )
598 {
599  QString filePath = acceptableFilePath( event );
600  if ( !filePath.isEmpty() )
601  {
602  event->acceptProposedAction();
603  emit fileDropped( filePath );
604  }
605 
606  setHighlighted( false );
607 }
608 
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:467
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