QGIS API Documentation  2.99.0-Master (7d4f81d)
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 = qMax( 30, qRound( 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 
393 {
394 }
395 
396 void QgsSvgSelectorWidget::setSvgPath( const QString &svgPath )
397 {
398  mCurrentSvgPath = svgPath;
399 
400  mFileLineEdit->blockSignals( true );
401  mFileLineEdit->setText( svgPath );
402  mFileLineEdit->blockSignals( false );
403 
404  mImagesListView->selectionModel()->blockSignals( true );
405  QAbstractItemModel *m = mImagesListView->model();
406  QItemSelectionModel *selModel = mImagesListView->selectionModel();
407  for ( int i = 0; i < m->rowCount(); i++ )
408  {
409  QModelIndex idx( m->index( i, 0 ) );
410  if ( m->data( idx ).toString() == svgPath )
411  {
412  selModel->select( idx, QItemSelectionModel::SelectCurrent );
413  selModel->setCurrentIndex( idx, QItemSelectionModel::SelectCurrent );
414  mImagesListView->scrollTo( idx );
415  break;
416  }
417  }
418  mImagesListView->selectionModel()->blockSignals( false );
419 }
420 
422 {
423  return mCurrentSvgPath;
424 }
425 
426 void QgsSvgSelectorWidget::updateCurrentSvgPath( const QString &svgPath )
427 {
428  mCurrentSvgPath = svgPath;
429  emit svgSelected( currentSvgPath() );
430 }
431 
432 void QgsSvgSelectorWidget::svgSelectionChanged( const QModelIndex &idx )
433 {
434  QString filePath = idx.data( Qt::UserRole ).toString();
435  mFileLineEdit->setText( filePath );
436  updateCurrentSvgPath( filePath );
437 }
438 
439 void QgsSvgSelectorWidget::populateIcons( const QModelIndex &idx )
440 {
441  QString path = idx.data( Qt::UserRole + 1 ).toString();
442 
443  QAbstractItemModel *oldModel = mImagesListView->model();
444  QgsSvgSelectorListModel *m = new QgsSvgSelectorListModel( mImagesListView, path, mIconSize );
445  mImagesListView->setModel( m );
446  delete oldModel; //explicitly delete old model to force any background threads to stop
447 
448  connect( mImagesListView->selectionModel(), &QItemSelectionModel::currentChanged,
449  this, &QgsSvgSelectorWidget::svgSelectionChanged );
450 
451 }
452 
453 void QgsSvgSelectorWidget::on_mFilePushButton_clicked()
454 {
455  QgsSettings settings;
456  QString openDir = settings.value( QStringLiteral( "UI/lastSVGMarkerDir" ), QDir::homePath() ).toString();
457 
458  QString lineEditText = mFileLineEdit->text();
459  if ( !lineEditText.isEmpty() )
460  {
461  QFileInfo openDirFileInfo( lineEditText );
462  openDir = openDirFileInfo.path();
463  }
464 
465  QString file = QFileDialog::getOpenFileName( nullptr,
466  tr( "Select SVG file" ),
467  openDir,
468  tr( "SVG files" ) + " (*.svg *.SVG)" );
469 
470  activateWindow(); // return window focus
471 
472  if ( file.isNull() )
473  return;
474 
475  QFileInfo fi( file );
476  if ( !fi.exists() || !fi.isReadable() )
477  {
478  updateCurrentSvgPath( QString() );
479  updateLineEditFeedback( false );
480  return;
481  }
482  settings.setValue( QStringLiteral( "UI/lastSVGMarkerDir" ), fi.absolutePath() );
483  mFileLineEdit->setText( file );
484  updateCurrentSvgPath( file );
485 }
486 
487 void QgsSvgSelectorWidget::updateLineEditFeedback( bool ok, const QString &tip )
488 {
489  // draw red text for path field if invalid (path can't be resolved)
490  mFileLineEdit->setStyleSheet( QString( !ok ? "QLineEdit{ color: rgb(225, 0, 0); }" : "" ) );
491  mFileLineEdit->setToolTip( !ok ? tr( "File not found" ) : tip );
492 }
493 
494 void QgsSvgSelectorWidget::on_mFileLineEdit_textChanged( const QString &text )
495 {
496  QString resolvedPath = QgsSymbolLayerUtils::svgSymbolNameToPath( text, QgsProject::instance()->pathResolver() );
497  bool validSVG = !resolvedPath.isNull();
498 
499  updateLineEditFeedback( validSVG, resolvedPath );
500  updateCurrentSvgPath( validSVG ? resolvedPath : QString() );
501 }
502 
504 {
505  QgsSvgSelectorGroupsModel *g = new QgsSvgSelectorGroupsModel( mGroupsTreeView );
506  mGroupsTreeView->setModel( g );
507  // Set the tree expanded at the first level
508  int rows = g->rowCount( g->indexFromItem( g->invisibleRootItem() ) );
509  for ( int i = 0; i < rows; i++ )
510  {
511  mGroupsTreeView->setExpanded( g->indexFromItem( g->item( i ) ), true );
512  }
513 
514  // Initially load the icons in the List view without any grouping
515  QAbstractItemModel *oldModel = mImagesListView->model();
516  QgsSvgSelectorListModel *m = new QgsSvgSelectorListModel( mImagesListView );
517  mImagesListView->setModel( m );
518  delete oldModel; //explicitly delete old model to force any background threads to stop
519 }
520 
521 //-- QgsSvgSelectorDialog
522 
523 QgsSvgSelectorDialog::QgsSvgSelectorDialog( QWidget *parent, Qt::WindowFlags fl,
524  QDialogButtonBox::StandardButtons buttons,
525  Qt::Orientation orientation )
526  : QDialog( parent, fl )
527 {
528  // TODO: pass 'orientation' to QgsSvgSelectorWidget for customizing its layout, once implemented
529  Q_UNUSED( orientation );
530 
531  // create buttonbox
532  mButtonBox = new QDialogButtonBox( buttons, orientation, this );
533  connect( mButtonBox, &QDialogButtonBox::accepted, this, &QDialog::accept );
534  connect( mButtonBox, &QDialogButtonBox::rejected, this, &QDialog::reject );
535 
536  setMinimumSize( 480, 320 );
537 
538  // dialog's layout
539  mLayout = new QVBoxLayout();
540  mSvgSelector = new QgsSvgSelectorWidget( this );
541  mLayout->addWidget( mSvgSelector );
542 
543  mLayout->addWidget( mButtonBox );
544  setLayout( mLayout );
545 
546  QgsSettings settings;
547  restoreGeometry( settings.value( QStringLiteral( "Windows/SvgSelectorDialog/geometry" ) ).toByteArray() );
548 }
549 
551 {
552  QgsSettings settings;
553  settings.setValue( QStringLiteral( "Windows/SvgSelectorDialog/geometry" ), saveGeometry() );
554 }
555 
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.