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 <QPainter>
#include <QtConcurrentRun>
MoodbarItemDelegate::Data::Data()
: state_(State_None)
{
}
MoodbarItemDelegate::MoodbarItemDelegate(Application* app, QObject* parent)
: QItemDelegate(parent),
@ -36,73 +42,154 @@ void MoodbarItemDelegate::paint(
QPainter* painter, const QStyleOptionViewItem& option,
const QModelIndex& index) const {
QPixmap pixmap = const_cast<MoodbarItemDelegate*>(this)->PixmapForIndex(
index, option.rect.size(), option.palette);
index, option.rect.size());
if (!pixmap.isNull()) {
painter->drawPixmap(option.rect, pixmap);
}
}
QPixmap MoodbarItemDelegate::PixmapForIndex(
const QModelIndex& index, const QSize& size, const QPalette& palette) {
// Do we have a pixmap already that's the right size?
QPixmap pixmap = index.data(Playlist::Role_MoodbarPixmap).value<QPixmap>();
if (!pixmap.isNull() && pixmap.size() == size) {
return pixmap;
const QModelIndex& index, const QSize& size) {
// Pixmaps are keyed off URL.
const QUrl url(index.sibling(index.row(), Playlist::Column_Filename).data().toUrl());
Data* data = data_[url];
if (!data) {
data = new Data;
data_.insert(url, data);
}
// Do we have colors?
ColorVector colors = index.data(Playlist::Role_MoodbarColors).value<ColorVector>();
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());
data->indexes_.insert(index);
data->desired_size_ = size;
QByteArray data;
MoodbarPipeline* pipeline = NULL;
switch (app_->moodbar_loader()->Load(url, &data, &pipeline)) {
case MoodbarLoader::CannotLoad:
return QPixmap();
switch (data->state_) {
case Data::State_CannotLoad:
case Data::State_LoadingData:
case Data::State_LoadingColors:
case Data::State_LoadingImage:
return data->pixmap_;
case MoodbarLoader::Loaded:
// Aww yeah
colors = MoodbarRenderer::Colors(data, MoodbarRenderer::Style_Normal, palette);
break;
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();
case Data::State_Loaded:
// Is the pixmap the right size?
if (data->pixmap_.size() != size) {
StartLoadingImage(url, data);
}
return data->pixmap_;
case Data::State_None:
break;
}
// We've got colors, let's make a pixmap.
pixmap = QPixmap(size);
QPainter p(&pixmap);
MoodbarRenderer::Render(colors, &p, QRect(QPoint(0, 0), size));
p.end();
// We have to start loading the data from scratch.
data->state_ = Data::State_LoadingData;
// Set these on the item so we don't have to look them up again.
QAbstractItemModel* model = const_cast<QAbstractItemModel*>(index.model());
model->setData(index, QVariant::fromValue(colors), Playlist::Role_MoodbarColors);
model->setData(index, pixmap, Playlist::Role_MoodbarPixmap);
// Load a mood file for this song and generate some colors from it
QByteArray bytes;
MoodbarPipeline* pipeline = NULL;
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(
MoodbarPipeline* pipeline, const QModelIndex& index, const QUrl& url) {
qLog(Debug) << "Finished" << pipeline;
// 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) {
void MoodbarItemDelegate::DataLoaded( const QUrl& url, MoodbarPipeline* pipeline) {
Data* data = data_[url];
if (!data) {
return;
}
// It's good. Create the color list and set them on the item.
ColorVector colors = MoodbarRenderer::Colors(
pipeline->data(), MoodbarRenderer::Style_Normal, qApp->palette());
QAbstractItemModel* model = const_cast<QAbstractItemModel*>(index.model());
model->setData(index, QVariant::fromValue(colors), Playlist::Role_MoodbarColors);
if (!pipeline->success()) {
data->state_ = Data::State_CannotLoad;
return;
}
// 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
#define MOODBARITEMDELEGATE_H
#include "moodbarrenderer.h"
#include <QCache>
#include <QItemDelegate>
#include <QFutureWatcher>
#include <QUrl>
class Application;
class MoodbarPipeline;
@ -35,15 +40,39 @@ public:
const QModelIndex& index) const;
private slots:
void RequestFinished(MoodbarPipeline* pipeline, const QModelIndex& index,
const QUrl& url);
void DataLoaded(const QUrl& url, MoodbarPipeline* pipeline);
void ColorsLoaded(const QUrl& url, QFutureWatcher<ColorVector>* watcher);
void ImageLoaded(const QUrl& url, QFutureWatcher<QImage>* watcher);
private:
QPixmap PixmapForIndex(const QModelIndex& index, const QSize& size,
const QPalette& palette);
struct Data {
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:
Application* app_;
QCache<QUrl, Data> data_;
};
#endif // MOODBARITEMDELEGATE_H

View File

@ -32,7 +32,7 @@
MoodbarLoader::MoodbarLoader(QObject* parent)
: QObject(parent),
cache_(new QNetworkDiskCache(this)),
kMaxActiveRequests(QThread::idealThreadCount()),
kMaxActiveRequests(QThread::idealThreadCount() / 2 + 1),
save_alongside_originals_(false)
{
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?
if (active_requests_.contains(url)) {
*async_pipeline = active_requests_[url];
return WillLoadAsync;
{
QMutexLocker l(&mutex_);
if (requests_.contains(url)) {
*async_pipeline = requests_[url];
return WillLoadAsync;
}
}
// Check if a mood file exists for this file already
@ -93,29 +96,30 @@ MoodbarLoader::Result MoodbarLoader::Load(
this, SLOT(RequestFinished(MoodbarPipeline*,QUrl)),
pipeline, url);
active_requests_[url] = pipeline;
if (active_requests_.count() > kMaxActiveRequests) {
// Just queue this request now, start it later when another request
// finishes.
{
QMutexLocker l(&mutex_);
requests_[url] = pipeline;
queued_requests_ << url;
} else if (!StartQueuedRequest(url)) {
return CannotLoad;
}
QMetaObject::invokeMethod(this, "MaybeTakeNextRequest", Qt::QueuedConnection);
*async_pipeline = pipeline;
return WillLoadAsync;
}
bool MoodbarLoader::StartQueuedRequest(const QUrl& url) {
if (!active_requests_[url]->Start()) {
delete active_requests_.take(url);
return false;
void MoodbarLoader::MaybeTakeNextRequest() {
QMutexLocker l(&mutex_);
if (active_requests_.count() > kMaxActiveRequests ||
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) {
@ -145,10 +149,12 @@ void MoodbarLoader::RequestFinished(MoodbarPipeline* request, const QUrl& url) {
qLog(Debug) << "Deleting" << request;
// 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()));
if (!queued_requests_.isEmpty()) {
StartQueuedRequest(queued_requests_.takeFirst());
}
QMetaObject::invokeMethod(this, "MaybeTakeNextRequest", Qt::QueuedConnection);
}

View File

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

View File

@ -32,7 +32,6 @@ MoodbarPipeline::MoodbarPipeline(const QString& local_filename)
}
MoodbarPipeline::~MoodbarPipeline() {
qLog(Debug) << "Actually deleting" << this;
Cleanup();
}
@ -84,6 +83,7 @@ bool MoodbarPipeline::Start() {
if (!filesrc || !convert_element_ || !fftwspectrum || !moodbar || !appsink) {
pipeline_ = NULL;
emit Finished(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,
const QPalette& palette);
static void Render(const ColorVector& colors, QPainter* p, const QRect& rect);
static QImage RenderToImage(const ColorVector& colors, const QSize& size);
private:
MoodbarRenderer();

View File

@ -43,7 +43,6 @@
#include "library/librarybackend.h"
#include "library/librarymodel.h"
#include "library/libraryplaylistitem.h"
#include "moodbar/moodbarrenderer.h"
#include "smartplaylists/generator.h"
#include "smartplaylists/generatorinserter.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()]->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::ToolTipRole:
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) {
int row = index.row();
PlaylistItemPtr item = item_at(row);
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)
return false;

View File

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

View File

@ -33,7 +33,6 @@
PlaylistItem::~PlaylistItem() {
delete moodbar_pixmap_;
}
PlaylistItem* PlaylistItem::NewFromType(const QString& type) {
@ -124,17 +123,3 @@ QColor PlaylistItem::GetCurrentForegroundColor() const {
bool PlaylistItem::HasCurrentForegroundColor() const {
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> {
public:
PlaylistItem(const QString& type)
: type_(type),
moodbar_pixmap_(NULL) {}
: type_(type) {}
virtual ~PlaylistItem();
static PlaylistItem* NewFromType(const QString& type);
@ -93,12 +92,6 @@ class PlaylistItem : public boost::enable_shared_from_this<PlaylistItem> {
// before actually using it.
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:
enum DatabaseColumn {
Column_LibraryId,
@ -115,10 +108,6 @@ class PlaylistItem : public boost::enable_shared_from_this<PlaylistItem> {
QMap<short, QColor> background_colors_;
QMap<short, QColor> foreground_colors_;
private:
QVector<QColor> moodbar_colors_;
QPixmap* moodbar_pixmap_;
};
typedef boost::shared_ptr<PlaylistItem> PlaylistItemPtr;
typedef QList<PlaylistItemPtr> PlaylistItemList;