Skip to content

Commit c60c4f7

Browse files
committed
Fix SVG preview blocks QGIS (fix #14255)
Now SVG preview loading occurs in a background thread so that dialogs can open instantly
1 parent d3f8763 commit c60c4f7

File tree

5 files changed

+420
-216
lines changed

5 files changed

+420
-216
lines changed

src/core/qgsapplication.cpp

+1-1
Original file line numberDiff line numberDiff line change
@@ -715,7 +715,7 @@ QStringList QgsApplication::svgPaths()
715715
}
716716

717717
myPathList << ABISYM( mDefaultSvgPaths );
718-
return myPathList;
718+
return myPathList.toSet().toList();
719719
}
720720

721721
/*!

src/gui/symbology-ng/qgssvgselectorwidget.cpp

+243-47
Original file line numberDiff line numberDiff line change
@@ -32,29 +32,221 @@
3232
#include <QStyle>
3333
#include <QTime>
3434

35+
// QgsSvgSelectorLoader
3536

36-
//--- QgsSvgSelectorListModel
37+
///@cond PRIVATE
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+
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+
mCancelled = true;
72+
while ( isRunning() ) {}
73+
}
74+
75+
void QgsSvgSelectorLoader::loadPath( const QString& path )
76+
{
77+
if ( mCancelled )
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 ( mCancelled )
88+
return;
89+
90+
loadPath( svgPath );
91+
}
92+
}
93+
else
94+
{
95+
loadImages( path );
96+
97+
QDir dir( path );
98+
Q_FOREACH ( const QString& item, dir.entryList( QDir::Dirs | QDir::NoDotAndDotDot ) )
99+
{
100+
if ( mCancelled )
101+
return;
102+
103+
QString newPath = dir.path() + '/' + item;
104+
loadPath( newPath );
105+
// QgsDebugMsg( QString( "added path: %1" ).arg( newPath ) );
106+
}
107+
}
108+
}
109+
110+
void QgsSvgSelectorLoader::loadImages( const QString& path )
111+
{
112+
QDir dir( path );
113+
Q_FOREACH ( const QString& item, dir.entryList( QStringList( "*.svg" ), QDir::Files ) )
114+
{
115+
if ( mCancelled )
116+
return;
117+
118+
// TODO test if it is correct SVG
119+
QString svgPath = dir.path() + '/' + item;
120+
// QgsDebugMsg( QString( "adding svg: %1" ).arg( svgPath ) );
121+
122+
// add it to the list of queued SVGs
123+
mQueuedSvgs << svgPath;
124+
125+
// we need to avoid spamming the model with notifications about new svgs, so foundSvgs
126+
// is only emitted for blocks of SVGs (otherwise the view goes all flickery)
127+
if ( mTimer.elapsed() > mTimerThreshold )
128+
{
129+
emit foundSvgs( mQueuedSvgs );
130+
mQueuedSvgs.clear();
131+
132+
// increase the timer threshold - this ensures that the first lots of svgs loaded are added
133+
// to the view quickly, but as the list grows new svgs are added at a slower rate.
134+
// ie, good for initial responsiveness but avoid being spammy as the list grows.
135+
if ( mTimerThreshold < 1000 )
136+
mTimerThreshold *= 2;
137+
mTimer.restart();
138+
}
139+
}
140+
}
141+
142+
143+
//
144+
// QgsSvgGroupLoader
145+
//
146+
147+
QgsSvgGroupLoader::QgsSvgGroupLoader( QObject* parent )
148+
: QThread( parent )
149+
, mCancelled( false )
150+
{
151+
152+
}
153+
154+
QgsSvgGroupLoader::~QgsSvgGroupLoader()
155+
{
156+
stop();
157+
}
158+
159+
void QgsSvgGroupLoader::run()
160+
{
161+
mCancelled = false;
162+
163+
while ( !mCancelled && !mParentPaths.isEmpty() )
164+
{
165+
QString parentPath = mParentPaths.takeFirst();
166+
loadGroup( parentPath );
167+
}
168+
}
169+
170+
void QgsSvgGroupLoader::stop()
171+
{
172+
mCancelled = true;
173+
while ( isRunning() ) {}
174+
}
175+
176+
void QgsSvgGroupLoader::loadGroup( const QString& parentPath )
177+
{
178+
QDir parentDir( parentPath );
179+
180+
Q_FOREACH ( const QString& item, parentDir.entryList( QDir::Dirs | QDir::NoDotAndDotDot ) )
181+
{
182+
if ( mCancelled )
183+
return;
184+
185+
emit foundPath( parentPath, item );
186+
mParentPaths.append( parentDir.path() + '/' + item );
187+
}
188+
}
189+
190+
///@endcond
191+
192+
//,
193+
// QgsSvgSelectorListModel
194+
//
37195

38196
QgsSvgSelectorListModel::QgsSvgSelectorListModel( QObject* parent )
39197
: QAbstractListModel( parent )
198+
, mSvgLoader( new QgsSvgSelectorLoader( this ) )
40199
{
41-
mSvgFiles = QgsSymbolLayerUtils::listSvgFiles();
200+
mSvgLoader->setPath( QString() );
201+
connect( mSvgLoader, SIGNAL( foundSvgs( QStringList ) ), this, SLOT( addSvgs( QStringList ) ) );
202+
mSvgLoader->start();
42203
}
43204

44-
// Constructor to create model for icons in a specific path
45205
QgsSvgSelectorListModel::QgsSvgSelectorListModel( QObject* parent, const QString& path )
46206
: QAbstractListModel( parent )
207+
, mSvgLoader( new QgsSvgSelectorLoader( this ) )
47208
{
48-
mSvgFiles = QgsSymbolLayerUtils::listSvgFilesAt( path );
209+
mSvgLoader->setPath( path );
210+
connect( mSvgLoader, SIGNAL( foundSvgs( QStringList ) ), this, SLOT( addSvgs( QStringList ) ) );
211+
mSvgLoader->start();
49212
}
50213

51-
int QgsSvgSelectorListModel::rowCount( const QModelIndex & parent ) const
214+
int QgsSvgSelectorListModel::rowCount( const QModelIndex& parent ) const
52215
{
53216
Q_UNUSED( parent );
54217
return mSvgFiles.count();
55218
}
56219

57-
QVariant QgsSvgSelectorListModel::data( const QModelIndex & index, int role ) const
220+
QPixmap QgsSvgSelectorListModel::createPreview( const QString& entry ) const
221+
{
222+
// render SVG file
223+
QColor fill, outline;
224+
double outlineWidth, fillOpacity, outlineOpacity;
225+
bool fillParam, fillOpacityParam, outlineParam, outlineWidthParam, outlineOpacityParam;
226+
bool hasDefaultFillColor = false, hasDefaultFillOpacity = false, hasDefaultOutlineColor = false,
227+
hasDefaultOutlineWidth = false, hasDefaultOutlineOpacity = false;
228+
QgsSvgCache::instance()->containsParams( entry, fillParam, hasDefaultFillColor, fill,
229+
fillOpacityParam, hasDefaultFillOpacity, fillOpacity,
230+
outlineParam, hasDefaultOutlineColor, outline,
231+
outlineWidthParam, hasDefaultOutlineWidth, outlineWidth,
232+
outlineOpacityParam, hasDefaultOutlineOpacity, outlineOpacity );
233+
234+
//if defaults not set in symbol, use these values
235+
if ( !hasDefaultFillColor )
236+
fill = QColor( 200, 200, 200 );
237+
fill.setAlphaF( hasDefaultFillOpacity ? fillOpacity : 1.0 );
238+
if ( !hasDefaultOutlineColor )
239+
outline = Qt::black;
240+
outline.setAlphaF( hasDefaultOutlineOpacity ? outlineOpacity : 1.0 );
241+
if ( !hasDefaultOutlineWidth )
242+
outlineWidth = 0.2;
243+
244+
bool fitsInCache; // should always fit in cache at these sizes (i.e. under 559 px ^ 2, or half cache size)
245+
const QImage& img = QgsSvgCache::instance()->svgAsImage( entry, 30.0, fill, outline, outlineWidth, 3.5 /*appr. 88 dpi*/, 1.0, fitsInCache );
246+
return QPixmap::fromImage( img );
247+
}
248+
249+
QVariant QgsSvgSelectorListModel::data( const QModelIndex& index, int role ) const
58250
{
59251
QString entry = mSvgFiles.at( index.row() );
60252

@@ -63,31 +255,7 @@ QVariant QgsSvgSelectorListModel::data( const QModelIndex & index, int role ) co
63255
QPixmap pixmap;
64256
if ( !QPixmapCache::find( entry, pixmap ) )
65257
{
66-
// render SVG file
67-
QColor fill, outline;
68-
double outlineWidth, fillOpacity, outlineOpacity;
69-
bool fillParam, fillOpacityParam, outlineParam, outlineWidthParam, outlineOpacityParam;
70-
bool hasDefaultFillColor = false, hasDefaultFillOpacity = false, hasDefaultOutlineColor = false,
71-
hasDefaultOutlineWidth = false, hasDefaultOutlineOpacity = false;
72-
QgsSvgCache::instance()->containsParams( entry, fillParam, hasDefaultFillColor, fill,
73-
fillOpacityParam, hasDefaultFillOpacity, fillOpacity,
74-
outlineParam, hasDefaultOutlineColor, outline,
75-
outlineWidthParam, hasDefaultOutlineWidth, outlineWidth,
76-
outlineOpacityParam, hasDefaultOutlineOpacity, outlineOpacity );
77-
78-
//if defaults not set in symbol, use these values
79-
if ( !hasDefaultFillColor )
80-
fill = QColor( 200, 200, 200 );
81-
fill.setAlphaF( hasDefaultFillOpacity ? fillOpacity : 1.0 );
82-
if ( !hasDefaultOutlineColor )
83-
outline = Qt::black;
84-
outline.setAlphaF( hasDefaultOutlineOpacity ? outlineOpacity : 1.0 );
85-
if ( !hasDefaultOutlineWidth )
86-
outlineWidth = 0.2;
87-
88-
bool fitsInCache; // should always fit in cache at these sizes (i.e. under 559 px ^ 2, or half cache size)
89-
const QImage& img = QgsSvgCache::instance()->svgAsImage( entry, 30.0, fill, outline, outlineWidth, 3.5 /*appr. 88 dpi*/, 1.0, fitsInCache );
90-
pixmap = QPixmap::fromImage( img );
258+
pixmap = createPreview( entry );
91259
QPixmapCache::insert( entry, pixmap );
92260
}
93261

@@ -101,18 +269,30 @@ QVariant QgsSvgSelectorListModel::data( const QModelIndex & index, int role ) co
101269
return QVariant();
102270
}
103271

272+
void QgsSvgSelectorListModel::addSvgs( const QStringList& svgs )
273+
{
274+
beginInsertRows( QModelIndex(), mSvgFiles.count(), mSvgFiles.count() + svgs.size() - 1 );
275+
mSvgFiles.append( svgs );
276+
endInsertRows();
277+
}
278+
279+
280+
281+
104282

105283
//--- QgsSvgSelectorGroupsModel
106284

107285
QgsSvgSelectorGroupsModel::QgsSvgSelectorGroupsModel( QObject* parent )
108286
: QStandardItemModel( parent )
287+
, mLoader( new QgsSvgGroupLoader( this ) )
109288
{
110289
QStringList svgPaths = QgsApplication::svgPaths();
111290
QStandardItem *parentItem = invisibleRootItem();
291+
QStringList parentPaths;
112292

113293
for ( int i = 0; i < svgPaths.size(); i++ )
114294
{
115-
QDir dir( svgPaths[i] );
295+
QDir dir( svgPaths.at( i ) );
116296
QStandardItem *baseGroup;
117297

118298
if ( dir.path().contains( QgsApplication::pkgDataPath() ) )
@@ -127,31 +307,41 @@ QgsSvgSelectorGroupsModel::QgsSvgSelectorGroupsModel( QObject* parent )
127307
{
128308
baseGroup = new QStandardItem( dir.dirName() );
129309
}
130-
baseGroup->setData( QVariant( svgPaths[i] ) );
310+
baseGroup->setData( QVariant( svgPaths.at( i ) ) );
131311
baseGroup->setEditable( false );
132312
baseGroup->setCheckable( false );
133313
baseGroup->setIcon( QgsApplication::style()->standardIcon( QStyle::SP_DirIcon ) );
134314
baseGroup->setToolTip( dir.path() );
135315
parentItem->appendRow( baseGroup );
136-
createTree( baseGroup );
316+
parentPaths << svgPaths.at( i );
317+
mPathItemHash.insert( svgPaths.at( i ), baseGroup );
137318
QgsDebugMsg( QString( "SVG base path %1: %2" ).arg( i ).arg( baseGroup->data().toString() ) );
138319
}
320+
mLoader->setParentPaths( parentPaths );
321+
connect( mLoader, SIGNAL( foundPath( QString, QString ) ), this, SLOT( addPath( QString, QString ) ) );
322+
mLoader->start();
139323
}
140324

141-
void QgsSvgSelectorGroupsModel::createTree( QStandardItem* &parentGroup )
325+
QgsSvgSelectorGroupsModel::~QgsSvgSelectorGroupsModel()
142326
{
143-
QDir parentDir( parentGroup->data().toString() );
144-
Q_FOREACH ( const QString& item, parentDir.entryList( QDir::Dirs | QDir::NoDotAndDotDot ) )
145-
{
146-
QStandardItem* group = new QStandardItem( item );
147-
group->setData( QVariant( parentDir.path() + '/' + item ) );
148-
group->setEditable( false );
149-
group->setCheckable( false );
150-
group->setToolTip( parentDir.path() + '/' + item );
151-
group->setIcon( QgsApplication::style()->standardIcon( QStyle::SP_DirIcon ) );
152-
parentGroup->appendRow( group );
153-
createTree( group );
154-
}
327+
mLoader->stop();
328+
}
329+
330+
void QgsSvgSelectorGroupsModel::addPath( const QString& parentPath, const QString& item )
331+
{
332+
QStandardItem* parentGroup = mPathItemHash.value( parentPath );
333+
if ( !parentGroup )
334+
return;
335+
336+
QString fullPath = parentPath + '/' + item;
337+
QStandardItem* group = new QStandardItem( item );
338+
group->setData( QVariant( fullPath ) );
339+
group->setEditable( false );
340+
group->setCheckable( false );
341+
group->setToolTip( fullPath );
342+
group->setIcon( QgsApplication::style()->standardIcon( QStyle::SP_DirIcon ) );
343+
parentGroup->appendRow( group );
344+
mPathItemHash.insert( fullPath, group );
155345
}
156346

157347

@@ -250,11 +440,14 @@ void QgsSvgSelectorWidget::populateIcons( const QModelIndex& idx )
250440
{
251441
QString path = idx.data( Qt::UserRole + 1 ).toString();
252442

443+
QAbstractItemModel* oldModel = mImagesListView->model();
253444
QgsSvgSelectorListModel* m = new QgsSvgSelectorListModel( mImagesListView, path );
254445
mImagesListView->setModel( m );
446+
delete oldModel; //explicitly delete old model to force any background threads to stop
255447

256448
connect( mImagesListView->selectionModel(), SIGNAL( currentChanged( const QModelIndex&, const QModelIndex& ) ),
257449
this, SLOT( svgSelectionChanged( const QModelIndex& ) ) );
450+
258451
}
259452

260453
void QgsSvgSelectorWidget::on_mFilePushButton_clicked()
@@ -319,8 +512,10 @@ void QgsSvgSelectorWidget::populateList()
319512
}
320513

321514
// Initally load the icons in the List view without any grouping
515+
QAbstractItemModel* oldModel = mImagesListView->model();
322516
QgsSvgSelectorListModel* m = new QgsSvgSelectorListModel( mImagesListView );
323517
mImagesListView->setModel( m );
518+
delete oldModel; //explicitly delete old model to force any background threads to stop
324519
}
325520

326521
//-- QgsSvgSelectorDialog
@@ -357,3 +552,4 @@ QgsSvgSelectorDialog::~QgsSvgSelectorDialog()
357552
QSettings settings;
358553
settings.setValue( "/Windows/SvgSelectorDialog/geometry", saveGeometry() );
359554
}
555+

0 commit comments

Comments
 (0)