QGIS API Documentation  3.6.0-Noosa (5873452)
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 <QFileDialog>
23 #include <QGridLayout>
24 #include <QUrl>
25 #include <QDropEvent>
26 
27 #include "qgssettings.h"
28 #include "qgsfilterlineedit.h"
29 #include "qgslogger.h"
30 #include "qgsproject.h"
31 #include "qgsapplication.h"
32 #include "qgsfileutils.h"
33 #include "qgsmimedatautils.h"
34 
35 QgsFileWidget::QgsFileWidget( QWidget *parent )
36  : QWidget( parent )
37 {
38  setBackgroundRole( QPalette::Window );
39  setAutoFillBackground( true );
40 
41  mLayout = new QHBoxLayout();
42  mLayout->setMargin( 0 );
43 
44  // If displaying a hyperlink, use a QLabel
45  mLinkLabel = new QLabel( this );
46  // Make Qt opens the link with the OS defined viewer
47  mLinkLabel->setOpenExternalLinks( true );
48  // Label should always be enabled to be able to open
49  // the link on read only mode.
50  mLinkLabel->setEnabled( true );
51  mLinkLabel->setSizePolicy( QSizePolicy::Expanding, QSizePolicy::Preferred );
52  mLinkLabel->setTextFormat( Qt::RichText );
53  mLinkLabel->hide(); // do not show by default
54 
55  // otherwise, use the traditional QLineEdit subclass
56  mLineEdit = new QgsFileDropEdit( this );
57  mLineEdit->setDragEnabled( true );
58  connect( mLineEdit, &QLineEdit::textChanged, this, &QgsFileWidget::textEdited );
59  mLayout->addWidget( mLineEdit );
60 
61  mFileWidgetButton = new QToolButton( this );
62  mFileWidgetButton->setText( QChar( 0x2026 ) );
63  connect( mFileWidgetButton, &QAbstractButton::clicked, this, &QgsFileWidget::openFileDialog );
64  mLayout->addWidget( mFileWidgetButton );
65 
66  setLayout( mLayout );
67 }
68 
70 {
71  return mFilePath;
72 }
73 
74 QStringList QgsFileWidget::splitFilePaths( const QString &path )
75 {
76  QStringList paths;
77  const QStringList pathParts = path.split( QRegExp( "\"\\s+\"" ), QString::SkipEmptyParts );
78  for ( const auto &pathsPart : pathParts )
79  {
80  QString cleaned = pathsPart;
81  cleaned.remove( QRegExp( "(^\\s*\")|(\"\\s*)" ) );
82  paths.append( cleaned );
83  }
84  return paths;
85 }
86 
87 void QgsFileWidget::setFilePath( QString path )
88 {
89  if ( path == QgsApplication::nullRepresentation() )
90  {
91  path.clear();
92  }
93 
94  //will trigger textEdited slot
95  mLineEdit->setValue( path );
96 
97 }
98 
99 void QgsFileWidget::setReadOnly( bool readOnly )
100 {
101  mFileWidgetButton->setEnabled( !readOnly );
102  mLineEdit->setEnabled( !readOnly );
103 }
104 
105 QString QgsFileWidget::dialogTitle() const
106 {
107  return mDialogTitle;
108 }
109 
110 void QgsFileWidget::setDialogTitle( const QString &title )
111 {
112  mDialogTitle = title;
113 }
114 
115 QString QgsFileWidget::filter() const
116 {
117  return mFilter;
118 }
119 
120 void QgsFileWidget::setFilter( const QString &filters )
121 {
122  mFilter = filters;
123  mLineEdit->setFilters( filters );
124 }
125 
127 {
128  return mButtonVisible;
129 }
130 
132 {
133  mButtonVisible = visible;
134  mFileWidgetButton->setVisible( visible );
135 }
136 
137 void QgsFileWidget::textEdited( const QString &path )
138 {
139  mFilePath = path;
140  mLinkLabel->setText( toUrl( path ) );
141  // Show tooltip if multiple files are selected
142  if ( path.contains( QStringLiteral( "\" \"" ) ) )
143  {
144  mLineEdit->setToolTip( tr( "Selected files:<br><ul><li>%1</li></ul><br>" ).arg( splitFilePaths( path ).join( QStringLiteral( "</li><li>" ) ) ) );
145  }
146  else
147  {
148  mLineEdit->setToolTip( QString() );
149  }
150  emit fileChanged( mFilePath );
151 }
152 
153 bool QgsFileWidget::useLink() const
154 {
155  return mUseLink;
156 }
157 
159 {
160  mUseLink = useLink;
161  mLinkLabel->setVisible( mUseLink );
162  mLineEdit->setVisible( !mUseLink );
163  if ( mUseLink )
164  {
165  mLayout->removeWidget( mLineEdit );
166  mLayout->insertWidget( 0, mLinkLabel );
167  }
168  else
169  {
170  mLayout->removeWidget( mLinkLabel );
171  mLayout->insertWidget( 0, mLineEdit );
172  }
173 }
174 
175 bool QgsFileWidget::fullUrl() const
176 {
177  return mFullUrl;
178 }
179 
181 {
182  mFullUrl = fullUrl;
183 }
184 
185 QString QgsFileWidget::defaultRoot() const
186 {
187  return mDefaultRoot;
188 }
189 
191 {
192  mDefaultRoot = defaultRoot;
193 }
194 
196 {
197  return mStorageMode;
198 }
199 
201 {
202  mStorageMode = storageMode;
203  mLineEdit->setStorageMode( storageMode );
204 }
205 
207 {
208  return mRelativeStorage;
209 }
210 
212 {
213  mRelativeStorage = relativeStorage;
214 }
215 
217 {
218  return mLineEdit;
219 }
220 
221 void QgsFileWidget::openFileDialog()
222 {
223  QgsSettings settings;
224  QString oldPath;
225 
226  // if we use a relative path option, we need to obtain the full path
227  // first choice is the current file path, if one is entered
228  if ( !mFilePath.isEmpty() )
229  {
230  oldPath = relativePath( mFilePath, false );
231  }
232  // If we use fixed default path
233  // second choice is the default root
234  else if ( !mDefaultRoot.isEmpty() )
235  {
236  oldPath = QDir::cleanPath( mDefaultRoot );
237  }
238 
239  // If there is no valid value, find a default path to use
240  QUrl url = QUrl::fromUserInput( oldPath );
241  if ( !url.isValid() )
242  {
243  QString defPath = QDir::cleanPath( QFileInfo( QgsProject::instance()->absoluteFilePath() ).path() );
244  if ( defPath.isEmpty() )
245  {
246  defPath = QDir::homePath();
247  }
248  oldPath = settings.value( QStringLiteral( "UI/lastFileNameWidgetDir" ), defPath ).toString();
249  }
250 
251  // Handle Storage
252  QString fileName;
253  QStringList fileNames;
254  QString title;
255 
256  switch ( mStorageMode )
257  {
258  case GetFile:
259  title = !mDialogTitle.isEmpty() ? mDialogTitle : tr( "Select a file" );
260  fileName = QFileDialog::getOpenFileName( this, title, QFileInfo( oldPath ).absoluteFilePath(), mFilter, &mSelectedFilter );
261  break;
262  case GetMultipleFiles:
263  title = !mDialogTitle.isEmpty() ? mDialogTitle : tr( "Select one or more files" );
264  fileNames = QFileDialog::getOpenFileNames( this, title, QFileInfo( oldPath ).absoluteFilePath(), mFilter, &mSelectedFilter );
265  break;
266  case GetDirectory:
267  title = !mDialogTitle.isEmpty() ? mDialogTitle : tr( "Select a directory" );
268  fileName = QFileDialog::getExistingDirectory( this, title, QFileInfo( oldPath ).absoluteFilePath(), QFileDialog::ShowDirsOnly );
269  break;
270  case SaveFile:
271  {
272  title = !mDialogTitle.isEmpty() ? mDialogTitle : tr( "Create or select a file" );
273  if ( !confirmOverwrite() )
274  {
275  fileName = QFileDialog::getSaveFileName( this, title, QFileInfo( oldPath ).absoluteFilePath(), mFilter, &mSelectedFilter, QFileDialog::DontConfirmOverwrite );
276  }
277  else
278  {
279  fileName = QFileDialog::getSaveFileName( this, title, QFileInfo( oldPath ).absoluteFilePath(), mFilter, &mSelectedFilter );
280  }
281 
282  // make sure filename ends with filter. This isn't automatically done by
283  // getSaveFileName on some platforms (e.g. gnome)
284  fileName = QgsFileUtils::addExtensionFromFilter( fileName, mSelectedFilter );
285  }
286  break;
287  }
288 
289  if ( fileName.isEmpty() && fileNames.isEmpty( ) )
290  return;
291 
292  if ( mStorageMode != GetMultipleFiles )
293  {
294  fileName = QDir::toNativeSeparators( QDir::cleanPath( QFileInfo( fileName ).absoluteFilePath() ) );
295  }
296  else
297  {
298  for ( int i = 0; i < fileNames.length(); i++ )
299  {
300  fileNames.replace( i, QDir::toNativeSeparators( QDir::cleanPath( QFileInfo( fileNames.at( i ) ).absoluteFilePath() ) ) );
301  }
302  }
303 
304  // Store the last used path:
305  switch ( mStorageMode )
306  {
307  case GetFile:
308  case SaveFile:
309  settings.setValue( QStringLiteral( "UI/lastFileNameWidgetDir" ), QFileInfo( fileName ).absolutePath() );
310  break;
311  case GetDirectory:
312  settings.setValue( QStringLiteral( "UI/lastFileNameWidgetDir" ), fileName );
313  break;
314  case GetMultipleFiles:
315  settings.setValue( QStringLiteral( "UI/lastFileNameWidgetDir" ), QFileInfo( fileNames.first( ) ).absolutePath() );
316  break;
317  }
318 
319  // Handle relative Path storage
320  if ( mStorageMode != GetMultipleFiles )
321  {
322  fileName = relativePath( fileName, true );
323  setFilePath( fileName );
324  }
325  else
326  {
327  for ( int i = 0; i < fileNames.length(); i++ )
328  {
329  fileNames.replace( i, relativePath( fileNames.at( i ), true ) );
330  }
331  if ( fileNames.length() > 1 )
332  {
333  setFilePath( QStringLiteral( "\"%1\"" ).arg( fileNames.join( QStringLiteral( "\" \"" ) ) ) );
334  }
335  else
336  {
337  setFilePath( fileNames.first( ) );
338  }
339  }
340 }
341 
342 
343 QString QgsFileWidget::relativePath( const QString &filePath, bool removeRelative ) const
344 {
345  QString RelativePath;
346  if ( mRelativeStorage == RelativeProject )
347  {
348  RelativePath = QDir::toNativeSeparators( QDir::cleanPath( QFileInfo( QgsProject::instance()->absoluteFilePath() ).path() ) );
349  }
350  else if ( mRelativeStorage == RelativeDefaultPath && !mDefaultRoot.isEmpty() )
351  {
352  RelativePath = QDir::toNativeSeparators( QDir::cleanPath( mDefaultRoot ) );
353  }
354 
355  if ( !RelativePath.isEmpty() )
356  {
357  if ( removeRelative )
358  {
359  return QDir::cleanPath( QDir( RelativePath ).relativeFilePath( filePath ) );
360  }
361  else
362  {
363  return QDir::cleanPath( QDir( RelativePath ).filePath( filePath ) );
364  }
365  }
366 
367  return filePath;
368 }
369 
370 
371 QString QgsFileWidget::toUrl( const QString &path ) const
372 {
373  QString rep;
374  if ( path.isEmpty() )
375  {
377  }
378 
379  QString urlStr = relativePath( path, false );
380  QUrl url = QUrl::fromUserInput( urlStr );
381  if ( !url.isValid() || !url.isLocalFile() )
382  {
383  QgsDebugMsg( QStringLiteral( "URL: %1 is not valid or not a local file!" ).arg( path ) );
384  rep = path;
385  }
386 
387  QString pathStr = url.toString();
388  if ( mFullUrl )
389  {
390  rep = QStringLiteral( "<a href=\"%1\">%2</a>" ).arg( pathStr, path );
391  }
392  else
393  {
394  QString fileName = QFileInfo( urlStr ).fileName();
395  rep = QStringLiteral( "<a href=\"%1\">%2</a>" ).arg( pathStr, fileName );
396  }
397 
398  return rep;
399 }
400 
401 
402 
404 
405 
406 QgsFileDropEdit::QgsFileDropEdit( QWidget *parent )
407  : QgsFilterLineEdit( parent )
408 {
409  mDragActive = false;
410  setAcceptDrops( true );
411 }
412 
413 void QgsFileDropEdit::setFilters( const QString &filters )
414 {
415  mAcceptableExtensions.clear();
416 
417  if ( filters.contains( QStringLiteral( "*.*" ) ) )
418  return; // everything is allowed!
419 
420  QRegularExpression rx( QStringLiteral( "\\*\\.(\\w+)" ) );
421  QRegularExpressionMatchIterator i = rx.globalMatch( filters );
422  while ( i.hasNext() )
423  {
424  QRegularExpressionMatch match = i.next();
425  if ( match.hasMatch() )
426  {
427  mAcceptableExtensions << match.captured( 1 ).toLower();
428  }
429  }
430 }
431 
432 QString QgsFileDropEdit::acceptableFilePath( QDropEvent *event ) const
433 {
434  QStringList rawPaths;
435  QStringList paths;
436  if ( event->mimeData()->hasUrls() )
437  {
438  const QList< QUrl > urls = event->mimeData()->urls();
439  rawPaths.reserve( urls.count() );
440  for ( const QUrl &url : urls )
441  {
442  const QString local = url.toLocalFile();
443  if ( !rawPaths.contains( local ) )
444  rawPaths.append( local );
445  }
446  }
447 
449  for ( const QgsMimeDataUtils::Uri &u : lst )
450  {
451  if ( !rawPaths.contains( u.uri ) )
452  rawPaths.append( u.uri );
453  }
454 
455  if ( !event->mimeData()->text().isEmpty() && !rawPaths.contains( event->mimeData()->text() ) )
456  rawPaths.append( event->mimeData()->text() );
457 
458  paths.reserve( rawPaths.count() );
459  for ( const QString &path : qgis::as_const( rawPaths ) )
460  {
461  QFileInfo file( path );
462  switch ( mStorageMode )
463  {
467  {
468  if ( file.isFile() && ( mAcceptableExtensions.isEmpty() || mAcceptableExtensions.contains( file.suffix(), Qt::CaseInsensitive ) ) )
469  paths.append( file.filePath() );
470 
471  break;
472  }
473 
475  {
476  if ( file.isDir() )
477  paths.append( file.filePath() );
478  else if ( file.isFile() )
479  {
480  // folder mode, but a file dropped. So get folder name from file
481  paths.append( file.absolutePath() );
482  }
483 
484  break;
485  }
486  }
487  }
488 
489  if ( paths.size() > 1 )
490  {
491  return QStringLiteral( "\"%1\"" ).arg( paths.join( QStringLiteral( "\" \"" ) ) );
492  }
493  else if ( paths.size() == 1 )
494  {
495  return paths.first();
496  }
497  else
498  {
499  return QString();
500  }
501 }
502 
503 void QgsFileDropEdit::dragEnterEvent( QDragEnterEvent *event )
504 {
505  QString filePath = acceptableFilePath( event );
506  if ( !filePath.isEmpty() )
507  {
508  event->acceptProposedAction();
509  mDragActive = true;
510  update();
511  }
512  else
513  {
514  event->ignore();
515  }
516 }
517 
518 void QgsFileDropEdit::dragLeaveEvent( QDragLeaveEvent *event )
519 {
520  QgsFilterLineEdit::dragLeaveEvent( event );
521  event->accept();
522  mDragActive = false;
523  update();
524 }
525 
526 void QgsFileDropEdit::dropEvent( QDropEvent *event )
527 {
528  QString filePath = acceptableFilePath( event );
529  if ( !filePath.isEmpty() )
530  {
531  setText( filePath );
532  selectAll();
533  setFocus( Qt::MouseFocusReason );
534  event->acceptProposedAction();
535  mDragActive = false;
536  update();
537  }
538 }
539 
540 void QgsFileDropEdit::paintEvent( QPaintEvent *e )
541 {
542  QgsFilterLineEdit::paintEvent( e );
543  if ( mDragActive )
544  {
545  QPainter p( this );
546  int width = 2; // width of highlight rectangle inside frame
547  p.setPen( QPen( palette().highlight(), width ) );
548  QRect r = rect().adjusted( width, width, -width, -width );
549  p.drawRect( r );
550  }
551 }
552 
void setDefaultRoot(const QString &defaultRoot)
determines the default root path used as the first shown location when picking a file and used if the...
bool useLink() const
determines if the file path will be shown as a link
QgsFileWidget::StorageMode storageMode() const
returns the storage mode (i.e. file or directory)
void setFilter(const QString &filter)
setFilter sets the filter used by the model to filters.
void fileChanged(const QString &)
emitted as soon as the current file or directory is changed
void setUseLink(bool useLink)
determines if the file path will be shown as a link
This class is a composition of two QSettings instances:
Definition: qgssettings.h:58
QVariant value(const QString &key, const QVariant &defaultValue=QVariant(), Section section=NoSection) const
Returns the value for setting key.
#define QgsDebugMsg(str)
Definition: qgslogger.h:38
Select a single file.
Definition: qgsfilewidget.h:66
static UriList decodeUriList(const QMimeData *data)
void setReadOnly(bool readOnly)
defines if the widget is readonly
void setStorageMode(QgsFileWidget::StorageMode storageMode)
determines the storage mode (i.e. file or directory)
QgsFilterLineEdit * lineEdit()
Returns a pointer to the widget&#39;s line edit, which can be used to customize the appearance and behavi...
bool confirmOverwrite() const
Returns whether a confirmation will be shown when overwriting an existing file.
QString filePath()
Returns the current file path(s) when multiple files are selected, they are quoted and separated by a...
QString defaultRoot() const
returns the default root path
QString dialogTitle() const
returns the open file dialog title
void setRelativeStorage(QgsFileWidget::RelativeStorage relativeStorage)
determines if the relative path is with respect to the project path or the default path ...
QgsFileWidget(QWidget *parent=nullptr)
QgsFileWidget creates a widget for selecting a file or a folder.
void setDialogTitle(const QString &title)
setDialogTitle defines the open file dialog title
QLineEdit subclass with built in support for clearing the widget&#39;s value and handling custom null val...
static QString nullRepresentation()
This string is used to represent the value NULL throughout QGIS.
void setFileWidgetButtonVisible(bool visible)
determines if the tool button is shown
StorageMode
The StorageMode enum determines if the file picker should pick files or directories.
Definition: qgsfilewidget.h:63
void setValue(const QString &key, const QVariant &value, QgsSettings::Section section=QgsSettings::NoSection)
Sets the value of setting key to value.
QgsFileWidget::RelativeStorage relativeStorage() const
returns if the relative path is with respect to the project path or the default path ...
static QgsProject * instance()
Returns the QgsProject singleton instance.
Definition: qgsproject.cpp:430
Select multiple files.
Definition: qgsfilewidget.h:68
QList< QgsMimeDataUtils::Uri > UriList
static QString addExtensionFromFilter(const QString &fileName, const QString &filter)
Ensures that a fileName ends with an extension from the specified filter string.
bool fileWidgetButtonVisible() const
determines if the tool button is shown
bool fullUrl() const
returns if the links shows the full path or not
QString filter() const
returns the filters used for QDialog::getOpenFileName
static QStringList splitFilePaths(const QString &path)
Split the the quoted and space separated path and returns a QString list.
void setFullUrl(bool fullUrl)
determines if the links shows the full path or not
void setFilePath(QString path)
Sets the file path.
RelativeStorage
The RelativeStorage enum determines if path is absolute, relative to the current project path or rela...
Definition: qgsfilewidget.h:74