QGIS API Documentation  3.2.0-Bonn (bc43194)
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 {
42 }
43 
44 QgsSvgSelectorLoader::~QgsSvgSelectorLoader()
45 {
46  stop();
47 }
48 
49 void QgsSvgSelectorLoader::run()
50 {
51  mCanceled = false;
52  mQueuedSvgs.clear();
53  mTraversedPaths.clear();
54 
55  // start with a small initial timeout (ms)
56  mTimerThreshold = 10;
57  mTimer.start();
58 
59  loadPath( mPath );
60 
61  if ( !mQueuedSvgs.isEmpty() )
62  {
63  // make sure we notify model of any remaining queued svgs (ie svgs added since last foundSvgs() signal was emitted)
64  emit foundSvgs( mQueuedSvgs );
65  }
66  mQueuedSvgs.clear();
67 }
68 
69 void QgsSvgSelectorLoader::stop()
70 {
71  mCanceled = true;
72  while ( isRunning() ) {}
73 }
74 
75 void QgsSvgSelectorLoader::loadPath( const QString &path )
76 {
77  if ( mCanceled )
78  return;
79 
80  // QgsDebugMsg( QString( "loading path: %1" ).arg( path ) );
81 
82  if ( path.isEmpty() )
83  {
84  QStringList svgPaths = QgsApplication::svgPaths();
85  Q_FOREACH ( const QString &svgPath, svgPaths )
86  {
87  if ( mCanceled )
88  return;
89 
90  if ( !svgPath.isEmpty() )
91  {
92  loadPath( svgPath );
93  }
94  }
95  }
96  else
97  {
98  QDir dir( path );
99 
100  //guard against circular symbolic links
101  QString canonicalPath = dir.canonicalPath();
102  if ( mTraversedPaths.contains( canonicalPath ) )
103  return;
104 
105  mTraversedPaths.insert( canonicalPath );
106 
107  loadImages( path );
108 
109  Q_FOREACH ( const QString &item, dir.entryList( QDir::Dirs | QDir::NoDotAndDotDot ) )
110  {
111  if ( mCanceled )
112  return;
113 
114  QString newPath = dir.path() + '/' + item;
115  loadPath( newPath );
116  // QgsDebugMsg( QString( "added path: %1" ).arg( newPath ) );
117  }
118  }
119 }
120 
121 void QgsSvgSelectorLoader::loadImages( const QString &path )
122 {
123  QDir dir( path );
124  Q_FOREACH ( const QString &item, dir.entryList( QStringList( "*.svg" ), QDir::Files ) )
125  {
126  if ( mCanceled )
127  return;
128 
129  // TODO test if it is correct SVG
130  QString svgPath = dir.path() + '/' + item;
131  // QgsDebugMsg( QString( "adding svg: %1" ).arg( svgPath ) );
132 
133  // add it to the list of queued SVGs
134  mQueuedSvgs << svgPath;
135 
136  // we need to avoid spamming the model with notifications about new svgs, so foundSvgs
137  // is only emitted for blocks of SVGs (otherwise the view goes all flickery)
138  if ( mTimer.elapsed() > mTimerThreshold && !mQueuedSvgs.isEmpty() )
139  {
140  emit foundSvgs( mQueuedSvgs );
141  mQueuedSvgs.clear();
142 
143  // increase the timer threshold - this ensures that the first lots of svgs loaded are added
144  // to the view quickly, but as the list grows new svgs are added at a slower rate.
145  // ie, good for initial responsiveness but avoid being spammy as the list grows.
146  if ( mTimerThreshold < 1000 )
147  mTimerThreshold *= 2;
148  mTimer.restart();
149  }
150  }
151 }
152 
153 
154 //
155 // QgsSvgGroupLoader
156 //
157 
158 QgsSvgGroupLoader::QgsSvgGroupLoader( QObject *parent )
159  : QThread( parent )
160 {
161 
162 }
163 
164 QgsSvgGroupLoader::~QgsSvgGroupLoader()
165 {
166  stop();
167 }
168 
169 void QgsSvgGroupLoader::run()
170 {
171  mCanceled = false;
172  mTraversedPaths.clear();
173 
174  while ( !mCanceled && !mParentPaths.isEmpty() )
175  {
176  QString parentPath = mParentPaths.takeFirst();
177  loadGroup( parentPath );
178  }
179 }
180 
181 void QgsSvgGroupLoader::stop()
182 {
183  mCanceled = true;
184  while ( isRunning() ) {}
185 }
186 
187 void QgsSvgGroupLoader::loadGroup( const QString &parentPath )
188 {
189  QDir parentDir( parentPath );
190 
191  //guard against circular symbolic links
192  QString canonicalPath = parentDir.canonicalPath();
193  if ( mTraversedPaths.contains( canonicalPath ) )
194  return;
195 
196  mTraversedPaths.insert( canonicalPath );
197 
198  Q_FOREACH ( const QString &item, parentDir.entryList( QDir::Dirs | QDir::NoDotAndDotDot ) )
199  {
200  if ( mCanceled )
201  return;
202 
203  emit foundPath( parentPath, item );
204  mParentPaths.append( parentDir.path() + '/' + item );
205  }
206 }
207 
209 
210 //,
211 // QgsSvgSelectorListModel
212 //
213 
214 QgsSvgSelectorListModel::QgsSvgSelectorListModel( QObject *parent, int iconSize )
215  : QAbstractListModel( parent )
216  , mSvgLoader( new QgsSvgSelectorLoader( this ) )
217  , mIconSize( iconSize )
218 {
219  mSvgLoader->setPath( QString() );
220  connect( mSvgLoader, &QgsSvgSelectorLoader::foundSvgs, this, &QgsSvgSelectorListModel::addSvgs );
221  mSvgLoader->start();
222 }
223 
224 QgsSvgSelectorListModel::QgsSvgSelectorListModel( QObject *parent, const QString &path, int iconSize )
225  : QAbstractListModel( parent )
226  , mSvgLoader( new QgsSvgSelectorLoader( this ) )
227  , mIconSize( iconSize )
228 {
229  mSvgLoader->setPath( path );
230  connect( mSvgLoader, &QgsSvgSelectorLoader::foundSvgs, this, &QgsSvgSelectorListModel::addSvgs );
231  mSvgLoader->start();
232 }
233 
234 int QgsSvgSelectorListModel::rowCount( const QModelIndex &parent ) const
235 {
236  Q_UNUSED( parent );
237  return mSvgFiles.count();
238 }
239 
240 QPixmap QgsSvgSelectorListModel::createPreview( const QString &entry ) const
241 {
242  // render SVG file
243  QColor fill, stroke;
244  double strokeWidth, fillOpacity, strokeOpacity;
245  bool fillParam, fillOpacityParam, strokeParam, strokeWidthParam, strokeOpacityParam;
246  bool hasDefaultFillColor = false, hasDefaultFillOpacity = false, hasDefaultStrokeColor = false,
247  hasDefaultStrokeWidth = false, hasDefaultStrokeOpacity = false;
248  QgsApplication::svgCache()->containsParams( entry, fillParam, hasDefaultFillColor, fill,
249  fillOpacityParam, hasDefaultFillOpacity, fillOpacity,
250  strokeParam, hasDefaultStrokeColor, stroke,
251  strokeWidthParam, hasDefaultStrokeWidth, strokeWidth,
252  strokeOpacityParam, hasDefaultStrokeOpacity, strokeOpacity );
253 
254  //if defaults not set in symbol, use these values
255  if ( !hasDefaultFillColor )
256  fill = QColor( 200, 200, 200 );
257  fill.setAlphaF( hasDefaultFillOpacity ? fillOpacity : 1.0 );
258  if ( !hasDefaultStrokeColor )
259  stroke = Qt::black;
260  stroke.setAlphaF( hasDefaultStrokeOpacity ? strokeOpacity : 1.0 );
261  if ( !hasDefaultStrokeWidth )
262  strokeWidth = 0.2;
263 
264  bool fitsInCache; // should always fit in cache at these sizes (i.e. under 559 px ^ 2, or half cache size)
265  QImage img = QgsApplication::svgCache()->svgAsImage( entry, mIconSize, fill, stroke, strokeWidth, 3.5 /*appr. 88 dpi*/, fitsInCache );
266  return QPixmap::fromImage( img );
267 }
268 
269 QVariant QgsSvgSelectorListModel::data( const QModelIndex &index, int role ) const
270 {
271  QString entry = mSvgFiles.at( index.row() );
272 
273  if ( role == Qt::DecorationRole ) // icon
274  {
275  QPixmap pixmap;
276  if ( !QPixmapCache::find( entry, pixmap ) )
277  {
278  pixmap = createPreview( entry );
279  QPixmapCache::insert( entry, pixmap );
280  }
281 
282  return pixmap;
283  }
284  else if ( role == Qt::UserRole || role == Qt::ToolTipRole )
285  {
286  return entry;
287  }
288 
289  return QVariant();
290 }
291 
292 void QgsSvgSelectorListModel::addSvgs( const QStringList &svgs )
293 {
294  beginInsertRows( QModelIndex(), mSvgFiles.count(), mSvgFiles.count() + svgs.size() - 1 );
295  mSvgFiles.append( svgs );
296  endInsertRows();
297 }
298 
299 
300 
301 
302 
303 //--- QgsSvgSelectorGroupsModel
304 
306  : QStandardItemModel( parent )
307  , mLoader( new QgsSvgGroupLoader( this ) )
308 {
309  QStringList svgPaths = QgsApplication::svgPaths();
310  QStandardItem *parentItem = invisibleRootItem();
311  QStringList parentPaths;
312  parentPaths.reserve( svgPaths.size() );
313 
314  for ( int i = 0; i < svgPaths.size(); i++ )
315  {
316  QDir dir( svgPaths.at( i ) );
317  QStandardItem *baseGroup = nullptr;
318 
319  if ( dir.path().contains( QgsApplication::pkgDataPath() ) )
320  {
321  baseGroup = new QStandardItem( tr( "App Symbols" ) );
322  }
323  else if ( dir.path().contains( QgsApplication::qgisSettingsDirPath() ) )
324  {
325  baseGroup = new QStandardItem( tr( "User Symbols" ) );
326  }
327  else
328  {
329  baseGroup = new QStandardItem( dir.dirName() );
330  }
331  baseGroup->setData( QVariant( svgPaths.at( i ) ) );
332  baseGroup->setEditable( false );
333  baseGroup->setCheckable( false );
334  baseGroup->setIcon( QgsApplication::style()->standardIcon( QStyle::SP_DirIcon ) );
335  baseGroup->setToolTip( dir.path() );
336  parentItem->appendRow( baseGroup );
337  parentPaths << svgPaths.at( i );
338  mPathItemHash.insert( svgPaths.at( i ), baseGroup );
339  QgsDebugMsg( QString( "SVG base path %1: %2" ).arg( i ).arg( baseGroup->data().toString() ) );
340  }
341  mLoader->setParentPaths( parentPaths );
342  connect( mLoader, &QgsSvgGroupLoader::foundPath, this, &QgsSvgSelectorGroupsModel::addPath );
343  mLoader->start();
344 }
345 
347 {
348  mLoader->stop();
349 }
350 
351 void QgsSvgSelectorGroupsModel::addPath( const QString &parentPath, const QString &item )
352 {
353  QStandardItem *parentGroup = mPathItemHash.value( parentPath );
354  if ( !parentGroup )
355  return;
356 
357  QString fullPath = parentPath + '/' + item;
358  QStandardItem *group = new QStandardItem( item );
359  group->setData( QVariant( fullPath ) );
360  group->setEditable( false );
361  group->setCheckable( false );
362  group->setToolTip( fullPath );
363  group->setIcon( QgsApplication::style()->standardIcon( QStyle::SP_DirIcon ) );
364  parentGroup->appendRow( group );
365  mPathItemHash.insert( fullPath, group );
366 }
367 
368 
369 //-- QgsSvgSelectorWidget
370 
372  : QWidget( parent )
373 {
374  // TODO: in-code gui setup with option to vertically or horizontally stack SVG groups/images widgets
375  setupUi( this );
376  connect( mFilePushButton, &QPushButton::clicked, this, &QgsSvgSelectorWidget::mFilePushButton_clicked );
377  connect( mFileLineEdit, &QLineEdit::textChanged, this, &QgsSvgSelectorWidget::mFileLineEdit_textChanged );
378 
379  mIconSize = std::max( 30, static_cast< int >( std::round( Qgis::UI_SCALE_FACTOR * fontMetrics().width( QStringLiteral( "XXXX" ) ) ) ) );
380  mImagesListView->setGridSize( QSize( mIconSize * 1.2, mIconSize * 1.2 ) );
381 
382  mGroupsTreeView->setHeaderHidden( true );
383  populateList();
384 
385  connect( mImagesListView->selectionModel(), &QItemSelectionModel::currentChanged,
386  this, &QgsSvgSelectorWidget::svgSelectionChanged );
387  connect( mGroupsTreeView->selectionModel(), &QItemSelectionModel::currentChanged,
388  this, &QgsSvgSelectorWidget::populateIcons );
389 }
390 
391 void QgsSvgSelectorWidget::setSvgPath( const QString &svgPath )
392 {
393  mCurrentSvgPath = svgPath;
394 
395  mFileLineEdit->blockSignals( true );
396  mFileLineEdit->setText( svgPath );
397  mFileLineEdit->blockSignals( false );
398 
399  mImagesListView->selectionModel()->blockSignals( true );
400  QAbstractItemModel *m = mImagesListView->model();
401  QItemSelectionModel *selModel = mImagesListView->selectionModel();
402  for ( int i = 0; i < m->rowCount(); i++ )
403  {
404  QModelIndex idx( m->index( i, 0 ) );
405  if ( m->data( idx ).toString() == svgPath )
406  {
407  selModel->select( idx, QItemSelectionModel::SelectCurrent );
408  selModel->setCurrentIndex( idx, QItemSelectionModel::SelectCurrent );
409  mImagesListView->scrollTo( idx );
410  break;
411  }
412  }
413  mImagesListView->selectionModel()->blockSignals( false );
414 }
415 
417 {
418  return mCurrentSvgPath;
419 }
420 
421 void QgsSvgSelectorWidget::updateCurrentSvgPath( const QString &svgPath )
422 {
423  mCurrentSvgPath = svgPath;
424  emit svgSelected( currentSvgPath() );
425 }
426 
427 void QgsSvgSelectorWidget::svgSelectionChanged( const QModelIndex &idx )
428 {
429  QString filePath = idx.data( Qt::UserRole ).toString();
430  mFileLineEdit->setText( filePath );
431  updateCurrentSvgPath( filePath );
432 }
433 
434 void QgsSvgSelectorWidget::populateIcons( const QModelIndex &idx )
435 {
436  QString path = idx.data( Qt::UserRole + 1 ).toString();
437 
438  QAbstractItemModel *oldModel = mImagesListView->model();
439  QgsSvgSelectorListModel *m = new QgsSvgSelectorListModel( mImagesListView, path, mIconSize );
440  mImagesListView->setModel( m );
441  delete oldModel; //explicitly delete old model to force any background threads to stop
442 
443  connect( mImagesListView->selectionModel(), &QItemSelectionModel::currentChanged,
444  this, &QgsSvgSelectorWidget::svgSelectionChanged );
445 
446 }
447 
448 void QgsSvgSelectorWidget::mFilePushButton_clicked()
449 {
450  QgsSettings settings;
451  QString openDir = settings.value( QStringLiteral( "UI/lastSVGMarkerDir" ), QDir::homePath() ).toString();
452 
453  QString lineEditText = mFileLineEdit->text();
454  if ( !lineEditText.isEmpty() )
455  {
456  QFileInfo openDirFileInfo( lineEditText );
457  openDir = openDirFileInfo.path();
458  }
459 
460  QString file = QFileDialog::getOpenFileName( nullptr,
461  tr( "Select SVG file" ),
462  openDir,
463  tr( "SVG files" ) + " (*.svg *.SVG)" );
464 
465  activateWindow(); // return window focus
466 
467  if ( file.isNull() )
468  return;
469 
470  QFileInfo fi( file );
471  if ( !fi.exists() || !fi.isReadable() )
472  {
473  updateCurrentSvgPath( QString() );
474  updateLineEditFeedback( false );
475  return;
476  }
477  settings.setValue( QStringLiteral( "UI/lastSVGMarkerDir" ), fi.absolutePath() );
478  mFileLineEdit->setText( file );
479  updateCurrentSvgPath( file );
480 }
481 
482 void QgsSvgSelectorWidget::updateLineEditFeedback( bool ok, const QString &tip )
483 {
484  // draw red text for path field if invalid (path can't be resolved)
485  mFileLineEdit->setStyleSheet( QString( !ok ? "QLineEdit{ color: rgb(225, 0, 0); }" : "" ) );
486  mFileLineEdit->setToolTip( !ok ? tr( "File not found" ) : tip );
487 }
488 
489 void QgsSvgSelectorWidget::mFileLineEdit_textChanged( const QString &text )
490 {
491  QString resolvedPath = QgsSymbolLayerUtils::svgSymbolNameToPath( text, QgsProject::instance()->pathResolver() );
492  bool validSVG = !resolvedPath.isNull();
493 
494  updateLineEditFeedback( validSVG, resolvedPath );
495  updateCurrentSvgPath( validSVG ? resolvedPath : QString() );
496 }
497 
499 {
500  QgsSvgSelectorGroupsModel *g = new QgsSvgSelectorGroupsModel( mGroupsTreeView );
501  mGroupsTreeView->setModel( g );
502  // Set the tree expanded at the first level
503  int rows = g->rowCount( g->indexFromItem( g->invisibleRootItem() ) );
504  for ( int i = 0; i < rows; i++ )
505  {
506  mGroupsTreeView->setExpanded( g->indexFromItem( g->item( i ) ), true );
507  }
508 
509  // Initially load the icons in the List view without any grouping
510  QAbstractItemModel *oldModel = mImagesListView->model();
511  QgsSvgSelectorListModel *m = new QgsSvgSelectorListModel( mImagesListView );
512  mImagesListView->setModel( m );
513  delete oldModel; //explicitly delete old model to force any background threads to stop
514 }
515 
516 //-- QgsSvgSelectorDialog
517 
518 QgsSvgSelectorDialog::QgsSvgSelectorDialog( QWidget *parent, Qt::WindowFlags fl,
519  QDialogButtonBox::StandardButtons buttons,
520  Qt::Orientation orientation )
521  : QDialog( parent, fl )
522 {
523  // TODO: pass 'orientation' to QgsSvgSelectorWidget for customizing its layout, once implemented
524  Q_UNUSED( orientation );
525 
526  // create buttonbox
527  mButtonBox = new QDialogButtonBox( buttons, orientation, this );
528  connect( mButtonBox, &QDialogButtonBox::accepted, this, &QDialog::accept );
529  connect( mButtonBox, &QDialogButtonBox::rejected, this, &QDialog::reject );
530 
531  setMinimumSize( 480, 320 );
532 
533  // dialog's layout
534  mLayout = new QVBoxLayout();
535  mSvgSelector = new QgsSvgSelectorWidget( this );
536  mLayout->addWidget( mSvgSelector );
537 
538  mLayout->addWidget( mButtonBox );
539  setLayout( mLayout );
540 
541  QgsSettings settings;
542  restoreGeometry( settings.value( QStringLiteral( "Windows/SvgSelectorDialog/geometry" ) ).toByteArray() );
543 }
544 
546 {
547  QgsSettings settings;
548  settings.setValue( QStringLiteral( "Windows/SvgSelectorDialog/geometry" ), saveGeometry() );
549 }
550 
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:151
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
QgsSvgSelectorGroupsModel(QObject *parent)
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 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.
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:391
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.