QGIS API Documentation  3.18.1-Zürich (202f1bf7e5)
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  const QStringList pathParts = path.split( QRegExp( "\"\\s+\"" ), QString::SkipEmptyParts );
85  for ( const auto &pathsPart : pathParts )
86  {
87  QString cleaned = pathsPart;
88  cleaned.remove( QRegExp( "(^\\s*\")|(\"\\s*)" ) );
89  paths.append( cleaned );
90  }
91  return paths;
92 }
93 
94 void QgsFileWidget::setFilePath( QString path )
95 {
96  //will trigger textEdited slot
97  mLineEdit->setValue( path );
98 }
99 
100 void QgsFileWidget::setReadOnly( bool readOnly )
101 {
102  if ( mReadOnly == readOnly )
103  return;
104 
105  mReadOnly = readOnly;
106 
107  updateLayout();
108 }
109 
111 {
112  return mDialogTitle;
113 }
114 
115 void QgsFileWidget::setDialogTitle( const QString &title )
116 {
117  mDialogTitle = title;
118 }
119 
120 QString QgsFileWidget::filter() const
121 {
122  return mFilter;
123 }
124 
125 void QgsFileWidget::setFilter( const QString &filters )
126 {
127  mFilter = filters;
128  mLineEdit->setFilters( filters );
129 }
130 
131 QFileDialog::Options QgsFileWidget::options() const
132 {
133  return mOptions;
134 }
135 
136 void QgsFileWidget::setOptions( QFileDialog::Options options )
137 {
138  mOptions = options;
139 }
140 
142 {
143  return mButtonVisible;
144 }
145 
147 {
148  mButtonVisible = visible;
149  mFileWidgetButton->setVisible( visible );
150 }
151 
152 void QgsFileWidget::textEdited( const QString &path )
153 {
154  mFilePath = path;
155  mLinkLabel->setText( toUrl( path ) );
156  // Show tooltip if multiple files are selected
157  if ( path.contains( QStringLiteral( "\" \"" ) ) )
158  {
159  mLineEdit->setToolTip( tr( "Selected files:<br><ul><li>%1</li></ul><br>" ).arg( splitFilePaths( path ).join( QLatin1String( "</li><li>" ) ) ) );
160  }
161  else
162  {
163  mLineEdit->setToolTip( QString() );
164  }
165  emit fileChanged( mFilePath );
166 }
167 
168 void QgsFileWidget::editLink()
169 {
170  if ( !mUseLink || mReadOnly )
171  return;
172 
173  mIsLinkEdited = !mIsLinkEdited;
174  updateLayout();
175 }
176 
178 {
179  return mUseLink;
180 }
181 
182 void QgsFileWidget::setUseLink( bool useLink )
183 {
184  if ( mUseLink == useLink )
185  return;
186 
187  mUseLink = useLink;
188  updateLayout();
189 }
190 
192 {
193  return mFullUrl;
194 }
195 
196 void QgsFileWidget::setFullUrl( bool fullUrl )
197 {
198  mFullUrl = fullUrl;
199 }
200 
202 {
203  return mDefaultRoot;
204 }
205 
206 void QgsFileWidget::setDefaultRoot( const QString &defaultRoot )
207 {
208  mDefaultRoot = defaultRoot;
209 }
210 
212 {
213  return mStorageMode;
214 }
215 
217 {
218  mStorageMode = storageMode;
219  mLineEdit->setStorageMode( storageMode );
220 }
221 
223 {
224  return mRelativeStorage;
225 }
226 
228 {
229  mRelativeStorage = relativeStorage;
230 }
231 
233 {
234  return mLineEdit;
235 }
236 
237 void QgsFileWidget::updateLayout()
238 {
239  mLayout->removeWidget( mLineEdit );
240  mLayout->removeWidget( mLinkLabel );
241  mLayout->removeWidget( mLinkEditButton );
242 
243  mLinkEditButton->setVisible( mUseLink && !mReadOnly );
244 
245  mFileWidgetButton->setEnabled( !mReadOnly );
246  mLineEdit->setEnabled( !mReadOnly );
247 
248  if ( mUseLink && !mIsLinkEdited )
249  {
250  mLayout->insertWidget( 0, mLinkLabel );
251  mLineEdit->setVisible( false );
252  mLinkLabel->setVisible( true );
253 
254  if ( !mReadOnly )
255  {
256  mLayout->insertWidget( 1, mLinkEditButton );
257  mLinkEditButton->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "/mActionToggleEditing.svg" ) ) );
258  }
259  }
260  else
261  {
262  mLayout->insertWidget( 0, mLineEdit );
263  mLineEdit->setVisible( true );
264  mLinkLabel->setVisible( false );
265 
266  if ( mIsLinkEdited )
267  {
268  mLayout->insertWidget( 1, mLinkEditButton );
269  mLinkEditButton->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "/mActionSaveEdits.svg" ) ) );
270  }
271  }
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() )
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 | QFileDialog::ShowDirsOnly );
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)
339  fileName = QgsFileUtils::addExtensionFromFilter( fileName, mSelectedFilter );
340  }
341  break;
342  }
343  }
344 
345  if ( fileName.isEmpty() && fileNames.isEmpty( ) )
346  return;
347 
348  if ( mStorageMode != GetMultipleFiles )
349  {
350  fileName = QDir::toNativeSeparators( QDir::cleanPath( QFileInfo( fileName ).absoluteFilePath() ) );
351  }
352  else
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 
360  // Store the last used path:
361  switch ( mStorageMode )
362  {
363  case GetFile:
364  case SaveFile:
365  settings.setValue( QStringLiteral( "UI/lastFileNameWidgetDir" ), QFileInfo( fileName ).absolutePath() );
366  break;
367  case GetDirectory:
368  settings.setValue( QStringLiteral( "UI/lastFileNameWidgetDir" ), fileName );
369  break;
370  case GetMultipleFiles:
371  settings.setValue( QStringLiteral( "UI/lastFileNameWidgetDir" ), QFileInfo( fileNames.first( ) ).absolutePath() );
372  break;
373  }
374 
375  // Handle relative Path storage
376  if ( mStorageMode != GetMultipleFiles )
377  {
378  fileName = relativePath( fileName, true );
379  setFilePath( fileName );
380  }
381  else
382  {
383  for ( int i = 0; i < fileNames.length(); i++ )
384  {
385  fileNames.replace( i, relativePath( fileNames.at( i ), true ) );
386  }
387  if ( fileNames.length() > 1 )
388  {
389  setFilePath( QStringLiteral( "\"%1\"" ).arg( fileNames.join( QLatin1String( "\" \"" ) ) ) );
390  }
391  else
392  {
393  setFilePath( fileNames.first( ) );
394  }
395  }
396 }
397 
398 
399 QString QgsFileWidget::relativePath( const QString &filePath, bool removeRelative ) const
400 {
401  QString RelativePath;
402  if ( mRelativeStorage == RelativeProject )
403  {
404  RelativePath = QDir::toNativeSeparators( QDir::cleanPath( QFileInfo( QgsProject::instance()->absoluteFilePath() ).path() ) );
405  }
406  else if ( mRelativeStorage == RelativeDefaultPath && !mDefaultRoot.isEmpty() )
407  {
408  RelativePath = QDir::toNativeSeparators( QDir::cleanPath( mDefaultRoot ) );
409  }
410 
411  if ( !RelativePath.isEmpty() )
412  {
413  if ( removeRelative )
414  {
415  return QDir::cleanPath( QDir( RelativePath ).relativeFilePath( filePath ) );
416  }
417  else
418  {
419  return QDir::cleanPath( QDir( RelativePath ).filePath( filePath ) );
420  }
421  }
422 
423  return filePath;
424 }
425 
426 
427 QString QgsFileWidget::toUrl( const QString &path ) const
428 {
429  QString rep;
430  if ( path.isEmpty() )
431  {
433  }
434 
435  QString urlStr = relativePath( path, false );
436  QUrl url = QUrl::fromUserInput( urlStr );
437  if ( !url.isValid() || !url.isLocalFile() )
438  {
439  QgsDebugMsgLevel( QStringLiteral( "URL: %1 is not valid or not a local file!" ).arg( path ), 2 );
440  rep = path;
441  }
442 
443  QString pathStr = url.toString();
444  if ( mFullUrl )
445  {
446  rep = QStringLiteral( "<a href=\"%1\">%2</a>" ).arg( pathStr, path );
447  }
448  else
449  {
450  QString fileName = QFileInfo( urlStr ).fileName();
451  rep = QStringLiteral( "<a href=\"%1\">%2</a>" ).arg( pathStr, fileName );
452  }
453 
454  return rep;
455 }
456 
457 
458 
460 
461 
462 QgsFileDropEdit::QgsFileDropEdit( QWidget *parent )
463  : QgsHighlightableLineEdit( parent )
464 {
465  setAcceptDrops( true );
466 }
467 
468 void QgsFileDropEdit::setFilters( const QString &filters )
469 {
470  mAcceptableExtensions.clear();
471 
472  if ( filters.contains( QStringLiteral( "*.*" ) ) )
473  return; // everything is allowed!
474 
475  QRegularExpression rx( QStringLiteral( "\\*\\.(\\w+)" ) );
476  QRegularExpressionMatchIterator i = rx.globalMatch( filters );
477  while ( i.hasNext() )
478  {
479  QRegularExpressionMatch match = i.next();
480  if ( match.hasMatch() )
481  {
482  mAcceptableExtensions << match.captured( 1 ).toLower();
483  }
484  }
485 }
486 
487 QString QgsFileDropEdit::acceptableFilePath( QDropEvent *event ) const
488 {
489  QStringList rawPaths;
490  QStringList paths;
491  if ( event->mimeData()->hasUrls() )
492  {
493  const QList< QUrl > urls = event->mimeData()->urls();
494  rawPaths.reserve( urls.count() );
495  for ( const QUrl &url : urls )
496  {
497  const QString local = url.toLocalFile();
498  if ( !rawPaths.contains( local ) )
499  rawPaths.append( local );
500  }
501  }
502 
504  for ( const QgsMimeDataUtils::Uri &u : qgis::as_const( lst ) )
505  {
506  if ( !rawPaths.contains( u.uri ) )
507  rawPaths.append( u.uri );
508  }
509 
510  if ( !event->mimeData()->text().isEmpty() && !rawPaths.contains( event->mimeData()->text() ) )
511  rawPaths.append( event->mimeData()->text() );
512 
513  paths.reserve( rawPaths.count() );
514  for ( const QString &path : qgis::as_const( rawPaths ) )
515  {
516  QFileInfo file( path );
517  switch ( mStorageMode )
518  {
522  {
523  if ( file.isFile() && ( mAcceptableExtensions.isEmpty() || mAcceptableExtensions.contains( file.suffix(), Qt::CaseInsensitive ) ) )
524  paths.append( file.filePath() );
525 
526  break;
527  }
528 
530  {
531  if ( file.isDir() )
532  paths.append( file.filePath() );
533  else if ( file.isFile() )
534  {
535  // folder mode, but a file dropped. So get folder name from file
536  paths.append( file.absolutePath() );
537  }
538 
539  break;
540  }
541  }
542  }
543 
544  if ( paths.size() > 1 )
545  {
546  return QStringLiteral( "\"%1\"" ).arg( paths.join( QLatin1String( "\" \"" ) ) );
547  }
548  else if ( paths.size() == 1 )
549  {
550  return paths.first();
551  }
552  else
553  {
554  return QString();
555  }
556 }
557 
558 void QgsFileDropEdit::dragEnterEvent( QDragEnterEvent *event )
559 {
560  QString filePath = acceptableFilePath( event );
561  if ( !filePath.isEmpty() )
562  {
563  event->acceptProposedAction();
564  setHighlighted( true );
565  }
566  else
567  {
568  event->ignore();
569  }
570 }
571 
572 void QgsFileDropEdit::dragLeaveEvent( QDragLeaveEvent *event )
573 {
574  QgsFilterLineEdit::dragLeaveEvent( event );
575  event->accept();
576  setHighlighted( false );
577 }
578 
579 void QgsFileDropEdit::dropEvent( QDropEvent *event )
580 {
581  QString filePath = acceptableFilePath( event );
582  if ( !filePath.isEmpty() )
583  {
584  setText( filePath );
585  selectAll();
586  setFocus( Qt::MouseFocusReason );
587  event->acceptProposedAction();
588  setHighlighted( false );
589  }
590 }
591 
static QString nullRepresentation()
This string is used to represent the value NULL throughout QGIS.
static QIcon getThemeIcon(const QString &name)
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:501
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