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