QGIS API Documentation  2.99.0-Master (9caa722)
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 
217 QgsSvgSelectorListModel::QgsSvgSelectorListModel( QObject *parent, int iconSize )
218  : QAbstractListModel( parent )
219  , mSvgLoader( new QgsSvgSelectorLoader( this ) )
220  , mIconSize( iconSize )
221 {
222  mSvgLoader->setPath( QString() );
223  connect( mSvgLoader, &QgsSvgSelectorLoader::foundSvgs, this, &QgsSvgSelectorListModel::addSvgs );
224  mSvgLoader->start();
225 }
226 
227 QgsSvgSelectorListModel::QgsSvgSelectorListModel( QObject *parent, const QString &path, int iconSize )
228  : QAbstractListModel( parent )
229  , mSvgLoader( new QgsSvgSelectorLoader( this ) )
230  , mIconSize( iconSize )
231 {
232  mSvgLoader->setPath( path );
233  connect( mSvgLoader, &QgsSvgSelectorLoader::foundSvgs, this, &QgsSvgSelectorListModel::addSvgs );
234  mSvgLoader->start();
235 }
236 
237 int QgsSvgSelectorListModel::rowCount( const QModelIndex &parent ) const
238 {
239  Q_UNUSED( parent );
240  return mSvgFiles.count();
241 }
242 
243 QPixmap QgsSvgSelectorListModel::createPreview( const QString &entry ) const
244 {
245  // render SVG file
246  QColor fill, stroke;
247  double strokeWidth, fillOpacity, strokeOpacity;
248  bool fillParam, fillOpacityParam, strokeParam, strokeWidthParam, strokeOpacityParam;
249  bool hasDefaultFillColor = false, hasDefaultFillOpacity = false, hasDefaultStrokeColor = false,
250  hasDefaultStrokeWidth = false, hasDefaultStrokeOpacity = false;
251  QgsApplication::svgCache()->containsParams( entry, fillParam, hasDefaultFillColor, fill,
252  fillOpacityParam, hasDefaultFillOpacity, fillOpacity,
253  strokeParam, hasDefaultStrokeColor, stroke,
254  strokeWidthParam, hasDefaultStrokeWidth, strokeWidth,
255  strokeOpacityParam, hasDefaultStrokeOpacity, strokeOpacity );
256 
257  //if defaults not set in symbol, use these values
258  if ( !hasDefaultFillColor )
259  fill = QColor( 200, 200, 200 );
260  fill.setAlphaF( hasDefaultFillOpacity ? fillOpacity : 1.0 );
261  if ( !hasDefaultStrokeColor )
262  stroke = Qt::black;
263  stroke.setAlphaF( hasDefaultStrokeOpacity ? strokeOpacity : 1.0 );
264  if ( !hasDefaultStrokeWidth )
265  strokeWidth = 0.2;
266 
267  bool fitsInCache; // should always fit in cache at these sizes (i.e. under 559 px ^ 2, or half cache size)
268  const QImage &img = QgsApplication::svgCache()->svgAsImage( entry, mIconSize, fill, stroke, strokeWidth, 3.5 /*appr. 88 dpi*/, fitsInCache );
269  return QPixmap::fromImage( img );
270 }
271 
272 QVariant QgsSvgSelectorListModel::data( const QModelIndex &index, int role ) const
273 {
274  QString entry = mSvgFiles.at( index.row() );
275 
276  if ( role == Qt::DecorationRole ) // icon
277  {
278  QPixmap pixmap;
279  if ( !QPixmapCache::find( entry, pixmap ) )
280  {
281  pixmap = createPreview( entry );
282  QPixmapCache::insert( entry, pixmap );
283  }
284 
285  return pixmap;
286  }
287  else if ( role == Qt::UserRole || role == Qt::ToolTipRole )
288  {
289  return entry;
290  }
291 
292  return QVariant();
293 }
294 
295 void QgsSvgSelectorListModel::addSvgs( const QStringList &svgs )
296 {
297  beginInsertRows( QModelIndex(), mSvgFiles.count(), mSvgFiles.count() + svgs.size() - 1 );
298  mSvgFiles.append( svgs );
299  endInsertRows();
300 }
301 
302 
303 
304 
305 
306 //--- QgsSvgSelectorGroupsModel
307 
309  : QStandardItemModel( parent )
310  , mLoader( new QgsSvgGroupLoader( this ) )
311 {
312  QStringList svgPaths = QgsApplication::svgPaths();
313  QStandardItem *parentItem = invisibleRootItem();
314  QStringList parentPaths;
315  parentPaths.reserve( svgPaths.size() );
316 
317  for ( int i = 0; i < svgPaths.size(); i++ )
318  {
319  QDir dir( svgPaths.at( i ) );
320  QStandardItem *baseGroup = nullptr;
321 
322  if ( dir.path().contains( QgsApplication::pkgDataPath() ) )
323  {
324  baseGroup = new QStandardItem( tr( "App Symbols" ) );
325  }
326  else if ( dir.path().contains( QgsApplication::qgisSettingsDirPath() ) )
327  {
328  baseGroup = new QStandardItem( tr( "User Symbols" ) );
329  }
330  else
331  {
332  baseGroup = new QStandardItem( dir.dirName() );
333  }
334  baseGroup->setData( QVariant( svgPaths.at( i ) ) );
335  baseGroup->setEditable( false );
336  baseGroup->setCheckable( false );
337  baseGroup->setIcon( QgsApplication::style()->standardIcon( QStyle::SP_DirIcon ) );
338  baseGroup->setToolTip( dir.path() );
339  parentItem->appendRow( baseGroup );
340  parentPaths << svgPaths.at( i );
341  mPathItemHash.insert( svgPaths.at( i ), baseGroup );
342  QgsDebugMsg( QString( "SVG base path %1: %2" ).arg( i ).arg( baseGroup->data().toString() ) );
343  }
344  mLoader->setParentPaths( parentPaths );
345  connect( mLoader, &QgsSvgGroupLoader::foundPath, this, &QgsSvgSelectorGroupsModel::addPath );
346  mLoader->start();
347 }
348 
350 {
351  mLoader->stop();
352 }
353 
354 void QgsSvgSelectorGroupsModel::addPath( const QString &parentPath, const QString &item )
355 {
356  QStandardItem *parentGroup = mPathItemHash.value( parentPath );
357  if ( !parentGroup )
358  return;
359 
360  QString fullPath = parentPath + '/' + item;
361  QStandardItem *group = new QStandardItem( item );
362  group->setData( QVariant( fullPath ) );
363  group->setEditable( false );
364  group->setCheckable( false );
365  group->setToolTip( fullPath );
366  group->setIcon( QgsApplication::style()->standardIcon( QStyle::SP_DirIcon ) );
367  parentGroup->appendRow( group );
368  mPathItemHash.insert( fullPath, group );
369 }
370 
371 
372 //-- QgsSvgSelectorWidget
373 
375  : QWidget( parent )
376 {
377  // TODO: in-code gui setup with option to vertically or horizontally stack SVG groups/images widgets
378  setupUi( this );
379 
380  mIconSize = std::max( 30, static_cast< int >( std::round( Qgis::UI_SCALE_FACTOR * fontMetrics().width( QStringLiteral( "XXXX" ) ) ) ) );
381  mImagesListView->setGridSize( QSize( mIconSize * 1.2, mIconSize * 1.2 ) );
382 
383  mGroupsTreeView->setHeaderHidden( true );
384  populateList();
385 
386  connect( mImagesListView->selectionModel(), &QItemSelectionModel::currentChanged,
387  this, &QgsSvgSelectorWidget::svgSelectionChanged );
388  connect( mGroupsTreeView->selectionModel(), &QItemSelectionModel::currentChanged,
389  this, &QgsSvgSelectorWidget::populateIcons );
390 }
391 
392 void QgsSvgSelectorWidget::setSvgPath( const QString &svgPath )
393 {
394  mCurrentSvgPath = svgPath;
395 
396  mFileLineEdit->blockSignals( true );
397  mFileLineEdit->setText( svgPath );
398  mFileLineEdit->blockSignals( false );
399 
400  mImagesListView->selectionModel()->blockSignals( true );
401  QAbstractItemModel *m = mImagesListView->model();
402  QItemSelectionModel *selModel = mImagesListView->selectionModel();
403  for ( int i = 0; i < m->rowCount(); i++ )
404  {
405  QModelIndex idx( m->index( i, 0 ) );
406  if ( m->data( idx ).toString() == svgPath )
407  {
408  selModel->select( idx, QItemSelectionModel::SelectCurrent );
409  selModel->setCurrentIndex( idx, QItemSelectionModel::SelectCurrent );
410  mImagesListView->scrollTo( idx );
411  break;
412  }
413  }
414  mImagesListView->selectionModel()->blockSignals( false );
415 }
416 
418 {
419  return mCurrentSvgPath;
420 }
421 
422 void QgsSvgSelectorWidget::updateCurrentSvgPath( const QString &svgPath )
423 {
424  mCurrentSvgPath = svgPath;
425  emit svgSelected( currentSvgPath() );
426 }
427 
428 void QgsSvgSelectorWidget::svgSelectionChanged( const QModelIndex &idx )
429 {
430  QString filePath = idx.data( Qt::UserRole ).toString();
431  mFileLineEdit->setText( filePath );
432  updateCurrentSvgPath( filePath );
433 }
434 
435 void QgsSvgSelectorWidget::populateIcons( const QModelIndex &idx )
436 {
437  QString path = idx.data( Qt::UserRole + 1 ).toString();
438 
439  QAbstractItemModel *oldModel = mImagesListView->model();
440  QgsSvgSelectorListModel *m = new QgsSvgSelectorListModel( mImagesListView, path, mIconSize );
441  mImagesListView->setModel( m );
442  delete oldModel; //explicitly delete old model to force any background threads to stop
443 
444  connect( mImagesListView->selectionModel(), &QItemSelectionModel::currentChanged,
445  this, &QgsSvgSelectorWidget::svgSelectionChanged );
446 
447 }
448 
449 void QgsSvgSelectorWidget::on_mFilePushButton_clicked()
450 {
451  QgsSettings settings;
452  QString openDir = settings.value( QStringLiteral( "UI/lastSVGMarkerDir" ), QDir::homePath() ).toString();
453 
454  QString lineEditText = mFileLineEdit->text();
455  if ( !lineEditText.isEmpty() )
456  {
457  QFileInfo openDirFileInfo( lineEditText );
458  openDir = openDirFileInfo.path();
459  }
460 
461  QString file = QFileDialog::getOpenFileName( nullptr,
462  tr( "Select SVG file" ),
463  openDir,
464  tr( "SVG files" ) + " (*.svg *.SVG)" );
465 
466  activateWindow(); // return window focus
467 
468  if ( file.isNull() )
469  return;
470 
471  QFileInfo fi( file );
472  if ( !fi.exists() || !fi.isReadable() )
473  {
474  updateCurrentSvgPath( QString() );
475  updateLineEditFeedback( false );
476  return;
477  }
478  settings.setValue( QStringLiteral( "UI/lastSVGMarkerDir" ), fi.absolutePath() );
479  mFileLineEdit->setText( file );
480  updateCurrentSvgPath( file );
481 }
482 
483 void QgsSvgSelectorWidget::updateLineEditFeedback( bool ok, const QString &tip )
484 {
485  // draw red text for path field if invalid (path can't be resolved)
486  mFileLineEdit->setStyleSheet( QString( !ok ? "QLineEdit{ color: rgb(225, 0, 0); }" : "" ) );
487  mFileLineEdit->setToolTip( !ok ? tr( "File not found" ) : tip );
488 }
489 
490 void QgsSvgSelectorWidget::on_mFileLineEdit_textChanged( const QString &text )
491 {
492  QString resolvedPath = QgsSymbolLayerUtils::svgSymbolNameToPath( text, QgsProject::instance()->pathResolver() );
493  bool validSVG = !resolvedPath.isNull();
494 
495  updateLineEditFeedback( validSVG, resolvedPath );
496  updateCurrentSvgPath( validSVG ? resolvedPath : QString() );
497 }
498 
500 {
501  QgsSvgSelectorGroupsModel *g = new QgsSvgSelectorGroupsModel( mGroupsTreeView );
502  mGroupsTreeView->setModel( g );
503  // Set the tree expanded at the first level
504  int rows = g->rowCount( g->indexFromItem( g->invisibleRootItem() ) );
505  for ( int i = 0; i < rows; i++ )
506  {
507  mGroupsTreeView->setExpanded( g->indexFromItem( g->item( i ) ), true );
508  }
509 
510  // Initially load the icons in the List view without any grouping
511  QAbstractItemModel *oldModel = mImagesListView->model();
512  QgsSvgSelectorListModel *m = new QgsSvgSelectorListModel( mImagesListView );
513  mImagesListView->setModel( m );
514  delete oldModel; //explicitly delete old model to force any background threads to stop
515 }
516 
517 //-- QgsSvgSelectorDialog
518 
519 QgsSvgSelectorDialog::QgsSvgSelectorDialog( QWidget *parent, Qt::WindowFlags fl,
520  QDialogButtonBox::StandardButtons buttons,
521  Qt::Orientation orientation )
522  : QDialog( parent, fl )
523 {
524  // TODO: pass 'orientation' to QgsSvgSelectorWidget for customizing its layout, once implemented
525  Q_UNUSED( orientation );
526 
527  // create buttonbox
528  mButtonBox = new QDialogButtonBox( buttons, orientation, this );
529  connect( mButtonBox, &QDialogButtonBox::accepted, this, &QDialog::accept );
530  connect( mButtonBox, &QDialogButtonBox::rejected, this, &QDialog::reject );
531 
532  setMinimumSize( 480, 320 );
533 
534  // dialog's layout
535  mLayout = new QVBoxLayout();
536  mSvgSelector = new QgsSvgSelectorWidget( this );
537  mLayout->addWidget( mSvgSelector );
538 
539  mLayout->addWidget( mButtonBox );
540  setLayout( mLayout );
541 
542  QgsSettings settings;
543  restoreGeometry( settings.value( QStringLiteral( "Windows/SvgSelectorDialog/geometry" ) ).toByteArray() );
544 }
545 
547 {
548  QgsSettings settings;
549  settings.setValue( QStringLiteral( "Windows/SvgSelectorDialog/geometry" ), saveGeometry() );
550 }
551 
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.
static const double UI_SCALE_FACTOR
UI scaling factor.
Definition: qgis.h:128
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.
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.
QgsSvgSelectorListModel(QObject *parent, int iconSize=30)
Constructor for QgsSvgSelectorListModel.
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:379
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.