Add a Mood column to the playlist

This commit is contained in:
David Sansome 2012-05-27 16:46:16 +01:00
parent f1dee1171b
commit 19c3e1d5ec
16 changed files with 283 additions and 36 deletions

View File

@ -199,6 +199,7 @@ set(SOURCES
library/sqlrow.cpp
moodbar/moodbarcontroller.cpp
moodbar/moodbaritemdelegate.cpp
moodbar/moodbarloader.cpp
moodbar/moodbarpipeline.cpp
moodbar/moodbarproxystyle.cpp
@ -466,6 +467,7 @@ set(HEADERS
library/librarywatcher.h
moodbar/moodbarcontroller.h
moodbar/moodbaritemdelegate.h
moodbar/moodbarloader.h
moodbar/moodbarpipeline.h
moodbar/moodbarproxystyle.h

View File

@ -0,0 +1,108 @@
/* This file is part of Clementine.
Copyright 2012, David Sansome <me@davidsansome.com>
Clementine is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Clementine is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Clementine. If not, see <http://www.gnu.org/licenses/>.
*/
#include "moodbaritemdelegate.h"
#include "moodbarloader.h"
#include "moodbarpipeline.h"
#include "moodbarrenderer.h"
#include "core/application.h"
#include "core/closure.h"
#include "playlist/playlist.h"
#include <QApplication>
#include <QPainter>
MoodbarItemDelegate::MoodbarItemDelegate(Application* app, QObject* parent)
: QItemDelegate(parent),
app_(app)
{
}
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);
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;
}
// 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());
QByteArray data;
MoodbarPipeline* pipeline = NULL;
switch (app_->moodbar_loader()->Load(url, &data, &pipeline)) {
case MoodbarLoader::CannotLoad:
return QPixmap();
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();
}
}
// 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();
// 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);
return pixmap;
}
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) {
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);
}

View File

@ -0,0 +1,49 @@
/* This file is part of Clementine.
Copyright 2012, David Sansome <me@davidsansome.com>
Clementine is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Clementine is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Clementine. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef MOODBARITEMDELEGATE_H
#define MOODBARITEMDELEGATE_H
#include <QItemDelegate>
class Application;
class MoodbarPipeline;
class QModelIndex;
class MoodbarItemDelegate : public QItemDelegate {
Q_OBJECT
public:
MoodbarItemDelegate(Application* app, QObject* parent = 0);
void paint(QPainter* painter, const QStyleOptionViewItem& option,
const QModelIndex& index) const;
private slots:
void RequestFinished(MoodbarPipeline* pipeline, const QModelIndex& index,
const QUrl& url);
private:
QPixmap PixmapForIndex(const QModelIndex& index, const QSize& size,
const QPalette& palette);
private:
Application* app_;
};
#endif // MOODBARITEMDELEGATE_H

View File

@ -23,6 +23,8 @@
#include <QDir>
#include <QFileInfo>
#include <QNetworkDiskCache>
#include <QTimer>
#include <QThread>
#include <QUrl>
#include <boost/scoped_ptr.hpp>
@ -30,7 +32,8 @@
MoodbarLoader::MoodbarLoader(QObject* parent)
: QObject(parent),
cache_(new QNetworkDiskCache(this)),
save_alongside_originals_(true)
kMaxActiveRequests(QThread::idealThreadCount()),
save_alongside_originals_(false)
{
cache_->setCacheDirectory(Utilities::GetConfigPath(Utilities::Path_MoodbarCache));
cache_->setMaximumCacheSize(1024 * 1024); // 1MB - enough for 333 moodbars
@ -86,22 +89,35 @@ MoodbarLoader::Result MoodbarLoader::Load(
// There was no existing file, analyze the audio file and create one.
MoodbarPipeline* pipeline = new MoodbarPipeline(filename);
if (!pipeline->Start()) {
delete pipeline;
return CannotLoad;
}
qLog(Info) << "Creating moodbar data for" << filename;
active_requests_[filename] = pipeline;
NewClosure(pipeline, SIGNAL(Finished(bool)),
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.
queued_requests_ << url;
} else if (!StartQueuedRequest(url)) {
return CannotLoad;
}
*async_pipeline = pipeline;
return WillLoadAsync;
}
bool MoodbarLoader::StartQueuedRequest(const QUrl& url) {
if (!active_requests_[url]->Start()) {
delete active_requests_.take(url);
return false;
}
qLog(Info) << "Creating moodbar data for" << url.toLocalFile();
return true;
}
void MoodbarLoader::RequestFinished(MoodbarPipeline* request, const QUrl& url) {
if (request->success()) {
qLog(Info) << "Moodbar data generated successfully for" << url.toLocalFile();
@ -125,8 +141,14 @@ 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);
request->deleteLater();
QTimer::singleShot(10, request, SLOT(deleteLater()));
if (!queued_requests_.isEmpty()) {
StartQueuedRequest(queued_requests_.takeFirst());
}
}

View File

@ -20,6 +20,7 @@
#include <QMap>
#include <QObject>
#include <QPair>
class QNetworkDiskCache;
class QUrl;
@ -53,11 +54,15 @@ private slots:
private:
static QStringList MoodFilenames(const QString& song_filename);
bool StartQueuedRequest(const QUrl& url);
private:
QNetworkDiskCache* cache_;
const int kMaxActiveRequests;
QMap<QUrl, MoodbarPipeline*> active_requests_;
QList<QUrl> queued_requests_;
bool save_alongside_originals_;
};

View File

@ -19,6 +19,7 @@
#include "core/logging.h"
bool MoodbarPipeline::sIsAvailable = false;
QMutex MoodbarPipeline::sFftwMutex;
MoodbarPipeline::MoodbarPipeline(const QString& local_filename)
: QObject(NULL),
@ -31,6 +32,7 @@ MoodbarPipeline::MoodbarPipeline(const QString& local_filename)
}
MoodbarPipeline::~MoodbarPipeline() {
qLog(Debug) << "Actually deleting" << this;
Cleanup();
}
@ -84,6 +86,8 @@ bool MoodbarPipeline::Start() {
pipeline_ = NULL;
return false;
}
QMutexLocker l(&sFftwMutex);
// Join them together
gst_element_link(filesrc, decodebin);
@ -188,14 +192,13 @@ GstBusSyncReply MoodbarPipeline::BusCallbackSync(GstBus*, GstMessage* msg, gpoin
void MoodbarPipeline::Stop(bool success) {
success_ = success;
emit Finished(success);
Cleanup();
}
void MoodbarPipeline::Cleanup() {
if (pipeline_) {
gst_bus_set_sync_handler(gst_pipeline_get_bus(GST_PIPELINE(pipeline_)), NULL, NULL);
g_source_remove(bus_callback_id_);
gst_element_set_state(pipeline_, GST_STATE_NULL);
gst_object_unref(pipeline_);
pipeline_ = NULL;
}

View File

@ -19,6 +19,7 @@
#define MOODBARPIPELINE_H
#include <QBuffer>
#include <QMutex>
#include <QObject>
#include <gst/gst.h>
@ -56,6 +57,7 @@ private:
private:
static bool sIsAvailable;
static QMutex sFftwMutex;
QString local_filename_;
GstElement* pipeline_;

View File

@ -34,7 +34,7 @@ MoodbarProxyStyle::MoodbarProxyStyle(QSlider* slider)
: QProxyStyle(slider->style()),
slider_(slider),
enabled_(true),
moodbar_style_(MoodbarRenderer::Style_SystemDefault),
moodbar_style_(MoodbarRenderer::Style_Normal),
state_(MoodbarOff),
fade_timeline_(new QTimeLine(1000, this)),
moodbar_colors_dirty_(true),
@ -238,15 +238,14 @@ void MoodbarProxyStyle::DrawArrow(const QStyleOptionSlider* option,
painter->save();
painter->setRenderHint(QPainter::Antialiasing);
painter->translate(0.5, 0.5);
painter->setPen(slider_->palette().color(QPalette::Active, QPalette::Highlight));
painter->setPen(Qt::black);
painter->setBrush(slider_->palette().brush(QPalette::Active, QPalette::Base));
painter->drawPolygon(poly);
painter->restore();
}
QPixmap MoodbarProxyStyle::MoodbarPixmap(
const MoodbarRenderer::ColorList& colors, const QSize& size,
const QPalette& palette) {
QPixmap MoodbarProxyStyle::MoodbarPixmap(const ColorVector& colors,
const QSize& size, const QPalette& palette) {
QRect rect(QPoint(0, 0), size);
QRect border_rect(rect);
border_rect.adjust(kMarginSize, kMarginSize, -kMarginSize, -kMarginSize);
@ -261,7 +260,7 @@ QPixmap MoodbarProxyStyle::MoodbarPixmap(
MoodbarRenderer::Render(colors, &p, inner_rect);
// Draw the border
p.setPen(QPen(palette.brush(QPalette::Active, QPalette::Highlight),
p.setPen(QPen(Qt::black,
kBorderSize, Qt::SolidLine, Qt::FlatCap, Qt::MiterJoin));
p.drawRect(border_rect.adjusted(0, 0, -1, -1));

View File

@ -69,7 +69,7 @@ private:
void EnsureMoodbarRendered();
void DrawArrow(const QStyleOptionSlider* option, QPainter* painter) const;
static QPixmap MoodbarPixmap(const MoodbarRenderer::ColorList& colors,
static QPixmap MoodbarPixmap(const ColorVector& colors,
const QSize& size, const QPalette& palette);
private slots:
@ -90,7 +90,7 @@ private:
bool moodbar_colors_dirty_;
bool moodbar_pixmap_dirty_;
MoodbarRenderer::ColorList moodbar_colors_;
ColorVector moodbar_colors_;
QPixmap moodbar_pixmap_;
};

View File

@ -22,7 +22,7 @@
const int MoodbarRenderer::kNumHues = 12;
MoodbarRenderer::ColorList MoodbarRenderer::Colors(
ColorVector MoodbarRenderer::Colors(
const QByteArray& data, MoodbarStyle style, const QPalette& palette) {
const int samples = data.size() / 3;
@ -32,6 +32,7 @@ MoodbarRenderer::ColorList MoodbarRenderer::Colors(
case Style_Angry: properties = StyleProperties(samples / 360 * 9, 45, -45, 200, 100); break;
case Style_Frozen: properties = StyleProperties(samples / 360 * 1, 140, 160, 50, 100); break;
case Style_Happy: properties = StyleProperties(samples / 360 * 2, 0, 359, 150, 250); break;
case Style_Normal: properties = StyleProperties(samples / 360 * 3, 0, 359, 100, 100); break;
case Style_SystemDefault:
default: {
const QColor highlight_color(palette.color(QPalette::Active, QPalette::Highlight));
@ -52,7 +53,7 @@ MoodbarRenderer::ColorList MoodbarRenderer::Colors(
memset(hue_distribution, 0, sizeof(hue_distribution));
ColorList colors;
ColorVector colors;
// Read the colors, keeping track of some histograms
for (int i=0; i<samples; ++i) {
@ -81,7 +82,7 @@ MoodbarRenderer::ColorList MoodbarRenderer::Colors(
// Now huedist is a hue mapper: huedist[h] is the new hue value
// for a bar with hue h
for (ColorList::iterator it = colors.begin() ; it != colors.end() ; ++it) {
for (ColorVector::iterator it = colors.begin() ; it != colors.end() ; ++it) {
const int hue = qMax(0, it->hue());
*it = QColor::fromHsv(
@ -93,9 +94,9 @@ MoodbarRenderer::ColorList MoodbarRenderer::Colors(
return colors;
}
void MoodbarRenderer::Render(const ColorList& colors, QPainter* p, const QRect& rect) {
void MoodbarRenderer::Render(const ColorVector& colors, QPainter* p, const QRect& rect) {
// Sample the colors and map them to screen pixels.
ColorList screen_colors;
ColorVector screen_colors;
for (int x=0; x<rect.width(); ++x) {
int r = 0;
int g = 0;

View File

@ -20,26 +20,27 @@
#include <QColor>
#include <QPixmap>
#include <QMetaType>
#include <QVector>
class QPalette;
typedef QVector<QColor> ColorVector;
class MoodbarRenderer {
public:
typedef QVector<QColor> ColorList;
enum MoodbarStyle {
Style_Angry,
Style_Frozen,
Style_Happy,
Style_Normal,
Style_SystemDefault
};
static const int kNumHues;
static ColorList Colors(const QByteArray& data, MoodbarStyle style,
const QPalette& palette);
static void Render(const ColorList& colors, QPainter* p, const QRect& rect);
static ColorVector Colors(const QByteArray& data, MoodbarStyle style,
const QPalette& palette);
static void Render(const ColorVector& colors, QPainter* p, const QRect& rect);
private:
MoodbarRenderer();
@ -58,4 +59,6 @@ private:
};
};
Q_DECLARE_METATYPE(QVector<QColor>)
#endif // MOODBARRENDERER_H

View File

@ -43,6 +43,7 @@
#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"
@ -243,6 +244,12 @@ 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: {
@ -309,9 +316,20 @@ QVariant Playlist::data(const QModelIndex& index, int role) const {
}
}
bool Playlist::setData(const QModelIndex &index, const QVariant &value, int) {
bool Playlist::setData(const QModelIndex& index, const QVariant& value, int role) {
int row = index.row();
Song song = item_at(row)->Metadata();
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;
@ -1152,6 +1170,7 @@ QString Playlist::column_name(Column column) {
case Column_Comment: return tr("Comment");
case Column_Source: return tr("Source");
case Column_Mood: return tr("Mood");
default: return QString();
}
return "";

View File

@ -111,6 +111,7 @@ class Playlist : public QAbstractListModel {
Column_Comment,
Column_Source,
Column_Mood,
ColumnCount
};
@ -121,6 +122,8 @@ class Playlist : public QAbstractListModel {
Role_StopAfter,
Role_QueuePosition,
Role_CanSetRating,
Role_MoodbarColors,
Role_MoodbarPixmap,
};
enum LastFMStatus {

View File

@ -32,6 +32,10 @@
#include <QtDebug>
PlaylistItem::~PlaylistItem() {
delete moodbar_pixmap_;
}
PlaylistItem* PlaylistItem::NewFromType(const QString& type) {
if (type == "Library")
return new LibraryPlaylistItem(type);
@ -120,3 +124,17 @@ 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,9 @@ class SqlRow;
class PlaylistItem : public boost::enable_shared_from_this<PlaylistItem> {
public:
PlaylistItem(const QString& type)
: type_(type) {}
virtual ~PlaylistItem() {}
: type_(type),
moodbar_pixmap_(NULL) {}
virtual ~PlaylistItem();
static PlaylistItem* NewFromType(const QString& type);
static PlaylistItem* NewFromSongsTable(const QString& table, const Song& song);
@ -92,6 +93,12 @@ 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,
@ -108,6 +115,10 @@ 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;

View File

@ -24,6 +24,7 @@
#include "core/logging.h"
#include "core/player.h"
#include "covers/currentartloader.h"
#include "moodbar/moodbaritemdelegate.h"
#include <QCleanlooksStyle>
#include <QClipboard>
@ -198,6 +199,7 @@ void PlaylistView::SetItemDelegates(LibraryBackend* backend) {
setItemDelegateForColumn(Playlist::Column_Filename, new NativeSeparatorsDelegate(this));
setItemDelegateForColumn(Playlist::Column_Rating, rating_delegate_);
setItemDelegateForColumn(Playlist::Column_LastPlayed, new LastPlayedItemDelegate(this));
setItemDelegateForColumn(Playlist::Column_Mood, new MoodbarItemDelegate(app_, this));
if (app_ && app_->player()) {
setItemDelegateForColumn(Playlist::Column_Source, new SongSourceDelegate(this, app_->player()));