QGIS API Documentation  2.99.0-Master (5b186ae)
qgssvgselectorwidget.cpp
Go to the documentation of this file.
1 /***************************************************************************
2  qgssvgselectorwidget.cpp - group and preview selector for SVG files
3  built off of work in qgssymbollayerwidget
4 
5  ---------------------
6  begin : April 2, 2013
7  copyright : (C) 2013 by Larry Shaffer
8  email : larrys at dakcarto dot com
9  ***************************************************************************
10  * *
11  * This program is free software; you can redistribute it and/or modify *
12  * it under the terms of the GNU General Public License as published by *
13  * the Free Software Foundation; either version 2 of the License, or *
14  * (at your option) any later version. *
15  * *
16  ***************************************************************************/
17 #include "qgssvgselectorwidget.h"
18 
19 #include "qgsapplication.h"
20 #include "qgslogger.h"
21 #include "qgspathresolver.h"
22 #include "qgsproject.h"
23 #include "qgssvgcache.h"
24 #include "qgssymbollayerutils.h"
25 #include "qgssettings.h"
26 
27 #include <QAbstractListModel>
28 #include <QCheckBox>
29 #include <QDir>
30 #include <QFileDialog>
31 #include <QModelIndex>
32 #include <QPixmapCache>
33 #include <QStyle>
34 #include <QTime>
35 
36 // QgsSvgSelectorLoader
37 
39 QgsSvgSelectorLoader::QgsSvgSelectorLoader( QObject *parent )
40  : QThread( parent )
41  , mCanceled( false )
42  , mTimerThreshold( 0 )
43 {
44 }
45 
46 QgsSvgSelectorLoader::~QgsSvgSelectorLoader()
47 {
48  stop();
49 }
50 
51 void QgsSvgSelectorLoader::run()
52 {
53  mCanceled = false;
54  mQueuedSvgs.clear();
55  mTraversedPaths.clear();
56 
57  // start with a small initial timeout (ms)
58  mTimerThreshold = 10;
59  mTimer.start();
60 
61  loadPath( mPath );
62 
63  if ( !mQueuedSvgs.isEmpty() )
64  {
65  // make sure we notify model of any remaining queued svgs (ie svgs added since last foundSvgs() signal was emitted)
66  emit foundSvgs( mQueuedSvgs );
67  }
68  mQueuedSvgs.clear();
69 }
70 
71 void QgsSvgSelectorLoader::stop()
72 {
73  mCanceled = true;
74  while ( isRunning() ) {}
75 }
76 
77 void QgsSvgSelectorLoader::loadPath( const QString &path )
78 {
79  if ( mCanceled )
80  return;
81 
82  // QgsDebugMsg( QString( "loading path: %1" ).arg( path ) );
83 
84  if ( path.isEmpty() )
85  {
86  QStringList svgPaths = QgsApplication::svgPaths();
87  Q_FOREACH ( const QString &svgPath, svgPaths )
88  {
89  if ( mCanceled )
90  return;
91 
92  if ( !svgPath.isEmpty() )
93  {
94  loadPath( svgPath );
95  }
96  }
97  }
98  else
99  {
100  QDir dir( path );
101 
102  //guard against circular symbolic links
103  QString canonicalPath = dir.canonicalPath();
104  if ( mTraversedPaths.contains( canonicalPath ) )
105  return;
106 
107  mTraversedPaths.insert( canonicalPath );
108 
109  loadImages( path );
110 
111  Q_FOREACH ( const QString &item, dir.entryList( QDir::Dirs | QDir::NoDotAndDotDot ) )
112  {
113  if ( mCanceled )
114  return;
115 
116  QString newPath = dir.path() + '/' + item;
117  loadPath( newPath );
118  // QgsDebugMsg( QString( "added path: %1" ).arg( newPath ) );
119  }
120  }
121 }
122 
123 void QgsSvgSelectorLoader::loadImages( const QString &path )
124 {
125  QDir dir( path );
126  Q_FOREACH ( const QString &item, dir.entryList( QStringList( "*.svg" ), QDir::Files ) )
127  {
128  if ( mCanceled )
129  return;
130 
131  // TODO test if it is correct SVG
132  QString svgPath = dir.path() + '/' + item;
133  // QgsDebugMsg( QString( "adding svg: %1" ).arg( svgPath ) );
134 
135  // add it to the list of queued SVGs
136  mQueuedSvgs << svgPath;
137 
138  // we need to avoid spamming the model with notifications about new svgs, so foundSvgs
139  // is only emitted for blocks of SVGs (otherwise the view goes all flickery)
140  if ( mTimer.elapsed() > mTimerThreshold && !mQueuedSvgs.isEmpty() )
141  {
142  emit foundSvgs( mQueuedSvgs );
143  mQueuedSvgs.clear();
144 
145  // increase the timer threshold - this ensures that the first lots of svgs loaded are added
146  // to the view quickly, but as the list grows new svgs are added at a slower rate.
147  // ie, good for initial responsiveness but avoid being spammy as the list grows.
148  if ( mTimerThreshold < 1000 )
149  mTimerThreshold *= 2;
150  mTimer.restart();
151  }
152  }
153 }
154 
155 
156 //
157 // QgsSvgGroupLoader
158 //
159 
160 QgsSvgGroupLoader::QgsSvgGroupLoader( QObject *parent )
161  : QThread( parent )
162  , mCanceled( false )
163 {
164 
165 }
166 
167 QgsSvgGroupLoader::~QgsSvgGroupLoader()
168 {
169  stop();
170 }
171 
172 void QgsSvgGroupLoader::run()
173 {
174  mCanceled = false;
175  mTraversedPaths.clear();
176 
177  while ( !mCanceled && !mParentPaths.isEmpty() )
178  {
179  QString parentPath = mParentPaths.takeFirst();
180  loadGroup( parentPath );
181  }
182 }
183 
184 void QgsSvgGroupLoader::stop()
185 {
186  mCanceled = true;
187  while ( isRunning() ) {}
188 }
189 
190 void QgsSvgGroupLoader::loadGroup( const QString &parentPath )
191 {
192  QDir parentDir( parentPath );
193 
194  //guard against circular symbolic links
195  QString canonicalPath = parentDir.canonicalPath();
196  if ( mTraversedPaths.contains( canonicalPath ) )
197  return;
198 
199  mTraversedPaths.insert( canonicalPath );
200 
201  Q_FOREACH ( const QString &item, parentDir.entryList( QDir::Dirs | QDir::NoDotAndDotDot ) )
202  {
203  if ( mCanceled )
204  return;
205 
206  emit foundPath( parentPath, item );
207  mParentPaths.append( parentDir.path() + '/' + item );
208  }
209 }
210 
212 
213 //,
214 // QgsSvgSelectorListModel
215 //
216 
218  : QAbstractListModel( parent )
219  , mSvgLoader( new QgsSvgSelectorLoader( this ) )
220 {
221  mSvgLoader->setPath( QString() );
222  connect( mSvgLoader, &QgsSvgSelectorLoader::foundSvgs, this, &QgsSvgSelectorListModel::addSvgs );
223  mSvgLoader->start();
224 }
225 
226 QgsSvgSelectorListModel::QgsSvgSelectorListModel( QObject *parent, const QString &path )
227  : QAbstractListModel( parent )
228  , mSvgLoader( new QgsSvgSelectorLoader( this ) )
229 {
230  mSvgLoader->setPath( path );
231  connect( mSvgLoader, &QgsSvgSelectorLoader::foundSvgs, this, &QgsSvgSelectorListModel::addSvgs );
232  mSvgLoader->start();
233 }
234 
235 int QgsSvgSelectorListModel::rowCount( const QModelIndex &parent ) const
236 {
237  Q_UNUSED( parent );
238  return mSvgFiles.count();
239 }
240 
241 QPixmap QgsSvgSelectorListModel::createPreview( const QString &entry ) const
242 {
243  // render SVG file
244  QColor fill, stroke;
245  double strokeWidth, fillOpacity, strokeOpacity;
246  bool fillParam, fillOpacityParam, strokeParam, strokeWidthParam, strokeOpacityParam;
247  bool hasDefaultFillColor = false, hasDefaultFillOpacity = false, hasDefaultStrokeColor = false,
248  hasDefaultStrokeWidth = false, hasDefaultStrokeOpacity = false;
249  QgsApplication::svgCache()->containsParams( entry, fillParam, hasDefaultFillColor, fill,
250  fillOpacityParam, hasDefaultFillOpacity, fillOpacity,
251  strokeParam, hasDefaultStrokeColor, stroke,
252  strokeWidthParam, hasDefaultStrokeWidth, strokeWidth,
253  strokeOpacityParam, hasDefaultStrokeOpacity, strokeOpacity );
254 
255  //if defaults not set in symbol, use these values
256  if ( !hasDefaultFillColor )
257  fill = QColor( 200, 200, 200 );
258  fill.setAlphaF( hasDefaultFillOpacity ? fillOpacity : 1.0 );
259  if ( !hasDefaultStrokeColor )
260  stroke = Qt::black;
261  stroke.setAlphaF( hasDefaultStrokeOpacity ? strokeOpacity : 1.0 );
262  if ( !hasDefaultStrokeWidth )
263  strokeWidth = 0.2;
264 
265  bool fitsInCache; // should always fit in cache at these sizes (i.e. under 559 px ^ 2, or half cache size)
266  const QImage &img = QgsApplication::svgCache()->svgAsImage( entry, 30.0, fill, stroke, strokeWidth, 3.5 /*appr. 88 dpi*/, fitsInCache );
267  return QPixmap::fromImage( img );
268 }
269 
270 QVariant QgsSvgSelectorListModel::data( const QModelIndex &index, int role ) const
271 {
272  QString entry = mSvgFiles.at( index.row() );
273 
274  if ( role == Qt::DecorationRole ) // icon
275  {
276  QPixmap pixmap;
277  if ( !QPixmapCache::find( entry, pixmap ) )
278  {
279  pixmap = createPreview( entry );
280  QPixmapCache::insert( entry, pixmap );
281  }
282 
283  return pixmap;
284  }
285  else if ( role == Qt::UserRole || role == Qt::ToolTipRole )
286  {
287  return entry;
288  }
289 
290  return QVariant();
291 }
292 
293 void QgsSvgSelectorListModel::addSvgs( const QStringList &svgs )
294 {
295  beginInsertRows( QModelIndex(), mSvgFiles.count(), mSvgFiles.count() + svgs.size() - 1 );
296  mSvgFiles.append( svgs );
297  endInsertRows();
298 }
299 
300 
301 
302 
303 
304 //--- QgsSvgSelectorGroupsModel
305 
307  : QStandardItemModel( parent )
308  , mLoader( new QgsSvgGroupLoader( this ) )
309 {
310  QStringList svgPaths = QgsApplication::svgPaths();
311  QStandardItem *parentItem = invisibleRootItem();
312  QStringList parentPaths;
313  parentPaths.reserve( svgPaths.size() );
314 
315  for ( int i = 0; i < svgPaths.size(); i++ )
316  {
317  QDir dir( svgPaths.at( i ) );
318  QStandardItem *baseGroup = nullptr;
319 
320  if ( dir.path().contains( QgsApplication::pkgDataPath() ) )
321  {
322  baseGroup = new QStandardItem( tr( "App Symbols" ) );
323  }
324  else if ( dir.path().contains( QgsApplication::qgisSettingsDirPath() ) )
325  {
326  baseGroup = new QStandardItem( tr( "User Symbols" ) );
327  }
328  else
329  {
330  baseGroup = new QStandardItem( dir.dirName() );
331  }
332  baseGroup->setData( QVariant( svgPaths.at( i ) ) );
333  baseGroup->setEditable( false );
334  baseGroup->setCheckable( false );
335  baseGroup->setIcon( QgsApplication::style()->standardIcon( QStyle::SP_DirIcon ) );
336  baseGroup->setToolTip( dir.path() );
337  parentItem->appendRow( baseGroup );
338  parentPaths << svgPaths.at( i );
339  mPathItemHash.insert( svgPaths.at( i ), baseGroup );
340  QgsDebugMsg( QString( "SVG base path %1: %2" ).arg( i ).arg( baseGroup->data().toString() ) );
341  }
342  mLoader->setParentPaths( parentPaths );
343  connect( mLoader, &QgsSvgGroupLoader::foundPath, this, &QgsSvgSelectorGroupsModel::addPath );
344  mLoader->start();
345 }
346 
348 {
349  mLoader->stop();
350 }
351 
352 void QgsSvgSelectorGroupsModel::addPath( const QString &parentPath, const QString &item )
353 {
354  QStandardItem *parentGroup = mPathItemHash.value( parentPath );
355  if ( !parentGroup )
356  return;
357 
358  QString fullPath = parentPath + '/' + item;
359  QStandardItem *group = new QStandardItem( item );
360  group->setData( QVariant( fullPath ) );
361  group->setEditable( false );
362  group->setCheckable( false );
363  group->setToolTip( fullPath );
364  group->setIcon( QgsApplication::style()->standardIcon( QStyle::SP_DirIcon ) );
365  parentGroup->appendRow( group );
366  mPathItemHash.insert( fullPath, group );
367 }
368 
369 
370 //-- QgsSvgSelectorWidget
371 
373  : QWidget( parent )
374 {
375  // TODO: in-code gui setup with option to vertically or horizontally stack SVG groups/images widgets
376  setupUi( this );
377 
378  mGroupsTreeView->setHeaderHidden( true );
379  populateList();
380 
381  connect( mImagesListView->selectionModel(), &QItemSelectionModel::currentChanged,
382  this, &QgsSvgSelectorWidget::svgSelectionChanged );
383  connect( mGroupsTreeView->selectionModel(), &QItemSelectionModel::currentChanged,
384  this, &QgsSvgSelectorWidget::populateIcons );
385 }
386 
388 {
389 }
390 
391 void QgsSvgSelectorWidget::setSvgPath( const QString &svgPath )
392 {
393  mCurrentSvgPath = svgPath;
394 
395  mFileLineEdit->blockSignals( true );
396  mFileLineEdit->setText( svgPath );
397  mFileLineEdit->blockSignals( false );
398 
399  mImagesListView->selectionModel()->blockSignals( true );
400  QAbstractItemModel *m = mImagesListView->model();
401  QItemSelectionModel *selModel = mImagesListView->selectionModel();
402  for ( int i = 0; i < m->rowCount(); i++ )
403  {
404  QModelIndex idx( m->index( i, 0 ) );
405  if ( m->data( idx ).toString() == svgPath )
406  {
407  selModel->select( idx, QItemSelectionModel::SelectCurrent );
408  selModel->setCurrentIndex( idx, QItemSelectionModel::SelectCurrent );
409  mImagesListView->scrollTo( idx );
410  break;
411  }
412  }
413  mImagesListView->selectionModel()->blockSignals( false );
414 }
415 
417 {
418  return mCurrentSvgPath;
419 }
420 
421 void QgsSvgSelectorWidget::updateCurrentSvgPath( const QString &svgPath )
422 {
423  mCurrentSvgPath = svgPath;
424  emit svgSelected( currentSvgPath() );
425 }
426 
427 void QgsSvgSelectorWidget::svgSelectionChanged( const QModelIndex &idx )
428 {
429  QString filePath = idx.data( Qt::UserRole ).toString();
430  mFileLineEdit->setText( filePath );
431  updateCurrentSvgPath( filePath );
432 }
433 
434 void QgsSvgSelectorWidget::populateIcons( const QModelIndex &idx )
435 {
436  QString path = idx.data( Qt::UserRole + 1 ).toString();
437 
438  QAbstractItemModel *oldModel = mImagesListView->model();
439  QgsSvgSelectorListModel *m = new QgsSvgSelectorListModel( mImagesListView, path );
440  mImagesListView->setModel( m );
441  delete oldModel; //explicitly delete old model to force any background threads to stop
442 
443  connect( mImagesListView->selectionModel(), &QItemSelectionModel::currentChanged,
444  this, &QgsSvgSelectorWidget::svgSelectionChanged );
445 
446 }
447 
448 void QgsSvgSelectorWidget::on_mFilePushButton_clicked()
449 {
450  QgsSettings settings;
451  QString openDir = settings.value( QStringLiteral( "UI/lastSVGMarkerDir" ), QDir::homePath() ).toString();
452 
453  QString lineEditText = mFileLineEdit->text();
454  if ( !lineEditText.isEmpty() )
455  {
456  QFileInfo openDirFileInfo( lineEditText );
457  openDir = openDirFileInfo.path();
458  }
459 
460  QString file = QFileDialog::getOpenFileName( nullptr,
461  tr( "Select SVG file" ),
462  openDir,
463  tr( "SVG files" ) + " (*.svg *.SVG)" );
464 
465  activateWindow(); // return window focus
466 
467  if ( file.isNull() )
468  return;
469 
470  QFileInfo fi( file );
471  if ( !fi.exists() || !fi.isReadable() )
472  {
473  updateCurrentSvgPath( QString() );
474  updateLineEditFeedback( false );
475  return;
476  }
477  settings.setValue( QStringLiteral( "UI/lastSVGMarkerDir" ), fi.absolutePath() );
478  mFileLineEdit->setText( file );
479  updateCurrentSvgPath( file );
480 }
481 
482 void QgsSvgSelectorWidget::updateLineEditFeedback( bool ok, const QString &tip )
483 {
484  // draw red text for path field if invalid (path can't be resolved)
485  mFileLineEdit->setStyleSheet( QString( !ok ? "QLineEdit{ color: rgb(225, 0, 0); }" : "" ) );
486  mFileLineEdit->setToolTip( !ok ? tr( "File not found" ) : tip );
487 }
488 
489 void QgsSvgSelectorWidget::on_mFileLineEdit_textChanged( const QString &text )
490 {
491  QString resolvedPath = QgsSymbolLayerUtils::svgSymbolNameToPath( text, QgsProject::instance()->pathResolver() );
492  bool validSVG = !resolvedPath.isNull();
493 
494  updateLineEditFeedback( validSVG, resolvedPath );
495  updateCurrentSvgPath( validSVG ? resolvedPath : QString() );
496 }
497 
499 {
500  QgsSvgSelectorGroupsModel *g = new QgsSvgSelectorGroupsModel( mGroupsTreeView );
501  mGroupsTreeView->setModel( g );
502  // Set the tree expanded at the first level
503  int rows = g->rowCount( g->indexFromItem( g->invisibleRootItem() ) );
504  for ( int i = 0; i < rows; i++ )
505  {
506  mGroupsTreeView->setExpanded( g->indexFromItem( g->item( i ) ), true );
507  }
508 
509  // Initially load the icons in the List view without any grouping
510  QAbstractItemModel *oldModel = mImagesListView->model();
511  QgsSvgSelectorListModel *m = new QgsSvgSelectorListModel( mImagesListView );
512  mImagesListView->setModel( m );
513  delete oldModel; //explicitly delete old model to force any background threads to stop
514 }
515 
516 //-- QgsSvgSelectorDialog
517 
518 QgsSvgSelectorDialog::QgsSvgSelectorDialog( QWidget *parent, Qt::WindowFlags fl,
519  QDialogButtonBox::StandardButtons buttons,
520  Qt::Orientation orientation )
521  : QDialog( parent, fl )
522 {
523  // TODO: pass 'orientation' to QgsSvgSelectorWidget for customizing its layout, once implemented
524  Q_UNUSED( orientation );
525 
526  // create buttonbox
527  mButtonBox = new QDialogButtonBox( buttons, orientation, this );
528  connect( mButtonBox, &QDialogButtonBox::accepted, this, &QDialog::accept );
529  connect( mButtonBox, &QDialogButtonBox::rejected, this, &QDialog::reject );
530 
531  setMinimumSize( 480, 320 );
532 
533  // dialog's layout
534  mLayout = new QVBoxLayout();
535  mSvgSelector = new QgsSvgSelectorWidget( this );
536  mLayout->addWidget( mSvgSelector );
537 
538  mLayout->addWidget( mButtonBox );
539  setLayout( mLayout );
540 
541  QgsSettings settings;
542  restoreGeometry( settings.value( QStringLiteral( "Windows/SvgSelectorDialog/geometry" ) ).toByteArray() );
543 }
544 
546 {
547  QgsSettings settings;
548  settings.setValue( QStringLiteral( "Windows/SvgSelectorDialog/geometry" ), saveGeometry() );
549 }
550 
QgsSvgSelectorWidget * mSvgSelector
static QgsSvgCache * svgCache()
Returns the application&#39;s SVG cache, used for caching SVG images and handling parameter replacement w...
static QString qgisSettingsDirPath()
Returns the path to the settings directory in user&#39;s home dir.
A model for displaying SVG files with a preview icon.
This class is a composition of two QSettings instances:
Definition: qgssettings.h:54
#define QgsDebugMsg(str)
Definition: qgslogger.h:37
QgsSvgSelectorGroupsModel(QObject *parent)
static QString svgSymbolNameToPath(QString name, const QgsPathResolver &pathResolver)
Get SVG symbol&#39;s path from its name.
QVariant data(const QModelIndex &index, int role=Qt::DisplayRole) const override
int rowCount(const QModelIndex &parent=QModelIndex()) const override
void setValue(const QString &key, const QVariant &value, const QgsSettings::Section section=QgsSettings::NoSection)
Sets the value of setting key to value.
QDialogButtonBox * mButtonBox
void svgSelected(const QString &path)
void setSvgPath(const QString &svgPath)
Accepts absolute paths.
static QString pkgDataPath()
Returns the common root path of all application data directories.
QgsSvgSelectorListModel(QObject *parent)
Constructor for QgsSvgSelectorListModel.
void containsParams(const QString &path, bool &hasFillParam, QColor &defaultFillColor, bool &hasStrokeParam, QColor &defaultStrokeColor, bool &hasStrokeWidthParam, double &defaultStrokeWidth) const
Tests if an svg file contains parameters for fill, stroke color, stroke width.
QImage svgAsImage(const QString &path, double size, const QColor &fill, const QColor &stroke, double strokeWidth, double widthScaleFactor, bool &fitsInCache)
Get SVG as QImage.
A model for displaying SVG search paths.
QVariant value(const QString &key, const QVariant &defaultValue=QVariant(), const Section section=NoSection) const
Returns the value for setting key.
static QgsProject * instance()
Returns the QgsProject singleton instance.
Definition: qgsproject.cpp:377
QgsSvgSelectorDialog(QWidget *parent=nullptr, Qt::WindowFlags fl=QgsGuiUtils::ModalDialogFlags, QDialogButtonBox::StandardButtons buttons=QDialogButtonBox::Close|QDialogButtonBox::Ok, Qt::Orientation orientation=Qt::Horizontal)
Constructor for QgsSvgSelectorDialog.
QgsSvgSelectorWidget(QWidget *parent=0)
static QStringList svgPaths()
Returns the paths to svg directories.