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