QGIS API Documentation  3.4.15-Madeira (e83d02e274)
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 #include <QMenu>
36 
37 // QgsSvgSelectorLoader
38 
40 QgsSvgSelectorLoader::QgsSvgSelectorLoader( QObject *parent )
41  : QThread( parent )
42 {
43 }
44 
45 QgsSvgSelectorLoader::~QgsSvgSelectorLoader()
46 {
47  stop();
48 }
49 
50 void QgsSvgSelectorLoader::run()
51 {
52  mCanceled = false;
53  mQueuedSvgs.clear();
54  mTraversedPaths.clear();
55 
56  // start with a small initial timeout (ms)
57  mTimerThreshold = 10;
58  mTimer.start();
59 
60  loadPath( mPath );
61 
62  if ( !mQueuedSvgs.isEmpty() )
63  {
64  // make sure we notify model of any remaining queued svgs (ie svgs added since last foundSvgs() signal was emitted)
65  emit foundSvgs( mQueuedSvgs );
66  }
67  mQueuedSvgs.clear();
68 }
69 
70 void QgsSvgSelectorLoader::stop()
71 {
72  mCanceled = true;
73  while ( isRunning() ) {}
74 }
75 
76 void QgsSvgSelectorLoader::loadPath( const QString &path )
77 {
78  if ( mCanceled )
79  return;
80 
81  // QgsDebugMsg( QStringLiteral( "loading path: %1" ).arg( path ) );
82 
83  if ( path.isEmpty() )
84  {
85  QStringList svgPaths = QgsApplication::svgPaths();
86  Q_FOREACH ( const QString &svgPath, svgPaths )
87  {
88  if ( mCanceled )
89  return;
90 
91  if ( !svgPath.isEmpty() )
92  {
93  loadPath( svgPath );
94  }
95  }
96  }
97  else
98  {
99  QDir dir( path );
100 
101  //guard against circular symbolic links
102  QString canonicalPath = dir.canonicalPath();
103  if ( mTraversedPaths.contains( canonicalPath ) )
104  return;
105 
106  mTraversedPaths.insert( canonicalPath );
107 
108  loadImages( path );
109 
110  Q_FOREACH ( const QString &item, dir.entryList( QDir::Dirs | QDir::NoDotAndDotDot ) )
111  {
112  if ( mCanceled )
113  return;
114 
115  QString newPath = dir.path() + '/' + item;
116  loadPath( newPath );
117  // QgsDebugMsg( QStringLiteral( "added path: %1" ).arg( newPath ) );
118  }
119  }
120 }
121 
122 void QgsSvgSelectorLoader::loadImages( const QString &path )
123 {
124  QDir dir( path );
125  Q_FOREACH ( const QString &item, dir.entryList( QStringList( "*.svg" ), QDir::Files ) )
126  {
127  if ( mCanceled )
128  return;
129 
130  // TODO test if it is correct SVG
131  QString svgPath = dir.path() + '/' + item;
132  // QgsDebugMsg( QStringLiteral( "adding svg: %1" ).arg( svgPath ) );
133 
134  // add it to the list of queued SVGs
135  mQueuedSvgs << svgPath;
136 
137  // we need to avoid spamming the model with notifications about new svgs, so foundSvgs
138  // is only emitted for blocks of SVGs (otherwise the view goes all flickery)
139  if ( mTimer.elapsed() > mTimerThreshold && !mQueuedSvgs.isEmpty() )
140  {
141  emit foundSvgs( mQueuedSvgs );
142  mQueuedSvgs.clear();
143 
144  // increase the timer threshold - this ensures that the first lots of svgs loaded are added
145  // to the view quickly, but as the list grows new svgs are added at a slower rate.
146  // ie, good for initial responsiveness but avoid being spammy as the list grows.
147  if ( mTimerThreshold < 1000 )
148  mTimerThreshold *= 2;
149  mTimer.restart();
150  }
151  }
152 }
153 
154 
155 //
156 // QgsSvgGroupLoader
157 //
158 
159 QgsSvgGroupLoader::QgsSvgGroupLoader( QObject *parent )
160  : QThread( parent )
161 {
162 
163 }
164 
165 QgsSvgGroupLoader::~QgsSvgGroupLoader()
166 {
167  stop();
168 }
169 
170 void QgsSvgGroupLoader::run()
171 {
172  mCanceled = false;
173  mTraversedPaths.clear();
174 
175  while ( !mCanceled && !mParentPaths.isEmpty() )
176  {
177  QString parentPath = mParentPaths.takeFirst();
178  loadGroup( parentPath );
179  }
180 }
181 
182 void QgsSvgGroupLoader::stop()
183 {
184  mCanceled = true;
185  while ( isRunning() ) {}
186 }
187 
188 void QgsSvgGroupLoader::loadGroup( const QString &parentPath )
189 {
190  QDir parentDir( parentPath );
191 
192  //guard against circular symbolic links
193  QString canonicalPath = parentDir.canonicalPath();
194  if ( mTraversedPaths.contains( canonicalPath ) )
195  return;
196 
197  mTraversedPaths.insert( canonicalPath );
198 
199  Q_FOREACH ( const QString &item, parentDir.entryList( QDir::Dirs | QDir::NoDotAndDotDot ) )
200  {
201  if ( mCanceled )
202  return;
203 
204  emit foundPath( parentPath, item );
205  mParentPaths.append( parentDir.path() + '/' + item );
206  }
207 }
208 
210 
211 //,
212 // QgsSvgSelectorListModel
213 //
214 
215 QgsSvgSelectorListModel::QgsSvgSelectorListModel( QObject *parent, int iconSize )
216  : QAbstractListModel( parent )
217  , mSvgLoader( new QgsSvgSelectorLoader( this ) )
218  , mIconSize( iconSize )
219 {
220  mSvgLoader->setPath( QString() );
221  connect( mSvgLoader, &QgsSvgSelectorLoader::foundSvgs, this, &QgsSvgSelectorListModel::addSvgs );
222  mSvgLoader->start();
223 }
224 
225 QgsSvgSelectorListModel::QgsSvgSelectorListModel( QObject *parent, const QString &path, int iconSize )
226  : QAbstractListModel( parent )
227  , mSvgLoader( new QgsSvgSelectorLoader( this ) )
228  , mIconSize( iconSize )
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  QImage img = QgsApplication::svgCache()->svgAsImage( entry, mIconSize, 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::getThemeIcon( QStringLiteral( "mIconFolder.svg" ) ) );
336  baseGroup->setToolTip( dir.path() );
337  parentItem->appendRow( baseGroup );
338  parentPaths << svgPaths.at( i );
339  mPathItemHash.insert( svgPaths.at( i ), baseGroup );
340  QgsDebugMsg( QStringLiteral( "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::getThemeIcon( QStringLiteral( "mIconFolder.svg" ) ) );
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  connect( mSvgSourceLineEdit, &QgsSvgSourceLineEdit::sourceChanged, this, &QgsSvgSelectorWidget::svgSourceChanged );
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  whileBlocking( mSvgSourceLineEdit )->setSource( svgPath );
397 
398  mImagesListView->selectionModel()->blockSignals( true );
399  QAbstractItemModel *m = mImagesListView->model();
400  QItemSelectionModel *selModel = mImagesListView->selectionModel();
401  for ( int i = 0; i < m->rowCount(); i++ )
402  {
403  QModelIndex idx( m->index( i, 0 ) );
404  if ( m->data( idx ).toString() == svgPath )
405  {
406  selModel->select( idx, QItemSelectionModel::SelectCurrent );
407  selModel->setCurrentIndex( idx, QItemSelectionModel::SelectCurrent );
408  mImagesListView->scrollTo( idx );
409  break;
410  }
411  }
412  mImagesListView->selectionModel()->blockSignals( false );
413 }
414 
416 {
417  return mCurrentSvgPath;
418 }
419 
420 void QgsSvgSelectorWidget::updateCurrentSvgPath( const QString &svgPath )
421 {
422  mCurrentSvgPath = svgPath;
423  emit svgSelected( currentSvgPath() );
424 }
425 
426 void QgsSvgSelectorWidget::svgSelectionChanged( const QModelIndex &idx )
427 {
428  QString filePath = idx.data( Qt::UserRole ).toString();
429  whileBlocking( mSvgSourceLineEdit )->setSource( filePath );
430  updateCurrentSvgPath( filePath );
431 }
432 
433 void QgsSvgSelectorWidget::populateIcons( const QModelIndex &idx )
434 {
435  QString path = idx.data( Qt::UserRole + 1 ).toString();
436 
437  QAbstractItemModel *oldModel = mImagesListView->model();
438  QgsSvgSelectorListModel *m = new QgsSvgSelectorListModel( mImagesListView, path, mIconSize );
439  mImagesListView->setModel( m );
440  delete oldModel; //explicitly delete old model to force any background threads to stop
441 
442  connect( mImagesListView->selectionModel(), &QItemSelectionModel::currentChanged,
443  this, &QgsSvgSelectorWidget::svgSelectionChanged );
444 }
445 
446 void QgsSvgSelectorWidget::svgSourceChanged( const QString &text )
447 {
448  QString resolvedPath = QgsSymbolLayerUtils::svgSymbolNameToPath( text, QgsProject::instance()->pathResolver() );
449  bool validSVG = !resolvedPath.isNull();
450 
451  updateCurrentSvgPath( validSVG ? resolvedPath : text );
452 }
453 
455 {
456  QgsSvgSelectorGroupsModel *g = new QgsSvgSelectorGroupsModel( mGroupsTreeView );
457  mGroupsTreeView->setModel( g );
458  // Set the tree expanded at the first level
459  int rows = g->rowCount( g->indexFromItem( g->invisibleRootItem() ) );
460  for ( int i = 0; i < rows; i++ )
461  {
462  mGroupsTreeView->setExpanded( g->indexFromItem( g->item( i ) ), true );
463  }
464 
465  // Initially load the icons in the List view without any grouping
466  QAbstractItemModel *oldModel = mImagesListView->model();
467  QgsSvgSelectorListModel *m = new QgsSvgSelectorListModel( mImagesListView );
468  mImagesListView->setModel( m );
469  delete oldModel; //explicitly delete old model to force any background threads to stop
470 }
471 
472 //-- QgsSvgSelectorDialog
473 
474 QgsSvgSelectorDialog::QgsSvgSelectorDialog( QWidget *parent, Qt::WindowFlags fl,
475  QDialogButtonBox::StandardButtons buttons,
476  Qt::Orientation orientation )
477  : QDialog( parent, fl )
478 {
479  // TODO: pass 'orientation' to QgsSvgSelectorWidget for customizing its layout, once implemented
480  Q_UNUSED( orientation );
481 
482  // create buttonbox
483  mButtonBox = new QDialogButtonBox( buttons, orientation, this );
484  connect( mButtonBox, &QDialogButtonBox::accepted, this, &QDialog::accept );
485  connect( mButtonBox, &QDialogButtonBox::rejected, this, &QDialog::reject );
486 
487  setMinimumSize( 480, 320 );
488 
489  // dialog's layout
490  mLayout = new QVBoxLayout();
491  mSvgSelector = new QgsSvgSelectorWidget( this );
492  mLayout->addWidget( mSvgSelector );
493 
494  mLayout->addWidget( mButtonBox );
495  setLayout( mLayout );
496 
497  QgsSettings settings;
498  restoreGeometry( settings.value( QStringLiteral( "Windows/SvgSelectorDialog/geometry" ) ).toByteArray() );
499 }
500 
502 {
503  QgsSettings settings;
504  settings.setValue( QStringLiteral( "Windows/SvgSelectorDialog/geometry" ), saveGeometry() );
505 }
506 
QgsSvgSelectorWidget * mSvgSelector
static QgsSvgCache * svgCache()
Returns the application&#39;s SVG cache, used for caching SVG images and handling parameter replacement w...
QgsSvgSelectorWidget(QWidget *parent=nullptr)
Constructor for QgsSvgSelectorWidget.
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:152
This class is a composition of two QSettings instances:
Definition: qgssettings.h:58
QImage svgAsImage(const QString &path, double size, const QColor &fill, const QColor &stroke, double strokeWidth, double widthScaleFactor, bool &fitsInCache, double fixedAspectRatio=0)
Gets SVG as QImage.
#define QgsDebugMsg(str)
Definition: qgslogger.h:38
QString currentSvgPath() const
QgsSvgSelectorGroupsModel(QObject *parent)
QVariant value(const QString &key, const QVariant &defaultValue=QVariant(), Section section=NoSection) const
Returns the value for setting key.
static QIcon getThemeIcon(const QString &name)
Helper to get a theme icon.
QVariant data(const QModelIndex &index, int role=Qt::DisplayRole) const override
void saveGeometry(QWidget *widget, const QString &keyName)
Save the wigget geometry into settings.
int rowCount(const QModelIndex &parent=QModelIndex()) const override
bool restoreGeometry(QWidget *widget, const QString &keyName)
Restore the wigget geometry from settings.
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 sourceChanged(const QString &source)
Emitted whenever the SVG source is changed in the widget.
A model for displaying SVG search paths.
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.
QgsSignalBlocker< Object > whileBlocking(Object *object)
Temporarily blocks signals from a QObject while calling a single method from the object.
Definition: qgis.h:225
QgsSvgSelectorListModel(QObject *parent, int iconSize=30)
Constructor for QgsSvgSelectorListModel.
void setValue(const QString &key, const QVariant &value, QgsSettings::Section section=QgsSettings::NoSection)
Sets the value of setting key to value.
static QgsProject * instance()
Returns the QgsProject singleton instance.
Definition: qgsproject.cpp:411
QgsSvgSelectorDialog(QWidget *parent=nullptr, Qt::WindowFlags fl=QgsGuiUtils::ModalDialogFlags, QDialogButtonBox::StandardButtons buttons=QDialogButtonBox::Close|QDialogButtonBox::Ok, Qt::Orientation orientation=Qt::Horizontal)
Constructor for QgsSvgSelectorDialog.
static QStringList svgPaths()
Returns the paths to svg directories.
static QString svgSymbolNameToPath(const QString &name, const QgsPathResolver &pathResolver)
Determines an SVG symbol&#39;s path from its name.