Skip to content

Commit

Permalink
Fix SVG preview blocks QGIS (fix #14255)
Browse files Browse the repository at this point in the history
Now SVG preview loading occurs in a background thread so that
dialogs can open instantly
  • Loading branch information
nyalldawson committed Oct 4, 2016
1 parent d3f8763 commit c60c4f7
Show file tree
Hide file tree
Showing 5 changed files with 420 additions and 216 deletions.
2 changes: 1 addition & 1 deletion src/core/qgsapplication.cpp
Expand Up @@ -715,7 +715,7 @@ QStringList QgsApplication::svgPaths()
}

myPathList << ABISYM( mDefaultSvgPaths );
return myPathList;
return myPathList.toSet().toList();
}

/*!
Expand Down
290 changes: 243 additions & 47 deletions src/gui/symbology-ng/qgssvgselectorwidget.cpp
Expand Up @@ -32,29 +32,221 @@
#include <QStyle>
#include <QTime>

// QgsSvgSelectorLoader

//--- QgsSvgSelectorListModel
///@cond PRIVATE
QgsSvgSelectorLoader::QgsSvgSelectorLoader( QObject* parent )
: QThread( parent )
, mCancelled( false )
, mTimerThreshold( 0 )
{
}

QgsSvgSelectorLoader::~QgsSvgSelectorLoader()
{
stop();
}

void QgsSvgSelectorLoader::run()
{
mCancelled = false;
mQueuedSvgs.clear();

// start with a small initial timeout (ms)
mTimerThreshold = 10;
mTimer.start();

loadPath( mPath );

if ( !mQueuedSvgs.isEmpty() )
{
// make sure we notify model of any remaining queued svgs (ie svgs added since last foundSvgs() signal was emitted)
emit foundSvgs( mQueuedSvgs );
}
mQueuedSvgs.clear();
}

void QgsSvgSelectorLoader::stop()
{
mCancelled = true;
while ( isRunning() ) {}
}

void QgsSvgSelectorLoader::loadPath( const QString& path )
{
if ( mCancelled )
return;

// QgsDebugMsg( QString( "loading path: %1" ).arg( path ) );

if ( path.isEmpty() )
{
QStringList svgPaths = QgsApplication::svgPaths();
Q_FOREACH ( const QString& svgPath, svgPaths )
{
if ( mCancelled )
return;

loadPath( svgPath );
}
}
else
{
loadImages( path );

QDir dir( path );
Q_FOREACH ( const QString& item, dir.entryList( QDir::Dirs | QDir::NoDotAndDotDot ) )
{
if ( mCancelled )
return;

QString newPath = dir.path() + '/' + item;
loadPath( newPath );
// QgsDebugMsg( QString( "added path: %1" ).arg( newPath ) );
}
}
}

void QgsSvgSelectorLoader::loadImages( const QString& path )
{
QDir dir( path );
Q_FOREACH ( const QString& item, dir.entryList( QStringList( "*.svg" ), QDir::Files ) )
{
if ( mCancelled )
return;

// TODO test if it is correct SVG
QString svgPath = dir.path() + '/' + item;
// QgsDebugMsg( QString( "adding svg: %1" ).arg( svgPath ) );

// add it to the list of queued SVGs
mQueuedSvgs << svgPath;

// we need to avoid spamming the model with notifications about new svgs, so foundSvgs
// is only emitted for blocks of SVGs (otherwise the view goes all flickery)
if ( mTimer.elapsed() > mTimerThreshold )
{
emit foundSvgs( mQueuedSvgs );
mQueuedSvgs.clear();

// increase the timer threshold - this ensures that the first lots of svgs loaded are added
// to the view quickly, but as the list grows new svgs are added at a slower rate.
// ie, good for initial responsiveness but avoid being spammy as the list grows.
if ( mTimerThreshold < 1000 )
mTimerThreshold *= 2;
mTimer.restart();
}
}
}


//
// QgsSvgGroupLoader
//

QgsSvgGroupLoader::QgsSvgGroupLoader( QObject* parent )
: QThread( parent )
, mCancelled( false )
{

}

QgsSvgGroupLoader::~QgsSvgGroupLoader()
{
stop();
}

void QgsSvgGroupLoader::run()
{
mCancelled = false;

while ( !mCancelled && !mParentPaths.isEmpty() )
{
QString parentPath = mParentPaths.takeFirst();
loadGroup( parentPath );
}
}

void QgsSvgGroupLoader::stop()
{
mCancelled = true;
while ( isRunning() ) {}
}

void QgsSvgGroupLoader::loadGroup( const QString& parentPath )
{
QDir parentDir( parentPath );

Q_FOREACH ( const QString& item, parentDir.entryList( QDir::Dirs | QDir::NoDotAndDotDot ) )
{
if ( mCancelled )
return;

emit foundPath( parentPath, item );
mParentPaths.append( parentDir.path() + '/' + item );
}
}

///@endcond

//,
// QgsSvgSelectorListModel
//

QgsSvgSelectorListModel::QgsSvgSelectorListModel( QObject* parent )
: QAbstractListModel( parent )
, mSvgLoader( new QgsSvgSelectorLoader( this ) )
{
mSvgFiles = QgsSymbolLayerUtils::listSvgFiles();
mSvgLoader->setPath( QString() );
connect( mSvgLoader, SIGNAL( foundSvgs( QStringList ) ), this, SLOT( addSvgs( QStringList ) ) );
mSvgLoader->start();
}

// Constructor to create model for icons in a specific path
QgsSvgSelectorListModel::QgsSvgSelectorListModel( QObject* parent, const QString& path )
: QAbstractListModel( parent )
, mSvgLoader( new QgsSvgSelectorLoader( this ) )
{
mSvgFiles = QgsSymbolLayerUtils::listSvgFilesAt( path );
mSvgLoader->setPath( path );
connect( mSvgLoader, SIGNAL( foundSvgs( QStringList ) ), this, SLOT( addSvgs( QStringList ) ) );
mSvgLoader->start();
}

int QgsSvgSelectorListModel::rowCount( const QModelIndex & parent ) const
int QgsSvgSelectorListModel::rowCount( const QModelIndex& parent ) const
{
Q_UNUSED( parent );
return mSvgFiles.count();
}

QVariant QgsSvgSelectorListModel::data( const QModelIndex & index, int role ) const
QPixmap QgsSvgSelectorListModel::createPreview( const QString& entry ) const
{
// render SVG file
QColor fill, outline;
double outlineWidth, fillOpacity, outlineOpacity;
bool fillParam, fillOpacityParam, outlineParam, outlineWidthParam, outlineOpacityParam;
bool hasDefaultFillColor = false, hasDefaultFillOpacity = false, hasDefaultOutlineColor = false,
hasDefaultOutlineWidth = false, hasDefaultOutlineOpacity = false;
QgsSvgCache::instance()->containsParams( entry, fillParam, hasDefaultFillColor, fill,
fillOpacityParam, hasDefaultFillOpacity, fillOpacity,
outlineParam, hasDefaultOutlineColor, outline,
outlineWidthParam, hasDefaultOutlineWidth, outlineWidth,
outlineOpacityParam, hasDefaultOutlineOpacity, outlineOpacity );

//if defaults not set in symbol, use these values
if ( !hasDefaultFillColor )
fill = QColor( 200, 200, 200 );
fill.setAlphaF( hasDefaultFillOpacity ? fillOpacity : 1.0 );
if ( !hasDefaultOutlineColor )
outline = Qt::black;
outline.setAlphaF( hasDefaultOutlineOpacity ? outlineOpacity : 1.0 );
if ( !hasDefaultOutlineWidth )
outlineWidth = 0.2;

bool fitsInCache; // should always fit in cache at these sizes (i.e. under 559 px ^ 2, or half cache size)
const QImage& img = QgsSvgCache::instance()->svgAsImage( entry, 30.0, fill, outline, outlineWidth, 3.5 /*appr. 88 dpi*/, 1.0, fitsInCache );
return QPixmap::fromImage( img );
}

QVariant QgsSvgSelectorListModel::data( const QModelIndex& index, int role ) const
{
QString entry = mSvgFiles.at( index.row() );

Expand All @@ -63,31 +255,7 @@ QVariant QgsSvgSelectorListModel::data( const QModelIndex & index, int role ) co
QPixmap pixmap;
if ( !QPixmapCache::find( entry, pixmap ) )
{
// render SVG file
QColor fill, outline;
double outlineWidth, fillOpacity, outlineOpacity;
bool fillParam, fillOpacityParam, outlineParam, outlineWidthParam, outlineOpacityParam;
bool hasDefaultFillColor = false, hasDefaultFillOpacity = false, hasDefaultOutlineColor = false,
hasDefaultOutlineWidth = false, hasDefaultOutlineOpacity = false;
QgsSvgCache::instance()->containsParams( entry, fillParam, hasDefaultFillColor, fill,
fillOpacityParam, hasDefaultFillOpacity, fillOpacity,
outlineParam, hasDefaultOutlineColor, outline,
outlineWidthParam, hasDefaultOutlineWidth, outlineWidth,
outlineOpacityParam, hasDefaultOutlineOpacity, outlineOpacity );

//if defaults not set in symbol, use these values
if ( !hasDefaultFillColor )
fill = QColor( 200, 200, 200 );
fill.setAlphaF( hasDefaultFillOpacity ? fillOpacity : 1.0 );
if ( !hasDefaultOutlineColor )
outline = Qt::black;
outline.setAlphaF( hasDefaultOutlineOpacity ? outlineOpacity : 1.0 );
if ( !hasDefaultOutlineWidth )
outlineWidth = 0.2;

bool fitsInCache; // should always fit in cache at these sizes (i.e. under 559 px ^ 2, or half cache size)
const QImage& img = QgsSvgCache::instance()->svgAsImage( entry, 30.0, fill, outline, outlineWidth, 3.5 /*appr. 88 dpi*/, 1.0, fitsInCache );
pixmap = QPixmap::fromImage( img );
pixmap = createPreview( entry );
QPixmapCache::insert( entry, pixmap );
}

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

void QgsSvgSelectorListModel::addSvgs( const QStringList& svgs )
{
beginInsertRows( QModelIndex(), mSvgFiles.count(), mSvgFiles.count() + svgs.size() - 1 );
mSvgFiles.append( svgs );
endInsertRows();
}





//--- QgsSvgSelectorGroupsModel

QgsSvgSelectorGroupsModel::QgsSvgSelectorGroupsModel( QObject* parent )
: QStandardItemModel( parent )
, mLoader( new QgsSvgGroupLoader( this ) )
{
QStringList svgPaths = QgsApplication::svgPaths();
QStandardItem *parentItem = invisibleRootItem();
QStringList parentPaths;

for ( int i = 0; i < svgPaths.size(); i++ )
{
QDir dir( svgPaths[i] );
QDir dir( svgPaths.at( i ) );
QStandardItem *baseGroup;

if ( dir.path().contains( QgsApplication::pkgDataPath() ) )
Expand All @@ -127,31 +307,41 @@ QgsSvgSelectorGroupsModel::QgsSvgSelectorGroupsModel( QObject* parent )
{
baseGroup = new QStandardItem( dir.dirName() );
}
baseGroup->setData( QVariant( svgPaths[i] ) );
baseGroup->setData( QVariant( svgPaths.at( i ) ) );
baseGroup->setEditable( false );
baseGroup->setCheckable( false );
baseGroup->setIcon( QgsApplication::style()->standardIcon( QStyle::SP_DirIcon ) );
baseGroup->setToolTip( dir.path() );
parentItem->appendRow( baseGroup );
createTree( baseGroup );
parentPaths << svgPaths.at( i );
mPathItemHash.insert( svgPaths.at( i ), baseGroup );
QgsDebugMsg( QString( "SVG base path %1: %2" ).arg( i ).arg( baseGroup->data().toString() ) );
}
mLoader->setParentPaths( parentPaths );
connect( mLoader, SIGNAL( foundPath( QString, QString ) ), this, SLOT( addPath( QString, QString ) ) );
mLoader->start();
}

void QgsSvgSelectorGroupsModel::createTree( QStandardItem* &parentGroup )
QgsSvgSelectorGroupsModel::~QgsSvgSelectorGroupsModel()
{
QDir parentDir( parentGroup->data().toString() );
Q_FOREACH ( const QString& item, parentDir.entryList( QDir::Dirs | QDir::NoDotAndDotDot ) )
{
QStandardItem* group = new QStandardItem( item );
group->setData( QVariant( parentDir.path() + '/' + item ) );
group->setEditable( false );
group->setCheckable( false );
group->setToolTip( parentDir.path() + '/' + item );
group->setIcon( QgsApplication::style()->standardIcon( QStyle::SP_DirIcon ) );
parentGroup->appendRow( group );
createTree( group );
}
mLoader->stop();
}

void QgsSvgSelectorGroupsModel::addPath( const QString& parentPath, const QString& item )
{
QStandardItem* parentGroup = mPathItemHash.value( parentPath );
if ( !parentGroup )
return;

QString fullPath = parentPath + '/' + item;
QStandardItem* group = new QStandardItem( item );
group->setData( QVariant( fullPath ) );
group->setEditable( false );
group->setCheckable( false );
group->setToolTip( fullPath );
group->setIcon( QgsApplication::style()->standardIcon( QStyle::SP_DirIcon ) );
parentGroup->appendRow( group );
mPathItemHash.insert( fullPath, group );
}


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

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

connect( mImagesListView->selectionModel(), SIGNAL( currentChanged( const QModelIndex&, const QModelIndex& ) ),
this, SLOT( svgSelectionChanged( const QModelIndex& ) ) );

}

void QgsSvgSelectorWidget::on_mFilePushButton_clicked()
Expand Down Expand Up @@ -319,8 +512,10 @@ void QgsSvgSelectorWidget::populateList()
}

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

//-- QgsSvgSelectorDialog
Expand Down Expand Up @@ -357,3 +552,4 @@ QgsSvgSelectorDialog::~QgsSvgSelectorDialog()
QSettings settings;
settings.setValue( "/Windows/SvgSelectorDialog/geometry", saveGeometry() );
}

0 comments on commit c60c4f7

Please sign in to comment.