Create and render moodbars in background threads to avoid blocking the UI

This commit is contained in:
David Sansome 2012-05-27 18:53:57 +01:00
parent 638a4b9739
commit a2feaa61e7
11 changed files with 221 additions and 124 deletions

View File

@ -25,6 +25,12 @@
#include <QApplication> #include <QApplication>
#include <QPainter> #include <QPainter>
#include <QtConcurrentRun>
MoodbarItemDelegate::Data::Data()
: state_(State_None)
{
}
MoodbarItemDelegate::MoodbarItemDelegate(Application* app, QObject* parent) MoodbarItemDelegate::MoodbarItemDelegate(Application* app, QObject* parent)
: QItemDelegate(parent), : QItemDelegate(parent),
@ -36,73 +42,154 @@ void MoodbarItemDelegate::paint(
QPainter* painter, const QStyleOptionViewItem& option, QPainter* painter, const QStyleOptionViewItem& option,
const QModelIndex& index) const { const QModelIndex& index) const {
QPixmap pixmap = const_cast<MoodbarItemDelegate*>(this)->PixmapForIndex( QPixmap pixmap = const_cast<MoodbarItemDelegate*>(this)->PixmapForIndex(
index, option.rect.size(), option.palette); index, option.rect.size());
if (!pixmap.isNull()) { if (!pixmap.isNull()) {
painter->drawPixmap(option.rect, pixmap); painter->drawPixmap(option.rect, pixmap);
} }
} }
QPixmap MoodbarItemDelegate::PixmapForIndex( QPixmap MoodbarItemDelegate::PixmapForIndex(
const QModelIndex& index, const QSize& size, const QPalette& palette) { const QModelIndex& index, const QSize& size) {
// Do we have a pixmap already that's the right size? // Pixmaps are keyed off URL.
QPixmap pixmap = index.data(Playlist::Role_MoodbarPixmap).value<QPixmap>(); const QUrl url(index.sibling(index.row(), Playlist::Column_Filename).data().toUrl());
if (!pixmap.isNull() && pixmap.size() == size) {
return pixmap; Data* data = data_[url];
if (!data) {
data = new Data;
data_.insert(url, data);
} }
// Do we have colors? data->indexes_.insert(index);
ColorVector colors = index.data(Playlist::Role_MoodbarColors).value<ColorVector>(); data->desired_size_ = size;
if (colors.isEmpty()) {
// Nope - we need to load a mood file for this song and generate some colors
// from it.
const QUrl url(index.sibling(index.row(), Playlist::Column_Filename).data().toUrl());
QByteArray data; switch (data->state_) {
MoodbarPipeline* pipeline = NULL; case Data::State_CannotLoad:
switch (app_->moodbar_loader()->Load(url, &data, &pipeline)) { case Data::State_LoadingData:
case MoodbarLoader::CannotLoad: case Data::State_LoadingColors:
return QPixmap(); case Data::State_LoadingImage:
return data->pixmap_;
case MoodbarLoader::Loaded: case Data::State_Loaded:
// Aww yeah // Is the pixmap the right size?
colors = MoodbarRenderer::Colors(data, MoodbarRenderer::Style_Normal, palette); if (data->pixmap_.size() != size) {
break; StartLoadingImage(url, data);
case MoodbarLoader::WillLoadAsync:
// Maybe in a little while.
qLog(Debug) << "Loading" << pipeline;
NewClosure(pipeline, SIGNAL(Finished(bool)),
this, SLOT(RequestFinished(MoodbarPipeline*,QModelIndex,QUrl)),
pipeline, index, url);
return QPixmap();
} }
return data->pixmap_;
case Data::State_None:
break;
} }
// We've got colors, let's make a pixmap. // We have to start loading the data from scratch.
pixmap = QPixmap(size); data->state_ = Data::State_LoadingData;
QPainter p(&pixmap);
MoodbarRenderer::Render(colors, &p, QRect(QPoint(0, 0), size));
p.end();
// Set these on the item so we don't have to look them up again. // Load a mood file for this song and generate some colors from it
QAbstractItemModel* model = const_cast<QAbstractItemModel*>(index.model()); QByteArray bytes;
model->setData(index, QVariant::fromValue(colors), Playlist::Role_MoodbarColors); MoodbarPipeline* pipeline = NULL;
model->setData(index, pixmap, Playlist::Role_MoodbarPixmap); switch (app_->moodbar_loader()->Load(url, &bytes, &pipeline)) {
case MoodbarLoader::CannotLoad:
data->state_ = Data::State_CannotLoad;
break;
return pixmap; case MoodbarLoader::Loaded:
// We got the data immediately.
StartLoadingColors(url, bytes, data);
break;
case MoodbarLoader::WillLoadAsync:
// Maybe in a little while.
NewClosure(pipeline, SIGNAL(Finished(bool)),
this, SLOT(DataLoaded(QUrl,MoodbarPipeline*)),
url, pipeline);
break;
}
return QPixmap();
} }
void MoodbarItemDelegate::RequestFinished( void MoodbarItemDelegate::DataLoaded( const QUrl& url, MoodbarPipeline* pipeline) {
MoodbarPipeline* pipeline, const QModelIndex& index, const QUrl& url) { Data* data = data_[url];
qLog(Debug) << "Finished" << pipeline; if (!data) {
// Is this index still valid, and does it still point to the same URL?
if (!index.isValid() || index.sibling(index.row(), Playlist::Column_Filename).data().toUrl() != url) {
return; return;
} }
// It's good. Create the color list and set them on the item. if (!pipeline->success()) {
ColorVector colors = MoodbarRenderer::Colors( data->state_ = Data::State_CannotLoad;
pipeline->data(), MoodbarRenderer::Style_Normal, qApp->palette()); return;
QAbstractItemModel* model = const_cast<QAbstractItemModel*>(index.model()); }
model->setData(index, QVariant::fromValue(colors), Playlist::Role_MoodbarColors);
// Load the colors next.
StartLoadingColors(url, pipeline->data(), data);
}
void MoodbarItemDelegate::StartLoadingColors(
const QUrl& url, const QByteArray& bytes, Data* data) {
data->state_ = Data::State_LoadingColors;
QFutureWatcher<ColorVector>* watcher = new QFutureWatcher<ColorVector>();
NewClosure(watcher, SIGNAL(finished()),
this, SLOT(ColorsLoaded(QUrl,QFutureWatcher<ColorVector>*)),
url, watcher);
QFuture<ColorVector> future = QtConcurrent::run(MoodbarRenderer::Colors,
bytes, MoodbarRenderer::Style_Normal, qApp->palette());
watcher->setFuture(future);
}
void MoodbarItemDelegate::ColorsLoaded(
const QUrl& url, QFutureWatcher<ColorVector>* watcher) {
watcher->deleteLater();
Data* data = data_[url];
if (!data) {
return;
}
data->colors_ = watcher->result();
// Load the image next.
StartLoadingImage(url, data);
}
void MoodbarItemDelegate::StartLoadingImage(const QUrl& url, Data* data) {
data->state_ = Data::State_LoadingImage;
QFutureWatcher<QImage>* watcher = new QFutureWatcher<QImage>();
NewClosure(watcher, SIGNAL(finished()),
this, SLOT(ImageLoaded(QUrl,QFutureWatcher<QImage>*)),
url, watcher);
QFuture<QImage> future = QtConcurrent::run(MoodbarRenderer::RenderToImage,
data->colors_, data->desired_size_);
watcher->setFuture(future);
}
void MoodbarItemDelegate::ImageLoaded(const QUrl& url, QFutureWatcher<QImage>* watcher) {
watcher->deleteLater();
Data* data = data_[url];
if (!data) {
return;
}
QImage image(watcher->result());
// If the desired size changed then don't even bother converting the image
// to a pixmap, just reload it at the new size.
if (!image.isNull() && data->desired_size_ != image.size()) {
StartLoadingImage(url, data);
return;
}
data->pixmap_ = QPixmap::fromImage(image);
data->state_ = Data::State_Loaded;
// Update all the indices with the new pixmap.
foreach (const QPersistentModelIndex& index, data->indexes_) {
if (index.isValid() && index.sibling(index.row(), Playlist::Column_Filename).data().toUrl() == url) {
const_cast<Playlist*>(reinterpret_cast<const Playlist*>(index.model()))
->MoodbarUpdated(index);
}
}
} }

View File

@ -18,7 +18,12 @@
#ifndef MOODBARITEMDELEGATE_H #ifndef MOODBARITEMDELEGATE_H
#define MOODBARITEMDELEGATE_H #define MOODBARITEMDELEGATE_H
#include "moodbarrenderer.h"
#include <QCache>
#include <QItemDelegate> #include <QItemDelegate>
#include <QFutureWatcher>
#include <QUrl>
class Application; class Application;
class MoodbarPipeline; class MoodbarPipeline;
@ -35,15 +40,39 @@ public:
const QModelIndex& index) const; const QModelIndex& index) const;
private slots: private slots:
void RequestFinished(MoodbarPipeline* pipeline, const QModelIndex& index, void DataLoaded(const QUrl& url, MoodbarPipeline* pipeline);
const QUrl& url); void ColorsLoaded(const QUrl& url, QFutureWatcher<ColorVector>* watcher);
void ImageLoaded(const QUrl& url, QFutureWatcher<QImage>* watcher);
private: private:
QPixmap PixmapForIndex(const QModelIndex& index, const QSize& size, struct Data {
const QPalette& palette); Data();
enum State {
State_None,
State_CannotLoad,
State_LoadingData,
State_LoadingColors,
State_LoadingImage,
State_Loaded
};
QSet<QPersistentModelIndex> indexes_;
State state_;
ColorVector colors_;
QSize desired_size_;
QPixmap pixmap_;
};
private:
QPixmap PixmapForIndex(const QModelIndex& index, const QSize& size);
void StartLoadingColors(const QUrl& url, const QByteArray& bytes, Data* data);
void StartLoadingImage(const QUrl& url, Data* data);
private: private:
Application* app_; Application* app_;
QCache<QUrl, Data> data_;
}; };
#endif // MOODBARITEMDELEGATE_H #endif // MOODBARITEMDELEGATE_H

View File

@ -32,7 +32,7 @@
MoodbarLoader::MoodbarLoader(QObject* parent) MoodbarLoader::MoodbarLoader(QObject* parent)
: QObject(parent), : QObject(parent),
cache_(new QNetworkDiskCache(this)), cache_(new QNetworkDiskCache(this)),
kMaxActiveRequests(QThread::idealThreadCount()), kMaxActiveRequests(QThread::idealThreadCount() / 2 + 1),
save_alongside_originals_(false) save_alongside_originals_(false)
{ {
cache_->setCacheDirectory(Utilities::GetConfigPath(Utilities::Path_MoodbarCache)); cache_->setCacheDirectory(Utilities::GetConfigPath(Utilities::Path_MoodbarCache));
@ -62,9 +62,12 @@ MoodbarLoader::Result MoodbarLoader::Load(
} }
// Are we in the middle of loading this moodbar already? // Are we in the middle of loading this moodbar already?
if (active_requests_.contains(url)) { {
*async_pipeline = active_requests_[url]; QMutexLocker l(&mutex_);
return WillLoadAsync; if (requests_.contains(url)) {
*async_pipeline = requests_[url];
return WillLoadAsync;
}
} }
// Check if a mood file exists for this file already // Check if a mood file exists for this file already
@ -93,29 +96,30 @@ MoodbarLoader::Result MoodbarLoader::Load(
this, SLOT(RequestFinished(MoodbarPipeline*,QUrl)), this, SLOT(RequestFinished(MoodbarPipeline*,QUrl)),
pipeline, url); pipeline, url);
active_requests_[url] = pipeline; {
QMutexLocker l(&mutex_);
if (active_requests_.count() > kMaxActiveRequests) { requests_[url] = pipeline;
// Just queue this request now, start it later when another request
// finishes.
queued_requests_ << url; queued_requests_ << url;
} else if (!StartQueuedRequest(url)) {
return CannotLoad;
} }
QMetaObject::invokeMethod(this, "MaybeTakeNextRequest", Qt::QueuedConnection);
*async_pipeline = pipeline; *async_pipeline = pipeline;
return WillLoadAsync; return WillLoadAsync;
} }
bool MoodbarLoader::StartQueuedRequest(const QUrl& url) { void MoodbarLoader::MaybeTakeNextRequest() {
if (!active_requests_[url]->Start()) { QMutexLocker l(&mutex_);
delete active_requests_.take(url); if (active_requests_.count() > kMaxActiveRequests ||
return false; queued_requests_.isEmpty()) {
return;
} }
qLog(Info) << "Creating moodbar data for" << url.toLocalFile(); const QUrl url = queued_requests_.takeFirst();
active_requests_ << url;
return true; qLog(Info) << "Creating moodbar data for" << url.toLocalFile();
requests_[url]->Start();
} }
void MoodbarLoader::RequestFinished(MoodbarPipeline* request, const QUrl& url) { void MoodbarLoader::RequestFinished(MoodbarPipeline* request, const QUrl& url) {
@ -145,10 +149,12 @@ void MoodbarLoader::RequestFinished(MoodbarPipeline* request, const QUrl& url) {
qLog(Debug) << "Deleting" << request; qLog(Debug) << "Deleting" << request;
// Remove the request from the active list and delete it // Remove the request from the active list and delete it
active_requests_.take(url); {
QMutexLocker l(&mutex_);
requests_.remove(url);
active_requests_.remove(url);
}
QTimer::singleShot(10, request, SLOT(deleteLater())); QTimer::singleShot(10, request, SLOT(deleteLater()));
if (!queued_requests_.isEmpty()) { QMetaObject::invokeMethod(this, "MaybeTakeNextRequest", Qt::QueuedConnection);
StartQueuedRequest(queued_requests_.takeFirst());
}
} }

View File

@ -19,8 +19,9 @@
#define MOODBARLOADER_H #define MOODBARLOADER_H
#include <QMap> #include <QMap>
#include <QMutex>
#include <QObject> #include <QObject>
#include <QPair> #include <QSet>
class QNetworkDiskCache; class QNetworkDiskCache;
class QUrl; class QUrl;
@ -51,18 +52,20 @@ public:
private slots: private slots:
void RequestFinished(MoodbarPipeline* request, const QUrl& filename); void RequestFinished(MoodbarPipeline* request, const QUrl& filename);
void MaybeTakeNextRequest();
private: private:
static QStringList MoodFilenames(const QString& song_filename); static QStringList MoodFilenames(const QString& song_filename);
bool StartQueuedRequest(const QUrl& url);
private: private:
QNetworkDiskCache* cache_; QNetworkDiskCache* cache_;
const int kMaxActiveRequests; const int kMaxActiveRequests;
QMap<QUrl, MoodbarPipeline*> active_requests_; QMutex mutex_;
QMap<QUrl, MoodbarPipeline*> requests_;
QList<QUrl> queued_requests_; QList<QUrl> queued_requests_;
QSet<QUrl> active_requests_;
bool save_alongside_originals_; bool save_alongside_originals_;
}; };

View File

@ -32,7 +32,6 @@ MoodbarPipeline::MoodbarPipeline(const QString& local_filename)
} }
MoodbarPipeline::~MoodbarPipeline() { MoodbarPipeline::~MoodbarPipeline() {
qLog(Debug) << "Actually deleting" << this;
Cleanup(); Cleanup();
} }
@ -84,6 +83,7 @@ bool MoodbarPipeline::Start() {
if (!filesrc || !convert_element_ || !fftwspectrum || !moodbar || !appsink) { if (!filesrc || !convert_element_ || !fftwspectrum || !moodbar || !appsink) {
pipeline_ = NULL; pipeline_ = NULL;
emit Finished(false);
return false; return false;
} }

View File

@ -139,3 +139,11 @@ void MoodbarRenderer::Render(const ColorVector& colors, QPainter* p, const QRect
} }
} }
} }
QImage MoodbarRenderer::RenderToImage(const ColorVector& colors, const QSize& size) {
QImage image(size, QImage::Format_ARGB32_Premultiplied);
QPainter p(&image);
Render(colors, &p, image.rect());
p.end();
return image;
}

View File

@ -41,6 +41,7 @@ public:
static ColorVector Colors(const QByteArray& data, MoodbarStyle style, static ColorVector Colors(const QByteArray& data, MoodbarStyle style,
const QPalette& palette); const QPalette& palette);
static void Render(const ColorVector& colors, QPainter* p, const QRect& rect); static void Render(const ColorVector& colors, QPainter* p, const QRect& rect);
static QImage RenderToImage(const ColorVector& colors, const QSize& size);
private: private:
MoodbarRenderer(); MoodbarRenderer();

View File

@ -43,7 +43,6 @@
#include "library/librarybackend.h" #include "library/librarybackend.h"
#include "library/librarymodel.h" #include "library/librarymodel.h"
#include "library/libraryplaylistitem.h" #include "library/libraryplaylistitem.h"
#include "moodbar/moodbarrenderer.h"
#include "smartplaylists/generator.h" #include "smartplaylists/generator.h"
#include "smartplaylists/generatorinserter.h" #include "smartplaylists/generatorinserter.h"
#include "smartplaylists/generatormimedata.h" #include "smartplaylists/generatormimedata.h"
@ -244,12 +243,6 @@ QVariant Playlist::data(const QModelIndex& index, int role) const {
items_[index.row()]->IsLocalLibraryItem() && items_[index.row()]->IsLocalLibraryItem() &&
items_[index.row()]->Metadata().id() != -1; items_[index.row()]->Metadata().id() != -1;
case Role_MoodbarColors:
return QVariant::fromValue(items_[index.row()]->MoodbarColors());
case Role_MoodbarPixmap:
return items_[index.row()]->MoodbarPixmap();
case Qt::EditRole: case Qt::EditRole:
case Qt::ToolTipRole: case Qt::ToolTipRole:
case Qt::DisplayRole: { case Qt::DisplayRole: {
@ -316,21 +309,16 @@ QVariant Playlist::data(const QModelIndex& index, int role) const {
} }
} }
void Playlist::MoodbarUpdated(const QModelIndex& index) {
emit dataChanged(index.sibling(index.row(), Column_Mood),
index.sibling(index.row(), Column_Mood));
}
bool Playlist::setData(const QModelIndex& index, const QVariant& value, int role) { bool Playlist::setData(const QModelIndex& index, const QVariant& value, int role) {
int row = index.row(); int row = index.row();
PlaylistItemPtr item = item_at(row); PlaylistItemPtr item = item_at(row);
Song song = item->Metadata(); Song song = item->Metadata();
if (role == Role_MoodbarColors) {
item->SetMoodbarColors(value.value<QVector<QColor> >());
emit dataChanged(index.sibling(index.row(), Column_Mood),
index.sibling(index.row(), Column_Mood));
return true;
} else if (role == Role_MoodbarPixmap) {
item->SetMoodbarPixmap(value.value<QPixmap>());
return true;
}
if (index.data() == value) if (index.data() == value)
return false; return false;

View File

@ -122,8 +122,6 @@ class Playlist : public QAbstractListModel {
Role_StopAfter, Role_StopAfter,
Role_QueuePosition, Role_QueuePosition,
Role_CanSetRating, Role_CanSetRating,
Role_MoodbarColors,
Role_MoodbarPixmap,
}; };
enum LastFMStatus { enum LastFMStatus {
@ -246,6 +244,9 @@ class Playlist : public QAbstractListModel {
// Unregisters a SongInsertVetoListener object. // Unregisters a SongInsertVetoListener object.
void RemoveSongInsertVetoListener(SongInsertVetoListener* listener); void RemoveSongInsertVetoListener(SongInsertVetoListener* listener);
// Just emits the dataChanged() signal so the mood column is repainted.
void MoodbarUpdated(const QModelIndex& index);
// QAbstractListModel // QAbstractListModel
int rowCount(const QModelIndex& = QModelIndex()) const { return items_.count(); } int rowCount(const QModelIndex& = QModelIndex()) const { return items_.count(); }
int columnCount(const QModelIndex& = QModelIndex()) const { return ColumnCount; } int columnCount(const QModelIndex& = QModelIndex()) const { return ColumnCount; }

View File

@ -33,7 +33,6 @@
PlaylistItem::~PlaylistItem() { PlaylistItem::~PlaylistItem() {
delete moodbar_pixmap_;
} }
PlaylistItem* PlaylistItem::NewFromType(const QString& type) { PlaylistItem* PlaylistItem::NewFromType(const QString& type) {
@ -124,17 +123,3 @@ QColor PlaylistItem::GetCurrentForegroundColor() const {
bool PlaylistItem::HasCurrentForegroundColor() const { bool PlaylistItem::HasCurrentForegroundColor() const {
return !foreground_colors_.isEmpty(); return !foreground_colors_.isEmpty();
} }
QPixmap PlaylistItem::MoodbarPixmap() const {
if (!moodbar_pixmap_) {
return QPixmap();
}
return *moodbar_pixmap_;
}
void PlaylistItem::SetMoodbarPixmap(const QPixmap& pixmap) {
if (!moodbar_pixmap_) {
moodbar_pixmap_ = new QPixmap;
}
*moodbar_pixmap_ = pixmap;
}

View File

@ -33,8 +33,7 @@ class SqlRow;
class PlaylistItem : public boost::enable_shared_from_this<PlaylistItem> { class PlaylistItem : public boost::enable_shared_from_this<PlaylistItem> {
public: public:
PlaylistItem(const QString& type) PlaylistItem(const QString& type)
: type_(type), : type_(type) {}
moodbar_pixmap_(NULL) {}
virtual ~PlaylistItem(); virtual ~PlaylistItem();
static PlaylistItem* NewFromType(const QString& type); static PlaylistItem* NewFromType(const QString& type);
@ -93,12 +92,6 @@ class PlaylistItem : public boost::enable_shared_from_this<PlaylistItem> {
// before actually using it. // before actually using it.
virtual bool IsLocalLibraryItem() const { return false; } virtual bool IsLocalLibraryItem() const { return false; }
// Moodbar accessors. These are lazy-loaded by MoodbarItemDelegate.
const QVector<QColor>& MoodbarColors() const { return moodbar_colors_; }
void SetMoodbarColors(const QVector<QColor>& colors) { moodbar_colors_ = colors; }
QPixmap MoodbarPixmap() const;
void SetMoodbarPixmap(const QPixmap& pixmap);
protected: protected:
enum DatabaseColumn { enum DatabaseColumn {
Column_LibraryId, Column_LibraryId,
@ -115,10 +108,6 @@ class PlaylistItem : public boost::enable_shared_from_this<PlaylistItem> {
QMap<short, QColor> background_colors_; QMap<short, QColor> background_colors_;
QMap<short, QColor> foreground_colors_; QMap<short, QColor> foreground_colors_;
private:
QVector<QColor> moodbar_colors_;
QPixmap* moodbar_pixmap_;
}; };
typedef boost::shared_ptr<PlaylistItem> PlaylistItemPtr; typedef boost::shared_ptr<PlaylistItem> PlaylistItemPtr;
typedef QList<PlaylistItemPtr> PlaylistItemList; typedef QList<PlaylistItemPtr> PlaylistItemList;