QGIS API Documentation  3.8.0-Zanzibar (11aff65)
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  const auto constSvgPaths = svgPaths;
87  for ( const QString &svgPath : constSvgPaths )
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  const auto constEntryList = dir.entryList( QDir::Dirs | QDir::NoDotAndDotDot );
112  for ( const QString &item : constEntryList )
113  {
114  if ( mCanceled )
115  return;
116 
117  QString newPath = dir.path() + '/' + item;
118  loadPath( newPath );
119  // QgsDebugMsg( QStringLiteral( "added path: %1" ).arg( newPath ) );
120  }
121  }
122 }
123 
124 void QgsSvgSelectorLoader::loadImages( const QString &path )
125 {
126  QDir dir( path );
127  const auto constEntryList = dir.entryList( QStringList( "*.svg" ), QDir::Files );
128  for ( const QString &item : constEntryList )
129  {
130  if ( mCanceled )
131  return;
132 
133  // TODO test if it is correct SVG
134  QString svgPath = dir.path() + '/' + item;
135  // QgsDebugMsg( QStringLiteral( "adding svg: %1" ).arg( svgPath ) );
136 
137  // add it to the list of queued SVGs
138  mQueuedSvgs << svgPath;
139 
140  // we need to avoid spamming the model with notifications about new svgs, so foundSvgs
141  // is only emitted for blocks of SVGs (otherwise the view goes all flickery)
142  if ( mTimer.elapsed() > mTimerThreshold && !mQueuedSvgs.isEmpty() )
143  {
144  emit foundSvgs( mQueuedSvgs );
145  mQueuedSvgs.clear();
146 
147  // increase the timer threshold - this ensures that the first lots of svgs loaded are added
148  // to the view quickly, but as the list grows new svgs are added at a slower rate.
149  // ie, good for initial responsiveness but avoid being spammy as the list grows.
150  if ( mTimerThreshold < 1000 )
151  mTimerThreshold *= 2;
152  mTimer.restart();
153  }
154  }
155 }
156 
157 
158 //
159 // QgsSvgGroupLoader
160 //
161 
162 QgsSvgGroupLoader::QgsSvgGroupLoader( QObject *parent )
163  : QThread( parent )
164 {
165 
166 }
167 
168 QgsSvgGroupLoader::~QgsSvgGroupLoader()
169 {
170  stop();
171 }
172 
173 void QgsSvgGroupLoader::run()
174 {
175  mCanceled = false;
176  mTraversedPaths.clear();
177 
178  while ( !mCanceled && !mParentPaths.isEmpty() )
179  {
180  QString parentPath = mParentPaths.takeFirst();
181  loadGroup( parentPath );
182  }
183 }
184 
185 void QgsSvgGroupLoader::stop()
186 {
187  mCanceled = true;
188  while ( isRunning() ) {}
189 }
190 
191 void QgsSvgGroupLoader::loadGroup( const QString &parentPath )
192 {
193  QDir parentDir( parentPath );
194 
195  //guard against circular symbolic links
196  QString canonicalPath = parentDir.canonicalPath();
197  if ( mTraversedPaths.contains( canonicalPath ) )
198  return;
199 
200  mTraversedPaths.insert( canonicalPath );
201 
202  const auto constEntryList = parentDir.entryList( QDir::Dirs | QDir::NoDotAndDotDot );
203  for ( const QString &item : constEntryList )
204  {
205  if ( mCanceled )
206  return;
207 
208  emit foundPath( parentPath, item );
209  mParentPaths.append( parentDir.path() + '/' + item );
210  }
211 }
212 
214 
215 //,
216 // QgsSvgSelectorListModel
217 //
218 
220  : QAbstractListModel( parent )
221  , mSvgLoader( new QgsSvgSelectorLoader( this ) )
222  , mIconSize( iconSize )
223 {
224  mSvgLoader->setPath( QString() );
225  connect( mSvgLoader, &QgsSvgSelectorLoader::foundSvgs, this, &QgsSvgSelectorListModel::addSvgs );
226  mSvgLoader->start();
227 }
228 
229 QgsSvgSelectorListModel::QgsSvgSelectorListModel( QObject *parent, const QString &path, int iconSize )
230  : QAbstractListModel( parent )
231  , mSvgLoader( new QgsSvgSelectorLoader( this ) )
232  , mIconSize( iconSize )
233 {
234  mSvgLoader->setPath( path );
235  connect( mSvgLoader, &QgsSvgSelectorLoader::foundSvgs, this, &QgsSvgSelectorListModel::addSvgs );
236  mSvgLoader->start();
237 }
238 
239 int QgsSvgSelectorListModel::rowCount( const QModelIndex &parent ) const
240 {
241  Q_UNUSED( parent )
242  return mSvgFiles.count();
243 }
244 
245 QPixmap QgsSvgSelectorListModel::createPreview( const QString &entry ) const
246 {
247  // render SVG file
248  QColor fill, stroke;
249  double strokeWidth, fillOpacity, strokeOpacity;
250  bool fillParam, fillOpacityParam, strokeParam, strokeWidthParam, strokeOpacityParam;
251  bool hasDefaultFillColor = false, hasDefaultFillOpacity = false, hasDefaultStrokeColor = false,
252  hasDefaultStrokeWidth = false, hasDefaultStrokeOpacity = false;
253  QgsApplication::svgCache()->containsParams( entry, fillParam, hasDefaultFillColor, fill,
254  fillOpacityParam, hasDefaultFillOpacity, fillOpacity,
255  strokeParam, hasDefaultStrokeColor, stroke,
256  strokeWidthParam, hasDefaultStrokeWidth, strokeWidth,
257  strokeOpacityParam, hasDefaultStrokeOpacity, strokeOpacity );
258 
259  //if defaults not set in symbol, use these values
260  if ( !hasDefaultFillColor )
261  fill = QColor( 200, 200, 200 );
262  fill.setAlphaF( hasDefaultFillOpacity ? fillOpacity : 1.0 );
263  if ( !hasDefaultStrokeColor )
264  stroke = Qt::black;
265  stroke.setAlphaF( hasDefaultStrokeOpacity ? strokeOpacity : 1.0 );
266  if ( !hasDefaultStrokeWidth )
267  strokeWidth = 0.2;
268 
269  bool fitsInCache; // should always fit in cache at these sizes (i.e. under 559 px ^ 2, or half cache size)
270  QImage img = QgsApplication::svgCache()->svgAsImage( entry, mIconSize, fill, stroke, strokeWidth, 3.5 /*appr. 88 dpi*/, fitsInCache );
271  return QPixmap::fromImage( img );
272 }
273 
274 QVariant QgsSvgSelectorListModel::data( const QModelIndex &index, int role ) const
275 {
276  QString entry = mSvgFiles.at( index.row() );
277 
278  if ( role == Qt::DecorationRole ) // icon
279  {
280  QPixmap pixmap;
281  if ( !QPixmapCache::find( entry, pixmap ) )
282  {
283  pixmap = createPreview( entry );
284  QPixmapCache::insert( entry, pixmap );
285  }
286 
287  return pixmap;
288  }
289  else if ( role == Qt::UserRole || role == Qt::ToolTipRole )
290  {
291  return entry;
292  }
293 
294  return QVariant();
295 }
296 
297 void QgsSvgSelectorListModel::addSvgs( const QStringList &svgs )
298 {
299  beginInsertRows( QModelIndex(), mSvgFiles.count(), mSvgFiles.count() + svgs.size() - 1 );
300  mSvgFiles.append( svgs );
301  endInsertRows();
302 }
303 
304 
305 
306 
307 
308 //--- QgsSvgSelectorGroupsModel
309 
311  : QStandardItemModel( parent )
312  , mLoader( new QgsSvgGroupLoader( this ) )
313 {
314  QStringList svgPaths = QgsApplication::svgPaths();
315  QStandardItem *parentItem = invisibleRootItem();
316  QStringList parentPaths;
317  parentPaths.reserve( svgPaths.size() );
318 
319  for ( int i = 0; i < svgPaths.size(); i++ )
320  {
321  QDir dir( svgPaths.at( i ) );
322  QStandardItem *baseGroup = nullptr;
323 
324  if ( dir.path().contains( QgsApplication::pkgDataPath() ) )
325  {
326  baseGroup = new QStandardItem( tr( "App Symbols" ) );
327  }
328  else if ( dir.path().contains( QgsApplication::qgisSettingsDirPath() ) )
329  {
330  baseGroup = new QStandardItem( tr( "User Symbols" ) );
331  }
332  else
333  {
334  baseGroup = new QStandardItem( dir.dirName() );
335  }
336  baseGroup->setData( QVariant( svgPaths.at( i ) ) );
337  baseGroup->setEditable( false );
338  baseGroup->setCheckable( false );
339  baseGroup->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "mIconFolder.svg" ) ) );
340  baseGroup->setToolTip( dir.path() );
341  parentItem->appendRow( baseGroup );
342  parentPaths << svgPaths.at( i );
343  mPathItemHash.insert( svgPaths.at( i ), baseGroup );
344  QgsDebugMsg( QStringLiteral( "SVG base path %1: %2" ).arg( i ).arg( baseGroup->data().toString() ) );
345  }
346  mLoader->setParentPaths( parentPaths );
347  connect( mLoader, &QgsSvgGroupLoader::foundPath, this, &QgsSvgSelectorGroupsModel::addPath );
348  mLoader->start();
349 }
350 
352 {
353  mLoader->stop();
354 }
355 
356 void QgsSvgSelectorGroupsModel::addPath( const QString &parentPath, const QString &item )
357 {
358  QStandardItem *parentGroup = mPathItemHash.value( parentPath );
359  if ( !parentGroup )
360  return;
361 
362  QString fullPath = parentPath + '/' + item;
363  QStandardItem *group = new QStandardItem( item );
364  group->setData( QVariant( fullPath ) );
365  group->setEditable( false );
366  group->setCheckable( false );
367  group->setToolTip( fullPath );
368  group->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "mIconFolder.svg" ) ) );
369  parentGroup->appendRow( group );
370  mPathItemHash.insert( fullPath, group );
371 }
372 
373 
374 //-- QgsSvgSelectorWidget
375 
377  : QWidget( parent )
378 {
379  // TODO: in-code gui setup with option to vertically or horizontally stack SVG groups/images widgets
380  setupUi( this );
381 
382  connect( mSvgSourceLineEdit, &QgsAbstractFileContentSourceLineEdit::sourceChanged, this, &QgsSvgSelectorWidget::svgSourceChanged );
383 
384  mIconSize = std::max( 30, static_cast< int >( std::round( Qgis::UI_SCALE_FACTOR * fontMetrics().width( QStringLiteral( "XXXX" ) ) ) ) );
385  mImagesListView->setGridSize( QSize( mIconSize * 1.2, mIconSize * 1.2 ) );
386 
387  mGroupsTreeView->setHeaderHidden( true );
388  populateList();
389 
390  connect( mImagesListView->selectionModel(), &QItemSelectionModel::currentChanged,
391  this, &QgsSvgSelectorWidget::svgSelectionChanged );
392  connect( mGroupsTreeView->selectionModel(), &QItemSelectionModel::currentChanged,
393  this, &QgsSvgSelectorWidget::populateIcons );
394 }
395 
396 void QgsSvgSelectorWidget::setSvgPath( const QString &svgPath )
397 {
398  mCurrentSvgPath = svgPath;
399 
400  whileBlocking( mSvgSourceLineEdit )->setSource( svgPath );
401 
402  mImagesListView->selectionModel()->blockSignals( true );
403  QAbstractItemModel *m = mImagesListView->model();
404  QItemSelectionModel *selModel = mImagesListView->selectionModel();
405  for ( int i = 0; i < m->rowCount(); i++ )
406  {
407  QModelIndex idx( m->index( i, 0 ) );
408  if ( m->data( idx ).toString() == svgPath )
409  {
410  selModel->select( idx, QItemSelectionModel::SelectCurrent );
411  selModel->setCurrentIndex( idx, QItemSelectionModel::SelectCurrent );
412  mImagesListView->scrollTo( idx );
413  break;
414  }
415  }
416  mImagesListView->selectionModel()->blockSignals( false );
417 }
418 
420 {
421  return mCurrentSvgPath;
422 }
423 
424 void QgsSvgSelectorWidget::updateCurrentSvgPath( const QString &svgPath )
425 {
426  mCurrentSvgPath = svgPath;
427  emit svgSelected( currentSvgPath() );
428 }
429 
430 void QgsSvgSelectorWidget::svgSelectionChanged( const QModelIndex &idx )
431 {
432  QString filePath = idx.data( Qt::UserRole ).toString();
433  whileBlocking( mSvgSourceLineEdit )->setSource( filePath );
434  updateCurrentSvgPath( filePath );
435 }
436 
437 void QgsSvgSelectorWidget::populateIcons( const QModelIndex &idx )
438 {
439  QString path = idx.data( Qt::UserRole + 1 ).toString();
440 
441  QAbstractItemModel *oldModel = mImagesListView->model();
442  QgsSvgSelectorListModel *m = new QgsSvgSelectorListModel( mImagesListView, path, mIconSize );
443  mImagesListView->setModel( m );
444  delete oldModel; //explicitly delete old model to force any background threads to stop
445 
446  connect( mImagesListView->selectionModel(), &QItemSelectionModel::currentChanged,
447  this, &QgsSvgSelectorWidget::svgSelectionChanged );
448 }
449 
450 void QgsSvgSelectorWidget::svgSourceChanged( const QString &text )
451 {
452  QString resolvedPath = QgsSymbolLayerUtils::svgSymbolNameToPath( text, QgsProject::instance()->pathResolver() );
453  bool validSVG = !resolvedPath.isNull();
454 
455  updateCurrentSvgPath( validSVG ? resolvedPath : text );
456 }
457 
459 {
460  QgsSvgSelectorGroupsModel *g = new QgsSvgSelectorGroupsModel( mGroupsTreeView );
461  mGroupsTreeView->setModel( g );
462  // Set the tree expanded at the first level
463  int rows = g->rowCount( g->indexFromItem( g->invisibleRootItem() ) );
464  for ( int i = 0; i < rows; i++ )
465  {
466  mGroupsTreeView->setExpanded( g->indexFromItem( g->item( i ) ), true );
467  }
468 
469  // Initially load the icons in the List view without any grouping
470  QAbstractItemModel *oldModel = mImagesListView->model();
471  QgsSvgSelectorListModel *m = new QgsSvgSelectorListModel( mImagesListView );
472  mImagesListView->setModel( m );
473  delete oldModel; //explicitly delete old model to force any background threads to stop
474 }
475 
476 //-- QgsSvgSelectorDialog
477 
478 QgsSvgSelectorDialog::QgsSvgSelectorDialog( QWidget *parent, Qt::WindowFlags fl,
479  QDialogButtonBox::StandardButtons buttons,
480  Qt::Orientation orientation )
481  : QDialog( parent, fl )
482 {
483  // TODO: pass 'orientation' to QgsSvgSelectorWidget for customizing its layout, once implemented
484  Q_UNUSED( orientation )
485 
486  // create buttonbox
487  mButtonBox = new QDialogButtonBox( buttons, orientation, this );
488  connect( mButtonBox, &QDialogButtonBox::accepted, this, &QDialog::accept );
489  connect( mButtonBox, &QDialogButtonBox::rejected, this, &QDialog::reject );
490 
491  setMinimumSize( 480, 320 );
492 
493  // dialog's layout
494  mLayout = new QVBoxLayout();
495  mSvgSelector = new QgsSvgSelectorWidget( this );
496  mLayout->addWidget( mSvgSelector );
497 
498  mLayout->addWidget( mButtonBox );
499  setLayout( mLayout );
500 
501  QgsSettings settings;
502  restoreGeometry( settings.value( QStringLiteral( "Windows/SvgSelectorDialog/geometry" ) ).toByteArray() );
503 }
504 
506 {
507  QgsSettings settings;
508  settings.setValue( QStringLiteral( "Windows/SvgSelectorDialog/geometry" ), saveGeometry() );
509 }
510 
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:139
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.
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
void sourceChanged(const QString &source)
Emitted whenever the file source is changed in the widget.
QgsSvgSelectorGroupsModel(QObject *parent)
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.
QSize iconSize(bool dockableToolbar)
Returns the user-preferred size of a window&#39;s toolbar icons.
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.
A model for displaying SVG search paths.
QgsSignalBlocker< Object > whileBlocking(Object *object)
Temporarily blocks signals from a QObject while calling a single method from the object.
Definition: qgis.h:212
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:438
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.