Add moodbar
This commit is contained in:
@@ -879,6 +879,28 @@ optional_source(HAVE_TIDAL
|
||||
settings/tidalsettingspage.ui
|
||||
)
|
||||
|
||||
# Moodbar
|
||||
optional_source(HAVE_MOODBAR
|
||||
SOURCES
|
||||
moodbar/moodbarbuilder.cpp
|
||||
moodbar/moodbarcontroller.cpp
|
||||
moodbar/moodbaritemdelegate.cpp
|
||||
moodbar/moodbarloader.cpp
|
||||
moodbar/moodbarpipeline.cpp
|
||||
moodbar/moodbarproxystyle.cpp
|
||||
moodbar/moodbarrenderer.cpp
|
||||
settings/moodbarsettingspage.cpp
|
||||
HEADERS
|
||||
moodbar/moodbarcontroller.h
|
||||
moodbar/moodbaritemdelegate.h
|
||||
moodbar/moodbarloader.h
|
||||
moodbar/moodbarpipeline.h
|
||||
moodbar/moodbarproxystyle.h
|
||||
settings/moodbarsettingspage.h
|
||||
UI
|
||||
settings/moodbarsettingspage.ui
|
||||
)
|
||||
|
||||
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/config.h.in ${CMAKE_CURRENT_BINARY_DIR}/config.h)
|
||||
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/version.h.in ${CMAKE_CURRENT_BINARY_DIR}/version.h)
|
||||
|
||||
@@ -1003,6 +1025,10 @@ if(HAVE_LIBPULSE)
|
||||
target_link_libraries(strawberry_lib ${LIBPULSE_LIBRARIES})
|
||||
endif()
|
||||
|
||||
if(HAVE_MOODBAR)
|
||||
target_link_libraries(strawberry_lib gstmoodbar)
|
||||
endif()
|
||||
|
||||
if (APPLE)
|
||||
target_link_libraries(strawberry_lib
|
||||
"-framework AppKit"
|
||||
@@ -1025,7 +1051,6 @@ if (WIN32)
|
||||
target_link_libraries(strawberry_lib
|
||||
${ZLIB_LIBRARIES}
|
||||
dsound
|
||||
${QT_QTGUI_LIBRARY}
|
||||
)
|
||||
endif (WIN32)
|
||||
|
||||
|
||||
@@ -50,6 +50,8 @@
|
||||
|
||||
#cmakedefine HAVE_TIDAL
|
||||
|
||||
#cmakedefine HAVE_MOODBAR
|
||||
|
||||
#cmakedefine HAVE_KEYSYMDEF_H
|
||||
#cmakedefine HAVE_XF86KEYSYM_H
|
||||
|
||||
|
||||
@@ -61,6 +61,8 @@
|
||||
#include "lyrics/auddlyricsprovider.h"
|
||||
#include "lyrics/chartlyricsprovider.h"
|
||||
|
||||
#include "scrobbler/audioscrobbler.h"
|
||||
|
||||
#include "internet/internetservices.h"
|
||||
#include "internet/internetsearch.h"
|
||||
|
||||
@@ -69,7 +71,10 @@
|
||||
# include "covermanager/tidalcoverprovider.h"
|
||||
#endif
|
||||
|
||||
#include "scrobbler/audioscrobbler.h"
|
||||
#ifdef HAVE_MOODBAR
|
||||
# include "moodbar/moodbarcontroller.h"
|
||||
# include "moodbar/moodbarloader.h"
|
||||
#endif
|
||||
|
||||
bool Application::kIsPortable = false;
|
||||
|
||||
@@ -136,7 +141,14 @@ class ApplicationImpl {
|
||||
#ifdef HAVE_TIDAL
|
||||
tidal_search_([=]() { return new InternetSearch(app, Song::Source_Tidal, app); }),
|
||||
#endif
|
||||
scrobbler_([=]() { return new AudioScrobbler(app, app); })
|
||||
scrobbler_([=]() { return new AudioScrobbler(app, app); }),
|
||||
|
||||
#ifdef HAVE_MOODBAR
|
||||
moodbar_loader_([=]() { return new MoodbarLoader(app, app); }),
|
||||
moodbar_controller_([=]() { return new MoodbarController(app, app); }),
|
||||
#endif
|
||||
dummy_([=]() { return nullptr; })
|
||||
|
||||
{}
|
||||
|
||||
Lazy<TagReaderClient> tag_reader_client_;
|
||||
@@ -160,6 +172,11 @@ class ApplicationImpl {
|
||||
Lazy<InternetSearch> tidal_search_;
|
||||
#endif
|
||||
Lazy<AudioScrobbler> scrobbler_;
|
||||
#ifdef HAVE_MOODBAR
|
||||
Lazy<MoodbarLoader> moodbar_loader_;
|
||||
Lazy<MoodbarController> moodbar_controller_;
|
||||
#endif
|
||||
Lazy<QVariant> dummy_;
|
||||
|
||||
};
|
||||
|
||||
@@ -231,3 +248,7 @@ InternetServices *Application::internet_services() const { return p_->internet_s
|
||||
InternetSearch *Application::tidal_search() const { return p_->tidal_search_.get(); }
|
||||
#endif
|
||||
AudioScrobbler *Application::scrobbler() const { return p_->scrobbler_.get(); }
|
||||
#ifdef HAVE_MOODBAR
|
||||
MoodbarController *Application::moodbar_controller() const { return p_->moodbar_controller_.get(); }
|
||||
MoodbarLoader *Application::moodbar_loader() const { return p_->moodbar_loader_.get(); }
|
||||
#endif
|
||||
|
||||
@@ -56,9 +56,13 @@ class CoverProviders;
|
||||
class AlbumCoverLoader;
|
||||
class CurrentArtLoader;
|
||||
class LyricsProviders;
|
||||
class AudioScrobbler;
|
||||
class InternetServices;
|
||||
class InternetSearch;
|
||||
class AudioScrobbler;
|
||||
#ifdef HAVE_MOODBAR
|
||||
class MoodbarController;
|
||||
class MoodbarLoader;
|
||||
#endif
|
||||
|
||||
class Application : public QObject {
|
||||
Q_OBJECT
|
||||
@@ -92,12 +96,17 @@ class Application : public QObject {
|
||||
|
||||
LyricsProviders *lyrics_providers() const;
|
||||
|
||||
AudioScrobbler *scrobbler() const;
|
||||
|
||||
InternetServices *internet_services() const;
|
||||
#ifdef HAVE_TIDAL
|
||||
InternetSearch *tidal_search() const;
|
||||
#endif
|
||||
|
||||
AudioScrobbler *scrobbler() const;
|
||||
#ifdef HAVE_MOODBAR
|
||||
MoodbarController *moodbar_controller() const;
|
||||
MoodbarLoader *moodbar_loader() const;
|
||||
#endif
|
||||
|
||||
void MoveToNewThread(QObject *object);
|
||||
void MoveToThread(QObject *object, QThread *thread);
|
||||
|
||||
@@ -151,6 +151,11 @@
|
||||
# include "core/macsystemtrayicon.h"
|
||||
#endif
|
||||
|
||||
#ifdef HAVE_MOODBAR
|
||||
# include "moodbar/moodbarcontroller.h"
|
||||
# include "moodbar/moodbarproxystyle.h"
|
||||
#endif
|
||||
|
||||
using std::bind;
|
||||
using std::floor;
|
||||
using std::stable_sort;
|
||||
@@ -667,6 +672,11 @@ MainWindow::MainWindow(Application *app, SystemTrayIcon *tray_icon, OSD *osd, co
|
||||
|
||||
ui_->track_slider->SetApplication(app);
|
||||
|
||||
#ifdef HAVE_MOODBAR
|
||||
// Moodbar connections
|
||||
connect(app_->moodbar_controller(), SIGNAL(CurrentMoodbarDataChanged(QByteArray)), ui_->track_slider->moodbar_style(), SLOT(SetMoodbarData(QByteArray)));
|
||||
#endif
|
||||
|
||||
// Playing widget
|
||||
qLog(Debug) << "Creating playing widget";
|
||||
ui_->widget_playing->set_ideal_height(ui_->status_bar->sizeHint().height() + ui_->player_controls->sizeHint().height());
|
||||
|
||||
@@ -67,6 +67,10 @@
|
||||
# include "ext/gstafc/gstafcsrc.h"
|
||||
#endif
|
||||
|
||||
#ifdef HAVE_MOODBAR
|
||||
# include "ext/gstmoodbar/gstmoodbarplugin.h"
|
||||
#endif
|
||||
|
||||
#include "settings/backendsettingspage.h"
|
||||
|
||||
using std::shared_ptr;
|
||||
@@ -118,6 +122,7 @@ bool GstEngine::Init() {
|
||||
SetEnvironment();
|
||||
|
||||
initialising_ = QtConcurrent::run(this, &GstEngine::InitialiseGStreamer);
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
@@ -415,6 +420,10 @@ void GstEngine::InitialiseGStreamer() {
|
||||
afcsrc_register_static();
|
||||
#endif
|
||||
|
||||
#ifdef HAVE_MOODBAR
|
||||
gstfastspectrum_register_static();
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
void GstEngine::SetEnvironment() {
|
||||
|
||||
199
src/moodbar/moodbarbuilder.cpp
Normal file
199
src/moodbar/moodbarbuilder.cpp
Normal file
@@ -0,0 +1,199 @@
|
||||
/* This file was part of Clementine.
|
||||
Copyright 2014, David Sansome <me@davidsansome.com>
|
||||
|
||||
Strawberry 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.
|
||||
|
||||
Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include <cmath>
|
||||
|
||||
#include <QList>
|
||||
#include <QByteArray>
|
||||
|
||||
#include "moodbarbuilder.h"
|
||||
#include "core/arraysize.h"
|
||||
|
||||
namespace {
|
||||
|
||||
static const int sBarkBands[] = {
|
||||
100, 200, 300, 400, 510, 630, 770, 920, 1080, 1270, 1480, 1720,
|
||||
2000, 2320, 2700, 3150, 3700, 4400, 5300, 6400, 7700, 9500, 12000, 15500};
|
||||
|
||||
static const int sBarkBandCount = arraysize(sBarkBands);
|
||||
|
||||
} // namespace
|
||||
|
||||
MoodbarBuilder::MoodbarBuilder() : bands_(0), rate_hz_(0) {}
|
||||
|
||||
int MoodbarBuilder::BandFrequency(int band) const {
|
||||
return ((rate_hz_ / 2) * band + rate_hz_ / 4) / bands_;
|
||||
}
|
||||
|
||||
void MoodbarBuilder::Init(int bands, int rate_hz) {
|
||||
|
||||
bands_ = bands;
|
||||
rate_hz_ = rate_hz;
|
||||
|
||||
barkband_table_.clear();
|
||||
barkband_table_.reserve(bands + 1);
|
||||
|
||||
int barkband = 0;
|
||||
for (int i = 0; i < bands + 1; ++i) {
|
||||
if (barkband < sBarkBandCount - 1 && BandFrequency(i) >= sBarkBands[barkband]) {
|
||||
barkband++;
|
||||
}
|
||||
|
||||
barkband_table_.append(barkband);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void MoodbarBuilder::AddFrame(const double* magnitudes, int size) {
|
||||
|
||||
if (size > barkband_table_.length()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate total magnitudes for different bark bands.
|
||||
double bands[sBarkBandCount];
|
||||
for (int i = 0; i < sBarkBandCount; ++i) {
|
||||
bands[i] = 0.0;
|
||||
}
|
||||
|
||||
for (int i = 0; i < size; ++i) {
|
||||
bands[barkband_table_[i]] += magnitudes[i];
|
||||
}
|
||||
|
||||
// Now divide the bark bands into thirds and compute their total amplitudes.
|
||||
double rgb[] = {0, 0, 0};
|
||||
for (int i = 0; i < sBarkBandCount; ++i) {
|
||||
rgb[(i * 3) / sBarkBandCount] += bands[i] * bands[i];
|
||||
}
|
||||
|
||||
frames_.append(Rgb(sqrt(rgb[0]), sqrt(rgb[1]), sqrt(rgb[2])));
|
||||
|
||||
}
|
||||
|
||||
void MoodbarBuilder::Normalize(QList<Rgb>* vals, double Rgb::*member) {
|
||||
|
||||
double mini = vals->at(0).*member;
|
||||
double maxi = vals->at(0).*member;
|
||||
for (int i = 1; i < vals->count(); i++) {
|
||||
const double value = vals->at(i).*member;
|
||||
if (value > maxi) {
|
||||
maxi = value;
|
||||
}
|
||||
else if (value < mini) {
|
||||
mini = value;
|
||||
}
|
||||
}
|
||||
|
||||
double avg = 0;
|
||||
int t = 0;
|
||||
for (const Rgb& rgb : *vals) {
|
||||
const double value = rgb.*member;
|
||||
if (value != mini && value != maxi) {
|
||||
avg += value / vals->count();
|
||||
t++;
|
||||
}
|
||||
}
|
||||
|
||||
double tu = 0;
|
||||
double tb = 0;
|
||||
double avgu = 0;
|
||||
double avgb = 0;
|
||||
for (const Rgb& rgb : *vals) {
|
||||
const double value = rgb.*member;
|
||||
if (value != mini && value != maxi) {
|
||||
if (value > avg) {
|
||||
avgu += value;
|
||||
tu++;
|
||||
}
|
||||
else {
|
||||
avgb += value;
|
||||
tb++;
|
||||
}
|
||||
}
|
||||
}
|
||||
avgu /= tu;
|
||||
avgb /= tb;
|
||||
|
||||
tu = 0;
|
||||
tb = 0;
|
||||
double avguu = 0;
|
||||
double avgbb = 0;
|
||||
for (const Rgb& rgb : *vals) {
|
||||
const double value = rgb.*member;
|
||||
if (value != mini && value != maxi) {
|
||||
if (value > avgu) {
|
||||
avguu += value;
|
||||
tu++;
|
||||
}
|
||||
else if (value < avgb) {
|
||||
avgbb += value;
|
||||
tb++;
|
||||
}
|
||||
}
|
||||
}
|
||||
avguu /= tu;
|
||||
avgbb /= tb;
|
||||
|
||||
mini = std::max(avg + (avgb - avg) * 2, avgbb);
|
||||
maxi = std::min(avg + (avgu - avg) * 2, avguu);
|
||||
double delta = maxi - mini;
|
||||
if (delta == 0) {
|
||||
delta = 1;
|
||||
}
|
||||
|
||||
for (auto it = vals->begin(); it != vals->end(); ++it) {
|
||||
double* value = &((*it).*member);
|
||||
*value = std::isfinite(*value) ? qBound(0.0, (*value - mini) / delta, 1.0) : 0;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
QByteArray MoodbarBuilder::Finish(int width) {
|
||||
|
||||
QByteArray ret;
|
||||
ret.resize(width * 3);
|
||||
char* data = ret.data();
|
||||
if (frames_.count() == 0) return ret;
|
||||
|
||||
Normalize(&frames_, &Rgb::r);
|
||||
Normalize(&frames_, &Rgb::g);
|
||||
Normalize(&frames_, &Rgb::b);
|
||||
|
||||
for (int i = 0; i < width; ++i) {
|
||||
Rgb rgb;
|
||||
int start = i * frames_.count() / width;
|
||||
int end = (i + 1) * frames_.count() / width;
|
||||
if (start == end) {
|
||||
end = start + 1;
|
||||
}
|
||||
|
||||
for (int j = start; j < end; j++) {
|
||||
const Rgb& frame = frames_[j];
|
||||
rgb.r += frame.r * 255;
|
||||
rgb.g += frame.g * 255;
|
||||
rgb.b += frame.b * 255;
|
||||
}
|
||||
|
||||
const int n = end - start;
|
||||
|
||||
*(data++) = rgb.r / n;
|
||||
*(data++) = rgb.g / n;
|
||||
*(data++) = rgb.b / n;
|
||||
}
|
||||
return ret;
|
||||
|
||||
}
|
||||
50
src/moodbar/moodbarbuilder.h
Normal file
50
src/moodbar/moodbarbuilder.h
Normal file
@@ -0,0 +1,50 @@
|
||||
/* This file was part of Clementine.
|
||||
Copyright 2014, David Sansome <me@davidsansome.com>
|
||||
|
||||
Strawberry 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.
|
||||
|
||||
Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef MOODBARBUILDER_H
|
||||
#define MOODBARBUILDER_H
|
||||
|
||||
#include <QList>
|
||||
#include <QByteArray>
|
||||
|
||||
class MoodbarBuilder {
|
||||
public:
|
||||
MoodbarBuilder();
|
||||
|
||||
void Init(int bands, int rate_hz);
|
||||
void AddFrame(const double* magnitudes, int size);
|
||||
QByteArray Finish(int width);
|
||||
|
||||
private:
|
||||
struct Rgb {
|
||||
Rgb() : r(0), g(0), b(0) {}
|
||||
Rgb(double r_, double g_, double b_) : r(r_), g(g_), b(b_) {}
|
||||
|
||||
double r, g, b;
|
||||
};
|
||||
|
||||
int BandFrequency(int band) const;
|
||||
static void Normalize(QList<Rgb>* vals, double Rgb::*member);
|
||||
|
||||
QList<uint> barkband_table_;
|
||||
int bands_;
|
||||
int rate_hz_;
|
||||
|
||||
QList<Rgb> frames_;
|
||||
};
|
||||
|
||||
#endif // MOODBARBUILDER_H
|
||||
91
src/moodbar/moodbarcontroller.cpp
Normal file
91
src/moodbar/moodbarcontroller.cpp
Normal file
@@ -0,0 +1,91 @@
|
||||
/* This file was part of Clementine.
|
||||
Copyright 2012, David Sansome <me@davidsansome.com>
|
||||
|
||||
Strawberry 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.
|
||||
|
||||
Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include <QObject>
|
||||
#include <QByteArray>
|
||||
#include <QUrl>
|
||||
|
||||
#include "core/application.h"
|
||||
#include "core/closure.h"
|
||||
#include "core/logging.h"
|
||||
#include "core/player.h"
|
||||
#include "playlist/playlistmanager.h"
|
||||
|
||||
#include "moodbarcontroller.h"
|
||||
#include "moodbarloader.h"
|
||||
#include "moodbarpipeline.h"
|
||||
|
||||
MoodbarController::MoodbarController(Application* app, QObject* parent)
|
||||
: QObject(parent),
|
||||
app_(app) {
|
||||
|
||||
connect(app_->playlist_manager(), SIGNAL(CurrentSongChanged(Song)), SLOT(CurrentSongChanged(Song)));
|
||||
connect(app_->player(), SIGNAL(Stopped()), SLOT(PlaybackStopped()));
|
||||
|
||||
}
|
||||
|
||||
void MoodbarController::CurrentSongChanged(const Song& song) {
|
||||
|
||||
QByteArray data;
|
||||
MoodbarPipeline* pipeline = nullptr;
|
||||
const MoodbarLoader::Result result = app_->moodbar_loader()->Load(song.url(), &data, &pipeline);
|
||||
|
||||
switch (result) {
|
||||
case MoodbarLoader::CannotLoad:
|
||||
emit CurrentMoodbarDataChanged(QByteArray());
|
||||
break;
|
||||
|
||||
case MoodbarLoader::Loaded:
|
||||
emit CurrentMoodbarDataChanged(data);
|
||||
break;
|
||||
|
||||
case MoodbarLoader::WillLoadAsync:
|
||||
// Emit an empty array for now so the GUI reverts to a normal progress
|
||||
// bar. Our slot will be called when the data is actually loaded.
|
||||
emit CurrentMoodbarDataChanged(QByteArray());
|
||||
|
||||
NewClosure(pipeline, SIGNAL(Finished(bool)), this, SLOT(AsyncLoadComplete(MoodbarPipeline*, QUrl)), pipeline, song.url());
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void MoodbarController::PlaybackStopped() {
|
||||
emit CurrentMoodbarDataChanged(QByteArray());
|
||||
}
|
||||
|
||||
void MoodbarController::AsyncLoadComplete(MoodbarPipeline* pipeline, const QUrl& url) {
|
||||
|
||||
// Is this song still playing?
|
||||
PlaylistItemPtr current_item = app_->player()->GetCurrentItem();
|
||||
if (current_item && current_item->Url() != url) {
|
||||
return;
|
||||
}
|
||||
// Did we stop the song?
|
||||
switch(app_->player()->GetState()) {
|
||||
case Engine::Error:
|
||||
case Engine::Empty:
|
||||
case Engine::Idle:
|
||||
return;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
emit CurrentMoodbarDataChanged(pipeline->data());
|
||||
|
||||
}
|
||||
47
src/moodbar/moodbarcontroller.h
Normal file
47
src/moodbar/moodbarcontroller.h
Normal file
@@ -0,0 +1,47 @@
|
||||
/* This file was part of Clementine.
|
||||
Copyright 2012, David Sansome <me@davidsansome.com>
|
||||
|
||||
Strawberry 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.
|
||||
|
||||
Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef MOODBARCONTROLLER_H
|
||||
#define MOODBARCONTROLLER_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QByteArray>
|
||||
#include <QUrl>
|
||||
|
||||
class Application;
|
||||
class MoodbarPipeline;
|
||||
class Song;
|
||||
|
||||
class MoodbarController : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
MoodbarController(Application* app, QObject* parent = nullptr);
|
||||
|
||||
signals:
|
||||
void CurrentMoodbarDataChanged(const QByteArray& data);
|
||||
|
||||
private slots:
|
||||
void CurrentSongChanged(const Song& song);
|
||||
void PlaybackStopped();
|
||||
void AsyncLoadComplete(MoodbarPipeline* pipeline, const QUrl& url);
|
||||
|
||||
private:
|
||||
Application* app_;
|
||||
};
|
||||
|
||||
#endif // MOODBARCONTROLLER_H
|
||||
274
src/moodbar/moodbaritemdelegate.cpp
Normal file
274
src/moodbar/moodbaritemdelegate.cpp
Normal file
@@ -0,0 +1,274 @@
|
||||
/* This file was part of Clementine.
|
||||
Copyright 2012, David Sansome <me@davidsansome.com>
|
||||
|
||||
Strawberry 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.
|
||||
|
||||
Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include <QApplication>
|
||||
#include <QtConcurrentRun>
|
||||
#include <QFuture>
|
||||
#include <QSettings>
|
||||
#include <QSortFilterProxyModel>
|
||||
#include <QItemDelegate>
|
||||
#include <QByteArray>
|
||||
#include <QUrl>
|
||||
#include <QImage>
|
||||
#include <QPixmap>
|
||||
#include <QPainter>
|
||||
|
||||
#include "core/application.h"
|
||||
#include "core/closure.h"
|
||||
#include "playlist/playlist.h"
|
||||
#include "playlist/playlistview.h"
|
||||
|
||||
#include "moodbaritemdelegate.h"
|
||||
#include "moodbarloader.h"
|
||||
#include "moodbarpipeline.h"
|
||||
#include "moodbarrenderer.h"
|
||||
|
||||
#include "settings/moodbarsettingspage.h"
|
||||
|
||||
MoodbarItemDelegate::Data::Data() : state_(State_None) {}
|
||||
|
||||
MoodbarItemDelegate::MoodbarItemDelegate(Application* app, PlaylistView* view, QObject* parent)
|
||||
: QItemDelegate(parent),
|
||||
app_(app),
|
||||
view_(view),
|
||||
style_(MoodbarRenderer::Style_Normal) {
|
||||
|
||||
connect(app_, SIGNAL(SettingsChanged()), SLOT(ReloadSettings()));
|
||||
ReloadSettings();
|
||||
|
||||
}
|
||||
|
||||
void MoodbarItemDelegate::ReloadSettings() {
|
||||
|
||||
QSettings s;
|
||||
s.beginGroup(MoodbarSettingsPage::kSettingsGroup);
|
||||
MoodbarRenderer::MoodbarStyle new_style = static_cast<MoodbarRenderer::MoodbarStyle>(s.value("style", MoodbarRenderer::Style_Normal).toInt());
|
||||
s.endGroup();
|
||||
|
||||
if (new_style != style_) {
|
||||
style_ = new_style;
|
||||
ReloadAllColors();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void MoodbarItemDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const {
|
||||
|
||||
QPixmap pixmap = const_cast<MoodbarItemDelegate*>(this)->PixmapForIndex(index, option.rect.size());
|
||||
|
||||
drawBackground(painter, option, index);
|
||||
|
||||
if (!pixmap.isNull()) {
|
||||
// Make a little border for the moodbar
|
||||
const QRect moodbar_rect(option.rect.adjusted(1, 1, -1, -1));
|
||||
painter->drawPixmap(moodbar_rect, pixmap);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
QPixmap MoodbarItemDelegate::PixmapForIndex(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);
|
||||
}
|
||||
|
||||
data->indexes_.insert(index);
|
||||
data->desired_size_ = size;
|
||||
|
||||
switch (data->state_) {
|
||||
case Data::State_CannotLoad:
|
||||
case Data::State_LoadingData:
|
||||
case Data::State_LoadingColors:
|
||||
case Data::State_LoadingImage:
|
||||
return data->pixmap_;
|
||||
|
||||
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 have to start loading the data from scratch.
|
||||
StartLoadingData(url, data);
|
||||
|
||||
return QPixmap();
|
||||
|
||||
}
|
||||
|
||||
void MoodbarItemDelegate::StartLoadingData(const QUrl& url, Data* data) {
|
||||
|
||||
data->state_ = Data::State_LoadingData;
|
||||
|
||||
// Load a mood file for this song and generate some colors from it
|
||||
QByteArray bytes;
|
||||
MoodbarPipeline* pipeline = nullptr;
|
||||
switch (app_->moodbar_loader()->Load(url, &bytes, &pipeline)) {
|
||||
case MoodbarLoader::CannotLoad:
|
||||
data->state_ = Data::State_CannotLoad;
|
||||
break;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
bool MoodbarItemDelegate::RemoveFromCacheIfIndexesInvalid(const QUrl& url, Data* data) {
|
||||
|
||||
for (const QPersistentModelIndex& index : data->indexes_) {
|
||||
if (index.isValid()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
data_.remove(url);
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
void MoodbarItemDelegate::ReloadAllColors() {
|
||||
|
||||
for (const QUrl& url : data_.keys()) {
|
||||
Data* data = data_[url];
|
||||
|
||||
if (data->state_ == Data::State_Loaded) {
|
||||
StartLoadingData(url, data);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void MoodbarItemDelegate::DataLoaded(const QUrl& url, MoodbarPipeline* pipeline) {
|
||||
|
||||
Data* data = data_[url];
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (RemoveFromCacheIfIndexesInvalid(url, data)) {
|
||||
return;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
QFuture<ColorVector> future = QtConcurrent::run(MoodbarRenderer::Colors, bytes, style_, qApp->palette());
|
||||
NewClosure(future, this, SLOT(ColorsLoaded(QUrl, QFuture<ColorVector>)), url, future);
|
||||
|
||||
}
|
||||
|
||||
void MoodbarItemDelegate::ColorsLoaded(const QUrl& url, QFuture<ColorVector> future) {
|
||||
|
||||
Data* data = data_[url];
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (RemoveFromCacheIfIndexesInvalid(url, data)) {
|
||||
return;
|
||||
}
|
||||
|
||||
data->colors_ = future.result();
|
||||
|
||||
// Load the image next.
|
||||
StartLoadingImage(url, data);
|
||||
|
||||
}
|
||||
|
||||
void MoodbarItemDelegate::StartLoadingImage(const QUrl& url, Data* data) {
|
||||
|
||||
data->state_ = Data::State_LoadingImage;
|
||||
|
||||
QFuture<QImage> future = QtConcurrent::run(MoodbarRenderer::RenderToImage, data->colors_, data->desired_size_);
|
||||
NewClosure(future, this, SLOT(ImageLoaded(QUrl, QFuture<QImage>)), url, future);
|
||||
|
||||
}
|
||||
|
||||
void MoodbarItemDelegate::ImageLoaded(const QUrl& url, QFuture<QImage> future) {
|
||||
|
||||
Data* data = data_[url];
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (RemoveFromCacheIfIndexesInvalid(url, data)) {
|
||||
return;
|
||||
}
|
||||
|
||||
QImage image(future.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;
|
||||
|
||||
Playlist* playlist = view_->playlist();
|
||||
const QSortFilterProxyModel* filter = playlist->proxy();
|
||||
|
||||
// Update all the indices with the new pixmap.
|
||||
for (const QPersistentModelIndex& index : data->indexes_) {
|
||||
if (index.isValid() && index.sibling(index.row(), Playlist::Column_Filename).data().toUrl() == url) {
|
||||
QModelIndex source_index = index;
|
||||
if (index.model() == filter) {
|
||||
source_index = filter->mapToSource(source_index);
|
||||
}
|
||||
|
||||
if (source_index.model() != playlist) {
|
||||
// The pixmap was for an index in a different playlist, maybe the user
|
||||
// switched to a different one.
|
||||
continue;
|
||||
}
|
||||
|
||||
playlist->MoodbarUpdated(source_index);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
90
src/moodbar/moodbaritemdelegate.h
Normal file
90
src/moodbar/moodbaritemdelegate.h
Normal file
@@ -0,0 +1,90 @@
|
||||
/* This file is part of Clementine.
|
||||
Copyright 2012, David Sansome <me@davidsansome.com>
|
||||
|
||||
Strawberry 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.
|
||||
|
||||
Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef MOODBARITEMDELEGATE_H
|
||||
#define MOODBARITEMDELEGATE_H
|
||||
|
||||
#include "moodbarrenderer.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QItemDelegate>
|
||||
#include <QCache>
|
||||
#include <QFuture>
|
||||
#include <QSet>
|
||||
#include <QByteArray>
|
||||
#include <QUrl>
|
||||
#include <QPainter>
|
||||
|
||||
class Application;
|
||||
class MoodbarPipeline;
|
||||
class PlaylistView;
|
||||
|
||||
class MoodbarItemDelegate : public QItemDelegate {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
MoodbarItemDelegate(Application* app, PlaylistView* view, QObject* parent = nullptr);
|
||||
|
||||
void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const;
|
||||
|
||||
private slots:
|
||||
void ReloadSettings();
|
||||
|
||||
void DataLoaded(const QUrl& url, MoodbarPipeline* pipeline);
|
||||
void ColorsLoaded(const QUrl& url, QFuture<ColorVector> future);
|
||||
void ImageLoaded(const QUrl& url, QFuture<QImage> future);
|
||||
|
||||
private:
|
||||
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 StartLoadingData(const QUrl& url, Data* data);
|
||||
void StartLoadingColors(const QUrl& url, const QByteArray& bytes, Data* data);
|
||||
void StartLoadingImage(const QUrl& url, Data* data);
|
||||
|
||||
bool RemoveFromCacheIfIndexesInvalid(const QUrl& url, Data* data);
|
||||
|
||||
void ReloadAllColors();
|
||||
|
||||
private:
|
||||
Application* app_;
|
||||
PlaylistView* view_;
|
||||
QCache<QUrl, Data> data_;
|
||||
|
||||
MoodbarRenderer::MoodbarStyle style_;
|
||||
};
|
||||
|
||||
#endif // MOODBARITEMDELEGATE_H
|
||||
203
src/moodbar/moodbarloader.cpp
Normal file
203
src/moodbar/moodbarloader.cpp
Normal file
@@ -0,0 +1,203 @@
|
||||
/* This file was part of Clementine.
|
||||
Copyright 2012, David Sansome <me@davidsansome.com>
|
||||
|
||||
Strawberry 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.
|
||||
|
||||
Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "moodbarloader.h"
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include <QObject>
|
||||
#include <QThread>
|
||||
#include <QCoreApplication>
|
||||
#include <QStandardPaths>
|
||||
#include <QIODevice>
|
||||
#include <QDir>
|
||||
#include <QFileInfo>
|
||||
#include <QNetworkDiskCache>
|
||||
#include <QTimer>
|
||||
#include <QString>
|
||||
#include <QUrl>
|
||||
|
||||
#include "core/application.h"
|
||||
#include "core/closure.h"
|
||||
#include "core/logging.h"
|
||||
|
||||
#include "moodbarpipeline.h"
|
||||
|
||||
#include "settings/moodbarsettingspage.h"
|
||||
|
||||
#ifdef Q_OS_WIN32
|
||||
# include <windows.h>
|
||||
#endif
|
||||
|
||||
using std::unique_ptr;
|
||||
|
||||
MoodbarLoader::MoodbarLoader(Application* app, QObject* parent)
|
||||
: QObject(parent),
|
||||
cache_(new QNetworkDiskCache(this)),
|
||||
thread_(new QThread(this)),
|
||||
kMaxActiveRequests(qMax(1, QThread::idealThreadCount() / 2)),
|
||||
enabled_(false),
|
||||
save_(false) {
|
||||
|
||||
cache_->setCacheDirectory(QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + "/moodbar");
|
||||
cache_->setMaximumCacheSize(60 * 1024 * 1024); // 60MB - enough for 20,000 moodbars
|
||||
|
||||
connect(app, SIGNAL(SettingsChanged()), SLOT(ReloadSettings()));
|
||||
ReloadSettings();
|
||||
|
||||
}
|
||||
|
||||
MoodbarLoader::~MoodbarLoader() {
|
||||
thread_->quit();
|
||||
thread_->wait(1000);
|
||||
}
|
||||
|
||||
void MoodbarLoader::ReloadSettings() {
|
||||
|
||||
QSettings s;
|
||||
s.beginGroup(MoodbarSettingsPage::kSettingsGroup);
|
||||
enabled_ = s.value("enabled", false).toBool();
|
||||
save_ = s.value("save", false).toBool();
|
||||
s.endGroup();
|
||||
|
||||
MaybeTakeNextRequest();
|
||||
|
||||
}
|
||||
|
||||
QStringList MoodbarLoader::MoodFilenames(const QString& song_filename) {
|
||||
|
||||
const QFileInfo file_info(song_filename);
|
||||
const QString dir_path(file_info.dir().path());
|
||||
const QString mood_filename = file_info.baseName() + ".mood";
|
||||
|
||||
return QStringList() << dir_path + "/." + mood_filename << dir_path + "/" + mood_filename;
|
||||
|
||||
}
|
||||
|
||||
MoodbarLoader::Result MoodbarLoader::Load(const QUrl& url, QByteArray* data, MoodbarPipeline** async_pipeline) {
|
||||
|
||||
if (url.scheme() != "file") {
|
||||
return CannotLoad;
|
||||
}
|
||||
|
||||
// Are we in the middle of loading this moodbar already?
|
||||
if (requests_.contains(url)) {
|
||||
*async_pipeline = requests_[url];
|
||||
return WillLoadAsync;
|
||||
}
|
||||
|
||||
// Check if a mood file exists for this file already
|
||||
const QString filename(url.toLocalFile());
|
||||
|
||||
for (const QString& possible_mood_file : MoodFilenames(filename)) {
|
||||
QFile f(possible_mood_file);
|
||||
if (f.open(QIODevice::ReadOnly)) {
|
||||
qLog(Info) << "Loading moodbar data from" << possible_mood_file;
|
||||
*data = f.readAll();
|
||||
return Loaded;
|
||||
}
|
||||
}
|
||||
|
||||
// Maybe it exists in the cache?
|
||||
std::unique_ptr<QIODevice> cache_device(cache_->data(url));
|
||||
if (cache_device) {
|
||||
qLog(Info) << "Loading cached moodbar data for" << filename;
|
||||
*data = cache_device->readAll();
|
||||
if (!data->isEmpty()) {
|
||||
return Loaded;
|
||||
}
|
||||
}
|
||||
|
||||
if (!thread_->isRunning()) thread_->start(QThread::IdlePriority);
|
||||
|
||||
// There was no existing file, analyze the audio file and create one.
|
||||
MoodbarPipeline* pipeline = new MoodbarPipeline(url);
|
||||
pipeline->moveToThread(thread_);
|
||||
NewClosure(pipeline, SIGNAL(Finished(bool)), this, SLOT(RequestFinished(MoodbarPipeline*, QUrl)), pipeline, url);
|
||||
|
||||
requests_[url] = pipeline;
|
||||
queued_requests_ << url;
|
||||
|
||||
MaybeTakeNextRequest();
|
||||
|
||||
*async_pipeline = pipeline;
|
||||
return WillLoadAsync;
|
||||
|
||||
}
|
||||
|
||||
void MoodbarLoader::MaybeTakeNextRequest() {
|
||||
|
||||
Q_ASSERT(QThread::currentThread() == qApp->thread());
|
||||
|
||||
if (active_requests_.count() >= kMaxActiveRequests || queued_requests_.isEmpty() || !enabled_) {
|
||||
return;
|
||||
}
|
||||
|
||||
const QUrl url = queued_requests_.takeFirst();
|
||||
active_requests_ << url;
|
||||
|
||||
qLog(Info) << "Creating moodbar data for" << url.toLocalFile();
|
||||
QMetaObject::invokeMethod(requests_[url], "Start", Qt::QueuedConnection);
|
||||
|
||||
}
|
||||
|
||||
void MoodbarLoader::RequestFinished(MoodbarPipeline* request, const QUrl& url) {
|
||||
|
||||
Q_ASSERT(QThread::currentThread() == qApp->thread());
|
||||
|
||||
if (request->success()) {
|
||||
qLog(Info) << "Moodbar data generated successfully for" << url.toLocalFile();
|
||||
|
||||
// Save the data in the cache
|
||||
QNetworkCacheMetaData metadata;
|
||||
metadata.setUrl(url);
|
||||
|
||||
QIODevice* cache_file = cache_->prepare(metadata);
|
||||
if (cache_file) {
|
||||
cache_file->write(request->data());
|
||||
cache_->insert(cache_file);
|
||||
}
|
||||
|
||||
// Save the data alongside the original as well if we're configured to.
|
||||
if (save_) {
|
||||
const QString mood_filename(MoodFilenames(url.toLocalFile())[0]);
|
||||
QFile mood_file(mood_filename);
|
||||
if (mood_file.open(QIODevice::WriteOnly)) {
|
||||
mood_file.write(request->data());
|
||||
|
||||
#ifdef Q_OS_WIN32
|
||||
if (!SetFileAttributes((LPCTSTR)mood_filename.utf16(), FILE_ATTRIBUTE_HIDDEN)) {
|
||||
qLog(Warning) << "Error setting hidden attribute for file" << mood_filename;
|
||||
}
|
||||
#endif
|
||||
|
||||
}
|
||||
else {
|
||||
qLog(Warning) << "Error opening mood file for writing" << mood_filename;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the request from the active list and delete it
|
||||
requests_.remove(url);
|
||||
active_requests_.remove(url);
|
||||
|
||||
QTimer::singleShot(1000, request, SLOT(deleteLater()));
|
||||
|
||||
MaybeTakeNextRequest();
|
||||
|
||||
}
|
||||
79
src/moodbar/moodbarloader.h
Normal file
79
src/moodbar/moodbarloader.h
Normal file
@@ -0,0 +1,79 @@
|
||||
/* This file was part of Clementine.
|
||||
Copyright 2012, David Sansome <me@davidsansome.com>
|
||||
|
||||
Strawberry 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.
|
||||
|
||||
Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef MOODBARLOADER_H
|
||||
#define MOODBARLOADER_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QThread>
|
||||
#include <QList>
|
||||
#include <QMap>
|
||||
#include <QSet>
|
||||
#include <QByteArray>
|
||||
#include <QStringList>
|
||||
#include <QUrl>
|
||||
#include <QNetworkDiskCache>
|
||||
|
||||
class Application;
|
||||
class MoodbarPipeline;
|
||||
|
||||
class MoodbarLoader : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
MoodbarLoader(Application* app, QObject* parent = nullptr);
|
||||
~MoodbarLoader();
|
||||
|
||||
enum Result {
|
||||
// The URL isn't a local file or the moodbar plugin was not available -
|
||||
// moodbar data can never be loaded.
|
||||
CannotLoad,
|
||||
|
||||
// Moodbar data was loaded and returned.
|
||||
Loaded,
|
||||
|
||||
// Moodbar data will be loaded in the background, a MoodbarPipeline* was
|
||||
// was returned that you can connect to the Finished() signal on.
|
||||
WillLoadAsync
|
||||
};
|
||||
|
||||
Result Load(const QUrl& url, QByteArray* data, MoodbarPipeline** async_pipeline);
|
||||
|
||||
private slots:
|
||||
void ReloadSettings();
|
||||
|
||||
void RequestFinished(MoodbarPipeline* request, const QUrl& filename);
|
||||
void MaybeTakeNextRequest();
|
||||
|
||||
private:
|
||||
static QStringList MoodFilenames(const QString& song_filename);
|
||||
|
||||
private:
|
||||
QNetworkDiskCache* cache_;
|
||||
QThread* thread_;
|
||||
|
||||
const int kMaxActiveRequests;
|
||||
|
||||
QMap<QUrl, MoodbarPipeline*> requests_;
|
||||
QList<QUrl> queued_requests_;
|
||||
QSet<QUrl> active_requests_;
|
||||
|
||||
bool enabled_;
|
||||
bool save_;
|
||||
};
|
||||
|
||||
#endif // MOODBARLOADER_H
|
||||
227
src/moodbar/moodbarpipeline.cpp
Normal file
227
src/moodbar/moodbarpipeline.cpp
Normal file
@@ -0,0 +1,227 @@
|
||||
/* This file was part of Clementine.
|
||||
Copyright 2012, David Sansome <me@davidsansome.com>
|
||||
|
||||
Strawberry 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.
|
||||
|
||||
Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "moodbarpipeline.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QCoreApplication>
|
||||
#include <QThread>
|
||||
#include <QString>
|
||||
#include <QUrl>
|
||||
|
||||
#include "core/logging.h"
|
||||
#include "core/signalchecker.h"
|
||||
#include "core/timeconstants.h"
|
||||
#include "core/utilities.h"
|
||||
#include "moodbar/moodbarbuilder.h"
|
||||
|
||||
#include "ext/gstmoodbar/gstfastspectrum.h"
|
||||
|
||||
bool MoodbarPipeline::sIsAvailable = false;
|
||||
const int MoodbarPipeline::kBands = 128;
|
||||
|
||||
MoodbarPipeline::MoodbarPipeline(const QUrl& local_filename)
|
||||
: QObject(nullptr),
|
||||
local_filename_(local_filename),
|
||||
pipeline_(nullptr),
|
||||
convert_element_(nullptr),
|
||||
success_(false),
|
||||
running_(false) {}
|
||||
|
||||
MoodbarPipeline::~MoodbarPipeline() { Cleanup(); }
|
||||
|
||||
bool MoodbarPipeline::IsAvailable() {
|
||||
|
||||
if (!sIsAvailable) {
|
||||
GstElementFactory* factory = gst_element_factory_find("fftwspectrum");
|
||||
if (!factory) {
|
||||
return false;
|
||||
}
|
||||
gst_object_unref(factory);
|
||||
|
||||
sIsAvailable = true;
|
||||
}
|
||||
|
||||
return sIsAvailable;
|
||||
|
||||
}
|
||||
|
||||
GstElement* MoodbarPipeline::CreateElement(const QString& factory_name) {
|
||||
|
||||
GstElement* ret = gst_element_factory_make(factory_name.toLatin1().constData(), nullptr);
|
||||
|
||||
if (ret) {
|
||||
gst_bin_add(GST_BIN(pipeline_), ret);
|
||||
}
|
||||
else {
|
||||
qLog(Warning) << "Unable to create gstreamer element" << factory_name;
|
||||
}
|
||||
|
||||
return ret;
|
||||
|
||||
}
|
||||
|
||||
void MoodbarPipeline::Start() {
|
||||
|
||||
Q_ASSERT(QThread::currentThread() != qApp->thread());
|
||||
|
||||
Utilities::SetThreadIOPriority(Utilities::IOPRIO_CLASS_IDLE);
|
||||
|
||||
if (pipeline_) {
|
||||
return;
|
||||
}
|
||||
|
||||
pipeline_ = gst_pipeline_new("moodbar-pipeline");
|
||||
|
||||
GstElement* decodebin = CreateElement("uridecodebin");
|
||||
convert_element_ = CreateElement("audioconvert");
|
||||
GstElement* spectrum = CreateElement("fastspectrum");
|
||||
GstElement* fakesink = CreateElement("fakesink");
|
||||
|
||||
if (!decodebin || !convert_element_ || !spectrum || !fakesink) {
|
||||
pipeline_ = nullptr;
|
||||
emit Finished(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Join them together
|
||||
if (!gst_element_link(convert_element_, spectrum) || !gst_element_link(spectrum, fakesink)) {
|
||||
qLog(Error) << "Failed to link elements";
|
||||
pipeline_ = nullptr;
|
||||
emit Finished(false);
|
||||
return;
|
||||
}
|
||||
|
||||
builder_.reset(new MoodbarBuilder);
|
||||
|
||||
// Set properties
|
||||
g_object_set(decodebin, "uri", local_filename_.toEncoded().constData(), nullptr);
|
||||
g_object_set(spectrum, "bands", kBands, nullptr);
|
||||
|
||||
GstFastSpectrum* fast_spectrum = GST_FASTSPECTRUM(spectrum);
|
||||
fast_spectrum->output_callback = [this](double* magnitudes, int size) { builder_->AddFrame(magnitudes, size); };
|
||||
|
||||
// Connect signals
|
||||
CHECKED_GCONNECT(decodebin, "pad-added", &NewPadCallback, this);
|
||||
GstBus* bus = gst_pipeline_get_bus(GST_PIPELINE(pipeline_));
|
||||
gst_bus_set_sync_handler(bus, BusCallbackSync, this, nullptr);
|
||||
gst_object_unref(bus);
|
||||
|
||||
// Start playing
|
||||
running_ = true;
|
||||
gst_element_set_state(pipeline_, GST_STATE_PLAYING);
|
||||
|
||||
}
|
||||
|
||||
void MoodbarPipeline::ReportError(GstMessage* msg) {
|
||||
|
||||
GError* error;
|
||||
gchar* debugs;
|
||||
|
||||
gst_message_parse_error(msg, &error, &debugs);
|
||||
QString message = QString::fromLocal8Bit(error->message);
|
||||
|
||||
g_error_free(error);
|
||||
free(debugs);
|
||||
|
||||
qLog(Error) << "Error processing" << local_filename_ << ":" << message;
|
||||
|
||||
}
|
||||
|
||||
void MoodbarPipeline::NewPadCallback(GstElement*, GstPad* pad, gpointer data) {
|
||||
|
||||
MoodbarPipeline* self = reinterpret_cast<MoodbarPipeline*>(data);
|
||||
|
||||
if (!self->running_) {
|
||||
qLog(Warning) << "Received gstreamer callback after pipeline has stopped.";
|
||||
return;
|
||||
}
|
||||
|
||||
GstPad* const audiopad = gst_element_get_static_pad(self->convert_element_, "sink");
|
||||
|
||||
if (GST_PAD_IS_LINKED(audiopad)) {
|
||||
qLog(Warning) << "audiopad is already linked, unlinking old pad";
|
||||
gst_pad_unlink(audiopad, GST_PAD_PEER(audiopad));
|
||||
}
|
||||
|
||||
gst_pad_link(pad, audiopad);
|
||||
gst_object_unref(audiopad);
|
||||
|
||||
int rate = 0;
|
||||
GstCaps* caps = gst_pad_get_current_caps(pad);
|
||||
GstStructure* structure = gst_caps_get_structure(caps, 0);
|
||||
gst_structure_get_int(structure, "rate", &rate);
|
||||
gst_caps_unref(caps);
|
||||
|
||||
if (self->builder_)
|
||||
self->builder_->Init(kBands, rate);
|
||||
else
|
||||
qLog(Error) << "Builder does not exist";
|
||||
|
||||
}
|
||||
|
||||
GstBusSyncReply MoodbarPipeline::BusCallbackSync(GstBus*, GstMessage* msg, gpointer data) {
|
||||
|
||||
MoodbarPipeline* self = reinterpret_cast<MoodbarPipeline*>(data);
|
||||
|
||||
switch (GST_MESSAGE_TYPE(msg)) {
|
||||
case GST_MESSAGE_EOS:
|
||||
self->Stop(true);
|
||||
break;
|
||||
|
||||
case GST_MESSAGE_ERROR:
|
||||
self->ReportError(msg);
|
||||
self->Stop(false);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return GST_BUS_PASS;
|
||||
|
||||
}
|
||||
|
||||
void MoodbarPipeline::Stop(bool success) {
|
||||
|
||||
success_ = success;
|
||||
running_ = false;
|
||||
if (builder_ != nullptr) {
|
||||
data_ = builder_->Finish(1000);
|
||||
builder_.reset();
|
||||
}
|
||||
|
||||
emit Finished(success);
|
||||
|
||||
}
|
||||
|
||||
void MoodbarPipeline::Cleanup() {
|
||||
|
||||
Q_ASSERT(QThread::currentThread() == thread());
|
||||
Q_ASSERT(QThread::currentThread() != qApp->thread());
|
||||
|
||||
running_ = false;
|
||||
if (pipeline_) {
|
||||
GstBus* bus = gst_pipeline_get_bus(GST_PIPELINE(pipeline_));
|
||||
gst_bus_set_sync_handler(bus, nullptr, nullptr, nullptr);
|
||||
gst_object_unref(bus);
|
||||
|
||||
gst_element_set_state(pipeline_, GST_STATE_NULL);
|
||||
gst_object_unref(pipeline_);
|
||||
pipeline_ = nullptr;
|
||||
}
|
||||
|
||||
}
|
||||
79
src/moodbar/moodbarpipeline.h
Normal file
79
src/moodbar/moodbarpipeline.h
Normal file
@@ -0,0 +1,79 @@
|
||||
/* This file was part of Clementine.
|
||||
Copyright 2012, David Sansome <me@davidsansome.com>
|
||||
|
||||
Strawberry 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.
|
||||
|
||||
Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef MOODBARPIPELINE_H
|
||||
#define MOODBARPIPELINE_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QByteArray>
|
||||
#include <QString>
|
||||
#include <QUrl>
|
||||
|
||||
#include <gst/gst.h>
|
||||
#include <gst/app/gstappsink.h>
|
||||
|
||||
#include <memory>
|
||||
|
||||
class MoodbarBuilder;
|
||||
|
||||
// Creates moodbar data for a single local music file.
|
||||
class MoodbarPipeline : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
MoodbarPipeline(const QUrl& local_filename);
|
||||
~MoodbarPipeline();
|
||||
|
||||
static bool IsAvailable();
|
||||
|
||||
bool success() const { return success_; }
|
||||
const QByteArray& data() const { return data_; }
|
||||
|
||||
public slots:
|
||||
void Start();
|
||||
|
||||
signals:
|
||||
void Finished(bool success);
|
||||
|
||||
private:
|
||||
GstElement* CreateElement(const QString& factory_name);
|
||||
|
||||
void ReportError(GstMessage* message);
|
||||
void Stop(bool success);
|
||||
void Cleanup();
|
||||
|
||||
static void NewPadCallback(GstElement*, GstPad* pad, gpointer data);
|
||||
static GstFlowReturn NewBufferCallback(GstAppSink* app_sink, gpointer self);
|
||||
static gboolean BusCallback(GstBus*, GstMessage* msg, gpointer data);
|
||||
static GstBusSyncReply BusCallbackSync(GstBus*, GstMessage* msg, gpointer data);
|
||||
|
||||
private:
|
||||
static bool sIsAvailable;
|
||||
static const int kBands;
|
||||
|
||||
QUrl local_filename_;
|
||||
GstElement* pipeline_;
|
||||
GstElement* convert_element_;
|
||||
|
||||
std::unique_ptr<MoodbarBuilder> builder_;
|
||||
|
||||
bool success_;
|
||||
bool running_;
|
||||
QByteArray data_;
|
||||
};
|
||||
|
||||
#endif // MOODBARPIPELINE_H
|
||||
397
src/moodbar/moodbarproxystyle.cpp
Normal file
397
src/moodbar/moodbarproxystyle.cpp
Normal file
@@ -0,0 +1,397 @@
|
||||
/* This file was part of Clementine.
|
||||
Copyright 2012, David Sansome <me@davidsansome.com>
|
||||
|
||||
Strawberry 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.
|
||||
|
||||
Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include <QProxyStyle>
|
||||
#include <QSettings>
|
||||
#include <QMenu>
|
||||
#include <QPixmap>
|
||||
#include <QPainter>
|
||||
#include <QSlider>
|
||||
#include <QStyleOptionComplex>
|
||||
#include <QStyleOptionSlider>
|
||||
#include <QTimeLine>
|
||||
#include <QEvent>
|
||||
#include <QContextMenuEvent>
|
||||
|
||||
#include "core/application.h"
|
||||
#include "core/logging.h"
|
||||
|
||||
#include "moodbarproxystyle.h"
|
||||
#include "settings/moodbarsettingspage.h"
|
||||
|
||||
const int MoodbarProxyStyle::kMarginSize = 3;
|
||||
const int MoodbarProxyStyle::kBorderSize = 1;
|
||||
const int MoodbarProxyStyle::kArrowWidth = 17;
|
||||
const int MoodbarProxyStyle::kArrowHeight = 13;
|
||||
|
||||
MoodbarProxyStyle::MoodbarProxyStyle(Application* app, QSlider* slider)
|
||||
: QProxyStyle(slider->style()),
|
||||
app_(app),
|
||||
slider_(slider),
|
||||
enabled_(true),
|
||||
moodbar_style_(MoodbarRenderer::Style_Normal),
|
||||
state_(MoodbarOff),
|
||||
fade_timeline_(new QTimeLine(1000, this)),
|
||||
moodbar_colors_dirty_(true),
|
||||
moodbar_pixmap_dirty_(true),
|
||||
context_menu_(nullptr),
|
||||
show_moodbar_action_(nullptr),
|
||||
style_action_group_(nullptr) {
|
||||
|
||||
slider->setStyle(this);
|
||||
slider->installEventFilter(this);
|
||||
|
||||
connect(fade_timeline_, SIGNAL(valueChanged(qreal)), SLOT(FaderValueChanged(qreal)));
|
||||
|
||||
connect(app, SIGNAL(SettingsChanged()), SLOT(ReloadSettings()));
|
||||
ReloadSettings();
|
||||
}
|
||||
|
||||
void MoodbarProxyStyle::ReloadSettings() {
|
||||
|
||||
QSettings s;
|
||||
s.beginGroup(MoodbarSettingsPage::kSettingsGroup);
|
||||
// Get the enabled/disabled setting, and start the timelines if there's a change.
|
||||
enabled_ = s.value("show", false).toBool();
|
||||
|
||||
NextState();
|
||||
|
||||
// Get the style, and redraw if there's a change.
|
||||
MoodbarRenderer::MoodbarStyle new_style = static_cast<MoodbarRenderer::MoodbarStyle>(s.value("style", MoodbarRenderer::Style_Normal).toInt());
|
||||
|
||||
s.endGroup();
|
||||
|
||||
if (new_style != moodbar_style_) {
|
||||
moodbar_style_ = new_style;
|
||||
moodbar_colors_dirty_ = true;
|
||||
slider_->update();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void MoodbarProxyStyle::SetMoodbarData(const QByteArray& data) {
|
||||
|
||||
data_ = data;
|
||||
moodbar_colors_dirty_ = true; // Redraw next time
|
||||
NextState();
|
||||
|
||||
}
|
||||
|
||||
void MoodbarProxyStyle::SetMoodbarEnabled(bool enabled) {
|
||||
|
||||
enabled_ = enabled;
|
||||
|
||||
// Save the enabled setting.
|
||||
QSettings s;
|
||||
s.beginGroup(MoodbarSettingsPage::kSettingsGroup);
|
||||
s.setValue("show", enabled);
|
||||
s.endGroup();
|
||||
|
||||
app_->ReloadSettings();
|
||||
|
||||
}
|
||||
|
||||
void MoodbarProxyStyle::NextState() {
|
||||
|
||||
const bool visible = enabled_ && !data_.isEmpty();
|
||||
|
||||
// While the regular slider should stay at the standard size (Fixed),
|
||||
// moodbars should use all available space (MinimumExpanding).
|
||||
slider_->setSizePolicy(QSizePolicy::Expanding, visible ? QSizePolicy::MinimumExpanding : QSizePolicy::Fixed);
|
||||
slider_->updateGeometry();
|
||||
|
||||
if (show_moodbar_action_) {
|
||||
show_moodbar_action_->setChecked(enabled_);
|
||||
}
|
||||
|
||||
if ((visible && (state_ == MoodbarOn || state_ == FadingToOn)) || (!visible && (state_ == MoodbarOff || state_ == FadingToOff))) {
|
||||
return;
|
||||
}
|
||||
|
||||
const QTimeLine::Direction direction = visible ? QTimeLine::Forward : QTimeLine::Backward;
|
||||
|
||||
if (state_ == MoodbarOn || state_ == MoodbarOff) {
|
||||
// Start the fade from the beginning.
|
||||
fade_timeline_->setDirection(direction);
|
||||
fade_timeline_->start();
|
||||
|
||||
fade_source_ = QPixmap();
|
||||
fade_target_ = QPixmap();
|
||||
}
|
||||
else {
|
||||
// Stop an existing fade and start fading the other direction from the
|
||||
// same place.
|
||||
fade_timeline_->stop();
|
||||
fade_timeline_->setDirection(direction);
|
||||
fade_timeline_->resume();
|
||||
}
|
||||
|
||||
state_ = visible ? FadingToOn : FadingToOff;
|
||||
|
||||
}
|
||||
|
||||
void MoodbarProxyStyle::FaderValueChanged(qreal value) { slider_->update(); }
|
||||
|
||||
bool MoodbarProxyStyle::eventFilter(QObject* object, QEvent* event) {
|
||||
|
||||
if (object == slider_) {
|
||||
switch (event->type()) {
|
||||
case QEvent::Resize:
|
||||
// The widget was resized, we've got to render a new pixmap.
|
||||
moodbar_pixmap_dirty_ = true;
|
||||
break;
|
||||
|
||||
case QEvent::ContextMenu:
|
||||
ShowContextMenu(static_cast<QContextMenuEvent*>(event)->globalPos());
|
||||
return true;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return QProxyStyle::eventFilter(object, event);
|
||||
|
||||
}
|
||||
|
||||
void MoodbarProxyStyle::drawComplexControl(ComplexControl control, const QStyleOptionComplex* option, QPainter* painter, const QWidget* widget) const {
|
||||
|
||||
if (control != CC_Slider || widget != slider_) {
|
||||
QProxyStyle::drawComplexControl(control, option, painter, widget);
|
||||
return;
|
||||
}
|
||||
|
||||
const_cast<MoodbarProxyStyle*>(this)->Render(control, qstyleoption_cast<const QStyleOptionSlider*>(option), painter, widget);
|
||||
|
||||
}
|
||||
|
||||
void MoodbarProxyStyle::Render(ComplexControl control, const QStyleOptionSlider* option, QPainter* painter, const QWidget* widget) {
|
||||
|
||||
const qreal fade_value = fade_timeline_->currentValue();
|
||||
|
||||
// Have we finished fading?
|
||||
if (state_ == FadingToOn && fade_value == 1.0) {
|
||||
state_ = MoodbarOn;
|
||||
}
|
||||
else if (state_ == FadingToOff && fade_value == 0.0) {
|
||||
state_ = MoodbarOff;
|
||||
}
|
||||
|
||||
switch (state_) {
|
||||
case FadingToOn:
|
||||
case FadingToOff:
|
||||
// Update the cached pixmaps if necessary
|
||||
if (fade_source_.isNull()) {
|
||||
// Draw the normal slider into the fade source pixmap.
|
||||
fade_source_ = QPixmap(option->rect.size());
|
||||
fade_source_.fill(option->palette.color(QPalette::Active, QPalette::Background));
|
||||
|
||||
QPainter p(&fade_source_);
|
||||
QStyleOptionSlider opt_copy(*option);
|
||||
opt_copy.rect.moveTo(0, 0);
|
||||
|
||||
QProxyStyle::drawComplexControl(control, &opt_copy, &p, widget);
|
||||
|
||||
p.end();
|
||||
}
|
||||
|
||||
if (fade_target_.isNull()) {
|
||||
if (state_ == FadingToOn) {
|
||||
EnsureMoodbarRendered(option);
|
||||
}
|
||||
fade_target_ = moodbar_pixmap_;
|
||||
QPainter p(&fade_target_);
|
||||
DrawArrow(option, &p);
|
||||
p.end();
|
||||
}
|
||||
|
||||
// Blend the pixmaps into each other
|
||||
painter->drawPixmap(option->rect, fade_source_);
|
||||
painter->setOpacity(fade_value);
|
||||
painter->drawPixmap(option->rect, fade_target_);
|
||||
painter->setOpacity(1.0);
|
||||
break;
|
||||
|
||||
case MoodbarOff:
|
||||
// It's a normal slider widget.
|
||||
QProxyStyle::drawComplexControl(control, option, painter, widget);
|
||||
break;
|
||||
|
||||
case MoodbarOn:
|
||||
EnsureMoodbarRendered(option);
|
||||
painter->drawPixmap(option->rect, moodbar_pixmap_);
|
||||
DrawArrow(option, painter);
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void MoodbarProxyStyle::EnsureMoodbarRendered(const QStyleOptionSlider* opt) {
|
||||
|
||||
if (moodbar_colors_dirty_) {
|
||||
moodbar_colors_ = MoodbarRenderer::Colors(data_, moodbar_style_, slider_->palette());
|
||||
moodbar_colors_dirty_ = false;
|
||||
moodbar_pixmap_dirty_ = true;
|
||||
}
|
||||
|
||||
if (moodbar_pixmap_dirty_) {
|
||||
moodbar_pixmap_ = MoodbarPixmap(moodbar_colors_, slider_->size(), slider_->palette(), opt);
|
||||
moodbar_pixmap_dirty_ = false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
QRect MoodbarProxyStyle::subControlRect(ComplexControl cc, const QStyleOptionComplex* opt, SubControl sc, const QWidget* widget) const {
|
||||
|
||||
if (cc != QStyle::CC_Slider || widget != slider_) {
|
||||
return QProxyStyle::subControlRect(cc, opt, sc, widget);
|
||||
}
|
||||
|
||||
switch (state_) {
|
||||
case MoodbarOff:
|
||||
case FadingToOff:
|
||||
break;
|
||||
|
||||
case MoodbarOn:
|
||||
case FadingToOn:
|
||||
switch (sc) {
|
||||
case SC_SliderGroove:
|
||||
return opt->rect.adjusted(kMarginSize, kMarginSize, -kMarginSize, -kMarginSize);
|
||||
|
||||
case SC_SliderHandle: {
|
||||
const QStyleOptionSlider* slider_opt = qstyleoption_cast<const QStyleOptionSlider*>(opt);
|
||||
int x_offset = 0;
|
||||
|
||||
/* slider_opt->{maximum,minimum} can have the value 0 (their default
|
||||
values), so this check avoids a division by 0. */
|
||||
if (slider_opt->maximum > slider_opt->minimum) {
|
||||
qint64 slider_delta = slider_opt->sliderValue - slider_opt->minimum;
|
||||
qint64 slider_range = slider_opt->maximum - slider_opt->minimum;
|
||||
int rectangle_effective_width = opt->rect.width() - kArrowWidth;
|
||||
|
||||
qint64 x = slider_delta * rectangle_effective_width / slider_range;
|
||||
x_offset = static_cast<int>(x);
|
||||
}
|
||||
|
||||
return QRect(QPoint(opt->rect.left() + x_offset, opt->rect.top()), QSize(kArrowWidth, kArrowHeight));
|
||||
}
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return QProxyStyle::subControlRect(cc, opt, sc, widget);
|
||||
}
|
||||
|
||||
void MoodbarProxyStyle::DrawArrow(const QStyleOptionSlider* option, QPainter* painter) const {
|
||||
|
||||
// Get the dimensions of the arrow
|
||||
const QRect rect = subControlRect(CC_Slider, option, SC_SliderHandle, slider_);
|
||||
|
||||
// Make a polygon
|
||||
QPolygon poly;
|
||||
poly << rect.topLeft() << rect.topRight() << QPoint(rect.center().x(), rect.bottom());
|
||||
|
||||
// Draw it
|
||||
painter->save();
|
||||
painter->setRenderHint(QPainter::Antialiasing);
|
||||
painter->translate(0.5, 0.5);
|
||||
painter->setPen(Qt::black);
|
||||
painter->setBrush(slider_->palette().brush(QPalette::Active, QPalette::Base));
|
||||
painter->drawPolygon(poly);
|
||||
painter->restore();
|
||||
|
||||
}
|
||||
|
||||
QPixmap MoodbarProxyStyle::MoodbarPixmap(const ColorVector& colors, const QSize& size, const QPalette& palette, const QStyleOptionSlider* opt) {
|
||||
|
||||
QRect rect(QPoint(0, 0), size);
|
||||
QRect border_rect(rect);
|
||||
border_rect.adjust(kMarginSize, kMarginSize, -kMarginSize, -kMarginSize);
|
||||
|
||||
QRect inner_rect(border_rect);
|
||||
inner_rect.adjust(kBorderSize, kBorderSize, -kBorderSize, -kBorderSize);
|
||||
|
||||
QPixmap ret(size);
|
||||
QPainter p(&ret);
|
||||
|
||||
// Draw the moodbar
|
||||
MoodbarRenderer::Render(colors, &p, inner_rect);
|
||||
|
||||
// Draw the border
|
||||
p.setPen(QPen(Qt::black, kBorderSize, Qt::SolidLine, Qt::FlatCap, Qt::MiterJoin));
|
||||
p.drawRect(border_rect.adjusted(0, 0, -1, -1));
|
||||
|
||||
// Draw the outer bit
|
||||
p.setPen(QPen(palette.brush(QPalette::Active, QPalette::Background), kMarginSize, Qt::SolidLine, Qt::FlatCap, Qt::MiterJoin));
|
||||
|
||||
p.drawRect(rect.adjusted(1, 1, -2, -2));
|
||||
|
||||
p.end();
|
||||
|
||||
return ret;
|
||||
|
||||
}
|
||||
|
||||
void MoodbarProxyStyle::ShowContextMenu(const QPoint& pos) {
|
||||
|
||||
if (!context_menu_) {
|
||||
context_menu_ = new QMenu(slider_);
|
||||
show_moodbar_action_ = context_menu_->addAction(tr("Show moodbar"), this, SLOT(SetMoodbarEnabled(bool)));
|
||||
|
||||
show_moodbar_action_->setCheckable(true);
|
||||
show_moodbar_action_->setChecked(enabled_);
|
||||
|
||||
QMenu* styles_menu = context_menu_->addMenu(tr("Moodbar style"));
|
||||
style_action_group_ = new QActionGroup(styles_menu);
|
||||
|
||||
for (int i = 0; i < MoodbarRenderer::StyleCount; ++i) {
|
||||
const MoodbarRenderer::MoodbarStyle style = MoodbarRenderer::MoodbarStyle(i);
|
||||
|
||||
QAction* action = style_action_group_->addAction(MoodbarRenderer::StyleName(style));
|
||||
action->setCheckable(true);
|
||||
action->setData(i);
|
||||
}
|
||||
|
||||
styles_menu->addActions(style_action_group_->actions());
|
||||
|
||||
connect(styles_menu, SIGNAL(triggered(QAction*)), SLOT(ChangeStyle(QAction*)));
|
||||
}
|
||||
|
||||
// Update the currently selected style
|
||||
for (QAction* action : style_action_group_->actions()) {
|
||||
if (MoodbarRenderer::MoodbarStyle(action->data().toInt()) == moodbar_style_) {
|
||||
action->setChecked(true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
context_menu_->popup(pos);
|
||||
|
||||
}
|
||||
|
||||
void MoodbarProxyStyle::ChangeStyle(QAction* action) {
|
||||
|
||||
QSettings s;
|
||||
s.beginGroup(MoodbarSettingsPage::kSettingsGroup);
|
||||
s.setValue("style", action->data().toInt());
|
||||
s.endGroup();
|
||||
|
||||
app_->ReloadSettings();
|
||||
|
||||
}
|
||||
103
src/moodbar/moodbarproxystyle.h
Normal file
103
src/moodbar/moodbarproxystyle.h
Normal file
@@ -0,0 +1,103 @@
|
||||
/* This file was part of Clementine.
|
||||
Copyright 2012, David Sansome <me@davidsansome.com>
|
||||
|
||||
Strawberry 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.
|
||||
|
||||
Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef MOODBARPROXYSTYLE_H
|
||||
#define MOODBARPROXYSTYLE_H
|
||||
|
||||
#include <QProxyStyle>
|
||||
#include <QTimeLine>
|
||||
#include <QByteArray>
|
||||
#include <QPixmap>
|
||||
#include <QPainter>
|
||||
#include <QSlider>
|
||||
#include <QMenu>
|
||||
#include <QActionGroup>
|
||||
#include <QStyleOptionSlider>
|
||||
#include <QEvent>
|
||||
|
||||
#include "moodbarrenderer.h"
|
||||
|
||||
class Application;
|
||||
|
||||
class MoodbarProxyStyle : public QProxyStyle {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
MoodbarProxyStyle(Application* app, QSlider* slider);
|
||||
|
||||
// QProxyStyle
|
||||
void drawComplexControl(ComplexControl control, const QStyleOptionComplex* option, QPainter* painter, const QWidget* widget) const;
|
||||
QRect subControlRect(ComplexControl cc, const QStyleOptionComplex* opt, SubControl sc, const QWidget* widget) const;
|
||||
|
||||
// QObject
|
||||
bool eventFilter(QObject* object, QEvent* event);
|
||||
|
||||
public slots:
|
||||
// An empty byte array means there's no moodbar, so just show a normal slider.
|
||||
void SetMoodbarData(const QByteArray& data);
|
||||
|
||||
// If the moodbar is disabled then a normal slider will always be shown.
|
||||
void SetMoodbarEnabled(bool enabled);
|
||||
|
||||
private:
|
||||
static const int kMarginSize;
|
||||
static const int kBorderSize;
|
||||
static const int kArrowWidth;
|
||||
static const int kArrowHeight;
|
||||
|
||||
enum State { MoodbarOn, MoodbarOff, FadingToOn, FadingToOff };
|
||||
|
||||
private:
|
||||
void NextState();
|
||||
|
||||
void Render(ComplexControl control, const QStyleOptionSlider* option, QPainter* painter, const QWidget* widget);
|
||||
void EnsureMoodbarRendered(const QStyleOptionSlider* opt);
|
||||
void DrawArrow(const QStyleOptionSlider* option, QPainter* painter) const;
|
||||
void ShowContextMenu(const QPoint& pos);
|
||||
|
||||
QPixmap MoodbarPixmap(const ColorVector& colors, const QSize& size, const QPalette& palette, const QStyleOptionSlider* opt);
|
||||
|
||||
private slots:
|
||||
void ReloadSettings();
|
||||
void FaderValueChanged(qreal value);
|
||||
void ChangeStyle(QAction* action);
|
||||
|
||||
private:
|
||||
Application* app_;
|
||||
QSlider* slider_;
|
||||
|
||||
bool enabled_;
|
||||
QByteArray data_;
|
||||
MoodbarRenderer::MoodbarStyle moodbar_style_;
|
||||
|
||||
State state_;
|
||||
QTimeLine* fade_timeline_;
|
||||
|
||||
QPixmap fade_source_;
|
||||
QPixmap fade_target_;
|
||||
|
||||
bool moodbar_colors_dirty_;
|
||||
bool moodbar_pixmap_dirty_;
|
||||
ColorVector moodbar_colors_;
|
||||
QPixmap moodbar_pixmap_;
|
||||
|
||||
QMenu* context_menu_;
|
||||
QAction* show_moodbar_action_;
|
||||
QActionGroup* style_action_group_;
|
||||
};
|
||||
|
||||
#endif // MOODBARPROXYSTYLE_H
|
||||
178
src/moodbar/moodbarrenderer.cpp
Normal file
178
src/moodbar/moodbarrenderer.cpp
Normal file
@@ -0,0 +1,178 @@
|
||||
/* This file was part of Clementine.
|
||||
Copyright 2012, David Sansome <me@davidsansome.com>
|
||||
|
||||
Strawberry 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.
|
||||
|
||||
Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "moodbarrenderer.h"
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QString>
|
||||
#include <QImage>
|
||||
#include <QPainter>
|
||||
#include <QPalette>
|
||||
#include <QColor>
|
||||
|
||||
#include "core/arraysize.h"
|
||||
|
||||
const int MoodbarRenderer::kNumHues = 12;
|
||||
|
||||
ColorVector MoodbarRenderer::Colors(const QByteArray& data, MoodbarStyle style, const QPalette& palette) {
|
||||
|
||||
const int samples = data.size() / 3;
|
||||
|
||||
// Set some parameters based on the moodbar style
|
||||
StyleProperties properties;
|
||||
switch (style) {
|
||||
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_SystemPalette:
|
||||
default: {
|
||||
const QColor highlight_color(palette.color(QPalette::Active, QPalette::Highlight));
|
||||
|
||||
properties.threshold_ = samples / 360 * 3;
|
||||
properties.range_start_ = (highlight_color.hsvHue() - 20 + 360) % 360;
|
||||
properties.range_delta_ = 20;
|
||||
properties.sat_ = highlight_color.hsvSaturation();
|
||||
properties.val_ = highlight_color.value() / 2;
|
||||
}
|
||||
}
|
||||
|
||||
const unsigned char* data_p = reinterpret_cast<const unsigned char*>(data.constData());
|
||||
|
||||
int hue_distribution[360];
|
||||
int total = 0;
|
||||
|
||||
memset(hue_distribution, 0, sizeof(hue_distribution));
|
||||
|
||||
ColorVector colors;
|
||||
|
||||
// Read the colors, keeping track of some histograms
|
||||
for (int i = 0; i < samples; ++i) {
|
||||
QColor color;
|
||||
color.setRed(int(*data_p++));
|
||||
color.setGreen(int(*data_p++));
|
||||
color.setBlue(int(*data_p++));
|
||||
|
||||
colors << color;
|
||||
|
||||
const int hue = qMax(0, color.hue());
|
||||
if (hue_distribution[hue]++ == properties.threshold_) {
|
||||
total++;
|
||||
}
|
||||
}
|
||||
|
||||
total = qMax(total, 1);
|
||||
|
||||
// Remap the hue values to be between rangeStart and
|
||||
// rangeStart + rangeDelta. Every time we see an input hue
|
||||
// above the threshold, increment the output hue by
|
||||
// (1/total) * rangeDelta.
|
||||
for (int i = 0, n = 0; i < 360; i++) {
|
||||
hue_distribution[i] = ((hue_distribution[i] > properties.threshold_ ? n++ : n) * properties.range_delta_ / total + properties.range_start_) % 360;
|
||||
}
|
||||
|
||||
// Now huedist is a hue mapper: huedist[h] is the new hue value
|
||||
// for a bar with hue h
|
||||
for (ColorVector::iterator it = colors.begin(); it != colors.end(); ++it) {
|
||||
const int hue = qMax(0, it->hue());
|
||||
|
||||
*it = QColor::fromHsv(qBound(0, hue_distribution[hue], 359), qBound(0, it->saturation() * properties.sat_ / 100, 255), qBound(0, it->value() * properties.val_ / 100, 255));
|
||||
}
|
||||
|
||||
return colors;
|
||||
}
|
||||
|
||||
void MoodbarRenderer::Render(const ColorVector& colors, QPainter* p, const QRect& rect) {
|
||||
|
||||
// Sample the colors and map them to screen pixels.
|
||||
ColorVector screen_colors;
|
||||
for (int x = 0; x < rect.width(); ++x) {
|
||||
int r = 0;
|
||||
int g = 0;
|
||||
int b = 0;
|
||||
|
||||
int start = x * colors.size() / rect.width();
|
||||
int end = (x + 1) * colors.size() / rect.width();
|
||||
|
||||
if (start == end) end = qMin(start + 1, colors.size() - 1);
|
||||
|
||||
for (int j = start; j < end; j++) {
|
||||
r += colors[j].red();
|
||||
g += colors[j].green();
|
||||
b += colors[j].blue();
|
||||
}
|
||||
|
||||
const int n = qMax(1, end - start);
|
||||
screen_colors.append(QColor(r / n, g / n, b / n));
|
||||
}
|
||||
|
||||
// Draw the actual moodbar.
|
||||
for (int x = 0; x < rect.width(); x++) {
|
||||
int h, s, v;
|
||||
screen_colors[x].getHsv(&h, &s, &v);
|
||||
|
||||
for (int y = 0; y <= rect.height() / 2; y++) {
|
||||
float coeff = float(y) / float(rect.height() / 2);
|
||||
float coeff2 = 1.0f - ((1.0f - coeff) * (1.0f - coeff));
|
||||
coeff = 1.0f - (1.0f - coeff) / 2.0f;
|
||||
coeff2 = 1.f - (1.f - coeff2) / 2.0f;
|
||||
|
||||
p->setPen(QColor::fromHsv(h, qBound(0, int(float(s) * coeff), 255), qBound(0, int(255.f - (255.f - float(v)) * coeff2), 255)));
|
||||
|
||||
p->drawPoint(rect.left() + x, rect.top() + y);
|
||||
p->drawPoint(rect.left() + x, rect.top() + rect.height() - 1 - y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
}
|
||||
|
||||
QString MoodbarRenderer::StyleName(MoodbarStyle style) {
|
||||
|
||||
switch (style) {
|
||||
case Style_Normal:
|
||||
return QObject::tr("Normal");
|
||||
case Style_Angry:
|
||||
return QObject::tr("Angry");
|
||||
case Style_Frozen:
|
||||
return QObject::tr("Frozen");
|
||||
case Style_Happy:
|
||||
return QObject::tr("Happy");
|
||||
case Style_SystemPalette:
|
||||
return QObject::tr("System colors");
|
||||
|
||||
default:
|
||||
return QString();
|
||||
}
|
||||
|
||||
}
|
||||
73
src/moodbar/moodbarrenderer.h
Normal file
73
src/moodbar/moodbarrenderer.h
Normal file
@@ -0,0 +1,73 @@
|
||||
/* This file was part of Clementine.
|
||||
Copyright 2012, David Sansome <me@davidsansome.com>
|
||||
|
||||
Strawberry 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.
|
||||
|
||||
Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef MOODBARRENDERER_H
|
||||
#define MOODBARRENDERER_H
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QImage>
|
||||
#include <QPixmap>
|
||||
#include <QMetaType>
|
||||
#include <QVector>
|
||||
#include <QColor>
|
||||
#include <QPainter>
|
||||
#include <QPalette>
|
||||
|
||||
typedef QVector<QColor> ColorVector;
|
||||
|
||||
class MoodbarRenderer {
|
||||
public:
|
||||
// These values are persisted. Remember to change moodbarsettingspage.ui when changing them.
|
||||
enum MoodbarStyle {
|
||||
Style_Normal = 0,
|
||||
Style_Angry,
|
||||
Style_Frozen,
|
||||
Style_Happy,
|
||||
Style_SystemPalette,
|
||||
StyleCount
|
||||
};
|
||||
|
||||
static const int kNumHues;
|
||||
|
||||
static QString StyleName(MoodbarStyle style);
|
||||
|
||||
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();
|
||||
|
||||
struct StyleProperties {
|
||||
StyleProperties(int threshold = 0, int range_start = 0, int range_delta = 0, int sat = 0, int val = 0)
|
||||
: threshold_(threshold),
|
||||
range_start_(range_start),
|
||||
range_delta_(range_delta),
|
||||
sat_(sat),
|
||||
val_(val) {}
|
||||
|
||||
int threshold_;
|
||||
int range_start_;
|
||||
int range_delta_;
|
||||
int sat_;
|
||||
int val_;
|
||||
};
|
||||
};
|
||||
|
||||
Q_DECLARE_METATYPE(QVector<QColor>)
|
||||
|
||||
#endif // MOODBARRENDERER_H
|
||||
@@ -365,6 +365,12 @@ QVariant Playlist::data(const QModelIndex &index, int role) const {
|
||||
|
||||
}
|
||||
|
||||
#ifdef HAVE_MOODBAR
|
||||
void Playlist::MoodbarUpdated(const QModelIndex& index) {
|
||||
emit dataChanged(index.sibling(index.row(), Column_Mood), index.sibling(index.row(), Column_Mood));
|
||||
}
|
||||
#endif
|
||||
|
||||
bool Playlist::setData(const QModelIndex &index, const QVariant &value, int role) {
|
||||
|
||||
int row = index.row();
|
||||
@@ -1183,6 +1189,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: qLog(Error) << "No such column" << column;;
|
||||
}
|
||||
return "";
|
||||
|
||||
@@ -124,6 +124,7 @@ class Playlist : public QAbstractListModel {
|
||||
Column_Comment,
|
||||
Column_Grouping,
|
||||
Column_Source,
|
||||
Column_Mood,
|
||||
ColumnCount
|
||||
};
|
||||
|
||||
@@ -251,6 +252,11 @@ class Playlist : public QAbstractListModel {
|
||||
// Unregisters a SongInsertVetoListener object.
|
||||
void RemoveSongInsertVetoListener(SongInsertVetoListener *listener);
|
||||
|
||||
// Just emits the dataChanged() signal so the mood column is repainted.
|
||||
#ifdef HAVE_MOODBAR
|
||||
void MoodbarUpdated(const QModelIndex& index);
|
||||
#endif
|
||||
|
||||
// QAbstractListModel
|
||||
int rowCount(const QModelIndex& = QModelIndex()) const { return items_.count(); }
|
||||
int columnCount(const QModelIndex& = QModelIndex()) const { return ColumnCount; }
|
||||
|
||||
@@ -104,6 +104,12 @@ void PlaylistHeader::contextMenuEvent(QContextMenuEvent *e) {
|
||||
|
||||
void PlaylistHeader::AddColumnAction(int index) {
|
||||
|
||||
#ifndef HAVE_MOODBAR
|
||||
if (index == Playlist::Column_Mood) {
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
QString title(model()->headerData(index, Qt::Horizontal).toString());
|
||||
|
||||
QAction *action = menu_->addAction(title, show_mapper_, SLOT(map()));
|
||||
|
||||
@@ -77,6 +77,10 @@
|
||||
#include "settings/appearancesettingspage.h"
|
||||
#include "settings/playlistsettingspage.h"
|
||||
|
||||
#ifdef HAVE_MOODBAR
|
||||
# include "moodbar/moodbaritemdelegate.h"
|
||||
#endif
|
||||
|
||||
using std::sort;
|
||||
|
||||
const int PlaylistView::kGlowIntensitySteps = 24;
|
||||
@@ -244,6 +248,10 @@ void PlaylistView::SetItemDelegates(CollectionBackend *backend) {
|
||||
|
||||
setItemDelegateForColumn(Playlist::Column_Source, new SongSourceDelegate(this));
|
||||
|
||||
#ifdef HAVE_MOODBAR
|
||||
setItemDelegateForColumn(Playlist::Column_Mood, new MoodbarItemDelegate(app_, this, this));
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
void PlaylistView::SetPlaylist(Playlist *playlist) {
|
||||
@@ -310,6 +318,7 @@ void PlaylistView::LoadGeometry() {
|
||||
header_->HideSection(Playlist::Column_LastPlayed);
|
||||
header_->HideSection(Playlist::Column_Comment);
|
||||
header_->HideSection(Playlist::Column_Grouping);
|
||||
header_->HideSection(Playlist::Column_Mood);
|
||||
|
||||
header_->moveSection(header_->visualIndex(Playlist::Column_Track), 0);
|
||||
setting_initial_header_layout_ = true;
|
||||
|
||||
122
src/settings/moodbarsettingspage.cpp
Normal file
122
src/settings/moodbarsettingspage.cpp
Normal file
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2012, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry 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.
|
||||
*
|
||||
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QIODevice>
|
||||
#include <QFile>
|
||||
#include <QByteArray>
|
||||
#include <QPixmap>
|
||||
#include <QPainter>
|
||||
#include <QSettings>
|
||||
|
||||
#include "core/application.h"
|
||||
#include "core/mainwindow.h"
|
||||
#include "core/iconloader.h"
|
||||
#include "core/logging.h"
|
||||
|
||||
#include "settingsdialog.h"
|
||||
|
||||
#ifdef HAVE_MOODBAR
|
||||
# include "moodbar/moodbarrenderer.h"
|
||||
#endif
|
||||
|
||||
#include "moodbarsettingspage.h"
|
||||
#include "ui_moodbarsettingspage.h"
|
||||
|
||||
const char *MoodbarSettingsPage::kSettingsGroup = "Moodbar";
|
||||
const int MoodbarSettingsPage::kMoodbarPreviewWidth = 150;
|
||||
const int MoodbarSettingsPage::kMoodbarPreviewHeight = 18;
|
||||
|
||||
MoodbarSettingsPage::MoodbarSettingsPage(SettingsDialog* dialog)
|
||||
: SettingsPage(dialog),
|
||||
ui_(new Ui_MoodbarSettingsPage),
|
||||
initialised_(false)
|
||||
{
|
||||
|
||||
ui_->setupUi(this);
|
||||
setWindowIcon(IconLoader::Load("moodbar"));
|
||||
|
||||
Load();
|
||||
|
||||
}
|
||||
|
||||
MoodbarSettingsPage::~MoodbarSettingsPage() { delete ui_; }
|
||||
|
||||
void MoodbarSettingsPage::Load() {
|
||||
|
||||
QSettings s;
|
||||
s.beginGroup(kSettingsGroup);
|
||||
ui_->moodbar_enabled->setChecked(s.value("enabled", false).toBool());
|
||||
ui_->moodbar_show->setChecked(s.value("show", false).toBool());
|
||||
ui_->moodbar_style->setCurrentIndex(s.value("style", 0).toInt());
|
||||
ui_->moodbar_save->setChecked(s.value("save", false).toBool());
|
||||
s.endGroup();
|
||||
|
||||
InitMoodbarPreviews();
|
||||
|
||||
}
|
||||
|
||||
void MoodbarSettingsPage::Save() {
|
||||
|
||||
QSettings s;
|
||||
s.beginGroup(kSettingsGroup);
|
||||
s.setValue("enabled", ui_->moodbar_enabled->isChecked());
|
||||
s.setValue("show", ui_->moodbar_show->isChecked());
|
||||
s.setValue("style", ui_->moodbar_style->currentIndex());
|
||||
s.setValue("save", ui_->moodbar_save->isChecked());
|
||||
s.endGroup();
|
||||
}
|
||||
|
||||
void MoodbarSettingsPage::Cancel() {}
|
||||
|
||||
void MoodbarSettingsPage::InitMoodbarPreviews() {
|
||||
|
||||
if (initialised_) return;
|
||||
initialised_ = true;
|
||||
|
||||
const QSize preview_size(kMoodbarPreviewWidth, kMoodbarPreviewHeight);
|
||||
ui_->moodbar_style->setIconSize(preview_size);
|
||||
|
||||
// Read the sample data
|
||||
QFile file(":/mood/sample.mood");
|
||||
if (!file.open(QIODevice::ReadOnly)) {
|
||||
qLog(Warning) << "Unable to open moodbar sample file";
|
||||
return;
|
||||
}
|
||||
QByteArray data(file.readAll());
|
||||
|
||||
// Render and set each preview
|
||||
for (int i = 0; i < MoodbarRenderer::StyleCount; ++i) {
|
||||
|
||||
const MoodbarRenderer::MoodbarStyle style = MoodbarRenderer::MoodbarStyle(i);
|
||||
const ColorVector colors = MoodbarRenderer::Colors(data, style, palette());
|
||||
|
||||
QPixmap pixmap(preview_size);
|
||||
QPainter p(&pixmap);
|
||||
MoodbarRenderer::Render(colors, &p, pixmap.rect());
|
||||
p.end();
|
||||
|
||||
ui_->moodbar_style->addItem(MoodbarRenderer::StyleName(style));
|
||||
ui_->moodbar_style->setItemData(i, pixmap, Qt::DecorationRole);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
54
src/settings/moodbarsettingspage.h
Normal file
54
src/settings/moodbarsettingspage.h
Normal file
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2012, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry 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.
|
||||
*
|
||||
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef MOODBARSETTINGSPAGE_H
|
||||
#define MOODBARSETTINGSPAGE_H
|
||||
|
||||
#include "settingspage.h"
|
||||
|
||||
#include <QObject>
|
||||
|
||||
class Ui_MoodbarSettingsPage;
|
||||
|
||||
class MoodbarSettingsPage : public SettingsPage {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
MoodbarSettingsPage(SettingsDialog* dialog);
|
||||
~MoodbarSettingsPage();
|
||||
|
||||
static const char *kSettingsGroup;
|
||||
|
||||
void Load();
|
||||
void Save();
|
||||
void Cancel();
|
||||
|
||||
private:
|
||||
static const int kMoodbarPreviewWidth;
|
||||
static const int kMoodbarPreviewHeight;
|
||||
|
||||
void InitMoodbarPreviews();
|
||||
|
||||
Ui_MoodbarSettingsPage* ui_;
|
||||
|
||||
bool initialised_;
|
||||
};
|
||||
|
||||
#endif // MOODBARSETTINGSPAGE_H
|
||||
61
src/settings/moodbarsettingspage.ui
Normal file
61
src/settings/moodbarsettingspage.ui
Normal file
@@ -0,0 +1,61 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>MoodbarSettingsPage</class>
|
||||
<widget class="QWidget" name="MoodbarSettingsPage">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>596</width>
|
||||
<height>666</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Moodbar</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="layout_moodbarsettingspage">
|
||||
<item>
|
||||
<widget class="QGroupBox" name="moodbar_group">
|
||||
<property name="title">
|
||||
<string>Moodbar</string>
|
||||
</property>
|
||||
<layout class="QFormLayout" name="layout_moodbar">
|
||||
<item row="1" column="0" colspan="2">
|
||||
<widget class="QCheckBox" name="moodbar_show">
|
||||
<property name="text">
|
||||
<string>Show a moodbar in the track progress bar</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="label_moodbar_style">
|
||||
<property name="text">
|
||||
<string>Moodbar style</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<widget class="QComboBox" name="moodbar_style"/>
|
||||
</item>
|
||||
<item row="3" column="0" colspan="2">
|
||||
<widget class="QCheckBox" name="moodbar_save">
|
||||
<property name="text">
|
||||
<string>Save the .mood files directly in the songs folders</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="0">
|
||||
<widget class="QCheckBox" name="moodbar_enabled">
|
||||
<property name="text">
|
||||
<string>Enabled</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
@@ -65,6 +65,9 @@
|
||||
#ifdef HAVE_TIDAL
|
||||
# include "tidalsettingspage.h"
|
||||
#endif
|
||||
#ifdef HAVE_MOODBAR
|
||||
# include "moodbarsettingspage.h"
|
||||
#endif
|
||||
|
||||
#include "ui_settingsdialog.h"
|
||||
|
||||
@@ -133,6 +136,10 @@ SettingsDialog::SettingsDialog(Application *app, QWidget *parent)
|
||||
AddPage(Page_GlobalShortcuts, new GlobalShortcutsSettingsPage(this), iface);
|
||||
#endif
|
||||
|
||||
#ifdef HAVE_MOODBAR
|
||||
AddPage(Page_Moodbar, new MoodbarSettingsPage(this), iface);
|
||||
#endif
|
||||
|
||||
#if defined(HAVE_TIDAL)
|
||||
QTreeWidgetItem *streaming = AddCategory(tr("Streaming"));
|
||||
#endif
|
||||
|
||||
@@ -83,6 +83,7 @@ class SettingsDialog : public QDialog {
|
||||
Page_Proxy,
|
||||
Page_Scrobbler,
|
||||
Page_Tidal,
|
||||
Page_Moodbar,
|
||||
};
|
||||
|
||||
enum Role {
|
||||
|
||||
@@ -35,11 +35,18 @@
|
||||
#include "clickablelabel.h"
|
||||
#include "tracksliderslider.h"
|
||||
|
||||
#ifdef HAVE_MOODBAR
|
||||
# include "moodbar/moodbarproxystyle.h"
|
||||
#endif
|
||||
|
||||
const char* TrackSlider::kSettingsGroup = "MainWindow";
|
||||
|
||||
TrackSlider::TrackSlider(QWidget* parent)
|
||||
: QWidget(parent),
|
||||
ui_(new Ui_TrackSlider),
|
||||
#ifdef HAVE_MOODBAR
|
||||
moodbar_style_(nullptr),
|
||||
#endif
|
||||
setting_value_(false),
|
||||
show_remaining_time_(true),
|
||||
slider_maximum_value_(0)
|
||||
@@ -64,6 +71,9 @@ TrackSlider::~TrackSlider() {
|
||||
}
|
||||
|
||||
void TrackSlider::SetApplication(Application* app) {
|
||||
#ifdef HAVE_MOODBAR
|
||||
moodbar_style_ = new MoodbarProxyStyle(app, ui_->slider);
|
||||
#endif
|
||||
}
|
||||
|
||||
void TrackSlider::UpdateLabelWidth() {
|
||||
|
||||
@@ -34,6 +34,9 @@
|
||||
class QEvent;
|
||||
|
||||
class Application;
|
||||
#ifdef HAVE_MOODBAR
|
||||
class MoodbarProxyStyle;
|
||||
#endif
|
||||
class Ui_TrackSlider;
|
||||
|
||||
class TrackSlider : public QWidget {
|
||||
@@ -51,6 +54,11 @@ class TrackSlider : public QWidget {
|
||||
// QObject
|
||||
bool event(QEvent *);
|
||||
|
||||
#ifdef HAVE_MOODBAR
|
||||
MoodbarProxyStyle *moodbar_style() const { return moodbar_style_; }
|
||||
#endif
|
||||
|
||||
|
||||
static const char* kSettingsGroup;
|
||||
|
||||
public slots:
|
||||
@@ -75,6 +83,10 @@ class TrackSlider : public QWidget {
|
||||
private:
|
||||
Ui_TrackSlider* ui_;
|
||||
|
||||
#ifdef HAVE_MOODBAR
|
||||
MoodbarProxyStyle* moodbar_style_;
|
||||
#endif
|
||||
|
||||
bool setting_value_;
|
||||
bool show_remaining_time_;
|
||||
int slider_maximum_value_; //we cache it to avoid unnecessary updates
|
||||
|
||||
Reference in New Issue
Block a user