QGIS API Documentation  2.99.0-Master (6a61179)
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 "qgsproject.h"
22 #include "qgssvgcache.h"
23 #include "qgssymbollayerutils.h"
24 
25 #include <QAbstractListModel>
26 #include <QCheckBox>
27 #include <QDir>
28 #include <QFileDialog>
29 #include <QModelIndex>
30 #include <QPixmapCache>
31 #include <QSettings>
32 #include <QStyle>
33 #include <QTime>
34 
35 // QgsSvgSelectorLoader
36 
38 QgsSvgSelectorLoader::QgsSvgSelectorLoader( QObject* parent )
39  : QThread( parent )
40  , mCancelled( false )
41  , mTimerThreshold( 0 )
42 {
43 }
44 
45 QgsSvgSelectorLoader::~QgsSvgSelectorLoader()
46 {
47  stop();
48 }
49 
50 void QgsSvgSelectorLoader::run()
51 {
52  mCancelled = 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  mCancelled = true;
73  while ( isRunning() ) {}
74 }
75 
76 void QgsSvgSelectorLoader::loadPath( const QString& path )
77 {
78  if ( mCancelled )
79  return;
80 
81  // QgsDebugMsg( QString( "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 ( mCancelled )
89  return;
90 
91  loadPath( svgPath );
92  }
93  }
94  else
95  {
96  QDir dir( path );
97 
98  //guard against circular symbolic links
99  QString canonicalPath = dir.canonicalPath();
100  if ( mTraversedPaths.contains( canonicalPath ) )
101  return;
102 
103  mTraversedPaths.insert( canonicalPath );
104 
105  loadImages( path );
106 
107  Q_FOREACH ( const QString& item, dir.entryList( QDir::Dirs | QDir::NoDotAndDotDot ) )
108  {
109  if ( mCancelled )
110  return;
111 
112  QString newPath = dir.path() + '/' + item;
113  loadPath( newPath );
114  // QgsDebugMsg( QString( "added path: %1" ).arg( newPath ) );
115  }
116  }
117 }
118 
119 void QgsSvgSelectorLoader::loadImages( const QString& path )
120 {
121  QDir dir( path );
122  Q_FOREACH ( const QString& item, dir.entryList( QStringList( "*.svg" ), QDir::Files ) )
123  {
124  if ( mCancelled )
125  return;
126 
127  // TODO test if it is correct SVG
128  QString svgPath = dir.path() + '/' + item;
129  // QgsDebugMsg( QString( "adding svg: %1" ).arg( svgPath ) );
130 
131  // add it to the list of queued SVGs
132  mQueuedSvgs << svgPath;
133 
134  // we need to avoid spamming the model with notifications about new svgs, so foundSvgs
135  // is only emitted for blocks of SVGs (otherwise the view goes all flickery)
136  if ( mTimer.elapsed() > mTimerThreshold && !mQueuedSvgs.isEmpty() )
137  {
138  emit foundSvgs( mQueuedSvgs );
139  mQueuedSvgs.clear();
140 
141  // increase the timer threshold - this ensures that the first lots of svgs loaded are added
142  // to the view quickly, but as the list grows new svgs are added at a slower rate.
143  // ie, good for initial responsiveness but avoid being spammy as the list grows.
144  if ( mTimerThreshold < 1000 )
145  mTimerThreshold *= 2;
146  mTimer.restart();
147  }
148  }
149 }
150 
151 
152 //
153 // QgsSvgGroupLoader
154 //
155 
156 QgsSvgGroupLoader::QgsSvgGroupLoader( QObject* parent )
157  : QThread( parent )
158  , mCancelled( false )
159 {
160 
161 }
162 
163 QgsSvgGroupLoader::~QgsSvgGroupLoader()
164 {
165  stop();
166 }
167 
168 void QgsSvgGroupLoader::run()
169 {
170  mCancelled = false;
171  mTraversedPaths.clear();
172 
173  while ( !mCancelled && !mParentPaths.isEmpty() )
174  {
175  QString parentPath = mParentPaths.takeFirst();
176  loadGroup( parentPath );
177  }
178 }
179 
180 void QgsSvgGroupLoader::stop()
181 {
182  mCancelled = true;
183  while ( isRunning() ) {}
184 }
185 
186 void QgsSvgGroupLoader::loadGroup( const QString& parentPath )
187 {
188  QDir parentDir( parentPath );
189 
190  //guard against circular symbolic links
191  QString canonicalPath = parentDir.canonicalPath();
192  if ( mTraversedPaths.contains( canonicalPath ) )
193  return;
194 
195  mTraversedPaths.insert( canonicalPath );
196 
197  Q_FOREACH ( const QString& item, parentDir.entryList( QDir::Dirs | QDir::NoDotAndDotDot ) )
198  {
199  if ( mCancelled )
200  return;
201 
202  emit foundPath( parentPath, item );
203  mParentPaths.append( parentDir.path() + '/' + item );
204  }
205 }
206 
208 
209 //,
210 // QgsSvgSelectorListModel
211 //
212 
214  : QAbstractListModel( parent )
215  , mSvgLoader( new QgsSvgSelectorLoader( this ) )
216 {
217  mSvgLoader->setPath( QString() );
218  connect( mSvgLoader, SIGNAL( foundSvgs( QStringList ) ), this, SLOT( addSvgs( QStringList ) ) );
219  mSvgLoader->start();
220 }
221 
222 QgsSvgSelectorListModel::QgsSvgSelectorListModel( QObject* parent, const QString& path )
223  : QAbstractListModel( parent )
224  , mSvgLoader( new QgsSvgSelectorLoader( this ) )
225 {
226  mSvgLoader->setPath( path );
227  connect( mSvgLoader, SIGNAL( foundSvgs( QStringList ) ), this, SLOT( addSvgs( QStringList ) ) );
228  mSvgLoader->start();
229 }
230 
231 int QgsSvgSelectorListModel::rowCount( const QModelIndex& parent ) const
232 {
233  Q_UNUSED( parent );
234  return mSvgFiles.count();
235 }
236 
237 QPixmap QgsSvgSelectorListModel::createPreview( const QString& entry ) const
238 {
239  // render SVG file
240  QColor fill, outline;
241  double outlineWidth, fillOpacity, outlineOpacity;
242  bool fillParam, fillOpacityParam, outlineParam, outlineWidthParam, outlineOpacityParam;
243  bool hasDefaultFillColor = false, hasDefaultFillOpacity = false, hasDefaultOutlineColor = false,
244  hasDefaultOutlineWidth = false, hasDefaultOutlineOpacity = false;
245  QgsSvgCache::instance()->containsParams( entry, fillParam, hasDefaultFillColor, fill,
246  fillOpacityParam, hasDefaultFillOpacity, fillOpacity,
247  outlineParam, hasDefaultOutlineColor, outline,
248  outlineWidthParam, hasDefaultOutlineWidth, outlineWidth,
249  outlineOpacityParam, hasDefaultOutlineOpacity, outlineOpacity );
250 
251  //if defaults not set in symbol, use these values
252  if ( !hasDefaultFillColor )
253  fill = QColor( 200, 200, 200 );
254  fill.setAlphaF( hasDefaultFillOpacity ? fillOpacity : 1.0 );
255  if ( !hasDefaultOutlineColor )
256  outline = Qt::black;
257  outline.setAlphaF( hasDefaultOutlineOpacity ? outlineOpacity : 1.0 );
258  if ( !hasDefaultOutlineWidth )
259  outlineWidth = 0.2;
260 
261  bool fitsInCache; // should always fit in cache at these sizes (i.e. under 559 px ^ 2, or half cache size)
262  const QImage& img = QgsSvgCache::instance()->svgAsImage( entry, 30.0, fill, outline, outlineWidth, 3.5 /*appr. 88 dpi*/, 1.0, fitsInCache );
263  return QPixmap::fromImage( img );
264 }
265 
266 QVariant QgsSvgSelectorListModel::data( const QModelIndex& index, int role ) const
267 {
268  QString entry = mSvgFiles.at( index.row() );
269 
270  if ( role == Qt::DecorationRole ) // icon
271  {
272  QPixmap pixmap;
273  if ( !QPixmapCache::find( entry, pixmap ) )
274  {
275  pixmap = createPreview( entry );
276  QPixmapCache::insert( entry, pixmap );
277  }
278 
279  return pixmap;
280  }
281  else if ( role == Qt::UserRole || role == Qt::ToolTipRole )
282  {
283  return entry;
284  }
285 
286  return QVariant();
287 }
288 
289 void QgsSvgSelectorListModel::addSvgs( const QStringList& svgs )
290 {
291  beginInsertRows( QModelIndex(), mSvgFiles.count(), mSvgFiles.count() + svgs.size() - 1 );
292  mSvgFiles.append( svgs );
293  endInsertRows();
294 }
295 
296 
297 
298 
299 
300 //--- QgsSvgSelectorGroupsModel
301 
303  : QStandardItemModel( parent )
304  , mLoader( new QgsSvgGroupLoader( this ) )
305 {
306  QStringList svgPaths = QgsApplication::svgPaths();
307  QStandardItem *parentItem = invisibleRootItem();
308  QStringList parentPaths;
309  parentPaths.reserve( svgPaths.size() );
310 
311  for ( int i = 0; i < svgPaths.size(); i++ )
312  {
313  QDir dir( svgPaths.at( i ) );
314  QStandardItem *baseGroup;
315 
316  if ( dir.path().contains( QgsApplication::pkgDataPath() ) )
317  {
318  baseGroup = new QStandardItem( tr( "App Symbols" ) );
319  }
320  else if ( dir.path().contains( QgsApplication::qgisSettingsDirPath() ) )
321  {
322  baseGroup = new QStandardItem( tr( "User Symbols" ) );
323  }
324  else
325  {
326  baseGroup = new QStandardItem( dir.dirName() );
327  }
328  baseGroup->setData( QVariant( svgPaths.at( i ) ) );
329  baseGroup->setEditable( false );
330  baseGroup->setCheckable( false );
331  baseGroup->setIcon( QgsApplication::style()->standardIcon( QStyle::SP_DirIcon ) );
332  baseGroup->setToolTip( dir.path() );
333  parentItem->appendRow( baseGroup );
334  parentPaths << svgPaths.at( i );
335  mPathItemHash.insert( svgPaths.at( i ), baseGroup );
336  QgsDebugMsg( QString( "SVG base path %1: %2" ).arg( i ).arg( baseGroup->data().toString() ) );
337  }
338  mLoader->setParentPaths( parentPaths );
339  connect( mLoader, SIGNAL( foundPath( QString, QString ) ), this, SLOT( addPath( QString, QString ) ) );
340  mLoader->start();
341 }
342 
344 {
345  mLoader->stop();
346 }
347 
348 void QgsSvgSelectorGroupsModel::addPath( const QString& parentPath, const QString& item )
349 {
350  QStandardItem* parentGroup = mPathItemHash.value( parentPath );
351  if ( !parentGroup )
352  return;
353 
354  QString fullPath = parentPath + '/' + item;
355  QStandardItem* group = new QStandardItem( item );
356  group->setData( QVariant( fullPath ) );
357  group->setEditable( false );
358  group->setCheckable( false );
359  group->setToolTip( fullPath );
360  group->setIcon( QgsApplication::style()->standardIcon( QStyle::SP_DirIcon ) );
361  parentGroup->appendRow( group );
362  mPathItemHash.insert( fullPath, group );
363 }
364 
365 
366 //-- QgsSvgSelectorWidget
367 
369  : QWidget( parent )
370 {
371  // TODO: in-code gui setup with option to vertically or horizontally stack SVG groups/images widgets
372  setupUi( this );
373 
374  mGroupsTreeView->setHeaderHidden( true );
375  populateList();
376 
377  connect( mImagesListView->selectionModel(), SIGNAL( currentChanged( const QModelIndex&, const QModelIndex& ) ),
378  this, SLOT( svgSelectionChanged( const QModelIndex& ) ) );
379  connect( mGroupsTreeView->selectionModel(), SIGNAL( currentChanged( const QModelIndex&, const QModelIndex& ) ),
380  this, SLOT( populateIcons( const QModelIndex& ) ) );
381 
382  QSettings settings;
383  bool useRelativePath = ( QgsProject::instance()->readBoolEntry( QStringLiteral( "Paths" ), QStringLiteral( "/Absolute" ), false )
384  || settings.value( QStringLiteral( "/Windows/SvgSelectorWidget/RelativePath" ) ).toBool() );
385  mRelativePathChkBx->setChecked( useRelativePath );
386 }
387 
389 {
390  QSettings settings;
391  settings.setValue( QStringLiteral( "/Windows/SvgSelectorWidget/RelativePath" ), mRelativePathChkBx->isChecked() );
392 }
393 
394 void QgsSvgSelectorWidget::setSvgPath( const QString& svgPath )
395 {
396  QString updatedPath( QLatin1String( "" ) );
397 
398  // skip possible urls, excepting those that may locally resolve
399  if ( !svgPath.contains( QLatin1String( "://" ) ) || ( svgPath.contains( QLatin1String( "file://" ), Qt::CaseInsensitive ) ) )
400  {
401  QString resolvedPath = QgsSymbolLayerUtils::symbolNameToPath( svgPath.trimmed() );
402  if ( !resolvedPath.isNull() )
403  {
404  updatedPath = resolvedPath;
405  }
406  }
407 
408  mCurrentSvgPath = updatedPath;
409 
410  mFileLineEdit->blockSignals( true );
411  mFileLineEdit->setText( updatedPath );
412  mFileLineEdit->blockSignals( false );
413 
414  mImagesListView->selectionModel()->blockSignals( true );
415  QAbstractItemModel* m = mImagesListView->model();
416  QItemSelectionModel* selModel = mImagesListView->selectionModel();
417  for ( int i = 0; i < m->rowCount(); i++ )
418  {
419  QModelIndex idx( m->index( i, 0 ) );
420  if ( m->data( idx ).toString() == svgPath )
421  {
422  selModel->select( idx, QItemSelectionModel::SelectCurrent );
423  selModel->setCurrentIndex( idx, QItemSelectionModel::SelectCurrent );
424  mImagesListView->scrollTo( idx );
425  break;
426  }
427  }
428  mImagesListView->selectionModel()->blockSignals( false );
429 }
430 
432 {
433  if ( mRelativePathChkBx->isChecked() )
434  return currentSvgPathToName();
435 
436  return mCurrentSvgPath;
437 }
438 
440 {
441  return QgsSymbolLayerUtils::symbolPathToName( mCurrentSvgPath );
442 }
443 
444 void QgsSvgSelectorWidget::updateCurrentSvgPath( const QString& svgPath )
445 {
446  mCurrentSvgPath = svgPath;
447  emit svgSelected( currentSvgPath() );
448 }
449 
450 void QgsSvgSelectorWidget::svgSelectionChanged( const QModelIndex& idx )
451 {
452  QString filePath = idx.data( Qt::UserRole ).toString();
453  mFileLineEdit->setText( filePath );
454  updateCurrentSvgPath( filePath );
455 }
456 
457 void QgsSvgSelectorWidget::populateIcons( const QModelIndex& idx )
458 {
459  QString path = idx.data( Qt::UserRole + 1 ).toString();
460 
461  QAbstractItemModel* oldModel = mImagesListView->model();
462  QgsSvgSelectorListModel* m = new QgsSvgSelectorListModel( mImagesListView, path );
463  mImagesListView->setModel( m );
464  delete oldModel; //explicitly delete old model to force any background threads to stop
465 
466  connect( mImagesListView->selectionModel(), SIGNAL( currentChanged( const QModelIndex&, const QModelIndex& ) ),
467  this, SLOT( svgSelectionChanged( const QModelIndex& ) ) );
468 
469 }
470 
471 void QgsSvgSelectorWidget::on_mFilePushButton_clicked()
472 {
473  QSettings settings;
474  QString openDir = settings.value( QStringLiteral( "/UI/lastSVGMarkerDir" ), QDir::homePath() ).toString();
475 
476  QString lineEditText = mFileLineEdit->text();
477  if ( !lineEditText.isEmpty() )
478  {
479  QFileInfo openDirFileInfo( lineEditText );
480  openDir = openDirFileInfo.path();
481  }
482 
483  QString file = QFileDialog::getOpenFileName( nullptr,
484  tr( "Select SVG file" ),
485  openDir,
486  tr( "SVG files" ) + " (*.svg *.SVG)" );
487 
488  activateWindow(); // return window focus
489 
490  if ( file.isNull() )
491  return;
492 
493  QFileInfo fi( file );
494  if ( !fi.exists() || !fi.isReadable() )
495  {
496  updateCurrentSvgPath( QString() );
497  updateLineEditFeedback( false );
498  return;
499  }
500  settings.setValue( QStringLiteral( "/UI/lastSVGMarkerDir" ), fi.absolutePath() );
501  mFileLineEdit->setText( file );
502  updateCurrentSvgPath( file );
503 }
504 
505 void QgsSvgSelectorWidget::updateLineEditFeedback( bool ok, const QString& tip )
506 {
507  // draw red text for path field if invalid (path can't be resolved)
508  mFileLineEdit->setStyleSheet( QString( !ok ? "QLineEdit{ color: rgb(225, 0, 0); }" : "" ) );
509  mFileLineEdit->setToolTip( !ok ? tr( "File not found" ) : tip );
510 }
511 
512 void QgsSvgSelectorWidget::on_mFileLineEdit_textChanged( const QString& text )
513 {
514  QString resolvedPath = QgsSymbolLayerUtils::symbolNameToPath( text );
515  bool validSVG = !resolvedPath.isNull();
516 
517  updateLineEditFeedback( validSVG, resolvedPath );
518  updateCurrentSvgPath( validSVG ? resolvedPath : QString() );
519 }
520 
522 {
523  QgsSvgSelectorGroupsModel* g = new QgsSvgSelectorGroupsModel( mGroupsTreeView );
524  mGroupsTreeView->setModel( g );
525  // Set the tree expanded at the first level
526  int rows = g->rowCount( g->indexFromItem( g->invisibleRootItem() ) );
527  for ( int i = 0; i < rows; i++ )
528  {
529  mGroupsTreeView->setExpanded( g->indexFromItem( g->item( i ) ), true );
530  }
531 
532  // Initally load the icons in the List view without any grouping
533  QAbstractItemModel* oldModel = mImagesListView->model();
534  QgsSvgSelectorListModel* m = new QgsSvgSelectorListModel( mImagesListView );
535  mImagesListView->setModel( m );
536  delete oldModel; //explicitly delete old model to force any background threads to stop
537 }
538 
539 //-- QgsSvgSelectorDialog
540 
541 QgsSvgSelectorDialog::QgsSvgSelectorDialog( QWidget *parent, Qt::WindowFlags fl,
542  QDialogButtonBox::StandardButtons buttons,
543  Qt::Orientation orientation )
544  : QDialog( parent, fl )
545 {
546  // TODO: pass 'orientation' to QgsSvgSelectorWidget for customizing its layout, once implemented
547  Q_UNUSED( orientation );
548 
549  // create buttonbox
550  mButtonBox = new QDialogButtonBox( buttons, orientation, this );
551  connect( mButtonBox, SIGNAL( accepted() ), this, SLOT( accept() ) );
552  connect( mButtonBox, SIGNAL( rejected() ), this, SLOT( reject() ) );
553 
554  setMinimumSize( 480, 320 );
555 
556  // dialog's layout
557  mLayout = new QVBoxLayout();
558  mSvgSelector = new QgsSvgSelectorWidget( this );
559  mLayout->addWidget( mSvgSelector );
560 
561  mLayout->addWidget( mButtonBox );
562  setLayout( mLayout );
563 
564  QSettings settings;
565  restoreGeometry( settings.value( QStringLiteral( "/Windows/SvgSelectorDialog/geometry" ) ).toByteArray() );
566 }
567 
569 {
570  QSettings settings;
571  settings.setValue( QStringLiteral( "/Windows/SvgSelectorDialog/geometry" ), saveGeometry() );
572 }
573 
QgsSvgSelectorWidget * mSvgSelector
QgsSvgSelectorDialog(QWidget *parent=nullptr, Qt::WindowFlags fl=QgisGui::ModalDialogFlags, QDialogButtonBox::StandardButtons buttons=QDialogButtonBox::Close|QDialogButtonBox::Ok, Qt::Orientation orientation=Qt::Horizontal)
Constructor for QgsSvgSelectorDialog.
static unsigned index
QString currentSvgPathToName() const
QgsSvgSelectorWidget(QWidget *parent=nullptr)
static QString qgisSettingsDirPath()
Returns the path to the settings directory in user&#39;s home dir.
bool readBoolEntry(const QString &scope, const QString &key, bool def=false, bool *ok=nullptr) const
A model for displaying SVG files with a preview icon.
#define QgsDebugMsg(str)
Definition: qgslogger.h:33
QgsSvgSelectorGroupsModel(QObject *parent)
void containsParams(const QString &path, bool &hasFillParam, QColor &defaultFillColor, bool &hasOutlineParam, QColor &defaultOutlineColor, bool &hasOutlineWidthParam, double &defaultOutlineWidth) const
Tests if an svg file contains parameters for fill, outline color, outline width.
static QString symbolNameToPath(QString name)
Get symbol&#39;s path from its name.
QVariant data(const QModelIndex &index, int role=Qt::DisplayRole) const override
static QgsSvgCache * instance()
Definition: qgssvgcache.cpp:97
int rowCount(const QModelIndex &parent=QModelIndex()) const override
const QImage & svgAsImage(const QString &file, double size, const QColor &fill, const QColor &outline, double outlineWidth, double widthScaleFactor, double rasterScaleFactor, bool &fitsInCache)
Get SVG as QImage.
QDialogButtonBox * mButtonBox
void svgSelected(const QString &path)
void setSvgPath(const QString &svgPath)
Accepts absolute and relative paths.
static QString pkgDataPath()
Returns the common root path of all application data directories.
QgsSvgSelectorListModel(QObject *parent)
Constructor for QgsSvgSelectorListModel.
A model for displaying SVG search paths.
static QString symbolPathToName(QString path)
Get symbols&#39;s name from its path.
static QgsProject * instance()
Returns the QgsProject singleton instance.
Definition: qgsproject.cpp:348
static QStringList svgPaths()
Returns the pathes to svg directories.